@tldraw/editor 4.5.0-next.dc46682213a8 → 4.5.1

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 (46) hide show
  1. package/dist-cjs/index.d.ts +26 -1
  2. package/dist-cjs/index.js +2 -1
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/editor/Editor.js +123 -169
  5. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  6. package/dist-cjs/lib/editor/managers/HistoryManager/HistoryManager.js +15 -16
  7. package/dist-cjs/lib/editor/managers/HistoryManager/HistoryManager.js.map +3 -3
  8. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +1 -1
  9. package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js +2 -3
  10. package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js.map +2 -2
  11. package/dist-cjs/lib/hooks/useCanvasEvents.js +3 -3
  12. package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
  13. package/dist-cjs/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.js +1 -2
  14. package/dist-cjs/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.js.map +2 -2
  15. package/dist-cjs/lib/utils/dom.js +8 -4
  16. package/dist-cjs/lib/utils/dom.js.map +2 -2
  17. package/dist-cjs/version.js +3 -3
  18. package/dist-cjs/version.js.map +1 -1
  19. package/dist-esm/index.d.mts +26 -1
  20. package/dist-esm/index.mjs +3 -1
  21. package/dist-esm/index.mjs.map +2 -2
  22. package/dist-esm/lib/editor/Editor.mjs +123 -169
  23. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  24. package/dist-esm/lib/editor/managers/HistoryManager/HistoryManager.mjs +15 -16
  25. package/dist-esm/lib/editor/managers/HistoryManager/HistoryManager.mjs.map +3 -3
  26. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +1 -1
  27. package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs +2 -3
  28. package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs.map +2 -2
  29. package/dist-esm/lib/hooks/useCanvasEvents.mjs +9 -4
  30. package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
  31. package/dist-esm/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.mjs +2 -3
  32. package/dist-esm/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.mjs.map +2 -2
  33. package/dist-esm/lib/utils/dom.mjs +8 -4
  34. package/dist-esm/lib/utils/dom.mjs.map +2 -2
  35. package/dist-esm/version.mjs +3 -3
  36. package/dist-esm/version.mjs.map +1 -1
  37. package/package.json +7 -7
  38. package/src/index.ts +1 -0
  39. package/src/lib/editor/Editor.ts +169 -269
  40. package/src/lib/editor/managers/HistoryManager/HistoryManager.ts +6 -5
  41. package/src/lib/editor/shapes/ShapeUtil.ts +1 -1
  42. package/src/lib/editor/tools/BaseBoxShapeTool/children/Pointing.ts +3 -3
  43. package/src/lib/hooks/useCanvasEvents.ts +9 -5
  44. package/src/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.ts +2 -5
  45. package/src/lib/utils/dom.ts +16 -8
  46. package/src/version.ts +3 -3
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/version.ts"],
4
- "sourcesContent": ["// This file is automatically generated by internal/scripts/refresh-assets.ts.\n// Do not edit manually. Or do, I'm a comment, not a cop.\n\nexport const version = '4.5.0-next.dc46682213a8'\nexport const publishDates = {\n\tmajor: '2025-09-18T14:39:22.803Z',\n\tminor: '2026-02-28T14:06:12.921Z',\n\tpatch: '2026-02-28T14:06:12.921Z',\n}\n"],
4
+ "sourcesContent": ["// This file is automatically generated by internal/scripts/refresh-assets.ts.\n// Do not edit manually. Or do, I'm a comment, not a cop.\n\nexport const version = '4.5.1'\nexport const publishDates = {\n\tmajor: '2025-09-18T14:39:22.803Z',\n\tminor: '2026-03-18T11:05:13.340Z',\n\tpatch: '2026-03-18T11:28:00.553Z',\n}\n"],
5
5
  "mappings": "AAGO,MAAM,UAAU;AAChB,MAAM,eAAe;AAAA,EAC3B,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AACR;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tldraw/editor",
3
3
  "description": "tldraw infinite canvas SDK (editor).",
