瀏覽代碼

[fix] copy bindings (#124)

* v0.0.103

* Copies bindings together with shapes that are bound

* Remove old shape bindings from copied shape handles
main
Steve Ruiz 3 年之前
父節點
當前提交
ea66362135
No account linked to committer's email address

+ 1
- 1
lerna.json 查看文件

@@ -1,5 +1,5 @@
1 1
 {
2
-  "version": "0.0.102",
2
+  "version": "0.0.103",
3 3
   "registry": "https://registry.npmjs.org/",
4 4
   "publishConfig": {
5 5
     "access": "public",

+ 3
- 3
packages/core/package.json 查看文件

@@ -1,6 +1,6 @@
1 1
 {
2 2
   "name": "@tldraw/core",
3
-  "version": "0.0.102",
3
+  "version": "0.0.103",
4 4
   "private": false,
5 5
   "description": "A tiny little drawing app (core)",
6 6
   "author": "@steveruizok",
@@ -57,8 +57,8 @@
57 57
     "react-dom": "^16.8 || ^17.0"
58 58
   },
59 59
   "dependencies": {
60
-    "@tldraw/intersect": "^0.0.102",
61
-    "@tldraw/vec": "^0.0.102",
60
+    "@tldraw/intersect": "^0.0.103",
61
+    "@tldraw/vec": "^0.0.103",
62 62
     "@use-gesture/react": "^10.0.0-beta.26"
63 63
   },
64 64
   "gitHead": "5cb031ddc264846ec6732d7179511cddea8ef034"

+ 2
- 2
packages/dev/package.json 查看文件

@@ -1,6 +1,6 @@
1 1
 {
2 2
   "name": "@tldraw/dev",
3
-  "version": "0.0.102",
3
+  "version": "0.0.103",
4 4
   "private": true,
5 5
   "description": "A tiny little drawing app (dev)",
6 6
   "author": "@steveruizok",
@@ -19,7 +19,7 @@
19 19
   ],
20 20
   "sideEffects": false,
21 21
   "dependencies": {
22
-    "@tldraw/tldraw": "^0.0.102",
22
+    "@tldraw/tldraw": "^0.0.103",
23 23
     "idb": "^6.1.2",
24 24
     "react": ">=16.8",
25 25
     "react-dom": "^16.8 || ^17.0",

+ 2
- 2
packages/intersect/package.json 查看文件

@@ -1,6 +1,6 @@
1 1
 {
2 2
   "name": "@tldraw/intersect",
3
-  "version": "0.0.102",
3
+  "version": "0.0.103",
4 4
   "private": false,
5 5
   "description": "A tiny little drawing app (intersect)",
6 6
   "author": "@steveruizok",
@@ -48,7 +48,7 @@
48 48
     "typescript": "^4.4.2"
49 49
   },
50 50
   "dependencies": {
51
-    "@tldraw/vec": "^0.0.102"
51
+    "@tldraw/vec": "^0.0.103"
52 52
   },
53 53
   "gitHead": "5cb031ddc264846ec6732d7179511cddea8ef034"
54 54
 }

+ 5
- 5
packages/tldraw/package.json 查看文件

@@ -1,6 +1,6 @@
1 1
 {
2 2
   "name": "@tldraw/tldraw",
3
-  "version": "0.0.102",
3
+  "version": "0.0.103",
4 4
   "private": false,
5 5
   "description": "A tiny little drawing app (editor)",
6 6
   "author": "@steveruizok",
@@ -64,12 +64,12 @@
64 64
     "@radix-ui/react-tooltip": "^0.0.20",
65 65
     "@stitches/core": "^1.2.0",
66 66
     "@stitches/react": "^1.0.0",
67
-    "@tldraw/core": "^0.0.102",
68
-    "@tldraw/intersect": "^0.0.102",
69
-    "@tldraw/vec": "^0.0.102",
67
+    "@tldraw/core": "^0.0.103",
68
+    "@tldraw/intersect": "^0.0.103",
69
+    "@tldraw/vec": "^0.0.103",
70 70
     "perfect-freehand": "^1.0.12",
71 71
     "react-hotkeys-hook": "^3.4.0",
72 72
     "rko": "^0.5.25"
73 73
   },
74 74
   "gitHead": "5cb031ddc264846ec6732d7179511cddea8ef034"
75
-}
75
+}

+ 4
- 0
packages/tldraw/src/state/__snapshots__/tlstate.spec.ts.snap 查看文件

@@ -16,6 +16,7 @@ Array [
16 16
         },
17 17
         "pages": Object {
18 18
           "page1": Object {
19
+            "bindings": Object {},
19 20
             "shapes": Object {
20 21
               "rect1": Object {
21 22
                 "childIndex": 1,
@@ -53,6 +54,7 @@ Array [
53 54
         },
54 55
         "pages": Object {
55 56
           "page1": Object {
57
+            "bindings": Object {},
56 58
             "shapes": Object {
57 59
               "rect1": undefined,
58 60
             },
@@ -74,6 +76,7 @@ Array [
74 76
         },
75 77
         "pages": Object {
76 78
           "page1": Object {
79
+            "bindings": Object {},
77 80
             "shapes": Object {
78 81
               "rect2": Object {
79 82
                 "childIndex": 1,
@@ -113,6 +116,7 @@ Array [
113 116
         },
114 117
         "pages": Object {
115 118
           "page1": Object {
119
+            "bindings": Object {},
116 120
             "shapes": Object {
117 121
               "rect2": undefined,
118 122
             },

+ 3
- 1
packages/tldraw/src/state/command/create/create.command.spec.ts 查看文件

@@ -21,7 +21,7 @@ describe('Create command', () => {
21 21
 
22 22
   it('does, undoes and redoes command', () => {
23 23
     const shape = { ...tlstate.getShape('rect1'), id: 'rect4' }
24
-    tlstate.create(shape)
24
+    tlstate.create([shape])
25 25
 
26 26
     expect(tlstate.getShape('rect4')).toBeTruthy()
27 27
 
@@ -33,4 +33,6 @@ describe('Create command', () => {
33 33
 
34 34
     expect(tlstate.getShape('rect4')).toBeTruthy()
35 35
   })
36
+
37
+  it.todo('Creates bindings')
36 38
 })

+ 16
- 2
packages/tldraw/src/state/command/create/create.command.ts 查看文件

@@ -1,8 +1,12 @@
1 1
 import type { Patch } from 'rko'
2 2
 import { TLDR } from '~state/tldr'
3
-import type { TLDrawShape, Data, TLDrawCommand } from '~types'
3
+import type { TLDrawShape, Data, TLDrawCommand, TLDrawBinding } from '~types'
4 4
 
5
-export function create(data: Data, shapes: TLDrawShape[]): TLDrawCommand {
5
+export function create(
6
+  data: Data,
7
+  shapes: TLDrawShape[],
8
+  bindings: TLDrawBinding[] = []
9
+): TLDrawCommand {
6 10
   const { currentPageId } = data.appState
7 11
 
8 12
   const beforeShapes: Record<string, Patch<TLDrawShape> | undefined> = {}
@@ -13,6 +17,14 @@ export function create(data: Data, shapes: TLDrawShape[]): TLDrawCommand {
13 17
     afterShapes[shape.id] = shape
14 18
   })
15 19
 
20
+  const beforeBindings: Record<string, Patch<TLDrawBinding> | undefined> = {}
21
+  const afterBindings: Record<string, Patch<TLDrawBinding> | undefined> = {}
22
+
23
+  bindings.forEach((binding) => {
24
+    beforeBindings[binding.id] = undefined
25
+    afterBindings[binding.id] = binding
26
+  })
27
+
16 28
   return {
17 29
     id: 'create',
18 30
     before: {
@@ -20,6 +32,7 @@ export function create(data: Data, shapes: TLDrawShape[]): TLDrawCommand {
20 32
         pages: {
21 33
           [currentPageId]: {
22 34
             shapes: beforeShapes,
35
+            bindings: beforeBindings,
23 36
           },
24 37
         },
25 38
         pageStates: {
@@ -34,6 +47,7 @@ export function create(data: Data, shapes: TLDrawShape[]): TLDrawCommand {
34 47
         pages: {
35 48
           [currentPageId]: {
36 49
             shapes: afterShapes,
50
+            bindings: afterBindings,
37 51
           },
38 52
         },
39 53
         pageStates: {

+ 1
- 1
packages/tldraw/src/state/session/sessions/draw/draw.session.spec.ts 查看文件

@@ -11,7 +11,7 @@ describe('Draw session', () => {
11 11
     expect(tlstate.getShape('draw1')).toBe(undefined)
12 12
 
13 13
     tlstate
14
-      .create({
14
+      .createShapes({
15 15
         id: 'draw1',
16 16
         parentId: 'page1',
17 17
         name: 'Draw',

+ 58
- 2
packages/tldraw/src/state/tlstate.spec.ts 查看文件

@@ -1,13 +1,13 @@
1 1
 import { TLDrawState } from './tlstate'
2 2
 import { mockDocument, TLStateUtils } from '~test'
3
-import { ColorStyle, TLDrawShapeType } from '~types'
3
+import { ArrowShape, ColorStyle, TLDrawShapeType } from '~types'
4 4
 
5 5
 describe('TLDrawState', () => {
6 6
   const tlstate = new TLDrawState()
7 7
 
8 8
   const tlu = new TLStateUtils(tlstate)
9 9
 
10
-  describe('Copy and Paste', () => {
10
+  describe('When copying and pasting...', () => {
11 11
     it('copies a shape', () => {
12 12
       tlstate.loadDocument(mockDocument).deselectAll().copy(['rect1'])
13 13
     })
@@ -45,6 +45,62 @@ describe('TLDrawState', () => {
45 45
 
46 46
       expect(Object.keys(tlstate.page.shapes).length).toBe(1)
47 47
     })
48
+
49
+    it.todo("Pastes in to the top child index of the page's children.")
50
+
51
+    it.todo('Pastes in the correct child index order.')
52
+  })
53
+
54
+  describe('When copying and pasting a shape with bindings', () => {
55
+    it('copies two bound shapes and their binding', () => {
56
+      const tlstate = new TLDrawState()
57
+
58
+      tlstate
59
+        .createShapes(
60
+          { type: TLDrawShapeType.Rectangle, id: 'target1', point: [0, 0], size: [100, 100] },
61
+          { type: TLDrawShapeType.Arrow, id: 'arrow1', point: [200, 200] }
62
+        )
63
+        .select('arrow1')
64
+        .startHandleSession([200, 200], 'start')
65
+        .updateHandleSession([55, 55])
66
+        .completeSession()
67
+
68
+      expect(tlstate.bindings.length).toBe(1)
69
+
70
+      tlstate.selectAll().copy().paste()
71
+
72
+      const newArrow = tlstate.shapes.sort((a, b) => b.childIndex - a.childIndex)[0] as ArrowShape
73
+
74
+      expect(newArrow.handles.start.bindingId).not.toBe(
75
+        tlstate.getShape<ArrowShape>('arrow1').handles.start.bindingId
76
+      )
77
+
78
+      expect(tlstate.bindings.length).toBe(2)
79
+    })
80
+
81
+    it('removes bindings from copied shape handles', () => {
82
+      const tlstate = new TLDrawState()
83
+
84
+      tlstate
85
+        .createShapes(
86
+          { type: TLDrawShapeType.Rectangle, id: 'target1', point: [0, 0], size: [100, 100] },
87
+          { type: TLDrawShapeType.Arrow, id: 'arrow1', point: [200, 200] }
88
+        )
89
+        .select('arrow1')
90
+        .startHandleSession([200, 200], 'start')
91
+        .updateHandleSession([55, 55])
92
+        .completeSession()
93
+
94
+      expect(tlstate.bindings.length).toBe(1)
95
+
96
+      expect(tlstate.getShape<ArrowShape>('arrow1').handles.start.bindingId).toBeDefined()
97
+
98
+      tlstate.select('arrow1').copy().paste()
99
+
100
+      const newArrow = tlstate.shapes.sort((a, b) => b.childIndex - a.childIndex)[0] as ArrowShape
101
+
102
+      expect(newArrow.handles.start.bindingId).toBeUndefined()
103
+    })
48 104
   })
49 105
 
50 106
   describe('Selection', () => {

+ 86
- 31
packages/tldraw/src/state/tlstate.ts 查看文件

@@ -107,7 +107,10 @@ export class TLDrawState extends StateManager<Data> {
107 107
     pointer: 0,
108 108
   }
109 109
 
110
-  clipboard?: TLDrawShape[]
110
+  clipboard?: {
111
+    shapes: TLDrawShape[]
112
+    bindings: TLDrawBinding[]
113
+  }
111 114
 
112 115
   session?: Session
113 116
 
@@ -845,16 +848,32 @@ export class TLDrawState extends StateManager<Data> {
845 848
    * @param ids The ids of the shapes to copy.
846 849
    */
847 850
   copy = (ids = this.selectedIds): this => {
848
-    const clones = ids
849
-      .flatMap((id) => TLDR.getDocumentBranch(this.state, id, this.currentPageId))
850
-      .map((id) => this.getShape(id, this.currentPageId))
851
+    const copyingShapeIds = ids.flatMap((id) =>
852
+      TLDR.getDocumentBranch(this.state, id, this.currentPageId)
853
+    )
851 854
 
852
-    if (clones.length === 0) return this
855
+    const copyingShapes = copyingShapeIds.map((id) =>
856
+      Utils.deepClone(this.getShape(id, this.currentPageId))
857
+    )
858
+
859
+    if (copyingShapes.length === 0) return this
860
+
861
+    const copyingBindings: TLDrawBinding[] = Object.values(this.page.bindings).filter(
862
+      (binding) =>
863
+        copyingShapeIds.includes(binding.fromId) && copyingShapeIds.includes(binding.toId)
864
+    )
853 865
 
854
-    this.clipboard = clones
866
+    this.clipboard = {
867
+      shapes: copyingShapes,
868
+      bindings: copyingBindings,
869
+    }
855 870
 
856 871
     try {
857
-      const text = JSON.stringify({ type: 'tldr/clipboard', shapes: clones })
872
+      const text = JSON.stringify({
873
+        type: 'tldr/clipboard',
874
+        shapes: copyingShapes,
875
+        bindings: copyingBindings,
876
+      })
858 877
 
859 878
       navigator.clipboard.writeText(text).then(
860 879
         () => {
@@ -879,15 +898,47 @@ export class TLDrawState extends StateManager<Data> {
879 898
    * @param point
880 899
    */
881 900
   paste = (point?: number[]) => {
882
-    const pasteInCurrentPage = (shapes: TLDrawShape[]) => {
883
-      const idsMap = Object.fromEntries(
884
-        shapes.map((shape: TLDrawShape) => [shape.id, Utils.uniqueId()])
885
-      )
901
+    const pasteInCurrentPage = (shapes: TLDrawShape[], bindings: TLDrawBinding[]) => {
902
+      const idsMap: Record<string, string> = {}
903
+
904
+      shapes.forEach((shape) => (idsMap[shape.id] = Utils.uniqueId()))
905
+
906
+      bindings.forEach((binding) => (idsMap[binding.id] = Utils.uniqueId()))
907
+
908
+      let startIndex = TLDR.getTopChildIndex(this.state, this.currentPageId)
909
+
910
+      const shapesToPaste = shapes
911
+        .sort((a, b) => a.childIndex - b.childIndex)
912
+        .map((shape) => {
913
+          const parentShapeId = idsMap[shape.parentId]
886 914
 
887
-      const shapesToPaste = shapes.map((shape: TLDrawShape) => ({
888
-        ...shape,
889
-        id: idsMap[shape.id],
890
-        parentId: idsMap[shape.parentId] || this.currentPageId,
915
+          const copy = {
916
+            ...shape,
917
+            id: idsMap[shape.id],
918
+            parentId: parentShapeId || this.currentPageId,
919
+          }
920
+
921
+          if (!parentShapeId) {
922
+            copy.childIndex = startIndex
923
+            startIndex++
924
+          }
925
+
926
+          if (copy.handles) {
927
+            Object.values(copy.handles).forEach((handle) => {
928
+              if (handle.bindingId) {
929
+                handle.bindingId = idsMap[handle.bindingId]
930
+              }
931
+            })
932
+          }
933
+
934
+          return copy
935
+        })
936
+
937
+      const bindingsToPaste = bindings.map((binding) => ({
938
+        ...binding,
939
+        id: idsMap[binding.id],
940
+        toId: idsMap[binding.toId],
941
+        fromId: idsMap[binding.fromId],
891 942
       }))
892 943
 
893 944
       const commonBounds = Utils.getCommonBounds(shapesToPaste.map(TLDR.getBounds))
@@ -915,11 +966,15 @@ export class TLDrawState extends StateManager<Data> {
915 966
         Utils.getBoundsCenter(commonBounds)
916 967
       )
917 968
 
918
-      this.createShapes(
919
-        ...shapesToPaste.map((shape) => ({
920
-          ...shape,
921
-          point: Vec.round(Vec.add(shape.point, delta)),
922
-        }))
969
+      this.create(
970
+        shapesToPaste.map((shape) =>
971
+          TLDR.getShapeUtils(shape.type).create({
972
+            ...shape,
973
+            point: Vec.round(Vec.add(shape.point, delta)),
974
+            parentId: shape.parentId || this.currentPageId,
975
+          })
976
+        ),
977
+        bindingsToPaste
923 978
       )
924 979
     }
925 980
     try {
@@ -929,15 +984,16 @@ export class TLDrawState extends StateManager<Data> {
929 984
 
930 985
       navigator.clipboard.readText().then((result) => {
931 986
         try {
932
-          const data: { type: string; shapes: TLDrawShape[] } = JSON.parse(result)
987
+          const data: { type: string; shapes: TLDrawShape[]; bindings: TLDrawBinding[] } =
988
+            JSON.parse(result)
933 989
 
934 990
           if (data.type !== 'tldr/clipboard') {
935 991
             throw Error('The pasted string was not from the tldraw clipboard.')
936 992
           }
937 993
 
938
-          pasteInCurrentPage(data.shapes)
994
+          pasteInCurrentPage(data.shapes, data.bindings)
939 995
         } catch (e) {
940
-          console.warn(e)
996
+          console.log(e)
941 997
 
942 998
           const shapeId = Utils.uniqueId()
943 999
 
@@ -953,12 +1009,11 @@ export class TLDrawState extends StateManager<Data> {
953 1009
           this.select(shapeId)
954 1010
         }
955 1011
       })
956
-    } catch (e: any) {
957
-      console.warn(e.message)
1012
+    } catch (e) {
958 1013
       // Navigator does not support clipboard. Note that this fallback will
959 1014
       // not support pasting from one document to another.
960 1015
       if (this.clipboard) {
961
-        pasteInCurrentPage(this.clipboard)
1016
+        pasteInCurrentPage(this.clipboard.shapes, this.clipboard.bindings)
962 1017
       }
963 1018
     }
964 1019
 
@@ -1761,12 +1816,12 @@ export class TLDrawState extends StateManager<Data> {
1761 1816
   ): this => {
1762 1817
     if (shapes.length === 0) return this
1763 1818
     return this.create(
1764
-      ...shapes.map((shape) => {
1765
-        return TLDR.getShapeUtils(shape.type).create({
1819
+      shapes.map((shape) =>
1820
+        TLDR.getShapeUtils(shape.type).create({
1766 1821
           ...shape,
1767 1822
           parentId: shape.parentId || this.currentPageId,
1768 1823
         })
1769
-      })
1824
+      )
1770 1825
     )
1771 1826
   }
1772 1827
 
@@ -1805,9 +1860,9 @@ export class TLDrawState extends StateManager<Data> {
1805 1860
    * @param shapes An array of shapes.
1806 1861
    * @command
1807 1862
    */
1808
-  create = (...shapes: TLDrawShape[]): this => {
1863
+  create = (shapes: TLDrawShape[] = [], bindings: TLDrawBinding[] = []): this => {
1809 1864
     if (shapes.length === 0) return this
1810
-    return this.setState(Commands.create(this.state, shapes))
1865
+    return this.setState(Commands.create(this.state, shapes, bindings))
1811 1866
   }
1812 1867
 
1813 1868
   /**

+ 1
- 1
packages/vec/package.json 查看文件

@@ -1,6 +1,6 @@
1 1
 {
2 2
   "name": "@tldraw/vec",
3
-  "version": "0.0.102",
3
+  "version": "0.0.103",
4 4
   "private": false,
5 5
   "description": "A tiny little drawing app (vec)",
6 6
   "author": "@steveruizok",

+ 2
- 2
packages/www/package.json 查看文件

@@ -1,6 +1,6 @@
1 1
 {
2 2
   "name": "www",
3
-  "version": "0.0.102",
3
+  "version": "0.0.103",
4 4
   "private": true,
5 5
   "description": "A tiny little drawing app (site).",
6 6
   "repository": {
@@ -21,7 +21,7 @@
21 21
     "@sentry/react": "^6.13.2",
22 22
     "@sentry/tracing": "^6.13.2",
23 23
     "@stitches/react": "^1.0.0",
24
-    "@tldraw/tldraw": "^0.0.102",
24
+    "@tldraw/tldraw": "^0.0.103",
25 25
     "next": "11.1.2",
26 26
     "next-auth": "3.29.0",
27 27
     "next-pwa": "^5.2.23",

Loading…
取消
儲存