Преглед на файлове

improves select display

main
Steve Ruiz преди 4 години
родител
ревизия
9fd8ef8421

+ 6
- 0
.prettierrc Целия файл

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

+ 13
- 11
components/canvas/bounds/bounding-box.tsx Целия файл

@@ -1,16 +1,17 @@
1
-import * as React from "react"
2
-import { Edge, Corner } from "types"
3
-import { useSelector } from "state"
4
-import { getSelectedShapes, isMobile } from "utils/utils"
1
+import * as React from 'react'
2
+import { Edge, Corner } from 'types'
3
+import { useSelector } from 'state'
4
+import { getSelectedShapes, isMobile } from 'utils/utils'
5 5
 
6
-import CenterHandle from "./center-handle"
7
-import CornerHandle from "./corner-handle"
8
-import EdgeHandle from "./edge-handle"
9
-import RotateHandle from "./rotate-handle"
6
+import CenterHandle from './center-handle'
7
+import CornerHandle from './corner-handle'
8
+import EdgeHandle from './edge-handle'
9
+import RotateHandle from './rotate-handle'
10
+import Selected from '../selected'
10 11
 
11 12
 export default function Bounds() {
12
-  const isBrushing = useSelector((s) => s.isIn("brushSelecting"))
13
-  const isSelecting = useSelector((s) => s.isIn("selecting"))
13
+  const isBrushing = useSelector((s) => s.isIn('brushSelecting'))
14
+  const isSelecting = useSelector((s) => s.isIn('selecting'))
14 15
   const zoom = useSelector((s) => s.data.camera.zoom)
15 16
   const bounds = useSelector((s) => s.values.selectedBounds)
16 17
   const rotation = useSelector(({ data }) =>
@@ -24,13 +25,14 @@ export default function Bounds() {
24 25
 
25 26
   return (
26 27
     <g
27
-      pointerEvents={isBrushing ? "none" : "all"}
28
+      pointerEvents={isBrushing ? 'none' : 'all'}
28 29
       transform={`
29 30
         rotate(${rotation * (180 / Math.PI)}, 
30 31
         ${(bounds.minX + bounds.maxX) / 2}, 
31 32
         ${(bounds.minY + bounds.maxY) / 2})
32 33
         translate(${bounds.minX},${bounds.minY})`}
33 34
     >
35
+      <Selected bounds={bounds} />
34 36
       <CenterHandle bounds={bounds} />
35 37
       <EdgeHandle size={size} bounds={bounds} edge={Edge.Top} />
36 38
       <EdgeHandle size={size} bounds={bounds} edge={Edge.Right} />

+ 23
- 21
components/canvas/canvas.tsx Целия файл

@@ -1,13 +1,14 @@
1
-import styled from "styles"
2
-import React, { useCallback, useRef } from "react"
3
-import useZoomEvents from "hooks/useZoomEvents"
4
-import useCamera from "hooks/useCamera"
5
-import Page from "./page"
6
-import Brush from "./brush"
7
-import state from "state"
8
-import Bounds from "./bounds/bounding-box"
9
-import BoundsBg from "./bounds/bounds-bg"
10
-import inputs from "state/inputs"
1
+import styled from 'styles'
2
+import state from 'state'
3
+import inputs from 'state/inputs'
4
+import React, { useCallback, useRef } from 'react'
5
+import useZoomEvents from 'hooks/useZoomEvents'
6
+import useCamera from 'hooks/useCamera'
7
+import Defs from './defs'
8
+import Page from './page'
9
+import Brush from './brush'
10
+import Bounds from './bounds/bounding-box'
11
+import BoundsBg from './bounds/bounds-bg'
11 12
 
12 13
 export default function Canvas() {
13 14
   const rCanvas = useRef<SVGSVGElement>(null)
@@ -18,16 +19,16 @@ export default function Canvas() {
18 19
 
19 20
   const handlePointerDown = useCallback((e: React.PointerEvent) => {
20 21
     rCanvas.current.setPointerCapture(e.pointerId)
21
-    state.send("POINTED_CANVAS", inputs.pointerDown(e, "canvas"))
22
+    state.send('POINTED_CANVAS', inputs.pointerDown(e, 'canvas'))
22 23
   }, [])
23 24
 
24 25
   const handlePointerMove = useCallback((e: React.PointerEvent) => {
25
-    state.send("MOVED_POINTER", inputs.pointerMove(e))
26
+    state.send('MOVED_POINTER', inputs.pointerMove(e))
26 27
   }, [])
27 28
 
28 29
   const handlePointerUp = useCallback((e: React.PointerEvent) => {
29 30
     rCanvas.current.releasePointerCapture(e.pointerId)
30
-    state.send("STOPPED_POINTING", { id: "canvas", ...inputs.pointerUp(e) })
31
+    state.send('STOPPED_POINTING', { id: 'canvas', ...inputs.pointerUp(e) })
31 32
   }, [])
32 33
 
33 34
   return (
@@ -38,6 +39,7 @@ export default function Canvas() {
38 39
       onPointerMove={handlePointerMove}
39 40
       onPointerUp={handlePointerUp}
40 41
     >
42
+      <Defs />
41 43
       <MainGroup ref={rGroup}>
42 44
         <BoundsBg />
43 45
         <Page />
@@ -48,18 +50,18 @@ export default function Canvas() {
48 50
   )
49 51
 }
50 52
 
51
-const MainSVG = styled("svg", {
52
-  position: "fixed",
53
+const MainSVG = styled('svg', {
54
+  position: 'fixed',
53 55
   top: 0,
54 56
   left: 0,
55
-  width: "100%",
56
-  height: "100%",
57
-  touchAction: "none",
57
+  width: '100%',
58
+  height: '100%',
59
+  touchAction: 'none',
58 60
   zIndex: 100,
59 61
 
60
-  "& *": {
61
-    userSelect: "none",
62
+  '& *': {
63
+    userSelect: 'none',
62 64
   },
63 65
 })
64 66
 
65
-const MainGroup = styled("g", {})
67
+const MainGroup = styled('g', {})

+ 6
- 4
components/canvas/page.tsx Целия файл

@@ -1,6 +1,6 @@
1
-import { useSelector } from "state"
2
-import { deepCompareArrays, getPage } from "utils/utils"
3
-import Shape from "./shape"
1
+import { useSelector } from 'state'
2
+import { deepCompareArrays, getPage } from 'utils/utils'
3
+import Shape from './shape'
4 4
 
5 5
 /* 
6 6
 On each state change, compare node ids of all shapes
@@ -15,10 +15,12 @@ export default function Page() {
15 15
       .map((shape) => shape.id)
16 16
   }, deepCompareArrays)
17 17
 
18
+  const isSelecting = useSelector((s) => s.isIn('selecting'))
19
+
18 20
   return (
19 21
     <>
20 22
       {currentPageShapeIds.map((shapeId) => (
21
-        <Shape key={shapeId} id={shapeId} />
23
+        <Shape key={shapeId} id={shapeId} isSelecting={isSelecting} />
22 24
       ))}
23 25
     </>
24 26
   )

+ 58
- 0
components/canvas/selected.tsx Целия файл

@@ -0,0 +1,58 @@
1
+import styled from 'styles'
2
+import { useSelector } from 'state'
3
+import {
4
+  deepCompareArrays,
5
+  getBoundsCenter,
6
+  getPage,
7
+  getSelectedShapes,
8
+} from 'utils/utils'
9
+import * as vec from 'utils/vec'
10
+import { getShapeUtils } from 'lib/shape-utils'
11
+import { Bounds } from 'types'
12
+import useShapeEvents from 'hooks/useShapeEvents'
13
+import { useRef } from 'react'
14
+
15
+export default function Selected({ bounds }: { bounds: Bounds }) {
16
+  const currentPageShapeIds = useSelector(({ data }) => {
17
+    return Array.from(data.selectedIds.values())
18
+  }, deepCompareArrays)
19
+
20
+  return (
21
+    <g>
22
+      {currentPageShapeIds.map((id) => (
23
+        <ShapeOutline key={id} id={id} bounds={bounds} />
24
+      ))}
25
+    </g>
26
+  )
27
+}
28
+
29
+export function ShapeOutline({ id, bounds }: { id: string; bounds: Bounds }) {
30
+  const rIndicator = useRef<SVGUseElement>(null)
31
+
32
+  const shape = useSelector(({ data }) => getPage(data).shapes[id])
33
+
34
+  const shapeBounds = getShapeUtils(shape).getBounds(shape)
35
+
36
+  const events = useShapeEvents(id, rIndicator)
37
+
38
+  return (
39
+    <Indicator
40
+      ref={rIndicator}
41
+      as="use"
42
+      href={'#' + id}
43
+      transform={`rotate(${shape.rotation * (180 / Math.PI)},${getBoundsCenter(
44
+        shapeBounds
45
+      )}) translate(${vec.sub(shape.point, [bounds.minX, bounds.minY])})`}
46
+      {...events}
47
+    />
48
+  )
49
+}
50
+
51
+const Indicator = styled('path', {
52
+  zStrokeWidth: 1,
53
+  strokeLineCap: 'round',
54
+  strokeLinejoin: 'round',
55
+  stroke: '$selected',
56
+  fill: 'transparent',
57
+  pointerEvents: 'all',
58
+})

+ 39
- 47
components/canvas/shape.tsx Целия файл

@@ -1,11 +1,12 @@
1
-import React, { useCallback, useRef, memo } from "react"
2
-import state, { useSelector } from "state"
3
-import inputs from "state/inputs"
4
-import styled from "styles"
5
-import { getShapeUtils } from "lib/shape-utils"
6
-import { getPage } from "utils/utils"
7
-
8
-function Shape({ id }: { id: string }) {
1
+import React, { useCallback, useRef, memo } from 'react'
2
+import state, { useSelector } from 'state'
3
+import inputs from 'state/inputs'
4
+import styled from 'styles'
5
+import { getShapeUtils } from 'lib/shape-utils'
6
+import { getPage } from 'utils/utils'
7
+import { ShapeStyles } from 'types'
8
+
9
+function Shape({ id, isSelecting }: { id: string; isSelecting: boolean }) {
9 10
   const isHovered = useSelector((state) => state.data.hoveredId === id)
10 11
 
11 12
   const isSelected = useSelector((state) => state.values.selectedIds.has(id))
@@ -18,7 +19,7 @@ function Shape({ id }: { id: string }) {
18 19
     (e: React.PointerEvent) => {
19 20
       e.stopPropagation()
20 21
       rGroup.current.setPointerCapture(e.pointerId)
21
-      state.send("POINTED_SHAPE", inputs.pointerDown(e, id))
22
+      state.send('POINTED_SHAPE', inputs.pointerDown(e, id))
22 23
     },
23 24
     [id]
24 25
   )
@@ -27,27 +28,27 @@ function Shape({ id }: { id: string }) {
27 28
     (e: React.PointerEvent) => {
28 29
       e.stopPropagation()
29 30
       rGroup.current.releasePointerCapture(e.pointerId)
30
-      state.send("STOPPED_POINTING", inputs.pointerUp(e))
31
+      state.send('STOPPED_POINTING', inputs.pointerUp(e))
31 32
     },
32 33
     [id]
33 34
   )
34 35
 
35 36
   const handlePointerEnter = useCallback(
36 37
     (e: React.PointerEvent) => {
37
-      state.send("HOVERED_SHAPE", inputs.pointerEnter(e, id))
38
+      state.send('HOVERED_SHAPE', inputs.pointerEnter(e, id))
38 39
     },
39 40
     [id, shape]
40 41
   )
41 42
 
42 43
   const handlePointerMove = useCallback(
43 44
     (e: React.PointerEvent) => {
44
-      state.send("MOVED_OVER_SHAPE", inputs.pointerEnter(e, id))
45
+      state.send('MOVED_OVER_SHAPE', inputs.pointerEnter(e, id))
45 46
     },
46 47
     [id, shape]
47 48
   )
48 49
 
49 50
   const handlePointerLeave = useCallback(
50
-    () => state.send("UNHOVERED_SHAPE", { target: id }),
51
+    () => state.send('UNHOVERED_SHAPE', { target: id }),
51 52
     [id]
52 53
   )
53 54
 
@@ -71,47 +72,38 @@ function Shape({ id }: { id: string }) {
71 72
       onPointerLeave={handlePointerLeave}
72 73
       onPointerMove={handlePointerMove}
73 74
     >
74
-      <defs>{getShapeUtils(shape).render(shape)}</defs>
75
-      <HoverIndicator as="use" href={"#" + id} />
76
-      <MainShape as="use" href={"#" + id} {...shape.style} />
77
-      <Indicator as="use" href={"#" + id} />
75
+      {isSelecting && <HoverIndicator as="use" href={'#' + id} />}
76
+      <StyledShape id={id} style={shape.style} />
78 77
     </StyledGroup>
79 78
   )
80 79
 }
81 80
 
82
-const MainShape = styled("use", {
83
-  zStrokeWidth: 1,
84
-})
81
+const StyledShape = memo(
82
+  ({ id, style }: { id: string; style: ShapeStyles }) => {
83
+    return <MainShape as="use" href={'#' + id} {...style} />
84
+  }
85
+)
85 86
 
86
-const Indicator = styled("path", {
87
-  fill: "none",
88
-  stroke: "transparent",
87
+const MainShape = styled('use', {
89 88
   zStrokeWidth: 1,
90
-  pointerEvents: "none",
91
-  strokeLineCap: "round",
92
-  strokeLinejoin: "round",
93 89
 })
94 90
 
95
-const HoverIndicator = styled("path", {
96
-  fill: "none",
97
-  stroke: "transparent",
98
-  pointerEvents: "all",
99
-  strokeLinecap: "round",
100
-  strokeLinejoin: "round",
101
-  transform: "all .2s",
91
+const HoverIndicator = styled('path', {
92
+  fill: 'none',
93
+  stroke: 'transparent',
94
+  pointerEvents: 'all',
95
+  strokeLinecap: 'round',
96
+  strokeLinejoin: 'round',
97
+  transform: 'all .2s',
102 98
 })
103 99
 
104
-const StyledGroup = styled("g", {
100
+const StyledGroup = styled('g', {
105 101
   [`& ${HoverIndicator}`]: {
106
-    opacity: "0",
102
+    opacity: '0',
107 103
   },
108 104
   variants: {
109 105
     isSelected: {
110
-      true: {
111
-        [`& ${Indicator}`]: {
112
-          stroke: "$selected",
113
-        },
114
-      },
106
+      true: {},
115 107
       false: {},
116 108
     },
117 109
     isHovered: {
@@ -125,8 +117,8 @@ const StyledGroup = styled("g", {
125 117
       isHovered: true,
126 118
       css: {
127 119
         [`& ${HoverIndicator}`]: {
128
-          opacity: "1",
129
-          stroke: "$hint",
120
+          opacity: '1',
121
+          stroke: '$hint',
130 122
           zStrokeWidth: [8, 4],
131 123
         },
132 124
       },
@@ -136,8 +128,8 @@ const StyledGroup = styled("g", {
136 128
       isHovered: false,
137 129
       css: {
138 130
         [`& ${HoverIndicator}`]: {
139
-          opacity: "1",
140
-          stroke: "$hint",
131
+          opacity: '1',
132
+          stroke: '$hint',
141 133
           zStrokeWidth: [6, 3],
142 134
         },
143 135
       },
@@ -147,8 +139,8 @@ const StyledGroup = styled("g", {
147 139
       isHovered: true,
148 140
       css: {
149 141
         [`& ${HoverIndicator}`]: {
150
-          opacity: "1",
151
-          stroke: "$hint",
142
+          opacity: '1',
143
+          stroke: '$hint',
152 144
           zStrokeWidth: [8, 4],
153 145
         },
154 146
       },
@@ -156,6 +148,6 @@ const StyledGroup = styled("g", {
156 148
   ],
157 149
 })
158 150
 
159
-export { Indicator, HoverIndicator }
151
+export { HoverIndicator }
160 152
 
161 153
 export default memo(Shape)

+ 53
- 0
hooks/useShapeEvents.ts Целия файл

@@ -0,0 +1,53 @@
1
+import { MutableRefObject, useCallback } from 'react'
2
+import state from 'state'
3
+import inputs from 'state/inputs'
4
+
5
+export default function useShapeEvents(
6
+  id: string,
7
+  rGroup: MutableRefObject<SVGElement>
8
+) {
9
+  const handlePointerDown = useCallback(
10
+    (e: React.PointerEvent) => {
11
+      e.stopPropagation()
12
+      rGroup.current.setPointerCapture(e.pointerId)
13
+      state.send('POINTED_SHAPE', inputs.pointerDown(e, id))
14
+    },
15
+    [id]
16
+  )
17
+
18
+  const handlePointerUp = useCallback(
19
+    (e: React.PointerEvent) => {
20
+      e.stopPropagation()
21
+      rGroup.current.releasePointerCapture(e.pointerId)
22
+      state.send('STOPPED_POINTING', inputs.pointerUp(e))
23
+    },
24
+    [id]
25
+  )
26
+
27
+  const handlePointerEnter = useCallback(
28
+    (e: React.PointerEvent) => {
29
+      state.send('HOVERED_SHAPE', inputs.pointerEnter(e, id))
30
+    },
31
+    [id]
32
+  )
33
+
34
+  const handlePointerMove = useCallback(
35
+    (e: React.PointerEvent) => {
36
+      state.send('MOVED_OVER_SHAPE', inputs.pointerEnter(e, id))
37
+    },
38
+    [id]
39
+  )
40
+
41
+  const handlePointerLeave = useCallback(
42
+    () => state.send('UNHOVERED_SHAPE', { target: id }),
43
+    [id]
44
+  )
45
+
46
+  return {
47
+    onPointerDown: handlePointerDown,
48
+    onPointerUp: handlePointerUp,
49
+    onPointerEnter: handlePointerEnter,
50
+    onPointerMove: handlePointerMove,
51
+    onPointerLeave: handlePointerLeave,
52
+  }
53
+}

+ 20
- 21
lib/shape-utils/draw.tsx Целия файл

@@ -1,19 +1,19 @@
1
-import { v4 as uuid } from "uuid"
2
-import * as vec from "utils/vec"
3
-import { DrawShape, ShapeType } from "types"
4
-import { registerShapeUtils } from "./index"
5
-import { intersectPolylineBounds } from "utils/intersections"
6
-import { boundsContainPolygon } from "utils/bounds"
7
-import getStroke from "perfect-freehand"
1
+import { v4 as uuid } from 'uuid'
2
+import * as vec from 'utils/vec'
3
+import { DrawShape, ShapeType } from 'types'
4
+import { registerShapeUtils } from './index'
5
+import { intersectPolylineBounds } from 'utils/intersections'
6
+import { boundsContainPolygon } from 'utils/bounds'
7
+import getStroke from 'perfect-freehand'
8 8
 import {
9 9
   getBoundsFromPoints,
10 10
   getSvgPathFromStroke,
11 11
   translateBounds,
12
-} from "utils/utils"
13
-import { DotCircle } from "components/canvas/misc"
14
-import { shades } from "lib/colors"
12
+} from 'utils/utils'
13
+import { DotCircle } from 'components/canvas/misc'
14
+import { shades } from 'lib/colors'
15 15
 
16
-const pathCache = new WeakMap<DrawShape, string>([])
16
+const pathCache = new WeakMap<number[][], string>([])
17 17
 
18 18
 const draw = registerShapeUtils<DrawShape>({
19 19
   boundsCache: new WeakMap([]),
@@ -23,8 +23,8 @@ const draw = registerShapeUtils<DrawShape>({
23 23
       id: uuid(),
24 24
       type: ShapeType.Draw,
25 25
       isGenerated: false,
26
-      name: "Draw",
27
-      parentId: "page0",
26
+      name: 'Draw',
27
+      parentId: 'page0',
28 28
       childIndex: 0,
29 29
       point: [0, 0],
30 30
       points: [[0, 0]],
@@ -32,10 +32,10 @@ const draw = registerShapeUtils<DrawShape>({
32 32
       ...props,
33 33
       style: {
34 34
         strokeWidth: 2,
35
-        strokeLinecap: "round",
36
-        strokeLinejoin: "round",
35
+        strokeLinecap: 'round',
36
+        strokeLinejoin: 'round',
37 37
         ...props.style,
38
-        stroke: "transparent",
38
+        stroke: 'transparent',
39 39
       },
40 40
     }
41 41
   },
@@ -44,19 +44,18 @@ const draw = registerShapeUtils<DrawShape>({
44 44
     const { id, point, points } = shape
45 45
 
46 46
     if (points.length < 2) {
47
-      return <DotCircle cx={point[0]} cy={point[1]} r={3} />
47
+      return <DotCircle id={id} cx={point[0]} cy={point[1]} r={3} />
48 48
     }
49 49
 
50
-    if (!pathCache.has(shape)) {
51
-      pathCache.set(shape, getSvgPathFromStroke(getStroke(points)))
50
+    if (!pathCache.has(points)) {
51
+      pathCache.set(points, getSvgPathFromStroke(getStroke(points)))
52 52
     }
53 53
 
54
-    return <path id={id} d={pathCache.get(shape)} />
54
+    return <path id={id} d={pathCache.get(points)} />
55 55
   },
56 56
 
57 57
   applyStyles(shape, style) {
58 58
     Object.assign(shape.style, style)
59
-    shape.style.fill = "transparent"
60 59
     return this
61 60
   },
62 61
 

+ 9
- 17
state/sessions/draw-session.ts Целия файл

@@ -1,10 +1,10 @@
1
-import { current } from "immer"
2
-import { Data, DrawShape } from "types"
3
-import BaseSession from "./base-session"
4
-import { getShapeUtils } from "lib/shape-utils"
5
-import { getPage, simplify } from "utils/utils"
6
-import * as vec from "utils/vec"
7
-import commands from "state/commands"
1
+import { current } from 'immer'
2
+import { Data, DrawShape } from 'types'
3
+import BaseSession from './base-session'
4
+import { getShapeUtils } from 'lib/shape-utils'
5
+import { getPage, simplify } from 'utils/utils'
6
+import * as vec from 'utils/vec'
7
+import commands from 'state/commands'
8 8
 
9 9
 export default class BrushSession extends BaseSession {
10 10
   origin: number[]
@@ -29,7 +29,7 @@ export default class BrushSession extends BaseSession {
29 29
   update = (data: Data, point: number[]) => {
30 30
     const { shapeId } = this
31 31
 
32
-    const lp = vec.med(this.previous, point)
32
+    const lp = vec.med(this.previous, vec.toPrecision(point))
33 33
     this.points.push(vec.sub(lp, this.origin))
34 34
     this.previous = lp
35 35
 
@@ -46,15 +46,7 @@ export default class BrushSession extends BaseSession {
46 46
   }
47 47
 
48 48
   complete = (data: Data) => {
49
-    commands.draw(
50
-      data,
51
-      this.shapeId,
52
-      this.snapshot.points,
53
-      simplify(this.points, 0.1 / data.camera.zoom).map(([x, y]) => [
54
-        Math.trunc(x * 100) / 100,
55
-        Math.trunc(y * 100) / 100,
56
-      ])
57
-    )
49
+    commands.draw(data, this.shapeId, this.snapshot.points, this.points)
58 50
   }
59 51
 }
60 52
 

+ 10
- 1
utils/vec.ts Целия файл

@@ -6,7 +6,7 @@
6 6
 export function clamp(n: number, min: number): number
7 7
 export function clamp(n: number, min: number, max: number): number
8 8
 export function clamp(n: number, min: number, max?: number): number {
9
-  return Math.max(min, typeof max !== "undefined" ? Math.min(n, max) : n)
9
+  return Math.max(min, typeof max !== 'undefined' ? Math.min(n, max) : n)
10 10
 }
11 11
 
12 12
 /**
@@ -477,3 +477,12 @@ export function distanceToLineSegment(
477 477
 export function nudge(A: number[], B: number[], d: number) {
478 478
   return add(A, mul(uni(vec(A, B)), d))
479 479
 }
480
+
481
+/**
482
+ * Round a vector to a precision length.
483
+ * @param a
484
+ * @param n
485
+ */
486
+export function toPrecision(a: number[], n = 3) {
487
+  return [+a[0].toPrecision(n), +a[1].toPrecision(n)]
488
+}

Loading…
Отказ
Запис