@tldraw/editor 4.3.0-next.f4772c19540d → 4.4.0-canary.29afdff6bb04
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/README.md +1 -1
- package/dist-cjs/index.d.ts +503 -155
- package/dist-cjs/index.js +8 -1
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/components/ErrorBoundary.js.map +1 -1
- package/dist-cjs/lib/components/GeometryDebuggingView.js +1 -17
- package/dist-cjs/lib/components/GeometryDebuggingView.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultCanvas.js +4 -5
- package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
- package/dist-cjs/lib/constants.js +1 -3
- package/dist-cjs/lib/constants.js.map +2 -2
- package/dist-cjs/lib/editor/Editor.js +346 -291
- package/dist-cjs/lib/editor/Editor.js.map +2 -2
- package/dist-cjs/lib/editor/bindings/BindingUtil.js.map +2 -2
- package/dist-cjs/lib/editor/derivations/bindingsIndex.js.map +2 -2
- package/dist-cjs/lib/editor/derivations/notVisibleShapes.js +16 -23
- package/dist-cjs/lib/editor/derivations/notVisibleShapes.js.map +3 -3
- package/dist-cjs/lib/editor/derivations/parentsToChildren.js +12 -3
- package/dist-cjs/lib/editor/derivations/parentsToChildren.js.map +2 -2
- package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js +1 -1
- package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js +5 -6
- package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js +591 -0
- package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js.map +7 -0
- package/dist-cjs/lib/editor/managers/SnapManager/SnapManager.js +1 -1
- package/dist-cjs/lib/editor/managers/SnapManager/SnapManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/SpatialIndexManager/RBushIndex.js +144 -0
- package/dist-cjs/lib/editor/managers/SpatialIndexManager/RBushIndex.js.map +7 -0
- package/dist-cjs/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.js +181 -0
- package/dist-cjs/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.js.map +7 -0
- package/dist-cjs/lib/editor/managers/TickManager/TickManager.js +1 -22
- package/dist-cjs/lib/editor/managers/TickManager/TickManager.js.map +2 -2
- package/dist-cjs/lib/editor/shapes/BaseBoxShapeUtil.js.map +1 -1
- package/dist-cjs/lib/editor/shapes/ShapeUtil.js +31 -23
- package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
- package/dist-cjs/lib/editor/shapes/group/DashedOutlineBox.js +1 -1
- package/dist-cjs/lib/editor/shapes/group/DashedOutlineBox.js.map +2 -2
- package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js.map +2 -2
- package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/BaseBoxShapeTool.js.map +2 -2
- package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js +3 -3
- package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js.map +2 -2
- package/dist-cjs/lib/editor/types/emit-types.js.map +1 -1
- 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 +2 -2
- package/dist-cjs/lib/globals/environment.js +45 -9
- package/dist-cjs/lib/globals/environment.js.map +2 -2
- package/dist-cjs/lib/globals/menus.js +1 -1
- package/dist-cjs/lib/globals/menus.js.map +2 -2
- package/dist-cjs/lib/hooks/useCoarsePointer.js +14 -29
- package/dist-cjs/lib/hooks/useCoarsePointer.js.map +2 -2
- package/dist-cjs/lib/hooks/useEvent.js +1 -1
- package/dist-cjs/lib/hooks/useEvent.js.map +2 -2
- package/dist-cjs/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/useGestureEvents.js +1 -1
- package/dist-cjs/lib/hooks/useGestureEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/usePassThroughMouseOverEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/useScreenBounds.js.map +2 -2
- package/dist-cjs/lib/hooks/useStateAttribute.js +4 -1
- package/dist-cjs/lib/hooks/useStateAttribute.js.map +2 -2
- package/dist-cjs/lib/hooks/useTransform.js.map +1 -1
- package/dist-cjs/lib/hooks/useZoomCss.js +4 -8
- package/dist-cjs/lib/hooks/useZoomCss.js.map +2 -2
- package/dist-cjs/lib/options.js +6 -1
- package/dist-cjs/lib/options.js.map +2 -2
- package/dist-cjs/lib/primitives/Box.js +3 -0
- package/dist-cjs/lib/primitives/Box.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Geometry2d.js +1 -0
- package/dist-cjs/lib/primitives/geometry/Geometry2d.js.map +2 -2
- package/dist-cjs/lib/utils/reparenting.js.map +2 -2
- package/dist-cjs/lib/utils/rotation.js +1 -1
- 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 +503 -155
- package/dist-esm/index.mjs +9 -2
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/components/ErrorBoundary.mjs.map +1 -1
- package/dist-esm/lib/components/GeometryDebuggingView.mjs +1 -17
- package/dist-esm/lib/components/GeometryDebuggingView.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +4 -5
- package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
- package/dist-esm/lib/constants.mjs +1 -3
- package/dist-esm/lib/constants.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +347 -294
- package/dist-esm/lib/editor/Editor.mjs.map +2 -2
- package/dist-esm/lib/editor/bindings/BindingUtil.mjs.map +2 -2
- package/dist-esm/lib/editor/derivations/bindingsIndex.mjs.map +2 -2
- package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs +16 -23
- package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs.map +3 -3
- package/dist-esm/lib/editor/derivations/parentsToChildren.mjs +13 -4
- package/dist-esm/lib/editor/derivations/parentsToChildren.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs +1 -1
- package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs +5 -6
- package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs +573 -0
- package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs.map +7 -0
- package/dist-esm/lib/editor/managers/SnapManager/SnapManager.mjs +1 -1
- package/dist-esm/lib/editor/managers/SnapManager/SnapManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/SpatialIndexManager/RBushIndex.mjs +114 -0
- package/dist-esm/lib/editor/managers/SpatialIndexManager/RBushIndex.mjs.map +7 -0
- package/dist-esm/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.mjs +161 -0
- package/dist-esm/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.mjs.map +7 -0
- package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs +1 -22
- package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs.map +2 -2
- package/dist-esm/lib/editor/shapes/BaseBoxShapeUtil.mjs.map +1 -1
- package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +31 -23
- package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/editor/shapes/group/DashedOutlineBox.mjs +1 -1
- package/dist-esm/lib/editor/shapes/group/DashedOutlineBox.mjs.map +2 -2
- package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/editor/tools/BaseBoxShapeTool/BaseBoxShapeTool.mjs.map +2 -2
- package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs +3 -3
- package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs.map +2 -2
- 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 +2 -2
- package/dist-esm/lib/globals/environment.mjs +45 -9
- package/dist-esm/lib/globals/environment.mjs.map +2 -2
- package/dist-esm/lib/globals/menus.mjs +1 -1
- package/dist-esm/lib/globals/menus.mjs.map +2 -2
- package/dist-esm/lib/hooks/useCoarsePointer.mjs +15 -30
- package/dist-esm/lib/hooks/useCoarsePointer.mjs.map +2 -2
- package/dist-esm/lib/hooks/useEvent.mjs +1 -1
- package/dist-esm/lib/hooks/useEvent.mjs.map +2 -2
- package/dist-esm/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/useGestureEvents.mjs +1 -1
- package/dist-esm/lib/hooks/useGestureEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/usePassThroughMouseOverEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/useScreenBounds.mjs.map +2 -2
- package/dist-esm/lib/hooks/useStateAttribute.mjs +4 -1
- package/dist-esm/lib/hooks/useStateAttribute.mjs.map +2 -2
- package/dist-esm/lib/hooks/useTransform.mjs.map +1 -1
- package/dist-esm/lib/hooks/useZoomCss.mjs +4 -8
- package/dist-esm/lib/hooks/useZoomCss.mjs.map +2 -2
- package/dist-esm/lib/options.mjs +6 -1
- package/dist-esm/lib/options.mjs.map +2 -2
- package/dist-esm/lib/primitives/Box.mjs +3 -0
- package/dist-esm/lib/primitives/Box.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Geometry2d.mjs +1 -0
- package/dist-esm/lib/primitives/geometry/Geometry2d.mjs.map +2 -2
- package/dist-esm/lib/utils/reparenting.mjs.map +2 -2
- package/dist-esm/lib/utils/rotation.mjs +1 -1
- 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/editor.css +14 -12
- package/package.json +21 -17
- package/src/index.ts +5 -1
- package/src/lib/components/ErrorBoundary.tsx +1 -1
- package/src/lib/components/GeometryDebuggingView.tsx +1 -19
- package/src/lib/components/default-components/DefaultCanvas.tsx +5 -8
- package/src/lib/config/TLUserPreferences.test.ts +40 -0
- package/src/lib/constants.ts +0 -2
- package/src/lib/editor/Editor.test.ts +150 -10
- package/src/lib/editor/Editor.ts +533 -384
- package/src/lib/editor/bindings/BindingUtil.ts +15 -9
- package/src/lib/editor/derivations/bindingsIndex.ts +2 -2
- package/src/lib/editor/derivations/notVisibleShapes.ts +21 -33
- package/src/lib/editor/derivations/parentsToChildren.ts +18 -7
- package/src/lib/editor/managers/ClickManager/ClickManager.test.ts +17 -31
- package/src/lib/editor/managers/ClickManager/ClickManager.ts +1 -1
- package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.test.ts +129 -79
- package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.ts +10 -6
- package/src/lib/editor/managers/FontManager/FontManager.test.ts +14 -4
- package/src/lib/editor/managers/InputsManager/InputsManager.ts +566 -0
- package/src/lib/editor/managers/ScribbleManager/ScribbleManager.test.ts +0 -4
- package/src/lib/editor/managers/SnapManager/SnapManager.test.ts +12 -0
- package/src/lib/editor/managers/SnapManager/SnapManager.ts +4 -4
- package/src/lib/editor/managers/SpatialIndexManager/RBushIndex.ts +144 -0
- package/src/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.ts +215 -0
- package/src/lib/editor/managers/TickManager/TickManager.test.ts +40 -107
- package/src/lib/editor/managers/TickManager/TickManager.ts +2 -32
- package/src/lib/editor/shapes/BaseBoxShapeUtil.tsx +2 -2
- package/src/lib/editor/shapes/ShapeUtil.ts +72 -32
- package/src/lib/editor/shapes/group/DashedOutlineBox.tsx +1 -1
- package/src/lib/editor/shapes/group/GroupShapeUtil.tsx +1 -3
- package/src/lib/editor/tools/BaseBoxShapeTool/BaseBoxShapeTool.ts +2 -1
- package/src/lib/editor/tools/BaseBoxShapeTool/children/Pointing.ts +6 -6
- package/src/lib/editor/types/emit-types.ts +3 -1
- package/src/lib/exports/getSvgJsx.test.ts +10 -19
- package/src/lib/exports/getSvgJsx.tsx +2 -5
- package/src/lib/exports/parseCss.test.ts +1 -0
- package/src/lib/exports/parseCss.ts +1 -1
- package/src/lib/globals/environment.ts +65 -10
- package/src/lib/globals/menus.ts +1 -1
- package/src/lib/hooks/useCoarsePointer.ts +16 -59
- package/src/lib/hooks/useEvent.tsx +1 -1
- package/src/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.ts +1 -1
- package/src/lib/hooks/useGestureEvents.ts +2 -2
- package/src/lib/hooks/usePassThroughMouseOverEvents.ts +1 -1
- package/src/lib/hooks/usePassThroughWheelEvents.ts +1 -1
- package/src/lib/hooks/useScreenBounds.ts +1 -1
- package/src/lib/hooks/useStateAttribute.ts +4 -1
- package/src/lib/hooks/useTransform.ts +1 -1
- package/src/lib/hooks/useZoomCss.ts +3 -8
- package/src/lib/options.ts +32 -0
- package/src/lib/primitives/Box.ts +9 -0
- package/src/lib/primitives/geometry/Geometry2d.ts +1 -0
- package/src/lib/utils/reparenting.ts +5 -5
- package/src/lib/utils/rotation.ts +1 -1
- package/src/version.ts +3 -3
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type { TLShapeId } from '@tldraw/tlschema'
|
|
2
|
+
import RBush from 'rbush'
|
|
3
|
+
import { Box } from '../../../primitives/Box'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Element stored in the R-tree spatial index.
|
|
7
|
+
* Contains bounds (minX, minY, maxX, maxY) and shape ID.
|
|
8
|
+
*/
|
|
9
|
+
export interface SpatialElement {
|
|
10
|
+
minX: number
|
|
11
|
+
minY: number
|
|
12
|
+
maxX: number
|
|
13
|
+
maxY: number
|
|
14
|
+
id: TLShapeId
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Custom RBush class for tldraw shapes.
|
|
19
|
+
*/
|
|
20
|
+
class TldrawRBush extends RBush<SpatialElement> {}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Wrapper around RBush R-tree for efficient spatial queries.
|
|
24
|
+
* Maintains a map of elements currently in the tree for efficient updates.
|
|
25
|
+
*/
|
|
26
|
+
export class RBushIndex {
|
|
27
|
+
private rBush: TldrawRBush
|
|
28
|
+
private elementsInTree: Map<TLShapeId, SpatialElement>
|
|
29
|
+
|
|
30
|
+
constructor() {
|
|
31
|
+
this.rBush = new TldrawRBush()
|
|
32
|
+
this.elementsInTree = new Map()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Search for shapes within the given bounds.
|
|
37
|
+
* Returns set of shape IDs that intersect with the bounds.
|
|
38
|
+
*/
|
|
39
|
+
search(bounds: Box): Set<TLShapeId> {
|
|
40
|
+
const results = this.rBush.search({
|
|
41
|
+
minX: bounds.minX,
|
|
42
|
+
minY: bounds.minY,
|
|
43
|
+
maxX: bounds.maxX,
|
|
44
|
+
maxY: bounds.maxY,
|
|
45
|
+
})
|
|
46
|
+
return new Set(results.map((e: SpatialElement) => e.id))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Insert or update a shape in the spatial index.
|
|
51
|
+
* If the shape already exists, it will be removed first to prevent duplicates.
|
|
52
|
+
*/
|
|
53
|
+
upsert(id: TLShapeId, bounds: Box): void {
|
|
54
|
+
// Remove existing entry to prevent map-tree desync
|
|
55
|
+
const existing = this.elementsInTree.get(id)
|
|
56
|
+
if (existing) {
|
|
57
|
+
this.rBush.remove(existing)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const element: SpatialElement = {
|
|
61
|
+
minX: bounds.minX,
|
|
62
|
+
minY: bounds.minY,
|
|
63
|
+
maxX: bounds.maxX,
|
|
64
|
+
maxY: bounds.maxY,
|
|
65
|
+
id,
|
|
66
|
+
}
|
|
67
|
+
this.rBush.insert(element)
|
|
68
|
+
this.elementsInTree.set(id, element)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Remove a shape from the spatial index.
|
|
73
|
+
*/
|
|
74
|
+
remove(id: TLShapeId): void {
|
|
75
|
+
const element = this.elementsInTree.get(id)
|
|
76
|
+
if (element) {
|
|
77
|
+
this.rBush.remove(element)
|
|
78
|
+
this.elementsInTree.delete(id)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Bulk load elements into the spatial index.
|
|
84
|
+
* More efficient than individual inserts for initial loading.
|
|
85
|
+
*/
|
|
86
|
+
bulkLoad(elements: SpatialElement[]): void {
|
|
87
|
+
this.rBush.load(elements)
|
|
88
|
+
for (const element of elements) {
|
|
89
|
+
this.elementsInTree.set(element.id, element)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Clear all elements from the spatial index.
|
|
95
|
+
*/
|
|
96
|
+
clear(): void {
|
|
97
|
+
this.rBush.clear()
|
|
98
|
+
this.elementsInTree.clear()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Check if a shape is in the spatial index.
|
|
103
|
+
*/
|
|
104
|
+
has(id: TLShapeId): boolean {
|
|
105
|
+
return this.elementsInTree.has(id)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get the number of elements in the spatial index.
|
|
110
|
+
*/
|
|
111
|
+
getSize(): number {
|
|
112
|
+
return this.elementsInTree.size
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get all shape IDs currently in the spatial index.
|
|
117
|
+
*/
|
|
118
|
+
getAllShapeIds(): TLShapeId[] {
|
|
119
|
+
return Array.from(this.elementsInTree.keys())
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get the bounds currently stored in the spatial index for a shape.
|
|
124
|
+
* Returns undefined if the shape is not in the index.
|
|
125
|
+
*/
|
|
126
|
+
getBounds(id: TLShapeId): Box | undefined {
|
|
127
|
+
const element = this.elementsInTree.get(id)
|
|
128
|
+
if (!element) return undefined
|
|
129
|
+
return new Box(
|
|
130
|
+
element.minX,
|
|
131
|
+
element.minY,
|
|
132
|
+
element.maxX - element.minX,
|
|
133
|
+
element.maxY - element.minY
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Dispose of the spatial index.
|
|
139
|
+
* Clears all data structures to prevent memory leaks.
|
|
140
|
+
*/
|
|
141
|
+
dispose(): void {
|
|
142
|
+
this.clear()
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { Computed, RESET_VALUE, computed, isUninitialized } from '@tldraw/state'
|
|
2
|
+
import type { RecordsDiff } from '@tldraw/store'
|
|
3
|
+
import type { TLRecord } from '@tldraw/tlschema'
|
|
4
|
+
import { TLPageId, TLShape, TLShapeId, isShape } from '@tldraw/tlschema'
|
|
5
|
+
import { objectMapValues } from '@tldraw/utils'
|
|
6
|
+
import { Box } from '../../../primitives/Box'
|
|
7
|
+
import type { Editor } from '../../Editor'
|
|
8
|
+
import { RBushIndex, type SpatialElement } from './RBushIndex'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Manages spatial indexing for efficient shape location queries.
|
|
12
|
+
*
|
|
13
|
+
* Uses an R-tree (via RBush) to enable O(log n) spatial queries instead of O(n) iteration.
|
|
14
|
+
* Handles shapes with computed bounds (arrows, groups, custom shapes) by checking all shapes'
|
|
15
|
+
* bounds on each update using the reactive bounds cache.
|
|
16
|
+
*
|
|
17
|
+
* Key features:
|
|
18
|
+
* - Incremental updates using filterHistory pattern
|
|
19
|
+
* - Leverages existing bounds cache reactivity for dependency tracking
|
|
20
|
+
* - Works with any custom shape type with computed bounds
|
|
21
|
+
* - Per-page index (rebuilds on page change)
|
|
22
|
+
* - Optimized for viewport culling queries
|
|
23
|
+
*
|
|
24
|
+
* @internal
|
|
25
|
+
*/
|
|
26
|
+
export class SpatialIndexManager {
|
|
27
|
+
private rbush: RBushIndex
|
|
28
|
+
private spatialIndexComputed: Computed<number>
|
|
29
|
+
private lastPageId: TLPageId | null = null
|
|
30
|
+
|
|
31
|
+
constructor(public readonly editor: Editor) {
|
|
32
|
+
this.rbush = new RBushIndex()
|
|
33
|
+
this.spatialIndexComputed = this.createSpatialIndexComputed()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private createSpatialIndexComputed() {
|
|
37
|
+
const shapeHistory = this.editor.store.query.filterHistory('shape')
|
|
38
|
+
|
|
39
|
+
return computed<number>('spatialIndex', (_prevValue, lastComputedEpoch) => {
|
|
40
|
+
if (isUninitialized(_prevValue)) {
|
|
41
|
+
return this.buildFromScratch(lastComputedEpoch)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const shapeDiff = shapeHistory.getDiffSince(lastComputedEpoch)
|
|
45
|
+
|
|
46
|
+
if (shapeDiff === RESET_VALUE) {
|
|
47
|
+
return this.buildFromScratch(lastComputedEpoch)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const currentPageId = this.editor.getCurrentPageId()
|
|
51
|
+
if (this.lastPageId !== currentPageId) {
|
|
52
|
+
return this.buildFromScratch(lastComputedEpoch)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// No shape changes - index is already up to date
|
|
56
|
+
if (shapeDiff.length === 0) {
|
|
57
|
+
return lastComputedEpoch
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Process incremental updates
|
|
61
|
+
this.processIncrementalUpdate(shapeDiff)
|
|
62
|
+
|
|
63
|
+
return lastComputedEpoch
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private buildFromScratch(epoch: number): number {
|
|
68
|
+
this.rbush.clear()
|
|
69
|
+
this.lastPageId = this.editor.getCurrentPageId()
|
|
70
|
+
|
|
71
|
+
const shapes = this.editor.getCurrentPageShapes()
|
|
72
|
+
const elements: SpatialElement[] = []
|
|
73
|
+
|
|
74
|
+
// Collect all shape elements for bulk loading
|
|
75
|
+
for (const shape of shapes) {
|
|
76
|
+
const bounds = this.editor.getShapePageBounds(shape.id)
|
|
77
|
+
if (bounds) {
|
|
78
|
+
elements.push({
|
|
79
|
+
minX: bounds.minX,
|
|
80
|
+
minY: bounds.minY,
|
|
81
|
+
maxX: bounds.maxX,
|
|
82
|
+
maxY: bounds.maxY,
|
|
83
|
+
id: shape.id,
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Bulk load for efficiency
|
|
89
|
+
this.rbush.bulkLoad(elements)
|
|
90
|
+
|
|
91
|
+
return epoch
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private processIncrementalUpdate(shapeDiff: RecordsDiff<TLRecord>[]): void {
|
|
95
|
+
// Track shapes we've already processed from the diff
|
|
96
|
+
const processedShapeIds = new Set<TLShapeId>()
|
|
97
|
+
|
|
98
|
+
// 1. Process shape additions, removals, and updates from diff
|
|
99
|
+
for (const changes of shapeDiff) {
|
|
100
|
+
// Handle additions (only for shapes on current page)
|
|
101
|
+
for (const shape of objectMapValues(changes.added) as TLShape[]) {
|
|
102
|
+
if (isShape(shape) && this.editor.getAncestorPageId(shape) === this.lastPageId) {
|
|
103
|
+
const bounds = this.editor.getShapePageBounds(shape.id)
|
|
104
|
+
if (bounds) {
|
|
105
|
+
this.rbush.upsert(shape.id, bounds)
|
|
106
|
+
}
|
|
107
|
+
processedShapeIds.add(shape.id)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Handle removals
|
|
112
|
+
for (const shape of objectMapValues(changes.removed) as TLShape[]) {
|
|
113
|
+
if (isShape(shape)) {
|
|
114
|
+
this.rbush.remove(shape.id)
|
|
115
|
+
processedShapeIds.add(shape.id)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Handle updated shapes: page changes and bounds updates
|
|
120
|
+
for (const [from, to] of objectMapValues(changes.updated) as [TLShape, TLShape][]) {
|
|
121
|
+
if (!isShape(to)) continue
|
|
122
|
+
processedShapeIds.add(to.id)
|
|
123
|
+
|
|
124
|
+
const wasOnPage = this.editor.getAncestorPageId(from) === this.lastPageId
|
|
125
|
+
const isOnPage = this.editor.getAncestorPageId(to) === this.lastPageId
|
|
126
|
+
|
|
127
|
+
if (isOnPage) {
|
|
128
|
+
const bounds = this.editor.getShapePageBounds(to.id)
|
|
129
|
+
if (bounds) {
|
|
130
|
+
this.rbush.upsert(to.id, bounds)
|
|
131
|
+
}
|
|
132
|
+
} else if (wasOnPage) {
|
|
133
|
+
this.rbush.remove(to.id)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 2. Check remaining shapes in index for bounds changes
|
|
139
|
+
// This handles shapes with computed bounds (arrows bound to moved shapes, groups with moved children, etc.)
|
|
140
|
+
const allShapeIds = this.rbush.getAllShapeIds()
|
|
141
|
+
|
|
142
|
+
for (const shapeId of allShapeIds) {
|
|
143
|
+
if (processedShapeIds.has(shapeId)) continue
|
|
144
|
+
|
|
145
|
+
const currentBounds = this.editor.getShapePageBounds(shapeId)
|
|
146
|
+
const indexedBounds = this.rbush.getBounds(shapeId)
|
|
147
|
+
|
|
148
|
+
if (!this.areBoundsEqual(currentBounds, indexedBounds)) {
|
|
149
|
+
if (currentBounds) {
|
|
150
|
+
this.rbush.upsert(shapeId, currentBounds)
|
|
151
|
+
} else {
|
|
152
|
+
this.rbush.remove(shapeId)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private areBoundsEqual(a: Box | undefined, b: Box | undefined): boolean {
|
|
159
|
+
if (!a && !b) return true
|
|
160
|
+
if (!a || !b) return false
|
|
161
|
+
return a.minX === b.minX && a.minY === b.minY && a.maxX === b.maxX && a.maxY === b.maxY
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Get shape IDs within the given bounds.
|
|
166
|
+
* Optimized for viewport culling queries.
|
|
167
|
+
*
|
|
168
|
+
* Note: Results are unordered. If you need z-order, combine with sorted shapes:
|
|
169
|
+
* ```ts
|
|
170
|
+
* const candidates = editor.spatialIndex.getShapeIdsInsideBounds(bounds)
|
|
171
|
+
* const sorted = editor.getCurrentPageShapesSorted().filter(s => candidates.has(s.id))
|
|
172
|
+
* ```
|
|
173
|
+
*
|
|
174
|
+
* @param bounds - The bounds to search within
|
|
175
|
+
* @returns Unordered set of shape IDs within the bounds
|
|
176
|
+
*
|
|
177
|
+
* @public
|
|
178
|
+
*/
|
|
179
|
+
getShapeIdsInsideBounds(bounds: Box): Set<TLShapeId> {
|
|
180
|
+
this.spatialIndexComputed.get()
|
|
181
|
+
return this.rbush.search(bounds)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Get shape IDs at a point (with optional margin).
|
|
186
|
+
* Creates a small bounding box around the point and searches the spatial index.
|
|
187
|
+
*
|
|
188
|
+
* Note: Results are unordered. If you need z-order, combine with sorted shapes:
|
|
189
|
+
* ```ts
|
|
190
|
+
* const candidates = editor.spatialIndex.getShapeIdsAtPoint(point, margin)
|
|
191
|
+
* const sorted = editor.getCurrentPageShapesSorted().filter(s => candidates.has(s.id))
|
|
192
|
+
* ```
|
|
193
|
+
*
|
|
194
|
+
* @param point - The point to search at
|
|
195
|
+
* @param margin - The margin around the point to search (default: 0)
|
|
196
|
+
* @returns Unordered set of shape IDs that could potentially contain the point
|
|
197
|
+
*
|
|
198
|
+
* @public
|
|
199
|
+
*/
|
|
200
|
+
getShapeIdsAtPoint(point: { x: number; y: number }, margin = 0): Set<TLShapeId> {
|
|
201
|
+
this.spatialIndexComputed.get()
|
|
202
|
+
return this.rbush.search(new Box(point.x - margin, point.y - margin, margin * 2, margin * 2))
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Dispose of the spatial index manager.
|
|
207
|
+
* Clears the R-tree to prevent memory leaks.
|
|
208
|
+
*
|
|
209
|
+
* @public
|
|
210
|
+
*/
|
|
211
|
+
dispose(): void {
|
|
212
|
+
this.rbush.dispose()
|
|
213
|
+
this.lastPageId = null
|
|
214
|
+
}
|
|
215
|
+
}
|
|
@@ -21,6 +21,17 @@ describe('TickManager', () => {
|
|
|
21
21
|
let tickManager: TickManager
|
|
22
22
|
let mockEmit: Mock
|
|
23
23
|
let mockDisposablesAdd: Mock
|
|
24
|
+
let mockInputs: {
|
|
25
|
+
_currentScreenPoint: Vec
|
|
26
|
+
getCurrentScreenPoint(): Vec
|
|
27
|
+
currentScreenPoint: Vec
|
|
28
|
+
setCurrentScreenPoint(value: Vec): void
|
|
29
|
+
_pointerVelocity: Vec
|
|
30
|
+
getPointerVelocity(): Vec
|
|
31
|
+
pointerVelocity: Vec
|
|
32
|
+
setPointerVelocity(value: Vec): void
|
|
33
|
+
updatePointerVelocity(elapsed: number): void
|
|
34
|
+
}
|
|
24
35
|
|
|
25
36
|
beforeEach(() => {
|
|
26
37
|
vi.clearAllMocks()
|
|
@@ -41,16 +52,40 @@ describe('TickManager', () => {
|
|
|
41
52
|
mockEmit = vi.fn()
|
|
42
53
|
mockDisposablesAdd = vi.fn()
|
|
43
54
|
|
|
55
|
+
// Create a mock inputs object with getters and setters
|
|
56
|
+
mockInputs = {
|
|
57
|
+
_currentScreenPoint: new Vec(100, 100),
|
|
58
|
+
getCurrentScreenPoint() {
|
|
59
|
+
return this._currentScreenPoint
|
|
60
|
+
},
|
|
61
|
+
get currentScreenPoint() {
|
|
62
|
+
return this.getCurrentScreenPoint()
|
|
63
|
+
},
|
|
64
|
+
setCurrentScreenPoint(value: Vec) {
|
|
65
|
+
this._currentScreenPoint = value
|
|
66
|
+
},
|
|
67
|
+
_pointerVelocity: new Vec(0, 0),
|
|
68
|
+
getPointerVelocity() {
|
|
69
|
+
return this._pointerVelocity
|
|
70
|
+
},
|
|
71
|
+
get pointerVelocity() {
|
|
72
|
+
return this.getPointerVelocity()
|
|
73
|
+
},
|
|
74
|
+
setPointerVelocity(value: Vec) {
|
|
75
|
+
this._pointerVelocity = value
|
|
76
|
+
},
|
|
77
|
+
updatePointerVelocity(_elapsed: number) {
|
|
78
|
+
// Mock implementation - no-op for tests
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
|
|
44
82
|
editor = {
|
|
45
83
|
emit: mockEmit,
|
|
46
84
|
disposables: {
|
|
47
85
|
add: mockDisposablesAdd,
|
|
48
86
|
},
|
|
49
|
-
inputs:
|
|
50
|
-
|
|
51
|
-
pointerVelocity: new Vec(0, 0),
|
|
52
|
-
},
|
|
53
|
-
} as any
|
|
87
|
+
inputs: mockInputs as unknown as Editor['inputs'],
|
|
88
|
+
} as unknown as Mocked<Editor>
|
|
54
89
|
|
|
55
90
|
tickManager = new TickManager(editor)
|
|
56
91
|
})
|
|
@@ -143,16 +178,6 @@ describe('TickManager', () => {
|
|
|
143
178
|
expect(mockEmit).toHaveBeenCalledTimes(2)
|
|
144
179
|
})
|
|
145
180
|
|
|
146
|
-
it('should update pointer velocity', () => {
|
|
147
|
-
const updatePointerVelocitySpy = vi.spyOn(tickManager as any, 'updatePointerVelocity')
|
|
148
|
-
tickManager.now = 1000
|
|
149
|
-
mockDateNow.mockReturnValue(1016)
|
|
150
|
-
|
|
151
|
-
tickManager.tick()
|
|
152
|
-
|
|
153
|
-
expect(updatePointerVelocitySpy).toHaveBeenCalledWith(16)
|
|
154
|
-
})
|
|
155
|
-
|
|
156
181
|
it('should schedule next RAF', () => {
|
|
157
182
|
tickManager.tick()
|
|
158
183
|
expect(mockRequestAnimationFrame).toHaveBeenCalled()
|
|
@@ -192,98 +217,6 @@ describe('TickManager', () => {
|
|
|
192
217
|
})
|
|
193
218
|
})
|
|
194
219
|
|
|
195
|
-
describe('updatePointerVelocity method', () => {
|
|
196
|
-
let updatePointerVelocity: any
|
|
197
|
-
|
|
198
|
-
beforeEach(() => {
|
|
199
|
-
// Access private method for testing
|
|
200
|
-
updatePointerVelocity = (tickManager as any).updatePointerVelocity.bind(tickManager)
|
|
201
|
-
// Reset the prevPoint to a known state
|
|
202
|
-
;(tickManager as any).prevPoint = new Vec(50, 50)
|
|
203
|
-
})
|
|
204
|
-
|
|
205
|
-
it('should return early if elapsed time is 0', () => {
|
|
206
|
-
const originalVelocity = editor.inputs.pointerVelocity.clone()
|
|
207
|
-
|
|
208
|
-
updatePointerVelocity(0)
|
|
209
|
-
|
|
210
|
-
expect(editor.inputs.pointerVelocity).toEqual(originalVelocity)
|
|
211
|
-
})
|
|
212
|
-
|
|
213
|
-
it('should calculate velocity based on pointer movement', () => {
|
|
214
|
-
editor.inputs.currentScreenPoint = new Vec(150, 150)
|
|
215
|
-
;(tickManager as any).prevPoint = new Vec(50, 50)
|
|
216
|
-
editor.inputs.pointerVelocity = new Vec(0, 0)
|
|
217
|
-
|
|
218
|
-
updatePointerVelocity(100) // 100ms elapsed
|
|
219
|
-
|
|
220
|
-
// Delta should be (100, 100), length = ~141.42, direction = (~0.707, ~0.707)
|
|
221
|
-
// Velocity should be length/elapsed = ~1.414 per ms in each direction
|
|
222
|
-
// But with linear interpolation (lrp factor 0.5), it will be halved
|
|
223
|
-
const expectedVelocity = new Vec(0.5, 0.5) // With lrp factor 0.5
|
|
224
|
-
expect(editor.inputs.pointerVelocity.x).toBeCloseTo(expectedVelocity.x, 1)
|
|
225
|
-
expect(editor.inputs.pointerVelocity.y).toBeCloseTo(expectedVelocity.y, 1)
|
|
226
|
-
})
|
|
227
|
-
|
|
228
|
-
it('should update prevPoint to current screen point', () => {
|
|
229
|
-
const newPoint = new Vec(200, 200)
|
|
230
|
-
editor.inputs.currentScreenPoint = newPoint
|
|
231
|
-
|
|
232
|
-
updatePointerVelocity(16)
|
|
233
|
-
|
|
234
|
-
expect((tickManager as any).prevPoint).toEqual(newPoint)
|
|
235
|
-
})
|
|
236
|
-
|
|
237
|
-
it('should use linear interpolation to smooth velocity', () => {
|
|
238
|
-
editor.inputs.currentScreenPoint = new Vec(150, 50)
|
|
239
|
-
;(tickManager as any).prevPoint = new Vec(50, 50)
|
|
240
|
-
editor.inputs.pointerVelocity = new Vec(2, 2)
|
|
241
|
-
|
|
242
|
-
updatePointerVelocity(100)
|
|
243
|
-
|
|
244
|
-
// Should interpolate between current velocity (2,2) and new velocity (1,0)
|
|
245
|
-
// lrp with factor 0.5 should give us something between them
|
|
246
|
-
expect(editor.inputs.pointerVelocity.x).toBeGreaterThan(0.5)
|
|
247
|
-
expect(editor.inputs.pointerVelocity.x).toBeLessThan(2)
|
|
248
|
-
})
|
|
249
|
-
|
|
250
|
-
it('should set very small velocity components to 0', () => {
|
|
251
|
-
editor.inputs.currentScreenPoint = new Vec(50.005, 50.005)
|
|
252
|
-
;(tickManager as any).prevPoint = new Vec(50, 50)
|
|
253
|
-
editor.inputs.pointerVelocity = new Vec(0, 0)
|
|
254
|
-
|
|
255
|
-
updatePointerVelocity(1000) // Long elapsed time = very small velocity
|
|
256
|
-
|
|
257
|
-
expect(editor.inputs.pointerVelocity.x).toBe(0)
|
|
258
|
-
expect(editor.inputs.pointerVelocity.y).toBe(0)
|
|
259
|
-
})
|
|
260
|
-
|
|
261
|
-
it('should handle zero movement (stationary pointer)', () => {
|
|
262
|
-
editor.inputs.currentScreenPoint = new Vec(100, 100)
|
|
263
|
-
;(tickManager as any).prevPoint = new Vec(100, 100)
|
|
264
|
-
editor.inputs.pointerVelocity = new Vec(1, 1)
|
|
265
|
-
|
|
266
|
-
updatePointerVelocity(16)
|
|
267
|
-
|
|
268
|
-
// Should interpolate towards zero velocity
|
|
269
|
-
expect(editor.inputs.pointerVelocity.x).toBeLessThan(1)
|
|
270
|
-
expect(editor.inputs.pointerVelocity.y).toBeLessThan(1)
|
|
271
|
-
})
|
|
272
|
-
|
|
273
|
-
it('should only update pointerVelocity if it actually changes', () => {
|
|
274
|
-
// Setup scenario where velocity won't change
|
|
275
|
-
editor.inputs.currentScreenPoint = new Vec(100, 100)
|
|
276
|
-
;(tickManager as any).prevPoint = new Vec(100, 100)
|
|
277
|
-
editor.inputs.pointerVelocity = new Vec(0, 0)
|
|
278
|
-
|
|
279
|
-
const originalVelocity = editor.inputs.pointerVelocity
|
|
280
|
-
updatePointerVelocity(16)
|
|
281
|
-
|
|
282
|
-
// Should still be the same object reference since no change occurred
|
|
283
|
-
expect(editor.inputs.pointerVelocity).toBe(originalVelocity)
|
|
284
|
-
})
|
|
285
|
-
})
|
|
286
|
-
|
|
287
220
|
describe('RAF throttling behavior', () => {
|
|
288
221
|
it('should use test-specific RAF behavior in test environment', () => {
|
|
289
222
|
// The TickManager should detect test environment and use requestAnimationFrame directly
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { throttleToNextFrame as _throttleToNextFrame, bind } from '@tldraw/utils'
|
|
2
|
-
import { Vec } from '../../../primitives/Vec'
|
|
3
2
|
import { Editor } from '../../Editor'
|
|
4
3
|
|
|
5
4
|
const throttleToNextFrame =
|
|
@@ -13,6 +12,7 @@ const throttleToNextFrame =
|
|
|
13
12
|
}
|
|
14
13
|
: _throttleToNextFrame
|
|
15
14
|
|
|
15
|
+
/** @internal */
|
|
16
16
|
export class TickManager {
|
|
17
17
|
constructor(public editor: Editor) {
|
|
18
18
|
this.editor.disposables.add(this.dispose)
|
|
@@ -40,7 +40,7 @@ export class TickManager {
|
|
|
40
40
|
const elapsed = now - this.now
|
|
41
41
|
this.now = now
|
|
42
42
|
|
|
43
|
-
this.updatePointerVelocity(elapsed)
|
|
43
|
+
this.editor.inputs.updatePointerVelocity(elapsed)
|
|
44
44
|
this.editor.emit('frame', elapsed)
|
|
45
45
|
this.editor.emit('tick', elapsed)
|
|
46
46
|
this.cancelRaf = throttleToNextFrame(this.tick)
|
|
@@ -53,34 +53,4 @@ export class TickManager {
|
|
|
53
53
|
|
|
54
54
|
this.cancelRaf?.()
|
|
55
55
|
}
|
|
56
|
-
|
|
57
|
-
private prevPoint = new Vec()
|
|
58
|
-
|
|
59
|
-
updatePointerVelocity(elapsed: number) {
|
|
60
|
-
const {
|
|
61
|
-
prevPoint,
|
|
62
|
-
editor: {
|
|
63
|
-
inputs: { currentScreenPoint, pointerVelocity },
|
|
64
|
-
},
|
|
65
|
-
} = this
|
|
66
|
-
|
|
67
|
-
if (elapsed === 0) return
|
|
68
|
-
|
|
69
|
-
const delta = Vec.Sub(currentScreenPoint, prevPoint)
|
|
70
|
-
this.prevPoint = currentScreenPoint.clone()
|
|
71
|
-
|
|
72
|
-
const length = delta.len()
|
|
73
|
-
const direction = length ? delta.div(length) : new Vec(0, 0)
|
|
74
|
-
|
|
75
|
-
// consider adjusting this with an easing rather than a linear interpolation
|
|
76
|
-
const next = pointerVelocity.clone().lrp(direction.mul(length / elapsed), 0.5)
|
|
77
|
-
|
|
78
|
-
// if the velocity is very small, just set it to 0
|
|
79
|
-
if (Math.abs(next.x) < 0.01) next.x = 0
|
|
80
|
-
if (Math.abs(next.y) < 0.01) next.y = 0
|
|
81
|
-
|
|
82
|
-
if (!pointerVelocity.equals(next)) {
|
|
83
|
-
this.editor.inputs.pointerVelocity = next
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
56
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ExtractShapeByProps } from '@tldraw/tlschema'
|
|
2
2
|
import { lerp } from '@tldraw/utils'
|
|
3
3
|
import { Geometry2d } from '../../primitives/geometry/Geometry2d'
|
|
4
4
|
import { Rectangle2d } from '../../primitives/geometry/Rectangle2d'
|
|
@@ -7,7 +7,7 @@ import { ShapeUtil, TLResizeInfo } from './ShapeUtil'
|
|
|
7
7
|
import { resizeBox } from './shared/resizeBox'
|
|
8
8
|
|
|
9
9
|
/** @public */
|
|
10
|
-
export type TLBaseBoxShape =
|
|
10
|
+
export type TLBaseBoxShape = ExtractShapeByProps<{ w: number; h: number }>
|
|
11
11
|
|
|
12
12
|
/** @public */
|
|
13
13
|
export abstract class BaseBoxShapeUtil<Shape extends TLBaseBoxShape> extends ShapeUtil<Shape> {
|