瀏覽代碼

Adds most of text feature, except creation

main
Steve Ruiz 4 年之前
父節點
當前提交
027815f199

+ 0
- 1
components/canvas/bounds/bounding-box.tsx 查看文件

@@ -15,7 +15,6 @@ import CenterHandle from './center-handle'
15 15
 import CornerHandle from './corner-handle'
16 16
 import EdgeHandle from './edge-handle'
17 17
 import RotateHandle from './rotate-handle'
18
-import Handles from './handles'
19 18
 
20 19
 export default function Bounds() {
21 20
   const isBrushing = useSelector((s) => s.isIn('brushSelecting'))

+ 4
- 3
components/canvas/bounds/bounds-bg.tsx 查看文件

@@ -1,4 +1,4 @@
1
-import { useCallback, useRef } from 'react'
1
+import { useRef } from 'react'
2 2
 import state, { useSelector } from 'state'
3 3
 import inputs from 'state/inputs'
4 4
 import styled from 'styles'
@@ -8,11 +8,12 @@ function handlePointerDown(e: React.PointerEvent<SVGRectElement>) {
8 8
   if (!inputs.canAccept(e.pointerId)) return
9 9
   e.stopPropagation()
10 10
   e.currentTarget.setPointerCapture(e.pointerId)
11
+  const info = inputs.pointerDown(e, 'bounds')
11 12
 
12 13
   if (e.button === 0) {
13
-    state.send('POINTED_BOUNDS', inputs.pointerDown(e, 'bounds'))
14
+    state.send('POINTED_BOUNDS', info)
14 15
   } else if (e.button === 2) {
15
-    state.send('RIGHT_POINTED', inputs.pointerDown(e, 'bounds'))
16
+    state.send('RIGHT_POINTED', info)
16 17
   }
17 18
 }
18 19
 

+ 2
- 2
components/canvas/bounds/center-handle.tsx 查看文件

@@ -23,12 +23,12 @@ export default function CenterHandle({
23 23
 const StyledBounds = styled('rect', {
24 24
   fill: 'none',
25 25
   stroke: '$bounds',
26
-  zStrokeWidth: 2,
26
+  zStrokeWidth: 1.5,
27 27
 
28 28
   variants: {
29 29
     isLocked: {
30 30
       true: {
31
-        zStrokeWidth: 1,
31
+        zStrokeWidth: 1.5,
32 32
         zDash: 2,
33 33
       },
34 34
     },

+ 3
- 3
components/canvas/bounds/corner-handle.tsx 查看文件

@@ -1,4 +1,4 @@
1
-import useHandleEvents from 'hooks/useBoundsHandleEvents'
1
+import useBoundsEvents from 'hooks/useBoundsEvents'
2 2
 import styled from 'styles'
3 3
 import { Corner, Bounds } from 'types'
4 4
 
@@ -11,7 +11,7 @@ export default function CornerHandle({
11 11
   bounds: Bounds
12 12
   corner: Corner
13 13
 }) {
14
-  const events = useHandleEvents(corner)
14
+  const events = useBoundsEvents(corner)
15 15
 
16 16
   const isTop = corner === Corner.TopLeft || corner === Corner.TopRight
17 17
   const isLeft = corner === Corner.TopLeft || corner === Corner.BottomLeft
@@ -53,5 +53,5 @@ const StyledCorner = styled('rect', {
53 53
 const StyledCornerInner = styled('rect', {
54 54
   stroke: '$bounds',
55 55
   fill: '#fff',
56
-  zStrokeWidth: 2,
56
+  zStrokeWidth: 1.5,
57 57
 })

+ 2
- 2
components/canvas/bounds/edge-handle.tsx 查看文件

@@ -1,4 +1,4 @@
1
-import useHandleEvents from 'hooks/useBoundsHandleEvents'
1
+import useBoundsEvents from 'hooks/useBoundsEvents'
2 2
 import styled from 'styles'
3 3
 import { Edge, Bounds } from 'types'
4 4
 
@@ -11,7 +11,7 @@ export default function EdgeHandle({
11 11
   bounds: Bounds
12 12
   edge: Edge
13 13
 }) {
14
-  const events = useHandleEvents(edge)
14
+  const events = useBoundsEvents(edge)
15 15
 
16 16
   const isHorizontal = edge === Edge.Top || edge === Edge.Bottom
17 17
   const isFarEdge = edge === Edge.Right || edge === Edge.Bottom

+ 2
- 2
components/canvas/bounds/rotate-handle.tsx 查看文件

@@ -1,4 +1,4 @@
1
-import useHandleEvents from 'hooks/useBoundsHandleEvents'
1
+import useHandleEvents from 'hooks/useBoundsEvents'
2 2
 import styled from 'styles'
3 3
 import { Bounds } from 'types'
4 4
 
@@ -33,6 +33,6 @@ export default function Rotate({
33 33
 const StyledRotateHandle = styled('circle', {
34 34
   stroke: '$bounds',
35 35
   fill: '#fff',
36
-  zStrokeWidth: 2,
36
+  zStrokeWidth: 1.5,
37 37
   cursor: 'grab',
38 38
 })

+ 4
- 1
components/canvas/defs.tsx 查看文件

@@ -9,6 +9,8 @@ export default function Defs() {
9 9
 
10 10
   const currentPageShapeIds = useSelector(({ data }) => {
11 11
     return Object.values(getPage(data).shapes)
12
+      .filter(Boolean)
13
+      .filter((shape) => !getShapeUtils(shape).isForeignObject)
12 14
       .sort((a, b) => a.childIndex - b.childIndex)
13 15
       .map((shape) => shape.id)
14 16
   }, deepCompareArrays)
@@ -29,6 +31,7 @@ export default function Defs() {
29 31
 
30 32
 const Def = memo(function Def({ id }: { id: string }) {
31 33
   const shape = useSelector((s) => getPage(s.data).shapes[id])
34
+
32 35
   if (!shape) return null
33
-  return getShapeUtils(shape).render(shape)
36
+  return getShapeUtils(shape).render(shape, { isEditing: false })
34 37
 })

+ 1
- 0
components/canvas/page.tsx 查看文件

@@ -26,6 +26,7 @@ export default function Page() {
26 26
         [window.innerWidth, window.innerHeight],
27 27
         s.data
28 28
       )
29
+
29 30
       viewportCache.set(pageState, {
30 31
         minX,
31 32
         minY,

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

@@ -1,5 +1,5 @@
1 1
 import React, { useRef, memo } from 'react'
2
-import { useSelector } from 'state'
2
+import state, { useSelector } from 'state'
3 3
 import styled from 'styles'
4 4
 import { getShapeUtils } from 'lib/shape-utils'
5 5
 import { getBoundsCenter, getPage } from 'utils/utils'
@@ -18,9 +18,11 @@ interface ShapeProps {
18 18
 function Shape({ id, isSelecting, parentPoint }: ShapeProps) {
19 19
   const shape = useSelector((s) => getPage(s.data).shapes[id])
20 20
 
21
+  const isEditing = useSelector((s) => s.data.editingId === id)
22
+
21 23
   const rGroup = useRef<SVGGElement>(null)
22 24
 
23
-  const events = useShapeEvents(id, shape?.type === ShapeType.Group, rGroup)
25
+  const events = useShapeEvents(id, getShapeUtils(shape)?.isParent, rGroup)
24 26
 
25 27
   // This is a problem with deleted shapes. The hooks in this component
26 28
   // may sometimes run before the hook in the Page component, which means
@@ -28,9 +30,13 @@ function Shape({ id, isSelecting, parentPoint }: ShapeProps) {
28 30
   // detects the change and pulls this component.
29 31
   if (!shape) return null
30 32
 
31
-  const isGroup = shape.type === ShapeType.Group
33
+  const utils = getShapeUtils(shape)
34
+  const style = getShapeStyle(shape.style)
35
+  const shapeUtils = getShapeUtils(shape)
36
+  const { isShy, isParent, isForeignObject } = shapeUtils
32 37
 
33
-  const center = getShapeUtils(shape).getCenter(shape)
38
+  const bounds = shapeUtils.getBounds(shape)
39
+  const center = shapeUtils.getCenter(shape)
34 40
   const rotation = shape.rotation * (180 / Math.PI)
35 41
 
36 42
   const transform = `
@@ -39,22 +45,46 @@ function Shape({ id, isSelecting, parentPoint }: ShapeProps) {
39 45
   translate(${shape.point})
40 46
   `
41 47
 
42
-  const style = getShapeStyle(shape.style)
43
-
44 48
   return (
45
-    <StyledGroup ref={rGroup} transform={transform}>
46
-      {isSelecting && !isGroup && (
47
-        <HoverIndicator
48
-          as="use"
49
-          href={'#' + id}
50
-          strokeWidth={+style.strokeWidth + 4}
51
-          variant={getShapeUtils(shape).canStyleFill ? 'filled' : 'hollow'}
52
-          {...events}
53
-        />
54
-      )}
55
-
56
-      {!shape.isHidden && <RealShape isGroup={isGroup} id={id} style={style} />}
57
-      {isGroup &&
49
+    <StyledGroup
50
+      ref={rGroup}
51
+      transform={transform}
52
+      onBlur={() => state.send('BLURRED_SHAPE', { target: id })}
53
+    >
54
+      {isSelecting &&
55
+        !isShy &&
56
+        (isForeignObject ? (
57
+          <HoverIndicator
58
+            as="rect"
59
+            width={bounds.width}
60
+            height={bounds.height}
61
+            strokeWidth={1.5}
62
+            variant={'ghost'}
63
+            {...events}
64
+          />
65
+        ) : (
66
+          <HoverIndicator
67
+            as="use"
68
+            href={'#' + id}
69
+            strokeWidth={+style.strokeWidth + 4}
70
+            variant={getShapeUtils(shape).canStyleFill ? 'filled' : 'hollow'}
71
+            {...events}
72
+          />
73
+        ))}
74
+
75
+      {!shape.isHidden &&
76
+        (isForeignObject ? (
77
+          shapeUtils.render(shape, { isEditing })
78
+        ) : (
79
+          <RealShape
80
+            isParent={isParent}
81
+            id={id}
82
+            style={style}
83
+            isEditing={isEditing}
84
+          />
85
+        ))}
86
+
87
+      {isParent &&
58 88
         shape.children.map((shapeId) => (
59 89
           <Shape
60 90
             key={shapeId}
@@ -68,17 +98,19 @@ function Shape({ id, isSelecting, parentPoint }: ShapeProps) {
68 98
 }
69 99
 
70 100
 interface RealShapeProps {
71
-  isGroup: boolean
72 101
   id: string
73 102
   style: Partial<React.SVGProps<SVGUseElement>>
103
+  isParent: boolean
104
+  isEditing: boolean
74 105
 }
75 106
 
76 107
 const RealShape = memo(function RealShape({
77
-  isGroup,
78 108
   id,
79 109
   style,
110
+  isParent,
111
+  isEditing,
80 112
 }: RealShapeProps) {
81
-  return <StyledShape as="use" data-shy={isGroup} href={'#' + id} {...style} />
113
+  return <StyledShape as="use" data-shy={isParent} href={'#' + id} {...style} />
82 114
 })
83 115
 
84 116
 const StyledShape = styled('path', {
@@ -91,11 +123,15 @@ const HoverIndicator = styled('path', {
91 123
   stroke: '$selected',
92 124
   strokeLinecap: 'round',
93 125
   strokeLinejoin: 'round',
94
-  transform: 'all .2s',
95 126
   fill: 'transparent',
96 127
   filter: 'url(#expand)',
97 128
   variants: {
98 129
     variant: {
130
+      ghost: {
131
+        pointerEvents: 'all',
132
+        filter: 'none',
133
+        opacity: 0,
134
+      },
99 135
       hollow: {
100 136
         pointerEvents: 'stroke',
101 137
       },

hooks/useBoundsHandleEvents.ts → hooks/useBoundsEvents.ts 查看文件

@@ -1,19 +1,25 @@
1
-import { useCallback, useRef } from 'react'
1
+import { useCallback } from 'react'
2 2
 import inputs from 'state/inputs'
3 3
 import { Edge, Corner } from 'types'
4 4
 
5 5
 import state from '../state'
6 6
 
7
-export default function useBoundsHandleEvents(
8
-  handle: Edge | Corner | 'rotate'
9
-) {
7
+export default function useBoundsEvents(handle: Edge | Corner | 'rotate') {
10 8
   const onPointerDown = useCallback(
11 9
     (e) => {
12
-      if (e.buttons !== 1) return
13 10
       if (!inputs.canAccept(e.pointerId)) return
14 11
       e.stopPropagation()
15 12
       e.currentTarget.setPointerCapture(e.pointerId)
16
-      state.send('POINTED_BOUNDS_HANDLE', inputs.pointerDown(e, handle))
13
+
14
+      if (e.button === 0) {
15
+        const info = inputs.pointerDown(e, handle)
16
+
17
+        if (inputs.isDoubleClick() && !(info.altKey || info.metaKey)) {
18
+          state.send('DOUBLE_POINTED_BOUNDS_HANDLE', info)
19
+        }
20
+
21
+        state.send('POINTED_BOUNDS_HANDLE', info)
22
+      }
17 23
     },
18 24
     [handle]
19 25
   )

+ 14
- 1
hooks/useKeyboardEvents.ts 查看文件

@@ -6,7 +6,20 @@ import { getKeyboardEventInfo, metaKey } from 'utils/utils'
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 (
10
+        metaKey(e) &&
11
+        ![
12
+          'a',
13
+          'i',
14
+          'r',
15
+          'j',
16
+          'ArrowLeft',
17
+          'ArrowRight',
18
+          'ArrowUp',
19
+          'ArrowDown',
20
+          'z',
21
+        ].includes(e.key)
22
+      ) {
10 23
         e.preventDefault()
11 24
       }
12 25
 

+ 5
- 5
hooks/useShapeEvents.ts 查看文件

@@ -4,12 +4,12 @@ import inputs from 'state/inputs'
4 4
 
5 5
 export default function useShapeEvents(
6 6
   id: string,
7
-  isGroup: boolean,
7
+  isParent: boolean,
8 8
   rGroup: MutableRefObject<SVGElement>
9 9
 ) {
10 10
   const handlePointerDown = useCallback(
11 11
     (e: React.PointerEvent) => {
12
-      if (isGroup) return
12
+      if (isParent) return
13 13
       if (!inputs.canAccept(e.pointerId)) return
14 14
       e.stopPropagation()
15 15
       rGroup.current.setPointerCapture(e.pointerId)
@@ -42,7 +42,7 @@ export default function useShapeEvents(
42 42
   const handlePointerEnter = useCallback(
43 43
     (e: React.PointerEvent) => {
44 44
       if (!inputs.canAccept(e.pointerId)) return
45
-      if (isGroup) {
45
+      if (isParent) {
46 46
         state.send('HOVERED_GROUP', inputs.pointerEnter(e, id))
47 47
       } else {
48 48
         state.send('HOVERED_SHAPE', inputs.pointerEnter(e, id))
@@ -55,7 +55,7 @@ export default function useShapeEvents(
55 55
     (e: React.PointerEvent) => {
56 56
       if (!inputs.canAccept(e.pointerId)) return
57 57
 
58
-      if (isGroup) {
58
+      if (isParent) {
59 59
         state.send('MOVED_OVER_GROUP', inputs.pointerEnter(e, id))
60 60
       } else {
61 61
         state.send('MOVED_OVER_SHAPE', inputs.pointerEnter(e, id))
@@ -67,7 +67,7 @@ export default function useShapeEvents(
67 67
   const handlePointerLeave = useCallback(
68 68
     (e: React.PointerEvent) => {
69 69
       if (!inputs.canAccept(e.pointerId)) return
70
-      if (isGroup) {
70
+      if (isParent) {
71 71
         state.send('UNHOVERED_GROUP', { target: id })
72 72
       } else {
73 73
         state.send('UNHOVERED_SHAPE', { target: id })

+ 1
- 1
lib/code/ray.ts 查看文件

@@ -15,7 +15,7 @@ export default class Ray extends CodeShape<RayShape> {
15 15
       type: ShapeType.Ray,
16 16
       isGenerated: true,
17 17
       name: 'Ray',
18
-      parentId: 'page0',
18
+      parentId: 'page1',
19 19
       childIndex: 0,
20 20
       point: [0, 0],
21 21
       direction: [0, 1],

+ 19
- 1
lib/shape-styles.ts 查看文件

@@ -1,5 +1,5 @@
1 1
 import { SVGProps } from 'react'
2
-import { ColorStyle, DashStyle, Shape, ShapeStyles, SizeStyle } from 'types'
2
+import { ColorStyle, DashStyle, FontSize, ShapeStyles, SizeStyle } from 'types'
3 3
 
4 4
 export const strokes: Record<ColorStyle, string> = {
5 5
   [ColorStyle.White]: 'rgba(248, 249, 250, 1.000)',
@@ -43,6 +43,14 @@ const dashArrays = {
43 43
   [DashStyle.Dotted]: (sw: number) => `0 ${sw * 1.5}`,
44 44
 }
45 45
 
46
+const fontSizes = {
47
+  [FontSize.Small]: 16,
48
+  [FontSize.Medium]: 28,
49
+  [FontSize.Large]: 32,
50
+  [FontSize.ExtraLarge]: 72,
51
+  auto: 'auto',
52
+}
53
+
46 54
 function getStrokeWidth(size: SizeStyle) {
47 55
   return strokeWidths[size]
48 56
 }
@@ -51,6 +59,16 @@ function getStrokeDashArray(dash: DashStyle, strokeWidth: number) {
51 59
   return dashArrays[dash](strokeWidth)
52 60
 }
53 61
 
62
+export function getFontSize(size: FontSize) {
63
+  return fontSizes[size]
64
+}
65
+
66
+export function getFontStyle(size: FontSize, style: ShapeStyles) {
67
+  const fontSize = getFontSize(size)
68
+
69
+  return `${fontSize}px Verveine Regular`
70
+}
71
+
54 72
 export function getShapeStyle(
55 73
   style: ShapeStyles
56 74
 ): Partial<SVGProps<SVGUseElement>> {

+ 1
- 1
lib/shape-utils/arrow.tsx 查看文件

@@ -70,7 +70,7 @@ const arrow = registerShapeUtils<ArrowShape>({
70 70
       type: ShapeType.Arrow,
71 71
       isGenerated: false,
72 72
       name: 'Arrow',
73
-      parentId: 'page0',
73
+      parentId: 'page1',
74 74
       childIndex: 0,
75 75
       point,
76 76
       rotation: 0,

+ 1
- 1
lib/shape-utils/circle.tsx 查看文件

@@ -18,7 +18,7 @@ const circle = registerShapeUtils<CircleShape>({
18 18
       type: ShapeType.Circle,
19 19
       isGenerated: false,
20 20
       name: 'Circle',
21
-      parentId: 'page0',
21
+      parentId: 'page1',
22 22
       childIndex: 0,
23 23
       point: [0, 0],
24 24
       rotation: 0,

+ 1
- 1
lib/shape-utils/dot.tsx 查看文件

@@ -17,7 +17,7 @@ const dot = registerShapeUtils<DotShape>({
17 17
       type: ShapeType.Dot,
18 18
       isGenerated: false,
19 19
       name: 'Dot',
20
-      parentId: 'page0',
20
+      parentId: 'page1',
21 21
       childIndex: 0,
22 22
       point: [0, 0],
23 23
       rotation: 0,

+ 1
- 1
lib/shape-utils/draw.tsx 查看文件

@@ -28,7 +28,7 @@ const draw = registerShapeUtils<DrawShape>({
28 28
       type: ShapeType.Draw,
29 29
       isGenerated: false,
30 30
       name: 'Draw',
31
-      parentId: 'page0',
31
+      parentId: 'page1',
32 32
       childIndex: 0,
33 33
       point: [0, 0],
34 34
       points: [],

+ 1
- 1
lib/shape-utils/ellipse.tsx 查看文件

@@ -21,7 +21,7 @@ const ellipse = registerShapeUtils<EllipseShape>({
21 21
       type: ShapeType.Ellipse,
22 22
       isGenerated: false,
23 23
       name: 'Ellipse',
24
-      parentId: 'page0',
24
+      parentId: 'page1',
25 25
       childIndex: 0,
26 26
       point: [0, 0],
27 27
       radiusX: 1,

+ 3
- 1
lib/shape-utils/group.tsx 查看文件

@@ -22,6 +22,8 @@ import { boundsContainPolygon } from 'utils/bounds'
22 22
 
23 23
 const group = registerShapeUtils<GroupShape>({
24 24
   boundsCache: new WeakMap([]),
25
+  isShy: true,
26
+  isParent: true,
25 27
 
26 28
   create(props) {
27 29
     return {
@@ -30,7 +32,7 @@ const group = registerShapeUtils<GroupShape>({
30 32
       type: ShapeType.Group,
31 33
       isGenerated: false,
32 34
       name: 'Group',
33
-      parentId: 'page0',
35
+      parentId: 'page1',
34 36
       childIndex: 0,
35 37
       point: [0, 0],
36 38
       size: [1, 1],

+ 47
- 7
lib/shape-utils/index.tsx 查看文件

@@ -8,7 +8,6 @@ import {
8 8
   ShapeBinding,
9 9
   Mutable,
10 10
   ShapeByType,
11
-  Data,
12 11
 } from 'types'
13 12
 import * as vec from 'utils/vec'
14 13
 import {
@@ -32,6 +31,7 @@ import ray from './ray'
32 31
 import draw from './draw'
33 32
 import arrow from './arrow'
34 33
 import group from './group'
34
+import text from './text'
35 35
 
36 36
 /*
37 37
 Shape Utiliies
@@ -51,12 +51,24 @@ export interface ShapeUtility<K extends Shape> {
51 51
   // Whether to show transform controls when this shape is selected.
52 52
   canTransform: boolean
53 53
 
54
-  // Whether the shape's aspect ratio can change
54
+  // Whether the shape's aspect ratio can change.
55 55
   canChangeAspectRatio: boolean
56 56
 
57
-  // Whether the shape's style can be filled
57
+  // Whether the shape's style can be filled.
58 58
   canStyleFill: boolean
59 59
 
60
+  // Whether the shape may be edited in an editing mode
61
+  canEdit: boolean
62
+
63
+  // Whether the shape is a foreign object.
64
+  isForeignObject: boolean
65
+
66
+  // Whether the shape can contain other shapes.
67
+  isParent: boolean
68
+
69
+  // Whether the shape is only shown when on hovered.
70
+  isShy: boolean
71
+
60 72
   // Create a new shape.
61 73
   create(props: Partial<K>): K
62 74
 
@@ -148,11 +160,21 @@ export interface ShapeUtility<K extends Shape> {
148 160
     handle: Partial<K['handles']>
149 161
   ): ShapeUtility<K>
150 162
 
163
+  // Respond when a user double clicks the shape's bounds.
164
+  onBoundsReset(this: ShapeUtility<K>, shape: Mutable<K>): ShapeUtility<K>
165
+
166
+  // Respond when a user double clicks the center of the shape.
167
+  onDoubleFocus(this: ShapeUtility<K>, shape: Mutable<K>): ShapeUtility<K>
168
+
151 169
   // Clean up changes when a session ends.
152 170
   onSessionComplete(this: ShapeUtility<K>, shape: Mutable<K>): ShapeUtility<K>
153 171
 
154 172
   // Render a shape to JSX.
155
-  render(this: ShapeUtility<K>, shape: K): JSX.Element
173
+  render(
174
+    this: ShapeUtility<K>,
175
+    shape: K,
176
+    info: { isEditing: boolean }
177
+  ): JSX.Element
156 178
 
157 179
   // Get the bounds of the a shape.
158 180
   getBounds(this: ShapeUtility<K>, shape: K): Bounds
@@ -168,6 +190,8 @@ export interface ShapeUtility<K extends Shape> {
168 190
 
169 191
   // Test whether bounds collide with or contain a shape.
170 192
   hitTestBounds(this: ShapeUtility<K>, shape: K, bounds: Bounds): boolean
193
+
194
+  getShouldDelete(this: ShapeUtility<K>, shape: K): boolean
171 195
 }
172 196
 
173 197
 // A mapping of shape types to shape utilities.
@@ -181,7 +205,7 @@ const shapeUtilityMap: Record<ShapeType, ShapeUtility<Shape>> = {
181 205
   [ShapeType.Ray]: ray,
182 206
   [ShapeType.Draw]: draw,
183 207
   [ShapeType.Arrow]: arrow,
184
-  [ShapeType.Text]: arrow,
208
+  [ShapeType.Text]: text,
185 209
   [ShapeType.Group]: group,
186 210
 }
187 211
 
@@ -191,7 +215,7 @@ const shapeUtilityMap: Record<ShapeType, ShapeUtility<Shape>> = {
191 215
  * @returns
192 216
  */
193 217
 export function getShapeUtils<T extends Shape>(shape: T): ShapeUtility<T> {
194
-  return shapeUtilityMap[shape.type] as ShapeUtility<T>
218
+  return shapeUtilityMap[shape?.type] as ShapeUtility<T>
195 219
 }
196 220
 
197 221
 function getDefaultShapeUtil<T extends Shape>(): ShapeUtility<T> {
@@ -200,6 +224,10 @@ function getDefaultShapeUtil<T extends Shape>(): ShapeUtility<T> {
200 224
     canTransform: true,
201 225
     canChangeAspectRatio: true,
202 226
     canStyleFill: true,
227
+    canEdit: false,
228
+    isShy: false,
229
+    isParent: false,
230
+    isForeignObject: false,
203 231
 
204 232
     create(props) {
205 233
       return {
@@ -207,7 +235,7 @@ function getDefaultShapeUtil<T extends Shape>(): ShapeUtility<T> {
207 235
         isGenerated: false,
208 236
         point: [0, 0],
209 237
         name: 'Shape',
210
-        parentId: 'page0',
238
+        parentId: 'page1',
211 239
         childIndex: 0,
212 240
         rotation: 0,
213 241
         isAspectRatioLocked: false,
@@ -262,6 +290,14 @@ function getDefaultShapeUtil<T extends Shape>(): ShapeUtility<T> {
262 290
       return this
263 291
     },
264 292
 
293
+    onDoubleFocus() {
294
+      return this
295
+    },
296
+
297
+    onBoundsReset() {
298
+      return this
299
+    },
300
+
265 301
     onSessionComplete() {
266 302
       return this
267 303
     },
@@ -313,6 +349,10 @@ function getDefaultShapeUtil<T extends Shape>(): ShapeUtility<T> {
313 349
       Object.assign(shape.style, style)
314 350
       return this
315 351
     },
352
+
353
+    getShouldDelete(shape) {
354
+      return false
355
+    },
316 356
   }
317 357
 }
318 358
 

+ 1
- 1
lib/shape-utils/line.tsx 查看文件

@@ -19,7 +19,7 @@ const line = registerShapeUtils<LineShape>({
19 19
       type: ShapeType.Line,
20 20
       isGenerated: false,
21 21
       name: 'Line',
22
-      parentId: 'page0',
22
+      parentId: 'page1',
23 23
       childIndex: 0,
24 24
       point: [0, 0],
25 25
       direction: [0, 0],

+ 1
- 1
lib/shape-utils/polyline.tsx 查看文件

@@ -17,7 +17,7 @@ const polyline = registerShapeUtils<PolylineShape>({
17 17
       type: ShapeType.Polyline,
18 18
       isGenerated: false,
19 19
       name: 'Polyline',
20
-      parentId: 'page0',
20
+      parentId: 'page1',
21 21
       childIndex: 0,
22 22
       point: [0, 0],
23 23
       points: [[0, 0]],

+ 1
- 1
lib/shape-utils/ray.tsx 查看文件

@@ -18,7 +18,7 @@ const ray = registerShapeUtils<RayShape>({
18 18
       type: ShapeType.Ray,
19 19
       isGenerated: false,
20 20
       name: 'Ray',
21
-      parentId: 'page0',
21
+      parentId: 'page1',
22 22
       childIndex: 0,
23 23
       point: [0, 0],
24 24
       direction: [0, 1],

+ 1
- 1
lib/shape-utils/rectangle.tsx 查看文件

@@ -24,7 +24,7 @@ const rectangle = registerShapeUtils<RectangleShape>({
24 24
       type: ShapeType.Rectangle,
25 25
       isGenerated: false,
26 26
       name: 'Rectangle',
27
-      parentId: 'page0',
27
+      parentId: 'page1',
28 28
       childIndex: 0,
29 29
       point: [0, 0],
30 30
       size: [1, 1],

+ 192
- 0
lib/shape-utils/text.tsx 查看文件

@@ -0,0 +1,192 @@
1
+import { v4 as uuid } from 'uuid'
2
+import * as vec from 'utils/vec'
3
+import { TextShape, ShapeType, FontSize } from 'types'
4
+import { registerShapeUtils } from './index'
5
+import { defaultStyle, getFontStyle, getShapeStyle } from 'lib/shape-styles'
6
+import styled from 'styles'
7
+import state from 'state'
8
+import { useEffect, useRef } from 'react'
9
+
10
+// A div used for measurement
11
+
12
+if (document.getElementById('__textMeasure')) {
13
+  document.getElementById('__textMeasure').remove()
14
+}
15
+
16
+const mdiv = document.createElement('pre')
17
+mdiv.id = '__textMeasure'
18
+mdiv.style.whiteSpace = 'pre'
19
+mdiv.style.width = 'auto'
20
+mdiv.style.border = '1px solid red'
21
+mdiv.style.padding = '4px'
22
+mdiv.style.margin = '0px'
23
+mdiv.style.opacity = '0'
24
+mdiv.style.position = 'absolute'
25
+mdiv.style.top = '-500px'
26
+mdiv.style.left = '0px'
27
+mdiv.style.zIndex = '9999'
28
+document.body.appendChild(mdiv)
29
+
30
+const text = registerShapeUtils<TextShape>({
31
+  isForeignObject: true,
32
+  canChangeAspectRatio: false,
33
+  canEdit: true,
34
+
35
+  boundsCache: new WeakMap([]),
36
+
37
+  create(props) {
38
+    return {
39
+      id: uuid(),
40
+      seed: Math.random(),
41
+      type: ShapeType.Text,
42
+      isGenerated: false,
43
+      name: 'Text',
44
+      parentId: 'page1',
45
+      childIndex: 0,
46
+      point: [0, 0],
47
+      rotation: 0,
48
+      isAspectRatioLocked: false,
49
+      isLocked: false,
50
+      isHidden: false,
51
+      style: defaultStyle,
52
+      text: '',
53
+      size: 'auto',
54
+      fontSize: FontSize.Medium,
55
+      ...props,
56
+    }
57
+  },
58
+
59
+  render(shape, { isEditing }) {
60
+    const { id, text, style } = shape
61
+    const styles = getShapeStyle(style)
62
+
63
+    const font = getFontStyle(shape.fontSize, shape.style)
64
+    const bounds = this.getBounds(shape)
65
+
66
+    const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
67
+      state.send('EDITED_SHAPE', { change: { text: e.currentTarget.value } })
68
+    }
69
+
70
+    return (
71
+      <foreignObject
72
+        id={id}
73
+        x={0}
74
+        y={0}
75
+        width={bounds.width}
76
+        height={bounds.height}
77
+        pointerEvents="none"
78
+      >
79
+        <StyledText
80
+          key={id}
81
+          style={{
82
+            font,
83
+            color: styles.fill,
84
+          }}
85
+          value={text}
86
+          onChange={handleChange}
87
+          isEditing={isEditing}
88
+          onFocus={(e) => e.currentTarget.select()}
89
+        />
90
+      </foreignObject>
91
+    )
92
+  },
93
+
94
+  getBounds(shape) {
95
+    const [minX, minY] = shape.point
96
+    let width: number
97
+    let height: number
98
+
99
+    if (shape.size === 'auto') {
100
+      // Calculate a size by rendering text into a div
101
+      mdiv.innerHTML = shape.text + '&nbsp;'
102
+      mdiv.style.font = getFontStyle(shape.fontSize, shape.style)
103
+      ;[width, height] = [mdiv.offsetWidth, mdiv.offsetHeight]
104
+    } else {
105
+      // Use the shape's explicit size for width and height.
106
+      ;[width, height] = shape.size
107
+    }
108
+
109
+    return {
110
+      minX,
111
+      maxX: minX + width,
112
+      minY,
113
+      maxY: minY + height,
114
+      width,
115
+      height,
116
+    }
117
+  },
118
+
119
+  hitTest(shape, test) {
120
+    return true
121
+  },
122
+
123
+  transform(shape, bounds, { initialShape, transformOrigin, scaleX, scaleY }) {
124
+    if (shape.rotation === 0 && !shape.isAspectRatioLocked) {
125
+      shape.size = [bounds.width, bounds.height]
126
+      shape.point = [bounds.minX, bounds.minY]
127
+    } else {
128
+      if (initialShape.size === 'auto') return
129
+
130
+      shape.size = vec.mul(
131
+        initialShape.size,
132
+        Math.min(Math.abs(scaleX), Math.abs(scaleY))
133
+      )
134
+
135
+      shape.point = [
136
+        bounds.minX +
137
+          (bounds.width - shape.size[0]) *
138
+            (scaleX < 0 ? 1 - transformOrigin[0] : transformOrigin[0]),
139
+        bounds.minY +
140
+          (bounds.height - shape.size[1]) *
141
+            (scaleY < 0 ? 1 - transformOrigin[1] : transformOrigin[1]),
142
+      ]
143
+
144
+      shape.rotation =
145
+        (scaleX < 0 && scaleY >= 0) || (scaleY < 0 && scaleX >= 0)
146
+          ? -initialShape.rotation
147
+          : initialShape.rotation
148
+    }
149
+
150
+    return this
151
+  },
152
+
153
+  transformSingle(shape, bounds) {
154
+    shape.size = [bounds.width, bounds.height]
155
+    shape.point = [bounds.minX, bounds.minY]
156
+    return this
157
+  },
158
+
159
+  onBoundsReset(shape) {
160
+    shape.size = 'auto'
161
+    return this
162
+  },
163
+
164
+  getShouldDelete(shape) {
165
+    return shape.text.length === 0
166
+  },
167
+})
168
+
169
+export default text
170
+
171
+const StyledText = styled('textarea', {
172
+  width: '100%',
173
+  height: '100%',
174
+  border: 'none',
175
+  padding: '4px',
176
+  whiteSpace: 'pre',
177
+  resize: 'none',
178
+  minHeight: 1,
179
+  minWidth: 1,
180
+  outline: 'none',
181
+  backgroundColor: 'transparent',
182
+  overflow: 'hidden',
183
+
184
+  variants: {
185
+    isEditing: {
186
+      true: {
187
+        backgroundColor: '$boundsBg',
188
+        pointerEvents: 'all',
189
+      },
190
+    },
191
+  },
192
+})

二進制
public/VerveineRegular.woff 查看文件


+ 0
- 4
public/vercel.svg 查看文件

@@ -1,4 +0,0 @@
1
-<svg width="283" height="64" viewBox="0 0 283 64" fill="none" 
2
-    xmlns="http://www.w3.org/2000/svg">
3
-    <path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/>
4
-</svg>

+ 40
- 0
state/commands/edit.ts 查看文件

@@ -0,0 +1,40 @@
1
+import Command from './command'
2
+import history from '../history'
3
+import { Data } from 'types'
4
+import { getPage, getShape } from 'utils/utils'
5
+import { EditSnapshot } from 'state/sessions/edit-session'
6
+import { getShapeUtils } from 'lib/shape-utils'
7
+
8
+export default function handleCommand(
9
+  data: Data,
10
+  before: EditSnapshot,
11
+  after: EditSnapshot
12
+) {
13
+  history.execute(
14
+    data,
15
+    new Command({
16
+      name: 'edited_shape',
17
+      category: 'canvas',
18
+      do(data, isInitial) {
19
+        const { initialShape, currentPageId } = after
20
+
21
+        const page = getPage(data, currentPageId)
22
+
23
+        page.shapes[initialShape.id] = initialShape
24
+
25
+        const shape = page.shapes[initialShape.id]
26
+
27
+        if (getShapeUtils(shape).getShouldDelete(shape)) {
28
+          delete page.shapes[initialShape.id]
29
+        }
30
+      },
31
+      undo(data) {
32
+        const { initialShape, currentPageId } = before
33
+
34
+        const page = getPage(data, currentPageId)
35
+
36
+        page.shapes[initialShape.id] = initialShape
37
+      },
38
+    })
39
+  )
40
+}

+ 4
- 0
state/commands/index.ts 查看文件

@@ -23,6 +23,8 @@ import transform from './transform'
23 23
 import transformSingle from './transform-single'
24 24
 import translate from './translate'
25 25
 import ungroup from './ungroup'
26
+import edit from './edit'
27
+import resetBounds from './reset-bounds'
26 28
 
27 29
 const commands = {
28 30
   align,
@@ -35,12 +37,14 @@ const commands = {
35 37
   distribute,
36 38
   draw,
37 39
   duplicate,
40
+  edit,
38 41
   generate,
39 42
   group,
40 43
   handle,
41 44
   move,
42 45
   moveToPage,
43 46
   nudge,
47
+  resetBounds,
44 48
   rotate,
45 49
   rotateCcw,
46 50
   stretch,

+ 31
- 0
state/commands/reset-bounds.ts 查看文件

@@ -0,0 +1,31 @@
1
+import Command from './command'
2
+import history from '../history'
3
+import { Data } from 'types'
4
+import { getPage, getSelectedShapes } from 'utils/utils'
5
+import { current } from 'immer'
6
+import { getShapeUtils } from 'lib/shape-utils'
7
+
8
+export default function resetBoundsCommand(data: Data) {
9
+  const initialShapes = Object.fromEntries(
10
+    getSelectedShapes(current(data)).map((shape) => [shape.id, shape])
11
+  )
12
+
13
+  history.execute(
14
+    data,
15
+    new Command({
16
+      name: 'reset_bounds',
17
+      category: 'canvas',
18
+      do(data) {
19
+        getSelectedShapes(data).forEach((shape) => {
20
+          getShapeUtils(shape).onBoundsReset(shape)
21
+        })
22
+      },
23
+      undo(data) {
24
+        const page = getPage(data)
25
+        getSelectedShapes(data).forEach((shape) => {
26
+          page.shapes[shape.id] = initialShapes[shape.id]
27
+        })
28
+      },
29
+    })
30
+  )
31
+}

+ 36
- 2
state/data.ts 查看文件

@@ -1,9 +1,9 @@
1
-import { Data, ShapeType } from 'types'
1
+import { Data, FontSize, ShapeType } from 'types'
2 2
 import shapeUtils from 'lib/shape-utils'
3 3
 
4 4
 export const defaultDocument: Data['document'] = {
5 5
   id: '0001',
6
-  name: 'My Document',
6
+  name: 'My Default Document',
7 7
   pages: {
8 8
     page1: {
9 9
       id: 'page1',
@@ -11,6 +11,40 @@ export const defaultDocument: Data['document'] = {
11 11
       name: 'Page 1',
12 12
       childIndex: 0,
13 13
       shapes: {
14
+        // textShape0: shapeUtils[ShapeType.Text].create({
15
+        //   id: 'textShape0',
16
+        //   point: [0, 0],
17
+        //   text: 'Short',
18
+        //   childIndex: 0,
19
+        // }),
20
+        // textShape1: shapeUtils[ShapeType.Text].create({
21
+        //   id: 'textShape1',
22
+        //   point: [100, 150],
23
+        //   fontSize: FontSize.Small,
24
+        //   text: 'Well, this is a pretty long title. I hope it all still works',
25
+        //   childIndex: 1,
26
+        // }),
27
+        // textShape2: shapeUtils[ShapeType.Text].create({
28
+        //   id: 'textShape2',
29
+        //   point: [100, 200],
30
+        //   fontSize: FontSize.Medium,
31
+        //   text: 'Well, this is a pretty long title. I hope it all still works',
32
+        //   childIndex: 2,
33
+        // }),
34
+        // textShape3: shapeUtils[ShapeType.Text].create({
35
+        //   id: 'textShape3',
36
+        //   point: [100, 250],
37
+        //   fontSize: FontSize.Large,
38
+        //   text: 'Well, this is a pretty long title. I hope it all still works',
39
+        //   childIndex: 3,
40
+        // }),
41
+        // textShape4: shapeUtils[ShapeType.Text].create({
42
+        //   id: 'textShape4',
43
+        //   point: [100, 300],
44
+        //   fontSize: FontSize.ExtraLarge,
45
+        //   text: 'Well, this is a pretty long title. I hope it all still works',
46
+        //   childIndex: 4,
47
+        // }),
14 48
         // arrowShape0: shapeUtils[ShapeType.Arrow].create({
15 49
         //   id: 'arrowShape0',
16 50
         //   point: [200, 200],

+ 14
- 6
state/inputs.tsx 查看文件

@@ -1,12 +1,13 @@
1 1
 import React from 'react'
2 2
 import { PointerInfo } from 'types'
3
+import * as vec from 'utils/vec'
3 4
 import { isDarwin, getPoint } from 'utils/utils'
4 5
 
5 6
 const DOUBLE_CLICK_DURATION = 300
6 7
 
7 8
 class Inputs {
8 9
   activePointerId?: number
9
-  lastPointerDownTime = 0
10
+  lastPointerUpTime = 0
10 11
   points: Record<string, PointerInfo> = {}
11 12
 
12 13
   touchStart(e: TouchEvent | React.TouchEvent, target: string) {
@@ -119,7 +120,7 @@ class Inputs {
119 120
     return info
120 121
   }
121 122
 
122
-  pointerUp(e: PointerEvent | React.PointerEvent) {
123
+  pointerUp = (e: PointerEvent | React.PointerEvent) => {
123 124
     const { shiftKey, ctrlKey, metaKey, altKey } = e
124 125
 
125 126
     const prev = this.points[e.pointerId]
@@ -137,24 +138,31 @@ class Inputs {
137 138
 
138 139
     delete this.points[e.pointerId]
139 140
     delete this.activePointerId
140
-    this.lastPointerDownTime = Date.now()
141
+
142
+    if (vec.dist(info.origin, info.point) < 8) {
143
+      this.lastPointerUpTime = Date.now()
144
+    }
141 145
 
142 146
     return info
143 147
   }
144 148
 
145
-  wheel(e: WheelEvent) {
149
+  wheel = (e: WheelEvent) => {
146 150
     const { shiftKey, ctrlKey, metaKey, altKey } = e
147 151
     return { point: getPoint(e), shiftKey, ctrlKey, metaKey, altKey }
148 152
   }
149 153
 
150
-  canAccept(pointerId: PointerEvent['pointerId']) {
154
+  canAccept = (pointerId: PointerEvent['pointerId']) => {
151 155
     return (
152 156
       this.activePointerId === undefined || this.activePointerId === pointerId
153 157
     )
154 158
   }
155 159
 
156 160
   isDoubleClick() {
157
-    return Date.now() - this.lastPointerDownTime < DOUBLE_CLICK_DURATION
161
+    const { origin, point } = this.pointer
162
+    return (
163
+      Date.now() - this.lastPointerUpTime < DOUBLE_CLICK_DURATION &&
164
+      vec.dist(origin, point) < 8
165
+    )
158 166
   }
159 167
 
160 168
   get pointer() {

+ 51
- 0
state/sessions/edit-session.ts 查看文件

@@ -0,0 +1,51 @@
1
+import { Data, LineShape, RayShape, Shape } from 'types'
2
+import * as vec from 'utils/vec'
3
+import BaseSession from './base-session'
4
+import commands from 'state/commands'
5
+import { current } from 'immer'
6
+import {
7
+  getPage,
8
+  getSelectedIds,
9
+  getSelectedShapes,
10
+  getShape,
11
+} from 'utils/utils'
12
+import { getShapeUtils } from 'lib/shape-utils'
13
+
14
+export default class EditSession extends BaseSession {
15
+  snapshot: EditSnapshot
16
+
17
+  constructor(data: Data) {
18
+    super(data)
19
+    this.snapshot = getEditSnapshot(data)
20
+  }
21
+
22
+  update(data: Data, change: Partial<Shape>) {
23
+    const initialShape = this.snapshot.initialShape
24
+    const shape = getShape(data, initialShape.id)
25
+    const utils = getShapeUtils(shape)
26
+    Object.entries(change).forEach(([key, value]) => {
27
+      utils.setProperty(shape, key as keyof Shape, value as Shape[keyof Shape])
28
+    })
29
+  }
30
+
31
+  cancel(data: Data) {
32
+    const initialShape = this.snapshot.initialShape
33
+    const page = getPage(data)
34
+    page.shapes[initialShape.id] = initialShape
35
+  }
36
+
37
+  complete(data: Data) {
38
+    commands.edit(data, this.snapshot, getEditSnapshot(data))
39
+  }
40
+}
41
+
42
+export function getEditSnapshot(data: Data) {
43
+  const initialShape = getSelectedShapes(current(data))[0]
44
+
45
+  return {
46
+    currentPageId: data.currentPageId,
47
+    initialShape,
48
+  }
49
+}
50
+
51
+export type EditSnapshot = ReturnType<typeof getEditSnapshot>

+ 2
- 0
state/sessions/index.ts 查看文件

@@ -8,6 +8,7 @@ import TransformSession from './transform-session'
8 8
 import TransformSingleSession from './transform-single-session'
9 9
 import TranslateSession from './translate-session'
10 10
 import HandleSession from './handle-session'
11
+import EditSession from './edit-session'
11 12
 
12 13
 export {
13 14
   ArrowSession,
@@ -20,4 +21,5 @@ export {
20 21
   TransformSingleSession,
21 22
   TranslateSession,
22 23
   HandleSession,
24
+  EditSession,
23 25
 }

+ 131
- 35
state/state.ts 查看文件

@@ -25,6 +25,7 @@ import {
25 25
   getCameraZoom,
26 26
   getSelectedIds,
27 27
   setSelectedIds,
28
+  getPageState,
28 29
 } from 'utils/utils'
29 30
 import {
30 31
   Data,
@@ -42,6 +43,7 @@ import {
42 43
   DashStyle,
43 44
   SizeStyle,
44 45
   ColorStyle,
46
+  FontSize,
45 47
 } from 'types'
46 48
 import session from './session'
47 49
 import { pointInBounds } from 'utils/bounds'
@@ -62,6 +64,7 @@ const initialData: Data = {
62 64
     size: SizeStyle.Medium,
63 65
     color: ColorStyle.Black,
64 66
     dash: DashStyle.Solid,
67
+    fontSize: FontSize.Medium,
65 68
     isFilled: false,
66 69
   },
67 70
   activeTool: 'select',
@@ -69,6 +72,7 @@ const initialData: Data = {
69 72
   boundsRotation: 0,
70 73
   pointedId: null,
71 74
   hoveredId: null,
75
+  editingId: null,
72 76
   currentPageId: 'page1',
73 77
   currentParentId: 'page1',
74 78
   currentCodeFileId: 'file0',
@@ -117,47 +121,16 @@ const state = createState({
117 121
         else: ['zoomCameraToFit', 'zoomCameraToActual'],
118 122
       },
119 123
       on: {
120
-        ZOOMED_CAMERA: {
121
-          do: 'zoomCamera',
122
-        },
123
-        PANNED_CAMERA: {
124
-          do: 'panCamera',
125
-        },
126
-        ZOOMED_TO_ACTUAL: {
127
-          if: 'hasSelection',
128
-          do: 'zoomCameraToSelectionActual',
129
-          else: 'zoomCameraToActual',
130
-        },
131
-        ZOOMED_TO_SELECTION: {
132
-          if: 'hasSelection',
133
-          do: 'zoomCameraToSelection',
134
-        },
135
-        ZOOMED_TO_FIT: ['zoomCameraToFit', 'zoomCameraToActual'],
136
-        ZOOMED_IN: 'zoomIn',
137
-        ZOOMED_OUT: 'zoomOut',
138
-        RESET_CAMERA: 'resetCamera',
139 124
         TOGGLED_SHAPE_LOCK: { if: 'hasSelection', do: 'lockSelection' },
140 125
         TOGGLED_SHAPE_HIDE: { if: 'hasSelection', do: 'hideSelection' },
141 126
         TOGGLED_SHAPE_ASPECT_LOCK: {
142 127
           if: 'hasSelection',
143 128
           do: 'aspectLockSelection',
144 129
         },
145
-        SELECTED_SELECT_TOOL: { to: 'selecting' },
146
-        SELECTED_DRAW_TOOL: { unless: 'isReadOnly', to: 'draw' },
147
-        SELECTED_ARROW_TOOL: { unless: 'isReadOnly', to: 'arrow' },
148
-        SELECTED_DOT_TOOL: { unless: 'isReadOnly', to: 'dot' },
149
-        SELECTED_CIRCLE_TOOL: { unless: 'isReadOnly', to: 'circle' },
150
-        SELECTED_ELLIPSE_TOOL: { unless: 'isReadOnly', to: 'ellipse' },
151
-        SELECTED_RAY_TOOL: { unless: 'isReadOnly', to: 'ray' },
152
-        SELECTED_LINE_TOOL: { unless: 'isReadOnly', to: 'line' },
153
-        SELECTED_POLYLINE_TOOL: { unless: 'isReadOnly', to: 'polyline' },
154
-        SELECTED_RECTANGLE_TOOL: { unless: 'isReadOnly', to: 'rectangle' },
155 130
         TOGGLED_CODE_PANEL_OPEN: 'toggleCodePanel',
156 131
         TOGGLED_STYLE_PANEL_OPEN: 'toggleStylePanel',
157 132
         POINTED_CANVAS: ['closeStylePanel', 'clearCurrentParentId'],
158 133
         CHANGED_STYLE: ['updateStyles', 'applyStylesToSelection'],
159
-        SELECTED_ALL: { to: 'selecting', do: 'selectAll' },
160
-        NUDGED: { do: 'nudgeSelection' },
161 134
         USED_PEN_DEVICE: 'enablePenLock',
162 135
         DISABLED_PEN_LOCK: 'disablePenLock',
163 136
         CLEARED_PAGE: {
@@ -169,6 +142,9 @@ const state = createState({
169 142
         CREATED_PAGE: ['clearSelectedIds', 'createPage'],
170 143
         DELETED_PAGE: { unless: 'hasOnlyOnePage', do: 'deletePage' },
171 144
         LOADED_FROM_FILE: 'loadDocumentFromJson',
145
+        PANNED_CAMERA: {
146
+          do: 'panCamera',
147
+        },
172 148
       },
173 149
       initial: 'selecting',
174 150
       states: {
@@ -206,6 +182,34 @@ const state = createState({
206 182
               if: ['hasSelection', 'selectionIncludesGroups'],
207 183
               do: 'ungroupSelection',
208 184
             },
185
+            SELECTED_ALL: { to: 'selecting', do: 'selectAll' },
186
+            NUDGED: { do: 'nudgeSelection' },
187
+            SELECTED_SELECT_TOOL: { to: 'selecting' },
188
+            SELECTED_DRAW_TOOL: { unless: 'isReadOnly', to: 'draw' },
189
+            SELECTED_ARROW_TOOL: { unless: 'isReadOnly', to: 'arrow' },
190
+            SELECTED_DOT_TOOL: { unless: 'isReadOnly', to: 'dot' },
191
+            SELECTED_CIRCLE_TOOL: { unless: 'isReadOnly', to: 'circle' },
192
+            SELECTED_ELLIPSE_TOOL: { unless: 'isReadOnly', to: 'ellipse' },
193
+            SELECTED_RAY_TOOL: { unless: 'isReadOnly', to: 'ray' },
194
+            SELECTED_LINE_TOOL: { unless: 'isReadOnly', to: 'line' },
195
+            SELECTED_POLYLINE_TOOL: { unless: 'isReadOnly', to: 'polyline' },
196
+            SELECTED_RECTANGLE_TOOL: { unless: 'isReadOnly', to: 'rectangle' },
197
+            ZOOMED_CAMERA: {
198
+              do: 'zoomCamera',
199
+            },
200
+            ZOOMED_TO_ACTUAL: {
201
+              if: 'hasSelection',
202
+              do: 'zoomCameraToSelectionActual',
203
+              else: 'zoomCameraToActual',
204
+            },
205
+            ZOOMED_TO_SELECTION: {
206
+              if: 'hasSelection',
207
+              do: 'zoomCameraToSelection',
208
+            },
209
+            ZOOMED_TO_FIT: ['zoomCameraToFit', 'zoomCameraToActual'],
210
+            ZOOMED_IN: 'zoomIn',
211
+            ZOOMED_OUT: 'zoomOut',
212
+            RESET_CAMERA: 'resetCamera',
209 213
           },
210 214
           initial: 'notPointing',
211 215
           states: {
@@ -226,6 +230,16 @@ const state = createState({
226 230
                   to: 'rotatingSelection',
227 231
                   else: { to: 'transformingSelection' },
228 232
                 },
233
+                STARTED_EDITING_SHAPE: {
234
+                  get: 'firstSelectedShape',
235
+                  if: ['hasSingleSelection', 'canEditSelectedShape'],
236
+                  do: 'setEditingId',
237
+                  to: 'editingShape',
238
+                },
239
+                DOUBLE_POINTED_BOUNDS_HANDLE: {
240
+                  if: 'hasSingleSelection',
241
+                  do: 'resetShapeBounds',
242
+                },
229 243
                 POINTED_HANDLE: { to: 'translatingHandles' },
230 244
                 MOVED_OVER_SHAPE: {
231 245
                   if: 'pointHitsShape',
@@ -240,6 +254,16 @@ const state = createState({
240 254
                 },
241 255
                 UNHOVERED_SHAPE: 'clearHoveredId',
242 256
                 DOUBLE_POINTED_SHAPE: [
257
+                  'setPointedId',
258
+                  {
259
+                    if: 'isPointedShapeSelected',
260
+                    then: {
261
+                      get: 'firstSelectedShape',
262
+                      if: 'canEditSelectedShape',
263
+                      do: 'setEditingId',
264
+                      to: 'editingShape',
265
+                    },
266
+                  },
243 267
                   {
244 268
                     unless: 'isPressingShiftKey',
245 269
                     do: [
@@ -385,6 +409,15 @@ const state = createState({
385 409
             },
386 410
           },
387 411
         },
412
+        editingShape: {
413
+          onEnter: 'startEditSession',
414
+          onExit: 'clearEditingId',
415
+          on: {
416
+            EDITED_SHAPE: { do: 'updateEditSession' },
417
+            BLURRED_SHAPE: { do: 'completeSession', to: 'selecting' },
418
+            CANCELLED: { do: 'cancelSession', to: 'selecting' },
419
+          },
420
+        },
388 421
         pinching: {
389 422
           on: {
390 423
             // Pinching uses hacks.fastPinchCamera
@@ -414,6 +447,35 @@ const state = createState({
414 447
               to: 'pinching.toolPinching',
415 448
             },
416 449
             TOGGLED_TOOL_LOCK: 'toggleToolLock',
450
+            SELECTED_SELECT_TOOL: { to: 'selecting' },
451
+            SELECTED_DRAW_TOOL: { unless: 'isReadOnly', to: 'draw' },
452
+            SELECTED_ARROW_TOOL: { unless: 'isReadOnly', to: 'arrow' },
453
+            SELECTED_DOT_TOOL: { unless: 'isReadOnly', to: 'dot' },
454
+            SELECTED_CIRCLE_TOOL: { unless: 'isReadOnly', to: 'circle' },
455
+            SELECTED_ELLIPSE_TOOL: { unless: 'isReadOnly', to: 'ellipse' },
456
+            SELECTED_RAY_TOOL: { unless: 'isReadOnly', to: 'ray' },
457
+            SELECTED_LINE_TOOL: { unless: 'isReadOnly', to: 'line' },
458
+            SELECTED_POLYLINE_TOOL: { unless: 'isReadOnly', to: 'polyline' },
459
+            SELECTED_RECTANGLE_TOOL: { unless: 'isReadOnly', to: 'rectangle' },
460
+            ZOOMED_CAMERA: {
461
+              do: 'zoomCamera',
462
+            },
463
+            PANNED_CAMERA: {
464
+              do: 'panCamera',
465
+            },
466
+            ZOOMED_TO_ACTUAL: {
467
+              if: 'hasSelection',
468
+              do: 'zoomCameraToSelectionActual',
469
+              else: 'zoomCameraToActual',
470
+            },
471
+            ZOOMED_TO_SELECTION: {
472
+              if: 'hasSelection',
473
+              do: 'zoomCameraToSelection',
474
+            },
475
+            ZOOMED_TO_FIT: ['zoomCameraToFit', 'zoomCameraToActual'],
476
+            ZOOMED_IN: 'zoomIn',
477
+            ZOOMED_OUT: 'zoomOut',
478
+            RESET_CAMERA: 'resetCamera',
417 479
           },
418 480
           states: {
419 481
             draw: {
@@ -781,6 +843,9 @@ const state = createState({
781 843
     newRectangle() {
782 844
       return ShapeType.Rectangle
783 845
     },
846
+    firstSelectedShape(data) {
847
+      return getSelectedShapes(data)[0]
848
+    },
784 849
   },
785 850
   conditions: {
786 851
     isPointingCanvas(data, payload: PointerInfo) {
@@ -799,6 +864,9 @@ const state = createState({
799 864
     isReadOnly(data) {
800 865
       return data.isReadOnly
801 866
     },
867
+    canEditSelectedShape(data, payload, result: Shape) {
868
+      return getShapeUtils(result).canEdit
869
+    },
802 870
     distanceImpliesDrag(data, payload: PointerInfo) {
803 871
       return vec.dist2(payload.origin, payload.point) > 8
804 872
     },
@@ -842,6 +910,9 @@ const state = createState({
842 910
     hasSelection(data) {
843 911
       return getSelectedIds(data).size > 0
844 912
     },
913
+    hasSingleSelection(data) {
914
+      return getSelectedIds(data).size === 1
915
+    },
845 916
     hasMultipleSelection(data) {
846 917
       return getSelectedIds(data).size > 1
847 918
     },
@@ -910,6 +981,14 @@ const state = createState({
910 981
       session.clear()
911 982
     },
912 983
 
984
+    // Editing
985
+    startEditSession(data) {
986
+      session.current = new Sessions.EditSession(data)
987
+    },
988
+    updateEditSession(data, payload: { change: Partial<Shape> }) {
989
+      session.current.update(data, payload.change)
990
+    },
991
+
913 992
     // Brushing
914 993
     startBrushSession(data, payload: PointerInfo) {
915 994
       session.current = new Sessions.BrushSession(
@@ -1197,6 +1276,23 @@ const state = createState({
1197 1276
     ungroupSelection(data) {
1198 1277
       commands.ungroup(data)
1199 1278
     },
1279
+    resetShapeBounds(data) {
1280
+      commands.resetBounds(data)
1281
+    },
1282
+
1283
+    /* --------------------- Editing -------------------- */
1284
+
1285
+    setEditingId(data) {
1286
+      const selectedShape = getSelectedShapes(data)[0]
1287
+      if (getShapeUtils(selectedShape).canEdit) {
1288
+        data.editingId = selectedShape.id
1289
+      }
1290
+
1291
+      getPageState(data).selectedIds = new Set([selectedShape.id])
1292
+    },
1293
+    clearEditingId(data) {
1294
+      data.editingId = null
1295
+    },
1200 1296
 
1201 1297
     /* ---------------------- Tool ---------------------- */
1202 1298
 
@@ -1478,6 +1574,10 @@ const state = createState({
1478 1574
 
1479 1575
     /* ---------------------- Data ---------------------- */
1480 1576
 
1577
+    restoreSavedData(data) {
1578
+      storage.firstLoad(data)
1579
+    },
1580
+
1481 1581
     saveToFileSystem(data) {
1482 1582
       storage.saveToFileSystem(data)
1483 1583
     },
@@ -1511,10 +1611,6 @@ const state = createState({
1511 1611
       storage.saveToLocalStorage(data)
1512 1612
     },
1513 1613
 
1514
-    restoreSavedData(data) {
1515
-      storage.firstLoad(data)
1516
-    },
1517
-
1518 1614
     clearBoundsRotation(data) {
1519 1615
       data.boundsRotation = 0
1520 1616
     },

+ 6
- 6
state/storage.ts 查看文件

@@ -1,6 +1,6 @@
1 1
 import * as fa from 'browser-fs-access'
2 2
 import { Data, Page, PageState, TLDocument } from 'types'
3
-import { lzw_decode, lzw_encode, setToArray } from 'utils/utils'
3
+import { decompress, compress, setToArray } from 'utils/utils'
4 4
 import state from './state'
5 5
 import { current } from 'immer'
6 6
 import { v4 as uuid } from 'uuid'
@@ -66,7 +66,7 @@ class Storage {
66 66
       return false
67 67
     }
68 68
 
69
-    const restoredData: any = JSON.parse(lzw_decode(savedData))
69
+    const restoredData: any = JSON.parse(decompress(savedData))
70 70
 
71 71
     this.load(data, restoredData)
72 72
   }
@@ -80,7 +80,7 @@ class Storage {
80 80
       )
81 81
 
82 82
       if (savedPage !== null) {
83
-        const restored: Page = JSON.parse(lzw_decode(savedPage))
83
+        const restored: Page = JSON.parse(decompress(savedPage))
84 84
         dataToSave.document.pages[pageId] = restored
85 85
       }
86 86
 
@@ -100,7 +100,7 @@ class Storage {
100 100
     // Save current data to local storage
101 101
     localStorage.setItem(
102 102
       storageId(fileId, 'document', fileId),
103
-      lzw_encode(dataToSave)
103
+      compress(dataToSave)
104 104
     )
105 105
   }
106 106
 
@@ -134,7 +134,7 @@ class Storage {
134 134
     const page = data.document.pages[pageId]
135 135
     const json = JSON.stringify(page)
136 136
 
137
-    localStorage.setItem(storageId(fileId, 'page', pageId), lzw_encode(json))
137
+    localStorage.setItem(storageId(fileId, 'page', pageId), compress(json))
138 138
 
139 139
     // Save page state
140 140
 
@@ -166,7 +166,7 @@ class Storage {
166 166
     const savedPage = localStorage.getItem(storageId(fileId, 'page', pageId))
167 167
 
168 168
     if (savedPage !== null) {
169
-      data.document.pages[pageId] = JSON.parse(lzw_decode(savedPage))
169
+      data.document.pages[pageId] = JSON.parse(decompress(savedPage))
170 170
     } else {
171 171
       data.document.pages[pageId] = {
172 172
         id: pageId,

+ 7
- 1
styles/globals.css 查看文件

@@ -1 +1,7 @@
1
-@import url("https://fonts.googleapis.com/css2?family=Recursive:wght@500;700&display=swap");
1
+@import url('https://fonts.googleapis.com/css2?family=Recursive:wght@500;700&display=swap');
2
+@font-face {
3
+  font-family: 'Verveine Regular';
4
+  font-style: normal;
5
+  font-weight: normal;
6
+  src: local('Verveine Regular'), url('/VerveineRegular.woff') format('woff');
7
+}

+ 15
- 1
types.ts 查看文件

@@ -18,12 +18,13 @@ export interface Data {
18 18
     isToolLocked: boolean
19 19
     isPenLocked: boolean
20 20
   }
21
-  currentStyle: ShapeStyles
21
+  currentStyle: ShapeStyles & TextStyles
22 22
   activeTool: ShapeType | 'select'
23 23
   brush?: Bounds
24 24
   boundsRotation: number
25 25
   pointedId?: string
26 26
   hoveredId?: string
27
+  editingId?: string
27 28
   currentPageId: string
28 29
   currentParentId: string
29 30
   currentCodeFileId: string
@@ -100,6 +101,13 @@ export enum DashStyle {
100 101
   Dotted = 'Dotted',
101 102
 }
102 103
 
104
+export enum FontSize {
105
+  Small = 'Small',
106
+  Medium = 'Medium',
107
+  Large = 'Large',
108
+  ExtraLarge = 'ExtraLarge',
109
+}
110
+
103 111
 export type ShapeStyles = {
104 112
   color: ColorStyle
105 113
   size: SizeStyle
@@ -107,6 +115,10 @@ export type ShapeStyles = {
107 115
   isFilled: boolean
108 116
 }
109 117
 
118
+export type TextStyles = {
119
+  fontSize: FontSize
120
+}
121
+
110 122
 export interface BaseShape {
111 123
   id: string
112 124
   seed: number
@@ -182,6 +194,8 @@ export interface ArrowShape extends BaseShape {
182 194
 export interface TextShape extends BaseShape {
183 195
   type: ShapeType.Text
184 196
   text: string
197
+  size: number[] | 'auto'
198
+  fontSize: FontSize
185 199
 }
186 200
 
187 201
 export interface GroupShape extends BaseShape {

+ 3
- 2
utils/utils.ts 查看文件

@@ -1771,8 +1771,9 @@ export function getPoint(
1771 1771
   ]
1772 1772
 }
1773 1773
 
1774
-export function lzw_encode(s: string) {
1774
+export function compress(s: string) {
1775 1775
   return s
1776
+
1776 1777
   const dict = {}
1777 1778
   const data = (s + '').split('')
1778 1779
 
@@ -1805,7 +1806,7 @@ export function lzw_encode(s: string) {
1805 1806
 }
1806 1807
 
1807 1808
 // Decompress an LZW-encoded string
1808
-export function lzw_decode(s: string) {
1809
+export function decompress(s: string) {
1809 1810
   return s
1810 1811
 
1811 1812
   const dict = {}

Loading…
取消
儲存