improves select display

This commit is contained in:
Steve Ruiz 2021-05-28 15:37:23 +01:00
parent 708223fffa
commit 9fd8ef8421
10 changed files with 237 additions and 122 deletions

6
.prettierrc Normal file
View file

@ -0,0 +1,6 @@
{
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"useTabs": false
}

View file

@ -1,16 +1,17 @@
import * as React from "react"
import { Edge, Corner } from "types"
import { useSelector } from "state"
import { getSelectedShapes, isMobile } from "utils/utils"
import * as React from 'react'
import { Edge, Corner } from 'types'
import { useSelector } from 'state'
import { getSelectedShapes, isMobile } from 'utils/utils'
import CenterHandle from "./center-handle"
import CornerHandle from "./corner-handle"
import EdgeHandle from "./edge-handle"
import RotateHandle from "./rotate-handle"
import CenterHandle from './center-handle'
import CornerHandle from './corner-handle'
import EdgeHandle from './edge-handle'
import RotateHandle from './rotate-handle'
import Selected from '../selected'
export default function Bounds() {
const isBrushing = useSelector((s) => s.isIn("brushSelecting"))
const isSelecting = useSelector((s) => s.isIn("selecting"))
const isBrushing = useSelector((s) => s.isIn('brushSelecting'))
const isSelecting = useSelector((s) => s.isIn('selecting'))
const zoom = useSelector((s) => s.data.camera.zoom)
const bounds = useSelector((s) => s.values.selectedBounds)
const rotation = useSelector(({ data }) =>
@ -24,13 +25,14 @@ export default function Bounds() {
return (
<g
pointerEvents={isBrushing ? "none" : "all"}
pointerEvents={isBrushing ? 'none' : 'all'}
transform={`
rotate(${rotation * (180 / Math.PI)},
${(bounds.minX + bounds.maxX) / 2},
${(bounds.minY + bounds.maxY) / 2})
translate(${bounds.minX},${bounds.minY})`}
>
<Selected bounds={bounds} />
<CenterHandle bounds={bounds} />
<EdgeHandle size={size} bounds={bounds} edge={Edge.Top} />
<EdgeHandle size={size} bounds={bounds} edge={Edge.Right} />

View file

@ -1,13 +1,14 @@
import styled from "styles"
import React, { useCallback, useRef } from "react"
import useZoomEvents from "hooks/useZoomEvents"
import useCamera from "hooks/useCamera"
import Page from "./page"
import Brush from "./brush"
import state from "state"
import Bounds from "./bounds/bounding-box"
import BoundsBg from "./bounds/bounds-bg"
import inputs from "state/inputs"
import styled from 'styles'
import state from 'state'
import inputs from 'state/inputs'
import React, { useCallback, useRef } from 'react'
import useZoomEvents from 'hooks/useZoomEvents'
import useCamera from 'hooks/useCamera'
import Defs from './defs'
import Page from './page'
import Brush from './brush'
import Bounds from './bounds/bounding-box'
import BoundsBg from './bounds/bounds-bg'
export default function Canvas() {
const rCanvas = useRef<SVGSVGElement>(null)
@ -18,16 +19,16 @@ export default function Canvas() {
const handlePointerDown = useCallback((e: React.PointerEvent) => {
rCanvas.current.setPointerCapture(e.pointerId)
state.send("POINTED_CANVAS", inputs.pointerDown(e, "canvas"))
state.send('POINTED_CANVAS', inputs.pointerDown(e, 'canvas'))
}, [])
const handlePointerMove = useCallback((e: React.PointerEvent) => {
state.send("MOVED_POINTER", inputs.pointerMove(e))
state.send('MOVED_POINTER', inputs.pointerMove(e))
}, [])
const handlePointerUp = useCallback((e: React.PointerEvent) => {
rCanvas.current.releasePointerCapture(e.pointerId)
state.send("STOPPED_POINTING", { id: "canvas", ...inputs.pointerUp(e) })
state.send('STOPPED_POINTING', { id: 'canvas', ...inputs.pointerUp(e) })
}, [])
return (
@ -38,6 +39,7 @@ export default function Canvas() {
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
>
<Defs />
<MainGroup ref={rGroup}>
<BoundsBg />
<Page />
@ -48,18 +50,18 @@ export default function Canvas() {
)
}
const MainSVG = styled("svg", {
position: "fixed",
const MainSVG = styled('svg', {
position: 'fixed',
top: 0,
left: 0,
width: "100%",
height: "100%",
touchAction: "none",
width: '100%',
height: '100%',
touchAction: 'none',
zIndex: 100,
"& *": {
userSelect: "none",
'& *': {
userSelect: 'none',
},
})
const MainGroup = styled("g", {})
const MainGroup = styled('g', {})

View file

@ -1,6 +1,6 @@
import { useSelector } from "state"
import { deepCompareArrays, getPage } from "utils/utils"
import Shape from "./shape"
import { useSelector } from 'state'
import { deepCompareArrays, getPage } from 'utils/utils'
import Shape from './shape'
/*
On each state change, compare node ids of all shapes
@ -15,10 +15,12 @@ export default function Page() {
.map((shape) => shape.id)
}, deepCompareArrays)
const isSelecting = useSelector((s) => s.isIn('selecting'))
return (
<>
{currentPageShapeIds.map((shapeId) => (
<Shape key={shapeId} id={shapeId} />
<Shape key={shapeId} id={shapeId} isSelecting={isSelecting} />
))}
</>
)

View file

@ -0,0 +1,58 @@
import styled from 'styles'
import { useSelector } from 'state'
import {
deepCompareArrays,
getBoundsCenter,
getPage,
getSelectedShapes,
} from 'utils/utils'
import * as vec from 'utils/vec'
import { getShapeUtils } from 'lib/shape-utils'
import { Bounds } from 'types'
import useShapeEvents from 'hooks/useShapeEvents'
import { useRef } from 'react'
export default function Selected({ bounds }: { bounds: Bounds }) {
const currentPageShapeIds = useSelector(({ data }) => {
return Array.from(data.selectedIds.values())
}, deepCompareArrays)
return (
<g>
{currentPageShapeIds.map((id) => (
<ShapeOutline key={id} id={id} bounds={bounds} />
))}
</g>
)
}
export function ShapeOutline({ id, bounds }: { id: string; bounds: Bounds }) {
const rIndicator = useRef<SVGUseElement>(null)
const shape = useSelector(({ data }) => getPage(data).shapes[id])
const shapeBounds = getShapeUtils(shape).getBounds(shape)
const events = useShapeEvents(id, rIndicator)
return (
<Indicator
ref={rIndicator}
as="use"
href={'#' + id}
transform={`rotate(${shape.rotation * (180 / Math.PI)},${getBoundsCenter(
shapeBounds
)}) translate(${vec.sub(shape.point, [bounds.minX, bounds.minY])})`}
{...events}
/>
)
}
const Indicator = styled('path', {
zStrokeWidth: 1,
strokeLineCap: 'round',
strokeLinejoin: 'round',
stroke: '$selected',
fill: 'transparent',
pointerEvents: 'all',
})

View file

@ -1,11 +1,12 @@
import React, { useCallback, useRef, memo } from "react"
import state, { useSelector } from "state"
import inputs from "state/inputs"
import styled from "styles"
import { getShapeUtils } from "lib/shape-utils"
import { getPage } from "utils/utils"
import React, { useCallback, useRef, memo } from 'react'
import state, { useSelector } from 'state'
import inputs from 'state/inputs'
import styled from 'styles'
import { getShapeUtils } from 'lib/shape-utils'
import { getPage } from 'utils/utils'
import { ShapeStyles } from 'types'
function Shape({ id }: { id: string }) {
function Shape({ id, isSelecting }: { id: string; isSelecting: boolean }) {
const isHovered = useSelector((state) => state.data.hoveredId === id)
const isSelected = useSelector((state) => state.values.selectedIds.has(id))
@ -18,7 +19,7 @@ function Shape({ id }: { id: string }) {
(e: React.PointerEvent) => {
e.stopPropagation()
rGroup.current.setPointerCapture(e.pointerId)
state.send("POINTED_SHAPE", inputs.pointerDown(e, id))
state.send('POINTED_SHAPE', inputs.pointerDown(e, id))
},
[id]
)
@ -27,27 +28,27 @@ function Shape({ id }: { id: string }) {
(e: React.PointerEvent) => {
e.stopPropagation()
rGroup.current.releasePointerCapture(e.pointerId)
state.send("STOPPED_POINTING", inputs.pointerUp(e))
state.send('STOPPED_POINTING', inputs.pointerUp(e))
},
[id]
)
const handlePointerEnter = useCallback(
(e: React.PointerEvent) => {
state.send("HOVERED_SHAPE", inputs.pointerEnter(e, id))
state.send('HOVERED_SHAPE', inputs.pointerEnter(e, id))
},
[id, shape]
)
const handlePointerMove = useCallback(
(e: React.PointerEvent) => {
state.send("MOVED_OVER_SHAPE", inputs.pointerEnter(e, id))
state.send('MOVED_OVER_SHAPE', inputs.pointerEnter(e, id))
},
[id, shape]
)
const handlePointerLeave = useCallback(
() => state.send("UNHOVERED_SHAPE", { target: id }),
() => state.send('UNHOVERED_SHAPE', { target: id }),
[id]
)
@ -71,47 +72,38 @@ function Shape({ id }: { id: string }) {
onPointerLeave={handlePointerLeave}
onPointerMove={handlePointerMove}
>
<defs>{getShapeUtils(shape).render(shape)}</defs>
<HoverIndicator as="use" href={"#" + id} />
<MainShape as="use" href={"#" + id} {...shape.style} />
<Indicator as="use" href={"#" + id} />
{isSelecting && <HoverIndicator as="use" href={'#' + id} />}
<StyledShape id={id} style={shape.style} />
</StyledGroup>
)
}
const MainShape = styled("use", {
const StyledShape = memo(
({ id, style }: { id: string; style: ShapeStyles }) => {
return <MainShape as="use" href={'#' + id} {...style} />
}
)
const MainShape = styled('use', {
zStrokeWidth: 1,
})
const Indicator = styled("path", {
fill: "none",
stroke: "transparent",
zStrokeWidth: 1,
pointerEvents: "none",
strokeLineCap: "round",
strokeLinejoin: "round",
const HoverIndicator = styled('path', {
fill: 'none',
stroke: 'transparent',
pointerEvents: 'all',
strokeLinecap: 'round',
strokeLinejoin: 'round',
transform: 'all .2s',
})
const HoverIndicator = styled("path", {
fill: "none",
stroke: "transparent",
pointerEvents: "all",
strokeLinecap: "round",
strokeLinejoin: "round",
transform: "all .2s",
})
const StyledGroup = styled("g", {
const StyledGroup = styled('g', {
[`& ${HoverIndicator}`]: {
opacity: "0",
opacity: '0',
},
variants: {
isSelected: {
true: {
[`& ${Indicator}`]: {
stroke: "$selected",
},
},
true: {},
false: {},
},
isHovered: {
@ -125,8 +117,8 @@ const StyledGroup = styled("g", {
isHovered: true,
css: {
[`& ${HoverIndicator}`]: {
opacity: "1",
stroke: "$hint",
opacity: '1',
stroke: '$hint',
zStrokeWidth: [8, 4],
},
},
@ -136,8 +128,8 @@ const StyledGroup = styled("g", {
isHovered: false,
css: {
[`& ${HoverIndicator}`]: {
opacity: "1",
stroke: "$hint",
opacity: '1',
stroke: '$hint',
zStrokeWidth: [6, 3],
},
},
@ -147,8 +139,8 @@ const StyledGroup = styled("g", {
isHovered: true,
css: {
[`& ${HoverIndicator}`]: {
opacity: "1",
stroke: "$hint",
opacity: '1',
stroke: '$hint',
zStrokeWidth: [8, 4],
},
},
@ -156,6 +148,6 @@ const StyledGroup = styled("g", {
],
})
export { Indicator, HoverIndicator }
export { HoverIndicator }
export default memo(Shape)

53
hooks/useShapeEvents.ts Normal file
View file

@ -0,0 +1,53 @@
import { MutableRefObject, useCallback } from 'react'
import state from 'state'
import inputs from 'state/inputs'
export default function useShapeEvents(
id: string,
rGroup: MutableRefObject<SVGElement>
) {
const handlePointerDown = useCallback(
(e: React.PointerEvent) => {
e.stopPropagation()
rGroup.current.setPointerCapture(e.pointerId)
state.send('POINTED_SHAPE', inputs.pointerDown(e, id))
},
[id]
)
const handlePointerUp = useCallback(
(e: React.PointerEvent) => {
e.stopPropagation()
rGroup.current.releasePointerCapture(e.pointerId)
state.send('STOPPED_POINTING', inputs.pointerUp(e))
},
[id]
)
const handlePointerEnter = useCallback(
(e: React.PointerEvent) => {
state.send('HOVERED_SHAPE', inputs.pointerEnter(e, id))
},
[id]
)
const handlePointerMove = useCallback(
(e: React.PointerEvent) => {
state.send('MOVED_OVER_SHAPE', inputs.pointerEnter(e, id))
},
[id]
)
const handlePointerLeave = useCallback(
() => state.send('UNHOVERED_SHAPE', { target: id }),
[id]
)
return {
onPointerDown: handlePointerDown,
onPointerUp: handlePointerUp,
onPointerEnter: handlePointerEnter,
onPointerMove: handlePointerMove,
onPointerLeave: handlePointerLeave,
}
}

View file

@ -1,19 +1,19 @@
import { v4 as uuid } from "uuid"
import * as vec from "utils/vec"
import { DrawShape, ShapeType } from "types"
import { registerShapeUtils } from "./index"
import { intersectPolylineBounds } from "utils/intersections"
import { boundsContainPolygon } from "utils/bounds"
import getStroke from "perfect-freehand"
import { v4 as uuid } from 'uuid'
import * as vec from 'utils/vec'
import { DrawShape, ShapeType } from 'types'
import { registerShapeUtils } from './index'
import { intersectPolylineBounds } from 'utils/intersections'
import { boundsContainPolygon } from 'utils/bounds'
import getStroke from 'perfect-freehand'
import {
getBoundsFromPoints,
getSvgPathFromStroke,
translateBounds,
} from "utils/utils"
import { DotCircle } from "components/canvas/misc"
import { shades } from "lib/colors"
} from 'utils/utils'
import { DotCircle } from 'components/canvas/misc'
import { shades } from 'lib/colors'
const pathCache = new WeakMap<DrawShape, string>([])
const pathCache = new WeakMap<number[][], string>([])
const draw = registerShapeUtils<DrawShape>({
boundsCache: new WeakMap([]),
@ -23,8 +23,8 @@ const draw = registerShapeUtils<DrawShape>({
id: uuid(),
type: ShapeType.Draw,
isGenerated: false,
name: "Draw",
parentId: "page0",
name: 'Draw',
parentId: 'page0',
childIndex: 0,
point: [0, 0],
points: [[0, 0]],
@ -32,10 +32,10 @@ const draw = registerShapeUtils<DrawShape>({
...props,
style: {
strokeWidth: 2,
strokeLinecap: "round",
strokeLinejoin: "round",
strokeLinecap: 'round',
strokeLinejoin: 'round',
...props.style,
stroke: "transparent",
stroke: 'transparent',
},
}
},
@ -44,19 +44,18 @@ const draw = registerShapeUtils<DrawShape>({
const { id, point, points } = shape
if (points.length < 2) {
return <DotCircle cx={point[0]} cy={point[1]} r={3} />
return <DotCircle id={id} cx={point[0]} cy={point[1]} r={3} />
}
if (!pathCache.has(shape)) {
pathCache.set(shape, getSvgPathFromStroke(getStroke(points)))
if (!pathCache.has(points)) {
pathCache.set(points, getSvgPathFromStroke(getStroke(points)))
}
return <path id={id} d={pathCache.get(shape)} />
return <path id={id} d={pathCache.get(points)} />
},
applyStyles(shape, style) {
Object.assign(shape.style, style)
shape.style.fill = "transparent"
return this
},

View file

@ -1,10 +1,10 @@
import { current } from "immer"
import { Data, DrawShape } from "types"
import BaseSession from "./base-session"
import { getShapeUtils } from "lib/shape-utils"
import { getPage, simplify } from "utils/utils"
import * as vec from "utils/vec"
import commands from "state/commands"
import { current } from 'immer'
import { Data, DrawShape } from 'types'
import BaseSession from './base-session'
import { getShapeUtils } from 'lib/shape-utils'
import { getPage, simplify } from 'utils/utils'
import * as vec from 'utils/vec'
import commands from 'state/commands'
export default class BrushSession extends BaseSession {
origin: number[]
@ -29,7 +29,7 @@ export default class BrushSession extends BaseSession {
update = (data: Data, point: number[]) => {
const { shapeId } = this
const lp = vec.med(this.previous, point)
const lp = vec.med(this.previous, vec.toPrecision(point))
this.points.push(vec.sub(lp, this.origin))
this.previous = lp
@ -46,15 +46,7 @@ export default class BrushSession extends BaseSession {
}
complete = (data: Data) => {
commands.draw(
data,
this.shapeId,
this.snapshot.points,
simplify(this.points, 0.1 / data.camera.zoom).map(([x, y]) => [
Math.trunc(x * 100) / 100,
Math.trunc(y * 100) / 100,
])
)
commands.draw(data, this.shapeId, this.snapshot.points, this.points)
}
}

View file

@ -6,7 +6,7 @@
export function clamp(n: number, min: number): number
export function clamp(n: number, min: number, max: number): number
export function clamp(n: number, min: number, max?: number): number {
return Math.max(min, typeof max !== "undefined" ? Math.min(n, max) : n)
return Math.max(min, typeof max !== 'undefined' ? Math.min(n, max) : n)
}
/**
@ -477,3 +477,12 @@ export function distanceToLineSegment(
export function nudge(A: number[], B: number[], d: number) {
return add(A, mul(uni(vec(A, B)), d))
}
/**
* Round a vector to a precision length.
* @param a
* @param n
*/
export function toPrecision(a: number[], n = 3) {
return [+a[0].toPrecision(n), +a[1].toPrecision(n)]
}