@tldraw/editor 5.4.0-canary.042da660fa66 → 5.4.0-canary.059d049864ed
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 +31 -1
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/lib/components/MenuClickCapture.js +6 -8
- package/dist-cjs/lib/components/MenuClickCapture.js.map +2 -2
- package/dist-cjs/lib/components/default-components/CanvasOverlays.js +1 -3
- package/dist-cjs/lib/components/default-components/CanvasOverlays.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js +1 -1
- package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js.map +1 -1
- package/dist-cjs/lib/config/TLUserPreferences.js +6 -2
- package/dist-cjs/lib/config/TLUserPreferences.js.map +2 -2
- package/dist-cjs/lib/config/createTLStore.js +1 -1
- package/dist-cjs/lib/config/createTLStore.js.map +2 -2
- package/dist-cjs/lib/editor/Editor.js +139 -268
- package/dist-cjs/lib/editor/Editor.js.map +2 -2
- package/dist-cjs/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.js +8 -8
- package/dist-cjs/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/FontManager/FontManager.js +2 -1
- package/dist-cjs/lib/editor/managers/FontManager/FontManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js +79 -6
- package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/SnapManager/BoundsSnaps.js +0 -2
- package/dist-cjs/lib/editor/managers/SnapManager/BoundsSnaps.js.map +2 -2
- package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js +1 -0
- package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
- package/dist-cjs/lib/editor/overlays/OverlayManager.js +3 -7
- package/dist-cjs/lib/editor/overlays/OverlayManager.js.map +2 -2
- package/dist-cjs/lib/editor/overlays/strokeShapeIndicators.js +0 -1
- package/dist-cjs/lib/editor/overlays/strokeShapeIndicators.js.map +2 -2
- package/dist-cjs/lib/editor/shapes/BaseFrameLikeShapeUtil.js +3 -1
- package/dist-cjs/lib/editor/shapes/BaseFrameLikeShapeUtil.js.map +2 -2
- package/dist-cjs/lib/editor/shapes/shared/getPerfectDashProps.js +1 -0
- package/dist-cjs/lib/editor/shapes/shared/getPerfectDashProps.js.map +2 -2
- package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js +1 -1
- package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js.map +2 -2
- package/dist-cjs/lib/editor/tools/StateNode.js +3 -2
- package/dist-cjs/lib/editor/tools/StateNode.js.map +2 -2
- package/dist-cjs/lib/globals/menus.js +9 -4
- package/dist-cjs/lib/globals/menus.js.map +2 -2
- package/dist-cjs/lib/hooks/useCanvasEvents.js +2 -0
- package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/useDocumentEvents.js +9 -7
- package/dist-cjs/lib/hooks/useDocumentEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/useLocalStore.js +10 -6
- package/dist-cjs/lib/hooks/useLocalStore.js.map +2 -2
- package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js +1 -0
- package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/usePeerIds.js +2 -1
- package/dist-cjs/lib/hooks/usePeerIds.js.map +2 -2
- package/dist-cjs/lib/primitives/Box.js +2 -1
- package/dist-cjs/lib/primitives/Box.js.map +2 -2
- package/dist-cjs/lib/primitives/Vec.js +4 -3
- package/dist-cjs/lib/primitives/Vec.js.map +2 -2
- package/dist-cjs/lib/primitives/utils.js +2 -1
- package/dist-cjs/lib/primitives/utils.js.map +2 -2
- package/dist-cjs/lib/utils/rotation.js +1 -9
- package/dist-cjs/lib/utils/rotation.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 +31 -1
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/lib/components/MenuClickCapture.mjs +6 -8
- package/dist-esm/lib/components/MenuClickCapture.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/CanvasOverlays.mjs +1 -3
- package/dist-esm/lib/components/default-components/CanvasOverlays.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs +1 -1
- package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs.map +1 -1
- package/dist-esm/lib/config/TLUserPreferences.mjs +6 -2
- package/dist-esm/lib/config/TLUserPreferences.mjs.map +2 -2
- package/dist-esm/lib/config/createTLStore.mjs +1 -1
- package/dist-esm/lib/config/createTLStore.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +147 -269
- package/dist-esm/lib/editor/Editor.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.mjs +9 -9
- package/dist-esm/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs +2 -1
- package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs +81 -8
- package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/SnapManager/BoundsSnaps.mjs +0 -2
- package/dist-esm/lib/editor/managers/SnapManager/BoundsSnaps.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +1 -0
- package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
- package/dist-esm/lib/editor/overlays/OverlayManager.mjs +3 -7
- package/dist-esm/lib/editor/overlays/OverlayManager.mjs.map +2 -2
- package/dist-esm/lib/editor/overlays/strokeShapeIndicators.mjs +0 -1
- package/dist-esm/lib/editor/overlays/strokeShapeIndicators.mjs.map +2 -2
- package/dist-esm/lib/editor/shapes/BaseFrameLikeShapeUtil.mjs +3 -1
- package/dist-esm/lib/editor/shapes/BaseFrameLikeShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/editor/shapes/shared/getPerfectDashProps.mjs +1 -0
- package/dist-esm/lib/editor/shapes/shared/getPerfectDashProps.mjs.map +2 -2
- package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs +1 -1
- package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs.map +2 -2
- package/dist-esm/lib/editor/tools/StateNode.mjs +3 -2
- package/dist-esm/lib/editor/tools/StateNode.mjs.map +2 -2
- package/dist-esm/lib/globals/menus.mjs +9 -4
- package/dist-esm/lib/globals/menus.mjs.map +2 -2
- package/dist-esm/lib/hooks/useCanvasEvents.mjs +2 -0
- package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/useDocumentEvents.mjs +9 -7
- package/dist-esm/lib/hooks/useDocumentEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/useLocalStore.mjs +11 -7
- package/dist-esm/lib/hooks/useLocalStore.mjs.map +2 -2
- package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs +1 -0
- package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/usePeerIds.mjs +2 -1
- package/dist-esm/lib/hooks/usePeerIds.mjs.map +2 -2
- package/dist-esm/lib/primitives/Box.mjs +2 -1
- package/dist-esm/lib/primitives/Box.mjs.map +2 -2
- package/dist-esm/lib/primitives/Vec.mjs +4 -3
- package/dist-esm/lib/primitives/Vec.mjs.map +2 -2
- package/dist-esm/lib/primitives/utils.mjs +2 -1
- package/dist-esm/lib/primitives/utils.mjs.map +2 -2
- package/dist-esm/lib/utils/rotation.mjs +1 -9
- package/dist-esm/lib/utils/rotation.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/components/MenuClickCapture.tsx +19 -18
- package/src/lib/components/default-components/CanvasOverlays.tsx +1 -3
- package/src/lib/components/default-components/DefaultErrorFallback.tsx +1 -1
- package/src/lib/config/TLUserPreferences.test.ts +18 -1
- package/src/lib/config/TLUserPreferences.ts +8 -4
- package/src/lib/config/createTLStore.ts +1 -1
- package/src/lib/editor/Editor.ts +196 -341
- package/src/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.ts +9 -9
- package/src/lib/editor/managers/FontManager/FontManager.ts +2 -1
- package/src/lib/editor/managers/InputsManager/InputsManager.test.ts +199 -3
- package/src/lib/editor/managers/InputsManager/InputsManager.ts +128 -9
- package/src/lib/editor/managers/SnapManager/BoundsSnaps.ts +0 -2
- package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +1 -0
- package/src/lib/editor/overlays/OverlayManager.ts +3 -7
- package/src/lib/editor/overlays/strokeShapeIndicators.ts +0 -1
- package/src/lib/editor/shapes/BaseFrameLikeShapeUtil.tsx +5 -1
- package/src/lib/editor/shapes/shared/getPerfectDashProps.ts +3 -0
- package/src/lib/editor/tools/BaseBoxShapeTool/children/Pointing.ts +4 -1
- package/src/lib/editor/tools/StateNode.test.ts +64 -0
- package/src/lib/editor/tools/StateNode.ts +12 -2
- package/src/lib/globals/menus.test.ts +33 -0
- package/src/lib/globals/menus.ts +11 -4
- package/src/lib/hooks/useCanvasEvents.ts +2 -0
- package/src/lib/hooks/useDocumentEvents.ts +14 -7
- package/src/lib/hooks/useLocalStore.ts +16 -8
- package/src/lib/hooks/usePassThroughWheelEvents.ts +3 -0
- package/src/lib/hooks/usePeerIds.ts +2 -1
- package/src/lib/primitives/Box.test.ts +9 -1
- package/src/lib/primitives/Box.ts +3 -1
- package/src/lib/primitives/Vec.test.ts +10 -0
- package/src/lib/primitives/Vec.ts +4 -4
- package/src/lib/primitives/utils.test.ts +10 -1
- package/src/lib/primitives/utils.ts +4 -1
- package/src/lib/test/useCanvasEvents.test.tsx +74 -0
- package/src/lib/test/useDocumentEvents.test.tsx +31 -0
- package/src/lib/test/usePassThroughWheelEvents.test.tsx +25 -2
- package/src/lib/utils/rotation.ts +4 -12
- package/src/version.ts +3 -3
package/src/lib/editor/Editor.ts
CHANGED
|
@@ -72,6 +72,7 @@ import {
|
|
|
72
72
|
Result,
|
|
73
73
|
ZERO_INDEX_KEY,
|
|
74
74
|
annotateError,
|
|
75
|
+
areArraysShallowEqual,
|
|
75
76
|
assert,
|
|
76
77
|
assertExists,
|
|
77
78
|
bind,
|
|
@@ -129,7 +130,13 @@ import { Geometry2d } from '../primitives/geometry/Geometry2d'
|
|
|
129
130
|
import { Group2d } from '../primitives/geometry/Group2d'
|
|
130
131
|
import { intersectPolygonPolygon } from '../primitives/intersect'
|
|
131
132
|
import { Mat, MatLike } from '../primitives/Mat'
|
|
132
|
-
import {
|
|
133
|
+
import {
|
|
134
|
+
HALF_PI,
|
|
135
|
+
approximately,
|
|
136
|
+
areAnglesCompatible,
|
|
137
|
+
clamp,
|
|
138
|
+
pointInPolygon,
|
|
139
|
+
} from '../primitives/utils'
|
|
133
140
|
import { Vec, VecLike } from '../primitives/Vec'
|
|
134
141
|
import { areShapesContentEqual } from '../utils/areShapesContentEqual'
|
|
135
142
|
import { dataUrlToFile } from '../utils/assets'
|
|
@@ -187,6 +194,7 @@ import { TLHistoryBatchOptions } from './types/history-types'
|
|
|
187
194
|
import {
|
|
188
195
|
OptionalKeys,
|
|
189
196
|
RequiredKeys,
|
|
197
|
+
TLCameraConstraints,
|
|
190
198
|
TLCameraMoveOptions,
|
|
191
199
|
TLCameraOptions,
|
|
192
200
|
TLGetShapeAtPointOptions,
|
|
@@ -545,6 +553,14 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
545
553
|
nextPageState.editingShapeId = null
|
|
546
554
|
}
|
|
547
555
|
|
|
556
|
+
if (
|
|
557
|
+
prevPageState.croppingShapeId &&
|
|
558
|
+
shapesNoLongerInPage.has(prevPageState.croppingShapeId)
|
|
559
|
+
) {
|
|
560
|
+
if (!nextPageState) nextPageState = { ...prevPageState }
|
|
561
|
+
nextPageState.croppingShapeId = null
|
|
562
|
+
}
|
|
563
|
+
|
|
548
564
|
const hintingShapeIds = prevPageState.hintingShapeIds.filter(
|
|
549
565
|
(id) => !shapesNoLongerInPage.has(id)
|
|
550
566
|
)
|
|
@@ -830,10 +846,11 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
830
846
|
|
|
831
847
|
if (prev?.selectedShapeIds !== next?.selectedShapeIds) {
|
|
832
848
|
// ensure that descendants and ancestors are not selected at the same time
|
|
849
|
+
const selectedShapeIds = new Set(next.selectedShapeIds)
|
|
833
850
|
const filtered = next.selectedShapeIds.filter((id) => {
|
|
834
851
|
let parentId = this.getShape(id)?.parentId
|
|
835
852
|
while (isShapeId(parentId)) {
|
|
836
|
-
if (
|
|
853
|
+
if (selectedShapeIds.has(parentId)) {
|
|
837
854
|
return false
|
|
838
855
|
}
|
|
839
856
|
parentId = this.getShape(parentId)?.parentId
|
|
@@ -938,7 +955,11 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
938
955
|
const mode = this.store.props.collaboration.mode
|
|
939
956
|
this.disposables.add(
|
|
940
957
|
react('update collaboration mode', () => {
|
|
941
|
-
|
|
958
|
+
const isReadonly = mode.get() === 'readonly'
|
|
959
|
+
// only track `mode`, and keep the sync out of the user's undo history
|
|
960
|
+
unsafe__withoutCapture(() =>
|
|
961
|
+
this._updateInstanceState({ isReadonly }, { history: 'ignore' })
|
|
962
|
+
)
|
|
942
963
|
})
|
|
943
964
|
)
|
|
944
965
|
}
|
|
@@ -1390,7 +1411,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
1390
1411
|
getShapeUtil(arg: string | { type: string }) {
|
|
1391
1412
|
const type = typeof arg === 'string' ? arg : arg.type
|
|
1392
1413
|
const shapeUtil = getOwnProperty(this.shapeUtils, type)
|
|
1393
|
-
|
|
1414
|
+
// hot path: avoid building the message (and the assert wrapper) on every successful call
|
|
1415
|
+
if (!shapeUtil) throw new Error(`No shape util found for type "${type}"`)
|
|
1394
1416
|
return shapeUtil
|
|
1395
1417
|
}
|
|
1396
1418
|
|
|
@@ -2467,9 +2489,9 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
2467
2489
|
const selectedShapes = this.getSelectedShapes()
|
|
2468
2490
|
if (!selectedShapes.length) return
|
|
2469
2491
|
const selectedShape = selectedShapes[0]
|
|
2470
|
-
const children =
|
|
2471
|
-
.map((id) => this.getShape(id))
|
|
2472
|
-
|
|
2492
|
+
const children = compact(
|
|
2493
|
+
this.getSortedChildIdsForParent(selectedShape.id).map((id) => this.getShape(id))
|
|
2494
|
+
)
|
|
2473
2495
|
const sortedChildren = this._getShapesInReadingOrder(children)
|
|
2474
2496
|
if (sortedChildren.length === 0) return
|
|
2475
2497
|
this._selectShapesAndZoom([sortedChildren[0].id])
|
|
@@ -2570,20 +2592,14 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
2570
2592
|
* @internal
|
|
2571
2593
|
*/
|
|
2572
2594
|
getShapesSharedRotation(shapeIds: TLShapeId[]) {
|
|
2573
|
-
let foundFirst = false // annoying but we can't use an i===0 check because we need to skip over undefineds
|
|
2574
2595
|
let rotation = 0
|
|
2575
2596
|
for (let i = 0, n = shapeIds.length; i < n; i++) {
|
|
2576
|
-
const
|
|
2577
|
-
if (
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
}
|
|
2583
|
-
} else {
|
|
2584
|
-
// First rotation found
|
|
2585
|
-
foundFirst = true
|
|
2586
|
-
rotation = pageTransform.rotation()
|
|
2597
|
+
const pageRotation = this.getShapePageTransform(shapeIds[i]).rotation()
|
|
2598
|
+
if (i === 0) {
|
|
2599
|
+
rotation = pageRotation
|
|
2600
|
+
} else if (pageRotation !== rotation) {
|
|
2601
|
+
// There are at least 2 different rotations, so the common rotation is zero
|
|
2602
|
+
return 0
|
|
2587
2603
|
}
|
|
2588
2604
|
}
|
|
2589
2605
|
|
|
@@ -2615,7 +2631,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
2615
2631
|
|
|
2616
2632
|
if (shapeIds.length === 1) {
|
|
2617
2633
|
const bounds = this.getShapeGeometry(shapeIds[0]).bounds.clone()
|
|
2618
|
-
const pageTransform = this.getShapePageTransform(shapeIds[0])
|
|
2634
|
+
const pageTransform = this.getShapePageTransform(shapeIds[0])
|
|
2619
2635
|
bounds.point = pageTransform.applyToPoint(bounds.point)
|
|
2620
2636
|
return bounds
|
|
2621
2637
|
}
|
|
@@ -2623,11 +2639,9 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
2623
2639
|
// need to 'un-rotate' all the outlines of the existing nodes so we can fit them inside a box
|
|
2624
2640
|
const boxFromRotatedVertices = Box.FromPoints(
|
|
2625
2641
|
shapeIds
|
|
2626
|
-
.flatMap((id) =>
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
return pageTransform.applyToPoints(this.getShapeGeometry(id).bounds.corners)
|
|
2630
|
-
})
|
|
2642
|
+
.flatMap((id) =>
|
|
2643
|
+
this.getShapePageTransform(id).applyToPoints(this.getShapeGeometry(id).bounds.corners)
|
|
2644
|
+
)
|
|
2631
2645
|
.map((p) => p.rot(-selectionRotation))
|
|
2632
2646
|
)
|
|
2633
2647
|
// now position box so that it's top-left corner is in the right place
|
|
@@ -2654,9 +2668,16 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
2654
2668
|
@computed getSelectionRotatedScreenBounds(): Box | undefined {
|
|
2655
2669
|
const bounds = this.getSelectionRotatedPageBounds()
|
|
2656
2670
|
if (!bounds) return undefined
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2671
|
+
// Don't use pageToScreen here: it reads the screen bounds without capturing them, so this
|
|
2672
|
+
// computed would never invalidate when the container moves
|
|
2673
|
+
const screenBounds = this.getViewportScreenBounds()
|
|
2674
|
+
const { x: cx, y: cy, z: zoom } = this.getCamera()
|
|
2675
|
+
return new Box(
|
|
2676
|
+
(bounds.x + cx) * zoom + screenBounds.x,
|
|
2677
|
+
(bounds.y + cy) * zoom + screenBounds.y,
|
|
2678
|
+
bounds.width * zoom,
|
|
2679
|
+
bounds.height * zoom
|
|
2680
|
+
)
|
|
2660
2681
|
}
|
|
2661
2682
|
|
|
2662
2683
|
// Focus Group
|
|
@@ -2796,57 +2817,38 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
2796
2817
|
setEditingShape(shape: TLShapeId | TLShape | null): this {
|
|
2797
2818
|
const id = typeof shape === 'string' ? shape : (shape?.id ?? null)
|
|
2798
2819
|
|
|
2799
|
-
|
|
2800
|
-
|
|
2820
|
+
// id was provided but the next editing shape was not editable or didn't exist, so do nothing
|
|
2821
|
+
if (id && !this.canEditShape(id)) return this
|
|
2822
|
+
|
|
2823
|
+
this.run(() => {
|
|
2824
|
+
// Clean up the previous editing shape. This runs outside the history-ignored batch below,
|
|
2825
|
+
// otherwise document changes made by onEditEnd (e.g. deleting an empty text shape) are
|
|
2826
|
+
// never recorded and leave a phantom undo entry whose redo resurrects the shape.
|
|
2827
|
+
const prevEditingShapeId = this.getEditingShapeId()
|
|
2828
|
+
if (prevEditingShapeId) {
|
|
2829
|
+
const prevEditingShape = this.getShape(prevEditingShapeId)
|
|
2830
|
+
if (prevEditingShape) {
|
|
2831
|
+
this.getShapeUtil(prevEditingShape).onEditEnd?.(prevEditingShape)
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
|
|
2801
2835
|
this.run(
|
|
2802
2836
|
() => {
|
|
2803
|
-
// Clean up the previous editing shape
|
|
2804
|
-
const prevEditingShapeId = this.getEditingShapeId()
|
|
2805
|
-
if (prevEditingShapeId) {
|
|
2806
|
-
const prevEditingShape = this.getShape(prevEditingShapeId)
|
|
2807
|
-
if (prevEditingShape) {
|
|
2808
|
-
this.getShapeUtil(prevEditingShape).onEditEnd?.(prevEditingShape)
|
|
2809
|
-
}
|
|
2810
|
-
}
|
|
2811
|
-
|
|
2812
2837
|
// Clean up the editing shape state and rich text editor
|
|
2813
2838
|
this._updateCurrentPageState({ editingShapeId: null })
|
|
2814
2839
|
this._currentRichTextEditor.set(null)
|
|
2815
|
-
},
|
|
2816
|
-
{ history: 'ignore' }
|
|
2817
|
-
)
|
|
2818
|
-
|
|
2819
|
-
return this
|
|
2820
|
-
}
|
|
2821
2840
|
|
|
2822
|
-
|
|
2823
|
-
if (!this.canEditShape(id)) return this
|
|
2824
|
-
|
|
2825
|
-
// id was provided and the next editing shape is editable, so set the rich text editor to null
|
|
2826
|
-
this.run(
|
|
2827
|
-
() => {
|
|
2828
|
-
// Clean up the previous editing shape
|
|
2829
|
-
const prevEditingShapeId = this.getEditingShapeId()
|
|
2830
|
-
if (prevEditingShapeId) {
|
|
2831
|
-
const prevEditingShape = this.getShape(prevEditingShapeId)
|
|
2832
|
-
if (prevEditingShape) {
|
|
2833
|
-
this.getShapeUtil(prevEditingShape).onEditEnd?.(prevEditingShape)
|
|
2834
|
-
}
|
|
2835
|
-
}
|
|
2836
|
-
|
|
2837
|
-
// Clean up the editing shape state and rich text editor
|
|
2838
|
-
this._updateCurrentPageState({ editingShapeId: null })
|
|
2839
|
-
this._currentRichTextEditor.set(null)
|
|
2841
|
+
if (!id) return
|
|
2840
2842
|
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
this._updateCurrentPageState({ editingShapeId: id })
|
|
2843
|
+
this.select(id)
|
|
2844
|
+
this._updateCurrentPageState({ editingShapeId: id })
|
|
2844
2845
|
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2846
|
+
const nextEditingShape = this.getShape(id)! // shape should be there because canEditShape checked it. Possible small chance that onEditEnd deleted it?
|
|
2847
|
+
this.getShapeUtil(nextEditingShape).onEditStart?.(nextEditingShape)
|
|
2848
|
+
},
|
|
2849
|
+
{ history: 'ignore' }
|
|
2850
|
+
)
|
|
2851
|
+
})
|
|
2850
2852
|
|
|
2851
2853
|
return this
|
|
2852
2854
|
}
|
|
@@ -3011,26 +3013,17 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
3011
3013
|
* @public
|
|
3012
3014
|
*/
|
|
3013
3015
|
setErasingShapes(shapes: TLShapeId[] | TLShape[]): this {
|
|
3016
|
+
// copy before sorting: the caller may pass a store-owned (frozen) array
|
|
3014
3017
|
const ids =
|
|
3015
3018
|
typeof shapes[0] === 'string'
|
|
3016
|
-
? (shapes as TLShapeId[])
|
|
3019
|
+
? (shapes as TLShapeId[]).slice()
|
|
3017
3020
|
: (shapes as TLShape[]).map((shape) => shape.id)
|
|
3018
3021
|
ids.sort() // sort the incoming ids
|
|
3019
3022
|
const erasingShapeIds = this.getErasingShapeIds()
|
|
3020
3023
|
this.run(
|
|
3021
3024
|
() => {
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
// presuming the current ids are also sorted, check each item to see if it's the same;
|
|
3025
|
-
// if we find any unequal, then we know the new ids are different.
|
|
3026
|
-
for (let i = 0; i < ids.length; i++) {
|
|
3027
|
-
if (ids[i] !== erasingShapeIds[i]) {
|
|
3028
|
-
this._updateCurrentPageState({ erasingShapeIds: ids })
|
|
3029
|
-
break
|
|
3030
|
-
}
|
|
3031
|
-
}
|
|
3032
|
-
} else {
|
|
3033
|
-
// if the ids are a different length, then we know they're different.
|
|
3025
|
+
// the current ids are also sorted, so a shallow comparison tells us whether they changed
|
|
3026
|
+
if (!areArraysShallowEqual(ids, erasingShapeIds)) {
|
|
3034
3027
|
this._updateCurrentPageState({ erasingShapeIds: ids })
|
|
3035
3028
|
}
|
|
3036
3029
|
},
|
|
@@ -3067,6 +3060,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
3067
3060
|
if (!_shape) return false
|
|
3068
3061
|
const util = this.getShapeUtil(_shape)
|
|
3069
3062
|
if (!util.canCrop(_shape)) return false
|
|
3063
|
+
if (this.getIsReadonly()) return false
|
|
3070
3064
|
if (this.isShapeOrAncestorLocked(_shape)) return false
|
|
3071
3065
|
return true
|
|
3072
3066
|
}
|
|
@@ -3146,11 +3140,13 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
3146
3140
|
const collaborators = this.getCollaborators()
|
|
3147
3141
|
let leaderPresence = null as null | TLInstancePresence
|
|
3148
3142
|
while (targetUserId && !visited.includes(targetUserId)) {
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3143
|
+
const nextPresence = collaborators.find((c) => c.userId === targetUserId)
|
|
3144
|
+
// Stop at the last resolvable presence, otherwise a leader whose own leader has left
|
|
3145
|
+
// (or whose presence hasn't arrived yet) can't be followed at all
|
|
3146
|
+
if (!nextPresence) break
|
|
3147
|
+
leaderPresence = nextPresence
|
|
3148
|
+
targetUserId = nextPresence.followingUserId ?? null
|
|
3149
|
+
visited.push(nextPresence.userId)
|
|
3154
3150
|
}
|
|
3155
3151
|
return leaderPresence
|
|
3156
3152
|
}
|
|
@@ -3268,44 +3264,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
3268
3264
|
*
|
|
3269
3265
|
* @public */
|
|
3270
3266
|
getInitialZoom() {
|
|
3271
|
-
|
|
3272
|
-
// If no camera constraints are provided, the default zoom is 100%
|
|
3273
|
-
if (!cameraOptions.constraints) return 1
|
|
3274
|
-
|
|
3275
|
-
// When defaultZoom is default, the default zoom is 100%
|
|
3276
|
-
if (cameraOptions.constraints.initialZoom === 'default') return 1
|
|
3277
|
-
|
|
3278
|
-
const { zx, zy } = getCameraFitXFitY(this, cameraOptions)
|
|
3279
|
-
|
|
3280
|
-
switch (cameraOptions.constraints.initialZoom) {
|
|
3281
|
-
case 'fit-min': {
|
|
3282
|
-
return Math.max(zx, zy)
|
|
3283
|
-
}
|
|
3284
|
-
case 'fit-max': {
|
|
3285
|
-
return Math.min(zx, zy)
|
|
3286
|
-
}
|
|
3287
|
-
case 'fit-x': {
|
|
3288
|
-
return zx
|
|
3289
|
-
}
|
|
3290
|
-
case 'fit-y': {
|
|
3291
|
-
return zy
|
|
3292
|
-
}
|
|
3293
|
-
case 'fit-min-100': {
|
|
3294
|
-
return Math.min(1, Math.max(zx, zy))
|
|
3295
|
-
}
|
|
3296
|
-
case 'fit-max-100': {
|
|
3297
|
-
return Math.min(1, Math.min(zx, zy))
|
|
3298
|
-
}
|
|
3299
|
-
case 'fit-x-100': {
|
|
3300
|
-
return Math.min(1, zx)
|
|
3301
|
-
}
|
|
3302
|
-
case 'fit-y-100': {
|
|
3303
|
-
return Math.min(1, zy)
|
|
3304
|
-
}
|
|
3305
|
-
default: {
|
|
3306
|
-
throw exhaustiveSwitchError(cameraOptions.constraints.initialZoom)
|
|
3307
|
-
}
|
|
3308
|
-
}
|
|
3267
|
+
return this._getFitZoom(this.getCameraOptions().constraints?.initialZoom ?? 'default')
|
|
3309
3268
|
}
|
|
3310
3269
|
|
|
3311
3270
|
/**
|
|
@@ -3318,16 +3277,16 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
3318
3277
|
*
|
|
3319
3278
|
* @public */
|
|
3320
3279
|
getBaseZoom() {
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
if (!cameraOptions.constraints) return 1
|
|
3280
|
+
return this._getFitZoom(this.getCameraOptions().constraints?.baseZoom ?? 'default')
|
|
3281
|
+
}
|
|
3324
3282
|
|
|
3325
|
-
|
|
3326
|
-
|
|
3283
|
+
private _getFitZoom(fit: TLCameraConstraints['initialZoom']): number {
|
|
3284
|
+
const cameraOptions = this.getCameraOptions()
|
|
3285
|
+
if (!cameraOptions.constraints || fit === 'default') return 1
|
|
3327
3286
|
|
|
3328
3287
|
const { zx, zy } = getCameraFitXFitY(this, cameraOptions)
|
|
3329
3288
|
|
|
3330
|
-
switch (
|
|
3289
|
+
switch (fit) {
|
|
3331
3290
|
case 'fit-min': {
|
|
3332
3291
|
return Math.max(zx, zy)
|
|
3333
3292
|
}
|
|
@@ -3353,7 +3312,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
3353
3312
|
return Math.min(1, zy)
|
|
3354
3313
|
}
|
|
3355
3314
|
default: {
|
|
3356
|
-
throw exhaustiveSwitchError(
|
|
3315
|
+
throw exhaustiveSwitchError(fit)
|
|
3357
3316
|
}
|
|
3358
3317
|
}
|
|
3359
3318
|
}
|
|
@@ -3390,7 +3349,9 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
3390
3349
|
...this._cameraOptions.__unsafe__getWithoutCapture(),
|
|
3391
3350
|
...opts,
|
|
3392
3351
|
})
|
|
3393
|
-
|
|
3352
|
+
// `undefined < 1` is false, so an explicit `zoomSteps: undefined` would otherwise get through
|
|
3353
|
+
// and make every later camera move throw
|
|
3354
|
+
if (!next.zoomSteps || next.zoomSteps.length < 1) next.zoomSteps = [1]
|
|
3394
3355
|
this._cameraOptions.set(next)
|
|
3395
3356
|
this.setCamera(this.getCamera())
|
|
3396
3357
|
return this
|
|
@@ -3998,9 +3959,9 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
3998
3959
|
const remaining = duration - elapsed
|
|
3999
3960
|
const t = easing(1 - remaining / duration)
|
|
4000
3961
|
|
|
4001
|
-
const left = start.minX
|
|
4002
|
-
const top = start.minY
|
|
4003
|
-
const right = start.maxX
|
|
3962
|
+
const left = lerp(start.minX, end.minX, t)
|
|
3963
|
+
const top = lerp(start.minY, end.minY, t)
|
|
3964
|
+
const right = lerp(start.maxX, end.maxX, t)
|
|
4004
3965
|
|
|
4005
3966
|
this._setCamera(new Vec(-left, -top, this.getViewportScreenBounds().width / (right - left)), {
|
|
4006
3967
|
force: true,
|
|
@@ -4115,9 +4076,10 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
4115
4076
|
let newCy = cy + dy
|
|
4116
4077
|
let newCz = cz
|
|
4117
4078
|
|
|
4118
|
-
// animate zoom if z direction is passed in
|
|
4079
|
+
// animate zoom if z direction is passed in. Use an exponential factor so that a single
|
|
4080
|
+
// long frame can't drive the zoom to zero or negative (which would give NaN coordinates)
|
|
4119
4081
|
if (dirZ !== 0) {
|
|
4120
|
-
newCz = cz * (
|
|
4082
|
+
newCz = cz * Math.exp(dirZ * currentSpeed * elapsed)
|
|
4121
4083
|
// Adjust x/y to keep the viewport center fixed while zooming
|
|
4122
4084
|
const center = this.getViewportScreenCenter()
|
|
4123
4085
|
newCx += center.x / newCz - center.x / cz
|
|
@@ -4702,7 +4664,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
4702
4664
|
let nextIndex = this.options.maxShapesPerPage * 2
|
|
4703
4665
|
let nextBackgroundIndex = this.options.maxShapesPerPage
|
|
4704
4666
|
|
|
4705
|
-
const erasingShapeIds = this.getErasingShapeIds()
|
|
4667
|
+
const erasingShapeIds = new Set(this.getErasingShapeIds())
|
|
4706
4668
|
|
|
4707
4669
|
const addShapeById = (id: TLShapeId, opacity: number, isAncestorErasing: boolean) => {
|
|
4708
4670
|
const shape = this.getShape(id)
|
|
@@ -4710,7 +4672,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
4710
4672
|
|
|
4711
4673
|
if (this.isShapeHidden(shape)) {
|
|
4712
4674
|
// process children just in case they are overriding the hidden state
|
|
4713
|
-
const isErasing = isAncestorErasing || erasingShapeIds.
|
|
4675
|
+
const isErasing = isAncestorErasing || erasingShapeIds.has(id)
|
|
4714
4676
|
for (const childId of this.getSortedChildIdsForParent(id)) {
|
|
4715
4677
|
addShapeById(childId, opacity, isErasing)
|
|
4716
4678
|
}
|
|
@@ -4722,7 +4684,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
4722
4684
|
const util = this.getShapeUtil(shape)
|
|
4723
4685
|
|
|
4724
4686
|
if (useEditorState) {
|
|
4725
|
-
isShapeErasing = !isAncestorErasing && erasingShapeIds.
|
|
4687
|
+
isShapeErasing = !isAncestorErasing && erasingShapeIds.has(id)
|
|
4726
4688
|
if (isShapeErasing) {
|
|
4727
4689
|
opacity *= 0.32
|
|
4728
4690
|
}
|
|
@@ -5481,11 +5443,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
5481
5443
|
/** @internal */
|
|
5482
5444
|
@computed private _getShapePageBoundsCache(): ComputedCache<Box, TLShape> {
|
|
5483
5445
|
return this.store.createComputedCache<Box, TLShape>('pageBoundsCache', (shape) => {
|
|
5484
|
-
const pageTransform = this.getShapePageTransform(shape)
|
|
5485
|
-
if (!pageTransform) return undefined
|
|
5486
|
-
|
|
5487
5446
|
return Box.FromPoints(
|
|
5488
|
-
|
|
5447
|
+
this.getShapePageTransform(shape).applyToPoints(this.getShapeGeometry(shape).boundsVertices)
|
|
5489
5448
|
)
|
|
5490
5449
|
})
|
|
5491
5450
|
}
|
|
@@ -6239,9 +6198,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6239
6198
|
if (!freshShape) return new Vec(0, 0)
|
|
6240
6199
|
if (isPageId(freshShape.parentId)) return Vec.From(point)
|
|
6241
6200
|
|
|
6242
|
-
|
|
6243
|
-
if (!parentTransform) return Vec.From(point)
|
|
6244
|
-
return parentTransform.clone().invert().applyToPoint(point)
|
|
6201
|
+
return this.getShapePageTransform(freshShape.parentId).clone().invert().applyToPoint(point)
|
|
6245
6202
|
}
|
|
6246
6203
|
|
|
6247
6204
|
/**
|
|
@@ -6408,26 +6365,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6408
6365
|
* @public
|
|
6409
6366
|
*/
|
|
6410
6367
|
isShapeInPage(shape: TLShape | TLShapeId, pageId = this.getCurrentPageId()): boolean {
|
|
6411
|
-
|
|
6412
|
-
const shapeToCheck = this.getShape(id)
|
|
6413
|
-
if (!shapeToCheck) return false
|
|
6414
|
-
|
|
6415
|
-
let shapeIsInPage = false
|
|
6416
|
-
|
|
6417
|
-
if (shapeToCheck.parentId === pageId) {
|
|
6418
|
-
shapeIsInPage = true
|
|
6419
|
-
} else {
|
|
6420
|
-
let parent = this.getShape(shapeToCheck.parentId)
|
|
6421
|
-
isInPageSearch: while (parent) {
|
|
6422
|
-
if (parent.parentId === pageId) {
|
|
6423
|
-
shapeIsInPage = true
|
|
6424
|
-
break isInPageSearch
|
|
6425
|
-
}
|
|
6426
|
-
parent = this.getShape(parent.parentId)
|
|
6427
|
-
}
|
|
6428
|
-
}
|
|
6429
|
-
|
|
6430
|
-
return shapeIsInPage
|
|
6368
|
+
return this.getAncestorPageId(shape) === pageId
|
|
6431
6369
|
}
|
|
6432
6370
|
|
|
6433
6371
|
/**
|
|
@@ -6538,13 +6476,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6538
6476
|
for (let i = 0; i < shapesToReparent.length; i++) {
|
|
6539
6477
|
const shape = shapesToReparent[i]
|
|
6540
6478
|
|
|
6541
|
-
const pageTransform = this.getShapePageTransform(shape)
|
|
6542
|
-
|
|
6543
|
-
|
|
6544
|
-
const pagePoint = pageTransform.point()
|
|
6545
|
-
if (!pagePoint) continue
|
|
6546
|
-
|
|
6547
|
-
const newPoint = invertedParentTransform.applyToPoint(pagePoint)
|
|
6479
|
+
const pageTransform = this.getShapePageTransform(shape)
|
|
6480
|
+
const newPoint = invertedParentTransform.applyToPoint(pageTransform.point())
|
|
6548
6481
|
const newRotation = pageTransform.rotation() - parentPageRotation
|
|
6549
6482
|
|
|
6550
6483
|
if (shape.id === parentId) {
|
|
@@ -6645,7 +6578,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6645
6578
|
*/
|
|
6646
6579
|
getShapeAndDescendantIds(ids: TLShapeId[]): Set<TLShapeId> {
|
|
6647
6580
|
const shapeIds = new Set<TLShapeId>()
|
|
6648
|
-
for (const shape of ids.map((id) => this.getShape(id)
|
|
6581
|
+
for (const shape of compact(ids.map((id) => this.getShape(id))).sort(sortByIndex)) {
|
|
6649
6582
|
shapeIds.add(shape.id)
|
|
6650
6583
|
this.visitDescendants(shape, (descendantId) => {
|
|
6651
6584
|
shapeIds.add(descendantId)
|
|
@@ -6669,6 +6602,9 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6669
6602
|
const draggingShapes = compact(droppingShapes.map((s) => this.getShape(s))).filter(
|
|
6670
6603
|
(s) => !s.isLocked && !this.isShapeHidden(s)
|
|
6671
6604
|
)
|
|
6605
|
+
// Descendants of the dragged shapes can't be the target, otherwise dragging a frame out of
|
|
6606
|
+
// its parent reports a nested child as the target and the parent never sees the drag leave
|
|
6607
|
+
const excludedIds = this.getShapeAndDescendantIds(draggingShapes.map((s) => s.id))
|
|
6672
6608
|
|
|
6673
6609
|
const maybeDraggingOverShapes = this.getShapesAtPoint(point, {
|
|
6674
6610
|
hitInside: true,
|
|
@@ -6678,7 +6614,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6678
6614
|
!droppingShapes.includes(s) &&
|
|
6679
6615
|
!s.isLocked &&
|
|
6680
6616
|
!this.isShapeHidden(s) &&
|
|
6681
|
-
!
|
|
6617
|
+
!excludedIds.has(s.id)
|
|
6682
6618
|
)
|
|
6683
6619
|
|
|
6684
6620
|
for (const maybeDraggingOverShape of maybeDraggingOverShapes) {
|
|
@@ -7052,8 +6988,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
7052
6988
|
for (const id of ids) {
|
|
7053
6989
|
const shape = this.getShape(id)!
|
|
7054
6990
|
const localDelta = Vec.From(offset)
|
|
7055
|
-
|
|
7056
|
-
if (parentTransform) localDelta.rot(-parentTransform.rotation())
|
|
6991
|
+
localDelta.rot(-this.getShapeParentTransform(shape).rotation())
|
|
7057
6992
|
|
|
7058
6993
|
changes.push(this.getChangesToTranslateShape(shape, localDelta.add(shape)))
|
|
7059
6994
|
}
|
|
@@ -7123,7 +7058,13 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
7123
7058
|
let ox = 0
|
|
7124
7059
|
let oy = 0
|
|
7125
7060
|
|
|
7126
|
-
|
|
7061
|
+
// Only offset the roots of the duplicated tree: a descendant that was also passed in
|
|
7062
|
+
// follows its duplicated parent, so offsetting it too would move it twice
|
|
7063
|
+
if (
|
|
7064
|
+
offset &&
|
|
7065
|
+
initialIds.has(originalId) &&
|
|
7066
|
+
!shapeIdSet.has(originalShape.parentId as TLShapeId)
|
|
7067
|
+
) {
|
|
7127
7068
|
const parentTransform = this.getShapeParentTransform(originalShape)
|
|
7128
7069
|
const vec = new Vec(offset.x, offset.y).rot(-parentTransform!.rotation())
|
|
7129
7070
|
ox = vec.x
|
|
@@ -7289,27 +7230,17 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
7289
7230
|
|
|
7290
7231
|
if (this.getIsReadonly() || ids.length === 0) return this
|
|
7291
7232
|
|
|
7292
|
-
let allLocked = true
|
|
7293
|
-
allUnlocked = true
|
|
7233
|
+
let allLocked = true
|
|
7294
7234
|
const shapesToToggle: TLShape[] = []
|
|
7295
7235
|
for (const id of ids) {
|
|
7296
7236
|
const shape = this.getShape(id)
|
|
7297
7237
|
if (shape) {
|
|
7298
7238
|
shapesToToggle.push(shape)
|
|
7299
|
-
if (shape.isLocked)
|
|
7300
|
-
allUnlocked = false
|
|
7301
|
-
} else {
|
|
7302
|
-
allLocked = false
|
|
7303
|
-
}
|
|
7239
|
+
if (!shape.isLocked) allLocked = false
|
|
7304
7240
|
}
|
|
7305
7241
|
}
|
|
7306
7242
|
this.run(() => {
|
|
7307
|
-
if (
|
|
7308
|
-
this.updateShapes(
|
|
7309
|
-
shapesToToggle.map((shape) => ({ id: shape.id, type: shape.type, isLocked: true }))
|
|
7310
|
-
)
|
|
7311
|
-
this.setSelectedShapes([])
|
|
7312
|
-
} else if (allLocked) {
|
|
7243
|
+
if (allLocked) {
|
|
7313
7244
|
this.updateShapes(
|
|
7314
7245
|
shapesToToggle.map((shape) => ({ id: shape.id, type: shape.type, isLocked: false }))
|
|
7315
7246
|
)
|
|
@@ -7317,6 +7248,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
7317
7248
|
this.updateShapes(
|
|
7318
7249
|
shapesToToggle.map((shape) => ({ id: shape.id, type: shape.type, isLocked: true }))
|
|
7319
7250
|
)
|
|
7251
|
+
this.setSelectedShapes([])
|
|
7320
7252
|
}
|
|
7321
7253
|
})
|
|
7322
7254
|
|
|
@@ -7456,9 +7388,12 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
7456
7388
|
|
|
7457
7389
|
// optionally filter to axis-aligned shapes (rotation is a multiple of 90 degrees)
|
|
7458
7390
|
if (opts?.filterAxisAligned) {
|
|
7459
|
-
freshShapes = freshShapes.filter(
|
|
7460
|
-
|
|
7461
|
-
|
|
7391
|
+
freshShapes = freshShapes.filter((s) => {
|
|
7392
|
+
// Page rotations come back through atan2 on a composed matrix, so a 270° shape can land
|
|
7393
|
+
// a hair either side of the multiple; an exact === 0 check drops it
|
|
7394
|
+
const remainder = Math.abs(this.getShapePageTransform(s).rotation() % HALF_PI)
|
|
7395
|
+
return Math.min(remainder, HALF_PI - remainder) < 1e-9
|
|
7396
|
+
})
|
|
7462
7397
|
}
|
|
7463
7398
|
|
|
7464
7399
|
const clusters: { shapes: TLShape[]; pageBounds: Box }[] = []
|
|
@@ -7679,13 +7614,14 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
7679
7614
|
|
|
7680
7615
|
let shapeGap: number = 0
|
|
7681
7616
|
|
|
7617
|
+
// Stack in spatial order rather than input (z) order, otherwise shapes swap places
|
|
7618
|
+
shapeClustersToStack.sort((a, b) => a.pageBounds[min] - b.pageBounds[min])
|
|
7619
|
+
|
|
7682
7620
|
if (_gap === 0) {
|
|
7683
7621
|
// note: this is not used in the current tldraw.com; there we use a specified stack
|
|
7684
7622
|
|
|
7685
7623
|
const gaps: Record<number, number> = {}
|
|
7686
7624
|
|
|
7687
|
-
shapeClustersToStack.sort((a, b) => a.pageBounds[min] - b.pageBounds[min])
|
|
7688
|
-
|
|
7689
7625
|
// Collect all of the gaps between shapes. We want to find
|
|
7690
7626
|
// patterns (equal gaps between shapes) and use the most common
|
|
7691
7627
|
// one as the gap for all of the shapes.
|
|
@@ -7738,8 +7674,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
7738
7674
|
// todo: ensure that the parent isn't being aligned together with its children
|
|
7739
7675
|
const parent = this.getShapeParent(shape)
|
|
7740
7676
|
if (parent) {
|
|
7741
|
-
|
|
7742
|
-
if (parentTransform) shapeDelta.rot(-parentTransform.rotation())
|
|
7677
|
+
shapeDelta.rot(-this.getShapePageTransform(parent).rotation())
|
|
7743
7678
|
}
|
|
7744
7679
|
|
|
7745
7680
|
shapeDelta.add(shape) // add the shape's x and y to the delta
|
|
@@ -7789,10 +7724,11 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
7789
7724
|
|
|
7790
7725
|
const maxWidth = commonBounds.width
|
|
7791
7726
|
|
|
7792
|
-
// sort the shape clusters by width and then height, descending
|
|
7727
|
+
// sort the shape clusters by width and then height, descending: potpack fills each row's
|
|
7728
|
+
// right-hand space with the shapes that follow, so they must be no taller than the row
|
|
7793
7729
|
shapeClustersToPack
|
|
7794
|
-
.sort((a, b) =>
|
|
7795
|
-
.sort((a, b) =>
|
|
7730
|
+
.sort((a, b) => b.pageBounds.width - a.pageBounds.width)
|
|
7731
|
+
.sort((a, b) => b.pageBounds.height - a.pageBounds.height)
|
|
7796
7732
|
|
|
7797
7733
|
// Start with is (sort of) the square of the area
|
|
7798
7734
|
const startWidth = Math.max(Math.ceil(Math.sqrt(area / 0.95)), maxWidth)
|
|
@@ -7862,8 +7798,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
7862
7798
|
|
|
7863
7799
|
const parent = this.getShapeParent(shape)
|
|
7864
7800
|
if (parent) {
|
|
7865
|
-
|
|
7866
|
-
if (parentTransform) shapeDelta.rot(-parentTransform.rotation())
|
|
7801
|
+
shapeDelta.rot(-this.getShapeParentTransform(shape).rotation())
|
|
7867
7802
|
}
|
|
7868
7803
|
|
|
7869
7804
|
shapeDelta.add(shape)
|
|
@@ -7953,8 +7888,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
7953
7888
|
// todo: ensure that the parent isn't being aligned together with its children
|
|
7954
7889
|
const parent = this.getShapeParent(shape)
|
|
7955
7890
|
if (parent) {
|
|
7956
|
-
|
|
7957
|
-
if (parentTransform) shapeDelta.rot(-parentTransform.rotation())
|
|
7891
|
+
shapeDelta.rot(-this.getShapePageTransform(parent).rotation())
|
|
7958
7892
|
}
|
|
7959
7893
|
|
|
7960
7894
|
shapeDelta.add(shape) // add the shape's x and y to the delta
|
|
@@ -8054,8 +7988,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
8054
7988
|
// todo: ensure that the parent isn't being aligned together with its children
|
|
8055
7989
|
const parent = this.getShapeParent(shape)
|
|
8056
7990
|
if (parent) {
|
|
8057
|
-
|
|
8058
|
-
if (parentTransform) shapeDelta.rot(-parentTransform.rotation())
|
|
7991
|
+
shapeDelta.rot(-this.getShapePageTransform(parent).rotation())
|
|
8059
7992
|
}
|
|
8060
7993
|
|
|
8061
7994
|
shapeDelta.add(shape) // add the shape's x and y to the delta
|
|
@@ -8123,8 +8056,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
8123
8056
|
for (const shape of shapes) {
|
|
8124
8057
|
// First translate
|
|
8125
8058
|
const shapeLocalOffset = localOffset.clone()
|
|
8126
|
-
|
|
8127
|
-
if (parentTransform) shapeLocalOffset.rot(-parentTransform.rotation())
|
|
8059
|
+
shapeLocalOffset.rot(-this.getShapeParentTransform(shape).rotation())
|
|
8128
8060
|
shapeLocalOffset.add(shape)
|
|
8129
8061
|
const changes = this.getChangesToTranslateShape(shape, shapeLocalOffset)
|
|
8130
8062
|
this.updateShape(changes)
|
|
@@ -8195,8 +8127,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
8195
8127
|
for (const shape of shapes) {
|
|
8196
8128
|
// First translate
|
|
8197
8129
|
const shapeLocalOffset = localOffset.clone()
|
|
8198
|
-
|
|
8199
|
-
if (parentTransform) shapeLocalOffset.rot(-parentTransform.rotation())
|
|
8130
|
+
shapeLocalOffset.rot(-this.getShapeParentTransform(shape).rotation())
|
|
8200
8131
|
shapeLocalOffset.add(shape)
|
|
8201
8132
|
const changes = this.getChangesToTranslateShape(shape, shapeLocalOffset)
|
|
8202
8133
|
this.updateShape(changes)
|
|
@@ -8260,12 +8191,9 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
8260
8191
|
const pageTransform = opts.initialPageTransform
|
|
8261
8192
|
? Mat.Cast(opts.initialPageTransform)
|
|
8262
8193
|
: this.getShapePageTransform(id)
|
|
8263
|
-
if (!pageTransform) return null
|
|
8264
8194
|
|
|
8265
8195
|
const pageRotation = pageTransform.rotation()
|
|
8266
8196
|
|
|
8267
|
-
if (pageRotation == null) return null
|
|
8268
|
-
|
|
8269
8197
|
const scaleAxisRotation = opts.scaleAxisRotation ?? pageRotation
|
|
8270
8198
|
|
|
8271
8199
|
const initialBounds = opts.initialBounds ?? this.getShapeGeometry(id).bounds
|
|
@@ -8502,7 +8430,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
8502
8430
|
)
|
|
8503
8431
|
|
|
8504
8432
|
// now calculate how far away the shape is from where it needs to be
|
|
8505
|
-
const pageTransform = this.getShapePageTransform(id)
|
|
8433
|
+
const pageTransform = this.getShapePageTransform(id)
|
|
8506
8434
|
// We need to use the local bounds center transformed to page space, not the axis-aligned
|
|
8507
8435
|
// page bounds center. This is because the page bounds are axis-aligned and their center
|
|
8508
8436
|
// changes when the rotation changes, but we want to use the same reference point as
|
|
@@ -8510,7 +8438,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
8510
8438
|
const currentLocalBounds = this.getShapeGeometry(id).bounds
|
|
8511
8439
|
const currentPageCenter = Mat.applyToPoint(pageTransform, currentLocalBounds.center)
|
|
8512
8440
|
const shapePageTransformOrigin = pageTransform.point()
|
|
8513
|
-
if (!currentPageCenter || !shapePageTransformOrigin) return this
|
|
8514
8441
|
const pageDelta = Vec.Sub(postScaleShapePageCenter, currentPageCenter)
|
|
8515
8442
|
|
|
8516
8443
|
// and finally figure out what the shape's new position should be
|
|
@@ -8898,10 +8825,10 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
8898
8825
|
|
|
8899
8826
|
updates.push({
|
|
8900
8827
|
...end,
|
|
8901
|
-
x: start.x
|
|
8902
|
-
y: start.y
|
|
8903
|
-
opacity: start.opacity
|
|
8904
|
-
rotation: start.rotation
|
|
8828
|
+
x: lerp(start.x, end.x, t),
|
|
8829
|
+
y: lerp(start.y, end.y, t),
|
|
8830
|
+
opacity: lerp(start.opacity, end.opacity, t),
|
|
8831
|
+
rotation: lerp(start.rotation, end.rotation, t),
|
|
8905
8832
|
props: this.getShapeUtil(end).getInterpolatedProps?.(start, end, t) ?? end.props,
|
|
8906
8833
|
})
|
|
8907
8834
|
}
|
|
@@ -9117,7 +9044,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
9117
9044
|
* @public
|
|
9118
9045
|
*/
|
|
9119
9046
|
updateShapes<T extends TLShape>(partials: (TLShapePartial<T> | null | undefined)[]) {
|
|
9120
|
-
const compactedPartials: TLShapePartial<T>[] =
|
|
9047
|
+
const compactedPartials: TLShapePartial<T>[] = []
|
|
9121
9048
|
|
|
9122
9049
|
for (let i = 0, n = partials.length; i < n; i++) {
|
|
9123
9050
|
const partial = partials[i]
|
|
@@ -9263,26 +9190,25 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
9263
9190
|
/* --------------------- Styles --------------------- */
|
|
9264
9191
|
|
|
9265
9192
|
/**
|
|
9266
|
-
*
|
|
9193
|
+
* Groups have no styles of their own: a style read or write on a selection applies to the
|
|
9194
|
+
* non-group shapes beneath each group, however deeply nested. Returns those shapes.
|
|
9267
9195
|
*
|
|
9268
9196
|
* @internal
|
|
9269
9197
|
*/
|
|
9270
|
-
private
|
|
9271
|
-
|
|
9272
|
-
|
|
9273
|
-
|
|
9274
|
-
|
|
9275
|
-
|
|
9276
|
-
|
|
9277
|
-
|
|
9278
|
-
|
|
9279
|
-
|
|
9280
|
-
}
|
|
9281
|
-
} else {
|
|
9282
|
-
for (const [style, propKey] of this.styleProps[shape.type]) {
|
|
9283
|
-
sharedStyleMap.applyValue(style, getOwnProperty(shape.props, propKey))
|
|
9198
|
+
private _getStyleableShapes(shapes: TLShape[]): TLShape[] {
|
|
9199
|
+
const result: TLShape[] = []
|
|
9200
|
+
const visit = (shape: TLShape) => {
|
|
9201
|
+
if (this.isShapeOfType(shape, 'group')) {
|
|
9202
|
+
for (const childId of this.getSortedChildIdsForParent(shape.id)) {
|
|
9203
|
+
const child = this.getShape(childId)
|
|
9204
|
+
if (child) visit(child)
|
|
9205
|
+
}
|
|
9206
|
+
} else {
|
|
9207
|
+
result.push(shape)
|
|
9284
9208
|
}
|
|
9285
9209
|
}
|
|
9210
|
+
for (const shape of shapes) visit(shape)
|
|
9211
|
+
return result
|
|
9286
9212
|
}
|
|
9287
9213
|
|
|
9288
9214
|
/**
|
|
@@ -9292,11 +9218,11 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
9292
9218
|
*/
|
|
9293
9219
|
@computed
|
|
9294
9220
|
private _getSelectionSharedStyles(): ReadonlySharedStyleMap {
|
|
9295
|
-
const selectedShapes = this.getSelectedShapes()
|
|
9296
|
-
|
|
9297
9221
|
const sharedStyles = new SharedStyleMap()
|
|
9298
|
-
for (const
|
|
9299
|
-
|
|
9222
|
+
for (const shape of this._getStyleableShapes(this.getSelectedShapes())) {
|
|
9223
|
+
for (const [style, propKey] of this.styleProps[shape.type]) {
|
|
9224
|
+
sharedStyles.applyValue(style, getOwnProperty(shape.props, propKey))
|
|
9225
|
+
}
|
|
9300
9226
|
}
|
|
9301
9227
|
|
|
9302
9228
|
return sharedStyles
|
|
@@ -9354,18 +9280,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
9354
9280
|
if (!currentTool) return styles
|
|
9355
9281
|
|
|
9356
9282
|
if (currentTool.shapeType) {
|
|
9357
|
-
|
|
9358
|
-
|
|
9359
|
-
!(this.getShapeUtil('frame')!.options as any).showColors
|
|
9360
|
-
) {
|
|
9361
|
-
for (const style of this.styleProps[currentTool.shapeType].keys()) {
|
|
9362
|
-
if (style.id === 'tldraw:color') continue
|
|
9363
|
-
styles.applyValue(style, this.getStyleForNextShape(style))
|
|
9364
|
-
}
|
|
9365
|
-
} else {
|
|
9366
|
-
for (const style of this.styleProps[currentTool.shapeType].keys()) {
|
|
9367
|
-
styles.applyValue(style, this.getStyleForNextShape(style))
|
|
9368
|
-
}
|
|
9283
|
+
for (const style of this.styleProps[currentTool.shapeType].keys()) {
|
|
9284
|
+
styles.applyValue(style, this.getStyleForNextShape(style))
|
|
9369
9285
|
}
|
|
9370
9286
|
}
|
|
9371
9287
|
|
|
@@ -9381,27 +9297,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
9381
9297
|
*/
|
|
9382
9298
|
@computed getSharedOpacity(): SharedStyle<number> {
|
|
9383
9299
|
if (this.isIn('select') && this.getSelectedShapeIds().length > 0) {
|
|
9384
|
-
const shapesToCheck: TLShape[] = []
|
|
9385
|
-
const addShape = (shapeId: TLShapeId) => {
|
|
9386
|
-
const shape = this.getShape(shapeId)
|
|
9387
|
-
if (!shape) return
|
|
9388
|
-
// For groups, ignore the opacity of the group shape and instead include
|
|
9389
|
-
// the opacity of the group's children. These are the shapes that would have
|
|
9390
|
-
// their opacity changed if the user called `setOpacity` on the current selection.
|
|
9391
|
-
if (this.isShapeOfType(shape, 'group')) {
|
|
9392
|
-
for (const childId of this.getSortedChildIdsForParent(shape.id)) {
|
|
9393
|
-
addShape(childId)
|
|
9394
|
-
}
|
|
9395
|
-
} else {
|
|
9396
|
-
shapesToCheck.push(shape)
|
|
9397
|
-
}
|
|
9398
|
-
}
|
|
9399
|
-
for (const shapeId of this.getSelectedShapeIds()) {
|
|
9400
|
-
addShape(shapeId)
|
|
9401
|
-
}
|
|
9402
|
-
|
|
9403
9300
|
let opacity: number | null = null
|
|
9404
|
-
for (const shape of
|
|
9301
|
+
for (const shape of this._getStyleableShapes(this.getSelectedShapes())) {
|
|
9405
9302
|
if (opacity === null) {
|
|
9406
9303
|
opacity = shape.opacity
|
|
9407
9304
|
} else if (opacity !== shape.opacity) {
|
|
@@ -9444,33 +9341,12 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
9444
9341
|
const selectedShapes = this.getSelectedShapes()
|
|
9445
9342
|
|
|
9446
9343
|
if (selectedShapes.length > 0) {
|
|
9447
|
-
const shapesToUpdate: TLShape[] = []
|
|
9448
|
-
|
|
9449
|
-
// We can have many deep levels of grouped shape
|
|
9450
|
-
// Making a recursive function to look through all the levels
|
|
9451
|
-
const addShapeById = (shape: TLShape) => {
|
|
9452
|
-
if (this.isShapeOfType(shape, 'group')) {
|
|
9453
|
-
const childIds = this.getSortedChildIdsForParent(shape)
|
|
9454
|
-
for (const childId of childIds) {
|
|
9455
|
-
addShapeById(this.getShape(childId)!)
|
|
9456
|
-
}
|
|
9457
|
-
} else {
|
|
9458
|
-
shapesToUpdate.push(shape)
|
|
9459
|
-
}
|
|
9460
|
-
}
|
|
9461
|
-
|
|
9462
|
-
for (const id of selectedShapes) {
|
|
9463
|
-
addShapeById(id)
|
|
9464
|
-
}
|
|
9465
|
-
|
|
9466
9344
|
this.updateShapes(
|
|
9467
|
-
|
|
9468
|
-
|
|
9469
|
-
|
|
9470
|
-
|
|
9471
|
-
|
|
9472
|
-
}
|
|
9473
|
-
})
|
|
9345
|
+
this._getStyleableShapes(selectedShapes).map((shape) => ({
|
|
9346
|
+
id: shape.id,
|
|
9347
|
+
type: shape.type,
|
|
9348
|
+
opacity,
|
|
9349
|
+
}))
|
|
9474
9350
|
)
|
|
9475
9351
|
}
|
|
9476
9352
|
|
|
@@ -9524,43 +9400,19 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
9524
9400
|
const selectedShapes = this.getSelectedShapes()
|
|
9525
9401
|
|
|
9526
9402
|
if (selectedShapes.length > 0) {
|
|
9527
|
-
const updates:
|
|
9528
|
-
|
|
9529
|
-
|
|
9530
|
-
|
|
9531
|
-
|
|
9532
|
-
|
|
9533
|
-
|
|
9534
|
-
|
|
9535
|
-
|
|
9536
|
-
if (this.isShapeOfType(shape, 'group')) {
|
|
9537
|
-
const childIds = this.getSortedChildIdsForParent(shape.id)
|
|
9538
|
-
for (const childId of childIds) {
|
|
9539
|
-
addShapeById(this.getShape(childId)!)
|
|
9540
|
-
}
|
|
9541
|
-
} else {
|
|
9542
|
-
const util = this.getShapeUtil(shape)
|
|
9543
|
-
const stylePropKey = this.styleProps[shape.type].get(style)
|
|
9544
|
-
if (stylePropKey) {
|
|
9545
|
-
const shapePartial: TLShapePartial = {
|
|
9546
|
-
id: shape.id,
|
|
9547
|
-
type: shape.type,
|
|
9548
|
-
props: { [stylePropKey]: value },
|
|
9549
|
-
}
|
|
9550
|
-
updates.push({
|
|
9551
|
-
util,
|
|
9552
|
-
originalShape: shape,
|
|
9553
|
-
updatePartial: shapePartial,
|
|
9554
|
-
})
|
|
9555
|
-
}
|
|
9403
|
+
const updates: TLShapePartial[] = []
|
|
9404
|
+
for (const shape of this._getStyleableShapes(selectedShapes)) {
|
|
9405
|
+
const stylePropKey = this.styleProps[shape.type].get(style)
|
|
9406
|
+
if (stylePropKey) {
|
|
9407
|
+
updates.push({
|
|
9408
|
+
id: shape.id,
|
|
9409
|
+
type: shape.type,
|
|
9410
|
+
props: { [stylePropKey]: value },
|
|
9411
|
+
})
|
|
9556
9412
|
}
|
|
9557
9413
|
}
|
|
9558
9414
|
|
|
9559
|
-
|
|
9560
|
-
addShapeById(shape)
|
|
9561
|
-
}
|
|
9562
|
-
|
|
9563
|
-
this.updateShapes(updates.map(({ updatePartial }) => updatePartial))
|
|
9415
|
+
this.updateShapes(updates)
|
|
9564
9416
|
}
|
|
9565
9417
|
|
|
9566
9418
|
return this
|
|
@@ -10092,7 +9944,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
10092
9944
|
if (shapeIdMap.has(newShape.parentId)) {
|
|
10093
9945
|
newShape.parentId = shapeIdMap.get(oldShape.parentId)!
|
|
10094
9946
|
} else {
|
|
10095
|
-
rootShapeIds.push(newShape.id)
|
|
10096
9947
|
// newShape.parentId = pasteParentId
|
|
10097
9948
|
newShape.index = index
|
|
10098
9949
|
index = getIndexAbove(index)
|
|
@@ -10135,10 +9986,14 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
10135
9986
|
(asset.type === 'video' && asset.props.src?.startsWith('data:video'))
|
|
10136
9987
|
) {
|
|
10137
9988
|
// it's src is a base64 image or video; we need to create a new asset without the src,
|
|
10138
|
-
// then create a new asset from the original src. So we
|
|
10139
|
-
//
|
|
10140
|
-
|
|
10141
|
-
asset
|
|
9989
|
+
// then create a new asset from the original src. So we keep the original asset for the
|
|
9990
|
+
// upload and create a copy with its src removed. Copy rather than mutate: when no
|
|
9991
|
+
// migration applies, migrateStoreSnapshot hands back the caller's own record objects.
|
|
9992
|
+
assetsToUpdate.push(asset as TLImageAsset | TLVideoAsset)
|
|
9993
|
+
const assetWithoutSrc = structuredClone(asset as TLImageAsset | TLVideoAsset)
|
|
9994
|
+
assetWithoutSrc.props.src = null
|
|
9995
|
+
assetsToCreate.push(assetWithoutSrc)
|
|
9996
|
+
continue
|
|
10142
9997
|
}
|
|
10143
9998
|
|
|
10144
9999
|
// Add the asset to the list of assets to create
|