@fieldnotes/core 0.8.6 → 0.8.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +630 -130
- package/dist/index.d.cts +46 -14
- package/dist/index.d.ts +46 -14
- package/dist/index.js +628 -130
- package/package.json +1 -1
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.
|
|
386
|
+
this.notifyPan();
|
|
240
387
|
}
|
|
241
388
|
moveTo(x, y) {
|
|
242
389
|
this.x = x;
|
|
243
390
|
this.y = y;
|
|
244
|
-
this.
|
|
391
|
+
this.notifyPan();
|
|
245
392
|
}
|
|
246
393
|
setZoom(level) {
|
|
247
394
|
this.z = Math.min(this.maxZoom, Math.max(this.minZoom, level));
|
|
248
|
-
this.
|
|
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.
|
|
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
|
-
|
|
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,77 +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
|
-
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
747
|
// src/elements/arrow-geometry.ts
|
|
656
748
|
function getArrowControlPoint(from, to, bend) {
|
|
657
749
|
const midX = (from.x + to.x) / 2;
|
|
@@ -752,6 +844,185 @@ function isNearLine(point, a, b, threshold) {
|
|
|
752
844
|
return Math.hypot(point.x - projX, point.y - projY) <= threshold;
|
|
753
845
|
}
|
|
754
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
|
+
|
|
755
1026
|
// src/elements/arrow-binding.ts
|
|
756
1027
|
var BINDABLE_TYPES = /* @__PURE__ */ new Set(["note", "text", "image", "html", "shape"]);
|
|
757
1028
|
function isBindable(element) {
|
|
@@ -766,15 +1037,6 @@ function getElementCenter(element) {
|
|
|
766
1037
|
y: element.position.y + element.size.h / 2
|
|
767
1038
|
};
|
|
768
1039
|
}
|
|
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
1040
|
function getEdgeIntersection(bounds, outsidePoint) {
|
|
779
1041
|
const cx = bounds.x + bounds.w / 2;
|
|
780
1042
|
const cy = bounds.y + bounds.h / 2;
|
|
@@ -2365,6 +2627,41 @@ var DomNodeManager = class {
|
|
|
2365
2627
|
}
|
|
2366
2628
|
};
|
|
2367
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
|
+
|
|
2368
2665
|
// src/canvas/render-loop.ts
|
|
2369
2666
|
var RenderLoop = class {
|
|
2370
2667
|
needsRender = false;
|
|
@@ -2377,6 +2674,12 @@ var RenderLoop = class {
|
|
|
2377
2674
|
toolManager;
|
|
2378
2675
|
layerManager;
|
|
2379
2676
|
domNodeManager;
|
|
2677
|
+
layerCache;
|
|
2678
|
+
activeDrawingLayerId = null;
|
|
2679
|
+
lastZoom;
|
|
2680
|
+
lastCamX;
|
|
2681
|
+
lastCamY;
|
|
2682
|
+
stats = new RenderStats();
|
|
2380
2683
|
constructor(deps) {
|
|
2381
2684
|
this.canvasEl = deps.canvasEl;
|
|
2382
2685
|
this.camera = deps.camera;
|
|
@@ -2386,6 +2689,10 @@ var RenderLoop = class {
|
|
|
2386
2689
|
this.toolManager = deps.toolManager;
|
|
2387
2690
|
this.layerManager = deps.layerManager;
|
|
2388
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;
|
|
2389
2696
|
}
|
|
2390
2697
|
requestRender() {
|
|
2391
2698
|
this.needsRender = true;
|
|
@@ -2412,19 +2719,63 @@ var RenderLoop = class {
|
|
|
2412
2719
|
setCanvasSize(width, height) {
|
|
2413
2720
|
this.canvasEl.width = width;
|
|
2414
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();
|
|
2415
2744
|
}
|
|
2416
2745
|
render() {
|
|
2746
|
+
const t0 = performance.now();
|
|
2417
2747
|
const ctx = this.canvasEl.getContext("2d");
|
|
2418
2748
|
if (!ctx) return;
|
|
2419
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
|
+
}
|
|
2420
2761
|
ctx.save();
|
|
2421
2762
|
ctx.scale(dpr, dpr);
|
|
2422
|
-
this.renderer.setCanvasSize(
|
|
2763
|
+
this.renderer.setCanvasSize(cssWidth, cssHeight);
|
|
2423
2764
|
this.background.render(ctx, this.camera);
|
|
2424
2765
|
ctx.save();
|
|
2425
2766
|
ctx.translate(this.camera.position.x, this.camera.position.y);
|
|
2426
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
|
+
};
|
|
2427
2776
|
const allElements = this.store.getAll();
|
|
2777
|
+
const layerElements = /* @__PURE__ */ new Map();
|
|
2778
|
+
const gridElements = [];
|
|
2428
2779
|
let domZIndex = 0;
|
|
2429
2780
|
for (const element of allElements) {
|
|
2430
2781
|
if (!this.layerManager.isLayerVisible(element.layerId)) {
|
|
@@ -2434,9 +2785,54 @@ var RenderLoop = class {
|
|
|
2434
2785
|
continue;
|
|
2435
2786
|
}
|
|
2436
2787
|
if (this.renderer.isDomElement(element)) {
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
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);
|
|
2440
2836
|
}
|
|
2441
2837
|
}
|
|
2442
2838
|
const activeTool = this.toolManager.activeTool;
|
|
@@ -2445,6 +2841,70 @@ var RenderLoop = class {
|
|
|
2445
2841
|
}
|
|
2446
2842
|
ctx.restore();
|
|
2447
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();
|
|
2448
2908
|
}
|
|
2449
2909
|
};
|
|
2450
2910
|
|
|
@@ -2501,6 +2961,10 @@ var Viewport = class {
|
|
|
2501
2961
|
this.interactMode = new InteractMode({
|
|
2502
2962
|
getNode: (id) => this.domNodeManager.getNode(id)
|
|
2503
2963
|
});
|
|
2964
|
+
const layerCache = new LayerCache(
|
|
2965
|
+
this.canvasEl.clientWidth || 800,
|
|
2966
|
+
this.canvasEl.clientHeight || 600
|
|
2967
|
+
);
|
|
2504
2968
|
this.renderLoop = new RenderLoop({
|
|
2505
2969
|
canvasEl: this.canvasEl,
|
|
2506
2970
|
camera: this.camera,
|
|
@@ -2509,22 +2973,34 @@ var Viewport = class {
|
|
|
2509
2973
|
renderer: this.renderer,
|
|
2510
2974
|
toolManager: this.toolManager,
|
|
2511
2975
|
layerManager: this.layerManager,
|
|
2512
|
-
domNodeManager: this.domNodeManager
|
|
2976
|
+
domNodeManager: this.domNodeManager,
|
|
2977
|
+
layerCache
|
|
2513
2978
|
});
|
|
2514
2979
|
this.unsubCamera = this.camera.onChange(() => {
|
|
2515
2980
|
this.applyCameraTransform();
|
|
2516
2981
|
this.requestRender();
|
|
2517
2982
|
});
|
|
2518
2983
|
this.unsubStore = [
|
|
2519
|
-
this.store.on("add", () =>
|
|
2984
|
+
this.store.on("add", (el) => {
|
|
2985
|
+
this.renderLoop.markLayerDirty(el.layerId);
|
|
2986
|
+
this.requestRender();
|
|
2987
|
+
}),
|
|
2520
2988
|
this.store.on("remove", (el) => {
|
|
2521
2989
|
this.unbindArrowsFrom(el);
|
|
2522
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
|
+
}
|
|
2523
2999
|
this.requestRender();
|
|
2524
3000
|
}),
|
|
2525
|
-
this.store.on("update", () => this.requestRender()),
|
|
2526
3001
|
this.store.on("clear", () => {
|
|
2527
3002
|
this.domNodeManager.clearDomNodes();
|
|
3003
|
+
this.renderLoop.markAllLayersDirty();
|
|
2528
3004
|
this.requestRender();
|
|
2529
3005
|
})
|
|
2530
3006
|
];
|
|
@@ -2730,8 +3206,8 @@ var Viewport = class {
|
|
|
2730
3206
|
}
|
|
2731
3207
|
};
|
|
2732
3208
|
hitTestWorld(world) {
|
|
2733
|
-
const
|
|
2734
|
-
for (const el of
|
|
3209
|
+
const candidates = this.store.queryPoint(world).reverse();
|
|
3210
|
+
for (const el of candidates) {
|
|
2735
3211
|
if (!("size" in el)) continue;
|
|
2736
3212
|
const { x, y } = el.position;
|
|
2737
3213
|
const { w, h } = el.size;
|
|
@@ -2882,6 +3358,9 @@ var HandTool = class {
|
|
|
2882
3358
|
var MIN_POINTS_FOR_STROKE = 2;
|
|
2883
3359
|
var DEFAULT_SMOOTHING = 1.5;
|
|
2884
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;
|
|
2885
3364
|
var PencilTool = class {
|
|
2886
3365
|
name = "pencil";
|
|
2887
3366
|
drawing = false;
|
|
@@ -2889,11 +3368,17 @@ var PencilTool = class {
|
|
|
2889
3368
|
color;
|
|
2890
3369
|
width;
|
|
2891
3370
|
smoothing;
|
|
3371
|
+
minPointDistance;
|
|
3372
|
+
progressiveThreshold;
|
|
3373
|
+
nextSimplifyAt;
|
|
2892
3374
|
optionListeners = /* @__PURE__ */ new Set();
|
|
2893
3375
|
constructor(options = {}) {
|
|
2894
3376
|
this.color = options.color ?? "#000000";
|
|
2895
3377
|
this.width = options.width ?? 2;
|
|
2896
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;
|
|
2897
3382
|
}
|
|
2898
3383
|
onActivate(ctx) {
|
|
2899
3384
|
ctx.setCursor?.("crosshair");
|
|
@@ -2902,7 +3387,13 @@ var PencilTool = class {
|
|
|
2902
3387
|
ctx.setCursor?.("default");
|
|
2903
3388
|
}
|
|
2904
3389
|
getOptions() {
|
|
2905
|
-
return {
|
|
3390
|
+
return {
|
|
3391
|
+
color: this.color,
|
|
3392
|
+
width: this.width,
|
|
3393
|
+
smoothing: this.smoothing,
|
|
3394
|
+
minPointDistance: this.minPointDistance,
|
|
3395
|
+
progressiveSimplifyThreshold: this.progressiveThreshold
|
|
3396
|
+
};
|
|
2906
3397
|
}
|
|
2907
3398
|
onOptionsChange(listener) {
|
|
2908
3399
|
this.optionListeners.add(listener);
|
|
@@ -2912,6 +3403,9 @@ var PencilTool = class {
|
|
|
2912
3403
|
if (options.color !== void 0) this.color = options.color;
|
|
2913
3404
|
if (options.width !== void 0) this.width = options.width;
|
|
2914
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;
|
|
2915
3409
|
this.notifyOptionsChange();
|
|
2916
3410
|
}
|
|
2917
3411
|
onPointerDown(state, ctx) {
|
|
@@ -2919,12 +3413,26 @@ var PencilTool = class {
|
|
|
2919
3413
|
const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
2920
3414
|
const pressure = state.pressure === 0 ? DEFAULT_PRESSURE : state.pressure;
|
|
2921
3415
|
this.points = [{ x: world.x, y: world.y, pressure }];
|
|
3416
|
+
this.nextSimplifyAt = this.progressiveThreshold;
|
|
2922
3417
|
}
|
|
2923
3418
|
onPointerMove(state, ctx) {
|
|
2924
3419
|
if (!this.drawing) return;
|
|
2925
3420
|
const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
2926
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
|
+
}
|
|
2927
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
|
+
}
|
|
2928
3436
|
ctx.requestRender();
|
|
2929
3437
|
}
|
|
2930
3438
|
onPointerUp(_state, ctx) {
|
|
@@ -3006,13 +3514,20 @@ var EraserTool = class {
|
|
|
3006
3514
|
}
|
|
3007
3515
|
eraseAt(state, ctx) {
|
|
3008
3516
|
const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
3009
|
-
const
|
|
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);
|
|
3010
3524
|
let erased = false;
|
|
3011
|
-
for (const
|
|
3012
|
-
if (
|
|
3013
|
-
if (ctx.
|
|
3014
|
-
if (
|
|
3015
|
-
|
|
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);
|
|
3016
3531
|
erased = true;
|
|
3017
3532
|
}
|
|
3018
3533
|
}
|
|
@@ -3389,7 +3904,7 @@ var SelectTool = class {
|
|
|
3389
3904
|
for (const id of this._selectedIds) {
|
|
3390
3905
|
const el = ctx.store.getById(id);
|
|
3391
3906
|
if (!el || !("size" in el)) continue;
|
|
3392
|
-
const bounds =
|
|
3907
|
+
const bounds = getElementBounds(el);
|
|
3393
3908
|
if (!bounds) continue;
|
|
3394
3909
|
const corners = this.getHandlePositions(bounds);
|
|
3395
3910
|
for (const [handle, pos] of corners) {
|
|
@@ -3437,7 +3952,7 @@ var SelectTool = class {
|
|
|
3437
3952
|
this.renderBindingHighlights(canvasCtx, el, zoom);
|
|
3438
3953
|
continue;
|
|
3439
3954
|
}
|
|
3440
|
-
const bounds =
|
|
3955
|
+
const bounds = getElementBounds(el);
|
|
3441
3956
|
if (!bounds) continue;
|
|
3442
3957
|
const pad = SELECTION_PAD / zoom;
|
|
3443
3958
|
canvasCtx.strokeRect(bounds.x - pad, bounds.y - pad, bounds.w + pad * 2, bounds.h + pad * 2);
|
|
@@ -3496,12 +4011,13 @@ var SelectTool = class {
|
|
|
3496
4011
|
return { x, y, w, h };
|
|
3497
4012
|
}
|
|
3498
4013
|
findElementsInRect(marquee, ctx) {
|
|
4014
|
+
const candidates = ctx.store.queryRect(marquee);
|
|
3499
4015
|
const ids = [];
|
|
3500
|
-
for (const el of
|
|
4016
|
+
for (const el of candidates) {
|
|
3501
4017
|
if (ctx.isLayerVisible && !ctx.isLayerVisible(el.layerId)) continue;
|
|
3502
4018
|
if (ctx.isLayerLocked && ctx.isLayerLocked(el.layerId)) continue;
|
|
3503
4019
|
if (el.type === "grid") continue;
|
|
3504
|
-
const bounds =
|
|
4020
|
+
const bounds = getElementBounds(el);
|
|
3505
4021
|
if (bounds && this.rectsOverlap(marquee, bounds)) {
|
|
3506
4022
|
ids.push(el.id);
|
|
3507
4023
|
}
|
|
@@ -3511,30 +4027,10 @@ var SelectTool = class {
|
|
|
3511
4027
|
rectsOverlap(a, b) {
|
|
3512
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;
|
|
3513
4029
|
}
|
|
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
4030
|
hitTest(world, ctx) {
|
|
3536
|
-
const
|
|
3537
|
-
|
|
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) {
|
|
3538
4034
|
if (ctx.isLayerVisible && !ctx.isLayerVisible(el.layerId)) continue;
|
|
3539
4035
|
if (ctx.isLayerLocked && ctx.isLayerLocked(el.layerId)) continue;
|
|
3540
4036
|
if (el.type === "grid") continue;
|
|
@@ -4028,7 +4524,7 @@ var UpdateLayerCommand = class {
|
|
|
4028
4524
|
};
|
|
4029
4525
|
|
|
4030
4526
|
// src/index.ts
|
|
4031
|
-
var VERSION = "0.8.
|
|
4527
|
+
var VERSION = "0.8.7";
|
|
4032
4528
|
export {
|
|
4033
4529
|
AddElementCommand,
|
|
4034
4530
|
ArrowTool,
|
|
@@ -4050,6 +4546,7 @@ export {
|
|
|
4050
4546
|
NoteEditor,
|
|
4051
4547
|
NoteTool,
|
|
4052
4548
|
PencilTool,
|
|
4549
|
+
Quadtree,
|
|
4053
4550
|
RemoveElementCommand,
|
|
4054
4551
|
RemoveLayerCommand,
|
|
4055
4552
|
SelectTool,
|
|
@@ -4060,6 +4557,7 @@ export {
|
|
|
4060
4557
|
UpdateLayerCommand,
|
|
4061
4558
|
VERSION,
|
|
4062
4559
|
Viewport,
|
|
4560
|
+
boundsIntersect,
|
|
4063
4561
|
clearStaleBindings,
|
|
4064
4562
|
createArrow,
|
|
4065
4563
|
createGrid,
|