@fieldnotes/core 0.8.5 → 0.8.7

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/index.cjs CHANGED
@@ -40,6 +40,7 @@ __export(index_exports, {
40
40
  NoteEditor: () => NoteEditor,
41
41
  NoteTool: () => NoteTool,
42
42
  PencilTool: () => PencilTool,
43
+ Quadtree: () => Quadtree,
43
44
  RemoveElementCommand: () => RemoveElementCommand,
44
45
  RemoveLayerCommand: () => RemoveLayerCommand,
45
46
  SelectTool: () => SelectTool,
@@ -50,6 +51,7 @@ __export(index_exports, {
50
51
  UpdateLayerCommand: () => UpdateLayerCommand,
51
52
  VERSION: () => VERSION,
52
53
  Viewport: () => Viewport,
54
+ boundsIntersect: () => boundsIntersect,
53
55
  clearStaleBindings: () => clearStaleBindings,
54
56
  createArrow: () => createArrow,
55
57
  createGrid: () => createGrid,
@@ -105,6 +107,153 @@ var EventBus = class {
105
107
  }
106
108
  };
107
109
 
110
+ // src/core/quadtree.ts
111
+ var MAX_ITEMS = 8;
112
+ var MAX_DEPTH = 8;
113
+ function intersects(a, b) {
114
+ return a.x <= b.x + b.w && a.x + a.w >= b.x && a.y <= b.y + b.h && a.y + a.h >= b.y;
115
+ }
116
+ var QuadNode = class _QuadNode {
117
+ constructor(bounds, depth) {
118
+ this.bounds = bounds;
119
+ this.depth = depth;
120
+ }
121
+ items = [];
122
+ children = null;
123
+ insert(entry) {
124
+ if (this.children) {
125
+ const idx = this.getChildIndex(entry.bounds);
126
+ if (idx !== -1) {
127
+ const child = this.children[idx];
128
+ if (child) child.insert(entry);
129
+ return;
130
+ }
131
+ this.items.push(entry);
132
+ return;
133
+ }
134
+ this.items.push(entry);
135
+ if (this.items.length > MAX_ITEMS && this.depth < MAX_DEPTH) {
136
+ this.split();
137
+ }
138
+ }
139
+ remove(id) {
140
+ const idx = this.items.findIndex((e) => e.id === id);
141
+ if (idx !== -1) {
142
+ this.items.splice(idx, 1);
143
+ return true;
144
+ }
145
+ if (this.children) {
146
+ for (const child of this.children) {
147
+ if (child.remove(id)) {
148
+ this.collapseIfEmpty();
149
+ return true;
150
+ }
151
+ }
152
+ }
153
+ return false;
154
+ }
155
+ query(rect, result) {
156
+ if (!intersects(this.bounds, rect)) return;
157
+ for (const item of this.items) {
158
+ if (intersects(item.bounds, rect)) {
159
+ result.push(item.id);
160
+ }
161
+ }
162
+ if (this.children) {
163
+ for (const child of this.children) {
164
+ child.query(rect, result);
165
+ }
166
+ }
167
+ }
168
+ getChildIndex(itemBounds) {
169
+ const midX = this.bounds.x + this.bounds.w / 2;
170
+ const midY = this.bounds.y + this.bounds.h / 2;
171
+ const left = itemBounds.x >= this.bounds.x && itemBounds.x + itemBounds.w <= midX;
172
+ const right = itemBounds.x >= midX && itemBounds.x + itemBounds.w <= this.bounds.x + this.bounds.w;
173
+ const top = itemBounds.y >= this.bounds.y && itemBounds.y + itemBounds.h <= midY;
174
+ const bottom = itemBounds.y >= midY && itemBounds.y + itemBounds.h <= this.bounds.y + this.bounds.h;
175
+ if (left && top) return 0;
176
+ if (right && top) return 1;
177
+ if (left && bottom) return 2;
178
+ if (right && bottom) return 3;
179
+ return -1;
180
+ }
181
+ split() {
182
+ const { x, y, w, h } = this.bounds;
183
+ const halfW = w / 2;
184
+ const halfH = h / 2;
185
+ const d = this.depth + 1;
186
+ this.children = [
187
+ new _QuadNode({ x, y, w: halfW, h: halfH }, d),
188
+ new _QuadNode({ x: x + halfW, y, w: halfW, h: halfH }, d),
189
+ new _QuadNode({ x, y: y + halfH, w: halfW, h: halfH }, d),
190
+ new _QuadNode({ x: x + halfW, y: y + halfH, w: halfW, h: halfH }, d)
191
+ ];
192
+ const remaining = [];
193
+ for (const item of this.items) {
194
+ const idx = this.getChildIndex(item.bounds);
195
+ if (idx !== -1) {
196
+ const target = this.children[idx];
197
+ if (target) target.insert(item);
198
+ } else {
199
+ remaining.push(item);
200
+ }
201
+ }
202
+ this.items = remaining;
203
+ }
204
+ collapseIfEmpty() {
205
+ if (!this.children) return;
206
+ let totalItems = this.items.length;
207
+ for (const child of this.children) {
208
+ if (child.children) return;
209
+ totalItems += child.items.length;
210
+ }
211
+ if (totalItems <= MAX_ITEMS) {
212
+ for (const child of this.children) {
213
+ this.items.push(...child.items);
214
+ }
215
+ this.children = null;
216
+ }
217
+ }
218
+ };
219
+ var Quadtree = class {
220
+ root;
221
+ _size = 0;
222
+ worldBounds;
223
+ constructor(worldBounds) {
224
+ this.worldBounds = worldBounds;
225
+ this.root = new QuadNode(worldBounds, 0);
226
+ }
227
+ get size() {
228
+ return this._size;
229
+ }
230
+ insert(id, bounds) {
231
+ this.root.insert({ id, bounds });
232
+ this._size++;
233
+ }
234
+ remove(id) {
235
+ if (this.root.remove(id)) {
236
+ this._size--;
237
+ }
238
+ }
239
+ update(id, newBounds) {
240
+ this.remove(id);
241
+ this.insert(id, newBounds);
242
+ }
243
+ query(rect) {
244
+ const result = [];
245
+ this.root.query(rect, result);
246
+ return result;
247
+ }
248
+ queryPoint(point) {
249
+ return this.query({ x: point.x, y: point.y, w: 0, h: 0 });
250
+ }
251
+ clear() {
252
+ this.root = new QuadNode(this.worldBounds, 0);
253
+ this._size = 0;
254
+ }
255
+ };
256
+
108
257
  // src/core/state-serializer.ts
109
258
  var CURRENT_VERSION = 2;
110
259
  function exportState(elements, camera, layers = []) {
@@ -319,16 +468,16 @@ var Camera = class {
319
468
  pan(dx, dy) {
320
469
  this.x += dx;
321
470
  this.y += dy;
322
- this.notifyChange();
471
+ this.notifyPan();
323
472
  }
324
473
  moveTo(x, y) {
325
474
  this.x = x;
326
475
  this.y = y;
327
- this.notifyChange();
476
+ this.notifyPan();
328
477
  }
329
478
  setZoom(level) {
330
479
  this.z = Math.min(this.maxZoom, Math.max(this.minZoom, level));
331
- this.notifyChange();
480
+ this.notifyZoom();
332
481
  }
333
482
  zoomAt(level, screenPoint) {
334
483
  const before = this.screenToWorld(screenPoint);
@@ -336,7 +485,7 @@ var Camera = class {
336
485
  const after = this.screenToWorld(screenPoint);
337
486
  this.x += (after.x - before.x) * this.z;
338
487
  this.y += (after.y - before.y) * this.z;
339
- this.notifyChange();
488
+ this.notifyPanAndZoom();
340
489
  }
341
490
  screenToWorld(screen) {
342
491
  return {
@@ -350,6 +499,16 @@ var Camera = class {
350
499
  y: world.y * this.z + this.y
351
500
  };
352
501
  }
502
+ getVisibleRect(canvasWidth, canvasHeight) {
503
+ const topLeft = this.screenToWorld({ x: 0, y: 0 });
504
+ const bottomRight = this.screenToWorld({ x: canvasWidth, y: canvasHeight });
505
+ return {
506
+ x: topLeft.x,
507
+ y: topLeft.y,
508
+ w: bottomRight.x - topLeft.x,
509
+ h: bottomRight.y - topLeft.y
510
+ };
511
+ }
353
512
  toCSSTransform() {
354
513
  return `translate3d(${this.x}px, ${this.y}px, 0) scale(${this.z})`;
355
514
  }
@@ -357,8 +516,14 @@ var Camera = class {
357
516
  this.changeListeners.add(listener);
358
517
  return () => this.changeListeners.delete(listener);
359
518
  }
360
- notifyChange() {
361
- this.changeListeners.forEach((fn) => fn());
519
+ notifyPan() {
520
+ this.changeListeners.forEach((fn) => fn({ panned: true, zoomed: false }));
521
+ }
522
+ notifyZoom() {
523
+ this.changeListeners.forEach((fn) => fn({ panned: false, zoomed: true }));
524
+ }
525
+ notifyPanAndZoom() {
526
+ this.changeListeners.forEach((fn) => fn({ panned: true, zoomed: true }));
362
527
  }
363
528
  };
364
529
 
@@ -664,68 +829,6 @@ var InputHandler = class {
664
829
  }
665
830
  };
666
831
 
667
- // src/elements/element-store.ts
668
- var ElementStore = class {
669
- elements = /* @__PURE__ */ new Map();
670
- bus = new EventBus();
671
- layerOrderMap = /* @__PURE__ */ new Map();
672
- get count() {
673
- return this.elements.size;
674
- }
675
- setLayerOrder(order) {
676
- this.layerOrderMap = new Map(order);
677
- }
678
- getAll() {
679
- return [...this.elements.values()].sort((a, b) => {
680
- const layerA = this.layerOrderMap.get(a.layerId) ?? 0;
681
- const layerB = this.layerOrderMap.get(b.layerId) ?? 0;
682
- if (layerA !== layerB) return layerA - layerB;
683
- return a.zIndex - b.zIndex;
684
- });
685
- }
686
- getById(id) {
687
- return this.elements.get(id);
688
- }
689
- getElementsByType(type) {
690
- return this.getAll().filter(
691
- (el) => el.type === type
692
- );
693
- }
694
- add(element) {
695
- this.elements.set(element.id, element);
696
- this.bus.emit("add", element);
697
- }
698
- update(id, partial) {
699
- const existing = this.elements.get(id);
700
- if (!existing) return;
701
- const updated = { ...existing, ...partial, id: existing.id, type: existing.type };
702
- this.elements.set(id, updated);
703
- this.bus.emit("update", { previous: existing, current: updated });
704
- }
705
- remove(id) {
706
- const element = this.elements.get(id);
707
- if (!element) return;
708
- this.elements.delete(id);
709
- this.bus.emit("remove", element);
710
- }
711
- clear() {
712
- this.elements.clear();
713
- this.bus.emit("clear", null);
714
- }
715
- snapshot() {
716
- return this.getAll().map((el) => ({ ...el }));
717
- }
718
- loadSnapshot(elements) {
719
- this.elements.clear();
720
- for (const el of elements) {
721
- this.elements.set(el.id, el);
722
- }
723
- }
724
- on(event, listener) {
725
- return this.bus.on(event, listener);
726
- }
727
- };
728
-
729
832
  // src/elements/arrow-geometry.ts
730
833
  function getArrowControlPoint(from, to, bend) {
731
834
  const midX = (from.x + to.x) / 2;
@@ -826,6 +929,185 @@ function isNearLine(point, a, b, threshold) {
826
929
  return Math.hypot(point.x - projX, point.y - projY) <= threshold;
827
930
  }
828
931
 
932
+ // src/elements/element-bounds.ts
933
+ var strokeBoundsCache = /* @__PURE__ */ new WeakMap();
934
+ function getElementBounds(element) {
935
+ if (element.type === "grid") return null;
936
+ if ("size" in element) {
937
+ return {
938
+ x: element.position.x,
939
+ y: element.position.y,
940
+ w: element.size.w,
941
+ h: element.size.h
942
+ };
943
+ }
944
+ if (element.type === "stroke") {
945
+ if (element.points.length === 0) return null;
946
+ const cached = strokeBoundsCache.get(element);
947
+ if (cached) return cached;
948
+ let minX = Infinity;
949
+ let minY = Infinity;
950
+ let maxX = -Infinity;
951
+ let maxY = -Infinity;
952
+ for (const p of element.points) {
953
+ const px = p.x + element.position.x;
954
+ const py = p.y + element.position.y;
955
+ if (px < minX) minX = px;
956
+ if (py < minY) minY = py;
957
+ if (px > maxX) maxX = px;
958
+ if (py > maxY) maxY = py;
959
+ }
960
+ const bounds = { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
961
+ strokeBoundsCache.set(element, bounds);
962
+ return bounds;
963
+ }
964
+ if (element.type === "arrow") {
965
+ return getArrowBoundsAnalytical(element.from, element.to, element.bend);
966
+ }
967
+ return null;
968
+ }
969
+ function getArrowBoundsAnalytical(from, to, bend) {
970
+ if (bend === 0) {
971
+ const minX2 = Math.min(from.x, to.x);
972
+ const minY2 = Math.min(from.y, to.y);
973
+ return {
974
+ x: minX2,
975
+ y: minY2,
976
+ w: Math.abs(to.x - from.x),
977
+ h: Math.abs(to.y - from.y)
978
+ };
979
+ }
980
+ const cp = getArrowControlPoint(from, to, bend);
981
+ let minX = Math.min(from.x, to.x);
982
+ let maxX = Math.max(from.x, to.x);
983
+ let minY = Math.min(from.y, to.y);
984
+ let maxY = Math.max(from.y, to.y);
985
+ const tx = from.x - 2 * cp.x + to.x;
986
+ if (tx !== 0) {
987
+ const t = (from.x - cp.x) / tx;
988
+ if (t > 0 && t < 1) {
989
+ const mt = 1 - t;
990
+ const x = mt * mt * from.x + 2 * mt * t * cp.x + t * t * to.x;
991
+ if (x < minX) minX = x;
992
+ if (x > maxX) maxX = x;
993
+ }
994
+ }
995
+ const ty = from.y - 2 * cp.y + to.y;
996
+ if (ty !== 0) {
997
+ const t = (from.y - cp.y) / ty;
998
+ if (t > 0 && t < 1) {
999
+ const mt = 1 - t;
1000
+ const y = mt * mt * from.y + 2 * mt * t * cp.y + t * t * to.y;
1001
+ if (y < minY) minY = y;
1002
+ if (y > maxY) maxY = y;
1003
+ }
1004
+ }
1005
+ return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
1006
+ }
1007
+ function boundsIntersect(a, b) {
1008
+ return a.x <= b.x + b.w && a.x + a.w >= b.x && a.y <= b.y + b.h && a.y + a.h >= b.y;
1009
+ }
1010
+
1011
+ // src/elements/element-store.ts
1012
+ var ElementStore = class {
1013
+ elements = /* @__PURE__ */ new Map();
1014
+ bus = new EventBus();
1015
+ layerOrderMap = /* @__PURE__ */ new Map();
1016
+ spatialIndex = new Quadtree({ x: -1e5, y: -1e5, w: 2e5, h: 2e5 });
1017
+ get count() {
1018
+ return this.elements.size;
1019
+ }
1020
+ setLayerOrder(order) {
1021
+ this.layerOrderMap = new Map(order);
1022
+ }
1023
+ getAll() {
1024
+ return [...this.elements.values()].sort((a, b) => {
1025
+ const layerA = this.layerOrderMap.get(a.layerId) ?? 0;
1026
+ const layerB = this.layerOrderMap.get(b.layerId) ?? 0;
1027
+ if (layerA !== layerB) return layerA - layerB;
1028
+ return a.zIndex - b.zIndex;
1029
+ });
1030
+ }
1031
+ getById(id) {
1032
+ return this.elements.get(id);
1033
+ }
1034
+ getElementsByType(type) {
1035
+ return this.getAll().filter(
1036
+ (el) => el.type === type
1037
+ );
1038
+ }
1039
+ add(element) {
1040
+ this.elements.set(element.id, element);
1041
+ const bounds = getElementBounds(element);
1042
+ if (bounds) this.spatialIndex.insert(element.id, bounds);
1043
+ this.bus.emit("add", element);
1044
+ }
1045
+ update(id, partial) {
1046
+ const existing = this.elements.get(id);
1047
+ if (!existing) return;
1048
+ const updated = { ...existing, ...partial, id: existing.id, type: existing.type };
1049
+ this.elements.set(id, updated);
1050
+ const newBounds = getElementBounds(updated);
1051
+ if (newBounds) {
1052
+ this.spatialIndex.update(id, newBounds);
1053
+ }
1054
+ this.bus.emit("update", { previous: existing, current: updated });
1055
+ }
1056
+ remove(id) {
1057
+ const element = this.elements.get(id);
1058
+ if (!element) return;
1059
+ this.elements.delete(id);
1060
+ this.spatialIndex.remove(id);
1061
+ this.bus.emit("remove", element);
1062
+ }
1063
+ clear() {
1064
+ this.elements.clear();
1065
+ this.spatialIndex.clear();
1066
+ this.bus.emit("clear", null);
1067
+ }
1068
+ snapshot() {
1069
+ return this.getAll().map((el) => ({ ...el }));
1070
+ }
1071
+ loadSnapshot(elements) {
1072
+ this.elements.clear();
1073
+ this.spatialIndex.clear();
1074
+ for (const el of elements) {
1075
+ this.elements.set(el.id, el);
1076
+ const bounds = getElementBounds(el);
1077
+ if (bounds) this.spatialIndex.insert(el.id, bounds);
1078
+ }
1079
+ }
1080
+ queryRect(rect) {
1081
+ const ids = this.spatialIndex.query(rect);
1082
+ const elements = [];
1083
+ for (const id of ids) {
1084
+ const el = this.elements.get(id);
1085
+ if (el) elements.push(el);
1086
+ }
1087
+ return elements.sort((a, b) => {
1088
+ const layerA = this.layerOrderMap.get(a.layerId) ?? 0;
1089
+ const layerB = this.layerOrderMap.get(b.layerId) ?? 0;
1090
+ if (layerA !== layerB) return layerA - layerB;
1091
+ return a.zIndex - b.zIndex;
1092
+ });
1093
+ }
1094
+ queryPoint(point) {
1095
+ return this.queryRect({ x: point.x, y: point.y, w: 0, h: 0 });
1096
+ }
1097
+ on(event, listener) {
1098
+ return this.bus.on(event, listener);
1099
+ }
1100
+ onChange(listener) {
1101
+ const unsubs = [
1102
+ this.bus.on("add", listener),
1103
+ this.bus.on("remove", listener),
1104
+ this.bus.on("update", listener),
1105
+ this.bus.on("clear", listener)
1106
+ ];
1107
+ return () => unsubs.forEach((fn) => fn());
1108
+ }
1109
+ };
1110
+
829
1111
  // src/elements/arrow-binding.ts
830
1112
  var BINDABLE_TYPES = /* @__PURE__ */ new Set(["note", "text", "image", "html", "shape"]);
831
1113
  function isBindable(element) {
@@ -840,15 +1122,6 @@ function getElementCenter(element) {
840
1122
  y: element.position.y + element.size.h / 2
841
1123
  };
842
1124
  }
843
- function getElementBounds(element) {
844
- if (!("size" in element)) return null;
845
- return {
846
- x: element.position.x,
847
- y: element.position.y,
848
- w: element.size.w,
849
- h: element.size.h
850
- };
851
- }
852
1125
  function getEdgeIntersection(bounds, outsidePoint) {
853
1126
  const cx = bounds.x + bounds.w / 2;
854
1127
  const cy = bounds.y + bounds.h / 2;
@@ -2175,6 +2448,10 @@ var LayerManager = class {
2175
2448
  this.updateLayerDirect(id, { locked });
2176
2449
  return true;
2177
2450
  }
2451
+ setLayerOpacity(id, opacity) {
2452
+ if (!this.layers.has(id)) return;
2453
+ this.updateLayerDirect(id, { opacity: Math.max(0, Math.min(1, opacity)) });
2454
+ }
2178
2455
  setActiveLayer(id) {
2179
2456
  if (!this.layers.has(id)) return;
2180
2457
  this._activeLayerId = id;
@@ -2230,36 +2507,522 @@ var LayerManager = class {
2230
2507
  }
2231
2508
  };
2232
2509
 
2233
- // src/canvas/viewport.ts
2234
- var Viewport = class {
2235
- constructor(container, options = {}) {
2236
- this.container = container;
2237
- this.camera = new Camera(options.camera);
2238
- this.background = new Background(options.background);
2239
- this._gridSize = options.background?.spacing ?? 24;
2240
- this.store = new ElementStore();
2241
- this.layerManager = new LayerManager(this.store);
2242
- this.toolManager = new ToolManager();
2243
- this.renderer = new ElementRenderer();
2244
- this.renderer.setStore(this.store);
2245
- this.renderer.setCamera(this.camera);
2246
- this.renderer.setOnImageLoad(() => this.requestRender());
2247
- this.noteEditor = new NoteEditor();
2248
- this.noteEditor.setOnStop((id) => this.onTextEditStop(id));
2249
- this.history = new HistoryStack();
2250
- this.historyRecorder = new HistoryRecorder(this.store, this.history);
2251
- this.wrapper = this.createWrapper();
2252
- this.canvasEl = this.createCanvas();
2253
- this.domLayer = this.createDomLayer();
2254
- this.wrapper.appendChild(this.canvasEl);
2255
- this.wrapper.appendChild(this.domLayer);
2256
- this.container.appendChild(this.wrapper);
2257
- this.toolContext = {
2258
- camera: this.camera,
2259
- store: this.store,
2260
- requestRender: () => this.requestRender(),
2261
- switchTool: (name) => this.toolManager.setTool(name, this.toolContext),
2262
- editElement: (id) => this.startEditingElement(id),
2510
+ // src/canvas/interact-mode.ts
2511
+ var InteractMode = class {
2512
+ interactingElementId = null;
2513
+ getNode;
2514
+ constructor(deps) {
2515
+ this.getNode = deps.getNode;
2516
+ }
2517
+ startInteracting(id) {
2518
+ this.stopInteracting();
2519
+ const node = this.getNode(id);
2520
+ if (!node) return;
2521
+ this.interactingElementId = id;
2522
+ node.style.pointerEvents = "auto";
2523
+ node.addEventListener("pointerdown", this.onNodePointerDown);
2524
+ window.addEventListener("keydown", this.onKeyDown);
2525
+ window.addEventListener("pointerdown", this.onPointerDown);
2526
+ }
2527
+ stopInteracting() {
2528
+ if (!this.interactingElementId) return;
2529
+ const node = this.getNode(this.interactingElementId);
2530
+ if (node) {
2531
+ node.style.pointerEvents = "none";
2532
+ node.removeEventListener("pointerdown", this.onNodePointerDown);
2533
+ }
2534
+ this.interactingElementId = null;
2535
+ window.removeEventListener("keydown", this.onKeyDown);
2536
+ window.removeEventListener("pointerdown", this.onPointerDown);
2537
+ }
2538
+ isInteracting() {
2539
+ return this.interactingElementId !== null;
2540
+ }
2541
+ destroy() {
2542
+ this.stopInteracting();
2543
+ }
2544
+ onNodePointerDown = (e) => {
2545
+ e.stopPropagation();
2546
+ };
2547
+ onKeyDown = (e) => {
2548
+ if (e.key === "Escape") {
2549
+ this.stopInteracting();
2550
+ }
2551
+ };
2552
+ onPointerDown = (e) => {
2553
+ if (!this.interactingElementId) return;
2554
+ const target = e.target;
2555
+ if (!(target instanceof Element)) {
2556
+ this.stopInteracting();
2557
+ return;
2558
+ }
2559
+ const node = this.getNode(this.interactingElementId);
2560
+ if (node && !node.contains(target)) {
2561
+ this.stopInteracting();
2562
+ }
2563
+ };
2564
+ };
2565
+
2566
+ // src/canvas/dom-node-manager.ts
2567
+ var DomNodeManager = class {
2568
+ domNodes = /* @__PURE__ */ new Map();
2569
+ htmlContent = /* @__PURE__ */ new Map();
2570
+ domLayer;
2571
+ onEditRequest;
2572
+ isEditingElement;
2573
+ constructor(deps) {
2574
+ this.domLayer = deps.domLayer;
2575
+ this.onEditRequest = deps.onEditRequest;
2576
+ this.isEditingElement = deps.isEditingElement;
2577
+ }
2578
+ getNode(id) {
2579
+ return this.domNodes.get(id);
2580
+ }
2581
+ storeHtmlContent(elementId, dom) {
2582
+ this.htmlContent.set(elementId, dom);
2583
+ }
2584
+ syncDomNode(element, zIndex = 0) {
2585
+ let node = this.domNodes.get(element.id);
2586
+ if (!node) {
2587
+ node = document.createElement("div");
2588
+ node.dataset["elementId"] = element.id;
2589
+ Object.assign(node.style, {
2590
+ position: "absolute",
2591
+ pointerEvents: "auto"
2592
+ });
2593
+ this.domLayer.appendChild(node);
2594
+ this.domNodes.set(element.id, node);
2595
+ }
2596
+ const size = "size" in element ? element.size : null;
2597
+ Object.assign(node.style, {
2598
+ display: "block",
2599
+ left: `${element.position.x}px`,
2600
+ top: `${element.position.y}px`,
2601
+ width: size ? `${size.w}px` : "auto",
2602
+ height: size ? `${size.h}px` : "auto",
2603
+ zIndex: String(zIndex)
2604
+ });
2605
+ this.renderDomContent(node, element);
2606
+ }
2607
+ hideDomNode(id) {
2608
+ const node = this.domNodes.get(id);
2609
+ if (node) node.style.display = "none";
2610
+ }
2611
+ removeDomNode(id) {
2612
+ this.htmlContent.delete(id);
2613
+ const node = this.domNodes.get(id);
2614
+ if (node) {
2615
+ node.remove();
2616
+ this.domNodes.delete(id);
2617
+ }
2618
+ }
2619
+ clearDomNodes() {
2620
+ this.domNodes.forEach((node) => node.remove());
2621
+ this.domNodes.clear();
2622
+ this.htmlContent.clear();
2623
+ }
2624
+ reattachHtmlContent(store) {
2625
+ for (const el of store.getElementsByType("html")) {
2626
+ if (el.domId) {
2627
+ const dom = document.getElementById(el.domId);
2628
+ if (dom) {
2629
+ this.htmlContent.set(el.id, dom);
2630
+ }
2631
+ }
2632
+ }
2633
+ }
2634
+ renderDomContent(node, element) {
2635
+ if (element.type === "note") {
2636
+ if (!node.dataset["initialized"]) {
2637
+ node.dataset["initialized"] = "true";
2638
+ Object.assign(node.style, {
2639
+ backgroundColor: element.backgroundColor,
2640
+ color: element.textColor,
2641
+ padding: "8px",
2642
+ borderRadius: "4px",
2643
+ boxShadow: "0 2px 8px rgba(0,0,0,0.15)",
2644
+ fontSize: "14px",
2645
+ overflow: "hidden",
2646
+ cursor: "default",
2647
+ userSelect: "none",
2648
+ wordWrap: "break-word"
2649
+ });
2650
+ node.textContent = element.text || "";
2651
+ node.addEventListener("dblclick", (e) => {
2652
+ e.stopPropagation();
2653
+ const id = node.dataset["elementId"];
2654
+ if (id) this.onEditRequest(id);
2655
+ });
2656
+ }
2657
+ if (!this.isEditingElement(element.id)) {
2658
+ if (node.textContent !== element.text) {
2659
+ node.textContent = element.text || "";
2660
+ }
2661
+ node.style.backgroundColor = element.backgroundColor;
2662
+ node.style.color = element.textColor;
2663
+ }
2664
+ }
2665
+ if (element.type === "html" && !node.dataset["initialized"]) {
2666
+ const content = this.htmlContent.get(element.id);
2667
+ if (content) {
2668
+ node.dataset["initialized"] = "true";
2669
+ Object.assign(node.style, {
2670
+ overflow: "hidden",
2671
+ pointerEvents: "none"
2672
+ });
2673
+ node.appendChild(content);
2674
+ }
2675
+ }
2676
+ if (element.type === "text") {
2677
+ if (!node.dataset["initialized"]) {
2678
+ node.dataset["initialized"] = "true";
2679
+ Object.assign(node.style, {
2680
+ padding: "2px",
2681
+ fontSize: `${element.fontSize}px`,
2682
+ color: element.color,
2683
+ textAlign: element.textAlign,
2684
+ background: "none",
2685
+ border: "none",
2686
+ boxShadow: "none",
2687
+ overflow: "visible",
2688
+ cursor: "default",
2689
+ userSelect: "none",
2690
+ wordWrap: "break-word",
2691
+ whiteSpace: "pre-wrap",
2692
+ lineHeight: "1.4"
2693
+ });
2694
+ node.textContent = element.text || "";
2695
+ node.addEventListener("dblclick", (e) => {
2696
+ e.stopPropagation();
2697
+ const id = node.dataset["elementId"];
2698
+ if (id) this.onEditRequest(id);
2699
+ });
2700
+ }
2701
+ if (!this.isEditingElement(element.id)) {
2702
+ if (node.textContent !== element.text) {
2703
+ node.textContent = element.text || "";
2704
+ }
2705
+ Object.assign(node.style, {
2706
+ fontSize: `${element.fontSize}px`,
2707
+ color: element.color,
2708
+ textAlign: element.textAlign
2709
+ });
2710
+ }
2711
+ }
2712
+ }
2713
+ };
2714
+
2715
+ // src/canvas/render-stats.ts
2716
+ var SAMPLE_SIZE = 60;
2717
+ var RenderStats = class {
2718
+ frameTimes = [];
2719
+ frameCount = 0;
2720
+ recordFrame(durationMs) {
2721
+ this.frameCount++;
2722
+ this.frameTimes.push(durationMs);
2723
+ if (this.frameTimes.length > SAMPLE_SIZE) {
2724
+ this.frameTimes.shift();
2725
+ }
2726
+ }
2727
+ getSnapshot() {
2728
+ const times = this.frameTimes;
2729
+ if (times.length === 0) {
2730
+ return { fps: 0, avgFrameMs: 0, p95FrameMs: 0, lastFrameMs: 0, frameCount: 0 };
2731
+ }
2732
+ const avg = times.reduce((a, b) => a + b, 0) / times.length;
2733
+ const sorted = [...times].sort((a, b) => a - b);
2734
+ const p95Index = Math.min(Math.floor(sorted.length * 0.95), sorted.length - 1);
2735
+ const lastFrame = times[times.length - 1] ?? 0;
2736
+ return {
2737
+ fps: avg > 0 ? Math.round(1e3 / avg) : 0,
2738
+ avgFrameMs: Math.round(avg * 100) / 100,
2739
+ p95FrameMs: Math.round((sorted[p95Index] ?? 0) * 100) / 100,
2740
+ lastFrameMs: Math.round(lastFrame * 100) / 100,
2741
+ frameCount: this.frameCount
2742
+ };
2743
+ }
2744
+ reset() {
2745
+ this.frameTimes = [];
2746
+ this.frameCount = 0;
2747
+ }
2748
+ };
2749
+
2750
+ // src/canvas/render-loop.ts
2751
+ var RenderLoop = class {
2752
+ needsRender = false;
2753
+ animFrameId = 0;
2754
+ canvasEl;
2755
+ camera;
2756
+ background;
2757
+ store;
2758
+ renderer;
2759
+ toolManager;
2760
+ layerManager;
2761
+ domNodeManager;
2762
+ layerCache;
2763
+ activeDrawingLayerId = null;
2764
+ lastZoom;
2765
+ lastCamX;
2766
+ lastCamY;
2767
+ stats = new RenderStats();
2768
+ constructor(deps) {
2769
+ this.canvasEl = deps.canvasEl;
2770
+ this.camera = deps.camera;
2771
+ this.background = deps.background;
2772
+ this.store = deps.store;
2773
+ this.renderer = deps.renderer;
2774
+ this.toolManager = deps.toolManager;
2775
+ this.layerManager = deps.layerManager;
2776
+ this.domNodeManager = deps.domNodeManager;
2777
+ this.layerCache = deps.layerCache;
2778
+ this.lastZoom = deps.camera.zoom;
2779
+ this.lastCamX = deps.camera.position.x;
2780
+ this.lastCamY = deps.camera.position.y;
2781
+ }
2782
+ requestRender() {
2783
+ this.needsRender = true;
2784
+ }
2785
+ flush() {
2786
+ if (this.needsRender) {
2787
+ this.render();
2788
+ this.needsRender = false;
2789
+ }
2790
+ }
2791
+ start() {
2792
+ const loop = () => {
2793
+ if (this.needsRender) {
2794
+ this.render();
2795
+ this.needsRender = false;
2796
+ }
2797
+ this.animFrameId = requestAnimationFrame(loop);
2798
+ };
2799
+ this.animFrameId = requestAnimationFrame(loop);
2800
+ }
2801
+ stop() {
2802
+ cancelAnimationFrame(this.animFrameId);
2803
+ }
2804
+ setCanvasSize(width, height) {
2805
+ this.canvasEl.width = width;
2806
+ this.canvasEl.height = height;
2807
+ this.layerCache.resize(this.canvasEl.clientWidth, this.canvasEl.clientHeight);
2808
+ }
2809
+ setActiveDrawingLayer(layerId) {
2810
+ this.activeDrawingLayerId = layerId;
2811
+ }
2812
+ markLayerDirty(layerId) {
2813
+ this.layerCache.markDirty(layerId);
2814
+ }
2815
+ markAllLayersDirty() {
2816
+ this.layerCache.markAllDirty();
2817
+ }
2818
+ getStats() {
2819
+ return this.stats.getSnapshot();
2820
+ }
2821
+ compositeLayerCache(ctx, layerId, dpr) {
2822
+ const cached = this.layerCache.getCanvas(layerId);
2823
+ ctx.save();
2824
+ ctx.scale(1 / this.camera.zoom, 1 / this.camera.zoom);
2825
+ ctx.translate(-this.camera.position.x, -this.camera.position.y);
2826
+ ctx.scale(1 / dpr, 1 / dpr);
2827
+ ctx.drawImage(cached, 0, 0);
2828
+ ctx.restore();
2829
+ }
2830
+ render() {
2831
+ const t0 = performance.now();
2832
+ const ctx = this.canvasEl.getContext("2d");
2833
+ if (!ctx) return;
2834
+ const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
2835
+ const cssWidth = this.canvasEl.clientWidth;
2836
+ const cssHeight = this.canvasEl.clientHeight;
2837
+ const currentZoom = this.camera.zoom;
2838
+ const currentCamX = this.camera.position.x;
2839
+ const currentCamY = this.camera.position.y;
2840
+ if (currentZoom !== this.lastZoom || currentCamX !== this.lastCamX || currentCamY !== this.lastCamY) {
2841
+ this.layerCache.markAllDirty();
2842
+ this.lastZoom = currentZoom;
2843
+ this.lastCamX = currentCamX;
2844
+ this.lastCamY = currentCamY;
2845
+ }
2846
+ ctx.save();
2847
+ ctx.scale(dpr, dpr);
2848
+ this.renderer.setCanvasSize(cssWidth, cssHeight);
2849
+ this.background.render(ctx, this.camera);
2850
+ ctx.save();
2851
+ ctx.translate(this.camera.position.x, this.camera.position.y);
2852
+ ctx.scale(this.camera.zoom, this.camera.zoom);
2853
+ const visibleRect = this.camera.getVisibleRect(cssWidth, cssHeight);
2854
+ const margin = Math.max(visibleRect.w, visibleRect.h) * 0.1;
2855
+ const cullingRect = {
2856
+ x: visibleRect.x - margin,
2857
+ y: visibleRect.y - margin,
2858
+ w: visibleRect.w + margin * 2,
2859
+ h: visibleRect.h + margin * 2
2860
+ };
2861
+ const allElements = this.store.getAll();
2862
+ const layerElements = /* @__PURE__ */ new Map();
2863
+ const gridElements = [];
2864
+ let domZIndex = 0;
2865
+ for (const element of allElements) {
2866
+ if (!this.layerManager.isLayerVisible(element.layerId)) {
2867
+ if (this.renderer.isDomElement(element)) {
2868
+ this.domNodeManager.hideDomNode(element.id);
2869
+ }
2870
+ continue;
2871
+ }
2872
+ if (this.renderer.isDomElement(element)) {
2873
+ const elBounds = getElementBounds(element);
2874
+ if (elBounds && !boundsIntersect(elBounds, cullingRect)) {
2875
+ this.domNodeManager.hideDomNode(element.id);
2876
+ } else {
2877
+ this.domNodeManager.syncDomNode(element, domZIndex++);
2878
+ }
2879
+ continue;
2880
+ }
2881
+ if (element.type === "grid") {
2882
+ gridElements.push(element);
2883
+ continue;
2884
+ }
2885
+ let group = layerElements.get(element.layerId);
2886
+ if (!group) {
2887
+ group = [];
2888
+ layerElements.set(element.layerId, group);
2889
+ }
2890
+ group.push(element);
2891
+ }
2892
+ for (const grid of gridElements) {
2893
+ this.renderer.renderCanvasElement(ctx, grid);
2894
+ }
2895
+ for (const [layerId, elements] of layerElements) {
2896
+ const isActiveDrawingLayer = layerId === this.activeDrawingLayerId;
2897
+ if (!this.layerCache.isDirty(layerId)) {
2898
+ this.compositeLayerCache(ctx, layerId, dpr);
2899
+ continue;
2900
+ }
2901
+ if (isActiveDrawingLayer) {
2902
+ this.compositeLayerCache(ctx, layerId, dpr);
2903
+ continue;
2904
+ }
2905
+ const offCtx = this.layerCache.getContext(layerId);
2906
+ if (offCtx) {
2907
+ const offCanvas = this.layerCache.getCanvas(layerId);
2908
+ offCtx.clearRect(0, 0, offCanvas.width, offCanvas.height);
2909
+ offCtx.save();
2910
+ offCtx.scale(dpr, dpr);
2911
+ offCtx.translate(this.camera.position.x, this.camera.position.y);
2912
+ offCtx.scale(this.camera.zoom, this.camera.zoom);
2913
+ for (const element of elements) {
2914
+ const elBounds = getElementBounds(element);
2915
+ if (elBounds && !boundsIntersect(elBounds, cullingRect)) continue;
2916
+ this.renderer.renderCanvasElement(offCtx, element);
2917
+ }
2918
+ offCtx.restore();
2919
+ this.layerCache.markClean(layerId);
2920
+ this.compositeLayerCache(ctx, layerId, dpr);
2921
+ }
2922
+ }
2923
+ const activeTool = this.toolManager.activeTool;
2924
+ if (activeTool?.renderOverlay) {
2925
+ activeTool.renderOverlay(ctx);
2926
+ }
2927
+ ctx.restore();
2928
+ ctx.restore();
2929
+ this.stats.recordFrame(performance.now() - t0);
2930
+ }
2931
+ };
2932
+
2933
+ // src/canvas/layer-cache.ts
2934
+ function createOffscreenCanvas(width, height) {
2935
+ if (typeof OffscreenCanvas !== "undefined") {
2936
+ return new OffscreenCanvas(width, height);
2937
+ }
2938
+ const canvas = document.createElement("canvas");
2939
+ canvas.width = width;
2940
+ canvas.height = height;
2941
+ return canvas;
2942
+ }
2943
+ var LayerCache = class {
2944
+ canvases = /* @__PURE__ */ new Map();
2945
+ dirtyFlags = /* @__PURE__ */ new Map();
2946
+ width;
2947
+ height;
2948
+ constructor(width, height) {
2949
+ const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
2950
+ this.width = Math.round(width * dpr);
2951
+ this.height = Math.round(height * dpr);
2952
+ }
2953
+ isDirty(layerId) {
2954
+ return this.dirtyFlags.get(layerId) !== false;
2955
+ }
2956
+ markDirty(layerId) {
2957
+ this.dirtyFlags.set(layerId, true);
2958
+ }
2959
+ markClean(layerId) {
2960
+ this.dirtyFlags.set(layerId, false);
2961
+ }
2962
+ markAllDirty() {
2963
+ for (const [id] of this.dirtyFlags) {
2964
+ this.dirtyFlags.set(id, true);
2965
+ }
2966
+ }
2967
+ getCanvas(layerId) {
2968
+ let canvas = this.canvases.get(layerId);
2969
+ if (!canvas) {
2970
+ canvas = createOffscreenCanvas(this.width, this.height);
2971
+ this.canvases.set(layerId, canvas);
2972
+ this.dirtyFlags.set(layerId, true);
2973
+ }
2974
+ return canvas;
2975
+ }
2976
+ getContext(layerId) {
2977
+ const canvas = this.getCanvas(layerId);
2978
+ return canvas.getContext("2d");
2979
+ }
2980
+ resize(width, height) {
2981
+ const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
2982
+ this.width = Math.round(width * dpr);
2983
+ this.height = Math.round(height * dpr);
2984
+ for (const [id, canvas] of this.canvases) {
2985
+ canvas.width = this.width;
2986
+ canvas.height = this.height;
2987
+ this.dirtyFlags.set(id, true);
2988
+ }
2989
+ }
2990
+ clear() {
2991
+ this.canvases.clear();
2992
+ this.dirtyFlags.clear();
2993
+ }
2994
+ };
2995
+
2996
+ // src/canvas/viewport.ts
2997
+ var Viewport = class {
2998
+ constructor(container, options = {}) {
2999
+ this.container = container;
3000
+ this.camera = new Camera(options.camera);
3001
+ this.background = new Background(options.background);
3002
+ this._gridSize = options.background?.spacing ?? 24;
3003
+ this.store = new ElementStore();
3004
+ this.layerManager = new LayerManager(this.store);
3005
+ this.toolManager = new ToolManager();
3006
+ this.renderer = new ElementRenderer();
3007
+ this.renderer.setStore(this.store);
3008
+ this.renderer.setCamera(this.camera);
3009
+ this.renderer.setOnImageLoad(() => this.requestRender());
3010
+ this.noteEditor = new NoteEditor();
3011
+ this.noteEditor.setOnStop((id) => this.onTextEditStop(id));
3012
+ this.history = new HistoryStack();
3013
+ this.historyRecorder = new HistoryRecorder(this.store, this.history);
3014
+ this.wrapper = this.createWrapper();
3015
+ this.canvasEl = this.createCanvas();
3016
+ this.domLayer = this.createDomLayer();
3017
+ this.wrapper.appendChild(this.canvasEl);
3018
+ this.wrapper.appendChild(this.domLayer);
3019
+ this.container.appendChild(this.wrapper);
3020
+ this.toolContext = {
3021
+ camera: this.camera,
3022
+ store: this.store,
3023
+ requestRender: () => this.requestRender(),
3024
+ switchTool: (name) => this.toolManager.setTool(name, this.toolContext),
3025
+ editElement: (id) => this.startEditingElement(id),
2263
3026
  setCursor: (cursor) => {
2264
3027
  this.wrapper.style.cursor = cursor;
2265
3028
  },
@@ -2275,18 +3038,56 @@ var Viewport = class {
2275
3038
  historyRecorder: this.historyRecorder,
2276
3039
  historyStack: this.history
2277
3040
  });
3041
+ this.domNodeManager = new DomNodeManager({
3042
+ domLayer: this.domLayer,
3043
+ onEditRequest: (id) => this.startEditingElement(id),
3044
+ isEditingElement: (id) => this.noteEditor.isEditing && this.noteEditor.editingElementId === id
3045
+ });
3046
+ this.interactMode = new InteractMode({
3047
+ getNode: (id) => this.domNodeManager.getNode(id)
3048
+ });
3049
+ const layerCache = new LayerCache(
3050
+ this.canvasEl.clientWidth || 800,
3051
+ this.canvasEl.clientHeight || 600
3052
+ );
3053
+ this.renderLoop = new RenderLoop({
3054
+ canvasEl: this.canvasEl,
3055
+ camera: this.camera,
3056
+ background: this.background,
3057
+ store: this.store,
3058
+ renderer: this.renderer,
3059
+ toolManager: this.toolManager,
3060
+ layerManager: this.layerManager,
3061
+ domNodeManager: this.domNodeManager,
3062
+ layerCache
3063
+ });
2278
3064
  this.unsubCamera = this.camera.onChange(() => {
2279
3065
  this.applyCameraTransform();
2280
3066
  this.requestRender();
2281
3067
  });
2282
3068
  this.unsubStore = [
2283
- this.store.on("add", () => this.requestRender()),
3069
+ this.store.on("add", (el) => {
3070
+ this.renderLoop.markLayerDirty(el.layerId);
3071
+ this.requestRender();
3072
+ }),
2284
3073
  this.store.on("remove", (el) => {
2285
3074
  this.unbindArrowsFrom(el);
2286
- this.removeDomNode(el.id);
3075
+ this.domNodeManager.removeDomNode(el.id);
3076
+ this.renderLoop.markLayerDirty(el.layerId);
3077
+ this.requestRender();
3078
+ }),
3079
+ this.store.on("update", ({ previous, current }) => {
3080
+ this.renderLoop.markLayerDirty(current.layerId);
3081
+ if (previous.layerId !== current.layerId) {
3082
+ this.renderLoop.markLayerDirty(previous.layerId);
3083
+ }
3084
+ this.requestRender();
2287
3085
  }),
2288
- this.store.on("update", () => this.requestRender()),
2289
- this.store.on("clear", () => this.clearDomNodes())
3086
+ this.store.on("clear", () => {
3087
+ this.domNodeManager.clearDomNodes();
3088
+ this.renderLoop.markAllLayersDirty();
3089
+ this.requestRender();
3090
+ })
2290
3091
  ];
2291
3092
  this.layerManager.on("change", () => {
2292
3093
  this.toolContext.activeLayerId = this.layerManager.activeLayerId;
@@ -2297,7 +3098,7 @@ var Viewport = class {
2297
3098
  this.wrapper.addEventListener("drop", this.onDrop);
2298
3099
  this.observeResize();
2299
3100
  this.syncCanvasSize();
2300
- this.startRenderLoop();
3101
+ this.renderLoop.start();
2301
3102
  }
2302
3103
  camera;
2303
3104
  store;
@@ -2316,13 +3117,11 @@ var Viewport = class {
2316
3117
  historyRecorder;
2317
3118
  toolContext;
2318
3119
  resizeObserver = null;
2319
- animFrameId = 0;
2320
3120
  _snapToGrid = false;
2321
3121
  _gridSize;
2322
- needsRender = true;
2323
- domNodes = /* @__PURE__ */ new Map();
2324
- htmlContent = /* @__PURE__ */ new Map();
2325
- interactingElementId = null;
3122
+ renderLoop;
3123
+ domNodeManager;
3124
+ interactMode;
2326
3125
  get ctx() {
2327
3126
  return this.canvasEl.getContext("2d");
2328
3127
  }
@@ -2334,7 +3133,7 @@ var Viewport = class {
2334
3133
  this.toolContext.snapToGrid = enabled;
2335
3134
  }
2336
3135
  requestRender() {
2337
- this.needsRender = true;
3136
+ this.renderLoop.requestRender();
2338
3137
  }
2339
3138
  exportState() {
2340
3139
  return exportState(this.store.snapshot(), this.camera, this.layerManager.snapshot());
@@ -2348,12 +3147,12 @@ var Viewport = class {
2348
3147
  loadState(state) {
2349
3148
  this.historyRecorder.pause();
2350
3149
  this.noteEditor.destroy(this.store);
2351
- this.clearDomNodes();
3150
+ this.domNodeManager.clearDomNodes();
2352
3151
  this.store.loadSnapshot(state.elements);
2353
3152
  if (state.layers && state.layers.length > 0) {
2354
3153
  this.layerManager.loadSnapshot(state.layers);
2355
3154
  }
2356
- this.reattachHtmlContent();
3155
+ this.domNodeManager.reattachHtmlContent(this.store);
2357
3156
  this.history.clear();
2358
3157
  this.historyRecorder.resume();
2359
3158
  this.camera.moveTo(state.camera.position.x, state.camera.position.y);
@@ -2392,7 +3191,7 @@ var Viewport = class {
2392
3191
  domId,
2393
3192
  layerId: this.layerManager.activeLayerId
2394
3193
  });
2395
- this.htmlContent.set(el.id, dom);
3194
+ this.domNodeManager.storeHtmlContent(el.id, dom);
2396
3195
  this.historyRecorder.begin();
2397
3196
  this.store.add(el);
2398
3197
  this.historyRecorder.commit();
@@ -2428,8 +3227,8 @@ var Viewport = class {
2428
3227
  this.requestRender();
2429
3228
  }
2430
3229
  destroy() {
2431
- cancelAnimationFrame(this.animFrameId);
2432
- this.stopInteracting();
3230
+ this.renderLoop.stop();
3231
+ this.interactMode.destroy();
2433
3232
  this.noteEditor.destroy(this.store);
2434
3233
  this.historyRecorder.destroy();
2435
3234
  this.wrapper.removeEventListener("dblclick", this.onDblClick);
@@ -2442,54 +3241,11 @@ var Viewport = class {
2442
3241
  this.resizeObserver = null;
2443
3242
  this.wrapper.remove();
2444
3243
  }
2445
- startRenderLoop() {
2446
- const loop = () => {
2447
- if (this.needsRender) {
2448
- this.render();
2449
- this.needsRender = false;
2450
- }
2451
- this.animFrameId = requestAnimationFrame(loop);
2452
- };
2453
- this.animFrameId = requestAnimationFrame(loop);
2454
- }
2455
- render() {
2456
- const ctx = this.ctx;
2457
- if (!ctx) return;
2458
- const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
2459
- ctx.save();
2460
- ctx.scale(dpr, dpr);
2461
- this.renderer.setCanvasSize(this.canvasEl.clientWidth, this.canvasEl.clientHeight);
2462
- this.background.render(ctx, this.camera);
2463
- ctx.save();
2464
- ctx.translate(this.camera.position.x, this.camera.position.y);
2465
- ctx.scale(this.camera.zoom, this.camera.zoom);
2466
- const allElements = this.store.getAll();
2467
- let domZIndex = 0;
2468
- for (const element of allElements) {
2469
- if (!this.layerManager.isLayerVisible(element.layerId)) {
2470
- if (this.renderer.isDomElement(element)) {
2471
- this.hideDomNode(element.id);
2472
- }
2473
- continue;
2474
- }
2475
- if (this.renderer.isDomElement(element)) {
2476
- this.syncDomNode(element, domZIndex++);
2477
- } else {
2478
- this.renderer.renderCanvasElement(ctx, element);
2479
- }
2480
- }
2481
- const activeTool = this.toolManager.activeTool;
2482
- if (activeTool?.renderOverlay) {
2483
- activeTool.renderOverlay(ctx);
2484
- }
2485
- ctx.restore();
2486
- ctx.restore();
2487
- }
2488
3244
  startEditingElement(id) {
2489
3245
  const element = this.store.getById(id);
2490
3246
  if (!element || element.type !== "note" && element.type !== "text") return;
2491
- this.render();
2492
- const node = this.domNodes.get(id);
3247
+ this.renderLoop.flush();
3248
+ const node = this.domNodeManager.getNode(id);
2493
3249
  if (node) {
2494
3250
  this.noteEditor.startEditing(node, id, this.store);
2495
3251
  }
@@ -2503,7 +3259,7 @@ var Viewport = class {
2503
3259
  this.historyRecorder.commit();
2504
3260
  return;
2505
3261
  }
2506
- const node = this.domNodes.get(elementId);
3262
+ const node = this.domNodeManager.getNode(elementId);
2507
3263
  if (node && "size" in element) {
2508
3264
  const measuredHeight = node.scrollHeight;
2509
3265
  if (measuredHeight !== element.size.h) {
@@ -2531,12 +3287,12 @@ var Viewport = class {
2531
3287
  const world = this.camera.screenToWorld(screen);
2532
3288
  const hit = this.hitTestWorld(world);
2533
3289
  if (hit?.type === "html") {
2534
- this.startInteracting(hit.id);
3290
+ this.interactMode.startInteracting(hit.id);
2535
3291
  }
2536
3292
  };
2537
3293
  hitTestWorld(world) {
2538
- const elements = this.store.getAll().reverse();
2539
- for (const el of elements) {
3294
+ const candidates = this.store.queryPoint(world).reverse();
3295
+ for (const el of candidates) {
2540
3296
  if (!("size" in el)) continue;
2541
3297
  const { x, y } = el.position;
2542
3298
  const { w, h } = el.size;
@@ -2546,44 +3302,9 @@ var Viewport = class {
2546
3302
  }
2547
3303
  return null;
2548
3304
  }
2549
- startInteracting(id) {
2550
- this.stopInteracting();
2551
- const node = this.domNodes.get(id);
2552
- if (!node) return;
2553
- this.interactingElementId = id;
2554
- node.style.pointerEvents = "auto";
2555
- node.addEventListener("pointerdown", this.onInteractNodePointerDown);
2556
- window.addEventListener("keydown", this.onInteractKeyDown);
2557
- window.addEventListener("pointerdown", this.onInteractPointerDown);
2558
- }
2559
3305
  stopInteracting() {
2560
- if (!this.interactingElementId) return;
2561
- const node = this.domNodes.get(this.interactingElementId);
2562
- if (node) {
2563
- node.style.pointerEvents = "none";
2564
- node.removeEventListener("pointerdown", this.onInteractNodePointerDown);
2565
- }
2566
- this.interactingElementId = null;
2567
- window.removeEventListener("keydown", this.onInteractKeyDown);
2568
- window.removeEventListener("pointerdown", this.onInteractPointerDown);
3306
+ this.interactMode.stopInteracting();
2569
3307
  }
2570
- onInteractNodePointerDown = (e) => {
2571
- e.stopPropagation();
2572
- };
2573
- onInteractKeyDown = (e) => {
2574
- if (e.key === "Escape") {
2575
- this.stopInteracting();
2576
- }
2577
- };
2578
- onInteractPointerDown = (e) => {
2579
- if (!this.interactingElementId) return;
2580
- const target = e.target;
2581
- if (!target) return;
2582
- const node = this.domNodes.get(this.interactingElementId);
2583
- if (node && !node.contains(target)) {
2584
- this.stopInteracting();
2585
- }
2586
- };
2587
3308
  onDragOver = (e) => {
2588
3309
  e.preventDefault();
2589
3310
  };
@@ -2605,108 +3326,6 @@ var Viewport = class {
2605
3326
  reader.readAsDataURL(file);
2606
3327
  }
2607
3328
  };
2608
- syncDomNode(element, zIndex = 0) {
2609
- let node = this.domNodes.get(element.id);
2610
- if (!node) {
2611
- node = document.createElement("div");
2612
- node.dataset["elementId"] = element.id;
2613
- Object.assign(node.style, {
2614
- position: "absolute",
2615
- pointerEvents: "auto"
2616
- });
2617
- this.domLayer.appendChild(node);
2618
- this.domNodes.set(element.id, node);
2619
- }
2620
- const size = "size" in element ? element.size : null;
2621
- Object.assign(node.style, {
2622
- display: "block",
2623
- left: `${element.position.x}px`,
2624
- top: `${element.position.y}px`,
2625
- width: size ? `${size.w}px` : "auto",
2626
- height: size ? `${size.h}px` : "auto",
2627
- zIndex: String(zIndex)
2628
- });
2629
- this.renderDomContent(node, element);
2630
- }
2631
- renderDomContent(node, element) {
2632
- if (element.type === "note") {
2633
- if (!node.dataset["initialized"]) {
2634
- node.dataset["initialized"] = "true";
2635
- Object.assign(node.style, {
2636
- backgroundColor: element.backgroundColor,
2637
- color: element.textColor,
2638
- padding: "8px",
2639
- borderRadius: "4px",
2640
- boxShadow: "0 2px 8px rgba(0,0,0,0.15)",
2641
- fontSize: "14px",
2642
- overflow: "hidden",
2643
- cursor: "default",
2644
- userSelect: "none",
2645
- wordWrap: "break-word"
2646
- });
2647
- node.textContent = element.text || "";
2648
- node.addEventListener("dblclick", (e) => {
2649
- e.stopPropagation();
2650
- const id = node.dataset["elementId"];
2651
- if (id) this.startEditingElement(id);
2652
- });
2653
- }
2654
- if (!this.noteEditor.isEditing || this.noteEditor.editingElementId !== element.id) {
2655
- if (node.textContent !== element.text) {
2656
- node.textContent = element.text || "";
2657
- }
2658
- node.style.backgroundColor = element.backgroundColor;
2659
- node.style.color = element.textColor;
2660
- }
2661
- }
2662
- if (element.type === "html" && !node.dataset["initialized"]) {
2663
- const content = this.htmlContent.get(element.id);
2664
- if (content) {
2665
- node.dataset["initialized"] = "true";
2666
- Object.assign(node.style, {
2667
- overflow: "hidden",
2668
- pointerEvents: "none"
2669
- });
2670
- node.appendChild(content);
2671
- }
2672
- }
2673
- if (element.type === "text") {
2674
- if (!node.dataset["initialized"]) {
2675
- node.dataset["initialized"] = "true";
2676
- Object.assign(node.style, {
2677
- padding: "2px",
2678
- fontSize: `${element.fontSize}px`,
2679
- color: element.color,
2680
- textAlign: element.textAlign,
2681
- background: "none",
2682
- border: "none",
2683
- boxShadow: "none",
2684
- overflow: "visible",
2685
- cursor: "default",
2686
- userSelect: "none",
2687
- wordWrap: "break-word",
2688
- whiteSpace: "pre-wrap",
2689
- lineHeight: "1.4"
2690
- });
2691
- node.textContent = element.text || "";
2692
- node.addEventListener("dblclick", (e) => {
2693
- e.stopPropagation();
2694
- const id = node.dataset["elementId"];
2695
- if (id) this.startEditingElement(id);
2696
- });
2697
- }
2698
- if (!this.noteEditor.isEditing || this.noteEditor.editingElementId !== element.id) {
2699
- if (node.textContent !== element.text) {
2700
- node.textContent = element.text || "";
2701
- }
2702
- Object.assign(node.style, {
2703
- fontSize: `${element.fontSize}px`,
2704
- color: element.color,
2705
- textAlign: element.textAlign
2706
- });
2707
- }
2708
- }
2709
- }
2710
3329
  unbindArrowsFrom(removedElement) {
2711
3330
  const boundArrows = findBoundArrows(removedElement.id, this.store);
2712
3331
  const bounds = getElementBounds(removedElement);
@@ -2741,35 +3360,6 @@ var Viewport = class {
2741
3360
  }
2742
3361
  }
2743
3362
  }
2744
- hideDomNode(id) {
2745
- const node = this.domNodes.get(id);
2746
- if (node) node.style.display = "none";
2747
- }
2748
- removeDomNode(id) {
2749
- this.htmlContent.delete(id);
2750
- const node = this.domNodes.get(id);
2751
- if (node) {
2752
- node.remove();
2753
- this.domNodes.delete(id);
2754
- }
2755
- this.requestRender();
2756
- }
2757
- clearDomNodes() {
2758
- this.domNodes.forEach((node) => node.remove());
2759
- this.domNodes.clear();
2760
- this.htmlContent.clear();
2761
- this.requestRender();
2762
- }
2763
- reattachHtmlContent() {
2764
- for (const el of this.store.getElementsByType("html")) {
2765
- if (el.domId) {
2766
- const dom = document.getElementById(el.domId);
2767
- if (dom) {
2768
- this.htmlContent.set(el.id, dom);
2769
- }
2770
- }
2771
- }
2772
- }
2773
3363
  createWrapper() {
2774
3364
  const el = document.createElement("div");
2775
3365
  Object.assign(el.style, {
@@ -2810,8 +3400,7 @@ var Viewport = class {
2810
3400
  syncCanvasSize() {
2811
3401
  const rect = this.container.getBoundingClientRect();
2812
3402
  const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
2813
- this.canvasEl.width = rect.width * dpr;
2814
- this.canvasEl.height = rect.height * dpr;
3403
+ this.renderLoop.setCanvasSize(rect.width * dpr, rect.height * dpr);
2815
3404
  this.requestRender();
2816
3405
  }
2817
3406
  observeResize() {
@@ -2854,6 +3443,9 @@ var HandTool = class {
2854
3443
  var MIN_POINTS_FOR_STROKE = 2;
2855
3444
  var DEFAULT_SMOOTHING = 1.5;
2856
3445
  var DEFAULT_PRESSURE = 0.5;
3446
+ var DEFAULT_MIN_POINT_DISTANCE = 3;
3447
+ var DEFAULT_PROGRESSIVE_THRESHOLD = 200;
3448
+ var PROGRESSIVE_HOT_ZONE = 30;
2857
3449
  var PencilTool = class {
2858
3450
  name = "pencil";
2859
3451
  drawing = false;
@@ -2861,10 +3453,17 @@ var PencilTool = class {
2861
3453
  color;
2862
3454
  width;
2863
3455
  smoothing;
3456
+ minPointDistance;
3457
+ progressiveThreshold;
3458
+ nextSimplifyAt;
3459
+ optionListeners = /* @__PURE__ */ new Set();
2864
3460
  constructor(options = {}) {
2865
3461
  this.color = options.color ?? "#000000";
2866
3462
  this.width = options.width ?? 2;
2867
3463
  this.smoothing = options.smoothing ?? DEFAULT_SMOOTHING;
3464
+ this.minPointDistance = options.minPointDistance ?? DEFAULT_MIN_POINT_DISTANCE;
3465
+ this.progressiveThreshold = options.progressiveSimplifyThreshold ?? DEFAULT_PROGRESSIVE_THRESHOLD;
3466
+ this.nextSimplifyAt = this.progressiveThreshold;
2868
3467
  }
2869
3468
  onActivate(ctx) {
2870
3469
  ctx.setCursor?.("crosshair");
@@ -2872,22 +3471,53 @@ var PencilTool = class {
2872
3471
  onDeactivate(ctx) {
2873
3472
  ctx.setCursor?.("default");
2874
3473
  }
3474
+ getOptions() {
3475
+ return {
3476
+ color: this.color,
3477
+ width: this.width,
3478
+ smoothing: this.smoothing,
3479
+ minPointDistance: this.minPointDistance,
3480
+ progressiveSimplifyThreshold: this.progressiveThreshold
3481
+ };
3482
+ }
3483
+ onOptionsChange(listener) {
3484
+ this.optionListeners.add(listener);
3485
+ return () => this.optionListeners.delete(listener);
3486
+ }
2875
3487
  setOptions(options) {
2876
3488
  if (options.color !== void 0) this.color = options.color;
2877
3489
  if (options.width !== void 0) this.width = options.width;
2878
3490
  if (options.smoothing !== void 0) this.smoothing = options.smoothing;
3491
+ if (options.minPointDistance !== void 0) this.minPointDistance = options.minPointDistance;
3492
+ if (options.progressiveSimplifyThreshold !== void 0)
3493
+ this.progressiveThreshold = options.progressiveSimplifyThreshold;
3494
+ this.notifyOptionsChange();
2879
3495
  }
2880
3496
  onPointerDown(state, ctx) {
2881
3497
  this.drawing = true;
2882
3498
  const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
2883
3499
  const pressure = state.pressure === 0 ? DEFAULT_PRESSURE : state.pressure;
2884
3500
  this.points = [{ x: world.x, y: world.y, pressure }];
3501
+ this.nextSimplifyAt = this.progressiveThreshold;
2885
3502
  }
2886
3503
  onPointerMove(state, ctx) {
2887
3504
  if (!this.drawing) return;
2888
3505
  const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
2889
3506
  const pressure = state.pressure === 0 ? DEFAULT_PRESSURE : state.pressure;
3507
+ const last = this.points[this.points.length - 1];
3508
+ if (last) {
3509
+ const dx = world.x - last.x;
3510
+ const dy = world.y - last.y;
3511
+ if (dx * dx + dy * dy < this.minPointDistance * this.minPointDistance) return;
3512
+ }
2890
3513
  this.points.push({ x: world.x, y: world.y, pressure });
3514
+ if (this.points.length > this.nextSimplifyAt) {
3515
+ const hotZone = this.points.slice(-PROGRESSIVE_HOT_ZONE);
3516
+ const coldZone = this.points.slice(0, -PROGRESSIVE_HOT_ZONE);
3517
+ const simplified = simplifyPoints(coldZone, this.smoothing * 2);
3518
+ this.points = [...simplified, ...hotZone];
3519
+ this.nextSimplifyAt = this.points.length + this.progressiveThreshold;
3520
+ }
2891
3521
  ctx.requestRender();
2892
3522
  }
2893
3523
  onPointerUp(_state, ctx) {
@@ -2908,6 +3538,9 @@ var PencilTool = class {
2908
3538
  this.points = [];
2909
3539
  ctx.requestRender();
2910
3540
  }
3541
+ notifyOptionsChange() {
3542
+ for (const listener of this.optionListeners) listener();
3543
+ }
2911
3544
  renderOverlay(ctx) {
2912
3545
  if (!this.drawing || this.points.length < 2) return;
2913
3546
  ctx.save();
@@ -2944,6 +3577,9 @@ var EraserTool = class {
2944
3577
  this.radius = options.radius ?? DEFAULT_RADIUS;
2945
3578
  this.cursor = makeEraserCursor(this.radius);
2946
3579
  }
3580
+ getOptions() {
3581
+ return { radius: this.radius };
3582
+ }
2947
3583
  onActivate(ctx) {
2948
3584
  ctx.setCursor?.(this.cursor);
2949
3585
  }
@@ -2963,13 +3599,20 @@ var EraserTool = class {
2963
3599
  }
2964
3600
  eraseAt(state, ctx) {
2965
3601
  const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
2966
- const strokes = ctx.store.getElementsByType("stroke");
3602
+ const queryBounds = {
3603
+ x: world.x - this.radius,
3604
+ y: world.y - this.radius,
3605
+ w: this.radius * 2,
3606
+ h: this.radius * 2
3607
+ };
3608
+ const candidates = ctx.store.queryRect(queryBounds);
2967
3609
  let erased = false;
2968
- for (const stroke of strokes) {
2969
- if (ctx.isLayerVisible && !ctx.isLayerVisible(stroke.layerId)) continue;
2970
- if (ctx.isLayerLocked && ctx.isLayerLocked(stroke.layerId)) continue;
2971
- if (this.strokeIntersects(stroke, world)) {
2972
- ctx.store.remove(stroke.id);
3610
+ for (const el of candidates) {
3611
+ if (el.type !== "stroke") continue;
3612
+ if (ctx.isLayerVisible && !ctx.isLayerVisible(el.layerId)) continue;
3613
+ if (ctx.isLayerLocked && ctx.isLayerLocked(el.layerId)) continue;
3614
+ if (this.strokeIntersects(el, world)) {
3615
+ ctx.store.remove(el.id);
2973
3616
  erased = true;
2974
3617
  }
2975
3618
  }
@@ -3346,7 +3989,7 @@ var SelectTool = class {
3346
3989
  for (const id of this._selectedIds) {
3347
3990
  const el = ctx.store.getById(id);
3348
3991
  if (!el || !("size" in el)) continue;
3349
- const bounds = this.getElementBounds(el);
3992
+ const bounds = getElementBounds(el);
3350
3993
  if (!bounds) continue;
3351
3994
  const corners = this.getHandlePositions(bounds);
3352
3995
  for (const [handle, pos] of corners) {
@@ -3394,7 +4037,7 @@ var SelectTool = class {
3394
4037
  this.renderBindingHighlights(canvasCtx, el, zoom);
3395
4038
  continue;
3396
4039
  }
3397
- const bounds = this.getElementBounds(el);
4040
+ const bounds = getElementBounds(el);
3398
4041
  if (!bounds) continue;
3399
4042
  const pad = SELECTION_PAD / zoom;
3400
4043
  canvasCtx.strokeRect(bounds.x - pad, bounds.y - pad, bounds.w + pad * 2, bounds.h + pad * 2);
@@ -3453,12 +4096,13 @@ var SelectTool = class {
3453
4096
  return { x, y, w, h };
3454
4097
  }
3455
4098
  findElementsInRect(marquee, ctx) {
4099
+ const candidates = ctx.store.queryRect(marquee);
3456
4100
  const ids = [];
3457
- for (const el of ctx.store.getAll()) {
4101
+ for (const el of candidates) {
3458
4102
  if (ctx.isLayerVisible && !ctx.isLayerVisible(el.layerId)) continue;
3459
4103
  if (ctx.isLayerLocked && ctx.isLayerLocked(el.layerId)) continue;
3460
4104
  if (el.type === "grid") continue;
3461
- const bounds = this.getElementBounds(el);
4105
+ const bounds = getElementBounds(el);
3462
4106
  if (bounds && this.rectsOverlap(marquee, bounds)) {
3463
4107
  ids.push(el.id);
3464
4108
  }
@@ -3468,30 +4112,10 @@ var SelectTool = class {
3468
4112
  rectsOverlap(a, b) {
3469
4113
  return a.x <= b.x + b.w && a.x + a.w >= b.x && a.y <= b.y + b.h && a.y + a.h >= b.y;
3470
4114
  }
3471
- getElementBounds(el) {
3472
- if ("size" in el) {
3473
- return { x: el.position.x, y: el.position.y, w: el.size.w, h: el.size.h };
3474
- }
3475
- if (el.type === "stroke" && el.points.length > 0) {
3476
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
3477
- for (const p of el.points) {
3478
- const px = p.x + el.position.x;
3479
- const py = p.y + el.position.y;
3480
- if (px < minX) minX = px;
3481
- if (py < minY) minY = py;
3482
- if (px > maxX) maxX = px;
3483
- if (py > maxY) maxY = py;
3484
- }
3485
- return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
3486
- }
3487
- if (el.type === "arrow") {
3488
- return getArrowBounds(el.from, el.to, el.bend);
3489
- }
3490
- return null;
3491
- }
3492
4115
  hitTest(world, ctx) {
3493
- const elements = ctx.store.getAll().reverse();
3494
- for (const el of elements) {
4116
+ const r = 10;
4117
+ const candidates = ctx.store.queryRect({ x: world.x - r, y: world.y - r, w: r * 2, h: r * 2 }).reverse();
4118
+ for (const el of candidates) {
3495
4119
  if (ctx.isLayerVisible && !ctx.isLayerVisible(el.layerId)) continue;
3496
4120
  if (ctx.isLayerLocked && ctx.isLayerLocked(el.layerId)) continue;
3497
4121
  if (el.type === "grid") continue;
@@ -3532,13 +4156,25 @@ var ArrowTool = class {
3532
4156
  fromBinding;
3533
4157
  fromTarget = null;
3534
4158
  toTarget = null;
4159
+ optionListeners = /* @__PURE__ */ new Set();
3535
4160
  constructor(options = {}) {
3536
4161
  this.color = options.color ?? "#000000";
3537
4162
  this.width = options.width ?? 2;
3538
4163
  }
4164
+ getOptions() {
4165
+ return { color: this.color, width: this.width };
4166
+ }
4167
+ onOptionsChange(listener) {
4168
+ this.optionListeners.add(listener);
4169
+ return () => this.optionListeners.delete(listener);
4170
+ }
3539
4171
  setOptions(options) {
3540
4172
  if (options.color !== void 0) this.color = options.color;
3541
4173
  if (options.width !== void 0) this.width = options.width;
4174
+ this.notifyOptionsChange();
4175
+ }
4176
+ notifyOptionsChange() {
4177
+ for (const listener of this.optionListeners) listener();
3542
4178
  }
3543
4179
  layerFilter(ctx) {
3544
4180
  const activeLayerId = ctx.activeLayerId;
@@ -3660,15 +4296,31 @@ var NoteTool = class {
3660
4296
  backgroundColor;
3661
4297
  textColor;
3662
4298
  size;
4299
+ optionListeners = /* @__PURE__ */ new Set();
3663
4300
  constructor(options = {}) {
3664
4301
  this.backgroundColor = options.backgroundColor ?? "#ffeb3b";
3665
4302
  this.textColor = options.textColor ?? "#000000";
3666
4303
  this.size = options.size ?? { w: 200, h: 100 };
3667
4304
  }
4305
+ getOptions() {
4306
+ return {
4307
+ backgroundColor: this.backgroundColor,
4308
+ textColor: this.textColor,
4309
+ size: { ...this.size }
4310
+ };
4311
+ }
4312
+ onOptionsChange(listener) {
4313
+ this.optionListeners.add(listener);
4314
+ return () => this.optionListeners.delete(listener);
4315
+ }
3668
4316
  setOptions(options) {
3669
4317
  if (options.backgroundColor !== void 0) this.backgroundColor = options.backgroundColor;
3670
4318
  if (options.textColor !== void 0) this.textColor = options.textColor;
3671
4319
  if (options.size !== void 0) this.size = options.size;
4320
+ this.notifyOptionsChange();
4321
+ }
4322
+ notifyOptionsChange() {
4323
+ for (const listener of this.optionListeners) listener();
3672
4324
  }
3673
4325
  onPointerDown(_state, _ctx) {
3674
4326
  }
@@ -3699,15 +4351,27 @@ var TextTool = class {
3699
4351
  fontSize;
3700
4352
  color;
3701
4353
  textAlign;
4354
+ optionListeners = /* @__PURE__ */ new Set();
3702
4355
  constructor(options = {}) {
3703
4356
  this.fontSize = options.fontSize ?? 16;
3704
4357
  this.color = options.color ?? "#1a1a1a";
3705
4358
  this.textAlign = options.textAlign ?? "left";
3706
4359
  }
4360
+ getOptions() {
4361
+ return { fontSize: this.fontSize, color: this.color, textAlign: this.textAlign };
4362
+ }
4363
+ onOptionsChange(listener) {
4364
+ this.optionListeners.add(listener);
4365
+ return () => this.optionListeners.delete(listener);
4366
+ }
3707
4367
  setOptions(options) {
3708
4368
  if (options.fontSize !== void 0) this.fontSize = options.fontSize;
3709
4369
  if (options.color !== void 0) this.color = options.color;
3710
4370
  if (options.textAlign !== void 0) this.textAlign = options.textAlign;
4371
+ this.notifyOptionsChange();
4372
+ }
4373
+ notifyOptionsChange() {
4374
+ for (const listener of this.optionListeners) listener();
3711
4375
  }
3712
4376
  onActivate(ctx) {
3713
4377
  ctx.setCursor?.("text");
@@ -3779,17 +4443,31 @@ var ShapeTool = class {
3779
4443
  strokeColor;
3780
4444
  strokeWidth;
3781
4445
  fillColor;
4446
+ optionListeners = /* @__PURE__ */ new Set();
3782
4447
  constructor(options = {}) {
3783
4448
  this.shape = options.shape ?? "rectangle";
3784
4449
  this.strokeColor = options.strokeColor ?? "#000000";
3785
4450
  this.strokeWidth = options.strokeWidth ?? 2;
3786
4451
  this.fillColor = options.fillColor ?? "none";
3787
4452
  }
4453
+ getOptions() {
4454
+ return {
4455
+ shape: this.shape,
4456
+ strokeColor: this.strokeColor,
4457
+ strokeWidth: this.strokeWidth,
4458
+ fillColor: this.fillColor
4459
+ };
4460
+ }
4461
+ onOptionsChange(listener) {
4462
+ this.optionListeners.add(listener);
4463
+ return () => this.optionListeners.delete(listener);
4464
+ }
3788
4465
  setOptions(options) {
3789
4466
  if (options.shape !== void 0) this.shape = options.shape;
3790
4467
  if (options.strokeColor !== void 0) this.strokeColor = options.strokeColor;
3791
4468
  if (options.strokeWidth !== void 0) this.strokeWidth = options.strokeWidth;
3792
4469
  if (options.fillColor !== void 0) this.fillColor = options.fillColor;
4470
+ this.notifyOptionsChange();
3793
4471
  }
3794
4472
  onActivate(_ctx) {
3795
4473
  if (typeof window !== "undefined") {
@@ -3876,6 +4554,9 @@ var ShapeTool = class {
3876
4554
  }
3877
4555
  return { position: { x, y }, size: { w, h } };
3878
4556
  }
4557
+ notifyOptionsChange() {
4558
+ for (const listener of this.optionListeners) listener();
4559
+ }
3879
4560
  snap(point, ctx) {
3880
4561
  return ctx.snapToGrid && ctx.gridSize ? snapPoint(point, ctx.gridSize) : point;
3881
4562
  }
@@ -3928,7 +4609,7 @@ var UpdateLayerCommand = class {
3928
4609
  };
3929
4610
 
3930
4611
  // src/index.ts
3931
- var VERSION = "0.8.5";
4612
+ var VERSION = "0.8.7";
3932
4613
  // Annotate the CommonJS export names for ESM import in node:
3933
4614
  0 && (module.exports = {
3934
4615
  AddElementCommand,
@@ -3951,6 +4632,7 @@ var VERSION = "0.8.5";
3951
4632
  NoteEditor,
3952
4633
  NoteTool,
3953
4634
  PencilTool,
4635
+ Quadtree,
3954
4636
  RemoveElementCommand,
3955
4637
  RemoveLayerCommand,
3956
4638
  SelectTool,
@@ -3961,6 +4643,7 @@ var VERSION = "0.8.5";
3961
4643
  UpdateLayerCommand,
3962
4644
  VERSION,
3963
4645
  Viewport,
4646
+ boundsIntersect,
3964
4647
  clearStaleBindings,
3965
4648
  createArrow,
3966
4649
  createGrid,