浏览代码

Merge branch 'main' of https://github.com/steveruizok/code-slate into main

main
Steve Ruiz 4 年前
父节点
当前提交
79c254a938

+ 6
- 0
.prettierrc 查看文件

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

+ 14
- 12
components/canvas/bounds/bounding-box.tsx 查看文件

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
 export default function Bounds() {
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
   const zoom = useSelector((s) => s.data.camera.zoom)
15
   const zoom = useSelector((s) => s.data.camera.zoom)
15
   const bounds = useSelector((s) => s.values.selectedBounds)
16
   const bounds = useSelector((s) => s.values.selectedBounds)
16
   const rotation = useSelector(({ data }) =>
17
   const rotation = useSelector(({ data }) =>
20
   if (!bounds) return null
21
   if (!bounds) return null
21
   if (!isSelecting) return null
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
   return (
26
   return (
26
     <g
27
     <g
27
-      pointerEvents={isBrushing ? "none" : "all"}
28
+      pointerEvents={isBrushing ? 'none' : 'all'}
28
       transform={`
29
       transform={`
29
         rotate(${rotation * (180 / Math.PI)}, 
30
         rotate(${rotation * (180 / Math.PI)}, 
30
         ${(bounds.minX + bounds.maxX) / 2}, 
31
         ${(bounds.minX + bounds.maxX) / 2}, 
31
         ${(bounds.minY + bounds.maxY) / 2})
32
         ${(bounds.minY + bounds.maxY) / 2})
32
         translate(${bounds.minX},${bounds.minY})`}
33
         translate(${bounds.minX},${bounds.minY})`}
33
     >
34
     >
35
+      <Selected bounds={bounds} />
34
       <CenterHandle bounds={bounds} />
36
       <CenterHandle bounds={bounds} />
35
       <EdgeHandle size={size} bounds={bounds} edge={Edge.Top} />
37
       <EdgeHandle size={size} bounds={bounds} edge={Edge.Top} />
36
       <EdgeHandle size={size} bounds={bounds} edge={Edge.Right} />
38
       <EdgeHandle size={size} bounds={bounds} edge={Edge.Right} />

+ 23
- 21
components/canvas/canvas.tsx 查看文件

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
 export default function Canvas() {
13
 export default function Canvas() {
13
   const rCanvas = useRef<SVGSVGElement>(null)
14
   const rCanvas = useRef<SVGSVGElement>(null)
18
 
19
 
19
   const handlePointerDown = useCallback((e: React.PointerEvent) => {
20
   const handlePointerDown = useCallback((e: React.PointerEvent) => {
20
     rCanvas.current.setPointerCapture(e.pointerId)
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
   const handlePointerMove = useCallback((e: React.PointerEvent) => {
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
   const handlePointerUp = useCallback((e: React.PointerEvent) => {
29
   const handlePointerUp = useCallback((e: React.PointerEvent) => {
29
     rCanvas.current.releasePointerCapture(e.pointerId)
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
   return (
34
   return (
38
       onPointerMove={handlePointerMove}
39
       onPointerMove={handlePointerMove}
39
       onPointerUp={handlePointerUp}
40
       onPointerUp={handlePointerUp}
40
     >
41
     >
42
+      <Defs />
41
       <MainGroup ref={rGroup}>
43
       <MainGroup ref={rGroup}>
42
         <BoundsBg />
44
         <BoundsBg />
43
         <Page />
45
         <Page />
48
   )
50
   )
49
 }
51
 }
50
 
52
 
51
-const MainSVG = styled("svg", {
52
-  position: "fixed",
53
+const MainSVG = styled('svg', {
54
+  position: 'fixed',
53
   top: 0,
55
   top: 0,
54
   left: 0,
56
   left: 0,
55
-  width: "100%",
56
-  height: "100%",
57
-  touchAction: "none",
57
+  width: '100%',
58
+  height: '100%',
59
+  touchAction: 'none',
58
   zIndex: 100,
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 查看文件

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 查看文件

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
 On each state change, compare node ids of all shapes
6
 On each state change, compare node ids of all shapes
15
       .map((shape) => shape.id)
15
       .map((shape) => shape.id)
16
   }, deepCompareArrays)
16
   }, deepCompareArrays)
17
 
17
 
18
+  const isSelecting = useSelector((s) => s.isIn('selecting'))
19
+
18
   return (
20
   return (
19
     <>
21
     <>
20
       {currentPageShapeIds.map((shapeId) => (
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 查看文件

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 查看文件

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
   const isHovered = useSelector((state) => state.data.hoveredId === id)
10
   const isHovered = useSelector((state) => state.data.hoveredId === id)
12
 
11
 
13
   const isSelected = useSelector((state) => state.values.selectedIds.has(id))
12
   const isSelected = useSelector((state) => state.values.selectedIds.has(id))
14
 
13
 
15
   const shape = useSelector(({ data }) => getPage(data).shapes[id])
14
   const shape = useSelector(({ data }) => getPage(data).shapes[id])
16
 
15
 
16
+  const rGroup = useRef<SVGGElement>(null)
17
+
17
   const handlePointerDown = useCallback(
18
   const handlePointerDown = useCallback(
18
     (e: React.PointerEvent) => {
19
     (e: React.PointerEvent) => {
19
       e.stopPropagation()
20
       e.stopPropagation()
20
       rGroup.current.setPointerCapture(e.pointerId)
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
     [id]
24
     [id]
24
   )
25
   )
27
     (e: React.PointerEvent) => {
28
     (e: React.PointerEvent) => {
28
       e.stopPropagation()
29
       e.stopPropagation()
29
       rGroup.current.releasePointerCapture(e.pointerId)
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
     [id]
33
     [id]
33
   )
34
   )
34
 
35
 
35
   const handlePointerEnter = useCallback(
36
   const handlePointerEnter = useCallback(
36
     (e: React.PointerEvent) => {
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
     [id, shape]
40
     [id, shape]
40
   )
41
   )
41
 
42
 
42
   const handlePointerMove = useCallback(
43
   const handlePointerMove = useCallback(
43
     (e: React.PointerEvent) => {
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
     [id, shape]
47
     [id, shape]
47
   )
48
   )
48
 
49
 
49
   const handlePointerLeave = useCallback(
50
   const handlePointerLeave = useCallback(
50
-    () => state.send("UNHOVERED_SHAPE", { target: id }),
51
+    () => state.send('UNHOVERED_SHAPE', { target: id }),
51
     [id]
52
     [id]
52
   )
53
   )
53
 
54
 
71
       onPointerLeave={handlePointerLeave}
72
       onPointerLeave={handlePointerLeave}
72
       onPointerMove={handlePointerMove}
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
     </StyledGroup>
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
   zStrokeWidth: 1,
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
   [`& ${HoverIndicator}`]: {
101
   [`& ${HoverIndicator}`]: {
106
-    opacity: "0",
102
+    opacity: '0',
107
   },
103
   },
108
   variants: {
104
   variants: {
109
     isSelected: {
105
     isSelected: {
110
-      true: {
111
-        [`& ${Indicator}`]: {
112
-          stroke: "$selected",
113
-        },
114
-      },
106
+      true: {},
115
       false: {},
107
       false: {},
116
     },
108
     },
117
     isHovered: {
109
     isHovered: {
125
       isHovered: true,
117
       isHovered: true,
126
       css: {
118
       css: {
127
         [`& ${HoverIndicator}`]: {
119
         [`& ${HoverIndicator}`]: {
128
-          opacity: "1",
129
-          stroke: "$hint",
120
+          opacity: '1',
121
+          stroke: '$hint',
130
           zStrokeWidth: [8, 4],
122
           zStrokeWidth: [8, 4],
131
         },
123
         },
132
       },
124
       },
136
       isHovered: false,
128
       isHovered: false,
137
       css: {
129
       css: {
138
         [`& ${HoverIndicator}`]: {
130
         [`& ${HoverIndicator}`]: {
139
-          opacity: "1",
140
-          stroke: "$hint",
131
+          opacity: '1',
132
+          stroke: '$hint',
141
           zStrokeWidth: [6, 3],
133
           zStrokeWidth: [6, 3],
142
         },
134
         },
143
       },
135
       },
147
       isHovered: true,
139
       isHovered: true,
148
       css: {
140
       css: {
149
         [`& ${HoverIndicator}`]: {
141
         [`& ${HoverIndicator}`]: {
150
-          opacity: "1",
151
-          stroke: "$hint",
142
+          opacity: '1',
143
+          stroke: '$hint',
152
           zStrokeWidth: [8, 4],
144
           zStrokeWidth: [8, 4],
153
         },
145
         },
154
       },
146
       },
156
   ],
148
   ],
157
 })
149
 })
158
 
150
 
159
-export { Indicator, HoverIndicator }
151
+export { HoverIndicator }
160
 
152
 
161
 export default memo(Shape)
153
 export default memo(Shape)

+ 53
- 0
hooks/useShapeEvents.ts 查看文件

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 查看文件

2
 import state from "state"
2
 import state from "state"
3
 import inputs from "state/inputs"
3
 import inputs from "state/inputs"
4
 import * as vec from "utils/vec"
4
 import * as vec from "utils/vec"
5
+import { usePinch } from "react-use-gesture"
5
 
6
 
6
 /**
7
 /**
7
  * Capture zoom gestures (pinches, wheels and pans) and send to the state.
8
  * Capture zoom gestures (pinches, wheels and pans) and send to the state.
65
     }
66
     }
66
   }, [ref])
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 查看文件

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
 import {
8
 import {
9
   getBoundsFromPoints,
9
   getBoundsFromPoints,
10
   getSvgPathFromStroke,
10
   getSvgPathFromStroke,
11
   translateBounds,
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
 const draw = registerShapeUtils<DrawShape>({
18
 const draw = registerShapeUtils<DrawShape>({
19
   boundsCache: new WeakMap([]),
19
   boundsCache: new WeakMap([]),
23
       id: uuid(),
23
       id: uuid(),
24
       type: ShapeType.Draw,
24
       type: ShapeType.Draw,
25
       isGenerated: false,
25
       isGenerated: false,
26
-      name: "Draw",
27
-      parentId: "page0",
26
+      name: 'Draw',
27
+      parentId: 'page0',
28
       childIndex: 0,
28
       childIndex: 0,
29
       point: [0, 0],
29
       point: [0, 0],
30
       points: [[0, 0]],
30
       points: [[0, 0]],
32
       ...props,
32
       ...props,
33
       style: {
33
       style: {
34
         strokeWidth: 2,
34
         strokeWidth: 2,
35
-        strokeLinecap: "round",
36
-        strokeLinejoin: "round",
35
+        strokeLinecap: 'round',
36
+        strokeLinejoin: 'round',
37
         ...props.style,
37
         ...props.style,
38
-        stroke: "transparent",
38
+        stroke: 'transparent',
39
       },
39
       },
40
     }
40
     }
41
   },
41
   },
44
     const { id, point, points } = shape
44
     const { id, point, points } = shape
45
 
45
 
46
     if (points.length < 2) {
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
   applyStyles(shape, style) {
57
   applyStyles(shape, style) {
58
     Object.assign(shape.style, style)
58
     Object.assign(shape.style, style)
59
-    shape.style.fill = "transparent"
60
     return this
59
     return this
61
   },
60
   },
62
 
61
 

+ 1
- 0
package.json 查看文件

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

+ 9
- 17
state/sessions/draw-session.ts 查看文件

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
 export default class BrushSession extends BaseSession {
9
 export default class BrushSession extends BaseSession {
10
   origin: number[]
10
   origin: number[]
29
   update = (data: Data, point: number[]) => {
29
   update = (data: Data, point: number[]) => {
30
     const { shapeId } = this
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
     this.points.push(vec.sub(lp, this.origin))
33
     this.points.push(vec.sub(lp, this.origin))
34
     this.previous = lp
34
     this.previous = lp
35
 
35
 
46
   }
46
   }
47
 
47
 
48
   complete = (data: Data) => {
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 查看文件

130
             STRETCHED: "stretchSelection",
130
             STRETCHED: "stretchSelection",
131
             DISTRIBUTED: "distributeSelection",
131
             DISTRIBUTED: "distributeSelection",
132
             MOVED: "moveSelection",
132
             MOVED: "moveSelection",
133
+            STARTED_PINCHING: { to: "pinching" },
133
           },
134
           },
134
           initial: "notPointing",
135
           initial: "notPointing",
135
           states: {
136
           states: {
248
             },
249
             },
249
           },
250
           },
250
         },
251
         },
252
+        pinching: {
253
+          on: {
254
+            STOPPED_PINCHING: { to: "selecting" },
255
+            PINCHED: { do: "pinchCamera" },
256
+          },
257
+        },
251
         draw: {
258
         draw: {
252
           initial: "creating",
259
           initial: "creating",
253
           states: {
260
           states: {
831
 
838
 
832
       setZoomCSS(camera.zoom)
839
       setZoomCSS(camera.zoom)
833
     },
840
     },
834
-    panCamera(data, payload: { delta: number[]; point: number[] }) {
841
+    panCamera(data, payload: { delta: number[] }) {
835
       const { camera } = data
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
     deleteSelectedIds(data) {
867
     deleteSelectedIds(data) {
842
       commands.deleteSelected(data)
868
       commands.deleteSelected(data)

+ 2
- 2
styles/stitches.config.ts 查看文件

45
     zStrokeWidth: () => (value: number | number[]) => {
45
     zStrokeWidth: () => (value: number | number[]) => {
46
       if (Array.isArray(value)) {
46
       if (Array.isArray(value)) {
47
         return {
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
       // }
61
       // }
62
 
62
 
63
       return {
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 查看文件

6
 export function clamp(n: number, min: number): number
6
 export function clamp(n: number, min: number): number
7
 export function clamp(n: number, min: number, max: number): number
7
 export function clamp(n: number, min: number, max: number): number
8
 export function clamp(n: number, min: number, max?: number): number {
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
 export function nudge(A: number[], B: number[], d: number) {
477
 export function nudge(A: number[], B: number[], d: number) {
478
   return add(A, mul(uni(vec(A, B)), d))
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 查看文件

6697
     invariant "^2.2.4"
6697
     invariant "^2.2.4"
6698
     tslib "^1.0.0"
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
 react@17.0.2:
6705
 react@17.0.2:
6701
   version "17.0.2"
6706
   version "17.0.2"
6702
   resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
6707
   resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"

正在加载...
取消
保存