@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.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.
|
|
471
|
+
this.notifyPan();
|
|
323
472
|
}
|
|
324
473
|
moveTo(x, y) {
|
|
325
474
|
this.x = x;
|
|
326
475
|
this.y = y;
|
|
327
|
-
this.
|
|
476
|
+
this.notifyPan();
|
|
328
477
|
}
|
|
329
478
|
setZoom(level) {
|
|
330
479
|
this.z = Math.min(this.maxZoom, Math.max(this.minZoom, level));
|
|
331
|
-
this.
|
|
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.
|
|
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
|
-
|
|
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(
|
|
593
|
+
this.renderDots(offCtx, camera, cssWidth, cssHeight);
|
|
397
594
|
} else if (this.pattern === "grid") {
|
|
398
|
-
this.renderGrid(
|
|
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 =
|
|
1205
|
-
for (
|
|
1206
|
-
const
|
|
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(
|
|
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)
|
|
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 = () =>
|
|
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
|
|
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
|
|
2015
|
-
if (imageElements.length === 0) return Promise.resolve(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
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", () =>
|
|
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
|
|
2817
|
-
for (const el of
|
|
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 {
|
|
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
|
|
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
|
|
3095
|
-
if (
|
|
3096
|
-
if (ctx.
|
|
3097
|
-
if (
|
|
3098
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
3620
|
-
|
|
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.
|
|
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,
|