@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 +755 -146
- package/dist/index.d.cts +56 -14
- package/dist/index.d.ts +56 -14
- package/dist/index.js +753 -146
- 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
|
|
|
@@ -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(
|
|
508
|
+
this.renderDots(offCtx, camera, cssWidth, cssHeight);
|
|
314
509
|
} else if (this.pattern === "grid") {
|
|
315
|
-
this.renderGrid(
|
|
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 =
|
|
1122
|
-
for (
|
|
1123
|
-
const
|
|
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(
|
|
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)
|
|
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 = () =>
|
|
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
|
|
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
|
|
1932
|
-
if (imageElements.length === 0) return Promise.resolve(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
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", () =>
|
|
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
|
|
2734
|
-
for (const el of
|
|
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 {
|
|
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
|
|
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
|
|
3012
|
-
if (
|
|
3013
|
-
if (ctx.
|
|
3014
|
-
if (
|
|
3015
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
3537
|
-
|
|
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.
|
|
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,
|