@tldraw/editor 4.3.0 → 4.4.0-canary.15cff7ea86f8

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.
Files changed (51) hide show
  1. package/README.md +1 -1
  2. package/dist-cjs/index.d.ts +9 -0
  3. package/dist-cjs/index.js +3 -1
  4. package/dist-cjs/index.js.map +2 -2
  5. package/dist-cjs/lib/components/default-components/DefaultCanvas.js +1 -2
  6. package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
  7. package/dist-cjs/lib/config/TLUserPreferences.js +9 -3
  8. package/dist-cjs/lib/config/TLUserPreferences.js.map +2 -2
  9. package/dist-cjs/lib/editor/Editor.js +58 -6
  10. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  11. package/dist-cjs/lib/editor/derivations/notVisibleShapes.js +13 -21
  12. package/dist-cjs/lib/editor/derivations/notVisibleShapes.js.map +2 -2
  13. package/dist-cjs/lib/editor/managers/SpatialIndexManager/RBushIndex.js +144 -0
  14. package/dist-cjs/lib/editor/managers/SpatialIndexManager/RBushIndex.js.map +7 -0
  15. package/dist-cjs/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.js +180 -0
  16. package/dist-cjs/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.js.map +7 -0
  17. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js +8 -3
  18. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
  19. package/dist-cjs/version.js +3 -3
  20. package/dist-cjs/version.js.map +1 -1
  21. package/dist-esm/index.d.mts +9 -0
  22. package/dist-esm/index.mjs +3 -1
  23. package/dist-esm/index.mjs.map +2 -2
  24. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +1 -2
  25. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  26. package/dist-esm/lib/config/TLUserPreferences.mjs +9 -3
  27. package/dist-esm/lib/config/TLUserPreferences.mjs.map +2 -2
  28. package/dist-esm/lib/editor/Editor.mjs +58 -6
  29. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  30. package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs +13 -21
  31. package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs.map +2 -2
  32. package/dist-esm/lib/editor/managers/SpatialIndexManager/RBushIndex.mjs +114 -0
  33. package/dist-esm/lib/editor/managers/SpatialIndexManager/RBushIndex.mjs.map +7 -0
  34. package/dist-esm/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.mjs +160 -0
  35. package/dist-esm/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.mjs.map +7 -0
  36. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +8 -3
  37. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
  38. package/dist-esm/version.mjs +3 -3
  39. package/dist-esm/version.mjs.map +1 -1
  40. package/package.json +10 -8
  41. package/src/index.ts +1 -0
  42. package/src/lib/components/default-components/DefaultCanvas.tsx +1 -5
  43. package/src/lib/config/TLUserPreferences.test.ts +1 -0
  44. package/src/lib/config/TLUserPreferences.ts +8 -0
  45. package/src/lib/editor/Editor.ts +84 -6
  46. package/src/lib/editor/derivations/notVisibleShapes.ts +15 -41
  47. package/src/lib/editor/managers/SpatialIndexManager/RBushIndex.ts +144 -0
  48. package/src/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.ts +214 -0
  49. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +24 -0
  50. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +8 -0
  51. package/src/version.ts +3 -3
@@ -1,32 +1,24 @@
1
1
  import { computed, isUninitialized } from "@tldraw/state";
