@tldraw/editor 3.15.0-next.f1dfcef63951 → 3.16.0-next.c30b1b5e551a

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/dist-cjs/index.d.ts +159 -44
  2. package/dist-cjs/index.js +20 -16
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/components/SVGContainer.js +1 -1
  5. package/dist-cjs/lib/components/SVGContainer.js.map +2 -2
  6. package/dist-cjs/lib/components/Shape.js +4 -26
  7. package/dist-cjs/lib/components/Shape.js.map +2 -2
  8. package/dist-cjs/lib/components/default-components/DefaultBrush.js +1 -1
  9. package/dist-cjs/lib/components/default-components/DefaultBrush.js.map +2 -2
  10. package/dist-cjs/lib/components/default-components/DefaultCanvas.js +1 -1
  11. package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
  12. package/dist-cjs/lib/components/default-components/DefaultCollaboratorHint.js +1 -1
  13. package/dist-cjs/lib/components/default-components/DefaultCollaboratorHint.js.map +2 -2
  14. package/dist-cjs/lib/components/default-components/DefaultCursor.js +1 -1
  15. package/dist-cjs/lib/components/default-components/DefaultCursor.js.map +2 -2
  16. package/dist-cjs/lib/components/default-components/DefaultGrid.js +1 -1
  17. package/dist-cjs/lib/components/default-components/DefaultGrid.js.map +2 -2
  18. package/dist-cjs/lib/components/default-components/DefaultHandles.js +1 -1
  19. package/dist-cjs/lib/components/default-components/DefaultHandles.js.map +2 -2
  20. package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js +1 -1
  21. package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js.map +2 -2
  22. package/dist-cjs/lib/components/default-components/DefaultShapeWrapper.js +53 -0
  23. package/dist-cjs/lib/components/default-components/DefaultShapeWrapper.js.map +7 -0
  24. package/dist-cjs/lib/components/default-components/DefaultSnapIndictor.js +1 -1
  25. package/dist-cjs/lib/components/default-components/DefaultSnapIndictor.js.map +2 -2
  26. package/dist-cjs/lib/components/default-components/DefaultSpinner.js +27 -15
  27. package/dist-cjs/lib/components/default-components/DefaultSpinner.js.map +3 -3
  28. package/dist-cjs/lib/config/TLUserPreferences.js +7 -1
  29. package/dist-cjs/lib/config/TLUserPreferences.js.map +2 -2
  30. package/dist-cjs/lib/editor/Editor.js +88 -43
  31. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  32. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +96 -101
  33. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
  34. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js +7 -2
  35. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
  36. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  37. package/dist-cjs/lib/editor/tools/StateNode.js +20 -1
  38. package/dist-cjs/lib/editor/tools/StateNode.js.map +2 -2
  39. package/dist-cjs/lib/editor/types/misc-types.js.map +1 -1
  40. package/dist-cjs/lib/hooks/useEditorComponents.js +2 -0
  41. package/dist-cjs/lib/hooks/useEditorComponents.js.map +2 -2
  42. package/dist-cjs/lib/license/Watermark.js +2 -2
  43. package/dist-cjs/lib/license/Watermark.js.map +2 -2
  44. package/dist-cjs/lib/primitives/geometry/Arc2d.js +1 -1
  45. package/dist-cjs/lib/primitives/geometry/Arc2d.js.map +2 -2
  46. package/dist-cjs/lib/primitives/geometry/Circle2d.js +1 -1
  47. package/dist-cjs/lib/primitives/geometry/Circle2d.js.map +2 -2
  48. package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js +3 -1
  49. package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js.map +2 -2
  50. package/dist-cjs/lib/primitives/geometry/Ellipse2d.js +1 -1
  51. package/dist-cjs/lib/primitives/geometry/Ellipse2d.js.map +2 -2
  52. package/dist-cjs/lib/primitives/geometry/geometry-constants.js +2 -2
  53. package/dist-cjs/lib/primitives/geometry/geometry-constants.js.map +2 -2
  54. package/dist-cjs/lib/primitives/intersect.js +4 -4
  55. package/dist-cjs/lib/primitives/intersect.js.map +2 -2
  56. package/dist-cjs/lib/primitives/utils.js +4 -0
  57. package/dist-cjs/lib/primitives/utils.js.map +2 -2
  58. package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js +0 -1
  59. package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js.map +2 -2
  60. package/dist-cjs/version.js +3 -3
  61. package/dist-cjs/version.js.map +1 -1
  62. package/dist-esm/index.d.mts +159 -44
  63. package/dist-esm/index.mjs +47 -41
  64. package/dist-esm/index.mjs.map +2 -2
  65. package/dist-esm/lib/components/SVGContainer.mjs +1 -1
  66. package/dist-esm/lib/components/SVGContainer.mjs.map +2 -2
  67. package/dist-esm/lib/components/Shape.mjs +4 -26
  68. package/dist-esm/lib/components/Shape.mjs.map +2 -2
  69. package/dist-esm/lib/components/default-components/DefaultBrush.mjs +1 -1
  70. package/dist-esm/lib/components/default-components/DefaultBrush.mjs.map +2 -2
  71. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +1 -1
  72. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  73. package/dist-esm/lib/components/default-components/DefaultCollaboratorHint.mjs +1 -1
  74. package/dist-esm/lib/components/default-components/DefaultCollaboratorHint.mjs.map +2 -2
  75. package/dist-esm/lib/components/default-components/DefaultCursor.mjs +1 -1
  76. package/dist-esm/lib/components/default-components/DefaultCursor.mjs.map +2 -2
  77. package/dist-esm/lib/components/default-components/DefaultGrid.mjs +1 -1
  78. package/dist-esm/lib/components/default-components/DefaultGrid.mjs.map +2 -2
  79. package/dist-esm/lib/components/default-components/DefaultHandles.mjs +1 -1
  80. package/dist-esm/lib/components/default-components/DefaultHandles.mjs.map +2 -2
  81. package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs +1 -1
  82. package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs.map +2 -2
  83. package/dist-esm/lib/components/default-components/DefaultShapeWrapper.mjs +23 -0
  84. package/dist-esm/lib/components/default-components/DefaultShapeWrapper.mjs.map +7 -0
  85. package/dist-esm/lib/components/default-components/DefaultSnapIndictor.mjs +1 -1
  86. package/dist-esm/lib/components/default-components/DefaultSnapIndictor.mjs.map +2 -2
  87. package/dist-esm/lib/components/default-components/DefaultSpinner.mjs +17 -15
  88. package/dist-esm/lib/components/default-components/DefaultSpinner.mjs.map +2 -2
  89. package/dist-esm/lib/config/TLUserPreferences.mjs +7 -1
  90. package/dist-esm/lib/config/TLUserPreferences.mjs.map +2 -2
  91. package/dist-esm/lib/editor/Editor.mjs +88 -43
  92. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  93. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +96 -101
  94. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
  95. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +7 -2
  96. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
  97. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  98. package/dist-esm/lib/editor/tools/StateNode.mjs +20 -1
  99. package/dist-esm/lib/editor/tools/StateNode.mjs.map +2 -2
  100. package/dist-esm/lib/hooks/useEditorComponents.mjs +4 -0
  101. package/dist-esm/lib/hooks/useEditorComponents.mjs.map +2 -2
  102. package/dist-esm/lib/license/Watermark.mjs +2 -2
  103. package/dist-esm/lib/license/Watermark.mjs.map +2 -2
  104. package/dist-esm/lib/primitives/geometry/Arc2d.mjs +2 -2
  105. package/dist-esm/lib/primitives/geometry/Arc2d.mjs.map +2 -2
  106. package/dist-esm/lib/primitives/geometry/Circle2d.mjs +2 -2
  107. package/dist-esm/lib/primitives/geometry/Circle2d.mjs.map +2 -2
  108. package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs +3 -1
  109. package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs.map +2 -2
  110. package/dist-esm/lib/primitives/geometry/Ellipse2d.mjs +2 -2
  111. package/dist-esm/lib/primitives/geometry/Ellipse2d.mjs.map +2 -2
  112. package/dist-esm/lib/primitives/geometry/geometry-constants.mjs +2 -2
  113. package/dist-esm/lib/primitives/geometry/geometry-constants.mjs.map +2 -2
  114. package/dist-esm/lib/primitives/intersect.mjs +5 -5
  115. package/dist-esm/lib/primitives/intersect.mjs.map +2 -2
  116. package/dist-esm/lib/primitives/utils.mjs +4 -0
  117. package/dist-esm/lib/primitives/utils.mjs.map +2 -2
  118. package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs +0 -1
  119. package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs.map +2 -2
  120. package/dist-esm/version.mjs +3 -3
  121. package/dist-esm/version.mjs.map +1 -1
  122. package/editor.css +21 -27
  123. package/package.json +9 -8
  124. package/src/index.ts +68 -62
  125. package/src/lib/components/SVGContainer.tsx +1 -1
  126. package/src/lib/components/Shape.tsx +6 -21
  127. package/src/lib/components/default-components/DefaultBrush.tsx +1 -1
  128. package/src/lib/components/default-components/DefaultCanvas.tsx +1 -1
  129. package/src/lib/components/default-components/DefaultCollaboratorHint.tsx +1 -1
  130. package/src/lib/components/default-components/DefaultCursor.tsx +1 -1
  131. package/src/lib/components/default-components/DefaultGrid.tsx +1 -1
  132. package/src/lib/components/default-components/DefaultHandles.tsx +5 -1
  133. package/src/lib/components/default-components/DefaultShapeIndicator.tsx +1 -1
  134. package/src/lib/components/default-components/DefaultShapeWrapper.tsx +35 -0
  135. package/src/lib/components/default-components/DefaultSnapIndictor.tsx +1 -1
  136. package/src/lib/components/default-components/DefaultSpinner.tsx +12 -12
  137. package/src/lib/config/TLUserPreferences.ts +7 -0
  138. package/src/lib/editor/Editor.test.ts +407 -0
  139. package/src/lib/editor/Editor.ts +106 -44
  140. package/src/lib/editor/managers/TextManager/TextManager.ts +108 -128
  141. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +21 -0
  142. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +8 -0
  143. package/src/lib/editor/shapes/ShapeUtil.ts +57 -0
  144. package/src/lib/editor/tools/StateNode.test.ts +285 -0
  145. package/src/lib/editor/tools/StateNode.ts +27 -1
  146. package/src/lib/editor/types/misc-types.ts +19 -0
  147. package/src/lib/hooks/useEditorComponents.tsx +8 -2
  148. package/src/lib/license/LicenseManager.test.ts +1 -1
  149. package/src/lib/license/Watermark.tsx +2 -2
  150. package/src/lib/primitives/geometry/Arc2d.ts +2 -2
  151. package/src/lib/primitives/geometry/Circle2d.ts +2 -2
  152. package/src/lib/primitives/geometry/CubicBezier2d.ts +4 -1
  153. package/src/lib/primitives/geometry/Ellipse2d.ts +2 -2
  154. package/src/lib/primitives/geometry/geometry-constants.ts +2 -1
  155. package/src/lib/primitives/intersect.test.ts +946 -0
  156. package/src/lib/primitives/intersect.ts +12 -5
  157. package/src/lib/primitives/utils.ts +11 -0
  158. package/src/lib/utils/sync/TLLocalSyncClient.ts +0 -1
  159. package/src/version.ts +3 -3
  160. package/src/lib/test/currentToolIdMask.test.ts +0 -49
