@tldraw/editor 3.15.0-canary.ee0606e7631e → 3.15.0-canary.ef2f8fce47e3

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 (44) hide show
  1. package/dist-cjs/index.d.ts +94 -3
  2. package/dist-cjs/index.js +1 -1
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/components/default-components/DefaultSpinner.js +27 -15
  5. package/dist-cjs/lib/components/default-components/DefaultSpinner.js.map +3 -3
  6. package/dist-cjs/lib/editor/Editor.js +67 -40
  7. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  8. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  9. package/dist-cjs/lib/editor/tools/StateNode.js +20 -1
  10. package/dist-cjs/lib/editor/tools/StateNode.js.map +2 -2
  11. package/dist-cjs/lib/editor/types/misc-types.js.map +1 -1
  12. package/dist-cjs/lib/hooks/useEditorComponents.js.map +1 -1
  13. package/dist-cjs/lib/license/Watermark.js +2 -2
  14. package/dist-cjs/lib/license/Watermark.js.map +2 -2
  15. package/dist-cjs/version.js +3 -3
  16. package/dist-cjs/version.js.map +1 -1
  17. package/dist-esm/index.d.mts +94 -3
  18. package/dist-esm/index.mjs +1 -1
  19. package/dist-esm/index.mjs.map +2 -2
  20. package/dist-esm/lib/components/default-components/DefaultSpinner.mjs +17 -15
  21. package/dist-esm/lib/components/default-components/DefaultSpinner.mjs.map +2 -2
  22. package/dist-esm/lib/editor/Editor.mjs +67 -40
  23. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  24. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  25. package/dist-esm/lib/editor/tools/StateNode.mjs +20 -1
  26. package/dist-esm/lib/editor/tools/StateNode.mjs.map +2 -2
  27. package/dist-esm/lib/hooks/useEditorComponents.mjs.map +1 -1
  28. package/dist-esm/lib/license/Watermark.mjs +2 -2
  29. package/dist-esm/lib/license/Watermark.mjs.map +2 -2
  30. package/dist-esm/version.mjs +3 -3
  31. package/dist-esm/version.mjs.map +1 -1
  32. package/editor.css +15 -2
  33. package/package.json +9 -8
  34. package/src/index.ts +1 -0
  35. package/src/lib/components/default-components/DefaultSpinner.tsx +12 -12
  36. package/src/lib/editor/Editor.ts +76 -39
  37. package/src/lib/editor/shapes/ShapeUtil.ts +57 -0
  38. package/src/lib/editor/tools/StateNode.test.ts +285 -0
  39. package/src/lib/editor/tools/StateNode.ts +27 -1
  40. package/src/lib/editor/types/misc-types.ts +19 -0
  41. package/src/lib/hooks/useEditorComponents.tsx +1 -1
  42. package/src/lib/license/Watermark.tsx +2 -2
  43. package/src/version.ts +3 -3
  44. package/src/lib/test/currentToolIdMask.test.ts +0 -49
@@ -178,6 +178,7 @@ import {
178
178
  TLCameraOptions,
179
179
  TLImageExportOptions,
180
180
  TLSvgExportOptions,
181
+ TLUpdatePointerOptions,
181
182
  } from './types/misc-types'
182
183
  import { TLAdjacentDirection, TLResizeHandle } from './types/selection-types'
183
184
 
