@tldraw/editor 4.5.0-canary.b245c8ea38ef → 4.5.0-canary.b83751dd9949

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 +20 -1
  2. package/dist-cjs/index.js +2 -1
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/components/default-components/DefaultCanvas.js +13 -16
  5. package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
  6. package/dist-cjs/lib/editor/Editor.js +117 -167
  7. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  8. package/dist-cjs/lib/editor/managers/HistoryManager/HistoryManager.js +15 -16
  9. package/dist-cjs/lib/editor/managers/HistoryManager/HistoryManager.js.map +3 -3
  10. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +1 -1
  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 +20 -1
  20. package/dist-esm/index.mjs +3 -1
  21. package/dist-esm/index.mjs.map +2 -2
  22. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +13 -16
  23. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  24. package/dist-esm/lib/editor/Editor.mjs +117 -167
  25. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  26. package/dist-esm/lib/editor/managers/HistoryManager/HistoryManager.mjs +15 -16
  27. package/dist-esm/lib/editor/managers/HistoryManager/HistoryManager.mjs.map +3 -3
  28. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +1 -1
  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/components/default-components/DefaultCanvas.tsx +19 -26
  40. package/src/lib/editor/Editor.ts +160 -269
  41. package/src/lib/editor/managers/HistoryManager/HistoryManager.ts +6 -5
  42. package/src/lib/editor/shapes/ShapeUtil.ts +1 -1
  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
@@ -4988,6 +4988,53 @@ class Editor extends (_a = import_eventemitter3.default, _getIsShapeHiddenCache_
4988
4988
  if (changes) this.updateShapes(changes);
4989
4989
  return this;
4990
4990
  }
4991
+ /**
4992
+ * Shared clustering logic for layout methods. Resolves shapes, optionally filters to
4993
+ * axis-aligned shapes, checks canBeLaidOut, and groups shapes into clusters via arrow bindings.
4994
+ *
4995
+ * @internal
4996
+ */
4997
+ getShapeClusters(shapes, type, opts) {
4998
+ const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
4999
+ let freshShapes = (0, import_utils.compact)(ids.map((id) => this.getShape(id)));
5000
+ if (opts?.filterAxisAligned) {
5001
+ freshShapes = freshShapes.filter(
5002
+ (s) => this.getShapePageTransform(s)?.rotation() % (import_utils2.PI / 2) === 0
5003
+ );
5004
+ }
5005
+ const clusters = [];
5006
+ const allBounds = [];
5007
+ const visited = /* @__PURE__ */ new Set();
5008
+ for (const shape of freshShapes) {
5009
+ if (visited.has(shape.id)) continue;
5010
+ visited.add(shape.id);
5011
+ const shapePageBounds = this.getShapePageBounds(shape);
5012
+ if (!shapePageBounds) continue;
5013
+ if (!this.getShapeUtil(shape).canBeLaidOut?.(shape, {
5014
+ type,
5015
+ shapes: freshShapes
5016
+ })) {
5017
+ continue;
5018
+ }
5019
+ const shapesMovingTogether = [shape];
5020
+ const boundsOfShapesMovingTogether = [shapePageBounds];
5021
+ this.collectShapesViaArrowBindings({
5022
+ bindings: this.getBindingsToShape(shape.id, "arrow"),
5023
+ initialShapes: freshShapes,
5024
+ resultShapes: shapesMovingTogether,
5025
+ resultBounds: boundsOfShapesMovingTogether,
5026
+ visited
5027
+ });
5028
+ const commonPageBounds = import_Box.Box.Common(boundsOfShapesMovingTogether);
5029
+ if (!commonPageBounds) continue;
5030
+ clusters.push({
5031
+ shapes: shapesMovingTogether,
5032
+ pageBounds: commonPageBounds
5033
+ });
5034
+ allBounds.push(commonPageBounds);
5035
+ }
5036
+ return { clusters, allBounds };
5037
+ }
4991
5038
  /**
4992
5039
  * @internal
4993
5040
  */
