@fieldnotes/core 0.8.6 → 0.8.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +630 -130
- package/dist/index.d.cts +46 -14
- package/dist/index.d.ts +46 -14
- package/dist/index.js +628 -130
- package/package.json +1 -1
package/dist/index.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
|
|
|
@@ -664,77 +829,6 @@ var InputHandler = class {
|
|
|
664
829
|
}
|
|
665
830
|
};
|
|
666
831
|
|
|
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
832
|
// src/elements/arrow-geometry.ts
|
|
739
833
|
function getArrowControlPoint(from, to, bend) {
|
|
740
834
|
const midX = (from.x + to.x) / 2;
|
|
@@ -835,6 +929,185 @@ function isNearLine(point, a, b, threshold) {
|
|
|
835
929
|
return Math.hypot(point.x - projX, point.y - projY) <= threshold;
|
|
836
930
|
}
|
|
837
931
|
|
|
932
|
+
// src/elements/element-bounds.ts
|
|
933
|
+
var strokeBoundsCache = /* @__PURE__ */ new WeakMap();
|
|
934
|
+
function getElementBounds(element) {
|
|
935
|
+
if (element.type === "grid") return null;
|
|
936
|
+
if ("size" in element) {
|
|
937
|
+
return {
|
|
938
|
+
x: element.position.x,
|
|
939
|
+
y: element.position.y,
|
|
940
|
+
w: element.size.w,
|
|
941
|
+
h: element.size.h
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
if (element.type === "stroke") {
|
|
945
|
+
if (element.points.length === 0) return null;
|
|
946
|
+
const cached = strokeBoundsCache.get(element);
|
|
947
|
+
if (cached) return cached;
|
|
948
|
+
let minX = Infinity;
|
|
949
|
+
let minY = Infinity;
|
|
950
|
+
let maxX = -Infinity;
|
|
951
|
+
let maxY = -Infinity;
|
|
952
|
+
for (const p of element.points) {
|
|
953
|
+
const px = p.x + element.position.x;
|
|
954
|
+
const py = p.y + element.position.y;
|
|
955
|
+
if (px < minX) minX = px;
|
|
956
|
+
if (py < minY) minY = py;
|
|
957
|
+
if (px > maxX) maxX = px;
|
|
958
|
+
if (py > maxY) maxY = py;
|
|
959
|
+
}
|
|
960
|
+
const bounds = { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
|
|
961
|
+
strokeBoundsCache.set(element, bounds);
|
|
962
|
+
return bounds;
|
|
963
|
+
}
|
|
964
|
+
if (element.type === "arrow") {
|
|
965
|
+
return getArrowBoundsAnalytical(element.from, element.to, element.bend);
|
|
966
|
+
}
|
|
967
|
+
return null;
|
|
968
|
+
}
|
|
969
|
+
function getArrowBoundsAnalytical(from, to, bend) {
|
|
970
|
+
if (bend === 0) {
|
|
971
|
+
const minX2 = Math.min(from.x, to.x);
|
|
972
|
+
const minY2 = Math.min(from.y, to.y);
|
|
973
|
+
return {
|
|
974
|
+
x: minX2,
|
|
975
|
+
y: minY2,
|
|
976
|
+
w: Math.abs(to.x - from.x),
|
|
977
|
+
h: Math.abs(to.y - from.y)
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
const cp = getArrowControlPoint(from, to, bend);
|
|
981
|
+
let minX = Math.min(from.x, to.x);
|
|
982
|
+
let maxX = Math.max(from.x, to.x);
|
|
983
|
+
let minY = Math.min(from.y, to.y);
|
|
984
|
+
let maxY = Math.max(from.y, to.y);
|
|
985
|
+
const tx = from.x - 2 * cp.x + to.x;
|
|
986
|
+
if (tx !== 0) {
|
|
987
|
+
const t = (from.x - cp.x) / tx;
|
|
988
|
+
if (t > 0 && t < 1) {
|
|
989
|
+
const mt = 1 - t;
|
|
990
|
+
const x = mt * mt * from.x + 2 * mt * t * cp.x + t * t * to.x;
|
|
991
|
+
if (x < minX) minX = x;
|
|
992
|
+
if (x > maxX) maxX = x;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
const ty = from.y - 2 * cp.y + to.y;
|
|
996
|
+
if (ty !== 0) {
|
|
997
|
+
const t = (from.y - cp.y) / ty;
|
|
998
|
+
if (t > 0 && t < 1) {
|
|
999
|
+
const mt = 1 - t;
|
|
1000
|
+
const y = mt * mt * from.y + 2 * mt * t * cp.y + t * t * to.y;
|
|
1001
|
+
if (y < minY) minY = y;
|
|
1002
|
+
if (y > maxY) maxY = y;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
|
|
1006
|
+
}
|
|
1007
|
+
function boundsIntersect(a, b) {
|
|
1008
|
+
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;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// src/elements/element-store.ts
|
|
1012
|
+
var ElementStore = class {
|
|
1013
|
+
elements = /* @__PURE__ */ new Map();
|
|
1014
|
+
bus = new EventBus();
|
|
1015
|
+
layerOrderMap = /* @__PURE__ */ new Map();
|
|
1016
|
+
spatialIndex = new Quadtree({ x: -1e5, y: -1e5, w: 2e5, h: 2e5 });
|
|
1017
|
+
get count() {
|
|
1018
|
+
return this.elements.size;
|
|
1019
|
+
}
|
|
1020
|
+
setLayerOrder(order) {
|
|
1021
|
+
this.layerOrderMap = new Map(order);
|
|
1022
|
+
}
|
|
1023
|
+
getAll() {
|
|
1024
|
+
return [...this.elements.values()].sort((a, b) => {
|
|
1025
|
+
const layerA = this.layerOrderMap.get(a.layerId) ?? 0;
|
|
1026
|
+
const layerB = this.layerOrderMap.get(b.layerId) ?? 0;
|
|
1027
|
+
if (layerA !== layerB) return layerA - layerB;
|
|
1028
|
+
return a.zIndex - b.zIndex;
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
getById(id) {
|
|
1032
|
+
return this.elements.get(id);
|
|
1033
|
+
}
|
|
1034
|
+
getElementsByType(type) {
|
|
1035
|
+
return this.getAll().filter(
|
|
1036
|
+
(el) => el.type === type
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
add(element) {
|
|
1040
|
+
this.elements.set(element.id, element);
|
|
1041
|
+
const bounds = getElementBounds(element);
|
|
1042
|
+
if (bounds) this.spatialIndex.insert(element.id, bounds);
|
|
1043
|
+
this.bus.emit("add", element);
|
|
1044
|
+
}
|
|
1045
|
+
update(id, partial) {
|
|
1046
|
+
const existing = this.elements.get(id);
|
|
1047
|
+
if (!existing) return;
|
|
1048
|
+
const updated = { ...existing, ...partial, id: existing.id, type: existing.type };
|
|
1049
|
+
this.elements.set(id, updated);
|
|
1050
|
+
const newBounds = getElementBounds(updated);
|
|
1051
|
+
if (newBounds) {
|
|
1052
|
+
this.spatialIndex.update(id, newBounds);
|
|
1053
|
+
}
|
|
1054
|
+
this.bus.emit("update", { previous: existing, current: updated });
|
|
1055
|
+
}
|
|
1056
|
+
remove(id) {
|
|
1057
|
+
const element = this.elements.get(id);
|
|
1058
|
+
if (!element) return;
|
|
1059
|
+
this.elements.delete(id);
|
|
1060
|
+
this.spatialIndex.remove(id);
|
|
1061
|
+
this.bus.emit("remove", element);
|
|
1062
|
+
}
|
|
1063
|
+
clear() {
|
|
1064
|
+
this.elements.clear();
|
|
1065
|
+
this.spatialIndex.clear();
|
|
1066
|
+
this.bus.emit("clear", null);
|
|
1067
|
+
}
|
|
1068
|
+
snapshot() {
|
|
1069
|
+
return this.getAll().map((el) => ({ ...el }));
|
|
1070
|
+
}
|
|
1071
|
+
loadSnapshot(elements) {
|
|
1072
|
+
this.elements.clear();
|
|
1073
|
+
this.spatialIndex.clear();
|
|
1074
|
+
for (const el of elements) {
|
|
1075
|
+
this.elements.set(el.id, el);
|
|
1076
|
+
const bounds = getElementBounds(el);
|
|
1077
|
+
if (bounds) this.spatialIndex.insert(el.id, bounds);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
queryRect(rect) {
|
|
1081
|
+
const ids = this.spatialIndex.query(rect);
|
|
1082
|
+
const elements = [];
|
|
1083
|
+
for (const id of ids) {
|
|
1084
|
+
const el = this.elements.get(id);
|
|
1085
|
+
if (el) elements.push(el);
|
|
1086
|
+
}
|
|
1087
|
+
return elements.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
|
+
queryPoint(point) {
|
|
1095
|
+
return this.queryRect({ x: point.x, y: point.y, w: 0, h: 0 });
|
|
1096
|
+
}
|
|
1097
|
+
on(event, listener) {
|
|
1098
|
+
return this.bus.on(event, listener);
|
|
1099
|
+
}
|
|
1100
|
+
onChange(listener) {
|
|
1101
|
+
const unsubs = [
|
|
1102
|
+
this.bus.on("add", listener),
|
|
1103
|
+
this.bus.on("remove", listener),
|
|
1104
|
+
this.bus.on("update", listener),
|
|
1105
|
+
this.bus.on("clear", listener)
|
|
1106
|
+
];
|
|
1107
|
+
return () => unsubs.forEach((fn) => fn());
|
|
1108
|
+
}
|
|
1109
|
+
};
|
|
1110
|
+
|
|
838
1111
|
// src/elements/arrow-binding.ts
|
|
839
1112
|
var BINDABLE_TYPES = /* @__PURE__ */ new Set(["note", "text", "image", "html", "shape"]);
|
|
840
1113
|
function isBindable(element) {
|
|
@@ -849,15 +1122,6 @@ function getElementCenter(element) {
|
|
|
849
1122
|
y: element.position.y + element.size.h / 2
|
|
850
1123
|
};
|
|
851
1124
|
}
|
|
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
1125
|
function getEdgeIntersection(bounds, outsidePoint) {
|
|
862
1126
|
const cx = bounds.x + bounds.w / 2;
|
|
863
1127
|
const cy = bounds.y + bounds.h / 2;
|
|
@@ -2448,6 +2712,41 @@ var DomNodeManager = class {
|
|
|
2448
2712
|
}
|
|
2449
2713
|
};
|
|
2450
2714
|
|
|
2715
|
+
// src/canvas/render-stats.ts
|
|
2716
|
+
var SAMPLE_SIZE = 60;
|
|
2717
|
+
var RenderStats = class {
|
|
2718
|
+
frameTimes = [];
|
|
2719
|
+
frameCount = 0;
|
|
2720
|
+
recordFrame(durationMs) {
|
|
2721
|
+
this.frameCount++;
|
|
2722
|
+
this.frameTimes.push(durationMs);
|
|
2723
|
+
if (this.frameTimes.length > SAMPLE_SIZE) {
|
|
2724
|
+
this.frameTimes.shift();
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
getSnapshot() {
|
|
2728
|
+
const times = this.frameTimes;
|
|
2729
|
+
if (times.length === 0) {
|
|
2730
|
+
return { fps: 0, avgFrameMs: 0, p95FrameMs: 0, lastFrameMs: 0, frameCount: 0 };
|
|
2731
|
+
}
|
|
2732
|
+
const avg = times.reduce((a, b) => a + b, 0) / times.length;
|
|
2733
|
+
const sorted = [...times].sort((a, b) => a - b);
|
|
2734
|
+
const p95Index = Math.min(Math.floor(sorted.length * 0.95), sorted.length - 1);
|
|
2735
|
+
const lastFrame = times[times.length - 1] ?? 0;
|
|
2736
|
+
return {
|
|
2737
|
+
fps: avg > 0 ? Math.round(1e3 / avg) : 0,
|
|
2738
|
+
avgFrameMs: Math.round(avg * 100) / 100,
|
|
2739
|
+
p95FrameMs: Math.round((sorted[p95Index] ?? 0) * 100) / 100,
|
|
2740
|
+
lastFrameMs: Math.round(lastFrame * 100) / 100,
|
|
2741
|
+
frameCount: this.frameCount
|
|
2742
|
+
};
|
|
2743
|
+
}
|
|
2744
|
+
reset() {
|
|
2745
|
+
this.frameTimes = [];
|
|
2746
|
+
this.frameCount = 0;
|
|
2747
|
+
}
|
|
2748
|
+
};
|
|
2749
|
+
|
|
2451
2750
|
// src/canvas/render-loop.ts
|
|
2452
2751
|
var RenderLoop = class {
|
|
2453
2752
|
needsRender = false;
|
|
@@ -2460,6 +2759,12 @@ var RenderLoop = class {
|
|
|
2460
2759
|
toolManager;
|
|
2461
2760
|
layerManager;
|
|
2462
2761
|
domNodeManager;
|
|
2762
|
+
layerCache;
|
|
2763
|
+
activeDrawingLayerId = null;
|
|
2764
|
+
lastZoom;
|
|
2765
|
+
lastCamX;
|
|
2766
|
+
lastCamY;
|
|
2767
|
+
stats = new RenderStats();
|
|
2463
2768
|
constructor(deps) {
|
|
2464
2769
|
this.canvasEl = deps.canvasEl;
|
|
2465
2770
|
this.camera = deps.camera;
|
|
@@ -2469,6 +2774,10 @@ var RenderLoop = class {
|
|
|
2469
2774
|
this.toolManager = deps.toolManager;
|
|
2470
2775
|
this.layerManager = deps.layerManager;
|
|
2471
2776
|
this.domNodeManager = deps.domNodeManager;
|
|
2777
|
+
this.layerCache = deps.layerCache;
|
|
2778
|
+
this.lastZoom = deps.camera.zoom;
|
|
2779
|
+
this.lastCamX = deps.camera.position.x;
|
|
2780
|
+
this.lastCamY = deps.camera.position.y;
|
|
2472
2781
|
}
|
|
2473
2782
|
requestRender() {
|
|
2474
2783
|
this.needsRender = true;
|
|
@@ -2495,19 +2804,63 @@ var RenderLoop = class {
|
|
|
2495
2804
|
setCanvasSize(width, height) {
|
|
2496
2805
|
this.canvasEl.width = width;
|
|
2497
2806
|
this.canvasEl.height = height;
|
|
2807
|
+
this.layerCache.resize(this.canvasEl.clientWidth, this.canvasEl.clientHeight);
|
|
2808
|
+
}
|
|
2809
|
+
setActiveDrawingLayer(layerId) {
|
|
2810
|
+
this.activeDrawingLayerId = layerId;
|
|
2811
|
+
}
|
|
2812
|
+
markLayerDirty(layerId) {
|
|
2813
|
+
this.layerCache.markDirty(layerId);
|
|
2814
|
+
}
|
|
2815
|
+
markAllLayersDirty() {
|
|
2816
|
+
this.layerCache.markAllDirty();
|
|
2817
|
+
}
|
|
2818
|
+
getStats() {
|
|
2819
|
+
return this.stats.getSnapshot();
|
|
2820
|
+
}
|
|
2821
|
+
compositeLayerCache(ctx, layerId, dpr) {
|
|
2822
|
+
const cached = this.layerCache.getCanvas(layerId);
|
|
2823
|
+
ctx.save();
|
|
2824
|
+
ctx.scale(1 / this.camera.zoom, 1 / this.camera.zoom);
|
|
2825
|
+
ctx.translate(-this.camera.position.x, -this.camera.position.y);
|
|
2826
|
+
ctx.scale(1 / dpr, 1 / dpr);
|
|
2827
|
+
ctx.drawImage(cached, 0, 0);
|
|
2828
|
+
ctx.restore();
|
|
2498
2829
|
}
|
|
2499
2830
|
render() {
|
|
2831
|
+
const t0 = performance.now();
|
|
2500
2832
|
const ctx = this.canvasEl.getContext("2d");
|
|
2501
2833
|
if (!ctx) return;
|
|
2502
2834
|
const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
|
|
2835
|
+
const cssWidth = this.canvasEl.clientWidth;
|
|
2836
|
+
const cssHeight = this.canvasEl.clientHeight;
|
|
2837
|
+
const currentZoom = this.camera.zoom;
|
|
2838
|
+
const currentCamX = this.camera.position.x;
|
|
2839
|
+
const currentCamY = this.camera.position.y;
|
|
2840
|
+
if (currentZoom !== this.lastZoom || currentCamX !== this.lastCamX || currentCamY !== this.lastCamY) {
|
|
2841
|
+
this.layerCache.markAllDirty();
|
|
2842
|
+
this.lastZoom = currentZoom;
|
|
2843
|
+
this.lastCamX = currentCamX;
|
|
2844
|
+
this.lastCamY = currentCamY;
|
|
2845
|
+
}
|
|
2503
2846
|
ctx.save();
|
|
2504
2847
|
ctx.scale(dpr, dpr);
|
|
2505
|
-
this.renderer.setCanvasSize(
|
|
2848
|
+
this.renderer.setCanvasSize(cssWidth, cssHeight);
|
|
2506
2849
|
this.background.render(ctx, this.camera);
|
|
2507
2850
|
ctx.save();
|
|
2508
2851
|
ctx.translate(this.camera.position.x, this.camera.position.y);
|
|
2509
2852
|
ctx.scale(this.camera.zoom, this.camera.zoom);
|
|
2853
|
+
const visibleRect = this.camera.getVisibleRect(cssWidth, cssHeight);
|
|
2854
|
+
const margin = Math.max(visibleRect.w, visibleRect.h) * 0.1;
|
|
2855
|
+
const cullingRect = {
|
|
2856
|
+
x: visibleRect.x - margin,
|
|
2857
|
+
y: visibleRect.y - margin,
|
|
2858
|
+
w: visibleRect.w + margin * 2,
|
|
2859
|
+
h: visibleRect.h + margin * 2
|
|
2860
|
+
};
|
|
2510
2861
|
const allElements = this.store.getAll();
|
|
2862
|
+
const layerElements = /* @__PURE__ */ new Map();
|
|
2863
|
+
const gridElements = [];
|
|
2511
2864
|
let domZIndex = 0;
|
|
2512
2865
|
for (const element of allElements) {
|
|
2513
2866
|
if (!this.layerManager.isLayerVisible(element.layerId)) {
|
|
@@ -2517,9 +2870,54 @@ var RenderLoop = class {
|
|
|
2517
2870
|
continue;
|
|
2518
2871
|
}
|
|
2519
2872
|
if (this.renderer.isDomElement(element)) {
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2873
|
+
const elBounds = getElementBounds(element);
|
|
2874
|
+
if (elBounds && !boundsIntersect(elBounds, cullingRect)) {
|
|
2875
|
+
this.domNodeManager.hideDomNode(element.id);
|
|
2876
|
+
} else {
|
|
2877
|
+
this.domNodeManager.syncDomNode(element, domZIndex++);
|
|
2878
|
+
}
|
|
2879
|
+
continue;
|
|
2880
|
+
}
|
|
2881
|
+
if (element.type === "grid") {
|
|
2882
|
+
gridElements.push(element);
|
|
2883
|
+
continue;
|
|
2884
|
+
}
|
|
2885
|
+
let group = layerElements.get(element.layerId);
|
|
2886
|
+
if (!group) {
|
|
2887
|
+
group = [];
|
|
2888
|
+
layerElements.set(element.layerId, group);
|
|
2889
|
+
}
|
|
2890
|
+
group.push(element);
|
|
2891
|
+
}
|
|
2892
|
+
for (const grid of gridElements) {
|
|
2893
|
+
this.renderer.renderCanvasElement(ctx, grid);
|
|
2894
|
+
}
|
|
2895
|
+
for (const [layerId, elements] of layerElements) {
|
|
2896
|
+
const isActiveDrawingLayer = layerId === this.activeDrawingLayerId;
|
|
2897
|
+
if (!this.layerCache.isDirty(layerId)) {
|
|
2898
|
+
this.compositeLayerCache(ctx, layerId, dpr);
|
|
2899
|
+
continue;
|
|
2900
|
+
}
|
|
2901
|
+
if (isActiveDrawingLayer) {
|
|
2902
|
+
this.compositeLayerCache(ctx, layerId, dpr);
|
|
2903
|
+
continue;
|
|
2904
|
+
}
|
|
2905
|
+
const offCtx = this.layerCache.getContext(layerId);
|
|
2906
|
+
if (offCtx) {
|
|
2907
|
+
const offCanvas = this.layerCache.getCanvas(layerId);
|
|
2908
|
+
offCtx.clearRect(0, 0, offCanvas.width, offCanvas.height);
|
|
2909
|
+
offCtx.save();
|
|
2910
|
+
offCtx.scale(dpr, dpr);
|
|
2911
|
+
offCtx.translate(this.camera.position.x, this.camera.position.y);
|
|
2912
|
+
offCtx.scale(this.camera.zoom, this.camera.zoom);
|
|
2913
|
+
for (const element of elements) {
|
|
2914
|
+
const elBounds = getElementBounds(element);
|
|
2915
|
+
if (elBounds && !boundsIntersect(elBounds, cullingRect)) continue;
|
|
2916
|
+
this.renderer.renderCanvasElement(offCtx, element);
|
|
2917
|
+
}
|
|
2918
|
+
offCtx.restore();
|
|
2919
|
+
this.layerCache.markClean(layerId);
|
|
2920
|
+
this.compositeLayerCache(ctx, layerId, dpr);
|
|
2523
2921
|
}
|
|
2524
2922
|
}
|
|
2525
2923
|
const activeTool = this.toolManager.activeTool;
|
|
@@ -2528,6 +2926,70 @@ var RenderLoop = class {
|
|
|
2528
2926
|
}
|
|
2529
2927
|
ctx.restore();
|
|
2530
2928
|
ctx.restore();
|
|
2929
|
+
this.stats.recordFrame(performance.now() - t0);
|
|
2930
|
+
}
|
|
2931
|
+
};
|
|
2932
|
+
|
|
2933
|
+
// src/canvas/layer-cache.ts
|
|
2934
|
+
function createOffscreenCanvas(width, height) {
|
|
2935
|
+
if (typeof OffscreenCanvas !== "undefined") {
|
|
2936
|
+
return new OffscreenCanvas(width, height);
|
|
2937
|
+
}
|
|
2938
|
+
const canvas = document.createElement("canvas");
|
|
2939
|
+
canvas.width = width;
|
|
2940
|
+
canvas.height = height;
|
|
2941
|
+
return canvas;
|
|
2942
|
+
}
|
|
2943
|
+
var LayerCache = class {
|
|
2944
|
+
canvases = /* @__PURE__ */ new Map();
|
|
2945
|
+
dirtyFlags = /* @__PURE__ */ new Map();
|
|
2946
|
+
width;
|
|
2947
|
+
height;
|
|
2948
|
+
constructor(width, height) {
|
|
2949
|
+
const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
|
|
2950
|
+
this.width = Math.round(width * dpr);
|
|
2951
|
+
this.height = Math.round(height * dpr);
|
|
2952
|
+
}
|
|
2953
|
+
isDirty(layerId) {
|
|
2954
|
+
return this.dirtyFlags.get(layerId) !== false;
|
|
2955
|
+
}
|
|
2956
|
+
markDirty(layerId) {
|
|
2957
|
+
this.dirtyFlags.set(layerId, true);
|
|
2958
|
+
}
|
|
2959
|
+
markClean(layerId) {
|
|
2960
|
+
this.dirtyFlags.set(layerId, false);
|
|
2961
|
+
}
|
|
2962
|
+
markAllDirty() {
|
|
2963
|
+
for (const [id] of this.dirtyFlags) {
|
|
2964
|
+
this.dirtyFlags.set(id, true);
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
getCanvas(layerId) {
|
|
2968
|
+
let canvas = this.canvases.get(layerId);
|
|
2969
|
+
if (!canvas) {
|
|
2970
|
+
canvas = createOffscreenCanvas(this.width, this.height);
|
|
2971
|
+
this.canvases.set(layerId, canvas);
|
|
2972
|
+
this.dirtyFlags.set(layerId, true);
|
|
2973
|
+
}
|
|
2974
|
+
return canvas;
|
|
2975
|
+
}
|
|
2976
|
+
getContext(layerId) {
|
|
2977
|
+
const canvas = this.getCanvas(layerId);
|
|
2978
|
+
return canvas.getContext("2d");
|
|
2979
|
+
}
|
|
2980
|
+
resize(width, height) {
|
|
2981
|
+
const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
|
|
2982
|
+
this.width = Math.round(width * dpr);
|
|
2983
|
+
this.height = Math.round(height * dpr);
|
|
2984
|
+
for (const [id, canvas] of this.canvases) {
|
|
2985
|
+
canvas.width = this.width;
|
|
2986
|
+
canvas.height = this.height;
|
|
2987
|
+
this.dirtyFlags.set(id, true);
|
|
2988
|
+
}
|
|
2989
|
+
}
|
|
2990
|
+
clear() {
|
|
2991
|
+
this.canvases.clear();
|
|
2992
|
+
this.dirtyFlags.clear();
|
|
2531
2993
|
}
|
|
2532
2994
|
};
|
|
2533
2995
|
|
|
@@ -2584,6 +3046,10 @@ var Viewport = class {
|
|
|
2584
3046
|
this.interactMode = new InteractMode({
|
|
2585
3047
|
getNode: (id) => this.domNodeManager.getNode(id)
|
|
2586
3048
|
});
|
|
3049
|
+
const layerCache = new LayerCache(
|
|
3050
|
+
this.canvasEl.clientWidth || 800,
|
|
3051
|
+
this.canvasEl.clientHeight || 600
|
|
3052
|
+
);
|
|
2587
3053
|
this.renderLoop = new RenderLoop({
|
|
2588
3054
|
canvasEl: this.canvasEl,
|
|
2589
3055
|
camera: this.camera,
|
|
@@ -2592,22 +3058,34 @@ var Viewport = class {
|
|
|
2592
3058
|
renderer: this.renderer,
|
|
2593
3059
|
toolManager: this.toolManager,
|
|
2594
3060
|
layerManager: this.layerManager,
|
|
2595
|
-
domNodeManager: this.domNodeManager
|
|
3061
|
+
domNodeManager: this.domNodeManager,
|
|
3062
|
+
layerCache
|
|
2596
3063
|
});
|
|
2597
3064
|
this.unsubCamera = this.camera.onChange(() => {
|
|
2598
3065
|
this.applyCameraTransform();
|
|
2599
3066
|
this.requestRender();
|
|
2600
3067
|
});
|
|
2601
3068
|
this.unsubStore = [
|
|
2602
|
-
this.store.on("add", () =>
|
|
3069
|
+
this.store.on("add", (el) => {
|
|
3070
|
+
this.renderLoop.markLayerDirty(el.layerId);
|
|
3071
|
+
this.requestRender();
|
|
3072
|
+
}),
|
|
2603
3073
|
this.store.on("remove", (el) => {
|
|
2604
3074
|
this.unbindArrowsFrom(el);
|
|
2605
3075
|
this.domNodeManager.removeDomNode(el.id);
|
|
3076
|
+
this.renderLoop.markLayerDirty(el.layerId);
|
|
3077
|
+
this.requestRender();
|
|
3078
|
+
}),
|
|
3079
|
+
this.store.on("update", ({ previous, current }) => {
|
|
3080
|
+
this.renderLoop.markLayerDirty(current.layerId);
|
|
3081
|
+
if (previous.layerId !== current.layerId) {
|
|
3082
|
+
this.renderLoop.markLayerDirty(previous.layerId);
|
|
3083
|
+
}
|
|
2606
3084
|
this.requestRender();
|
|
2607
3085
|
}),
|
|
2608
|
-
this.store.on("update", () => this.requestRender()),
|
|
2609
3086
|
this.store.on("clear", () => {
|
|
2610
3087
|
this.domNodeManager.clearDomNodes();
|
|
3088
|
+
this.renderLoop.markAllLayersDirty();
|
|
2611
3089
|
this.requestRender();
|
|
2612
3090
|
})
|
|
2613
3091
|
];
|
|
@@ -2813,8 +3291,8 @@ var Viewport = class {
|
|
|
2813
3291
|
}
|
|
2814
3292
|
};
|
|
2815
3293
|
hitTestWorld(world) {
|
|
2816
|
-
const
|
|
2817
|
-
for (const el of
|
|
3294
|
+
const candidates = this.store.queryPoint(world).reverse();
|
|
3295
|
+
for (const el of candidates) {
|
|
2818
3296
|
if (!("size" in el)) continue;
|
|
2819
3297
|
const { x, y } = el.position;
|
|
2820
3298
|
const { w, h } = el.size;
|
|
@@ -2965,6 +3443,9 @@ var HandTool = class {
|
|
|
2965
3443
|
var MIN_POINTS_FOR_STROKE = 2;
|
|
2966
3444
|
var DEFAULT_SMOOTHING = 1.5;
|
|
2967
3445
|
var DEFAULT_PRESSURE = 0.5;
|
|
3446
|
+
var DEFAULT_MIN_POINT_DISTANCE = 3;
|
|
3447
|
+
var DEFAULT_PROGRESSIVE_THRESHOLD = 200;
|
|
3448
|
+
var PROGRESSIVE_HOT_ZONE = 30;
|
|
2968
3449
|
var PencilTool = class {
|
|
2969
3450
|
name = "pencil";
|
|
2970
3451
|
drawing = false;
|
|
@@ -2972,11 +3453,17 @@ var PencilTool = class {
|
|
|
2972
3453
|
color;
|
|
2973
3454
|
width;
|
|
2974
3455
|
smoothing;
|
|
3456
|
+
minPointDistance;
|
|
3457
|
+
progressiveThreshold;
|
|
3458
|
+
nextSimplifyAt;
|
|
2975
3459
|
optionListeners = /* @__PURE__ */ new Set();
|
|
2976
3460
|
constructor(options = {}) {
|
|
2977
3461
|
this.color = options.color ?? "#000000";
|
|
2978
3462
|
this.width = options.width ?? 2;
|
|
2979
3463
|
this.smoothing = options.smoothing ?? DEFAULT_SMOOTHING;
|
|
3464
|
+
this.minPointDistance = options.minPointDistance ?? DEFAULT_MIN_POINT_DISTANCE;
|
|
3465
|
+
this.progressiveThreshold = options.progressiveSimplifyThreshold ?? DEFAULT_PROGRESSIVE_THRESHOLD;
|
|
3466
|
+
this.nextSimplifyAt = this.progressiveThreshold;
|
|
2980
3467
|
}
|
|
2981
3468
|
onActivate(ctx) {
|
|
2982
3469
|
ctx.setCursor?.("crosshair");
|
|
@@ -2985,7 +3472,13 @@ var PencilTool = class {
|
|
|
2985
3472
|
ctx.setCursor?.("default");
|
|
2986
3473
|
}
|
|
2987
3474
|
getOptions() {
|
|
2988
|
-
return {
|
|
3475
|
+
return {
|
|
3476
|
+
color: this.color,
|
|
3477
|
+
width: this.width,
|
|
3478
|
+
smoothing: this.smoothing,
|
|
3479
|
+
minPointDistance: this.minPointDistance,
|
|
3480
|
+
progressiveSimplifyThreshold: this.progressiveThreshold
|
|
3481
|
+
};
|
|
2989
3482
|
}
|
|
2990
3483
|
onOptionsChange(listener) {
|
|
2991
3484
|
this.optionListeners.add(listener);
|
|
@@ -2995,6 +3488,9 @@ var PencilTool = class {
|
|
|
2995
3488
|
if (options.color !== void 0) this.color = options.color;
|
|
2996
3489
|
if (options.width !== void 0) this.width = options.width;
|
|
2997
3490
|
if (options.smoothing !== void 0) this.smoothing = options.smoothing;
|
|
3491
|
+
if (options.minPointDistance !== void 0) this.minPointDistance = options.minPointDistance;
|
|
3492
|
+
if (options.progressiveSimplifyThreshold !== void 0)
|
|
3493
|
+
this.progressiveThreshold = options.progressiveSimplifyThreshold;
|
|
2998
3494
|
this.notifyOptionsChange();
|
|
2999
3495
|
}
|
|
3000
3496
|
onPointerDown(state, ctx) {
|
|
@@ -3002,12 +3498,26 @@ var PencilTool = class {
|
|
|
3002
3498
|
const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
3003
3499
|
const pressure = state.pressure === 0 ? DEFAULT_PRESSURE : state.pressure;
|
|
3004
3500
|
this.points = [{ x: world.x, y: world.y, pressure }];
|
|
3501
|
+
this.nextSimplifyAt = this.progressiveThreshold;
|
|
3005
3502
|
}
|
|
3006
3503
|
onPointerMove(state, ctx) {
|
|
3007
3504
|
if (!this.drawing) return;
|
|
3008
3505
|
const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
3009
3506
|
const pressure = state.pressure === 0 ? DEFAULT_PRESSURE : state.pressure;
|
|
3507
|
+
const last = this.points[this.points.length - 1];
|
|
3508
|
+
if (last) {
|
|
3509
|
+
const dx = world.x - last.x;
|
|
3510
|
+
const dy = world.y - last.y;
|
|
3511
|
+
if (dx * dx + dy * dy < this.minPointDistance * this.minPointDistance) return;
|
|
3512
|
+
}
|
|
3010
3513
|
this.points.push({ x: world.x, y: world.y, pressure });
|
|
3514
|
+
if (this.points.length > this.nextSimplifyAt) {
|
|
3515
|
+
const hotZone = this.points.slice(-PROGRESSIVE_HOT_ZONE);
|
|
3516
|
+
const coldZone = this.points.slice(0, -PROGRESSIVE_HOT_ZONE);
|
|
3517
|
+
const simplified = simplifyPoints(coldZone, this.smoothing * 2);
|
|
3518
|
+
this.points = [...simplified, ...hotZone];
|
|
3519
|
+
this.nextSimplifyAt = this.points.length + this.progressiveThreshold;
|
|
3520
|
+
}
|
|
3011
3521
|
ctx.requestRender();
|
|
3012
3522
|
}
|
|
3013
3523
|
onPointerUp(_state, ctx) {
|
|
@@ -3089,13 +3599,20 @@ var EraserTool = class {
|
|
|
3089
3599
|
}
|
|
3090
3600
|
eraseAt(state, ctx) {
|
|
3091
3601
|
const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
3092
|
-
const
|
|
3602
|
+
const queryBounds = {
|
|
3603
|
+
x: world.x - this.radius,
|
|
3604
|
+
y: world.y - this.radius,
|
|
3605
|
+
w: this.radius * 2,
|
|
3606
|
+
h: this.radius * 2
|
|
3607
|
+
};
|
|
3608
|
+
const candidates = ctx.store.queryRect(queryBounds);
|
|
3093
3609
|
let erased = false;
|
|
3094
|
-
for (const
|
|
3095
|
-
if (
|
|
3096
|
-
if (ctx.
|
|
3097
|
-
if (
|
|
3098
|
-
|
|
3610
|
+
for (const el of candidates) {
|
|
3611
|
+
if (el.type !== "stroke") continue;
|
|
3612
|
+
if (ctx.isLayerVisible && !ctx.isLayerVisible(el.layerId)) continue;
|
|
3613
|
+
if (ctx.isLayerLocked && ctx.isLayerLocked(el.layerId)) continue;
|
|
3614
|
+
if (this.strokeIntersects(el, world)) {
|
|
3615
|
+
ctx.store.remove(el.id);
|
|
3099
3616
|
erased = true;
|
|
3100
3617
|
}
|
|
3101
3618
|
}
|
|
@@ -3472,7 +3989,7 @@ var SelectTool = class {
|
|
|
3472
3989
|
for (const id of this._selectedIds) {
|
|
3473
3990
|
const el = ctx.store.getById(id);
|
|
3474
3991
|
if (!el || !("size" in el)) continue;
|
|
3475
|
-
const bounds =
|
|
3992
|
+
const bounds = getElementBounds(el);
|
|
3476
3993
|
if (!bounds) continue;
|
|
3477
3994
|
const corners = this.getHandlePositions(bounds);
|
|
3478
3995
|
for (const [handle, pos] of corners) {
|
|
@@ -3520,7 +4037,7 @@ var SelectTool = class {
|
|
|
3520
4037
|
this.renderBindingHighlights(canvasCtx, el, zoom);
|
|
3521
4038
|
continue;
|
|
3522
4039
|
}
|
|
3523
|
-
const bounds =
|
|
4040
|
+
const bounds = getElementBounds(el);
|
|
3524
4041
|
if (!bounds) continue;
|
|
3525
4042
|
const pad = SELECTION_PAD / zoom;
|
|
3526
4043
|
canvasCtx.strokeRect(bounds.x - pad, bounds.y - pad, bounds.w + pad * 2, bounds.h + pad * 2);
|
|
@@ -3579,12 +4096,13 @@ var SelectTool = class {
|
|
|
3579
4096
|
return { x, y, w, h };
|
|
3580
4097
|
}
|
|
3581
4098
|
findElementsInRect(marquee, ctx) {
|
|
4099
|
+
const candidates = ctx.store.queryRect(marquee);
|
|
3582
4100
|
const ids = [];
|
|
3583
|
-
for (const el of
|
|
4101
|
+
for (const el of candidates) {
|
|
3584
4102
|
if (ctx.isLayerVisible && !ctx.isLayerVisible(el.layerId)) continue;
|
|
3585
4103
|
if (ctx.isLayerLocked && ctx.isLayerLocked(el.layerId)) continue;
|
|
3586
4104
|
if (el.type === "grid") continue;
|
|
3587
|
-
const bounds =
|
|
4105
|
+
const bounds = getElementBounds(el);
|
|
3588
4106
|
if (bounds && this.rectsOverlap(marquee, bounds)) {
|
|
3589
4107
|
ids.push(el.id);
|
|
3590
4108
|
}
|
|
@@ -3594,30 +4112,10 @@ var SelectTool = class {
|
|
|
3594
4112
|
rectsOverlap(a, b) {
|
|
3595
4113
|
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
4114
|
}
|
|
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
4115
|
hitTest(world, ctx) {
|
|
3619
|
-
const
|
|
3620
|
-
|
|
4116
|
+
const r = 10;
|
|
4117
|
+
const candidates = ctx.store.queryRect({ x: world.x - r, y: world.y - r, w: r * 2, h: r * 2 }).reverse();
|
|
4118
|
+
for (const el of candidates) {
|
|
3621
4119
|
if (ctx.isLayerVisible && !ctx.isLayerVisible(el.layerId)) continue;
|
|
3622
4120
|
if (ctx.isLayerLocked && ctx.isLayerLocked(el.layerId)) continue;
|
|
3623
4121
|
if (el.type === "grid") continue;
|
|
@@ -4111,7 +4609,7 @@ var UpdateLayerCommand = class {
|
|
|
4111
4609
|
};
|
|
4112
4610
|
|
|
4113
4611
|
// src/index.ts
|
|
4114
|
-
var VERSION = "0.8.
|
|
4612
|
+
var VERSION = "0.8.7";
|
|
4115
4613
|
// Annotate the CommonJS export names for ESM import in node:
|
|
4116
4614
|
0 && (module.exports = {
|
|
4117
4615
|
AddElementCommand,
|
|
@@ -4134,6 +4632,7 @@ var VERSION = "0.8.6";
|
|
|
4134
4632
|
NoteEditor,
|
|
4135
4633
|
NoteTool,
|
|
4136
4634
|
PencilTool,
|
|
4635
|
+
Quadtree,
|
|
4137
4636
|
RemoveElementCommand,
|
|
4138
4637
|
RemoveLayerCommand,
|
|
4139
4638
|
SelectTool,
|
|
@@ -4144,6 +4643,7 @@ var VERSION = "0.8.6";
|
|
|
4144
4643
|
UpdateLayerCommand,
|
|
4145
4644
|
VERSION,
|
|
4146
4645
|
Viewport,
|
|
4646
|
+
boundsIntersect,
|
|
4147
4647
|
clearStaleBindings,
|
|
4148
4648
|
createArrow,
|
|
4149
4649
|
createGrid,
|