@@ -584,6 +584,15 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
584
584
  */
585
585
  onResizeEnd?(initial: Shape, current: Shape): TLShapePartial<Shape> | void
586
586
 
587
+ /**
588
+ * A callback called when a shape resize is cancelled.
589
+ *
590
+ * @param initial - The shape at the start of the resize.
591
+ * @param current - The current shape.
592
+ * @public
593
+ */
594
+ onResizeCancel?(initial: Shape, current: Shape): void
595
+
587
596
  /**
588
597
  * A callback called when a shape starts being translated.
589
598
  *
@@ -613,6 +622,25 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
613
622
  */
614
623
  onTranslateEnd?(initial: Shape, current: Shape): TLShapePartial<Shape> | void
615
624
 
625
+ /**
626
+ * A callback called when a shape translation is cancelled.
627
+ *
628
+ * @param initial - The shape at the start of the translation.
629
+ * @param current - The current shape.
630
+ * @public
631
+ */
632
+ onTranslateCancel?(initial: Shape, current: Shape): void
633
+
634
+ /**
635
+ * A callback called when a shape's handle starts being dragged.
636
+ *
637
+ * @param shape - The shape.
638
+ * @param info - An object containing the handle and whether the handle is 'precise' or not.
639
+ * @returns A change to apply to the shape, or void.
640
+ * @public
641
+ */
642
+ onHandleDragStart?(shape: Shape, info: TLHandleDragInfo<Shape>): TLShapePartial<Shape> | void
643
+
616
644
  /**
617
645
  * A callback called when a shape's handle changes.
618
646
  *
@@ -623,6 +651,25 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
623
651
  */
