Browse Source

Adds tool lock

main
Steve Ruiz 4 years ago
parent
commit
fe3980c80c

+ 23
- 4
components/canvas/shape.tsx View File

@@ -23,9 +23,9 @@ function Shape({ id, isSelecting }: { id: string; isSelecting: boolean }) {
23 23
   // detects the change and pulls this component.
24 24
   if (!shape) return null
25 25
 
26
+  const center = getShapeUtils(shape).getCenter(shape)
26 27
   const transform = `
27
-  rotate(${shape.rotation * (180 / Math.PI)},
28
-  ${getShapeUtils(shape).getCenter(shape)})
28
+  rotate(${shape.rotation * (180 / Math.PI)}, ${center})
29 29
   translate(${shape.point})`
30 30
 
31 31
   return (
@@ -38,18 +38,37 @@ function Shape({ id, isSelecting }: { id: string; isSelecting: boolean }) {
38 38
     >
39 39
       {isSelecting && <HoverIndicator as="use" href={'#' + id} />}
40 40
       <StyledShape id={id} style={shape.style} />
41
+      {/* 
42
+      <text
43
+        y={4}
44
+        x={4}
45
+        fontSize={18}
46
+        fill="black"
47
+        stroke="none"
48
+        alignmentBaseline="text-before-edge"
49
+        pointerEvents="none"
50
+      >
51
+        {center.toString()}
52
+      </text> */}
41 53
     </StyledGroup>
42 54
   )
43 55
 }
44 56
 
45 57
 const StyledShape = memo(
46 58
   ({ id, style }: { id: string; style: ShapeStyles }) => {
47
-    return <MainShape as="use" href={'#' + id} {...style} />
59
+    return (
60
+      <MainShape
61
+        as="use"
62
+        href={'#' + id}
63
+        {...style}
64
+        // css={{ zStrokeWidth: Number(style.strokeWidth) }}
65
+      />
66
+    )
48 67
   }
49 68
 )
50 69
 
51 70
 const MainShape = styled('use', {
52
-  zStrokeWidth: 1,
71
+  // zStrokeWidth: 1,
53 72
 })
54 73
 
55 74
 const HoverIndicator = styled('path', {

+ 66
- 55
components/panel.tsx View File

@@ -1,20 +1,20 @@
1
-import styled from "styles"
1
+import styled from 'styles'
2 2
 
3
-export const Root = styled("div", {
4
-  position: "relative",
5
-  backgroundColor: "$panel",
6
-  borderRadius: "4px",
7
-  overflow: "hidden",
8
-  border: "1px solid $border",
9
-  pointerEvents: "all",
10
-  userSelect: "none",
3
+export const Root = styled('div', {
4
+  position: 'relative',
5
+  backgroundColor: '$panel',
6
+  borderRadius: '4px',
7
+  overflow: 'hidden',
8
+  border: '1px solid $border',
9
+  pointerEvents: 'all',
10
+  userSelect: 'none',
11 11
   zIndex: 200,
12
-  boxShadow: "0px 2px 25px rgba(0,0,0,.16)",
12
+  boxShadow: '0px 2px 25px rgba(0,0,0,.16)',
13 13
 
14 14
   variants: {
15 15
     isOpen: {
16 16
       true: {
17
-        width: "auto",
17
+        width: 'auto',
18 18
         minWidth: 300,
19 19
       },
20 20
       false: {
@@ -25,63 +25,74 @@ export const Root = styled("div", {
25 25
   },
26 26
 })
27 27
 
28
-export const Layout = styled("div", {
29
-  display: "grid",
30
-  gridTemplateColumns: "1fr",
31
-  gridTemplateRows: "auto 1fr",
32
-  gridAutoRows: "28px",
33
-  height: "100%",
34
-  width: "auto",
35
-  minWidth: "100%",
28
+export const Layout = styled('div', {
29
+  display: 'grid',
30
+  gridTemplateColumns: '1fr',
31
+  gridTemplateRows: 'auto 1fr',
32
+  gridAutoRows: '28px',
33
+  height: '100%',
34
+  width: 'auto',
35
+  minWidth: '100%',
36 36
   maxWidth: 560,
37
-  overflow: "hidden",
38
-  userSelect: "none",
39
-  pointerEvents: "all",
37
+  overflow: 'hidden',
38
+  userSelect: 'none',
39
+  pointerEvents: 'all',
40 40
 })
41 41
 
42
-export const Header = styled("div", {
43
-  pointerEvents: "all",
44
-  display: "flex",
45
-  width: "100%",
46
-  alignItems: "center",
47
-  justifyContent: "space-between",
48
-  borderBottom: "1px solid $border",
49
-  position: "relative",
42
+export const Header = styled('div', {
43
+  pointerEvents: 'all',
44
+  display: 'flex',
45
+  width: '100%',
46
+  alignItems: 'center',
47
+  justifyContent: 'space-between',
48
+  borderBottom: '1px solid $border',
49
+  position: 'relative',
50 50
 
51
-  "& h3": {
52
-    position: "absolute",
51
+  '& h3': {
52
+    position: 'absolute',
53 53
     top: 0,
54 54
     left: 0,
55
-    width: "100%",
56
-    height: "100%",
57
-    textAlign: "center",
55
+    width: '100%',
56
+    height: '100%',
57
+    textAlign: 'center',
58 58
     padding: 0,
59 59
     margin: 0,
60
-    display: "flex",
61
-    justifyContent: "center",
62
-    alignItems: "center",
63
-    fontSize: "13px",
64
-    pointerEvents: "none",
65
-    userSelect: "none",
60
+    display: 'flex',
61
+    justifyContent: 'center',
62
+    alignItems: 'center',
63
+    fontSize: '13px',
64
+    pointerEvents: 'none',
65
+    userSelect: 'none',
66
+  },
67
+
68
+  variants: {
69
+    side: {
70
+      left: {
71
+        flexDirection: 'row',
72
+      },
73
+      right: {
74
+        flexDirection: 'row-reverse',
75
+      },
76
+    },
66 77
   },
67 78
 })
68 79
 
69
-export const ButtonsGroup = styled("div", {
70
-  display: "flex",
80
+export const ButtonsGroup = styled('div', {
81
+  display: 'flex',
71 82
 })
72 83
 
73
-export const Content = styled("div", {
74
-  position: "relative",
75
-  pointerEvents: "all",
76
-  overflowY: "scroll",
84
+export const Content = styled('div', {
85
+  position: 'relative',
86
+  pointerEvents: 'all',
87
+  overflowY: 'scroll',
77 88
 })
78 89
 
79
-export const Footer = styled("div", {
80
-  overflowX: "scroll",
81
-  color: "$text",
82
-  font: "$debug",
83
-  padding: "0 12px",
84
-  display: "flex",
85
-  alignItems: "center",
86
-  borderTop: "1px solid $border",
90
+export const Footer = styled('div', {
91
+  overflowX: 'scroll',
92
+  color: '$text',
93
+  font: '$debug',
94
+  padding: '0 12px',
95
+  display: 'flex',
96
+  alignItems: 'center',
97
+  borderTop: '1px solid $border',
87 98
 })

+ 21
- 21
components/style-panel/align-distribute.tsx View File

@@ -9,50 +9,50 @@ import {
9 9
   SpaceEvenlyVerticallyIcon,
10 10
   StretchHorizontallyIcon,
11 11
   StretchVerticallyIcon,
12
-} from "@radix-ui/react-icons"
13
-import { IconButton } from "components/shared"
14
-import state from "state"
15
-import styled from "styles"
16
-import { AlignType, DistributeType, StretchType } from "types"
12
+} from '@radix-ui/react-icons'
13
+import { IconButton } from 'components/shared'
14
+import state from 'state'
15
+import styled from 'styles'
16
+import { AlignType, DistributeType, StretchType } from 'types'
17 17
 
18 18
 function alignTop() {
19
-  state.send("ALIGNED", { type: AlignType.Top })
19
+  state.send('ALIGNED', { type: AlignType.Top })
20 20
 }
21 21
 
22 22
 function alignCenterVertical() {
23
-  state.send("ALIGNED", { type: AlignType.CenterVertical })
23
+  state.send('ALIGNED', { type: AlignType.CenterVertical })
24 24
 }
25 25
 
26 26
 function alignBottom() {
27
-  state.send("ALIGNED", { type: AlignType.Bottom })
27
+  state.send('ALIGNED', { type: AlignType.Bottom })
28 28
 }
29 29
 
30 30
 function stretchVertically() {
31
-  state.send("STRETCHED", { type: StretchType.Vertical })
31
+  state.send('STRETCHED', { type: StretchType.Vertical })
32 32
 }
33 33
 
34 34
 function distributeVertically() {
35
-  state.send("DISTRIBUTED", { type: DistributeType.Vertical })
35
+  state.send('DISTRIBUTED', { type: DistributeType.Vertical })
36 36
 }
37 37
 
38 38
 function alignLeft() {
39
-  state.send("ALIGNED", { type: AlignType.Left })
39
+  state.send('ALIGNED', { type: AlignType.Left })
40 40
 }
41 41
 
42 42
 function alignCenterHorizontal() {
43
-  state.send("ALIGNED", { type: AlignType.CenterHorizontal })
43
+  state.send('ALIGNED', { type: AlignType.CenterHorizontal })
44 44
 }
45 45
 
46 46
 function alignRight() {
47
-  state.send("ALIGNED", { type: AlignType.Right })
47
+  state.send('ALIGNED', { type: AlignType.Right })
48 48
 }
49 49
 
50 50
 function stretchHorizontally() {
51
-  state.send("STRETCHED", { type: StretchType.Horizontal })
51
+  state.send('STRETCHED', { type: StretchType.Horizontal })
52 52
 }
53 53
 
54 54
 function distributeHorizontally() {
55
-  state.send("DISTRIBUTED", { type: DistributeType.Horizontal })
55
+  state.send('DISTRIBUTED', { type: DistributeType.Horizontal })
56 56
 }
57 57
 
58 58
 export default function AlignDistribute({
@@ -98,15 +98,15 @@ export default function AlignDistribute({
98 98
   )
99 99
 }
100 100
 
101
-const Container = styled("div", {
102
-  display: "grid",
101
+const Container = styled('div', {
102
+  display: 'grid',
103 103
   padding: 4,
104
-  gridTemplateColumns: "repeat(5, auto)",
104
+  gridTemplateColumns: 'repeat(5, auto)',
105 105
   [`& ${IconButton}`]: {
106
-    color: "$text",
106
+    color: '$text',
107 107
   },
108 108
   [`& ${IconButton} > svg`]: {
109
-    fill: "red",
110
-    stroke: "transparent",
109
+    fill: 'red',
110
+    stroke: 'transparent',
111 111
   },
112 112
 })

+ 48
- 48
components/style-panel/color-picker.tsx View File

@@ -1,6 +1,6 @@
1
-import * as DropdownMenu from "@radix-ui/react-dropdown-menu"
2
-import { Square } from "react-feather"
3
-import styled from "styles"
1
+import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
2
+import { Square } from 'react-feather'
3
+import styled from 'styles'
4 4
 
5 5
 interface Props {
6 6
   label: string
@@ -13,7 +13,7 @@ export default function ColorPicker({ label, color, colors, onChange }: Props) {
13 13
   return (
14 14
     <DropdownMenu.Root>
15 15
       <CurrentColor>
16
-        <h3>{label}</h3>
16
+        <label>{label}</label>
17 17
         <ColorIcon color={color} />
18 18
       </CurrentColor>
19 19
       <Colors sideOffset={4}>
@@ -31,96 +31,96 @@ function ColorIcon({ color }: { color: string }) {
31 31
   return (
32 32
     <Square
33 33
       fill={color}
34
-      strokeDasharray={color === "transparent" ? "2, 3" : "none"}
34
+      strokeDasharray={color === 'transparent' ? '2, 3' : 'none'}
35 35
     />
36 36
   )
37 37
 }
38 38
 
39 39
 const Colors = styled(DropdownMenu.Content, {
40
-  display: "grid",
40
+  display: 'grid',
41 41
   padding: 4,
42
-  gridTemplateColumns: "repeat(6, 1fr)",
43
-  border: "1px solid $border",
44
-  backgroundColor: "$panel",
42
+  gridTemplateColumns: 'repeat(6, 1fr)',
43
+  border: '1px solid $border',
44
+  backgroundColor: '$panel',
45 45
   borderRadius: 4,
46
-  boxShadow: "0px 5px 15px -5px hsla(206,22%,7%,.15)",
46
+  boxShadow: '0px 5px 15px -5px hsla(206,22%,7%,.15)',
47 47
 })
48 48
 
49 49
 const ColorButton = styled(DropdownMenu.Item, {
50
-  position: "relative",
51
-  cursor: "pointer",
50
+  position: 'relative',
51
+  cursor: 'pointer',
52 52
   height: 32,
53 53
   width: 32,
54
-  border: "none",
55
-  padding: "none",
56
-  background: "none",
57
-  display: "flex",
58
-  alignItems: "center",
59
-  justifyContent: "center",
54
+  border: 'none',
55
+  padding: 'none',
56
+  background: 'none',
57
+  display: 'flex',
58
+  alignItems: 'center',
59
+  justifyContent: 'center',
60 60
 
61
-  "&::before": {
61
+  '&::before': {
62 62
     content: "''",
63
-    position: "absolute",
63
+    position: 'absolute',
64 64
     top: 4,
65 65
     left: 4,
66 66
     right: 4,
67 67
     bottom: 4,
68
-    pointerEvents: "none",
68
+    pointerEvents: 'none',
69 69
     zIndex: 0,
70 70
   },
71 71
 
72
-  "&:hover::before": {
73
-    backgroundColor: "$hover",
72
+  '&:hover::before': {
73
+    backgroundColor: '$hover',
74 74
     borderRadius: 4,
75 75
   },
76 76
 
77
-  "& svg": {
78
-    position: "relative",
79
-    stroke: "rgba(0,0,0,.2)",
77
+  '& svg': {
78
+    position: 'relative',
79
+    stroke: 'rgba(0,0,0,.2)',
80 80
     strokeWidth: 1,
81 81
     zIndex: 1,
82 82
   },
83 83
 })
84 84
 
85 85
 const CurrentColor = styled(DropdownMenu.Trigger, {
86
-  position: "relative",
87
-  display: "flex",
88
-  width: "100%",
89
-  background: "none",
90
-  border: "none",
91
-  cursor: "pointer",
92
-  outline: "none",
93
-  alignItems: "center",
94
-  justifyContent: "space-between",
95
-  padding: "4px 6px 4px 12px",
86
+  position: 'relative',
87
+  display: 'flex',
88
+  width: '100%',
89
+  background: 'none',
90
+  border: 'none',
91
+  cursor: 'pointer',
92
+  outline: 'none',
93
+  alignItems: 'center',
94
+  justifyContent: 'space-between',
95
+  padding: '4px 6px 4px 12px',
96 96
 
97
-  "&::before": {
97
+  '&::before': {
98 98
     content: "''",
99
-    position: "absolute",
99
+    position: 'absolute',
100 100
     top: 0,
101 101
     left: 0,
102 102
     right: 0,
103 103
     bottom: 0,
104
-    pointerEvents: "none",
104
+    pointerEvents: 'none',
105 105
     zIndex: -1,
106 106
   },
107 107
 
108
-  "&:hover::before": {
109
-    backgroundColor: "$hover",
108
+  '&:hover::before': {
109
+    backgroundColor: '$hover',
110 110
     borderRadius: 4,
111 111
   },
112 112
 
113
-  "& h3": {
114
-    fontFamily: "$ui",
115
-    fontSize: "$2",
116
-    fontWeight: "$1",
113
+  '& label': {
114
+    fontFamily: '$ui',
115
+    fontSize: '$2',
116
+    fontWeight: '$1',
117 117
     margin: 0,
118 118
     padding: 0,
119 119
   },
120 120
 
121
-  "& svg": {
122
-    position: "relative",
123
-    stroke: "rgba(0,0,0,.2)",
121
+  '& svg': {
122
+    position: 'relative',
123
+    stroke: 'rgba(0,0,0,.2)',
124 124
     strokeWidth: 1,
125 125
     zIndex: 1,
126 126
   },

+ 81
- 27
components/style-panel/style-panel.tsx View File

@@ -1,15 +1,17 @@
1
-import styled from "styles"
2
-import state, { useSelector } from "state"
3
-import * as Panel from "components/panel"
4
-import { useRef } from "react"
5
-import { IconButton } from "components/shared"
6
-import { Circle, Trash, X } from "react-feather"
7
-import { deepCompare, deepCompareArrays, getSelectedShapes } from "utils/utils"
8
-import { shades, fills, strokes } from "lib/colors"
9
-
10
-import ColorPicker from "./color-picker"
11
-import AlignDistribute from "./align-distribute"
12
-import { ShapeStyles } from "types"
1
+import styled from 'styles'
2
+import state, { useSelector } from 'state'
3
+import * as Panel from 'components/panel'
4
+import { useRef } from 'react'
5
+import { IconButton } from 'components/shared'
6
+import { Circle, Copy, Lock, Trash, Unlock, X } from 'react-feather'
7
+import { deepCompare, deepCompareArrays, getSelectedShapes } from 'utils/utils'
8
+import { shades, fills, strokes } from 'lib/colors'
9
+
10
+import ColorPicker from './color-picker'
11
+import AlignDistribute from './align-distribute'
12
+import { ShapeStyles } from 'types'
13
+import WidthPicker from './width-picker'
14
+import { CopyIcon } from '@radix-ui/react-icons'
13 15
 
14 16
 const fillColors = { ...shades, ...fills }
15 17
 const strokeColors = { ...shades, ...strokes }
@@ -23,7 +25,7 @@ export default function StylePanel() {
23 25
       {isOpen ? (
24 26
         <SelectedShapeStyles />
25 27
       ) : (
26
-        <IconButton onClick={() => state.send("TOGGLED_STYLE_PANEL_OPEN")}>
28
+        <IconButton onClick={() => state.send('TOGGLED_STYLE_PANEL_OPEN')}>
27 29
           <Circle />
28 30
         </IconButton>
29 31
       )}
@@ -72,17 +74,9 @@ function SelectedShapeStyles({}: {}) {
72 74
 
73 75
   return (
74 76
     <Panel.Layout>
75
-      <Panel.Header>
77
+      <Panel.Header side="right">
76 78
         <h3>Style</h3>
77
-        <Panel.ButtonsGroup>
78
-          <IconButton
79
-            disabled={!hasSelection}
80
-            onClick={() => state.send("DELETED")}
81
-          >
82
-            <Trash />
83
-          </IconButton>
84
-        </Panel.ButtonsGroup>
85
-        <IconButton onClick={() => state.send("TOGGLED_STYLE_PANEL_OPEN")}>
79
+        <IconButton onClick={() => state.send('TOGGLED_STYLE_PANEL_OPEN')}>
86 80
           <X />
87 81
         </IconButton>
88 82
       </Panel.Header>
@@ -91,18 +85,40 @@ function SelectedShapeStyles({}: {}) {
91 85
           label="Fill"
92 86
           color={shapesStyle.fill}
93 87
           colors={fillColors}
94
-          onChange={(color) => state.send("CHANGED_STYLE", { fill: color })}
88
+          onChange={(color) => state.send('CHANGED_STYLE', { fill: color })}
95 89
         />
96 90
         <ColorPicker
97 91
           label="Stroke"
98 92
           color={shapesStyle.stroke}
99 93
           colors={strokeColors}
100
-          onChange={(color) => state.send("CHANGED_STYLE", { stroke: color })}
94
+          onChange={(color) => state.send('CHANGED_STYLE', { stroke: color })}
101 95
         />
96
+        <Row>
97
+          <label htmlFor="width">Width</label>
98
+          <WidthPicker strokeWidth={Number(shapesStyle.strokeWidth)} />
99
+        </Row>
102 100
         <AlignDistribute
103 101
           hasTwoOrMore={selectedIds.length > 1}
104 102
           hasThreeOrMore={selectedIds.length > 2}
105 103
         />
104
+        <ButtonsRow>
105
+          <IconButton
106
+            disabled={!hasSelection}
107
+            onClick={() => state.send('DELETED')}
108
+          >
109
+            <Trash />
110
+          </IconButton>
111
+          <IconButton
112
+            disabled={!hasSelection}
113
+            onClick={() => state.send('DUPLICATED')}
114
+          >
115
+            <Copy />
116
+          </IconButton>
117
+
118
+          <IconButton>
119
+            <Unlock />
120
+          </IconButton>
121
+        </ButtonsRow>
106 122
       </Content>
107 123
     </Panel.Layout>
108 124
   )
@@ -112,8 +128,8 @@ const StylePanelRoot = styled(Panel.Root, {
112 128
   minWidth: 1,
113 129
   width: 184,
114 130
   maxWidth: 184,
115
-  overflow: "hidden",
116
-  position: "relative",
131
+  overflow: 'hidden',
132
+  position: 'relative',
117 133
 
118 134
   variants: {
119 135
     isOpen: {
@@ -129,3 +145,41 @@ const StylePanelRoot = styled(Panel.Root, {
129 145
 const Content = styled(Panel.Content, {
130 146
   padding: 8,
131 147
 })
148
+
149
+const Row = styled('div', {
150
+  position: 'relative',
151
+  display: 'flex',
152
+  width: '100%',
153
+  background: 'none',
154
+  border: 'none',
155
+  cursor: 'pointer',
156
+  outline: 'none',
157
+  alignItems: 'center',
158
+  justifyContent: 'space-between',
159
+  padding: '4px 2px 4px 12px',
160
+
161
+  '& label': {
162
+    fontFamily: '$ui',
163
+    fontSize: '$2',
164
+    fontWeight: '$1',
165
+    margin: 0,
166
+    padding: 0,
167
+  },
168
+
169
+  '& > svg': {
170
+    position: 'relative',
171
+  },
172
+})
173
+
174
+const ButtonsRow = styled('div', {
175
+  position: 'relative',
176
+  display: 'flex',
177
+  width: '100%',
178
+  background: 'none',
179
+  border: 'none',
180
+  cursor: 'pointer',
181
+  outline: 'none',
182
+  alignItems: 'center',
183
+  justifyContent: 'flex-start',
184
+  padding: 4,
185
+})

+ 82
- 0
components/style-panel/width-picker.tsx View File

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

+ 74
- 61
components/toolbar.tsx View File

@@ -1,134 +1,147 @@
1
-import state, { useSelector } from "state"
2
-import styled from "styles"
3
-import { Menu } from "react-feather"
1
+import state, { useSelector } from 'state'
2
+import styled from 'styles'
3
+import { Lock, Menu, Unlock } from 'react-feather'
4
+import { IconButton } from './shared'
4 5
 
5 6
 export default function Toolbar() {
6 7
   const activeTool = useSelector((state) =>
7 8
     state.whenIn({
8
-      selecting: "select",
9
-      dot: "dot",
10
-      circle: "circle",
11
-      ellipse: "ellipse",
12
-      ray: "ray",
13
-      line: "line",
14
-      polyline: "polyline",
15
-      rectangle: "rectangle",
16
-      draw: "draw",
9
+      selecting: 'select',
10
+      dot: 'dot',
11
+      circle: 'circle',
12
+      ellipse: 'ellipse',
13
+      ray: 'ray',
14
+      line: 'line',
15
+      polyline: 'polyline',
16
+      rectangle: 'rectangle',
17
+      draw: 'draw',
17 18
     })
18 19
   )
19 20
 
21
+  const isToolLocked = useSelector((s) => s.data.settings.isToolLocked)
22
+
20 23
   return (
21 24
     <ToolbarContainer>
22 25
       <Section>
23 26
         <Button>
24 27
           <Menu />
25 28
         </Button>
29
+        <Button onClick={() => state.send('TOGGLED_TOOL_LOCK')}>
30
+          {isToolLocked ? <Lock /> : <Unlock />}
31
+        </Button>
26 32
         <Button
27
-          isSelected={activeTool === "select"}
28
-          onClick={() => state.send("SELECTED_SELECT_TOOL")}
33
+          isSelected={activeTool === 'select'}
34
+          onClick={() => state.send('SELECTED_SELECT_TOOL')}
29 35
         >
30 36
           Select
31 37
         </Button>
32 38
         <Button
33
-          isSelected={activeTool === "draw"}
34
-          onClick={() => state.send("SELECTED_DRAW_TOOL")}
39
+          isSelected={activeTool === 'draw'}
40
+          onClick={() => state.send('SELECTED_DRAW_TOOL')}
35 41
         >
36 42
           Draw
37 43
         </Button>
38 44
         <Button
39
-          isSelected={activeTool === "dot"}
40
-          onClick={() => state.send("SELECTED_DOT_TOOL")}
45
+          isSelected={activeTool === 'dot'}
46
+          onClick={() => state.send('SELECTED_DOT_TOOL')}
41 47
         >
42 48
           Dot
43 49
         </Button>
44 50
         <Button
45
-          isSelected={activeTool === "circle"}
46
-          onClick={() => state.send("SELECTED_CIRCLE_TOOL")}
51
+          isSelected={activeTool === 'circle'}
52
+          onClick={() => state.send('SELECTED_CIRCLE_TOOL')}
47 53
         >
48 54
           Circle
49 55
         </Button>
50 56
         <Button
51
-          isSelected={activeTool === "ellipse"}
52
-          onClick={() => state.send("SELECTED_ELLIPSE_TOOL")}
57
+          isSelected={activeTool === 'ellipse'}
58
+          onClick={() => state.send('SELECTED_ELLIPSE_TOOL')}
53 59
         >
54 60
           Ellipse
55 61
         </Button>
56 62
         <Button
57
-          isSelected={activeTool === "ray"}
58
-          onClick={() => state.send("SELECTED_RAY_TOOL")}
63
+          isSelected={activeTool === 'ray'}
64
+          onClick={() => state.send('SELECTED_RAY_TOOL')}
59 65
         >
60 66
           Ray
61 67
         </Button>
62 68
         <Button
63
-          isSelected={activeTool === "line"}
64
-          onClick={() => state.send("SELECTED_LINE_TOOL")}
69
+          isSelected={activeTool === 'line'}
70
+          onClick={() => state.send('SELECTED_LINE_TOOL')}
65 71
         >
66 72
           Line
67 73
         </Button>
68 74
         <Button
69
-          isSelected={activeTool === "polyline"}
70
-          onClick={() => state.send("SELECTED_POLYLINE_TOOL")}
75
+          isSelected={activeTool === 'polyline'}
76
+          onClick={() => state.send('SELECTED_POLYLINE_TOOL')}
71 77
         >
72 78
           Polyline
73 79
         </Button>
74 80
         <Button
75
-          isSelected={activeTool === "rectangle"}
76
-          onClick={() => state.send("SELECTED_RECTANGLE_TOOL")}
81
+          isSelected={activeTool === 'rectangle'}
82
+          onClick={() => state.send('SELECTED_RECTANGLE_TOOL')}
77 83
         >
78 84
           Rectangle
79 85
         </Button>
80
-        <Button onClick={() => state.send("RESET_CAMERA")}>Reset Camera</Button>
86
+        <Button onClick={() => state.send('RESET_CAMERA')}>Reset Camera</Button>
81 87
       </Section>
82 88
       <Section>
83
-        <Button onClick={() => state.send("UNDO")}>Undo</Button>
84
-        <Button onClick={() => state.send("REDO")}>Redo</Button>
89
+        <Button onClick={() => state.send('UNDO')}>Undo</Button>
90
+        <Button onClick={() => state.send('REDO')}>Redo</Button>
85 91
       </Section>
86 92
     </ToolbarContainer>
87 93
   )
88 94
 }
89 95
 
90
-const ToolbarContainer = styled("div", {
91
-  gridArea: "toolbar",
92
-  userSelect: "none",
93
-  borderBottom: "1px solid black",
94
-  display: "flex",
95
-  alignItems: "center",
96
-  justifyContent: "space-between",
97
-  backgroundColor: "$panel",
96
+const ToolbarContainer = styled('div', {
97
+  gridArea: 'toolbar',
98
+  userSelect: 'none',
99
+  borderBottom: '1px solid black',
100
+  display: 'flex',
101
+  alignItems: 'center',
102
+  justifyContent: 'space-between',
103
+  backgroundColor: '$panel',
98 104
   gap: 8,
99
-  fontSize: "$1",
105
+  fontSize: '$1',
100 106
   zIndex: 200,
101 107
 })
102 108
 
103
-const Section = styled("div", {
104
-  whiteSpace: "nowrap",
105
-  overflow: "hidden",
106
-  display: "flex",
109
+const Section = styled('div', {
110
+  whiteSpace: 'nowrap',
111
+  overflowY: 'hidden',
112
+  overflowX: 'auto',
113
+  display: 'flex',
114
+  scrollbarWidth: 'none',
115
+  '&::-webkit-scrollbar': {
116
+    '-webkit-appearance': 'none',
117
+    width: 0,
118
+    height: 0,
119
+  },
107 120
 })
108 121
 
109
-const Button = styled("button", {
110
-  display: "flex",
111
-  alignItems: "center",
112
-  cursor: "pointer",
113
-  font: "$ui",
114
-  fontSize: "$ui",
115
-  height: "40px",
116
-  outline: "none",
122
+const Button = styled('button', {
123
+  display: 'flex',
124
+  alignItems: 'center',
125
+  cursor: 'pointer',
126
+  font: '$ui',
127
+  fontSize: '$ui',
128
+  height: '40px',
129
+  outline: 'none',
117 130
   borderRadius: 0,
118
-  border: "none",
119
-  padding: "0 12px",
120
-  background: "none",
121
-  "&:hover": {
122
-    backgroundColor: "$hint",
131
+  border: 'none',
132
+  padding: '0 12px',
133
+  background: 'none',
134
+  '&:hover': {
135
+    backgroundColor: '$hint',
123 136
   },
124
-  "& svg": {
137
+  '& svg': {
125 138
     height: 16,
126 139
     width: 16,
127 140
   },
128 141
   variants: {
129 142
     isSelected: {
130 143
       true: {
131
-        color: "$selected",
144
+        color: '$selected',
132 145
       },
133 146
       false: {},
134 147
     },

+ 85
- 65
hooks/useKeyboardEvents.ts View File

@@ -1,197 +1,217 @@
1
-import { useEffect } from "react"
2
-import state from "state"
3
-import { MoveType } from "types"
4
-import { getKeyboardEventInfo, metaKey } from "utils/utils"
1
+import { useEffect } from 'react'
2
+import state from 'state'
3
+import { MoveType } from 'types'
4
+import { getKeyboardEventInfo, metaKey } from 'utils/utils'
5 5
 
6 6
 export default function useKeyboardEvents() {
7 7
   useEffect(() => {
8 8
     function handleKeyDown(e: KeyboardEvent) {
9
-      if (metaKey(e) && !["i", "r", "j"].includes(e.key)) {
9
+      if (metaKey(e) && !['i', 'r', 'j'].includes(e.key)) {
10 10
         e.preventDefault()
11 11
       }
12 12
 
13 13
       switch (e.key) {
14
-        case "!": {
14
+        case 'ArrowUp': {
15
+          state.send('NUDGED', { delta: [0, -1], ...getKeyboardEventInfo(e) })
16
+          break
17
+        }
18
+        case 'ArrowRight': {
19
+          state.send('NUDGED', { delta: [1, 0], ...getKeyboardEventInfo(e) })
20
+          break
21
+        }
22
+        case 'ArrowDown': {
23
+          state.send('NUDGED', { delta: [0, 1], ...getKeyboardEventInfo(e) })
24
+          break
25
+        }
26
+        case 'ArrowLeft': {
27
+          state.send('NUDGED', { delta: [-1, 0], ...getKeyboardEventInfo(e) })
28
+          break
29
+        }
30
+        case '!': {
15 31
           // Shift + 1
16 32
           if (e.shiftKey) {
17
-            state.send("ZOOMED_TO_FIT")
33
+            state.send('ZOOMED_TO_FIT')
18 34
           }
19 35
           break
20 36
         }
21
-        case "@": {
37
+        case '@': {
22 38
           // Shift + 2
23 39
           if (e.shiftKey) {
24
-            state.send("ZOOMED_TO_SELECTION")
40
+            state.send('ZOOMED_TO_SELECTION')
25 41
           }
26 42
           break
27 43
         }
28
-        case ")": {
44
+        case ')': {
29 45
           // Shift + 0
30 46
           if (e.shiftKey) {
31
-            state.send("ZOOMED_TO_ACTUAL")
47
+            state.send('ZOOMED_TO_ACTUAL')
32 48
           }
33 49
           break
34 50
         }
35
-        case "Escape": {
36
-          state.send("CANCELLED")
51
+        case 'Escape': {
52
+          state.send('CANCELLED')
37 53
           break
38 54
         }
39
-        case "z": {
55
+        case 'z': {
40 56
           if (metaKey(e)) {
41 57
             if (e.shiftKey) {
42
-              state.send("REDO", getKeyboardEventInfo(e))
58
+              state.send('REDO', getKeyboardEventInfo(e))
43 59
             } else {
44
-              state.send("UNDO", getKeyboardEventInfo(e))
60
+              state.send('UNDO', getKeyboardEventInfo(e))
45 61
             }
46 62
           }
47 63
           break
48 64
         }
49
-        case "‘": {
65
+        case '‘': {
50 66
           if (metaKey(e)) {
51
-            state.send("MOVED", {
67
+            state.send('MOVED', {
52 68
               ...getKeyboardEventInfo(e),
53 69
               type: MoveType.ToFront,
54 70
             })
55 71
           }
56 72
           break
57 73
         }
58
-        case "“": {
74
+        case '“': {
59 75
           if (metaKey(e)) {
60
-            state.send("MOVED", {
76
+            state.send('MOVED', {
61 77
               ...getKeyboardEventInfo(e),
62 78
               type: MoveType.ToBack,
63 79
             })
64 80
           }
65 81
           break
66 82
         }
67
-        case "]": {
83
+        case ']': {
68 84
           if (metaKey(e)) {
69
-            state.send("MOVED", {
85
+            state.send('MOVED', {
70 86
               ...getKeyboardEventInfo(e),
71 87
               type: MoveType.Forward,
72 88
             })
73 89
           }
74 90
           break
75 91
         }
76
-        case "[": {
92
+        case '[': {
77 93
           if (metaKey(e)) {
78
-            state.send("MOVED", {
94
+            state.send('MOVED', {
79 95
               ...getKeyboardEventInfo(e),
80 96
               type: MoveType.Backward,
81 97
             })
82 98
           }
83 99
           break
84 100
         }
85
-        case "Shift": {
86
-          state.send("PRESSED_SHIFT_KEY", getKeyboardEventInfo(e))
101
+        case 'Shift': {
102
+          state.send('PRESSED_SHIFT_KEY', getKeyboardEventInfo(e))
87 103
           break
88 104
         }
89
-        case "Alt": {
90
-          state.send("PRESSED_ALT_KEY", getKeyboardEventInfo(e))
105
+        case 'Alt': {
106
+          state.send('PRESSED_ALT_KEY', getKeyboardEventInfo(e))
91 107
           break
92 108
         }
93
-        case "Backspace": {
94
-          state.send("DELETED", getKeyboardEventInfo(e))
109
+        case 'Backspace': {
110
+          state.send('DELETED', getKeyboardEventInfo(e))
95 111
           break
96 112
         }
97
-        case "s": {
113
+        case 's': {
98 114
           if (metaKey(e)) {
99
-            state.send("SAVED", getKeyboardEventInfo(e))
115
+            state.send('SAVED', getKeyboardEventInfo(e))
100 116
           }
101 117
           break
102 118
         }
103
-        case "a": {
119
+        case 'a': {
104 120
           if (metaKey(e)) {
105
-            state.send("SELECTED_ALL", getKeyboardEventInfo(e))
121
+            state.send('SELECTED_ALL', getKeyboardEventInfo(e))
106 122
           }
107 123
           break
108 124
         }
109
-        case "v": {
125
+        case 'v': {
110 126
           if (metaKey(e)) {
111
-            state.send("PASTED", getKeyboardEventInfo(e))
127
+            state.send('PASTED', getKeyboardEventInfo(e))
112 128
           } else {
113
-            state.send("SELECTED_SELECT_TOOL", getKeyboardEventInfo(e))
129
+            state.send('SELECTED_SELECT_TOOL', getKeyboardEventInfo(e))
114 130
           }
115 131
           break
116 132
         }
117
-        case "d": {
118
-          state.send("SELECTED_DRAW_TOOL", getKeyboardEventInfo(e))
133
+        case 'd': {
134
+          if (metaKey(e)) {
135
+            state.send('DUPLICATED', getKeyboardEventInfo(e))
136
+          } else {
137
+            state.send('SELECTED_DRAW_TOOL', getKeyboardEventInfo(e))
138
+          }
119 139
           break
120 140
         }
121
-        case "t": {
141
+        case 't': {
122 142
           if (metaKey(e)) {
123
-            state.send("DUPLICATED", getKeyboardEventInfo(e))
143
+            state.send('DUPLICATED', getKeyboardEventInfo(e))
124 144
           } else {
125
-            state.send("SELECTED_DOT_TOOL", getKeyboardEventInfo(e))
145
+            state.send('SELECTED_DOT_TOOL', getKeyboardEventInfo(e))
126 146
           }
127 147
           break
128 148
         }
129
-        case "c": {
149
+        case 'c': {
130 150
           if (metaKey(e)) {
131
-            state.send("COPIED", getKeyboardEventInfo(e))
151
+            state.send('COPIED', getKeyboardEventInfo(e))
132 152
           } else {
133
-            state.send("SELECTED_CIRCLE_TOOL", getKeyboardEventInfo(e))
153
+            state.send('SELECTED_CIRCLE_TOOL', getKeyboardEventInfo(e))
134 154
           }
135 155
           break
136 156
         }
137
-        case "i": {
157
+        case 'i': {
138 158
           if (metaKey(e)) {
139 159
           } else {
140
-            state.send("SELECTED_ELLIPSE_TOOL", getKeyboardEventInfo(e))
160
+            state.send('SELECTED_ELLIPSE_TOOL', getKeyboardEventInfo(e))
141 161
           }
142 162
           break
143 163
         }
144
-        case "l": {
164
+        case 'l': {
145 165
           if (metaKey(e)) {
146 166
           } else {
147
-            state.send("SELECTED_LINE_TOOL", getKeyboardEventInfo(e))
167
+            state.send('SELECTED_LINE_TOOL', getKeyboardEventInfo(e))
148 168
           }
149 169
           break
150 170
         }
151
-        case "y": {
171
+        case 'y': {
152 172
           if (metaKey(e)) {
153 173
           } else {
154
-            state.send("SELECTED_RAY_TOOL", getKeyboardEventInfo(e))
174
+            state.send('SELECTED_RAY_TOOL', getKeyboardEventInfo(e))
155 175
           }
156 176
           break
157 177
         }
158
-        case "p": {
178
+        case 'p': {
159 179
           if (metaKey(e)) {
160 180
           } else {
161
-            state.send("SELECTED_POLYLINE_TOOL", getKeyboardEventInfo(e))
181
+            state.send('SELECTED_POLYLINE_TOOL', getKeyboardEventInfo(e))
162 182
           }
163 183
           break
164 184
         }
165
-        case "r": {
185
+        case 'r': {
166 186
           if (metaKey(e)) {
167 187
           } else {
168
-            state.send("SELECTED_RECTANGLE_TOOL", getKeyboardEventInfo(e))
188
+            state.send('SELECTED_RECTANGLE_TOOL', getKeyboardEventInfo(e))
169 189
           }
170 190
           break
171 191
         }
172 192
         default: {
173
-          state.send("PRESSED_KEY", getKeyboardEventInfo(e))
193
+          state.send('PRESSED_KEY', getKeyboardEventInfo(e))
174 194
         }
175 195
       }
176 196
     }
177 197
 
178 198
     function handleKeyUp(e: KeyboardEvent) {
179
-      if (e.key === "Shift") {
180
-        state.send("RELEASED_SHIFT_KEY", getKeyboardEventInfo(e))
199
+      if (e.key === 'Shift') {
200
+        state.send('RELEASED_SHIFT_KEY', getKeyboardEventInfo(e))
181 201
       }
182 202
 
183
-      if (e.key === "Alt") {
184
-        state.send("RELEASED_ALT_KEY", getKeyboardEventInfo(e))
203
+      if (e.key === 'Alt') {
204
+        state.send('RELEASED_ALT_KEY', getKeyboardEventInfo(e))
185 205
       }
186 206
 
187
-      state.send("RELEASED_KEY", getKeyboardEventInfo(e))
207
+      state.send('RELEASED_KEY', getKeyboardEventInfo(e))
188 208
     }
189 209
 
190
-    document.body.addEventListener("keydown", handleKeyDown)
191
-    document.body.addEventListener("keyup", handleKeyUp)
210
+    document.body.addEventListener('keydown', handleKeyDown)
211
+    document.body.addEventListener('keyup', handleKeyUp)
192 212
     return () => {
193
-      document.body.removeEventListener("keydown", handleKeyDown)
194
-      document.body.removeEventListener("keyup", handleKeyUp)
213
+      document.body.removeEventListener('keydown', handleKeyDown)
214
+      document.body.removeEventListener('keyup', handleKeyUp)
195 215
     }
196 216
   }, [])
197 217
 }

+ 30
- 30
lib/colors.ts View File

@@ -1,38 +1,38 @@
1 1
 export const shades = {
2
-  transparent: "transparent",
3
-  white: "rgba(248, 249, 250, 1.000)",
4
-  lightGray: "rgba(224, 226, 230, 1.000)",
5
-  gray: "rgba(172, 181, 189, 1.000)",
6
-  darkGray: "rgba(52, 58, 64, 1.000)",
7
-  black: "rgba(0,0,0, 1.000)",
2
+  none: 'none',
3
+  white: 'rgba(248, 249, 250, 1.000)',
4
+  lightGray: 'rgba(224, 226, 230, 1.000)',
5
+  gray: 'rgba(172, 181, 189, 1.000)',
6
+  darkGray: 'rgba(52, 58, 64, 1.000)',
7
+  black: 'rgba(0,0,0, 1.000)',
8 8
 }
9 9
 
10 10
 export const strokes = {
11
-  lime: "rgba(115, 184, 23, 1.000)",
12
-  green: "rgba(54, 178, 77, 1.000)",
13
-  teal: "rgba(9, 167, 120, 1.000)",
14
-  cyan: "rgba(14, 152, 173, 1.000)",
15
-  blue: "rgba(28, 126, 214, 1.000)",
16
-  indigo: "rgba(66, 99, 235, 1.000)",
17
-  violet: "rgba(112, 72, 232, 1.000)",
18
-  grape: "rgba(174, 62, 200, 1.000)",
19
-  pink: "rgba(214, 51, 108, 1.000)",
20
-  red: "rgba(240, 63, 63, 1.000)",
21
-  orange: "rgba(247, 103, 6, 1.000)",
22
-  yellow: "rgba(245, 159, 0, 1.000)",
11
+  lime: 'rgba(115, 184, 23, 1.000)',
12
+  green: 'rgba(54, 178, 77, 1.000)',
13
+  teal: 'rgba(9, 167, 120, 1.000)',
14
+  cyan: 'rgba(14, 152, 173, 1.000)',
15
+  blue: 'rgba(28, 126, 214, 1.000)',
16
+  indigo: 'rgba(66, 99, 235, 1.000)',
17
+  violet: 'rgba(112, 72, 232, 1.000)',
18
+  grape: 'rgba(174, 62, 200, 1.000)',
19
+  pink: 'rgba(214, 51, 108, 1.000)',
20
+  red: 'rgba(240, 63, 63, 1.000)',
21
+  orange: 'rgba(247, 103, 6, 1.000)',
22
+  yellow: 'rgba(245, 159, 0, 1.000)',
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(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)',
38 38
 }

+ 22
- 15
lib/shape-utils/circle.tsx View File

@@ -1,11 +1,11 @@
1
-import { v4 as uuid } from "uuid"
2
-import * as vec from "utils/vec"
3
-import { CircleShape, ShapeType } from "types"
4
-import { registerShapeUtils } from "./index"
5
-import { boundsContained } from "utils/bounds"
6
-import { intersectCircleBounds } from "utils/intersections"
7
-import { pointInCircle } from "utils/hitTests"
8
-import { translateBounds } from "utils/utils"
1
+import { v4 as uuid } from 'uuid'
2
+import * as vec from 'utils/vec'
3
+import { CircleShape, ShapeType } from 'types'
4
+import { registerShapeUtils } from './index'
5
+import { boundsContained } from 'utils/bounds'
6
+import { intersectCircleBounds } from 'utils/intersections'
7
+import { pointInCircle } from 'utils/hitTests'
8
+import { translateBounds } from 'utils/utils'
9 9
 
10 10
 const circle = registerShapeUtils<CircleShape>({
11 11
   boundsCache: new WeakMap([]),
@@ -15,22 +15,29 @@ const circle = registerShapeUtils<CircleShape>({
15 15
       id: uuid(),
16 16
       type: ShapeType.Circle,
17 17
       isGenerated: false,
18
-      name: "Circle",
19
-      parentId: "page0",
18
+      name: 'Circle',
19
+      parentId: 'page0',
20 20
       childIndex: 0,
21 21
       point: [0, 0],
22 22
       rotation: 0,
23 23
       radius: 1,
24 24
       style: {
25
-        fill: "#c6cacb",
26
-        stroke: "#000",
25
+        fill: '#c6cacb',
26
+        stroke: '#000',
27 27
       },
28 28
       ...props,
29 29
     }
30 30
   },
31 31
 
32
-  render({ id, radius }) {
33
-    return <circle id={id} cx={radius} cy={radius} r={radius} />
32
+  render({ id, radius, style }) {
33
+    return (
34
+      <circle
35
+        id={id}
36
+        cx={radius}
37
+        cy={radius}
38
+        r={Math.max(0, radius - Number(style.strokeWidth) / 2)}
39
+      />
40
+    )
34 41
   },
35 42
 
36 43
   applyStyles(shape, style) {
@@ -92,7 +99,7 @@ const circle = registerShapeUtils<CircleShape>({
92 99
   },
93 100
 
94 101
   translateTo(shape, point) {
95
-    shape.point = point
102
+    shape.point = vec.toPrecision(point)
96 103
     return this
97 104
   },
98 105
 

+ 13
- 13
lib/shape-utils/dot.tsx View File

@@ -1,11 +1,11 @@
1
-import { v4 as uuid } from "uuid"
2
-import * as vec from "utils/vec"
3
-import { DotShape, ShapeType } from "types"
4
-import { registerShapeUtils } from "./index"
5
-import { boundsContained } from "utils/bounds"
6
-import { intersectCircleBounds } from "utils/intersections"
7
-import { DotCircle } from "components/canvas/misc"
8
-import { translateBounds } from "utils/utils"
1
+import { v4 as uuid } from 'uuid'
2
+import * as vec from 'utils/vec'
3
+import { DotShape, ShapeType } from 'types'
4
+import { registerShapeUtils } from './index'
5
+import { boundsContained } from 'utils/bounds'
6
+import { intersectCircleBounds } from 'utils/intersections'
7
+import { DotCircle } from 'components/canvas/misc'
8
+import { translateBounds } from 'utils/utils'
9 9
 
10 10
 const dot = registerShapeUtils<DotShape>({
11 11
   boundsCache: new WeakMap([]),
@@ -15,14 +15,14 @@ const dot = registerShapeUtils<DotShape>({
15 15
       id: uuid(),
16 16
       type: ShapeType.Dot,
17 17
       isGenerated: false,
18
-      name: "Dot",
19
-      parentId: "page0",
18
+      name: 'Dot',
19
+      parentId: 'page0',
20 20
       childIndex: 0,
21 21
       point: [0, 0],
22 22
       rotation: 0,
23 23
       style: {
24
-        fill: "#c6cacb",
25
-        strokeWidth: "0",
24
+        fill: '#c6cacb',
25
+        strokeWidth: '0',
26 26
       },
27 27
       ...props,
28 28
     }
@@ -79,7 +79,7 @@ const dot = registerShapeUtils<DotShape>({
79 79
   },
80 80
 
81 81
   translateTo(shape, point) {
82
-    shape.point = point
82
+    shape.point = vec.toPrecision(point)
83 83
     return this
84 84
   },
85 85
 

+ 10
- 4
lib/shape-utils/draw.tsx View File

@@ -34,13 +34,13 @@ const draw = registerShapeUtils<DrawShape>({
34 34
         strokeLinecap: 'round',
35 35
         strokeLinejoin: 'round',
36 36
         ...props.style,
37
-        stroke: 'transparent',
37
+        fill: props.style.stroke,
38 38
       },
39 39
     }
40 40
   },
41 41
 
42 42
   render(shape) {
43
-    const { id, point, points } = shape
43
+    const { id, point, points, style } = shape
44 44
 
45 45
     if (!pathCache.has(points)) {
46 46
       if (points.length < 2) {
@@ -51,7 +51,12 @@ const draw = registerShapeUtils<DrawShape>({
51 51
         }
52 52
         pathCache.set(points, getSvgPathFromStroke(d))
53 53
       } else {
54
-        pathCache.set(points, getSvgPathFromStroke(getStroke(points)))
54
+        pathCache.set(
55
+          points,
56
+          getSvgPathFromStroke(
57
+            getStroke(points, { size: Number(style.strokeWidth) * 2 })
58
+          )
59
+        )
55 60
       }
56 61
     }
57 62
 
@@ -60,6 +65,7 @@ const draw = registerShapeUtils<DrawShape>({
60 65
 
61 66
   applyStyles(shape, style) {
62 67
     Object.assign(shape.style, style)
68
+    shape.style.fill = shape.style.stroke
63 69
     return this
64 70
   },
65 71
 
@@ -128,7 +134,7 @@ const draw = registerShapeUtils<DrawShape>({
128 134
   },
129 135
 
130 136
   translateTo(shape, point) {
131
-    shape.point = point
137
+    shape.point = vec.toPrecision(point)
132 138
     return this
133 139
   },
134 140
 

+ 21
- 15
lib/shape-utils/ellipse.tsx View File

@@ -1,16 +1,16 @@
1
-import { v4 as uuid } from "uuid"
2
-import * as vec from "utils/vec"
3
-import { EllipseShape, ShapeType } from "types"
4
-import { registerShapeUtils } from "./index"
5
-import { boundsContained, getRotatedEllipseBounds } from "utils/bounds"
6
-import { intersectEllipseBounds } from "utils/intersections"
7
-import { pointInEllipse } from "utils/hitTests"
1
+import { v4 as uuid } from 'uuid'
2
+import * as vec from 'utils/vec'
3
+import { EllipseShape, ShapeType } from 'types'
4
+import { registerShapeUtils } from './index'
5
+import { boundsContained, getRotatedEllipseBounds } from 'utils/bounds'
6
+import { intersectEllipseBounds } from 'utils/intersections'
7
+import { pointInEllipse } from 'utils/hitTests'
8 8
 import {
9 9
   getBoundsFromPoints,
10 10
   getRotatedCorners,
11 11
   rotateBounds,
12 12
   translateBounds,
13
-} from "utils/utils"
13
+} from 'utils/utils'
14 14
 
15 15
 const ellipse = registerShapeUtils<EllipseShape>({
16 16
   boundsCache: new WeakMap([]),
@@ -20,24 +20,30 @@ const ellipse = registerShapeUtils<EllipseShape>({
20 20
       id: uuid(),
21 21
       type: ShapeType.Ellipse,
22 22
       isGenerated: false,
23
-      name: "Ellipse",
24
-      parentId: "page0",
23
+      name: 'Ellipse',
24
+      parentId: 'page0',
25 25
       childIndex: 0,
26 26
       point: [0, 0],
27 27
       radiusX: 1,
28 28
       radiusY: 1,
29 29
       rotation: 0,
30 30
       style: {
31
-        fill: "#c6cacb",
32
-        stroke: "#000",
31
+        fill: '#c6cacb',
32
+        stroke: '#000',
33 33
       },
34 34
       ...props,
35 35
     }
36 36
   },
37 37
 
38
-  render({ id, radiusX, radiusY }) {
38
+  render({ id, radiusX, radiusY, style }) {
39 39
     return (
40
-      <ellipse id={id} cx={radiusX} cy={radiusY} rx={radiusX} ry={radiusY} />
40
+      <ellipse
41
+        id={id}
42
+        cx={radiusX}
43
+        cy={radiusY}
44
+        rx={Math.max(0, radiusX - Number(style.strokeWidth) / 2)}
45
+        ry={Math.max(0, radiusY - Number(style.strokeWidth) / 2)}
46
+      />
41 47
     )
42 48
   },
43 49
 
@@ -110,7 +116,7 @@ const ellipse = registerShapeUtils<EllipseShape>({
110 116
   },
111 117
 
112 118
   translateTo(shape, point) {
113
-    shape.point = point
119
+    shape.point = vec.toPrecision(point)
114 120
     return this
115 121
   },
116 122
 

+ 13
- 13
lib/shape-utils/line.tsx View File

@@ -1,11 +1,11 @@
1
-import { v4 as uuid } from "uuid"
2
-import * as vec from "utils/vec"
3
-import { LineShape, ShapeType } from "types"
4
-import { registerShapeUtils } from "./index"
5
-import { boundsContained } from "utils/bounds"
6
-import { intersectCircleBounds } from "utils/intersections"
7
-import { DotCircle } from "components/canvas/misc"
8
-import { translateBounds } from "utils/utils"
1
+import { v4 as uuid } from 'uuid'
2
+import * as vec from 'utils/vec'
3
+import { LineShape, ShapeType } from 'types'
4
+import { registerShapeUtils } from './index'
5
+import { boundsContained } from 'utils/bounds'
6
+import { intersectCircleBounds } from 'utils/intersections'
7
+import { DotCircle } from 'components/canvas/misc'
8
+import { translateBounds } from 'utils/utils'
9 9
 
10 10
 const line = registerShapeUtils<LineShape>({
11 11
   boundsCache: new WeakMap([]),
@@ -15,15 +15,15 @@ const line = registerShapeUtils<LineShape>({
15 15
       id: uuid(),
16 16
       type: ShapeType.Line,
17 17
       isGenerated: false,
18
-      name: "Line",
19
-      parentId: "page0",
18
+      name: 'Line',
19
+      parentId: 'page0',
20 20
       childIndex: 0,
21 21
       point: [0, 0],
22 22
       direction: [0, 0],
23 23
       rotation: 0,
24 24
       style: {
25
-        fill: "#c6cacb",
26
-        stroke: "#000",
25
+        fill: '#c6cacb',
26
+        stroke: '#000',
27 27
       },
28 28
       ...props,
29 29
     }
@@ -88,7 +88,7 @@ const line = registerShapeUtils<LineShape>({
88 88
   },
89 89
 
90 90
   translateTo(shape, point) {
91
-    shape.point = point
91
+    shape.point = vec.toPrecision(point)
92 92
     return this
93 93
   },
94 94
 

+ 12
- 12
lib/shape-utils/polyline.tsx View File

@@ -1,10 +1,10 @@
1
-import { v4 as uuid } from "uuid"
2
-import * as vec from "utils/vec"
3
-import { PolylineShape, ShapeType } from "types"
4
-import { registerShapeUtils } from "./index"
5
-import { intersectPolylineBounds } from "utils/intersections"
6
-import { boundsContainPolygon } from "utils/bounds"
7
-import { getBoundsFromPoints, translateBounds } from "utils/utils"
1
+import { v4 as uuid } from 'uuid'
2
+import * as vec from 'utils/vec'
3
+import { PolylineShape, ShapeType } from 'types'
4
+import { registerShapeUtils } from './index'
5
+import { intersectPolylineBounds } from 'utils/intersections'
6
+import { boundsContainPolygon } from 'utils/bounds'
7
+import { getBoundsFromPoints, translateBounds } from 'utils/utils'
8 8
 
9 9
 const polyline = registerShapeUtils<PolylineShape>({
10 10
   boundsCache: new WeakMap([]),
@@ -14,16 +14,16 @@ const polyline = registerShapeUtils<PolylineShape>({
14 14
       id: uuid(),
15 15
       type: ShapeType.Polyline,
16 16
       isGenerated: false,
17
-      name: "Polyline",
18
-      parentId: "page0",
17
+      name: 'Polyline',
18
+      parentId: 'page0',
19 19
       childIndex: 0,
20 20
       point: [0, 0],
21 21
       points: [[0, 0]],
22 22
       rotation: 0,
23 23
       style: {
24 24
         strokeWidth: 2,
25
-        strokeLinecap: "round",
26
-        strokeLinejoin: "round",
25
+        strokeLinecap: 'round',
26
+        strokeLinejoin: 'round',
27 27
       },
28 28
       ...props,
29 29
     }
@@ -97,7 +97,7 @@ const polyline = registerShapeUtils<PolylineShape>({
97 97
   },
98 98
 
99 99
   translateTo(shape, point) {
100
-    shape.point = point
100
+    shape.point = vec.toPrecision(point)
101 101
     return this
102 102
   },
103 103
 

+ 13
- 13
lib/shape-utils/ray.tsx View File

@@ -1,11 +1,11 @@
1
-import { v4 as uuid } from "uuid"
2
-import * as vec from "utils/vec"
3
-import { RayShape, ShapeType } from "types"
4
-import { registerShapeUtils } from "./index"
5
-import { boundsContained } from "utils/bounds"
6
-import { intersectCircleBounds } from "utils/intersections"
7
-import { DotCircle } from "components/canvas/misc"
8
-import { translateBounds } from "utils/utils"
1
+import { v4 as uuid } from 'uuid'
2
+import * as vec from 'utils/vec'
3
+import { RayShape, ShapeType } from 'types'
4
+import { registerShapeUtils } from './index'
5
+import { boundsContained } from 'utils/bounds'
6
+import { intersectCircleBounds } from 'utils/intersections'
7
+import { DotCircle } from 'components/canvas/misc'
8
+import { translateBounds } from 'utils/utils'
9 9
 
10 10
 const ray = registerShapeUtils<RayShape>({
11 11
   boundsCache: new WeakMap([]),
@@ -15,15 +15,15 @@ const ray = registerShapeUtils<RayShape>({
15 15
       id: uuid(),
16 16
       type: ShapeType.Ray,
17 17
       isGenerated: false,
18
-      name: "Ray",
19
-      parentId: "page0",
18
+      name: 'Ray',
19
+      parentId: 'page0',
20 20
       childIndex: 0,
21 21
       point: [0, 0],
22 22
       direction: [0, 1],
23 23
       rotation: 0,
24 24
       style: {
25
-        fill: "#c6cacb",
26
-        stroke: "#000",
25
+        fill: '#c6cacb',
26
+        stroke: '#000',
27 27
         strokeWidth: 1,
28 28
       },
29 29
       ...props,
@@ -88,7 +88,7 @@ const ray = registerShapeUtils<RayShape>({
88 88
   },
89 89
 
90 90
   translateTo(shape, point) {
91
-    shape.point = point
91
+    shape.point = vec.toPrecision(point)
92 92
     return this
93 93
   },
94 94
 

+ 14
- 25
lib/shape-utils/rectangle.tsx View File

@@ -1,13 +1,13 @@
1
-import { v4 as uuid } from "uuid"
2
-import * as vec from "utils/vec"
3
-import { RectangleShape, ShapeType } from "types"
4
-import { registerShapeUtils } from "./index"
5
-import { boundsCollidePolygon, boundsContainPolygon } from "utils/bounds"
1
+import { v4 as uuid } from 'uuid'
2
+import * as vec from 'utils/vec'
3
+import { RectangleShape, ShapeType } from 'types'
4
+import { registerShapeUtils } from './index'
5
+import { boundsCollidePolygon, boundsContainPolygon } from 'utils/bounds'
6 6
 import {
7 7
   getBoundsFromPoints,
8 8
   getRotatedCorners,
9 9
   translateBounds,
10
-} from "utils/utils"
10
+} from 'utils/utils'
11 11
 
12 12
 const rectangle = registerShapeUtils<RectangleShape>({
13 13
   boundsCache: new WeakMap([]),
@@ -17,42 +17,31 @@ const rectangle = registerShapeUtils<RectangleShape>({
17 17
       id: uuid(),
18 18
       type: ShapeType.Rectangle,
19 19
       isGenerated: false,
20
-      name: "Rectangle",
21
-      parentId: "page0",
20
+      name: 'Rectangle',
21
+      parentId: 'page0',
22 22
       childIndex: 0,
23 23
       point: [0, 0],
24 24
       size: [1, 1],
25 25
       radius: 2,
26 26
       rotation: 0,
27 27
       style: {
28
-        fill: "#c6cacb",
29
-        stroke: "#000",
28
+        fill: '#c6cacb',
29
+        stroke: '#000',
30 30
       },
31 31
       ...props,
32 32
     }
33 33
   },
34 34
 
35
-  render({ id, size, radius, childIndex }) {
35
+  render({ id, size, radius, style }) {
36 36
     return (
37 37
       <g id={id}>
38 38
         <rect
39 39
           id={id}
40
-          width={size[0]}
41
-          height={size[1]}
42 40
           rx={radius}
43 41
           ry={radius}
42
+          width={Math.max(0, size[0] - Number(style.strokeWidth) / 2)}
43
+          height={Math.max(0, size[1] - Number(style.strokeWidth) / 2)}
44 44
         />
45
-        <text
46
-          y={4}
47
-          x={4}
48
-          fontSize={18}
49
-          fill="black"
50
-          stroke="none"
51
-          alignmentBaseline="text-before-edge"
52
-          pointerEvents="none"
53
-        >
54
-          {childIndex}
55
-        </text>
56 45
       </g>
57 46
     )
58 47
   },
@@ -113,7 +102,7 @@ const rectangle = registerShapeUtils<RectangleShape>({
113 102
   },
114 103
 
115 104
   translateTo(shape, point) {
116
-    shape.point = point
105
+    shape.point = vec.toPrecision(point)
117 106
     return this
118 107
   },
119 108
 

+ 1
- 0
package.json View File

@@ -11,6 +11,7 @@
11 11
     "@monaco-editor/react": "^4.1.3",
12 12
     "@radix-ui/react-dropdown-menu": "^0.0.19",
13 13
     "@radix-ui/react-icons": "^1.0.3",
14
+    "@radix-ui/react-radio-group": "^0.0.16",
14 15
     "@state-designer/react": "^1.7.1",
15 16
     "@stitches/react": "^0.1.9",
16 17
     "framer-motion": "^4.1.16",

+ 48
- 0
state/commands/duplicate.ts View File

@@ -0,0 +1,48 @@
1
+import Command from './command'
2
+import history from '../history'
3
+import { Data } from 'types'
4
+import { getPage, getSelectedShapes } from 'utils/utils'
5
+import { v4 as uuid } from 'uuid'
6
+import { current } from 'immer'
7
+import * as vec from 'utils/vec'
8
+
9
+export default function duplicateCommand(data: Data) {
10
+  const { currentPageId } = data
11
+  const selectedShapes = getSelectedShapes(current(data))
12
+  const duplicates = selectedShapes.map((shape) => ({
13
+    ...shape,
14
+    id: uuid(),
15
+    point: vec.add(shape.point, vec.div([16, 16], data.camera.zoom)),
16
+  }))
17
+
18
+  history.execute(
19
+    data,
20
+    new Command({
21
+      name: 'duplicate_shapes',
22
+      category: 'canvas',
23
+      manualSelection: true,
24
+      do(data) {
25
+        const { shapes } = getPage(data, currentPageId)
26
+
27
+        data.selectedIds.clear()
28
+
29
+        for (const duplicate of duplicates) {
30
+          shapes[duplicate.id] = duplicate
31
+          data.selectedIds.add(duplicate.id)
32
+        }
33
+      },
34
+      undo(data) {
35
+        const { shapes } = getPage(data, currentPageId)
36
+        data.selectedIds.clear()
37
+
38
+        for (const duplicate of duplicates) {
39
+          delete shapes[duplicate.id]
40
+        }
41
+
42
+        for (let id in selectedShapes) {
43
+          data.selectedIds.add(id)
44
+        }
45
+      },
46
+    })
47
+  )
48
+}

+ 17
- 13
state/commands/index.ts View File

@@ -1,22 +1,25 @@
1
-import align from "./align"
2
-import deleteSelected from "./delete-selected"
3
-import direct from "./direct"
4
-import distribute from "./distribute"
5
-import generate from "./generate"
6
-import move from "./move"
7
-import draw from "./draw"
8
-import rotate from "./rotate"
9
-import stretch from "./stretch"
10
-import style from "./style"
11
-import transform from "./transform"
12
-import transformSingle from "./transform-single"
13
-import translate from "./translate"
1
+import align from './align'
2
+import deleteSelected from './delete-selected'
3
+import direct from './direct'
4
+import distribute from './distribute'
5
+import duplicate from './duplicate'
6
+import generate from './generate'
7
+import move from './move'
8
+import draw from './draw'
9
+import rotate from './rotate'
10
+import stretch from './stretch'
11
+import style from './style'
12
+import transform from './transform'
13
+import transformSingle from './transform-single'
14
+import translate from './translate'
15
+import nudge from './nudge'
14 16
 
15 17
 const commands = {
16 18
   align,
17 19
   deleteSelected,
18 20
   direct,
19 21
   distribute,
22
+  duplicate,
20 23
   generate,
21 24
   move,
22 25
   draw,
@@ -26,6 +29,7 @@ const commands = {
26 29
   transform,
27 30
   transformSingle,
28 31
   translate,
32
+  nudge,
29 33
 }
30 34
 
31 35
 export default commands

+ 40
- 0
state/commands/nudge.ts View File

@@ -0,0 +1,40 @@
1
+import Command from './command'
2
+import history from '../history'
3
+import { Data } from 'types'
4
+import { getPage, getSelectedShapes } from 'utils/utils'
5
+import { getShapeUtils } from 'lib/shape-utils'
6
+import * as vec from 'utils/vec'
7
+
8
+export default function nudgeCommand(data: Data, delta: number[]) {
9
+  const { currentPageId } = data
10
+  const selectedShapes = getSelectedShapes(data)
11
+  const shapeBounds = Object.fromEntries(
12
+    selectedShapes.map(
13
+      (shape) => [shape.id, getShapeUtils(shape).getBounds(shape)] as const
14
+    )
15
+  )
16
+
17
+  history.execute(
18
+    data,
19
+    new Command({
20
+      name: 'set_direction',
21
+      category: 'canvas',
22
+      do(data) {
23
+        const { shapes } = getPage(data, currentPageId)
24
+
25
+        for (let id in shapeBounds) {
26
+          const shape = shapes[id]
27
+          getShapeUtils(shape).translateTo(shape, vec.add(shape.point, delta))
28
+        }
29
+      },
30
+      undo(data) {
31
+        const { shapes } = getPage(data, currentPageId)
32
+
33
+        for (let id in shapeBounds) {
34
+          const shape = shapes[id]
35
+          getShapeUtils(shape).translateTo(shape, vec.sub(shape.point, delta))
36
+        }
37
+      },
38
+    })
39
+  )
40
+}

+ 10
- 10
state/commands/transform-single.ts View File

@@ -1,10 +1,10 @@
1
-import Command from "./command"
2
-import history from "../history"
3
-import { Data, Corner, Edge } from "types"
4
-import { getShapeUtils } from "lib/shape-utils"
5
-import { current } from "immer"
6
-import { TransformSingleSnapshot } from "state/sessions/transform-single-session"
7
-import { getPage } from "utils/utils"
1
+import Command from './command'
2
+import history from '../history'
3
+import { Data, Corner, Edge } from 'types'
4
+import { getShapeUtils } from 'lib/shape-utils'
5
+import { current } from 'immer'
6
+import { TransformSingleSnapshot } from 'state/sessions/transform-single-session'
7
+import { getPage } from 'utils/utils'
8 8
 
9 9
 export default function transformSingleCommand(
10 10
   data: Data,
@@ -14,13 +14,13 @@ export default function transformSingleCommand(
14 14
   scaleY: number,
15 15
   isCreating: boolean
16 16
 ) {
17
-  const shape = getPage(data, after.currentPageId).shapes[after.id]
17
+  const shape = current(getPage(data, after.currentPageId).shapes[after.id])
18 18
 
19 19
   history.execute(
20 20
     data,
21 21
     new Command({
22
-      name: "transform_single_shape",
23
-      category: "canvas",
22
+      name: 'transform_single_shape',
23
+      category: 'canvas',
24 24
       manualSelection: true,
25 25
       do(data) {
26 26
         const { id, type, initialShape, initialShapeBounds } = after

+ 260
- 196
state/state.ts View File

@@ -41,10 +41,15 @@ const initialData: Data = {
41 41
     isDarkMode: false,
42 42
     isCodeOpen: false,
43 43
     isStyleOpen: false,
44
+    isToolLocked: false,
45
+    isPenLocked: false,
46
+    nudgeDistanceLarge: 10,
47
+    nudgeDistanceSmall: 1,
44 48
   },
45 49
   currentStyle: {
46 50
     fill: shades.lightGray,
47 51
     stroke: shades.darkGray,
52
+    strokeWidth: 2,
48 53
   },
49 54
   camera: {
50 55
     point: [0, 0],
@@ -94,6 +99,9 @@ const state = createState({
94 99
       else: 'zoomCameraToActual',
95 100
     },
96 101
     SELECTED_ALL: { to: 'selecting', do: 'selectAll' },
102
+    NUDGED: { do: 'nudgeSelection' },
103
+    USED_PEN_DEVICE: 'enablePenLock',
104
+    DISABLED_PEN_LOCK: 'disablePenLock',
97 105
   },
98 106
   initial: 'loading',
99 107
   states: {
@@ -124,20 +132,22 @@ const state = createState({
124 132
         selecting: {
125 133
           on: {
126 134
             SAVED: 'forceSave',
127
-            UNDO: { do: 'undo' },
128
-            REDO: { do: 'redo' },
129
-            CANCELLED: { do: 'clearSelectedIds' },
130
-            DELETED: { do: 'deleteSelectedIds' },
135
+            UNDO: 'undo',
136
+            REDO: 'redo',
131 137
             SAVED_CODE: 'saveCode',
132
-            GENERATED_FROM_CODE: ['setCodeControls', 'setGeneratedShapes'],
138
+            CANCELLED: 'clearSelectedIds',
139
+            DELETED: 'deleteSelectedIds',
140
+            STARTED_PINCHING: { to: 'pinching' },
133 141
             INCREASED_CODE_FONT_SIZE: 'increaseCodeFontSize',
134 142
             DECREASED_CODE_FONT_SIZE: 'decreaseCodeFontSize',
135 143
             CHANGED_CODE_CONTROL: 'updateControls',
136
-            ALIGNED: 'alignSelection',
137
-            STRETCHED: 'stretchSelection',
138
-            DISTRIBUTED: 'distributeSelection',
139
-            MOVED: 'moveSelection',
140
-            STARTED_PINCHING: { to: 'pinching' },
144
+            GENERATED_FROM_CODE: ['setCodeControls', 'setGeneratedShapes'],
145
+            TOGGLED_TOOL_LOCK: 'toggleToolLock',
146
+            MOVED: { if: 'hasSelection', do: 'moveSelection' },
147
+            ALIGNED: { if: 'hasSelection', do: 'alignSelection' },
148
+            STRETCHED: { if: 'hasSelection', do: 'stretchSelection' },
149
+            DISTRIBUTED: { if: 'hasSelection', do: 'distributeSelection' },
150
+            DUPLICATED: { if: 'hasSelection', do: 'duplicateSelection' },
141 151
           },
142 152
           initial: 'notPointing',
143 153
           states: {
@@ -262,241 +272,259 @@ const state = createState({
262 272
             PINCHED: { do: 'pinchCamera' },
263 273
           },
264 274
         },
265
-        draw: {
266
-          initial: 'creating',
275
+        usingTool: {
276
+          initial: 'draw',
267 277
           states: {
268
-            creating: {
269
-              on: {
270
-                CANCELLED: { to: 'selecting' },
271
-                POINTED_CANVAS: {
272
-                  get: 'newDraw',
273
-                  do: 'createShape',
274
-                  to: 'draw.editing',
278
+            draw: {
279
+              initial: 'creating',
280
+              states: {
281
+                creating: {
282
+                  on: {
283
+                    CANCELLED: { to: 'selecting' },
284
+                    POINTED_CANVAS: {
285
+                      get: 'newDraw',
286
+                      do: 'createShape',
287
+                      to: 'draw.editing',
288
+                    },
289
+                    UNDO: { do: 'undo' },
290
+                    REDO: { do: 'redo' },
291
+                  },
292
+                },
293
+                editing: {
294
+                  onEnter: 'startDrawSession',
295
+                  on: {
296
+                    STOPPED_POINTING: {
297
+                      do: 'completeSession',
298
+                      to: 'draw.creating',
299
+                    },
300
+                    CANCELLED: {
301
+                      do: ['cancelSession', 'deleteSelectedIds'],
302
+                      to: 'selecting',
303
+                    },
304
+                    MOVED_POINTER: 'updateDrawSession',
305
+                    PANNED_CAMERA: 'updateDrawSession',
306
+                  },
275 307
                 },
276
-                UNDO: { do: 'undo' },
277
-                REDO: { do: 'redo' },
278 308
               },
279 309
             },
280
-            editing: {
281
-              onEnter: 'startDrawSession',
282
-              on: {
283
-                STOPPED_POINTING: {
284
-                  do: 'completeSession',
285
-                  to: 'draw.creating',
310
+            dot: {
311
+              initial: 'creating',
312
+              states: {
313
+                creating: {
314
+                  on: {
315
+                    CANCELLED: { to: 'selecting' },
316
+                    POINTED_CANVAS: {
317
+                      get: 'newDot',
318
+                      do: 'createShape',
319
+                      to: 'dot.editing',
320
+                    },
321
+                  },
286 322
                 },
287
-                CANCELLED: {
288
-                  do: ['cancelSession', 'deleteSelectedIds'],
289
-                  to: 'selecting',
323
+                editing: {
324
+                  on: {
325
+                    STOPPED_POINTING: [
326
+                      'completeSession',
327
+                      {
328
+                        if: 'isToolLocked',
329
+                        to: 'dot.creating',
330
+                        else: {
331
+                          to: 'selecting',
332
+                        },
333
+                      },
334
+                    ],
335
+                    CANCELLED: {
336
+                      do: ['cancelSession', 'deleteSelectedIds'],
337
+                      to: 'selecting',
338
+                    },
339
+                  },
340
+                  initial: 'inactive',
341
+                  states: {
342
+                    inactive: {
343
+                      on: {
344
+                        MOVED_POINTER: {
345
+                          if: 'distanceImpliesDrag',
346
+                          to: 'dot.editing.active',
347
+                        },
348
+                      },
349
+                    },
350
+                    active: {
351
+                      onEnter: 'startTranslateSession',
352
+                      on: {
353
+                        MOVED_POINTER: 'updateTranslateSession',
354
+                        PANNED_CAMERA: 'updateTranslateSession',
355
+                      },
356
+                    },
357
+                  },
290 358
                 },
291
-                MOVED_POINTER: 'updateDrawSession',
292
-                PANNED_CAMERA: 'updateDrawSession',
293 359
               },
294 360
             },
295
-          },
296
-        },
297
-        dot: {
298
-          initial: 'creating',
299
-          states: {
300
-            creating: {
301
-              on: {
302
-                CANCELLED: { to: 'selecting' },
303
-                POINTED_CANVAS: {
304
-                  get: 'newDot',
305
-                  do: 'createShape',
306
-                  to: 'dot.editing',
361
+            circle: {
362
+              initial: 'creating',
363
+              states: {
364
+                creating: {
365
+                  on: {
366
+                    CANCELLED: { to: 'selecting' },
367
+                    POINTED_CANVAS: {
368
+                      to: 'circle.editing',
369
+                    },
370
+                  },
307 371
                 },
308
-              },
309
-            },
310
-            editing: {
311
-              on: {
312
-                STOPPED_POINTING: { do: 'completeSession', to: 'selecting' },
313
-                CANCELLED: {
314
-                  do: ['cancelSession', 'deleteSelectedIds'],
315
-                  to: 'selecting',
372
+                editing: {
373
+                  on: {
374
+                    STOPPED_POINTING: { to: 'selecting' },
375
+                    CANCELLED: { to: 'selecting' },
376
+                    MOVED_POINTER: {
377
+                      if: 'distanceImpliesDrag',
378
+                      then: {
379
+                        get: 'newCircle',
380
+                        do: 'createShape',
381
+                        to: 'drawingShape.bounds',
382
+                      },
383
+                    },
384
+                  },
316 385
                 },
317 386
               },
318
-              initial: 'inactive',
387
+            },
388
+            ellipse: {
389
+              initial: 'creating',
319 390
               states: {
320
-                inactive: {
391
+                creating: {
321 392
                   on: {
322
-                    MOVED_POINTER: {
323
-                      if: 'distanceImpliesDrag',
324
-                      to: 'dot.editing.active',
393
+                    CANCELLED: { to: 'selecting' },
394
+                    POINTED_CANVAS: {
395
+                      to: 'ellipse.editing',
325 396
                     },
326 397
                   },
327 398
                 },
328
-                active: {
329
-                  onEnter: 'startTranslateSession',
399
+                editing: {
330 400
                   on: {
331
-                    MOVED_POINTER: 'updateTranslateSession',
332
-                    PANNED_CAMERA: 'updateTranslateSession',
401
+                    STOPPED_POINTING: { to: 'selecting' },
402
+                    CANCELLED: { to: 'selecting' },
403
+                    MOVED_POINTER: {
404
+                      if: 'distanceImpliesDrag',
405
+                      then: {
406
+                        get: 'newEllipse',
407
+                        do: 'createShape',
408
+                        to: 'drawingShape.bounds',
409
+                      },
410
+                    },
333 411
                   },
334 412
                 },
335 413
               },
336 414
             },
337
-          },
338
-        },
339
-        circle: {
340
-          initial: 'creating',
341
-          states: {
342
-            creating: {
343
-              on: {
344
-                CANCELLED: { to: 'selecting' },
345
-                POINTED_CANVAS: {
346
-                  to: 'circle.editing',
415
+            rectangle: {
416
+              initial: 'creating',
417
+              states: {
418
+                creating: {
419
+                  on: {
420
+                    CANCELLED: { to: 'selecting' },
421
+                    POINTED_CANVAS: {
422
+                      to: 'rectangle.editing',
423
+                    },
424
+                  },
347 425
                 },
348
-              },
349
-            },
350
-            editing: {
351
-              on: {
352
-                STOPPED_POINTING: { to: 'selecting' },
353
-                CANCELLED: { to: 'selecting' },
354
-                MOVED_POINTER: {
355
-                  if: 'distanceImpliesDrag',
356
-                  then: {
357
-                    get: 'newCircle',
358
-                    do: 'createShape',
359
-                    to: 'drawingShape.bounds',
426
+                editing: {
427
+                  on: {
428
+                    STOPPED_POINTING: { to: 'selecting' },
429
+                    CANCELLED: { to: 'selecting' },
430
+                    MOVED_POINTER: {
431
+                      if: 'distanceImpliesDrag',
432
+                      then: {
433
+                        get: 'newRectangle',
434
+                        do: 'createShape',
435
+                        to: 'drawingShape.bounds',
436
+                      },
437
+                    },
360 438
                   },
361 439
                 },
362 440
               },
363 441
             },
364
-          },
365
-        },
366
-        ellipse: {
367
-          initial: 'creating',
368
-          states: {
369
-            creating: {
370
-              on: {
371
-                CANCELLED: { to: 'selecting' },
372
-                POINTED_CANVAS: {
373
-                  to: 'ellipse.editing',
442
+            ray: {
443
+              initial: 'creating',
444
+              states: {
445
+                creating: {
446
+                  on: {
447
+                    CANCELLED: { to: 'selecting' },
448
+                    POINTED_CANVAS: {
449
+                      get: 'newRay',
450
+                      do: 'createShape',
451
+                      to: 'ray.editing',
452
+                    },
453
+                  },
374 454
                 },
375
-              },
376
-            },
377
-            editing: {
378
-              on: {
379
-                STOPPED_POINTING: { to: 'selecting' },
380
-                CANCELLED: { to: 'selecting' },
381
-                MOVED_POINTER: {
382
-                  if: 'distanceImpliesDrag',
383
-                  then: {
384
-                    get: 'newEllipse',
385
-                    do: 'createShape',
386
-                    to: 'drawingShape.bounds',
455
+                editing: {
456
+                  on: {
457
+                    STOPPED_POINTING: { to: 'selecting' },
458
+                    CANCELLED: { to: 'selecting' },
459
+                    MOVED_POINTER: {
460
+                      if: 'distanceImpliesDrag',
461
+                      to: 'drawingShape.direction',
462
+                    },
387 463
                   },
388 464
                 },
389 465
               },
390 466
             },
391
-          },
392
-        },
393
-        rectangle: {
394
-          initial: 'creating',
395
-          states: {
396
-            creating: {
397
-              on: {
398
-                CANCELLED: { to: 'selecting' },
399
-                POINTED_CANVAS: {
400
-                  to: 'rectangle.editing',
467
+            line: {
468
+              initial: 'creating',
469
+              states: {
470
+                creating: {
471
+                  on: {
472
+                    CANCELLED: { to: 'selecting' },
473
+                    POINTED_CANVAS: {
474
+                      get: 'newLine',
475
+                      do: 'createShape',
476
+                      to: 'line.editing',
477
+                    },
478
+                  },
401 479
                 },
402
-              },
403
-            },
404
-            editing: {
405
-              on: {
406
-                STOPPED_POINTING: { to: 'selecting' },
407
-                CANCELLED: { to: 'selecting' },
408
-                MOVED_POINTER: {
409
-                  if: 'distanceImpliesDrag',
410
-                  then: {
411
-                    get: 'newRectangle',
412
-                    do: 'createShape',
413
-                    to: 'drawingShape.bounds',
480
+                editing: {
481
+                  on: {
482
+                    STOPPED_POINTING: { to: 'selecting' },
483
+                    CANCELLED: { to: 'selecting' },
484
+                    MOVED_POINTER: {
485
+                      if: 'distanceImpliesDrag',
486
+                      to: 'drawingShape.direction',
487
+                    },
414 488
                   },
415 489
                 },
416 490
               },
417 491
             },
492
+            polyline: {},
418 493
           },
419 494
         },
420
-        ray: {
421
-          initial: 'creating',
422
-          states: {
423
-            creating: {
424
-              on: {
425
-                CANCELLED: { to: 'selecting' },
426
-                POINTED_CANVAS: {
427
-                  get: 'newRay',
428
-                  do: 'createShape',
429
-                  to: 'ray.editing',
430
-                },
431
-              },
432
-            },
433
-            editing: {
434
-              on: {
435
-                STOPPED_POINTING: { to: 'selecting' },
436
-                CANCELLED: { to: 'selecting' },
437
-                MOVED_POINTER: {
438
-                  if: 'distanceImpliesDrag',
439
-                  to: 'drawingShape.direction',
440
-                },
495
+        drawingShape: {
496
+          on: {
497
+            STOPPED_POINTING: [
498
+              'completeSession',
499
+              {
500
+                if: 'isToolLocked',
501
+                to: 'usingTool.previous',
502
+                else: { to: 'selecting' },
441 503
               },
504
+            ],
505
+            CANCELLED: {
506
+              do: ['cancelSession', 'deleteSelectedIds'],
507
+              to: 'selecting',
442 508
             },
443 509
           },
444
-        },
445
-        line: {
446
-          initial: 'creating',
510
+          initial: 'drawingShapeBounds',
447 511
           states: {
448
-            creating: {
512
+            bounds: {
513
+              onEnter: 'startDrawTransformSession',
449 514
               on: {
450
-                CANCELLED: { to: 'selecting' },
451
-                POINTED_CANVAS: {
452
-                  get: 'newLine',
453
-                  do: 'createShape',
454
-                  to: 'line.editing',
455
-                },
515
+                MOVED_POINTER: 'updateTransformSession',
516
+                PANNED_CAMERA: 'updateTransformSession',
456 517
               },
457 518
             },
458
-            editing: {
519
+            direction: {
520
+              onEnter: 'startDirectionSession',
459 521
               on: {
460
-                STOPPED_POINTING: { to: 'selecting' },
461
-                CANCELLED: { to: 'selecting' },
462
-                MOVED_POINTER: {
463
-                  if: 'distanceImpliesDrag',
464
-                  to: 'drawingShape.direction',
465
-                },
522
+                MOVED_POINTER: 'updateDirectionSession',
523
+                PANNED_CAMERA: 'updateDirectionSession',
466 524
               },
467 525
             },
468 526
           },
469 527
         },
470
-        polyline: {},
471
-      },
472
-    },
473
-    drawingShape: {
474
-      on: {
475
-        STOPPED_POINTING: {
476
-          do: 'completeSession',
477
-          to: 'selecting',
478
-        },
479
-        CANCELLED: {
480
-          do: ['cancelSession', 'deleteSelectedIds'],
481
-          to: 'selecting',
482
-        },
483
-      },
484
-      initial: 'drawingShapeBounds',
485
-      states: {
486
-        bounds: {
487
-          onEnter: 'startDrawTransformSession',
488
-          on: {
489
-            MOVED_POINTER: 'updateTransformSession',
490
-            PANNED_CAMERA: 'updateTransformSession',
491
-          },
492
-        },
493
-        direction: {
494
-          onEnter: 'startDirectionSession',
495
-          on: {
496
-            MOVED_POINTER: 'updateDirectionSession',
497
-            PANNED_CAMERA: 'updateDirectionSession',
498
-          },
499
-        },
500 528
       },
501 529
     },
502 530
   },
@@ -562,6 +590,12 @@ const state = createState({
562 590
     hasSelection(data) {
563 591
       return data.selectedIds.size > 0
564 592
     },
593
+    isToolLocked(data) {
594
+      return data.settings.isToolLocked
595
+    },
596
+    isPenLocked(data) {
597
+      return data.settings.isPenLocked
598
+    },
565 599
   },
566 600
   actions: {
567 601
     /* --------------------- Shapes --------------------- */
@@ -712,6 +746,19 @@ const state = createState({
712 746
       session.update(data, screenToWorld(payload.point, data))
713 747
     },
714 748
 
749
+    // Nudges
750
+    nudgeSelection(data, payload: { delta: number[]; shiftKey: boolean }) {
751
+      commands.nudge(
752
+        data,
753
+        vec.mul(
754
+          payload.delta,
755
+          payload.shiftKey
756
+            ? data.settings.nudgeDistanceLarge
757
+            : data.settings.nudgeDistanceSmall
758
+        )
759
+      )
760
+    },
761
+
715 762
     /* -------------------- Selection ------------------- */
716 763
 
717 764
     selectAll(data) {
@@ -756,6 +803,9 @@ const state = createState({
756 803
     distributeSelection(data, payload: { type: DistributeType }) {
757 804
       commands.distribute(data, payload.type)
758 805
     },
806
+    duplicateSelection(data) {
807
+      commands.duplicate(data)
808
+    },
759 809
 
760 810
     /* --------------------- Camera --------------------- */
761 811
 
@@ -913,6 +963,7 @@ const state = createState({
913 963
     },
914 964
 
915 965
     /* ---------------------- Code ---------------------- */
966
+
916 967
     closeCodePanel(data) {
917 968
       data.settings.isCodeOpen = false
918 969
     },
@@ -962,7 +1013,20 @@ const state = createState({
962 1013
       history.enable()
963 1014
     },
964 1015
 
965
-    // Data
1016
+    /* -------------------- Settings -------------------- */
1017
+
1018
+    enablePenLock(data) {
1019
+      data.settings.isPenLocked = true
1020
+    },
1021
+    disablePenLock(data) {
1022
+      data.settings.isPenLocked = false
1023
+    },
1024
+    toggleToolLock(data) {
1025
+      data.settings.isToolLocked = !data.settings.isToolLocked
1026
+    },
1027
+
1028
+    /* ---------------------- Data ---------------------- */
1029
+
966 1030
     saveCode(data, payload: { code: string }) {
967 1031
       data.document.code[data.currentCodeFileId].code = payload.code
968 1032
       history.save(data)

+ 29
- 28
styles/stitches.config.ts View File

@@ -1,4 +1,4 @@
1
-import { createCss, defaultThemeMap } from "@stitches/react"
1
+import { createCss, defaultThemeMap } from '@stitches/react'
2 2
 
3 3
 const { styled, global, css, theme, getCssString } = createCss({
4 4
   themeMap: {
@@ -6,26 +6,27 @@ const { styled, global, css, theme, getCssString } = createCss({
6 6
   },
7 7
   theme: {
8 8
     colors: {
9
-      brushFill: "rgba(0,0,0,.1)",
10
-      brushStroke: "rgba(0,0,0,.5)",
11
-      hint: "rgba(66, 133, 244, 0.200)",
12
-      selected: "rgba(66, 133, 244, 1.000)",
13
-      bounds: "rgba(65, 132, 244, 1.000)",
14
-      boundsBg: "rgba(65, 132, 244, 0.100)",
15
-      border: "#aaa",
16
-      panel: "#fefefe",
17
-      hover: "#efefef",
18
-      text: "#333",
19
-      input: "#f3f3f3",
20
-      inputBorder: "#ddd",
9
+      brushFill: 'rgba(0,0,0,.1)',
10
+      brushStroke: 'rgba(0,0,0,.5)',
11
+      hint: 'rgba(66, 133, 244, 0.200)',
12
+      selected: 'rgba(66, 133, 244, 1.000)',
13
+      bounds: 'rgba(65, 132, 244, 1.000)',
14
+      boundsBg: 'rgba(65, 132, 244, 0.100)',
15
+      border: '#aaa',
16
+      panel: '#fefefe',
17
+      inactive: '#cccccf',
18
+      hover: '#efefef',
19
+      text: '#333',
20
+      input: '#f3f3f3',
21
+      inputBorder: '#ddd',
21 22
     },
22 23
     space: {},
23 24
     fontSizes: {
24
-      0: "10px",
25
-      1: "12px",
26
-      2: "13px",
27
-      3: "16px",
28
-      4: "18px",
25
+      0: '10px',
26
+      1: '12px',
27
+      2: '13px',
28
+      3: '16px',
29
+      4: '18px',
29 30
     },
30 31
     fonts: {
31 32
       ui: '"Recursive", system-ui, sans-serif',
@@ -72,17 +73,17 @@ const light = theme({})
72 73
 const dark = theme({})
73 74
 
74 75
 const globalStyles = global({
75
-  "*": { boxSizing: "border-box" },
76
-  ":root": {
77
-    "--camera-zoom": 1,
78
-    "--scale": "calc(1 / var(--camera-zoom))",
76
+  '*': { boxSizing: 'border-box' },
77
+  ':root': {
78
+    '--camera-zoom': 1,
79
+    '--scale': 'calc(1 / var(--camera-zoom))',
79 80
   },
80
-  "html, body": {
81
-    padding: "0px",
82
-    margin: "0px",
83
-    overscrollBehavior: "none",
84
-    fontFamily: "$ui",
85
-    fontSize: "$2",
81
+  'html, body': {
82
+    padding: '0px',
83
+    margin: '0px',
84
+    overscrollBehavior: 'none',
85
+    fontFamily: '$ui',
86
+    fontSize: '$2',
86 87
   },
87 88
 })
88 89
 

+ 28
- 24
types.ts View File

@@ -1,6 +1,6 @@
1
-import * as monaco from "monaco-editor/esm/vs/editor/editor.api"
1
+import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'
2 2
 
3
-import React from "react"
3
+import React from 'react'
4 4
 
5 5
 /* -------------------------------------------------- */
6 6
 /*                    Client State                    */
@@ -13,6 +13,10 @@ export interface Data {
13 13
     isDarkMode: boolean
14 14
     isCodeOpen: boolean
15 15
     isStyleOpen: boolean
16
+    nudgeDistanceSmall: number
17
+    nudgeDistanceLarge: number
18
+    isToolLocked: boolean
19
+    isPenLocked: boolean
16 20
   }
17 21
   currentStyle: ShapeStyles
18 22
   camera: {
@@ -39,21 +43,21 @@ export interface Data {
39 43
 
40 44
 export interface Page {
41 45
   id: string
42
-  type: "page"
46
+  type: 'page'
43 47
   childIndex: number
44 48
   name: string
45 49
   shapes: Record<string, Shape>
46 50
 }
47 51
 
48 52
 export enum ShapeType {
49
-  Dot = "dot",
50
-  Circle = "circle",
51
-  Ellipse = "ellipse",
52
-  Line = "line",
53
-  Ray = "ray",
54
-  Polyline = "polyline",
55
-  Rectangle = "rectangle",
56
-  Draw = "draw",
53
+  Dot = 'dot',
54
+  Circle = 'circle',
55
+  Ellipse = 'ellipse',
56
+  Line = 'line',
57
+  Ray = 'ray',
58
+  Polyline = 'polyline',
59
+  Rectangle = 'rectangle',
60
+  Draw = 'draw',
57 61
 }
58 62
 
59 63
 // Consider:
@@ -164,17 +168,17 @@ export interface PointerInfo {
164 168
 }
165 169
 
166 170
 export enum Edge {
167
-  Top = "top_edge",
168
-  Right = "right_edge",
169
-  Bottom = "bottom_edge",
170
-  Left = "left_edge",
171
+  Top = 'top_edge',
172
+  Right = 'right_edge',
173
+  Bottom = 'bottom_edge',
174
+  Left = 'left_edge',
171 175
 }
172 176
 
173 177
 export enum Corner {
174
-  TopLeft = "top_left_corner",
175
-  TopRight = "top_right_corner",
176
-  BottomRight = "bottom_right_corner",
177
-  BottomLeft = "bottom_left_corner",
178
+  TopLeft = 'top_left_corner',
179
+  TopRight = 'top_right_corner',
180
+  BottomRight = 'bottom_right_corner',
181
+  BottomLeft = 'bottom_left_corner',
178 182
 }
179 183
 
180 184
 export interface Bounds {
@@ -262,10 +266,10 @@ export type IMonaco = typeof monaco
262 266
 export type IMonacoEditor = monaco.editor.IStandaloneCodeEditor
263 267
 
264 268
 export enum ControlType {
265
-  Number = "number",
266
-  Vector = "vector",
267
-  Text = "text",
268
-  Select = "select",
269
+  Number = 'number',
270
+  Vector = 'vector',
271
+  Text = 'text',
272
+  Select = 'select',
269 273
 }
270 274
 
271 275
 export interface BaseCodeControl {
@@ -296,7 +300,7 @@ export interface TextCodeControl extends BaseCodeControl {
296 300
   format?: (value: string) => string
297 301
 }
298 302
 
299
-export interface SelectCodeControl<T extends string = "">
303
+export interface SelectCodeControl<T extends string = ''>
300 304
   extends BaseCodeControl {
301 305
   type: ControlType.Select
302 306
   value: T

+ 26
- 26
utils/utils.ts View File

@@ -1,9 +1,9 @@
1
-import Vector from "lib/code/vector"
2
-import React from "react"
3
-import { Data, Bounds, Edge, Corner, Shape, ShapeStyles } from "types"
4
-import * as vec from "./vec"
5
-import _isMobile from "ismobilejs"
6
-import { getShapeUtils } from "lib/shape-utils"
1
+import Vector from 'lib/code/vector'
2
+import React from 'react'
3
+import { Data, Bounds, Edge, Corner, Shape, ShapeStyles } from 'types'
4
+import * as vec from './vec'
5
+import _isMobile from 'ismobilejs'
6
+import { getShapeUtils } from 'lib/shape-utils'
7 7
 
8 8
 export function screenToWorld(point: number[], data: Data) {
9 9
   return vec.sub(vec.div(point, data.camera.zoom), data.camera.point)
@@ -132,7 +132,7 @@ export function getBezierCurveSegments(points: number[][], tension = 0.4) {
132 132
     cpoints: number[][] = [...points]
133 133
 
134 134
   if (len < 2) {
135
-    throw Error("Curve must have at least two points.")
135
+    throw Error('Curve must have at least two points.')
136 136
   }
137 137
 
138 138
   for (let i = 1; i < len - 1; i++) {
@@ -260,12 +260,12 @@ export function copyToClipboard(string: string) {
260 260
     navigator.clipboard.writeText(string)
261 261
   } catch (e) {
262 262
     try {
263
-      textarea = document.createElement("textarea")
264
-      textarea.setAttribute("position", "fixed")
265
-      textarea.setAttribute("top", "0")
266
-      textarea.setAttribute("readonly", "true")
267
-      textarea.setAttribute("contenteditable", "true")
268
-      textarea.style.position = "fixed" // prevent scroll from jumping to the bottom when focus is set.
263
+      textarea = document.createElement('textarea')
264
+      textarea.setAttribute('position', 'fixed')
265
+      textarea.setAttribute('top', '0')
266
+      textarea.setAttribute('readonly', 'true')
267
+      textarea.setAttribute('contenteditable', 'true')
268
+      textarea.style.position = 'fixed' // prevent scroll from jumping to the bottom when focus is set.
269 269
       textarea.value = string
270 270
 
271 271
       document.body.appendChild(textarea)
@@ -281,7 +281,7 @@ export function copyToClipboard(string: string) {
281 281
       sel.addRange(range)
282 282
 
283 283
       textarea.setSelectionRange(0, textarea.value.length)
284
-      result = document.execCommand("copy")
284
+      result = document.execCommand('copy')
285 285
     } catch (err) {
286 286
       result = null
287 287
     } finally {
@@ -549,7 +549,7 @@ export function arrsIntersect<T>(
549 549
 
550 550
 export function getTouchDisplay() {
551 551
   return (
552
-    "ontouchstart" in window ||
552
+    'ontouchstart' in window ||
553 553
     navigator.maxTouchPoints > 0 ||
554 554
     navigator.msMaxTouchPoints > 0
555 555
   )
@@ -604,7 +604,7 @@ export function modulate(
604 604
 export function clamp(n: number, min: number): number
605 605
 export function clamp(n: number, min: number, max: number): number
606 606
 export function clamp(n: number, min: number, max?: number): number {
607
-  return Math.max(min, typeof max !== "undefined" ? Math.min(n, max) : n)
607
+  return Math.max(min, typeof max !== 'undefined' ? Math.min(n, max) : n)
608 608
 }
609 609
 
610 610
 // CURVES
@@ -871,8 +871,8 @@ export async function postJsonToEndpoint(
871 871
   const d = await fetch(
872 872
     `${process.env.NEXT_PUBLIC_BASE_API_URL}/api/${endpoint}`,
873 873
     {
874
-      method: "POST",
875
-      headers: { "Content-Type": "application/json" },
874
+      method: 'POST',
875
+      headers: { 'Content-Type': 'application/json' },
876 876
       body: JSON.stringify(data),
877 877
     }
878 878
   )
@@ -962,7 +962,7 @@ export function getTransformAnchor(
962 962
 }
963 963
 
964 964
 export function vectorToPoint(point: number[] | Vector | undefined) {
965
-  if (typeof point === "undefined") {
965
+  if (typeof point === 'undefined') {
966 966
     return [0, 0]
967 967
   }
968 968
 
@@ -1062,7 +1062,7 @@ export function getRotatedCorners(b: Bounds, rotation: number) {
1062 1062
 
1063 1063
 export function getTransformedBoundingBox(
1064 1064
   bounds: Bounds,
1065
-  handle: Corner | Edge | "center",
1065
+  handle: Corner | Edge | 'center',
1066 1066
   delta: number[],
1067 1067
   rotation = 0,
1068 1068
   isAspectRatioLocked = false
@@ -1076,7 +1076,7 @@ export function getTransformedBoundingBox(
1076 1076
   let [bx1, by1] = [bounds.maxX, bounds.maxY]
1077 1077
 
1078 1078
   // If the drag is on the center, just translate the bounds.
1079
-  if (handle === "center") {
1079
+  if (handle === 'center') {
1080 1080
     return {
1081 1081
       minX: bx0 + delta[0],
1082 1082
       minY: by0 + delta[1],
@@ -1491,7 +1491,7 @@ export function forceIntegerChildIndices(shapes: Shape[]) {
1491 1491
   }
1492 1492
 }
1493 1493
 export function setZoomCSS(zoom: number) {
1494
-  document.documentElement.style.setProperty("--camera-zoom", zoom.toString())
1494
+  document.documentElement.style.setProperty('--camera-zoom', zoom.toString())
1495 1495
 }
1496 1496
 
1497 1497
 export function getCurrent<T extends object>(source: T): T {
@@ -1539,7 +1539,7 @@ export function simplify(points: number[][], tolerance = 1) {
1539 1539
 }
1540 1540
 
1541 1541
 export function getSvgPathFromStroke(stroke: number[][]) {
1542
-  if (!stroke.length) return ""
1542
+  if (!stroke.length) return ''
1543 1543
 
1544 1544
   const d = stroke.reduce(
1545 1545
     (acc, [x0, y0], i, arr) => {
@@ -1547,9 +1547,9 @@ export function getSvgPathFromStroke(stroke: number[][]) {
1547 1547
       acc.push(x0, y0, (x0 + x1) / 2, (y0 + y1) / 2)
1548 1548
       return acc
1549 1549
     },
1550
-    ["M", ...stroke[0], "Q"]
1550
+    ['M', ...stroke[0], 'Q']
1551 1551
   )
1552 1552
 
1553
-  d.push("Z")
1554
-  return d.join(" ")
1553
+  d.push('Z')
1554
+  return d.join(' ')
1555 1555
 }

+ 1
- 1
utils/vec.ts View File

@@ -483,6 +483,6 @@ export function nudge(A: number[], B: number[], d: number) {
483 483
  * @param a
484 484
  * @param n
485 485
  */
486
-export function toPrecision(a: number[], n = 3) {
486
+export function toPrecision(a: number[], n = 4) {
487 487
   return [+a[0].toPrecision(n), +a[1].toPrecision(n)]
488 488
 }

+ 29
- 0
yarn.lock View File

@@ -1358,6 +1358,17 @@
1358 1358
   dependencies:
1359 1359
     "@babel/runtime" "^7.13.10"
1360 1360
 
1361
+"@radix-ui/react-label@0.0.13":
1362
+  version "0.0.13"
1363
+  resolved "https://registry.yarnpkg.com/@radix-ui/react-label/-/react-label-0.0.13.tgz#b71930fa16a2cf859296317436cb88e31efb8ecf"
1364
+  integrity sha512-csNElm8qA38pOHr772CXIvBXd/eCGaoAMImuLdawUxQNzwxQ4npd8lr/f9fi/4OLkgeNOVOqjsaVamiNmF/lIw==
1365
+  dependencies:
1366
+    "@babel/runtime" "^7.13.10"
1367
+    "@radix-ui/react-compose-refs" "0.0.5"
1368
+    "@radix-ui/react-id" "0.0.6"
1369
+    "@radix-ui/react-polymorphic" "0.0.11"
1370
+    "@radix-ui/react-primitive" "0.0.13"
1371
+
1361 1372
 "@radix-ui/react-menu@0.0.18":
1362 1373
   version "0.0.18"
1363 1374
   resolved "https://registry.yarnpkg.com/@radix-ui/react-menu/-/react-menu-0.0.18.tgz#b36f7657eb6757c623ffc688c48a4781ffd82351"
@@ -1431,6 +1442,24 @@
1431 1442
     "@babel/runtime" "^7.13.10"
1432 1443
     "@radix-ui/react-polymorphic" "0.0.11"
1433 1444
 
1445
+"@radix-ui/react-radio-group@^0.0.16":
1446
+  version "0.0.16"
1447
+  resolved "https://registry.yarnpkg.com/@radix-ui/react-radio-group/-/react-radio-group-0.0.16.tgz#10fc6e5c3102599cf422e9f6f8d2766088e602a1"
1448
+  integrity sha512-vOtgflNWcauSul+EvnPCxATdmPw7fb1cuqBJX07yJdjbrw1Iv5v/+d79fNyIwPR+KrkhP+uCMIBfF0gvo6K7ZQ==
1449
+  dependencies:
1450
+    "@babel/runtime" "^7.13.10"
1451
+    "@radix-ui/primitive" "0.0.5"
1452
+    "@radix-ui/react-compose-refs" "0.0.5"
1453
+    "@radix-ui/react-context" "0.0.5"
1454
+    "@radix-ui/react-label" "0.0.13"
1455
+    "@radix-ui/react-polymorphic" "0.0.11"
1456
+    "@radix-ui/react-presence" "0.0.14"
1457
+    "@radix-ui/react-primitive" "0.0.13"
1458
+    "@radix-ui/react-roving-focus" "0.0.13"
1459
+    "@radix-ui/react-slot" "0.0.10"
1460
+    "@radix-ui/react-use-callback-ref" "0.0.5"
1461
+    "@radix-ui/react-use-controllable-state" "0.0.6"
1462
+
1434 1463
 "@radix-ui/react-roving-focus@0.0.13":
1435 1464
   version "0.0.13"
1436 1465
   resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-0.0.13.tgz#c72f503832577979c4caa9efcfd59140730c2f80"

Loading…
Cancel
Save