@tldraw/driver 5.3.2 → 5.4.0-canary.02cd0bd3b597

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.
package/src/lib/Driver.ts CHANGED
@@ -28,6 +28,19 @@ export type PointerEventInit = Partial<TLPointerEventInfo> | TLShapeId
28
28
  /** Modifier keys for events. @public */
29
29
  export type EventModifiers = Partial<Pick<TLPointerEventInfo, 'shiftKey' | 'ctrlKey' | 'altKey'>>
30
30
 
31
+ const KEY_CODES: Record<string, string> = {
32
+ Shift: 'ShiftLeft',
33
+ Alt: 'AltLeft',
34
+ Control: 'CtrlLeft',
35
+ Meta: 'MetaLeft',
36
+ ' ': 'Space',
37
+ Enter: 'Enter',
38
+ ArrowRight: 'ArrowRight',
39
+ ArrowLeft: 'ArrowLeft',
40
+ ArrowUp: 'ArrowUp',
41
+ ArrowDown: 'ArrowDown',
42
+ }
43
+
31
44
  /**
32
45
  * Driver wraps an Editor instance and provides an imperative API for driving it
33
46
  * programmatically. Useful for scripting, automation, REPL usage, and testing.
@@ -38,7 +51,8 @@ export type EventModifiers = Partial<Pick<TLPointerEventInfo, 'shiftKey' | 'ctrl
38
51
  */
