@tldraw/editor 5.2.0-canary.b5edd6c3c8b1 → 5.2.0-canary.bfe99a647fa6
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.js +1 -1
- package/dist-cjs/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.js +4 -2
- package/dist-cjs/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.js.map +2 -2
- package/dist-cjs/version.js +3 -3
- package/dist-cjs/version.js.map +1 -1
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.mjs +4 -2
- package/dist-esm/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.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/editor/managers/SpatialIndexManager/SpatialIndexManager.ts +9 -2
- package/src/version.ts +3 -3
package/dist-cjs/index.js
CHANGED
|
@@ -380,7 +380,7 @@ var import_uniq = require("./lib/utils/uniq");
|
|
|
380
380
|
var import_defaultThemes2 = require("./lib/editor/managers/ThemeManager/defaultThemes");
|
|
381
381
|
(0, import_utils.registerTldrawLibraryVersion)(
|
|
382
382
|
"@tldraw/editor",
|
|
383
|
-
"5.2.0-canary.
|
|
383
|
+
"5.2.0-canary.bfe99a647fa6",
|
|
384
384
|
"cjs"
|
|
385
385
|
);
|
|
386
386
|
//# sourceMappingURL=index.js.map
|
|
@@ -47,19 +47,21 @@ class SpatialIndexManager {
|
|
|
47
47
|
}
|
|
48
48
|
createSpatialIndexComputed() {
|
|
49
49
|
const shapeHistory = this.editor.store.query.filterHistory("shape");
|
|
50
|
+
const bindingHistory = this.editor.store.query.filterHistory("binding");
|
|
50
51
|
return (0, import_state.computed)("spatialIndex", (_prevValue, lastComputedEpoch) => {
|
|
51
52
|
if ((0, import_state.isUninitialized)(_prevValue)) {
|
|
52
53
|
return this.rebuildAndBumpEpoch();
|
|
53
54
|
}
|
|
54
55
|
const shapeDiff = shapeHistory.getDiffSince(lastComputedEpoch);
|
|
55
|
-
|
|
56
|
+
const bindingDiff = bindingHistory.getDiffSince(lastComputedEpoch);
|
|
57
|
+
if (shapeDiff === import_state.RESET_VALUE || bindingDiff === import_state.RESET_VALUE) {
|
|
56
58
|
return this.rebuildAndBumpEpoch();
|
|
57
59
|
}
|
|
58
60
|
const currentPageId = this.editor.getCurrentPageId();
|
|
59
61
|
if (this.lastPageId !== currentPageId) {
|
|
60
62
|
return this.rebuildAndBumpEpoch();
|
|
61
63
|
}
|
|
62
|
-
if (shapeDiff.length === 0) return this._boundsEpoch;
|
|
64
|
+
if (shapeDiff.length === 0 && bindingDiff.length === 0) return this._boundsEpoch;
|
|
63
65
|
if (this.processIncrementalUpdate(shapeDiff)) {
|
|
64
66
|
this._boundsEpoch++;
|
|
65
67
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.ts"],
|
|
4
|
-
"sourcesContent": ["import { Computed, RESET_VALUE, computed, isUninitialized } from '@tldraw/state'\nimport type { RecordsDiff } from '@tldraw/store'\nimport { TLPageId, TLShape, TLShapeId, isShape } from '@tldraw/tlschema'\nimport type { TLRecord } from '@tldraw/tlschema'\nimport { objectMapValues } from '@tldraw/utils'\nimport { Box } from '../../../primitives/Box'\nimport type { Editor } from '../../Editor'\nimport { RBushIndex, type SpatialElement } from './RBushIndex'\n\n/**\n * Manages spatial indexing for efficient shape location queries.\n *\n * Uses an R-tree (via RBush) to enable O(log n) spatial queries instead of O(n) iteration.\n * Handles shapes with computed bounds (arrows, groups, custom shapes) by checking all shapes'\n * bounds on each update using the reactive bounds cache.\n *\n * Key features:\n * - Incremental updates using filterHistory pattern\n * - Leverages existing bounds cache reactivity for dependency tracking\n * - Works with any custom shape type with computed bounds\n * - Per-page index (rebuilds on page change)\n * - Optimized for viewport culling queries\n *\n * @internal\n */\nexport class SpatialIndexManager {\n\tprivate rbush: RBushIndex\n\tprivate spatialIndexComputed: Computed<number>\n\tprivate lastPageId: TLPageId | null = null\n\n\t// Bumps only when the rbush content may have changed. Consumers subscribe\n\t// via the computed; a stable epoch lets prop-only diffs skip downstream\n\t// invalidations.\n\tprivate _boundsEpoch = 0\n\n\tconstructor(public readonly editor: Editor) {\n\t\tthis.rbush = new RBushIndex()\n\t\tthis.spatialIndexComputed = this.createSpatialIndexComputed()\n\t}\n\n\tprivate rebuildAndBumpEpoch(): number {\n\t\tthis.buildFromScratch()\n\t\tthis._boundsEpoch++\n\t\treturn this._boundsEpoch\n\t}\n\n\tprivate createSpatialIndexComputed() {\n\t\tconst shapeHistory = this.editor.store.query.filterHistory('shape')\n\n\t\treturn computed<number>('spatialIndex', (_prevValue, lastComputedEpoch) => {\n\t\t\tif (isUninitialized(_prevValue)) {\n\t\t\t\treturn this.rebuildAndBumpEpoch()\n\t\t\t}\n\n\t\t\tconst shapeDiff = shapeHistory.getDiffSince(lastComputedEpoch)\n\n\t\t\tif (shapeDiff === RESET_VALUE) {\n\t\t\t\treturn this.rebuildAndBumpEpoch()\n\t\t\t}\n\n\t\t\tconst currentPageId = this.editor.getCurrentPageId()\n\t\t\tif (this.lastPageId !== currentPageId) {\n\t\t\t\treturn this.rebuildAndBumpEpoch()\n\t\t\t}\n\n\t\t\tif (shapeDiff.length === 0) return this._boundsEpoch\n\n\t\t\tif (this.processIncrementalUpdate(shapeDiff)) {\n\t\t\t\tthis._boundsEpoch++\n\t\t\t}\n\t\t\treturn this._boundsEpoch\n\t\t})\n\t}\n\n\tprivate buildFromScratch(): void {\n\t\tthis.rbush.clear()\n\t\tthis.lastPageId = this.editor.getCurrentPageId()\n\n\t\tconst elements: SpatialElement[] = []\n\t\tfor (const shape of this.editor.getCurrentPageShapes()) {\n\t\t\tconst bounds = this.editor.getShapePageBounds(shape.id)\n\t\t\tif (bounds && bounds.isValid()) {\n\t\t\t\telements.push({\n\t\t\t\t\tminX: bounds.minX,\n\t\t\t\t\tminY: bounds.minY,\n\t\t\t\t\tmaxX: bounds.maxX,\n\t\t\t\t\tmaxY: bounds.maxY,\n\t\t\t\t\tid: shape.id,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\n\t\t// Bulk load for efficiency\n\t\tthis.rbush.bulkLoad(elements)\n\t}\n\n\tprivate processIncrementalUpdate(shapeDiff: RecordsDiff<TLRecord>[]): boolean {\n\t\tconst processedShapeIds = new Set<TLShapeId>()\n\t\tlet changed = false\n\n\t\t// Step 1: apply diff entries directly. `changed` flips only on real\n\t\t// rbush mutations, so prop-only updates and no-op removes (e.g. shapes\n\t\t// from other pages, or never-indexed shapes with invalid bounds) don't\n\t\t// bump the epoch.\n\t\tfor (const changes of shapeDiff) {\n\t\t\tfor (const shape of objectMapValues(changes.added) as TLShape[]) {\n\t\t\t\tif (isShape(shape) && this.editor.getAncestorPageId(shape) === this.lastPageId) {\n\t\t\t\t\tconst bounds = this.editor.getShapePageBounds(shape.id)\n\t\t\t\t\tif (bounds && bounds.isValid()) {\n\t\t\t\t\t\tthis.rbush.upsert(shape.id, bounds)\n\t\t\t\t\t\tchanged = true\n\t\t\t\t\t}\n\t\t\t\t\tprocessedShapeIds.add(shape.id)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfor (const shape of objectMapValues(changes.removed) as TLShape[]) {\n\t\t\t\tif (isShape(shape)) {\n\t\t\t\t\tif (this.rbush.has(shape.id)) {\n\t\t\t\t\t\tthis.rbush.remove(shape.id)\n\t\t\t\t\t\tchanged = true\n\t\t\t\t\t}\n\t\t\t\t\tprocessedShapeIds.add(shape.id)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfor (const [, to] of objectMapValues(changes.updated) as [TLShape, TLShape][]) {\n\t\t\t\tif (!isShape(to)) continue\n\t\t\t\tprocessedShapeIds.add(to.id)\n\n\t\t\t\tconst isOnPage = this.editor.getAncestorPageId(to) === this.lastPageId\n\n\t\t\t\tif (isOnPage) {\n\t\t\t\t\tconst bounds = this.editor.getShapePageBounds(to.id)\n\t\t\t\t\tif (bounds && bounds.isValid()) {\n\t\t\t\t\t\tconst indexedElement = this.rbush.getElement(to.id)\n\t\t\t\t\t\tif (!this.areBoundsEqualToSpatialElement(bounds, indexedElement)) {\n\t\t\t\t\t\t\tthis.rbush.upsert(to.id, bounds)\n\t\t\t\t\t\t\tchanged = true\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if (this.rbush.has(to.id)) {\n\t\t\t\t\t\tthis.rbush.remove(to.id)\n\t\t\t\t\t\tchanged = true\n\t\t\t\t\t}\n\t\t\t\t} else if (this.rbush.has(to.id)) {\n\t\t\t\t\tthis.rbush.remove(to.id)\n\t\t\t\t\tchanged = true\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Step 2: must always run. Diff entries can dirty derived bounds \u2014\n\t\t// arrows bound to moved shapes, groups with moved children \u2014 without\n\t\t// touching any record visited in step 1. Also catches outline-only\n\t\t// changes (e.g. geo rectangle\u2192ellipse at the same w/h) that shift a\n\t\t// bound arrow's intersection points: step 1 sees the geo's\n\t\t// axis-aligned bounds unchanged and skips, but the dependent arrow's\n\t\t// bounds have moved.\n\t\t//\n\t\t// Iterating the rbush's element map directly avoids allocating a\n\t\t// shape-id array per pointer move. Mutation here is limited to\n\t\t// upserts of existing keys and deletions, both safe during Map\n\t\t// iteration.\n\t\tfor (const [shapeId, indexedElement] of this.rbush.entries()) {\n\t\t\tif (processedShapeIds.has(shapeId)) continue\n\n\t\t\tconst currentBounds = this.editor.getShapePageBounds(shapeId)\n\t\t\tif (this.areBoundsEqualToSpatialElement(currentBounds, indexedElement)) continue\n\n\t\t\tif (currentBounds && currentBounds.isValid()) {\n\t\t\t\tthis.rbush.upsert(shapeId, currentBounds)\n\t\t\t} else {\n\t\t\t\tthis.rbush.remove(shapeId)\n\t\t\t}\n\t\t\tchanged = true\n\t\t}\n\n\t\treturn changed\n\t}\n\n\tprivate areBoundsEqualToSpatialElement(\n\t\ta: Box | undefined,\n\t\tb: SpatialElement | undefined\n\t): boolean {\n\t\tif (!a && !b) return true\n\t\tif (!a || !b) return false\n\t\treturn a.minX === b.minX && a.minY === b.minY && a.maxX === b.maxX && a.maxY === b.maxY\n\t}\n\n\t/**\n\t * Get shape IDs within the given bounds.\n\t * Optimized for viewport culling queries.\n\t *\n\t * Note: Results are unordered. If you need z-order, combine with sorted shapes:\n\t * ```ts\n\t * const candidates = editor.spatialIndex.getShapeIdsInsideBounds(bounds)\n\t * const sorted = editor.getCurrentPageShapesSorted().filter(s => candidates.has(s.id))\n\t * ```\n\t *\n\t * @param bounds - The bounds to search within\n\t * @returns Unordered set of shape IDs within the bounds\n\t *\n\t * @public\n\t */\n\tgetShapeIdsInsideBounds(bounds: Box): Set<TLShapeId> {\n\t\tthis.spatialIndexComputed.get()\n\t\treturn this.rbush.search(bounds)\n\t}\n\n\t/**\n\t * Get shape IDs at a point (with optional margin).\n\t * Creates a small bounding box around the point and searches the spatial index.\n\t *\n\t * Note: Results are unordered. If you need z-order, combine with sorted shapes:\n\t * ```ts\n\t * const candidates = editor.spatialIndex.getShapeIdsAtPoint(point, margin)\n\t * const sorted = editor.getCurrentPageShapesSorted().filter(s => candidates.has(s.id))\n\t * ```\n\t *\n\t * @param point - The point to search at\n\t * @param margin - The margin around the point to search (default: 0)\n\t * @returns Unordered set of shape IDs that could potentially contain the point\n\t *\n\t * @public\n\t */\n\tgetShapeIdsAtPoint(point: { x: number; y: number }, margin = 0): Set<TLShapeId> {\n\t\tthis.spatialIndexComputed.get()\n\t\treturn this.rbush.search(new Box(point.x - margin, point.y - margin, margin * 2, margin * 2))\n\t}\n\n\t/**\n\t * Dispose of the spatial index manager.\n\t * Clears the R-tree to prevent memory leaks.\n\t *\n\t * @public\n\t */\n\tdispose(): void {\n\t\tthis.rbush.dispose()\n\t\tthis.lastPageId = null\n\t}\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAAiE;AAEjE,sBAAsD;AAEtD,mBAAgC;AAChC,iBAAoB;AAEpB,wBAAgD;AAkBzC,MAAM,oBAAoB;AAAA,EAUhC,YAA4B,QAAgB;AAAhB;AAC3B,SAAK,QAAQ,IAAI,6BAAW;AAC5B,SAAK,uBAAuB,KAAK,2BAA2B;AAAA,EAC7D;AAAA,EAH4B;AAAA,EATpB;AAAA,EACA;AAAA,EACA,aAA8B;AAAA;AAAA;AAAA;AAAA,EAK9B,eAAe;AAAA,EAOf,sBAA8B;AACrC,SAAK,iBAAiB;AACtB,SAAK;AACL,WAAO,KAAK;AAAA,EACb;AAAA,EAEQ,6BAA6B;AACpC,UAAM,eAAe,KAAK,OAAO,MAAM,MAAM,cAAc,OAAO;
|
|
4
|
+
"sourcesContent": ["import { Computed, RESET_VALUE, computed, isUninitialized } from '@tldraw/state'\nimport type { RecordsDiff } from '@tldraw/store'\nimport { TLPageId, TLShape, TLShapeId, isShape } from '@tldraw/tlschema'\nimport type { TLRecord } from '@tldraw/tlschema'\nimport { objectMapValues } from '@tldraw/utils'\nimport { Box } from '../../../primitives/Box'\nimport type { Editor } from '../../Editor'\nimport { RBushIndex, type SpatialElement } from './RBushIndex'\n\n/**\n * Manages spatial indexing for efficient shape location queries.\n *\n * Uses an R-tree (via RBush) to enable O(log n) spatial queries instead of O(n) iteration.\n * Handles shapes with computed bounds (arrows, groups, custom shapes) by checking all shapes'\n * bounds on each update using the reactive bounds cache.\n *\n * Key features:\n * - Incremental updates using filterHistory pattern\n * - Leverages existing bounds cache reactivity for dependency tracking\n * - Works with any custom shape type with computed bounds\n * - Per-page index (rebuilds on page change)\n * - Optimized for viewport culling queries\n *\n * @internal\n */\nexport class SpatialIndexManager {\n\tprivate rbush: RBushIndex\n\tprivate spatialIndexComputed: Computed<number>\n\tprivate lastPageId: TLPageId | null = null\n\n\t// Bumps only when the rbush content may have changed. Consumers subscribe\n\t// via the computed; a stable epoch lets prop-only diffs skip downstream\n\t// invalidations.\n\tprivate _boundsEpoch = 0\n\n\tconstructor(public readonly editor: Editor) {\n\t\tthis.rbush = new RBushIndex()\n\t\tthis.spatialIndexComputed = this.createSpatialIndexComputed()\n\t}\n\n\tprivate rebuildAndBumpEpoch(): number {\n\t\tthis.buildFromScratch()\n\t\tthis._boundsEpoch++\n\t\treturn this._boundsEpoch\n\t}\n\n\tprivate createSpatialIndexComputed() {\n\t\tconst shapeHistory = this.editor.store.query.filterHistory('shape')\n\t\t// Binding changes can move a shape's derived bounds (e.g. creating or\n\t\t// deleting an arrow binding relocates the arrow's body) without\n\t\t// touching any shape record, so they must also invalidate the index.\n\t\tconst bindingHistory = this.editor.store.query.filterHistory('binding')\n\n\t\treturn computed<number>('spatialIndex', (_prevValue, lastComputedEpoch) => {\n\t\t\tif (isUninitialized(_prevValue)) {\n\t\t\t\treturn this.rebuildAndBumpEpoch()\n\t\t\t}\n\n\t\t\tconst shapeDiff = shapeHistory.getDiffSince(lastComputedEpoch)\n\t\t\tconst bindingDiff = bindingHistory.getDiffSince(lastComputedEpoch)\n\n\t\t\tif (shapeDiff === RESET_VALUE || bindingDiff === RESET_VALUE) {\n\t\t\t\treturn this.rebuildAndBumpEpoch()\n\t\t\t}\n\n\t\t\tconst currentPageId = this.editor.getCurrentPageId()\n\t\t\tif (this.lastPageId !== currentPageId) {\n\t\t\t\treturn this.rebuildAndBumpEpoch()\n\t\t\t}\n\n\t\t\tif (shapeDiff.length === 0 && bindingDiff.length === 0) return this._boundsEpoch\n\n\t\t\t// A binding-only diff passes an empty shape diff: step 1 is a no-op\n\t\t\t// and the step-2 sweep re-checks the indexed bounds of every shape.\n\t\t\tif (this.processIncrementalUpdate(shapeDiff)) {\n\t\t\t\tthis._boundsEpoch++\n\t\t\t}\n\t\t\treturn this._boundsEpoch\n\t\t})\n\t}\n\n\tprivate buildFromScratch(): void {\n\t\tthis.rbush.clear()\n\t\tthis.lastPageId = this.editor.getCurrentPageId()\n\n\t\tconst elements: SpatialElement[] = []\n\t\tfor (const shape of this.editor.getCurrentPageShapes()) {\n\t\t\tconst bounds = this.editor.getShapePageBounds(shape.id)\n\t\t\tif (bounds && bounds.isValid()) {\n\t\t\t\telements.push({\n\t\t\t\t\tminX: bounds.minX,\n\t\t\t\t\tminY: bounds.minY,\n\t\t\t\t\tmaxX: bounds.maxX,\n\t\t\t\t\tmaxY: bounds.maxY,\n\t\t\t\t\tid: shape.id,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\n\t\t// Bulk load for efficiency\n\t\tthis.rbush.bulkLoad(elements)\n\t}\n\n\tprivate processIncrementalUpdate(shapeDiff: RecordsDiff<TLRecord>[]): boolean {\n\t\tconst processedShapeIds = new Set<TLShapeId>()\n\t\tlet changed = false\n\n\t\t// Step 1: apply diff entries directly. `changed` flips only on real\n\t\t// rbush mutations, so prop-only updates and no-op removes (e.g. shapes\n\t\t// from other pages, or never-indexed shapes with invalid bounds) don't\n\t\t// bump the epoch.\n\t\tfor (const changes of shapeDiff) {\n\t\t\tfor (const shape of objectMapValues(changes.added) as TLShape[]) {\n\t\t\t\tif (isShape(shape) && this.editor.getAncestorPageId(shape) === this.lastPageId) {\n\t\t\t\t\tconst bounds = this.editor.getShapePageBounds(shape.id)\n\t\t\t\t\tif (bounds && bounds.isValid()) {\n\t\t\t\t\t\tthis.rbush.upsert(shape.id, bounds)\n\t\t\t\t\t\tchanged = true\n\t\t\t\t\t}\n\t\t\t\t\tprocessedShapeIds.add(shape.id)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfor (const shape of objectMapValues(changes.removed) as TLShape[]) {\n\t\t\t\tif (isShape(shape)) {\n\t\t\t\t\tif (this.rbush.has(shape.id)) {\n\t\t\t\t\t\tthis.rbush.remove(shape.id)\n\t\t\t\t\t\tchanged = true\n\t\t\t\t\t}\n\t\t\t\t\tprocessedShapeIds.add(shape.id)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfor (const [, to] of objectMapValues(changes.updated) as [TLShape, TLShape][]) {\n\t\t\t\tif (!isShape(to)) continue\n\t\t\t\tprocessedShapeIds.add(to.id)\n\n\t\t\t\tconst isOnPage = this.editor.getAncestorPageId(to) === this.lastPageId\n\n\t\t\t\tif (isOnPage) {\n\t\t\t\t\tconst bounds = this.editor.getShapePageBounds(to.id)\n\t\t\t\t\tif (bounds && bounds.isValid()) {\n\t\t\t\t\t\tconst indexedElement = this.rbush.getElement(to.id)\n\t\t\t\t\t\tif (!this.areBoundsEqualToSpatialElement(bounds, indexedElement)) {\n\t\t\t\t\t\t\tthis.rbush.upsert(to.id, bounds)\n\t\t\t\t\t\t\tchanged = true\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if (this.rbush.has(to.id)) {\n\t\t\t\t\t\tthis.rbush.remove(to.id)\n\t\t\t\t\t\tchanged = true\n\t\t\t\t\t}\n\t\t\t\t} else if (this.rbush.has(to.id)) {\n\t\t\t\t\tthis.rbush.remove(to.id)\n\t\t\t\t\tchanged = true\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Step 2: must always run. Diff entries can dirty derived bounds \u2014\n\t\t// arrows bound to moved shapes, groups with moved children \u2014 without\n\t\t// touching any record visited in step 1. Also catches outline-only\n\t\t// changes (e.g. geo rectangle\u2192ellipse at the same w/h) that shift a\n\t\t// bound arrow's intersection points: step 1 sees the geo's\n\t\t// axis-aligned bounds unchanged and skips, but the dependent arrow's\n\t\t// bounds have moved.\n\t\t//\n\t\t// Iterating the rbush's element map directly avoids allocating a\n\t\t// shape-id array per pointer move. Mutation here is limited to\n\t\t// upserts of existing keys and deletions, both safe during Map\n\t\t// iteration.\n\t\tfor (const [shapeId, indexedElement] of this.rbush.entries()) {\n\t\t\tif (processedShapeIds.has(shapeId)) continue\n\n\t\t\tconst currentBounds = this.editor.getShapePageBounds(shapeId)\n\t\t\tif (this.areBoundsEqualToSpatialElement(currentBounds, indexedElement)) continue\n\n\t\t\tif (currentBounds && currentBounds.isValid()) {\n\t\t\t\tthis.rbush.upsert(shapeId, currentBounds)\n\t\t\t} else {\n\t\t\t\tthis.rbush.remove(shapeId)\n\t\t\t}\n\t\t\tchanged = true\n\t\t}\n\n\t\treturn changed\n\t}\n\n\tprivate areBoundsEqualToSpatialElement(\n\t\ta: Box | undefined,\n\t\tb: SpatialElement | undefined\n\t): boolean {\n\t\tif (!a && !b) return true\n\t\tif (!a || !b) return false\n\t\treturn a.minX === b.minX && a.minY === b.minY && a.maxX === b.maxX && a.maxY === b.maxY\n\t}\n\n\t/**\n\t * Get shape IDs within the given bounds.\n\t * Optimized for viewport culling queries.\n\t *\n\t * Note: Results are unordered. If you need z-order, combine with sorted shapes:\n\t * ```ts\n\t * const candidates = editor.spatialIndex.getShapeIdsInsideBounds(bounds)\n\t * const sorted = editor.getCurrentPageShapesSorted().filter(s => candidates.has(s.id))\n\t * ```\n\t *\n\t * @param bounds - The bounds to search within\n\t * @returns Unordered set of shape IDs within the bounds\n\t *\n\t * @public\n\t */\n\tgetShapeIdsInsideBounds(bounds: Box): Set<TLShapeId> {\n\t\tthis.spatialIndexComputed.get()\n\t\treturn this.rbush.search(bounds)\n\t}\n\n\t/**\n\t * Get shape IDs at a point (with optional margin).\n\t * Creates a small bounding box around the point and searches the spatial index.\n\t *\n\t * Note: Results are unordered. If you need z-order, combine with sorted shapes:\n\t * ```ts\n\t * const candidates = editor.spatialIndex.getShapeIdsAtPoint(point, margin)\n\t * const sorted = editor.getCurrentPageShapesSorted().filter(s => candidates.has(s.id))\n\t * ```\n\t *\n\t * @param point - The point to search at\n\t * @param margin - The margin around the point to search (default: 0)\n\t * @returns Unordered set of shape IDs that could potentially contain the point\n\t *\n\t * @public\n\t */\n\tgetShapeIdsAtPoint(point: { x: number; y: number }, margin = 0): Set<TLShapeId> {\n\t\tthis.spatialIndexComputed.get()\n\t\treturn this.rbush.search(new Box(point.x - margin, point.y - margin, margin * 2, margin * 2))\n\t}\n\n\t/**\n\t * Dispose of the spatial index manager.\n\t * Clears the R-tree to prevent memory leaks.\n\t *\n\t * @public\n\t */\n\tdispose(): void {\n\t\tthis.rbush.dispose()\n\t\tthis.lastPageId = null\n\t}\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAAiE;AAEjE,sBAAsD;AAEtD,mBAAgC;AAChC,iBAAoB;AAEpB,wBAAgD;AAkBzC,MAAM,oBAAoB;AAAA,EAUhC,YAA4B,QAAgB;AAAhB;AAC3B,SAAK,QAAQ,IAAI,6BAAW;AAC5B,SAAK,uBAAuB,KAAK,2BAA2B;AAAA,EAC7D;AAAA,EAH4B;AAAA,EATpB;AAAA,EACA;AAAA,EACA,aAA8B;AAAA;AAAA;AAAA;AAAA,EAK9B,eAAe;AAAA,EAOf,sBAA8B;AACrC,SAAK,iBAAiB;AACtB,SAAK;AACL,WAAO,KAAK;AAAA,EACb;AAAA,EAEQ,6BAA6B;AACpC,UAAM,eAAe,KAAK,OAAO,MAAM,MAAM,cAAc,OAAO;AAIlE,UAAM,iBAAiB,KAAK,OAAO,MAAM,MAAM,cAAc,SAAS;AAEtE,eAAO,uBAAiB,gBAAgB,CAAC,YAAY,sBAAsB;AAC1E,cAAI,8BAAgB,UAAU,GAAG;AAChC,eAAO,KAAK,oBAAoB;AAAA,MACjC;AAEA,YAAM,YAAY,aAAa,aAAa,iBAAiB;AAC7D,YAAM,cAAc,eAAe,aAAa,iBAAiB;AAEjE,UAAI,cAAc,4BAAe,gBAAgB,0BAAa;AAC7D,eAAO,KAAK,oBAAoB;AAAA,MACjC;AAEA,YAAM,gBAAgB,KAAK,OAAO,iBAAiB;AACnD,UAAI,KAAK,eAAe,eAAe;AACtC,eAAO,KAAK,oBAAoB;AAAA,MACjC;AAEA,UAAI,UAAU,WAAW,KAAK,YAAY,WAAW,EAAG,QAAO,KAAK;AAIpE,UAAI,KAAK,yBAAyB,SAAS,GAAG;AAC7C,aAAK;AAAA,MACN;AACA,aAAO,KAAK;AAAA,IACb,CAAC;AAAA,EACF;AAAA,EAEQ,mBAAyB;AAChC,SAAK,MAAM,MAAM;AACjB,SAAK,aAAa,KAAK,OAAO,iBAAiB;AAE/C,UAAM,WAA6B,CAAC;AACpC,eAAW,SAAS,KAAK,OAAO,qBAAqB,GAAG;AACvD,YAAM,SAAS,KAAK,OAAO,mBAAmB,MAAM,EAAE;AACtD,UAAI,UAAU,OAAO,QAAQ,GAAG;AAC/B,iBAAS,KAAK;AAAA,UACb,MAAM,OAAO;AAAA,UACb,MAAM,OAAO;AAAA,UACb,MAAM,OAAO;AAAA,UACb,MAAM,OAAO;AAAA,UACb,IAAI,MAAM;AAAA,QACX,CAAC;AAAA,MACF;AAAA,IACD;AAGA,SAAK,MAAM,SAAS,QAAQ;AAAA,EAC7B;AAAA,EAEQ,yBAAyB,WAA6C;AAC7E,UAAM,oBAAoB,oBAAI,IAAe;AAC7C,QAAI,UAAU;AAMd,eAAW,WAAW,WAAW;AAChC,iBAAW,aAAS,8BAAgB,QAAQ,KAAK,GAAgB;AAChE,gBAAI,yBAAQ,KAAK,KAAK,KAAK,OAAO,kBAAkB,KAAK,MAAM,KAAK,YAAY;AAC/E,gBAAM,SAAS,KAAK,OAAO,mBAAmB,MAAM,EAAE;AACtD,cAAI,UAAU,OAAO,QAAQ,GAAG;AAC/B,iBAAK,MAAM,OAAO,MAAM,IAAI,MAAM;AAClC,sBAAU;AAAA,UACX;AACA,4BAAkB,IAAI,MAAM,EAAE;AAAA,QAC/B;AAAA,MACD;AAEA,iBAAW,aAAS,8BAAgB,QAAQ,OAAO,GAAgB;AAClE,gBAAI,yBAAQ,KAAK,GAAG;AACnB,cAAI,KAAK,MAAM,IAAI,MAAM,EAAE,GAAG;AAC7B,iBAAK,MAAM,OAAO,MAAM,EAAE;AAC1B,sBAAU;AAAA,UACX;AACA,4BAAkB,IAAI,MAAM,EAAE;AAAA,QAC/B;AAAA,MACD;AAEA,iBAAW,CAAC,EAAE,EAAE,SAAK,8BAAgB,QAAQ,OAAO,GAA2B;AAC9E,YAAI,KAAC,yBAAQ,EAAE,EAAG;AAClB,0BAAkB,IAAI,GAAG,EAAE;AAE3B,cAAM,WAAW,KAAK,OAAO,kBAAkB,EAAE,MAAM,KAAK;AAE5D,YAAI,UAAU;AACb,gBAAM,SAAS,KAAK,OAAO,mBAAmB,GAAG,EAAE;AACnD,cAAI,UAAU,OAAO,QAAQ,GAAG;AAC/B,kBAAM,iBAAiB,KAAK,MAAM,WAAW,GAAG,EAAE;AAClD,gBAAI,CAAC,KAAK,+BAA+B,QAAQ,cAAc,GAAG;AACjE,mBAAK,MAAM,OAAO,GAAG,IAAI,MAAM;AAC/B,wBAAU;AAAA,YACX;AAAA,UACD,WAAW,KAAK,MAAM,IAAI,GAAG,EAAE,GAAG;AACjC,iBAAK,MAAM,OAAO,GAAG,EAAE;AACvB,sBAAU;AAAA,UACX;AAAA,QACD,WAAW,KAAK,MAAM,IAAI,GAAG,EAAE,GAAG;AACjC,eAAK,MAAM,OAAO,GAAG,EAAE;AACvB,oBAAU;AAAA,QACX;AAAA,MACD;AAAA,IACD;AAcA,eAAW,CAAC,SAAS,cAAc,KAAK,KAAK,MAAM,QAAQ,GAAG;AAC7D,UAAI,kBAAkB,IAAI,OAAO,EAAG;AAEpC,YAAM,gBAAgB,KAAK,OAAO,mBAAmB,OAAO;AAC5D,UAAI,KAAK,+BAA+B,eAAe,cAAc,EAAG;AAExE,UAAI,iBAAiB,cAAc,QAAQ,GAAG;AAC7C,aAAK,MAAM,OAAO,SAAS,aAAa;AAAA,MACzC,OAAO;AACN,aAAK,MAAM,OAAO,OAAO;AAAA,MAC1B;AACA,gBAAU;AAAA,IACX;AAEA,WAAO;AAAA,EACR;AAAA,EAEQ,+BACP,GACA,GACU;AACV,QAAI,CAAC,KAAK,CAAC,EAAG,QAAO;AACrB,QAAI,CAAC,KAAK,CAAC,EAAG,QAAO;AACrB,WAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE;AAAA,EACpF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAiBA,wBAAwB,QAA6B;AACpD,SAAK,qBAAqB,IAAI;AAC9B,WAAO,KAAK,MAAM,OAAO,MAAM;AAAA,EAChC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAkBA,mBAAmB,OAAiC,SAAS,GAAmB;AAC/E,SAAK,qBAAqB,IAAI;AAC9B,WAAO,KAAK,MAAM,OAAO,IAAI,eAAI,MAAM,IAAI,QAAQ,MAAM,IAAI,QAAQ,SAAS,GAAG,SAAS,CAAC,CAAC;AAAA,EAC7F;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,UAAgB;AACf,SAAK,MAAM,QAAQ;AACnB,SAAK,aAAa;AAAA,EACnB;AACD;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist-cjs/version.js
CHANGED
|
@@ -22,10 +22,10 @@ __export(version_exports, {
|
|
|
22
22
|
version: () => version
|
|
23
23
|
});
|
|
24
24
|
module.exports = __toCommonJS(version_exports);
|
|
25
|
-
const version = "5.2.0-canary.
|
|
25
|
+
const version = "5.2.0-canary.bfe99a647fa6";
|
|
26
26
|
const publishDates = {
|
|
27
27
|
major: "2026-05-06T16:28:18.473Z",
|
|
28
|
-
minor: "2026-06-
|
|
29
|
-
patch: "2026-06-
|
|
28
|
+
minor: "2026-06-13T21:47:37.302Z",
|
|
29
|
+
patch: "2026-06-13T21:47:37.302Z"
|
|
30
30
|
};
|
|
31
31
|
//# sourceMappingURL=version.js.map
|
package/dist-cjs/version.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/version.ts"],
|
|
4
|
-
"sourcesContent": ["// This file is automatically generated by internal/scripts/refresh-assets.ts.\n// Do not edit manually. Or do, I'm a comment, not a cop.\n\nexport const version = '5.2.0-canary.
|
|
4
|
+
"sourcesContent": ["// This file is automatically generated by internal/scripts/refresh-assets.ts.\n// Do not edit manually. Or do, I'm a comment, not a cop.\n\nexport const version = '5.2.0-canary.bfe99a647fa6'\nexport const publishDates = {\n\tmajor: '2026-05-06T16:28:18.473Z',\n\tminor: '2026-06-13T21:47:37.302Z',\n\tpatch: '2026-06-13T21:47:37.302Z',\n}\n"],
|
|
5
5
|
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAGO,MAAM,UAAU;AAChB,MAAM,eAAe;AAAA,EAC3B,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AACR;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist-esm/index.mjs
CHANGED
|
@@ -298,7 +298,7 @@ import { LocalIndexedDb, Table } from "./lib/utils/sync/LocalIndexedDb.mjs";
|
|
|
298
298
|
import { uniq } from "./lib/utils/uniq.mjs";
|
|
299
299
|
registerTldrawLibraryVersion(
|
|
300
300
|
"@tldraw/editor",
|
|
301
|
-
"5.2.0-canary.
|
|
301
|
+
"5.2.0-canary.bfe99a647fa6",
|
|
302
302
|
"esm"
|
|
303
303
|
);
|
|
304
304
|
import { getColorValue } from "./lib/editor/managers/ThemeManager/defaultThemes.mjs";
|
|
@@ -24,19 +24,21 @@ class SpatialIndexManager {
|
|
|
24
24
|
}
|
|
25
25
|
createSpatialIndexComputed() {
|
|
26
26
|
const shapeHistory = this.editor.store.query.filterHistory("shape");
|
|
27
|
+
const bindingHistory = this.editor.store.query.filterHistory("binding");
|
|
27
28
|
return computed("spatialIndex", (_prevValue, lastComputedEpoch) => {
|
|
28
29
|
if (isUninitialized(_prevValue)) {
|
|
29
30
|
return this.rebuildAndBumpEpoch();
|
|
30
31
|
}
|
|
31
32
|
const shapeDiff = shapeHistory.getDiffSince(lastComputedEpoch);
|
|
32
|
-
|
|
33
|
+
const bindingDiff = bindingHistory.getDiffSince(lastComputedEpoch);
|
|
34
|
+
if (shapeDiff === RESET_VALUE || bindingDiff === RESET_VALUE) {
|
|
33
35
|
return this.rebuildAndBumpEpoch();
|
|
34
36
|
}
|
|
35
37
|
const currentPageId = this.editor.getCurrentPageId();
|
|
36
38
|
if (this.lastPageId !== currentPageId) {
|
|
37
39
|
return this.rebuildAndBumpEpoch();
|
|
38
40
|
}
|
|
39
|
-
if (shapeDiff.length === 0) return this._boundsEpoch;
|
|
41
|
+
if (shapeDiff.length === 0 && bindingDiff.length === 0) return this._boundsEpoch;
|
|
40
42
|
if (this.processIncrementalUpdate(shapeDiff)) {
|
|
41
43
|
this._boundsEpoch++;
|
|
42
44
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.ts"],
|
|
4
|
-
"sourcesContent": ["import { Computed, RESET_VALUE, computed, isUninitialized } from '@tldraw/state'\nimport type { RecordsDiff } from '@tldraw/store'\nimport { TLPageId, TLShape, TLShapeId, isShape } from '@tldraw/tlschema'\nimport type { TLRecord } from '@tldraw/tlschema'\nimport { objectMapValues } from '@tldraw/utils'\nimport { Box } from '../../../primitives/Box'\nimport type { Editor } from '../../Editor'\nimport { RBushIndex, type SpatialElement } from './RBushIndex'\n\n/**\n * Manages spatial indexing for efficient shape location queries.\n *\n * Uses an R-tree (via RBush) to enable O(log n) spatial queries instead of O(n) iteration.\n * Handles shapes with computed bounds (arrows, groups, custom shapes) by checking all shapes'\n * bounds on each update using the reactive bounds cache.\n *\n * Key features:\n * - Incremental updates using filterHistory pattern\n * - Leverages existing bounds cache reactivity for dependency tracking\n * - Works with any custom shape type with computed bounds\n * - Per-page index (rebuilds on page change)\n * - Optimized for viewport culling queries\n *\n * @internal\n */\nexport class SpatialIndexManager {\n\tprivate rbush: RBushIndex\n\tprivate spatialIndexComputed: Computed<number>\n\tprivate lastPageId: TLPageId | null = null\n\n\t// Bumps only when the rbush content may have changed. Consumers subscribe\n\t// via the computed; a stable epoch lets prop-only diffs skip downstream\n\t// invalidations.\n\tprivate _boundsEpoch = 0\n\n\tconstructor(public readonly editor: Editor) {\n\t\tthis.rbush = new RBushIndex()\n\t\tthis.spatialIndexComputed = this.createSpatialIndexComputed()\n\t}\n\n\tprivate rebuildAndBumpEpoch(): number {\n\t\tthis.buildFromScratch()\n\t\tthis._boundsEpoch++\n\t\treturn this._boundsEpoch\n\t}\n\n\tprivate createSpatialIndexComputed() {\n\t\tconst shapeHistory = this.editor.store.query.filterHistory('shape')\n\n\t\treturn computed<number>('spatialIndex', (_prevValue, lastComputedEpoch) => {\n\t\t\tif (isUninitialized(_prevValue)) {\n\t\t\t\treturn this.rebuildAndBumpEpoch()\n\t\t\t}\n\n\t\t\tconst shapeDiff = shapeHistory.getDiffSince(lastComputedEpoch)\n\n\t\t\tif (shapeDiff === RESET_VALUE) {\n\t\t\t\treturn this.rebuildAndBumpEpoch()\n\t\t\t}\n\n\t\t\tconst currentPageId = this.editor.getCurrentPageId()\n\t\t\tif (this.lastPageId !== currentPageId) {\n\t\t\t\treturn this.rebuildAndBumpEpoch()\n\t\t\t}\n\n\t\t\tif (shapeDiff.length === 0) return this._boundsEpoch\n\n\t\t\tif (this.processIncrementalUpdate(shapeDiff)) {\n\t\t\t\tthis._boundsEpoch++\n\t\t\t}\n\t\t\treturn this._boundsEpoch\n\t\t})\n\t}\n\n\tprivate buildFromScratch(): void {\n\t\tthis.rbush.clear()\n\t\tthis.lastPageId = this.editor.getCurrentPageId()\n\n\t\tconst elements: SpatialElement[] = []\n\t\tfor (const shape of this.editor.getCurrentPageShapes()) {\n\t\t\tconst bounds = this.editor.getShapePageBounds(shape.id)\n\t\t\tif (bounds && bounds.isValid()) {\n\t\t\t\telements.push({\n\t\t\t\t\tminX: bounds.minX,\n\t\t\t\t\tminY: bounds.minY,\n\t\t\t\t\tmaxX: bounds.maxX,\n\t\t\t\t\tmaxY: bounds.maxY,\n\t\t\t\t\tid: shape.id,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\n\t\t// Bulk load for efficiency\n\t\tthis.rbush.bulkLoad(elements)\n\t}\n\n\tprivate processIncrementalUpdate(shapeDiff: RecordsDiff<TLRecord>[]): boolean {\n\t\tconst processedShapeIds = new Set<TLShapeId>()\n\t\tlet changed = false\n\n\t\t// Step 1: apply diff entries directly. `changed` flips only on real\n\t\t// rbush mutations, so prop-only updates and no-op removes (e.g. shapes\n\t\t// from other pages, or never-indexed shapes with invalid bounds) don't\n\t\t// bump the epoch.\n\t\tfor (const changes of shapeDiff) {\n\t\t\tfor (const shape of objectMapValues(changes.added) as TLShape[]) {\n\t\t\t\tif (isShape(shape) && this.editor.getAncestorPageId(shape) === this.lastPageId) {\n\t\t\t\t\tconst bounds = this.editor.getShapePageBounds(shape.id)\n\t\t\t\t\tif (bounds && bounds.isValid()) {\n\t\t\t\t\t\tthis.rbush.upsert(shape.id, bounds)\n\t\t\t\t\t\tchanged = true\n\t\t\t\t\t}\n\t\t\t\t\tprocessedShapeIds.add(shape.id)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfor (const shape of objectMapValues(changes.removed) as TLShape[]) {\n\t\t\t\tif (isShape(shape)) {\n\t\t\t\t\tif (this.rbush.has(shape.id)) {\n\t\t\t\t\t\tthis.rbush.remove(shape.id)\n\t\t\t\t\t\tchanged = true\n\t\t\t\t\t}\n\t\t\t\t\tprocessedShapeIds.add(shape.id)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfor (const [, to] of objectMapValues(changes.updated) as [TLShape, TLShape][]) {\n\t\t\t\tif (!isShape(to)) continue\n\t\t\t\tprocessedShapeIds.add(to.id)\n\n\t\t\t\tconst isOnPage = this.editor.getAncestorPageId(to) === this.lastPageId\n\n\t\t\t\tif (isOnPage) {\n\t\t\t\t\tconst bounds = this.editor.getShapePageBounds(to.id)\n\t\t\t\t\tif (bounds && bounds.isValid()) {\n\t\t\t\t\t\tconst indexedElement = this.rbush.getElement(to.id)\n\t\t\t\t\t\tif (!this.areBoundsEqualToSpatialElement(bounds, indexedElement)) {\n\t\t\t\t\t\t\tthis.rbush.upsert(to.id, bounds)\n\t\t\t\t\t\t\tchanged = true\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if (this.rbush.has(to.id)) {\n\t\t\t\t\t\tthis.rbush.remove(to.id)\n\t\t\t\t\t\tchanged = true\n\t\t\t\t\t}\n\t\t\t\t} else if (this.rbush.has(to.id)) {\n\t\t\t\t\tthis.rbush.remove(to.id)\n\t\t\t\t\tchanged = true\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Step 2: must always run. Diff entries can dirty derived bounds \u2014\n\t\t// arrows bound to moved shapes, groups with moved children \u2014 without\n\t\t// touching any record visited in step 1. Also catches outline-only\n\t\t// changes (e.g. geo rectangle\u2192ellipse at the same w/h) that shift a\n\t\t// bound arrow's intersection points: step 1 sees the geo's\n\t\t// axis-aligned bounds unchanged and skips, but the dependent arrow's\n\t\t// bounds have moved.\n\t\t//\n\t\t// Iterating the rbush's element map directly avoids allocating a\n\t\t// shape-id array per pointer move. Mutation here is limited to\n\t\t// upserts of existing keys and deletions, both safe during Map\n\t\t// iteration.\n\t\tfor (const [shapeId, indexedElement] of this.rbush.entries()) {\n\t\t\tif (processedShapeIds.has(shapeId)) continue\n\n\t\t\tconst currentBounds = this.editor.getShapePageBounds(shapeId)\n\t\t\tif (this.areBoundsEqualToSpatialElement(currentBounds, indexedElement)) continue\n\n\t\t\tif (currentBounds && currentBounds.isValid()) {\n\t\t\t\tthis.rbush.upsert(shapeId, currentBounds)\n\t\t\t} else {\n\t\t\t\tthis.rbush.remove(shapeId)\n\t\t\t}\n\t\t\tchanged = true\n\t\t}\n\n\t\treturn changed\n\t}\n\n\tprivate areBoundsEqualToSpatialElement(\n\t\ta: Box | undefined,\n\t\tb: SpatialElement | undefined\n\t): boolean {\n\t\tif (!a && !b) return true\n\t\tif (!a || !b) return false\n\t\treturn a.minX === b.minX && a.minY === b.minY && a.maxX === b.maxX && a.maxY === b.maxY\n\t}\n\n\t/**\n\t * Get shape IDs within the given bounds.\n\t * Optimized for viewport culling queries.\n\t *\n\t * Note: Results are unordered. If you need z-order, combine with sorted shapes:\n\t * ```ts\n\t * const candidates = editor.spatialIndex.getShapeIdsInsideBounds(bounds)\n\t * const sorted = editor.getCurrentPageShapesSorted().filter(s => candidates.has(s.id))\n\t * ```\n\t *\n\t * @param bounds - The bounds to search within\n\t * @returns Unordered set of shape IDs within the bounds\n\t *\n\t * @public\n\t */\n\tgetShapeIdsInsideBounds(bounds: Box): Set<TLShapeId> {\n\t\tthis.spatialIndexComputed.get()\n\t\treturn this.rbush.search(bounds)\n\t}\n\n\t/**\n\t * Get shape IDs at a point (with optional margin).\n\t * Creates a small bounding box around the point and searches the spatial index.\n\t *\n\t * Note: Results are unordered. If you need z-order, combine with sorted shapes:\n\t * ```ts\n\t * const candidates = editor.spatialIndex.getShapeIdsAtPoint(point, margin)\n\t * const sorted = editor.getCurrentPageShapesSorted().filter(s => candidates.has(s.id))\n\t * ```\n\t *\n\t * @param point - The point to search at\n\t * @param margin - The margin around the point to search (default: 0)\n\t * @returns Unordered set of shape IDs that could potentially contain the point\n\t *\n\t * @public\n\t */\n\tgetShapeIdsAtPoint(point: { x: number; y: number }, margin = 0): Set<TLShapeId> {\n\t\tthis.spatialIndexComputed.get()\n\t\treturn this.rbush.search(new Box(point.x - margin, point.y - margin, margin * 2, margin * 2))\n\t}\n\n\t/**\n\t * Dispose of the spatial index manager.\n\t * Clears the R-tree to prevent memory leaks.\n\t *\n\t * @public\n\t */\n\tdispose(): void {\n\t\tthis.rbush.dispose()\n\t\tthis.lastPageId = null\n\t}\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAmB,aAAa,UAAU,uBAAuB;AAEjE,SAAuC,eAAe;AAEtD,SAAS,uBAAuB;AAChC,SAAS,WAAW;AAEpB,SAAS,kBAAuC;AAkBzC,MAAM,oBAAoB;AAAA,EAUhC,YAA4B,QAAgB;AAAhB;AAC3B,SAAK,QAAQ,IAAI,WAAW;AAC5B,SAAK,uBAAuB,KAAK,2BAA2B;AAAA,EAC7D;AAAA,EAH4B;AAAA,EATpB;AAAA,EACA;AAAA,EACA,aAA8B;AAAA;AAAA;AAAA;AAAA,EAK9B,eAAe;AAAA,EAOf,sBAA8B;AACrC,SAAK,iBAAiB;AACtB,SAAK;AACL,WAAO,KAAK;AAAA,EACb;AAAA,EAEQ,6BAA6B;AACpC,UAAM,eAAe,KAAK,OAAO,MAAM,MAAM,cAAc,OAAO;
|
|
4
|
+
"sourcesContent": ["import { Computed, RESET_VALUE, computed, isUninitialized } from '@tldraw/state'\nimport type { RecordsDiff } from '@tldraw/store'\nimport { TLPageId, TLShape, TLShapeId, isShape } from '@tldraw/tlschema'\nimport type { TLRecord } from '@tldraw/tlschema'\nimport { objectMapValues } from '@tldraw/utils'\nimport { Box } from '../../../primitives/Box'\nimport type { Editor } from '../../Editor'\nimport { RBushIndex, type SpatialElement } from './RBushIndex'\n\n/**\n * Manages spatial indexing for efficient shape location queries.\n *\n * Uses an R-tree (via RBush) to enable O(log n) spatial queries instead of O(n) iteration.\n * Handles shapes with computed bounds (arrows, groups, custom shapes) by checking all shapes'\n * bounds on each update using the reactive bounds cache.\n *\n * Key features:\n * - Incremental updates using filterHistory pattern\n * - Leverages existing bounds cache reactivity for dependency tracking\n * - Works with any custom shape type with computed bounds\n * - Per-page index (rebuilds on page change)\n * - Optimized for viewport culling queries\n *\n * @internal\n */\nexport class SpatialIndexManager {\n\tprivate rbush: RBushIndex\n\tprivate spatialIndexComputed: Computed<number>\n\tprivate lastPageId: TLPageId | null = null\n\n\t// Bumps only when the rbush content may have changed. Consumers subscribe\n\t// via the computed; a stable epoch lets prop-only diffs skip downstream\n\t// invalidations.\n\tprivate _boundsEpoch = 0\n\n\tconstructor(public readonly editor: Editor) {\n\t\tthis.rbush = new RBushIndex()\n\t\tthis.spatialIndexComputed = this.createSpatialIndexComputed()\n\t}\n\n\tprivate rebuildAndBumpEpoch(): number {\n\t\tthis.buildFromScratch()\n\t\tthis._boundsEpoch++\n\t\treturn this._boundsEpoch\n\t}\n\n\tprivate createSpatialIndexComputed() {\n\t\tconst shapeHistory = this.editor.store.query.filterHistory('shape')\n\t\t// Binding changes can move a shape's derived bounds (e.g. creating or\n\t\t// deleting an arrow binding relocates the arrow's body) without\n\t\t// touching any shape record, so they must also invalidate the index.\n\t\tconst bindingHistory = this.editor.store.query.filterHistory('binding')\n\n\t\treturn computed<number>('spatialIndex', (_prevValue, lastComputedEpoch) => {\n\t\t\tif (isUninitialized(_prevValue)) {\n\t\t\t\treturn this.rebuildAndBumpEpoch()\n\t\t\t}\n\n\t\t\tconst shapeDiff = shapeHistory.getDiffSince(lastComputedEpoch)\n\t\t\tconst bindingDiff = bindingHistory.getDiffSince(lastComputedEpoch)\n\n\t\t\tif (shapeDiff === RESET_VALUE || bindingDiff === RESET_VALUE) {\n\t\t\t\treturn this.rebuildAndBumpEpoch()\n\t\t\t}\n\n\t\t\tconst currentPageId = this.editor.getCurrentPageId()\n\t\t\tif (this.lastPageId !== currentPageId) {\n\t\t\t\treturn this.rebuildAndBumpEpoch()\n\t\t\t}\n\n\t\t\tif (shapeDiff.length === 0 && bindingDiff.length === 0) return this._boundsEpoch\n\n\t\t\t// A binding-only diff passes an empty shape diff: step 1 is a no-op\n\t\t\t// and the step-2 sweep re-checks the indexed bounds of every shape.\n\t\t\tif (this.processIncrementalUpdate(shapeDiff)) {\n\t\t\t\tthis._boundsEpoch++\n\t\t\t}\n\t\t\treturn this._boundsEpoch\n\t\t})\n\t}\n\n\tprivate buildFromScratch(): void {\n\t\tthis.rbush.clear()\n\t\tthis.lastPageId = this.editor.getCurrentPageId()\n\n\t\tconst elements: SpatialElement[] = []\n\t\tfor (const shape of this.editor.getCurrentPageShapes()) {\n\t\t\tconst bounds = this.editor.getShapePageBounds(shape.id)\n\t\t\tif (bounds && bounds.isValid()) {\n\t\t\t\telements.push({\n\t\t\t\t\tminX: bounds.minX,\n\t\t\t\t\tminY: bounds.minY,\n\t\t\t\t\tmaxX: bounds.maxX,\n\t\t\t\t\tmaxY: bounds.maxY,\n\t\t\t\t\tid: shape.id,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\n\t\t// Bulk load for efficiency\n\t\tthis.rbush.bulkLoad(elements)\n\t}\n\n\tprivate processIncrementalUpdate(shapeDiff: RecordsDiff<TLRecord>[]): boolean {\n\t\tconst processedShapeIds = new Set<TLShapeId>()\n\t\tlet changed = false\n\n\t\t// Step 1: apply diff entries directly. `changed` flips only on real\n\t\t// rbush mutations, so prop-only updates and no-op removes (e.g. shapes\n\t\t// from other pages, or never-indexed shapes with invalid bounds) don't\n\t\t// bump the epoch.\n\t\tfor (const changes of shapeDiff) {\n\t\t\tfor (const shape of objectMapValues(changes.added) as TLShape[]) {\n\t\t\t\tif (isShape(shape) && this.editor.getAncestorPageId(shape) === this.lastPageId) {\n\t\t\t\t\tconst bounds = this.editor.getShapePageBounds(shape.id)\n\t\t\t\t\tif (bounds && bounds.isValid()) {\n\t\t\t\t\t\tthis.rbush.upsert(shape.id, bounds)\n\t\t\t\t\t\tchanged = true\n\t\t\t\t\t}\n\t\t\t\t\tprocessedShapeIds.add(shape.id)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfor (const shape of objectMapValues(changes.removed) as TLShape[]) {\n\t\t\t\tif (isShape(shape)) {\n\t\t\t\t\tif (this.rbush.has(shape.id)) {\n\t\t\t\t\t\tthis.rbush.remove(shape.id)\n\t\t\t\t\t\tchanged = true\n\t\t\t\t\t}\n\t\t\t\t\tprocessedShapeIds.add(shape.id)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfor (const [, to] of objectMapValues(changes.updated) as [TLShape, TLShape][]) {\n\t\t\t\tif (!isShape(to)) continue\n\t\t\t\tprocessedShapeIds.add(to.id)\n\n\t\t\t\tconst isOnPage = this.editor.getAncestorPageId(to) === this.lastPageId\n\n\t\t\t\tif (isOnPage) {\n\t\t\t\t\tconst bounds = this.editor.getShapePageBounds(to.id)\n\t\t\t\t\tif (bounds && bounds.isValid()) {\n\t\t\t\t\t\tconst indexedElement = this.rbush.getElement(to.id)\n\t\t\t\t\t\tif (!this.areBoundsEqualToSpatialElement(bounds, indexedElement)) {\n\t\t\t\t\t\t\tthis.rbush.upsert(to.id, bounds)\n\t\t\t\t\t\t\tchanged = true\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if (this.rbush.has(to.id)) {\n\t\t\t\t\t\tthis.rbush.remove(to.id)\n\t\t\t\t\t\tchanged = true\n\t\t\t\t\t}\n\t\t\t\t} else if (this.rbush.has(to.id)) {\n\t\t\t\t\tthis.rbush.remove(to.id)\n\t\t\t\t\tchanged = true\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Step 2: must always run. Diff entries can dirty derived bounds \u2014\n\t\t// arrows bound to moved shapes, groups with moved children \u2014 without\n\t\t// touching any record visited in step 1. Also catches outline-only\n\t\t// changes (e.g. geo rectangle\u2192ellipse at the same w/h) that shift a\n\t\t// bound arrow's intersection points: step 1 sees the geo's\n\t\t// axis-aligned bounds unchanged and skips, but the dependent arrow's\n\t\t// bounds have moved.\n\t\t//\n\t\t// Iterating the rbush's element map directly avoids allocating a\n\t\t// shape-id array per pointer move. Mutation here is limited to\n\t\t// upserts of existing keys and deletions, both safe during Map\n\t\t// iteration.\n\t\tfor (const [shapeId, indexedElement] of this.rbush.entries()) {\n\t\t\tif (processedShapeIds.has(shapeId)) continue\n\n\t\t\tconst currentBounds = this.editor.getShapePageBounds(shapeId)\n\t\t\tif (this.areBoundsEqualToSpatialElement(currentBounds, indexedElement)) continue\n\n\t\t\tif (currentBounds && currentBounds.isValid()) {\n\t\t\t\tthis.rbush.upsert(shapeId, currentBounds)\n\t\t\t} else {\n\t\t\t\tthis.rbush.remove(shapeId)\n\t\t\t}\n\t\t\tchanged = true\n\t\t}\n\n\t\treturn changed\n\t}\n\n\tprivate areBoundsEqualToSpatialElement(\n\t\ta: Box | undefined,\n\t\tb: SpatialElement | undefined\n\t): boolean {\n\t\tif (!a && !b) return true\n\t\tif (!a || !b) return false\n\t\treturn a.minX === b.minX && a.minY === b.minY && a.maxX === b.maxX && a.maxY === b.maxY\n\t}\n\n\t/**\n\t * Get shape IDs within the given bounds.\n\t * Optimized for viewport culling queries.\n\t *\n\t * Note: Results are unordered. If you need z-order, combine with sorted shapes:\n\t * ```ts\n\t * const candidates = editor.spatialIndex.getShapeIdsInsideBounds(bounds)\n\t * const sorted = editor.getCurrentPageShapesSorted().filter(s => candidates.has(s.id))\n\t * ```\n\t *\n\t * @param bounds - The bounds to search within\n\t * @returns Unordered set of shape IDs within the bounds\n\t *\n\t * @public\n\t */\n\tgetShapeIdsInsideBounds(bounds: Box): Set<TLShapeId> {\n\t\tthis.spatialIndexComputed.get()\n\t\treturn this.rbush.search(bounds)\n\t}\n\n\t/**\n\t * Get shape IDs at a point (with optional margin).\n\t * Creates a small bounding box around the point and searches the spatial index.\n\t *\n\t * Note: Results are unordered. If you need z-order, combine with sorted shapes:\n\t * ```ts\n\t * const candidates = editor.spatialIndex.getShapeIdsAtPoint(point, margin)\n\t * const sorted = editor.getCurrentPageShapesSorted().filter(s => candidates.has(s.id))\n\t * ```\n\t *\n\t * @param point - The point to search at\n\t * @param margin - The margin around the point to search (default: 0)\n\t * @returns Unordered set of shape IDs that could potentially contain the point\n\t *\n\t * @public\n\t */\n\tgetShapeIdsAtPoint(point: { x: number; y: number }, margin = 0): Set<TLShapeId> {\n\t\tthis.spatialIndexComputed.get()\n\t\treturn this.rbush.search(new Box(point.x - margin, point.y - margin, margin * 2, margin * 2))\n\t}\n\n\t/**\n\t * Dispose of the spatial index manager.\n\t * Clears the R-tree to prevent memory leaks.\n\t *\n\t * @public\n\t */\n\tdispose(): void {\n\t\tthis.rbush.dispose()\n\t\tthis.lastPageId = null\n\t}\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist-esm/version.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
const version = "5.2.0-canary.
|
|
1
|
+
const version = "5.2.0-canary.bfe99a647fa6";
|
|
2
2
|
const publishDates = {
|
|
3
3
|
major: "2026-05-06T16:28:18.473Z",
|
|
4
|
-
minor: "2026-06-
|
|
5
|
-
patch: "2026-06-
|
|
4
|
+
minor: "2026-06-13T21:47:37.302Z",
|
|
5
|
+
patch: "2026-06-13T21:47:37.302Z"
|
|
6
6
|
};
|
|
7
7
|
export {
|
|
8
8
|
publishDates,
|
package/dist-esm/version.mjs.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/version.ts"],
|
|
4
|
-
"sourcesContent": ["// This file is automatically generated by internal/scripts/refresh-assets.ts.\n// Do not edit manually. Or do, I'm a comment, not a cop.\n\nexport const version = '5.2.0-canary.
|
|
4
|
+
"sourcesContent": ["// This file is automatically generated by internal/scripts/refresh-assets.ts.\n// Do not edit manually. Or do, I'm a comment, not a cop.\n\nexport const version = '5.2.0-canary.bfe99a647fa6'\nexport const publishDates = {\n\tmajor: '2026-05-06T16:28:18.473Z',\n\tminor: '2026-06-13T21:47:37.302Z',\n\tpatch: '2026-06-13T21:47:37.302Z',\n}\n"],
|
|
5
5
|
"mappings": "AAGO,MAAM,UAAU;AAChB,MAAM,eAAe;AAAA,EAC3B,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AACR;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tldraw/editor",
|
|
3
3
|
"description": "tldraw infinite canvas SDK (editor).",
|
|
4
|
-
"version": "5.2.0-canary.
|
|
4
|
+
"version": "5.2.0-canary.bfe99a647fa6",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "tldraw Inc.",
|
|
7
7
|
"email": "hello@tldraw.com"
|
|
@@ -49,12 +49,12 @@
|
|
|
49
49
|
"@tiptap/core": "^3.12.1",
|
|
50
50
|
"@tiptap/pm": "^3.12.1",
|
|
51
51
|
"@tiptap/react": "^3.12.1",
|
|
52
|
-
"@tldraw/state": "5.2.0-canary.
|
|
53
|
-
"@tldraw/state-react": "5.2.0-canary.
|
|
54
|
-
"@tldraw/store": "5.2.0-canary.
|
|
55
|
-
"@tldraw/tlschema": "5.2.0-canary.
|
|
56
|
-
"@tldraw/utils": "5.2.0-canary.
|
|
57
|
-
"@tldraw/validate": "5.2.0-canary.
|
|
52
|
+
"@tldraw/state": "5.2.0-canary.bfe99a647fa6",
|
|
53
|
+
"@tldraw/state-react": "5.2.0-canary.bfe99a647fa6",
|
|
54
|
+
"@tldraw/store": "5.2.0-canary.bfe99a647fa6",
|
|
55
|
+
"@tldraw/tlschema": "5.2.0-canary.bfe99a647fa6",
|
|
56
|
+
"@tldraw/utils": "5.2.0-canary.bfe99a647fa6",
|
|
57
|
+
"@tldraw/validate": "5.2.0-canary.bfe99a647fa6",
|
|
58
58
|
"classnames": "^2.5.1",
|
|
59
59
|
"eventemitter3": "^4.0.7",
|
|
60
60
|
"idb": "^7.1.1",
|
|
@@ -46,6 +46,10 @@ export class SpatialIndexManager {
|
|
|
46
46
|
|
|
47
47
|
private createSpatialIndexComputed() {
|
|
48
48
|
const shapeHistory = this.editor.store.query.filterHistory('shape')
|
|
49
|
+
// Binding changes can move a shape's derived bounds (e.g. creating or
|
|
50
|
+
// deleting an arrow binding relocates the arrow's body) without
|
|
51
|
+
// touching any shape record, so they must also invalidate the index.
|
|
52
|
+
const bindingHistory = this.editor.store.query.filterHistory('binding')
|
|
49
53
|
|
|
50
54
|
return computed<number>('spatialIndex', (_prevValue, lastComputedEpoch) => {
|
|
51
55
|
if (isUninitialized(_prevValue)) {
|
|
@@ -53,8 +57,9 @@ export class SpatialIndexManager {
|
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
const shapeDiff = shapeHistory.getDiffSince(lastComputedEpoch)
|
|
60
|
+
const bindingDiff = bindingHistory.getDiffSince(lastComputedEpoch)
|
|
56
61
|
|
|
57
|
-
if (shapeDiff === RESET_VALUE) {
|
|
62
|
+
if (shapeDiff === RESET_VALUE || bindingDiff === RESET_VALUE) {
|
|
58
63
|
return this.rebuildAndBumpEpoch()
|
|
59
64
|
}
|
|
60
65
|
|
|
@@ -63,8 +68,10 @@ export class SpatialIndexManager {
|
|
|
63
68
|
return this.rebuildAndBumpEpoch()
|
|
64
69
|
}
|
|
65
70
|
|
|
66
|
-
if (shapeDiff.length === 0) return this._boundsEpoch
|
|
71
|
+
if (shapeDiff.length === 0 && bindingDiff.length === 0) return this._boundsEpoch
|
|
67
72
|
|
|
73
|
+
// A binding-only diff passes an empty shape diff: step 1 is a no-op
|
|
74
|
+
// and the step-2 sweep re-checks the indexed bounds of every shape.
|
|
68
75
|
if (this.processIncrementalUpdate(shapeDiff)) {
|
|
69
76
|
this._boundsEpoch++
|
|
70
77
|
}
|
package/src/version.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// This file is automatically generated by internal/scripts/refresh-assets.ts.
|
|
2
2
|
// Do not edit manually. Or do, I'm a comment, not a cop.
|
|
3
3
|
|
|
4
|
-
export const version = '5.2.0-canary.
|
|
4
|
+
export const version = '5.2.0-canary.bfe99a647fa6'
|
|
5
5
|
export const publishDates = {
|
|
6
6
|
major: '2026-05-06T16:28:18.473Z',
|
|
7
|
-
minor: '2026-06-
|
|
8
|
-
patch: '2026-06-
|
|
7
|
+
minor: '2026-06-13T21:47:37.302Z',
|
|
8
|
+
patch: '2026-06-13T21:47:37.302Z',
|
|
9
9
|
}
|