Steve Ruiz 4 anni fa
parent
commit
815bf1109c

+ 3
- 3
components/canvas/bounds/handles.tsx Vedi File

@@ -18,9 +18,9 @@ export default function Handles() {
18 18
       selectedIds.length === 1 && getPage(data).shapes[selectedIds[0]]
19 19
   )
20 20
 
21
-  const isTranslatingHandles = useSelector((s) => s.isIn('translatingHandles'))
21
+  const isSelecting = useSelector((s) => s.isIn('selecting.notPointing'))
22 22
 
23
-  if (!shape.handles || isTranslatingHandles) return null
23
+  if (!shape.handles || !isSelecting) return null
24 24
 
25 25
   return (
26 26
     <g>
@@ -57,7 +57,7 @@ function Handle({
57 57
       pointerEvents="all"
58 58
       transform={`translate(${point})`}
59 59
     >
60
-      <HandleCircleOuter r={8} />
60
+      <HandleCircleOuter r={12} />
61 61
       <DotCircle r={4} />
62 62
     </g>
63 63
   )

+ 11
- 0
components/canvas/canvas.tsx Vedi File

@@ -34,10 +34,20 @@ export default function Canvas() {
34 34
     } else {
35 35
       if (isMobile()) {
36 36
         state.send('TOUCHED_CANVAS')
37
+        // state.send('POINTED_CANVAS', inputs.touchStart(e, 'canvas'))
38
+        // e.preventDefault()
39
+        // e.stopPropagation()
37 40
       }
38 41
     }
39 42
   }, [])
40 43
 
