@tldraw/editor 5.4.2 → 5.5.0-canary.0241f11f2e99
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 +7 -2
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/lib/constants.js +2 -0
- package/dist-cjs/lib/constants.js.map +2 -2
- package/dist-cjs/lib/editor/Editor.js +131 -151
- 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/EdgeScrollManager/EdgeScrollManager.js +10 -6
- package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/ThemeManager/ThemeManager.js +7 -3
- package/dist-cjs/lib/editor/managers/ThemeManager/ThemeManager.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/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/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/primitives/geometry/Arc2d.js +1 -1
- package/dist-cjs/lib/primitives/geometry/Arc2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js +1 -1
- package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Edge2d.js +1 -1
- package/dist-cjs/lib/primitives/geometry/Edge2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Group2d.js +8 -5
- package/dist-cjs/lib/primitives/geometry/Group2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Point2d.js +1 -1
- package/dist-cjs/lib/primitives/geometry/Point2d.js.map +2 -2
- package/dist-cjs/lib/utils/sync/LocalIndexedDb.js +13 -17
- package/dist-cjs/lib/utils/sync/LocalIndexedDb.js.map +2 -2
- package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js +78 -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 +7 -2
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/lib/constants.mjs +2 -0
- package/dist-esm/lib/constants.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +132 -151
- 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/EdgeScrollManager/EdgeScrollManager.mjs +10 -6
- package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/ThemeManager/ThemeManager.mjs +7 -3
- package/dist-esm/lib/editor/managers/ThemeManager/ThemeManager.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/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/primitives/geometry/Arc2d.mjs +1 -1
- package/dist-esm/lib/primitives/geometry/Arc2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs +1 -1
- package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Edge2d.mjs +1 -1
- package/dist-esm/lib/primitives/geometry/Edge2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Group2d.mjs +12 -6
- package/dist-esm/lib/primitives/geometry/Group2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Point2d.mjs +1 -1
- package/dist-esm/lib/primitives/geometry/Point2d.mjs.map +2 -2
- package/dist-esm/lib/utils/sync/LocalIndexedDb.mjs +13 -17
- package/dist-esm/lib/utils/sync/LocalIndexedDb.mjs.map +2 -2
- package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs +78 -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/package.json +7 -7
- package/src/lib/constants.ts +8 -0
- package/src/lib/editor/Editor.test.ts +20 -0
- package/src/lib/editor/Editor.ts +202 -255
- package/src/lib/editor/derivations/bindingsIndex.ts +3 -0
- package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.test.ts +35 -4
- package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.ts +13 -7
- package/src/lib/editor/managers/ThemeManager/ThemeManager.test.ts +53 -0
- package/src/lib/editor/managers/ThemeManager/ThemeManager.ts +9 -3
- package/src/lib/editor/shapes/group/GroupShapeUtil.tsx +23 -8
- 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/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/primitives/geometry/Arc2d.test.ts +9 -0
- package/src/lib/primitives/geometry/Arc2d.ts +1 -1
- package/src/lib/primitives/geometry/CubicBezier2d.test.ts +13 -0
- package/src/lib/primitives/geometry/CubicBezier2d.ts +1 -1
- package/src/lib/primitives/geometry/Edge2d.test.ts +21 -3
- package/src/lib/primitives/geometry/Edge2d.ts +1 -1
- package/src/lib/primitives/geometry/Group2d.test.ts +26 -5
- package/src/lib/primitives/geometry/Group2d.ts +18 -7
- package/src/lib/primitives/geometry/Point2d.test.ts +14 -3
- package/src/lib/primitives/geometry/Point2d.ts +1 -1
- package/src/lib/utils/sync/LocalIndexedDb.test.ts +28 -0
- package/src/lib/utils/sync/LocalIndexedDb.ts +18 -20
- package/src/lib/utils/sync/TLLocalSyncClient.test.ts +236 -0
- package/src/lib/utils/sync/TLLocalSyncClient.ts +106 -54
- package/src/version.ts +3 -3
package/src/lib/editor/Editor.ts
CHANGED
|
@@ -110,6 +110,7 @@ import {
|
|
|
110
110
|
import {
|
|
111
111
|
DEFAULT_ANIMATION_OPTIONS,
|
|
112
112
|
DEFAULT_CAMERA_OPTIONS,
|
|
113
|
+
FRAME_MS_60HZ,
|
|
113
114
|
INTERNAL_POINTER_IDS,
|
|
114
115
|
LEFT_MOUSE_BUTTON,
|
|
115
116
|
MIDDLE_MOUSE_BUTTON,
|
|
@@ -340,6 +341,11 @@ export interface TLRenderingShape {
|
|
|
340
341
|
|
|
341
342
|
const RENDERING_SHAPES_SORT_CACHE_THRESHOLD = 100
|
|
342
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
|
+
|
|
343
349
|
/** @public */
|
|
344
350
|
export class Editor extends EventEmitter<TLEventMap> {
|
|
345
351
|
readonly id = uniqueId()
|
|
@@ -752,7 +758,9 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
752
758
|
}
|
|
753
759
|
|
|
754
760
|
if (deleteBindingIds.length) {
|
|
755
|
-
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)
|
|
756
764
|
}
|
|
757
765
|
},
|
|
758
766
|
},
|
|
@@ -1969,6 +1977,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
1969
1977
|
* @public
|
|
1970
1978
|
**/
|
|
1971
1979
|
updateDocumentSettings(settings: Partial<TLDocument>): this {
|
|
1980
|
+
if (this.getIsReadonly()) return this
|
|
1972
1981
|
this.run(
|
|
1973
1982
|
() => {
|
|
1974
1983
|
this.store.put([{ ...this.getDocumentSettings(), ...settings }])
|
|
@@ -2196,10 +2205,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
2196
2205
|
* @public
|
|
2197
2206
|
*/
|
|
2198
2207
|
select(...shapes: TLShapeId[] | TLShape[]): this {
|
|
2199
|
-
const ids =
|
|
2200
|
-
typeof shapes[0] === 'string'
|
|
2201
|
-
? (shapes as TLShapeId[])
|
|
2202
|
-
: (shapes as TLShape[]).map((shape) => shape.id)
|
|
2208
|
+
const ids = toShapeIds(shapes)
|
|
2203
2209
|
this.setSelectedShapes(ids)
|
|
2204
2210
|
return this
|
|
2205
2211
|
}
|
|
@@ -2215,10 +2221,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
2215
2221
|
* @public
|
|
2216
2222
|
*/
|
|
2217
2223
|
deselect(...shapes: TLShapeId[] | TLShape[]): this {
|
|
2218
|
-
const ids =
|
|
2219
|
-
typeof shapes[0] === 'string'
|
|
2220
|
-
? (shapes as TLShapeId[])
|
|
2221
|
-
: (shapes as TLShape[]).map((shape) => shape.id)
|
|
2224
|
+
const ids = toShapeIds(shapes)
|
|
2222
2225
|
const selectedShapeIds = this.getSelectedShapeIds()
|
|
2223
2226
|
if (selectedShapeIds.length > 0 && ids.length > 0) {
|
|
2224
2227
|
this.setSelectedShapes(selectedShapeIds.filter((id) => !ids.includes(id)))
|
|
@@ -2263,10 +2266,11 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
2263
2266
|
parentToSelectWithinId = this.getCurrentPageId()
|
|
2264
2267
|
}
|
|
2265
2268
|
|
|
2266
|
-
// 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.
|
|
2267
2271
|
const ids = this.getSortedChildIdsForParent(parentToSelectWithinId)
|
|
2268
2272
|
if (ids.length <= 0) return this
|
|
2269
|
-
this.setSelectedShapes(this.
|
|
2273
|
+
this.setSelectedShapes(ids.filter((id) => !this.getShape(id)?.isLocked))
|
|
2270
2274
|
return this
|
|
2271
2275
|
}
|
|
2272
2276
|
|
|
@@ -2287,12 +2291,14 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
2287
2291
|
firstParentId &&
|
|
2288
2292
|
selectedShapeIds.every((shapeId) => this.getShape(shapeId)?.parentId === firstParentId) &&
|
|
2289
2293
|
!isPageId(firstParentId)
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
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)
|
|
2296
2302
|
const currentShapeId: TLShapeId | undefined =
|
|
2297
2303
|
selectedShapeIds.length === 1
|
|
2298
2304
|
? selectedShapeIds[0]
|
|
@@ -2301,10 +2307,16 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
2301
2307
|
let adjacentShapeId: TLShapeId
|
|
2302
2308
|
if (direction === 'next' || direction === 'prev') {
|
|
2303
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
|
|
2304
2313
|
|
|
2305
2314
|
const currentIndex = currentShapeId ? shapeIds.indexOf(currentShapeId) : -1
|
|
2315
|
+
// With no current index, stepping from -1 makes 'prev' land one shape
|
|
2316
|
+
// before the last; seed it from 0 so it wraps to the last shape. See #10559.
|
|
2317
|
+
const startIndex = currentIndex === -1 && direction === 'prev' ? 0 : currentIndex
|
|
2306
2318
|
const adjacentIndex =
|
|
2307
|
-
(
|
|
2319
|
+
(startIndex + (direction === 'next' ? 1 : -1) + shapeIds.length) % shapeIds.length
|
|
2308
2320
|
adjacentShapeId = shapeIds[adjacentIndex]
|
|
2309
2321
|
} else {
|
|
2310
2322
|
if (!currentShapeId) return
|
|
@@ -2446,8 +2458,9 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
2446
2458
|
|
|
2447
2459
|
// Ok, now score that subset of shapes.
|
|
2448
2460
|
const lowestScoringShape = minBy(shapesInDirection, ({ center }) => {
|
|
2449
|
-
//
|
|
2450
|
-
|
|
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)
|
|
2451
2464
|
|
|
2452
2465
|
// Distance along the primary axis.
|
|
2453
2466
|
const dirProp = ['left', 'right'].includes(direction) ? 'x' : 'y'
|
|
@@ -2457,9 +2470,11 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
2457
2470
|
const offProp = ['left', 'right'].includes(direction) ? 'y' : 'x'
|
|
2458
2471
|
const offAxisDeviation = Math.abs(center[offProp] - currentCenter[offProp])
|
|
2459
2472
|
|
|
2460
|
-
//
|
|
2461
|
-
|
|
2462
|
-
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)
|
|
2463
2478
|
|
|
2464
2479
|
// Calculate final score (lower is better).
|
|
2465
2480
|
// Weight factors to prioritize:
|
|
@@ -2964,10 +2979,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
2964
2979
|
* @public
|
|
2965
2980
|
*/
|
|
2966
2981
|
setHintingShapes(shapes: TLShapeId[] | TLShape[]): this {
|
|
2967
|
-
const ids =
|
|
2968
|
-
typeof shapes[0] === 'string'
|
|
2969
|
-
? (shapes as TLShapeId[])
|
|
2970
|
-
: (shapes as TLShape[]).map((shape) => shape.id)
|
|
2982
|
+
const ids = toShapeIds(shapes)
|
|
2971
2983
|
// always ephemeral
|
|
2972
2984
|
this.run(
|
|
2973
2985
|
() => {
|
|
@@ -3014,10 +3026,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
3014
3026
|
*/
|
|
3015
3027
|
setErasingShapes(shapes: TLShapeId[] | TLShape[]): this {
|
|
3016
3028
|
// copy before sorting: the caller may pass a store-owned (frozen) array
|
|
3017
|
-
const ids =
|
|
3018
|
-
typeof shapes[0] === 'string'
|
|
3019
|
-
? (shapes as TLShapeId[]).slice()
|
|
3020
|
-
: (shapes as TLShape[]).map((shape) => shape.id)
|
|
3029
|
+
const ids = toShapeIds(shapes).slice()
|
|
3021
3030
|
ids.sort() // sort the incoming ids
|
|
3022
3031
|
const erasingShapeIds = this.getErasingShapeIds()
|
|
3023
3032
|
this.run(
|
|
@@ -3591,6 +3600,20 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
3591
3600
|
const { isLocked } = this._cameraOptions.__unsafe__getWithoutCapture()
|
|
3592
3601
|
if (isLocked && !opts?.force) return this
|
|
3593
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
|
+
|
|
3594
3617
|
// Stop any camera animations
|
|
3595
3618
|
this.stopCameraAnimation()
|
|
3596
3619
|
|
|
@@ -3599,12 +3622,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
3599
3622
|
this.stopFollowingUser()
|
|
3600
3623
|
}
|
|
3601
3624
|
|
|
3602
|
-
const _point = Vec.Cast(point)
|
|
3603
|
-
|
|
3604
|
-
if (!Number.isFinite(_point.x)) _point.x = 0
|
|
3605
|
-
if (!Number.isFinite(_point.y)) _point.y = 0
|
|
3606
|
-
if (_point.z === undefined || !Number.isFinite(_point.z)) point.z = this.getZoomLevel()
|
|
3607
|
-
|
|
3608
3625
|
const camera = this.getConstrainedCamera(_point, opts)
|
|
3609
3626
|
|
|
3610
3627
|
if (opts?.animation) {
|
|
@@ -4086,8 +4103,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
4086
4103
|
newCy += center.y / newCz - center.y / cz
|
|
4087
4104
|
}
|
|
4088
4105
|
|
|
4089
|
-
// Apply friction
|
|
4090
|
-
currentSpeed *= 1 - friction
|
|
4106
|
+
// Apply friction per unit of elapsed time, not per tick, or a 120 Hz display decays twice as fast
|
|
4107
|
+
currentSpeed *= (1 - friction) ** (elapsed / FRAME_MS_60HZ)
|
|
4091
4108
|
if (currentSpeed < speedThreshold) {
|
|
4092
4109
|
cancel()
|
|
4093
4110
|
} else {
|
|
@@ -4130,6 +4147,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
4130
4147
|
// If we're not on the same page, move to the page they're on
|
|
4131
4148
|
const isOnSamePage = presence.currentPageId === this.getCurrentPageId()
|
|
4132
4149
|
if (!isOnSamePage) {
|
|
4150
|
+
this.markHistoryStoppingPoint('change-page')
|
|
4133
4151
|
this.setCurrentPage(presence.currentPageId)
|
|
4134
4152
|
}
|
|
4135
4153
|
|
|
@@ -5227,7 +5245,10 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
5227
5245
|
|
|
5228
5246
|
this.run(
|
|
5229
5247
|
() => {
|
|
5230
|
-
|
|
5248
|
+
// the asset store's remove is async; surface failures instead of leaving an unhandled rejection
|
|
5249
|
+
Promise.resolve(this.store.props.assets.remove?.(ids)).catch((err) =>
|
|
5250
|
+
console.error('Error while removing assets from the asset store:', err)
|
|
5251
|
+
)
|
|
5231
5252
|
this.store.remove(ids)
|
|
5232
5253
|
},
|
|
5233
5254
|
{ history: 'ignore' }
|
|
@@ -5689,10 +5710,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
5689
5710
|
return
|
|
5690
5711
|
}
|
|
5691
5712
|
|
|
5692
|
-
const ids =
|
|
5693
|
-
typeof shapes[0] === 'string'
|
|
5694
|
-
? (shapes as TLShapeId[])
|
|
5695
|
-
: (shapes as TLShape[]).map((s) => s.id)
|
|
5713
|
+
const ids = toShapeIds(shapes)
|
|
5696
5714
|
const freshShapes = compact(ids.map((id) => this.getShape(id)))
|
|
5697
5715
|
|
|
5698
5716
|
if (freshShapes.length === 1) {
|
|
@@ -6423,8 +6441,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6423
6441
|
* @public
|
|
6424
6442
|
*/
|
|
6425
6443
|
reparentShapes(shapes: TLShapeId[] | TLShape[], parentId: TLParentId, insertIndex?: IndexKey) {
|
|
6426
|
-
const ids =
|
|
6427
|
-
typeof shapes[0] === 'string' ? (shapes as TLShapeId[]) : shapes.map((s) => (s as TLShape).id)
|
|
6444
|
+
const ids = toShapeIds(shapes)
|
|
6428
6445
|
if (ids.length === 0) return this
|
|
6429
6446
|
|
|
6430
6447
|
const changes: TLShapePartial[] = []
|
|
@@ -6773,6 +6790,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6773
6790
|
* binding, but the `type`, `toId`, and `fromId` must all be provided.
|
|
6774
6791
|
*/
|
|
6775
6792
|
createBindings<B extends TLBinding = TLBinding>(partials: TLBindingCreate<B>[]) {
|
|
6793
|
+
if (this.getIsReadonly()) return this
|
|
6794
|
+
|
|
6776
6795
|
const bindings: TLBinding[] = []
|
|
6777
6796
|
for (const partial of partials) {
|
|
6778
6797
|
const fromShape = this.getShape(partial.fromId)
|
|
@@ -6812,6 +6831,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6812
6831
|
* binding is skipped. The changes from the partial are merged into the existing record.
|
|
6813
6832
|
*/
|
|
6814
6833
|
updateBindings(partials: (TLBindingUpdate | null | undefined)[]) {
|
|
6834
|
+
if (this.getIsReadonly()) return this
|
|
6835
|
+
|
|
6815
6836
|
const updated: TLBinding[] = []
|
|
6816
6837
|
|
|
6817
6838
|
for (const partial of partials) {
|
|
@@ -6849,6 +6870,17 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6849
6870
|
* Delete several bindings by their IDs. If a binding ID doesn't exist, it's ignored.
|
|
6850
6871
|
*/
|
|
6851
6872
|
deleteBindings(bindings: (TLBinding | TLBindingId)[], { isolateShapes = false } = {}) {
|
|
6873
|
+
if (this.getIsReadonly()) return this
|
|
6874
|
+
return this._deleteBindings(bindings, { isolateShapes })
|
|
6875
|
+
}
|
|
6876
|
+
|
|
6877
|
+
/**
|
|
6878
|
+
* Unguarded so that withIsolatedShapes can transiently isolate shapes for copy and export in
|
|
6879
|
+
* readonly mode; the public deleteBindings is what readonly blocks.
|
|
6880
|
+
*
|
|
6881
|
+
* @internal
|
|
6882
|
+
*/
|
|
6883
|
+
_deleteBindings(bindings: (TLBinding | TLBindingId)[], { isolateShapes = false } = {}) {
|
|
6852
6884
|
const ids = bindings.map((binding) => (typeof binding === 'string' ? binding : binding.id))
|
|
6853
6885
|
if (isolateShapes) {
|
|
6854
6886
|
this.store.atomic(() => {
|
|
@@ -6923,10 +6955,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6923
6955
|
delta: number,
|
|
6924
6956
|
opts?: { center?: VecLike }
|
|
6925
6957
|
): this {
|
|
6926
|
-
const ids =
|
|
6927
|
-
typeof shapes[0] === 'string'
|
|
6928
|
-
? (shapes as TLShapeId[])
|
|
6929
|
-
: (shapes as TLShape[]).map((s) => s.id)
|
|
6958
|
+
const ids = toShapeIds(shapes)
|
|
6930
6959
|
|
|
6931
6960
|
if (ids.length <= 0) return this
|
|
6932
6961
|
|
|
@@ -6973,6 +7002,15 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6973
7002
|
return workingShape
|
|
6974
7003
|
}
|
|
6975
7004
|
|
|
7005
|
+
// Counter-rotates the page-space delta into the parent's space; without that a child of a rotated
|
|
7006
|
+
// frame or group would move along its parent's axes instead of the page's.
|
|
7007
|
+
// todo: a shape laid out together with its own parent moves twice, once with the parent and once
|
|
7008
|
+
// on its own; the layout commands should skip shapes whose ancestors are also being laid out
|
|
7009
|
+
private getChangesToTranslateShapeByPageDelta(shape: TLShape, pageDelta: VecLike): TLShape {
|
|
7010
|
+
const localDelta = Vec.From(pageDelta).rot(-this.getShapeParentTransform(shape).rotation())
|
|
7011
|
+
return this.getChangesToTranslateShape(shape, localDelta.add(shape))
|
|
7012
|
+
}
|
|
7013
|
+
|
|
6976
7014
|
/**
|
|
6977
7015
|
* Move shapes by a delta.
|
|
6978
7016
|
*
|
|
@@ -6985,20 +7023,14 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6985
7023
|
* @param offset - The offset to apply to the shapes.
|
|
6986
7024
|
*/
|
|
6987
7025
|
nudgeShapes(shapes: TLShapeId[] | TLShape[], offset: VecLike): this {
|
|
6988
|
-
const ids =
|
|
6989
|
-
typeof shapes[0] === 'string'
|
|
6990
|
-
? (shapes as TLShapeId[])
|
|
6991
|
-
: (shapes as TLShape[]).map((s) => s.id)
|
|
7026
|
+
const ids = toShapeIds(shapes)
|
|
6992
7027
|
|
|
6993
7028
|
if (ids.length <= 0) return this
|
|
6994
7029
|
const changes: TLShapePartial[] = []
|
|
6995
7030
|
|
|
6996
7031
|
for (const id of ids) {
|
|
6997
7032
|
const shape = this.getShape(id)!
|
|
6998
|
-
|
|
6999
|
-
localDelta.rot(-this.getShapeParentTransform(shape).rotation())
|
|
7000
|
-
|
|
7001
|
-
changes.push(this.getChangesToTranslateShape(shape, localDelta.add(shape)))
|
|
7033
|
+
changes.push(this.getChangesToTranslateShapeByPageDelta(shape, offset))
|
|
7002
7034
|
}
|
|
7003
7035
|
|
|
7004
7036
|
this.updateShapes(changes)
|
|
@@ -7022,10 +7054,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
7022
7054
|
*/
|
|
7023
7055
|
duplicateShapes(shapes: TLShapeId[] | TLShape[], offset?: VecLike): this {
|
|
7024
7056
|
this.run(() => {
|
|
7025
|
-
const _ids =
|
|
7026
|
-
typeof shapes[0] === 'string'
|
|
7027
|
-
? (shapes as TLShapeId[])
|
|
7028
|
-
: (shapes as TLShape[]).map((s) => s.id)
|
|
7057
|
+
const _ids = toShapeIds(shapes)
|
|
7029
7058
|
|
|
7030
7059
|
const ids = this._shouldIgnoreShapeLock ? _ids : this._getUnlockedShapeIds(_ids)
|
|
7031
7060
|
if (ids.length <= 0) return this
|
|
@@ -7167,10 +7196,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
7167
7196
|
* @public
|
|
7168
7197
|
*/
|
|
7169
7198
|
moveShapesToPage(shapes: TLShapeId[] | TLShape[], pageId: TLPageId): this {
|
|
7170
|
-
const ids =
|
|
7171
|
-
typeof shapes[0] === 'string'
|
|
7172
|
-
? (shapes as TLShapeId[])
|
|
7173
|
-
: (shapes as TLShape[]).map((s) => s.id)
|
|
7199
|
+
const ids = toShapeIds(shapes)
|
|
7174
7200
|
|
|
7175
7201
|
if (ids.length === 0) return this
|
|
7176
7202
|
if (this.getIsReadonly()) return this
|
|
@@ -7231,10 +7257,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
7231
7257
|
* @public
|
|
7232
7258
|
*/
|
|
7233
7259
|
toggleLock(shapes: TLShapeId[] | TLShape[]): this {
|
|
7234
|
-
const ids =
|
|
7235
|
-
typeof shapes[0] === 'string'
|
|
7236
|
-
? (shapes as TLShapeId[])
|
|
7237
|
-
: (shapes as TLShape[]).map((s) => s.id)
|
|
7260
|
+
const ids = toShapeIds(shapes)
|
|
7238
7261
|
|
|
7239
7262
|
if (this.getIsReadonly() || ids.length === 0) return this
|
|
7240
7263
|
|
|
@@ -7277,11 +7300,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
7277
7300
|
* @public
|
|
7278
7301
|
*/
|
|
7279
7302
|
sendToBack(shapes: TLShapeId[] | TLShape[]): this {
|
|
7280
|
-
const ids =
|
|
7281
|
-
|
|
7282
|
-
? (shapes as TLShapeId[])
|
|
7283
|
-
: (shapes as TLShape[]).map((s) => s.id)
|
|
7284
|
-
const changes = getReorderingShapesChanges(this, 'toBack', ids as TLShapeId[], {
|
|
7303
|
+
const ids = toShapeIds(shapes)
|
|
7304
|
+
const changes = getReorderingShapesChanges(this, 'toBack', ids, {
|
|
7285
7305
|
considerAllShapes: true,
|
|
7286
7306
|
})
|
|
7287
7307
|
if (changes) this.updateShapes(changes)
|
|
@@ -7311,11 +7331,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
7311
7331
|
* @public
|
|
7312
7332
|
*/
|
|
7313
7333
|
sendBackward(shapes: TLShapeId[] | TLShape[], opts: { considerAllShapes?: boolean } = {}): this {
|
|
7314
|
-
const ids =
|
|
7315
|
-
|
|
7316
|
-
? (shapes as TLShapeId[])
|
|
7317
|
-
: (shapes as TLShape[]).map((s) => s.id)
|
|
7318
|
-
const changes = getReorderingShapesChanges(this, 'backward', ids as TLShapeId[], opts)
|
|
7334
|
+
const ids = toShapeIds(shapes)
|
|
7335
|
+
const changes = getReorderingShapesChanges(this, 'backward', ids, opts)
|
|
7319
7336
|
if (changes) this.updateShapes(changes)
|
|
7320
7337
|
return this
|
|
7321
7338
|
}
|
|
@@ -7343,11 +7360,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
7343
7360
|
* @public
|
|
7344
7361
|
*/
|
|
7345
7362
|
bringForward(shapes: TLShapeId[] | TLShape[], opts: { considerAllShapes?: boolean } = {}): this {
|
|
7346
|
-
const ids =
|
|
7347
|
-
|
|
7348
|
-
? (shapes as TLShapeId[])
|
|
7349
|
-
: (shapes as TLShape[]).map((s) => s.id)
|
|
7350
|
-
const changes = getReorderingShapesChanges(this, 'forward', ids as TLShapeId[], opts)
|
|
7363
|
+
const ids = toShapeIds(shapes)
|
|
7364
|
+
const changes = getReorderingShapesChanges(this, 'forward', ids, opts)
|
|
7351
7365
|
if (changes) this.updateShapes(changes)
|
|
7352
7366
|
return this
|
|
7353
7367
|
}
|
|
@@ -7366,11 +7380,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
7366
7380
|
* @public
|
|
7367
7381
|
*/
|
|
7368
7382
|
bringToFront(shapes: TLShapeId[] | TLShape[]): this {
|
|
7369
|
-
const ids =
|
|
7370
|
-
|
|
7371
|
-
? (shapes as TLShapeId[])
|
|
7372
|
-
: (shapes as TLShape[]).map((s) => s.id)
|
|
7373
|
-
const changes = getReorderingShapesChanges(this, 'toFront', ids as TLShapeId[])
|
|
7383
|
+
const ids = toShapeIds(shapes)
|
|
7384
|
+
const changes = getReorderingShapesChanges(this, 'toFront', ids)
|
|
7374
7385
|
if (changes) this.updateShapes(changes)
|
|
7375
7386
|
return this
|
|
7376
7387
|
}
|
|
@@ -7386,10 +7397,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
7386
7397
|
type: TLShapeUtilCanBeLaidOutOpts['type'],
|
|
7387
7398
|
opts?: { filterAxisAligned?: boolean }
|
|
7388
7399
|
): { clusters: { shapes: TLShape[]; pageBounds: Box }[]; allBounds: Box[] } {
|
|
7389
|
-
const ids =
|
|
7390
|
-
typeof shapes[0] === 'string'
|
|
7391
|
-
? (shapes as TLShapeId[])
|
|
7392
|
-
: (shapes as TLShape[]).map((s) => s.id)
|
|
7400
|
+
const ids = toShapeIds(shapes)
|
|
7393
7401
|
|
|
7394
7402
|
// always fresh shapes
|
|
7395
7403
|
let freshShapes = compact(ids.map((id) => this.getShape(id)))
|
|
@@ -7427,8 +7435,10 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
7427
7435
|
const shapesMovingTogether = [shape]
|
|
7428
7436
|
const boundsOfShapesMovingTogether: Box[] = [shapePageBounds]
|
|
7429
7437
|
|
|
7438
|
+
// Seed with bindings in both directions, otherwise an arrow visited before the shapes it
|
|
7439
|
+
// binds ends up in a cluster of its own and the result depends on input order
|
|
7430
7440
|
this.collectShapesViaArrowBindings({
|
|
7431
|
-
bindings: this.
|
|
7441
|
+
bindings: this.getBindingsInvolvingShape(shape.id, 'arrow'),
|
|
7432
7442
|
initialShapes: freshShapes,
|
|
7433
7443
|
resultShapes: shapesMovingTogether,
|
|
7434
7444
|
resultBounds: boundsOfShapesMovingTogether,
|
|
@@ -7497,10 +7507,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
7497
7507
|
flipShapes(shapes: TLShapeId[] | TLShape[], operation: 'horizontal' | 'vertical'): this {
|
|
7498
7508
|
if (this.getIsReadonly()) return this
|
|
7499
7509
|
|
|
7500
|
-
const ids =
|
|
7501
|
-
typeof shapes[0] === 'string'
|
|
7502
|
-
? (shapes as TLShapeId[])
|
|
7503
|
-
: (shapes as TLShape[]).map((s) => s.id)
|
|
7510
|
+
const ids = toShapeIds(shapes)
|
|
7504
7511
|
|
|
7505
7512
|
// Collect a greedy list of shapes to flip
|
|
7506
7513
|
const shapesToFlipFirstPass = compact(ids.map((id) => this.getShape(id)))
|
|
@@ -7603,22 +7610,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
7603
7610
|
const len = shapeClustersToStack.length
|
|
7604
7611
|
if ((_gap === 0 && len < 3) || len < 2) return this
|
|
7605
7612
|
|
|
7606
|
-
|
|
7607
|
-
let min: 'minX' | 'minY'
|
|
7608
|
-
let max: 'maxX' | 'maxY'
|
|
7609
|
-
let dim: 'width' | 'height'
|
|
7610
|
-
|
|
7611
|
-
if (operation === 'horizontal') {
|
|
7612
|
-
val = 'x'
|
|
7613
|
-
min = 'minX'
|
|
7614
|
-
max = 'maxX'
|
|
7615
|
-
dim = 'width'
|
|
7616
|
-
} else {
|
|
7617
|
-
val = 'y'
|
|
7618
|
-
min = 'minY'
|
|
7619
|
-
max = 'maxY'
|
|
7620
|
-
dim = 'height'
|
|
7621
|
-
}
|
|
7613
|
+
const { val, min, max, dim } = AXIS[operation]
|
|
7622
7614
|
|
|
7623
7615
|
let shapeGap: number = 0
|
|
7624
7616
|
|
|
@@ -7676,17 +7668,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
7676
7668
|
delta[val] = v + shapeGap - pageBounds[val]
|
|
7677
7669
|
|
|
7678
7670
|
for (const shape of shapes) {
|
|
7679
|
-
|
|
7680
|
-
|
|
7681
|
-
// If the shape has another shape as its parent, and if the parent has a rotation, we need to rotate the counter-rotate delta
|
|
7682
|
-
// todo: ensure that the parent isn't being aligned together with its children
|
|
7683
|
-
const parent = this.getShapeParent(shape)
|
|
7684
|
-
if (parent) {
|
|
7685
|
-
shapeDelta.rot(-this.getShapePageTransform(parent).rotation())
|
|
7686
|
-
}
|
|
7687
|
-
|
|
7688
|
-
shapeDelta.add(shape) // add the shape's x and y to the delta
|
|
7689
|
-
changes.push(this.getChangesToTranslateShape(shape, shapeDelta))
|
|
7671
|
+
changes.push(this.getChangesToTranslateShapeByPageDelta(shape, delta))
|
|
7690
7672
|
}
|
|
7691
7673
|
|
|
7692
7674
|
v += pageBounds[dim] + shapeGap
|
|
@@ -7802,15 +7784,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
7802
7784
|
const delta = Vec.Sub(nextPageBounds.point, pageBounds.point).add(centerDelta)
|
|
7803
7785
|
|
|
7804
7786
|
for (const shape of shapes) {
|
|
7805
|
-
|
|
7806
|
-
|
|
7807
|
-
const parent = this.getShapeParent(shape)
|
|
7808
|
-
if (parent) {
|
|
7809
|
-
shapeDelta.rot(-this.getShapeParentTransform(shape).rotation())
|
|
7810
|
-
}
|
|
7811
|
-
|
|
7812
|
-
shapeDelta.add(shape)
|
|
7813
|
-
changes.push(this.getChangesToTranslateShape(shape, shapeDelta))
|
|
7787
|
+
changes.push(this.getChangesToTranslateShapeByPageDelta(shape, delta))
|
|
7814
7788
|
}
|
|
7815
7789
|
}
|
|
7816
7790
|
|
|
@@ -7890,17 +7864,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
7890
7864
|
}
|
|
7891
7865
|
|
|
7892
7866
|
for (const shape of shapes) {
|
|
7893
|
-
|
|
7894
|
-
|
|
7895
|
-
// If the shape has another shape as its parent, and if the parent has a rotation, we need to rotate the counter-rotate delta
|
|
7896
|
-
// todo: ensure that the parent isn't being aligned together with its children
|
|
7897
|
-
const parent = this.getShapeParent(shape)
|
|
7898
|
-
if (parent) {
|
|
7899
|
-
shapeDelta.rot(-this.getShapePageTransform(parent).rotation())
|
|
7900
|
-
}
|
|
7901
|
-
|
|
7902
|
-
shapeDelta.add(shape) // add the shape's x and y to the delta
|
|
7903
|
-
changes.push(this.getChangesToTranslateShape(shape, shapeDelta))
|
|
7867
|
+
changes.push(this.getChangesToTranslateShapeByPageDelta(shape, delta))
|
|
7904
7868
|
}
|
|
7905
7869
|
})
|
|
7906
7870
|
|
|
@@ -7929,22 +7893,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
7929
7893
|
|
|
7930
7894
|
if (shapeClustersToDistribute.length < 3) return this
|
|
7931
7895
|
|
|
7932
|
-
|
|
7933
|
-
let min: 'minX' | 'minY'
|
|
7934
|
-
let max: 'maxX' | 'maxY'
|
|
7935
|
-
let dim: 'width' | 'height'
|
|
7936
|
-
|
|
7937
|
-
if (operation === 'horizontal') {
|
|
7938
|
-
val = 'x'
|
|
7939
|
-
min = 'minX'
|
|
7940
|
-
max = 'maxX'
|
|
7941
|
-
dim = 'width'
|
|
7942
|
-
} else {
|
|
7943
|
-
val = 'y'
|
|
7944
|
-
min = 'minY'
|
|
7945
|
-
max = 'maxY'
|
|
7946
|
-
dim = 'height'
|
|
7947
|
-
}
|
|
7896
|
+
const { val, min, max, dim } = AXIS[operation]
|
|
7948
7897
|
const changes: TLShapePartial[] = []
|
|
7949
7898
|
|
|
7950
7899
|
const first = shapeClustersToDistribute.sort((a, b) => a.pageBounds[min] - b.pageBounds[min])[0]
|
|
@@ -7953,10 +7902,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
7953
7902
|
// If the first shape group is also the last shape group, distribute without it
|
|
7954
7903
|
if (first === last) {
|
|
7955
7904
|
const excludedShapeIds = new Set(first.shapes.map((s) => s.id))
|
|
7956
|
-
const ids =
|
|
7957
|
-
typeof shapes[0] === 'string'
|
|
7958
|
-
? (shapes as TLShapeId[])
|
|
7959
|
-
: (shapes as TLShape[]).map((s) => s.id)
|
|
7905
|
+
const ids = toShapeIds(shapes)
|
|
7960
7906
|
return this.distributeShapes(
|
|
7961
7907
|
ids.filter((id) => !excludedShapeIds.has(id)),
|
|
7962
7908
|
operation
|
|
@@ -7990,17 +7936,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
7990
7936
|
}
|
|
7991
7937
|
|
|
7992
7938
|
for (const shape of shapes) {
|
|
7993
|
-
|
|
7994
|
-
|
|
7995
|
-
// If the shape has another shape as its parent, and if the parent has a rotation, we need to rotate the counter-rotate delta
|
|
7996
|
-
// todo: ensure that the parent isn't being aligned together with its children
|
|
7997
|
-
const parent = this.getShapeParent(shape)
|
|
7998
|
-
if (parent) {
|
|
7999
|
-
shapeDelta.rot(-this.getShapePageTransform(parent).rotation())
|
|
8000
|
-
}
|
|
8001
|
-
|
|
8002
|
-
shapeDelta.add(shape) // add the shape's x and y to the delta
|
|
8003
|
-
changes.push(this.getChangesToTranslateShape(shape, shapeDelta))
|
|
7939
|
+
changes.push(this.getChangesToTranslateShapeByPageDelta(shape, delta))
|
|
8004
7940
|
}
|
|
8005
7941
|
|
|
8006
7942
|
v += pageBounds[dim] + gap
|
|
@@ -8036,24 +7972,12 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
8036
7972
|
if (shapeClustersToStretch.length < 2) return this
|
|
8037
7973
|
|
|
8038
7974
|
const commonBounds = Box.Common(allBounds)
|
|
8039
|
-
|
|
8040
|
-
let min: 'minX' | 'minY'
|
|
8041
|
-
let dim: 'width' | 'height'
|
|
8042
|
-
|
|
8043
|
-
if (operation === 'horizontal') {
|
|
8044
|
-
val = 'x'
|
|
8045
|
-
min = 'minX'
|
|
8046
|
-
dim = 'width'
|
|
8047
|
-
} else {
|
|
8048
|
-
val = 'y'
|
|
8049
|
-
min = 'minY'
|
|
8050
|
-
dim = 'height'
|
|
8051
|
-
}
|
|
7975
|
+
const { val, min, dim } = AXIS[operation]
|
|
8052
7976
|
|
|
8053
7977
|
this.run(() => {
|
|
8054
7978
|
shapeClustersToStretch.forEach(({ shapes, pageBounds }) => {
|
|
8055
|
-
const
|
|
8056
|
-
|
|
7979
|
+
const pageOffset = new Vec()
|
|
7980
|
+
pageOffset[val] = commonBounds[min] - pageBounds[min]
|
|
8057
7981
|
|
|
8058
7982
|
const scaleOrigin = pageBounds.center.clone()
|
|
8059
7983
|
scaleOrigin[val] = commonBounds[min]
|
|
@@ -8063,11 +7987,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
8063
7987
|
|
|
8064
7988
|
for (const shape of shapes) {
|
|
8065
7989
|
// First translate
|
|
8066
|
-
|
|
8067
|
-
shapeLocalOffset.rot(-this.getShapeParentTransform(shape).rotation())
|
|
8068
|
-
shapeLocalOffset.add(shape)
|
|
8069
|
-
const changes = this.getChangesToTranslateShape(shape, shapeLocalOffset)
|
|
8070
|
-
this.updateShape(changes)
|
|
7990
|
+
this.updateShape(this.getChangesToTranslateShapeByPageDelta(shape, pageOffset))
|
|
8071
7991
|
|
|
8072
7992
|
// Then resize
|
|
8073
7993
|
this.resizeShape(shape.id, scale, {
|
|
@@ -8120,7 +8040,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
8120
8040
|
const scale = new Vec(scaleX, scaleY)
|
|
8121
8041
|
|
|
8122
8042
|
shapeClusters.forEach(({ shapes, pageBounds }) => {
|
|
8123
|
-
const
|
|
8043
|
+
const pageOffset = new Vec(
|
|
8124
8044
|
targetBounds.minX -
|
|
8125
8045
|
commonBounds.minX +
|
|
8126
8046
|
(pageBounds.minX - commonBounds.minX) * (scaleX - 1),
|
|
@@ -8134,11 +8054,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
8134
8054
|
|
|
8135
8055
|
for (const shape of shapes) {
|
|
8136
8056
|
// First translate
|
|
8137
|
-
|
|
8138
|
-
shapeLocalOffset.rot(-this.getShapeParentTransform(shape).rotation())
|
|
8139
|
-
shapeLocalOffset.add(shape)
|
|
8140
|
-
const changes = this.getChangesToTranslateShape(shape, shapeLocalOffset)
|
|
8141
|
-
this.updateShape(changes)
|
|
8057
|
+
this.updateShape(this.getChangesToTranslateShapeByPageDelta(shape, pageOffset))
|
|
8142
8058
|
|
|
8143
8059
|
// Then resize
|
|
8144
8060
|
this.resizeShape(shape.id, scale, {
|
|
@@ -8783,6 +8699,11 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
8783
8699
|
|
|
8784
8700
|
const animations: ShapeAnimation[] = []
|
|
8785
8701
|
|
|
8702
|
+
// Snapshot the lock override now: when this animation is started inside
|
|
8703
|
+
// editor.run(..., { ignoreShapeLock: true }), run() restores the flag before any tick
|
|
8704
|
+
// fires, so the final updateShapes below would refuse the locked shape and strand it
|
|
8705
|
+
const ignoreShapeLock = this._shouldIgnoreShapeLock
|
|
8706
|
+
|
|
8786
8707
|
let partial: TLShapePartial | null | undefined, result: ShapeAnimation
|
|
8787
8708
|
for (let i = 0, n = partials.length; i < n; i++) {
|
|
8788
8709
|
partial = partials[i]
|
|
@@ -8791,6 +8712,14 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
8791
8712
|
const shape = this.getShape(partial.id)!
|
|
8792
8713
|
if (!shape) continue
|
|
8793
8714
|
|
|
8715
|
+
// Apply the same lock rule as updateShapes up front: the intermediate frames go through
|
|
8716
|
+
// _updateShapes, which doesn't check locks, so a locked shape would otherwise be moved by
|
|
8717
|
+
// every frame but the last and end up stranded at the penultimate one
|
|
8718
|
+
const unlocks = shape.isLocked && Object.hasOwn(partial, 'isLocked') && !partial.isLocked
|
|
8719
|
+
if (!ignoreShapeLock && !unlocks && this.isShapeOrAncestorLocked(shape)) {
|
|
8720
|
+
continue
|
|
8721
|
+
}
|
|
8722
|
+
|
|
8794
8723
|
result = {
|
|
8795
8724
|
start: structuredClone(shape),
|
|
8796
8725
|
end: applyPartialToRecordWithProps(structuredClone(shape), partial),
|
|
@@ -8811,7 +8740,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
8811
8740
|
if (partialsToUpdate.length) {
|
|
8812
8741
|
// the regular update shapes also removes the shape from
|
|
8813
8742
|
// the animating shapes set
|
|
8814
|
-
this.updateShapes(partialsToUpdate)
|
|
8743
|
+
this.run(() => this.updateShapes(partialsToUpdate), { ignoreShapeLock })
|
|
8815
8744
|
}
|
|
8816
8745
|
|
|
8817
8746
|
this.off('tick', handleTick)
|
|
@@ -8878,18 +8807,16 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
8878
8807
|
}
|
|
8879
8808
|
if (this.getIsReadonly()) return this
|
|
8880
8809
|
|
|
8881
|
-
const ids =
|
|
8882
|
-
typeof shapes[0] === 'string'
|
|
8883
|
-
? (shapes as TLShapeId[])
|
|
8884
|
-
: (shapes.map((s) => (s as TLShape).id) as TLShapeId[])
|
|
8885
|
-
|
|
8886
|
-
if (ids.length <= 1) return this
|
|
8810
|
+
const ids = toShapeIds(shapes)
|
|
8887
8811
|
|
|
8888
8812
|
const shapesToGroup = compact(
|
|
8889
8813
|
(this._shouldIgnoreShapeLock ? ids : this._getUnlockedShapeIds(ids)).map((id) =>
|
|
8890
8814
|
this.getShape(id)
|
|
8891
8815
|
)
|
|
8892
8816
|
)
|
|
8817
|
+
// Re-check after the lock filter: Box.Common of nothing is not a valid box and would throw
|
|
8818
|
+
if (shapesToGroup.length <= 1) return this
|
|
8819
|
+
|
|
8893
8820
|
const sortedShapeIds = shapesToGroup.sort(sortByIndex).map((s) => s.id)
|
|
8894
8821
|
const childBounds = compact(shapesToGroup.map((shape) => this.getShapePageBounds(shape)))
|
|
8895
8822
|
const pageBounds = Box.Common(childBounds)
|
|
@@ -8902,11 +8829,15 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
8902
8829
|
|
|
8903
8830
|
const parentId = this.findCommonAncestor(shapesToGroup) ?? this.getCurrentPageId()
|
|
8904
8831
|
|
|
8905
|
-
//
|
|
8906
|
-
|
|
8832
|
+
// createShapes bails out when the page is full, so check first; otherwise the shapes get
|
|
8833
|
+
// reparented into a group that was never created and vanish from the page
|
|
8834
|
+
if (!this.canCreateShapes([groupId])) {
|
|
8835
|
+
alertMaxShapes(this)
|
|
8836
|
+
return this
|
|
8837
|
+
}
|
|
8907
8838
|
|
|
8908
|
-
// If
|
|
8909
|
-
if (!this.isIn('select.idle')) {
|
|
8839
|
+
// If the select tool is mid-interaction, cancel it (get back to idle) before grouping
|
|
8840
|
+
if (this.isIn('select') && !this.isIn('select.idle')) {
|
|
8910
8841
|
this.cancel()
|
|
8911
8842
|
}
|
|
8912
8843
|
|
|
@@ -8960,10 +8891,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
8960
8891
|
if (this.getIsReadonly()) return this
|
|
8961
8892
|
|
|
8962
8893
|
const { select = true } = opts
|
|
8963
|
-
const ids =
|
|
8964
|
-
typeof shapes[0] === 'string'
|
|
8965
|
-
? (shapes as TLShapeId[])
|
|
8966
|
-
: (shapes as TLShape[]).map((s) => s.id)
|
|
8894
|
+
const ids = toShapeIds(shapes)
|
|
8967
8895
|
|
|
8968
8896
|
const shapesToUngroup = compact(
|
|
8969
8897
|
(this._shouldIgnoreShapeLock ? ids : this._getUnlockedShapeIds(ids)).map((id) =>
|
|
@@ -8973,9 +8901,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
8973
8901
|
|
|
8974
8902
|
if (shapesToUngroup.length === 0) return this
|
|
8975
8903
|
|
|
8976
|
-
//
|
|
8977
|
-
if (this.
|
|
8978
|
-
if (!this.isIn('select.idle')) {
|
|
8904
|
+
// If the select tool is mid-interaction, cancel it (get back to idle) before ungrouping
|
|
8905
|
+
if (this.isIn('select') && !this.isIn('select.idle')) {
|
|
8979
8906
|
this.cancel()
|
|
8980
8907
|
}
|
|
8981
8908
|
|
|
@@ -8998,10 +8925,12 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
8998
8925
|
if (groups.length === 0) return this
|
|
8999
8926
|
|
|
9000
8927
|
this.run(() => {
|
|
9001
|
-
let group: TLGroupShape
|
|
9002
|
-
|
|
9003
8928
|
for (let i = 0, n = groups.length; i < n; i++) {
|
|
9004
|
-
group
|
|
8929
|
+
// Re-read the group: ungrouping an outer group earlier in this loop reparents the
|
|
8930
|
+
// inner ones, and the stale parentId would send their children into a group that's
|
|
8931
|
+
// about to be deleted
|
|
8932
|
+
const group = this.getShape<TLGroupShape>(groups[i].id)
|
|
8933
|
+
if (!group) continue
|
|
9005
8934
|
const childIds = this.getSortedChildIdsForParent(group.id)
|
|
9006
8935
|
|
|
9007
8936
|
for (let j = 0, n = childIds.length; j < n; j++) {
|
|
@@ -9128,7 +9057,9 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
9128
9057
|
|
|
9129
9058
|
/** @internal */
|
|
9130
9059
|
private _getUnlockedShapeIds(ids: TLShapeId[]): TLShapeId[] {
|
|
9131
|
-
|
|
9060
|
+
// Match updateShapes, which also refuses shapes under a locked ancestor; otherwise a child
|
|
9061
|
+
// of a locked frame can't be moved but can still be deleted or duplicated
|
|
9062
|
+
return ids.filter((id) => !this.isShapeOrAncestorLocked(id))
|
|
9132
9063
|
}
|
|
9133
9064
|
|
|
9134
9065
|
/**
|
|
@@ -9152,8 +9083,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
9152
9083
|
throw Error('Editor.deleteShapes: must provide an array of shapes or shapeIds')
|
|
9153
9084
|
}
|
|
9154
9085
|
|
|
9155
|
-
const shapeIds =
|
|
9156
|
-
typeof _ids[0] === 'string' ? (_ids as TLShapeId[]) : (_ids as TLShape[]).map((s) => s.id)
|
|
9086
|
+
const shapeIds = toShapeIds(_ids)
|
|
9157
9087
|
|
|
9158
9088
|
// Normally we don't want to delete locked shapes, but if the force option is set, we'll delete them anyway
|
|
9159
9089
|
const shapeIdsToDelete = this._shouldIgnoreShapeLock
|
|
@@ -9627,10 +9557,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
9627
9557
|
*/
|
|
9628
9558
|
getContentFromCurrentPage(shapes: TLShapeId[] | TLShape[]): TLContent | undefined {
|
|
9629
9559
|
// todo: make this work with any page, not just the current page
|
|
9630
|
-
const ids =
|
|
9631
|
-
typeof shapes[0] === 'string'
|
|
9632
|
-
? (shapes as TLShapeId[])
|
|
9633
|
-
: (shapes as TLShape[]).map((s) => s.id)
|
|
9560
|
+
const ids = toShapeIds(shapes)
|
|
9634
9561
|
|
|
9635
9562
|
if (!ids) return
|
|
9636
9563
|
if (ids.length === 0) return
|
|
@@ -9717,21 +9644,34 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
9717
9644
|
!asset.props.src?.startsWith('data:video') &&
|
|
9718
9645
|
!asset.props.src?.startsWith('http')
|
|
9719
9646
|
) {
|
|
9720
|
-
|
|
9721
|
-
|
|
9722
|
-
|
|
9723
|
-
|
|
9724
|
-
|
|
9725
|
-
|
|
9726
|
-
|
|
9727
|
-
|
|
9728
|
-
|
|
9729
|
-
|
|
9730
|
-
|
|
9731
|
-
|
|
9732
|
-
|
|
9733
|
-
|
|
9647
|
+
// If the asset can't be inlined (unresolvable src, fetch failure), fall through and
|
|
9648
|
+
// keep the original record; dropping it leaves the pasted shapes pointing at an
|
|
9649
|
+
// asset that doesn't exist
|
|
9650
|
+
try {
|
|
9651
|
+
const objectUrl = await this.store.props.assets.resolve(asset, {
|
|
9652
|
+
screenScale: 1,
|
|
9653
|
+
steppedScreenScale: 1,
|
|
9654
|
+
dpr: 1,
|
|
9655
|
+
networkEffectiveType: null,
|
|
9656
|
+
shouldResolveToOriginal: true,
|
|
9657
|
+
})
|
|
9658
|
+
if (objectUrl) {
|
|
9659
|
+
// fetch resolves on 4xx/5xx, so without this check a 404 error page would be
|
|
9660
|
+
// inlined as a data:text/html src
|
|
9661
|
+
const response = await fetch(objectUrl)
|
|
9662
|
+
if (response.ok) {
|
|
9663
|
+
const assetWithDataUrl = structuredClone(asset as TLImageAsset | TLVideoAsset)
|
|
9664
|
+
assetWithDataUrl.props.src = await FileHelpers.blobToDataUrl(await response.blob())
|
|
9665
|
+
assets.push(assetWithDataUrl)
|
|
9666
|
+
return
|
|
9667
|
+
}
|
|
9668
|
+
console.warn(`Could not inline asset ${asset.id}: fetch returned ${response.status}`)
|
|
9669
|
+
}
|
|
9670
|
+
} catch (err) {
|
|
9671
|
+
console.warn(`Could not inline asset ${asset.id}`, err)
|
|
9672
|
+
}
|
|
9734
9673
|
}
|
|
9674
|
+
assets.push(asset)
|
|
9735
9675
|
})
|
|
9736
9676
|
)
|
|
9737
9677
|
content.assets = assets
|
|
@@ -10153,12 +10093,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
10153
10093
|
* @public
|
|
10154
10094
|
*/
|
|
10155
10095
|
async getSvgElement(shapes: TLShapeId[] | TLShape[], opts: TLSvgExportOptions = {}) {
|
|
10156
|
-
const ids =
|
|
10157
|
-
shapes.length === 0
|
|
10158
|
-
? this.getCurrentPageShapeIdsSorted()
|
|
10159
|
-
: typeof shapes[0] === 'string'
|
|
10160
|
-
? (shapes as TLShapeId[])
|
|
10161
|
-
: (shapes as TLShape[]).map((s) => s.id)
|
|
10096
|
+
const ids = shapes.length === 0 ? this.getCurrentPageShapeIdsSorted() : toShapeIds(shapes)
|
|
10162
10097
|
|
|
10163
10098
|
if (ids.length === 0) return undefined
|
|
10164
10099
|
|
|
@@ -11485,9 +11420,11 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
11485
11420
|
break
|
|
11486
11421
|
}
|
|
11487
11422
|
case 'keyboard': {
|
|
11488
|
-
//
|
|
11489
|
-
|
|
11490
|
-
|
|
11423
|
+
// Left and right modifier keys are the same key to us. `inputs.keys` stores
|
|
11424
|
+
// `code`, so normalize that: a `ShiftRight` left as-is would never match the
|
|
11425
|
+
// `ShiftLeft` that nudging checks or that `_releaseShiftKey` clears.
|
|
11426
|
+
if (info.code === 'ShiftRight') info.code = 'ShiftLeft'
|
|
11427
|
+
if (info.code === 'AltRight') info.code = 'AltLeft'
|
|
11491
11428
|
if (info.code === 'ControlRight') info.code = 'ControlLeft'
|
|
11492
11429
|
if (info.code === 'MetaRight') info.code = 'MetaLeft'
|
|
11493
11430
|
|
|
@@ -11693,6 +11630,16 @@ function pushShapeWithDescendants(editor: Editor, id: TLShapeId, result: TLShape
|
|
|
11693
11630
|
}
|
|
11694
11631
|
}
|
|
11695
11632
|
|
|
11633
|
+
/**
|
|
11634
|
+
* When `shapes` already holds ids this returns that same array, not a copy, so a caller that
|
|
11635
|
+
* mutates the result (for example to sort it) must copy it first.
|
|
11636
|
+
*/
|
|
11637
|
+
function toShapeIds(shapes: TLShapeId[] | TLShape[]): TLShapeId[] {
|
|
11638
|
+
return typeof shapes[0] === 'string'
|
|
11639
|
+
? (shapes as TLShapeId[])
|
|
11640
|
+
: (shapes as TLShape[]).map((shape) => shape.id)
|
|
11641
|
+
}
|
|
11642
|
+
|
|
11696
11643
|
/**
|
|
11697
11644
|
* Run `callback` in a world where all bindings from the shapes in `shapeIds` to shapes not in
|
|
11698
11645
|
* `shapeIds` are removed. This is useful when you want to duplicate/copy shapes without worrying
|
|
@@ -11730,7 +11677,7 @@ function withIsolatedShapes<T>(
|
|
|
11730
11677
|
}
|
|
11731
11678
|
}
|
|
11732
11679
|
|
|
11733
|
-
editor.
|
|
11680
|
+
editor._deleteBindings([...bindingsToRemove], { isolateShapes: true })
|
|
11734
11681
|
|
|
11735
11682
|
try {
|
|
11736
11683
|
result = Result.ok(callback(bindingsWithBoth))
|