39
52
  export class Driver {
40
53
  /** The underlying Editor instance. */
41
- private _cleanup: (() => void) | null = null
54
+ private _cleanup: (() => void) | null
55
+ private _lastCreatedShapes: TLShape[] = []
42
56
 
43
57
  constructor(public readonly editor: Editor) {
44
58
  this._cleanup = this.editor.sideEffects.registerAfterCreateHandler('shape', (record) => {
@@ -58,8 +72,6 @@ export class Driver {
58
72
  /** Local clipboard content. Used by copy, cut, and paste. */
59
73
  clipboard: TLContent | null = null
60
74
 
61
- private _lastCreatedShapes: TLShape[] = []
62
-
63
75
  /**
64
76
  * Get the last created shapes.
65
77
  * @param count - The number of shapes to get.
@@ -116,10 +128,7 @@ export class Driver {
116
128
  */
117
129
  cut(ids = this.editor.getSelectedShapeIds()) {
118
130
  if (ids.length > 0) {
119
- const content = this.editor.getContentFromCurrentPage(ids)
120
- if (content) {
121
- this.clipboard = content
122
- }
131
+ this.copy(ids)
123
132
  this.editor.deleteShapes(ids)
124
133
  }
125
134
  return this
@@ -174,10 +183,7 @@ export class Driver {
174
183
  */
175
184
  getPageRotationById(id: TLShapeId): number {
176
185
  const pageTransform = this.editor.getShapePageTransform(id)
177
- if (pageTransform) {
178
- return Mat.Decompose(pageTransform).rotation
179
- }
180
- return 0
186
+ return pageTransform ? pageTransform.rotation() : 0
181
187
  }
182
188
 
183
189
  /**
@@ -199,6 +205,17 @@ export class Driver {
199
205
 
200
206
  /* --------------- Event building --------------- */
201
207
 
208
+ private getModifierKeys() {
209
+ const { inputs } = this.editor
210
+ return {
211
+ shiftKey: inputs.getShiftKey(),
212
+ ctrlKey: inputs.getCtrlKey(),
213
+ altKey: inputs.getAltKey(),
214
+ metaKey: inputs.getMetaKey(),
215
+ accelKey: isAccelKey(inputs),
216
+ }
217
+ }
218
+
202
219
  /**
203
220
  * Builds a TLPointerEventInfo object for input simulation.
204
221
  * @param x - Screen x coordinate. Defaults to current pointer.
@@ -221,10 +238,7 @@ export class Driver {
221
238
  name: 'pointer_down',
222
239
  type: 'pointer',
223
240
  pointerId: 1,
224
- shiftKey: this.editor.inputs.getShiftKey(),
225
- ctrlKey: this.editor.inputs.getCtrlKey(),
226
- altKey: this.editor.inputs.getAltKey(),
227
- metaKey: this.editor.inputs.getMetaKey(),
241
+ ...this.getModifierKeys(),
228
242
  accelKey: isAccelKey({ ...this.editor.inputs.toJson(), ...modifiers }),
229
243
  point: { x, y, z: null },
230
244
  button: 0,
@@ -268,26 +282,9 @@ export class Driver {
268
282
  }
269
283
  return {
270
284
  ...flags,
271
- accelKey: flags.accelKey ?? (tlenv.isDarwin ? flags.metaKey : flags.ctrlKey || flags.metaKey),
285
+ accelKey: flags.accelKey ?? isAccelKey(flags),
272
286
  name,
273
- code:
274
- key === 'Shift'
275
- ? 'ShiftLeft'
276
- : key === 'Alt'
277
- ? 'AltLeft'
278
- : key === 'Control'
279
- ? 'CtrlLeft'
280
- : key === 'Meta'
281
- ? 'MetaLeft'
282
- : key === ' '
283
- ? 'Space'
284
- : key === 'Enter' ||
285
- key === 'ArrowRight' ||
286
- key === 'ArrowLeft' ||
287
- key === 'ArrowUp' ||
288
- key === 'ArrowDown'
289
- ? key
290
- : 'Key' + key[0].toUpperCase() + key.slice(1),
287
+ code: KEY_CODES[key] ?? 'Key' + key[0].toUpperCase() + key.slice(1),
291
288
  type: 'keyboard',
292
289
  key,
293
290
  }
@@ -428,8 +425,7 @@ export class Driver {
428
425
  options?: PointerEventInit,
429
426
  modifiers?: EventModifiers
430
427
  ) {
431
- this.pointerDown(x, y, options, modifiers)
432
- this.pointerUp(x, y, options, modifiers)
428
+ this.click(x, y, options, modifiers)
433
429
  this.editor.dispatch({
434
430
  ...this.getPointerEventInfo(x, y, options, modifiers),
435
431
  type: 'click',
@@ -463,7 +459,7 @@ export class Driver {
463
459
  * @param options - Partial keyboard event overrides.
464
460
  */
465
461
  keyDown(key: string, options = {} as Partial<Omit<TLKeyboardEventInfo, 'key'>>) {
466
- this.editor.dispatch({ ...this.getKeyboardEventInfo(key, 'key_down', options) })
462
+ this.editor.dispatch(this.getKeyboardEventInfo(key, 'key_down', options))
467
463
  this.forceTick()
468
464
  return this
469
465
  }
@@ -474,7 +470,7 @@ export class Driver {
474
470
  * @param options - Partial keyboard event overrides.
475
471
  */
476
472
  keyRepeat(key: string, options = {} as Partial<Omit<TLKeyboardEventInfo, 'key'>>) {
477
- this.editor.dispatch({ ...this.getKeyboardEventInfo(key, 'key_repeat', options) })
473
+ this.editor.dispatch(this.getKeyboardEventInfo(key, 'key_repeat', options))
478
474
  this.forceTick()
479
475
  return this
480
476
  }
@@ -485,15 +481,15 @@ export class Driver {
485
481
  * @param options - Partial keyboard event overrides.
486
482
  */
487
483
  keyUp(key: string, options = {} as Partial<Omit<TLKeyboardEventInfo, 'key'>>) {
488
- this.editor.dispatch({
489
- ...this.getKeyboardEventInfo(key, 'key_up', {
484
+ this.editor.dispatch(
485
+ this.getKeyboardEventInfo(key, 'key_up', {
490
486
  shiftKey: this.editor.inputs.getShiftKey() && key !== 'Shift',
491
487
  ctrlKey: this.editor.inputs.getCtrlKey() && !(key === 'Control' || key === 'Meta'),
492
488
  altKey: this.editor.inputs.getAltKey() && key !== 'Alt',
493
489
  metaKey: this.editor.inputs.getMetaKey() && key !== 'Meta',
494
490
  ...options,
495
- }),
496
- })
491
+ })
492
+ )
497
493
  this.forceTick()
498
494
  return this
499
495
  }
@@ -510,11 +506,7 @@ export class Driver {
510
506
  type: 'wheel',
511
507
  name: 'wheel',
512
508
  point: new Vec(currentScreenPoint.x, currentScreenPoint.y),
513
- shiftKey: this.editor.inputs.getShiftKey(),
514
- ctrlKey: this.editor.inputs.getCtrlKey(),
515
- altKey: this.editor.inputs.getAltKey(),
516
- metaKey: this.editor.inputs.getMetaKey(),
517
- accelKey: isAccelKey(this.editor.inputs),
509
+ ...this.getModifierKeys(),
518
510
  ...options,
519
511
  delta: { x: dx, y: dy },
520
512
  })
@@ -537,6 +529,26 @@ export class Driver {
537
529
  return this
538
530
  }
539
531
 
532
+ private pinch(
533
+ name: TLPinchEventInfo['name'],
534
+ x: number,
535
+ y: number,
536
+ z: number,
537
+ dx: number,
538
+ dy: number,
539
+ dz: number,
540
+ options: Partial<Omit<TLPinchEventInfo, 'point' | 'delta' | 'offset'>>
541
+ ) {
542
+ this.editor.dispatch({
543
+ type: 'pinch',
544
+ name,
545
+ ...this.getModifierKeys(),
546
+ ...options,
547
+ point: { x, y, z },
548
+ delta: { x: dx, y: dy, z: dz },
549
+ })
550
+ }
551
+
540
552
  /**
541
553
  * Dispatches a pinch start event.
542
554
  * @param x - Screen x coordinate. Defaults to current pointer.
@@ -556,18 +568,7 @@ export class Driver {
556
568
  dz: number,
557
569
  options = {} as Partial<Omit<TLPinchEventInfo, 'point' | 'delta' | 'offset'>>
558
570
  ) {
559
- this.editor.dispatch({
560
- type: 'pinch',
561
- name: 'pinch_start',
562
- shiftKey: this.editor.inputs.getShiftKey(),
563
- ctrlKey: this.editor.inputs.getCtrlKey(),
564
- altKey: this.editor.inputs.getAltKey(),
565
- metaKey: this.editor.inputs.getMetaKey(),
566
- accelKey: isAccelKey(this.editor.inputs),
567
- ...options,
568
- point: { x, y, z },
569
- delta: { x: dx, y: dy, z: dz },
570
- })
571
+ this.pinch('pinch_start', x, y, z, dx, dy, dz, options)
571
572
  this.forceTick()
572
573
  return this
573
574
  }
@@ -591,18 +592,7 @@ export class Driver {
591
592
  dz: number,
592
593
  options = {} as Partial<Omit<TLPinchEventInfo, 'point' | 'delta' | 'offset'>>
593
594
  ) {
594
- this.editor.dispatch({
595
- type: 'pinch',
596
- name: 'pinch',
597
- shiftKey: this.editor.inputs.getShiftKey(),
598
- ctrlKey: this.editor.inputs.getCtrlKey(),
599
- altKey: this.editor.inputs.getAltKey(),
600
- metaKey: this.editor.inputs.getMetaKey(),
601
- accelKey: isAccelKey(this.editor.inputs),
602
- ...options,
603
- point: { x, y, z },
604
- delta: { x: dx, y: dy, z: dz },
605
- })
595
+ this.pinch('pinch', x, y, z, dx, dy, dz, options)
606
596
  return this
607
597
  }
608
598
 
@@ -625,33 +615,13 @@ export class Driver {
625
615
  dz: number,
626
616
  options = {} as Partial<Omit<TLPinchEventInfo, 'point' | 'delta' | 'offset'>>
627
617
  ) {
628
- this.editor.dispatch({
629
- type: 'pinch',
630
- name: 'pinch_end',
631
- shiftKey: this.editor.inputs.getShiftKey(),
632
- ctrlKey: this.editor.inputs.getCtrlKey(),
633
- altKey: this.editor.inputs.getAltKey(),
634
- metaKey: this.editor.inputs.getMetaKey(),
635
- accelKey: isAccelKey(this.editor.inputs),
636
- ...options,
637
- point: { x, y, z },
638
- delta: { x: dx, y: dy, z: dz },
639
- })
618
+ this.pinch('pinch_end', x, y, z, dx, dy, dz, options)
640
619
  this.forceTick()
641
620
  return this
642
621
  }
643
622
 
644
623
  /* --------------- Interaction helpers --------------- */
645
624
 
646
- /**
647
- * Converts a point from page coordinates to screen coordinates.
648
- * Pointer events operate in screen space, so page-space points must be
649
- * converted before being passed to pointerDown/Move/Up.
650
- */
651
- private pageToScreen(point: VecLike) {
652
- return this.editor.pageToScreen(point)
653
- }
654
-
655
625
  /**
656
626
  * Simulates rotating the current selection by the given angle in radians via the rotation handle.
657
627
  * @param angleRadians - Rotation angle in radians.
@@ -668,19 +638,16 @@ export class Driver {
668
638
 
669
639
  this.editor.setCurrentTool('select')
670
640
 
671
- const handlePoint = this.editor
672
- .getSelectionRotatedPageBounds()!
641
+ const bounds = this.editor.getSelectionRotatedPageBounds()!
642
+ const handlePoint = bounds
673
643
  .getHandlePoint(ROTATE_CORNER_TO_SELECTION_CORNER[handle])
674
644
  .clone()
675
- .rotWith(
676
- this.editor.getSelectionRotatedPageBounds()!.point,
677
- this.editor.getSelectionRotation()
678
- )
645
+ .rotWith(bounds.point, this.editor.getSelectionRotation())
679
646
 
680
647
  const targetHandlePoint = Vec.RotWith(handlePoint, this.getSelectionPageCenter()!, angleRadians)
681
648
 
682
- const screenHandle = this.pageToScreen(handlePoint)
683
- const screenTarget = this.pageToScreen(targetHandlePoint)
649
+ const screenHandle = this.editor.pageToScreen(handlePoint)
650
+ const screenTarget = this.editor.pageToScreen(targetHandlePoint)
684
651
 
685
652
  this.pointerDown(screenHandle.x, screenHandle.y, { target: 'selection', handle })
686
653
  this.pointerMove(screenTarget.x, screenTarget.y, { shiftKey })
@@ -701,18 +668,18 @@ export class Driver {
701
668
  this.editor.setCurrentTool('select')
702
669
 
703
670
  const center = this.getSelectionPageCenter()!
704
- const screenCenter = this.pageToScreen(center)
671
+ const screenCenter = this.editor.pageToScreen(center)
705
672
 
706
673
  this.pointerDown(screenCenter.x, screenCenter.y, this.editor.getSelectedShapeIds()[0])
707
674
  const numSteps = 10
708
675
  for (let i = 1; i < numSteps; i++) {
709
- const p = this.pageToScreen({
676
+ const p = this.editor.pageToScreen({
710
677
  x: center.x + (i * dx) / numSteps,
711
678
  y: center.y + (i * dy) / numSteps,
712
679
  })
713
680
  this.pointerMove(p.x, p.y, options)
714
681
  }
715
- const endScreen = this.pageToScreen({ x: center.x + dx, y: center.y + dy })
682
+ const endScreen = this.editor.pageToScreen({ x: center.x + dx, y: center.y + dy })
716
683
  this.pointerUp(endScreen.x, endScreen.y, options)
717
684
  return this
718
685
  }
@@ -734,6 +701,7 @@ export class Driver {
734
701
  }
735
702
  this.editor.setCurrentTool('select')
736
703
  const bounds = this.editor.getSelectionRotatedPageBounds()!
704
+ const rotation = this.editor.getSelectionRotation()
737
705
  const preRotationHandlePoint = bounds.getHandlePoint(handle)
738
706
 
739
707
  const preRotationScaleOriginPoint = options?.altKey
@@ -748,19 +716,11 @@ export class Driver {
748
716
  preRotationScaleOriginPoint
749
717
  )
750
718
 
751
- const handlePoint = Vec.RotWith(
752
- preRotationHandlePoint,
753
- bounds.point,
754
- this.editor.getSelectionRotation()
755
- )
756
- const targetHandlePoint = Vec.RotWith(
757
- preRotationTargetHandlePoint,
758
- bounds.point,
759
- this.editor.getSelectionRotation()
760
- )
719
+ const handlePoint = Vec.RotWith(preRotationHandlePoint, bounds.point, rotation)
720
+ const targetHandlePoint = Vec.RotWith(preRotationTargetHandlePoint, bounds.point, rotation)
761
721
 
762
- const screenHandle = this.pageToScreen(handlePoint)
763
- const screenTarget = this.pageToScreen(targetHandlePoint)
722
+ const screenHandle = this.editor.pageToScreen(handlePoint)
723
+ const screenTarget = this.editor.pageToScreen(targetHandlePoint)
764
724
 
765
725
  this.pointerDown(screenHandle.x, screenHandle.y, { target: 'selection', handle }, options)
766
726
  this.pointerMove(screenTarget.x, screenTarget.y, options)