2
2
  function notVisibleShapes(editor) {
3
- return computed("notVisibleShapes", function updateNotVisibleShapes(prevValue) {
4
- const shapeIds = editor.getCurrentPageShapeIds();
5
- const nextValue = /* @__PURE__ */ new Set();
3
+ return computed("notVisibleShapes", function(prevValue) {
4
+ const allShapeIds = editor.getCurrentPageShapeIds();
6
5
  const viewportPageBounds = editor.getViewportPageBounds();
7
- const viewMinX = viewportPageBounds.minX;
8
- const viewMinY = viewportPageBounds.minY;
9
- const viewMaxX = viewportPageBounds.maxX;
10
- const viewMaxY = viewportPageBounds.maxY;
11
- for (const id of shapeIds) {
12
- const pageBounds = editor.getShapePageBounds(id);
13
- if (pageBounds !== void 0 && pageBounds.maxX >= viewMinX && pageBounds.minX <= viewMaxX && pageBounds.maxY >= viewMinY && pageBounds.minY <= viewMaxY) {
14
- continue;
6
+ const visibleIds = editor.getShapeIdsInsideBounds(viewportPageBounds);
7
+ const nextValue = /* @__PURE__ */ new Set();
8
+ for (const id of allShapeIds) {
9
+ if (!visibleIds.has(id)) {
10
+ const shape = editor.getShape(id);
11
+ if (!shape) continue;
12
+ const canCull = editor.getShapeUtil(shape.type).canCull(shape);
13
+ if (!canCull) continue;
14
+ nextValue.add(id);
15
15
  }
16
- const shape = editor.getShape(id);
17
- if (!shape) continue;
18
- const canCull = editor.getShapeUtil(shape.type).canCull(shape);
19
- if (!canCull) continue;
20
- nextValue.add(id);
21
16
  }
22
- if (isUninitialized(prevValue)) {
17
+ if (isUninitialized(prevValue) || prevValue.size !== nextValue.size) {
23
18
  return nextValue;
24
19
  }
25
- if (prevValue.size !== nextValue.size) return nextValue;
26
20
  for (const prev of prevValue) {
27
- if (!nextValue.has(prev)) {
28
- return nextValue;
29
- }
21
+ if (!nextValue.has(prev)) return nextValue;
30
22
  }
31
23
  return prevValue;
32
24
  });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/lib/editor/derivations/notVisibleShapes.ts"],
4
- "sourcesContent": ["import { computed, isUninitialized } from '@tldraw/state'\nimport { TLShapeId } from '@tldraw/tlschema'\nimport { Editor } from '../Editor'\n\n/**\n * Non visible shapes are shapes outside of the viewport page bounds.\n *\n * @param editor - Instance of the tldraw Editor.\n * @returns Incremental derivation of non visible shapes.\n */\nexport function notVisibleShapes(editor: Editor) {\n\treturn computed<Set<TLShapeId>>('notVisibleShapes', function updateNotVisibleShapes(prevValue) {\n\t\tconst shapeIds = editor.getCurrentPageShapeIds()\n\t\tconst nextValue = new Set<TLShapeId>()\n\n\t\t// Extract viewport bounds once to avoid repeated property access\n\t\tconst viewportPageBounds = editor.getViewportPageBounds()\n\t\tconst viewMinX = viewportPageBounds.minX\n\t\tconst viewMinY = viewportPageBounds.minY\n\t\tconst viewMaxX = viewportPageBounds.maxX\n\t\tconst viewMaxY = viewportPageBounds.maxY\n\n\t\tfor (const id of shapeIds) {\n\t\t\tconst pageBounds = editor.getShapePageBounds(id)\n\n\t\t\t// Hybrid check: if bounds exist and shape overlaps viewport, it's visible.\n\t\t\t// This inlines Box.Collides to avoid function call overhead and the\n\t\t\t// redundant Contains check that Box.Includes was doing.\n\t\t\tif (\n\t\t\t\tpageBounds !== undefined &&\n\t\t\t\tpageBounds.maxX >= viewMinX &&\n\t\t\t\tpageBounds.minX <= viewMaxX &&\n\t\t\t\tpageBounds.maxY >= viewMinY &&\n\t\t\t\tpageBounds.minY <= viewMaxY\n\t\t\t) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Shape is outside viewport or has no bounds - check if it can be culled.\n\t\t\t// We defer getShape and canCull checks until here since most shapes are\n\t\t\t// typically visible and we can skip these calls for them.\n\t\t\tconst shape = editor.getShape(id)\n\t\t\tif (!shape) continue\n\n\t\t\tconst canCull = editor.getShapeUtil(shape.type).canCull(shape)\n\t\t\tif (!canCull) continue\n\n\t\t\tnextValue.add(id)\n\t\t}\n\n\t\tif (isUninitialized(prevValue)) {\n\t\t\treturn nextValue\n\t\t}\n\n\t\t// If there are more or less shapes, we know there's a change\n\t\tif (prevValue.size !== nextValue.size) return nextValue\n\n\t\t// If any of the old shapes are not in the new set, we know there's a change\n\t\tfor (const prev of prevValue) {\n\t\t\tif (!nextValue.has(prev)) {\n\t\t\t\treturn nextValue\n\t\t\t}\n\t\t}\n\n\t\t// If we've made it here, we know that the set is the same\n\t\treturn prevValue\n\t})\n}\n"],
5
- "mappings": "AAAA,SAAS,UAAU,uBAAuB;AAUnC,SAAS,iBAAiB,QAAgB;AAChD,SAAO,SAAyB,oBAAoB,SAAS,uBAAuB,WAAW;AAC9F,UAAM,WAAW,OAAO,uBAAuB;AAC/C,UAAM,YAAY,oBAAI,IAAe;AAGrC,UAAM,qBAAqB,OAAO,sBAAsB;AACxD,UAAM,WAAW,mBAAmB;AACpC,UAAM,WAAW,mBAAmB;AACpC,UAAM,WAAW,mBAAmB;AACpC,UAAM,WAAW,mBAAmB;AAEpC,eAAW,MAAM,UAAU;AAC1B,YAAM,aAAa,OAAO,mBAAmB,EAAE;AAK/C,UACC,eAAe,UACf,WAAW,QAAQ,YACnB,WAAW,QAAQ,YACnB,WAAW,QAAQ,YACnB,WAAW,QAAQ,UAClB;AACD;AAAA,MACD;AAKA,YAAM,QAAQ,OAAO,SAAS,EAAE;AAChC,UAAI,CAAC,MAAO;AAEZ,YAAM,UAAU,OAAO,aAAa,MAAM,IAAI,EAAE,QAAQ,KAAK;AAC7D,UAAI,CAAC,QAAS;AAEd,gBAAU,IAAI,EAAE;AAAA,IACjB;AAEA,QAAI,gBAAgB,SAAS,GAAG;AAC/B,aAAO;AAAA,IACR;AAGA,QAAI,UAAU,SAAS,UAAU,KAAM,QAAO;AAG9C,eAAW,QAAQ,WAAW;AAC7B,UAAI,CAAC,UAAU,IAAI,IAAI,GAAG;AACzB,eAAO;AAAA,MACR;AAAA,IACD;AAGA,WAAO;AAAA,EACR,CAAC;AACF;",
4
+ "sourcesContent": ["import { computed, isUninitialized } from '@tldraw/state'\nimport { TLShapeId } from '@tldraw/tlschema'\nimport { Editor } from '../Editor'\n\n/**\n * Non visible shapes are shapes outside of the viewport page bounds.\n *\n * @param editor - Instance of the tldraw Editor.\n * @returns Incremental derivation of non visible shapes.\n */\nexport function notVisibleShapes(editor: Editor) {\n\treturn computed<Set<TLShapeId>>('notVisibleShapes', function (prevValue) {\n\t\tconst allShapeIds = editor.getCurrentPageShapeIds()\n\t\tconst viewportPageBounds = editor.getViewportPageBounds()\n\t\tconst visibleIds = editor.getShapeIdsInsideBounds(viewportPageBounds)\n\n\t\tconst nextValue = new Set<TLShapeId>()\n\n\t\t// Non-visible shapes are all shapes minus visible shapes\n\t\tfor (const id of allShapeIds) {\n\t\t\tif (!visibleIds.has(id)) {\n\t\t\t\tconst shape = editor.getShape(id)\n\t\t\t\tif (!shape) continue\n\n\t\t\t\tconst canCull = editor.getShapeUtil(shape.type).canCull(shape)\n\t\t\t\tif (!canCull) continue\n\n\t\t\t\tnextValue.add(id)\n\t\t\t}\n\t\t}\n\n\t\tif (isUninitialized(prevValue) || prevValue.size !== nextValue.size) {\n\t\t\treturn nextValue\n\t\t}\n\n\t\tfor (const prev of prevValue) {\n\t\t\tif (!nextValue.has(prev)) return nextValue\n\t\t}\n\n\t\treturn prevValue\n\t})\n}\n"],
5
+ "mappings": "AAAA,SAAS,UAAU,uBAAuB;AAUnC,SAAS,iBAAiB,QAAgB;AAChD,SAAO,SAAyB,oBAAoB,SAAU,WAAW;AACxE,UAAM,cAAc,OAAO,uBAAuB;AAClD,UAAM,qBAAqB,OAAO,sBAAsB;AACxD,UAAM,aAAa,OAAO,wBAAwB,kBAAkB;AAEpE,UAAM,YAAY,oBAAI,IAAe;AAGrC,eAAW,MAAM,aAAa;AAC7B,UAAI,CAAC,WAAW,IAAI,EAAE,GAAG;AACxB,cAAM,QAAQ,OAAO,SAAS,EAAE;AAChC,YAAI,CAAC,MAAO;AAEZ,cAAM,UAAU,OAAO,aAAa,MAAM,IAAI,EAAE,QAAQ,KAAK;AAC7D,YAAI,CAAC,QAAS;AAEd,kBAAU,IAAI,EAAE;AAAA,MACjB;AAAA,IACD;AAEA,QAAI,gBAAgB,SAAS,KAAK,UAAU,SAAS,UAAU,MAAM;AACpE,aAAO;AAAA,IACR;AAEA,eAAW,QAAQ,WAAW;AAC7B,UAAI,CAAC,UAAU,IAAI,IAAI,EAAG,QAAO;AAAA,IAClC;AAEA,WAAO;AAAA,EACR,CAAC;AACF;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,114 @@
1
+ import RBush from "rbush";
2
+ import { Box } from "../../../primitives/Box.mjs";
3
+ class TldrawRBush extends RBush {
4
+ }
5
+ class RBushIndex {
6
+ rBush;
7
+ elementsInTree;
8
+ constructor() {
9
+ this.rBush = new TldrawRBush();
10
+ this.elementsInTree = /* @__PURE__ */ new Map();
11
+ }
12
+ /**
13
+ * Search for shapes within the given bounds.
14
+ * Returns set of shape IDs that intersect with the bounds.
15
+ */
16
+ search(bounds) {
17
+ const results = this.rBush.search({
18
+ minX: bounds.minX,
19
+ minY: bounds.minY,
20
+ maxX: bounds.maxX,
21
+ maxY: bounds.maxY
22
+ });
23
+ return new Set(results.map((e) => e.id));
24
+ }
25
+ /**
26
+ * Insert or update a shape in the spatial index.
27
+ * If the shape already exists, it will be removed first to prevent duplicates.
28
+ */
29
+ upsert(id, bounds) {
30
+ const existing = this.elementsInTree.get(id);
31
+ if (existing) {
32
+ this.rBush.remove(existing);
33
+ }
34
+ const element = {
35
+ minX: bounds.minX,
36
+ minY: bounds.minY,
37
+ maxX: bounds.maxX,
38
+ maxY: bounds.maxY,
39
+ id
40
+ };
41
+ this.rBush.insert(element);
42
+ this.elementsInTree.set(id, element);
43
+ }
44
+ /**
45
+ * Remove a shape from the spatial index.
46
+ */
47
+ remove(id) {
48
+ const element = this.elementsInTree.get(id);
49
+ if (element) {
50
+ this.rBush.remove(element);
51
+ this.elementsInTree.delete(id);
52
+ }
53
+ }
54
+ /**
55
+ * Bulk load elements into the spatial index.
56
+ * More efficient than individual inserts for initial loading.
57
+ */
58
+ bulkLoad(elements) {
59
+ this.rBush.load(elements);
60
+ for (const element of elements) {
61
+ this.elementsInTree.set(element.id, element);
62
+ }
63
+ }
64
+ /**
65
+ * Clear all elements from the spatial index.
66
+ */
67
+ clear() {
68
+ this.rBush.clear();
69
+ this.elementsInTree.clear();
70
+ }
71
+ /**
72
+ * Check if a shape is in the spatial index.
73
+ */
74
+ has(id) {
75
+ return this.elementsInTree.has(id);
76
+ }
77
+ /**
78
+ * Get the number of elements in the spatial index.
79
+ */
80
+ getSize() {
81
+ return this.elementsInTree.size;
82
+ }
83
+ /**
84
+ * Get all shape IDs currently in the spatial index.
85
+ */
86
+ getAllShapeIds() {
87
+ return Array.from(this.elementsInTree.keys());
88
+ }
89
+ /**
90
+ * Get the bounds currently stored in the spatial index for a shape.
91
+ * Returns undefined if the shape is not in the index.
92
+ */
93
+ getBounds(id) {
94
+ const element = this.elementsInTree.get(id);
95
+ if (!element) return void 0;
96
+ return new Box(
97
+ element.minX,
98
+ element.minY,
99
+ element.maxX - element.minX,
100
+ element.maxY - element.minY
101
+ );
102
+ }
103
+ /**
104
+ * Dispose of the spatial index.
105
+ * Clears all data structures to prevent memory leaks.
106
+ */
107
+ dispose() {
108
+ this.clear();
109
+ }
110
+ }
111
+ export {
112
+ RBushIndex
113
+ };
114
+ //# sourceMappingURL=RBushIndex.mjs.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../src/lib/editor/managers/SpatialIndexManager/RBushIndex.ts"],
4
+ "sourcesContent": ["import type { TLShapeId } from '@tldraw/tlschema'\nimport RBush from 'rbush'\nimport { Box } from '../../../primitives/Box'\n\n/**\n * Element stored in the R-tree spatial index.\n * Contains bounds (minX, minY, maxX, maxY) and shape ID.\n */\nexport interface SpatialElement {\n\tminX: number\n\tminY: number\n\tmaxX: number\n\tmaxY: number\n\tid: TLShapeId\n}\n\n/**\n * Custom RBush class for tldraw shapes.\n */\nclass TldrawRBush extends RBush<SpatialElement> {}\n\n/**\n * Wrapper around RBush R-tree for efficient spatial queries.\n * Maintains a map of elements currently in the tree for efficient updates.\n */\nexport class RBushIndex {\n\tprivate rBush: TldrawRBush\n\tprivate elementsInTree: Map<TLShapeId, SpatialElement>\n\n\tconstructor() {\n\t\tthis.rBush = new TldrawRBush()\n\t\tthis.elementsInTree = new Map()\n\t}\n\n\t/**\n\t * Search for shapes within the given bounds.\n\t * Returns set of shape IDs that intersect with the bounds.\n\t */\n\tsearch(bounds: Box): Set<TLShapeId> {\n\t\tconst results = this.rBush.search({\n\t\t\tminX: bounds.minX,\n\t\t\tminY: bounds.minY,\n\t\t\tmaxX: bounds.maxX,\n\t\t\tmaxY: bounds.maxY,\n\t\t})\n\t\treturn new Set(results.map((e: SpatialElement) => e.id))\n\t}\n\n\t/**\n\t * Insert or update a shape in the spatial index.\n\t * If the shape already exists, it will be removed first to prevent duplicates.\n\t */\n\tupsert(id: TLShapeId, bounds: Box): void {\n\t\t// Remove existing entry to prevent map-tree desync\n\t\tconst existing = this.elementsInTree.get(id)\n\t\tif (existing) {\n\t\t\tthis.rBush.remove(existing)\n\t\t}\n\n\t\tconst element: SpatialElement = {\n\t\t\tminX: bounds.minX,\n\t\t\tminY: bounds.minY,\n\t\t\tmaxX: bounds.maxX,\n\t\t\tmaxY: bounds.maxY,\n\t\t\tid,\n\t\t}\n\t\tthis.rBush.insert(element)\n\t\tthis.elementsInTree.set(id, element)\n\t}\n\n\t/**\n\t * Remove a shape from the spatial index.\n\t */\n\tremove(id: TLShapeId): void {\n\t\tconst element = this.elementsInTree.get(id)\n\t\tif (element) {\n\t\t\tthis.rBush.remove(element)\n\t\t\tthis.elementsInTree.delete(id)\n\t\t}\n\t}\n\n\t/**\n\t * Bulk load elements into the spatial index.\n\t * More efficient than individual inserts for initial loading.\n\t */\n\tbulkLoad(elements: SpatialElement[]): void {\n\t\tthis.rBush.load(elements)\n\t\tfor (const element of elements) {\n\t\t\tthis.elementsInTree.set(element.id, element)\n\t\t}\n\t}\n\n\t/**\n\t * Clear all elements from the spatial index.\n\t */\n\tclear(): void {\n\t\tthis.rBush.clear()\n\t\tthis.elementsInTree.clear()\n\t}\n\n\t/**\n\t * Check if a shape is in the spatial index.\n\t */\n\thas(id: TLShapeId): boolean {\n\t\treturn this.elementsInTree.has(id)\n\t}\n\n\t/**\n\t * Get the number of elements in the spatial index.\n\t */\n\tgetSize(): number {\n\t\treturn this.elementsInTree.size\n\t}\n\n\t/**\n\t * Get all shape IDs currently in the spatial index.\n\t */\n\tgetAllShapeIds(): TLShapeId[] {\n\t\treturn Array.from(this.elementsInTree.keys())\n\t}\n\n\t/**\n\t * Get the bounds currently stored in the spatial index for a shape.\n\t * Returns undefined if the shape is not in the index.\n\t */\n\tgetBounds(id: TLShapeId): Box | undefined {\n\t\tconst element = this.elementsInTree.get(id)\n\t\tif (!element) return undefined\n\t\treturn new Box(\n\t\t\telement.minX,\n\t\t\telement.minY,\n\t\t\telement.maxX - element.minX,\n\t\t\telement.maxY - element.minY\n\t\t)\n\t}\n\n\t/**\n\t * Dispose of the spatial index.\n\t * Clears all data structures to prevent memory leaks.\n\t */\n\tdispose(): void {\n\t\tthis.clear()\n\t}\n}\n"],
5
+ "mappings": "AACA,OAAO,WAAW;AAClB,SAAS,WAAW;AAiBpB,MAAM,oBAAoB,MAAsB;AAAC;AAM1C,MAAM,WAAW;AAAA,EACf;AAAA,EACA;AAAA,EAER,cAAc;AACb,SAAK,QAAQ,IAAI,YAAY;AAC7B,SAAK,iBAAiB,oBAAI,IAAI;AAAA,EAC/B;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,OAAO,QAA6B;AACnC,UAAM,UAAU,KAAK,MAAM,OAAO;AAAA,MACjC,MAAM,OAAO;AAAA,MACb,MAAM,OAAO;AAAA,MACb,MAAM,OAAO;AAAA,MACb,MAAM,OAAO;AAAA,IACd,CAAC;AACD,WAAO,IAAI,IAAI,QAAQ,IAAI,CAAC,MAAsB,EAAE,EAAE,CAAC;AAAA,EACxD;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,OAAO,IAAe,QAAmB;AAExC,UAAM,WAAW,KAAK,eAAe,IAAI,EAAE;AAC3C,QAAI,UAAU;AACb,WAAK,MAAM,OAAO,QAAQ;AAAA,IAC3B;AAEA,UAAM,UAA0B;AAAA,MAC/B,MAAM,OAAO;AAAA,MACb,MAAM,OAAO;AAAA,MACb,MAAM,OAAO;AAAA,MACb,MAAM,OAAO;AAAA,MACb;AAAA,IACD;AACA,SAAK,MAAM,OAAO,OAAO;AACzB,SAAK,eAAe,IAAI,IAAI,OAAO;AAAA,EACpC;AAAA;AAAA;AAAA;AAAA,EAKA,OAAO,IAAqB;AAC3B,UAAM,UAAU,KAAK,eAAe,IAAI,EAAE;AAC1C,QAAI,SAAS;AACZ,WAAK,MAAM,OAAO,OAAO;AACzB,WAAK,eAAe,OAAO,EAAE;AAAA,IAC9B;AAAA,EACD;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,SAAS,UAAkC;AAC1C,SAAK,MAAM,KAAK,QAAQ;AACxB,eAAW,WAAW,UAAU;AAC/B,WAAK,eAAe,IAAI,QAAQ,IAAI,OAAO;AAAA,IAC5C;AAAA,EACD;AAAA;AAAA;AAAA;AAAA,EAKA,QAAc;AACb,SAAK,MAAM,MAAM;AACjB,SAAK,eAAe,MAAM;AAAA,EAC3B;AAAA;AAAA;AAAA;AAAA,EAKA,IAAI,IAAwB;AAC3B,WAAO,KAAK,eAAe,IAAI,EAAE;AAAA,EAClC;AAAA;AAAA;AAAA;AAAA,EAKA,UAAkB;AACjB,WAAO,KAAK,eAAe;AAAA,EAC5B;AAAA;AAAA;AAAA;AAAA,EAKA,iBAA8B;AAC7B,WAAO,MAAM,KAAK,KAAK,eAAe,KAAK,CAAC;AAAA,EAC7C;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,UAAU,IAAgC;AACzC,UAAM,UAAU,KAAK,eAAe,IAAI,EAAE;AAC1C,QAAI,CAAC,QAAS,QAAO;AACrB,WAAO,IAAI;AAAA,MACV,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,QAAQ,OAAO,QAAQ;AAAA,MACvB,QAAQ,OAAO,QAAQ;AAAA,IACxB;AAAA,EACD;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,UAAgB;AACf,SAAK,MAAM;AAAA,EACZ;AACD;",
6
+ "names": []
7
+ }
@@ -0,0 +1,160 @@
1
+ import { RESET_VALUE, computed, isUninitialized } from "@tldraw/state";
2
+ import { isShape } from "@tldraw/tlschema";
3
+ import { objectMapValues } from "@tldraw/utils";
4
+ import { Box } from "../../../primitives/Box.mjs";
5
+ import { RBushIndex } from "./RBushIndex.mjs";
6
+ class SpatialIndexManager {
7
+ constructor(editor) {
8
+ this.editor = editor;
9
+ this.rbush = new RBushIndex();
10
+ this.spatialIndexComputed = this.createSpatialIndexComputed();
11
+ }
12
+ rbush;
13
+ spatialIndexComputed;
14
+ lastPageId = null;
15
+ createSpatialIndexComputed() {
16
+ const shapeHistory = this.editor.store.query.filterHistory("shape");
17
+ return computed("spatialIndex", (_prevValue, lastComputedEpoch) => {
18
+ if (isUninitialized(_prevValue)) {
19
+ return this.buildFromScratch(lastComputedEpoch);
20
+ }
21
+ const shapeDiff = shapeHistory.getDiffSince(lastComputedEpoch);
22
+ if (shapeDiff === RESET_VALUE) {
23
+ return this.buildFromScratch(lastComputedEpoch);
24
+ }
25
+ const currentPageId = this.editor.getCurrentPageId();
26
+ if (this.lastPageId !== currentPageId) {
27
+ return this.buildFromScratch(lastComputedEpoch);
28
+ }
29
+ if (shapeDiff.length === 0) {
30
+ return lastComputedEpoch;
31
+ }
32
+ this.processIncrementalUpdate(shapeDiff);
33
+ return lastComputedEpoch;
34
+ });
35
+ }
36
+ buildFromScratch(epoch) {
37
+ this.rbush.clear();
38
+ this.lastPageId = this.editor.getCurrentPageId();
39
+ const shapes = this.editor.getCurrentPageShapes();
40
+ const elements = [];
41
+ for (const shape of shapes) {
42
+ const bounds = this.editor.getShapePageBounds(shape.id);
43
+ if (bounds) {
44
+ elements.push({
45
+ minX: bounds.minX,
46
+ minY: bounds.minY,
47
+ maxX: bounds.maxX,
48
+ maxY: bounds.maxY,
49
+ id: shape.id
50
+ });
51
+ }
52
+ }
53
+ this.rbush.bulkLoad(elements);
54
+ return epoch;
55
+ }
56
+ processIncrementalUpdate(shapeDiff) {
57
+ const processedShapeIds = /* @__PURE__ */ new Set();
58
+ for (const changes of shapeDiff) {
59
+ for (const shape of objectMapValues(changes.added)) {
60
+ if (isShape(shape) && this.editor.getAncestorPageId(shape) === this.lastPageId) {
61
+ const bounds = this.editor.getShapePageBounds(shape.id);
62
+ if (bounds) {
63
+ this.rbush.upsert(shape.id, bounds);
64
+ }
65
+ processedShapeIds.add(shape.id);
66
+ }
67
+ }
68
+ for (const shape of objectMapValues(changes.removed)) {
69
+ if (isShape(shape)) {
70
+ this.rbush.remove(shape.id);
71
+ processedShapeIds.add(shape.id);
72
+ }
73
+ }
74
+ for (const [, to] of objectMapValues(changes.updated)) {
75
+ if (!isShape(to)) continue;
76
+ processedShapeIds.add(to.id);
77
+ const isOnPage = this.editor.getAncestorPageId(to) === this.lastPageId;
78
+ if (isOnPage) {
79
+ const bounds = this.editor.getShapePageBounds(to.id);
80
+ if (bounds) {
81
+ this.rbush.upsert(to.id, bounds);
82
+ }
83
+ } else {
84
+ this.rbush.remove(to.id);
85
+ }
86
+ }
87
+ }
88
+ const allShapeIds = this.rbush.getAllShapeIds();
89
+ for (const shapeId of allShapeIds) {
90
+ if (processedShapeIds.has(shapeId)) continue;
91
+ const currentBounds = this.editor.getShapePageBounds(shapeId);
92
+ const indexedBounds = this.rbush.getBounds(shapeId);
93
+ if (!this.areBoundsEqual(currentBounds, indexedBounds)) {
94
+ if (currentBounds) {
95
+ this.rbush.upsert(shapeId, currentBounds);
96
+ } else {
97
+ this.rbush.remove(shapeId);
98
+ }
99
+ }
100
+ }
101
+ }
102
+ areBoundsEqual(a, b) {
103
+ if (!a && !b) return true;
104
+ if (!a || !b) return false;
105
+ return a.minX === b.minX && a.minY === b.minY && a.maxX === b.maxX && a.maxY === b.maxY;
106
+ }
107
+ /**
108
+ * Get shape IDs within the given bounds.
109
+ * Optimized for viewport culling queries.
110
+ *
111
+ * Note: Results are unordered. If you need z-order, combine with sorted shapes:
112
+ * ```ts
113
+ * const candidates = editor.spatialIndex.getShapeIdsInsideBounds(bounds)
114
+ * const sorted = editor.getCurrentPageShapesSorted().filter(s => candidates.has(s.id))
115
+ * ```
116
+ *
117
+ * @param bounds - The bounds to search within
118
+ * @returns Unordered set of shape IDs within the bounds
119
+ *
120
+ * @public
121
+ */
122
+ getShapeIdsInsideBounds(bounds) {
123
+ this.spatialIndexComputed.get();
124
+ return this.rbush.search(bounds);
125
+ }
126
+ /**
127
+ * Get shape IDs at a point (with optional margin).
128
+ * Creates a small bounding box around the point and searches the spatial index.
129
+ *
130
+ * Note: Results are unordered. If you need z-order, combine with sorted shapes:
131
+ * ```ts
132
+ * const candidates = editor.spatialIndex.getShapeIdsAtPoint(point, margin)
133
+ * const sorted = editor.getCurrentPageShapesSorted().filter(s => candidates.has(s.id))
134
+ * ```
135
+ *
136
+ * @param point - The point to search at
137
+ * @param margin - The margin around the point to search (default: 0)
138
+ * @returns Unordered set of shape IDs that could potentially contain the point
139
+ *
140
+ * @public
141
+ */
142
+ getShapeIdsAtPoint(point, margin = 0) {
143
+ this.spatialIndexComputed.get();
144
+ return this.rbush.search(new Box(point.x - margin, point.y - margin, margin * 2, margin * 2));
145
+ }
146
+ /**
147
+ * Dispose of the spatial index manager.
148
+ * Clears the R-tree to prevent memory leaks.
149
+ *
150
+ * @public
151
+ */
152
+ dispose() {
153
+ this.rbush.dispose();
154
+ this.lastPageId = null;
155
+ }
156
+ }
157
+ export {
158
+ SpatialIndexManager
159
+ };
160
+ //# sourceMappingURL=SpatialIndexManager.mjs.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 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 type { TLRecord } from '@tldraw/tlschema'\nimport { TLPageId, TLShape, TLShapeId, isShape } 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\tconstructor(public readonly editor: Editor) {\n\t\tthis.rbush = new RBushIndex()\n\t\tthis.spatialIndexComputed = this.createSpatialIndexComputed()\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.buildFromScratch(lastComputedEpoch)\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.buildFromScratch(lastComputedEpoch)\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.buildFromScratch(lastComputedEpoch)\n\t\t\t}\n\n\t\t\t// No shape changes - index is already up to date\n\t\t\tif (shapeDiff.length === 0) {\n\t\t\t\treturn lastComputedEpoch\n\t\t\t}\n\n\t\t\t// Process incremental updates\n\t\t\tthis.processIncrementalUpdate(shapeDiff)\n\n\t\t\treturn lastComputedEpoch\n\t\t})\n\t}\n\n\tprivate buildFromScratch(epoch: number): number {\n\t\tthis.rbush.clear()\n\t\tthis.lastPageId = this.editor.getCurrentPageId()\n\n\t\tconst shapes = this.editor.getCurrentPageShapes()\n\t\tconst elements: SpatialElement[] = []\n\n\t\t// Collect all shape elements for bulk loading\n\t\tfor (const shape of shapes) {\n\t\t\tconst bounds = this.editor.getShapePageBounds(shape.id)\n\t\t\tif (bounds) {\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\n\t\treturn epoch\n\t}\n\n\tprivate processIncrementalUpdate(shapeDiff: RecordsDiff<TLRecord>[]): void {\n\t\t// Track shapes we've already processed from the diff\n\t\tconst processedShapeIds = new Set<TLShapeId>()\n\n\t\t// 1. Process shape additions, removals, and updates from diff\n\t\tfor (const changes of shapeDiff) {\n\t\t\t// Handle additions (only for shapes on current page)\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) {\n\t\t\t\t\t\tthis.rbush.upsert(shape.id, bounds)\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\t// Handle removals\n\t\t\tfor (const shape of objectMapValues(changes.removed) as TLShape[]) {\n\t\t\t\tif (isShape(shape)) {\n\t\t\t\t\tthis.rbush.remove(shape.id)\n\t\t\t\t\tprocessedShapeIds.add(shape.id)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Handle updated shapes: page changes and bounds updates\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) {\n\t\t\t\t\t\tthis.rbush.upsert(to.id, bounds)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tthis.rbush.remove(to.id)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// 2. Check remaining shapes in index for bounds changes\n\t\t// This handles shapes with computed bounds (arrows bound to moved shapes, groups with moved children, etc.)\n\t\tconst allShapeIds = this.rbush.getAllShapeIds()\n\n\t\tfor (const shapeId of allShapeIds) {\n\t\t\tif (processedShapeIds.has(shapeId)) continue\n\n\t\t\tconst currentBounds = this.editor.getShapePageBounds(shapeId)\n\t\t\tconst indexedBounds = this.rbush.getBounds(shapeId)\n\n\t\t\tif (!this.areBoundsEqual(currentBounds, indexedBounds)) {\n\t\t\t\tif (currentBounds) {\n\t\t\t\t\tthis.rbush.upsert(shapeId, currentBounds)\n\t\t\t\t} else {\n\t\t\t\t\tthis.rbush.remove(shapeId)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate areBoundsEqual(a: Box | undefined, b: Box | undefined): 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
+ "names": []
7
+ }
@@ -46,10 +46,10 @@ var __privateIn = (member, obj) => Object(obj) !== obj ? __typeError('Cannot use
46
46
  var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
47
47
  var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
48
48
  var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
49
- var _getInputMode_dec, _getEnhancedA11yMode_dec, _getIsPasteAtCursorMode_dec, _getIsDynamicResizeMode_dec, _getIsWrapMode_dec, _getIsSnapMode_dec, _getColor_dec, _getLocale_dec, _getName_dec, _getId_dec, _getAreKeyboardShortcutsEnabled_dec, _getAnimationSpeed_dec, _getEdgeScrollSpeed_dec, _getIsDarkMode_dec, _getUserPreferences_dec, _init;
49
+ var _getIsZoomDirectionInverted_dec, _getInputMode_dec, _getEnhancedA11yMode_dec, _getIsPasteAtCursorMode_dec, _getIsDynamicResizeMode_dec, _getIsWrapMode_dec, _getIsSnapMode_dec, _getColor_dec, _getLocale_dec, _getName_dec, _getId_dec, _getAreKeyboardShortcutsEnabled_dec, _getAnimationSpeed_dec, _getEdgeScrollSpeed_dec, _getIsDarkMode_dec, _getUserPreferences_dec, _init;
50
50
  import { atom, computed } from "@tldraw/state";
51
51
  import { defaultUserPreferences } from "../../../config/TLUserPreferences.mjs";
52
- _getUserPreferences_dec = [computed], _getIsDarkMode_dec = [computed], _getEdgeScrollSpeed_dec = [computed], _getAnimationSpeed_dec = [computed], _getAreKeyboardShortcutsEnabled_dec = [computed], _getId_dec = [computed], _getName_dec = [computed], _getLocale_dec = [computed], _getColor_dec = [computed], _getIsSnapMode_dec = [computed], _getIsWrapMode_dec = [computed], _getIsDynamicResizeMode_dec = [computed], _getIsPasteAtCursorMode_dec = [computed], _getEnhancedA11yMode_dec = [computed], _getInputMode_dec = [computed];
52
+ _getUserPreferences_dec = [computed], _getIsDarkMode_dec = [computed], _getEdgeScrollSpeed_dec = [computed], _getAnimationSpeed_dec = [computed], _getAreKeyboardShortcutsEnabled_dec = [computed], _getId_dec = [computed], _getName_dec = [computed], _getLocale_dec = [computed], _getColor_dec = [computed], _getIsSnapMode_dec = [computed], _getIsWrapMode_dec = [computed], _getIsDynamicResizeMode_dec = [computed], _getIsPasteAtCursorMode_dec = [computed], _getEnhancedA11yMode_dec = [computed], _getInputMode_dec = [computed], _getIsZoomDirectionInverted_dec = [computed];
53
53
  class UserPreferencesManager {
54
54
  constructor(user, inferDarkMode) {
55
55
  this.user = user;
@@ -95,7 +95,8 @@ class UserPreferencesManager {
95
95
  isWrapMode: this.getIsWrapMode(),
96
96
  isDynamicResizeMode: this.getIsDynamicResizeMode(),
97
97
  enhancedA11yMode: this.getEnhancedA11yMode(),
98
- inputMode: this.getInputMode()
98
+ inputMode: this.getInputMode(),
99
+ isZoomDirectionInverted: this.getIsZoomDirectionInverted()
99
100
  };
100
101
  }
101
102
  getIsDarkMode() {
@@ -149,6 +150,9 @@ class UserPreferencesManager {
149
150
  getInputMode() {
150
151
  return this.user.userPreferences.get().inputMode ?? defaultUserPreferences.inputMode;
151
152
  }
153
+ getIsZoomDirectionInverted() {
154
+ return this.user.userPreferences.get().isZoomDirectionInverted ?? defaultUserPreferences.isZoomDirectionInverted;
155
+ }
152
156
  }
153
157
  _init = __decoratorStart(null);
154
158
  __decorateElement(_init, 1, "getUserPreferences", _getUserPreferences_dec, UserPreferencesManager);
@@ -166,6 +170,7 @@ __decorateElement(_init, 1, "getIsDynamicResizeMode", _getIsDynamicResizeMode_de
166
170
  __decorateElement(_init, 1, "getIsPasteAtCursorMode", _getIsPasteAtCursorMode_dec, UserPreferencesManager);
167
171
  __decorateElement(_init, 1, "getEnhancedA11yMode", _getEnhancedA11yMode_dec, UserPreferencesManager);
168
172
  __decorateElement(_init, 1, "getInputMode", _getInputMode_dec, UserPreferencesManager);
173
+ __decorateElement(_init, 1, "getIsZoomDirectionInverted", _getIsZoomDirectionInverted_dec, UserPreferencesManager);
169
174
  __decoratorMetadata(_init, UserPreferencesManager);
170
175
  export {
171
176
  UserPreferencesManager
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts"],
4
- "sourcesContent": ["import { atom, computed } from '@tldraw/state'\nimport { TLUserPreferences, defaultUserPreferences } from '../../../config/TLUserPreferences'\nimport { TLUser } from '../../../config/createTLUser'\n\n/** @public */\nexport class UserPreferencesManager {\n\tsystemColorScheme = atom<'dark' | 'light'>('systemColorScheme', 'light')\n\tdisposables = new Set<() => void>()\n\tdispose() {\n\t\tthis.disposables.forEach((d) => d())\n\t}\n\tconstructor(\n\t\tprivate readonly user: TLUser,\n\t\tprivate readonly inferDarkMode: boolean\n\t) {\n\t\tif (typeof window === 'undefined' || !window.matchMedia) return\n\n\t\tconst darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)')\n\t\tif (darkModeMediaQuery?.matches) {\n\t\t\tthis.systemColorScheme.set('dark')\n\t\t}\n\t\tconst handleChange = (e: MediaQueryListEvent) => {\n\t\t\tif (e.matches) {\n\t\t\t\tthis.systemColorScheme.set('dark')\n\t\t\t} else {\n\t\t\t\tthis.systemColorScheme.set('light')\n\t\t\t}\n\t\t}\n\t\tdarkModeMediaQuery?.addEventListener('change', handleChange)\n\t\tthis.disposables.add(() => darkModeMediaQuery?.removeEventListener('change', handleChange))\n\t}\n\n\tupdateUserPreferences(userPreferences: Partial<TLUserPreferences>) {\n\t\tthis.user.setUserPreferences({\n\t\t\t...this.user.userPreferences.get(),\n\t\t\t...userPreferences,\n\t\t})\n\t}\n\t@computed getUserPreferences() {\n\t\treturn {\n\t\t\tid: this.getId(),\n\t\t\tname: this.getName(),\n\t\t\tlocale: this.getLocale(),\n\t\t\tcolor: this.getColor(),\n\t\t\tanimationSpeed: this.getAnimationSpeed(),\n\t\t\tareKeyboardShortcutsEnabled: this.getAreKeyboardShortcutsEnabled(),\n\t\t\tisSnapMode: this.getIsSnapMode(),\n\t\t\tcolorScheme: this.user.userPreferences.get().colorScheme,\n\t\t\tisDarkMode: this.getIsDarkMode(),\n\t\t\tisWrapMode: this.getIsWrapMode(),\n\t\t\tisDynamicResizeMode: this.getIsDynamicResizeMode(),\n\t\t\tenhancedA11yMode: this.getEnhancedA11yMode(),\n\t\t\tinputMode: this.getInputMode(),\n\t\t}\n\t}\n\n\t@computed getIsDarkMode() {\n\t\tswitch (this.user.userPreferences.get().colorScheme) {\n\t\t\tcase 'dark':\n\t\t\t\treturn true\n\t\t\tcase 'light':\n\t\t\t\treturn false\n\t\t\tcase 'system':\n\t\t\t\treturn this.systemColorScheme.get() === 'dark'\n\t\t\tdefault:\n\t\t\t\treturn this.inferDarkMode ? this.systemColorScheme.get() === 'dark' : false\n\t\t}\n\t}\n\n\t/**\n\t * The speed at which the user can scroll by dragging toward the edge of the screen.\n\t */\n\t@computed getEdgeScrollSpeed() {\n\t\treturn this.user.userPreferences.get().edgeScrollSpeed ?? defaultUserPreferences.edgeScrollSpeed\n\t}\n\n\t@computed getAnimationSpeed() {\n\t\treturn this.user.userPreferences.get().animationSpeed ?? defaultUserPreferences.animationSpeed\n\t}\n\n\t@computed getAreKeyboardShortcutsEnabled() {\n\t\treturn (\n\t\t\tthis.user.userPreferences.get().areKeyboardShortcutsEnabled ??\n\t\t\tdefaultUserPreferences.areKeyboardShortcutsEnabled\n\t\t)\n\t}\n\n\t@computed getId() {\n\t\treturn this.user.userPreferences.get().id\n\t}\n\n\t@computed getName() {\n\t\treturn this.user.userPreferences.get().name?.trim() ?? defaultUserPreferences.name\n\t}\n\n\t@computed getLocale() {\n\t\treturn this.user.userPreferences.get().locale ?? defaultUserPreferences.locale\n\t}\n\n\t@computed getColor() {\n\t\treturn this.user.userPreferences.get().color ?? defaultUserPreferences.color\n\t}\n\n\t@computed getIsSnapMode() {\n\t\treturn this.user.userPreferences.get().isSnapMode ?? defaultUserPreferences.isSnapMode\n\t}\n\n\t@computed getIsWrapMode() {\n\t\treturn this.user.userPreferences.get().isWrapMode ?? defaultUserPreferences.isWrapMode\n\t}\n\n\t@computed getIsDynamicResizeMode() {\n\t\treturn (\n\t\t\tthis.user.userPreferences.get().isDynamicSizeMode ?? defaultUserPreferences.isDynamicSizeMode\n\t\t)\n\t}\n\n\t@computed getIsPasteAtCursorMode() {\n\t\treturn (\n\t\t\tthis.user.userPreferences.get().isPasteAtCursorMode ??\n\t\t\tdefaultUserPreferences.isPasteAtCursorMode\n\t\t)\n\t}\n\n\t@computed getEnhancedA11yMode() {\n\t\treturn (\n\t\t\tthis.user.userPreferences.get().enhancedA11yMode ?? defaultUserPreferences.enhancedA11yMode\n\t\t)\n\t}\n\n\t@computed getInputMode() {\n\t\treturn this.user.userPreferences.get().inputMode ?? defaultUserPreferences.inputMode\n\t}\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { atom, computed } from '@tldraw/state'\nimport { TLUserPreferences, defaultUserPreferences } from '../../../config/TLUserPreferences'\nimport { TLUser } from '../../../config/createTLUser'\n\n/** @public */\nexport class UserPreferencesManager {\n\tsystemColorScheme = atom<'dark' | 'light'>('systemColorScheme', 'light')\n\tdisposables = new Set<() => void>()\n\tdispose() {\n\t\tthis.disposables.forEach((d) => d())\n\t}\n\tconstructor(\n\t\tprivate readonly user: TLUser,\n\t\tprivate readonly inferDarkMode: boolean\n\t) {\n\t\tif (typeof window === 'undefined' || !window.matchMedia) return\n\n\t\tconst darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)')\n\t\tif (darkModeMediaQuery?.matches) {\n\t\t\tthis.systemColorScheme.set('dark')\n\t\t}\n\t\tconst handleChange = (e: MediaQueryListEvent) => {\n\t\t\tif (e.matches) {\n\t\t\t\tthis.systemColorScheme.set('dark')\n\t\t\t} else {\n\t\t\t\tthis.systemColorScheme.set('light')\n\t\t\t}\n\t\t}\n\t\tdarkModeMediaQuery?.addEventListener('change', handleChange)\n\t\tthis.disposables.add(() => darkModeMediaQuery?.removeEventListener('change', handleChange))\n\t}\n\n\tupdateUserPreferences(userPreferences: Partial<TLUserPreferences>) {\n\t\tthis.user.setUserPreferences({\n\t\t\t...this.user.userPreferences.get(),\n\t\t\t...userPreferences,\n\t\t})\n\t}\n\t@computed getUserPreferences() {\n\t\treturn {\n\t\t\tid: this.getId(),\n\t\t\tname: this.getName(),\n\t\t\tlocale: this.getLocale(),\n\t\t\tcolor: this.getColor(),\n\t\t\tanimationSpeed: this.getAnimationSpeed(),\n\t\t\tareKeyboardShortcutsEnabled: this.getAreKeyboardShortcutsEnabled(),\n\t\t\tisSnapMode: this.getIsSnapMode(),\n\t\t\tcolorScheme: this.user.userPreferences.get().colorScheme,\n\t\t\tisDarkMode: this.getIsDarkMode(),\n\t\t\tisWrapMode: this.getIsWrapMode(),\n\t\t\tisDynamicResizeMode: this.getIsDynamicResizeMode(),\n\t\t\tenhancedA11yMode: this.getEnhancedA11yMode(),\n\t\t\tinputMode: this.getInputMode(),\n\t\t\tisZoomDirectionInverted: this.getIsZoomDirectionInverted(),\n\t\t}\n\t}\n\n\t@computed getIsDarkMode() {\n\t\tswitch (this.user.userPreferences.get().colorScheme) {\n\t\t\tcase 'dark':\n\t\t\t\treturn true\n\t\t\tcase 'light':\n\t\t\t\treturn false\n\t\t\tcase 'system':\n\t\t\t\treturn this.systemColorScheme.get() === 'dark'\n\t\t\tdefault:\n\t\t\t\treturn this.inferDarkMode ? this.systemColorScheme.get() === 'dark' : false\n\t\t}\n\t}\n\n\t/**\n\t * The speed at which the user can scroll by dragging toward the edge of the screen.\n\t */\n\t@computed getEdgeScrollSpeed() {\n\t\treturn this.user.userPreferences.get().edgeScrollSpeed ?? defaultUserPreferences.edgeScrollSpeed\n\t}\n\n\t@computed getAnimationSpeed() {\n\t\treturn this.user.userPreferences.get().animationSpeed ?? defaultUserPreferences.animationSpeed\n\t}\n\n\t@computed getAreKeyboardShortcutsEnabled() {\n\t\treturn (\n\t\t\tthis.user.userPreferences.get().areKeyboardShortcutsEnabled ??\n\t\t\tdefaultUserPreferences.areKeyboardShortcutsEnabled\n\t\t)\n\t}\n\n\t@computed getId() {\n\t\treturn this.user.userPreferences.get().id\n\t}\n\n\t@computed getName() {\n\t\treturn this.user.userPreferences.get().name?.trim() ?? defaultUserPreferences.name\n\t}\n\n\t@computed getLocale() {\n\t\treturn this.user.userPreferences.get().locale ?? defaultUserPreferences.locale\n\t}\n\n\t@computed getColor() {\n\t\treturn this.user.userPreferences.get().color ?? defaultUserPreferences.color\n\t}\n\n\t@computed getIsSnapMode() {\n\t\treturn this.user.userPreferences.get().isSnapMode ?? defaultUserPreferences.isSnapMode\n\t}\n\n\t@computed getIsWrapMode() {\n\t\treturn this.user.userPreferences.get().isWrapMode ?? defaultUserPreferences.isWrapMode\n\t}\n\n\t@computed getIsDynamicResizeMode() {\n\t\treturn (\n\t\t\tthis.user.userPreferences.get().isDynamicSizeMode ?? defaultUserPreferences.isDynamicSizeMode\n\t\t)\n\t}\n\n\t@computed getIsPasteAtCursorMode() {\n\t\treturn (\n\t\t\tthis.user.userPreferences.get().isPasteAtCursorMode ??\n\t\t\tdefaultUserPreferences.isPasteAtCursorMode\n\t\t)\n\t}\n\n\t@computed getEnhancedA11yMode() {\n\t\treturn (\n\t\t\tthis.user.userPreferences.get().enhancedA11yMode ?? defaultUserPreferences.enhancedA11yMode\n\t\t)\n\t}\n\n\t@computed getInputMode() {\n\t\treturn this.user.userPreferences.get().inputMode ?? defaultUserPreferences.inputMode\n\t}\n\n\t@computed getIsZoomDirectionInverted() {\n\t\treturn (\n\t\t\tthis.user.userPreferences.get().isZoomDirectionInverted ??\n\t\t\tdefaultUserPreferences.isZoomDirectionInverted\n\t\t)\n\t}\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,8 +1,8 @@
1
- const version = "4.3.0";
1
+ const version = "4.4.0-canary.15cff7ea86f8";
2
2
  const publishDates = {
3
3
  major: "2025-09-18T14:39:22.803Z",
4
- minor: "2026-01-21T11:56:39.106Z",
5
- patch: "2026-01-21T11:56:39.106Z"
4
+ minor: "2026-01-26T14:52:43.591Z",
5
+ patch: "2026-01-26T14:52:43.591Z"
6
6
  };
7
7
  export {
8
8
  publishDates,
@@ -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 = '4.3.0'\nexport const publishDates = {\n\tmajor: '2025-09-18T14:39:22.803Z',\n\tminor: '2026-01-21T11:56:39.106Z',\n\tpatch: '2026-01-21T11:56:39.106Z',\n}\n"],
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 = '4.4.0-canary.15cff7ea86f8'\nexport const publishDates = {\n\tmajor: '2025-09-18T14:39:22.803Z',\n\tminor: '2026-01-26T14:52:43.591Z',\n\tpatch: '2026-01-26T14:52:43.591Z',\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": "4.3.0",
4
+ "version": "4.4.0-canary.15cff7ea86f8",
5
5
  "author": {
6
6
  "name": "tldraw Inc.",
7
7
  "email": "hello@tldraw.com"
@@ -50,19 +50,20 @@
50
50
  "@tiptap/core": "^3.12.1",
51
51
  "@tiptap/pm": "^3.12.1",
52
52
  "@tiptap/react": "^3.12.1",
53
- "@tldraw/state": "4.3.0",
54
- "@tldraw/state-react": "4.3.0",
55
- "@tldraw/store": "4.3.0",
56
- "@tldraw/tlschema": "4.3.0",
57
- "@tldraw/utils": "4.3.0",
58
- "@tldraw/validate": "4.3.0",
53
+ "@tldraw/state": "4.4.0-canary.15cff7ea86f8",
54
+ "@tldraw/state-react": "4.4.0-canary.15cff7ea86f8",
55
+ "@tldraw/store": "4.4.0-canary.15cff7ea86f8",
56
+ "@tldraw/tlschema": "4.4.0-canary.15cff7ea86f8",
57
+ "@tldraw/utils": "4.4.0-canary.15cff7ea86f8",
58
+ "@tldraw/validate": "4.4.0-canary.15cff7ea86f8",
59
59
  "@types/core-js": "^2.5.8",
60
60
  "@use-gesture/react": "^10.3.1",
61
61
  "classnames": "^2.5.1",
62
62
  "core-js": "^3.40.0",
63
63
  "eventemitter3": "^4.0.7",
64
64
  "idb": "^7.1.1",
65
- "is-plain-object": "^5.0.0"
65
+ "is-plain-object": "^5.0.0",
66
+ "rbush": "^4.0.1"
66
67
  },
67
68
  "peerDependencies": {
68
69
  "react": "^18.2.0 || ^19.2.1",
@@ -73,6 +74,7 @@
73
74
  "@testing-library/dom": "^10.0.0",
74
75
  "@testing-library/react": "^16.0.0",
75
76
  "@types/benchmark": "^2.1.5",
77
+ "@types/rbush": "^4.0.0",
76
78
  "@types/react": "^19.2.7",
77
79
  "@types/react-dom": "^19.2.3",
78
80
  "@types/wicg-file-system-access": "^2020.9.8",
package/src/index.ts CHANGED
@@ -164,6 +164,7 @@ export {
164
164
  type SnapData,
165
165
  type SnapIndicator,
166
166
  } from './lib/editor/managers/SnapManager/SnapManager'
167
+ export { SpatialIndexManager } from './lib/editor/managers/SpatialIndexManager/SpatialIndexManager'
167
168
  export {
168
169
  TextManager,
169
170
  type TLMeasureTextOpts,
@@ -412,11 +412,7 @@ function ReflowIfNeeded() {
412
412
  'reflow for culled shapes',
413
413
  () => {
414
414
  const culledShapes = editor.getCulledShapes()
415
- if (
416
- culledShapesRef.current.size === culledShapes.size &&
417
- [...culledShapes].every((id) => culledShapesRef.current.has(id))
418
- )
419
- return
415
+ if (culledShapesRef.current === culledShapes) return
420
416
 
421
417
  culledShapesRef.current = culledShapes
422
418
  const canvas = document.getElementsByClassName('tl-canvas')
@@ -21,6 +21,7 @@ describe('TLUserPreferences consistency', () => {
21
21
  'isPasteAtCursorMode',
22
22
  'enhancedA11yMode',
23
23
  'inputMode',
24
+ 'isZoomDirectionInverted',
24
25
  ] as const
25
26
 
26
27
  it('defaultUserPreferences contains all TLUserPreferences keys (except id)', () => {
@@ -26,6 +26,7 @@ export interface TLUserPreferences {
26
26
  isPasteAtCursorMode?: boolean | null
27
27
  enhancedA11yMode?: boolean | null
28
28
  inputMode?: 'trackpad' | 'mouse' | null
29
+ isZoomDirectionInverted?: boolean | null
29
30
  }
30
31
 
31
32
  interface UserDataSnapshot {
@@ -56,6 +57,7 @@ export const userTypeValidator: T.Validator<TLUserPreferences> = T.object<TLUser
56
57
  isPasteAtCursorMode: T.boolean.nullable().optional(),
57
58
  enhancedA11yMode: T.boolean.nullable().optional(),
58
59
  inputMode: T.literalEnum('trackpad', 'mouse').nullable().optional(),
60
+ isZoomDirectionInverted: T.boolean.nullable().optional(),
59
61
  })
60
62
 
61
63
  const Versions = {
@@ -71,6 +73,7 @@ const Versions = {
71
73
  AddShowUiLabels: 10,
72
74
  AddPointerPeripheral: 11,
73
75
  RenameShowUiLabelsToEnhancedA11yMode: 12,
76
+ AddZoomDirectionInverted: 13,
74
77
  } as const
75
78
 
76
79
  const CURRENT_VERSION = Math.max(...Object.values(Versions))
@@ -121,6 +124,10 @@ function migrateSnapshot(data: { version: number; user: any }) {
121
124
  data.user.inputMode = null
122
125
  }
123
126
 
127
+ if (data.version < Versions.AddZoomDirectionInverted) {
128
+ data.user.isZoomDirectionInverted = false
129
+ }
130
+
124
131
  // finally
125
132
  data.version = CURRENT_VERSION
126
133
  }
@@ -171,6 +178,7 @@ export const defaultUserPreferences = Object.freeze({
171
178
  enhancedA11yMode: false,
172
179
  colorScheme: 'light',
173
180
  inputMode: null,
181
+ isZoomDirectionInverted: false,
174
182
  }) satisfies Readonly<Omit<TLUserPreferences, 'id'>>
175
183
 
176
184
  /** @public */