Steve Ruiz vor 4 Jahren
Ursprung
Commit
79c254a938

+ 6
- 0
.prettierrc Datei anzeigen

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

+ 14
- 12
components/canvas/bounds/bounding-box.tsx Datei anzeigen

@@ -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 }) =>
@@ -20,17 +21,18 @@ export default function Bounds() {
20 21
   if (!bounds) return null
21 22
   if (!isSelecting) return null
22 23
 
23
-  const size = (isMobile().any ? 16 : 8) / zoom // Touch target size
24
+  const size = (isMobile().any ? 12 : 8) / zoom // Touch target size
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 Datei anzeigen

@@ -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', {})

+ 25
- 0
components/canvas/defs.tsx Datei anzeigen

@@ -0,0 +1,25 @@
1
+import { getShapeUtils } from "lib/shape-utils"
2
+import { useSelector } from "state"
3
+import { deepCompareArrays, getPage } from "utils/utils"
4
+
5
+export default function Defs() {
6
+  const currentPageShapeIds = useSelector(({ data }) => {
7
+    return Object.values(getPage(data).shapes)
8
+      .sort((a, b) => a.childIndex - b.childIndex)
9
+      .map((shape) => shape.id)
10
+  }, deepCompareArrays)
11
+
12
+  return (
13
+    <defs>
14
+      {currentPageShapeIds.map((id) => (
15
+        <Def key={id} id={id} />
16
+      ))}
17
+    </defs>
18
+  )
19
+}
20
+
21
+export function Def({ id }: { id: string }) {
22
+  const shape = useSelector(({ data }) => getPage(data).shapes[id])
23
+
24
+  return getShapeUtils(shape).render(shape)
25
+}

+ 6
- 4
components/canvas/page.tsx Datei anzeigen

@@ -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 Datei anzeigen

@@ -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
+})

+ 41
- 49
components/canvas/shape.tsx Datei anzeigen

