@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.cjs CHANGED
@@ -40,6 +40,7 @@ __export(index_exports, {
40
40
  NoteEditor: () => NoteEditor,
41
41
  NoteTool: () => NoteTool,
42
42
  PencilTool: () => PencilTool,
43
+ Quadtree: () => Quadtree,
43
44
  RemoveElementCommand: () => RemoveElementCommand,
44
45
  RemoveLayerCommand: () => RemoveLayerCommand,
45
46
  SelectTool: () => SelectTool,
@@ -50,6 +51,7 @@ __export(index_exports, {
50
51
  UpdateLayerCommand: () => UpdateLayerCommand,
51
52
  VERSION: () => VERSION,
52
53
  Viewport: () => Viewport,
54
+ boundsIntersect: () => boundsIntersect,
53
55
  clearStaleBindings: () => clearStaleBindings,
54
56
  createArrow: () => createArrow,
55
57
  createGrid: () => createGrid,
@@ -105,6 +107,153 @@ var EventBus = class {
105
107
  }
106
108
  };
107
109
 
110
+ // src/core/quadtree.ts
111
+ var MAX_ITEMS = 8;
112
+ var MAX_DEPTH = 8;
113
+ function intersects(a, b) {
114
+ return a.x <= b.x + b.w && a.x + a.w >= b.x && a.y <= b.y + b.h && a.y + a.h >= b.y;
115
+ }
116
+ var QuadNode = class _QuadNode {
117
+ constructor(bounds, depth) {
118
+ this.bounds = bounds;
119
+ this.depth = depth;
120
+ }
121
+ items = [];
122
+ children = null;
123
+ insert(entry) {
124
+ if (this.children) {
125
+ const idx = this.getChildIndex(entry.bounds);
126
+ if (idx !== -1) {
127
+ const child = this.children[idx];
128
+ if (child) child.insert(entry);
129
+ return;
130
+ }
131
+ this.items.push(entry);
132
+ return;
133
+ }
134
+ this.items.push(entry);
135
+ if (this.items.length > MAX_ITEMS && this.depth < MAX_DEPTH) {
136
+ this.split();
137
+ }
138
+ }
139
+ remove(id) {
140
+ const idx = this.items.findIndex((e) => e.id === id);
141
+ if (idx !== -1) {
142
+ this.items.splice(idx, 1);
143
+ return true;
144
+ }
145
+ if (this.children) {
146
+ for (const child of this.children) {
147
+ if (child.remove(id)) {
148
+ this.collapseIfEmpty();
149
+ return true;
150
+ }
151
+ }
152
+ }
153
+ return false;
154
+ }
155
+ query(rect, result) {
156
+ if (!intersects(this.bounds, rect)) return;
157
+ for (const item of this.items) {
158
+ if (intersects(item.bounds, rect)) {
159
+ result.push(item.id);
160
+ }
161
+ }
162
+ if (this.children) {
163
+ for (const child of this.children) {
164
+ child.query(rect, result);
165
+ }
166
+ }
167
+ }
168
+ getChildIndex(itemBounds) {
169
+ const midX = this.bounds.x + this.bounds.w / 2;
170
+ const midY = this.bounds.y + this.bounds.h / 2;
171
+ const left = itemBounds.x >= this.bounds.x && itemBounds.x + itemBounds.w <= midX;
172
+ const right = itemBounds.x >= midX && itemBounds.x + itemBounds.w <= this.bounds.x + this.bounds.w;
173
+ const top = itemBounds.y >= this.bounds.y && itemBounds.y + itemBounds.h <= midY;
174
+ const bottom = itemBounds.y >= midY && itemBounds.y + itemBounds.h <= this.bounds.y + this.bounds.h;
175
+ if (left && top) return 0;
176
+ if (right && top) return 1;
177
+ if (left && bottom) return 2;
178
+ if (right && bottom) return 3;
179
+ return -1;
180
+ }
181
+ split() {
182
+ const { x, y, w, h } = this.bounds;
183
+ const halfW = w / 2;
184
+ const halfH = h / 2;
185
+ const d = this.depth + 1;
186
+ this.children = [
187
+ new _QuadNode({ x, y, w: halfW, h: halfH }, d),
188
+ new _QuadNode({ x: x + halfW, y, w: halfW, h: halfH }, d),
189
+ new _QuadNode({ x, y: y + halfH, w: halfW, h: halfH }, d),
190
+ new _QuadNode({ x: x + halfW, y: y + halfH, w: halfW, h: halfH }, d)
191
+ ];
192
+ const remaining = [];
193
+ for (const item of this.items) {
194
+ const idx = this.getChildIndex(item.bounds);
195
+ if (idx !== -1) {
196
+ const target = this.children[idx];
197
+ if (target) target.insert(item);
198
+ } else {
199
+ remaining.push(item);
200
+ }
201
+ }
202
+ this.items = remaining;
203
+ }
204
+ collapseIfEmpty() {
205
+ if (!this.children) return;
206
+ let totalItems = this.items.length;
207
+ for (const child of this.children) {
208
+ if (child.children) return;
209
+ totalItems += child.items.length;
210
+ }
211
+ if (totalItems <= MAX_ITEMS) {
212
+ for (const child of this.children) {
213
+ this.items.push(...child.items);
214
+ }
215
+ this.children = null;
216
+ }
217
+ }
218
+ };
219
+ var Quadtree = class {
220
+ root;
221
+ _size = 0;
222
+ worldBounds;
223
+ constructor(worldBounds) {
224
+ this.worldBounds = worldBounds;
225
+ this.root = new QuadNode(worldBounds, 0);
226
+ }
227
+ get size() {
228
+ return this._size;
229
+ }
230
+ insert(id, bounds) {
231
+ this.root.insert({ id, bounds });
232
+ this._size++;
233
+ }
234
+ remove(id) {
235
+ if (this.root.remove(id)) {
236
+ this._size--;
237
+ }
238
+ }
239
+ update(id, newBounds) {
240
+ this.remove(id);
241
+ this.insert(id, newBounds);
242
+ }
243
+ query(rect) {
244
+ const result = [];
245
+ this.root.query(rect, result);
246
+ return result;
247
+ }
248
+ queryPoint(point) {
249
+ return this.query({ x: point.x, y: point.y, w: 0, h: 0 });
250
+ }
251
+ clear() {
252
+ this.root = new QuadNode(this.worldBounds, 0);
253
+ this._size = 0;
254
+ }
255
+ };
256
+
108
257
  // src/core/state-serializer.ts
109
258
  var CURRENT_VERSION = 2;
110
259
  function exportState(elements, camera, layers = []) {
@@ -319,16 +468,16 @@ var Camera = class {
319
468
  pan(dx, dy) {
320
469
  this.x += dx;
321
470
  this.y += dy;
322
- this.notifyChange();
471
+ this.notifyPan();
323
472
  }
324
473
  moveTo(x, y) {
325
474
  this.x = x;
326
475
  this.y = y;
327
- this.notifyChange();
476
+ this.notifyPan();
328
477
  }
329
478
  setZoom(level) {
330
479
  this.z = Math.min(this.maxZoom, Math.max(this.minZoom, level));
331
- this.notifyChange();
480
+ this.notifyZoom();
332
481
  }
333
482
  zoomAt(level, screenPoint) {
334
483
  const before = this.screenToWorld(screenPoint);
@@ -336,7 +485,7 @@ var Camera = class {
336
485
  const after = this.screenToWorld(screenPoint);
337
486
  this.x += (after.x - before.x) * this.z;
338
487
  this.y += (after.y - before.y) * this.z;
339
- this.notifyChange();
488
+ this.notifyPanAndZoom();
340
489
  }
341
490
  screenToWorld(screen) {
342
491
  return {
@@ -350,6 +499,16 @@ var Camera = class {
350
499
  y: world.y * this.z + this.y
351
500
  };
352
501
  }
502
+ getVisibleRect(canvasWidth, canvasHeight) {
503
+ const topLeft = this.screenToWorld({ x: 0, y: 0 });
504
+ const bottomRight = this.screenToWorld({ x: canvasWidth, y: canvasHeight });
505
+ return {
506
+ x: topLeft.x,
507
+ y: topLeft.y,
508
+ w: bottomRight.x - topLeft.x,
509
+ h: bottomRight.y - topLeft.y
510
+ };
511
+ }
353
512
  toCSSTransform() {
354
513
  return `translate3d(${this.x}px, ${this.y}px, 0) scale(${this.z})`;
355
514
  }
@@ -357,8 +516,14 @@ var Camera = class {
357
516
  this.changeListeners.add(listener);
358
517
  return () => this.changeListeners.delete(listener);
359
518
  }
360
- notifyChange() {
361
- this.changeListeners.forEach((fn) => fn());
519
+ notifyPan() {
520
+ this.changeListeners.forEach((fn) => fn({ panned: true, zoomed: false }));
521
+ }
522
+ notifyZoom() {
523
+ this.changeListeners.forEach((fn) => fn({ panned: false, zoomed: true }));
524
+ }
525
+ notifyPanAndZoom() {
526
+ this.changeListeners.forEach((fn) => fn({ panned: true, zoomed: true }));
362
527
  }
363
528
  };
364
529
 
@@ -377,6 +542,13 @@ var Background = class {
377
542
  color;
378
543
  dotRadius;
379
544
  lineWidth;
545
+ cachedCanvas = null;
546
+ cachedCtx = null;
547
+ lastZoom = -1;
548
+ lastOffsetX = -Infinity;
549
+ lastOffsetY = -Infinity;
550
+ lastWidth = 0;
551
+ lastHeight = 0;
380
552
  constructor(options = {}) {
381
553
  this.pattern = options.pattern ?? DEFAULTS.pattern;
382
554
  this.spacing = options.spacing ?? DEFAULTS.spacing;
@@ -392,13 +564,69 @@ var Background = class {
392
564
  ctx.save();
393
565
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
394
566
  ctx.clearRect(0, 0, cssWidth, cssHeight);
567
+ if (this.pattern === "none") {
568
+ ctx.restore();
569
+ return;
570
+ }
571
+ const spacing = this.adaptSpacing(this.spacing, camera.zoom);
572
+ const keyZoom = camera.zoom;
573
+ const keyX = Math.floor(camera.position.x % spacing);
574
+ const keyY = Math.floor(camera.position.y % spacing);
575
+ if (this.cachedCanvas !== null && keyZoom === this.lastZoom && keyX === this.lastOffsetX && keyY === this.lastOffsetY && cssWidth === this.lastWidth && cssHeight === this.lastHeight) {
576
+ ctx.drawImage(this.cachedCanvas, 0, 0);
577
+ ctx.restore();
578
+ return;
579
+ }
580
+ this.ensureCachedCanvas(cssWidth, cssHeight, dpr);
581
+ if (this.cachedCtx === null) {
582
+ if (this.pattern === "dots") {
583
+ this.renderDots(ctx, camera, cssWidth, cssHeight);
584
+ } else if (this.pattern === "grid") {
585
+ this.renderGrid(ctx, camera, cssWidth, cssHeight);
586
+ }
587
+ ctx.restore();
588
+ return;
589
+ }
590
+ const offCtx = this.cachedCtx;
591
+ offCtx.clearRect(0, 0, cssWidth, cssHeight);
395
592
  if (this.pattern === "dots") {
396
- this.renderDots(ctx, camera, cssWidth, cssHeight);
593
+ this.renderDots(offCtx, camera, cssWidth, cssHeight);
397
594
  } else if (this.pattern === "grid") {
398
- this.renderGrid(ctx, camera, cssWidth, cssHeight);
399
- }
595
+ this.renderGrid(offCtx, camera, cssWidth, cssHeight);
596
+ }
597
+ this.lastZoom = keyZoom;
598
+ this.lastOffsetX = keyX;
599
+ this.lastOffsetY = keyY;
600
+ this.lastWidth = cssWidth;
601
+ this.lastHeight = cssHeight;
602
+ ctx.drawImage(this.cachedCanvas, 0, 0);
400
603
  ctx.restore();
401
604
  }
605
+ ensureCachedCanvas(cssWidth, cssHeight, dpr) {
606
+ if (this.cachedCanvas !== null && this.lastWidth === cssWidth && this.lastHeight === cssHeight) {
607
+ return;
608
+ }
609
+ const physWidth = Math.round(cssWidth * dpr);
610
+ const physHeight = Math.round(cssHeight * dpr);
611
+ if (typeof OffscreenCanvas !== "undefined") {
612
+ this.cachedCanvas = new OffscreenCanvas(physWidth, physHeight);
613
+ } else if (typeof document !== "undefined") {
614
+ const el = document.createElement("canvas");
615
+ el.width = physWidth;
616
+ el.height = physHeight;
617
+ this.cachedCanvas = el;
618
+ } else {
619
+ this.cachedCanvas = null;
620
+ this.cachedCtx = null;
621
+ return;
622
+ }
623
+ const offCtx = this.cachedCanvas.getContext("2d");
624
+ if (offCtx !== null) {
625
+ offCtx.scale(dpr, dpr);
626
+ }
627
+ this.cachedCtx = offCtx;
628
+ this.lastZoom = -1;
629
+ }
402
630
  adaptSpacing(baseSpacing, zoom) {
403
631
  let spacing = baseSpacing * zoom;
404
632
  while (spacing < MIN_PATTERN_SPACING) {
@@ -664,77 +892,6 @@ var InputHandler = class {
664
892
  }
665
893
  };
666
894
 
667
- // src/elements/element-store.ts
668
- var ElementStore = class {
669
- elements = /* @__PURE__ */ new Map();
670
- bus = new EventBus();
671
- layerOrderMap = /* @__PURE__ */ new Map();
672
- get count() {
673
- return this.elements.size;
674
- }
675
- setLayerOrder(order) {
676
- this.layerOrderMap = new Map(order);
677
- }
678
- getAll() {
679
- return [...this.elements.values()].sort((a, b) => {
680
- const layerA = this.layerOrderMap.get(a.layerId) ?? 0;
681
- const layerB = this.layerOrderMap.get(b.layerId) ?? 0;
682
- if (layerA !== layerB) return layerA - layerB;
683
- return a.zIndex - b.zIndex;
684
- });
685
- }
686
- getById(id) {
687
- return this.elements.get(id);
688
- }
689
- getElementsByType(type) {
690
- return this.getAll().filter(
691
- (el) => el.type === type
692
- );
693
- }
694
- add(element) {
695
- this.elements.set(element.id, element);
696
- this.bus.emit("add", element);
697
- }
698
- update(id, partial) {
699
- const existing = this.elements.get(id);
700
- if (!existing) return;
701
- const updated = { ...existing, ...partial, id: existing.id, type: existing.type };
702
- this.elements.set(id, updated);
703
- this.bus.emit("update", { previous: existing, current: updated });
704
- }
705
- remove(id) {
706
- const element = this.elements.get(id);
707
- if (!element) return;
708
- this.elements.delete(id);
709
- this.bus.emit("remove", element);
710
- }
711
- clear() {
712
- this.elements.clear();
713
- this.bus.emit("clear", null);
714
- }
715
- snapshot() {
716
- return this.getAll().map((el) => ({ ...el }));
717
- }
718
- loadSnapshot(elements) {
719
- this.elements.clear();
720
- for (const el of elements) {
721
- this.elements.set(el.id, el);
722
- }
723
- }
724
- on(event, listener) {
725
- return this.bus.on(event, listener);
726
- }
727
- onChange(listener) {
728
- const unsubs = [
729
- this.bus.on("add", listener),
730
- this.bus.on("remove", listener),
731
- this.bus.on("update", listener),
732
- this.bus.on("clear", listener)
733
- ];
734
- return () => unsubs.forEach((fn) => fn());
735
- }
736
- };
737
-
738
895
  // src/elements/arrow-geometry.ts
739
896
  function getArrowControlPoint(from, to, bend) {
740
897
  const midX = (from.x + to.x) / 2;
@@ -835,6 +992,189 @@ function isNearLine(point, a, b, threshold) {
835
992
  return Math.hypot(point.x - projX, point.y - projY) <= threshold;
836
993
  }
837
994
 
995
+ // src/elements/element-bounds.ts
996
+ var strokeBoundsCache = /* @__PURE__ */ new WeakMap();
997
+ function getElementBounds(element) {
998
+ if (element.type === "grid") return null;
999
+ if ("size" in element) {
1000
+ return {
1001
+ x: element.position.x,
1002
+ y: element.position.y,
1003
+ w: element.size.w,
1004
+ h: element.size.h
1005
+ };
1006
+ }
1007
+ if (element.type === "stroke") {
1008
+ if (element.points.length === 0) return null;
1009
+ const cached = strokeBoundsCache.get(element);
1010
+ if (cached) return cached;
1011
+ let minX = Infinity;
1012
+ let minY = Infinity;
1013
+ let maxX = -Infinity;
1014
+ let maxY = -Infinity;
1015
+ for (const p of element.points) {
1016
+ const px = p.x + element.position.x;
1017
+ const py = p.y + element.position.y;
1018
+ if (px < minX) minX = px;
1019
+ if (py < minY) minY = py;
1020
+ if (px > maxX) maxX = px;
1021
+ if (py > maxY) maxY = py;
1022
+ }
1023
+ const bounds = { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
1024
+ strokeBoundsCache.set(element, bounds);
1025
+ return bounds;
1026
+ }
1027
+ if (element.type === "arrow") {
1028
+ return getArrowBoundsAnalytical(element.from, element.to, element.bend);
1029
+ }
1030
+ return null;
1031
+ }
1032
+ function getArrowBoundsAnalytical(from, to, bend) {
1033
+ if (bend === 0) {
1034
+ const minX2 = Math.min(from.x, to.x);
1035
+ const minY2 = Math.min(from.y, to.y);
1036
+ return {
1037
+ x: minX2,
1038
+ y: minY2,
1039
+ w: Math.abs(to.x - from.x),
1040
+ h: Math.abs(to.y - from.y)
1041
+ };
1042
+ }
1043
+ const cp = getArrowControlPoint(from, to, bend);
1044
+ let minX = Math.min(from.x, to.x);
1045
+ let maxX = Math.max(from.x, to.x);
1046
+ let minY = Math.min(from.y, to.y);
1047
+ let maxY = Math.max(from.y, to.y);
1048
+ const tx = from.x - 2 * cp.x + to.x;
1049
+ if (tx !== 0) {
1050
+ const t = (from.x - cp.x) / tx;
1051
+ if (t > 0 && t < 1) {
1052
+ const mt = 1 - t;
1053
+ const x = mt * mt * from.x + 2 * mt * t * cp.x + t * t * to.x;
1054
+ if (x < minX) minX = x;
1055
+ if (x > maxX) maxX = x;
1056
+ }
1057
+ }
1058
+ const ty = from.y - 2 * cp.y + to.y;
1059
+ if (ty !== 0) {
1060
+ const t = (from.y - cp.y) / ty;
1061
+ if (t > 0 && t < 1) {
1062
+ const mt = 1 - t;
1063
+ const y = mt * mt * from.y + 2 * mt * t * cp.y + t * t * to.y;
1064
+ if (y < minY) minY = y;
1065
+ if (y > maxY) maxY = y;
1066
+ }
1067
+ }
1068
+ return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
1069
+ }
1070
+ function boundsIntersect(a, b) {
1071
+ 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;
1072
+ }
1073
+
1074
+ // src/elements/element-store.ts
1075
+ var ElementStore = class {
1076
+ elements = /* @__PURE__ */ new Map();
1077
+ bus = new EventBus();
1078
+ layerOrderMap = /* @__PURE__ */ new Map();
1079
+ spatialIndex = new Quadtree({ x: -1e5, y: -1e5, w: 2e5, h: 2e5 });
1080
+ get count() {
1081
+ return this.elements.size;
1082
+ }
1083
+ setLayerOrder(order) {
1084
+ this.layerOrderMap = new Map(order);
1085
+ }
1086
+ getAll() {
1087
+ return [...this.elements.values()].sort((a, b) => {
1088
+ const layerA = this.layerOrderMap.get(a.layerId) ?? 0;
1089
+ const layerB = this.layerOrderMap.get(b.layerId) ?? 0;
1090
+ if (layerA !== layerB) return layerA - layerB;
1091
+ return a.zIndex - b.zIndex;
1092
+ });
1093
+ }
1094
+ getById(id) {
1095
+ return this.elements.get(id);
1096
+ }
1097
+ getElementsByType(type) {
1098
+ return this.getAll().filter(
1099
+ (el) => el.type === type
1100
+ );
1101
+ }
1102
+ add(element) {
1103
+ this.elements.set(element.id, element);
1104
+ const bounds = getElementBounds(element);
1105
+ if (bounds) this.spatialIndex.insert(element.id, bounds);
1106
+ this.bus.emit("add", element);
1107
+ }
1108
+ update(id, partial) {
1109
+ const existing = this.elements.get(id);
1110
+ if (!existing) return;
1111
+ const updated = { ...existing, ...partial, id: existing.id, type: existing.type };
1112
+ if (updated.type === "arrow") {
1113
+ const arrow = updated;
1114
+ arrow.cachedControlPoint = getArrowControlPoint(arrow.from, arrow.to, arrow.bend);
1115
+ }
1116
+ this.elements.set(id, updated);
1117
+ const newBounds = getElementBounds(updated);
1118
+ if (newBounds) {
1119
+ this.spatialIndex.update(id, newBounds);
1120
+ }
1121
+ this.bus.emit("update", { previous: existing, current: updated });
1122
+ }
1123
+ remove(id) {
1124
+ const element = this.elements.get(id);
1125
+ if (!element) return;
1126
+ this.elements.delete(id);
1127
+ this.spatialIndex.remove(id);
1128
+ this.bus.emit("remove", element);
1129
+ }
1130
+ clear() {
1131
+ this.elements.clear();
1132
+ this.spatialIndex.clear();
1133
+ this.bus.emit("clear", null);
1134
+ }
1135
+ snapshot() {
1136
+ return this.getAll().map((el) => ({ ...el }));
1137
+ }
1138
+ loadSnapshot(elements) {
1139
+ this.elements.clear();
1140
+ this.spatialIndex.clear();
1141
+ for (const el of elements) {
1142
+ this.elements.set(el.id, el);
1143
+ const bounds = getElementBounds(el);
1144
+ if (bounds) this.spatialIndex.insert(el.id, bounds);
1145
+ }
1146
+ }
1147
+ queryRect(rect) {
1148
+ const ids = this.spatialIndex.query(rect);
1149
+ const elements = [];
1150
+ for (const id of ids) {
1151
+ const el = this.elements.get(id);
1152
+ if (el) elements.push(el);
1153
+ }
1154
+ return elements.sort((a, b) => {
1155
+ const layerA = this.layerOrderMap.get(a.layerId) ?? 0;
1156
+ const layerB = this.layerOrderMap.get(b.layerId) ?? 0;
1157
+ if (layerA !== layerB) return layerA - layerB;
1158
+ return a.zIndex - b.zIndex;
1159
+ });
1160
+ }
1161
+ queryPoint(point) {
1162
+ return this.queryRect({ x: point.x, y: point.y, w: 0, h: 0 });
1163
+ }
1164
+ on(event, listener) {
1165
+ return this.bus.on(event, listener);
1166
+ }
1167
+ onChange(listener) {
1168
+ const unsubs = [
1169
+ this.bus.on("add", listener),
1170
+ this.bus.on("remove", listener),
1171
+ this.bus.on("update", listener),
1172
+ this.bus.on("clear", listener)
1173
+ ];
1174
+ return () => unsubs.forEach((fn) => fn());
1175
+ }
1176
+ };
1177
+
838
1178
  // src/elements/arrow-binding.ts
839
1179
  var BINDABLE_TYPES = /* @__PURE__ */ new Set(["note", "text", "image", "html", "shape"]);
840
1180
  function isBindable(element) {
@@ -849,15 +1189,6 @@ function getElementCenter(element) {
849
1189
  y: element.position.y + element.size.h / 2
850
1190
  };
851
1191
  }
852
- function getElementBounds(element) {
853
- if (!("size" in element)) return null;
854
- return {
855
- x: element.position.x,
856
- y: element.position.y,
857
- w: element.size.w,
858
- h: element.size.h
859
- };
860
- }
861
1192
  function getEdgeIntersection(bounds, outsidePoint) {
862
1193
  const cx = bounds.x + bounds.w / 2;
863
1194
  const cy = bounds.y + bounds.h / 2;
@@ -1037,6 +1368,25 @@ function smoothToSegments(points) {
1037
1368
  return segments;
1038
1369
  }
1039
1370
 
1371
+ // src/elements/stroke-cache.ts
1372
+ var cache = /* @__PURE__ */ new WeakMap();
1373
+ function computeStrokeSegments(stroke) {
1374
+ const segments = smoothToSegments(stroke.points);
1375
+ const widths = [];
1376
+ for (const seg of segments) {
1377
+ const w = (pressureToWidth(seg.start.pressure, stroke.width) + pressureToWidth(seg.end.pressure, stroke.width)) / 2;
1378
+ widths.push(w);
1379
+ }
1380
+ const data = { segments, widths };
1381
+ cache.set(stroke, data);
1382
+ return data;
1383
+ }
1384
+ function getStrokeRenderData(stroke) {
1385
+ const cached = cache.get(stroke);
1386
+ if (cached) return cached;
1387
+ return computeStrokeSegments(stroke);
1388
+ }
1389
+
1040
1390
  // src/elements/grid-renderer.ts
1041
1391
  function getSquareGridLines(bounds, cellSize) {
1042
1392
  if (cellSize <= 0) return { verticals: [], horizontals: [] };
@@ -1201,9 +1551,11 @@ var ElementRenderer = class {
1201
1551
  ctx.lineCap = "round";
1202
1552
  ctx.lineJoin = "round";
1203
1553
  ctx.globalAlpha = stroke.opacity;
1204
- const segments = smoothToSegments(stroke.points);
1205
- for (const seg of segments) {
1206
- const w = (pressureToWidth(seg.start.pressure, stroke.width) + pressureToWidth(seg.end.pressure, stroke.width)) / 2;
1554
+ const { segments, widths } = getStrokeRenderData(stroke);
1555
+ for (let i = 0; i < segments.length; i++) {
1556
+ const seg = segments[i];
1557
+ const w = widths[i];
1558
+ if (!seg || w === void 0) continue;
1207
1559
  ctx.lineWidth = w;
1208
1560
  ctx.beginPath();
1209
1561
  ctx.moveTo(seg.start.x, seg.start.y);
@@ -1224,7 +1576,7 @@ var ElementRenderer = class {
1224
1576
  ctx.beginPath();
1225
1577
  ctx.moveTo(visualFrom.x, visualFrom.y);
1226
1578
  if (arrow.bend !== 0) {
1227
- const cp = getArrowControlPoint(arrow.from, arrow.to, arrow.bend);
1579
+ const cp = arrow.cachedControlPoint ?? getArrowControlPoint(arrow.from, arrow.to, arrow.bend);
1228
1580
  ctx.quadraticCurveTo(cp.x, cp.y, visualTo.x, visualTo.y);
1229
1581
  } else {
1230
1582
  ctx.lineTo(visualTo.x, visualTo.y);
@@ -1365,15 +1717,33 @@ var ElementRenderer = class {
1365
1717
  renderImage(ctx, image) {
1366
1718
  const img = this.getImage(image.src);
1367
1719
  if (!img) return;
1368
- ctx.drawImage(img, image.position.x, image.position.y, image.size.w, image.size.h);
1720
+ ctx.drawImage(
1721
+ img,
1722
+ image.position.x,
1723
+ image.position.y,
1724
+ image.size.w,
1725
+ image.size.h
1726
+ );
1369
1727
  }
1370
1728
  getImage(src) {
1371
1729
  const cached = this.imageCache.get(src);
1372
- if (cached) return cached.complete ? cached : null;
1730
+ if (cached) {
1731
+ if (cached instanceof HTMLImageElement) return cached.complete ? cached : null;
1732
+ return cached;
1733
+ }
1373
1734
  const img = new Image();
1374
1735
  img.src = src;
1375
1736
  this.imageCache.set(src, img);
1376
- img.onload = () => this.onImageLoad?.();
1737
+ img.onload = () => {
1738
+ this.onImageLoad?.();
1739
+ if (typeof createImageBitmap !== "undefined") {
1740
+ createImageBitmap(img).then((bitmap) => {
1741
+ this.imageCache.set(src, bitmap);
1742
+ this.onImageLoad?.();
1743
+ }).catch(() => {
1744
+ });
1745
+ }
1746
+ };
1377
1747
  return null;
1378
1748
  }
1379
1749
  };
@@ -1749,6 +2119,7 @@ function createNote(input) {
1749
2119
  };
1750
2120
  }
1751
2121
  function createArrow(input) {
2122
+ const bend = input.bend ?? 0;
1752
2123
  const result = {
1753
2124
  id: createId("arrow"),
1754
2125
  type: "arrow",
@@ -1758,9 +2129,10 @@ function createArrow(input) {
1758
2129
  layerId: input.layerId ?? "",
1759
2130
  from: input.from,
1760
2131
  to: input.to,
1761
- bend: input.bend ?? 0,
2132
+ bend,
1762
2133
  color: input.color ?? "#000000",
1763
- width: input.width ?? 2
2134
+ width: input.width ?? 2,
2135
+ cachedControlPoint: getArrowControlPoint(input.from, input.to, bend)
1764
2136
  };
1765
2137
  if (input.fromBinding) result.fromBinding = input.fromBinding;
1766
2138
  if (input.toBinding) result.toBinding = input.toBinding;
@@ -2011,19 +2383,19 @@ function loadImages(elements) {
2011
2383
  const imageElements = elements.filter(
2012
2384
  (el) => el.type === "image" && "src" in el
2013
2385
  );
2014
- const cache = /* @__PURE__ */ new Map();
2015
- if (imageElements.length === 0) return Promise.resolve(cache);
2386
+ const cache2 = /* @__PURE__ */ new Map();
2387
+ if (imageElements.length === 0) return Promise.resolve(cache2);
2016
2388
  return new Promise((resolve) => {
2017
2389
  let remaining = imageElements.length;
2018
2390
  const done = () => {
2019
2391
  remaining--;
2020
- if (remaining <= 0) resolve(cache);
2392
+ if (remaining <= 0) resolve(cache2);
2021
2393
  };
2022
2394
  for (const el of imageElements) {
2023
2395
  const img = new Image();
2024
2396
  img.crossOrigin = "anonymous";
2025
2397
  img.onload = () => {
2026
- cache.set(el.id, img);
2398
+ cache2.set(el.id, img);
2027
2399
  done();
2028
2400
  };
2029
2401
  img.onerror = done;
@@ -2448,6 +2820,41 @@ var DomNodeManager = class {
2448
2820
  }
2449
2821
  };
2450
2822
 
2823
+ // src/canvas/render-stats.ts
2824
+ var SAMPLE_SIZE = 60;
2825
+ var RenderStats = class {
2826
+ frameTimes = [];
2827
+ frameCount = 0;
2828
+ recordFrame(durationMs) {
2829
+ this.frameCount++;
2830
+ this.frameTimes.push(durationMs);
2831
+ if (this.frameTimes.length > SAMPLE_SIZE) {
2832
+ this.frameTimes.shift();
2833
+ }
2834
+ }
2835
+ getSnapshot() {
2836
+ const times = this.frameTimes;
2837
+ if (times.length === 0) {
2838
+ return { fps: 0, avgFrameMs: 0, p95FrameMs: 0, lastFrameMs: 0, frameCount: 0 };
2839
+ }
2840
+ const avg = times.reduce((a, b) => a + b, 0) / times.length;
2841
+ const sorted = [...times].sort((a, b) => a - b);
2842
+ const p95Index = Math.min(Math.floor(sorted.length * 0.95), sorted.length - 1);
2843
+ const lastFrame = times[times.length - 1] ?? 0;
2844
+ return {
2845
+ fps: avg > 0 ? Math.round(1e3 / avg) : 0,
2846
+ avgFrameMs: Math.round(avg * 100) / 100,
2847
+ p95FrameMs: Math.round((sorted[p95Index] ?? 0) * 100) / 100,
2848
+ lastFrameMs: Math.round(lastFrame * 100) / 100,
2849
+ frameCount: this.frameCount
2850
+ };
2851
+ }
2852
+ reset() {
2853
+ this.frameTimes = [];
2854
+ this.frameCount = 0;
2855
+ }
2856
+ };
2857
+
2451
2858
  // src/canvas/render-loop.ts
2452
2859
  var RenderLoop = class {
2453
2860
  needsRender = false;
@@ -2460,6 +2867,12 @@ var RenderLoop = class {
2460
2867
  toolManager;
2461
2868
  layerManager;
2462
2869
  domNodeManager;
2870
+ layerCache;
2871
+ activeDrawingLayerId = null;
2872
+ lastZoom;
2873
+ lastCamX;
2874
+ lastCamY;
2875
+ stats = new RenderStats();
2463
2876
  constructor(deps) {
2464
2877
  this.canvasEl = deps.canvasEl;
2465
2878
  this.camera = deps.camera;
@@ -2469,6 +2882,10 @@ var RenderLoop = class {
2469
2882
  this.toolManager = deps.toolManager;
2470
2883
  this.layerManager = deps.layerManager;
2471
2884
  this.domNodeManager = deps.domNodeManager;
2885
+ this.layerCache = deps.layerCache;
2886
+ this.lastZoom = deps.camera.zoom;
2887
+ this.lastCamX = deps.camera.position.x;
2888
+ this.lastCamY = deps.camera.position.y;
2472
2889
  }
2473
2890
  requestRender() {
2474
2891
  this.needsRender = true;
@@ -2495,19 +2912,63 @@ var RenderLoop = class {
2495
2912
  setCanvasSize(width, height) {
2496
2913
  this.canvasEl.width = width;
2497
2914
  this.canvasEl.height = height;
2915
+ this.layerCache.resize(this.canvasEl.clientWidth, this.canvasEl.clientHeight);
2916
+ }
2917
+ setActiveDrawingLayer(layerId) {
2918
+ this.activeDrawingLayerId = layerId;
2919
+ }
2920
+ markLayerDirty(layerId) {
2921
+ this.layerCache.markDirty(layerId);
2922
+ }
2923
+ markAllLayersDirty() {
2924
+ this.layerCache.markAllDirty();
2925
+ }
2926
+ getStats() {
2927
+ return this.stats.getSnapshot();
2928
+ }
2929
+ compositeLayerCache(ctx, layerId, dpr) {
2930
+ const cached = this.layerCache.getCanvas(layerId);
2931
+ ctx.save();
2932
+ ctx.scale(1 / this.camera.zoom, 1 / this.camera.zoom);
2933
+ ctx.translate(-this.camera.position.x, -this.camera.position.y);
2934
+ ctx.scale(1 / dpr, 1 / dpr);
2935
+ ctx.drawImage(cached, 0, 0);
2936
+ ctx.restore();
2498
2937
  }
2499
2938
  render() {
2939
+ const t0 = performance.now();
2500
2940
  const ctx = this.canvasEl.getContext("2d");
2501
2941
  if (!ctx) return;
2502
2942
  const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
2943
+ const cssWidth = this.canvasEl.clientWidth;
2944
+ const cssHeight = this.canvasEl.clientHeight;
2945
+ const currentZoom = this.camera.zoom;
2946
+ const currentCamX = this.camera.position.x;
2947
+ const currentCamY = this.camera.position.y;
2948
+ if (currentZoom !== this.lastZoom || currentCamX !== this.lastCamX || currentCamY !== this.lastCamY) {
2949
+ this.layerCache.markAllDirty();
2950
+ this.lastZoom = currentZoom;
2951
+ this.lastCamX = currentCamX;
2952
+ this.lastCamY = currentCamY;
2953
+ }
2503
2954
  ctx.save();
2504
2955
  ctx.scale(dpr, dpr);
2505
- this.renderer.setCanvasSize(this.canvasEl.clientWidth, this.canvasEl.clientHeight);
2956
+ this.renderer.setCanvasSize(cssWidth, cssHeight);
2506
2957
  this.background.render(ctx, this.camera);
2507
2958
  ctx.save();
2508
2959
  ctx.translate(this.camera.position.x, this.camera.position.y);
2509
2960
  ctx.scale(this.camera.zoom, this.camera.zoom);
2961
+ const visibleRect = this.camera.getVisibleRect(cssWidth, cssHeight);
2962
+ const margin = Math.max(visibleRect.w, visibleRect.h) * 0.1;
2963
+ const cullingRect = {
2964
+ x: visibleRect.x - margin,
2965
+ y: visibleRect.y - margin,
2966
+ w: visibleRect.w + margin * 2,
2967
+ h: visibleRect.h + margin * 2
2968
+ };
2510
2969
  const allElements = this.store.getAll();
2970
+ const layerElements = /* @__PURE__ */ new Map();
2971
+ const gridElements = [];
2511
2972
  let domZIndex = 0;
2512
2973
  for (const element of allElements) {
2513
2974
  if (!this.layerManager.isLayerVisible(element.layerId)) {
@@ -2517,9 +2978,54 @@ var RenderLoop = class {
2517
2978
  continue;
2518
2979
  }
2519
2980
  if (this.renderer.isDomElement(element)) {
2520
- this.domNodeManager.syncDomNode(element, domZIndex++);
2521
- } else {
2522
- this.renderer.renderCanvasElement(ctx, element);
2981
+ const elBounds = getElementBounds(element);
2982
+ if (elBounds && !boundsIntersect(elBounds, cullingRect)) {
2983
+ this.domNodeManager.hideDomNode(element.id);
2984
+ } else {
2985
+ this.domNodeManager.syncDomNode(element, domZIndex++);
2986
+ }
2987
+ continue;
2988
+ }
2989
+ if (element.type === "grid") {
2990
+ gridElements.push(element);
2991
+ continue;
2992
+ }
2993
+ let group = layerElements.get(element.layerId);
2994
+ if (!group) {
2995
+ group = [];
2996
+ layerElements.set(element.layerId, group);
2997
+ }
2998
+ group.push(element);
2999
+ }
3000
+ for (const grid of gridElements) {
3001
+ this.renderer.renderCanvasElement(ctx, grid);
3002
+ }
3003
+ for (const [layerId, elements] of layerElements) {
3004
+ const isActiveDrawingLayer = layerId === this.activeDrawingLayerId;
3005
+ if (!this.layerCache.isDirty(layerId)) {
3006
+ this.compositeLayerCache(ctx, layerId, dpr);
3007
+ continue;
3008
+ }
3009
+ if (isActiveDrawingLayer) {
3010
+ this.compositeLayerCache(ctx, layerId, dpr);
3011
+ continue;
3012
+ }
3013
+ const offCtx = this.layerCache.getContext(layerId);
3014
+ if (offCtx) {
3015
+ const offCanvas = this.layerCache.getCanvas(layerId);
3016
+ offCtx.clearRect(0, 0, offCanvas.width, offCanvas.height);
3017
+ offCtx.save();
3018
+ offCtx.scale(dpr, dpr);
3019
+ offCtx.translate(this.camera.position.x, this.camera.position.y);
3020
+ offCtx.scale(this.camera.zoom, this.camera.zoom);
3021
+ for (const element of elements) {
3022
+ const elBounds = getElementBounds(element);
3023
+ if (elBounds && !boundsIntersect(elBounds, cullingRect)) continue;
3024
+ this.renderer.renderCanvasElement(offCtx, element);
3025
+ }
3026
+ offCtx.restore();
3027
+ this.layerCache.markClean(layerId);
3028
+ this.compositeLayerCache(ctx, layerId, dpr);
2523
3029
  }
2524
3030
  }
2525
3031
  const activeTool = this.toolManager.activeTool;
@@ -2528,6 +3034,70 @@ var RenderLoop = class {
2528
3034
  }
2529
3035
  ctx.restore();
2530
3036
  ctx.restore();
3037
+ this.stats.recordFrame(performance.now() - t0);
3038
+ }
3039
+ };
3040
+
3041
+ // src/canvas/layer-cache.ts
3042
+ function createOffscreenCanvas(width, height) {
3043
+ if (typeof OffscreenCanvas !== "undefined") {
3044
+ return new OffscreenCanvas(width, height);
3045
+ }
3046
+ const canvas = document.createElement("canvas");
3047
+ canvas.width = width;
3048
+ canvas.height = height;
3049
+ return canvas;
3050
+ }
3051
+ var LayerCache = class {
3052
+ canvases = /* @__PURE__ */ new Map();
3053
+ dirtyFlags = /* @__PURE__ */ new Map();
3054
+ width;
3055
+ height;
3056
+ constructor(width, height) {
3057
+ const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
3058
+ this.width = Math.round(width * dpr);
3059
+ this.height = Math.round(height * dpr);
3060
+ }
3061
+ isDirty(layerId) {
3062
+ return this.dirtyFlags.get(layerId) !== false;
3063
+ }
3064
+ markDirty(layerId) {
3065
+ this.dirtyFlags.set(layerId, true);
3066
+ }
3067
+ markClean(layerId) {
3068
+ this.dirtyFlags.set(layerId, false);
3069
+ }
3070
+ markAllDirty() {
3071
+ for (const [id] of this.dirtyFlags) {
3072
+ this.dirtyFlags.set(id, true);
3073
+ }
3074
+ }
3075
+ getCanvas(layerId) {
3076
+ let canvas = this.canvases.get(layerId);
3077
+ if (!canvas) {
3078
+ canvas = createOffscreenCanvas(this.width, this.height);
3079
+ this.canvases.set(layerId, canvas);
3080
+ this.dirtyFlags.set(layerId, true);
3081
+ }
3082
+ return canvas;
3083
+ }
3084
+ getContext(layerId) {
3085
+ const canvas = this.getCanvas(layerId);
3086
+ return canvas.getContext("2d");
3087
+ }
3088
+ resize(width, height) {
3089
+ const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
3090
+ this.width = Math.round(width * dpr);
3091
+ this.height = Math.round(height * dpr);
3092
+ for (const [id, canvas] of this.canvases) {
3093
+ canvas.width = this.width;
3094
+ canvas.height = this.height;
3095
+ this.dirtyFlags.set(id, true);
3096
+ }
3097
+ }
3098
+ clear() {
3099
+ this.canvases.clear();
3100
+ this.dirtyFlags.clear();
2531
3101
  }
2532
3102
  };
2533
3103
 
@@ -2584,6 +3154,10 @@ var Viewport = class {
2584
3154
  this.interactMode = new InteractMode({
2585
3155
  getNode: (id) => this.domNodeManager.getNode(id)
2586
3156
  });
3157
+ const layerCache = new LayerCache(
3158
+ this.canvasEl.clientWidth || 800,
3159
+ this.canvasEl.clientHeight || 600
3160
+ );
2587
3161
  this.renderLoop = new RenderLoop({
2588
3162
  canvasEl: this.canvasEl,
2589
3163
  camera: this.camera,
@@ -2592,22 +3166,34 @@ var Viewport = class {
2592
3166
  renderer: this.renderer,
2593
3167
  toolManager: this.toolManager,
2594
3168
  layerManager: this.layerManager,
2595
- domNodeManager: this.domNodeManager
3169
+ domNodeManager: this.domNodeManager,
3170
+ layerCache
2596
3171
  });
2597
3172
  this.unsubCamera = this.camera.onChange(() => {
2598
3173
  this.applyCameraTransform();
2599
3174
  this.requestRender();
2600
3175
  });
2601
3176
  this.unsubStore = [
2602
- this.store.on("add", () => this.requestRender()),
3177
+ this.store.on("add", (el) => {
3178
+ this.renderLoop.markLayerDirty(el.layerId);
3179
+ this.requestRender();
3180
+ }),
2603
3181
  this.store.on("remove", (el) => {
2604
3182
  this.unbindArrowsFrom(el);
2605
3183
  this.domNodeManager.removeDomNode(el.id);
3184
+ this.renderLoop.markLayerDirty(el.layerId);
3185
+ this.requestRender();
3186
+ }),
3187
+ this.store.on("update", ({ previous, current }) => {
3188
+ this.renderLoop.markLayerDirty(current.layerId);
3189
+ if (previous.layerId !== current.layerId) {
3190
+ this.renderLoop.markLayerDirty(previous.layerId);
3191
+ }
2606
3192
  this.requestRender();
2607
3193
  }),
2608
- this.store.on("update", () => this.requestRender()),
2609
3194
  this.store.on("clear", () => {
2610
3195
  this.domNodeManager.clearDomNodes();
3196
+ this.renderLoop.markAllLayersDirty();
2611
3197
  this.requestRender();
2612
3198
  })
2613
3199
  ];
@@ -2813,8 +3399,8 @@ var Viewport = class {
2813
3399
  }
2814
3400
  };
2815
3401
  hitTestWorld(world) {
2816
- const elements = this.store.getAll().reverse();
2817
- for (const el of elements) {
3402
+ const candidates = this.store.queryPoint(world).reverse();
3403
+ for (const el of candidates) {
2818
3404
  if (!("size" in el)) continue;
2819
3405
  const { x, y } = el.position;
2820
3406
  const { w, h } = el.size;
@@ -2965,6 +3551,9 @@ var HandTool = class {
2965
3551
  var MIN_POINTS_FOR_STROKE = 2;
2966
3552
  var DEFAULT_SMOOTHING = 1.5;
2967
3553
  var DEFAULT_PRESSURE = 0.5;
3554
+ var DEFAULT_MIN_POINT_DISTANCE = 3;
3555
+ var DEFAULT_PROGRESSIVE_THRESHOLD = 200;
3556
+ var PROGRESSIVE_HOT_ZONE = 30;
2968
3557
  var PencilTool = class {
2969
3558
  name = "pencil";
2970
3559
  drawing = false;
@@ -2972,11 +3561,17 @@ var PencilTool = class {
2972
3561
  color;
2973
3562
  width;
2974
3563
  smoothing;
3564
+ minPointDistance;
3565
+ progressiveThreshold;
3566
+ nextSimplifyAt;
2975
3567
  optionListeners = /* @__PURE__ */ new Set();
2976
3568
  constructor(options = {}) {
2977
3569
  this.color = options.color ?? "#000000";
2978
3570
  this.width = options.width ?? 2;
2979
3571
  this.smoothing = options.smoothing ?? DEFAULT_SMOOTHING;
3572
+ this.minPointDistance = options.minPointDistance ?? DEFAULT_MIN_POINT_DISTANCE;
3573
+ this.progressiveThreshold = options.progressiveSimplifyThreshold ?? DEFAULT_PROGRESSIVE_THRESHOLD;
3574
+ this.nextSimplifyAt = this.progressiveThreshold;
2980
3575
  }
2981
3576
  onActivate(ctx) {
2982
3577
  ctx.setCursor?.("crosshair");
@@ -2985,7 +3580,13 @@ var PencilTool = class {
2985
3580
  ctx.setCursor?.("default");
2986
3581
  }
2987
3582
  getOptions() {
2988
- return { color: this.color, width: this.width, smoothing: this.smoothing };
3583
+ return {
3584
+ color: this.color,
3585
+ width: this.width,
3586
+ smoothing: this.smoothing,
3587
+ minPointDistance: this.minPointDistance,
3588
+ progressiveSimplifyThreshold: this.progressiveThreshold
3589
+ };
2989
3590
  }
2990
3591
  onOptionsChange(listener) {
2991
3592
  this.optionListeners.add(listener);
@@ -2995,6 +3596,9 @@ var PencilTool = class {
2995
3596
  if (options.color !== void 0) this.color = options.color;
2996
3597
  if (options.width !== void 0) this.width = options.width;
2997
3598
  if (options.smoothing !== void 0) this.smoothing = options.smoothing;
3599
+ if (options.minPointDistance !== void 0) this.minPointDistance = options.minPointDistance;
3600
+ if (options.progressiveSimplifyThreshold !== void 0)
3601
+ this.progressiveThreshold = options.progressiveSimplifyThreshold;
2998
3602
  this.notifyOptionsChange();
2999
3603
  }
3000
3604
  onPointerDown(state, ctx) {
@@ -3002,12 +3606,26 @@ var PencilTool = class {
3002
3606
  const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
3003
3607
  const pressure = state.pressure === 0 ? DEFAULT_PRESSURE : state.pressure;
3004
3608
  this.points = [{ x: world.x, y: world.y, pressure }];
3609
+ this.nextSimplifyAt = this.progressiveThreshold;
3005
3610
  }
3006
3611
  onPointerMove(state, ctx) {
3007
3612
  if (!this.drawing) return;
3008
3613
  const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
3009
3614
  const pressure = state.pressure === 0 ? DEFAULT_PRESSURE : state.pressure;
3615
+ const last = this.points[this.points.length - 1];
3616
+ if (last) {
3617
+ const dx = world.x - last.x;
3618
+ const dy = world.y - last.y;
3619
+ if (dx * dx + dy * dy < this.minPointDistance * this.minPointDistance) return;
3620
+ }
3010
3621
  this.points.push({ x: world.x, y: world.y, pressure });
3622
+ if (this.points.length > this.nextSimplifyAt) {
3623
+ const hotZone = this.points.slice(-PROGRESSIVE_HOT_ZONE);
3624
+ const coldZone = this.points.slice(0, -PROGRESSIVE_HOT_ZONE);
3625
+ const simplified = simplifyPoints(coldZone, this.smoothing * 2);
3626
+ this.points = [...simplified, ...hotZone];
3627
+ this.nextSimplifyAt = this.points.length + this.progressiveThreshold;
3628
+ }
3011
3629
  ctx.requestRender();
3012
3630
  }
3013
3631
  onPointerUp(_state, ctx) {
@@ -3025,6 +3643,7 @@ var PencilTool = class {
3025
3643
  layerId: ctx.activeLayerId ?? ""
3026
3644
  });
3027
3645
  ctx.store.add(stroke);
3646
+ computeStrokeSegments(stroke);
3028
3647
  this.points = [];
3029
3648
  ctx.requestRender();
3030
3649
  }
@@ -3089,13 +3708,20 @@ var EraserTool = class {
3089
3708
  }
3090
3709
  eraseAt(state, ctx) {
3091
3710
  const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
3092
- const strokes = ctx.store.getElementsByType("stroke");
3711
+ const queryBounds = {
3712
+ x: world.x - this.radius,
3713
+ y: world.y - this.radius,
3714
+ w: this.radius * 2,
3715
+ h: this.radius * 2
3716
+ };
3717
+ const candidates = ctx.store.queryRect(queryBounds);
3093
3718
  let erased = false;
3094
- for (const stroke of strokes) {
3095
- if (ctx.isLayerVisible && !ctx.isLayerVisible(stroke.layerId)) continue;
3096
- if (ctx.isLayerLocked && ctx.isLayerLocked(stroke.layerId)) continue;
3097
- if (this.strokeIntersects(stroke, world)) {
3098
- ctx.store.remove(stroke.id);
3719
+ for (const el of candidates) {
3720
+ if (el.type !== "stroke") continue;
3721
+ if (ctx.isLayerVisible && !ctx.isLayerVisible(el.layerId)) continue;
3722
+ if (ctx.isLayerLocked && ctx.isLayerLocked(el.layerId)) continue;
3723
+ if (this.strokeIntersects(el, world)) {
3724
+ ctx.store.remove(el.id);
3099
3725
  erased = true;
3100
3726
  }
3101
3727
  }
@@ -3472,7 +4098,7 @@ var SelectTool = class {
3472
4098
  for (const id of this._selectedIds) {
3473
4099
  const el = ctx.store.getById(id);
3474
4100
  if (!el || !("size" in el)) continue;
3475
- const bounds = this.getElementBounds(el);
4101
+ const bounds = getElementBounds(el);
3476
4102
  if (!bounds) continue;
3477
4103
  const corners = this.getHandlePositions(bounds);
3478
4104
  for (const [handle, pos] of corners) {
@@ -3520,7 +4146,7 @@ var SelectTool = class {
3520
4146
  this.renderBindingHighlights(canvasCtx, el, zoom);
3521
4147
  continue;
3522
4148
  }
3523
- const bounds = this.getElementBounds(el);
4149
+ const bounds = getElementBounds(el);
3524
4150
  if (!bounds) continue;
3525
4151
  const pad = SELECTION_PAD / zoom;
3526
4152
  canvasCtx.strokeRect(bounds.x - pad, bounds.y - pad, bounds.w + pad * 2, bounds.h + pad * 2);
@@ -3579,12 +4205,13 @@ var SelectTool = class {
3579
4205
  return { x, y, w, h };
3580
4206
  }
3581
4207
  findElementsInRect(marquee, ctx) {
4208
+ const candidates = ctx.store.queryRect(marquee);
3582
4209
  const ids = [];
3583
- for (const el of ctx.store.getAll()) {
4210
+ for (const el of candidates) {
3584
4211
  if (ctx.isLayerVisible && !ctx.isLayerVisible(el.layerId)) continue;
3585
4212
  if (ctx.isLayerLocked && ctx.isLayerLocked(el.layerId)) continue;
3586
4213
  if (el.type === "grid") continue;
3587
- const bounds = this.getElementBounds(el);
4214
+ const bounds = getElementBounds(el);
3588
4215
  if (bounds && this.rectsOverlap(marquee, bounds)) {
3589
4216
  ids.push(el.id);
3590
4217
  }
@@ -3594,30 +4221,10 @@ var SelectTool = class {
3594
4221
  rectsOverlap(a, b) {
3595
4222
  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;
3596
4223
  }
3597
- getElementBounds(el) {
3598
- if ("size" in el) {
3599
- return { x: el.position.x, y: el.position.y, w: el.size.w, h: el.size.h };
3600
- }
3601
- if (el.type === "stroke" && el.points.length > 0) {
3602
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
3603
- for (const p of el.points) {
3604
- const px = p.x + el.position.x;
3605
- const py = p.y + el.position.y;
3606
- if (px < minX) minX = px;
3607
- if (py < minY) minY = py;
3608
- if (px > maxX) maxX = px;
3609
- if (py > maxY) maxY = py;
3610
- }
3611
- return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
3612
- }
3613
- if (el.type === "arrow") {
3614
- return getArrowBounds(el.from, el.to, el.bend);
3615
- }
3616
- return null;
3617
- }
3618
4224
  hitTest(world, ctx) {
3619
- const elements = ctx.store.getAll().reverse();
3620
- for (const el of elements) {
4225
+ const r = 10;
4226
+ const candidates = ctx.store.queryRect({ x: world.x - r, y: world.y - r, w: r * 2, h: r * 2 }).reverse();
4227
+ for (const el of candidates) {
3621
4228
  if (ctx.isLayerVisible && !ctx.isLayerVisible(el.layerId)) continue;
3622
4229
  if (ctx.isLayerLocked && ctx.isLayerLocked(el.layerId)) continue;
3623
4230
  if (el.type === "grid") continue;
@@ -4111,7 +4718,7 @@ var UpdateLayerCommand = class {
4111
4718
  };
4112
4719
 
4113
4720
  // src/index.ts
4114
- var VERSION = "0.8.6";
4721
+ var VERSION = "0.8.8";
4115
4722
  // Annotate the CommonJS export names for ESM import in node:
4116
4723
  0 && (module.exports = {
4117
4724
  AddElementCommand,
@@ -4134,6 +4741,7 @@ var VERSION = "0.8.6";
4134
4741
  NoteEditor,
4135
4742
  NoteTool,
4136
4743
  PencilTool,
4744
+ Quadtree,
4137
4745
  RemoveElementCommand,
4138
4746
  RemoveLayerCommand,
4139
4747
  SelectTool,
@@ -4144,6 +4752,7 @@ var VERSION = "0.8.6";
4144
4752
  UpdateLayerCommand,
4145
4753
  VERSION,
4146
4754
  Viewport,
4755
+ boundsIntersect,
4147
4756
  clearStaleBindings,
4148
4757
  createArrow,
4149
4758
  createGrid,