624
652
  onHandleDrag?(shape: Shape, info: TLHandleDragInfo<Shape>): TLShapePartial<Shape> | void
625
653
 
654
+ /**
655
+ * A callback called when a shape's handle finishes being dragged.
656
+ *
657
+ * @param current - The current shape.
658
+ * @param info - An object containing the handle and whether the handle is 'precise' or not.
659
+ * @returns A change to apply to the shape, or void.
660
+ * @public
661
+ */
662
+ onHandleDragEnd?(current: Shape, info: TLHandleDragInfo<Shape>): TLShapePartial<Shape> | void
663
+
664
+ /**
665
+ * A callback called when a shape's handle drag is cancelled.
666
+ *
667
+ * @param current - The current shape.
668
+ * @param info - An object containing the handle and whether the handle is 'precise' or not.
669
+ * @public
670
+ */
671
+ onHandleDragCancel?(current: Shape, info: TLHandleDragInfo<Shape>): void
672
+
626
673
  /**
627
674
  * A callback called when a shape starts being rotated.
628
675
  *
@@ -652,6 +699,15 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
652
699
  */
653
700
  onRotateEnd?(initial: Shape, current: Shape): TLShapePartial<Shape> | void
654
701
 
702
+ /**
703
+ * A callback called when a shape rotation is cancelled.
704
+ *
705
+ * @param initial - The shape at the start of the rotation.
706
+ * @param current - The current shape.
707
+ * @public
708
+ */
709
+ onRotateCancel?(initial: Shape, current: Shape): void
710
+
655
711
  /**
656
712
  * Not currently used.
657
713
  *
@@ -819,5 +875,6 @@ export interface TLResizeInfo<T extends TLShape> {
819
875
  export interface TLHandleDragInfo<T extends TLShape> {
820
876
  handle: TLHandle
821
877
  isPrecise: boolean
878
+ isCreatingShape: boolean
822
879
  initial?: T | undefined
823
880
  }
@@ -0,0 +1,285 @@
1
+ import { createTLStore } from '../../config/createTLStore'
2
+ import { Editor } from '../Editor'
3
+ import { StateNode } from './StateNode'
4
+
5
+ describe('StateNode.addChild', () => {
6
+ // Test state node classes for addChild tests
7
+ class ParentState extends StateNode {
8
+ static override id = 'parent'
9
+ static override initial = 'child1'
10
+ static override children() {
11
+ return [ChildState1]
12
+ }
13
+ }
14
+
15
+ class ChildState1 extends StateNode {
16
+ static override id = 'child1'
17
+ }
18
+
19
+ class ChildState2 extends StateNode {
20
+ static override id = 'child2'
21
+ }
22
+
23
+ class ChildState3 extends StateNode {
24
+ static override id = 'child3'
25
+ }
26
+
27
+ class LeafState extends StateNode {
28
+ static override id = 'leaf'
29
+ }
30
+
31
+ class RootState extends StateNode {
32
+ static override id = 'root'
33
+ static override initial = 'child1'
34
+ static override children() {
35
+ return [ChildState1]
36
+ }
37
+ }
38
+
39
+ class RootStateWithoutChildren extends StateNode {
40
+ static override id = 'rootWithoutChildren'
41
+ }
42
+
43
+ let editor: Editor
44
+
45
+ beforeEach(() => {
46
+ editor = new Editor({
47
+ initialState: 'parent',
48
+ shapeUtils: [],
49
+ bindingUtils: [],
50
+ tools: [
51
+ ParentState,
52
+ ChildState1,
53
+ ChildState2,
54
+ ChildState3,
55
+ LeafState,
56
+ RootState,
57
+ RootStateWithoutChildren,
58
+ ],
59
+ store: createTLStore({ shapeUtils: [], bindingUtils: [] }),
60
+ getContainer: () => document.body,
61
+ })
62
+ })
63
+
64
+ it('should add a child to a branch state node', () => {
65
+ const parentState = editor.root.children!['parent'] as ParentState
66
+
67
+ // Initially should have one child
68
+ expect(Object.keys(parentState.children!)).toHaveLength(1)
69
+ expect(parentState.children!['child1']).toBeDefined()
70
+
71
+ // Add a new child
72
+ parentState.addChild(ChildState2)
73
+
74
+ // Should now have two children
75
+ expect(Object.keys(parentState.children!)).toHaveLength(2)
76
+ expect(parentState.children!['child1']).toBeDefined()
77
+ expect(parentState.children!['child2']).toBeDefined()
78
+ expect(parentState.children!['child2']).toBeInstanceOf(ChildState2)
79
+ })
80
+
81
+ it('should add a child to a root state node', () => {
82
+ const rootState = editor.root.children!['root'] as RootState
83
+
84
+ // Initially should have one child
85
+ expect(Object.keys(rootState.children!)).toHaveLength(1)
86
+ expect(rootState.children!['child1']).toBeDefined()
87
+
88
+ // Add a new child
89
+ rootState.addChild(ChildState2)
90
+
91
+ // Should now have two children
92
+ expect(Object.keys(rootState.children!)).toHaveLength(2)
93
+ expect(rootState.children!['child1']).toBeDefined()
94
+ expect(rootState.children!['child2']).toBeDefined()
95
+ expect(rootState.children!['child2']).toBeInstanceOf(ChildState2)
96
+ })
97
+
98
+ it('should throw an error when trying to add a child to a leaf state node', () => {
99
+ const leafState = editor.root.children!['leaf'] as LeafState
100
+
101
+ // Leaf state should not have children
102
+ expect(leafState.children).toBeUndefined()
103
+
104
+ // Should throw an error when trying to add a child
105
+ expect(() => {
106
+ leafState.addChild(ChildState2)
107
+ }).toThrow('StateNode.addChild: cannot add child to a leaf node')
108
+ })
109
+
110
+ it('should return the parent state node for chaining', () => {
111
+ const parentState = editor.root.children!['parent'] as ParentState
112
+
113
+ const result = parentState.addChild(ChildState2)
114
+
115
+ expect(result).toBe(parentState)
116
+ })
117
+
118
+ it('should create the child with the correct editor and parent', () => {
119
+ const parentState = editor.root.children!['parent'] as ParentState
120
+
121
+ parentState.addChild(ChildState2)
122
+ const childState = parentState.children!['child2'] as ChildState2
123
+
124
+ expect(childState.editor).toBe(editor)
125
+ expect(childState.parent).toBe(parentState)
126
+ })
127
+
128
+ it('should allow adding multiple children', () => {
129
+ const parentState = editor.root.children!['parent'] as ParentState
130
+
131
+ // Add multiple children
132
+ parentState.addChild(ChildState2).addChild(ChildState3)
133
+
134
+ // Should have three children
135
+ expect(Object.keys(parentState.children!)).toHaveLength(3)
136
+ expect(parentState.children!['child1']).toBeDefined()
137
+ expect(parentState.children!['child2']).toBeDefined()
138
+ expect(parentState.children!['child3']).toBeDefined()
139
+ expect(parentState.children!['child2']).toBeInstanceOf(ChildState2)
140
+ expect(parentState.children!['child3']).toBeInstanceOf(ChildState3)
141
+ })
142
+
143
+ it('should allow transitioning to added children', () => {
144
+ const parentState = editor.root.children!['parent'] as ParentState
145
+
146
+ // Add a new child
147
+ parentState.addChild(ChildState2)
148
+
149
+ // Should be able to transition to the new child
150
+ expect(() => {
151
+ parentState.transition('child2')
152
+ }).not.toThrow()
153
+
154
+ // The current state should be the new child
155
+ expect(parentState.getCurrent()?.id).toBe('child2')
156
+ })
157
+
158
+ it('should maintain existing children when adding new ones', () => {
159
+ const parentState = editor.root.children!['parent'] as ParentState
160
+ const originalChild = parentState.children!['child1']
161
+
162
+ // Add a new child
163
+ parentState.addChild(ChildState2)
164
+
165
+ // Original child should still exist and be the same instance
166
+ expect(parentState.children!['child1']).toBe(originalChild)
167
+ expect(parentState.children!['child1']).toBeInstanceOf(ChildState1)
168
+ })
169
+
170
+ it('should initialize children object for root nodes without static children', () => {
171
+ // Create a StateNode directly as a root node (no parent)
172
+ const mockEditor = {} as Editor
173
+ const rootStateWithoutChildren = new RootStateWithoutChildren(mockEditor, undefined)
174
+
175
+ // Root state without static children should not have children initially
176
+ expect(rootStateWithoutChildren.children).toBeUndefined()
177
+
178
+ // Adding a child should initialize the children object
179
+ rootStateWithoutChildren.addChild(ChildState2)
180
+
181
+ // Should now have children object with the added child
182
+ expect(rootStateWithoutChildren.children).toBeDefined()
183
+ expect(Object.keys(rootStateWithoutChildren.children!)).toHaveLength(1)
184
+ expect(rootStateWithoutChildren.children!['child2']).toBeDefined()
185
+ expect(rootStateWithoutChildren.children!['child2']).toBeInstanceOf(ChildState2)
186
+ })
187
+
188
+ it('should throw an error when trying to add a child with a duplicate ID', () => {
189
+ const parentState = editor.root.children!['parent'] as ParentState
190
+
191
+ // Initially should have one child
192
+ expect(Object.keys(parentState.children!)).toHaveLength(1)
193
+ expect(parentState.children!['child1']).toBeDefined()
194
+
195
+ // Should throw an error when trying to add a child with the same ID
196
+ expect(() => {
197
+ parentState.addChild(ChildState1)
198
+ }).toThrow("StateNode.addChild: a child with id 'child1' already exists")
199
+
200
+ // Should still have only one child
201
+ expect(Object.keys(parentState.children!)).toHaveLength(1)
202
+ expect(parentState.children!['child1']).toBeDefined()
203
+ })
204
+
205
+ it('should throw an error when trying to add a child with a duplicate ID to a root state', () => {
206
+ const rootState = editor.root.children!['root'] as RootState
207
+
208
+ // Initially should have one child
209
+ expect(Object.keys(rootState.children!)).toHaveLength(1)
210
+ expect(rootState.children!['child1']).toBeDefined()
211
+
212
+ // Should throw an error when trying to add a child with the same ID
213
+ expect(() => {
214
+ rootState.addChild(ChildState1)
215
+ }).toThrow("StateNode.addChild: a child with id 'child1' already exists")
216
+
217
+ // Should still have only one child
218
+ expect(Object.keys(rootState.children!)).toHaveLength(1)
219
+ expect(rootState.children!['child1']).toBeDefined()
220
+ })
221
+
222
+ it('should throw an error when trying to add a child with a duplicate ID to a root state without static children', () => {
223
+ // Create a StateNode directly as a root node (no parent)
224
+ const mockEditor = {} as Editor
225
+ const rootStateWithoutChildren = new RootStateWithoutChildren(mockEditor, undefined)
226
+
227
+ // Add a child first
228
+ rootStateWithoutChildren.addChild(ChildState1)
229
+
230
+ // Should throw an error when trying to add a child with the same ID
231
+ expect(() => {
232
+ rootStateWithoutChildren.addChild(ChildState1)
233
+ }).toThrow("StateNode.addChild: a child with id 'child1' already exists")
234
+
235
+ // Should still have only one child
236
+ expect(Object.keys(rootStateWithoutChildren.children!)).toHaveLength(1)
237
+ expect(rootStateWithoutChildren.children!['child1']).toBeDefined()
238
+ })
239
+ })
240
+
241
+ describe('current tool id mask', () => {
242
+ // Tool mask test classes
243
+ class ToolA extends StateNode {
244
+ static override id = 'A'
245
+ }
246
+
247
+ class ToolB extends StateNode {
248
+ static override id = 'B'
249
+ }
250
+
251
+ class ToolC extends StateNode {
252
+ static override id = 'C'
253
+
254
+ override onEnter() {
255
+ this.setCurrentToolIdMask('A')
256
+ }
257
+ }
258
+
259
+ let toolMaskEditor: Editor
260
+
261
+ beforeEach(() => {
262
+ toolMaskEditor = new Editor({
263
+ initialState: 'A',
264
+ shapeUtils: [],
265
+ bindingUtils: [],
266
+ tools: [ToolA, ToolB, ToolC],
267
+ store: createTLStore({ shapeUtils: [], bindingUtils: [] }),
268
+ getContainer: () => document.body,
269
+ })
270
+ })
271
+
272
+ it('starts with the correct tool id', () => {
273
+ expect(toolMaskEditor.getCurrentToolId()).toBe('A')
274
+ })
275
+
276
+ it('updates the current tool id', () => {
277
+ toolMaskEditor.setCurrentTool('B')
278
+ expect(toolMaskEditor.getCurrentToolId()).toBe('B')
279
+ })
280
+
281
+ it('masks the current tool id', () => {
282
+ toolMaskEditor.setCurrentTool('C')
283
+ expect(toolMaskEditor.getCurrentToolId()).toBe('A')
284
+ })
285
+ })
@@ -62,7 +62,7 @@ export abstract class StateNode implements Partial<TLEventHandlers> {
62
62
 
63
63
  this.parent = parent ?? ({} as any)
64
64
 
65
- if (this.parent) {
65
+ if (parent) {
66
66
  if (children && initial) {
67
67
  this.type = 'branch'
68
68
  this.initial = initial
@@ -238,6 +238,32 @@ export abstract class StateNode implements Partial<TLEventHandlers> {
238
238
  this._currentToolIdMask.set(id)
239
239
  }
240
240
 
241
+ /**
242
+ * Add a child node to this state node.
243
+ *
244
+ * @public
245
+ */
246
+ addChild(childConstructor: TLStateNodeConstructor): this {
247
+ if (this.type === 'leaf') {
248
+ throw new Error('StateNode.addChild: cannot add child to a leaf node')
249
+ }
250
+
251
+ // Initialize children if it's undefined (for root nodes without static children)
252
+ if (!this.children) {
253
+ this.children = {}
254
+ }
255
+
256
+ const child = new childConstructor(this.editor, this)
257
+
258
+ // Check if a child with this ID already exists
259
+ if (this.children[child.id]) {
260
+ throw new Error(`StateNode.addChild: a child with id '${child.id}' already exists`)
261
+ }
262
+
263
+ this.children[child.id] = child
264
+ return this
265
+ }
266
+
241
267
  onWheel?(info: TLWheelEventInfo): void
