소스 검색

Adds dashes

main
Steve Ruiz 4 년 전
부모
커밋
815bf1109c

+ 3
- 3
components/canvas/bounds/handles.tsx 파일 보기

18
       selectedIds.length === 1 && getPage(data).shapes[selectedIds[0]]
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
   return (
25
   return (
26
     <g>
26
     <g>
57
       pointerEvents="all"
57
       pointerEvents="all"
58
       transform={`translate(${point})`}
58
       transform={`translate(${point})`}
59
     >
59
     >
60
-      <HandleCircleOuter r={8} />
60
+      <HandleCircleOuter r={12} />
61
       <DotCircle r={4} />
61
       <DotCircle r={4} />
62
     </g>
62
     </g>
63
   )
63
   )

+ 11
- 0
components/canvas/canvas.tsx 파일 보기

34
     } else {
34
     } else {
35
       if (isMobile()) {
35
       if (isMobile()) {
36
         state.send('TOUCHED_CANVAS')
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
   const handlePointerMove = useCallback((e: React.PointerEvent) => {
51
   const handlePointerMove = useCallback((e: React.PointerEvent) => {
42
     if (!inputs.canAccept(e.pointerId)) return
52
     if (!inputs.canAccept(e.pointerId)) return
43
     if (inputs.canAccept(e.pointerId)) {
53
     if (inputs.canAccept(e.pointerId)) {
58
       onPointerMove={handlePointerMove}
68
       onPointerMove={handlePointerMove}
59
       onPointerUp={handlePointerUp}
69
       onPointerUp={handlePointerUp}
60
       onTouchStart={handleTouchStart}
70
       onTouchStart={handleTouchStart}
71
+      // onTouchMove={handleTouchMove}
61
     >
72
     >
62
       <Defs />
73
       <Defs />
63
       {isReady && (
74
       {isReady && (

+ 6
- 4
components/canvas/selected.tsx 파일 보기

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

+ 61
- 22
components/canvas/shape.tsx 파일 보기

3
 import styled from 'styles'
3
 import styled from 'styles'
4
 import { getShapeUtils } from 'lib/shape-utils'
4
 import { getShapeUtils } from 'lib/shape-utils'
5
 import { getPage } from 'utils/utils'
5
 import { getPage } from 'utils/utils'
6
-import { ShapeStyles } from 'types'
6
+import { DashStyle, ShapeStyles } from 'types'
7
 import useShapeEvents from 'hooks/useShapeEvents'
7
 import useShapeEvents from 'hooks/useShapeEvents'
8
+import { shades, strokes } from 'lib/colors'
8
 
9
 
9
 function Shape({ id, isSelecting }: { id: string; isSelecting: boolean }) {
10
 function Shape({ id, isSelecting }: { id: string; isSelecting: boolean }) {
10
   const isHovered = useSelector((state) => state.data.hoveredId === id)
11
   const isHovered = useSelector((state) => state.data.hoveredId === id)
35
       isHovered={isHovered}
36
       isHovered={isHovered}
36
       isSelected={isSelected}
37
       isSelected={isSelected}
37
       transform={transform}
38
       transform={transform}
38
-      {...events}
39
+      stroke={'red'}
40
+      strokeWidth={10}
39
     >
41
     >
40
       {isSelecting && (
42
       {isSelecting && (
41
         <HoverIndicator
43
         <HoverIndicator
42
           as="use"
44
           as="use"
43
           href={'#' + id}
45
           href={'#' + id}
44
           strokeWidth={+shape.style.strokeWidth + 8}
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
     </StyledGroup>
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
 const HoverIndicator = styled('path', {
74
 const HoverIndicator = styled('path', {
59
-  fill: 'none',
75
+  fill: 'transparent',
60
   stroke: 'transparent',
76
   stroke: 'transparent',
61
-  pointerEvents: 'all',
62
   strokeLinecap: 'round',
77
   strokeLinecap: 'round',
63
   strokeLinejoin: 'round',
78
   strokeLinejoin: 'round',
64
   transform: 'all .2s',
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
 const StyledGroup = styled('g', {
92
 const StyledGroup = styled('g', {
93
+  pointerEvents: 'none',
68
   [`& ${HoverIndicator}`]: {
94
   [`& ${HoverIndicator}`]: {
69
     opacity: '0',
95
     opacity: '0',
70
   },
96
   },
84
       isHovered: true,
110
       isHovered: true,
85
       css: {
111
       css: {
86
         [`& ${HoverIndicator}`]: {
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
       isHovered: false,
120
       isHovered: false,
97
       css: {
121
       css: {
98
         [`& ${HoverIndicator}`]: {
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
       isHovered: true,
130
       isHovered: true,
109
       css: {
131
       css: {
110
         [`& ${HoverIndicator}`]: {
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
   )
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
 export { HoverIndicator }
176
 export { HoverIndicator }
138
 
177
 
139
 export default memo(Shape)
178
 export default memo(Shape)

+ 12
- 8
components/editor.tsx 파일 보기

33
   )
33
   )
34
 }
34
 }
35
 
35
 
36
-const Layout = styled('div', {
36
+const Layout = styled('main', {
37
   position: 'fixed',
37
   position: 'fixed',
38
   top: 0,
38
   top: 0,
39
   left: 0,
39
   left: 0,
51
   `,
51
   `,
52
 })
52
 })
53
 
53
 
54
-const LeftPanels = styled('main', {
54
+const LeftPanels = styled('div', {
55
   display: 'grid',
55
   display: 'grid',
56
   gridArea: 'leftPanels',
56
   gridArea: 'leftPanels',
57
   gridTemplateRows: '1fr auto',
57
   gridTemplateRows: '1fr auto',
58
   padding: 8,
58
   padding: 8,
59
   gap: 8,
59
   gap: 8,
60
+  zIndex: 250,
61
+  pointerEvents: 'none',
60
 })
62
 })
61
 
63
 
62
-const RightPanels = styled('main', {
64
+const RightPanels = styled('div', {
63
   gridArea: 'rightPanels',
65
   gridArea: 'rightPanels',
64
   padding: 8,
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 파일 보기

14
       {children}
14
       {children}
15
       <Colors sideOffset={4}>
15
       <Colors sideOffset={4}>
16
         {Object.entries(colors).map(([name, color]) => (
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
             <ColorIcon color={color} />
18
             <ColorIcon color={color} />
19
           </ColorButton>
19
           </ColorButton>
20
         ))}
20
         ))}
29
   )
29
   )
30
 }
30
 }
31
 
31
 
32
-const Colors = styled(DropdownMenu.Content, {
32
+export const Colors = styled(DropdownMenu.Content, {
33
   display: 'grid',
33
   display: 'grid',
34
   padding: 4,
34
   padding: 4,
35
   gridTemplateColumns: 'repeat(6, 1fr)',
35
   gridTemplateColumns: 'repeat(6, 1fr)',
117
     strokeWidth: 1,
117
     strokeWidth: 1,
118
     zIndex: 1,
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 파일 보기

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 파일 보기

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 파일 보기

3
 import * as Panel from 'components/panel'
3
 import * as Panel from 'components/panel'
4
 import { useRef } from 'react'
4
 import { useRef } from 'react'
5
 import { IconButton } from 'components/shared'
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
 import { shades, fills, strokes } from 'lib/colors'
9
 import { shades, fills, strokes } from 'lib/colors'
14
-
15
 import ColorPicker, { ColorIcon, CurrentColor } from './color-picker'
10
 import ColorPicker, { ColorIcon, CurrentColor } from './color-picker'
16
 import AlignDistribute from './align-distribute'
11
 import AlignDistribute from './align-distribute'
17
 import { MoveType, ShapeStyles } from 'types'
12
 import { MoveType, ShapeStyles } from 'types'
18
 import WidthPicker from './width-picker'
13
 import WidthPicker from './width-picker'
19
 import {
14
 import {
20
-  AlignTopIcon,
21
   ArrowDownIcon,
15
   ArrowDownIcon,
22
   ArrowUpIcon,
16
   ArrowUpIcon,
23
   AspectRatioIcon,
17
   AspectRatioIcon,
24
   BoxIcon,
18
   BoxIcon,
19
+  CheckIcon,
25
   CopyIcon,
20
   CopyIcon,
26
-  DotsHorizontalIcon,
21
+  DotsVerticalIcon,
27
   EyeClosedIcon,
22
   EyeClosedIcon,
28
   EyeOpenIcon,
23
   EyeOpenIcon,
29
   LockClosedIcon,
24
   LockClosedIcon,
31
   PinBottomIcon,
26
   PinBottomIcon,
32
   PinTopIcon,
27
   PinTopIcon,
33
   RotateCounterClockwiseIcon,
28
   RotateCounterClockwiseIcon,
34
-  TrashIcon,
35
 } from '@radix-ui/react-icons'
29
 } from '@radix-ui/react-icons'
30
+import DashPicker from './dash-picker'
36
 
31
 
37
 const fillColors = { ...shades, ...fills }
32
 const fillColors = { ...shades, ...fills }
38
 const strokeColors = { ...shades, ...strokes }
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
 export default function StylePanel() {
41
 export default function StylePanel() {
41
   const rContainer = useRef<HTMLDivElement>(null)
42
   const rContainer = useRef<HTMLDivElement>(null)
46
       {isOpen ? (
47
       {isOpen ? (
47
         <SelectedShapeStyles />
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
     </StylePanelRoot>
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
 // This panel is going to be hard to keep cool, as we're selecting computed
85
 // This panel is going to be hard to keep cool, as we're selecting computed
58
 // information, based on the user's current selection. We might have to keep
86
 // information, based on the user's current selection. We might have to keep
59
 // track of this data manually within our state.
87
 // track of this data manually within our state.
79
     return selectedIds.every((id) => page.shapes[id].isHidden)
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
   const hasSelection = selectedIds.length > 0
112
   const hasSelection = selectedIds.length > 0
111
 
113
 
118
         </IconButton>
120
         </IconButton>
119
       </Panel.Header>
121
       </Panel.Header>
120
       <Content>
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
         <ColorPicker
123
         <ColorPicker
131
           colors={strokeColors}
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
           <CurrentColor>
132
           <CurrentColor>
135
-            <label>Stroke</label>
133
+            <label>Color</label>
136
             <ColorIcon color={commonStyle.stroke} />
134
             <ColorIcon color={commonStyle.stroke} />
137
           </CurrentColor>
135
           </CurrentColor>
138
         </ColorPicker>
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
         <Row>
151
         <Row>
140
           <label htmlFor="width">Width</label>
152
           <label htmlFor="width">Width</label>
141
           <WidthPicker strokeWidth={Number(commonStyle.strokeWidth)} />
153
           <WidthPicker strokeWidth={Number(commonStyle.strokeWidth)} />
142
         </Row>
154
         </Row>
155
+        <Row>
156
+          <label htmlFor="dash">Dash</label>
157
+          <DashPicker dash={commonStyle.dash} />
158
+        </Row>
143
         <ButtonsRow>
159
         <ButtonsRow>
144
           <IconButton
160
           <IconButton
145
             disabled={!hasSelection}
161
             disabled={!hasSelection}
221
   position: 'relative',
237
   position: 'relative',
222
   border: '1px solid $panel',
238
   border: '1px solid $panel',
223
   boxShadow: '0px 2px 4px rgba(0,0,0,.12)',
239
   boxShadow: '0px 2px 4px rgba(0,0,0,.12)',
240
+  display: 'flex',
241
+  alignItems: 'center',
242
+  pointerEvents: 'all',
224
 
243
 
225
   variants: {
244
   variants: {
226
     isOpen: {
245
     isOpen: {
227
       true: {},
246
       true: {},
228
       false: {
247
       false: {
229
         padding: 2,
248
         padding: 2,
230
-        height: 38,
231
-        width: 38,
249
+        width: 'fit-content',
232
       },
250
       },
233
     },
251
     },
234
   },
252
   },
275
   justifyContent: 'flex-start',
293
   justifyContent: 'flex-start',
276
   padding: 4,
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 파일 보기

1
-import * as RadioGroup from '@radix-ui/react-radio-group'
1
+import { Group, RadioItem } from './shared'
2
 import { ChangeEvent } from 'react'
2
 import { ChangeEvent } from 'react'
3
 import { Circle } from 'react-feather'
3
 import { Circle } from 'react-feather'
4
 import state from 'state'
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
   state.send('CHANGED_STYLE', {
7
   state.send('CHANGED_STYLE', {
9
     strokeWidth: Number(e.currentTarget.value),
8
     strokeWidth: Number(e.currentTarget.value),
10
   })
9
   })
16
   strokeWidth?: number
15
   strokeWidth?: number
17
 }) {
16
 }) {
18
   return (
17
   return (
19
-    <Group name="width" onValueChange={setWidth}>
18
+    <Group name="width" onValueChange={handleChange}>
20
       <RadioItem value="2" isActive={strokeWidth === 2}>
19
       <RadioItem value="2" isActive={strokeWidth === 2}>
21
         <Circle size={6} />
20
         <Circle size={6} />
22
       </RadioItem>
21
       </RadioItem>
29
     </Group>
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 파일 보기

23
 }
23
 }
24
 
24
 
25
 export const fills = {
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 파일 보기

3
 import * as svg from 'utils/svg'
3
 import * as svg from 'utils/svg'
4
 import { ArrowShape, ShapeHandle, ShapeType } from 'types'
4
 import { ArrowShape, ShapeHandle, ShapeType } from 'types'
5
 import { registerShapeUtils } from './index'
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
 import { getBoundsFromPoints, translateBounds } from 'utils/utils'
12
 import { getBoundsFromPoints, translateBounds } from 'utils/utils'
10
 import { pointInCircle } from 'utils/hitTests'
13
 import { pointInCircle } from 'utils/hitTests'
11
 
14
 
12
 const ctpCache = new WeakMap<ArrowShape['handles'], number[]>()
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
 const arrow = registerShapeUtils<ArrowShape>({
29
 const arrow = registerShapeUtils<ArrowShape>({
15
   boundsCache: new WeakMap([]),
30
   boundsCache: new WeakMap([]),
16
 
31
 
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
     const { start, end, bend: _bend } = handles
89
     const { start, end, bend: _bend } = handles
74
 
90
 
75
     const arrowDist = vec.dist(start.point, end.point)
91
     const arrowDist = vec.dist(start.point, end.point)
91
       )
107
       )
92
     }
108
     }
93
 
109
 
94
-    const circle = showCircle && ctpCache.get(handles)
110
+    const circle = showCircle && getCtp(shape)
95
 
111
 
96
     return (
112
     return (
97
       <g id={id}>
113
       <g id={id}>
114
           cy={start.point[1]}
130
           cy={start.point[1]}
115
           r={+style.strokeWidth}
131
           r={+style.strokeWidth}
116
           fill={style.stroke}
132
           fill={style.stroke}
133
+          strokeDasharray="none"
117
         />
134
         />
118
         <polyline
135
         <polyline
119
           points={[b, points[1], c].join()}
136
           points={[b, points[1], c].join()}
120
           strokeLinecap="round"
137
           strokeLinecap="round"
121
           strokeLinejoin="round"
138
           strokeLinejoin="round"
122
           fill="none"
139
           fill="none"
140
+          strokeDasharray="none"
123
         />
141
         />
124
       </g>
142
       </g>
125
     )
143
     )
127
 
145
 
128
   applyStyles(shape, style) {
146
   applyStyles(shape, style) {
129
     Object.assign(shape.style, style)
147
     Object.assign(shape.style, style)
148
+    shape.style.fill = 'none'
130
     return this
149
     return this
131
   },
150
   },
132
 
151
 
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
     return !pointInCircle(point, vec.add(shape.point, [cx, cy]), r - 4)
183
     return !pointInCircle(point, vec.add(shape.point, [cx, cy]), r - 4)
172
   },
184
   },
173
 
185
 
174
   hitTestBounds(this, shape, brushBounds) {
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
   rotateTo(shape, rotation) {
206
   rotateTo(shape, rotation) {
219
     start.point = shape.points[0]
243
     start.point = shape.points[0]
220
     end.point = shape.points[1]
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
     shape.points = [shape.handles.start.point, shape.handles.end.point]
248
     shape.points = [shape.handles.start.point, shape.handles.end.point]
232
 
249
 
244
   },
261
   },
245
 
262
 
246
   onHandleMove(shape, handles) {
263
   onHandleMove(shape, handles) {
247
-    const { start, end, bend } = shape.handles
248
-
249
     for (let id in handles) {
264
     for (let id in handles) {
250
       const handle = handles[id]
265
       const handle = handles[id]
251
 
266
 
255
         shape.points[handle.index] = handle.point
270
         shape.points[handle.index] = handle.point
256
       }
271
       }
257
 
272
 
273
+      const { start, end, bend } = shape.handles
274
+
258
       const dist = vec.dist(start.point, end.point)
275
       const dist = vec.dist(start.point, end.point)
259
 
276
 
260
       if (handle.id === 'bend') {
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
     return this
296
     return this
286
   },
297
   },
287
 
298
 
288
   canTransform: true,
299
   canTransform: true,
289
   canChangeAspectRatio: true,
300
   canChangeAspectRatio: true,
301
+  canStyleFill: false,
290
 })
302
 })
291
 
303
 
292
 export default arrow
304
 export default arrow
311
     end.point[1],
323
     end.point[1],
312
   ].join(' ')
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 파일 보기

135
 
135
 
136
   canTransform: true,
136
   canTransform: true,
137
   canChangeAspectRatio: false,
137
   canChangeAspectRatio: false,
138
+  canStyleFill: true,
138
 })
139
 })
139
 
140
 
140
 export default circle
141
 export default circle

+ 1
- 0
lib/shape-utils/dot.tsx 파일 보기

104
 
104
 
105
   canTransform: false,
105
   canTransform: false,
106
   canChangeAspectRatio: false,
106
   canChangeAspectRatio: false,
107
+  canStyleFill: true,
107
 })
108
 })
108
 
109
 
109
 export default dot
110
 export default dot

+ 6
- 0
lib/shape-utils/draw.tsx 파일 보기

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

+ 1
- 0
lib/shape-utils/ellipse.tsx 파일 보기

149
 
149
 
150
   canTransform: true,
150
   canTransform: true,
151
   canChangeAspectRatio: true,
151
   canChangeAspectRatio: true,
152
+  canStyleFill: true,
152
 })
153
 })
153
 
154
 
154
 export default ellipse
155
 export default ellipse

+ 3
- 0
lib/shape-utils/index.tsx 파일 보기

39
   // Whether the shape's aspect ratio can change
39
   // Whether the shape's aspect ratio can change
40
   canChangeAspectRatio: boolean
40
   canChangeAspectRatio: boolean
41
 
41
 
42
+  // Whether the shape's style can be filled
43
+  canStyleFill: boolean
44
+
42
   // Create a new shape.
45
   // Create a new shape.
43
   create(props: Partial<K>): K
46
   create(props: Partial<K>): K
44
 
47
 

+ 1
- 0
lib/shape-utils/line.tsx 파일 보기

113
 
113
 
114
   canTransform: false,
114
   canTransform: false,
115
   canChangeAspectRatio: false,
115
   canChangeAspectRatio: false,
116
+  canStyleFill: false,
116
 })
117
 })
117
 
118
 
118
 export default line
119
 export default line

+ 1
- 0
lib/shape-utils/polyline.tsx 파일 보기

137
 
137
 
138
   canTransform: true,
138
   canTransform: true,
139
   canChangeAspectRatio: true,
139
   canChangeAspectRatio: true,
140
+  canStyleFill: false,
140
 })
141
 })
141
 
142
 
142
 export default polyline
143
 export default polyline

+ 1
- 0
lib/shape-utils/ray.tsx 파일 보기

112
 
112
 
113
   canTransform: false,
113
   canTransform: false,
114
   canChangeAspectRatio: false,
114
   canChangeAspectRatio: false,
115
+  canStyleFill: false,
115
 })
116
 })
116
 
117
 
117
 export default ray
118
 export default ray

+ 1
- 0
lib/shape-utils/rectangle.tsx 파일 보기

150
 
150
 
151
   canTransform: true,
151
   canTransform: true,
152
   canChangeAspectRatio: true,
152
   canChangeAspectRatio: true,
153
+  canStyleFill: true,
153
 })
154
 })
154
 
155
 
155
 export default rectangle
156
 export default rectangle

+ 1
- 0
package.json 파일 보기

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

+ 47
- 0
state/inputs.tsx 파일 보기

1
+import React from 'react'
1
 import { PointerInfo } from 'types'
2
 import { PointerInfo } from 'types'
2
 import { isDarwin } from 'utils/utils'
3
 import { isDarwin } from 'utils/utils'
3
 
4
 
5
   activePointerId?: number
6
   activePointerId?: number
6
   points: Record<string, PointerInfo> = {}
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
   pointerDown(e: PointerEvent | React.PointerEvent, target: string) {
55
   pointerDown(e: PointerEvent | React.PointerEvent, target: string) {
9
     const { shiftKey, ctrlKey, metaKey, altKey } = e
56
     const { shiftKey, ctrlKey, metaKey, altKey } = e
10
 
57
 

+ 32
- 0
state/state.ts 파일 보기

15
   getCurrent,
15
   getCurrent,
16
   getPage,
16
   getPage,
17
   getSelectedBounds,
17
   getSelectedBounds,
18
+  getSelectedShapes,
18
   getShape,
19
   getShape,
19
   screenToWorld,
20
   screenToWorld,
20
   setZoomCSS,
21
   setZoomCSS,
32
   DistributeType,
33
   DistributeType,
33
   AlignType,
34
   AlignType,
34
   StretchType,
35
   StretchType,
36
+  DashStyle,
35
 } from 'types'
37
 } from 'types'
36
 
38
 
37
 const initialData: Data = {
39
 const initialData: Data = {
50
     fill: shades.lightGray,
52
     fill: shades.lightGray,
51
     stroke: shades.darkGray,
53
     stroke: shades.darkGray,
52
     strokeWidth: 2,
54
     strokeWidth: 2,
55
+    dash: DashStyle.Solid,
53
   },
56
   },
54
   camera: {
57
   camera: {
55
     point: [0, 0],
58
     point: [0, 0],
1296
         ...shapes.map((shape) => getShapeUtils(shape).getRotatedBounds(shape))
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 파일 보기

8
     colors: {
8
     colors: {
9
       brushFill: 'rgba(0,0,0,.1)',
9
       brushFill: 'rgba(0,0,0,.1)',
10
       brushStroke: 'rgba(0,0,0,.5)',
10
       brushStroke: 'rgba(0,0,0,.5)',
11
-      hint: 'rgba(66, 133, 244, 0.200)',
11
+      hint: 'rgba(216, 226, 249, 1.000)',
12
       selected: 'rgba(66, 133, 244, 1.000)',
12
       selected: 'rgba(66, 133, 244, 1.000)',
13
       bounds: 'rgba(65, 132, 244, 1.000)',
13
       bounds: 'rgba(65, 132, 244, 1.000)',
14
       boundsBg: 'rgba(65, 132, 244, 0.100)',
14
       boundsBg: 'rgba(65, 132, 244, 0.100)',

+ 12
- 2
types.ts 파일 보기

67
 // Cubic = "cubic",
67
 // Cubic = "cubic",
68
 // Conic = "conic",
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
 export interface BaseShape {
76
 export interface BaseShape {
73
   id: string
77
   id: string
173
 }
177
 }
174
 
178
 
175
 export enum Decoration {
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
 export interface ShapeBinding {
189
 export interface ShapeBinding {

+ 137
- 25
utils/intersections.ts 파일 보기

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
 interface Intersection {
5
 interface Intersection {
5
   didIntersect: boolean
6
   didIntersect: boolean
26
   const u_b = BV[1] * AV[0] - BV[0] * AV[1]
27
   const u_b = BV[1] * AV[0] - BV[0] * AV[1]
27
 
28
 
28
   if (ua_t === 0 || ub_t === 0) {
29
   if (ua_t === 0 || ub_t === 0) {
29
-    return getIntersection("coincident")
30
+    return getIntersection('coincident')
30
   }
31
   }
31
 
32
 
32
   if (u_b === 0) {
33
   if (u_b === 0) {
33
-    return getIntersection("parallel")
34
+    return getIntersection('parallel')
34
   }
35
   }
35
 
36
 
36
   if (u_b != 0) {
37
   if (u_b != 0) {
37
     const ua = ua_t / u_b
38
     const ua = ua_t / u_b
38
     const ub = ub_t / u_b
39
     const ub = ub_t / u_b
39
     if (0 <= ua && ua <= 1 && 0 <= ub && ub <= 1) {
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
 export function intersectCircleLineSegment(
48
 export function intersectCircleLineSegment(
65
   const deter = b * b - 4 * a * cc
66
   const deter = b * b - 4 * a * cc
66
 
67
 
67
   if (deter < 0) {
68
   if (deter < 0) {
68
-    return getIntersection("outside")
69
+    return getIntersection('outside')
69
   }
70
   }
70
 
71
 
71
   if (deter === 0) {
72
   if (deter === 0) {
72
-    return getIntersection("tangent")
73
+    return getIntersection('tangent')
73
   }
74
   }
74
 
75
 
75
   var e = Math.sqrt(deter)
76
   var e = Math.sqrt(deter)
77
   var u2 = (-b - e) / (2 * a)
78
   var u2 = (-b - e) / (2 * a)
78
   if ((u1 < 0 || u1 > 1) && (u2 < 0 || u2 > 1)) {
79
   if ((u1 < 0 || u1 > 1) && (u2 < 0 || u2 > 1)) {
79
     if ((u1 < 0 && u2 < 0) || (u1 > 1 && u2 > 1)) {
80
     if ((u1 < 0 && u2 < 0) || (u1 > 1 && u2 > 1)) {
80
-      return getIntersection("outside")
81
+      return getIntersection('outside')
81
     } else {
82
     } else {
82
-      return getIntersection("inside")
83
+      return getIntersection('inside')
83
     }
84
     }
84
   }
85
   }
85
 
86
 
87
   if (0 <= u1 && u1 <= 1) results.push(vec.lrp(a1, a2, u1))
88
   if (0 <= u1 && u1 <= 1) results.push(vec.lrp(a1, a2, u1))
88
   if (0 <= u2 && u2 <= 1) results.push(vec.lrp(a1, a2, u2))
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
 export function intersectEllipseLineSegment(
94
 export function intersectEllipseLineSegment(
100
 ) {
101
 ) {
101
   // If the ellipse or line segment are empty, return no tValues.
102
   // If the ellipse or line segment are empty, return no tValues.
102
   if (rx === 0 || ry === 0 || vec.isEqual(a1, a2)) {
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
   // Get the semimajor and semiminor axes.
107
   // Get the semimajor and semiminor axes.
141
     .map((t) => vec.add(center, vec.add(a1, vec.mul(vec.sub(a2, a1), t))))
142
     .map((t) => vec.add(center, vec.add(a1, vec.mul(vec.sub(a2, a1), t))))
142
     .map((p) => vec.rotWith(p, center, rotation))
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
 export function intersectCircleRectangle(
173
 export function intersectCircleRectangle(
163
   const leftIntersection = intersectCircleLineSegment(c, r, tl, bl)
189
   const leftIntersection = intersectCircleLineSegment(c, r, tl, bl)
164
 
190
 
165
   if (topIntersection.didIntersect) {
191
   if (topIntersection.didIntersect) {
166
-    intersections.push({ ...topIntersection, message: "top" })
192
+    intersections.push({ ...topIntersection, message: 'top' })
167
   }
193
   }
168
 
194
 
169
   if (rightIntersection.didIntersect) {
195
   if (rightIntersection.didIntersect) {
170
-    intersections.push({ ...rightIntersection, message: "right" })
196
+    intersections.push({ ...rightIntersection, message: 'right' })
171
   }
197
   }
172
 
198
 
173
   if (bottomIntersection.didIntersect) {
199
   if (bottomIntersection.didIntersect) {
174
-    intersections.push({ ...bottomIntersection, message: "bottom" })
200
+    intersections.push({ ...bottomIntersection, message: 'bottom' })
175
   }
201
   }
176
 
202
 
177
   if (leftIntersection.didIntersect) {
203
   if (leftIntersection.didIntersect) {
178
-    intersections.push({ ...leftIntersection, message: "left" })
204
+    intersections.push({ ...leftIntersection, message: 'left' })
179
   }
205
   }
180
 
206
 
181
   return intersections
207
   return intersections
230
   )
256
   )
231
 
257
 
232
   if (topIntersection.didIntersect) {
258
   if (topIntersection.didIntersect) {
233
-    intersections.push({ ...topIntersection, message: "top" })
259
+    intersections.push({ ...topIntersection, message: 'top' })
234
   }
260
   }
235
 
261
 
236
   if (rightIntersection.didIntersect) {
262
   if (rightIntersection.didIntersect) {
237
-    intersections.push({ ...rightIntersection, message: "right" })
263
+    intersections.push({ ...rightIntersection, message: 'right' })
238
   }
264
   }
239
 
265
 
240
   if (bottomIntersection.didIntersect) {
266
   if (bottomIntersection.didIntersect) {
241
-    intersections.push({ ...bottomIntersection, message: "bottom" })
267
+    intersections.push({ ...bottomIntersection, message: 'bottom' })
242
   }
268
   }
243
 
269
 
244
   if (leftIntersection.didIntersect) {
270
   if (leftIntersection.didIntersect) {
245
-    intersections.push({ ...leftIntersection, message: "left" })
271
+    intersections.push({ ...leftIntersection, message: 'left' })
246
   }
272
   }
247
 
273
 
248
   return intersections
274
   return intersections
267
   const leftIntersection = intersectLineSegments(a1, a2, tl, bl)
293
   const leftIntersection = intersectLineSegments(a1, a2, tl, bl)
268
 
294
 
269
   if (topIntersection.didIntersect) {
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
   if (rightIntersection.didIntersect) {
366
   if (rightIntersection.didIntersect) {
274
-    intersections.push({ ...rightIntersection, message: "right" })
367
+    intersections.push({ ...rightIntersection, message: 'right' })
275
   }
368
   }
276
 
369
 
277
   if (bottomIntersection.didIntersect) {
370
   if (bottomIntersection.didIntersect) {
278
-    intersections.push({ ...bottomIntersection, message: "bottom" })
371
+    intersections.push({ ...bottomIntersection, message: 'bottom' })
279
   }
372
   }
280
 
373
 
281
   if (leftIntersection.didIntersect) {
374
   if (leftIntersection.didIntersect) {
282
-    intersections.push({ ...leftIntersection, message: "left" })
375
+    intersections.push({ ...leftIntersection, message: 'left' })
283
   }
376
   }
284
 
377
 
285
   return intersections
378
   return intersections
360
 
453
 
361
   return intersections
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 파일 보기

1566
   d.push('Z')
1566
   d.push('Z')
1567
   return d.join(' ')
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 파일 보기

1283
     "@radix-ui/react-polymorphic" "0.0.11"
1283
     "@radix-ui/react-polymorphic" "0.0.11"
1284
     "@radix-ui/react-primitive" "0.0.13"
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
 "@radix-ui/react-collection@0.0.12":
1301
 "@radix-ui/react-collection@0.0.12":
1287
   version "0.0.12"
1302
   version "0.0.12"
1288
   resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-0.0.12.tgz#5cd09312cdec34fdbbe1d31affaba69eb768e342"
1303
   resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-0.0.12.tgz#5cd09312cdec34fdbbe1d31affaba69eb768e342"

Loading…
취소
저장