@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.js CHANGED
@@ -22,6 +22,153 @@ var EventBus = class {
22
22
  }
23
23
  };
24
24
 
25
+ // src/core/quadtree.ts
26
+ var MAX_ITEMS = 8;
27
+ var MAX_DEPTH = 8;
28
+ function intersects(a, b) {
29
+ 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;
30
+ }
31
+ var QuadNode = class _QuadNode {
32
+ constructor(bounds, depth) {
33
+ this.bounds = bounds;
34
+ this.depth = depth;
35
+ }
36
+ items = [];
37
+ children = null;
38
+ insert(entry) {
39
+ if (this.children) {
40
+ const idx = this.getChildIndex(entry.bounds);
41
+ if (idx !== -1) {
42
+ const child = this.children[idx];
43
+ if (child) child.insert(entry);
44
+ return;
45
+ }
46
+ this.items.push(entry);
47
+ return;
48
+ }
49
+ this.items.push(entry);
50
+ if (this.items.length > MAX_ITEMS && this.depth < MAX_DEPTH) {
51
+ this.split();
52
+ }
53
+ }
54
+ remove(id) {
55
+ const idx = this.items.findIndex((e) => e.id === id);
56
+ if (idx !== -1) {
57
+ this.items.splice(idx, 1);
58
+ return true;
59
+ }
60
+ if (this.children) {
61
+ for (const child of this.children) {
62
+ if (child.remove(id)) {
63
+ this.collapseIfEmpty();
64
+ return true;
65
+ }
66
+ }
67
+ }
68
+ return false;
69
+ }
70
+ query(rect, result) {
71
+ if (!intersects(this.bounds, rect)) return;
72
+ for (const item of this.items) {
73
+ if (intersects(item.bounds, rect)) {
74
+ result.push(item.id);
75
+ }
76
+ }
77
+ if (this.children) {
78
+ for (const child of this.children) {
79
+ child.query(rect, result);
80
+ }
81
+ }
82
+ }
83
+ getChildIndex(itemBounds) {
84
+ const midX = this.bounds.x + this.bounds.w / 2;
85
+ const midY = this.bounds.y + this.bounds.h / 2;
86
+ const left = itemBounds.x >= this.bounds.x && itemBounds.x + itemBounds.w <= midX;
87
+ const right = itemBounds.x >= midX && itemBounds.x + itemBounds.w <= this.bounds.x + this.bounds.w;
88
+ const top = itemBounds.y >= this.bounds.y && itemBounds.y + itemBounds.h <= midY;
89
+ const bottom = itemBounds.y >= midY && itemBounds.y + itemBounds.h <= this.bounds.y + this.bounds.h;
90
+ if (left && top) return 0;
91
+ if (right && top) return 1;
92
+ if (left && bottom) return 2;
93
+ if (right && bottom) return 3;
94
+ return -1;
95
+ }
96
+ split() {
97
+ const { x, y, w, h } = this.bounds;
98
+ const halfW = w / 2;
99
+ const halfH = h / 2;
100
+ const d = this.depth + 1;
101
+ this.children = [
102
+ new _QuadNode({ x, y, w: halfW, h: halfH }, d),
103
+ new _QuadNode({ x: x + halfW, y, w: halfW, h: halfH }, d),
104
+ new _QuadNode({ x, y: y + halfH, w: halfW, h: halfH }, d),
105
+ new _QuadNode({ x: x + halfW, y: y + halfH, w: halfW, h: halfH }, d)
106
+ ];
107
+ const remaining = [];
108
+ for (const item of this.items) {
109
+ const idx = this.getChildIndex(item.bounds);
110
+ if (idx !== -1) {
111
+ const target = this.children[idx];
112
+ if (target) target.insert(item);
113
+ } else {
114
+ remaining.push(item);
115
+ }
116
+ }
117
+ this.items = remaining;
118
+ }
119
+ collapseIfEmpty() {
120
+ if (!this.children) return;
121
+ let totalItems = this.items.length;
122
+ for (const child of this.children) {
123
+ if (child.children) return;
124
+ totalItems += child.items.length;
125
+ }
126
+ if (totalItems <= MAX_ITEMS) {
127
+ for (const child of this.children) {
128
+ this.items.push(...child.items);
129
+ }
130
+ this.children = null;
131
+ }
132
+ }
133
+ };
134
+ var Quadtree = class {
135
+ root;
136
+ _size = 0;
137
+ worldBounds;
138
+ constructor(worldBounds) {
139
+ this.worldBounds = worldBounds;
140
+ this.root = new QuadNode(worldBounds, 0);
141
+ }
142
+ get size() {
143
+ return this._size;
144
+ }
145
+ insert(id, bounds) {
146
+ this.root.insert({ id, bounds });
147
+ this._size++;
148
+ }
149
+ remove(id) {
150
+ if (this.root.remove(id)) {
151
+ this._size--;
152
+ }
153
+ }
154
+ update(id, newBounds) {
155
+ this.remove(id);
156
+ this.insert(id, newBounds);
157
+ }
158
+ query(rect) {
159
+ const result = [];
160
+ this.root.query(rect, result);
161
+ return result;
162
+ }
163
+ queryPoint(point) {
164
+ return this.query({ x: point.x, y: point.y, w: 0, h: 0 });
165
+ }
166
+ clear() {
167
+ this.root = new QuadNode(this.worldBounds, 0);
168
+ this._size = 0;
169
+ }
170
+ };
171
+
25
172
  // src/core/state-serializer.ts
26
173
  var CURRENT_VERSION = 2;
