@tldraw/editor
Version:
tldraw infinite canvas SDK (editor).
182 lines (181 loc) • 6.65 kB
JavaScript
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var SpatialIndexManager_exports = {};
__export(SpatialIndexManager_exports, {
SpatialIndexManager: () => SpatialIndexManager
});
module.exports = __toCommonJS(SpatialIndexManager_exports);
var import_state = require("@tldraw/state");
var import_tlschema = require("@tldraw/tlschema");
var import_utils = require("@tldraw/utils");
var import_Box = require("../../../primitives/Box");
var import_RBushIndex = require("./RBushIndex");
class SpatialIndexManager {
constructor(editor) {
this.editor = editor;
this.rbush = new import_RBushIndex.RBushIndex();
this.spatialIndexComputed = this.createSpatialIndexComputed();
}
editor;
rbush;
spatialIndexComputed;
lastPageId = null;
createSpatialIndexComputed() {
const shapeHistory = this.editor.store.query.filterHistory("shape");
return (0, import_state.computed)("spatialIndex", (_prevValue, lastComputedEpoch) => {
if ((0, import_state.isUninitialized)(_prevValue)) {
return this.buildFromScratch(lastComputedEpoch);
}
const shapeDiff = shapeHistory.getDiffSince(lastComputedEpoch);
if (shapeDiff === import_state.RESET_VALUE) {
return this.buildFromScratch(lastComputedEpoch);
}
const currentPageId = this.editor.getCurrentPageId();
if (this.lastPageId !== currentPageId) {
return this.buildFromScratch(lastComputedEpoch);
}
if (shapeDiff.length === 0) {
return lastComputedEpoch;
}
this.processIncrementalUpdate(shapeDiff);
return lastComputedEpoch;
});
}
buildFromScratch(epoch) {
this.rbush.clear();
this.lastPageId = this.editor.getCurrentPageId();
const shapes = this.editor.getCurrentPageShapes();
const elements = [];
for (const shape of shapes) {
const bounds = this.editor.getShapePageBounds(shape.id);
if (bounds && bounds.isValid()) {
elements.push({
minX: bounds.minX,
minY: bounds.minY,
maxX: bounds.maxX,
maxY: bounds.maxY,
id: shape.id
});
}
}
this.rbush.bulkLoad(elements);
return epoch;
}
processIncrementalUpdate(shapeDiff) {
const processedShapeIds = /* @__PURE__ */ new Set();
for (const changes of shapeDiff) {
for (const shape of (0, import_utils.objectMapValues)(changes.added)) {
if ((0, import_tlschema.isShape)(shape) && this.editor.getAncestorPageId(shape) === this.lastPageId) {
const bounds = this.editor.getShapePageBounds(shape.id);
if (bounds && bounds.isValid()) {
this.rbush.upsert(shape.id, bounds);
}
processedShapeIds.add(shape.id);
}
}
for (const shape of (0, import_utils.objectMapValues)(changes.removed)) {
if ((0, import_tlschema.isShape)(shape)) {
this.rbush.remove(shape.id);
processedShapeIds.add(shape.id);
}
}
for (const [, to] of (0, import_utils.objectMapValues)(changes.updated)) {
if (!(0, import_tlschema.isShape)(to)) continue;
processedShapeIds.add(to.id);
const isOnPage = this.editor.getAncestorPageId(to) === this.lastPageId;
if (isOnPage) {
const bounds = this.editor.getShapePageBounds(to.id);
if (bounds && bounds.isValid()) {
this.rbush.upsert(to.id, bounds);
}
} else {
this.rbush.remove(to.id);
}
}
}
const allShapeIds = this.rbush.getAllShapeIds();
for (const shapeId of allShapeIds) {
if (processedShapeIds.has(shapeId)) continue;
const currentBounds = this.editor.getShapePageBounds(shapeId);
const indexedBounds = this.rbush.getBounds(shapeId);
if (!this.areBoundsEqual(currentBounds, indexedBounds)) {
if (currentBounds && currentBounds.isValid()) {
this.rbush.upsert(shapeId, currentBounds);
} else {
this.rbush.remove(shapeId);
}
}
}
}
areBoundsEqual(a, b) {
if (!a && !b) return true;
if (!a || !b) return false;
return a.minX === b.minX && a.minY === b.minY && a.maxX === b.maxX && a.maxY === b.maxY;
}
/**
* Get shape IDs within the given bounds.
* Optimized for viewport culling queries.
*
* Note: Results are unordered. If you need z-order, combine with sorted shapes:
* ```ts
* const candidates = editor.spatialIndex.getShapeIdsInsideBounds(bounds)
* const sorted = editor.getCurrentPageShapesSorted().filter(s => candidates.has(s.id))
* ```
*
* @param bounds - The bounds to search within
* @returns Unordered set of shape IDs within the bounds
*
* @public
*/
getShapeIdsInsideBounds(bounds) {
this.spatialIndexComputed.get();
return this.rbush.search(bounds);
}
/**
* Get shape IDs at a point (with optional margin).
* Creates a small bounding box around the point and searches the spatial index.
*
* Note: Results are unordered. If you need z-order, combine with sorted shapes:
* ```ts
* const candidates = editor.spatialIndex.getShapeIdsAtPoint(point, margin)
* const sorted = editor.getCurrentPageShapesSorted().filter(s => candidates.has(s.id))
* ```
*
* @param point - The point to search at
* @param margin - The margin around the point to search (default: 0)
* @returns Unordered set of shape IDs that could potentially contain the point
*
* @public
*/
getShapeIdsAtPoint(point, margin = 0) {
this.spatialIndexComputed.get();
return this.rbush.search(new import_Box.Box(point.x - margin, point.y - margin, margin * 2, margin * 2));
}
/**
* Dispose of the spatial index manager.
* Clears the R-tree to prevent memory leaks.
*
* @public
*/
dispose() {
this.rbush.dispose();
this.lastPageId = null;
}
}
//# sourceMappingURL=SpatialIndexManager.js.map