瀏覽代碼

Bumps state-designer

main
Steve Ruiz 3 年之前
父節點
當前提交
eca210da6a

+ 129
- 18
__tests__/commands/group.test.ts 查看文件

@@ -1,31 +1,142 @@
1
+import { ShapeType } from 'types'
1 2
 import TestState from '../test-utils'
2 3
 
3 4
 describe('group command', () => {
4 5
   const tt = new TestState()
5 6
   tt.resetDocumentState()
7
+    .createShape(
8
+      {
9
+        type: ShapeType.Rectangle,
10
+        point: [0, 0],
11
+        size: [100, 100],
12
+        childIndex: 1,
13
+        isLocked: false,
14
+        isHidden: false,
15
+        isAspectRatioLocked: false,
16
+      },
17
+      'rect1'
18
+    )
19
+    .createShape(
20
+      {
21
+        type: ShapeType.Rectangle,
22
+        point: [400, 0],
23
+        size: [100, 100],
24
+        childIndex: 2,
25
+        isHidden: false,
26
+        isLocked: false,
27
+        isAspectRatioLocked: false,
28
+      },
29
+      'rect2'
30
+    )
31
+    .save()
6 32
 
7
-  describe('when one item is selected', () => {
8
-    it('does not change anything', () => {
9
-      // TODO
10
-      null
11
-    })
33
+  // it('deletes the group if it has only one child', () => {
34
+  //   tt.restore()
35
+  //     .clickShape('rect1')
36
+  //     .clickShape('rect2', { shiftKey: true })
37
+  //     .send('GROUPED')
38
+
39
+  //   const groupId = tt.getShape('rect1').parentId
40
+
41
+  //   expect(groupId === tt.data.currentPageId).toBe(false)
42
+
43
+  //   tt.doubleClickShape('rect1')
44
+
45
+  //   tt.send('DELETED')
46
+
47
+  //   expect(tt.getShape(groupId)).toBe(undefined)
48
+  //   expect(tt.getShape('rect2')).toBeTruthy()
49
+  // })
50
+
51
+  it('deletes the group if all children are deleted', () => {
52
+    tt.restore()
53
+      .clickShape('rect1')
54
+      .clickShape('rect2', { shiftKey: true })
55
+      .send('GROUPED')
56
+
57
+    const groupId = tt.getShape('rect1').parentId
58
+
59
+    expect(groupId === tt.data.currentPageId).toBe(false)
60
+
61
+    tt.doubleClickShape('rect1').clickShape('rect2', { shiftKey: true })
62
+
63
+    tt.send('DELETED')
64
+
65
+    expect(tt.getShape(groupId)).toBe(undefined)
66
+  })
67
+
68
+  it('creates a group', () => {
69
+    tt.restore()
70
+      .clickShape('rect1')
71
+      .clickShape('rect2', { shiftKey: true })
72
+      .send('GROUPED')
73
+
74
+    const groupId = tt.getShape('rect1').parentId
75
+
76
+    expect(groupId === tt.data.currentPageId).toBe(false)
12 77
   })
13 78
 
14
-  describe('when multiple items are selected', () => {
15
-    it('does command', () => {
16
-      // TODO
17
-      null
18
-    })
79
+  it('selects the group on single click', () => {
80
+    tt.restore()
81
+      .clickShape('rect1')
82
+      .clickShape('rect2', { shiftKey: true })
83
+      .send('GROUPED')
84
+      .clickShape('rect1')
85
+
86
+    const groupId = tt.getShape('rect1').parentId
87
+
88
+    expect(tt.selectedIds).toEqual([groupId])
89
+  })
90
+
91
+  it('selects the item on double click', () => {
92
+    tt.restore()
93
+      .clickShape('rect1')
94
+      .clickShape('rect2', { shiftKey: true })
95
+      .send('GROUPED')
96
+      .doubleClickShape('rect1')
97
+
98
+    const groupId = tt.getShape('rect1').parentId
99
+
100
+    expect(tt.data.currentParentId).toBe(groupId)
101
+
102
+    expect(tt.selectedIds).toEqual(['rect1'])
103
+  })
104
+
105
+  it('resets currentPageId when clicking the canvas', () => {
106
+    tt.restore()
107
+      .clickShape('rect1')
108
+      .clickShape('rect2', { shiftKey: true })
109
+      .send('GROUPED')
110
+      .doubleClickShape('rect1')
111
+      .clickCanvas()
112
+      .clickShape('rect1')
113
+
114
+    const groupId = tt.getShape('rect1').parentId
115
+
116
+    expect(tt.data.currentParentId).toBe(tt.data.currentPageId)
117
+
118
+    expect(tt.selectedIds).toEqual([groupId])
119
+  })
120
+
121
+  it('creates a group and undoes and redoes', () => {
122
+    tt.restore()
123
+      .clickShape('rect1')
124
+      .clickShape('rect2', { shiftKey: true })
125
+      .send('GROUPED')
126
+
127
+    const groupId = tt.getShape('rect1').parentId
128
+
129
+    expect(groupId === tt.data.currentPageId).toBe(false)
130
+
131
+    tt.undo()
132
+
133
+    expect(tt.getShape('rect1').parentId === tt.data.currentPageId).toBe(true)
134
+    expect(tt.getShape(groupId)).toBe(undefined)
19 135
 
20
-    it('un-does command', () => {
21
-      // TODO
22
-      null
23
-    })
136
+    tt.redo()
24 137
 
25
-    it('re-does command', () => {
26
-      // TODO
27
-      null
28
-    })
138
+    expect(tt.getShape('rect1').parentId === tt.data.currentPageId).toBe(false)
139
+    expect(tt.getShape(groupId)).toBeTruthy()
29 140
   })
30 141
 
31 142
   it('groups shapes with different parents', () => {

+ 4
- 6
__tests__/shapes/arrow.test.ts 查看文件

@@ -1,11 +1,9 @@
1
-import state from 'state'
2
-import * as json from '../__mocks__/document.json'
3
-
4
-state.reset()
5
-state.send('MOUNTED').send('LOADED_FROM_FILE', { json: JSON.stringify(json) })
6
-state.send('CLEARED_PAGE')
1
+import TestState from '../test-utils'
7 2
 
8 3
 describe('arrow shape', () => {
4
+  const tt = new TestState()
5
+  tt.resetDocumentState().send('SELECTED_ARROW_TOOL').save()
6
+
9 7
   it('creates shape', () => {
10 8
     // TODO
11 9
     null

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

@@ -19,11 +19,7 @@ export default function Bounds(): JSX.Element {
19 19
 
20 20
   const bounds = useSelector((s) => s.values.selectedBounds)
21 21
 
22
-  const rotation = useSelector((s) =>
23
-    s.values.selectedIds.length === 1
24
-      ? tld.getSelectedShapes(s.data)[0].rotation
25
-      : 0
26
-  )
22
+  const rotation = useSelector((s) => s.values.selectedRotation)
27 23
 
28 24
   const isAllLocked = useSelector((s) => {
29 25
     const page = tld.getPage(s.data)

+ 1
- 12
components/canvas/bounds/bounds-bg.tsx 查看文件

@@ -33,18 +33,7 @@ export default function BoundsBg(): JSX.Element {
33 33
     s.isInAny('selecting', 'selectPinching')
34 34
   )
35 35
 
36
-  const rotation = useSelector((s) => {
37
-    const selectedIds = s.values.selectedIds
38
-
39
-    if (selectedIds.length === 1) {
40
-      const selected = selectedIds[0]
41
-      const page = tld.getPage(s.data)
42
-
43
-      return page.shapes[selected]?.rotation
44
-    } else {
45
-      return 0
46
-    }
47
-  })
36
+  const rotation = useSelector((s) => s.values.selectedRotation)
48 37
 
49 38
   const isAllHandles = useSelector((s) => {
50 39
     const selectedIds = s.values.selectedIds

+ 3
- 1
jest.config.js 查看文件

@@ -1,7 +1,9 @@
1 1
 module.exports = {
2 2
   testEnvironment: 'jsdom',
3 3
   testPathIgnorePatterns: ['node_modules', '.next'],
4
-  transformIgnorePatterns: ['node_modules/(?!(sucrase|browser-fs-access)/)'],
4
+  transformIgnorePatterns: [
5
+    'node_modules/(?!(sucrase|@state-designer/core|@state-designer/react|browser-fs-access)/)',
6
+  ],
5 7
   transform: {
6 8
     '^.+\\.(ts|tsx|mjs)$': 'babel-jest',
7 9
   },

+ 2
- 1
package.json 查看文件

@@ -46,13 +46,14 @@
46 46
     "@sentry/react": "^6.8.0",
47 47
     "@sentry/tracing": "^6.8.0",
48 48
     "@sentry/webpack-plugin": "^1.15.1",
49
-    "@state-designer/react": "^1.7.4",
49
+    "@state-designer/react": "^2.0.3",
50 50
     "@stitches/react": "^0.2.2",
51 51
     "@types/uuid": "^8.3.0",
52 52
     "browser-fs-access": "^0.17.3",
53 53
     "framer-motion": "^4.1.17",
54 54
     "gtag": "^1.0.1",
55 55
     "idb-keyval": "^5.0.6",
56
+    "immer": "^9.0.5",
56 57
     "ismobilejs": "^1.1.1",
57 58
     "monaco-editor": "^0.25.2",
58 59
     "next": "^11.0.1",

+ 1
- 4
state/commands/command.ts 查看文件

@@ -55,13 +55,10 @@ export class BaseCommand<T extends any> {
55 55
   redo = (data: T, initial = false): void => {
56 56
     if (this.manualSelection) {
57 57
       this.doFn(data, initial)
58
-
59 58
       return
60 59
     }
61 60
 
62
-    if (initial) {
63
-      this.restoreBeforeSelectionState = this.saveSelectionState(data)
64
-    } else {
61
+    if (!initial) {
65 62
       this.restoreBeforeSelectionState(data)
66 63
     }
67 64
 

+ 37
- 0
state/commands/create-shapes.ts 查看文件

@@ -0,0 +1,37 @@
1
+import Command from './command'
2
+import history from '../history'
3
+import { Data, Shape } from 'types'
4
+import tld from 'utils/tld'
5
+import { deepClone } from 'utils'
6
+
7
+// Used when creating new shapes.
8
+
9
+export default function createShapesCommand(
10
+  data: Data,
11
+  shapes: Shape[],
12
+  name = 'create_shapes'
13
+): void {
14
+  const snapshot = deepClone(shapes)
15
+  const shapeIds = snapshot.map((shape) => shape.id)
16
+
17
+  history.execute(
18
+    data,
19
+    new Command({
20
+      name,
21
+      category: 'canvas',
22
+      manualSelection: true,
23
+      do(data) {
24
+        tld.createShapes(data, snapshot)
25
+        tld.setSelectedIds(data, shapeIds)
26
+        data.hoveredId = undefined
27
+        data.currentParentId = undefined
28
+      },
29
+      undo(data) {
30
+        tld.deleteShapes(data, shapeIds)
31
+        tld.setSelectedIds(data, [])
32
+        data.hoveredId = undefined
33
+        data.currentParentId = undefined
34
+      },
35
+    })
36
+  )
37
+}

+ 0
- 111
state/commands/delete-selected.ts 查看文件

@@ -1,111 +0,0 @@
1
-import Command from './command'
2
-import history from '../history'
3
-import { Data, Shape } from 'types'
4
-import { deepClone } from 'utils'
5
-import tld from 'utils/tld'
6
-import { getShapeUtils } from 'state/shape-utils'
7
-
8
-export default function deleteSelected(data: Data): void {
9
-  const selectedShapes = tld.getSelectedShapes(data)
10
-
11
-  const selectedIdsArr = selectedShapes
12
-    .filter((shape) => !shape.isLocked)
13
-    .map((shape) => shape.id)
14
-
15
-  const shapeIdsToDelete = selectedIdsArr.flatMap((id) =>
16
-    tld.getDocumentBranch(data, id)
17
-  )
18
-
19
-  const remainingIds = selectedShapes
20
-    .filter((shape) => shape.isLocked)
21
-    .map((shape) => shape.id)
22
-
23
-  let deletedShapes: Shape[] = []
24
-
25
-  history.execute(
26
-    data,
27
-    new Command({
28
-      name: 'delete_selection',
29
-      category: 'canvas',
30
-      manualSelection: true,
31
-      do(data) {
32
-        // Update selected ids
33
-        tld.setSelectedIds(data, remainingIds)
34
-
35
-        // Recursively delete shapes (and maybe their parents too)
36
-        deletedShapes = deleteShapes(data, shapeIdsToDelete)
37
-      },
38
-      undo(data) {
39
-        const page = tld.getPage(data)
40
-
41
-        // Update selected ids
42
-        tld.setSelectedIds(data, selectedIdsArr)
43
-
44
-        // Restore deleted shapes
45
-        deletedShapes.forEach((shape) => (page.shapes[shape.id] = shape))
46
-
47
-        // Update parents
48
-        deletedShapes.forEach((shape) => {
49
-          if (shape.parentId === data.currentPageId) return
50
-
51
-          const parent = page.shapes[shape.parentId]
52
-
53
-          getShapeUtils(parent)
54
-            .setProperty(parent, 'children', [...parent.children, shape.id])
55
-            .onChildrenChange(
56
-              parent,
57
-              parent.children.map((id) => page.shapes[id])
58
-            )
59
-        })
60
-      },
61
-    })
62
-  )
63
-}
64
-
65
-/** Recursively delete shapes and their parents */
66
-
67
-function deleteShapes(
68
-  data: Data,
69
-  shapeIds: string[],
70
-  shapesDeleted: Shape[] = []
71
-): Shape[] {
72
-  const parentsToDelete: string[] = []
73
-
74
-  const page = tld.getPage(data)
75
-
76
-  const parentIds = new Set(shapeIds.map((id) => page.shapes[id].parentId))
77
-
78
-  // Delete shapes
79
-  shapeIds.forEach((id) => {
80
-    shapesDeleted.push(deepClone(page.shapes[id]))
81
-    delete page.shapes[id]
82
-  })
83
-
84
-  // Update parents
85
-  parentIds.forEach((id) => {
86
-    const parent = page.shapes[id]
87
-
88
-    if (!parent || id === page.id) return
89
-
90
-    getShapeUtils(parent)
91
-      .setProperty(
92
-        parent,
93
-        'children',
94
-        parent.children.filter((childId) => !shapeIds.includes(childId))
95
-      )
96
-      .onChildrenChange(
97
-        parent,
98
-        parent.children.map((id) => page.shapes[id])
99
-      )
100
-
101
-    if (getShapeUtils(parent).shouldDelete(parent)) {
102
-      parentsToDelete.push(parent.id)
103
-    }
104
-  })
105
-
106
-  if (parentsToDelete.length > 0) {
107
-    return deleteShapes(data, parentsToDelete, shapesDeleted)
108
-  }
109
-
110
-  return shapesDeleted
111
-}

+ 37
- 0
state/commands/delete-shapes.ts 查看文件

@@ -0,0 +1,37 @@
1
+import Command from './command'
2
+import history from '../history'
3
+import { Data, Shape } from 'types'
4
+import tld from 'utils/tld'
5
+
6
+export default function deleteShapes(data: Data, shapes: Shape[]): void {
7
+  const initialSelectedIds = [...tld.getSelectedIds(data)]
8
+
9
+  const shapeIdsToDelete = shapes.flatMap((shape) =>
10
+    shape.isLocked ? [] : tld.getDocumentBranch(data, shape.id)
11
+  )
12
+
13
+  const remainingIds = initialSelectedIds.filter(
14
+    (id) => !shapeIdsToDelete.includes(id)
15
+  )
16
+
17
+  // We're going to delete the shapes and their children, too; and possibly
18
+  // their parents, if we delete all of a group shape's children.
19
+  let deletedShapes: Shape[] = []
20
+
21
+  history.execute(
22
+    data,
23
+    new Command({
24
+      name: 'delete_selection',
25
+      category: 'canvas',
26
+      manualSelection: true,
27
+      do(data) {
28
+        deletedShapes = tld.deleteShapes(data, shapeIdsToDelete)
29
+        tld.setSelectedIds(data, remainingIds)
30
+      },
31
+      undo(data) {
32
+        tld.createShapes(data, deletedShapes)
33
+        tld.setSelectedIds(data, initialSelectedIds)
34
+      },
35
+    })
36
+  )
37
+}

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

@@ -2,7 +2,8 @@ import align from './align'
2 2
 import changePage from './change-page'
3 3
 import createPage from './create-page'
4 4
 import deletePage from './delete-page'
5
-import deleteSelected from './delete-selected'
5
+import deleteShapes from './delete-shapes'
6
+import createShapes from './create-shapes'
6 7
 import distribute from './distribute'
7 8
 import doublePointHandle from './double-point-handle'
8 9
 import draw from './draw'
@@ -30,8 +31,9 @@ const commands = {
30 31
   align,
31 32
   changePage,
32 33
   createPage,
34
+  createShapes,
33 35
   deletePage,
34
-  deleteSelected,
36
+  deleteShapes,
35 37
   distribute,
36 38
   doublePointHandle,
37 39
   draw,

+ 19
- 3
state/sessions/handle-session.ts 查看文件

@@ -12,12 +12,20 @@ export default class HandleSession extends BaseSession {
12 12
   shiftKey: boolean
13 13
   initialShape: Shape
14 14
   handleId: string
15
+  isCreating: boolean
15 16
 
16
-  constructor(data: Data, shapeId: string, handleId: string, point: number[]) {
17
+  constructor(
18
+    data: Data,
19
+    shapeId: string,
20
+    handleId: string,
21
+    point: number[],
22
+    isCreating: boolean
23
+  ) {
17 24
     super(data)
18 25
     this.origin = point
19 26
     this.handleId = handleId
20 27
     this.initialShape = deepClone(tld.getShape(data, shapeId))
28
+    this.isCreating = isCreating
21 29
   }
22 30
 
23 31
   update(
@@ -48,13 +56,21 @@ export default class HandleSession extends BaseSession {
48 56
   }
49 57
 
50 58
   cancel(data: Data): void {
51
-    tld.getPage(data).shapes[this.initialShape.id] = this.initialShape
59
+    if (this.isCreating) {
60
+      tld.deleteShapes(data, [this.initialShape])
61
+    } else {
62
+      tld.getPage(data).shapes[this.initialShape.id] = this.initialShape
63
+    }
52 64
   }
53 65
 
54 66
   complete(data: Data): void {
55 67
     const before = this.initialShape
56 68
     const after = deepClone(tld.getShape(data, before.id))
57
-    commands.mutate(data, [before], [after])
69
+    if (this.isCreating) {
70
+      commands.createShapes(data, [after])
71
+    } else {
72
+      commands.mutate(data, [before], [after])
73
+    }
58 74
   }
59 75
 }
60 76
 

+ 4
- 0
state/shape-utils/group.tsx 查看文件

@@ -98,6 +98,10 @@ const group = registerShapeUtils<GroupShape>({
98 98
     return this
99 99
   },
100 100
 
101
+  shouldDelete(shape) {
102
+    return shape.children.length === 0 // should be <= 1
103
+  },
104
+
101 105
   onChildrenChange(shape, children) {
102 106
     if (shape.children.length === 0) return
103 107
 

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

@@ -884,9 +884,9 @@ const state = createState({
884 884
             },
885 885
             arrow: {
886 886
               onEnter: 'setActiveToolArrow',
887
-              initial: 'creating',
887
+              initial: 'idle',
888 888
               states: {
889
-                creating: {
889
+                idle: {
890 890
                   on: {
891 891
                     CANCELLED: { to: 'selecting' },
892 892
                     POINTED_SHAPE: {
@@ -1453,7 +1453,7 @@ const state = createState({
1453 1453
     breakSession(data) {
1454 1454
       session.cancel(data)
1455 1455
       history.disable()
1456
-      commands.deleteSelected(data)
1456
+      commands.deleteShapes(data, tld.getSelectedShapes(data))
1457 1457
       history.enable()
1458 1458
     },
1459 1459
     cancelSession(data) {
@@ -1550,7 +1550,8 @@ const state = createState({
1550 1550
           data,
1551 1551
           shapeId,
1552 1552
           handleId,
1553
-          tld.screenToWorld(inputs.pointer.origin, data)
1553
+          tld.screenToWorld(inputs.pointer.origin, data),
1554
+          false
1554 1555
         )
1555 1556
       )
1556 1557
     },
@@ -1667,7 +1668,8 @@ const state = createState({
1667 1668
           data,
1668 1669
           shapeId,
1669 1670
           handleId,
1670
-          tld.screenToWorld(inputs.pointer.origin, data)
1671
+          tld.screenToWorld(inputs.pointer.origin, data),
1672
+          true
1671 1673
         )
1672 1674
       )
1673 1675
     },
@@ -1778,7 +1780,7 @@ const state = createState({
1778 1780
       commands.toggle(data, 'isAspectRatioLocked')
1779 1781
     },
1780 1782
     deleteSelection(data) {
1781
-      commands.deleteSelected(data)
1783
+      commands.deleteShapes(data, tld.getSelectedShapes(data))
1782 1784
     },
1783 1785
     rotateSelectionCcw(data) {
1784 1786
       commands.rotateCcw(data)
@@ -2250,7 +2252,18 @@ const state = createState({
2250 2252
 
2251 2253
       return commonStyle
2252 2254
     },
2255
+    selectedRotation(data) {
2256
+      const selectedIds = tld.getSelectedIds(data)
2257
+
2258
+      if (selectedIds.length === 1) {
2259
+        const selected = selectedIds[0]
2260
+        const page = tld.getPage(data)
2253 2261
 
2262
+        return page.shapes[selected]?.rotation
2263
+      } else {
2264
+        return 0
2265
+      }
2266
+    },
2254 2267
     shapesToRender(data) {
2255 2268
       const viewport = tld.getViewport(data)
2256 2269
 

+ 150
- 0
utils/tld.ts 查看文件

@@ -15,6 +15,7 @@ import {
15 15
   ShapeTreeNode,
16 16
 } from 'types'
17 17
 import { AssertionError } from 'assert'
18
+import { lerp } from './utils'
18 19
 
19 20
 export default class StateUtils {
20 21
   static getCameraZoom(zoom: number): number {
@@ -93,6 +94,155 @@ export default class StateUtils {
93 94
     return Object.values(page.shapes)
94 95
   }
95 96
 
97
+  /**
98
+   * Add the shapes to the current page.
99
+   *
100
+   * ### Example
101
+   *
102
+   *```ts
103
+   * tld.createShape(data, [shape1])
104
+   * tld.createShape(data, [shape1, shape2, shape3])
105
+   *```
106
+   */
107
+  static createShapes(data: Data, shapes: Shape[]): void {
108
+    const page = this.getPage(data)
109
+    const shapeIds = shapes.map((shape) => shape.id)
110
+
111
+    // Update selected ids
112
+    this.setSelectedIds(data, shapeIds)
113
+
114
+    // Restore deleted shapes
115
+    shapes.forEach((shape) => {
116
+      const newShape = { ...shape }
117
+      page.shapes[shape.id] = newShape
118
+    })
119
+
120
+    // Update parents
121
+    shapes.forEach((shape) => {
122
+      if (shape.parentId === data.currentPageId) return
123
+
124
+      const parent = page.shapes[shape.parentId]
125
+
126
+      getShapeUtils(parent)
127
+        .setProperty(
128
+          parent,
129
+          'children',
130
+          parent.children.includes(shape.id)
131
+            ? parent.children
132
+            : [...parent.children, shape.id]
133
+        )
134
+        .onChildrenChange(
135
+          parent,
136
+          parent.children.map((id) => page.shapes[id])
137
+        )
138
+    })
139
+  }
140
+
141
+  /**
142
+   * Delete the shapes from the current page.
143
+   *
144
+   * ### Example
145
+   *
146
+   *```ts
147
+   * tld.deleteShape(data, [shape1])
148
+   * tld.deleteShape(data, [shape1, shape1, shape1])
149
+   *```
150
+   */
151
+  static deleteShapes(
152
+    data: Data,
153
+    shapeIds: string[] | Shape[],
154
+    shapesDeleted: Shape[] = []
155
+  ): Shape[] {
156
+    const ids =
157
+      typeof shapeIds[0] === 'string'
158
+        ? (shapeIds as string[])
159
+        : (shapeIds as Shape[]).map((shape) => shape.id)
160
+
161
+    const parentsToDelete: string[] = []
162
+
163
+    const page = this.getPage(data)
164
+
165
+    const parentIds = new Set(ids.map((id) => page.shapes[id].parentId))
166
+
167
+    // Delete shapes
168
+    ids.forEach((id) => {
169
+      shapesDeleted.push(deepClone(page.shapes[id]))
170
+      delete page.shapes[id]
171
+    })
172
+
173
+    // Update parents
174
+    parentIds.forEach((id) => {
175
+      const parent = page.shapes[id]
176
+
177
+      // The parent was either deleted or a is a page.
178
+      if (!parent) return
179
+
180
+      const utils = getShapeUtils(parent)
181
+
182
+      // Remove deleted ids from the parent's children and update the parent
183
+      utils
184
+        .setProperty(
185
+          parent,
186
+          'children',
187
+          parent.children.filter((childId) => !ids.includes(childId))
188
+        )
189
+        .onChildrenChange(
190
+          parent,
191
+          parent.children.map((id) => page.shapes[id])
192
+        )
193
+
194
+      if (utils.shouldDelete(parent)) {
195
+        // If the parent decides it should delete, then we need to reparent
196
+        // the parent's remaining children to the parent's parent, and
197
+        // assign them correct child indices, and then delete the parent on
198
+        // the next recursive step.
199
+
200
+        const nextIndex = this.getChildIndexAbove(data, parent.id)
201
+
202
+        const len = parent.children.length
203
+
204
+        // Reparent the children and assign them new child indices
205
+        parent.children.forEach((childId, i) => {
206
+          const child = this.getShape(data, childId)
207
+
208
+          getShapeUtils(child)
209
+            .setProperty(child, 'parentId', parent.parentId)
210
+            .setProperty(
211
+              child,
212
+              'childIndex',
213
+              lerp(parent.childIndex, nextIndex, i / len)
214
+            )
215
+        })
216
+
217
+        if (parent.parentId !== page.id) {
218
+          // If the parent is not a page, then we add the parent's children
219
+          // to the parent's parent shape before emptying that array. If the
220
+          // parent is a page, then we don't need to do this step.
221
+          // TODO: Consider adding explicit children array to page shapes.
222
+          const grandParent = page.shapes[parent.parentId]
223
+
224
+          getShapeUtils(grandParent)
225
+            .setProperty(grandParent, 'children', [...parent.children])
226
+            .onChildrenChange(
227
+              grandParent,
228
+              grandParent.children.map((id) => page.shapes[id])
229
+            )
230
+        }
231
+
232
+        // Empty the parent's children array and delete the parent on the next
233
+        // iteration step.
234
+        getShapeUtils(parent).setProperty(parent, 'children', [])
235
+        parentsToDelete.push(parent.id)
236
+      }
237
+    })
238
+
239
+    if (parentsToDelete.length > 0) {
240
+      return this.deleteShapes(data, parentsToDelete, shapesDeleted)
241
+    }
242
+
243
+    return shapesDeleted
244
+  }
245
+
96 246
   /**
97 247
    * Get the current selected shapes as an array.
98 248
    * @param data

+ 114
- 2944
yarn.lock
文件差異過大導致無法顯示
查看文件


Loading…
取消
儲存