@@ -1,24 +1,25 @@
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 }) {
9
-  const rGroup = useRef<SVGGElement>(null)
10
-
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 }) {
11 10
   const isHovered = useSelector((state) => state.data.hoveredId === id)
12 11
 
13 12
   const isSelected = useSelector((state) => state.values.selectedIds.has(id))
14 13
 
15 14
   const shape = useSelector(({ data }) => getPage(data).shapes[id])
16 15
 
16
+  const rGroup = useRef<SVGGElement>(null)
17
+
17 18
   const handlePointerDown = useCallback(
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" xlinkHref={"#" + id} />
76
-      <MainShape as="use" xlinkHref={"#" + id} {...shape.style} />
77
-      <Indicator as="use" xlinkHref={"#" + 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 Datei anzeigen

@@ -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
+}

+ 33
- 1
hooks/useZoomEvents.ts Datei anzeigen

@@ -2,6 +2,7 @@ import React, { useEffect, useRef } from "react"
2 2
 import state from "state"
3 3
 import inputs from "state/inputs"
4 4
 import * as vec from "utils/vec"
5
+import { usePinch } from "react-use-gesture"
5 6
 
6 7
 /**
7 8
  * Capture zoom gestures (pinches, wheels and pans) and send to the state.
@@ -65,5 +66,36 @@ export default function useZoomEvents(
65 66
     }
66 67
   }, [ref])
67 68
 
68
-  return {}
69
+  const rPinchDa = useRef<number[] | undefined>(undefined)
70
+  const rPinchAngle = useRef<number>(undefined)
71
+  const rPinchPoint = useRef<number[] | undefined>(undefined)
72
+
73
+  const bind = usePinch(({ pinching, da, origin }) => {
74
+    if (!pinching) {
75
+      state.send("STOPPED_PINCHING")
76
+      rPinchDa.current = undefined
77
+      rPinchPoint.current = undefined
78
+      return
79
+    }
80
+
81
+    if (rPinchPoint.current === undefined) {
82
+      state.send("STARTED_PINCHING")
83
+      rPinchDa.current = da
84
+      rPinchPoint.current = origin
85
+    }
86
+
87
+    const [distanceDelta, angleDelta] = vec.sub(rPinchDa.current, da)
88
+
89
+    state.send("PINCHED", {
90
+      delta: vec.sub(rPinchPoint.current, origin),
91
+      point: origin,
92
+      distanceDelta,
93
+      angleDelta,
94
+    })
95
+
96
+    rPinchDa.current = da
97
+    rPinchPoint.current = origin
98
+  })
99
+
100
+  return { ...bind() }
69 101
 }

+ 20
- 21
lib/shape-utils/draw.tsx Datei anzeigen

@@ -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
 

+ 1
- 0
package.json Datei anzeigen

@@ -21,6 +21,7 @@
21 21
     "react": "17.0.2",
22 22
     "react-dom": "17.0.2",
23 23
     "react-feather": "^2.0.9",
24
+    "react-use-gesture": "^9.1.3",
24 25
     "uuid": "^8.3.2"
25 26
   },
26 27
   "devDependencies": {

+ 9
- 17
state/sessions/draw-session.ts Datei anzeigen

@@ -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
 

+ 31
- 5
state/state.ts Datei anzeigen

@@ -130,6 +130,7 @@ const state = createState({
130 130
             STRETCHED: "stretchSelection",
131 131
             DISTRIBUTED: "distributeSelection",
132 132
             MOVED: "moveSelection",
133
+            STARTED_PINCHING: { to: "pinching" },
133 134
           },
134 135
           initial: "notPointing",
135 136
           states: {
@@ -248,6 +249,12 @@ const state = createState({
248 249
             },
249 250
           },
250 251
         },
252
+        pinching: {
253
+          on: {
254
+            STOPPED_PINCHING: { to: "selecting" },
255
+            PINCHED: { do: "pinchCamera" },
256
+          },
257
+        },
251 258
         draw: {
252 259
           initial: "creating",
253 260
           states: {
@@ -831,12 +838,31 @@ const state = createState({
831 838
 
832 839
       setZoomCSS(camera.zoom)
833 840
     },
834
-    panCamera(data, payload: { delta: number[]; point: number[] }) {
841
+    panCamera(data, payload: { delta: number[] }) {
835 842
       const { camera } = data
836
-      data.camera.point = vec.sub(
837
-        camera.point,
838
-        vec.div(payload.delta, camera.zoom)
839
-      )
843
+      camera.point = vec.sub(camera.point, vec.div(payload.delta, camera.zoom))
844
+    },
845
+    pinchCamera(
846
+      data,
847
+      payload: {
848
+        delta: number[]
849
+        distanceDelta: number
850
+        angleDelta: number
851
+        point: number[]
852
+      }
853
+    ) {
854
+      const { camera } = data
855
+
856
+      camera.point = vec.sub(camera.point, vec.div(payload.delta, camera.zoom))
857
+
858
+      const next = camera.zoom - (payload.distanceDelta / 300) * camera.zoom
859
+
860
+      const p0 = screenToWorld(payload.point, data)
861
+      camera.zoom = clamp(next, 0.1, 3)
862
+      const p1 = screenToWorld(payload.point, data)
863
+      camera.point = vec.add(camera.point, vec.sub(p1, p0))
864
+
865
+      setZoomCSS(camera.zoom)
840 866
     },
841 867
     deleteSelectedIds(data) {
842 868
       commands.deleteSelected(data)

+ 2
- 2
styles/stitches.config.ts Datei anzeigen

@@ -45,7 +45,7 @@ const { styled, global, css, theme, getCssString } = createCss({
45 45
     zStrokeWidth: () => (value: number | number[]) => {
46 46
       if (Array.isArray(value)) {
47 47
         return {
48
-          strokeWidth: `calc(${value[0]} / var(--camera-zoom))`,
48
+          strokeWidth: `calc(${value[0]}px / var(--camera-zoom))`,
49 49
         }
50 50
       }
51 51
 
@@ -61,7 +61,7 @@ const { styled, global, css, theme, getCssString } = createCss({
61 61
       // }
62 62
 
63 63
       return {
64
-        strokeWidth: `calc(${value} / var(--camera-zoom))`,
64
+        strokeWidth: `calc(${value}px / var(--camera-zoom))`,
65 65
       }
66 66
     },
67 67
   },

+ 10
- 1
utils/vec.ts Datei anzeigen

@@ -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
+}

+ 5
- 0
yarn.lock Datei anzeigen

@@ -6697,6 +6697,11 @@ react-style-singleton@^2.1.0:
6697 6697
     invariant "^2.2.4"
6698 6698
     tslib "^1.0.0"
6699 6699
 
6700
+react-use-gesture@^9.1.3:
6701
+  version "9.1.3"
6702
+  resolved "https://registry.yarnpkg.com/react-use-gesture/-/react-use-gesture-9.1.3.tgz#92bd143e4f58e69bd424514a5bfccba2a1d62ec0"
6703
+  integrity sha512-CdqA2SmS/fj3kkS2W8ZU8wjTbVBAIwDWaRprX7OKaj7HlGwBasGEFggmk5qNklknqk9zK/h8D355bEJFTpqEMg==
6704
+
6700 6705
 react@17.0.2:
6701 6706
   version "17.0.2"
6702 6707
   resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"

Laden…
Abbrechen
Speichern