27
174
  function exportState(elements, camera, layers = []) {
@@ -236,16 +383,16 @@ var Camera = class {
236
383
  pan(dx, dy) {
237
384
  this.x += dx;
238
385
  this.y += dy;
239
- this.notifyChange();
386
+ this.notifyPan();
240
387
  }
241
388
  moveTo(x, y) {
242
389
  this.x = x;
243
390
  this.y = y;
244
- this.notifyChange();
391
+ this.notifyPan();
245
392
  }
246
393
  setZoom(level) {
247
394
  this.z = Math.min(this.maxZoom, Math.max(this.minZoom, level));
248
- this.notifyChange();
395
+ this.notifyZoom();
249
396
  }
250
397
  zoomAt(level, screenPoint) {
251
398
  const before = this.screenToWorld(screenPoint);
@@ -253,7 +400,7 @@ var Camera = class {
253
400
  const after = this.screenToWorld(screenPoint);
254
401
  this.x += (after.x - before.x) * this.z;
255
402
  this.y += (after.y - before.y) * this.z;
256
- this.notifyChange();
403
+ this.notifyPanAndZoom();
257
404
  }
258
405
  screenToWorld(screen) {
259
406
  return {
@@ -267,6 +414,16 @@ var Camera = class {
267
414
  y: world.y * this.z + this.y
268
415
  };
269
416
  }
417
+ getVisibleRect(canvasWidth, canvasHeight) {
418
+ const topLeft = this.screenToWorld({ x: 0, y: 0 });
419
+ const bottomRight = this.screenToWorld({ x: canvasWidth, y: canvasHeight });
420
+ return {
421
+ x: topLeft.x,
422
+ y: topLeft.y,
423
+ w: bottomRight.x - topLeft.x,
424
+ h: bottomRight.y - topLeft.y
425
+ };
426
+ }
270
427
  toCSSTransform() {
271
428
  return `translate3d(${this.x}px, ${this.y}px, 0) scale(${this.z})`;
272
429
  }
@@ -274,8 +431,14 @@ var Camera = class {
274
431
  this.changeListeners.add(listener);
275
432
  return () => this.changeListeners.delete(listener);
276
433
  }
277
- notifyChange() {
278
- this.changeListeners.forEach((fn) => fn());
434
+ notifyPan() {
435
+ this.changeListeners.forEach((fn) => fn({ panned: true, zoomed: false }));
436
+ }
437
+ notifyZoom() {
438
+ this.changeListeners.forEach((fn) => fn({ panned: false, zoomed: true }));
439
+ }
440
+ notifyPanAndZoom() {
441
+ this.changeListeners.forEach((fn) => fn({ panned: true, zoomed: true }));
279
442
  }
280
443
  };
281
444
 
@@ -581,68 +744,6 @@ var InputHandler = class {
581
744
  }
582
745
  };
583
746
 
584
- // src/elements/element-store.ts
585
- var ElementStore = class {
586
- elements = /* @__PURE__ */ new Map();
587
- bus = new EventBus();
588
- layerOrderMap = /* @__PURE__ */ new Map();
589
- get count() {
590
- return this.elements.size;
591
- }
592
- setLayerOrder(order) {
593
- this.layerOrderMap = new Map(order);
594
- }
595
- getAll() {
596
- return [...this.elements.values()].sort((a, b) => {
597
- const layerA = this.layerOrderMap.get(a.layerId) ?? 0;
598
- const layerB = this.layerOrderMap.get(b.layerId) ?? 0;
599
- if (layerA !== layerB) return layerA - layerB;
600
- return a.zIndex - b.zIndex;
601
- });
602
- }
603
- getById(id) {
604
- return this.elements.get(id);
605
- }
606
- getElementsByType(type) {
607
- return this.getAll().filter(
608
- (el) => el.type === type
609
- );
610
- }
611
- add(element) {
612
- this.elements.set(element.id, element);
613
- this.bus.emit("add", element);
614
- }
615
- update(id, partial) {
616
- const existing = this.elements.get(id);
617
- if (!existing) return;
618
- const updated = { ...existing, ...partial, id: existing.id, type: existing.type };
619
- this.elements.set(id, updated);
620
- this.bus.emit("update", { previous: existing, current: updated });
621
- }
622
- remove(id) {
623
- const element = this.elements.get(id);
624
- if (!element) return;
625
- this.elements.delete(id);
626
- this.bus.emit("remove", element);
627
- }
628
- clear() {
629
- this.elements.clear();
630
- this.bus.emit("clear", null);
631
- }
632
- snapshot() {
633
- return this.getAll().map((el) => ({ ...el }));
634
- }
635
- loadSnapshot(elements) {
636
- this.elements.clear();
637
- for (const el of elements) {
638
- this.elements.set(el.id, el);
639
- }
640
- }
641
- on(event, listener) {
642
- return this.bus.on(event, listener);
643
- }
644
- };
645
-
646
747
  // src/elements/arrow-geometry.ts
647
748
  function getArrowControlPoint(from, to, bend) {
648
749
  const midX = (from.x + to.x) / 2;
@@ -743,6 +844,185 @@ function isNearLine(point, a, b, threshold) {
743
844
  return Math.hypot(point.x - projX, point.y - projY) <= threshold;
744
845
  }
745
846
 
847
+ // src/elements/element-bounds.ts
848
+ var strokeBoundsCache = /* @__PURE__ */ new WeakMap();
849
+ function getElementBounds(element) {
850
+ if (element.type === "grid") return null;
851
+ if ("size" in element) {
852
+ return {
853
+ x: element.position.x,
854
+ y: element.position.y,
855
+ w: element.size.w,
856
+ h: element.size.h
857
+ };
858
+ }
859
+ if (element.type === "stroke") {
860
+ if (element.points.length === 0) return null;
861
+ const cached = strokeBoundsCache.get(element);
862
+ if (cached) return cached;
863
+ let minX = Infinity;
864
+ let minY = Infinity;
865
+ let maxX = -Infinity;
866
+ let maxY = -Infinity;
867
+ for (const p of element.points) {
868
+ const px = p.x + element.position.x;
869
+ const py = p.y + element.position.y;
870
+ if (px < minX) minX = px;
871
+ if (py < minY) minY = py;
872
+ if (px > maxX) maxX = px;
873
+ if (py > maxY) maxY = py;
874
+ }
875
+ const bounds = { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
876
+ strokeBoundsCache.set(element, bounds);
877
+ return bounds;
878
+ }
879
+ if (element.type === "arrow") {
880
+ return getArrowBoundsAnalytical(element.from, element.to, element.bend);
881
+ }
882
+ return null;
883
+ }
884
+ function getArrowBoundsAnalytical(from, to, bend) {
885
+ if (bend === 0) {
886
+ const minX2 = Math.min(from.x, to.x);
887
+ const minY2 = Math.min(from.y, to.y);
888
+ return {
889
+ x: minX2,
890
+ y: minY2,
891
+ w: Math.abs(to.x - from.x),
892
+ h: Math.abs(to.y - from.y)
893
+ };
894
+ }
895
+ const cp = getArrowControlPoint(from, to, bend);
896
+ let minX = Math.min(from.x, to.x);
897
+ let maxX = Math.max(from.x, to.x);
898
+ let minY = Math.min(from.y, to.y);
899
+ let maxY = Math.max(from.y, to.y);
900
+ const tx = from.x - 2 * cp.x + to.x;
901
+ if (tx !== 0) {
902
+ const t = (from.x - cp.x) / tx;
903
+ if (t > 0 && t < 1) {
904
+ const mt = 1 - t;
905
+ const x = mt * mt * from.x + 2 * mt * t * cp.x + t * t * to.x;
906
+ if (x < minX) minX = x;
907
+ if (x > maxX) maxX = x;
908
+ }
909
+ }
910
+ const ty = from.y - 2 * cp.y + to.y;
911
+ if (ty !== 0) {
912
+ const t = (from.y - cp.y) / ty;
913
+ if (t > 0 && t < 1) {
914
+ const mt = 1 - t;
915
+ const y = mt * mt * from.y + 2 * mt * t * cp.y + t * t * to.y;
916
+ if (y < minY) minY = y;
917
+ if (y > maxY) maxY = y;
918
+ }
919
+ }
920
+ return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
921
+ }
922
+ function boundsIntersect(a, b) {
923
+ 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;
924
+ }
925
+
926
+ // src/elements/element-store.ts
927
+ var ElementStore = class {
928
+ elements = /* @__PURE__ */ new Map();
929
+ bus = new EventBus();
930
+ layerOrderMap = /* @__PURE__ */ new Map();
931
+ spatialIndex = new Quadtree({ x: -1e5, y: -1e5, w: 2e5, h: 2e5 });
932
+ get count() {
933
+ return this.elements.size;
934
+ }
935
+ setLayerOrder(order) {
936
+ this.layerOrderMap = new Map(order);
937
+ }
938
+ getAll() {
939
+ return [...this.elements.values()].sort((a, b) => {
940
+ const layerA = this.layerOrderMap.get(a.layerId) ?? 0;
941
+ const layerB = this.layerOrderMap.get(b.layerId) ?? 0;
942
+ if (layerA !== layerB) return layerA - layerB;
943
+ return a.zIndex - b.zIndex;
944
+ });
945
+ }
946
+ getById(id) {
947
+ return this.elements.get(id);
948
+ }
949
+ getElementsByType(type) {
950
+ return this.getAll().filter(
951
+ (el) => el.type === type
952
+ );
953
+ }
954
+ add(element) {
955
+ this.elements.set(element.id, element);
956
+ const bounds = getElementBounds(element);
957
+ if (bounds) this.spatialIndex.insert(element.id, bounds);
958
+ this.bus.emit("add", element);
959
+ }
960
+ update(id, partial) {
961
+ const existing = this.elements.get(id);
962
+ if (!existing) return;
963
+ const updated = { ...existing, ...partial, id: existing.id, type: existing.type };
964
+ this.elements.set(id, updated);
965
+ const newBounds = getElementBounds(updated);
966
+ if (newBounds) {
967
+ this.spatialIndex.update(id, newBounds);
968
+ }
969
+ this.bus.emit("update", { previous: existing, current: updated });
970
+ }
971
+ remove(id) {
972
+ const element = this.elements.get(id);
973
+ if (!element) return;
974
+ this.elements.delete(id);
975
+ this.spatialIndex.remove(id);
976
+ this.bus.emit("remove", element);
977
+ }
978
+ clear() {
979
+ this.elements.clear();
980
+ this.spatialIndex.clear();
981
+ this.bus.emit("clear", null);
982
+ }
983
+ snapshot() {
984
+ return this.getAll().map((el) => ({ ...el }));
985
+ }
986
+ loadSnapshot(elements) {
987
+ this.elements.clear();
988
+ this.spatialIndex.clear();
989
+ for (const el of elements) {
990
+ this.elements.set(el.id, el);
991
+ const bounds = getElementBounds(el);
992
+ if (bounds) this.spatialIndex.insert(el.id, bounds);
993
+ }
994
+ }
995
+ queryRect(rect) {
996
+ const ids = this.spatialIndex.query(rect);
997
+ const elements = [];
998
+ for (const id of ids) {
999
+ const el = this.elements.get(id);
1000
+ if (el) elements.push(el);
1001
+ }
1002
+ return elements.sort((a, b) => {
1003
+ const layerA = this.layerOrderMap.get(a.layerId) ?? 0;
1004
+ const layerB = this.layerOrderMap.get(b.layerId) ?? 0;
1005
+ if (layerA !== layerB) return layerA - layerB;
1006
+ return a.zIndex - b.zIndex;
1007
+ });
1008
+ }
1009
+ queryPoint(point) {
1010
+ return this.queryRect({ x: point.x, y: point.y, w: 0, h: 0 });
1011
+ }
1012
+ on(event, listener) {
1013
+ return this.bus.on(event, listener);
1014
+ }
1015
+ onChange(listener) {
1016
+ const unsubs = [
1017
+ this.bus.on("add", listener),
1018
+ this.bus.on("remove", listener),
1019
+ this.bus.on("update", listener),
1020
+ this.bus.on("clear", listener)
1021
+ ];
1022
+ return () => unsubs.forEach((fn) => fn());
1023
+ }
1024
+ };
1025
+
746
1026
  // src/elements/arrow-binding.ts
747
1027
  var BINDABLE_TYPES = /* @__PURE__ */ new Set(["note", "text", "image", "html", "shape"]);
748
1028
  function isBindable(element) {
@@ -757,15 +1037,6 @@ function getElementCenter(element) {
757
1037
  y: element.position.y + element.size.h / 2
758
1038
  };
759
1039
  }
760
- function getElementBounds(element) {
761
- if (!("size" in element)) return null;
762
- return {
763
- x: element.position.x,
764
- y: element.position.y,
765
- w: element.size.w,
766
- h: element.size.h
767
- };
768
- }
769
1040
  function getEdgeIntersection(bounds, outsidePoint) {
770
1041
  const cx = bounds.x + bounds.w / 2;
771
1042
  const cy = bounds.y + bounds.h / 2;
@@ -2092,6 +2363,10 @@ var LayerManager = class {
2092
2363
  this.updateLayerDirect(id, { locked });
2093
2364
  return true;
2094
2365
  }
2366
+ setLayerOpacity(id, opacity) {
2367
+ if (!this.layers.has(id)) return;
2368
+ this.updateLayerDirect(id, { opacity: Math.max(0, Math.min(1, opacity)) });
2369
+ }
2095
2370
  setActiveLayer(id) {
2096
2371
  if (!this.layers.has(id)) return;
2097
2372
  this._activeLayerId = id;
@@ -2147,36 +2422,522 @@ var LayerManager = class {
2147
2422
  }
2148
2423
  };
2149
2424
 
2150
- // src/canvas/viewport.ts
2151
- var Viewport = class {
2152
- constructor(container, options = {}) {
2153
- this.container = container;
2154
- this.camera = new Camera(options.camera);
2155
- this.background = new Background(options.background);
2156
- this._gridSize = options.background?.spacing ?? 24;
2157
- this.store = new ElementStore();
2158
- this.layerManager = new LayerManager(this.store);
2159
- this.toolManager = new ToolManager();
2160
- this.renderer = new ElementRenderer();
2161
- this.renderer.setStore(this.store);
2162
- this.renderer.setCamera(this.camera);
2163
- this.renderer.setOnImageLoad(() => this.requestRender());
2164
- this.noteEditor = new NoteEditor();
2165
- this.noteEditor.setOnStop((id) => this.onTextEditStop(id));
2166
- this.history = new HistoryStack();
2167
- this.historyRecorder = new HistoryRecorder(this.store, this.history);
2168
- this.wrapper = this.createWrapper();
2169
- this.canvasEl = this.createCanvas();
2170
- this.domLayer = this.createDomLayer();
2171
- this.wrapper.appendChild(this.canvasEl);
2172
- this.wrapper.appendChild(this.domLayer);
2173
- this.container.appendChild(this.wrapper);
2174
- this.toolContext = {
2175
- camera: this.camera,
2176
- store: this.store,
2177
- requestRender: () => this.requestRender(),
2178
- switchTool: (name) => this.toolManager.setTool(name, this.toolContext),
2179
- editElement: (id) => this.startEditingElement(id),
2425
+ // src/canvas/interact-mode.ts
2426
+ var InteractMode = class {
2427
+ interactingElementId = null;
2428
+ getNode;
2429
+ constructor(deps) {
2430
+ this.getNode = deps.getNode;
2431
+ }
2432
+ startInteracting(id) {
2433
+ this.stopInteracting();
2434
+ const node = this.getNode(id);
2435
+ if (!node) return;
2436
+ this.interactingElementId = id;
2437
+ node.style.pointerEvents = "auto";
2438
+ node.addEventListener("pointerdown", this.onNodePointerDown);
2439
+ window.addEventListener("keydown", this.onKeyDown);
2440
+ window.addEventListener("pointerdown", this.onPointerDown);
2441
+ }
2442
+ stopInteracting() {
2443
+ if (!this.interactingElementId) return;
2444
+ const node = this.getNode(this.interactingElementId);
2445
+ if (node) {
2446
+ node.style.pointerEvents = "none";
2447
+ node.removeEventListener("pointerdown", this.onNodePointerDown);
2448
+ }
2449
+ this.interactingElementId = null;
2450
+ window.removeEventListener("keydown", this.onKeyDown);
2451
+ window.removeEventListener("pointerdown", this.onPointerDown);
2452
+ }
2453
+ isInteracting() {
2454
+ return this.interactingElementId !== null;
2455
+ }
2456
+ destroy() {
2457
+ this.stopInteracting();
2458
+ }
2459
+ onNodePointerDown = (e) => {
2460
+ e.stopPropagation();
2461
+ };
2462
+ onKeyDown = (e) => {
2463
+ if (e.key === "Escape") {
2464
+ this.stopInteracting();
2465
+ }
2466
+ };
2467
+ onPointerDown = (e) => {
2468
+ if (!this.interactingElementId) return;
2469
+ const target = e.target;
2470
+ if (!(target instanceof Element)) {
2471
+ this.stopInteracting();
2472
+ return;
2473
+ }
2474
+ const node = this.getNode(this.interactingElementId);
2475
+ if (node && !node.contains(target)) {
2476
+ this.stopInteracting();
2477
+ }
2478
+ };
2479
+ };
2480
+
2481
+ // src/canvas/dom-node-manager.ts
2482
+ var DomNodeManager = class {
2483
+ domNodes = /* @__PURE__ */ new Map();
2484
+ htmlContent = /* @__PURE__ */ new Map();
2485
+ domLayer;
2486
+ onEditRequest;
2487
+ isEditingElement;
2488
+ constructor(deps) {
2489
+ this.domLayer = deps.domLayer;
2490
+ this.onEditRequest = deps.onEditRequest;
2491
+ this.isEditingElement = deps.isEditingElement;
2492
+ }
2493
+ getNode(id) {
2494
+ return this.domNodes.get(id);
2495
+ }
2496
+ storeHtmlContent(elementId, dom) {
2497
+ this.htmlContent.set(elementId, dom);
2498
+ }
2499
+ syncDomNode(element, zIndex = 0) {
2500
+ let node = this.domNodes.get(element.id);
2501
+ if (!node) {
2502
+ node = document.createElement("div");
2503
+ node.dataset["elementId"] = element.id;
2504
+ Object.assign(node.style, {
2505
+ position: "absolute",
2506
+ pointerEvents: "auto"
2507
+ });
2508
+ this.domLayer.appendChild(node);
2509
+ this.domNodes.set(element.id, node);
2510
+ }
2511
+ const size = "size" in element ? element.size : null;
2512
+ Object.assign(node.style, {
2513
+ display: "block",
2514
+ left: `${element.position.x}px`,
2515
+ top: `${element.position.y}px`,
2516
+ width: size ? `${size.w}px` : "auto",
2517
+ height: size ? `${size.h}px` : "auto",
2518
+ zIndex: String(zIndex)
2519
+ });
2520
+ this.renderDomContent(node, element);
2521
+ }
2522
+ hideDomNode(id) {
2523
+ const node = this.domNodes.get(id);
2524
+ if (node) node.style.display = "none";
2525
+ }
2526
+ removeDomNode(id) {
2527
+ this.htmlContent.delete(id);
2528
+ const node = this.domNodes.get(id);
2529
+ if (node) {
2530
+ node.remove();
2531
+ this.domNodes.delete(id);
2532
+ }
2533
+ }
2534
+ clearDomNodes() {
2535
+ this.domNodes.forEach((node) => node.remove());
2536
+ this.domNodes.clear();
2537
+ this.htmlContent.clear();
2538
+ }
2539
+ reattachHtmlContent(store) {
2540
+ for (const el of store.getElementsByType("html")) {
2541
+ if (el.domId) {
2542
+ const dom = document.getElementById(el.domId);
2543
+ if (dom) {
2544
+ this.htmlContent.set(el.id, dom);
2545
+ }
2546
+ }
2547
+ }
2548
+ }
2549
+ renderDomContent(node, element) {
2550
+ if (element.type === "note") {
2551
+ if (!node.dataset["initialized"]) {
2552
+ node.dataset["initialized"] = "true";
2553
+ Object.assign(node.style, {
2554
+ backgroundColor: element.backgroundColor,
2555
+ color: element.textColor,
2556
+ padding: "8px",
2557
+ borderRadius: "4px",
2558
+ boxShadow: "0 2px 8px rgba(0,0,0,0.15)",
2559
+ fontSize: "14px",
2560
+ overflow: "hidden",
2561
+ cursor: "default",
2562
+ userSelect: "none",
2563
+ wordWrap: "break-word"
2564
+ });
2565
+ node.textContent = element.text || "";
2566
+ node.addEventListener("dblclick", (e) => {
2567
+ e.stopPropagation();
2568
+ const id = node.dataset["elementId"];
2569
+ if (id) this.onEditRequest(id);
2570
+ });
2571
+ }
2572
+ if (!this.isEditingElement(element.id)) {
2573
+ if (node.textContent !== element.text) {
2574
+ node.textContent = element.text || "";
2575
+ }
2576
+ node.style.backgroundColor = element.backgroundColor;
2577
+ node.style.color = element.textColor;
2578
+ }
2579
+ }
2580
+ if (element.type === "html" && !node.dataset["initialized"]) {
2581
+ const content = this.htmlContent.get(element.id);
2582
+ if (content) {
2583
+ node.dataset["initialized"] = "true";
2584
+ Object.assign(node.style, {
2585
+ overflow: "hidden",
2586
+ pointerEvents: "none"
2587
+ });
2588
+ node.appendChild(content);
2589
+ }
2590
+ }
2591
+ if (element.type === "text") {
2592
+ if (!node.dataset["initialized"]) {
2593
+ node.dataset["initialized"] = "true";
2594
+ Object.assign(node.style, {
2595
+ padding: "2px",
2596
+ fontSize: `${element.fontSize}px`,
2597
+ color: element.color,
2598
+ textAlign: element.textAlign,
2599
+ background: "none",
2600
+ border: "none",
2601
+ boxShadow: "none",
2602
+ overflow: "visible",
2603
+ cursor: "default",
2604
+ userSelect: "none",
2605
+ wordWrap: "break-word",
2606
+ whiteSpace: "pre-wrap",
2607
+ lineHeight: "1.4"
2608
+ });
2609
+ node.textContent = element.text || "";
2610
+ node.addEventListener("dblclick", (e) => {
2611
+ e.stopPropagation();
2612
+ const id = node.dataset["elementId"];
2613
+ if (id) this.onEditRequest(id);
2614
+ });
2615
+ }
2616
+ if (!this.isEditingElement(element.id)) {
2617
+ if (node.textContent !== element.text) {
2618
+ node.textContent = element.text || "";
2619
+ }
2620
+ Object.assign(node.style, {
2621
+ fontSize: `${element.fontSize}px`,
2622
+ color: element.color,
2623
+ textAlign: element.textAlign
2624
+ });
2625
+ }
2626
+ }
2627
+ }
2628
+ };
2629
+
2630
+ // src/canvas/render-stats.ts
2631
+ var SAMPLE_SIZE = 60;
2632
+ var RenderStats = class {
2633
+ frameTimes = [];
2634
+ frameCount = 0;
2635
+ recordFrame(durationMs) {
2636
+ this.frameCount++;
2637
+ this.frameTimes.push(durationMs);
2638
+ if (this.frameTimes.length > SAMPLE_SIZE) {
2639
+ this.frameTimes.shift();
2640
+ }
2641
+ }
2642
+ getSnapshot() {
2643
+ const times = this.frameTimes;
2644
+ if (times.length === 0) {
2645
+ return { fps: 0, avgFrameMs: 0, p95FrameMs: 0, lastFrameMs: 0, frameCount: 0 };
2646
+ }
2647
+ const avg = times.reduce((a, b) => a + b, 0) / times.length;
2648
+ const sorted = [...times].sort((a, b) => a - b);
2649
+ const p95Index = Math.min(Math.floor(sorted.length * 0.95), sorted.length - 1);
2650
+ const lastFrame = times[times.length - 1] ?? 0;
2651
+ return {
2652
+ fps: avg > 0 ? Math.round(1e3 / avg) : 0,
2653
+ avgFrameMs: Math.round(avg * 100) / 100,
2654
+ p95FrameMs: Math.round((sorted[p95Index] ?? 0) * 100) / 100,
2655
+ lastFrameMs: Math.round(lastFrame * 100) / 100,
2656
+ frameCount: this.frameCount
2657
+ };
2658
+ }
2659
+ reset() {
2660
+ this.frameTimes = [];
2661
+ this.frameCount = 0;
2662
+ }
2663
+ };
2664
+
2665
+ // src/canvas/render-loop.ts
2666
+ var RenderLoop = class {
2667
+ needsRender = false;
2668
+ animFrameId = 0;
2669
+ canvasEl;
2670
+ camera;
2671
+ background;
2672
+ store;
2673
+ renderer;
2674
+ toolManager;
2675
+ layerManager;
2676
+ domNodeManager;
2677
+ layerCache;
2678
+ activeDrawingLayerId = null;
2679
+ lastZoom;
2680
+ lastCamX;
2681
+ lastCamY;
2682
+ stats = new RenderStats();
2683
+ constructor(deps) {
2684
+ this.canvasEl = deps.canvasEl;
2685
+ this.camera = deps.camera;
2686
+ this.background = deps.background;
2687
+ this.store = deps.store;
2688
+ this.renderer = deps.renderer;
2689
+ this.toolManager = deps.toolManager;
2690
+ this.layerManager = deps.layerManager;
2691
+ this.domNodeManager = deps.domNodeManager;
2692
+ this.layerCache = deps.layerCache;
2693
+ this.lastZoom = deps.camera.zoom;
2694
+ this.lastCamX = deps.camera.position.x;
2695
+ this.lastCamY = deps.camera.position.y;
2696
+ }
2697
+ requestRender() {
2698
+ this.needsRender = true;
2699
+ }
2700
+ flush() {
2701
+ if (this.needsRender) {
2702
+ this.render();
2703
+ this.needsRender = false;
2704
+ }
2705
+ }
2706
+ start() {
2707
+ const loop = () => {
2708
+ if (this.needsRender) {
2709
+ this.render();
2710
+ this.needsRender = false;
2711
+ }
2712
+ this.animFrameId = requestAnimationFrame(loop);
2713
+ };
2714
+ this.animFrameId = requestAnimationFrame(loop);
2715
+ }
2716
+ stop() {
2717
+ cancelAnimationFrame(this.animFrameId);
2718
+ }
2719
+ setCanvasSize(width, height) {
2720
+ this.canvasEl.width = width;
2721
+ this.canvasEl.height = height;
2722
+ this.layerCache.resize(this.canvasEl.clientWidth, this.canvasEl.clientHeight);
2723
+ }
2724
+ setActiveDrawingLayer(layerId) {
2725
+ this.activeDrawingLayerId = layerId;
2726
+ }
2727
+ markLayerDirty(layerId) {
2728
+ this.layerCache.markDirty(layerId);
2729
+ }
2730
+ markAllLayersDirty() {
2731
+ this.layerCache.markAllDirty();
2732
+ }
2733
+ getStats() {
2734
+ return this.stats.getSnapshot();
2735
+ }
2736
+ compositeLayerCache(ctx, layerId, dpr) {
2737
+ const cached = this.layerCache.getCanvas(layerId);
2738
+ ctx.save();
2739
+ ctx.scale(1 / this.camera.zoom, 1 / this.camera.zoom);
2740
+ ctx.translate(-this.camera.position.x, -this.camera.position.y);
2741
+ ctx.scale(1 / dpr, 1 / dpr);
2742
+ ctx.drawImage(cached, 0, 0);
2743
+ ctx.restore();
2744
+ }
2745
+ render() {
2746
+ const t0 = performance.now();
2747
+ const ctx = this.canvasEl.getContext("2d");
2748
+ if (!ctx) return;
2749
+ const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
2750
+ const cssWidth = this.canvasEl.clientWidth;
2751
+ const cssHeight = this.canvasEl.clientHeight;
2752
+ const currentZoom = this.camera.zoom;
2753
+ const currentCamX = this.camera.position.x;
2754
+ const currentCamY = this.camera.position.y;
2755
+ if (currentZoom !== this.lastZoom || currentCamX !== this.lastCamX || currentCamY !== this.lastCamY) {
2756
+ this.layerCache.markAllDirty();
2757
+ this.lastZoom = currentZoom;
2758
+ this.lastCamX = currentCamX;
2759
+ this.lastCamY = currentCamY;
2760
+ }
2761
+ ctx.save();
2762
+ ctx.scale(dpr, dpr);
2763
+ this.renderer.setCanvasSize(cssWidth, cssHeight);
2764
+ this.background.render(ctx, this.camera);
2765
+ ctx.save();
2766
+ ctx.translate(this.camera.position.x, this.camera.position.y);
2767
+ ctx.scale(this.camera.zoom, this.camera.zoom);
2768
+ const visibleRect = this.camera.getVisibleRect(cssWidth, cssHeight);
2769
+ const margin = Math.max(visibleRect.w, visibleRect.h) * 0.1;
2770
+ const cullingRect = {
2771
+ x: visibleRect.x - margin,
2772
+ y: visibleRect.y - margin,
2773
+ w: visibleRect.w + margin * 2,
2774
+ h: visibleRect.h + margin * 2
2775
+ };
2776
+ const allElements = this.store.getAll();
2777
+ const layerElements = /* @__PURE__ */ new Map();
2778
+ const gridElements = [];
2779
+ let domZIndex = 0;
2780
+ for (const element of allElements) {
2781
+ if (!this.layerManager.isLayerVisible(element.layerId)) {
2782
+ if (this.renderer.isDomElement(element)) {
2783
+ this.domNodeManager.hideDomNode(element.id);
2784
+ }
2785
+ continue;
2786
+ }
2787
+ if (this.renderer.isDomElement(element)) {
2788
+ const elBounds = getElementBounds(element);
2789
+ if (elBounds && !boundsIntersect(elBounds, cullingRect)) {
2790
+ this.domNodeManager.hideDomNode(element.id);
2791
+ } else {
2792
+ this.domNodeManager.syncDomNode(element, domZIndex++);
2793
+ }
2794
+ continue;
2795
+ }
2796
+ if (element.type === "grid") {
2797
+ gridElements.push(element);
2798
+ continue;
2799
+ }
2800
+ let group = layerElements.get(element.layerId);
2801
+ if (!group) {
2802
+ group = [];
2803
+ layerElements.set(element.layerId, group);
2804
+ }
2805
+ group.push(element);
2806
+ }
2807
+ for (const grid of gridElements) {
2808
+ this.renderer.renderCanvasElement(ctx, grid);
2809
+ }
2810
+ for (const [layerId, elements] of layerElements) {
2811
+ const isActiveDrawingLayer = layerId === this.activeDrawingLayerId;
2812
+ if (!this.layerCache.isDirty(layerId)) {
2813
+ this.compositeLayerCache(ctx, layerId, dpr);
2814
+ continue;
2815
+ }
2816
+ if (isActiveDrawingLayer) {
2817
+ this.compositeLayerCache(ctx, layerId, dpr);
2818
+ continue;
2819
+ }
2820
+ const offCtx = this.layerCache.getContext(layerId);
2821
+ if (offCtx) {
2822
+ const offCanvas = this.layerCache.getCanvas(layerId);
2823
+ offCtx.clearRect(0, 0, offCanvas.width, offCanvas.height);
2824
+ offCtx.save();
2825
+ offCtx.scale(dpr, dpr);
2826
+ offCtx.translate(this.camera.position.x, this.camera.position.y);
2827
+ offCtx.scale(this.camera.zoom, this.camera.zoom);
2828
+ for (const element of elements) {
2829
+ const elBounds = getElementBounds(element);
2830
+ if (elBounds && !boundsIntersect(elBounds, cullingRect)) continue;
2831
+ this.renderer.renderCanvasElement(offCtx, element);
2832
+ }
2833
+ offCtx.restore();
2834
+ this.layerCache.markClean(layerId);
2835
+ this.compositeLayerCache(ctx, layerId, dpr);
2836
+ }
2837
+ }
2838
+ const activeTool = this.toolManager.activeTool;
2839
+ if (activeTool?.renderOverlay) {
2840
+ activeTool.renderOverlay(ctx);
2841
+ }
2842
+ ctx.restore();
2843
+ ctx.restore();
2844
+ this.stats.recordFrame(performance.now() - t0);
2845
+ }
2846
+ };
2847
+
2848
+ // src/canvas/layer-cache.ts
2849
+ function createOffscreenCanvas(width, height) {
2850
+ if (typeof OffscreenCanvas !== "undefined") {
2851
+ return new OffscreenCanvas(width, height);
2852
+ }
2853
+ const canvas = document.createElement("canvas");
2854
+ canvas.width = width;
2855
+ canvas.height = height;
2856
+ return canvas;
2857
+ }
2858
+ var LayerCache = class {
2859
+ canvases = /* @__PURE__ */ new Map();
2860
+ dirtyFlags = /* @__PURE__ */ new Map();
2861
+ width;
2862
+ height;
2863
+ constructor(width, height) {
2864
+ const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
2865
+ this.width = Math.round(width * dpr);
2866
+ this.height = Math.round(height * dpr);
2867
+ }
2868
+ isDirty(layerId) {
2869
+ return this.dirtyFlags.get(layerId) !== false;
2870
+ }
2871
+ markDirty(layerId) {
2872
+ this.dirtyFlags.set(layerId, true);
2873
+ }
2874
+ markClean(layerId) {
2875
+ this.dirtyFlags.set(layerId, false);
2876
+ }
2877
+ markAllDirty() {
2878
+ for (const [id] of this.dirtyFlags) {
2879
+ this.dirtyFlags.set(id, true);
2880
+ }
2881
+ }
2882
+ getCanvas(layerId) {
2883
+ let canvas = this.canvases.get(layerId);
2884
+ if (!canvas) {
2885
+ canvas = createOffscreenCanvas(this.width, this.height);
2886
+ this.canvases.set(layerId, canvas);
2887
+ this.dirtyFlags.set(layerId, true);
2888
+ }
2889
+ return canvas;
2890
+ }
2891
+ getContext(layerId) {
2892
+ const canvas = this.getCanvas(layerId);
2893
+ return canvas.getContext("2d");
2894
+ }
2895
+ resize(width, height) {
2896
+ const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
2897
+ this.width = Math.round(width * dpr);
2898
+ this.height = Math.round(height * dpr);
2899
+ for (const [id, canvas] of this.canvases) {
2900
+ canvas.width = this.width;
2901
+ canvas.height = this.height;
2902
+ this.dirtyFlags.set(id, true);
2903
+ }
2904
+ }
2905
+ clear() {
2906
+ this.canvases.clear();
2907
+ this.dirtyFlags.clear();
2908
+ }
2909
+ };
2910
+
2911
+ // src/canvas/viewport.ts
2912
+ var Viewport = class {
2913
+ constructor(container, options = {}) {
2914
+ this.container = container;
2915
+ this.camera = new Camera(options.camera);
2916
+ this.background = new Background(options.background);
2917
+ this._gridSize = options.background?.spacing ?? 24;
2918
+ this.store = new ElementStore();
2919
+ this.layerManager = new LayerManager(this.store);
2920
+ this.toolManager = new ToolManager();
2921
+ this.renderer = new ElementRenderer();
2922
+ this.renderer.setStore(this.store);
2923
+ this.renderer.setCamera(this.camera);
2924
+ this.renderer.setOnImageLoad(() => this.requestRender());
2925
+ this.noteEditor = new NoteEditor();
2926
+ this.noteEditor.setOnStop((id) => this.onTextEditStop(id));
2927
+ this.history = new HistoryStack();
2928
+ this.historyRecorder = new HistoryRecorder(this.store, this.history);
2929
+ this.wrapper = this.createWrapper();
2930
+ this.canvasEl = this.createCanvas();
2931
+ this.domLayer = this.createDomLayer();
2932
+ this.wrapper.appendChild(this.canvasEl);
2933
+ this.wrapper.appendChild(this.domLayer);
2934
+ this.container.appendChild(this.wrapper);
2935
+ this.toolContext = {
2936
+ camera: this.camera,
2937
+ store: this.store,
2938
+ requestRender: () => this.requestRender(),
2939
+ switchTool: (name) => this.toolManager.setTool(name, this.toolContext),
2940
+ editElement: (id) => this.startEditingElement(id),
2180
2941
  setCursor: (cursor) => {
2181
2942
  this.wrapper.style.cursor = cursor;
2182
2943
  },
@@ -2192,18 +2953,56 @@ var Viewport = class {
2192
2953
  historyRecorder: this.historyRecorder,
2193
2954
  historyStack: this.history
2194
2955
  });
2956
+ this.domNodeManager = new DomNodeManager({
2957
+ domLayer: this.domLayer,
2958
+ onEditRequest: (id) => this.startEditingElement(id),
2959
+ isEditingElement: (id) => this.noteEditor.isEditing && this.noteEditor.editingElementId === id
2960
+ });
2961
+ this.interactMode = new InteractMode({
2962
+ getNode: (id) => this.domNodeManager.getNode(id)
2963
+ });
2964
+ const layerCache = new LayerCache(
2965
+ this.canvasEl.clientWidth || 800,
2966
+ this.canvasEl.clientHeight || 600
2967
+ );
2968
+ this.renderLoop = new RenderLoop({
2969
+ canvasEl: this.canvasEl,
2970
+ camera: this.camera,
2971
+ background: this.background,
2972
+ store: this.store,
2973
+ renderer: this.renderer,
2974
+ toolManager: this.toolManager,
2975
+ layerManager: this.layerManager,
2976
+ domNodeManager: this.domNodeManager,
2977
+ layerCache
2978
+ });
2195
2979
  this.unsubCamera = this.camera.onChange(() => {
2196
2980
  this.applyCameraTransform();
2197
2981
  this.requestRender();
2198
2982
  });
2199
2983
  this.unsubStore = [
2200
- this.store.on("add", () => this.requestRender()),
2984
+ this.store.on("add", (el) => {
2985
+ this.renderLoop.markLayerDirty(el.layerId);
2986
+ this.requestRender();
2987
+ }),
2201
2988
  this.store.on("remove", (el) => {
2202
2989
  this.unbindArrowsFrom(el);
2203
- this.removeDomNode(el.id);
2990
+ this.domNodeManager.removeDomNode(el.id);
2991
+ this.renderLoop.markLayerDirty(el.layerId);
2992
+ this.requestRender();
2993
+ }),
2994
+ this.store.on("update", ({ previous, current }) => {
2995
+ this.renderLoop.markLayerDirty(current.layerId);
2996
+ if (previous.layerId !== current.layerId) {
2997
+ this.renderLoop.markLayerDirty(previous.layerId);
2998
+ }
2999
+ this.requestRender();
2204
3000
  }),
2205
- this.store.on("update", () => this.requestRender()),
2206
- this.store.on("clear", () => this.clearDomNodes())
3001
+ this.store.on("clear", () => {
3002
+ this.domNodeManager.clearDomNodes();
3003
+ this.renderLoop.markAllLayersDirty();
3004
+ this.requestRender();
3005
+ })
2207
3006
  ];
2208
3007
  this.layerManager.on("change", () => {
2209
3008
  this.toolContext.activeLayerId = this.layerManager.activeLayerId;
@@ -2214,7 +3013,7 @@ var Viewport = class {
2214
3013
  this.wrapper.addEventListener("drop", this.onDrop);
2215
3014
  this.observeResize();
2216
3015
  this.syncCanvasSize();
2217
- this.startRenderLoop();
3016
+ this.renderLoop.start();
2218
3017
  }
2219
3018
  camera;
2220
3019
  store;
@@ -2233,13 +3032,11 @@ var Viewport = class {
2233
3032
  historyRecorder;
2234
3033
  toolContext;
2235
3034
  resizeObserver = null;
2236
- animFrameId = 0;
2237
3035
  _snapToGrid = false;
2238
3036
  _gridSize;
2239
- needsRender = true;
2240
- domNodes = /* @__PURE__ */ new Map();
2241
- htmlContent = /* @__PURE__ */ new Map();
2242
- interactingElementId = null;
3037
+ renderLoop;
3038
+ domNodeManager;
3039
+ interactMode;
2243
3040
  get ctx() {
2244
3041
  return this.canvasEl.getContext("2d");
2245
3042
  }
@@ -2251,7 +3048,7 @@ var Viewport = class {
2251
3048
  this.toolContext.snapToGrid = enabled;
2252
3049
  }
2253
3050
  requestRender() {
2254
- this.needsRender = true;
3051
+ this.renderLoop.requestRender();
2255
3052
  }
2256
3053
  exportState() {
2257
3054
  return exportState(this.store.snapshot(), this.camera, this.layerManager.snapshot());
@@ -2265,12 +3062,12 @@ var Viewport = class {
2265
3062
  loadState(state) {
2266
3063
  this.historyRecorder.pause();
2267
3064
  this.noteEditor.destroy(this.store);
2268
- this.clearDomNodes();
3065
+ this.domNodeManager.clearDomNodes();
2269
3066
  this.store.loadSnapshot(state.elements);
2270
3067
  if (state.layers && state.layers.length > 0) {
2271
3068
  this.layerManager.loadSnapshot(state.layers);
2272
3069
  }
2273
- this.reattachHtmlContent();
3070
+ this.domNodeManager.reattachHtmlContent(this.store);
2274
3071
  this.history.clear();
2275
3072
  this.historyRecorder.resume();
2276
3073
  this.camera.moveTo(state.camera.position.x, state.camera.position.y);
@@ -2309,7 +3106,7 @@ var Viewport = class {
2309
3106
  domId,
2310
3107
  layerId: this.layerManager.activeLayerId
2311
3108
  });
2312
- this.htmlContent.set(el.id, dom);
3109
+ this.domNodeManager.storeHtmlContent(el.id, dom);
2313
3110
  this.historyRecorder.begin();
2314
3111
  this.store.add(el);
2315
3112
  this.historyRecorder.commit();
@@ -2345,8 +3142,8 @@ var Viewport = class {
2345
3142
  this.requestRender();
2346
3143
  }
2347
3144
  destroy() {
2348
- cancelAnimationFrame(this.animFrameId);
2349
- this.stopInteracting();
3145
+ this.renderLoop.stop();
3146
+ this.interactMode.destroy();
2350
3147
  this.noteEditor.destroy(this.store);
2351
3148
  this.historyRecorder.destroy();
2352
3149
  this.wrapper.removeEventListener("dblclick", this.onDblClick);
@@ -2359,54 +3156,11 @@ var Viewport = class {
2359
3156
  this.resizeObserver = null;
2360
3157
  this.wrapper.remove();
2361
3158
  }
2362
- startRenderLoop() {
2363
- const loop = () => {
2364
- if (this.needsRender) {
2365
- this.render();
2366
- this.needsRender = false;
2367
- }
2368
- this.animFrameId = requestAnimationFrame(loop);
2369
- };
2370
- this.animFrameId = requestAnimationFrame(loop);
2371
- }
2372
- render() {
2373
- const ctx = this.ctx;
2374
- if (!ctx) return;
2375
- const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
2376
- ctx.save();
2377
- ctx.scale(dpr, dpr);
2378
- this.renderer.setCanvasSize(this.canvasEl.clientWidth, this.canvasEl.clientHeight);
2379
- this.background.render(ctx, this.camera);
2380
- ctx.save();
2381
- ctx.translate(this.camera.position.x, this.camera.position.y);
2382
- ctx.scale(this.camera.zoom, this.camera.zoom);
2383
- const allElements = this.store.getAll();
2384
- let domZIndex = 0;
2385
- for (const element of allElements) {
2386
- if (!this.layerManager.isLayerVisible(element.layerId)) {
2387
- if (this.renderer.isDomElement(element)) {
2388
- this.hideDomNode(element.id);
2389
- }
2390
- continue;
2391
- }
2392
- if (this.renderer.isDomElement(element)) {
2393
- this.syncDomNode(element, domZIndex++);
2394
- } else {
2395
- this.renderer.renderCanvasElement(ctx, element);
2396
- }
2397
- }
2398
- const activeTool = this.toolManager.activeTool;
2399
- if (activeTool?.renderOverlay) {
2400
- activeTool.renderOverlay(ctx);
2401
- }
2402
- ctx.restore();
2403
- ctx.restore();
2404
- }
2405
3159
  startEditingElement(id) {
2406
3160
  const element = this.store.getById(id);
2407
3161
  if (!element || element.type !== "note" && element.type !== "text") return;
2408
- this.render();
2409
- const node = this.domNodes.get(id);
3162
+ this.renderLoop.flush();
3163
+ const node = this.domNodeManager.getNode(id);
2410
3164
  if (node) {
2411
3165
  this.noteEditor.startEditing(node, id, this.store);
2412
3166
  }
@@ -2420,7 +3174,7 @@ var Viewport = class {
2420
3174
  this.historyRecorder.commit();
2421
3175
  return;
2422
3176
  }
2423
- const node = this.domNodes.get(elementId);
3177
+ const node = this.domNodeManager.getNode(elementId);
2424
3178
  if (node && "size" in element) {
2425
3179
  const measuredHeight = node.scrollHeight;
2426
3180
  if (measuredHeight !== element.size.h) {
@@ -2448,12 +3202,12 @@ var Viewport = class {
2448
3202
  const world = this.camera.screenToWorld(screen);
2449
3203
  const hit = this.hitTestWorld(world);
2450
3204
  if (hit?.type === "html") {
2451
- this.startInteracting(hit.id);
3205
+ this.interactMode.startInteracting(hit.id);
2452
3206
  }
2453
3207
  };
2454
3208
  hitTestWorld(world) {
2455
- const elements = this.store.getAll().reverse();
2456
- for (const el of elements) {
3209
+ const candidates = this.store.queryPoint(world).reverse();
3210
+ for (const el of candidates) {
2457
3211
  if (!("size" in el)) continue;
2458
3212
  const { x, y } = el.position;
2459
3213
  const { w, h } = el.size;
@@ -2463,44 +3217,9 @@ var Viewport = class {
2463
3217
  }
2464
3218
  return null;
2465
3219
  }
2466
- startInteracting(id) {
2467
- this.stopInteracting();
2468
- const node = this.domNodes.get(id);
2469
- if (!node) return;
2470
- this.interactingElementId = id;
2471
- node.style.pointerEvents = "auto";
2472
- node.addEventListener("pointerdown", this.onInteractNodePointerDown);
2473
- window.addEventListener("keydown", this.onInteractKeyDown);
2474
- window.addEventListener("pointerdown", this.onInteractPointerDown);
2475
- }
2476
3220
  stopInteracting() {
2477
- if (!this.interactingElementId) return;
2478
- const node = this.domNodes.get(this.interactingElementId);
2479
- if (node) {
2480
- node.style.pointerEvents = "none";
2481
- node.removeEventListener("pointerdown", this.onInteractNodePointerDown);
2482
- }
2483
- this.interactingElementId = null;
2484
- window.removeEventListener("keydown", this.onInteractKeyDown);
2485
- window.removeEventListener("pointerdown", this.onInteractPointerDown);
3221
+ this.interactMode.stopInteracting();
2486
3222
  }
2487
- onInteractNodePointerDown = (e) => {
2488
- e.stopPropagation();
2489
- };
2490
- onInteractKeyDown = (e) => {
2491
- if (e.key === "Escape") {
2492
- this.stopInteracting();
2493
- }
2494
- };
2495
- onInteractPointerDown = (e) => {
2496
- if (!this.interactingElementId) return;
2497
- const target = e.target;
2498
- if (!target) return;
2499
- const node = this.domNodes.get(this.interactingElementId);
2500
- if (node && !node.contains(target)) {
2501
- this.stopInteracting();
2502
- }
2503
- };
2504
3223
  onDragOver = (e) => {
2505
3224
  e.preventDefault();
2506
3225
  };
@@ -2522,108 +3241,6 @@ var Viewport = class {
2522
3241
  reader.readAsDataURL(file);
2523
3242
  }
2524
3243
  };
2525
- syncDomNode(element, zIndex = 0) {
2526
- let node = this.domNodes.get(element.id);
2527
- if (!node) {
2528
- node = document.createElement("div");
2529
- node.dataset["elementId"] = element.id;
2530
- Object.assign(node.style, {
2531
- position: "absolute",
2532
- pointerEvents: "auto"
2533
- });
2534
- this.domLayer.appendChild(node);
2535
- this.domNodes.set(element.id, node);
2536
- }
2537
- const size = "size" in element ? element.size : null;
2538
- Object.assign(node.style, {
2539
- display: "block",
2540
- left: `${element.position.x}px`,
2541
- top: `${element.position.y}px`,
2542
- width: size ? `${size.w}px` : "auto",
2543
- height: size ? `${size.h}px` : "auto",
2544
- zIndex: String(zIndex)
2545
- });
2546
- this.renderDomContent(node, element);
2547
- }
2548
- renderDomContent(node, element) {
2549
- if (element.type === "note") {
2550
- if (!node.dataset["initialized"]) {
2551
- node.dataset["initialized"] = "true";
2552
- Object.assign(node.style, {
2553
- backgroundColor: element.backgroundColor,
2554
- color: element.textColor,
2555
- padding: "8px",
2556
- borderRadius: "4px",
2557
- boxShadow: "0 2px 8px rgba(0,0,0,0.15)",
2558
- fontSize: "14px",
2559
- overflow: "hidden",
2560
- cursor: "default",
2561
- userSelect: "none",
2562
- wordWrap: "break-word"
2563
- });
2564
- node.textContent = element.text || "";
2565
- node.addEventListener("dblclick", (e) => {
2566
- e.stopPropagation();
2567
- const id = node.dataset["elementId"];
2568
- if (id) this.startEditingElement(id);
2569
- });
2570
- }
2571
- if (!this.noteEditor.isEditing || this.noteEditor.editingElementId !== element.id) {
2572
- if (node.textContent !== element.text) {
2573
- node.textContent = element.text || "";
2574
- }
2575
- node.style.backgroundColor = element.backgroundColor;
2576
- node.style.color = element.textColor;
2577
- }
2578
- }
2579
- if (element.type === "html" && !node.dataset["initialized"]) {
2580
- const content = this.htmlContent.get(element.id);
2581
- if (content) {
2582
- node.dataset["initialized"] = "true";
2583
- Object.assign(node.style, {
2584
- overflow: "hidden",
2585
- pointerEvents: "none"
2586
- });
2587
- node.appendChild(content);
2588
- }
2589
- }
2590
- if (element.type === "text") {
2591
- if (!node.dataset["initialized"]) {
2592
- node.dataset["initialized"] = "true";
2593
- Object.assign(node.style, {
2594
- padding: "2px",
2595
- fontSize: `${element.fontSize}px`,
2596
- color: element.color,
2597
- textAlign: element.textAlign,
2598
- background: "none",
2599
- border: "none",
2600
- boxShadow: "none",
2601
- overflow: "visible",
2602
- cursor: "default",
2603
- userSelect: "none",
2604
- wordWrap: "break-word",
2605
- whiteSpace: "pre-wrap",
2606
- lineHeight: "1.4"
2607
- });
2608
- node.textContent = element.text || "";
2609
- node.addEventListener("dblclick", (e) => {
2610
- e.stopPropagation();
2611
- const id = node.dataset["elementId"];
2612
- if (id) this.startEditingElement(id);
2613
- });
2614
- }
2615
- if (!this.noteEditor.isEditing || this.noteEditor.editingElementId !== element.id) {
2616
- if (node.textContent !== element.text) {
2617
- node.textContent = element.text || "";
2618
- }
2619
- Object.assign(node.style, {
2620
- fontSize: `${element.fontSize}px`,
2621
- color: element.color,
2622
- textAlign: element.textAlign
2623
- });
2624
- }
2625
- }
2626
- }
2627
3244
  unbindArrowsFrom(removedElement) {
2628
3245
  const boundArrows = findBoundArrows(removedElement.id, this.store);
2629
3246
  const bounds = getElementBounds(removedElement);
@@ -2658,35 +3275,6 @@ var Viewport = class {
2658
3275
  }
2659
3276
  }
2660
3277
  }
2661
- hideDomNode(id) {
2662
- const node = this.domNodes.get(id);
2663
- if (node) node.style.display = "none";
2664
- }
2665
- removeDomNode(id) {
2666
- this.htmlContent.delete(id);
2667
- const node = this.domNodes.get(id);
2668
- if (node) {
2669
- node.remove();
2670
- this.domNodes.delete(id);
2671
- }
2672
- this.requestRender();
2673
- }
2674
- clearDomNodes() {
2675
- this.domNodes.forEach((node) => node.remove());
2676
- this.domNodes.clear();
2677
- this.htmlContent.clear();
2678
- this.requestRender();
2679
- }
2680
- reattachHtmlContent() {
2681
- for (const el of this.store.getElementsByType("html")) {
2682
- if (el.domId) {
2683
- const dom = document.getElementById(el.domId);
2684
- if (dom) {
2685
- this.htmlContent.set(el.id, dom);
2686
- }
2687
- }
2688
- }
2689
- }
2690
3278
  createWrapper() {
2691
3279
  const el = document.createElement("div");
2692
3280
  Object.assign(el.style, {
@@ -2727,8 +3315,7 @@ var Viewport = class {
2727
3315
  syncCanvasSize() {
2728
3316
  const rect = this.container.getBoundingClientRect();
2729
3317
  const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
2730
- this.canvasEl.width = rect.width * dpr;
2731
- this.canvasEl.height = rect.height * dpr;
3318
+ this.renderLoop.setCanvasSize(rect.width * dpr, rect.height * dpr);
2732
3319
  this.requestRender();
2733
3320
  }
2734
3321
  observeResize() {
@@ -2771,6 +3358,9 @@ var HandTool = class {
2771
3358
  var MIN_POINTS_FOR_STROKE = 2;
2772
3359
  var DEFAULT_SMOOTHING = 1.5;
2773
3360
  var DEFAULT_PRESSURE = 0.5;
3361
+ var DEFAULT_MIN_POINT_DISTANCE = 3;
3362
+ var DEFAULT_PROGRESSIVE_THRESHOLD = 200;
3363
+ var PROGRESSIVE_HOT_ZONE = 30;
2774
3364
  var PencilTool = class {
2775
3365
  name = "pencil";
2776
3366
  drawing = false;
@@ -2778,10 +3368,17 @@ var PencilTool = class {
2778
3368
  color;
2779
3369
  width;
2780
3370
  smoothing;
3371
+ minPointDistance;
3372
+ progressiveThreshold;
3373
+ nextSimplifyAt;
3374
+ optionListeners = /* @__PURE__ */ new Set();
2781
3375
  constructor(options = {}) {
2782
3376
  this.color = options.color ?? "#000000";
2783
3377
  this.width = options.width ?? 2;
2784
3378
  this.smoothing = options.smoothing ?? DEFAULT_SMOOTHING;
3379
+ this.minPointDistance = options.minPointDistance ?? DEFAULT_MIN_POINT_DISTANCE;
3380
+ this.progressiveThreshold = options.progressiveSimplifyThreshold ?? DEFAULT_PROGRESSIVE_THRESHOLD;
3381
+ this.nextSimplifyAt = this.progressiveThreshold;
2785
3382
  }
2786
3383
  onActivate(ctx) {
2787
3384
  ctx.setCursor?.("crosshair");
@@ -2789,22 +3386,53 @@ var PencilTool = class {
2789
3386
  onDeactivate(ctx) {
2790
3387
  ctx.setCursor?.("default");
2791
3388
  }
3389
+ getOptions() {
3390
+ return {
3391
+ color: this.color,
3392
+ width: this.width,
3393
+ smoothing: this.smoothing,
3394
+ minPointDistance: this.minPointDistance,
3395
+ progressiveSimplifyThreshold: this.progressiveThreshold
3396
+ };
3397
+ }
3398
+ onOptionsChange(listener) {
3399
+ this.optionListeners.add(listener);
3400
+ return () => this.optionListeners.delete(listener);
3401
+ }
2792
3402
  setOptions(options) {
2793
3403
  if (options.color !== void 0) this.color = options.color;
2794
3404
  if (options.width !== void 0) this.width = options.width;
2795
3405
  if (options.smoothing !== void 0) this.smoothing = options.smoothing;
3406
+ if (options.minPointDistance !== void 0) this.minPointDistance = options.minPointDistance;
3407
+ if (options.progressiveSimplifyThreshold !== void 0)
3408
+ this.progressiveThreshold = options.progressiveSimplifyThreshold;
3409
+ this.notifyOptionsChange();
2796
3410
  }
2797
3411
  onPointerDown(state, ctx) {
2798
3412
  this.drawing = true;
2799
3413
  const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
2800
3414
  const pressure = state.pressure === 0 ? DEFAULT_PRESSURE : state.pressure;
2801
3415
  this.points = [{ x: world.x, y: world.y, pressure }];
3416
+ this.nextSimplifyAt = this.progressiveThreshold;
2802
3417
  }
2803
3418
  onPointerMove(state, ctx) {
2804
3419
  if (!this.drawing) return;
2805
3420
  const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
2806
3421
  const pressure = state.pressure === 0 ? DEFAULT_PRESSURE : state.pressure;
3422
+ const last = this.points[this.points.length - 1];
3423
+ if (last) {
3424
+ const dx = world.x - last.x;
3425
+ const dy = world.y - last.y;
3426
+ if (dx * dx + dy * dy < this.minPointDistance * this.minPointDistance) return;
3427
+ }
2807
3428
  this.points.push({ x: world.x, y: world.y, pressure });
3429
+ if (this.points.length > this.nextSimplifyAt) {
3430
+ const hotZone = this.points.slice(-PROGRESSIVE_HOT_ZONE);
3431
+ const coldZone = this.points.slice(0, -PROGRESSIVE_HOT_ZONE);
3432
+ const simplified = simplifyPoints(coldZone, this.smoothing * 2);
3433
+ this.points = [...simplified, ...hotZone];
3434
+ this.nextSimplifyAt = this.points.length + this.progressiveThreshold;
3435
+ }
2808
3436
  ctx.requestRender();
2809
3437
  }
2810
3438
  onPointerUp(_state, ctx) {
@@ -2825,6 +3453,9 @@ var PencilTool = class {
2825
3453
  this.points = [];
2826
3454
  ctx.requestRender();
2827
3455
  }
3456
+ notifyOptionsChange() {
3457
+ for (const listener of this.optionListeners) listener();
3458
+ }
2828
3459
  renderOverlay(ctx) {
2829
3460
  if (!this.drawing || this.points.length < 2) return;
2830
3461
  ctx.save();
@@ -2861,6 +3492,9 @@ var EraserTool = class {
2861
3492
  this.radius = options.radius ?? DEFAULT_RADIUS;
2862
3493
  this.cursor = makeEraserCursor(this.radius);
2863
3494
  }
3495
+ getOptions() {
3496
+ return { radius: this.radius };
3497
+ }
2864
3498
  onActivate(ctx) {
2865
3499
  ctx.setCursor?.(this.cursor);
2866
3500
  }
@@ -2880,13 +3514,20 @@ var EraserTool = class {
2880
3514
  }
2881
3515
  eraseAt(state, ctx) {
2882
3516
  const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
2883
- const strokes = ctx.store.getElementsByType("stroke");
3517
+ const queryBounds = {
3518
+ x: world.x - this.radius,
3519
+ y: world.y - this.radius,
3520
+ w: this.radius * 2,
3521
+ h: this.radius * 2
3522
+ };
3523
+ const candidates = ctx.store.queryRect(queryBounds);
2884
3524
  let erased = false;
2885
- for (const stroke of strokes) {
2886
- if (ctx.isLayerVisible && !ctx.isLayerVisible(stroke.layerId)) continue;
2887
- if (ctx.isLayerLocked && ctx.isLayerLocked(stroke.layerId)) continue;
2888
- if (this.strokeIntersects(stroke, world)) {
2889
- ctx.store.remove(stroke.id);
3525
+ for (const el of candidates) {
3526
+ if (el.type !== "stroke") continue;
3527
+ if (ctx.isLayerVisible && !ctx.isLayerVisible(el.layerId)) continue;
3528
+ if (ctx.isLayerLocked && ctx.isLayerLocked(el.layerId)) continue;
3529
+ if (this.strokeIntersects(el, world)) {
3530
+ ctx.store.remove(el.id);
2890
3531
  erased = true;
2891
3532
  }
2892
3533
  }
@@ -3263,7 +3904,7 @@ var SelectTool = class {
3263
3904
  for (const id of this._selectedIds) {
3264
3905
  const el = ctx.store.getById(id);
3265
3906
  if (!el || !("size" in el)) continue;
3266
- const bounds = this.getElementBounds(el);
3907
+ const bounds = getElementBounds(el);
3267
3908
  if (!bounds) continue;
3268
3909
  const corners = this.getHandlePositions(bounds);
3269
3910
  for (const [handle, pos] of corners) {
@@ -3311,7 +3952,7 @@ var SelectTool = class {
3311
3952
  this.renderBindingHighlights(canvasCtx, el, zoom);
3312
3953
  continue;
3313
3954
  }
3314
- const bounds = this.getElementBounds(el);
3955
+ const bounds = getElementBounds(el);
3315
3956
  if (!bounds) continue;
3316
3957
  const pad = SELECTION_PAD / zoom;
3317
3958
  canvasCtx.strokeRect(bounds.x - pad, bounds.y - pad, bounds.w + pad * 2, bounds.h + pad * 2);
@@ -3370,12 +4011,13 @@ var SelectTool = class {
3370
4011
  return { x, y, w, h };
3371
4012
  }
3372
4013
  findElementsInRect(marquee, ctx) {
4014
+ const candidates = ctx.store.queryRect(marquee);
3373
4015
  const ids = [];
3374
- for (const el of ctx.store.getAll()) {
4016
+ for (const el of candidates) {
3375
4017
  if (ctx.isLayerVisible && !ctx.isLayerVisible(el.layerId)) continue;
3376
4018
  if (ctx.isLayerLocked && ctx.isLayerLocked(el.layerId)) continue;
3377
4019
  if (el.type === "grid") continue;
3378
- const bounds = this.getElementBounds(el);
4020
+ const bounds = getElementBounds(el);
3379
4021
  if (bounds && this.rectsOverlap(marquee, bounds)) {
3380
4022
  ids.push(el.id);
3381
4023
  }
@@ -3385,30 +4027,10 @@ var SelectTool = class {
3385
4027
  rectsOverlap(a, b) {
3386
4028
  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;
3387
4029
  }
3388
- getElementBounds(el) {
3389
- if ("size" in el) {
3390
- return { x: el.position.x, y: el.position.y, w: el.size.w, h: el.size.h };
3391
- }
3392
- if (el.type === "stroke" && el.points.length > 0) {
3393
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
3394
- for (const p of el.points) {
3395
- const px = p.x + el.position.x;
3396
- const py = p.y + el.position.y;
3397
- if (px < minX) minX = px;
3398
- if (py < minY) minY = py;
3399
- if (px > maxX) maxX = px;
3400
- if (py > maxY) maxY = py;
3401
- }
3402
- return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
3403
- }
3404
- if (el.type === "arrow") {
3405
- return getArrowBounds(el.from, el.to, el.bend);
3406
- }
3407
- return null;
3408
- }
3409
4030
  hitTest(world, ctx) {
3410
- const elements = ctx.store.getAll().reverse();
3411
- for (const el of elements) {
4031
+ const r = 10;
4032
+ const candidates = ctx.store.queryRect({ x: world.x - r, y: world.y - r, w: r * 2, h: r * 2 }).reverse();
4033
+ for (const el of candidates) {
3412
4034
  if (ctx.isLayerVisible && !ctx.isLayerVisible(el.layerId)) continue;
3413
4035
  if (ctx.isLayerLocked && ctx.isLayerLocked(el.layerId)) continue;
3414
4036
  if (el.type === "grid") continue;
@@ -3449,13 +4071,25 @@ var ArrowTool = class {
3449
4071
  fromBinding;
3450
4072
  fromTarget = null;
3451
4073
  toTarget = null;
4074
+ optionListeners = /* @__PURE__ */ new Set();
3452
4075
  constructor(options = {}) {
3453
4076
  this.color = options.color ?? "#000000";
3454
4077
  this.width = options.width ?? 2;
3455
4078
  }
4079
+ getOptions() {
4080
+ return { color: this.color, width: this.width };
4081
+ }
4082
+ onOptionsChange(listener) {
4083
+ this.optionListeners.add(listener);
4084
+ return () => this.optionListeners.delete(listener);
4085
+ }
3456
4086
  setOptions(options) {
3457
4087
  if (options.color !== void 0) this.color = options.color;
3458
4088
  if (options.width !== void 0) this.width = options.width;
4089
+ this.notifyOptionsChange();
4090
+ }
4091
+ notifyOptionsChange() {
4092
+ for (const listener of this.optionListeners) listener();
3459
4093
  }
3460
4094
  layerFilter(ctx) {
3461
4095
  const activeLayerId = ctx.activeLayerId;
@@ -3577,15 +4211,31 @@ var NoteTool = class {
3577
4211
  backgroundColor;
3578
4212
  textColor;
3579
4213
  size;
4214
+ optionListeners = /* @__PURE__ */ new Set();
3580
4215
  constructor(options = {}) {
3581
4216
  this.backgroundColor = options.backgroundColor ?? "#ffeb3b";
3582
4217
  this.textColor = options.textColor ?? "#000000";
3583
4218
  this.size = options.size ?? { w: 200, h: 100 };
3584
4219
  }
4220
+ getOptions() {
4221
+ return {
4222
+ backgroundColor: this.backgroundColor,
4223
+ textColor: this.textColor,
4224
+ size: { ...this.size }
4225
+ };
4226
+ }
4227
+ onOptionsChange(listener) {
4228
+ this.optionListeners.add(listener);
4229
+ return () => this.optionListeners.delete(listener);
4230
+ }
3585
4231
  setOptions(options) {
3586
4232
  if (options.backgroundColor !== void 0) this.backgroundColor = options.backgroundColor;
3587
4233
  if (options.textColor !== void 0) this.textColor = options.textColor;
3588
4234
  if (options.size !== void 0) this.size = options.size;
4235
+ this.notifyOptionsChange();
4236
+ }
4237
+ notifyOptionsChange() {
4238
+ for (const listener of this.optionListeners) listener();
3589
4239
  }
3590
4240
  onPointerDown(_state, _ctx) {
3591
4241
  }
@@ -3616,15 +4266,27 @@ var TextTool = class {
3616
4266
  fontSize;
3617
4267
  color;
3618
4268
  textAlign;
4269
+ optionListeners = /* @__PURE__ */ new Set();
3619
4270
  constructor(options = {}) {
3620
4271
  this.fontSize = options.fontSize ?? 16;
3621
4272
  this.color = options.color ?? "#1a1a1a";
3622
4273
  this.textAlign = options.textAlign ?? "left";
3623
4274
  }
4275
+ getOptions() {
4276
+ return { fontSize: this.fontSize, color: this.color, textAlign: this.textAlign };
4277
+ }
4278
+ onOptionsChange(listener) {
4279
+ this.optionListeners.add(listener);
4280
+ return () => this.optionListeners.delete(listener);
4281
+ }
3624
4282
  setOptions(options) {
3625
4283
  if (options.fontSize !== void 0) this.fontSize = options.fontSize;
3626
4284
  if (options.color !== void 0) this.color = options.color;
3627
4285
  if (options.textAlign !== void 0) this.textAlign = options.textAlign;
4286
+ this.notifyOptionsChange();
4287
+ }
4288
+ notifyOptionsChange() {
4289
+ for (const listener of this.optionListeners) listener();
3628
4290
  }
3629
4291
  onActivate(ctx) {
3630
4292
  ctx.setCursor?.("text");
@@ -3696,17 +4358,31 @@ var ShapeTool = class {
3696
4358
  strokeColor;
3697
4359
  strokeWidth;
3698
4360
  fillColor;
4361
+ optionListeners = /* @__PURE__ */ new Set();
3699
4362
  constructor(options = {}) {
3700
4363
  this.shape = options.shape ?? "rectangle";
3701
4364
  this.strokeColor = options.strokeColor ?? "#000000";
3702
4365
  this.strokeWidth = options.strokeWidth ?? 2;
3703
4366
  this.fillColor = options.fillColor ?? "none";
3704
4367
  }
4368
+ getOptions() {
4369
+ return {
4370
+ shape: this.shape,
4371
+ strokeColor: this.strokeColor,
4372
+ strokeWidth: this.strokeWidth,
4373
+ fillColor: this.fillColor
4374
+ };
4375
+ }
4376
+ onOptionsChange(listener) {
4377
+ this.optionListeners.add(listener);
4378
+ return () => this.optionListeners.delete(listener);
4379
+ }
3705
4380
  setOptions(options) {
3706
4381
  if (options.shape !== void 0) this.shape = options.shape;
3707
4382
  if (options.strokeColor !== void 0) this.strokeColor = options.strokeColor;
3708
4383
  if (options.strokeWidth !== void 0) this.strokeWidth = options.strokeWidth;
3709
4384
  if (options.fillColor !== void 0) this.fillColor = options.fillColor;
4385
+ this.notifyOptionsChange();
3710
4386
  }
3711
4387
  onActivate(_ctx) {
3712
4388
  if (typeof window !== "undefined") {
@@ -3793,6 +4469,9 @@ var ShapeTool = class {
3793
4469
  }
3794
4470
  return { position: { x, y }, size: { w, h } };
3795
4471
  }
4472
+ notifyOptionsChange() {
4473
+ for (const listener of this.optionListeners) listener();
4474
+ }
3796
4475
  snap(point, ctx) {
3797
4476
  return ctx.snapToGrid && ctx.gridSize ? snapPoint(point, ctx.gridSize) : point;
3798
4477
  }
@@ -3845,7 +4524,7 @@ var UpdateLayerCommand = class {
3845
4524
  };
3846
4525
 
3847
4526
  // src/index.ts
3848
- var VERSION = "0.8.5";
4527
+ var VERSION = "0.8.7";
3849
4528
  export {
3850
4529
  AddElementCommand,
3851
4530
  ArrowTool,
@@ -3867,6 +4546,7 @@ export {
3867
4546
  NoteEditor,
3868
4547
  NoteTool,
3869
4548
  PencilTool,
4549
+ Quadtree,
3870
4550
  RemoveElementCommand,
3871
4551
  RemoveLayerCommand,
3872
4552
  SelectTool,
@@ -3877,6 +4557,7 @@ export {
3877
4557
  UpdateLayerCommand,
3878
4558
  VERSION,
3879
4559
  Viewport,
4560
+ boundsIntersect,
3880
4561
  clearStaleBindings,
3881
4562
  createArrow,
3882
4563
  createGrid,