@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.
- package/dist-cjs/index.d.ts +26 -1
- package/dist-cjs/index.js +2 -1
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/editor/Editor.js +123 -169
- package/dist-cjs/lib/editor/Editor.js.map +2 -2
- package/dist-cjs/lib/editor/managers/HistoryManager/HistoryManager.js +15 -16
- package/dist-cjs/lib/editor/managers/HistoryManager/HistoryManager.js.map +3 -3
- package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +1 -1
- package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js +2 -3
- package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js.map +2 -2
- package/dist-cjs/lib/hooks/useCanvasEvents.js +3 -3
- package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.js +1 -2
- package/dist-cjs/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.js.map +2 -2
- package/dist-cjs/lib/utils/dom.js +8 -4
- package/dist-cjs/lib/utils/dom.js.map +2 -2
- package/dist-cjs/version.js +3 -3
- package/dist-cjs/version.js.map +1 -1
- package/dist-esm/index.d.mts +26 -1
- package/dist-esm/index.mjs +3 -1
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +123 -169
- package/dist-esm/lib/editor/Editor.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/HistoryManager/HistoryManager.mjs +15 -16
- package/dist-esm/lib/editor/managers/HistoryManager/HistoryManager.mjs.map +3 -3
- package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +1 -1
- package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs +2 -3
- package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs.map +2 -2
- package/dist-esm/lib/hooks/useCanvasEvents.mjs +9 -4
- package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.mjs +2 -3
- package/dist-esm/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.mjs.map +2 -2
- package/dist-esm/lib/utils/dom.mjs +8 -4
- package/dist-esm/lib/utils/dom.mjs.map +2 -2
- package/dist-esm/version.mjs +3 -3
- package/dist-esm/version.mjs.map +1 -1
- package/package.json +7 -7
- package/src/index.ts +1 -0
- package/src/lib/editor/Editor.ts +169 -269
- package/src/lib/editor/managers/HistoryManager/HistoryManager.ts +6 -5
- package/src/lib/editor/shapes/ShapeUtil.ts +1 -1
- package/src/lib/editor/tools/BaseBoxShapeTool/children/Pointing.ts +3 -3
- package/src/lib/hooks/useCanvasEvents.ts +9 -5
- package/src/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.ts +2 -5
- package/src/lib/utils/dom.ts +16 -8
- package/src/version.ts +3 -3
package/dist-esm/version.mjs.map
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
53
|
-
"@tldraw/state-react": "4.5.
|
|
54
|
-
"@tldraw/store": "4.5.
|
|
55
|
-
"@tldraw/tlschema": "4.5.
|
|
56
|
-
"@tldraw/utils": "4.5.
|
|
57
|
-
"@tldraw/validate": "4.5.
|
|
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
package/src/lib/editor/Editor.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
7134
|
-
|
|
7135
|
-
|
|
7136
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
7553
|
-
|
|
7554
|
-
|
|
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)
|
|
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
|
-
|
|
15
|
-
Recording
|
|
16
|
-
RecordingPreserveRedoStack
|
|
17
|
-
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
|
-
|
|
110
|
+
const scale = this.editor.getResizeScaleFactor()
|
|
111
111
|
|
|
112
|
-
|
|
113
|
-
|
|
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 {
|
|
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.
|
|
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
|
-
//
|
|
88
|
-
e.target
|
|
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
|
-
|
|
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
|