@fieldnotes/core 0.8.6 → 0.8.8

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
 
@@ -294,6 +457,13 @@ var Background = class {
294
457
  color;
295
458
  dotRadius;
296
459
  lineWidth;
460
+ cachedCanvas = null;
461
+ cachedCtx = null;
462
+ lastZoom = -1;
463
+ lastOffsetX = -Infinity;
464
+ lastOffsetY = -Infinity;
465
+ lastWidth = 0;
466
+ lastHeight = 0;
297
467
  constructor(options = {}) {
298
468
  this.pattern = options.pattern ?? DEFAULTS.pattern;
299
469
  this.spacing = options.spacing ?? DEFAULTS.spacing;
@@ -309,13 +479,69 @@ var Background = class {
309
479
  ctx.save();
310
480
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
311
481
  ctx.clearRect(0, 0, cssWidth, cssHeight);
482
+ if (this.pattern === "none") {
483
+ ctx.restore();
484
+ return;
485
+ }
486
+ const spacing = this.adaptSpacing(this.spacing, camera.zoom);
487
+ const keyZoom = camera.zoom;
488
+ const keyX = Math.floor(camera.position.x % spacing);
489
+ const keyY = Math.floor(camera.position.y % spacing);
490
+ if (this.cachedCanvas !== null && keyZoom === this.lastZoom && keyX === this.lastOffsetX && keyY === this.lastOffsetY && cssWidth === this.lastWidth && cssHeight === this.lastHeight) {
491
+ ctx.drawImage(this.cachedCanvas, 0, 0);
492
+ ctx.restore();
493
+ return;
494
+ }
495
+ this.ensureCachedCanvas(cssWidth, cssHeight, dpr);
496
+ if (this.cachedCtx === null) {
497
+ if (this.pattern === "dots") {
498
+ this.renderDots(ctx, camera, cssWidth, cssHeight);
499
+ } else if (this.pattern === "grid") {
500
+ this.renderGrid(ctx, camera, cssWidth, cssHeight);
501
+ }
502
+ ctx.restore();
503
+ return;
504
+ }
505
+ const offCtx = this.cachedCtx;
506
+ offCtx.clearRect(0, 0, cssWidth, cssHeight);
312
507
  if (this.pattern === "dots") {
313
- this.renderDots(ctx, camera, cssWidth, cssHeight);
508
+ this.renderDots(offCtx, camera, cssWidth, cssHeight);
314
509
  } else if (this.pattern === "grid") {
315
- this.renderGrid(ctx, camera, cssWidth, cssHeight);
316
- }
510
+ this.renderGrid(offCtx, camera, cssWidth, cssHeight);
511
+ }
512
+ this.lastZoom = keyZoom;
513
+ this.lastOffsetX = keyX;
514
+ this.lastOffsetY = keyY;
515
+ this.lastWidth = cssWidth;
516
+ this.lastHeight = cssHeight;
517
+ ctx.drawImage(this.cachedCanvas, 0, 0);
317
518
  ctx.restore();
318
519
  }
520
+ ensureCachedCanvas(cssWidth, cssHeight, dpr) {
521
+ if (this.cachedCanvas !== null && this.lastWidth === cssWidth && this.lastHeight === cssHeight) {
522
+ return;
523
+ }
524
+ const physWidth = Math.round(cssWidth * dpr);
525
+ const physHeight = Math.round(cssHeight * dpr);
526
+ if (typeof OffscreenCanvas !== "undefined") {
527
+ this.cachedCanvas = new OffscreenCanvas(physWidth, physHeight);
528
+ } else if (typeof document !== "undefined") {
529
+ const el = document.createElement("canvas");
530
+ el.width = physWidth;
531
+ el.height = physHeight;
532
+ this.cachedCanvas = el;
533
+ } else {
534
+ this.cachedCanvas = null;
535
+ this.cachedCtx = null;
536
+ return;
537
+ }
538
+ const offCtx = this.cachedCanvas.getContext("2d");
539
+ if (offCtx !== null) {
540
+ offCtx.scale(dpr, dpr);
541
+ }
542
+ this.cachedCtx = offCtx;
543
+ this.lastZoom = -1;
544
+ }
319
545
  adaptSpacing(baseSpacing, zoom) {
320
546
  let spacing = baseSpacing * zoom;
321
547
  while (spacing < MIN_PATTERN_SPACING) {
@@ -581,77 +807,6 @@ var InputHandler = class {
581
807
  }
582
808
  };
583
809
 
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
- onChange(listener) {
645
- const unsubs = [
646
- this.bus.on("add", listener),
647
- this.bus.on("remove", listener),
648
- this.bus.on("update", listener),
649
- this.bus.on("clear", listener)
650
- ];
651
- return () => unsubs.forEach((fn) => fn());
652
- }
653
- };
654
-
655
810
  // src/elements/arrow-geometry.ts
656
811
  function getArrowControlPoint(from, to, bend) {
657
812
  const midX = (from.x + to.x) / 2;
@@ -752,6 +907,189 @@ function isNearLine(point, a, b, threshold) {
752
907
  return Math.hypot(point.x - projX, point.y - projY) <= threshold;
753
908
  }
754
909
 
910
+ // src/elements/element-bounds.ts
911
+ var strokeBoundsCache = /* @__PURE__ */ new WeakMap();
912
+ function getElementBounds(element) {
913
+ if (element.type === "grid") return null;
914
+ if ("size" in element) {
915
+ return {
916
+ x: element.position.x,
917
+ y: element.position.y,
918
+ w: element.size.w,
919
+ h: element.size.h
920
+ };
921
+ }
922
+ if (element.type === "stroke") {
923
+ if (element.points.length === 0) return null;
924
+ const cached = strokeBoundsCache.get(element);
925
+ if (cached) return cached;
926
+ let minX = Infinity;
927
+ let minY = Infinity;
928
+ let maxX = -Infinity;
929
+ let maxY = -Infinity;
930
+ for (const p of element.points) {
931
+ const px = p.x + element.position.x;
932
+ const py = p.y + element.position.y;
933
+ if (px < minX) minX = px;
934
+ if (py < minY) minY = py;
935
+ if (px > maxX) maxX = px;
936
+ if (py > maxY) maxY = py;
937
+ }
938
+ const bounds = { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
939
+ strokeBoundsCache.set(element, bounds);
940
+ return bounds;
941
+ }
942
+ if (element.type === "arrow") {
943
+ return getArrowBoundsAnalytical(element.from, element.to, element.bend);
944
+ }
945
+ return null;
946
+ }
947
+ function getArrowBoundsAnalytical(from, to, bend) {
948
+ if (bend === 0) {
949
+ const minX2 = Math.min(from.x, to.x);
950
+ const minY2 = Math.min(from.y, to.y);
951
+ return {
952
+ x: minX2,
953
+ y: minY2,
954
+ w: Math.abs(to.x - from.x),
955
+ h: Math.abs(to.y - from.y)
956
+ };
957
+ }
958
+ const cp = getArrowControlPoint(from, to, bend);
959
+ let minX = Math.min(from.x, to.x);
960
+ let maxX = Math.max(from.x, to.x);
961
+ let minY = Math.min(from.y, to.y);
962
+ let maxY = Math.max(from.y, to.y);
963
+ const tx = from.x - 2 * cp.x + to.x;
964
+ if (tx !== 0) {
965
+ const t = (from.x - cp.x) / tx;
966
+ if (t > 0 && t < 1) {
967
+ const mt = 1 - t;
968
+ const x = mt * mt * from.x + 2 * mt * t * cp.x + t * t * to.x;
969
+ if (x < minX) minX = x;
970
+ if (x > maxX) maxX = x;
971
+ }
972
+ }
973
+ const ty = from.y - 2 * cp.y + to.y;
974
+ if (ty !== 0) {
975
+ const t = (from.y - cp.y) / ty;
976
+ if (t > 0 && t < 1) {
977
+ const mt = 1 - t;
978
+ const y = mt * mt * from.y + 2 * mt * t * cp.y + t * t * to.y;
979
+ if (y < minY) minY = y;
980
+ if (y > maxY) maxY = y;
981
+ }
982
+ }
983
+ return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
984
+ }
985
+ function boundsIntersect(a, b) {
986
+ 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;
987
+ }
988
+
989
+ // src/elements/element-store.ts
990
+ var ElementStore = class {
991
+ elements = /* @__PURE__ */ new Map();
992
+ bus = new EventBus();
993
+ layerOrderMap = /* @__PURE__ */ new Map();
994
+ spatialIndex = new Quadtree({ x: -1e5, y: -1e5, w: 2e5, h: 2e5 });
995
+ get count() {
996
+ return this.elements.size;
997
+ }
998
+ setLayerOrder(order) {
999
+ this.layerOrderMap = new Map(order);
1000
+ }
1001
+ getAll() {
1002
+ return [...this.elements.values()].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
+ getById(id) {
1010
+ return this.elements.get(id);
1011
+ }
1012
+ getElementsByType(type) {
1013
+ return this.getAll().filter(
1014
+ (el) => el.type === type
1015
+ );
1016
+ }
1017
+ add(element) {
1018
+ this.elements.set(element.id, element);
1019
+ const bounds = getElementBounds(element);
1020
+ if (bounds) this.spatialIndex.insert(element.id, bounds);
1021
+ this.bus.emit("add", element);
1022
+ }
1023
+ update(id, partial) {
1024
+ const existing = this.elements.get(id);
1025
+ if (!existing) return;
1026
+ const updated = { ...existing, ...partial, id: existing.id, type: existing.type };
1027
+ if (updated.type === "arrow") {
1028
+ const arrow = updated;
1029
+ arrow.cachedControlPoint = getArrowControlPoint(arrow.from, arrow.to, arrow.bend);
1030
+ }
1031
+ this.elements.set(id, updated);
1032
+ const newBounds = getElementBounds(updated);
1033
+ if (newBounds) {
1034
+ this.spatialIndex.update(id, newBounds);
1035
+ }
1036
+ this.bus.emit("update", { previous: existing, current: updated });
1037
+ }
1038
+ remove(id) {
1039
+ const element = this.elements.get(id);
1040
+ if (!element) return;
1041
+ this.elements.delete(id);
1042
+ this.spatialIndex.remove(id);
1043
+ this.bus.emit("remove", element);
1044
+ }
1045
+ clear() {
1046
+ this.elements.clear();
1047
+ this.spatialIndex.clear();
1048
+ this.bus.emit("clear", null);
1049
+ }
1050
+ snapshot() {
1051
+ return this.getAll().map((el) => ({ ...el }));
1052
+ }
1053
+ loadSnapshot(elements) {
1054
+ this.elements.clear();
1055
+ this.spatialIndex.clear();
1056
+ for (const el of elements) {
1057
+ this.elements.set(el.id, el);
1058
+ const bounds = getElementBounds(el);
1059
+ if (bounds) this.spatialIndex.insert(el.id, bounds);
1060
+ }
1061
+ }
1062
+ queryRect(rect) {
1063
+ const ids = this.spatialIndex.query(rect);
1064
+ const elements = [];
1065
+ for (const id of ids) {
1066
+ const el = this.elements.get(id);
1067
+ if (el) elements.push(el);
1068
+ }
1069
+ return elements.sort((a, b) => {
1070
+ const layerA = this.layerOrderMap.get(a.layerId) ?? 0;
1071
+ const layerB = this.layerOrderMap.get(b.layerId) ?? 0;
1072
+ if (layerA !== layerB) return layerA - layerB;
1073
+ return a.zIndex - b.zIndex;
1074
+ });
1075
+ }
1076
+ queryPoint(point) {
1077
+ return this.queryRect({ x: point.x, y: point.y, w: 0, h: 0 });
1078
+ }
1079
+ on(event, listener) {
1080
+ return this.bus.on(event, listener);
1081
+ }
1082
+ onChange(listener) {
1083
+ const unsubs = [
1084
+ this.bus.on("add", listener),
1085
+ this.bus.on("remove", listener),
1086
+ this.bus.on("update", listener),
1087
+ this.bus.on("clear", listener)
1088
+ ];
1089
+ return () => unsubs.forEach((fn) => fn());
1090
+ }
1091
+ };
1092
+
755
1093
  // src/elements/arrow-binding.ts
756
1094
  var BINDABLE_TYPES = /* @__PURE__ */ new Set(["note", "text", "image", "html", "shape"]);
757
1095
  function isBindable(element) {
@@ -766,15 +1104,6 @@ function getElementCenter(element) {
766
1104
  y: element.position.y + element.size.h / 2
767
1105
  };
768
1106
  }
769
- function getElementBounds(element) {
770
- if (!("size" in element)) return null;
771
- return {
772
- x: element.position.x,
773
- y: element.position.y,
774
- w: element.size.w,
775
- h: element.size.h
776
- };
777
- }
778
1107
  function getEdgeIntersection(bounds, outsidePoint) {
779
1108
  const cx = bounds.x + bounds.w / 2;
780
1109
  const cy = bounds.y + bounds.h / 2;
@@ -954,6 +1283,25 @@ function smoothToSegments(points) {
954
1283
  return segments;
955
1284
  }
956
1285
 
1286
+ // src/elements/stroke-cache.ts
1287
+ var cache = /* @__PURE__ */ new WeakMap();
1288
+ function computeStrokeSegments(stroke) {
1289
+ const segments = smoothToSegments(stroke.points);
1290
+ const widths = [];
1291
+ for (const seg of segments) {
1292
+ const w = (pressureToWidth(seg.start.pressure, stroke.width) + pressureToWidth(seg.end.pressure, stroke.width)) / 2;
1293
+ widths.push(w);
1294
+ }
1295
+ const data = { segments, widths };
1296
+ cache.set(stroke, data);
1297
+ return data;
1298
+ }
1299
+ function getStrokeRenderData(stroke) {
1300
+ const cached = cache.get(stroke);
1301
+ if (cached) return cached;
1302
+ return computeStrokeSegments(stroke);
1303
+ }
1304
+
957
1305
  // src/elements/grid-renderer.ts
958
1306
  function getSquareGridLines(bounds, cellSize) {
959
1307
  if (cellSize <= 0) return { verticals: [], horizontals: [] };
@@ -1118,9 +1466,11 @@ var ElementRenderer = class {
1118
1466
  ctx.lineCap = "round";
1119
1467
  ctx.lineJoin = "round";
1120
1468
  ctx.globalAlpha = stroke.opacity;
1121
- const segments = smoothToSegments(stroke.points);
1122
- for (const seg of segments) {
1123
- const w = (pressureToWidth(seg.start.pressure, stroke.width) + pressureToWidth(seg.end.pressure, stroke.width)) / 2;
1469
+ const { segments, widths } = getStrokeRenderData(stroke);
1470
+ for (let i = 0; i < segments.length; i++) {
1471
+ const seg = segments[i];
1472
+ const w = widths[i];
1473
+ if (!seg || w === void 0) continue;
1124
1474
  ctx.lineWidth = w;
1125
1475
  ctx.beginPath();
1126
1476
  ctx.moveTo(seg.start.x, seg.start.y);
@@ -1141,7 +1491,7 @@ var ElementRenderer = class {
1141
1491
  ctx.beginPath();
1142
1492
  ctx.moveTo(visualFrom.x, visualFrom.y);
1143
1493
  if (arrow.bend !== 0) {
1144
- const cp = getArrowControlPoint(arrow.from, arrow.to, arrow.bend);
1494
+ const cp = arrow.cachedControlPoint ?? getArrowControlPoint(arrow.from, arrow.to, arrow.bend);
1145
1495
  ctx.quadraticCurveTo(cp.x, cp.y, visualTo.x, visualTo.y);
1146
1496
  } else {
1147
1497
  ctx.lineTo(visualTo.x, visualTo.y);
@@ -1282,15 +1632,33 @@ var ElementRenderer = class {
1282
1632
  renderImage(ctx, image) {
1283
1633
  const img = this.getImage(image.src);
1284
1634
  if (!img) return;
1285
- ctx.drawImage(img, image.position.x, image.position.y, image.size.w, image.size.h);
1635
+ ctx.drawImage(
1636
+ img,
1637
+ image.position.x,
1638
+ image.position.y,
1639
+ image.size.w,
1640
+ image.size.h
1641
+ );
1286
1642
  }
1287
1643
  getImage(src) {
1288
1644
  const cached = this.imageCache.get(src);
1289
- if (cached) return cached.complete ? cached : null;
1645
+ if (cached) {
1646
+ if (cached instanceof HTMLImageElement) return cached.complete ? cached : null;
1647
+ return cached;
1648
+ }
1290
1649
  const img = new Image();
1291
1650
  img.src = src;
1292
1651
  this.imageCache.set(src, img);
1293
- img.onload = () => this.onImageLoad?.();
1652
+ img.onload = () => {
1653
+ this.onImageLoad?.();
1654
+ if (typeof createImageBitmap !== "undefined") {
1655
+ createImageBitmap(img).then((bitmap) => {
1656
+ this.imageCache.set(src, bitmap);
1657
+ this.onImageLoad?.();
1658
+ }).catch(() => {
1659
+ });
1660
+ }
1661
+ };
1294
1662
  return null;
1295
1663
  }
1296
1664
  };
@@ -1666,6 +2034,7 @@ function createNote(input) {
1666
2034
  };
1667
2035
  }
1668
2036
  function createArrow(input) {
2037
+ const bend = input.bend ?? 0;
1669
2038
  const result = {
1670
2039
  id: createId("arrow"),
1671
2040
  type: "arrow",
@@ -1675,9 +2044,10 @@ function createArrow(input) {
1675
2044
  layerId: input.layerId ?? "",
1676
2045
  from: input.from,
1677
2046
  to: input.to,
1678
- bend: input.bend ?? 0,
2047
+ bend,
1679
2048
  color: input.color ?? "#000000",
1680
- width: input.width ?? 2
2049
+ width: input.width ?? 2,
2050
+ cachedControlPoint: getArrowControlPoint(input.from, input.to, bend)
1681
2051
  };
1682
2052
  if (input.fromBinding) result.fromBinding = input.fromBinding;
1683
2053
  if (input.toBinding) result.toBinding = input.toBinding;
@@ -1928,19 +2298,19 @@ function loadImages(elements) {
1928
2298
  const imageElements = elements.filter(
1929
2299
  (el) => el.type === "image" && "src" in el
1930
2300
  );
1931
- const cache = /* @__PURE__ */ new Map();
1932
- if (imageElements.length === 0) return Promise.resolve(cache);
2301
+ const cache2 = /* @__PURE__ */ new Map();
2302
+ if (imageElements.length === 0) return Promise.resolve(cache2);
1933
2303
  return new Promise((resolve) => {
1934
2304
  let remaining = imageElements.length;
1935
2305
  const done = () => {
1936
2306
  remaining--;
1937
- if (remaining <= 0) resolve(cache);
2307
+ if (remaining <= 0) resolve(cache2);
1938
2308
  };
1939
2309
  for (const el of imageElements) {
1940
2310
  const img = new Image();
1941
2311
  img.crossOrigin = "anonymous";
1942
2312
  img.onload = () => {
1943
- cache.set(el.id, img);
2313
+ cache2.set(el.id, img);
1944
2314
  done();
1945
2315
  };
1946
2316
  img.onerror = done;
@@ -2365,6 +2735,41 @@ var DomNodeManager = class {
2365
2735
  }
2366
2736
  };
2367
2737
 
2738
+ // src/canvas/render-stats.ts
2739
+ var SAMPLE_SIZE = 60;
2740
+ var RenderStats = class {
2741
+ frameTimes = [];
2742
+ frameCount = 0;
2743
+ recordFrame(durationMs) {
2744
+ this.frameCount++;
2745
+ this.frameTimes.push(durationMs);
2746
+ if (this.frameTimes.length > SAMPLE_SIZE) {
2747
+ this.frameTimes.shift();
2748
+ }
2749
+ }
2750
+ getSnapshot() {
2751
+ const times = this.frameTimes;
2752
+ if (times.length === 0) {
2753
+ return { fps: 0, avgFrameMs: 0, p95FrameMs: 0, lastFrameMs: 0, frameCount: 0 };
2754
+ }
2755
+ const avg = times.reduce((a, b) => a + b, 0) / times.length;
2756
+ const sorted = [...times].sort((a, b) => a - b);
2757
+ const p95Index = Math.min(Math.floor(sorted.length * 0.95), sorted.length - 1);
2758
+ const lastFrame = times[times.length - 1] ?? 0;
2759
+ return {
2760
+ fps: avg > 0 ? Math.round(1e3 / avg) : 0,
2761
+ avgFrameMs: Math.round(avg * 100) / 100,
2762
+ p95FrameMs: Math.round((sorted[p95Index] ?? 0) * 100) / 100,
2763
+ lastFrameMs: Math.round(lastFrame * 100) / 100,
2764
+ frameCount: this.frameCount
2765
+ };
2766
+ }
2767
+ reset() {
2768
+ this.frameTimes = [];
2769
+ this.frameCount = 0;
2770
+ }
2771
+ };
2772
+
2368
2773
  // src/canvas/render-loop.ts
2369
2774
  var RenderLoop = class {
2370
2775
  needsRender = false;
@@ -2377,6 +2782,12 @@ var RenderLoop = class {
2377
2782
  toolManager;
2378
2783
  layerManager;
2379
2784
  domNodeManager;
2785
+ layerCache;
2786
+ activeDrawingLayerId = null;
2787
+ lastZoom;
2788
+ lastCamX;
2789
+ lastCamY;
2790
+ stats = new RenderStats();
2380
2791
  constructor(deps) {
2381
2792
  this.canvasEl = deps.canvasEl;
2382
2793
  this.camera = deps.camera;
@@ -2386,6 +2797,10 @@ var RenderLoop = class {
2386
2797
  this.toolManager = deps.toolManager;
2387
2798
  this.layerManager = deps.layerManager;
2388
2799
  this.domNodeManager = deps.domNodeManager;
2800
+ this.layerCache = deps.layerCache;
2801
+ this.lastZoom = deps.camera.zoom;
2802
+ this.lastCamX = deps.camera.position.x;
2803
+ this.lastCamY = deps.camera.position.y;
2389
2804
  }
2390
2805
  requestRender() {
2391
2806
  this.needsRender = true;
@@ -2412,19 +2827,63 @@ var RenderLoop = class {
2412
2827
  setCanvasSize(width, height) {
2413
2828
  this.canvasEl.width = width;
2414
2829
  this.canvasEl.height = height;
2830
+ this.layerCache.resize(this.canvasEl.clientWidth, this.canvasEl.clientHeight);
2831
+ }
2832
+ setActiveDrawingLayer(layerId) {
2833
+ this.activeDrawingLayerId = layerId;
2834
+ }
2835
+ markLayerDirty(layerId) {
2836
+ this.layerCache.markDirty(layerId);
2837
+ }
2838
+ markAllLayersDirty() {
2839
+ this.layerCache.markAllDirty();
2840
+ }
2841
+ getStats() {
2842
+ return this.stats.getSnapshot();
2843
+ }
2844
+ compositeLayerCache(ctx, layerId, dpr) {
2845
+ const cached = this.layerCache.getCanvas(layerId);
2846
+ ctx.save();
2847
+ ctx.scale(1 / this.camera.zoom, 1 / this.camera.zoom);
2848
+ ctx.translate(-this.camera.position.x, -this.camera.position.y);
2849
+ ctx.scale(1 / dpr, 1 / dpr);
2850
+ ctx.drawImage(cached, 0, 0);
2851
+ ctx.restore();
2415
2852
  }
2416
2853
  render() {
2854
+ const t0 = performance.now();
2417
2855
  const ctx = this.canvasEl.getContext("2d");
2418
2856
  if (!ctx) return;
2419
2857
  const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
2858
+ const cssWidth = this.canvasEl.clientWidth;
2859
+ const cssHeight = this.canvasEl.clientHeight;
2860
+ const currentZoom = this.camera.zoom;
2861
+ const currentCamX = this.camera.position.x;
2862
+ const currentCamY = this.camera.position.y;
2863
+ if (currentZoom !== this.lastZoom || currentCamX !== this.lastCamX || currentCamY !== this.lastCamY) {
2864
+ this.layerCache.markAllDirty();
2865
+ this.lastZoom = currentZoom;
2866
+ this.lastCamX = currentCamX;
2867
+ this.lastCamY = currentCamY;
2868
+ }
2420
2869
  ctx.save();
2421
2870
  ctx.scale(dpr, dpr);
2422
- this.renderer.setCanvasSize(this.canvasEl.clientWidth, this.canvasEl.clientHeight);
2871
+ this.renderer.setCanvasSize(cssWidth, cssHeight);
2423
2872
  this.background.render(ctx, this.camera);
2424
2873
  ctx.save();
2425
2874
  ctx.translate(this.camera.position.x, this.camera.position.y);
2426
2875
  ctx.scale(this.camera.zoom, this.camera.zoom);
2876
+ const visibleRect = this.camera.getVisibleRect(cssWidth, cssHeight);
2877
+ const margin = Math.max(visibleRect.w, visibleRect.h) * 0.1;
2878
+ const cullingRect = {
2879
+ x: visibleRect.x - margin,
2880
+ y: visibleRect.y - margin,
2881
+ w: visibleRect.w + margin * 2,
2882
+ h: visibleRect.h + margin * 2
2883
+ };
2427
2884
  const allElements = this.store.getAll();
2885
+ const layerElements = /* @__PURE__ */ new Map();
2886
+ const gridElements = [];
2428
2887
  let domZIndex = 0;
2429
2888
  for (const element of allElements) {
2430
2889
  if (!this.layerManager.isLayerVisible(element.layerId)) {
@@ -2434,9 +2893,54 @@ var RenderLoop = class {
2434
2893
  continue;
2435
2894
  }
2436
2895
  if (this.renderer.isDomElement(element)) {
2437
- this.domNodeManager.syncDomNode(element, domZIndex++);
2438
- } else {
2439
- this.renderer.renderCanvasElement(ctx, element);
2896
+ const elBounds = getElementBounds(element);
2897
+ if (elBounds && !boundsIntersect(elBounds, cullingRect)) {
2898
+ this.domNodeManager.hideDomNode(element.id);
2899
+ } else {
2900
+ this.domNodeManager.syncDomNode(element, domZIndex++);
2901
+ }
2902
+ continue;
2903
+ }
2904
+ if (element.type === "grid") {
2905
+ gridElements.push(element);
2906
+ continue;
2907
+ }
2908
+ let group = layerElements.get(element.layerId);
2909
+ if (!group) {
2910
+ group = [];
2911
+ layerElements.set(element.layerId, group);
2912
+ }
2913
+ group.push(element);
2914
+ }
2915
+ for (const grid of gridElements) {
2916
+ this.renderer.renderCanvasElement(ctx, grid);
2917
+ }
2918
+ for (const [layerId, elements] of layerElements) {
2919
+ const isActiveDrawingLayer = layerId === this.activeDrawingLayerId;
2920
+ if (!this.layerCache.isDirty(layerId)) {
2921
+ this.compositeLayerCache(ctx, layerId, dpr);
2922
+ continue;
2923
+ }
2924
+ if (isActiveDrawingLayer) {
2925
+ this.compositeLayerCache(ctx, layerId, dpr);
2926
+ continue;
2927
+ }
2928
+ const offCtx = this.layerCache.getContext(layerId);
2929
+ if (offCtx) {
2930
+ const offCanvas = this.layerCache.getCanvas(layerId);
2931
+ offCtx.clearRect(0, 0, offCanvas.width, offCanvas.height);
2932
+ offCtx.save();
2933
+ offCtx.scale(dpr, dpr);
2934
+ offCtx.translate(this.camera.position.x, this.camera.position.y);
2935
+ offCtx.scale(this.camera.zoom, this.camera.zoom);
2936
+ for (const element of elements) {
2937
+ const elBounds = getElementBounds(element);
2938
+ if (elBounds && !boundsIntersect(elBounds, cullingRect)) continue;
2939
+ this.renderer.renderCanvasElement(offCtx, element);
2940
+ }
2941
+ offCtx.restore();
2942
+ this.layerCache.markClean(layerId);
2943
+ this.compositeLayerCache(ctx, layerId, dpr);
2440
2944
  }
2441
2945
  }
2442
2946
  const activeTool = this.toolManager.activeTool;
@@ -2445,6 +2949,70 @@ var RenderLoop = class {
2445
2949
  }
2446
2950
  ctx.restore();
2447
2951
  ctx.restore();
2952
+ this.stats.recordFrame(performance.now() - t0);
2953
+ }
2954
+ };
2955
+
2956
+ // src/canvas/layer-cache.ts
2957
+ function createOffscreenCanvas(width, height) {
2958
+ if (typeof OffscreenCanvas !== "undefined") {
2959
+ return new OffscreenCanvas(width, height);
2960
+ }
2961
+ const canvas = document.createElement("canvas");
2962
+ canvas.width = width;
2963
+ canvas.height = height;
2964
+ return canvas;
2965
+ }
2966
+ var LayerCache = class {
2967
+ canvases = /* @__PURE__ */ new Map();
2968
+ dirtyFlags = /* @__PURE__ */ new Map();
2969
+ width;
2970
+ height;
2971
+ constructor(width, height) {
2972
+ const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
2973
+ this.width = Math.round(width * dpr);
2974
+ this.height = Math.round(height * dpr);
2975
+ }
2976
+ isDirty(layerId) {
2977
+ return this.dirtyFlags.get(layerId) !== false;
2978
+ }
2979
+ markDirty(layerId) {
2980
+ this.dirtyFlags.set(layerId, true);
2981
+ }
2982
+ markClean(layerId) {
2983
+ this.dirtyFlags.set(layerId, false);
2984
+ }
2985
+ markAllDirty() {
2986
+ for (const [id] of this.dirtyFlags) {
2987
+ this.dirtyFlags.set(id, true);
2988
+ }
2989
+ }
2990
+ getCanvas(layerId) {
2991
+ let canvas = this.canvases.get(layerId);
2992
+ if (!canvas) {
2993
+ canvas = createOffscreenCanvas(this.width, this.height);
2994
+ this.canvases.set(layerId, canvas);
2995
+ this.dirtyFlags.set(layerId, true);
2996
+ }
2997
+ return canvas;
2998
+ }
2999
+ getContext(layerId) {
3000
+ const canvas = this.getCanvas(layerId);
3001
+ return canvas.getContext("2d");
3002
+ }
3003
+ resize(width, height) {
3004
+ const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
3005
+ this.width = Math.round(width * dpr);
3006
+ this.height = Math.round(height * dpr);
3007
+ for (const [id, canvas] of this.canvases) {
3008
+ canvas.width = this.width;
3009
+ canvas.height = this.height;
3010
+ this.dirtyFlags.set(id, true);
3011
+ }
3012
+ }
3013
+ clear() {
3014
+ this.canvases.clear();
3015
+ this.dirtyFlags.clear();
2448
3016
  }
2449
3017
  };
2450
3018
 
@@ -2501,6 +3069,10 @@ var Viewport = class {
2501
3069
  this.interactMode = new InteractMode({
2502
3070
  getNode: (id) => this.domNodeManager.getNode(id)
2503
3071
  });
3072
+ const layerCache = new LayerCache(
3073
+ this.canvasEl.clientWidth || 800,
3074
+ this.canvasEl.clientHeight || 600
3075
+ );
2504
3076
  this.renderLoop = new RenderLoop({
2505
3077
  canvasEl: this.canvasEl,
2506
3078
  camera: this.camera,
@@ -2509,22 +3081,34 @@ var Viewport = class {
2509
3081
  renderer: this.renderer,
2510
3082
  toolManager: this.toolManager,
2511
3083
  layerManager: this.layerManager,
2512
- domNodeManager: this.domNodeManager
3084
+ domNodeManager: this.domNodeManager,
3085
+ layerCache
2513
3086
  });
2514
3087
  this.unsubCamera = this.camera.onChange(() => {
2515
3088
  this.applyCameraTransform();
2516
3089
  this.requestRender();
2517
3090
  });
2518
3091
  this.unsubStore = [
2519
- this.store.on("add", () => this.requestRender()),
3092
+ this.store.on("add", (el) => {
3093
+ this.renderLoop.markLayerDirty(el.layerId);
3094
+ this.requestRender();
3095
+ }),
2520
3096
  this.store.on("remove", (el) => {
2521
3097
  this.unbindArrowsFrom(el);
2522
3098
  this.domNodeManager.removeDomNode(el.id);
3099
+ this.renderLoop.markLayerDirty(el.layerId);
3100
+ this.requestRender();
3101
+ }),
3102
+ this.store.on("update", ({ previous, current }) => {
3103
+ this.renderLoop.markLayerDirty(current.layerId);
3104
+ if (previous.layerId !== current.layerId) {
3105
+ this.renderLoop.markLayerDirty(previous.layerId);
3106
+ }
2523
3107
  this.requestRender();
2524
3108
  }),
2525
- this.store.on("update", () => this.requestRender()),
2526
3109
  this.store.on("clear", () => {
2527
3110
  this.domNodeManager.clearDomNodes();
3111
+ this.renderLoop.markAllLayersDirty();
2528
3112
  this.requestRender();
2529
3113
  })
2530
3114
  ];
@@ -2730,8 +3314,8 @@ var Viewport = class {
2730
3314
  }
2731
3315
  };
2732
3316
  hitTestWorld(world) {
2733
- const elements = this.store.getAll().reverse();
2734
- for (const el of elements) {
3317
+ const candidates = this.store.queryPoint(world).reverse();
3318
+ for (const el of candidates) {
2735
3319
  if (!("size" in el)) continue;
2736
3320
  const { x, y } = el.position;
2737
3321
  const { w, h } = el.size;
@@ -2882,6 +3466,9 @@ var HandTool = class {
2882
3466
  var MIN_POINTS_FOR_STROKE = 2;
2883
3467
  var DEFAULT_SMOOTHING = 1.5;
2884
3468
  var DEFAULT_PRESSURE = 0.5;
3469
+ var DEFAULT_MIN_POINT_DISTANCE = 3;
3470
+ var DEFAULT_PROGRESSIVE_THRESHOLD = 200;
3471
+ var PROGRESSIVE_HOT_ZONE = 30;
2885
3472
  var PencilTool = class {
2886
3473
  name = "pencil";
2887
3474
  drawing = false;
@@ -2889,11 +3476,17 @@ var PencilTool = class {
2889
3476
  color;
2890
3477
  width;
2891
3478
  smoothing;
3479
+ minPointDistance;
3480
+ progressiveThreshold;
3481
+ nextSimplifyAt;
2892
3482
  optionListeners = /* @__PURE__ */ new Set();
2893
3483
  constructor(options = {}) {
2894
3484
  this.color = options.color ?? "#000000";
2895
3485
  this.width = options.width ?? 2;
2896
3486
  this.smoothing = options.smoothing ?? DEFAULT_SMOOTHING;
3487
+ this.minPointDistance = options.minPointDistance ?? DEFAULT_MIN_POINT_DISTANCE;
3488
+ this.progressiveThreshold = options.progressiveSimplifyThreshold ?? DEFAULT_PROGRESSIVE_THRESHOLD;
3489
+ this.nextSimplifyAt = this.progressiveThreshold;
2897
3490
  }
2898
3491
  onActivate(ctx) {
2899
3492
  ctx.setCursor?.("crosshair");
@@ -2902,7 +3495,13 @@ var PencilTool = class {
2902
3495
  ctx.setCursor?.("default");
2903
3496
  }
2904
3497
  getOptions() {
2905
- return { color: this.color, width: this.width, smoothing: this.smoothing };
3498
+ return {
3499
+ color: this.color,
3500
+ width: this.width,
3501
+ smoothing: this.smoothing,
3502
+ minPointDistance: this.minPointDistance,
3503
+ progressiveSimplifyThreshold: this.progressiveThreshold
3504
+ };
2906
3505
  }
2907
3506
  onOptionsChange(listener) {
2908
3507
  this.optionListeners.add(listener);
@@ -2912,6 +3511,9 @@ var PencilTool = class {
2912
3511
  if (options.color !== void 0) this.color = options.color;
2913
3512
  if (options.width !== void 0) this.width = options.width;
2914
3513
  if (options.smoothing !== void 0) this.smoothing = options.smoothing;
3514
+ if (options.minPointDistance !== void 0) this.minPointDistance = options.minPointDistance;
3515
+ if (options.progressiveSimplifyThreshold !== void 0)
3516
+ this.progressiveThreshold = options.progressiveSimplifyThreshold;
2915
3517
  this.notifyOptionsChange();
2916
3518
  }
2917
3519
  onPointerDown(state, ctx) {
@@ -2919,12 +3521,26 @@ var PencilTool = class {
2919
3521
  const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
2920
3522
  const pressure = state.pressure === 0 ? DEFAULT_PRESSURE : state.pressure;
2921
3523
  this.points = [{ x: world.x, y: world.y, pressure }];
3524
+ this.nextSimplifyAt = this.progressiveThreshold;
2922
3525
  }
2923
3526
  onPointerMove(state, ctx) {
2924
3527
  if (!this.drawing) return;
2925
3528
  const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
2926
3529
  const pressure = state.pressure === 0 ? DEFAULT_PRESSURE : state.pressure;
3530
+ const last = this.points[this.points.length - 1];
3531
+ if (last) {
3532
+ const dx = world.x - last.x;
3533
+ const dy = world.y - last.y;
3534
+ if (dx * dx + dy * dy < this.minPointDistance * this.minPointDistance) return;
3535
+ }
2927
3536
  this.points.push({ x: world.x, y: world.y, pressure });
3537
+ if (this.points.length > this.nextSimplifyAt) {
3538
+ const hotZone = this.points.slice(-PROGRESSIVE_HOT_ZONE);
3539
+ const coldZone = this.points.slice(0, -PROGRESSIVE_HOT_ZONE);
3540
+ const simplified = simplifyPoints(coldZone, this.smoothing * 2);
3541
+ this.points = [...simplified, ...hotZone];
3542
+ this.nextSimplifyAt = this.points.length + this.progressiveThreshold;
3543
+ }
2928
3544
  ctx.requestRender();
2929
3545
  }
2930
3546
  onPointerUp(_state, ctx) {
@@ -2942,6 +3558,7 @@ var PencilTool = class {
2942
3558
  layerId: ctx.activeLayerId ?? ""
2943
3559
  });
2944
3560
  ctx.store.add(stroke);
3561
+ computeStrokeSegments(stroke);
2945
3562
  this.points = [];
2946
3563
  ctx.requestRender();
2947
3564
  }
@@ -3006,13 +3623,20 @@ var EraserTool = class {
3006
3623
  }
3007
3624
  eraseAt(state, ctx) {
3008
3625
  const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
3009
- const strokes = ctx.store.getElementsByType("stroke");
3626
+ const queryBounds = {
3627
+ x: world.x - this.radius,
3628
+ y: world.y - this.radius,
3629
+ w: this.radius * 2,
3630
+ h: this.radius * 2
3631
+ };
3632
+ const candidates = ctx.store.queryRect(queryBounds);
3010
3633
  let erased = false;
3011
- for (const stroke of strokes) {
3012
- if (ctx.isLayerVisible && !ctx.isLayerVisible(stroke.layerId)) continue;
3013
- if (ctx.isLayerLocked && ctx.isLayerLocked(stroke.layerId)) continue;
3014
- if (this.strokeIntersects(stroke, world)) {
3015
- ctx.store.remove(stroke.id);
3634
+ for (const el of candidates) {
3635
+ if (el.type !== "stroke") continue;
3636
+ if (ctx.isLayerVisible && !ctx.isLayerVisible(el.layerId)) continue;
3637
+ if (ctx.isLayerLocked && ctx.isLayerLocked(el.layerId)) continue;
3638
+ if (this.strokeIntersects(el, world)) {
3639
+ ctx.store.remove(el.id);
3016
3640
  erased = true;
3017
3641
  }
3018
3642
  }
@@ -3389,7 +4013,7 @@ var SelectTool = class {
3389
4013
  for (const id of this._selectedIds) {
3390
4014
  const el = ctx.store.getById(id);
3391
4015
  if (!el || !("size" in el)) continue;
3392
- const bounds = this.getElementBounds(el);
4016
+ const bounds = getElementBounds(el);
3393
4017
  if (!bounds) continue;
3394
4018
  const corners = this.getHandlePositions(bounds);
3395
4019
  for (const [handle, pos] of corners) {
@@ -3437,7 +4061,7 @@ var SelectTool = class {
3437
4061
  this.renderBindingHighlights(canvasCtx, el, zoom);
3438
4062
  continue;
3439
4063
  }
3440
- const bounds = this.getElementBounds(el);
4064
+ const bounds = getElementBounds(el);
3441
4065
  if (!bounds) continue;
3442
4066
  const pad = SELECTION_PAD / zoom;
3443
4067
  canvasCtx.strokeRect(bounds.x - pad, bounds.y - pad, bounds.w + pad * 2, bounds.h + pad * 2);
@@ -3496,12 +4120,13 @@ var SelectTool = class {
3496
4120
  return { x, y, w, h };
3497
4121
  }
3498
4122
  findElementsInRect(marquee, ctx) {
4123
+ const candidates = ctx.store.queryRect(marquee);
3499
4124
  const ids = [];
3500
- for (const el of ctx.store.getAll()) {
4125
+ for (const el of candidates) {
3501
4126
  if (ctx.isLayerVisible && !ctx.isLayerVisible(el.layerId)) continue;
3502
4127
  if (ctx.isLayerLocked && ctx.isLayerLocked(el.layerId)) continue;
3503
4128
  if (el.type === "grid") continue;
3504
- const bounds = this.getElementBounds(el);
4129
+ const bounds = getElementBounds(el);
3505
4130
  if (bounds && this.rectsOverlap(marquee, bounds)) {
3506
4131
  ids.push(el.id);
3507
4132
  }
@@ -3511,30 +4136,10 @@ var SelectTool = class {
3511
4136
  rectsOverlap(a, b) {
3512
4137
  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;
3513
4138
  }
3514
- getElementBounds(el) {
3515
- if ("size" in el) {
3516
- return { x: el.position.x, y: el.position.y, w: el.size.w, h: el.size.h };
3517
- }
3518
- if (el.type === "stroke" && el.points.length > 0) {
3519
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
3520
- for (const p of el.points) {
3521
- const px = p.x + el.position.x;
3522
- const py = p.y + el.position.y;
3523
- if (px < minX) minX = px;
3524
- if (py < minY) minY = py;
3525
- if (px > maxX) maxX = px;
3526
- if (py > maxY) maxY = py;
3527
- }
3528
- return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
3529
- }
3530
- if (el.type === "arrow") {
3531
- return getArrowBounds(el.from, el.to, el.bend);
3532
- }
3533
- return null;
3534
- }
3535
4139
  hitTest(world, ctx) {
3536
- const elements = ctx.store.getAll().reverse();
3537
- for (const el of elements) {
4140
+ const r = 10;
4141
+ const candidates = ctx.store.queryRect({ x: world.x - r, y: world.y - r, w: r * 2, h: r * 2 }).reverse();
4142
+ for (const el of candidates) {
3538
4143
  if (ctx.isLayerVisible && !ctx.isLayerVisible(el.layerId)) continue;
3539
4144
  if (ctx.isLayerLocked && ctx.isLayerLocked(el.layerId)) continue;
3540
4145
  if (el.type === "grid") continue;
@@ -4028,7 +4633,7 @@ var UpdateLayerCommand = class {
4028
4633
  };
4029
4634
 
4030
4635
  // src/index.ts
4031
- var VERSION = "0.8.6";
4636
+ var VERSION = "0.8.8";
4032
4637
  export {
4033
4638
  AddElementCommand,
4034
4639
  ArrowTool,
@@ -4050,6 +4655,7 @@ export {
4050
4655
  NoteEditor,
4051
4656
  NoteTool,
4052
4657
  PencilTool,
4658
+ Quadtree,
4053
4659
  RemoveElementCommand,
4054
4660
  RemoveLayerCommand,
4055
4661
  SelectTool,
@@ -4060,6 +4666,7 @@ export {
4060
4666
  UpdateLayerCommand,
4061
4667
  VERSION,
4062
4668
  Viewport,
4669
+ boundsIntersect,
4063
4670
  clearStaleBindings,
4064
4671
  createArrow,
4065
4672
  createGrid,