4
- "version": "4.5.0-next.dc46682213a8",
4
+ "version": "4.5.1",
5
5
  "author": {
6
6
  "name": "tldraw Inc.",
7
7
  "email": "hello@tldraw.com"
@@ -49,12 +49,12 @@
49
49
  "@tiptap/core": "^3.12.1",
50
50
  "@tiptap/pm": "^3.12.1",
51
51
  "@tiptap/react": "^3.12.1",
52
- "@tldraw/state": "4.5.0-next.dc46682213a8",
53
- "@tldraw/state-react": "4.5.0-next.dc46682213a8",
54
- "@tldraw/store": "4.5.0-next.dc46682213a8",
55
- "@tldraw/tlschema": "4.5.0-next.dc46682213a8",
56
- "@tldraw/utils": "4.5.0-next.dc46682213a8",
57
- "@tldraw/validate": "4.5.0-next.dc46682213a8",
52
+ "@tldraw/state": "4.5.1",
53
+ "@tldraw/state-react": "4.5.1",
54
+ "@tldraw/store": "4.5.1",
55
+ "@tldraw/tlschema": "4.5.1",
56
+ "@tldraw/utils": "4.5.1",
57
+ "@tldraw/validate": "4.5.1",
58
58
  "@use-gesture/react": "^10.3.1",
59
59
  "classnames": "^2.5.1",
60
60
  "eventemitter3": "^4.0.7",
package/src/index.ts CHANGED
@@ -447,6 +447,7 @@ export {
447
447
  } from './lib/utils/deepLinks'
448
448
  export {
449
449
  activeElementShouldCaptureKeys,
450
+ elementShouldCaptureKeys,
450
451
  loopToHtmlElement,
451
452
  preventDefault,
452
453
  releasePointerCapture,
@@ -158,6 +158,7 @@ import {
158
158
  TLEditStartInfo,
159
159
  TLGeometryOpts,
160
160
  TLResizeMode,
161
+ TLShapeUtilCanBeLaidOutOpts,
161
162
  TLShapeUtilCanBindOpts,
162
163
  } from './shapes/ShapeUtil'
163
164
  import { RootState } from './tools/RootState'
@@ -2771,6 +2772,15 @@ export class Editor extends EventEmitter<TLEventMap> {
2771
2772
  return this.getCamera().z
2772
2773
  }
2773
2774
 
2775
+ /**
2776
+ * Get the scale factor used when creating or resizing shapes in dynamic size mode.
2777
+ *
2778
+ * @public
2779
+ */
2780
+ @computed getResizeScaleFactor() {
2781
+ return this.user.getIsDynamicResizeMode() ? 1 / this.getZoomLevel() : 1
2782
+ }
2783
+
2774
2784
  private _debouncedZoomLevel = atom('debounced zoom level', 1)
2775
2785
 
2776
2786
  /**
@@ -6771,6 +6781,77 @@ export class Editor extends EventEmitter<TLEventMap> {
6771
6781
  return this
6772
6782
  }
6773
6783
 
6784
+ /**
6785
+ * Shared clustering logic for layout methods. Resolves shapes, optionally filters to
6786
+ * axis-aligned shapes, checks canBeLaidOut, and groups shapes into clusters via arrow bindings.
6787
+ *
6788
+ * @internal
6789
+ */
6790
+ private getShapeClusters(
6791
+ shapes: TLShapeId[] | TLShape[],
6792
+ type: TLShapeUtilCanBeLaidOutOpts['type'],
6793
+ opts?: { filterAxisAligned?: boolean }
6794
+ ): { clusters: { shapes: TLShape[]; pageBounds: Box }[]; allBounds: Box[] } {
6795
+ const ids =
6796
+ typeof shapes[0] === 'string'
6797
+ ? (shapes as TLShapeId[])
6798
+ : (shapes as TLShape[]).map((s) => s.id)
6799
+
6800
+ // always fresh shapes
6801
+ let freshShapes = compact(ids.map((id) => this.getShape(id)))
6802
+
6803
+ // optionally filter to axis-aligned shapes (rotation is a multiple of 90 degrees)
6804
+ if (opts?.filterAxisAligned) {
6805
+ freshShapes = freshShapes.filter(
6806
+ (s) => this.getShapePageTransform(s)?.rotation() % (PI / 2) === 0
6807
+ )
6808
+ }
6809
+
6810
+ const clusters: { shapes: TLShape[]; pageBounds: Box }[] = []
6811
+ const allBounds: Box[] = []
6812
+ const visited = new Set<TLShapeId>()
6813
+
6814
+ for (const shape of freshShapes) {
6815
+ if (visited.has(shape.id)) continue
6816
+ visited.add(shape.id)
6817
+
6818
+ const shapePageBounds = this.getShapePageBounds(shape)
6819
+ if (!shapePageBounds) continue
6820
+
6821
+ if (
6822
+ !this.getShapeUtil(shape).canBeLaidOut?.(shape, {
6823
+ type,
6824
+ shapes: freshShapes,
6825
+ })
6826
+ ) {
6827
+ continue
6828
+ }
6829
+
6830
+ const shapesMovingTogether = [shape]
6831
+ const boundsOfShapesMovingTogether: Box[] = [shapePageBounds]
6832
+
6833
+ this.collectShapesViaArrowBindings({
6834
+ bindings: this.getBindingsToShape(shape.id, 'arrow'),
6835
+ initialShapes: freshShapes,
6836
+ resultShapes: shapesMovingTogether,
6837
+ resultBounds: boundsOfShapesMovingTogether,
6838
+ visited,
6839
+ })
6840
+
6841
+ const commonPageBounds = Box.Common(boundsOfShapesMovingTogether)
6842
+ if (!commonPageBounds) continue
6843
+
6844
+ clusters.push({
6845
+ shapes: shapesMovingTogether,
6846
+ pageBounds: commonPageBounds,
6847
+ })
6848
+
6849
+ allBounds.push(commonPageBounds)
6850
+ }
6851
+
6852
+ return { clusters, allBounds }
6853
+ }
6854
+
6774
6855
  /**
6775
6856
  * @internal
6776
6857
  */
@@ -6916,61 +6997,11 @@ export class Editor extends EventEmitter<TLEventMap> {
6916
6997
  gap?: number
6917
6998
  ): this {
6918
6999
  const _gap = gap ?? this.options.adjacentShapeMargin
6919
- const ids =
6920
- typeof shapes[0] === 'string'
6921
- ? (shapes as TLShapeId[])
6922
- : (shapes as TLShape[]).map((s) => s.id)
6923
7000
  if (this.getIsReadonly()) return this
6924
7001
 
6925
7002
  // todo: this has a lot of extra code to handle stacking with custom gaps or auto gaps or other things like that. I don't think anyone has ever used this stuff.
6926
7003
 
6927
- // always fresh shapes
6928
- const shapesToStackFirstPass = compact(ids.map((id) => this.getShape(id)))
6929
-
6930
- const shapeClustersToStack: {
6931
- shapes: TLShape[]
6932
- pageBounds: Box
6933
- }[] = []
6934
- const allBounds: Box[] = []
6935
- const visited = new Set<TLShapeId>()
6936
-
6937
- for (const shape of shapesToStackFirstPass) {
6938
- if (visited.has(shape.id)) continue
6939
- visited.add(shape.id)
6940
-
6941
- const shapePageBounds = this.getShapePageBounds(shape)
6942
- if (!shapePageBounds) continue
6943
-
6944
- if (
6945
- !this.getShapeUtil(shape).canBeLaidOut?.(shape, {
6946
- type: 'stack',
6947
- shapes: shapesToStackFirstPass,
6948
- })
6949
- ) {
6950
- continue
6951
- }
6952
-
6953
- const shapesMovingTogether = [shape]
6954
- const boundsOfShapesMovingTogether: Box[] = [shapePageBounds]
6955
-
6956
- this.collectShapesViaArrowBindings({
6957
- bindings: this.getBindingsToShape(shape.id, 'arrow'),
6958
- initialShapes: shapesToStackFirstPass,
6959
- resultShapes: shapesMovingTogether,
6960
- resultBounds: boundsOfShapesMovingTogether,
6961
- visited,
6962
- })
6963
-
6964
- const commonPageBounds = Box.Common(boundsOfShapesMovingTogether)
6965
- if (!commonPageBounds) continue
6966
-
6967
- shapeClustersToStack.push({
6968
- shapes: shapesMovingTogether,
6969
- pageBounds: commonPageBounds,
6970
- })
6971
-
6972
- allBounds.push(commonPageBounds)
6973
- }
7004
+ const { clusters: shapeClustersToStack } = this.getShapeClusters(shapes, 'stack')
6974
7005
 
6975
7006
  const len = shapeClustersToStack.length
6976
7007
  if ((_gap === 0 && len < 3) || len < 2) return this
@@ -7086,61 +7117,12 @@ export class Editor extends EventEmitter<TLEventMap> {
7086
7117
 
7087
7118
  const gap = _gap ?? this.options.adjacentShapeMargin
7088
7119
 
7089
- const ids =
7090
- typeof shapes[0] === 'string'
7091
- ? (shapes as TLShapeId[])
7092
- : (shapes as TLShape[]).map((s) => s.id)
7093
-
7094
- // Always fresh shapes
7095
- const shapesToPackFirstPass = compact(ids.map((id) => this.getShape(id)))
7096
-
7097
- const shapeClustersToPack: {
7098
- shapes: TLShape[]
7099
- pageBounds: Box
7100
- nextPageBounds: Box
7101
- }[] = []
7102
-
7103
- const allBounds: Box[] = []
7104
- const visited = new Set<TLShapeId>()
7105
-
7106
- for (const shape of shapesToPackFirstPass) {
7107
- if (visited.has(shape.id)) continue
7108
- visited.add(shape.id)
7109
-
7110
- const shapePageBounds = this.getShapePageBounds(shape)
7111
- if (!shapePageBounds) continue
7112
-
7113
- if (
7114
- !this.getShapeUtil(shape).canBeLaidOut?.(shape, {
7115
- type: 'pack',
7116
- shapes: shapesToPackFirstPass,
7117
- })
7118
- ) {
7119
- continue
7120
- }
7121
-
7122
- const shapesMovingTogether = [shape]
7123
- const boundsOfShapesMovingTogether: Box[] = [shapePageBounds]
7124
-
7125
- this.collectShapesViaArrowBindings({
7126
- bindings: this.getBindingsToShape(shape.id, 'arrow'),
7127
- initialShapes: shapesToPackFirstPass,
7128
- resultShapes: shapesMovingTogether,
7129
- resultBounds: boundsOfShapesMovingTogether,
7130
- visited,
7131
- })
7120
+ const { clusters, allBounds } = this.getShapeClusters(shapes, 'pack')
7132
7121
 
7133
- const commonPageBounds = Box.Common(boundsOfShapesMovingTogether)
7134
- if (!commonPageBounds) continue
7135
-
7136
- shapeClustersToPack.push({
7137
- shapes: shapesMovingTogether,
7138
- pageBounds: commonPageBounds,
7139
- nextPageBounds: commonPageBounds.clone(),
7140
- })
7141
-
7142
- allBounds.push(commonPageBounds)
7143
- }
7122
+ const shapeClustersToPack = clusters.map((cluster) => ({
7123
+ ...cluster,
7124
+ nextPageBounds: cluster.pageBounds.clone(),
7125
+ }))
7144
7126
 
7145
7127
  if (shapeClustersToPack.length < 2) return this
7146
7128
 
@@ -7262,63 +7244,7 @@ export class Editor extends EventEmitter<TLEventMap> {
7262
7244
  ): this {
7263
7245
  if (this.getIsReadonly()) return this
7264
7246
 
7265
- const ids =
7266
- typeof shapes[0] === 'string'
7267
- ? (shapes as TLShapeId[])
7268
- : (shapes as TLShape[]).map((s) => s.id)
7269
-
7270
- // Always get fresh shapes
7271
- const shapesToAlignFirstPass = compact(ids.map((id) => this.getShape(id)))
7272
-
7273
- const shapeClustersToAlign: {
7274
- shapes: TLShape[]
7275
- pageBounds: Box
7276
- }[] = []
7277
- const allBounds: Box[] = []
7278
- const visited = new Set<TLShapeId>()
7279
-
7280
- for (const shape of shapesToAlignFirstPass) {
7281
- if (visited.has(shape.id)) continue
7282
- visited.add(shape.id)
7283
-
7284
- const shapePageBounds = this.getShapePageBounds(shape)
7285
- if (!shapePageBounds) continue
7286
-
7287
- if (
7288
- !this.getShapeUtil(shape).canBeLaidOut?.(shape, {
7289
- type: 'align',
7290
- shapes: shapesToAlignFirstPass,
7291
- })
7292
- ) {
7293
- continue
7294
- }
7295
-
7296
- // In this implementation, we want to create psuedo-groups out of shapes that
7297
- // are moving together. At the moment shapes only move together if they're connected
7298
- // by arrows. So let's say A -> B -> C -> D and A, B, and C are selected. If we're
7299
- // aligning A, B, and C, then we want these to move together as one unit.
7300
-
7301
- const shapesMovingTogether = [shape]
7302
- const boundsOfShapesMovingTogether: Box[] = [shapePageBounds]
7303
-
7304
- this.collectShapesViaArrowBindings({
7305
- bindings: this.getBindingsToShape(shape.id, 'arrow'),
7306
- initialShapes: shapesToAlignFirstPass,
7307
- resultShapes: shapesMovingTogether,
7308
- resultBounds: boundsOfShapesMovingTogether,
7309
- visited,
7310
- })
7311
-
7312
- const commonPageBounds = Box.Common(boundsOfShapesMovingTogether)
7313
- if (!commonPageBounds) continue
7314
-
7315
- shapeClustersToAlign.push({
7316
- shapes: shapesMovingTogether,
7317
- pageBounds: commonPageBounds,
7318
- })
7319
-
7320
- allBounds.push(commonPageBounds)
7321
- }
7247
+ const { clusters: shapeClustersToAlign, allBounds } = this.getShapeClusters(shapes, 'align')
7322
7248
 
7323
7249
  if (shapeClustersToAlign.length < 2) return this
7324
7250
 
@@ -7393,59 +7319,7 @@ export class Editor extends EventEmitter<TLEventMap> {
7393
7319
  distributeShapes(shapes: TLShapeId[] | TLShape[], operation: 'horizontal' | 'vertical'): this {
7394
7320
  if (this.getIsReadonly()) return this
7395
7321
 
7396
- const ids =
7397
- typeof shapes[0] === 'string'
7398
- ? (shapes as TLShapeId[])
7399
- : (shapes as TLShape[]).map((s) => s.id)
7400
-
7401
- // always fresh shapes
7402
- const shapesToDistributeFirstPass = compact(ids.map((id) => this.getShape(id)))
7403
-
7404
- const shapeClustersToDistribute: {
7405
- shapes: TLShape[]
7406
- pageBounds: Box
7407
- }[] = []
7408
-
7409
- const allBounds: Box[] = []
7410
- const visited = new Set<TLShapeId>()
7411
-
7412
- for (const shape of shapesToDistributeFirstPass) {
7413
- if (visited.has(shape.id)) continue
7414
- visited.add(shape.id)
7415
-
7416
- const shapePageBounds = this.getShapePageBounds(shape)
7417
- if (!shapePageBounds) continue
7418
-
7419
- if (
7420
- !this.getShapeUtil(shape).canBeLaidOut?.(shape, {
7421
- type: 'distribute',
7422
- shapes: shapesToDistributeFirstPass,
7423
- })
7424
- ) {
7425
- continue
7426
- }
7427
-
7428
- const shapesMovingTogether = [shape]
7429
- const boundsOfShapesMovingTogether: Box[] = [shapePageBounds]
7430
-
7431
- this.collectShapesViaArrowBindings({
7432
- bindings: this.getBindingsToShape(shape.id, 'arrow'),
7433
- initialShapes: shapesToDistributeFirstPass,
7434
- resultShapes: shapesMovingTogether,
7435
- resultBounds: boundsOfShapesMovingTogether,
7436
- visited,
7437
- })
7438
-
7439
- const commonPageBounds = Box.Common(boundsOfShapesMovingTogether)
7440
- if (!commonPageBounds) continue
7441
-
7442
- shapeClustersToDistribute.push({
7443
- shapes: shapesMovingTogether,
7444
- pageBounds: commonPageBounds,
7445
- })
7446
-
7447
- allBounds.push(commonPageBounds)
7448
- }
7322
+ const { clusters: shapeClustersToDistribute } = this.getShapeClusters(shapes, 'distribute')
7449
7323
 
7450
7324
  if (shapeClustersToDistribute.length < 3) return this
7451
7325
 
@@ -7473,6 +7347,10 @@ export class Editor extends EventEmitter<TLEventMap> {
7473
7347
  // If the first shape group is also the last shape group, distribute without it
7474
7348
  if (first === last) {
7475
7349
  const excludedShapeIds = new Set(first.shapes.map((s) => s.id))
7350
+ const ids =
7351
+ typeof shapes[0] === 'string'
7352
+ ? (shapes as TLShapeId[])
7353
+ : (shapes as TLShape[]).map((s) => s.id)
7476
7354
  return this.distributeShapes(
7477
7355
  ids.filter((id) => !excludedShapeIds.has(id)),
7478
7356
  operation
@@ -7542,63 +7420,14 @@ export class Editor extends EventEmitter<TLEventMap> {
7542
7420
  * @public
7543
7421
  */
7544
7422
  stretchShapes(shapes: TLShapeId[] | TLShape[], operation: 'horizontal' | 'vertical'): this {
7545
- const ids =
7546
- typeof shapes[0] === 'string'
7547
- ? (shapes as TLShapeId[])
7548
- : (shapes as TLShape[]).map((s) => s.id)
7549
-
7550
7423
  if (this.getIsReadonly()) return this
7551
7424
 
7552
- // always fresh shapes, skip anything that isn't rotated 90 deg
7553
- const shapesToStretchFirstPass = compact(ids.map((id) => this.getShape(id))).filter(
7554
- (s) => this.getShapePageTransform(s)?.rotation() % (PI / 2) === 0
7425
+ const { clusters: shapeClustersToStretch, allBounds } = this.getShapeClusters(
7426
+ shapes,
7427
+ 'stretch',
7428
+ { filterAxisAligned: true }
7555
7429
  )
7556
7430
 
7557
- const shapeClustersToStretch: {
7558
- shapes: TLShape[]
7559
- pageBounds: Box
7560
- }[] = []
7561
-
7562
- const allBounds: Box[] = []
7563
- const visited = new Set<TLShapeId>()
7564
-
7565
- for (const shape of shapesToStretchFirstPass) {
7566
- if (visited.has(shape.id)) continue
7567
- visited.add(shape.id)
7568
-
7569
- const shapePageBounds = this.getShapePageBounds(shape)
7570
- if (!shapePageBounds) continue
7571
-
7572
- const shapesMovingTogether = [shape]
7573
- const boundsOfShapesMovingTogether: Box[] = [shapePageBounds]
7574
-
7575
- if (
7576
- !this.getShapeUtil(shape).canBeLaidOut?.(shape, {
7577
- type: 'stretch',
7578
- })
7579
- ) {
7580
- continue
7581
- }
7582
-
7583
- this.collectShapesViaArrowBindings({
7584
- bindings: this.getBindingsToShape(shape.id, 'arrow'),
7585
- initialShapes: shapesToStretchFirstPass,
7586
- resultShapes: shapesMovingTogether,
7587
- resultBounds: boundsOfShapesMovingTogether,
7588
- visited,
7589
- })
7590
-
7591
- const commonPageBounds = Box.Common(boundsOfShapesMovingTogether)
7592
- if (!commonPageBounds) continue
7593
-
7594
- shapeClustersToStretch.push({
7595
- shapes: shapesMovingTogether,
7596
- pageBounds: commonPageBounds,
7597
- })
7598
-
7599
- allBounds.push(commonPageBounds)
7600
- }
7601
-
7602
7431
  if (shapeClustersToStretch.length < 2) return this
7603
7432
 
7604
7433
  const commonBounds = Box.Common(allBounds)
@@ -7631,7 +7460,7 @@ export class Editor extends EventEmitter<TLEventMap> {
7631
7460
  // First translate
7632
7461
  const shapeLocalOffset = localOffset.clone()
7633
7462
  const parentTransform = this.getShapeParentTransform(shape)
7634
- if (parentTransform) localOffset.rot(-parentTransform.rotation())
7463
+ if (parentTransform) shapeLocalOffset.rot(-parentTransform.rotation())
7635
7464
  shapeLocalOffset.add(shape)
7636
7465
  const changes = this.getChangesToTranslateShape(shape, shapeLocalOffset)
7637
7466
  this.updateShape(changes)
@@ -7650,6 +7479,77 @@ export class Editor extends EventEmitter<TLEventMap> {
7650
7479
  return this
7651
7480
  }
7652
7481
 
7482
+ /**
7483
+ * Resize and reposition a set of shapes so that their combined page bounds matches the given
7484
+ * target bounds.
7485
+ *
7486
+ * @example
7487
+ * ```ts
7488
+ * editor.resizeToBounds([box1, box2], { x: 0, y: 0, w: 500, h: 500 })
7489
+ * editor.resizeToBounds(editor.getSelectedShapeIds(), new Box(0, 0, 500, 500))
7490
+ * ```
7491
+ *
7492
+ * @param shapes - The shapes (or shape ids) to resize.
7493
+ * @param bounds - The target bounding box.
7494
+ *
7495
+ * @public
7496
+ */
7497
+ resizeToBounds(shapes: TLShapeId[] | TLShape[], bounds: BoxLike): this {
7498
+ if (this.getIsReadonly()) return this
7499
+
7500
+ const targetBounds = Box.From(bounds)
7501
+
7502
+ const { clusters: shapeClusters, allBounds } = this.getShapeClusters(
7503
+ shapes,
7504
+ 'resize_to_bounds',
7505
+ { filterAxisAligned: true }
7506
+ )
7507
+
7508
+ if (shapeClusters.length === 0) return this
7509
+
7510
+ const commonBounds = Box.Common(allBounds)
7511
+ if (!commonBounds) return this
7512
+ if (commonBounds.width === 0 || commonBounds.height === 0) return this
7513
+
7514
+ const scaleX = targetBounds.width / commonBounds.width
7515
+ const scaleY = targetBounds.height / commonBounds.height
7516
+ const scale = new Vec(scaleX, scaleY)
7517
+
7518
+ shapeClusters.forEach(({ shapes, pageBounds }) => {
7519
+ const localOffset = new Vec(
7520
+ targetBounds.minX -
7521
+ commonBounds.minX +
7522
+ (pageBounds.minX - commonBounds.minX) * (scaleX - 1),
7523
+ targetBounds.minY - commonBounds.minY + (pageBounds.minY - commonBounds.minY) * (scaleY - 1)
7524
+ )
7525
+
7526
+ const scaleOrigin = new Vec(
7527
+ targetBounds.minX + (pageBounds.minX - commonBounds.minX) * scaleX,
7528
+ targetBounds.minY + (pageBounds.minY - commonBounds.minY) * scaleY
7529
+ )
7530
+
7531
+ for (const shape of shapes) {
7532
+ // First translate
7533
+ const shapeLocalOffset = localOffset.clone()
7534
+ const parentTransform = this.getShapeParentTransform(shape)
7535
+ if (parentTransform) shapeLocalOffset.rot(-parentTransform.rotation())
7536
+ shapeLocalOffset.add(shape)
7537
+ const changes = this.getChangesToTranslateShape(shape, shapeLocalOffset)
7538
+ this.updateShape(changes)
7539
+
7540
+ // Then resize
7541
+ this.resizeShape(shape.id, scale, {
7542
+ initialBounds: this.getShapeGeometry(shape).bounds,
7543
+ scaleOrigin,
7544
+ isAspectRatioLocked: this.getShapeUtil(shape).isAspectRatioLocked(shape),
7545
+ scaleAxisRotation: 0,
7546
+ })
7547
+ }
7548
+ })
7549
+
7550
+ return this
7551
+ }
7552
+
7653
7553
  /**
7654
7554
  * Resize a shape.
7655
7555
  *
@@ -11,11 +11,12 @@ import {
11
11
  import { exhaustiveSwitchError, noop } from '@tldraw/utils'
12
12
  import { TLHistoryBatchOptions, TLHistoryEntry } from '../../types/history-types'
13
13
 
14
- enum HistoryRecorderState {
15
- Recording = 'recording',
16
- RecordingPreserveRedoStack = 'recordingPreserveRedoStack',
17
- Paused = 'paused',
18
- }
14
+ const HistoryRecorderState = {
15
+ Recording: 'recording',
16
+ RecordingPreserveRedoStack: 'recordingPreserveRedoStack',
17
+ Paused: 'paused',
18
+ } as const
19
+ type HistoryRecorderState = (typeof HistoryRecorderState)[keyof typeof HistoryRecorderState]
19
20
 
20
21
  /** @public */
21
22
  export class HistoryManager<R extends UnknownRecord> {
@@ -69,7 +69,7 @@ export interface TLShapeUtilCanBindOpts<Shape extends TLShape = TLShape> {
69
69
  */
70
70
  export interface TLShapeUtilCanBeLaidOutOpts {
71
71
  /** The type of action causing the layout. */
72
- type?: 'align' | 'distribute' | 'pack' | 'stack' | 'flip' | 'stretch'
72
+ type?: 'align' | 'distribute' | 'pack' | 'stack' | 'flip' | 'stretch' | 'resize_to_bounds'
73
73
  /** The other shapes being laid out */
74
74
  shapes?: TLShape[]
75
75
  }
@@ -107,10 +107,10 @@ export class Pointing extends StateNode {
107
107
  const delta = new Vec(w / 2, h / 2)
108
108
  const parentTransform = this.editor.getShapeParentTransform(shape)
109
109
  if (parentTransform) delta.rot(-parentTransform.rotation())
110
- let scale = 1
110
+ const scale = this.editor.getResizeScaleFactor()
111
111
 
112
- if (this.editor.user.getIsDynamicResizeMode()) {
113
- scale = 1 / this.editor.getZoomLevel()
112
+ // A scale factor of 1 means dynamic sizing is not affecting this shape.
113
+ if (scale !== 1) {
114
114
  w *= scale
115
115
  h *= scale
116
116
  delta.mul(scale)
@@ -2,7 +2,12 @@ import { useValue } from '@tldraw/state-react'
2
2
  import React, { useEffect, useMemo } from 'react'
3
3
  import { RIGHT_MOUSE_BUTTON } from '../constants'
4
4
  import { tlenv } from '../globals/environment'
5
- import { preventDefault, releasePointerCapture, setPointerCapture } from '../utils/dom'
5
+ import {
6
+ elementShouldCaptureKeys,
7
+ preventDefault,
8
+ releasePointerCapture,
9
+ setPointerCapture,
10
+ } from '../utils/dom'
6
11
  import { getPointerInfo } from '../utils/getPointerInfo'
7
12
  import { useEditor } from './useEditor'
8
13
 
@@ -78,15 +83,14 @@ export function useCanvasEvents() {
78
83
  // check that e.target is an HTMLElement
79
84
  if (!(e.target instanceof HTMLElement)) return
80
85
 
81
- const editingShapeId = editor.getEditingShape()?.id
86
+ const editingShapeId = editor.getEditingShapeId()
82
87
  if (
83
88
  // if the target is not inside the editing shape
84
89
  !(editingShapeId && e.target.closest(`[data-shape-id="${editingShapeId}"]`)) &&
85
90
  // and the target is not an clickable element
86
91
  e.target.tagName !== 'A' &&
87
- // or a TextArea.tsx ?
88
- e.target.tagName !== 'TEXTAREA' &&
89
- !e.target.isContentEditable
92
+ // and the target is not an editable element
93
+ !elementShouldCaptureKeys(e.target, false)
90
94
  ) {
91
95
  preventDefault(e)
92
96
  }
@@ -1,9 +1,7 @@
1
1
  import { useEffect } from 'react'
2
- import { preventDefault } from '../utils/dom'
2
+ import { elementShouldCaptureKeys, preventDefault } from '../utils/dom'
3
3
  import { useEditor } from './useEditor'
4
4
 
5
- const IGNORED_TAGS = ['textarea', 'input']
6
-
7
5
  /**
8
6
  * When double tapping with the pencil in iOS, it enables a little zoom window in the UI. We don't
9
7
  * want this for drawing operations and can disable it by setting 'disableDoubleTapZoom' in the main
@@ -24,8 +22,7 @@ export function useFixSafariDoubleTapZoomPencilEvents(ref: React.RefObject<HTMLE
24
22
 
25
23
  // Allow events to propagate if the app is editing a shape, or if the event is occurring in a text area or input
26
24
  if (
27
- IGNORED_TAGS.includes((target as Element).tagName?.toLocaleLowerCase()) ||
28
- (target as HTMLElement).isContentEditable ||
25
+ elementShouldCaptureKeys(target instanceof Element ? target : null, false) ||
29
26
  editor.isIn('select.editing_shape')
30
27
  ) {
31
28
  return