@tldraw/editor 5.5.0-next.f9308d324e1c → 5.5.0
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 +49 -23
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/TldrawEditor.js +16 -12
- package/dist-cjs/lib/TldrawEditor.js.map +2 -2
- package/dist-cjs/lib/components/MenuClickCapture.js +42 -3
- package/dist-cjs/lib/components/MenuClickCapture.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultCanvas.js +5 -1
- package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js +2 -5
- package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js.map +3 -3
- package/dist-cjs/lib/components/default-components/DefaultSpinner.js.map +2 -2
- package/dist-cjs/lib/editor/Editor.js +168 -181
- package/dist-cjs/lib/editor/Editor.js.map +3 -3
- package/dist-cjs/lib/editor/derivations/bindingsIndex.js +2 -0
- package/dist-cjs/lib/editor/derivations/bindingsIndex.js.map +2 -2
- package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js +3 -2
- package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/EditorManager.js +11 -2
- package/dist-cjs/lib/editor/managers/EditorManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/PerformanceManager/PerformanceManager.js +49 -63
- package/dist-cjs/lib/editor/managers/PerformanceManager/PerformanceManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js +1 -1
- package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/SnapManager/BoundsSnaps.js +24 -24
- package/dist-cjs/lib/editor/managers/SnapManager/BoundsSnaps.js.map +2 -2
- package/dist-cjs/lib/editor/managers/SnapManager/HandleSnaps.js +7 -1
- package/dist-cjs/lib/editor/managers/SnapManager/HandleSnaps.js.map +2 -2
- package/dist-cjs/lib/editor/managers/SnapManager/SnapManager.js +4 -3
- package/dist-cjs/lib/editor/managers/SnapManager/SnapManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +4 -4
- package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
- package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js +14 -6
- package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js.map +3 -3
- package/dist-cjs/lib/editor/types/external-content.js.map +1 -1
- package/dist-cjs/lib/exports/ExportDelay.js +10 -2
- package/dist-cjs/lib/exports/ExportDelay.js.map +2 -2
- package/dist-cjs/lib/exports/FontEmbedder.js +25 -11
- package/dist-cjs/lib/exports/FontEmbedder.js.map +2 -2
- package/dist-cjs/lib/exports/StyleEmbedder.js +9 -4
- package/dist-cjs/lib/exports/StyleEmbedder.js.map +2 -2
- package/dist-cjs/lib/exports/fetchCache.js +3 -0
- package/dist-cjs/lib/exports/fetchCache.js.map +2 -2
- package/dist-cjs/lib/exports/getSvgAsImage.js +10 -4
- package/dist-cjs/lib/exports/getSvgAsImage.js.map +2 -2
- package/dist-cjs/lib/exports/getSvgJsx.js +18 -9
- package/dist-cjs/lib/exports/getSvgJsx.js.map +2 -2
- package/dist-cjs/lib/exports/parseCss.js +1 -1
- package/dist-cjs/lib/exports/parseCss.js.map +1 -1
- package/dist-cjs/lib/hooks/EditorComponentsContext.js.map +2 -2
- package/dist-cjs/lib/hooks/useCanvasEvents.js +24 -0
- package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/useIdentity.js +6 -2
- package/dist-cjs/lib/hooks/useIdentity.js.map +3 -3
- package/dist-cjs/lib/license/LicenseManager.js +9 -1
- package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
- package/dist-cjs/lib/license/LicenseProvider.js +12 -4
- package/dist-cjs/lib/license/LicenseProvider.js.map +2 -2
- package/dist-cjs/lib/utils/debug-flags.js +0 -6
- package/dist-cjs/lib/utils/debug-flags.js.map +2 -2
- package/dist-cjs/lib/utils/deepLinks.js +2 -1
- package/dist-cjs/lib/utils/deepLinks.js.map +2 -2
- package/dist-cjs/lib/utils/sync/LocalIndexedDb.js +32 -25
- package/dist-cjs/lib/utils/sync/LocalIndexedDb.js.map +2 -2
- package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js +81 -40
- package/dist-cjs/lib/utils/sync/TLLocalSyncClient.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 +49 -23
- package/dist-esm/index.mjs +4 -2
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/TldrawEditor.mjs +17 -13
- package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
- package/dist-esm/lib/components/MenuClickCapture.mjs +42 -3
- package/dist-esm/lib/components/MenuClickCapture.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +5 -1
- package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs +2 -5
- package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs.map +3 -3
- package/dist-esm/lib/components/default-components/DefaultSpinner.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +168 -181
- package/dist-esm/lib/editor/Editor.mjs.map +3 -3
- package/dist-esm/lib/editor/derivations/bindingsIndex.mjs +2 -0
- package/dist-esm/lib/editor/derivations/bindingsIndex.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs +3 -2
- package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/EditorManager.mjs +11 -2
- package/dist-esm/lib/editor/managers/EditorManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/PerformanceManager/PerformanceManager.mjs +49 -63
- package/dist-esm/lib/editor/managers/PerformanceManager/PerformanceManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs +1 -1
- package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/SnapManager/BoundsSnaps.mjs +24 -24
- package/dist-esm/lib/editor/managers/SnapManager/BoundsSnaps.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/SnapManager/HandleSnaps.mjs +7 -1
- package/dist-esm/lib/editor/managers/SnapManager/HandleSnaps.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/SnapManager/SnapManager.mjs +4 -3
- package/dist-esm/lib/editor/managers/SnapManager/SnapManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +4 -4
- package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
- package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs +14 -6
- package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs.map +3 -3
- package/dist-esm/lib/exports/ExportDelay.mjs +11 -3
- package/dist-esm/lib/exports/ExportDelay.mjs.map +2 -2
- package/dist-esm/lib/exports/FontEmbedder.mjs +25 -11
- package/dist-esm/lib/exports/FontEmbedder.mjs.map +2 -2
- package/dist-esm/lib/exports/StyleEmbedder.mjs +9 -4
- package/dist-esm/lib/exports/StyleEmbedder.mjs.map +2 -2
- package/dist-esm/lib/exports/fetchCache.mjs +3 -0
- package/dist-esm/lib/exports/fetchCache.mjs.map +2 -2
- package/dist-esm/lib/exports/getSvgAsImage.mjs +10 -4
- package/dist-esm/lib/exports/getSvgAsImage.mjs.map +2 -2
- package/dist-esm/lib/exports/getSvgJsx.mjs +18 -9
- package/dist-esm/lib/exports/getSvgJsx.mjs.map +2 -2
- package/dist-esm/lib/exports/parseCss.mjs +1 -1
- package/dist-esm/lib/exports/parseCss.mjs.map +1 -1
- package/dist-esm/lib/hooks/EditorComponentsContext.mjs.map +2 -2
- package/dist-esm/lib/hooks/useCanvasEvents.mjs +24 -0
- package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/useIdentity.mjs +7 -3
- package/dist-esm/lib/hooks/useIdentity.mjs.map +3 -3
- package/dist-esm/lib/license/LicenseManager.mjs +9 -1
- package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
- package/dist-esm/lib/license/LicenseProvider.mjs +13 -5
- package/dist-esm/lib/license/LicenseProvider.mjs.map +2 -2
- package/dist-esm/lib/utils/debug-flags.mjs +0 -6
- package/dist-esm/lib/utils/debug-flags.mjs.map +2 -2
- package/dist-esm/lib/utils/deepLinks.mjs +2 -1
- package/dist-esm/lib/utils/deepLinks.mjs.map +2 -2
- package/dist-esm/lib/utils/sync/LocalIndexedDb.mjs +32 -25
- package/dist-esm/lib/utils/sync/LocalIndexedDb.mjs.map +2 -2
- package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs +81 -40
- package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs.map +2 -2
- package/dist-esm/version.mjs +3 -3
- package/dist-esm/version.mjs.map +1 -1
- package/editor.css +9 -1
- package/package.json +8 -11
- package/src/index.ts +5 -1
- package/src/lib/TldrawEditor.tsx +35 -21
- package/src/lib/components/MenuClickCapture.tsx +55 -4
- package/src/lib/components/default-components/DefaultCanvas.tsx +7 -1
- package/src/lib/components/default-components/DefaultErrorFallback.tsx +13 -5
- package/src/lib/components/default-components/DefaultSpinner.tsx +4 -1
- package/src/lib/editor/Editor.test.ts +65 -0
- package/src/lib/editor/Editor.ts +261 -294
- package/src/lib/editor/derivations/bindingsIndex.ts +3 -0
- package/src/lib/editor/managers/ClickManager/ClickManager.test.ts +1 -0
- package/src/lib/editor/managers/ClickManager/ClickManager.ts +3 -2
- package/src/lib/editor/managers/EditorManager.test.ts +32 -1
- package/src/lib/editor/managers/EditorManager.ts +15 -3
- package/src/lib/editor/managers/PerformanceManager/PerformanceManager.test.ts +80 -0
- package/src/lib/editor/managers/PerformanceManager/PerformanceManager.ts +54 -81
- package/src/lib/editor/managers/ScribbleManager/ScribbleManager.ts +1 -1
- package/src/lib/editor/managers/SnapManager/BoundsSnaps.ts +32 -36
- package/src/lib/editor/managers/SnapManager/HandleSnaps.ts +13 -2
- package/src/lib/editor/managers/SnapManager/SnapManager.test.ts +13 -0
- package/src/lib/editor/managers/SnapManager/SnapManager.ts +6 -4
- package/src/lib/editor/managers/TextManager/TextManager.test.ts +74 -0
- package/src/lib/editor/managers/TextManager/TextManager.ts +6 -5
- package/src/lib/editor/shapes/group/GroupShapeUtil.tsx +23 -8
- package/src/lib/editor/types/external-content.ts +5 -0
- package/src/lib/exports/ExportDelay.tsx +16 -4
- package/src/lib/exports/FontEmbedder.test.ts +99 -0
- package/src/lib/exports/FontEmbedder.ts +32 -11
- package/src/lib/exports/StyleEmbedder.ts +13 -4
- package/src/lib/exports/fetchCache.test.ts +22 -0
- package/src/lib/exports/fetchCache.ts +4 -0
- package/src/lib/exports/getSvgAsImage.ts +13 -5
- package/src/lib/exports/getSvgJsx.test.ts +43 -1
- package/src/lib/exports/getSvgJsx.tsx +28 -16
- package/src/lib/exports/parseCss.test.ts +4 -0
- package/src/lib/exports/parseCss.ts +2 -2
- package/src/lib/hooks/EditorComponentsContext.tsx +2 -1
- package/src/lib/hooks/useCanvasEvents.ts +42 -0
- package/src/lib/hooks/useIdentity.tsx +12 -1
- package/src/lib/license/LicenseManager.ts +13 -1
- package/src/lib/license/LicenseProvider.test.tsx +76 -0
- package/src/lib/license/LicenseProvider.tsx +19 -5
- package/src/lib/test/preserveFocus.test.tsx +81 -0
- package/src/lib/utils/debug-flags.ts +0 -6
- package/src/lib/utils/deepLinks.test.ts +15 -0
- package/src/lib/utils/deepLinks.ts +8 -3
- package/src/lib/utils/sync/LocalIndexedDb.test.ts +121 -1
- package/src/lib/utils/sync/LocalIndexedDb.ts +41 -28
- package/src/lib/utils/sync/TLLocalSyncClient.test.ts +257 -1
- package/src/lib/utils/sync/TLLocalSyncClient.ts +112 -54
- package/src/version.ts +3 -3
- package/dist-cjs/lib/primitives/geometry/geometry.bench.js +0 -133
- package/dist-cjs/lib/primitives/geometry/geometry.bench.js.map +0 -7
- package/dist-esm/lib/primitives/geometry/geometry.bench.mjs +0 -132
- package/dist-esm/lib/primitives/geometry/geometry.bench.mjs.map +0 -7
- package/src/lib/primitives/geometry/geometry.bench.ts +0 -179
package/src/lib/editor/Editor.ts
CHANGED
|
@@ -341,6 +341,11 @@ export interface TLRenderingShape {
|
|
|
341
341
|
|
|
342
342
|
const RENDERING_SHAPES_SORT_CACHE_THRESHOLD = 100
|
|
343
343
|
|
|
344
|
+
const AXIS = {
|
|
345
|
+
horizontal: { val: 'x', min: 'minX', max: 'maxX', dim: 'width' },
|
|
346
|
+
vertical: { val: 'y', min: 'minY', max: 'maxY', dim: 'height' },
|
|
347
|
+
} as const
|
|
348
|
+
|
|
344
349
|
/** @public */
|
|
345
350
|
export class Editor extends EventEmitter<TLEventMap> {
|
|
346
351
|
readonly id = uniqueId()
|
|
@@ -753,7 +758,9 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
753
758
|
}
|
|
754
759
|
|
|
755
760
|
if (deleteBindingIds.length) {
|
|
756
|
-
this
|
|
761
|
+
// straight to the store: this cleanup must run even when deleteBindings would
|
|
762
|
+
// refuse (readonly), e.g. for a deletion that arrived from a remote peer
|
|
763
|
+
this.store.remove(deleteBindingIds)
|
|
757
764
|
}
|
|
758
765
|
},
|
|
759
766
|
},
|
|
@@ -1970,6 +1977,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
1970
1977
|
* @public
|
|
1971
1978
|
**/
|
|
1972
1979
|
updateDocumentSettings(settings: Partial<TLDocument>): this {
|
|
1980
|
+
if (this.getIsReadonly()) return this
|
|
1973
1981
|
this.run(
|
|
1974
1982
|
() => {
|
|
1975
1983
|
this.store.put([{ ...this.getDocumentSettings(), ...settings }])
|
|
@@ -2005,7 +2013,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
2005
2013
|
this._updateInstanceState(partial, { history: 'ignore', ...historyOptions })
|
|
2006
2014
|
|
|
2007
2015
|
if (partial.isChangingStyle !== undefined) {
|
|
2008
|
-
clearTimeout(this._isChangingStyleTimeout)
|
|
2016
|
+
this.timers.clearTimeout(this._isChangingStyleTimeout)
|
|
2009
2017
|
if (partial.isChangingStyle === true) {
|
|
2010
2018
|
// If we've set to true, set a new reset timeout to change the value back to false after 1 seconds
|
|
2011
2019
|
this._isChangingStyleTimeout = this.timers.setTimeout(() => {
|
|
@@ -2197,10 +2205,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
2197
2205
|
* @public
|
|
2198
2206
|
*/
|
|
2199
2207
|
select(...shapes: TLShapeId[] | TLShape[]): this {
|
|
2200
|
-
const ids =
|
|
2201
|
-
typeof shapes[0] === 'string'
|
|
2202
|
-
? (shapes as TLShapeId[])
|
|
2203
|
-
: (shapes as TLShape[]).map((shape) => shape.id)
|
|
2208
|
+
const ids = toShapeIds(shapes)
|
|
2204
2209
|
this.setSelectedShapes(ids)
|
|
2205
2210
|
return this
|
|
2206
2211
|
}
|
|
@@ -2216,10 +2221,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
2216
2221
|
* @public
|
|
2217
2222
|
*/
|
|
2218
2223
|
deselect(...shapes: TLShapeId[] | TLShape[]): this {
|
|
2219
|
-
const ids =
|
|
2220
|
-
typeof shapes[0] === 'string'
|
|
2221
|
-
? (shapes as TLShapeId[])
|
|
2222
|
-
: (shapes as TLShape[]).map((shape) => shape.id)
|
|
2224
|
+
const ids = toShapeIds(shapes)
|
|
2223
2225
|
const selectedShapeIds = this.getSelectedShapeIds()
|
|
2224
2226
|
if (selectedShapeIds.length > 0 && ids.length > 0) {
|
|
2225
2227
|
this.setSelectedShapes(selectedShapeIds.filter((id) => !ids.includes(id)))
|
|
@@ -2264,10 +2266,11 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
2264
2266
|
parentToSelectWithinId = this.getCurrentPageId()
|
|
2265
2267
|
}
|
|
2266
2268
|
|
|
2267
|
-
// Select all the unlocked shapes within the parent
|
|
2269
|
+
// Select all the unlocked shapes within the parent. Only the shape's own lock matters here:
|
|
2270
|
+
// selecting inside a locked frame or group is allowed, mutating is not.
|
|
2268
2271
|
const ids = this.getSortedChildIdsForParent(parentToSelectWithinId)
|
|
2269
2272
|
if (ids.length <= 0) return this
|
|
2270
|
-
this.setSelectedShapes(this.
|
|
2273
|
+
this.setSelectedShapes(ids.filter((id) => !this.getShape(id)?.isLocked))
|
|
2271
2274
|
return this
|
|
2272
2275
|
}
|
|
2273
2276
|
|
|
@@ -2288,12 +2291,14 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
2288
2291
|
firstParentId &&
|
|
2289
2292
|
selectedShapeIds.every((shapeId) => this.getShape(shapeId)?.parentId === firstParentId) &&
|
|
2290
2293
|
!isPageId(firstParentId)
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2294
|
+
// Locked shapes (and children of locked containers) can't be selected by clicking or
|
|
2295
|
+
// select all, so traversal skips them too
|
|
2296
|
+
const filteredShapes = this.getCurrentPageShapes().filter(
|
|
2297
|
+
(shape) =>
|
|
2298
|
+
!this.isShapeOrAncestorLocked(shape) &&
|
|
2299
|
+
(isSelectedWithinContainer ? shape.parentId === firstParentId : isPageId(shape.parentId))
|
|
2300
|
+
)
|
|
2301
|
+
const readingOrderShapes = this._getShapesInReadingOrder(filteredShapes)
|
|
2297
2302
|
const currentShapeId: TLShapeId | undefined =
|
|
2298
2303
|
selectedShapeIds.length === 1
|
|
2299
2304
|
? selectedShapeIds[0]
|
|
@@ -2302,6 +2307,9 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
2302
2307
|
let adjacentShapeId: TLShapeId
|
|
2303
2308
|
if (direction === 'next' || direction === 'prev') {
|
|
2304
2309
|
const shapeIds = readingOrderShapes.map((shape) => shape.id)
|
|
2310
|
+
// Every candidate can be filtered out (e.g. a locked shape is selected and nothing
|
|
2311
|
+
// else is unlocked); indexing an empty list would hand getShape undefined
|
|
2312
|
+
if (shapeIds.length === 0) return
|
|
2305
2313
|
|
|
2306
2314
|
const currentIndex = currentShapeId ? shapeIds.indexOf(currentShapeId) : -1
|
|
2307
2315
|
// With no current index, stepping from -1 makes 'prev' land one shape
|
|
@@ -2450,8 +2458,9 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
2450
2458
|
|
|
2451
2459
|
// Ok, now score that subset of shapes.
|
|
2452
2460
|
const lowestScoringShape = minBy(shapesInDirection, ({ center }) => {
|
|
2453
|
-
//
|
|
2454
|
-
|
|
2461
|
+
// Linear, not squared: the off-axis and diagonal penalties below are page units too, and
|
|
2462
|
+
// squared distance swamps them, so a nearer diagonal shape beats an aligned one.
|
|
2463
|
+
const distance = Vec.Dist(currentCenter, center)
|
|
2455
2464
|
|
|
2456
2465
|
// Distance along the primary axis.
|
|
2457
2466
|
const dirProp = ['left', 'right'].includes(direction) ? 'x' : 'y'
|
|
@@ -2461,9 +2470,11 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
2461
2470
|
const offProp = ['left', 'right'].includes(direction) ? 'y' : 'x'
|
|
2462
2471
|
const offAxisDeviation = Math.abs(center[offProp] - currentCenter[offProp])
|
|
2463
2472
|
|
|
2464
|
-
//
|
|
2465
|
-
|
|
2466
|
-
const
|
|
2473
|
+
// atan2 gives -180..180, so 'up' is -90; normalize to 0..360 to match 270, and wrap the
|
|
2474
|
+
// deviation so that 350 is 10 away from 'right' (0), not 350.
|
|
2475
|
+
const angle = (Vec.Angle(currentCenter, center) * (180 / Math.PI) + 360) % 360
|
|
2476
|
+
const rawAngleDeviation = Math.abs(angle - directionToAngle[direction])
|
|
2477
|
+
const angleDeviation = Math.min(rawAngleDeviation, 360 - rawAngleDeviation)
|
|
2467
2478
|
|
|
2468
2479
|
// Calculate final score (lower is better).
|
|
2469
2480
|
// Weight factors to prioritize:
|
|
@@ -2968,10 +2979,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
2968
2979
|
* @public
|
|
2969
2980
|
*/
|
|
2970
2981
|
setHintingShapes(shapes: TLShapeId[] | TLShape[]): this {
|
|
2971
|
-
const ids =
|
|
2972
|
-
typeof shapes[0] === 'string'
|
|
2973
|
-
? (shapes as TLShapeId[])
|
|
2974
|
-
: (shapes as TLShape[]).map((shape) => shape.id)
|
|
2982
|
+
const ids = toShapeIds(shapes)
|
|
2975
2983
|
// always ephemeral
|
|
2976
2984
|
this.run(
|
|
2977
2985
|
() => {
|
|
@@ -3018,10 +3026,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
3018
3026
|
*/
|
|
3019
3027
|
setErasingShapes(shapes: TLShapeId[] | TLShape[]): this {
|
|
3020
3028
|
// copy before sorting: the caller may pass a store-owned (frozen) array
|
|
3021
|
-
const ids =
|
|
3022
|
-
typeof shapes[0] === 'string'
|
|
3023
|
-
? (shapes as TLShapeId[]).slice()
|
|
3024
|
-
: (shapes as TLShape[]).map((shape) => shape.id)
|
|
3029
|
+
const ids = toShapeIds(shapes).slice()
|
|
3025
3030
|
ids.sort() // sort the incoming ids
|
|
3026
3031
|
const erasingShapeIds = this.getErasingShapeIds()
|
|
3027
3032
|
this.run(
|
|
@@ -3595,6 +3600,20 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
3595
3600
|
const { isLocked } = this._cameraOptions.__unsafe__getWithoutCapture()
|
|
3596
3601
|
if (isLocked && !opts?.force) return this
|
|
3597
3602
|
|
|
3603
|
+
// Resolve the zoom before building the Vec: Vec.Cast would default a missing z to 1,
|
|
3604
|
+
// and a missing z should keep the current zoom level instead
|
|
3605
|
+
const _point = new Vec(point.x, point.y, point.z ?? this.getZoomLevel())
|
|
3606
|
+
|
|
3607
|
+
// Reject non-finite values before anything else, so the call is a no-op rather than a
|
|
3608
|
+
// partial one. An animated move writes the camera from a 'tick' listener, and a listener
|
|
3609
|
+
// that throws stops TickManager scheduling the next frame, which kills every frame-driven
|
|
3610
|
+
// behavior for the rest of the session instead of surfacing the error to the caller.
|
|
3611
|
+
if (!Number.isFinite(_point.x) || !Number.isFinite(_point.y) || !Number.isFinite(_point.z)) {
|
|
3612
|
+
throw Error(
|
|
3613
|
+
`Editor.setCamera: expected finite values, got (${_point.x}, ${_point.y}, ${_point.z}).`
|
|
3614
|
+
)
|
|
3615
|
+
}
|
|
3616
|
+
|
|
3598
3617
|
// Stop any camera animations
|
|
3599
3618
|
this.stopCameraAnimation()
|
|
3600
3619
|
|
|
@@ -3603,12 +3622,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
3603
3622
|
this.stopFollowingUser()
|
|
3604
3623
|
}
|
|
3605
3624
|
|
|
3606
|
-
const _point = Vec.Cast(point)
|
|
3607
|
-
|
|
3608
|
-
if (!Number.isFinite(_point.x)) _point.x = 0
|
|
3609
|
-
if (!Number.isFinite(_point.y)) _point.y = 0
|
|
3610
|
-
if (_point.z === undefined || !Number.isFinite(_point.z)) point.z = this.getZoomLevel()
|
|
3611
|
-
|
|
3612
3625
|
const camera = this.getConstrainedCamera(_point, opts)
|
|
3613
3626
|
|
|
3614
3627
|
if (opts?.animation) {
|
|
@@ -3943,6 +3956,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
3943
3956
|
easing(t: number): number
|
|
3944
3957
|
start: Box
|
|
3945
3958
|
end: Box
|
|
3959
|
+
opts: TLCameraMoveOptions
|
|
3946
3960
|
}
|
|
3947
3961
|
|
|
3948
3962
|
/** @internal */
|
|
@@ -3951,12 +3965,17 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
3951
3965
|
|
|
3952
3966
|
this._viewportAnimation.elapsed += ms
|
|
3953
3967
|
|
|
3954
|
-
const { elapsed, easing, duration, start, end } = this._viewportAnimation
|
|
3968
|
+
const { elapsed, easing, duration, start, end, opts } = this._viewportAnimation
|
|
3955
3969
|
|
|
3956
3970
|
if (elapsed > duration) {
|
|
3957
3971
|
this.off('tick', this._animateViewport)
|
|
3958
3972
|
this._viewportAnimation = null
|
|
3959
|
-
|
|
3973
|
+
// Forward the caller's options, otherwise a forced move to a position outside the
|
|
3974
|
+
// constraints animates there and then snaps back on this last frame
|
|
3975
|
+
this._setCamera(
|
|
3976
|
+
new Vec(-end.x, -end.y, this.getViewportScreenBounds().width / end.width),
|
|
3977
|
+
opts
|
|
3978
|
+
)
|
|
3960
3979
|
return
|
|
3961
3980
|
}
|
|
3962
3981
|
|
|
@@ -4010,6 +4029,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
4010
4029
|
easing,
|
|
4011
4030
|
start: viewportPageBounds.clone(),
|
|
4012
4031
|
end: targetViewportPage.clone(),
|
|
4032
|
+
opts: rest,
|
|
4013
4033
|
}
|
|
4014
4034
|
|
|
4015
4035
|
// If we ever get a "stop-camera-animation" event, we stop
|
|
@@ -4134,6 +4154,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
4134
4154
|
// If we're not on the same page, move to the page they're on
|
|
4135
4155
|
const isOnSamePage = presence.currentPageId === this.getCurrentPageId()
|
|
4136
4156
|
if (!isOnSamePage) {
|
|
4157
|
+
this.markHistoryStoppingPoint('change-page')
|
|
4137
4158
|
this.setCurrentPage(presence.currentPageId)
|
|
4138
4159
|
}
|
|
4139
4160
|
|
|
@@ -5231,7 +5252,10 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
5231
5252
|
|
|
5232
5253
|
this.run(
|
|
5233
5254
|
() => {
|
|
5234
|
-
|
|
5255
|
+
// the asset store's remove is async; surface failures instead of leaving an unhandled rejection
|
|
5256
|
+
Promise.resolve(this.store.props.assets.remove?.(ids)).catch((err) =>
|
|
5257
|
+
console.error('Error while removing assets from the asset store:', err)
|
|
5258
|
+
)
|
|
5235
5259
|
this.store.remove(ids)
|
|
5236
5260
|
},
|
|
5237
5261
|
{ history: 'ignore' }
|
|
@@ -5693,10 +5717,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
5693
5717
|
return
|
|
5694
5718
|
}
|
|
5695
5719
|
|
|
5696
|
-
const ids =
|
|
5697
|
-
typeof shapes[0] === 'string'
|
|
5698
|
-
? (shapes as TLShapeId[])
|
|
5699
|
-
: (shapes as TLShape[]).map((s) => s.id)
|
|
5720
|
+
const ids = toShapeIds(shapes)
|
|
5700
5721
|
const freshShapes = compact(ids.map((id) => this.getShape(id)))
|
|
5701
5722
|
|
|
5702
5723
|
if (freshShapes.length === 1) {
|
|
@@ -5844,14 +5865,16 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
5844
5865
|
*/
|
|
5845
5866
|
getSelectedShapeAtPoint(point: VecLike): TLShape | undefined {
|
|
5846
5867
|
const selectedShapeIds = this.getSelectedShapeIds()
|
|
5847
|
-
|
|
5868
|
+
if (selectedShapeIds.length === 0) return undefined
|
|
5869
|
+
const selectedShapeIdSet = new Set(selectedShapeIds)
|
|
5870
|
+
const margin = this.getHitTestMargin()
|
|
5848
5871
|
const sortedShapes = this.getCurrentPageShapesSorted()
|
|
5849
5872
|
|
|
5850
5873
|
// iterate from the top (highest z-index) to find the top-most matching shape
|
|
5851
5874
|
for (let i = sortedShapes.length - 1; i >= 0; i--) {
|
|
5852
5875
|
const shape = sortedShapes[i]
|
|
5853
5876
|
if (shape.type === 'group') continue
|
|
5854
|
-
if (!
|
|
5877
|
+
if (!selectedShapeIdSet.has(shape.id)) continue
|
|
5855
5878
|
if (
|
|
5856
5879
|
this.getShapeGeometry(shape).hitTestPoint(
|
|
5857
5880
|
this.getPointInShapeSpace(shape, point),
|
|
@@ -6022,9 +6045,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6022
6045
|
// other hits would be occluded by the shape.
|
|
6023
6046
|
return inMarginClosestToEdgeHit || shape
|
|
6024
6047
|
} else {
|
|
6025
|
-
// If the shape is bigger than the viewport, then skip it.
|
|
6026
|
-
if (this.getShapePageBounds(shape)!.contains(viewportPageBounds)) continue
|
|
6027
|
-
|
|
6028
6048
|
// If we're close to the edge of the shape, and if it's the closest edge among
|
|
6029
6049
|
// all the edges that we've gotten close to so far, then we will want to hit the
|
|
6030
6050
|
// shape unless we hit something else or closer in later iterations.
|
|
@@ -6045,6 +6065,10 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6045
6065
|
inMarginClosestToEdgeHit = shape
|
|
6046
6066
|
}
|
|
6047
6067
|
} else if (!inMarginClosestToEdgeHit) {
|
|
6068
|
+
// If the shape is bigger than the viewport, then skip it. (Only here: its
|
|
6069
|
+
// edges should still be hittable within the margin.)
|
|
6070
|
+
if (this.getShapePageBounds(shape)!.contains(viewportPageBounds)) continue
|
|
6071
|
+
|
|
6048
6072
|
// If we're not within margin distance to any edge, and if the
|
|
6049
6073
|
// shape is hollow, then we want to hit the shape with the
|
|
6050
6074
|
// smallest area. (There's a bug here with self-intersecting
|
|
@@ -6063,6 +6087,11 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6063
6087
|
// If the distance is less than the margin, return the shape as the hit.
|
|
6064
6088
|
// Use the editor's configurable hit test margin.
|
|
6065
6089
|
if (distance < this.getHitTestMargin()) {
|
|
6090
|
+
// An edge we already hit (above this shape) that is at least as close still wins,
|
|
6091
|
+
// matching the closest-edge rule used for hollow shapes
|
|
6092
|
+
if (inMarginClosestToEdgeHit && inMarginClosestToEdgeDistance <= distance) {
|
|
6093
|
+
return inMarginClosestToEdgeHit
|
|
6094
|
+
}
|
|
6066
6095
|
return shape
|
|
6067
6096
|
}
|
|
6068
6097
|
}
|
|
@@ -6427,8 +6456,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6427
6456
|
* @public
|
|
6428
6457
|
*/
|
|
6429
6458
|
reparentShapes(shapes: TLShapeId[] | TLShape[], parentId: TLParentId, insertIndex?: IndexKey) {
|
|
6430
|
-
const ids =
|
|
6431
|
-
typeof shapes[0] === 'string' ? (shapes as TLShapeId[]) : shapes.map((s) => (s as TLShape).id)
|
|
6459
|
+
const ids = toShapeIds(shapes)
|
|
6432
6460
|
if (ids.length === 0) return this
|
|
6433
6461
|
|
|
6434
6462
|
const changes: TLShapePartial[] = []
|
|
@@ -6777,6 +6805,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6777
6805
|
* binding, but the `type`, `toId`, and `fromId` must all be provided.
|
|
6778
6806
|
*/
|
|
6779
6807
|
createBindings<B extends TLBinding = TLBinding>(partials: TLBindingCreate<B>[]) {
|
|
6808
|
+
if (this.getIsReadonly()) return this
|
|
6809
|
+
|
|
6780
6810
|
const bindings: TLBinding[] = []
|
|
6781
6811
|
for (const partial of partials) {
|
|
6782
6812
|
const fromShape = this.getShape(partial.fromId)
|
|
@@ -6816,6 +6846,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6816
6846
|
* binding is skipped. The changes from the partial are merged into the existing record.
|
|
6817
6847
|
*/
|
|
6818
6848
|
updateBindings(partials: (TLBindingUpdate | null | undefined)[]) {
|
|
6849
|
+
if (this.getIsReadonly()) return this
|
|
6850
|
+
|
|
6819
6851
|
const updated: TLBinding[] = []
|
|
6820
6852
|
|
|
6821
6853
|
for (const partial of partials) {
|
|
@@ -6853,6 +6885,17 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6853
6885
|
* Delete several bindings by their IDs. If a binding ID doesn't exist, it's ignored.
|
|
6854
6886
|
*/
|
|
6855
6887
|
deleteBindings(bindings: (TLBinding | TLBindingId)[], { isolateShapes = false } = {}) {
|
|
6888
|
+
if (this.getIsReadonly()) return this
|
|
6889
|
+
return this._deleteBindings(bindings, { isolateShapes })
|
|
6890
|
+
}
|
|
6891
|
+
|
|
6892
|
+
/**
|
|
6893
|
+
* Unguarded so that withIsolatedShapes can transiently isolate shapes for copy and export in
|
|
6894
|
+
* readonly mode; the public deleteBindings is what readonly blocks.
|
|
6895
|
+
*
|
|
6896
|
+
* @internal
|
|
6897
|
+
*/
|
|
6898
|
+
_deleteBindings(bindings: (TLBinding | TLBindingId)[], { isolateShapes = false } = {}) {
|
|
6856
6899
|
const ids = bindings.map((binding) => (typeof binding === 'string' ? binding : binding.id))
|
|
6857
6900
|
if (isolateShapes) {
|
|
6858
6901
|
this.store.atomic(() => {
|
|
@@ -6927,10 +6970,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6927
6970
|
delta: number,
|
|
6928
6971
|
opts?: { center?: VecLike }
|
|
6929
6972
|
): this {
|
|
6930
|
-
const ids =
|
|
6931
|
-
typeof shapes[0] === 'string'
|
|
6932
|
-
? (shapes as TLShapeId[])
|
|
6933
|
-
: (shapes as TLShape[]).map((s) => s.id)
|
|
6973
|
+
const ids = toShapeIds(shapes)
|
|
6934
6974
|
|
|
6935
6975
|
if (ids.length <= 0) return this
|
|
6936
6976
|
|
|
@@ -6977,6 +7017,15 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6977
7017
|
return workingShape
|
|
6978
7018
|
}
|
|
6979
7019
|
|
|
7020
|
+
// Counter-rotates the page-space delta into the parent's space; without that a child of a rotated
|
|
7021
|
+
// frame or group would move along its parent's axes instead of the page's.
|
|
7022
|
+
// todo: a shape laid out together with its own parent moves twice, once with the parent and once
|
|
7023
|
+
// on its own; the layout commands should skip shapes whose ancestors are also being laid out
|
|
7024
|
+
private getChangesToTranslateShapeByPageDelta(shape: TLShape, pageDelta: VecLike): TLShape {
|
|
7025
|
+
const localDelta = Vec.From(pageDelta).rot(-this.getShapeParentTransform(shape).rotation())
|
|
7026
|
+
return this.getChangesToTranslateShape(shape, localDelta.add(shape))
|
|
7027
|
+
}
|
|
7028
|
+
|
|
6980
7029
|
/**
|
|
6981
7030
|
* Move shapes by a delta.
|
|
6982
7031
|
*
|
|
@@ -6989,20 +7038,14 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6989
7038
|
* @param offset - The offset to apply to the shapes.
|
|
6990
7039
|
*/
|
|
6991
7040
|
nudgeShapes(shapes: TLShapeId[] | TLShape[], offset: VecLike): this {
|
|
6992
|
-
const ids =
|
|
6993
|
-
typeof shapes[0] === 'string'
|
|
6994
|
-
? (shapes as TLShapeId[])
|
|
6995
|
-
: (shapes as TLShape[]).map((s) => s.id)
|
|
7041
|
+
const ids = toShapeIds(shapes)
|
|
6996
7042
|
|
|
6997
7043
|
if (ids.length <= 0) return this
|
|
6998
7044
|
const changes: TLShapePartial[] = []
|
|
6999
7045
|
|
|
7000
7046
|
for (const id of ids) {
|
|
7001
7047
|
const shape = this.getShape(id)!
|
|
7002
|
-
|
|
7003
|
-
localDelta.rot(-this.getShapeParentTransform(shape).rotation())
|
|
7004
|
-
|
|
7005
|
-
changes.push(this.getChangesToTranslateShape(shape, localDelta.add(shape)))
|
|
7048
|
+
changes.push(this.getChangesToTranslateShapeByPageDelta(shape, offset))
|
|
7006
7049
|
}
|
|
7007
7050
|
|
|
7008
7051
|
this.updateShapes(changes)
|
|
@@ -7026,10 +7069,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
7026
7069
|
*/
|
|
7027
7070
|
duplicateShapes(shapes: TLShapeId[] | TLShape[], offset?: VecLike): this {
|
|
7028
7071
|
this.run(() => {
|
|
7029
|
-
const _ids =
|
|
7030
|
-
typeof shapes[0] === 'string'
|
|
7031
|
-
? (shapes as TLShapeId[])
|
|
7032
|
-
: (shapes as TLShape[]).map((s) => s.id)
|
|
7072
|
+
const _ids = toShapeIds(shapes)
|
|
7033
7073
|
|
|
7034
7074
|
const ids = this._shouldIgnoreShapeLock ? _ids : this._getUnlockedShapeIds(_ids)
|
|
7035
7075
|
if (ids.length <= 0) return this
|
|
@@ -7171,10 +7211,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
7171
7211
|
* @public
|
|
7172
7212
|
*/
|
|
7173
7213
|
moveShapesToPage(shapes: TLShapeId[] | TLShape[], pageId: TLPageId): this {
|
|
7174
|
-
const ids =
|
|
7175
|
-
typeof shapes[0] === 'string'
|
|
7176
|
-
? (shapes as TLShapeId[])
|
|
7177
|
-
: (shapes as TLShape[]).map((s) => s.id)
|
|
7214
|
+
const ids = toShapeIds(shapes)
|
|
7178
7215
|
|
|
7179
7216
|
if (ids.length === 0) return this
|
|
7180
7217
|
if (this.getIsReadonly()) return this
|
|
@@ -7235,10 +7272,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
7235
7272
|
* @public
|
|
7236
7273
|
*/
|
|
7237
7274
|
toggleLock(shapes: TLShapeId[] | TLShape[]): this {
|
|
7238
|
-
const ids =
|
|
7239
|
-
typeof shapes[0] === 'string'
|
|
7240
|
-
? (shapes as TLShapeId[])
|
|
7241
|
-
: (shapes as TLShape[]).map((s) => s.id)
|
|
7275
|
+
const ids = toShapeIds(shapes)
|
|
7242
7276
|
|
|
7243
7277
|
if (this.getIsReadonly() || ids.length === 0) return this
|
|
7244
7278
|
|
|
@@ -7281,11 +7315,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
7281
7315
|
* @public
|
|
7282
7316
|
*/
|
|
7283
7317
|
sendToBack(shapes: TLShapeId[] | TLShape[]): this {
|
|
7284
|
-
const ids =
|
|
7285
|
-
|
|
7286
|
-
? (shapes as TLShapeId[])
|
|
7287
|
-
: (shapes as TLShape[]).map((s) => s.id)
|
|
7288
|
-
const changes = getReorderingShapesChanges(this, 'toBack', ids as TLShapeId[], {
|
|
7318
|
+
const ids = toShapeIds(shapes)
|
|
7319
|
+
const changes = getReorderingShapesChanges(this, 'toBack', ids, {
|
|
7289
7320
|
considerAllShapes: true,
|
|
7290
7321
|
})
|
|
7291
7322
|
if (changes) this.updateShapes(changes)
|
|
@@ -7315,11 +7346,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
7315
7346
|
* @public
|
|
7316
7347
|
*/
|
|
7317
7348
|
sendBackward(shapes: TLShapeId[] | TLShape[], opts: { considerAllShapes?: boolean } = {}): this {
|
|
7318
|
-
const ids =
|
|
7319
|
-
|
|
7320
|
-
? (shapes as TLShapeId[])
|
|
7321
|
-
: (shapes as TLShape[]).map((s) => s.id)
|
|
7322
|
-
const changes = getReorderingShapesChanges(this, 'backward', ids as TLShapeId[], opts)
|
|
7349
|
+
const ids = toShapeIds(shapes)
|
|
7350
|
+
const changes = getReorderingShapesChanges(this, 'backward', ids, opts)
|
|
7323
7351
|
if (changes) this.updateShapes(changes)
|
|
7324
7352
|
return this
|
|
7325
7353
|
}
|
|
@@ -7347,11 +7375,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
7347
7375
|
* @public
|
|
7348
7376
|
*/
|
|
7349
7377
|
bringForward(shapes: TLShapeId[] | TLShape[], opts: { considerAllShapes?: boolean } = {}): this {
|
|
7350
|
-
const ids =
|
|
7351
|
-
|
|
7352
|
-
? (shapes as TLShapeId[])
|
|
7353
|
-
: (shapes as TLShape[]).map((s) => s.id)
|
|
7354
|
-
const changes = getReorderingShapesChanges(this, 'forward', ids as TLShapeId[], opts)
|
|
7378
|
+
const ids = toShapeIds(shapes)
|
|
7379
|
+
const changes = getReorderingShapesChanges(this, 'forward', ids, opts)
|
|
7355
7380
|
if (changes) this.updateShapes(changes)
|
|
7356
7381
|
return this
|
|
7357
7382
|
}
|
|
@@ -7370,11 +7395,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
7370
7395
|
* @public
|
|
7371
7396
|
*/
|
|
7372
7397
|
bringToFront(shapes: TLShapeId[] | TLShape[]): this {
|
|
7373
|
-
const ids =
|
|
7374
|
-
|
|
7375
|
-
? (shapes as TLShapeId[])
|
|
7376
|
-
: (shapes as TLShape[]).map((s) => s.id)
|
|
7377
|
-
const changes = getReorderingShapesChanges(this, 'toFront', ids as TLShapeId[])
|
|
7398
|
+
const ids = toShapeIds(shapes)
|
|
7399
|
+
const changes = getReorderingShapesChanges(this, 'toFront', ids)
|
|
7378
7400
|
if (changes) this.updateShapes(changes)
|
|
7379
7401
|
return this
|
|
7380
7402
|
}
|
|
@@ -7390,10 +7412,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
7390
7412
|
type: TLShapeUtilCanBeLaidOutOpts['type'],
|
|
7391
7413
|
opts?: { filterAxisAligned?: boolean }
|
|
7392
7414
|
): { clusters: { shapes: TLShape[]; pageBounds: Box }[]; allBounds: Box[] } {
|
|
7393
|
-
const ids =
|
|
7394
|
-
typeof shapes[0] === 'string'
|
|
7395
|
-
? (shapes as TLShapeId[])
|
|
7396
|
-
: (shapes as TLShape[]).map((s) => s.id)
|
|
7415
|
+
const ids = toShapeIds(shapes)
|
|
7397
7416
|
|
|
7398
7417
|
// always fresh shapes
|
|
7399
7418
|
let freshShapes = compact(ids.map((id) => this.getShape(id)))
|
|
@@ -7431,8 +7450,10 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
7431
7450
|
const shapesMovingTogether = [shape]
|
|
7432
7451
|
const boundsOfShapesMovingTogether: Box[] = [shapePageBounds]
|
|
7433
7452
|
|
|
7453
|
+
// Seed with bindings in both directions, otherwise an arrow visited before the shapes it
|
|
7454
|
+
// binds ends up in a cluster of its own and the result depends on input order
|
|
7434
7455
|
this.collectShapesViaArrowBindings({
|
|
7435
|
-
bindings: this.
|
|
7456
|
+
bindings: this.getBindingsInvolvingShape(shape.id, 'arrow'),
|
|
7436
7457
|
initialShapes: freshShapes,
|
|
7437
7458
|
resultShapes: shapesMovingTogether,
|
|
7438
7459
|
resultBounds: boundsOfShapesMovingTogether,
|
|
@@ -7501,10 +7522,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
7501
7522
|
flipShapes(shapes: TLShapeId[] | TLShape[], operation: 'horizontal' | 'vertical'): this {
|
|
7502
7523
|
if (this.getIsReadonly()) return this
|
|
7503
7524
|
|
|
7504
|
-
const ids =
|
|
7505
|
-
typeof shapes[0] === 'string'
|
|
7506
|
-
? (shapes as TLShapeId[])
|
|
7507
|
-
: (shapes as TLShape[]).map((s) => s.id)
|
|
7525
|
+
const ids = toShapeIds(shapes)
|
|
7508
7526
|
|
|
7509
7527
|
// Collect a greedy list of shapes to flip
|
|
7510
7528
|
const shapesToFlipFirstPass = compact(ids.map((id) => this.getShape(id)))
|
|
@@ -7607,22 +7625,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
7607
7625
|
const len = shapeClustersToStack.length
|
|
7608
7626
|
if ((_gap === 0 && len < 3) || len < 2) return this
|
|
7609
7627
|
|
|
7610
|
-
|
|
7611
|
-
let min: 'minX' | 'minY'
|
|
7612
|
-
let max: 'maxX' | 'maxY'
|
|
7613
|
-
let dim: 'width' | 'height'
|
|
7614
|
-
|
|
7615
|
-
if (operation === 'horizontal') {
|
|
7616
|
-
val = 'x'
|
|
7617
|
-
min = 'minX'
|
|
7618
|
-
max = 'maxX'
|
|
7619
|
-
dim = 'width'
|
|
7620
|
-
} else {
|
|
7621
|
-
val = 'y'
|
|
7622
|
-
min = 'minY'
|
|
7623
|
-
max = 'maxY'
|
|
7624
|
-
dim = 'height'
|
|
7625
|
-
}
|
|
7628
|
+
const { val, min, max, dim } = AXIS[operation]
|
|
7626
7629
|
|
|
7627
7630
|
let shapeGap: number = 0
|
|
7628
7631
|
|
|
@@ -7680,17 +7683,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
7680
7683
|
delta[val] = v + shapeGap - pageBounds[val]
|
|
7681
7684
|
|
|
7682
7685
|
for (const shape of shapes) {
|
|
7683
|
-
|
|
7684
|
-
|
|
7685
|
-
// If the shape has another shape as its parent, and if the parent has a rotation, we need to rotate the counter-rotate delta
|
|
7686
|
-
// todo: ensure that the parent isn't being aligned together with its children
|
|
7687
|
-
const parent = this.getShapeParent(shape)
|
|
7688
|
-
if (parent) {
|
|
7689
|
-
shapeDelta.rot(-this.getShapePageTransform(parent).rotation())
|
|
7690
|
-
}
|
|
7691
|
-
|
|
7692
|
-
shapeDelta.add(shape) // add the shape's x and y to the delta
|
|
7693
|
-
changes.push(this.getChangesToTranslateShape(shape, shapeDelta))
|
|
7686
|
+
changes.push(this.getChangesToTranslateShapeByPageDelta(shape, delta))
|
|
7694
7687
|
}
|
|
7695
7688
|
|
|
7696
7689
|
v += pageBounds[dim] + shapeGap
|
|
@@ -7806,15 +7799,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
7806
7799
|
const delta = Vec.Sub(nextPageBounds.point, pageBounds.point).add(centerDelta)
|
|
7807
7800
|
|
|
7808
7801
|
for (const shape of shapes) {
|
|
7809
|
-
|
|
7810
|
-
|
|
7811
|
-
const parent = this.getShapeParent(shape)
|
|
7812
|
-
if (parent) {
|
|
7813
|
-
shapeDelta.rot(-this.getShapeParentTransform(shape).rotation())
|
|
7814
|
-
}
|
|
7815
|
-
|
|
7816
|
-
shapeDelta.add(shape)
|
|
7817
|
-
changes.push(this.getChangesToTranslateShape(shape, shapeDelta))
|
|
7802
|
+
changes.push(this.getChangesToTranslateShapeByPageDelta(shape, delta))
|
|
7818
7803
|
}
|
|
7819
7804
|
}
|
|
7820
7805
|
|
|
@@ -7894,17 +7879,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
7894
7879
|
}
|
|
7895
7880
|
|
|
7896
7881
|
for (const shape of shapes) {
|
|
7897
|
-
|
|
7898
|
-
|
|
7899
|
-
// If the shape has another shape as its parent, and if the parent has a rotation, we need to rotate the counter-rotate delta
|
|
7900
|
-
// todo: ensure that the parent isn't being aligned together with its children
|
|
7901
|
-
const parent = this.getShapeParent(shape)
|
|
7902
|
-
if (parent) {
|
|
7903
|
-
shapeDelta.rot(-this.getShapePageTransform(parent).rotation())
|
|
7904
|
-
}
|
|
7905
|
-
|
|
7906
|
-
shapeDelta.add(shape) // add the shape's x and y to the delta
|
|
7907
|
-
changes.push(this.getChangesToTranslateShape(shape, shapeDelta))
|
|
7882
|
+
changes.push(this.getChangesToTranslateShapeByPageDelta(shape, delta))
|
|
7908
7883
|
}
|
|
7909
7884
|
})
|
|
7910
7885
|
|
|
@@ -7933,22 +7908,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
7933
7908
|
|
|
7934
7909
|
if (shapeClustersToDistribute.length < 3) return this
|
|
7935
7910
|
|
|
7936
|
-
|
|
7937
|
-
let min: 'minX' | 'minY'
|
|
7938
|
-
let max: 'maxX' | 'maxY'
|
|
7939
|
-
let dim: 'width' | 'height'
|
|
7940
|
-
|
|
7941
|
-
if (operation === 'horizontal') {
|
|
7942
|
-
val = 'x'
|
|
7943
|
-
min = 'minX'
|
|
7944
|
-
max = 'maxX'
|
|
7945
|
-
dim = 'width'
|
|
7946
|
-
} else {
|
|
7947
|
-
val = 'y'
|
|
7948
|
-
min = 'minY'
|
|
7949
|
-
max = 'maxY'
|
|
7950
|
-
dim = 'height'
|
|
7951
|
-
}
|
|
7911
|
+
const { val, min, max, dim } = AXIS[operation]
|
|
7952
7912
|
const changes: TLShapePartial[] = []
|
|
7953
7913
|
|
|
7954
7914
|
const first = shapeClustersToDistribute.sort((a, b) => a.pageBounds[min] - b.pageBounds[min])[0]
|
|
@@ -7957,10 +7917,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
7957
7917
|
// If the first shape group is also the last shape group, distribute without it
|
|
7958
7918
|
if (first === last) {
|
|
7959
7919
|
const excludedShapeIds = new Set(first.shapes.map((s) => s.id))
|
|
7960
|
-
const ids =
|
|
7961
|
-
typeof shapes[0] === 'string'
|
|
7962
|
-
? (shapes as TLShapeId[])
|
|
7963
|
-
: (shapes as TLShape[]).map((s) => s.id)
|
|
7920
|
+
const ids = toShapeIds(shapes)
|
|
7964
7921
|
return this.distributeShapes(
|
|
7965
7922
|
ids.filter((id) => !excludedShapeIds.has(id)),
|
|
7966
7923
|
operation
|
|
@@ -7994,17 +7951,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
7994
7951
|
}
|
|
7995
7952
|
|
|
7996
7953
|
for (const shape of shapes) {
|
|
7997
|
-
|
|
7998
|
-
|
|
7999
|
-
// If the shape has another shape as its parent, and if the parent has a rotation, we need to rotate the counter-rotate delta
|
|
8000
|
-
// todo: ensure that the parent isn't being aligned together with its children
|
|
8001
|
-
const parent = this.getShapeParent(shape)
|
|
8002
|
-
if (parent) {
|
|
8003
|
-
shapeDelta.rot(-this.getShapePageTransform(parent).rotation())
|
|
8004
|
-
}
|
|
8005
|
-
|
|
8006
|
-
shapeDelta.add(shape) // add the shape's x and y to the delta
|
|
8007
|
-
changes.push(this.getChangesToTranslateShape(shape, shapeDelta))
|
|
7954
|
+
changes.push(this.getChangesToTranslateShapeByPageDelta(shape, delta))
|
|
8008
7955
|
}
|
|
8009
7956
|
|
|
8010
7957
|
v += pageBounds[dim] + gap
|
|
@@ -8040,24 +7987,12 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
8040
7987
|
if (shapeClustersToStretch.length < 2) return this
|
|
8041
7988
|
|
|
8042
7989
|
const commonBounds = Box.Common(allBounds)
|
|
8043
|
-
|
|
8044
|
-
let min: 'minX' | 'minY'
|
|
8045
|
-
let dim: 'width' | 'height'
|
|
8046
|
-
|
|
8047
|
-
if (operation === 'horizontal') {
|
|
8048
|
-
val = 'x'
|
|
8049
|
-
min = 'minX'
|
|
8050
|
-
dim = 'width'
|
|
8051
|
-
} else {
|
|
8052
|
-
val = 'y'
|
|
8053
|
-
min = 'minY'
|
|
8054
|
-
dim = 'height'
|
|
8055
|
-
}
|
|
7990
|
+
const { val, min, dim } = AXIS[operation]
|
|
8056
7991
|
|
|
8057
7992
|
this.run(() => {
|
|
8058
7993
|
shapeClustersToStretch.forEach(({ shapes, pageBounds }) => {
|
|
8059
|
-
const
|
|
8060
|
-
|
|
7994
|
+
const pageOffset = new Vec()
|
|
7995
|
+
pageOffset[val] = commonBounds[min] - pageBounds[min]
|
|
8061
7996
|
|
|
8062
7997
|
const scaleOrigin = pageBounds.center.clone()
|
|
8063
7998
|
scaleOrigin[val] = commonBounds[min]
|
|
@@ -8067,11 +8002,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
8067
8002
|
|
|
8068
8003
|
for (const shape of shapes) {
|
|
8069
8004
|
// First translate
|
|
8070
|
-
|
|
8071
|
-
shapeLocalOffset.rot(-this.getShapeParentTransform(shape).rotation())
|
|
8072
|
-
shapeLocalOffset.add(shape)
|
|
8073
|
-
const changes = this.getChangesToTranslateShape(shape, shapeLocalOffset)
|
|
8074
|
-
this.updateShape(changes)
|
|
8005
|
+
this.updateShape(this.getChangesToTranslateShapeByPageDelta(shape, pageOffset))
|
|
8075
8006
|
|
|
8076
8007
|
// Then resize
|
|
8077
8008
|
this.resizeShape(shape.id, scale, {
|
|
@@ -8124,7 +8055,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
8124
8055
|
const scale = new Vec(scaleX, scaleY)
|
|
8125
8056
|
|
|
8126
8057
|
shapeClusters.forEach(({ shapes, pageBounds }) => {
|
|
8127
|
-
const
|
|
8058
|
+
const pageOffset = new Vec(
|
|
8128
8059
|
targetBounds.minX -
|
|
8129
8060
|
commonBounds.minX +
|
|
8130
8061
|
(pageBounds.minX - commonBounds.minX) * (scaleX - 1),
|
|
@@ -8138,11 +8069,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
8138
8069
|
|
|
8139
8070
|
for (const shape of shapes) {
|
|
8140
8071
|
// First translate
|
|
8141
|
-
|
|
8142
|
-
shapeLocalOffset.rot(-this.getShapeParentTransform(shape).rotation())
|
|
8143
|
-
shapeLocalOffset.add(shape)
|
|
8144
|
-
const changes = this.getChangesToTranslateShape(shape, shapeLocalOffset)
|
|
8145
|
-
this.updateShape(changes)
|
|
8072
|
+
this.updateShape(this.getChangesToTranslateShapeByPageDelta(shape, pageOffset))
|
|
8146
8073
|
|
|
8147
8074
|
// Then resize
|
|
8148
8075
|
this.resizeShape(shape.id, scale, {
|
|
@@ -8787,6 +8714,11 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
8787
8714
|
|
|
8788
8715
|
const animations: ShapeAnimation[] = []
|
|
8789
8716
|
|
|
8717
|
+
// Snapshot the lock override now: when this animation is started inside
|
|
8718
|
+
// editor.run(..., { ignoreShapeLock: true }), run() restores the flag before any tick
|
|
8719
|
+
// fires, so the final updateShapes below would refuse the locked shape and strand it
|
|
8720
|
+
const ignoreShapeLock = this._shouldIgnoreShapeLock
|
|
8721
|
+
|
|
8790
8722
|
let partial: TLShapePartial | null | undefined, result: ShapeAnimation
|
|
8791
8723
|
for (let i = 0, n = partials.length; i < n; i++) {
|
|
8792
8724
|
partial = partials[i]
|
|
@@ -8795,6 +8727,14 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
8795
8727
|
const shape = this.getShape(partial.id)!
|
|
8796
8728
|
if (!shape) continue
|
|
8797
8729
|
|
|
8730
|
+
// Apply the same lock rule as updateShapes up front: the intermediate frames go through
|
|
8731
|
+
// _updateShapes, which doesn't check locks, so a locked shape would otherwise be moved by
|
|
8732
|
+
// every frame but the last and end up stranded at the penultimate one
|
|
8733
|
+
const unlocks = shape.isLocked && Object.hasOwn(partial, 'isLocked') && !partial.isLocked
|
|
8734
|
+
if (!ignoreShapeLock && !unlocks && this.isShapeOrAncestorLocked(shape)) {
|
|
8735
|
+
continue
|
|
8736
|
+
}
|
|
8737
|
+
|
|
8798
8738
|
result = {
|
|
8799
8739
|
start: structuredClone(shape),
|
|
8800
8740
|
end: applyPartialToRecordWithProps(structuredClone(shape), partial),
|
|
@@ -8815,7 +8755,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
8815
8755
|
if (partialsToUpdate.length) {
|
|
8816
8756
|
// the regular update shapes also removes the shape from
|
|
8817
8757
|
// the animating shapes set
|
|
8818
|
-
this.updateShapes(partialsToUpdate)
|
|
8758
|
+
this.run(() => this.updateShapes(partialsToUpdate), { ignoreShapeLock })
|
|
8819
8759
|
}
|
|
8820
8760
|
|
|
8821
8761
|
this.off('tick', handleTick)
|
|
@@ -8882,18 +8822,16 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
8882
8822
|
}
|
|
8883
8823
|
if (this.getIsReadonly()) return this
|
|
8884
8824
|
|
|
8885
|
-
const ids =
|
|
8886
|
-
typeof shapes[0] === 'string'
|
|
8887
|
-
? (shapes as TLShapeId[])
|
|
8888
|
-
: (shapes.map((s) => (s as TLShape).id) as TLShapeId[])
|
|
8889
|
-
|
|
8890
|
-
if (ids.length <= 1) return this
|
|
8825
|
+
const ids = toShapeIds(shapes)
|
|
8891
8826
|
|
|
8892
8827
|
const shapesToGroup = compact(
|
|
8893
8828
|
(this._shouldIgnoreShapeLock ? ids : this._getUnlockedShapeIds(ids)).map((id) =>
|
|
8894
8829
|
this.getShape(id)
|
|
8895
8830
|
)
|
|
8896
8831
|
)
|
|
8832
|
+
// Re-check after the lock filter: Box.Common of nothing is not a valid box and would throw
|
|
8833
|
+
if (shapesToGroup.length <= 1) return this
|
|
8834
|
+
|
|
8897
8835
|
const sortedShapeIds = shapesToGroup.sort(sortByIndex).map((s) => s.id)
|
|
8898
8836
|
const childBounds = compact(shapesToGroup.map((shape) => this.getShapePageBounds(shape)))
|
|
8899
8837
|
const pageBounds = Box.Common(childBounds)
|
|
@@ -8906,11 +8844,15 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
8906
8844
|
|
|
8907
8845
|
const parentId = this.findCommonAncestor(shapesToGroup) ?? this.getCurrentPageId()
|
|
8908
8846
|
|
|
8909
|
-
//
|
|
8910
|
-
|
|
8847
|
+
// createShapes bails out when the page is full, so check first; otherwise the shapes get
|
|
8848
|
+
// reparented into a group that was never created and vanish from the page
|
|
8849
|
+
if (!this.canCreateShapes([groupId])) {
|
|
8850
|
+
alertMaxShapes(this)
|
|
8851
|
+
return this
|
|
8852
|
+
}
|
|
8911
8853
|
|
|
8912
|
-
// If
|
|
8913
|
-
if (!this.isIn('select.idle')) {
|
|
8854
|
+
// If the select tool is mid-interaction, cancel it (get back to idle) before grouping
|
|
8855
|
+
if (this.isIn('select') && !this.isIn('select.idle')) {
|
|
8914
8856
|
this.cancel()
|
|
8915
8857
|
}
|
|
8916
8858
|
|
|
@@ -8964,10 +8906,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
8964
8906
|
if (this.getIsReadonly()) return this
|
|
8965
8907
|
|
|
8966
8908
|
const { select = true } = opts
|
|
8967
|
-
const ids =
|
|
8968
|
-
typeof shapes[0] === 'string'
|
|
8969
|
-
? (shapes as TLShapeId[])
|
|
8970
|
-
: (shapes as TLShape[]).map((s) => s.id)
|
|
8909
|
+
const ids = toShapeIds(shapes)
|
|
8971
8910
|
|
|
8972
8911
|
const shapesToUngroup = compact(
|
|
8973
8912
|
(this._shouldIgnoreShapeLock ? ids : this._getUnlockedShapeIds(ids)).map((id) =>
|
|
@@ -8977,9 +8916,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
8977
8916
|
|
|
8978
8917
|
if (shapesToUngroup.length === 0) return this
|
|
8979
8918
|
|
|
8980
|
-
//
|
|
8981
|
-
if (this.
|
|
8982
|
-
if (!this.isIn('select.idle')) {
|
|
8919
|
+
// If the select tool is mid-interaction, cancel it (get back to idle) before ungrouping
|
|
8920
|
+
if (this.isIn('select') && !this.isIn('select.idle')) {
|
|
8983
8921
|
this.cancel()
|
|
8984
8922
|
}
|
|
8985
8923
|
|
|
@@ -9002,10 +8940,12 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
9002
8940
|
if (groups.length === 0) return this
|
|
9003
8941
|
|
|
9004
8942
|
this.run(() => {
|
|
9005
|
-
let group: TLGroupShape
|
|
9006
|
-
|
|
9007
8943
|
for (let i = 0, n = groups.length; i < n; i++) {
|
|
9008
|
-
group
|
|
8944
|
+
// Re-read the group: ungrouping an outer group earlier in this loop reparents the
|
|
8945
|
+
// inner ones, and the stale parentId would send their children into a group that's
|
|
8946
|
+
// about to be deleted
|
|
8947
|
+
const group = this.getShape<TLGroupShape>(groups[i].id)
|
|
8948
|
+
if (!group) continue
|
|
9009
8949
|
const childIds = this.getSortedChildIdsForParent(group.id)
|
|
9010
8950
|
|
|
9011
8951
|
for (let j = 0, n = childIds.length; j < n; j++) {
|
|
@@ -9132,7 +9072,9 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
9132
9072
|
|
|
9133
9073
|
/** @internal */
|
|
9134
9074
|
private _getUnlockedShapeIds(ids: TLShapeId[]): TLShapeId[] {
|
|
9135
|
-
|
|
9075
|
+
// Match updateShapes, which also refuses shapes under a locked ancestor; otherwise a child
|
|
9076
|
+
// of a locked frame can't be moved but can still be deleted or duplicated
|
|
9077
|
+
return ids.filter((id) => !this.isShapeOrAncestorLocked(id))
|
|
9136
9078
|
}
|
|
9137
9079
|
|
|
9138
9080
|
/**
|
|
@@ -9156,8 +9098,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
9156
9098
|
throw Error('Editor.deleteShapes: must provide an array of shapes or shapeIds')
|
|
9157
9099
|
}
|
|
9158
9100
|
|
|
9159
|
-
const shapeIds =
|
|
9160
|
-
typeof _ids[0] === 'string' ? (_ids as TLShapeId[]) : (_ids as TLShape[]).map((s) => s.id)
|
|
9101
|
+
const shapeIds = toShapeIds(_ids)
|
|
9161
9102
|
|
|
9162
9103
|
// Normally we don't want to delete locked shapes, but if the force option is set, we'll delete them anyway
|
|
9163
9104
|
const shapeIdsToDelete = this._shouldIgnoreShapeLock
|
|
@@ -9631,10 +9572,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
9631
9572
|
*/
|
|
9632
9573
|
getContentFromCurrentPage(shapes: TLShapeId[] | TLShape[]): TLContent | undefined {
|
|
9633
9574
|
// todo: make this work with any page, not just the current page
|
|
9634
|
-
const ids =
|
|
9635
|
-
typeof shapes[0] === 'string'
|
|
9636
|
-
? (shapes as TLShapeId[])
|
|
9637
|
-
: (shapes as TLShape[]).map((s) => s.id)
|
|
9575
|
+
const ids = toShapeIds(shapes)
|
|
9638
9576
|
|
|
9639
9577
|
if (!ids) return
|
|
9640
9578
|
if (ids.length === 0) return
|
|
@@ -9721,21 +9659,34 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
9721
9659
|
!asset.props.src?.startsWith('data:video') &&
|
|
9722
9660
|
!asset.props.src?.startsWith('http')
|
|
9723
9661
|
) {
|
|
9724
|
-
|
|
9725
|
-
|
|
9726
|
-
|
|
9727
|
-
|
|
9728
|
-
|
|
9729
|
-
|
|
9730
|
-
|
|
9731
|
-
|
|
9732
|
-
|
|
9733
|
-
|
|
9734
|
-
|
|
9735
|
-
|
|
9736
|
-
|
|
9737
|
-
|
|
9662
|
+
// If the asset can't be inlined (unresolvable src, fetch failure), fall through and
|
|
9663
|
+
// keep the original record; dropping it leaves the pasted shapes pointing at an
|
|
9664
|
+
// asset that doesn't exist
|
|
9665
|
+
try {
|
|
9666
|
+
const objectUrl = await this.store.props.assets.resolve(asset, {
|
|
9667
|
+
screenScale: 1,
|
|
9668
|
+
steppedScreenScale: 1,
|
|
9669
|
+
dpr: 1,
|
|
9670
|
+
networkEffectiveType: null,
|
|
9671
|
+
shouldResolveToOriginal: true,
|
|
9672
|
+
})
|
|
9673
|
+
if (objectUrl) {
|
|
9674
|
+
// fetch resolves on 4xx/5xx, so without this check a 404 error page would be
|
|
9675
|
+
// inlined as a data:text/html src
|
|
9676
|
+
const response = await fetch(objectUrl)
|
|
9677
|
+
if (response.ok) {
|
|
9678
|
+
const assetWithDataUrl = structuredClone(asset as TLImageAsset | TLVideoAsset)
|
|
9679
|
+
assetWithDataUrl.props.src = await FileHelpers.blobToDataUrl(await response.blob())
|
|
9680
|
+
assets.push(assetWithDataUrl)
|
|
9681
|
+
return
|
|
9682
|
+
}
|
|
9683
|
+
console.warn(`Could not inline asset ${asset.id}: fetch returned ${response.status}`)
|
|
9684
|
+
}
|
|
9685
|
+
} catch (err) {
|
|
9686
|
+
console.warn(`Could not inline asset ${asset.id}`, err)
|
|
9687
|
+
}
|
|
9738
9688
|
}
|
|
9689
|
+
assets.push(asset)
|
|
9739
9690
|
})
|
|
9740
9691
|
)
|
|
9741
9692
|
content.assets = assets
|
|
@@ -10157,12 +10108,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
10157
10108
|
* @public
|
|
10158
10109
|
*/
|
|
10159
10110
|
async getSvgElement(shapes: TLShapeId[] | TLShape[], opts: TLSvgExportOptions = {}) {
|
|
10160
|
-
const ids =
|
|
10161
|
-
shapes.length === 0
|
|
10162
|
-
? this.getCurrentPageShapeIdsSorted()
|
|
10163
|
-
: typeof shapes[0] === 'string'
|
|
10164
|
-
? (shapes as TLShapeId[])
|
|
10165
|
-
: (shapes as TLShape[]).map((s) => s.id)
|
|
10111
|
+
const ids = shapes.length === 0 ? this.getCurrentPageShapeIdsSorted() : toShapeIds(shapes)
|
|
10166
10112
|
|
|
10167
10113
|
if (ids.length === 0) return undefined
|
|
10168
10114
|
|
|
@@ -10851,22 +10797,22 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
10851
10797
|
const releaseMeta = this._metaKeyTimeout !== -1
|
|
10852
10798
|
|
|
10853
10799
|
if (releaseShift) {
|
|
10854
|
-
clearTimeout(this._shiftKeyTimeout)
|
|
10800
|
+
this.timers.clearTimeout(this._shiftKeyTimeout)
|
|
10855
10801
|
this._shiftKeyTimeout = -1
|
|
10856
10802
|
this.inputs.setShiftKey(false)
|
|
10857
10803
|
}
|
|
10858
10804
|
if (releaseAlt) {
|
|
10859
|
-
clearTimeout(this._altKeyTimeout)
|
|
10805
|
+
this.timers.clearTimeout(this._altKeyTimeout)
|
|
10860
10806
|
this._altKeyTimeout = -1
|
|
10861
10807
|
this.inputs.setAltKey(false)
|
|
10862
10808
|
}
|
|
10863
10809
|
if (releaseCtrl) {
|
|
10864
|
-
clearTimeout(this._ctrlKeyTimeout)
|
|
10810
|
+
this.timers.clearTimeout(this._ctrlKeyTimeout)
|
|
10865
10811
|
this._ctrlKeyTimeout = -1
|
|
10866
10812
|
this.inputs.setCtrlKey(false)
|
|
10867
10813
|
}
|
|
10868
10814
|
if (releaseMeta) {
|
|
10869
|
-
clearTimeout(this._metaKeyTimeout)
|
|
10815
|
+
this.timers.clearTimeout(this._metaKeyTimeout)
|
|
10870
10816
|
this._metaKeyTimeout = -1
|
|
10871
10817
|
this.inputs.setMetaKey(false)
|
|
10872
10818
|
}
|
|
@@ -11016,7 +10962,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
11016
10962
|
}
|
|
11017
10963
|
|
|
11018
10964
|
if (info.shiftKey) {
|
|
11019
|
-
clearTimeout(this._shiftKeyTimeout)
|
|
10965
|
+
this.timers.clearTimeout(this._shiftKeyTimeout)
|
|
11020
10966
|
this._shiftKeyTimeout = -1
|
|
11021
10967
|
inputs.setShiftKey(true)
|
|
11022
10968
|
} else if (!info.shiftKey && inputs.getShiftKey() && this._shiftKeyTimeout === -1) {
|
|
@@ -11024,7 +10970,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
11024
10970
|
}
|
|
11025
10971
|
|
|
11026
10972
|
if (info.altKey) {
|
|
11027
|
-
clearTimeout(this._altKeyTimeout)
|
|
10973
|
+
this.timers.clearTimeout(this._altKeyTimeout)
|
|
11028
10974
|
this._altKeyTimeout = -1
|
|
11029
10975
|
inputs.setAltKey(true)
|
|
11030
10976
|
} else if (!info.altKey && inputs.getAltKey() && this._altKeyTimeout === -1) {
|
|
@@ -11032,7 +10978,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
11032
10978
|
}
|
|
11033
10979
|
|
|
11034
10980
|
if (info.ctrlKey) {
|
|
11035
|
-
clearTimeout(this._ctrlKeyTimeout)
|
|
10981
|
+
this.timers.clearTimeout(this._ctrlKeyTimeout)
|
|
11036
10982
|
this._ctrlKeyTimeout = -1
|
|
11037
10983
|
inputs.setCtrlKey(true)
|
|
11038
10984
|
} else if (!info.ctrlKey && inputs.getCtrlKey() && this._ctrlKeyTimeout === -1) {
|
|
@@ -11042,7 +10988,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
11042
10988
|
if (info.metaKey && info.name !== 'key_up') {
|
|
11043
10989
|
// Unlike the other modifiers, the native metaKey property is still true on keyup.
|
|
11044
10990
|
// If we don't have this guard, then the metakey will be left true without the timeout.
|
|
11045
|
-
clearTimeout(this._metaKeyTimeout)
|
|
10991
|
+
this.timers.clearTimeout(this._metaKeyTimeout)
|
|
11046
10992
|
this._metaKeyTimeout = -1
|
|
11047
10993
|
inputs.setMetaKey(true)
|
|
11048
10994
|
} else if (!info.metaKey && inputs.getMetaKey() && this._metaKeyTimeout === -1) {
|
|
@@ -11059,8 +11005,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
11059
11005
|
|
|
11060
11006
|
switch (type) {
|
|
11061
11007
|
case 'pinch': {
|
|
11062
|
-
|
|
11063
|
-
clearTimeout(this._longPressTimeout)
|
|
11008
|
+
this.timers.clearTimeout(this._longPressTimeout)
|
|
11064
11009
|
this.inputs.updateFromEvent(info)
|
|
11065
11010
|
|
|
11066
11011
|
switch (info.name) {
|
|
@@ -11097,6 +11042,9 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
11097
11042
|
}
|
|
11098
11043
|
case 'pinch': {
|
|
11099
11044
|
if (!inputs.getIsPinching()) return
|
|
11045
|
+
// Lock the zoom, not the gesture: skipping pinch_start would send the fingers'
|
|
11046
|
+
// pointer events to the tool, and skipping pinch_end would leave isPinching stuck.
|
|
11047
|
+
if (cameraOptions.isLocked) return
|
|
11100
11048
|
|
|
11101
11049
|
const {
|
|
11102
11050
|
point: { z = 1 },
|
|
@@ -11170,8 +11118,9 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
11170
11118
|
let wheelBehavior = cameraOptions.wheelBehavior
|
|
11171
11119
|
const inputMode = this.user.getUserPreferences().inputMode
|
|
11172
11120
|
|
|
11173
|
-
//
|
|
11174
|
-
|
|
11121
|
+
// The user's input mode preference picks between pan and zoom, but it must not
|
|
11122
|
+
// re-enable a wheel the app disabled with `wheelBehavior: 'none'`.
|
|
11123
|
+
if (inputMode !== null && wheelBehavior !== 'none') {
|
|
11175
11124
|
wheelBehavior = inputMode === 'trackpad' ? 'pan' : 'zoom'
|
|
11176
11125
|
}
|
|
11177
11126
|
|
|
@@ -11245,15 +11194,18 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
11245
11194
|
// Ignore pointer events while we're pinching
|
|
11246
11195
|
if (inputs.getIsPinching()) return
|
|
11247
11196
|
|
|
11248
|
-
this.inputs.updateFromEvent(info)
|
|
11249
11197
|
const { isPen } = info
|
|
11250
11198
|
const { isPenMode } = instanceState
|
|
11251
11199
|
|
|
11200
|
+
// In pen mode, reject non-pen input (a resting palm) before it touches any
|
|
11201
|
+
// state: otherwise updateFromEvent moves the origin point out from under the
|
|
11202
|
+
// pen's drag, and a palm pointer_up clears the pen's isPointing/isDragging.
|
|
11203
|
+
if (isPenMode && !isPen) return
|
|
11204
|
+
|
|
11205
|
+
this.inputs.updateFromEvent(info)
|
|
11206
|
+
|
|
11252
11207
|
switch (info.name) {
|
|
11253
11208
|
case 'pointer_down': {
|
|
11254
|
-
// If we're in pen mode and the input is not a pen type, then stop here
|
|
11255
|
-
if (isPenMode && !isPen) return
|
|
11256
|
-
|
|
11257
11209
|
// A pointer down starts a new interaction, so flush any modifier that's
|
|
11258
11210
|
// still lingering in its release-debounce window: treat it as released now.
|
|
11259
11211
|
this._releaseDebouncedModifiers()
|
|
@@ -11295,6 +11247,10 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
11295
11247
|
inputs.setIsPointing(true)
|
|
11296
11248
|
inputs.setIsDragging(false)
|
|
11297
11249
|
|
|
11250
|
+
// A camera still animating under a held pointer would shift the page
|
|
11251
|
+
// point past the drag threshold and turn a click into a drag (#10706)
|
|
11252
|
+
this.stopCameraAnimation()
|
|
11253
|
+
|
|
11298
11254
|
// If pen mode is off, turn it on for direct-display pen input only (e.g. Apple
|
|
11299
11255
|
// Pencil on an iPad or a Surface Pen on a touchscreen). Indirect desktop tablet
|
|
11300
11256
|
// styluses still draw as pens, but should not auto-enable pen mode.
|
|
@@ -11305,8 +11261,10 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
11305
11261
|
this.interrupt()
|
|
11306
11262
|
}
|
|
11307
11263
|
|
|
11308
|
-
// On devices with erasers (like the Surface Pen or Wacom Pen), button 5 is the eraser
|
|
11309
|
-
|
|
11264
|
+
// On devices with erasers (like the Surface Pen or Wacom Pen), button 5 is the eraser.
|
|
11265
|
+
// Guarded on the eraser tool existing: the bare editor can be configured without
|
|
11266
|
+
// it, and an unguarded transition would throw and crash the editor.
|
|
11267
|
+
if (info.button === STYLUS_ERASER_BUTTON && this.getStateDescendant('eraser')) {
|
|
11310
11268
|
this._restoreToolId = this.getCurrentToolId()
|
|
11311
11269
|
this.complete()
|
|
11312
11270
|
this.setCurrentTool('eraser')
|
|
@@ -11316,10 +11274,10 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
11316
11274
|
this._prevCursor = this.getInstanceState().cursor.type
|
|
11317
11275
|
}
|
|
11318
11276
|
this.inputs.setIsPanning(true)
|
|
11319
|
-
clearTimeout(this._longPressTimeout)
|
|
11277
|
+
this.timers.clearTimeout(this._longPressTimeout)
|
|
11320
11278
|
} else if (info.button === RIGHT_MOUSE_BUTTON && this.options.rightClickPanning) {
|
|
11321
11279
|
this.inputs.setIsRightPointing(true)
|
|
11322
|
-
clearTimeout(this._longPressTimeout)
|
|
11280
|
+
this.timers.clearTimeout(this._longPressTimeout)
|
|
11323
11281
|
return this
|
|
11324
11282
|
}
|
|
11325
11283
|
|
|
@@ -11334,9 +11292,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
11334
11292
|
break
|
|
11335
11293
|
}
|
|
11336
11294
|
case 'pointer_move': {
|
|
11337
|
-
// If the user is in pen mode, but the pointer is not a pen, stop here.
|
|
11338
|
-
if (!isPen && isPenMode) return
|
|
11339
|
-
|
|
11340
11295
|
const { x: cx, y: cy, z: cz } = unsafe__withoutCapture(() => this.getCamera())
|
|
11341
11296
|
|
|
11342
11297
|
// Right-click pointing: waiting to see if this becomes a drag
|
|
@@ -11387,7 +11342,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
11387
11342
|
) {
|
|
11388
11343
|
// Start dragging
|
|
11389
11344
|
inputs.setIsDragging(true)
|
|
11390
|
-
clearTimeout(this._longPressTimeout)
|
|
11345
|
+
this.timers.clearTimeout(this._longPressTimeout)
|
|
11391
11346
|
}
|
|
11392
11347
|
break
|
|
11393
11348
|
}
|
|
@@ -11395,13 +11350,10 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
11395
11350
|
// Stop dragging / pointing
|
|
11396
11351
|
inputs.setIsDragging(false)
|
|
11397
11352
|
inputs.setIsPointing(false)
|
|
11398
|
-
clearTimeout(this._longPressTimeout)
|
|
11353
|
+
this.timers.clearTimeout(this._longPressTimeout)
|
|
11399
11354
|
// Remove the button from the buttons set
|
|
11400
11355
|
inputs.buttons.delete(info.button)
|
|
11401
11356
|
|
|
11402
|
-
// If we're in pen mode and we're not using a pen, stop here
|
|
11403
|
-
if (instanceState.isPenMode && !isPen) return
|
|
11404
|
-
|
|
11405
11357
|
// Right-click pointing ended without dragging—this is a static
|
|
11406
11358
|
// right-click, so let it through to the state chart as right_click.
|
|
11407
11359
|
// Check isPanning first: if we transitioned to panning, isRightPointing
|
|
@@ -11471,7 +11423,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
11471
11423
|
})
|
|
11472
11424
|
}
|
|
11473
11425
|
} else {
|
|
11474
|
-
if (info.button === STYLUS_ERASER_BUTTON) {
|
|
11426
|
+
if (info.button === STYLUS_ERASER_BUTTON && this.getStateDescendant('eraser')) {
|
|
11475
11427
|
// If we were erasing with a stylus button, restore the tool we were using before we started erasing
|
|
11476
11428
|
this.complete()
|
|
11477
11429
|
this.setCurrentTool(this._restoreToolId)
|
|
@@ -11489,9 +11441,11 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
11489
11441
|
break
|
|
11490
11442
|
}
|
|
11491
11443
|
case 'keyboard': {
|
|
11492
|
-
//
|
|
11493
|
-
|
|
11494
|
-
|
|
11444
|
+
// Left and right modifier keys are the same key to us. `inputs.keys` stores
|
|
11445
|
+
// `code`, so normalize that: a `ShiftRight` left as-is would never match the
|
|
11446
|
+
// `ShiftLeft` that nudging checks or that `_releaseShiftKey` clears.
|
|
11447
|
+
if (info.code === 'ShiftRight') info.code = 'ShiftLeft'
|
|
11448
|
+
if (info.code === 'AltRight') info.code = 'AltLeft'
|
|
11495
11449
|
if (info.code === 'ControlRight') info.code = 'ControlLeft'
|
|
11496
11450
|
if (info.code === 'MetaRight') info.code = 'MetaLeft'
|
|
11497
11451
|
|
|
@@ -11509,7 +11463,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
11509
11463
|
|
|
11510
11464
|
this.inputs.setIsPanning(true)
|
|
11511
11465
|
this.inputs.setIsSpacebarPanning(true)
|
|
11512
|
-
clearTimeout(this._longPressTimeout)
|
|
11466
|
+
this.timers.clearTimeout(this._longPressTimeout)
|
|
11513
11467
|
this.setCursor({
|
|
11514
11468
|
type: this.inputs.getIsPointing() ? 'grabbing' : 'grab',
|
|
11515
11469
|
rotation: 0,
|
|
@@ -11537,7 +11491,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
11537
11491
|
}
|
|
11538
11492
|
}
|
|
11539
11493
|
|
|
11540
|
-
|
|
11494
|
+
// `_animateToViewport` bypasses `setCamera`, so honor the lock here.
|
|
11495
|
+
if (offset && !cameraOptions.isLocked) {
|
|
11541
11496
|
const bounds = this.getViewportPageBounds()
|
|
11542
11497
|
const next = bounds.clone().translate(offset.mulV({ x: bounds.w, y: bounds.h }))
|
|
11543
11498
|
this._animateToViewport(next, { animation: { duration: 320 } })
|
|
@@ -11599,9 +11554,11 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
11599
11554
|
this.setCurrentTool('select')
|
|
11600
11555
|
}
|
|
11601
11556
|
|
|
11602
|
-
//
|
|
11557
|
+
// Send the event to the click manager. In pen mode only pen clicks count; otherwise
|
|
11558
|
+
// every pointer does, including an indirect tablet stylus (a pen that never enables
|
|
11559
|
+
// pen mode), which would otherwise never produce double-click events.
|
|
11603
11560
|
const { isPenMode } = this.store.unsafeGetWithoutCapture(TLINSTANCE_ID)!
|
|
11604
|
-
if (info.isPen
|
|
11561
|
+
if (!isPenMode || info.isPen) {
|
|
11605
11562
|
// The click manager may return a new event, i.e. a double click event
|
|
11606
11563
|
// depending on the event coming in and its own state. If the event has
|
|
11607
11564
|
// changed then hand both events to the statechart
|
|
@@ -11633,7 +11590,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
11633
11590
|
private maybeTrackPerformance(name: string) {
|
|
11634
11591
|
if (debugFlags.measurePerformance.get()) {
|
|
11635
11592
|
if (this.performanceTracker.isStarted()) {
|
|
11636
|
-
clearTimeout(this.performanceTrackerTimeout)
|
|
11593
|
+
this.timers.clearTimeout(this.performanceTrackerTimeout)
|
|
11637
11594
|
} else {
|
|
11638
11595
|
this.performanceTracker.start(name)
|
|
11639
11596
|
}
|
|
@@ -11697,6 +11654,16 @@ function pushShapeWithDescendants(editor: Editor, id: TLShapeId, result: TLShape
|
|
|
11697
11654
|
}
|
|
11698
11655
|
}
|
|
11699
11656
|
|
|
11657
|
+
/**
|
|
11658
|
+
* When `shapes` already holds ids this returns that same array, not a copy, so a caller that
|
|
11659
|
+
* mutates the result (for example to sort it) must copy it first.
|
|
11660
|
+
*/
|
|
11661
|
+
function toShapeIds(shapes: TLShapeId[] | TLShape[]): TLShapeId[] {
|
|
11662
|
+
return typeof shapes[0] === 'string'
|
|
11663
|
+
? (shapes as TLShapeId[])
|
|
11664
|
+
: (shapes as TLShape[]).map((shape) => shape.id)
|
|
11665
|
+
}
|
|
11666
|
+
|
|
11700
11667
|
/**
|
|
11701
11668
|
* Run `callback` in a world where all bindings from the shapes in `shapeIds` to shapes not in
|
|
11702
11669
|
* `shapeIds` are removed. This is useful when you want to duplicate/copy shapes without worrying
|
|
@@ -11734,7 +11701,7 @@ function withIsolatedShapes<T>(
|
|
|
11734
11701
|
}
|
|
11735
11702
|
}
|
|
11736
11703
|
|
|
11737
|
-
editor.
|
|
11704
|
+
editor._deleteBindings([...bindingsToRemove], { isolateShapes: true })
|
|
11738
11705
|
|
|
11739
11706
|
try {
|
|
11740
11707
|
result = Result.ok(callback(bindingsWithBoth))
|