242
268
  onPointerDown?(info: TLPointerEventInfo): void
243
269
  onPointerMove?(info: TLPointerEventInfo): void
@@ -187,3 +187,22 @@ export interface TLCameraConstraints {
187
187
  y: 'free' | 'fixed' | 'inside' | 'outside' | 'contain'
188
188
  }
189
189
  }
190
+
191
+ /** @public */
192
+ export interface TLUpdatePointerOptions {
193
+ /** Whether to update the pointer immediately, rather than on the next tick. */
194
+ immediate?: boolean
195
+ /**
196
+ * The point, in screen-space, to update the pointer to. Defaults to the position of the last
197
+ * pointer event.
198
+ */
199
+ point?: VecLike
200
+ pointerId?: number
201
+ ctrlKey?: boolean
202
+ altKey?: boolean
203
+ shiftKey?: boolean
204
+ metaKey?: boolean
205
+ accelKey?: boolean
206
+ isPen?: boolean
207
+ button?: number
208
+ }
@@ -1,4 +1,4 @@
1
- import { ComponentType, ReactNode, createContext, useContext, useMemo } from 'react'
1
+ import { ComponentType, ReactNode, RefAttributes, createContext, useContext, useMemo } from 'react'
2
2
  import { DefaultBackground } from '../components/default-components/DefaultBackground'