@@ -5098,40 +5145,8 @@ class Editor extends (_a = import_eventemitter3.default, _getIsShapeHiddenCache_
5098
5145
  */
5099
5146
  stackShapes(shapes, operation, gap) {
5100
5147
  const _gap = gap ?? this.options.adjacentShapeMargin;
5101
- const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
5102
5148
  if (this.getIsReadonly()) return this;
5103
- const shapesToStackFirstPass = (0, import_utils.compact)(ids.map((id) => this.getShape(id)));
5104
- const shapeClustersToStack = [];
5105
- const allBounds = [];
5106
- const visited = /* @__PURE__ */ new Set();
5107
- for (const shape of shapesToStackFirstPass) {
5108
- if (visited.has(shape.id)) continue;
5109
- visited.add(shape.id);
5110
- const shapePageBounds = this.getShapePageBounds(shape);
5111
- if (!shapePageBounds) continue;
5112
- if (!this.getShapeUtil(shape).canBeLaidOut?.(shape, {
5113
- type: "stack",
5114
- shapes: shapesToStackFirstPass
5115
- })) {
5116
- continue;
5117
- }
5118
- const shapesMovingTogether = [shape];
5119
- const boundsOfShapesMovingTogether = [shapePageBounds];
5120
- this.collectShapesViaArrowBindings({
5121
- bindings: this.getBindingsToShape(shape.id, "arrow"),
5122
- initialShapes: shapesToStackFirstPass,
5123
- resultShapes: shapesMovingTogether,
5124
- resultBounds: boundsOfShapesMovingTogether,
5125
- visited
5126
- });
5127
- const commonPageBounds = import_Box.Box.Common(boundsOfShapesMovingTogether);
5128
- if (!commonPageBounds) continue;
5129
- shapeClustersToStack.push({
5130
- shapes: shapesMovingTogether,
5131
- pageBounds: commonPageBounds
5132
- });
5133
- allBounds.push(commonPageBounds);
5134
- }
5149
+ const { clusters: shapeClustersToStack } = this.getShapeClusters(shapes, "stack");
5135
5150
  const len = shapeClustersToStack.length;
5136
5151
  if (_gap === 0 && len < 3 || len < 2) return this;
5137
5152
  let val;
@@ -5217,40 +5232,11 @@ class Editor extends (_a = import_eventemitter3.default, _getIsShapeHiddenCache_
5217
5232
  packShapes(shapes, _gap) {
5218
5233
  if (this.getIsReadonly()) return this;
5219
5234
  const gap = _gap ?? this.options.adjacentShapeMargin;
5220
- const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
5221
- const shapesToPackFirstPass = (0, import_utils.compact)(ids.map((id) => this.getShape(id)));
5222
- const shapeClustersToPack = [];
5223
- const allBounds = [];
5224
- const visited = /* @__PURE__ */ new Set();
5225
- for (const shape of shapesToPackFirstPass) {
5226
- if (visited.has(shape.id)) continue;
5227
- visited.add(shape.id);
5228
- const shapePageBounds = this.getShapePageBounds(shape);
5229
- if (!shapePageBounds) continue;
5230
- if (!this.getShapeUtil(shape).canBeLaidOut?.(shape, {
5231
- type: "pack",
5232
- shapes: shapesToPackFirstPass
5233
- })) {
5234
- continue;
5235
- }
5236
- const shapesMovingTogether = [shape];
5237
- const boundsOfShapesMovingTogether = [shapePageBounds];
5238
- this.collectShapesViaArrowBindings({
5239
- bindings: this.getBindingsToShape(shape.id, "arrow"),
5240
- initialShapes: shapesToPackFirstPass,
5241
- resultShapes: shapesMovingTogether,
5242
- resultBounds: boundsOfShapesMovingTogether,
5243
- visited
5244
- });
5245
- const commonPageBounds = import_Box.Box.Common(boundsOfShapesMovingTogether);
5246
- if (!commonPageBounds) continue;
5247
- shapeClustersToPack.push({
5248
- shapes: shapesMovingTogether,
5249
- pageBounds: commonPageBounds,
5250
- nextPageBounds: commonPageBounds.clone()
5251
- });
5252
- allBounds.push(commonPageBounds);
5253
- }
5235
+ const { clusters, allBounds } = this.getShapeClusters(shapes, "pack");
5236
+ const shapeClustersToPack = clusters.map((cluster) => ({
5237
+ ...cluster,
5238
+ nextPageBounds: cluster.pageBounds.clone()
5239
+ }));
5254
5240
  if (shapeClustersToPack.length < 2) return this;
5255
5241
  let area = 0;
5256
5242
  for (const { pageBounds } of shapeClustersToPack) {
@@ -5334,39 +5320,7 @@ class Editor extends (_a = import_eventemitter3.default, _getIsShapeHiddenCache_
5334
5320
  */
5335
5321
  alignShapes(shapes, operation) {
5336
5322
  if (this.getIsReadonly()) return this;
5337
- const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
5338
- const shapesToAlignFirstPass = (0, import_utils.compact)(ids.map((id) => this.getShape(id)));
5339
- const shapeClustersToAlign = [];
5340
- const allBounds = [];
5341
- const visited = /* @__PURE__ */ new Set();
5342
- for (const shape of shapesToAlignFirstPass) {
5343
- if (visited.has(shape.id)) continue;
5344
- visited.add(shape.id);
5345
- const shapePageBounds = this.getShapePageBounds(shape);
5346
- if (!shapePageBounds) continue;
5347
- if (!this.getShapeUtil(shape).canBeLaidOut?.(shape, {
5348
- type: "align",
5349
- shapes: shapesToAlignFirstPass
5350
- })) {
5351
- continue;
5352
- }
5353
- const shapesMovingTogether = [shape];
5354
- const boundsOfShapesMovingTogether = [shapePageBounds];
5355
- this.collectShapesViaArrowBindings({
5356
- bindings: this.getBindingsToShape(shape.id, "arrow"),
5357
- initialShapes: shapesToAlignFirstPass,
5358
- resultShapes: shapesMovingTogether,
5359
- resultBounds: boundsOfShapesMovingTogether,
5360
- visited
5361
- });
5362
- const commonPageBounds = import_Box.Box.Common(boundsOfShapesMovingTogether);
5363
- if (!commonPageBounds) continue;
5364
- shapeClustersToAlign.push({
5365
- shapes: shapesMovingTogether,
5366
- pageBounds: commonPageBounds
5367
- });
5368
- allBounds.push(commonPageBounds);
5369
- }
5323
+ const { clusters: shapeClustersToAlign, allBounds } = this.getShapeClusters(shapes, "align");
5370
5324
  if (shapeClustersToAlign.length < 2) return this;
5371
5325
  const commonBounds = import_Box.Box.Common(allBounds);
5372
5326
  const changes = [];
@@ -5428,39 +5382,7 @@ class Editor extends (_a = import_eventemitter3.default, _getIsShapeHiddenCache_
5428
5382
  */
5429
5383
  distributeShapes(shapes, operation) {
5430
5384
  if (this.getIsReadonly()) return this;
5431
- const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
5432
- const shapesToDistributeFirstPass = (0, import_utils.compact)(ids.map((id) => this.getShape(id)));
5433
- const shapeClustersToDistribute = [];
5434
- const allBounds = [];
5435
- const visited = /* @__PURE__ */ new Set();
5436
- for (const shape of shapesToDistributeFirstPass) {
5437
- if (visited.has(shape.id)) continue;
5438
- visited.add(shape.id);
5439
- const shapePageBounds = this.getShapePageBounds(shape);
5440
- if (!shapePageBounds) continue;
5441
- if (!this.getShapeUtil(shape).canBeLaidOut?.(shape, {
5442
- type: "distribute",
5443
- shapes: shapesToDistributeFirstPass
5444
- })) {
5445
- continue;
5446
- }
5447
- const shapesMovingTogether = [shape];
5448
- const boundsOfShapesMovingTogether = [shapePageBounds];
5449
- this.collectShapesViaArrowBindings({
5450
- bindings: this.getBindingsToShape(shape.id, "arrow"),
5451
- initialShapes: shapesToDistributeFirstPass,
5452
- resultShapes: shapesMovingTogether,
5453
- resultBounds: boundsOfShapesMovingTogether,
5454
- visited
5455
- });
5456
- const commonPageBounds = import_Box.Box.Common(boundsOfShapesMovingTogether);
5457
- if (!commonPageBounds) continue;
5458
- shapeClustersToDistribute.push({
5459
- shapes: shapesMovingTogether,
5460
- pageBounds: commonPageBounds
5461
- });
5462
- allBounds.push(commonPageBounds);
5463
- }
5385
+ const { clusters: shapeClustersToDistribute } = this.getShapeClusters(shapes, "distribute");
5464
5386
  if (shapeClustersToDistribute.length < 3) return this;
5465
5387
  let val;
5466
5388
  let min;
@@ -5482,6 +5404,7 @@ class Editor extends (_a = import_eventemitter3.default, _getIsShapeHiddenCache_
5482
5404
  const last2 = shapeClustersToDistribute.sort((a, b) => b.pageBounds[max] - a.pageBounds[max])[0];
5483
5405
  if (first === last2) {
5484
5406
  const excludedShapeIds = new Set(first.shapes.map((s) => s.id));
5407
+ const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
5485
5408
  return this.distributeShapes(
5486
5409
  ids.filter((id) => !excludedShapeIds.has(id)),
5487
5410
  operation
@@ -5534,41 +5457,12 @@ class Editor extends (_a = import_eventemitter3.default, _getIsShapeHiddenCache_
5534
5457
  * @public
5535
5458
  */
5536
5459
  stretchShapes(shapes, operation) {
5537
- const ids = typeof shapes[0] === "string" ? shapes : shapes.map((s) => s.id);
5538
5460
  if (this.getIsReadonly()) return this;
5539
- const shapesToStretchFirstPass = (0, import_utils.compact)(ids.map((id) => this.getShape(id))).filter(
5540
- (s) => this.getShapePageTransform(s)?.rotation() % (import_utils2.PI / 2) === 0
5461
+ const { clusters: shapeClustersToStretch, allBounds } = this.getShapeClusters(
5462
+ shapes,
5463
+ "stretch",
5464
+ { filterAxisAligned: true }
5541
5465
  );
5542
- const shapeClustersToStretch = [];
5543
- const allBounds = [];
5544
- const visited = /* @__PURE__ */ new Set();
5545
- for (const shape of shapesToStretchFirstPass) {
5546
- if (visited.has(shape.id)) continue;
5547
- visited.add(shape.id);
5548
- const shapePageBounds = this.getShapePageBounds(shape);
5549
- if (!shapePageBounds) continue;
5550
- const shapesMovingTogether = [shape];
5551
- const boundsOfShapesMovingTogether = [shapePageBounds];
5552
- if (!this.getShapeUtil(shape).canBeLaidOut?.(shape, {
5553
- type: "stretch"
5554
- })) {
5555
- continue;
5556
- }
5557
- this.collectShapesViaArrowBindings({
5558
- bindings: this.getBindingsToShape(shape.id, "arrow"),
5559
- initialShapes: shapesToStretchFirstPass,
5560
- resultShapes: shapesMovingTogether,
5561
- resultBounds: boundsOfShapesMovingTogether,
5562
- visited
5563
- });
5564
- const commonPageBounds = import_Box.Box.Common(boundsOfShapesMovingTogether);
5565
- if (!commonPageBounds) continue;
5566
- shapeClustersToStretch.push({
5567
- shapes: shapesMovingTogether,
5568
- pageBounds: commonPageBounds
5569
- });
5570
- allBounds.push(commonPageBounds);
5571
- }
5572
5466
  if (shapeClustersToStretch.length < 2) return this;
5573
5467
  const commonBounds = import_Box.Box.Common(allBounds);
5574
5468
  let val;
@@ -5594,7 +5488,7 @@ class Editor extends (_a = import_eventemitter3.default, _getIsShapeHiddenCache_
5594
5488
  for (const shape of shapes2) {
5595
5489
  const shapeLocalOffset = localOffset.clone();
5596
5490
  const parentTransform = this.getShapeParentTransform(shape);
5597
- if (parentTransform) localOffset.rot(-parentTransform.rotation());
5491
+ if (parentTransform) shapeLocalOffset.rot(-parentTransform.rotation());
5598
5492
  shapeLocalOffset.add(shape);
5599
5493
  const changes = this.getChangesToTranslateShape(shape, shapeLocalOffset);
5600
5494
  this.updateShape(changes);
@@ -5609,6 +5503,62 @@ class Editor extends (_a = import_eventemitter3.default, _getIsShapeHiddenCache_
5609
5503
  });
5610
5504
  return this;
5611
5505
  }
5506
+ /**
5507
+ * Resize and reposition a set of shapes so that their combined page bounds matches the given
5508
+ * target bounds.
5509
+ *
5510
+ * @example
5511
+ * ```ts
5512
+ * editor.resizeToBounds([box1, box2], { x: 0, y: 0, w: 500, h: 500 })
5513
+ * editor.resizeToBounds(editor.getSelectedShapeIds(), new Box(0, 0, 500, 500))
5514
+ * ```
5515
+ *
5516
+ * @param shapes - The shapes (or shape ids) to resize.
5517
+ * @param bounds - The target bounding box.
5518
+ *
5519
+ * @public
5520
+ */
5521
+ resizeToBounds(shapes, bounds) {
5522
+ if (this.getIsReadonly()) return this;
5523
+ const targetBounds = import_Box.Box.From(bounds);
5524
+ const { clusters: shapeClusters, allBounds } = this.getShapeClusters(
5525
+ shapes,
5526
+ "resize_to_bounds",
5527
+ { filterAxisAligned: true }
5528
+ );
5529
+ if (shapeClusters.length === 0) return this;
5530
+ const commonBounds = import_Box.Box.Common(allBounds);
5531
+ if (!commonBounds) return this;
5532
+ if (commonBounds.width === 0 || commonBounds.height === 0) return this;
5533
+ const scaleX = targetBounds.width / commonBounds.width;
5534
+ const scaleY = targetBounds.height / commonBounds.height;
5535
+ const scale = new import_Vec.Vec(scaleX, scaleY);
5536
+ shapeClusters.forEach(({ shapes: shapes2, pageBounds }) => {
5537
+ const localOffset = new import_Vec.Vec(
5538
+ targetBounds.minX - commonBounds.minX + (pageBounds.minX - commonBounds.minX) * (scaleX - 1),
5539
+ targetBounds.minY - commonBounds.minY + (pageBounds.minY - commonBounds.minY) * (scaleY - 1)
5540
+ );
5541
+ const scaleOrigin = new import_Vec.Vec(
5542
+ targetBounds.minX + (pageBounds.minX - commonBounds.minX) * scaleX,
5543
+ targetBounds.minY + (pageBounds.minY - commonBounds.minY) * scaleY
5544
+ );
5545
+ for (const shape of shapes2) {
5546
+ const shapeLocalOffset = localOffset.clone();
5547
+ const parentTransform = this.getShapeParentTransform(shape);
5548
+ if (parentTransform) shapeLocalOffset.rot(-parentTransform.rotation());
5549
+ shapeLocalOffset.add(shape);
5550
+ const changes = this.getChangesToTranslateShape(shape, shapeLocalOffset);
5551
+ this.updateShape(changes);
5552
+ this.resizeShape(shape.id, scale, {
5553
+ initialBounds: this.getShapeGeometry(shape).bounds,
5554
+ scaleOrigin,
5555
+ isAspectRatioLocked: this.getShapeUtil(shape).isAspectRatioLocked(shape),
5556
+ scaleAxisRotation: 0
5557
+ });
5558
+ }
5559
+ });
5560
+ return this;
5561
+ }
5612
5562
  /**
5613
5563
  * Resize a shape.
5614
5564
  *