44
+  // const handleTouchMove = useCallback((e: React.TouchEvent) => {
45
+  //   if (!inputs.canAccept(e.touches[0].identifier)) return
46
+  //   if (inputs.canAccept(e.touches[0].identifier)) {
47
+  //     state.send('MOVED_POINTER', inputs.touchMove(e))
48
+  //   }
49
+  // }, [])
50
+
41 51
   const handlePointerMove = useCallback((e: React.PointerEvent) => {
42 52
     if (!inputs.canAccept(e.pointerId)) return
43 53
     if (inputs.canAccept(e.pointerId)) {
@@ -58,6 +68,7 @@ export default function Canvas() {
58 68
       onPointerMove={handlePointerMove}
59 69
       onPointerUp={handlePointerUp}
60 70
       onTouchStart={handleTouchStart}
71
+      // onTouchMove={handleTouchMove}
61 72
     >
62 73
       <Defs />
63 74
       {isReady && (

+ 6
- 4
components/canvas/selected.tsx Vedi File

@@ -39,7 +39,7 @@ export function ShapeOutline({ id }: { id: string }) {
39 39
   `
40 40
 
41 41
   return (
42
-    <Indicator
42
+    <SelectIndicator
43 43
       ref={rIndicator}
44 44
       as="use"
45 45
       href={'#' + id}
@@ -50,13 +50,14 @@ export function ShapeOutline({ id }: { id: string }) {
50 50
   )
51 51
 }
52 52
 
53
-const Indicator = styled('path', {
54
-  zStrokeWidth: 1,
53
+const SelectIndicator = styled('path', {
54
+  zStrokeWidth: 3,
55 55
   strokeLineCap: 'round',
56 56
   strokeLinejoin: 'round',
57 57
   stroke: '$selected',
58 58
   fill: 'transparent',
59
-  pointerEvents: 'all',
59
+  pointerEvents: 'none',
60
+  paintOrder: 'stroke fill markers',
60 61
 
61 62
   variants: {
62 63
     isLocked: {
@@ -65,5 +66,6 @@ const Indicator = styled('path', {
65 66
       },
66 67
       false: {},
67 68
     },
69
+    variant: {},
68 70
   },
69 71
 })

+ 61
- 22
components/canvas/shape.tsx Vedi File

@@ -3,8 +3,9 @@ import { useSelector } from 'state'
3 3
 import styled from 'styles'
4 4
 import { getShapeUtils } from 'lib/shape-utils'
5 5
 import { getPage } from 'utils/utils'
6
-import { ShapeStyles } from 'types'
6
+import { DashStyle, ShapeStyles } from 'types'
7 7
 import useShapeEvents from 'hooks/useShapeEvents'
8
+import { shades, strokes } from 'lib/colors'
8 9
 
9 10
 function Shape({ id, isSelecting }: { id: string; isSelecting: boolean }) {
10 11
   const isHovered = useSelector((state) => state.data.hoveredId === id)
@@ -35,36 +36,61 @@ function Shape({ id, isSelecting }: { id: string; isSelecting: boolean }) {
35 36
       isHovered={isHovered}
36 37
       isSelected={isSelected}
37 38
       transform={transform}
38
-      {...events}
39
+      stroke={'red'}
40
+      strokeWidth={10}
39 41
     >
40 42
       {isSelecting && (
41 43
         <HoverIndicator
42 44
           as="use"
43 45
           href={'#' + id}
44 46
           strokeWidth={+shape.style.strokeWidth + 8}
47
+          variant={shape.style.fill === 'none' ? 'hollow' : 'filled'}
48
+          {...events}
45 49
         />
46 50
       )}
47
-      {!shape.isHidden && <StyledShape id={id} style={shape.style} />}
51
+      {!shape.isHidden && (
52
+        <RealShape id={id} style={sanitizeStyle(shape.style)} />
53
+      )}
48 54
     </StyledGroup>
49 55
   )
50 56
 }
51 57
 
52
-const StyledShape = memo(
53
-  ({ id, style }: { id: string; style: ShapeStyles }) => {
54
-    return <use href={'#' + id} {...style} />
55
-  }
56
-)
58
+const RealShape = memo(({ id, style }: { id: string; style: ShapeStyles }) => {
59
+  return (
60
+    <StyledShape
61
+      as="use"
62
+      href={'#' + id}
63
+      {...style}
64
+      strokeDasharray={getDash(style.dash, +style.strokeWidth)}
65
+    />
66
+  )
67
+})
68
+
69
+const StyledShape = styled('path', {
70
+  strokeLinecap: 'round',
71
+  strokeLinejoin: 'round',
72
+})
57 73
 
58 74
 const HoverIndicator = styled('path', {
59
-  fill: 'none',
75
+  fill: 'transparent',
60 76
   stroke: 'transparent',
61
-  pointerEvents: 'all',
62 77
   strokeLinecap: 'round',
63 78
   strokeLinejoin: 'round',
64 79
   transform: 'all .2s',
80
+  variants: {
81
+    variant: {
82
+      hollow: {
83
+        pointerEvents: 'stroke',
84
+      },
85
+      filled: {
86
+        pointerEvents: 'all',
87
+      },
88
+    },
89
+  },
65 90
 })
66 91
 
67 92
 const StyledGroup = styled('g', {
93
+  pointerEvents: 'none',
68 94
   [`& ${HoverIndicator}`]: {
69 95
     opacity: '0',
70 96
   },
@@ -84,10 +110,8 @@ const StyledGroup = styled('g', {
84 110
       isHovered: true,
85 111
       css: {
86 112
         [`& ${HoverIndicator}`]: {
87
-          opacity: '1',
88
-          stroke: '$hint',
89
-          fill: '$hint',
90
-          // zStrokeWidth: [8, 4],
113
+          opacity: '.4',
114
+          stroke: '$selected',
91 115
         },
92 116
       },
93 117
     },
@@ -96,10 +120,8 @@ const StyledGroup = styled('g', {
96 120
       isHovered: false,
97 121
       css: {
98 122
         [`& ${HoverIndicator}`]: {
99
-          opacity: '1',
100
-          stroke: '$hint',
101
-          fill: '$hint',
102
-          // zStrokeWidth: [6, 3],
123
+          opacity: '.2',
124
+          stroke: '$selected',
103 125
         },
104 126
       },
105 127
     },
@@ -108,10 +130,8 @@ const StyledGroup = styled('g', {
108 130
       isHovered: true,
109 131
       css: {
110 132
         [`& ${HoverIndicator}`]: {
111
-          opacity: '1',
112
-          stroke: '$hint',
113
-          fill: '$hint',
114
-          // zStrokeWidth: [8, 4],
133
+          opacity: '.2',
134
+          stroke: '$selected',
115 135
         },
116 136
       },
117 137
     },
@@ -134,6 +154,25 @@ function Label({ text }: { text: string }) {
134 154
   )
135 155
 }
136 156
 
157
+function getDash(dash: DashStyle, s: number) {
158
+  switch (dash) {
159
+    case DashStyle.Solid: {
160
+      return 'none'
161
+    }
162
+    case DashStyle.Dashed: {
163
+      return `${s} ${s * 2}`
164
+    }
165
+    case DashStyle.Dotted: {
166
+      return `0 ${s * 1.5}`
167
+    }
168
+  }
169
+}
170
+
171
+function sanitizeStyle(style: ShapeStyles) {
172
+  const next = { ...style }
173
+  return next
174
+}
175
+
137 176
 export { HoverIndicator }
138 177
 
139 178
 export default memo(Shape)

+ 12
- 8
components/editor.tsx Vedi File

@@ -33,7 +33,7 @@ export default function Editor() {
33 33
   )
34 34
 }
35 35
 
36
-const Layout = styled('div', {
36
+const Layout = styled('main', {
37 37
   position: 'fixed',
38 38
   top: 0,
39 39
   left: 0,
@@ -51,20 +51,24 @@ const Layout = styled('div', {
51 51
   `,
52 52
 })
53 53
 
54
-const LeftPanels = styled('main', {
54
+const LeftPanels = styled('div', {
55 55
   display: 'grid',
56 56
   gridArea: 'leftPanels',
57 57
   gridTemplateRows: '1fr auto',
58 58
   padding: 8,
59 59
   gap: 8,
60
+  zIndex: 250,
61
+  pointerEvents: 'none',
60 62
 })
61 63
 
62
-const RightPanels = styled('main', {
64
+const RightPanels = styled('div', {
63 65
   gridArea: 'rightPanels',
64 66
   padding: 8,
65
-  // display: 'grid',
66
-  // gridTemplateRows: 'auto',
67
-  // height: 'fit-content',
68
-  // justifyContent: 'flex-end',
69
-  // gap: 8,
67
+  display: 'grid',
68
+  gridTemplateRows: 'auto',
69
+  height: 'fit-content',
70
+  justifyContent: 'flex-end',
71
+  gap: 8,
72
+  zIndex: 300,
73
+  pointerEvents: 'none',
70 74
 })

+ 11
- 2
components/style-panel/color-picker.tsx Vedi File

@@ -14,7 +14,7 @@ export default function ColorPicker({ colors, onChange, children }: Props) {
14 14
       {children}
15 15
       <Colors sideOffset={4}>
16 16
         {Object.entries(colors).map(([name, color]) => (
17
-          <ColorButton key={name} title={name} onSelect={() => onChange(color)}>
17
+          <ColorButton key={name} title={name} onSelect={() => onChange(name)}>
18 18
             <ColorIcon color={color} />
19 19
           </ColorButton>
20 20
         ))}
@@ -29,7 +29,7 @@ export function ColorIcon({ color }: { color: string }) {
29 29
   )
30 30
 }
31 31
 
32
-const Colors = styled(DropdownMenu.Content, {
32
+export const Colors = styled(DropdownMenu.Content, {
33 33
   display: 'grid',
34 34
   padding: 4,
35 35
   gridTemplateColumns: 'repeat(6, 1fr)',
@@ -117,4 +117,13 @@ export const CurrentColor = styled(DropdownMenu.Trigger, {
117 117
     strokeWidth: 1,
118 118
     zIndex: 1,
119 119
   },
120
+
121
+  variants: {
122
+    size: {
123
+      icon: {
124
+        padding: '4px ',
125
+        width: 'auto',
126
+      },
127
+    },
128
+  },
120 129
 })

+ 64
- 0
components/style-panel/dash-picker.tsx Vedi File

@@ -0,0 +1,64 @@
1
+import { Group, RadioItem } from './shared'
2
+import { DashStyle } from 'types'
3
+import state from 'state'
4
+import { ChangeEvent } from 'react'
5
+
6
+function handleChange(e: ChangeEvent<HTMLInputElement>) {
7
+  state.send('CHANGED_STYLE', {
8
+    dash: e.currentTarget.value,
9
+  })
10
+}
11
+
12
+interface Props {
13
+  dash: DashStyle
14
+}
15
+
16
+export default function DashPicker({ dash }: Props) {
17
+  return (
18
+    <Group name="Dash" onValueChange={handleChange}>
19
+      <RadioItem value={DashStyle.Solid} isActive={dash === DashStyle.Solid}>
20
+        <DashSolidIcon />
21
+      </RadioItem>
22
+      <RadioItem value={DashStyle.Dashed} isActive={dash === DashStyle.Dashed}>
23
+        <DashDashedIcon />
24
+      </RadioItem>
25
+      <RadioItem value={DashStyle.Dotted} isActive={dash === DashStyle.Dotted}>
26
+        <DashDottedIcon />
27
+      </RadioItem>
28
+    </Group>
29
+  )
30
+}
31
+
32
+function DashSolidIcon() {
33
+  return (
34
+    <svg width="16" height="16">
35
+      <path d="M 3,8 L 13,8" strokeWidth={3} strokeLinecap="round" />
36
+    </svg>
37
+  )
38
+}
39
+
40
+function DashDashedIcon() {
41
+  return (
42
+    <svg width="16" height="16">
43
+      <path
44
+        d="M 2,8 L 14,8"
45
+        strokeWidth={3}
46
+        strokeLinecap="round"
47
+        strokeDasharray="4 4"
48
+      />
49
+    </svg>
50
+  )
51
+}
52
+
53
+function DashDottedIcon() {
54
+  return (
55
+    <svg width="16" height="16">
56
+      <path
57
+        d="M 3,8 L 14,8"
58
+        strokeWidth={3}
59
+        strokeLinecap="round"
60
+        strokeDasharray="1 4"
61
+      />
62
+    </svg>
63
+  )
64
+}

+ 76
- 0
components/style-panel/shared.tsx Vedi File

@@ -0,0 +1,76 @@
1
+import * as RadioGroup from '@radix-ui/react-radio-group'
2
+import * as Panel from '../panel'
3
+import styled from 'styles'
4
+
5
+export const StylePanelRoot = styled(Panel.Root, {
6
+  minWidth: 1,
7
+  width: 184,
8
+  maxWidth: 184,
9
+  overflow: 'hidden',
10
+  position: 'relative',
11
+  border: '1px solid $panel',
12
+  boxShadow: '0px 2px 4px rgba(0,0,0,.12)',
13
+
14
+  variants: {
15
+    isOpen: {
16
+      true: {},
17
+      false: {
18
+        padding: 2,
19
+        height: 38,
20
+        width: 38,
21
+      },
22
+    },
23
+  },
24
+})
25
+
26
+export const Group = styled(RadioGroup.Root, {
27
+  display: 'flex',
28
+})
29
+
30
+export const RadioItem = styled(RadioGroup.Item, {
31
+  height: '32px',
32
+  width: '32px',
33
+  backgroundColor: '$panel',
34
+  borderRadius: '4px',
35
+  padding: '0',
36
+  margin: '0',
37
+  display: 'flex',
38
+  alignItems: 'center',
39
+  justifyContent: 'center',
40
+  outline: 'none',
41
+  border: 'none',
42
+  pointerEvents: 'all',
43
+  cursor: 'pointer',
44
+
45
+  '&:hover:not(:disabled)': {
46
+    backgroundColor: '$hover',
47
+    '& svg': {
48
+      stroke: '$text',
49
+      fill: '$text',
50
+      strokeWidth: '0',
51
+    },
52
+  },
53
+
54
+  '&:disabled': {
55
+    opacity: '0.5',
56
+  },
57
+
58
+  variants: {
59
+    isActive: {
60
+      true: {
61
+        '& svg': {
62
+          fill: '$text',
63
+          stroke: '$text',
64
+          strokeWidth: '0',
65
+        },
66
+      },
67
+      false: {
68
+        '& svg': {
69
+          fill: '$inactive',
70
+          stroke: '$inactive',
71
+          strokeWidth: '0',
72
+        },
73
+      },
74
+    },
75
+  },
76
+})

+ 91
- 54
components/style-panel/style-panel.tsx Vedi File

@@ -3,27 +3,22 @@ import state, { useSelector } from 'state'
3 3
 import * as Panel from 'components/panel'
4 4
 import { useRef } from 'react'
5 5
 import { IconButton } from 'components/shared'
6
-import { Circle, Copy, Lock, Trash, Trash2, Unlock, X } from 'react-feather'
7
-import {
8
-  deepCompare,
9
-  deepCompareArrays,
10
-  getPage,
11
-  getSelectedShapes,
12
-} from 'utils/utils'
6
+import * as Checkbox from '@radix-ui/react-checkbox'
7
+import { Trash2, X } from 'react-feather'
8
+import { deepCompare, deepCompareArrays, getPage } from 'utils/utils'
13 9
 import { shades, fills, strokes } from 'lib/colors'
14
-
15 10
 import ColorPicker, { ColorIcon, CurrentColor } from './color-picker'
16 11
 import AlignDistribute from './align-distribute'
17 12
 import { MoveType, ShapeStyles } from 'types'
18 13
 import WidthPicker from './width-picker'
19 14
 import {
20
-  AlignTopIcon,
21 15
   ArrowDownIcon,
22 16
   ArrowUpIcon,
23 17
   AspectRatioIcon,
24 18
   BoxIcon,
19
+  CheckIcon,
25 20
   CopyIcon,
26
-  DotsHorizontalIcon,
21
+  DotsVerticalIcon,
27 22
   EyeClosedIcon,
28 23
   EyeOpenIcon,
29 24
   LockClosedIcon,
@@ -31,11 +26,17 @@ import {
31 26
   PinBottomIcon,
32 27
   PinTopIcon,
33 28
   RotateCounterClockwiseIcon,
34
-  TrashIcon,
35 29
 } from '@radix-ui/react-icons'
30
+import DashPicker from './dash-picker'
36 31
 
37 32
 const fillColors = { ...shades, ...fills }
38 33
 const strokeColors = { ...shades, ...strokes }
34
+const getFillColor = (color: string) => {
35
+  if (shades[color]) {
36
+    return '#fff'
37
+  }
38
+  return fillColors[color]
39
+}
39 40
 
40 41
 export default function StylePanel() {
41 42
   const rContainer = useRef<HTMLDivElement>(null)
@@ -46,14 +47,41 @@ export default function StylePanel() {
46 47
       {isOpen ? (
47 48
         <SelectedShapeStyles />
48 49
       ) : (
49
-        <IconButton onClick={() => state.send('TOGGLED_STYLE_PANEL_OPEN')}>
50
-          <DotsHorizontalIcon />
51
-        </IconButton>
50
+        <>
51
+          <QuickColorSelect prop="stroke" colors={strokeColors} />
52
+          <IconButton
53
+            title="Style"
54
+            onClick={() => state.send('TOGGLED_STYLE_PANEL_OPEN')}
55
+          >
56
+            <DotsVerticalIcon />
57
+          </IconButton>
58
+        </>
52 59
       )}
53 60
     </StylePanelRoot>
54 61
   )
55 62
 }
56 63
 
64
+function QuickColorSelect({
65
+  prop,
66
+  colors,
67
+}: {
68
+  prop: ShapeStyles['fill'] | ShapeStyles['stroke']
69
+  colors: Record<string, string>
70
+}) {
71
+  const value = useSelector((s) => s.values.selectedStyle[prop])
72
+
73
+  return (
74
+    <ColorPicker
75
+      colors={colors}
76
+      onChange={(color) => state.send('CHANGED_STYLE', { [prop]: color })}
77
+    >
78
+      <CurrentColor size="icon" title={prop}>
79
+        <ColorIcon color={value} />
80
+      </CurrentColor>
81
+    </ColorPicker>
82
+  )
83
+}
84
+
57 85
 // This panel is going to be hard to keep cool, as we're selecting computed
58 86
 // information, based on the user's current selection. We might have to keep
59 87
 // track of this data manually within our state.
@@ -79,33 +107,7 @@ function SelectedShapeStyles({}: {}) {
79 107
     return selectedIds.every((id) => page.shapes[id].isHidden)
80 108
   })
81 109
 
82
-  const commonStyle = useSelector((s) => {
83
-    const { currentStyle } = s.data
84
-
85
-    if (selectedIds.length === 0) {
86
-      return currentStyle
87
-    }
88
-    const page = getPage(s.data)
89
-    const shapeStyles = selectedIds.map((id) => page.shapes[id].style)
90
-
91
-    const commonStyle: Partial<ShapeStyles> = {}
92
-    const overrides = new Set<string>([])
93
-
94
-    for (const shapeStyle of shapeStyles) {
95
-      for (let key in currentStyle) {
96
-        if (overrides.has(key)) continue
97
-        if (commonStyle[key] === undefined) {
98
-          commonStyle[key] = shapeStyle[key]
99
-        } else {
100
-          if (commonStyle[key] === shapeStyle[key]) continue
101
-          commonStyle[key] = currentStyle[key]
102
-          overrides.add(key)
103
-        }
104
-      }
105
-    }
106
-
107
-    return commonStyle
108
-  }, deepCompare)
110
+  const commonStyle = useSelector((s) => s.values.selectedStyle, deepCompare)
109 111
 
110 112
   const hasSelection = selectedIds.length > 0
111 113
 
@@ -118,28 +120,42 @@ function SelectedShapeStyles({}: {}) {
118 120
         </IconButton>
119 121
       </Panel.Header>
120 122
       <Content>
121
-        <ColorPicker
122
-          colors={fillColors}
123
-          onChange={(color) => state.send('CHANGED_STYLE', { fill: color })}
124
-        >
125
-          <CurrentColor>
126
-            <label>Fill</label>
127
-            <ColorIcon color={commonStyle.fill} />
128
-          </CurrentColor>
129
-        </ColorPicker>
130 123
         <ColorPicker
131 124
           colors={strokeColors}
132
-          onChange={(color) => state.send('CHANGED_STYLE', { stroke: color })}
125
+          onChange={(color) =>
126
+            state.send('CHANGED_STYLE', {
127
+              stroke: strokeColors[color],
128
+              fill: getFillColor(color),
129
+            })
130
+          }
133 131
         >
134 132
           <CurrentColor>
135
-            <label>Stroke</label>
133
+            <label>Color</label>
136 134
             <ColorIcon color={commonStyle.stroke} />
137 135
           </CurrentColor>
138 136
         </ColorPicker>
137
+        {/* <Row>
138
+          <label htmlFor="filled">Filled</label>
139
+          <StyledCheckbox
140
+            checked={commonStyle.isFilled}
141
+            onCheckedChange={(e: React.ChangeEvent<HTMLInputElement>) => {
142
+              console.log(e.target.value)
143
+              state.send('CHANGED_STYLE', {
144
+                isFilled: e.target.value === 'on',
145
+              })
146
+            }}
147
+          >
148
+            <Checkbox.Indicator as={CheckIcon} />
149
+          </StyledCheckbox> 
150
+        </Row>*/}
139 151
         <Row>
140 152
           <label htmlFor="width">Width</label>
141 153
           <WidthPicker strokeWidth={Number(commonStyle.strokeWidth)} />
142 154
         </Row>
155
+        <Row>
156
+          <label htmlFor="dash">Dash</label>
157
+          <DashPicker dash={commonStyle.dash} />
158
+        </Row>
143 159
         <ButtonsRow>
144 160
           <IconButton
145 161
             disabled={!hasSelection}
@@ -221,14 +237,16 @@ const StylePanelRoot = styled(Panel.Root, {
221 237
   position: 'relative',
222 238
   border: '1px solid $panel',
223 239
   boxShadow: '0px 2px 4px rgba(0,0,0,.12)',
240
+  display: 'flex',
241
+  alignItems: 'center',
242
+  pointerEvents: 'all',
224 243
 
225 244
   variants: {
226 245
     isOpen: {
227 246
       true: {},
228 247
       false: {
229 248
         padding: 2,
230
-        height: 38,
231
-        width: 38,
249
+        width: 'fit-content',
232 250
       },
233 251
     },
234 252
   },
@@ -275,3 +293,22 @@ const ButtonsRow = styled('div', {
275 293
   justifyContent: 'flex-start',
276 294
   padding: 4,
277 295
 })
296
+
297
+const StyledCheckbox = styled(Checkbox.Root, {
298
+  appearance: 'none',
299
+  backgroundColor: 'transparent',
300
+  border: 'none',
301
+  padding: 0,
302
+  boxShadow: 'inset 0 0 0 1px gainsboro',
303
+  width: 15,
304
+  height: 15,
305
+  borderRadius: 2,
306
+  display: 'flex',
307
+  alignItems: 'center',
308
+  justifyContent: 'center',
309
+
310
+  '&:focus': {
311
+    outline: 'none',
312
+    boxShadow: 'inset 0 0 0 1px dodgerblue, 0 0 0 1px dodgerblue',
313
+  },
314
+})

+ 3
- 53
components/style-panel/width-picker.tsx Vedi File

@@ -1,10 +1,9 @@
1
-import * as RadioGroup from '@radix-ui/react-radio-group'
1
+import { Group, RadioItem } from './shared'
2 2
 import { ChangeEvent } from 'react'
3 3
 import { Circle } from 'react-feather'
4 4
 import state from 'state'
5
-import styled from 'styles'
6 5
 
7
-function setWidth(e: ChangeEvent<HTMLInputElement>) {
6
+function handleChange(e: ChangeEvent<HTMLInputElement>) {
8 7
   state.send('CHANGED_STYLE', {
9 8
     strokeWidth: Number(e.currentTarget.value),
10 9
   })
@@ -16,7 +15,7 @@ export default function WidthPicker({
16 15
   strokeWidth?: number
17 16
 }) {
18 17
   return (
19
-    <Group name="width" onValueChange={setWidth}>
18
+    <Group name="width" onValueChange={handleChange}>
20 19
       <RadioItem value="2" isActive={strokeWidth === 2}>
21 20
         <Circle size={6} />
22 21
       </RadioItem>
@@ -29,52 +28,3 @@ export default function WidthPicker({
29 28
     </Group>
30 29
   )
31 30
 }
32
-
33
-const Group = styled(RadioGroup.Root, {
34
-  display: 'flex',
35
-})
36
-
37
-const RadioItem = styled(RadioGroup.Item, {
38
-  height: '32px',
39
-  width: '32px',
40
-  backgroundColor: '$panel',
41
-  borderRadius: '4px',
42
-  padding: '0',
43
-  margin: '0',
44
-  display: 'flex',
45
-  alignItems: 'center',
46
-  justifyContent: 'center',
47
-  outline: 'none',
48
-  border: 'none',
49
-  pointerEvents: 'all',
50
-  cursor: 'pointer',
51
-
52
-  '&:hover:not(:disabled)': {
53
-    backgroundColor: '$hover',
54
-    '& svg': {
55
-      fill: '$text',
56
-      strokeWidth: '0',
57
-    },
58
-  },
59
-
60
-  '&:disabled': {
61
-    opacity: '0.5',
62
-  },
63
-
64
-  variants: {
65
-    isActive: {
66
-      true: {
67
-        '& svg': {
68
-          fill: '$text',
69
-          strokeWidth: '0',
70
-        },
71
-      },
72
-      false: {
73
-        '& svg': {
74
-          fill: '$inactive',
75
-          strokeWidth: '0',
76
-        },
77
-      },
78
-    },
79
-  },
80
-})

+ 12
- 12
lib/colors.ts Vedi File

@@ -23,16 +23,16 @@ export const strokes = {
23 23
 }
24 24
 
25 25
 export const fills = {
26
-  lime: 'rgba(217, 245, 162, 1.000)',
27
-  green: 'rgba(177, 242, 188, 1.000)',
28
-  teal: 'rgba(149, 242, 215, 1.000)',
29
-  cyan: 'rgba(153, 233, 242, 1.000)',
30
-  blue: 'rgba(166, 216, 255, 1.000)',
31
-  indigo: 'rgba(186, 200, 255, 1.000)',
32
-  violet: 'rgba(208, 191, 255, 1.000)',
33
-  grape: 'rgba(237, 190, 250, 1.000)',
34
-  pink: 'rgba(252, 194, 215, 1.000)',
35
-  red: 'rgba(255, 201, 201, 1.000)',
36
-  orange: 'rgba(255, 216, 168, 1.000)',
37
-  yellow: 'rgba(255, 236, 153, 1.000)',
26
+  lime: 'rgba(243, 252, 227, 1.000)',
27
+  green: 'rgba(235, 251, 238, 1.000)',
28
+  teal: 'rgba(230, 252, 245, 1.000)',
29
+  cyan: 'rgba(227, 250, 251, 1.000)',
30
+  blue: 'rgba(231, 245, 255, 1.000)',
31
+  indigo: 'rgba(237, 242, 255, 1.000)',
32
+  violet: 'rgba(242, 240, 255, 1.000)',
33
+  grape: 'rgba(249, 240, 252, 1.000)',
34
+  pink: 'rgba(254, 241, 246, 1.000)',
35
+  red: 'rgba(255, 245, 245, 1.000)',
36
+  orange: 'rgba(255, 244, 229, 1.000)',
37
+  yellow: 'rgba(255, 249, 219, 1.000)',
38 38
 }

+ 73
- 48
lib/shape-utils/arrow.tsx Vedi File

@@ -3,14 +3,29 @@ import * as vec from 'utils/vec'
3 3
 import * as svg from 'utils/svg'
4 4
 import { ArrowShape, ShapeHandle, ShapeType } from 'types'
5 5
 import { registerShapeUtils } from './index'
6
-import { circleFromThreePoints, clamp, getSweep } from 'utils/utils'
7
-import { boundsContained } from 'utils/bounds'
8
-import { intersectCircleBounds } from 'utils/intersections'
6
+import { circleFromThreePoints, clamp, isAngleBetween } from 'utils/utils'
7
+import { pointInBounds } from 'utils/bounds'
8
+import {
9
+  intersectArcBounds,
10
+  intersectLineSegmentBounds,
11
+} from 'utils/intersections'
9 12
 import { getBoundsFromPoints, translateBounds } from 'utils/utils'
10 13
 import { pointInCircle } from 'utils/hitTests'
11 14
 
12 15
 const ctpCache = new WeakMap<ArrowShape['handles'], number[]>()
13 16
 
17
+function getCtp(shape: ArrowShape) {
18
+  if (!ctpCache.has(shape.handles)) {
19
+    const { start, end, bend } = shape.handles
20
+    ctpCache.set(
21
+      shape.handles,
22
+      circleFromThreePoints(start.point, end.point, bend.point)
23
+    )
24
+  }
25
+
26
+  return ctpCache.get(shape.handles)
27
+}
28
+
14 29
 const arrow = registerShapeUtils<ArrowShape>({
15 30
   boundsCache: new WeakMap([]),
16 31
 
@@ -69,7 +84,8 @@ const arrow = registerShapeUtils<ArrowShape>({
69 84
     }
70 85
   },
71 86
 
72
-  render({ id, bend, points, handles, style }) {
87
+  render(shape) {
88
+    const { id, bend, points, handles, style } = shape
73 89
     const { start, end, bend: _bend } = handles
74 90
 
75 91
     const arrowDist = vec.dist(start.point, end.point)
@@ -91,7 +107,7 @@ const arrow = registerShapeUtils<ArrowShape>({
91 107
       )
92 108
     }
93 109
 
94
-    const circle = showCircle && ctpCache.get(handles)
110
+    const circle = showCircle && getCtp(shape)
95 111
 
96 112
     return (
97 113
       <g id={id}>
@@ -114,12 +130,14 @@ const arrow = registerShapeUtils<ArrowShape>({
114 130
           cy={start.point[1]}
115 131
           r={+style.strokeWidth}
116 132
           fill={style.stroke}
133
+          strokeDasharray="none"
117 134
         />
118 135
         <polyline
119 136
           points={[b, points[1], c].join()}
120 137
           strokeLinecap="round"
121 138
           strokeLinejoin="round"
122 139
           fill="none"
140
+          strokeDasharray="none"
123 141
         />
124 142
       </g>
125 143
     )
@@ -127,6 +145,7 @@ const arrow = registerShapeUtils<ArrowShape>({
127 145
 
128 146
   applyStyles(shape, style) {
129 147
     Object.assign(shape.style, style)
148
+    shape.style.fill = 'none'
130 149
     return this
131 150
   },
132 151
 
@@ -159,24 +178,29 @@ const arrow = registerShapeUtils<ArrowShape>({
159 178
       )
160 179
     }
161 180
 
162
-    if (!ctpCache.has(shape.handles)) {
163
-      ctpCache.set(
164
-        shape.handles,
165
-        circleFromThreePoints(start.point, end.point, bend.point)
166
-      )
167
-    }
168
-
169
-    const [cx, cy, r] = ctpCache.get(shape.handles)
181
+    const [cx, cy, r] = getCtp(shape)
170 182
 
171 183
     return !pointInCircle(point, vec.add(shape.point, [cx, cy]), r - 4)
172 184
   },
173 185
 
174 186
   hitTestBounds(this, shape, brushBounds) {
175
-    const shapeBounds = this.getBounds(shape)
176
-    return (
177
-      boundsContained(shapeBounds, brushBounds) ||
178
-      intersectCircleBounds(shape.point, 4, brushBounds).length > 0
179
-    )
187
+    const { start, end, bend } = shape.handles
188
+
189
+    const sp = vec.add(shape.point, start.point)
190
+    const ep = vec.add(shape.point, end.point)
191
+
192
+    if (pointInBounds(sp, brushBounds) || pointInBounds(ep, brushBounds)) {
193
+      return true
194
+    }
195
+
196
+    if (vec.isEqual(vec.med(start.point, end.point), bend.point)) {
197
+      return intersectLineSegmentBounds(sp, ep, brushBounds).length > 0
198
+    } else {
199
+      const [cx, cy, r] = getCtp(shape)
200
+      const cp = vec.add(shape.point, [cx, cy])
201
+
202
+      return intersectArcBounds(sp, ep, cp, r, brushBounds).length > 0
203
+    }
180 204
   },
181 205
 
182 206
   rotateTo(shape, rotation) {
@@ -219,14 +243,7 @@ const arrow = registerShapeUtils<ArrowShape>({
219 243
     start.point = shape.points[0]
220 244
     end.point = shape.points[1]
221 245
 
222
-    const bendDist = (vec.dist(start.point, end.point) / 2) * shape.bend
223
-    const midPoint = vec.med(start.point, end.point)
224
-    const u = vec.uni(vec.vec(start.point, end.point))
225
-
226
-    bend.point =
227
-      Math.abs(bendDist) > 10
228
-        ? vec.add(midPoint, vec.mul(vec.per(u), bendDist))
229
-        : midPoint
246
+    bend.point = getBendPoint(shape)
230 247
 
231 248
     shape.points = [shape.handles.start.point, shape.handles.end.point]
232 249
 
@@ -244,8 +261,6 @@ const arrow = registerShapeUtils<ArrowShape>({
244 261
   },
245 262
 
246 263
   onHandleMove(shape, handles) {
247
-    const { start, end, bend } = shape.handles
248
-
249 264
     for (let id in handles) {
250 265
       const handle = handles[id]
251 266
 
@@ -255,38 +270,35 @@ const arrow = registerShapeUtils<ArrowShape>({
255 270
         shape.points[handle.index] = handle.point
256 271
       }
257 272
 
273
+      const { start, end, bend } = shape.handles
274
+
258 275
       const dist = vec.dist(start.point, end.point)
259 276
 
260 277
       if (handle.id === 'bend') {
261
-        const distance = vec.distanceToLineSegment(
262
-          start.point,
263
-          end.point,
264
-          handle.point,
265
-          true
266
-        )
267
-        shape.bend = clamp(distance / (dist / 2), -1, 1)
268
-
269
-        const a0 = vec.angle(handle.point, end.point)
270
-        const a1 = vec.angle(start.point, end.point)
271
-        if (a0 - a1 < 0) shape.bend *= -1
278
+        const midPoint = vec.med(start.point, end.point)
279
+        const u = vec.uni(vec.vec(start.point, end.point))
280
+        const ap = vec.add(midPoint, vec.mul(vec.per(u), dist / 2))
281
+        const bp = vec.sub(midPoint, vec.mul(vec.per(u), dist / 2))
282
+
283
+        bend.point = vec.nearestPointOnLineSegment(ap, bp, bend.point, true)
284
+        shape.bend = vec.dist(bend.point, midPoint) / (dist / 2)
285
+
286
+        const sa = vec.angle(end.point, start.point)
287
+        const la = sa - Math.PI / 2
288
+        if (isAngleBetween(sa, la, vec.angle(end.point, bend.point))) {
289
+          shape.bend *= -1
290
+        }
272 291
       }
273 292
     }
274 293
 
275
-    const dist = vec.dist(start.point, end.point)
276
-    const midPoint = vec.med(start.point, end.point)
277
-    const bendDist = (dist / 2) * shape.bend
278
-    const u = vec.uni(vec.vec(start.point, end.point))
279
-
280
-    shape.handles.bend.point =
281
-      Math.abs(bendDist) > 10
282
-        ? vec.add(midPoint, vec.mul(vec.per(u), bendDist))
283
-        : midPoint
294
+    shape.handles.bend.point = getBendPoint(shape)
284 295
 
285 296
     return this
286 297
   },
287 298
 
288 299
   canTransform: true,
289 300
   canChangeAspectRatio: true,
301
+  canStyleFill: false,
290 302
 })
291 303
 
292 304
 export default arrow
@@ -311,3 +323,16 @@ function getArrowArcPath(
311 323
     end.point[1],
312 324
   ].join(' ')
313 325
 }
326
+
327
+function getBendPoint(shape: ArrowShape) {
328
+  const { start, end, bend } = shape.handles
329
+
330
+  const dist = vec.dist(start.point, end.point)
331
+  const midPoint = vec.med(start.point, end.point)
332
+  const bendDist = (dist / 2) * shape.bend
333
+  const u = vec.uni(vec.vec(start.point, end.point))
334
+
335
+  return Math.abs(bendDist) < 10
336
+    ? midPoint
337
+    : vec.add(midPoint, vec.mul(vec.per(u), bendDist))
338
+}

+ 1
- 0
lib/shape-utils/circle.tsx Vedi File

@@ -135,6 +135,7 @@ const circle = registerShapeUtils<CircleShape>({
135 135
 
136 136
   canTransform: true,
137 137
   canChangeAspectRatio: false,
138
+  canStyleFill: true,
138 139
 })
139 140
 
140 141
 export default circle

+ 1
- 0
lib/shape-utils/dot.tsx Vedi File

@@ -104,6 +104,7 @@ const dot = registerShapeUtils<DotShape>({
104 104
 
105 105
   canTransform: false,
106 106
   canChangeAspectRatio: false,
107
+  canStyleFill: true,
107 108
 })
108 109
 
109 110
 export default dot

+ 6
- 0
lib/shape-utils/draw.tsx Vedi File

@@ -11,6 +11,7 @@ import {
11 11
   getSvgPathFromStroke,
12 12
   translateBounds,
13 13
 } from 'utils/utils'
14
+import styled from 'styles'
14 15
 
15 16
 const pathCache = new WeakMap<DrawShape['points'], string>([])
16 17
 
@@ -190,6 +191,11 @@ const draw = registerShapeUtils<DrawShape>({
190 191
 
191 192
   canTransform: true,
192 193
   canChangeAspectRatio: true,
194
+  canStyleFill: false,
193 195
 })
194 196
 
195 197
 export default draw
198
+
199
+const DrawPath = styled('path', {
200
+  strokeWidth: 0,
201
+})

+ 1
- 0
lib/shape-utils/ellipse.tsx Vedi File

@@ -149,6 +149,7 @@ const ellipse = registerShapeUtils<EllipseShape>({
149 149
 
150 150
   canTransform: true,
151 151
   canChangeAspectRatio: true,
152
+  canStyleFill: true,
152 153
 })
153 154
 
154 155
 export default ellipse

+ 3
- 0
lib/shape-utils/index.tsx Vedi File

@@ -39,6 +39,9 @@ export interface ShapeUtility<K extends Readonly<Shape>> {
39 39
   // Whether the shape's aspect ratio can change
40 40
   canChangeAspectRatio: boolean
41 41
 
42
+  // Whether the shape's style can be filled
43
+  canStyleFill: boolean
44
+
42 45
   // Create a new shape.
43 46
   create(props: Partial<K>): K
44 47
 

+ 1
- 0
lib/shape-utils/line.tsx Vedi File

@@ -113,6 +113,7 @@ const line = registerShapeUtils<LineShape>({
113 113
 
114 114
   canTransform: false,
115 115
   canChangeAspectRatio: false,
116
+  canStyleFill: false,
116 117
 })
117 118
 
118 119
 export default line

+ 1
- 0
lib/shape-utils/polyline.tsx Vedi File

@@ -137,6 +137,7 @@ const polyline = registerShapeUtils<PolylineShape>({
137 137
 
138 138
   canTransform: true,
139 139
   canChangeAspectRatio: true,
140
+  canStyleFill: false,
140 141
 })
141 142
 
142 143
 export default polyline

+ 1
- 0
lib/shape-utils/ray.tsx Vedi File

@@ -112,6 +112,7 @@ const ray = registerShapeUtils<RayShape>({
112 112
 
113 113
   canTransform: false,
114 114
   canChangeAspectRatio: false,
115
+  canStyleFill: false,
115 116
 })
116 117
 
117 118
 export default ray

+ 1
- 0
lib/shape-utils/rectangle.tsx Vedi File

@@ -150,6 +150,7 @@ const rectangle = registerShapeUtils<RectangleShape>({
150 150
 
151 151
   canTransform: true,
152 152
   canChangeAspectRatio: true,
153
+  canStyleFill: true,
153 154
 })
154 155
 
155 156
 export default rectangle

+ 1
- 0
package.json Vedi File

@@ -9,6 +9,7 @@
9 9
   },
10 10
   "dependencies": {
11 11
     "@monaco-editor/react": "^4.1.3",
12
+    "@radix-ui/react-checkbox": "^0.0.15",
12 13
     "@radix-ui/react-dropdown-menu": "^0.0.19",
13 14
     "@radix-ui/react-icons": "^1.0.3",
14 15
     "@radix-ui/react-radio-group": "^0.0.16",

+ 47
- 0
state/inputs.tsx Vedi File

@@ -1,3 +1,4 @@
1
+import React from 'react'
1 2
 import { PointerInfo } from 'types'
2 3
 import { isDarwin } from 'utils/utils'
3 4
 
@@ -5,6 +6,52 @@ class Inputs {
5 6
   activePointerId?: number
6 7
   points: Record<string, PointerInfo> = {}
7 8
 
9
+  touchStart(e: TouchEvent | React.TouchEvent, target: string) {
10
+    const { shiftKey, ctrlKey, metaKey, altKey } = e
11
+
12
+    const touch = e.changedTouches[0]
13
+
14
+    const info = {
15
+      target,
16
+      pointerId: touch.identifier,
17
+      origin: [touch.clientX, touch.clientY],
18
+      point: [touch.clientX, touch.clientY],
19
+      shiftKey,
20
+      ctrlKey,
21
+      metaKey: isDarwin() ? metaKey : ctrlKey,
22
+      altKey,
23
+    }
24
+
25
+    this.points[touch.identifier] = info
26
+    this.activePointerId = touch.identifier
27
+
28
+    return info
29
+  }
30
+
31
+  touchMove(e: TouchEvent | React.TouchEvent) {
32
+    const { shiftKey, ctrlKey, metaKey, altKey } = e
33
+
34
+    const touch = e.changedTouches[0]
35
+
36
+    const prev = this.points[touch.identifier]
37
+
38
+    const info = {
39
+      ...prev,
40
+      pointerId: touch.identifier,
41
+      point: [touch.clientX, touch.clientY],
42
+      shiftKey,
43
+      ctrlKey,
44
+      metaKey: isDarwin() ? metaKey : ctrlKey,
45
+      altKey,
46
+    }
47
+
48
+    if (this.points[touch.identifier]) {
49
+      this.points[touch.identifier] = info
50
+    }
51
+
52
+    return info
53
+  }
54
+
8 55
   pointerDown(e: PointerEvent | React.PointerEvent, target: string) {
9 56
     const { shiftKey, ctrlKey, metaKey, altKey } = e
10 57
 

+ 32
- 0
state/state.ts Vedi File

@@ -15,6 +15,7 @@ import {
15 15
   getCurrent,
16 16
   getPage,
17 17
   getSelectedBounds,
18
+  getSelectedShapes,
18 19
   getShape,
19 20
   screenToWorld,
20 21
   setZoomCSS,
@@ -32,6 +33,7 @@ import {
32 33
   DistributeType,
33 34
   AlignType,
34 35
   StretchType,
36
+  DashStyle,
35 37
 } from 'types'
36 38
 
37 39
 const initialData: Data = {
@@ -50,6 +52,7 @@ const initialData: Data = {
50 52
     fill: shades.lightGray,
51 53
     stroke: shades.darkGray,
52 54
     strokeWidth: 2,
55
+    dash: DashStyle.Solid,
53 56
   },
54 57
   camera: {
55 58
     point: [0, 0],
@@ -1296,6 +1299,35 @@ const state = createState({
1296 1299
         ...shapes.map((shape) => getShapeUtils(shape).getRotatedBounds(shape))
1297 1300
       )
1298 1301
     },
1302
+    selectedStyle(data) {
1303
+      const selectedIds = Array.from(data.selectedIds.values())
1304
+      const { currentStyle } = data
1305
+
1306
+      if (selectedIds.length === 0) {
1307
+        return currentStyle
1308
+      }
1309
+      const page = getPage(data)
1310
+      const shapeStyles = selectedIds.map((id) => page.shapes[id].style)
1311
+
1312
+      const commonStyle: Partial<ShapeStyles> = {}
1313
+
1314
+      const overrides = new Set<string>([])
1315
+
1316
+      for (const shapeStyle of shapeStyles) {
1317
+        for (let key in currentStyle) {
1318
+          if (overrides.has(key)) continue
1319
+          if (commonStyle[key] === undefined) {
1320
+            commonStyle[key] = shapeStyle[key]
1321
+          } else {
1322
+            if (commonStyle[key] === shapeStyle[key]) continue
1323
+            commonStyle[key] = currentStyle[key]
1324
+            overrides.add(key)
1325
+          }
1326
+        }
1327
+      }
1328
+
1329
+      return commonStyle
1330
+    },
1299 1331
   },
1300 1332
 })
1301 1333
 

+ 1
- 1
styles/stitches.config.ts Vedi File

@@ -8,7 +8,7 @@ const { styled, global, css, theme, getCssString } = createCss({
8 8
     colors: {
9 9
       brushFill: 'rgba(0,0,0,.1)',
10 10
       brushStroke: 'rgba(0,0,0,.5)',
11
-      hint: 'rgba(66, 133, 244, 0.200)',
11
+      hint: 'rgba(216, 226, 249, 1.000)',
12 12
       selected: 'rgba(66, 133, 244, 1.000)',
13 13
       bounds: 'rgba(65, 132, 244, 1.000)',
14 14
       boundsBg: 'rgba(65, 132, 244, 0.100)',

+ 12
- 2
types.ts Vedi File

@@ -67,7 +67,11 @@ export enum ShapeType {
67 67
 // Cubic = "cubic",
68 68
 // Conic = "conic",
69 69
 
70
-export type ShapeStyles = Partial<React.SVGProps<SVGUseElement>>
70
+export type ShapeStyles = Partial<
71
+  React.SVGProps<SVGUseElement> & {
72
+    dash: DashStyle
73
+  }
74
+>
71 75
 
72 76
 export interface BaseShape {
73 77
   id: string
@@ -173,7 +177,13 @@ export interface CodeFile {
173 177
 }
174 178
 
175 179
 export enum Decoration {
176
-  Arrow,
180
+  Arrow = 'Arrow',
181
+}
182
+
183
+export enum DashStyle {
184
+  Solid = 'Solid',
185
+  Dashed = 'Dashed',
186
+  Dotted = 'Dotted',
177 187
 }
178 188
 
179 189
 export interface ShapeBinding {

+ 137
- 25
utils/intersections.ts Vedi File

@@ -1,5 +1,6 @@
1
-import { Bounds } from "types"
2
-import * as vec from "utils/vec"
1
+import { Bounds } from 'types'
2
+import * as vec from 'utils/vec'
3
+import { isAngleBetween } from './utils'
3 4
 
4 5
 interface Intersection {
5 6
   didIntersect: boolean
@@ -26,22 +27,22 @@ export function intersectLineSegments(
26 27
   const u_b = BV[1] * AV[0] - BV[0] * AV[1]
27 28
 
28 29
   if (ua_t === 0 || ub_t === 0) {
29
-    return getIntersection("coincident")
30
+    return getIntersection('coincident')
30 31
   }
31 32
 
32 33
   if (u_b === 0) {
33
-    return getIntersection("parallel")
34
+    return getIntersection('parallel')
34 35
   }
35 36
 
36 37
   if (u_b != 0) {
37 38
     const ua = ua_t / u_b
38 39
     const ub = ub_t / u_b
39 40
     if (0 <= ua && ua <= 1 && 0 <= ub && ub <= 1) {
40
-      return getIntersection("intersection", vec.add(a1, vec.mul(AV, ua)))
41
+      return getIntersection('intersection', vec.add(a1, vec.mul(AV, ua)))
41 42
     }
42 43
   }
43 44
 
44
-  return getIntersection("no intersection")
45
+  return getIntersection('no intersection')
45 46
 }
46 47
 
47 48
 export function intersectCircleLineSegment(
@@ -65,11 +66,11 @@ export function intersectCircleLineSegment(
65 66
   const deter = b * b - 4 * a * cc
66 67
 
67 68
   if (deter < 0) {
68
-    return getIntersection("outside")
69
+    return getIntersection('outside')
69 70
   }
70 71
 
71 72
   if (deter === 0) {
72
-    return getIntersection("tangent")
73
+    return getIntersection('tangent')
73 74
   }
74 75
 
75 76
   var e = Math.sqrt(deter)
@@ -77,9 +78,9 @@ export function intersectCircleLineSegment(
77 78
   var u2 = (-b - e) / (2 * a)
78 79
   if ((u1 < 0 || u1 > 1) && (u2 < 0 || u2 > 1)) {
79 80
     if ((u1 < 0 && u2 < 0) || (u1 > 1 && u2 > 1)) {
80
-      return getIntersection("outside")
81
+      return getIntersection('outside')
81 82
     } else {
82
-      return getIntersection("inside")
83
+      return getIntersection('inside')
83 84
     }
84 85
   }
85 86
 
@@ -87,7 +88,7 @@ export function intersectCircleLineSegment(
87 88
   if (0 <= u1 && u1 <= 1) results.push(vec.lrp(a1, a2, u1))
88 89
   if (0 <= u2 && u2 <= 1) results.push(vec.lrp(a1, a2, u2))
89 90
 
90
-  return getIntersection("intersection", ...results)
91
+  return getIntersection('intersection', ...results)
91 92
 }
92 93
 
93 94
 export function intersectEllipseLineSegment(
@@ -100,7 +101,7 @@ export function intersectEllipseLineSegment(
100 101
 ) {
101 102
   // If the ellipse or line segment are empty, return no tValues.
102 103
   if (rx === 0 || ry === 0 || vec.isEqual(a1, a2)) {
103
-    return getIntersection("No intersection")
104
+    return getIntersection('No intersection')
104 105
   }
105 106
 
106 107
   // Get the semimajor and semiminor axes.
@@ -141,7 +142,32 @@ export function intersectEllipseLineSegment(
141 142
     .map((t) => vec.add(center, vec.add(a1, vec.mul(vec.sub(a2, a1), t))))
142 143
     .map((p) => vec.rotWith(p, center, rotation))
143 144
 
144
-  return getIntersection("intersection", ...points)
145
+  return getIntersection('intersection', ...points)
146
+}
147
+
148
+export function intersectArcLineSegment(
149
+  start: number[],
150
+  end: number[],
151
+  center: number[],
152
+  radius: number,
153
+  A: number[],
154
+  B: number[]
155
+) {
156
+  const sa = vec.angle(center, start)
157
+  const ea = vec.angle(center, end)
158
+  const ellipseTest = intersectEllipseLineSegment(center, radius, radius, A, B)
159
+
160
+  if (!ellipseTest.didIntersect) return getIntersection('No intersection')
161
+
162
+  const points = ellipseTest.points.filter((point) =>
163
+    isAngleBetween(sa, ea, vec.angle(center, point))
164
+  )
165
+
166
+  if (points.length === 0) {
167
+    return getIntersection('No intersection')
168
+  }
169
+
170
+  return getIntersection('intersection', ...points)
145 171
 }
146 172
 
147 173
 export function intersectCircleRectangle(
@@ -163,19 +189,19 @@ export function intersectCircleRectangle(
163 189
   const leftIntersection = intersectCircleLineSegment(c, r, tl, bl)
164 190
 
165 191
   if (topIntersection.didIntersect) {
166
-    intersections.push({ ...topIntersection, message: "top" })
192
+    intersections.push({ ...topIntersection, message: 'top' })
167 193
   }
168 194
 
169 195
   if (rightIntersection.didIntersect) {
170
-    intersections.push({ ...rightIntersection, message: "right" })
196
+    intersections.push({ ...rightIntersection, message: 'right' })
171 197
   }
172 198
 
173 199
   if (bottomIntersection.didIntersect) {
174
-    intersections.push({ ...bottomIntersection, message: "bottom" })
200
+    intersections.push({ ...bottomIntersection, message: 'bottom' })
175 201
   }
176 202
 
177 203
   if (leftIntersection.didIntersect) {
178
-    intersections.push({ ...leftIntersection, message: "left" })
204
+    intersections.push({ ...leftIntersection, message: 'left' })
179 205
   }
180 206
 
181 207
   return intersections
@@ -230,19 +256,19 @@ export function intersectEllipseRectangle(
230 256
   )
231 257
 
232 258
   if (topIntersection.didIntersect) {
233
-    intersections.push({ ...topIntersection, message: "top" })
259
+    intersections.push({ ...topIntersection, message: 'top' })
234 260
   }
235 261
 
236 262
   if (rightIntersection.didIntersect) {
237
-    intersections.push({ ...rightIntersection, message: "right" })
263
+    intersections.push({ ...rightIntersection, message: 'right' })
238 264
   }
239 265
 
240 266
   if (bottomIntersection.didIntersect) {
241
-    intersections.push({ ...bottomIntersection, message: "bottom" })
267
+    intersections.push({ ...bottomIntersection, message: 'bottom' })
242 268
   }
243 269
 
244 270
   if (leftIntersection.didIntersect) {
245
-    intersections.push({ ...leftIntersection, message: "left" })
271
+    intersections.push({ ...leftIntersection, message: 'left' })
246 272
   }
247 273
 
248 274
   return intersections
@@ -267,19 +293,86 @@ export function intersectRectangleLineSegment(
267 293
   const leftIntersection = intersectLineSegments(a1, a2, tl, bl)
268 294
 
269 295
   if (topIntersection.didIntersect) {
270
-    intersections.push({ ...topIntersection, message: "top" })
296
+    intersections.push({ ...topIntersection, message: 'top' })
297
+  }
298
+
299
+  if (rightIntersection.didIntersect) {
300
+    intersections.push({ ...rightIntersection, message: 'right' })
301
+  }
302
+
303
+  if (bottomIntersection.didIntersect) {
304
+    intersections.push({ ...bottomIntersection, message: 'bottom' })
305
+  }
306
+
307
+  if (leftIntersection.didIntersect) {
308
+    intersections.push({ ...leftIntersection, message: 'left' })
309
+  }
310
+
311
+  return intersections
312
+}
313
+
314
+export function intersectArcRectangle(
315
+  start: number[],
316
+  end: number[],
317
+  center: number[],
318
+  radius: number,
319
+  point: number[],
320
+  size: number[]
321
+) {
322
+  const tl = point
323
+  const tr = vec.add(point, [size[0], 0])
324
+  const br = vec.add(point, size)
325
+  const bl = vec.add(point, [0, size[1]])
326
+
327
+  const intersections: Intersection[] = []
328
+
329
+  const topIntersection = intersectArcLineSegment(
330
+    start,
331
+    end,
332
+    center,
333
+    radius,
334
+    tl,
335
+    tr
336
+  )
337
+  const rightIntersection = intersectArcLineSegment(
338
+    start,
339
+    end,
340
+    center,
341
+    radius,
342
+    tr,
343
+    br
344
+  )
345
+  const bottomIntersection = intersectArcLineSegment(
346
+    start,
347
+    end,
348
+    center,
349
+    radius,
350
+    bl,
351
+    br
352
+  )
353
+  const leftIntersection = intersectArcLineSegment(
354
+    start,
355
+    end,
356
+    center,
357
+    radius,
358
+    tl,
359
+    bl
360
+  )
361
+
362
+  if (topIntersection.didIntersect) {
363
+    intersections.push({ ...topIntersection, message: 'top' })
271 364
   }
272 365
 
273 366
   if (rightIntersection.didIntersect) {
274
-    intersections.push({ ...rightIntersection, message: "right" })
367
+    intersections.push({ ...rightIntersection, message: 'right' })
275 368
   }
276 369
 
277 370
   if (bottomIntersection.didIntersect) {
278
-    intersections.push({ ...bottomIntersection, message: "bottom" })
371
+    intersections.push({ ...bottomIntersection, message: 'bottom' })
279 372
   }
280 373
 
281 374
   if (leftIntersection.didIntersect) {
282
-    intersections.push({ ...leftIntersection, message: "left" })
375
+    intersections.push({ ...leftIntersection, message: 'left' })
283 376
   }
284 377
 
285 378
   return intersections
@@ -360,3 +453,22 @@ export function intersectPolygonBounds(points: number[][], bounds: Bounds) {
360 453
 
361 454
   return intersections
362 455
 }
456
+
457
+export function intersectArcBounds(
458
+  start: number[],
459
+  end: number[],
460
+  center: number[],
461
+  radius: number,
462
+  bounds: Bounds
463
+) {
464
+  const { minX, minY, width, height } = bounds
465
+
466
+  return intersectArcRectangle(
467
+    start,
468
+    end,
469
+    center,
470
+    radius,
471
+    [minX, minY],
472
+    [width, height]
473
+  )
474
+}

+ 15
- 0
utils/utils.ts Vedi File

@@ -1566,3 +1566,18 @@ export function getSvgPathFromStroke(stroke: number[][]) {
1566 1566
   d.push('Z')
1567 1567
   return d.join(' ')
1568 1568
 }
1569
+
1570
+const PI2 = Math.PI * 2
1571
+
1572
+/**
1573
+ * Is angle c between angles a and b?
1574
+ * @param a
1575
+ * @param b
1576
+ * @param c
1577
+ */
1578
+export function isAngleBetween(a: number, b: number, c: number) {
1579
+  if (c === a || c === b) return true
1580
+  const AB = (b - a + PI2) % PI2
1581
+  const AC = (c - a + PI2) % PI2
1582
+  return AB <= Math.PI !== AC > AB
1583
+}

+ 15
- 0
yarn.lock Vedi File

@@ -1283,6 +1283,21 @@
1283 1283
     "@radix-ui/react-polymorphic" "0.0.11"
1284 1284
     "@radix-ui/react-primitive" "0.0.13"
1285 1285
 
1286
+"@radix-ui/react-checkbox@^0.0.15":
1287
+  version "0.0.15"
1288
+  resolved "https://registry.yarnpkg.com/@radix-ui/react-checkbox/-/react-checkbox-0.0.15.tgz#d53b56854fbba65e74ed4486116107638951b9d1"
1289
+  integrity sha512-R8ErERPlu2kvmqNjxRyyLcS1y3D7J2bQUUEPsvP0BL2AfisUjbT7c9t19k2K/Un3Iieqe93gTPG4LRdbDQQjBw==
1290
+  dependencies:
1291
+    "@babel/runtime" "^7.13.10"
1292
+    "@radix-ui/primitive" "0.0.5"
1293
+    "@radix-ui/react-compose-refs" "0.0.5"
1294
+    "@radix-ui/react-context" "0.0.5"
1295
+    "@radix-ui/react-label" "0.0.13"
1296
+    "@radix-ui/react-polymorphic" "0.0.11"
1297
+    "@radix-ui/react-presence" "0.0.14"
1298
+    "@radix-ui/react-primitive" "0.0.13"
1299
+    "@radix-ui/react-use-controllable-state" "0.0.6"
1300
+
1286 1301
 "@radix-ui/react-collection@0.0.12":
1287 1302
   version "0.0.12"
1288 1303
   resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-0.0.12.tgz#5cd09312cdec34fdbbe1d31affaba69eb768e342"

Loading…
Annulla
Salva