3
3
  import { DefaultBrush, TLBrushProps } from '../components/default-components/DefaultBrush'
4
4
  import {
@@ -37,6 +37,10 @@ import {
37
37
  TLShapeIndicatorErrorFallbackComponent,
38
38
  } from '../components/default-components/DefaultShapeIndicatorErrorFallback'
39
39
  import { DefaultShapeIndicators } from '../components/default-components/DefaultShapeIndicators'
40
+ import {
41
+ DefaultShapeWrapper,
42
+ TLShapeWrapperProps,
43
+ } from '../components/default-components/DefaultShapeWrapper'
40
44
  import {
41
45
  DefaultSnapIndicator,
42
46
  TLSnapIndicatorProps,
@@ -68,8 +72,9 @@ export interface TLEditorComponents {
68
72
  SelectionForeground?: ComponentType<TLSelectionForegroundProps> | null
69
73
  ShapeIndicator?: ComponentType<TLShapeIndicatorProps> | null
70
74
  ShapeIndicators?: ComponentType | null
75
+ ShapeWrapper?: ComponentType<TLShapeWrapperProps & RefAttributes<HTMLDivElement>> | null
71
76
  SnapIndicator?: ComponentType<TLSnapIndicatorProps> | null
72
- Spinner?: ComponentType | null
77
+ Spinner?: ComponentType<React.SVGProps<SVGSVGElement>> | null
73
78
  SvgDefs?: ComponentType | null
74
79
  ZoomBrush?: ComponentType<TLBrushProps> | null
75
80
 
@@ -114,6 +119,7 @@ export function EditorComponentsProvider({
114
119
  SelectionForeground: DefaultSelectionForeground,
115
120
  ShapeIndicator: DefaultShapeIndicator,
116
121
  ShapeIndicators: DefaultShapeIndicators,
122
+ ShapeWrapper: DefaultShapeWrapper,
117
123
  SnapIndicator: DefaultSnapIndicator,
118
124
  Spinner: DefaultSpinner,
119
125
  SvgDefs: DefaultSvgDefs,
@@ -417,7 +417,7 @@ function importPrivateKey(pemContents: string) {
417
417
  // base64 decode the string to get the binary data
418
418
  const binaryDerString = atob(pemContents)
419
419
  // convert from a binary string to an ArrayBuffer
420
- const binaryDer = str2ab(binaryDerString) as Uint8Array
420
+ const binaryDer = str2ab(binaryDerString)
421
421
 
422
422
  return crypto.subtle.importKey(
423
423
  'pkcs8',
@@ -143,7 +143,7 @@ To remove the watermark, please purchase a license at tldraw.dev.
143
143
  }
144
144
 
145
145
  .${className}:hover > button {
146
- animation: delayed_link 0.2s forwards ease-in-out;
146
+ animation: ${className}_delayed_link 0.2s forwards ease-in-out;
147
147
  animation-delay: 0.32s;
148
148
  }
149
149
 
@@ -153,7 +153,7 @@ To remove the watermark, please purchase a license at tldraw.dev.
153
153
  }
154
154
 
155
155
 
156
- @keyframes delayed_link {
156
+ @keyframes ${className}_delayed_link {
157
157
  0% {
158
158
  cursor: inherit;
159
159
  opacity: .38;
@@ -2,7 +2,7 @@ import { Vec, VecLike } from '../Vec'
2
2
  import { intersectLineSegmentCircle } from '../intersect'
3
3
  import { getArcMeasure, getPointInArcT, getPointOnCircle } from '../utils'
4
4
  import { Geometry2d, Geometry2dOptions } from './Geometry2d'
5
- import { getVerticesCountForLength } from './geometry-constants'
5
+ import { getVerticesCountForArcLength } from './geometry-constants'
6
6
 
7
7
  /** @public */
8
8
  export class Arc2d extends Geometry2d {
@@ -94,7 +94,7 @@ export class Arc2d extends Geometry2d {
94
94
  getVertices(): Vec[] {
95
95
  const { _center, _measure: measure, length, _radius: radius, _angleStart: angleStart } = this
96
96
  const vertices: Vec[] = []
97
- for (let i = 0, n = getVerticesCountForLength(Math.abs(length)); i < n + 1; i++) {
97
+ for (let i = 0, n = getVerticesCountForArcLength(Math.abs(length)); i < n + 1; i++) {
98
98
  const t = (i / n) * measure
99
99
  const angle = angleStart + t
100
100
  vertices.push(getPointOnCircle(_center, radius, angle))
@@ -3,7 +3,7 @@ import { Vec, VecLike } from '../Vec'
3
3
  import { intersectLineSegmentCircle } from '../intersect'
4
4
  import { PI2, getPointOnCircle } from '../utils'
5
5
  import { Geometry2d, Geometry2dOptions } from './Geometry2d'
6
- import { getVerticesCountForLength } from './geometry-constants'
6
+ import { getVerticesCountForArcLength } from './geometry-constants'
7
7
 
8
8
  /** @public */
9
9
  export class Circle2d extends Geometry2d {
@@ -36,7 +36,7 @@ export class Circle2d extends Geometry2d {
36
36
  const { _center, _radius: radius } = this
37
37
  const perimeter = PI2 * radius
38
38
  const vertices: Vec[] = []
39
- for (let i = 0, n = getVerticesCountForLength(perimeter); i < n; i++) {
39
+ for (let i = 0, n = getVerticesCountForArcLength(perimeter); i < n; i++) {
40
40
  const angle = (i / n) * PI2
41
41
  vertices.push(getPointOnCircle(_center, radius, angle))
42
42
  }
@@ -8,6 +8,7 @@ export class CubicBezier2d extends Polyline2d {
8
8
  private _b: Vec
9
9
  private _c: Vec
10
10
  private _d: Vec
11
+ private _resolution: number
11
12
 
12
13
  constructor(
13
14
  config: Omit<Geometry2dOptions, 'isFilled' | 'isClosed'> & {
@@ -15,6 +16,7 @@ export class CubicBezier2d extends Polyline2d {
15
16
  cp1: Vec
16
17
  cp2: Vec
17
18
  end: Vec
19
+ resolution?: number
18
20
  }
19
21
  ) {
20
22
  const { start: a, cp1: b, cp2: c, end: d } = config
@@ -24,13 +26,14 @@ export class CubicBezier2d extends Polyline2d {
24
26
  this._b = b
25
27
  this._c = c
26
28
  this._d = d
29
+ this._resolution = config.resolution ?? 10
27
30
  }
28
31
 
29
32
  override getVertices() {
30
33
  const vertices = [] as Vec[]
31
34
  const { _a: a, _b: b, _c: c, _d: d } = this
32
35
  // we'll always use ten vertices for each bezier curve
33
- for (let i = 0, n = 10; i <= n; i++) {
36
+ for (let i = 0, n = this._resolution; i <= n; i++) {
34
37
  const t = i / n
35
38
  vertices.push(
36
39
  new Vec(
@@ -3,7 +3,7 @@ import { Vec, VecLike } from '../Vec'
3
3
  import { PI, PI2, clamp, perimeterOfEllipse } from '../utils'
4
4
  import { Edge2d } from './Edge2d'
5
5
  import { Geometry2d, Geometry2dOptions } from './Geometry2d'
6
- import { getVerticesCountForLength } from './geometry-constants'
6
+ import { getVerticesCountForArcLength } from './geometry-constants'
7
7
 
8
8
  /** @public */
9
9
  export class Ellipse2d extends Geometry2d {
@@ -47,7 +47,7 @@ export class Ellipse2d extends Geometry2d {
47
47
  const q = Math.pow(cx - cy, 2) / Math.pow(cx + cy, 2)
48
48
  const p = PI * (cx + cy) * (1 + (3 * q) / (10 + Math.sqrt(4 - 3 * q)))
49
49
  // Number of points
50
- const len = getVerticesCountForLength(p)
50
+ const len = getVerticesCountForArcLength(p)
51
51
  // Size of step
52
52
  const step = PI2 / len
53
53
 
@@ -1,6 +1,7 @@
1
1
  const SPACING = 20
2
2
  const MIN_COUNT = 8
3
3
 
4
- export function getVerticesCountForLength(length: number, spacing = SPACING) {
4
+ /** @internal */
5
+ export function getVerticesCountForArcLength(length: number, spacing = SPACING) {
5
6
  return Math.max(MIN_COUNT, Math.ceil(length / spacing))
6
7
  }