@@ -1863,10 +1864,11 @@ export class Editor extends EventEmitter<TLEventMap> {
1863
1864
  firstParentId &&
1864
1865
  selectedShapeIds.every((shapeId) => this.getShape(shapeId)?.parentId === firstParentId) &&
1865
1866
  !isPageId(firstParentId)
1867
+ const filteredShapes = isSelectedWithinContainer
1868
+ ? this.getCurrentPageShapes().filter((shape) => shape.parentId === firstParentId)
1869
+ : this.getCurrentPageShapes().filter((shape) => isPageId(shape.parentId))
1866
1870
  const readingOrderShapes = isSelectedWithinContainer
1867
- ? this._getShapesInReadingOrder(
1868
- this.getCurrentPageShapes().filter((shape) => shape.parentId === firstParentId)
1869
- )
1871
+ ? this._getShapesInReadingOrder(filteredShapes)
1870
1872
  : this.getCurrentPageShapesInReadingOrder()
1871
1873
  const currentShapeId: TLShapeId | undefined =
1872
1874
  selectedShapeIds.length === 1
@@ -1883,7 +1885,7 @@ export class Editor extends EventEmitter<TLEventMap> {
1883
1885
  adjacentShapeId = shapeIds[adjacentIndex]
1884
1886
  } else {
1885
1887
  if (!currentShapeId) return
1886
- adjacentShapeId = this.getNearestAdjacentShape(currentShapeId, direction)
1888
+ adjacentShapeId = this.getNearestAdjacentShape(filteredShapes, currentShapeId, direction)
1887
1889
  }
1888
1890
 
1889
1891
  const shape = this.getShape(adjacentShapeId)
@@ -1982,6 +1984,7 @@ export class Editor extends EventEmitter<TLEventMap> {
1982
1984
  * @public
1983
1985
  */
1984
1986
  getNearestAdjacentShape(
1987
+ shapes: TLShape[],
1985
1988
  currentShapeId: TLShapeId,
1986
1989
  direction: 'left' | 'right' | 'up' | 'down'
1987
1990
  ): TLShapeId {
@@ -1989,7 +1992,6 @@ export class Editor extends EventEmitter<TLEventMap> {
1989
1992
  const currentShape = this.getShape(currentShapeId)
1990
1993
  if (!currentShape) return currentShapeId
1991
1994
 
1992
- const shapes = this.getCurrentPageShapes()
1993
1995
  const tabbableShapes = shapes.filter(
1994
1996
  (shape) => this.getShapeUtil(shape).canTabTo(shape) && shape.id !== currentShapeId
1995
1997
  )
@@ -3071,7 +3073,6 @@ export class Editor extends EventEmitter<TLEventMap> {
3071
3073
  // Dispatch a new pointer move because the pointer's page will have changed
3072
3074
  // (its screen position will compute to a new page position given the new camera position)
3073
3075
  const { currentScreenPoint, currentPagePoint } = this.inputs
3074
- const { screenBounds } = this.store.unsafeGetWithoutCapture(TLINSTANCE_ID)!
3075
3076
 
3076
3077
  // compare the next page point (derived from the current camera) to the current page point
3077
3078
  if (
@@ -3079,27 +3080,10 @@ export class Editor extends EventEmitter<TLEventMap> {
3079
3080
  currentScreenPoint.y / z - y !== currentPagePoint.y
3080
3081
  ) {
3081
3082
  // If it's changed, dispatch a pointer event
3082
- const event: TLPointerEventInfo = {
3083
- type: 'pointer',
3084
- target: 'canvas',
3085
- name: 'pointer_move',
3086
- // weird but true: we need to put the screen point back into client space
3087
- point: Vec.AddXY(currentScreenPoint, screenBounds.x, screenBounds.y),
3083
+ this.updatePointer({
3084
+ immediate: opts?.immediate,
3088
3085
  pointerId: INTERNAL_POINTER_IDS.CAMERA_MOVE,
3089
- ctrlKey: this.inputs.ctrlKey,
3090
- altKey: this.inputs.altKey,
3091
- shiftKey: this.inputs.shiftKey,
3092
- metaKey: this.inputs.metaKey,
3093
- accelKey: isAccelKey(this.inputs),
3094
- button: 0,
3095
- isPen: this.getInstanceState().isPenMode ?? false,
3096
- }
3097
-
3098
- if (opts?.immediate) {
3099
- this._flushEventForTick(event)
3100
- } else {
3101
- this.dispatch(event)
3102
- }
3086
+ })
3103
3087
  }
3104
3088
 
3105
3089
  this._tickCameraState()
@@ -4420,21 +4404,28 @@ export class Editor extends EventEmitter<TLEventMap> {
4420
4404
  */
4421
4405
  deletePage(page: TLPageId | TLPage): this {
4422
4406
  const id = typeof page === 'string' ? page : page.id
4423
- this.run(() => {
4424
- if (this.getIsReadonly()) return
4425
- const pages = this.getPages()
4426
- if (pages.length === 1) return
4407
+ this.run(
4408
+ () => {
4409
+ if (this.getIsReadonly()) return
4410
+ const pages = this.getPages()
4411
+ if (pages.length === 1) return
4427
4412
 
4428
- const deletedPage = this.getPage(id)
4429
- if (!deletedPage) return
4413
+ const deletedPage = this.getPage(id)
4414
+ if (!deletedPage) return
4430
4415
 
4431
- if (id === this.getCurrentPageId()) {
4432
- const index = pages.findIndex((page) => page.id === id)
4433
- const next = pages[index - 1] ?? pages[index + 1]
4434
- this.setCurrentPage(next.id)
4435
- }
4436
- this.store.remove([deletedPage.id])
4437
- })
4416
+ if (id === this.getCurrentPageId()) {
4417
+ const index = pages.findIndex((page) => page.id === id)
4418
+ const next = pages[index - 1] ?? pages[index + 1]
4419
+ this.setCurrentPage(next.id)
4420
+ }
4421
+
4422
+ const shapes = this.getSortedChildIdsForParent(deletedPage.id)
4423
+ this.deleteShapes(shapes)
4424
+
4425
+ this.store.remove([deletedPage.id])
4426
+ },
4427
+ { ignoreShapeLock: true }
4428
+ )
4438
4429
  return this
4439
4430
  }
4440
4431
 
@@ -9672,6 +9663,52 @@ export class Editor extends EventEmitter<TLEventMap> {
9672
9663
  return this
9673
9664
  }
9674
9665
 
9666
+ /**
9667
+ * Dispatch a pointer move event in the current position of the pointer. This is useful when
9668
+ * external circumstances have changed (e.g. the camera moved or a shape was moved) and you want
9669
+ * the current interaction to respond to that change.
9670
+ *
9671
+ * @example
9672
+ * ```ts
9673
+ * editor.updatePointer()
9674
+ * ```
9675
+ *
9676
+ * @param options - The options for updating the pointer.
9677
+ * @returns The editor instance.
9678
+ * @public
9679
+ */
9680
+ updatePointer(options?: TLUpdatePointerOptions): this {
9681
+ const event: TLPointerEventInfo = {
9682
+ type: 'pointer',
9683
+ target: 'canvas',
9684
+ name: 'pointer_move',
9685
+ point:
9686
+ options?.point ??
9687
+ // weird but true: what `inputs` calls screen-space is actually viewport space. so
9688
+ // we need to convert back into true screen space first. we should fix this...
9689
+ Vec.Add(
9690
+ this.inputs.currentScreenPoint,
9691
+ this.store.unsafeGetWithoutCapture(TLINSTANCE_ID)!.screenBounds
9692
+ ),
9693
+ pointerId: options?.pointerId ?? 0,
9694
+ button: options?.button ?? 0,
9695
+ isPen: options?.isPen ?? this.inputs.isPen,
9696
+ shiftKey: options?.shiftKey ?? this.inputs.shiftKey,
9697
+ altKey: options?.altKey ?? this.inputs.altKey,
9698
+ ctrlKey: options?.ctrlKey ?? this.inputs.ctrlKey,
9699
+ metaKey: options?.metaKey ?? this.inputs.metaKey,
9700
+ accelKey: options?.accelKey ?? isAccelKey(this.inputs),
9701
+ }
9702
+
9703
+ if (options?.immediate) {
9704
+ this._flushEventForTick(event)
9705
+ } else {
9706
+ this.dispatch(event)
9707
+ }
9708
+
9709
+ return this
9710
+ }
9711
+
9675
9712
  /**
9676
9713
  * Puts the editor into focused mode.
9677
9714
  *
@@ -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
+ }
@@ -69,7 +69,7 @@ export interface TLEditorComponents {
69
69
  ShapeIndicator?: ComponentType<TLShapeIndicatorProps> | null
70
70
  ShapeIndicators?: ComponentType | null
71
71
  SnapIndicator?: ComponentType<TLSnapIndicatorProps> | null
72
- Spinner?: ComponentType | null
72
+ Spinner?: ComponentType<React.SVGProps<SVGSVGElement>> | null
73
73
  SvgDefs?: ComponentType | null
74
74
  ZoomBrush?: ComponentType<TLBrushProps> | null
75
75
 
@@ -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;
package/src/version.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  // This file is automatically generated by internal/scripts/refresh-assets.ts.
2
2
  // Do not edit manually. Or do, I'm a comment, not a cop.
3
3
 
4
- export const version = '3.15.0-canary.ee0606e7631e'
4
+ export const version = '3.15.0-canary.ef2f8fce47e3'
5
5
  export const publishDates = {
6
6
  major: '2024-09-13T14:36:29.063Z',
7
- minor: '2025-07-14T15:28:44.028Z',
8
- patch: '2025-07-14T15:28:44.028Z',
7
+ minor: '2025-07-28T14:10:47.467Z',
8
+ patch: '2025-07-28T14:10:47.467Z',
9
9
  }