@lupinum/board-core 0.1.0
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/LICENSE +21 -0
- package/README.md +55 -0
- package/dist/chunk-5JZXHZWR.js +68 -0
- package/dist/colors.d.ts +9 -0
- package/dist/engine/camera-session.d.ts +14 -0
- package/dist/engine/command-runtime.d.ts +31 -0
- package/dist/engine/events.d.ts +21 -0
- package/dist/engine/interaction-adapter.d.ts +3 -0
- package/dist/engine/node-shape.d.ts +16 -0
- package/dist/engine/options.d.ts +17 -0
- package/dist/engine/persistence.d.ts +18 -0
- package/dist/engine/subscribables.d.ts +34 -0
- package/dist/engine/transaction.d.ts +56 -0
- package/dist/engine.d.ts +17 -0
- package/dist/errors.d.ts +17 -0
- package/dist/helpers/animation.d.ts +9 -0
- package/dist/helpers/clone.d.ts +6 -0
- package/dist/helpers/ids.d.ts +2 -0
- package/dist/helpers/node-shape.d.ts +2 -0
- package/dist/helpers/selection-helpers.d.ts +8 -0
- package/dist/hierarchy.d.ts +16 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +4159 -0
- package/dist/internal.d.ts +9 -0
- package/dist/internal.js +10 -0
- package/dist/invariants.d.ts +3 -0
- package/dist/math.d.ts +16 -0
- package/dist/resize.d.ts +26 -0
- package/dist/selection.d.ts +7 -0
- package/dist/snap.d.ts +29 -0
- package/dist/state/initial.d.ts +2 -0
- package/dist/state/selectors.d.ts +5 -0
- package/dist/state/types.d.ts +31 -0
- package/dist/subscribable.d.ts +20 -0
- package/dist/types.d.ts +576 -0
- package/package.json +50 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,4159 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BoardConflictError,
|
|
3
|
+
BoardDestroyedError,
|
|
4
|
+
BoardError,
|
|
5
|
+
BoardInputError,
|
|
6
|
+
BoardNotFoundError,
|
|
7
|
+
assertInternalBoardPlugin,
|
|
8
|
+
registerBoardInteractionAdapter
|
|
9
|
+
} from "./chunk-5JZXHZWR.js";
|
|
10
|
+
|
|
11
|
+
// src/math.ts
|
|
12
|
+
function clamp(value, min, max) {
|
|
13
|
+
return Math.min(max, Math.max(min, value));
|
|
14
|
+
}
|
|
15
|
+
function lerp(a, b, t) {
|
|
16
|
+
return a + (b - a) * t;
|
|
17
|
+
}
|
|
18
|
+
function lerpCamera(from, to, t) {
|
|
19
|
+
return {
|
|
20
|
+
x: lerp(from.x, to.x, t),
|
|
21
|
+
y: lerp(from.y, to.y, t),
|
|
22
|
+
z: lerp(from.z, to.z, t)
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function screenToWorld(point, camera) {
|
|
26
|
+
return {
|
|
27
|
+
x: point.x / camera.z - camera.x,
|
|
28
|
+
y: point.y / camera.z - camera.y
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function worldToScreen(point, camera) {
|
|
32
|
+
return {
|
|
33
|
+
x: (point.x + camera.x) * camera.z,
|
|
34
|
+
y: (point.y + camera.y) * camera.z
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function getVisibleBounds(width, height, camera) {
|
|
38
|
+
const topLeft = screenToWorld({ x: 0, y: 0 }, camera);
|
|
39
|
+
const bottomRight = screenToWorld({ x: width, y: height }, camera);
|
|
40
|
+
return {
|
|
41
|
+
minX: topLeft.x,
|
|
42
|
+
minY: topLeft.y,
|
|
43
|
+
maxX: bottomRight.x,
|
|
44
|
+
maxY: bottomRight.y
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function pointInBounds(point, bounds) {
|
|
48
|
+
return point.x >= bounds.minX && point.x <= bounds.maxX && point.y >= bounds.minY && point.y <= bounds.maxY;
|
|
49
|
+
}
|
|
50
|
+
function boundsIntersect(a, b) {
|
|
51
|
+
return !(a.maxX < b.minX || a.minX > b.maxX || a.maxY < b.minY || a.minY > b.maxY);
|
|
52
|
+
}
|
|
53
|
+
function boundsContain(outer, inner) {
|
|
54
|
+
return inner.minX >= outer.minX && inner.maxX <= outer.maxX && inner.minY >= outer.minY && inner.maxY <= outer.maxY;
|
|
55
|
+
}
|
|
56
|
+
function getBoundsFromPoints(a, b) {
|
|
57
|
+
return {
|
|
58
|
+
minX: Math.min(a.x, b.x),
|
|
59
|
+
minY: Math.min(a.y, b.y),
|
|
60
|
+
maxX: Math.max(a.x, b.x),
|
|
61
|
+
maxY: Math.max(a.y, b.y)
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function snapValue(value, step) {
|
|
65
|
+
if (step <= 0) {
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
return Math.round(value / step) * step;
|
|
69
|
+
}
|
|
70
|
+
function snapPoint(point, step) {
|
|
71
|
+
return {
|
|
72
|
+
x: snapValue(point.x, step),
|
|
73
|
+
y: snapValue(point.y, step)
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function snapSize(value, step, min) {
|
|
77
|
+
return Math.max(min, snapValue(value, step));
|
|
78
|
+
}
|
|
79
|
+
function zoomCameraAtScreenPoint(screenPoint, delta, camera, min, max) {
|
|
80
|
+
const nextZoom = clamp(camera.z * Math.pow(2, -delta * 0.01), min, max);
|
|
81
|
+
const before = screenToWorld(screenPoint, camera);
|
|
82
|
+
const after = {
|
|
83
|
+
x: screenPoint.x / nextZoom - camera.x,
|
|
84
|
+
y: screenPoint.y / nextZoom - camera.y
|
|
85
|
+
};
|
|
86
|
+
return {
|
|
87
|
+
x: camera.x + (after.x - before.x),
|
|
88
|
+
y: camera.y + (after.y - before.y),
|
|
89
|
+
z: nextZoom
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// src/hierarchy.ts
|
|
94
|
+
function getBoundsFromNode(node) {
|
|
95
|
+
return {
|
|
96
|
+
minX: node.x,
|
|
97
|
+
minY: node.y,
|
|
98
|
+
maxX: node.x + node.width,
|
|
99
|
+
maxY: node.y + node.height
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function groupArea(node) {
|
|
103
|
+
return node.width * node.height;
|
|
104
|
+
}
|
|
105
|
+
function addDescendants(rootId, nodes, out) {
|
|
106
|
+
for (const n of nodes.values()) {
|
|
107
|
+
if (n.parentId === rootId && !out.has(n.id)) {
|
|
108
|
+
out.add(n.id);
|
|
109
|
+
if (n.type === "group") {
|
|
110
|
+
addDescendants(n.id, nodes, out);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function expandGroupDragSeeds(seedIds, nodes) {
|
|
116
|
+
const out = /* @__PURE__ */ new Set();
|
|
117
|
+
for (const id of seedIds) {
|
|
118
|
+
out.add(id);
|
|
119
|
+
const n = nodes.get(id);
|
|
120
|
+
if (n?.type === "group") {
|
|
121
|
+
addDescendants(id, nodes, out);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
function collectSubtreeIds(rootId, nodes, into) {
|
|
127
|
+
into.add(rootId);
|
|
128
|
+
for (const n of nodes.values()) {
|
|
129
|
+
if (n.parentId === rootId) {
|
|
130
|
+
collectSubtreeIds(n.id, nodes, into);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function collectUniformTranslationTargets(seedIds, nodes) {
|
|
135
|
+
const expanded = expandGroupDragSeeds(seedIds, nodes);
|
|
136
|
+
const roots = [];
|
|
137
|
+
for (const id of expanded) {
|
|
138
|
+
const n = nodes.get(id);
|
|
139
|
+
if (!n) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (!n.parentId || !expanded.has(n.parentId)) {
|
|
143
|
+
roots.push(id);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const out = /* @__PURE__ */ new Set();
|
|
147
|
+
for (const r of roots) {
|
|
148
|
+
collectSubtreeIds(r, nodes, out);
|
|
149
|
+
}
|
|
150
|
+
return [...out];
|
|
151
|
+
}
|
|
152
|
+
function isStrictDescendantOf(maybeDescendant, ancestorId, nodes) {
|
|
153
|
+
let walk = nodes.get(maybeDescendant)?.parentId;
|
|
154
|
+
const seen = /* @__PURE__ */ new Set();
|
|
155
|
+
while (walk) {
|
|
156
|
+
if (seen.has(walk)) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
seen.add(walk);
|
|
160
|
+
if (walk === ancestorId) {
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
walk = nodes.get(walk)?.parentId;
|
|
164
|
+
}
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
function findContainingGroup(node, nodes) {
|
|
168
|
+
const nodeBounds = getBoundsFromNode(node);
|
|
169
|
+
const candidates = [];
|
|
170
|
+
for (const g of nodes.values()) {
|
|
171
|
+
if (g.type !== "group" || !g.visible) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (g.id === node.id) {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (isStrictDescendantOf(g.id, node.id, nodes)) {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (!boundsContain(getBoundsFromNode(g), nodeBounds)) {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
candidates.push(g);
|
|
184
|
+
}
|
|
185
|
+
if (candidates.length === 0) {
|
|
186
|
+
return void 0;
|
|
187
|
+
}
|
|
188
|
+
candidates.sort((a, b) => groupArea(a) - groupArea(b));
|
|
189
|
+
return candidates[0].id;
|
|
190
|
+
}
|
|
191
|
+
function sortIdsByZIndex(ids, nodes) {
|
|
192
|
+
return [...ids].sort((a, b) => {
|
|
193
|
+
const za = nodes.get(a)?.zIndex ?? 0;
|
|
194
|
+
const zb = nodes.get(b)?.zIndex ?? 0;
|
|
195
|
+
return za - zb;
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// src/resize.ts
|
|
200
|
+
function applyResizeDelta(node, handle, deltaX, deltaY, constraints) {
|
|
201
|
+
let { x, y, width, height } = node;
|
|
202
|
+
if (handle.includes("e")) {
|
|
203
|
+
width = Math.max(constraints.minWidth, node.width + deltaX);
|
|
204
|
+
}
|
|
205
|
+
if (handle.includes("s")) {
|
|
206
|
+
height = Math.max(constraints.minHeight, node.height + deltaY);
|
|
207
|
+
}
|
|
208
|
+
if (handle.includes("w")) {
|
|
209
|
+
const nextWidth = Math.max(constraints.minWidth, node.width - deltaX);
|
|
210
|
+
const consumed = node.width - nextWidth;
|
|
211
|
+
width = nextWidth;
|
|
212
|
+
x = node.x + consumed;
|
|
213
|
+
}
|
|
214
|
+
if (handle.includes("n")) {
|
|
215
|
+
const nextHeight = Math.max(constraints.minHeight, node.height - deltaY);
|
|
216
|
+
const consumed = node.height - nextHeight;
|
|
217
|
+
height = nextHeight;
|
|
218
|
+
y = node.y + consumed;
|
|
219
|
+
}
|
|
220
|
+
return { x, y, width, height };
|
|
221
|
+
}
|
|
222
|
+
function snapResizedBounds(bounds, handle, gridSize, constraints) {
|
|
223
|
+
let { x, y, width, height } = bounds;
|
|
224
|
+
const right = bounds.x + bounds.width;
|
|
225
|
+
const bottom = bounds.y + bounds.height;
|
|
226
|
+
if (handle.includes("e")) {
|
|
227
|
+
width = snapSize(width, gridSize, constraints.minWidth);
|
|
228
|
+
} else {
|
|
229
|
+
x = snapValue(x, gridSize);
|
|
230
|
+
}
|
|
231
|
+
if (handle.includes("s")) {
|
|
232
|
+
height = snapSize(height, gridSize, constraints.minHeight);
|
|
233
|
+
} else {
|
|
234
|
+
y = snapValue(y, gridSize);
|
|
235
|
+
}
|
|
236
|
+
if (handle.includes("w")) {
|
|
237
|
+
width = snapSize(width, gridSize, constraints.minWidth);
|
|
238
|
+
x = snapValue(right - width, gridSize);
|
|
239
|
+
width = Math.max(constraints.minWidth, right - x);
|
|
240
|
+
}
|
|
241
|
+
if (handle.includes("n")) {
|
|
242
|
+
height = snapSize(height, gridSize, constraints.minHeight);
|
|
243
|
+
y = snapValue(bottom - height, gridSize);
|
|
244
|
+
height = Math.max(constraints.minHeight, bottom - y);
|
|
245
|
+
}
|
|
246
|
+
return { x, y, width, height };
|
|
247
|
+
}
|
|
248
|
+
function applyResizeDeltaLocked(node, handle, deltaX, deltaY, constraints, aspectRatio) {
|
|
249
|
+
const { width: w, height: h } = node;
|
|
250
|
+
let cdx = deltaX;
|
|
251
|
+
let cdy = deltaY;
|
|
252
|
+
let effectiveHandle = handle;
|
|
253
|
+
if (handle === "e") {
|
|
254
|
+
const newW = Math.max(constraints.minWidth, w + deltaX);
|
|
255
|
+
cdx = newW - w;
|
|
256
|
+
cdy = Math.max(constraints.minHeight, newW / aspectRatio) - h;
|
|
257
|
+
effectiveHandle = "se";
|
|
258
|
+
} else if (handle === "w") {
|
|
259
|
+
const newW = Math.max(constraints.minWidth, w - deltaX);
|
|
260
|
+
cdy = Math.max(constraints.minHeight, newW / aspectRatio) - h;
|
|
261
|
+
effectiveHandle = "sw";
|
|
262
|
+
} else if (handle === "s") {
|
|
263
|
+
const newH = Math.max(constraints.minHeight, h + deltaY);
|
|
264
|
+
cdy = newH - h;
|
|
265
|
+
cdx = Math.max(constraints.minWidth, newH * aspectRatio) - w;
|
|
266
|
+
effectiveHandle = "se";
|
|
267
|
+
} else if (handle === "n") {
|
|
268
|
+
const newH = Math.max(constraints.minHeight, h - deltaY);
|
|
269
|
+
cdx = Math.max(constraints.minWidth, newH * aspectRatio) - w;
|
|
270
|
+
effectiveHandle = "ne";
|
|
271
|
+
} else {
|
|
272
|
+
const xSign = handle.includes("e") ? 1 : -1;
|
|
273
|
+
const ySign = handle.includes("s") ? 1 : -1;
|
|
274
|
+
const growX = xSign * deltaX;
|
|
275
|
+
const growY = ySign * deltaY;
|
|
276
|
+
let newW;
|
|
277
|
+
let newH;
|
|
278
|
+
if (Math.abs(growX / w) >= Math.abs(growY / h)) {
|
|
279
|
+
newW = Math.max(constraints.minWidth, w + growX);
|
|
280
|
+
newH = Math.max(constraints.minHeight, newW / aspectRatio);
|
|
281
|
+
} else {
|
|
282
|
+
newH = Math.max(constraints.minHeight, h + growY);
|
|
283
|
+
newW = Math.max(constraints.minWidth, newH * aspectRatio);
|
|
284
|
+
}
|
|
285
|
+
cdx = xSign * (newW - w);
|
|
286
|
+
cdy = ySign * (newH - h);
|
|
287
|
+
}
|
|
288
|
+
return applyResizeDelta(node, effectiveHandle, cdx, cdy, constraints);
|
|
289
|
+
}
|
|
290
|
+
function snapResizedBoundsLocked(bounds, startBounds, handle, gridSize, constraints, aspectRatio) {
|
|
291
|
+
const right = startBounds.x + startBounds.width;
|
|
292
|
+
const bottom = startBounds.y + startBounds.height;
|
|
293
|
+
if (handle === "n" || handle === "s") {
|
|
294
|
+
const snappedH2 = snapSize(bounds.height, gridSize, constraints.minHeight);
|
|
295
|
+
const snappedW2 = Math.max(constraints.minWidth, snappedH2 * aspectRatio);
|
|
296
|
+
const y2 = handle === "n" ? bottom - snappedH2 : bounds.y;
|
|
297
|
+
return { x: bounds.x, y: y2, width: snappedW2, height: snappedH2 };
|
|
298
|
+
}
|
|
299
|
+
const snappedW = snapSize(bounds.width, gridSize, constraints.minWidth);
|
|
300
|
+
const snappedH = Math.max(constraints.minHeight, snappedW / aspectRatio);
|
|
301
|
+
const x = handle.includes("w") ? right - snappedW : bounds.x;
|
|
302
|
+
const y = handle.includes("n") ? bottom - snappedH : bounds.y;
|
|
303
|
+
return { x, y, width: snappedW, height: snappedH };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// src/snap.ts
|
|
307
|
+
function collectNodeEdges(node) {
|
|
308
|
+
const right = node.x + node.width;
|
|
309
|
+
const bottom = node.y + node.height;
|
|
310
|
+
return [
|
|
311
|
+
{ axis: "x", value: node.x, extentMin: node.y, extentMax: bottom },
|
|
312
|
+
{ axis: "x", value: right, extentMin: node.y, extentMax: bottom },
|
|
313
|
+
{ axis: "y", value: node.y, extentMin: node.x, extentMax: right },
|
|
314
|
+
{ axis: "y", value: bottom, extentMin: node.x, extentMax: right }
|
|
315
|
+
];
|
|
316
|
+
}
|
|
317
|
+
function buildSnapEdgeIndex(nodes) {
|
|
318
|
+
const x = [];
|
|
319
|
+
const y = [];
|
|
320
|
+
for (const node of nodes) {
|
|
321
|
+
if (!node.visible) continue;
|
|
322
|
+
for (const edge of collectNodeEdges(node)) {
|
|
323
|
+
;
|
|
324
|
+
(edge.axis === "x" ? x : y).push({ ...edge, nodeId: node.id });
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
x.sort((left, right) => left.value - right.value);
|
|
328
|
+
y.sort((left, right) => left.value - right.value);
|
|
329
|
+
return { x, y };
|
|
330
|
+
}
|
|
331
|
+
function lowerBound(candidates, value) {
|
|
332
|
+
let low = 0;
|
|
333
|
+
let high = candidates.length;
|
|
334
|
+
while (low < high) {
|
|
335
|
+
const middle = low + high >>> 1;
|
|
336
|
+
if (candidates[middle].value < value) low = middle + 1;
|
|
337
|
+
else high = middle;
|
|
338
|
+
}
|
|
339
|
+
return low;
|
|
340
|
+
}
|
|
341
|
+
function findBestSnap(activeValue, activeExtentMin, activeExtentMax, axis, candidates, threshold, index, excludeIds) {
|
|
342
|
+
let bestDist = threshold;
|
|
343
|
+
let bestCandidate = null;
|
|
344
|
+
const source = index?.[axis] ?? candidates;
|
|
345
|
+
const start = index ? lowerBound(source, activeValue - threshold) : 0;
|
|
346
|
+
for (let position = start; position < source.length; position += 1) {
|
|
347
|
+
const candidate = source[position];
|
|
348
|
+
if (index && candidate.value >= activeValue + threshold) break;
|
|
349
|
+
if (candidate.axis !== axis) continue;
|
|
350
|
+
if (candidate.nodeId && excludeIds?.has(candidate.nodeId)) continue;
|
|
351
|
+
const dist = Math.abs(candidate.value - activeValue);
|
|
352
|
+
if (dist < bestDist) {
|
|
353
|
+
bestDist = dist;
|
|
354
|
+
bestCandidate = candidate;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (!bestCandidate) return null;
|
|
358
|
+
const guideFrom = Math.min(activeExtentMin, bestCandidate.extentMin);
|
|
359
|
+
const guideTo = Math.max(activeExtentMax, bestCandidate.extentMax);
|
|
360
|
+
return {
|
|
361
|
+
snappedValue: bestCandidate.value,
|
|
362
|
+
guide: {
|
|
363
|
+
axis,
|
|
364
|
+
position: bestCandidate.value,
|
|
365
|
+
from: guideFrom,
|
|
366
|
+
to: guideTo
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
function snapBoundsToEdges(bounds, handle, otherEdges, threshold, excludeIds) {
|
|
371
|
+
let { x, y, width, height } = bounds;
|
|
372
|
+
const guides = [];
|
|
373
|
+
const right = x + width;
|
|
374
|
+
const bottom = y + height;
|
|
375
|
+
if (handle.includes("e")) {
|
|
376
|
+
const snap = findBestSnap(
|
|
377
|
+
right,
|
|
378
|
+
y,
|
|
379
|
+
bottom,
|
|
380
|
+
"x",
|
|
381
|
+
Array.isArray(otherEdges) ? otherEdges : [],
|
|
382
|
+
threshold,
|
|
383
|
+
Array.isArray(otherEdges) ? void 0 : otherEdges,
|
|
384
|
+
excludeIds
|
|
385
|
+
);
|
|
386
|
+
if (snap) {
|
|
387
|
+
width = snap.snappedValue - x;
|
|
388
|
+
guides.push(snap.guide);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (handle.includes("w")) {
|
|
392
|
+
const snap = findBestSnap(
|
|
393
|
+
x,
|
|
394
|
+
y,
|
|
395
|
+
bottom,
|
|
396
|
+
"x",
|
|
397
|
+
Array.isArray(otherEdges) ? otherEdges : [],
|
|
398
|
+
threshold,
|
|
399
|
+
Array.isArray(otherEdges) ? void 0 : otherEdges,
|
|
400
|
+
excludeIds
|
|
401
|
+
);
|
|
402
|
+
if (snap) {
|
|
403
|
+
const oldRight = x + width;
|
|
404
|
+
x = snap.snappedValue;
|
|
405
|
+
width = oldRight - x;
|
|
406
|
+
guides.push(snap.guide);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
if (handle.includes("s")) {
|
|
410
|
+
const snap = findBestSnap(
|
|
411
|
+
bottom,
|
|
412
|
+
x,
|
|
413
|
+
right,
|
|
414
|
+
"y",
|
|
415
|
+
Array.isArray(otherEdges) ? otherEdges : [],
|
|
416
|
+
threshold,
|
|
417
|
+
Array.isArray(otherEdges) ? void 0 : otherEdges,
|
|
418
|
+
excludeIds
|
|
419
|
+
);
|
|
420
|
+
if (snap) {
|
|
421
|
+
height = snap.snappedValue - y;
|
|
422
|
+
guides.push(snap.guide);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (handle.includes("n")) {
|
|
426
|
+
const snap = findBestSnap(
|
|
427
|
+
y,
|
|
428
|
+
x,
|
|
429
|
+
right,
|
|
430
|
+
"y",
|
|
431
|
+
Array.isArray(otherEdges) ? otherEdges : [],
|
|
432
|
+
threshold,
|
|
433
|
+
Array.isArray(otherEdges) ? void 0 : otherEdges,
|
|
434
|
+
excludeIds
|
|
435
|
+
);
|
|
436
|
+
if (snap) {
|
|
437
|
+
const oldBottom = y + height;
|
|
438
|
+
y = snap.snappedValue;
|
|
439
|
+
height = oldBottom - y;
|
|
440
|
+
guides.push(snap.guide);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return { bounds: { x, y, width, height }, guides };
|
|
444
|
+
}
|
|
445
|
+
function snapPositionToEdges(bounds, otherEdges, threshold, excludeIds) {
|
|
446
|
+
let dx = 0;
|
|
447
|
+
let dy = 0;
|
|
448
|
+
const guides = [];
|
|
449
|
+
const { x, y, width, height } = bounds;
|
|
450
|
+
const right = x + width;
|
|
451
|
+
const bottom = y + height;
|
|
452
|
+
const candidates = Array.isArray(otherEdges) ? otherEdges : [];
|
|
453
|
+
const index = Array.isArray(otherEdges) ? void 0 : otherEdges;
|
|
454
|
+
const snapLeft = findBestSnap(
|
|
455
|
+
x,
|
|
456
|
+
y,
|
|
457
|
+
bottom,
|
|
458
|
+
"x",
|
|
459
|
+
candidates,
|
|
460
|
+
threshold,
|
|
461
|
+
index,
|
|
462
|
+
excludeIds
|
|
463
|
+
);
|
|
464
|
+
const snapRight = findBestSnap(
|
|
465
|
+
right,
|
|
466
|
+
y,
|
|
467
|
+
bottom,
|
|
468
|
+
"x",
|
|
469
|
+
candidates,
|
|
470
|
+
threshold,
|
|
471
|
+
index,
|
|
472
|
+
excludeIds
|
|
473
|
+
);
|
|
474
|
+
if (snapLeft && snapRight) {
|
|
475
|
+
const distLeft = Math.abs(snapLeft.snappedValue - x);
|
|
476
|
+
const distRight = Math.abs(snapRight.snappedValue - right);
|
|
477
|
+
if (distLeft <= distRight) {
|
|
478
|
+
dx = snapLeft.snappedValue - x;
|
|
479
|
+
guides.push(snapLeft.guide);
|
|
480
|
+
} else {
|
|
481
|
+
dx = snapRight.snappedValue - right;
|
|
482
|
+
guides.push(snapRight.guide);
|
|
483
|
+
}
|
|
484
|
+
} else if (snapLeft) {
|
|
485
|
+
dx = snapLeft.snappedValue - x;
|
|
486
|
+
guides.push(snapLeft.guide);
|
|
487
|
+
} else if (snapRight) {
|
|
488
|
+
dx = snapRight.snappedValue - right;
|
|
489
|
+
guides.push(snapRight.guide);
|
|
490
|
+
}
|
|
491
|
+
const snapTop = findBestSnap(
|
|
492
|
+
y,
|
|
493
|
+
x + dx,
|
|
494
|
+
right + dx,
|
|
495
|
+
"y",
|
|
496
|
+
candidates,
|
|
497
|
+
threshold,
|
|
498
|
+
index,
|
|
499
|
+
excludeIds
|
|
500
|
+
);
|
|
501
|
+
const snapBottom = findBestSnap(
|
|
502
|
+
bottom,
|
|
503
|
+
x + dx,
|
|
504
|
+
right + dx,
|
|
505
|
+
"y",
|
|
506
|
+
candidates,
|
|
507
|
+
threshold,
|
|
508
|
+
index,
|
|
509
|
+
excludeIds
|
|
510
|
+
);
|
|
511
|
+
if (snapTop && snapBottom) {
|
|
512
|
+
const distTop = Math.abs(snapTop.snappedValue - y);
|
|
513
|
+
const distBottom = Math.abs(snapBottom.snappedValue - bottom);
|
|
514
|
+
if (distTop <= distBottom) {
|
|
515
|
+
dy = snapTop.snappedValue - y;
|
|
516
|
+
guides.push(snapTop.guide);
|
|
517
|
+
} else {
|
|
518
|
+
dy = snapBottom.snappedValue - bottom;
|
|
519
|
+
guides.push(snapBottom.guide);
|
|
520
|
+
}
|
|
521
|
+
} else if (snapTop) {
|
|
522
|
+
dy = snapTop.snappedValue - y;
|
|
523
|
+
guides.push(snapTop.guide);
|
|
524
|
+
} else if (snapBottom) {
|
|
525
|
+
dy = snapBottom.snappedValue - bottom;
|
|
526
|
+
guides.push(snapBottom.guide);
|
|
527
|
+
}
|
|
528
|
+
return { dx, dy, guides };
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// src/helpers/clone.ts
|
|
532
|
+
function freezeClone(value) {
|
|
533
|
+
if (Array.isArray(value)) {
|
|
534
|
+
for (const entry of value) {
|
|
535
|
+
freezeClone(entry);
|
|
536
|
+
}
|
|
537
|
+
return Object.freeze(value);
|
|
538
|
+
}
|
|
539
|
+
if (value && typeof value === "object") {
|
|
540
|
+
for (const child of Object.values(value)) {
|
|
541
|
+
freezeClone(child);
|
|
542
|
+
}
|
|
543
|
+
return Object.freeze(value);
|
|
544
|
+
}
|
|
545
|
+
return value;
|
|
546
|
+
}
|
|
547
|
+
function sameArray(a, b) {
|
|
548
|
+
if (a.length !== b.length) {
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
return a.every((value, index) => value === b[index]);
|
|
552
|
+
}
|
|
553
|
+
function readonlyMapView(source) {
|
|
554
|
+
let view;
|
|
555
|
+
view = Object.freeze({
|
|
556
|
+
get size() {
|
|
557
|
+
return source.size;
|
|
558
|
+
},
|
|
559
|
+
get: (key) => source.get(key),
|
|
560
|
+
has: (key) => source.has(key),
|
|
561
|
+
entries: () => source.entries(),
|
|
562
|
+
keys: () => source.keys(),
|
|
563
|
+
values: () => source.values(),
|
|
564
|
+
forEach: (callback, thisArg) => {
|
|
565
|
+
source.forEach((value, key) => callback.call(thisArg, value, key, view));
|
|
566
|
+
},
|
|
567
|
+
[Symbol.iterator]: () => source[Symbol.iterator]()
|
|
568
|
+
});
|
|
569
|
+
return view;
|
|
570
|
+
}
|
|
571
|
+
function readonlySetView(source) {
|
|
572
|
+
let view;
|
|
573
|
+
view = Object.freeze({
|
|
574
|
+
get size() {
|
|
575
|
+
return source.size;
|
|
576
|
+
},
|
|
577
|
+
has: (value) => source.has(value),
|
|
578
|
+
entries: () => source.entries(),
|
|
579
|
+
keys: () => source.keys(),
|
|
580
|
+
values: () => source.values(),
|
|
581
|
+
forEach: (callback, thisArg) => {
|
|
582
|
+
source.forEach((value) => callback.call(thisArg, value, value, view));
|
|
583
|
+
},
|
|
584
|
+
[Symbol.iterator]: () => source[Symbol.iterator](),
|
|
585
|
+
union: (other) => {
|
|
586
|
+
const result = new Set(source);
|
|
587
|
+
for (const value of iteratorValues(other.keys())) result.add(value);
|
|
588
|
+
return result;
|
|
589
|
+
},
|
|
590
|
+
intersection: (other) => {
|
|
591
|
+
const result = /* @__PURE__ */ new Set();
|
|
592
|
+
for (const value of source) {
|
|
593
|
+
if (other.has(value)) result.add(value);
|
|
594
|
+
}
|
|
595
|
+
return result;
|
|
596
|
+
},
|
|
597
|
+
difference: (other) => {
|
|
598
|
+
const result = /* @__PURE__ */ new Set();
|
|
599
|
+
for (const value of source) {
|
|
600
|
+
if (!other.has(value)) result.add(value);
|
|
601
|
+
}
|
|
602
|
+
return result;
|
|
603
|
+
},
|
|
604
|
+
symmetricDifference: (other) => {
|
|
605
|
+
const result = new Set(source);
|
|
606
|
+
for (const value of iteratorValues(other.keys())) {
|
|
607
|
+
if (source.has(value)) result.delete(value);
|
|
608
|
+
else result.add(value);
|
|
609
|
+
}
|
|
610
|
+
return result;
|
|
611
|
+
},
|
|
612
|
+
isSubsetOf: (other) => {
|
|
613
|
+
for (const value of source) if (!other.has(value)) return false;
|
|
614
|
+
return true;
|
|
615
|
+
},
|
|
616
|
+
isSupersetOf: (other) => {
|
|
617
|
+
for (const value of iteratorValues(other.keys())) {
|
|
618
|
+
if (!source.has(value)) return false;
|
|
619
|
+
}
|
|
620
|
+
return true;
|
|
621
|
+
},
|
|
622
|
+
isDisjointFrom: (other) => {
|
|
623
|
+
for (const value of source) if (other.has(value)) return false;
|
|
624
|
+
return true;
|
|
625
|
+
}
|
|
626
|
+
});
|
|
627
|
+
return view;
|
|
628
|
+
}
|
|
629
|
+
function* iteratorValues(iterator) {
|
|
630
|
+
while (true) {
|
|
631
|
+
const result = iterator.next();
|
|
632
|
+
if (result.done) return;
|
|
633
|
+
yield result.value;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// src/helpers/ids.ts
|
|
638
|
+
function createNodeId() {
|
|
639
|
+
return crypto.randomUUID();
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// src/helpers/animation.ts
|
|
643
|
+
function getAnimationFrameDriver() {
|
|
644
|
+
const raf = globalThis.requestAnimationFrame?.bind(globalThis);
|
|
645
|
+
const caf = globalThis.cancelAnimationFrame?.bind(globalThis);
|
|
646
|
+
if (typeof raf === "function" && typeof caf === "function") {
|
|
647
|
+
return { raf, caf };
|
|
648
|
+
}
|
|
649
|
+
return {
|
|
650
|
+
raf: (cb) => globalThis.setTimeout(() => cb(Date.now()), 16),
|
|
651
|
+
caf: (handle) => globalThis.clearTimeout(handle)
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
var AnimationCancelled = class extends Error {
|
|
655
|
+
constructor() {
|
|
656
|
+
super("Animation cancelled");
|
|
657
|
+
this.name = "AnimationCancelled";
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
|
|
661
|
+
// src/state/types.ts
|
|
662
|
+
var DEFAULT_CAMERA = { x: 0, y: 0, z: 1 };
|
|
663
|
+
var DEFAULT_ZOOM = { min: 0.1, max: 8 };
|
|
664
|
+
var DEFAULT_GRID = {
|
|
665
|
+
size: 10,
|
|
666
|
+
majorEvery: 5,
|
|
667
|
+
snap: true,
|
|
668
|
+
edgeSnap: true,
|
|
669
|
+
edgeSnapThreshold: 8,
|
|
670
|
+
pattern: "line"
|
|
671
|
+
};
|
|
672
|
+
var DEFAULT_NODE_CONSTRAINTS = {
|
|
673
|
+
minWidth: 50,
|
|
674
|
+
minHeight: 50,
|
|
675
|
+
defaultWidth: 240,
|
|
676
|
+
defaultHeight: 160
|
|
677
|
+
};
|
|
678
|
+
var DEFAULT_VIEWPORT_SIZE = { x: 1280, y: 720 };
|
|
679
|
+
|
|
680
|
+
// src/state/initial.ts
|
|
681
|
+
function normalizeExistingNode(node) {
|
|
682
|
+
const parentId = typeof node.parentId === "string" && node.parentId.length > 0 ? node.parentId : void 0;
|
|
683
|
+
return {
|
|
684
|
+
...node,
|
|
685
|
+
color: node.color,
|
|
686
|
+
locked: Boolean(node.locked),
|
|
687
|
+
visible: node.visible !== false,
|
|
688
|
+
parentId
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// src/helpers/node-shape.ts
|
|
693
|
+
function materializeNode(node) {
|
|
694
|
+
return freezeClone({
|
|
695
|
+
id: node.id,
|
|
696
|
+
type: node.type,
|
|
697
|
+
x: node.x,
|
|
698
|
+
y: node.y,
|
|
699
|
+
width: node.width,
|
|
700
|
+
height: node.height,
|
|
701
|
+
...node.color !== void 0 ? { color: node.color } : {},
|
|
702
|
+
...node.text !== void 0 ? { text: node.text } : {},
|
|
703
|
+
...node.file !== void 0 ? { file: node.file } : {},
|
|
704
|
+
...node.subpath !== void 0 ? { subpath: node.subpath } : {},
|
|
705
|
+
...node.url !== void 0 ? { url: node.url } : {},
|
|
706
|
+
...node.label !== void 0 ? { label: node.label } : {},
|
|
707
|
+
...node.background !== void 0 ? { background: node.background } : {},
|
|
708
|
+
...node.backgroundStyle !== void 0 ? { backgroundStyle: node.backgroundStyle } : {},
|
|
709
|
+
zIndex: node.zIndex,
|
|
710
|
+
locked: node.locked,
|
|
711
|
+
visible: node.visible,
|
|
712
|
+
...node.parentId !== void 0 ? { parentId: node.parentId } : {}
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// src/invariants.ts
|
|
717
|
+
function cloneInteraction(interaction) {
|
|
718
|
+
switch (interaction.mode) {
|
|
719
|
+
case "idle":
|
|
720
|
+
return { mode: "idle" };
|
|
721
|
+
case "editing-text":
|
|
722
|
+
return { mode: "editing-text", nodeId: interaction.nodeId };
|
|
723
|
+
case "panning":
|
|
724
|
+
return {
|
|
725
|
+
mode: "panning",
|
|
726
|
+
pointerId: interaction.pointerId,
|
|
727
|
+
lastScreenPoint: { ...interaction.lastScreenPoint }
|
|
728
|
+
};
|
|
729
|
+
case "dragging-nodes":
|
|
730
|
+
return {
|
|
731
|
+
mode: "dragging-nodes",
|
|
732
|
+
pointerId: interaction.pointerId,
|
|
733
|
+
nodeIds: [...interaction.nodeIds],
|
|
734
|
+
startScreenPoint: { ...interaction.startScreenPoint },
|
|
735
|
+
startNodePositions: Object.fromEntries(
|
|
736
|
+
Object.entries(interaction.startNodePositions).map(([key, value]) => [
|
|
737
|
+
key,
|
|
738
|
+
{ ...value }
|
|
739
|
+
])
|
|
740
|
+
)
|
|
741
|
+
};
|
|
742
|
+
case "resizing-node":
|
|
743
|
+
return {
|
|
744
|
+
mode: "resizing-node",
|
|
745
|
+
pointerId: interaction.pointerId,
|
|
746
|
+
nodeId: interaction.nodeId,
|
|
747
|
+
handle: interaction.handle,
|
|
748
|
+
startScreenPoint: { ...interaction.startScreenPoint },
|
|
749
|
+
startNodeBounds: { ...interaction.startNodeBounds },
|
|
750
|
+
aspectRatio: interaction.aspectRatio
|
|
751
|
+
};
|
|
752
|
+
case "box-select":
|
|
753
|
+
return {
|
|
754
|
+
mode: "box-select",
|
|
755
|
+
pointerId: interaction.pointerId,
|
|
756
|
+
selectionMode: interaction.selectionMode,
|
|
757
|
+
startScreenPoint: { ...interaction.startScreenPoint },
|
|
758
|
+
currentScreenPoint: { ...interaction.currentScreenPoint },
|
|
759
|
+
startWorldPoint: { ...interaction.startWorldPoint },
|
|
760
|
+
currentWorldPoint: { ...interaction.currentWorldPoint }
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
function validateState(state, grid, context) {
|
|
765
|
+
const failures = [];
|
|
766
|
+
const push = (name, message) => {
|
|
767
|
+
failures.push({ name, message, context, state });
|
|
768
|
+
};
|
|
769
|
+
if (!Number.isFinite(state.camera.x) || !Number.isFinite(state.camera.y) || !Number.isFinite(state.camera.z) || state.camera.z <= 0) {
|
|
770
|
+
push(
|
|
771
|
+
"camera.valid",
|
|
772
|
+
"Camera position must be finite and zoom must be greater than 0."
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
if (grid.size <= 0 || !Number.isFinite(grid.size)) {
|
|
776
|
+
push("grid.size", "Grid size must be a finite number greater than 0.");
|
|
777
|
+
}
|
|
778
|
+
if (grid.majorEvery < 1 || !Number.isFinite(grid.majorEvery) || !Number.isInteger(grid.majorEvery)) {
|
|
779
|
+
push(
|
|
780
|
+
"grid.majorEvery",
|
|
781
|
+
"Grid majorEvery must be an integer greater than or equal to 1."
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
if (grid.edgeSnapThreshold <= 0 || !Number.isFinite(grid.edgeSnapThreshold)) {
|
|
785
|
+
push(
|
|
786
|
+
"grid.edgeSnapThreshold",
|
|
787
|
+
"Grid edgeSnapThreshold must be a finite number greater than 0."
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
const zIndexes = /* @__PURE__ */ new Set();
|
|
791
|
+
for (const node of state.nodes.values()) {
|
|
792
|
+
validateNode(node, push);
|
|
793
|
+
validateNodeParent(node, state, push);
|
|
794
|
+
if (zIndexes.has(node.zIndex)) {
|
|
795
|
+
push(
|
|
796
|
+
"node.zIndex.unique",
|
|
797
|
+
`Node ${node.id} shares a z-index with another node.`
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
zIndexes.add(node.zIndex);
|
|
801
|
+
}
|
|
802
|
+
for (const id of state.selection.values()) {
|
|
803
|
+
if (!state.nodes.has(id)) {
|
|
804
|
+
push("selection.exists", `Selected node ${id} does not exist.`);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
if (state.interaction.mode === "editing-text" && !state.nodes.has(state.interaction.nodeId)) {
|
|
808
|
+
push(
|
|
809
|
+
"interaction.node",
|
|
810
|
+
`Editing node ${state.interaction.nodeId} does not exist.`
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
if (state.interaction.mode === "resizing-node" && !state.nodes.has(state.interaction.nodeId)) {
|
|
814
|
+
push(
|
|
815
|
+
"interaction.node",
|
|
816
|
+
`Resizing node ${state.interaction.nodeId} does not exist.`
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
if (state.interaction.mode === "dragging-nodes") {
|
|
820
|
+
for (const id of state.interaction.nodeIds) {
|
|
821
|
+
if (!state.nodes.has(id)) {
|
|
822
|
+
push("interaction.node", `Dragging node ${id} does not exist.`);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
return failures;
|
|
827
|
+
}
|
|
828
|
+
function validateNode(node, push) {
|
|
829
|
+
if (!Number.isFinite(node.x) || !Number.isFinite(node.y) || !Number.isFinite(node.width) || !Number.isFinite(node.height)) {
|
|
830
|
+
push("node.finite", `Node ${node.id} contains non-finite geometry.`);
|
|
831
|
+
}
|
|
832
|
+
if (node.width <= 0 || node.height <= 0) {
|
|
833
|
+
push("node.size", `Node ${node.id} must have positive width and height.`);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
function validateNodeParent(node, state, push) {
|
|
837
|
+
if (node.parentId === void 0) {
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
if (node.parentId === node.id) {
|
|
841
|
+
push("node.parentId", `Node ${node.id} cannot be its own parent.`);
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
const parent = state.nodes.get(node.parentId);
|
|
845
|
+
if (!parent) {
|
|
846
|
+
push(
|
|
847
|
+
"node.parentId",
|
|
848
|
+
`Node ${node.id} references missing parent ${node.parentId}.`
|
|
849
|
+
);
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
if (parent.type !== "group") {
|
|
853
|
+
push(
|
|
854
|
+
"node.parentId",
|
|
855
|
+
`Node ${node.id} parent must be type "group", got "${parent.type}".`
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
let walk = parent;
|
|
859
|
+
const seen = /* @__PURE__ */ new Set();
|
|
860
|
+
while (walk) {
|
|
861
|
+
if (seen.has(walk.id)) {
|
|
862
|
+
push(
|
|
863
|
+
"node.parentId",
|
|
864
|
+
`Cycle detected in parent chain for node ${node.id}.`
|
|
865
|
+
);
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
seen.add(walk.id);
|
|
869
|
+
if (walk.id === node.id) {
|
|
870
|
+
push(
|
|
871
|
+
"node.parentId",
|
|
872
|
+
`Node ${node.id} would create a cycle in the parent chain.`
|
|
873
|
+
);
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
if (!walk.parentId) {
|
|
877
|
+
break;
|
|
878
|
+
}
|
|
879
|
+
walk = state.nodes.get(walk.parentId);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// src/state/selectors.ts
|
|
884
|
+
var materializedNodes = /* @__PURE__ */ new WeakMap();
|
|
885
|
+
function getMaterializedNode(node) {
|
|
886
|
+
const cached = materializedNodes.get(node);
|
|
887
|
+
if (cached) return cached;
|
|
888
|
+
const materialized = materializeNode(node);
|
|
889
|
+
materializedNodes.set(node, materialized);
|
|
890
|
+
return materialized;
|
|
891
|
+
}
|
|
892
|
+
function buildPublicNodeMap(state) {
|
|
893
|
+
return new Map(
|
|
894
|
+
Array.from(
|
|
895
|
+
state.nodes.values(),
|
|
896
|
+
(node) => [node.id, getMaterializedNode(node)]
|
|
897
|
+
)
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
function buildSnapshot(state, grid, publicNodes) {
|
|
901
|
+
const nodes = Array.from(publicNodes.values()).sort(
|
|
902
|
+
(a, b) => a.zIndex - b.zIndex
|
|
903
|
+
);
|
|
904
|
+
return freezeClone({
|
|
905
|
+
nodes,
|
|
906
|
+
camera: { ...state.camera },
|
|
907
|
+
grid: { ...grid },
|
|
908
|
+
selection: Array.from(state.selection.values()),
|
|
909
|
+
interaction: cloneInteraction(state.interaction),
|
|
910
|
+
snapGuides: state.snapGuides.map((guide) => ({ ...guide })),
|
|
911
|
+
nextZIndex: state.nextZIndex
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
function buildPublicState(state, grid, publicNodes) {
|
|
915
|
+
return {
|
|
916
|
+
camera: freezeClone({ ...state.camera }),
|
|
917
|
+
grid: freezeClone({ ...grid }),
|
|
918
|
+
nodes: new Map(publicNodes),
|
|
919
|
+
selection: new Set(state.selection),
|
|
920
|
+
interaction: cloneInteraction(state.interaction),
|
|
921
|
+
snapGuides: state.snapGuides.map((guide) => freezeClone({ ...guide }))
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// src/helpers/selection-helpers.ts
|
|
926
|
+
function getSelectionNodes(state) {
|
|
927
|
+
return Array.from(state.selection.values()).map((id) => state.nodes.get(id)).filter((node) => Boolean(node));
|
|
928
|
+
}
|
|
929
|
+
function getCopyClosureNodes(state) {
|
|
930
|
+
const selected = getSelectionNodes(state);
|
|
931
|
+
const ids = expandGroupDragSeeds(
|
|
932
|
+
selected.map((node) => node.id),
|
|
933
|
+
state.nodes
|
|
934
|
+
);
|
|
935
|
+
return Array.from(ids).map((id) => state.nodes.get(id)).filter((node) => Boolean(node)).sort((a, b) => a.zIndex - b.zIndex);
|
|
936
|
+
}
|
|
937
|
+
function duplicateForest(state, grid, nodes, offset) {
|
|
938
|
+
const sorted = [...nodes].sort((a, b) => a.zIndex - b.zIndex);
|
|
939
|
+
const idMap = /* @__PURE__ */ new Map();
|
|
940
|
+
for (const node of sorted) {
|
|
941
|
+
idMap.set(node.id, createNodeId());
|
|
942
|
+
}
|
|
943
|
+
return {
|
|
944
|
+
nodes: sorted.map((node) => ({
|
|
945
|
+
...node,
|
|
946
|
+
id: idMap.get(node.id),
|
|
947
|
+
parentId: node.parentId && idMap.has(node.parentId) ? idMap.get(node.parentId) : void 0,
|
|
948
|
+
x: grid.snap ? snapValue(node.x + offset.x, grid.size) : node.x + offset.x,
|
|
949
|
+
y: grid.snap ? snapValue(node.y + offset.y, grid.size) : node.y + offset.y,
|
|
950
|
+
zIndex: state.nextZIndex++
|
|
951
|
+
})),
|
|
952
|
+
idMap
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// src/engine/events.ts
|
|
957
|
+
function createEventBus(opts) {
|
|
958
|
+
const listeners = /* @__PURE__ */ new Map();
|
|
959
|
+
const trace = [];
|
|
960
|
+
const queuedEvents = [];
|
|
961
|
+
let transactionDepth = 0;
|
|
962
|
+
function reportUnhandledError(error, context) {
|
|
963
|
+
if (opts.onUnhandledError) {
|
|
964
|
+
try {
|
|
965
|
+
opts.onUnhandledError(error, context);
|
|
966
|
+
} catch (reportingError) {
|
|
967
|
+
console.error(
|
|
968
|
+
`[board] onUnhandledError failed while reporting ${context.source}:`,
|
|
969
|
+
reportingError
|
|
970
|
+
);
|
|
971
|
+
}
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
const subject = context.source === "event-listener" ? `handler for "${context.event}"` : context.source === "subscriber" ? `subscriber for "${context.channel}"` : `commit effect for "${context.commit}"`;
|
|
975
|
+
console.error(`[board] ${subject} threw:`, error);
|
|
976
|
+
}
|
|
977
|
+
function emit(event, ...args) {
|
|
978
|
+
if (transactionDepth > 0) {
|
|
979
|
+
queuedEvents.push({ event, args });
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
publish(event, args);
|
|
983
|
+
}
|
|
984
|
+
function publish(event, args) {
|
|
985
|
+
if (opts.diagnosticsEnabled) {
|
|
986
|
+
trace.push({ event: String(event), timestamp: Date.now(), args });
|
|
987
|
+
if (trace.length > opts.traceLimit) {
|
|
988
|
+
trace.shift();
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
for (const handler of listeners.get(event) ?? []) {
|
|
992
|
+
try {
|
|
993
|
+
;
|
|
994
|
+
handler(
|
|
995
|
+
...args
|
|
996
|
+
);
|
|
997
|
+
} catch (error) {
|
|
998
|
+
reportUnhandledError(error, {
|
|
999
|
+
source: "event-listener",
|
|
1000
|
+
event: String(event)
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
function emitImmediate(event, ...args) {
|
|
1006
|
+
publish(event, args);
|
|
1007
|
+
}
|
|
1008
|
+
function on(event, handler) {
|
|
1009
|
+
const set = listeners.get(event) ?? /* @__PURE__ */ new Set();
|
|
1010
|
+
set.add(handler);
|
|
1011
|
+
listeners.set(event, set);
|
|
1012
|
+
return () => off(event, handler);
|
|
1013
|
+
}
|
|
1014
|
+
function once(event, handler) {
|
|
1015
|
+
const unsubscribe = on(event, ((...args) => {
|
|
1016
|
+
unsubscribe();
|
|
1017
|
+
handler(...args);
|
|
1018
|
+
}));
|
|
1019
|
+
return unsubscribe;
|
|
1020
|
+
}
|
|
1021
|
+
function off(event, handler) {
|
|
1022
|
+
listeners.get(event)?.delete(handler);
|
|
1023
|
+
}
|
|
1024
|
+
function exportTrace() {
|
|
1025
|
+
return trace.map(
|
|
1026
|
+
(entry) => Object.freeze({
|
|
1027
|
+
...entry,
|
|
1028
|
+
args: Object.freeze([...entry.args])
|
|
1029
|
+
})
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
1032
|
+
function clear() {
|
|
1033
|
+
listeners.clear();
|
|
1034
|
+
trace.length = 0;
|
|
1035
|
+
queuedEvents.length = 0;
|
|
1036
|
+
transactionDepth = 0;
|
|
1037
|
+
}
|
|
1038
|
+
function beginTransaction() {
|
|
1039
|
+
transactionDepth += 1;
|
|
1040
|
+
}
|
|
1041
|
+
function commitTransaction() {
|
|
1042
|
+
if (transactionDepth === 0) return;
|
|
1043
|
+
transactionDepth -= 1;
|
|
1044
|
+
if (transactionDepth !== 0) return;
|
|
1045
|
+
const events = queuedEvents.splice(0);
|
|
1046
|
+
for (const entry of events) {
|
|
1047
|
+
publishQueued(entry);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
function publishQueued(entry) {
|
|
1051
|
+
publish(
|
|
1052
|
+
entry.event,
|
|
1053
|
+
entry.args
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
1056
|
+
function rollbackTransaction() {
|
|
1057
|
+
if (transactionDepth === 0) return;
|
|
1058
|
+
transactionDepth -= 1;
|
|
1059
|
+
if (transactionDepth === 0) {
|
|
1060
|
+
queuedEvents.length = 0;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
return {
|
|
1064
|
+
emit,
|
|
1065
|
+
emitImmediate,
|
|
1066
|
+
on,
|
|
1067
|
+
once,
|
|
1068
|
+
off,
|
|
1069
|
+
exportTrace,
|
|
1070
|
+
beginTransaction,
|
|
1071
|
+
commitTransaction,
|
|
1072
|
+
rollbackTransaction,
|
|
1073
|
+
reportUnhandledError,
|
|
1074
|
+
clear
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
// src/engine/command-runtime.ts
|
|
1079
|
+
var BATCH_COMMAND_METADATA = {
|
|
1080
|
+
history: "record"
|
|
1081
|
+
};
|
|
1082
|
+
function createCommandGuardRegistry() {
|
|
1083
|
+
const guards = [];
|
|
1084
|
+
function add(fn) {
|
|
1085
|
+
guards.push(fn);
|
|
1086
|
+
return () => {
|
|
1087
|
+
const index = guards.indexOf(fn);
|
|
1088
|
+
if (index !== -1) guards.splice(index, 1);
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
function run(name, args, metadata) {
|
|
1092
|
+
const command = Object.freeze({
|
|
1093
|
+
name,
|
|
1094
|
+
args: Object.freeze([...args]),
|
|
1095
|
+
metadata: Object.freeze({ ...metadata })
|
|
1096
|
+
});
|
|
1097
|
+
for (const guard of guards) {
|
|
1098
|
+
const result = guard(command);
|
|
1099
|
+
if (result !== true) {
|
|
1100
|
+
return result;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
return null;
|
|
1104
|
+
}
|
|
1105
|
+
return {
|
|
1106
|
+
add,
|
|
1107
|
+
run,
|
|
1108
|
+
clear: () => {
|
|
1109
|
+
guards.length = 0;
|
|
1110
|
+
}
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
function createValidator(deps) {
|
|
1114
|
+
return function validate(context) {
|
|
1115
|
+
const failures = validateState(deps.getState(), deps.getGrid(), context);
|
|
1116
|
+
for (const failure of failures) {
|
|
1117
|
+
deps.emitFailure(failure);
|
|
1118
|
+
}
|
|
1119
|
+
if (failures.length > 0) {
|
|
1120
|
+
throw new BoardInputError(
|
|
1121
|
+
`Board validation failed in ${context}: ${failures[0]?.message}`
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
function createBatchCommandController(deps) {
|
|
1127
|
+
let depth = 0;
|
|
1128
|
+
let startedAt = 0;
|
|
1129
|
+
let validationPending = false;
|
|
1130
|
+
let hasFailed = false;
|
|
1131
|
+
let failure;
|
|
1132
|
+
function flushBatchNotifications() {
|
|
1133
|
+
const pending = [...deps.batchCtrl.pending];
|
|
1134
|
+
deps.batchCtrl.pending.clear();
|
|
1135
|
+
deps.batchCtrl.rollbacks.clear();
|
|
1136
|
+
for (const flush of pending) flush();
|
|
1137
|
+
}
|
|
1138
|
+
function rollbackBatchNotifications() {
|
|
1139
|
+
const rollbacks = [...deps.batchCtrl.rollbacks];
|
|
1140
|
+
deps.batchCtrl.pending.clear();
|
|
1141
|
+
deps.batchCtrl.rollbacks.clear();
|
|
1142
|
+
for (const rollback of rollbacks) rollback();
|
|
1143
|
+
}
|
|
1144
|
+
function begin() {
|
|
1145
|
+
if (depth === 0) {
|
|
1146
|
+
startedAt = performance.now();
|
|
1147
|
+
deps.batchCtrl.depth += 1;
|
|
1148
|
+
deps.emitCommandBefore("batch", [], BATCH_COMMAND_METADATA);
|
|
1149
|
+
}
|
|
1150
|
+
depth += 1;
|
|
1151
|
+
}
|
|
1152
|
+
function end(beforePublish) {
|
|
1153
|
+
depth -= 1;
|
|
1154
|
+
if (depth !== 0) return;
|
|
1155
|
+
try {
|
|
1156
|
+
if (hasFailed) {
|
|
1157
|
+
throw failure;
|
|
1158
|
+
}
|
|
1159
|
+
if (validationPending) {
|
|
1160
|
+
deps.validate("batch");
|
|
1161
|
+
}
|
|
1162
|
+
beforePublish?.();
|
|
1163
|
+
deps.batchCtrl.depth -= 1;
|
|
1164
|
+
flushBatchNotifications();
|
|
1165
|
+
} catch (error) {
|
|
1166
|
+
deps.batchCtrl.depth -= 1;
|
|
1167
|
+
rollbackBatchNotifications();
|
|
1168
|
+
throw error;
|
|
1169
|
+
} finally {
|
|
1170
|
+
validationPending = false;
|
|
1171
|
+
hasFailed = false;
|
|
1172
|
+
failure = void 0;
|
|
1173
|
+
}
|
|
1174
|
+
deps.emitCommandAfter(
|
|
1175
|
+
"batch",
|
|
1176
|
+
[],
|
|
1177
|
+
performance.now() - startedAt,
|
|
1178
|
+
BATCH_COMMAND_METADATA
|
|
1179
|
+
);
|
|
1180
|
+
}
|
|
1181
|
+
function batch(fn, beforePublish) {
|
|
1182
|
+
begin();
|
|
1183
|
+
try {
|
|
1184
|
+
fn();
|
|
1185
|
+
} catch (error) {
|
|
1186
|
+
if (!hasFailed) {
|
|
1187
|
+
hasFailed = true;
|
|
1188
|
+
failure = error;
|
|
1189
|
+
}
|
|
1190
|
+
depth -= 1;
|
|
1191
|
+
if (depth === 0) {
|
|
1192
|
+
validationPending = false;
|
|
1193
|
+
deps.batchCtrl.depth -= 1;
|
|
1194
|
+
rollbackBatchNotifications();
|
|
1195
|
+
hasFailed = false;
|
|
1196
|
+
failure = void 0;
|
|
1197
|
+
}
|
|
1198
|
+
throw error;
|
|
1199
|
+
}
|
|
1200
|
+
end(beforePublish);
|
|
1201
|
+
}
|
|
1202
|
+
return {
|
|
1203
|
+
isBatching: () => depth > 0,
|
|
1204
|
+
markValidationPending: () => {
|
|
1205
|
+
validationPending = true;
|
|
1206
|
+
},
|
|
1207
|
+
begin,
|
|
1208
|
+
end,
|
|
1209
|
+
batch,
|
|
1210
|
+
flushBatchNotifications,
|
|
1211
|
+
rollbackBatchNotifications
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
// src/subscribable.ts
|
|
1216
|
+
function createBatchController() {
|
|
1217
|
+
return { depth: 0, pending: /* @__PURE__ */ new Set(), rollbacks: /* @__PURE__ */ new Set() };
|
|
1218
|
+
}
|
|
1219
|
+
function createSubscribable(initial, batch, reportError) {
|
|
1220
|
+
let current = initial;
|
|
1221
|
+
let prev = initial;
|
|
1222
|
+
let transactionPrev;
|
|
1223
|
+
let hasTransactionPrev = false;
|
|
1224
|
+
let destroyed = false;
|
|
1225
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1226
|
+
function assertActive() {
|
|
1227
|
+
if (destroyed) {
|
|
1228
|
+
throw new BoardDestroyedError();
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
function notify() {
|
|
1232
|
+
assertActive();
|
|
1233
|
+
if (batch.depth > 0) {
|
|
1234
|
+
batch.pending.add(flush);
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
flush();
|
|
1238
|
+
}
|
|
1239
|
+
function flush() {
|
|
1240
|
+
assertActive();
|
|
1241
|
+
const snapshot = current;
|
|
1242
|
+
const prevSnapshot = hasTransactionPrev ? transactionPrev : prev;
|
|
1243
|
+
prev = current;
|
|
1244
|
+
transactionPrev = void 0;
|
|
1245
|
+
hasTransactionPrev = false;
|
|
1246
|
+
for (const fn of listeners) {
|
|
1247
|
+
try {
|
|
1248
|
+
fn(snapshot, prevSnapshot);
|
|
1249
|
+
} catch (error) {
|
|
1250
|
+
reportError(error);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
return {
|
|
1255
|
+
get() {
|
|
1256
|
+
assertActive();
|
|
1257
|
+
return current;
|
|
1258
|
+
},
|
|
1259
|
+
set(value) {
|
|
1260
|
+
assertActive();
|
|
1261
|
+
if (batch.depth > 0 && !hasTransactionPrev) {
|
|
1262
|
+
transactionPrev = current;
|
|
1263
|
+
hasTransactionPrev = true;
|
|
1264
|
+
batch.rollbacks.add(rollback);
|
|
1265
|
+
}
|
|
1266
|
+
prev = current;
|
|
1267
|
+
current = value;
|
|
1268
|
+
notify();
|
|
1269
|
+
},
|
|
1270
|
+
subscribe(callback) {
|
|
1271
|
+
assertActive();
|
|
1272
|
+
listeners.add(callback);
|
|
1273
|
+
return () => {
|
|
1274
|
+
listeners.delete(callback);
|
|
1275
|
+
};
|
|
1276
|
+
},
|
|
1277
|
+
notify,
|
|
1278
|
+
destroy() {
|
|
1279
|
+
destroyed = true;
|
|
1280
|
+
listeners.clear();
|
|
1281
|
+
batch.pending.delete(flush);
|
|
1282
|
+
batch.rollbacks.delete(rollback);
|
|
1283
|
+
}
|
|
1284
|
+
};
|
|
1285
|
+
function rollback() {
|
|
1286
|
+
if (!hasTransactionPrev) return;
|
|
1287
|
+
current = transactionPrev;
|
|
1288
|
+
prev = transactionPrev;
|
|
1289
|
+
transactionPrev = void 0;
|
|
1290
|
+
hasTransactionPrev = false;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
// src/engine/options.ts
|
|
1295
|
+
function requireFinite(name, value) {
|
|
1296
|
+
if (!Number.isFinite(value)) {
|
|
1297
|
+
throw new BoardInputError(`${name} must be a finite number.`);
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
function requirePositive(name, value) {
|
|
1301
|
+
requireFinite(name, value);
|
|
1302
|
+
if (value <= 0) {
|
|
1303
|
+
throw new BoardInputError(`${name} must be greater than 0.`);
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
function validateCamera(camera) {
|
|
1307
|
+
requireFinite("camera.x", camera.x);
|
|
1308
|
+
requireFinite("camera.y", camera.y);
|
|
1309
|
+
requirePositive("camera.z", camera.z);
|
|
1310
|
+
}
|
|
1311
|
+
function validateGridSettings(grid) {
|
|
1312
|
+
requirePositive("grid.size", grid.size);
|
|
1313
|
+
requirePositive("grid.majorEvery", grid.majorEvery);
|
|
1314
|
+
requirePositive("grid.edgeSnapThreshold", grid.edgeSnapThreshold);
|
|
1315
|
+
if (!Number.isInteger(grid.majorEvery)) {
|
|
1316
|
+
throw new BoardInputError("grid.majorEvery must be an integer.");
|
|
1317
|
+
}
|
|
1318
|
+
if (typeof grid.snap !== "boolean" || typeof grid.edgeSnap !== "boolean") {
|
|
1319
|
+
throw new BoardInputError("grid snap settings must be boolean values.");
|
|
1320
|
+
}
|
|
1321
|
+
if (!["dot", "line", "cross", "none"].includes(grid.pattern)) {
|
|
1322
|
+
throw new BoardInputError(
|
|
1323
|
+
`Grid pattern "${String(grid.pattern)}" is unsupported.`
|
|
1324
|
+
);
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
function validateBoardConfiguration(config) {
|
|
1328
|
+
validateCamera(config.camera);
|
|
1329
|
+
requirePositive("zoom.min", config.zoom.min);
|
|
1330
|
+
requirePositive("zoom.max", config.zoom.max);
|
|
1331
|
+
if (config.zoom.min > config.zoom.max) {
|
|
1332
|
+
throw new BoardInputError(
|
|
1333
|
+
"zoom.min must be less than or equal to zoom.max."
|
|
1334
|
+
);
|
|
1335
|
+
}
|
|
1336
|
+
validateGridSettings(config.grid);
|
|
1337
|
+
if (!["autocad", "contain", "intersect"].includes(config.boxSelectBehavior)) {
|
|
1338
|
+
throw new BoardInputError(
|
|
1339
|
+
`Box-select behavior "${String(config.boxSelectBehavior)}" is unsupported.`
|
|
1340
|
+
);
|
|
1341
|
+
}
|
|
1342
|
+
requirePositive("nodes.minWidth", config.nodeConstraints.minWidth);
|
|
1343
|
+
requirePositive("nodes.minHeight", config.nodeConstraints.minHeight);
|
|
1344
|
+
requirePositive("nodes.defaultWidth", config.nodeConstraints.defaultWidth);
|
|
1345
|
+
requirePositive("nodes.defaultHeight", config.nodeConstraints.defaultHeight);
|
|
1346
|
+
if (config.nodeConstraints.defaultWidth < config.nodeConstraints.minWidth) {
|
|
1347
|
+
throw new BoardInputError(
|
|
1348
|
+
"nodes.defaultWidth must be greater than or equal to nodes.minWidth."
|
|
1349
|
+
);
|
|
1350
|
+
}
|
|
1351
|
+
if (config.nodeConstraints.defaultHeight < config.nodeConstraints.minHeight) {
|
|
1352
|
+
throw new BoardInputError(
|
|
1353
|
+
"nodes.defaultHeight must be greater than or equal to nodes.minHeight."
|
|
1354
|
+
);
|
|
1355
|
+
}
|
|
1356
|
+
if (typeof config.diagnostics === "object") {
|
|
1357
|
+
const limit = config.diagnostics.traceLimit;
|
|
1358
|
+
if (limit !== void 0 && (!Number.isInteger(limit) || limit < 0)) {
|
|
1359
|
+
throw new BoardInputError(
|
|
1360
|
+
"diagnostics.traceLimit must be a non-negative integer."
|
|
1361
|
+
);
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
const pluginNames = /* @__PURE__ */ new Set();
|
|
1365
|
+
for (const plugin of config.plugins) {
|
|
1366
|
+
if (pluginNames.has(plugin.name)) {
|
|
1367
|
+
throw new BoardConflictError(
|
|
1368
|
+
`Board plugin name "${plugin.name}" is registered more than once.`
|
|
1369
|
+
);
|
|
1370
|
+
}
|
|
1371
|
+
pluginNames.add(plugin.name);
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
// src/engine/subscribables.ts
|
|
1376
|
+
function createReactiveLayer(deps) {
|
|
1377
|
+
const { getState, getGrid, emit, getEffectiveNodes, reportSubscriberError } = deps;
|
|
1378
|
+
const initialState = getState();
|
|
1379
|
+
const initialGrid = getGrid();
|
|
1380
|
+
const batchCtrl = createBatchController();
|
|
1381
|
+
const $camera = createSubscribable(
|
|
1382
|
+
freezeClone({ ...initialState.camera }),
|
|
1383
|
+
batchCtrl,
|
|
1384
|
+
(error) => reportSubscriberError("$camera", error)
|
|
1385
|
+
);
|
|
1386
|
+
const $grid = createSubscribable(
|
|
1387
|
+
freezeClone({ ...initialGrid }),
|
|
1388
|
+
batchCtrl,
|
|
1389
|
+
(error) => reportSubscriberError("$grid", error)
|
|
1390
|
+
);
|
|
1391
|
+
const $nodes = createSubscribable(
|
|
1392
|
+
readonlyMapView(/* @__PURE__ */ new Map()),
|
|
1393
|
+
batchCtrl,
|
|
1394
|
+
(error) => reportSubscriberError("$nodes", error)
|
|
1395
|
+
);
|
|
1396
|
+
const $selection = createSubscribable(
|
|
1397
|
+
readonlySetView(new Set(initialState.selection)),
|
|
1398
|
+
batchCtrl,
|
|
1399
|
+
(error) => reportSubscriberError("$selection", error)
|
|
1400
|
+
);
|
|
1401
|
+
const $interaction = createSubscribable(
|
|
1402
|
+
cloneInteraction(initialState.interaction),
|
|
1403
|
+
batchCtrl,
|
|
1404
|
+
(error) => reportSubscriberError("$interaction", error)
|
|
1405
|
+
);
|
|
1406
|
+
const $snapGuides = createSubscribable(
|
|
1407
|
+
initialState.snapGuides.map((guide) => freezeClone({ ...guide })),
|
|
1408
|
+
batchCtrl,
|
|
1409
|
+
(error) => reportSubscriberError("$snapGuides", error)
|
|
1410
|
+
);
|
|
1411
|
+
let cachedPublicNodeMap = null;
|
|
1412
|
+
function getPublicNodeMap() {
|
|
1413
|
+
if (!cachedPublicNodeMap) {
|
|
1414
|
+
cachedPublicNodeMap = buildPublicNodeMap({ nodes: getEffectiveNodes() });
|
|
1415
|
+
}
|
|
1416
|
+
return cachedPublicNodeMap;
|
|
1417
|
+
}
|
|
1418
|
+
function invalidateNodeCache() {
|
|
1419
|
+
cachedPublicNodeMap = null;
|
|
1420
|
+
}
|
|
1421
|
+
function notifyNodesChanged() {
|
|
1422
|
+
cachedPublicNodeMap = null;
|
|
1423
|
+
if (batchCtrl.depth > 0) {
|
|
1424
|
+
batchCtrl.pending.add(publishNodes);
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1427
|
+
publishNodes();
|
|
1428
|
+
}
|
|
1429
|
+
function publishNodes() {
|
|
1430
|
+
$nodes.set(readonlyMapView(getPublicNodeMap()));
|
|
1431
|
+
}
|
|
1432
|
+
function notifyCameraChanged() {
|
|
1433
|
+
$camera.set(freezeClone({ ...getState().camera }));
|
|
1434
|
+
}
|
|
1435
|
+
function notifyGridChanged() {
|
|
1436
|
+
$grid.set(freezeClone({ ...getGrid() }));
|
|
1437
|
+
}
|
|
1438
|
+
function notifySelectionChanged() {
|
|
1439
|
+
$selection.set(readonlySetView(new Set(getState().selection)));
|
|
1440
|
+
}
|
|
1441
|
+
function notifyInteractionChanged() {
|
|
1442
|
+
$interaction.set(cloneInteraction(getState().interaction));
|
|
1443
|
+
}
|
|
1444
|
+
function notifySnapGuidesChanged() {
|
|
1445
|
+
$snapGuides.set(
|
|
1446
|
+
getState().snapGuides.map((guide) => freezeClone({ ...guide }))
|
|
1447
|
+
);
|
|
1448
|
+
}
|
|
1449
|
+
function setCamera(next) {
|
|
1450
|
+
validateCamera(next);
|
|
1451
|
+
const state = getState();
|
|
1452
|
+
const prev = { ...state.camera };
|
|
1453
|
+
if (prev.x === next.x && prev.y === next.y && prev.z === next.z) return;
|
|
1454
|
+
state.camera = next;
|
|
1455
|
+
$camera.set(freezeClone({ ...next }));
|
|
1456
|
+
emit("camera:change", freezeClone({ ...next }), freezeClone(prev));
|
|
1457
|
+
}
|
|
1458
|
+
function setSelection(nextSelection) {
|
|
1459
|
+
const state = getState();
|
|
1460
|
+
const prev = Array.from(state.selection.values());
|
|
1461
|
+
const next = Array.from(nextSelection);
|
|
1462
|
+
if (sameArray(prev, next)) return;
|
|
1463
|
+
state.selection = new Set(next);
|
|
1464
|
+
notifySelectionChanged();
|
|
1465
|
+
emit("selection:change", next, prev);
|
|
1466
|
+
}
|
|
1467
|
+
function setInteraction(next) {
|
|
1468
|
+
const state = getState();
|
|
1469
|
+
const prev = state.interaction;
|
|
1470
|
+
state.interaction = next;
|
|
1471
|
+
notifyInteractionChanged();
|
|
1472
|
+
if (prev.mode === "idle" && next.mode !== "idle") {
|
|
1473
|
+
emit("interaction:start", cloneInteraction(next));
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
if (prev.mode !== "idle" && next.mode === "idle") {
|
|
1477
|
+
emit("interaction:end", cloneInteraction(prev));
|
|
1478
|
+
return;
|
|
1479
|
+
}
|
|
1480
|
+
if (prev.mode !== "idle" && next.mode !== "idle") {
|
|
1481
|
+
emit("interaction:update", cloneInteraction(next));
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
function setSnapGuides(next) {
|
|
1485
|
+
const state = getState();
|
|
1486
|
+
state.snapGuides = next;
|
|
1487
|
+
notifySnapGuidesChanged();
|
|
1488
|
+
}
|
|
1489
|
+
function destroy() {
|
|
1490
|
+
$camera.destroy();
|
|
1491
|
+
$grid.destroy();
|
|
1492
|
+
$nodes.destroy();
|
|
1493
|
+
$selection.destroy();
|
|
1494
|
+
$interaction.destroy();
|
|
1495
|
+
$snapGuides.destroy();
|
|
1496
|
+
batchCtrl.pending.clear();
|
|
1497
|
+
batchCtrl.rollbacks.clear();
|
|
1498
|
+
}
|
|
1499
|
+
return {
|
|
1500
|
+
batchCtrl,
|
|
1501
|
+
$camera,
|
|
1502
|
+
$grid,
|
|
1503
|
+
$nodes,
|
|
1504
|
+
$selection,
|
|
1505
|
+
$interaction,
|
|
1506
|
+
$snapGuides,
|
|
1507
|
+
getPublicNodeMap,
|
|
1508
|
+
invalidateNodeCache,
|
|
1509
|
+
notifyNodesChanged,
|
|
1510
|
+
notifyCameraChanged,
|
|
1511
|
+
notifyGridChanged,
|
|
1512
|
+
notifySelectionChanged,
|
|
1513
|
+
notifyInteractionChanged,
|
|
1514
|
+
notifySnapGuidesChanged,
|
|
1515
|
+
setCamera,
|
|
1516
|
+
setSelection,
|
|
1517
|
+
setInteraction,
|
|
1518
|
+
setSnapGuides,
|
|
1519
|
+
destroy
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
// src/engine/transaction.ts
|
|
1524
|
+
function createTransactionExecutor(deps) {
|
|
1525
|
+
const defaultMetadata = { history: "record" };
|
|
1526
|
+
function prepare(name, args, metadata, emitBefore) {
|
|
1527
|
+
deps.assertAlive();
|
|
1528
|
+
const blockedReason = deps.runGuard(name, args, metadata);
|
|
1529
|
+
if (blockedReason) {
|
|
1530
|
+
deps.emitBlocked(name, args, metadata);
|
|
1531
|
+
throw deps.createBlockedError(name, args, blockedReason);
|
|
1532
|
+
}
|
|
1533
|
+
if (emitBefore) deps.emitBefore(name, args, metadata);
|
|
1534
|
+
}
|
|
1535
|
+
function runCommand(name, args, fn, metadata = defaultMetadata, commitOverride) {
|
|
1536
|
+
const inBatch = deps.isBatching();
|
|
1537
|
+
prepare(name, args, metadata, false);
|
|
1538
|
+
const started = performance.now();
|
|
1539
|
+
const ownsEffects = deps.canOwnEffects();
|
|
1540
|
+
if (ownsEffects) deps.beginEffects();
|
|
1541
|
+
if (!inBatch) deps.emitBefore(name, args, metadata);
|
|
1542
|
+
let historyBefore = ownsEffects ? deps.captureHistoryRoot() : null;
|
|
1543
|
+
const checkpoint = ownsEffects && metadata.validate !== false ? deps.beginPersistentTransaction() : null;
|
|
1544
|
+
try {
|
|
1545
|
+
historyBefore = deps.beforeExecute(name, metadata, historyBefore) ?? historyBefore;
|
|
1546
|
+
const result = fn();
|
|
1547
|
+
if (metadata.validate !== false) {
|
|
1548
|
+
if (inBatch) deps.markValidationPending();
|
|
1549
|
+
else deps.validate(name);
|
|
1550
|
+
}
|
|
1551
|
+
const commitBefore = commitOverride?.before ?? historyBefore;
|
|
1552
|
+
const preparedCommit = commitBefore ? deps.prepareCommit(
|
|
1553
|
+
commitOverride?.label ?? name,
|
|
1554
|
+
commitOverride?.metadata ?? metadata,
|
|
1555
|
+
commitBefore
|
|
1556
|
+
) : null;
|
|
1557
|
+
const commitErrors = preparedCommit?.finalize() ?? [];
|
|
1558
|
+
if (!inBatch) {
|
|
1559
|
+
deps.emitAfter(name, args, performance.now() - started, metadata);
|
|
1560
|
+
}
|
|
1561
|
+
if (ownsEffects) deps.commitEffects();
|
|
1562
|
+
for (const error of commitErrors) {
|
|
1563
|
+
deps.reportCommitError(preparedCommit.label, error);
|
|
1564
|
+
}
|
|
1565
|
+
return result;
|
|
1566
|
+
} catch (error) {
|
|
1567
|
+
if (checkpoint) deps.rollbackPersistentTransaction(checkpoint);
|
|
1568
|
+
if (ownsEffects) deps.rollbackEffects();
|
|
1569
|
+
throw error;
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
async function runAsyncCommand(name, args, fn, metadata = defaultMetadata) {
|
|
1573
|
+
prepare(name, args, metadata, false);
|
|
1574
|
+
const started = performance.now();
|
|
1575
|
+
try {
|
|
1576
|
+
const result = await fn();
|
|
1577
|
+
if (metadata.validate !== false) deps.validate(name);
|
|
1578
|
+
deps.emitBefore(name, args, metadata);
|
|
1579
|
+
deps.emitAfter(name, args, performance.now() - started, metadata);
|
|
1580
|
+
return result;
|
|
1581
|
+
} catch (error) {
|
|
1582
|
+
if (deps.isCancellation(error)) return void 0;
|
|
1583
|
+
throw error;
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
return { runCommand, runAsyncCommand };
|
|
1587
|
+
}
|
|
1588
|
+
function stagePersistentRoots(roots) {
|
|
1589
|
+
return {
|
|
1590
|
+
state: {
|
|
1591
|
+
camera: { ...roots.state.camera },
|
|
1592
|
+
nodes: new Map(roots.state.nodes),
|
|
1593
|
+
selection: new Set(roots.state.selection),
|
|
1594
|
+
interaction: cloneInteraction(roots.state.interaction),
|
|
1595
|
+
snapGuides: roots.state.snapGuides.map((guide) => ({ ...guide })),
|
|
1596
|
+
nextZIndex: roots.state.nextZIndex
|
|
1597
|
+
},
|
|
1598
|
+
grid: { ...roots.grid },
|
|
1599
|
+
pluginStates: new Map(
|
|
1600
|
+
Array.from(roots.pluginStates, ([name, pluginState]) => [
|
|
1601
|
+
name,
|
|
1602
|
+
{ state: pluginState.state }
|
|
1603
|
+
])
|
|
1604
|
+
)
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
// src/colors.ts
|
|
1609
|
+
var BOARD_COLOR_PRESETS = [
|
|
1610
|
+
{ preset: "1", hex: "#e5476a", label: "Rose" },
|
|
1611
|
+
{ preset: "2", hex: "#d97a1c", label: "Amber" },
|
|
1612
|
+
{ preset: "3", hex: "#b89a14", label: "Citron" },
|
|
1613
|
+
{ preset: "4", hex: "#2fa560", label: "Moss" },
|
|
1614
|
+
{ preset: "5", hex: "#3b7de0", label: "Azure" },
|
|
1615
|
+
{ preset: "6", hex: "#7e5ae4", label: "Violet" }
|
|
1616
|
+
];
|
|
1617
|
+
function isBoardColorPreset(value) {
|
|
1618
|
+
return typeof value === "string" && BOARD_COLOR_PRESETS.some((option) => option.preset === value);
|
|
1619
|
+
}
|
|
1620
|
+
function colorForPreset(preset) {
|
|
1621
|
+
if (!preset) {
|
|
1622
|
+
return "";
|
|
1623
|
+
}
|
|
1624
|
+
return BOARD_COLOR_PRESETS.find((option) => option.preset === preset)?.hex ?? "";
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
// src/engine/persistence.ts
|
|
1628
|
+
var JSON_CANVAS_NODE_TYPES = /* @__PURE__ */ new Set([
|
|
1629
|
+
"text",
|
|
1630
|
+
"file",
|
|
1631
|
+
"link",
|
|
1632
|
+
"group"
|
|
1633
|
+
]);
|
|
1634
|
+
var JSON_CANVAS_SIDES = /* @__PURE__ */ new Set([
|
|
1635
|
+
"top",
|
|
1636
|
+
"right",
|
|
1637
|
+
"bottom",
|
|
1638
|
+
"left"
|
|
1639
|
+
]);
|
|
1640
|
+
var JSON_CANVAS_EDGE_ENDS = /* @__PURE__ */ new Set(["none", "arrow"]);
|
|
1641
|
+
var JSON_CANVAS_BACKGROUND_STYLES = /* @__PURE__ */ new Set(["cover", "ratio", "repeat"]);
|
|
1642
|
+
function isJsonCanvasNodeType(value) {
|
|
1643
|
+
return typeof value === "string" && JSON_CANVAS_NODE_TYPES.has(value);
|
|
1644
|
+
}
|
|
1645
|
+
function normalizeNodeType(value) {
|
|
1646
|
+
if (value === void 0) return "text";
|
|
1647
|
+
if (isJsonCanvasNodeType(value)) return value;
|
|
1648
|
+
throw new BoardInputError(
|
|
1649
|
+
`Unsupported JSON Canvas node type "${String(value)}".`
|
|
1650
|
+
);
|
|
1651
|
+
}
|
|
1652
|
+
function withNodeFields(base, input) {
|
|
1653
|
+
switch (base.type) {
|
|
1654
|
+
case "file":
|
|
1655
|
+
return {
|
|
1656
|
+
...base,
|
|
1657
|
+
file: typeof input.file === "string" ? input.file : "",
|
|
1658
|
+
...typeof input.subpath === "string" ? { subpath: input.subpath } : {}
|
|
1659
|
+
};
|
|
1660
|
+
case "link":
|
|
1661
|
+
return {
|
|
1662
|
+
...base,
|
|
1663
|
+
url: typeof input.url === "string" ? input.url : ""
|
|
1664
|
+
};
|
|
1665
|
+
case "group":
|
|
1666
|
+
return {
|
|
1667
|
+
...base,
|
|
1668
|
+
...typeof input.label === "string" ? { label: input.label } : {},
|
|
1669
|
+
...typeof input.background === "string" ? { background: input.background } : {},
|
|
1670
|
+
...input.backgroundStyle === "cover" || input.backgroundStyle === "ratio" || input.backgroundStyle === "repeat" ? { backgroundStyle: input.backgroundStyle } : {}
|
|
1671
|
+
};
|
|
1672
|
+
case "text":
|
|
1673
|
+
default:
|
|
1674
|
+
return {
|
|
1675
|
+
...base,
|
|
1676
|
+
text: typeof input.text === "string" ? input.text : ""
|
|
1677
|
+
};
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
function validateJsonCanvasNodeFields(node) {
|
|
1681
|
+
if (node.color !== void 0 && !isBoardColorPreset(node.color) && !/^#[0-9a-fA-F]{6}$/.test(node.color)) {
|
|
1682
|
+
throw new BoardInputError(
|
|
1683
|
+
`Invalid board document: node "${node.id}" has invalid color.`
|
|
1684
|
+
);
|
|
1685
|
+
}
|
|
1686
|
+
if (node.type === "text" && typeof node.text !== "string") {
|
|
1687
|
+
throw new BoardInputError(
|
|
1688
|
+
`Invalid board document: text node "${node.id}" is missing required text.`
|
|
1689
|
+
);
|
|
1690
|
+
}
|
|
1691
|
+
if (node.type === "file" && typeof node.file !== "string") {
|
|
1692
|
+
throw new BoardInputError(
|
|
1693
|
+
`Invalid board document: file node "${node.id}" is missing required file.`
|
|
1694
|
+
);
|
|
1695
|
+
}
|
|
1696
|
+
if (node.type === "link" && typeof node.url !== "string") {
|
|
1697
|
+
throw new BoardInputError(
|
|
1698
|
+
`Invalid board document: link node "${node.id}" is missing required url.`
|
|
1699
|
+
);
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
function jsonNodeToBoardNode(node, meta, index) {
|
|
1703
|
+
validateJsonCanvasNodeFields(node);
|
|
1704
|
+
const base = {
|
|
1705
|
+
id: node.id,
|
|
1706
|
+
type: node.type,
|
|
1707
|
+
x: node.x,
|
|
1708
|
+
y: node.y,
|
|
1709
|
+
width: node.width,
|
|
1710
|
+
height: node.height,
|
|
1711
|
+
...node.color !== void 0 ? { color: node.color } : {},
|
|
1712
|
+
zIndex: Number.isFinite(meta?.zIndex) ? meta.zIndex : index + 1,
|
|
1713
|
+
locked: Boolean(meta?.locked),
|
|
1714
|
+
visible: meta?.visible !== false,
|
|
1715
|
+
...typeof meta?.parentId === "string" ? { parentId: meta.parentId } : {}
|
|
1716
|
+
};
|
|
1717
|
+
return withNodeFields(base, node);
|
|
1718
|
+
}
|
|
1719
|
+
function boardNodeToJsonNode(node) {
|
|
1720
|
+
const base = {
|
|
1721
|
+
id: node.id,
|
|
1722
|
+
type: node.type,
|
|
1723
|
+
x: node.x,
|
|
1724
|
+
y: node.y,
|
|
1725
|
+
width: node.width,
|
|
1726
|
+
height: node.height,
|
|
1727
|
+
...node.color !== void 0 ? { color: node.color } : {}
|
|
1728
|
+
};
|
|
1729
|
+
switch (node.type) {
|
|
1730
|
+
case "file":
|
|
1731
|
+
return {
|
|
1732
|
+
...base,
|
|
1733
|
+
type: "file",
|
|
1734
|
+
file: node.file ?? "",
|
|
1735
|
+
...node.subpath !== void 0 ? { subpath: node.subpath } : {}
|
|
1736
|
+
};
|
|
1737
|
+
case "link":
|
|
1738
|
+
return { ...base, type: "link", url: node.url ?? "" };
|
|
1739
|
+
case "group":
|
|
1740
|
+
return {
|
|
1741
|
+
...base,
|
|
1742
|
+
type: "group",
|
|
1743
|
+
...node.label !== void 0 ? { label: node.label } : {},
|
|
1744
|
+
...node.background !== void 0 ? { background: node.background } : {},
|
|
1745
|
+
...node.backgroundStyle !== void 0 ? { backgroundStyle: node.backgroundStyle } : {}
|
|
1746
|
+
};
|
|
1747
|
+
case "text":
|
|
1748
|
+
default:
|
|
1749
|
+
return { ...base, type: "text", text: node.text ?? "" };
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
function mergeMetadata(base, patch) {
|
|
1753
|
+
if (!patch) return base;
|
|
1754
|
+
return {
|
|
1755
|
+
...base,
|
|
1756
|
+
...patch,
|
|
1757
|
+
nodes: base.nodes || patch.nodes ? { ...base.nodes ?? {}, ...patch.nodes ?? {} } : void 0,
|
|
1758
|
+
edges: base.edges || patch.edges ? { ...base.edges ?? {}, ...patch.edges ?? {} } : void 0
|
|
1759
|
+
};
|
|
1760
|
+
}
|
|
1761
|
+
function getDocumentMetadata(document) {
|
|
1762
|
+
return document["x-vue-board"];
|
|
1763
|
+
}
|
|
1764
|
+
function toPersistedDocument(snapshot, featureDocuments) {
|
|
1765
|
+
let metadata = {
|
|
1766
|
+
camera: snapshot.camera,
|
|
1767
|
+
grid: snapshot.grid,
|
|
1768
|
+
selection: snapshot.selection,
|
|
1769
|
+
nextZIndex: snapshot.nextZIndex,
|
|
1770
|
+
nodes: Object.fromEntries(
|
|
1771
|
+
snapshot.nodes.map((node) => [
|
|
1772
|
+
node.id,
|
|
1773
|
+
{
|
|
1774
|
+
zIndex: node.zIndex,
|
|
1775
|
+
locked: node.locked,
|
|
1776
|
+
visible: node.visible,
|
|
1777
|
+
...node.parentId !== void 0 ? { parentId: node.parentId } : {}
|
|
1778
|
+
}
|
|
1779
|
+
])
|
|
1780
|
+
)
|
|
1781
|
+
};
|
|
1782
|
+
let edges;
|
|
1783
|
+
for (const featureDocument of featureDocuments) {
|
|
1784
|
+
if (featureDocument.edges !== void 0) {
|
|
1785
|
+
edges = featureDocument.edges;
|
|
1786
|
+
}
|
|
1787
|
+
metadata = mergeMetadata(metadata, getDocumentMetadata(featureDocument));
|
|
1788
|
+
}
|
|
1789
|
+
return {
|
|
1790
|
+
nodes: snapshot.nodes.map((node) => boardNodeToJsonNode(node)),
|
|
1791
|
+
...edges !== void 0 ? { edges } : {},
|
|
1792
|
+
"x-vue-board": metadata
|
|
1793
|
+
};
|
|
1794
|
+
}
|
|
1795
|
+
function isRecord(value) {
|
|
1796
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
1797
|
+
}
|
|
1798
|
+
function assertRecord(value, message) {
|
|
1799
|
+
if (!isRecord(value)) {
|
|
1800
|
+
throw new BoardInputError(message);
|
|
1801
|
+
}
|
|
1802
|
+
return value;
|
|
1803
|
+
}
|
|
1804
|
+
function assertOptionalFiniteNumber(value, message) {
|
|
1805
|
+
if (value !== void 0 && !Number.isFinite(value)) {
|
|
1806
|
+
throw new BoardInputError(message);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
function assertOptionalBoolean(value, message) {
|
|
1810
|
+
if (value !== void 0 && typeof value !== "boolean") {
|
|
1811
|
+
throw new BoardInputError(message);
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
function validateDocumentMetadata(metadata) {
|
|
1815
|
+
if (metadata === void 0) return;
|
|
1816
|
+
const meta = assertRecord(
|
|
1817
|
+
metadata,
|
|
1818
|
+
"Invalid board document: board metadata must be an object."
|
|
1819
|
+
);
|
|
1820
|
+
if (meta.camera !== void 0) {
|
|
1821
|
+
const camera = assertRecord(
|
|
1822
|
+
meta.camera,
|
|
1823
|
+
"Invalid board document: board metadata camera must be an object."
|
|
1824
|
+
);
|
|
1825
|
+
for (const key of ["x", "y", "z"]) {
|
|
1826
|
+
assertOptionalFiniteNumber(
|
|
1827
|
+
camera[key],
|
|
1828
|
+
`Invalid board document: board metadata camera.${key} must be finite.`
|
|
1829
|
+
);
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
if (meta.grid !== void 0) {
|
|
1833
|
+
const grid = assertRecord(
|
|
1834
|
+
meta.grid,
|
|
1835
|
+
"Invalid board document: board metadata grid must be an object."
|
|
1836
|
+
);
|
|
1837
|
+
for (const key of ["size", "majorEvery", "edgeSnapThreshold"]) {
|
|
1838
|
+
assertOptionalFiniteNumber(
|
|
1839
|
+
grid[key],
|
|
1840
|
+
`Invalid board document: board metadata grid.${key} must be finite.`
|
|
1841
|
+
);
|
|
1842
|
+
}
|
|
1843
|
+
assertOptionalBoolean(
|
|
1844
|
+
grid.snap,
|
|
1845
|
+
"Invalid board document: board metadata grid.snap must be boolean."
|
|
1846
|
+
);
|
|
1847
|
+
assertOptionalBoolean(
|
|
1848
|
+
grid.edgeSnap,
|
|
1849
|
+
"Invalid board document: board metadata grid.edgeSnap must be boolean."
|
|
1850
|
+
);
|
|
1851
|
+
if (grid.pattern !== void 0 && grid.pattern !== "dot" && grid.pattern !== "line" && grid.pattern !== "cross" && grid.pattern !== "none") {
|
|
1852
|
+
throw new BoardInputError(
|
|
1853
|
+
`Invalid board document: board metadata grid.pattern "${String(grid.pattern)}" is unsupported.`
|
|
1854
|
+
);
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
if (meta.selection !== void 0 && !Array.isArray(meta.selection)) {
|
|
1858
|
+
throw new BoardInputError(
|
|
1859
|
+
"Invalid board document: board metadata selection must be an array."
|
|
1860
|
+
);
|
|
1861
|
+
}
|
|
1862
|
+
if (Array.isArray(meta.selection) && meta.selection.some((id) => typeof id !== "string")) {
|
|
1863
|
+
throw new BoardInputError(
|
|
1864
|
+
"Invalid board document: board metadata selection must contain only node IDs."
|
|
1865
|
+
);
|
|
1866
|
+
}
|
|
1867
|
+
assertOptionalFiniteNumber(
|
|
1868
|
+
meta.nextZIndex,
|
|
1869
|
+
"Invalid board document: board metadata nextZIndex must be finite."
|
|
1870
|
+
);
|
|
1871
|
+
if (meta.nextZIndex !== void 0 && (!Number.isInteger(meta.nextZIndex) || meta.nextZIndex < 1)) {
|
|
1872
|
+
throw new BoardInputError(
|
|
1873
|
+
"Invalid board document: board metadata nextZIndex must be a positive integer."
|
|
1874
|
+
);
|
|
1875
|
+
}
|
|
1876
|
+
if (meta.nodes !== void 0) {
|
|
1877
|
+
const nodes = assertRecord(
|
|
1878
|
+
meta.nodes,
|
|
1879
|
+
"Invalid board document: board metadata nodes must be an object."
|
|
1880
|
+
);
|
|
1881
|
+
for (const [id, nodeMeta] of Object.entries(nodes)) {
|
|
1882
|
+
const node = assertRecord(
|
|
1883
|
+
nodeMeta,
|
|
1884
|
+
`Invalid board document: metadata for node "${id}" must be an object.`
|
|
1885
|
+
);
|
|
1886
|
+
assertOptionalFiniteNumber(
|
|
1887
|
+
node.zIndex,
|
|
1888
|
+
`Invalid board document: metadata for node "${id}" has invalid zIndex.`
|
|
1889
|
+
);
|
|
1890
|
+
assertOptionalBoolean(
|
|
1891
|
+
node.locked,
|
|
1892
|
+
`Invalid board document: metadata for node "${id}" has invalid locked flag.`
|
|
1893
|
+
);
|
|
1894
|
+
assertOptionalBoolean(
|
|
1895
|
+
node.visible,
|
|
1896
|
+
`Invalid board document: metadata for node "${id}" has invalid visible flag.`
|
|
1897
|
+
);
|
|
1898
|
+
if (node.parentId !== void 0 && typeof node.parentId !== "string") {
|
|
1899
|
+
throw new BoardInputError(
|
|
1900
|
+
`Invalid board document: metadata for node "${id}" has invalid parentId.`
|
|
1901
|
+
);
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
if (meta.edges !== void 0) {
|
|
1906
|
+
const edges = assertRecord(
|
|
1907
|
+
meta.edges,
|
|
1908
|
+
"Invalid board document: board metadata edges must be an object."
|
|
1909
|
+
);
|
|
1910
|
+
for (const [id, edgeMeta] of Object.entries(edges)) {
|
|
1911
|
+
const edge = assertRecord(
|
|
1912
|
+
edgeMeta,
|
|
1913
|
+
`Invalid board document: metadata for edge "${id}" must be an object.`
|
|
1914
|
+
);
|
|
1915
|
+
assertOptionalFiniteNumber(
|
|
1916
|
+
edge.zIndex,
|
|
1917
|
+
`Invalid board document: metadata for edge "${id}" has invalid zIndex.`
|
|
1918
|
+
);
|
|
1919
|
+
if (edge.data !== void 0 && !isRecord(edge.data)) {
|
|
1920
|
+
throw new BoardInputError(
|
|
1921
|
+
`Invalid board document: metadata for edge "${id}" has invalid data.`
|
|
1922
|
+
);
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
function normalizeDocumentForImport(raw) {
|
|
1928
|
+
const parsed = assertRecord(
|
|
1929
|
+
raw,
|
|
1930
|
+
"Invalid board document: document must be an object."
|
|
1931
|
+
);
|
|
1932
|
+
if (!Array.isArray(parsed.nodes)) {
|
|
1933
|
+
throw new BoardInputError("Invalid board document: missing nodes array.");
|
|
1934
|
+
}
|
|
1935
|
+
for (const key of [
|
|
1936
|
+
"camera",
|
|
1937
|
+
"grid",
|
|
1938
|
+
"selection",
|
|
1939
|
+
"interaction",
|
|
1940
|
+
"snapGuides",
|
|
1941
|
+
"nextZIndex"
|
|
1942
|
+
]) {
|
|
1943
|
+
if (key in parsed) {
|
|
1944
|
+
throw new BoardInputError(
|
|
1945
|
+
`Invalid board document: runtime field "${key}" belongs under x-vue-board.`
|
|
1946
|
+
);
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
validateDocumentMetadata(getDocumentMetadata(parsed));
|
|
1950
|
+
const seenNodes = /* @__PURE__ */ new Set();
|
|
1951
|
+
const nodes = parsed.nodes.map((node) => {
|
|
1952
|
+
if (!isRecord(node)) {
|
|
1953
|
+
throw new BoardInputError(
|
|
1954
|
+
"Invalid board document: node entries must be objects."
|
|
1955
|
+
);
|
|
1956
|
+
}
|
|
1957
|
+
if (typeof node.id !== "string" || typeof node.x !== "number" || typeof node.y !== "number" || typeof node.width !== "number" || typeof node.height !== "number" || !Number.isFinite(node.x) || !Number.isFinite(node.y) || !Number.isFinite(node.width) || !Number.isFinite(node.height) || node.width <= 0 || node.height <= 0) {
|
|
1958
|
+
throw new BoardInputError(
|
|
1959
|
+
`Invalid board document: node "${String(node.id ?? "?")}" has invalid geometry.`
|
|
1960
|
+
);
|
|
1961
|
+
}
|
|
1962
|
+
if (!isJsonCanvasNodeType(node.type)) {
|
|
1963
|
+
throw new BoardInputError(
|
|
1964
|
+
`Invalid board document: node "${String(node.id)}" has unsupported type "${String(node.type)}".`
|
|
1965
|
+
);
|
|
1966
|
+
}
|
|
1967
|
+
if (node.type === "group" && node.backgroundStyle !== void 0 && !JSON_CANVAS_BACKGROUND_STYLES.has(String(node.backgroundStyle))) {
|
|
1968
|
+
throw new BoardInputError(
|
|
1969
|
+
`Invalid board document: node "${String(node.id)}" has unsupported backgroundStyle "${String(node.backgroundStyle)}".`
|
|
1970
|
+
);
|
|
1971
|
+
}
|
|
1972
|
+
const normalized = { ...node };
|
|
1973
|
+
validateJsonCanvasNodeFields(normalized);
|
|
1974
|
+
if (seenNodes.has(normalized.id)) {
|
|
1975
|
+
throw new BoardInputError(
|
|
1976
|
+
`Invalid board document: duplicate node id "${normalized.id}".`
|
|
1977
|
+
);
|
|
1978
|
+
}
|
|
1979
|
+
seenNodes.add(normalized.id);
|
|
1980
|
+
return normalized;
|
|
1981
|
+
});
|
|
1982
|
+
let edges;
|
|
1983
|
+
if (parsed.edges !== void 0) {
|
|
1984
|
+
if (!Array.isArray(parsed.edges)) {
|
|
1985
|
+
throw new BoardInputError(
|
|
1986
|
+
"Invalid board document: edges must be an array."
|
|
1987
|
+
);
|
|
1988
|
+
}
|
|
1989
|
+
const seenEdges = /* @__PURE__ */ new Set();
|
|
1990
|
+
edges = parsed.edges.map((edge) => {
|
|
1991
|
+
if (!isRecord(edge)) {
|
|
1992
|
+
throw new BoardInputError(
|
|
1993
|
+
"Invalid board document: edge entries must be objects."
|
|
1994
|
+
);
|
|
1995
|
+
}
|
|
1996
|
+
if (typeof edge.id !== "string" || typeof edge.fromNode !== "string" || typeof edge.toNode !== "string") {
|
|
1997
|
+
throw new BoardInputError(
|
|
1998
|
+
`Invalid board document: edge "${String(edge.id ?? "?")}" has invalid endpoints.`
|
|
1999
|
+
);
|
|
2000
|
+
}
|
|
2001
|
+
const id = edge.id;
|
|
2002
|
+
const fromNode = edge.fromNode;
|
|
2003
|
+
const toNode = edge.toNode;
|
|
2004
|
+
if (seenEdges.has(id)) {
|
|
2005
|
+
throw new BoardInputError(
|
|
2006
|
+
`Invalid board document: duplicate edge id "${id}".`
|
|
2007
|
+
);
|
|
2008
|
+
}
|
|
2009
|
+
seenEdges.add(id);
|
|
2010
|
+
if (!seenNodes.has(fromNode) || !seenNodes.has(toNode)) {
|
|
2011
|
+
throw new BoardInputError(
|
|
2012
|
+
`Invalid board document: edge "${id}" references a missing node.`
|
|
2013
|
+
);
|
|
2014
|
+
}
|
|
2015
|
+
if (edge.fromSide !== void 0 && !JSON_CANVAS_SIDES.has(edge.fromSide)) {
|
|
2016
|
+
throw new BoardInputError(
|
|
2017
|
+
`Invalid board document: edge "${id}" has unsupported fromSide "${String(edge.fromSide)}".`
|
|
2018
|
+
);
|
|
2019
|
+
}
|
|
2020
|
+
if (edge.toSide !== void 0 && !JSON_CANVAS_SIDES.has(edge.toSide)) {
|
|
2021
|
+
throw new BoardInputError(
|
|
2022
|
+
`Invalid board document: edge "${id}" has unsupported toSide "${String(edge.toSide)}".`
|
|
2023
|
+
);
|
|
2024
|
+
}
|
|
2025
|
+
if (edge.fromEnd !== void 0 && !JSON_CANVAS_EDGE_ENDS.has(edge.fromEnd)) {
|
|
2026
|
+
throw new BoardInputError(
|
|
2027
|
+
`Invalid board document: edge "${id}" has unsupported fromEnd "${String(edge.fromEnd)}".`
|
|
2028
|
+
);
|
|
2029
|
+
}
|
|
2030
|
+
if (edge.toEnd !== void 0 && !JSON_CANVAS_EDGE_ENDS.has(edge.toEnd)) {
|
|
2031
|
+
throw new BoardInputError(
|
|
2032
|
+
`Invalid board document: edge "${id}" has unsupported toEnd "${String(edge.toEnd)}".`
|
|
2033
|
+
);
|
|
2034
|
+
}
|
|
2035
|
+
if (edge.label !== void 0 && typeof edge.label !== "string") {
|
|
2036
|
+
throw new BoardInputError(
|
|
2037
|
+
`Invalid board document: edge "${id}" has invalid label.`
|
|
2038
|
+
);
|
|
2039
|
+
}
|
|
2040
|
+
return { ...edge, id, fromNode, toNode };
|
|
2041
|
+
});
|
|
2042
|
+
}
|
|
2043
|
+
return {
|
|
2044
|
+
nodes,
|
|
2045
|
+
...edges !== void 0 ? { edges } : {},
|
|
2046
|
+
...getDocumentMetadata(parsed) !== void 0 ? { "x-vue-board": getDocumentMetadata(parsed) } : {}
|
|
2047
|
+
};
|
|
2048
|
+
}
|
|
2049
|
+
function materializeSnapshotNodes(snapshot) {
|
|
2050
|
+
return [...snapshot.nodes];
|
|
2051
|
+
}
|
|
2052
|
+
function documentToSnapshot(document) {
|
|
2053
|
+
const metadata = getDocumentMetadata(document);
|
|
2054
|
+
const nodes = document.nodes.map(
|
|
2055
|
+
(node, index) => normalizeExistingNode(
|
|
2056
|
+
jsonNodeToBoardNode(node, metadata?.nodes?.[node.id], index)
|
|
2057
|
+
)
|
|
2058
|
+
);
|
|
2059
|
+
const gridSettings = {
|
|
2060
|
+
...DEFAULT_GRID,
|
|
2061
|
+
...metadata?.grid ?? {}
|
|
2062
|
+
};
|
|
2063
|
+
const selection = Array.isArray(metadata?.selection) ? metadata.selection.filter(
|
|
2064
|
+
(id) => typeof id === "string" && nodes.some((node) => node.id === id)
|
|
2065
|
+
).map((id) => id) : [];
|
|
2066
|
+
const nextZIndex = metadata?.nextZIndex ?? nodes.reduce((max, node) => Math.max(max, node.zIndex), 0) + 1;
|
|
2067
|
+
const camera = { ...DEFAULT_CAMERA, ...metadata?.camera ?? {} };
|
|
2068
|
+
const snapshot = {
|
|
2069
|
+
camera,
|
|
2070
|
+
grid: gridSettings,
|
|
2071
|
+
nodes,
|
|
2072
|
+
selection,
|
|
2073
|
+
interaction: { mode: "idle" },
|
|
2074
|
+
snapGuides: [],
|
|
2075
|
+
nextZIndex
|
|
2076
|
+
};
|
|
2077
|
+
const failures = validateState(
|
|
2078
|
+
{
|
|
2079
|
+
camera,
|
|
2080
|
+
grid: gridSettings,
|
|
2081
|
+
nodes: new Map(nodes.map((node) => [node.id, node])),
|
|
2082
|
+
selection: new Set(selection),
|
|
2083
|
+
interaction: { mode: "idle" },
|
|
2084
|
+
snapGuides: []
|
|
2085
|
+
},
|
|
2086
|
+
gridSettings,
|
|
2087
|
+
"loadDocument"
|
|
2088
|
+
);
|
|
2089
|
+
if (failures.length > 0) {
|
|
2090
|
+
throw new BoardInputError(
|
|
2091
|
+
`Invalid board document: ${failures[0]?.message ?? "invariant failed."}`
|
|
2092
|
+
);
|
|
2093
|
+
}
|
|
2094
|
+
return snapshot;
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
// src/engine/camera-session.ts
|
|
2098
|
+
function createCameraSession(deps) {
|
|
2099
|
+
let animationToken = 0;
|
|
2100
|
+
function cancelAnimations() {
|
|
2101
|
+
animationToken += 1;
|
|
2102
|
+
}
|
|
2103
|
+
async function animateTo(target) {
|
|
2104
|
+
validateCamera(target);
|
|
2105
|
+
animationToken += 1;
|
|
2106
|
+
const token = animationToken;
|
|
2107
|
+
const start = { ...deps.getCamera() };
|
|
2108
|
+
const started = performance.now();
|
|
2109
|
+
const duration = 280;
|
|
2110
|
+
const { raf } = getAnimationFrameDriver();
|
|
2111
|
+
await new Promise((resolve, reject) => {
|
|
2112
|
+
const tick = () => {
|
|
2113
|
+
if (token !== animationToken) {
|
|
2114
|
+
reject(new AnimationCancelled());
|
|
2115
|
+
return;
|
|
2116
|
+
}
|
|
2117
|
+
const elapsed = performance.now() - started;
|
|
2118
|
+
const t = clamp(elapsed / duration, 0, 1);
|
|
2119
|
+
const eased = 1 - Math.pow(1 - t, 3);
|
|
2120
|
+
deps.setCamera(lerpCamera(start, target, eased));
|
|
2121
|
+
if (t < 1) raf(tick);
|
|
2122
|
+
else resolve();
|
|
2123
|
+
};
|
|
2124
|
+
raf(tick);
|
|
2125
|
+
});
|
|
2126
|
+
}
|
|
2127
|
+
function computeFit(ids, padding = 40) {
|
|
2128
|
+
const idSet = ids ? new Set(ids) : null;
|
|
2129
|
+
const source = Array.from(deps.getNodes()).filter(
|
|
2130
|
+
(node) => node.visible && (!idSet || idSet.has(node.id))
|
|
2131
|
+
);
|
|
2132
|
+
if (source.length === 0) return null;
|
|
2133
|
+
const bounds = source.reduce((acc, node) => {
|
|
2134
|
+
const current = getBoundsFromNode(node);
|
|
2135
|
+
return {
|
|
2136
|
+
minX: Math.min(acc.minX, current.minX),
|
|
2137
|
+
minY: Math.min(acc.minY, current.minY),
|
|
2138
|
+
maxX: Math.max(acc.maxX, current.maxX),
|
|
2139
|
+
maxY: Math.max(acc.maxY, current.maxY)
|
|
2140
|
+
};
|
|
2141
|
+
}, getBoundsFromNode(source[0]));
|
|
2142
|
+
const viewport = deps.getViewportSize();
|
|
2143
|
+
const width = Math.max(1, bounds.maxX - bounds.minX);
|
|
2144
|
+
const height = Math.max(1, bounds.maxY - bounds.minY);
|
|
2145
|
+
const zoomLevel = clamp(
|
|
2146
|
+
Math.min(
|
|
2147
|
+
(viewport.x - padding * 2) / width,
|
|
2148
|
+
(viewport.y - padding * 2) / height
|
|
2149
|
+
),
|
|
2150
|
+
deps.zoom.min,
|
|
2151
|
+
deps.zoom.max
|
|
2152
|
+
);
|
|
2153
|
+
const center = {
|
|
2154
|
+
x: (bounds.minX + bounds.maxX) / 2,
|
|
2155
|
+
y: (bounds.minY + bounds.maxY) / 2
|
|
2156
|
+
};
|
|
2157
|
+
return {
|
|
2158
|
+
x: viewport.x / (2 * zoomLevel) - center.x,
|
|
2159
|
+
y: viewport.y / (2 * zoomLevel) - center.y,
|
|
2160
|
+
z: zoomLevel
|
|
2161
|
+
};
|
|
2162
|
+
}
|
|
2163
|
+
return { animateTo, cancelAnimations, computeFit };
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
// src/engine/node-shape.ts
|
|
2167
|
+
function assertValidNodeGeometry(id, geometry) {
|
|
2168
|
+
if (!Number.isFinite(geometry.x) || !Number.isFinite(geometry.y) || !Number.isFinite(geometry.width) || !Number.isFinite(geometry.height) || geometry.width <= 0 || geometry.height <= 0) {
|
|
2169
|
+
throw new BoardInputError(`Invalid node geometry for "${id}".`);
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
function assertValidParentLink(nodes, id, parentId) {
|
|
2173
|
+
if (parentId === void 0) return;
|
|
2174
|
+
if (parentId === id) {
|
|
2175
|
+
throw new BoardInputError(`Node "${id}" cannot be its own parent.`);
|
|
2176
|
+
}
|
|
2177
|
+
const parent = nodes.get(parentId);
|
|
2178
|
+
if (!parent) {
|
|
2179
|
+
throw new BoardInputError(
|
|
2180
|
+
`Node "${id}" references missing parent "${parentId}".`
|
|
2181
|
+
);
|
|
2182
|
+
}
|
|
2183
|
+
if (parent.type !== "group") {
|
|
2184
|
+
throw new BoardInputError(
|
|
2185
|
+
`Node "${id}" parent "${parentId}" must be a group.`
|
|
2186
|
+
);
|
|
2187
|
+
}
|
|
2188
|
+
let walk = parent;
|
|
2189
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2190
|
+
while (walk) {
|
|
2191
|
+
if (walk.id === id || seen.has(walk.id)) {
|
|
2192
|
+
throw new BoardInputError(`Node "${id}" cannot create a parent cycle.`);
|
|
2193
|
+
}
|
|
2194
|
+
seen.add(walk.id);
|
|
2195
|
+
walk = walk.parentId ? nodes.get(walk.parentId) : void 0;
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
function normalizeNodeInput(input, context) {
|
|
2199
|
+
const { nodes, grid, constraints } = context;
|
|
2200
|
+
const rawPoint = { x: input.x ?? 0, y: input.y ?? 0 };
|
|
2201
|
+
const point = grid.snap ? snapPoint(rawPoint, grid.size) : rawPoint;
|
|
2202
|
+
const width = grid.snap ? snapSize(
|
|
2203
|
+
input.width ?? constraints.defaultWidth,
|
|
2204
|
+
grid.size,
|
|
2205
|
+
constraints.minWidth
|
|
2206
|
+
) : input.width ?? constraints.defaultWidth;
|
|
2207
|
+
const height = grid.snap ? snapSize(
|
|
2208
|
+
input.height ?? constraints.defaultHeight,
|
|
2209
|
+
grid.size,
|
|
2210
|
+
constraints.minHeight
|
|
2211
|
+
) : input.height ?? constraints.defaultHeight;
|
|
2212
|
+
const type = normalizeNodeType(input.type);
|
|
2213
|
+
const parentId = typeof input.parentId === "string" && input.parentId.length > 0 ? input.parentId : void 0;
|
|
2214
|
+
const id = input.id ?? createNodeId();
|
|
2215
|
+
if (nodes.has(id)) {
|
|
2216
|
+
throw new BoardConflictError(
|
|
2217
|
+
`Cannot create node: node "${id}" already exists.`
|
|
2218
|
+
);
|
|
2219
|
+
}
|
|
2220
|
+
assertValidNodeGeometry(id, { x: point.x, y: point.y, width, height });
|
|
2221
|
+
assertValidParentLink(nodes, id, parentId);
|
|
2222
|
+
return {
|
|
2223
|
+
node: withNodeFields(
|
|
2224
|
+
{
|
|
2225
|
+
id,
|
|
2226
|
+
type,
|
|
2227
|
+
x: point.x,
|
|
2228
|
+
y: point.y,
|
|
2229
|
+
width,
|
|
2230
|
+
height,
|
|
2231
|
+
color: input.color,
|
|
2232
|
+
zIndex: context.nextZIndex,
|
|
2233
|
+
locked: Boolean(input.locked),
|
|
2234
|
+
visible: input.visible !== false,
|
|
2235
|
+
parentId
|
|
2236
|
+
},
|
|
2237
|
+
input
|
|
2238
|
+
),
|
|
2239
|
+
nextZIndex: context.nextZIndex + 1
|
|
2240
|
+
};
|
|
2241
|
+
}
|
|
2242
|
+
function applyNodePatchToNode(node, patch, context) {
|
|
2243
|
+
const invalidFields = node.type === "text" ? ["file", "subpath", "url", "label", "background", "backgroundStyle"] : node.type === "file" ? ["text", "url", "label", "background", "backgroundStyle"] : node.type === "link" ? [
|
|
2244
|
+
"text",
|
|
2245
|
+
"file",
|
|
2246
|
+
"subpath",
|
|
2247
|
+
"label",
|
|
2248
|
+
"background",
|
|
2249
|
+
"backgroundStyle"
|
|
2250
|
+
] : ["text", "file", "subpath", "url"];
|
|
2251
|
+
const invalidField = invalidFields.find((field) => field in patch);
|
|
2252
|
+
if (invalidField) {
|
|
2253
|
+
throw new BoardInputError(
|
|
2254
|
+
`Cannot update ${node.type} node "${node.id}" with field "${invalidField}".`
|
|
2255
|
+
);
|
|
2256
|
+
}
|
|
2257
|
+
const nextCommon = {
|
|
2258
|
+
x: patch.x ?? node.x,
|
|
2259
|
+
y: patch.y ?? node.y,
|
|
2260
|
+
width: patch.width ?? node.width,
|
|
2261
|
+
height: patch.height ?? node.height,
|
|
2262
|
+
parentId: "parentId" in patch ? patch.parentId : node.parentId,
|
|
2263
|
+
color: "color" in patch ? patch.color : node.color,
|
|
2264
|
+
locked: patch.locked ?? node.locked,
|
|
2265
|
+
visible: patch.visible ?? node.visible
|
|
2266
|
+
};
|
|
2267
|
+
const { grid, constraints, nodes } = context;
|
|
2268
|
+
const x = grid.snap ? snapValue(nextCommon.x, grid.size) : nextCommon.x;
|
|
2269
|
+
const y = grid.snap ? snapValue(nextCommon.y, grid.size) : nextCommon.y;
|
|
2270
|
+
const width = grid.snap ? snapSize(nextCommon.width, grid.size, constraints.minWidth) : nextCommon.width;
|
|
2271
|
+
const height = grid.snap ? snapSize(nextCommon.height, grid.size, constraints.minHeight) : nextCommon.height;
|
|
2272
|
+
assertValidNodeGeometry(node.id, { x, y, width, height });
|
|
2273
|
+
assertValidParentLink(nodes, node.id, nextCommon.parentId);
|
|
2274
|
+
const common = {
|
|
2275
|
+
id: node.id,
|
|
2276
|
+
x,
|
|
2277
|
+
y,
|
|
2278
|
+
width,
|
|
2279
|
+
height,
|
|
2280
|
+
color: nextCommon.color,
|
|
2281
|
+
zIndex: node.zIndex,
|
|
2282
|
+
locked: nextCommon.locked,
|
|
2283
|
+
visible: nextCommon.visible,
|
|
2284
|
+
parentId: nextCommon.parentId
|
|
2285
|
+
};
|
|
2286
|
+
switch (node.type) {
|
|
2287
|
+
case "text":
|
|
2288
|
+
return { ...common, type: "text", text: patch.text ?? node.text };
|
|
2289
|
+
case "file":
|
|
2290
|
+
return {
|
|
2291
|
+
...common,
|
|
2292
|
+
type: "file",
|
|
2293
|
+
file: patch.file ?? node.file,
|
|
2294
|
+
subpath: "subpath" in patch ? patch.subpath : node.subpath
|
|
2295
|
+
};
|
|
2296
|
+
case "link":
|
|
2297
|
+
return { ...common, type: "link", url: patch.url ?? node.url };
|
|
2298
|
+
case "group":
|
|
2299
|
+
return {
|
|
2300
|
+
...common,
|
|
2301
|
+
type: "group",
|
|
2302
|
+
label: "label" in patch ? patch.label : node.label,
|
|
2303
|
+
background: "background" in patch ? patch.background : node.background,
|
|
2304
|
+
backgroundStyle: "backgroundStyle" in patch ? patch.backgroundStyle : node.backgroundStyle
|
|
2305
|
+
};
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
// src/engine.ts
|
|
2310
|
+
var CommandBlockedError = class extends BoardError {
|
|
2311
|
+
constructor(command, args, reason) {
|
|
2312
|
+
super(`Command "${command}" was blocked: ${reason}`);
|
|
2313
|
+
this.command = command;
|
|
2314
|
+
this.args = args;
|
|
2315
|
+
this.reason = reason;
|
|
2316
|
+
this.name = "CommandBlockedError";
|
|
2317
|
+
}
|
|
2318
|
+
command;
|
|
2319
|
+
args;
|
|
2320
|
+
reason;
|
|
2321
|
+
};
|
|
2322
|
+
var CONNECTIONS_FEATURE_NAME = "connections";
|
|
2323
|
+
var RECORD_COMMAND = { history: "record" };
|
|
2324
|
+
var IGNORE_COMMAND = { history: "ignore" };
|
|
2325
|
+
var IGNORE_UNVALIDATED_COMMAND = {
|
|
2326
|
+
history: "ignore",
|
|
2327
|
+
validate: false
|
|
2328
|
+
};
|
|
2329
|
+
function requireFiniteInput(name, ...values) {
|
|
2330
|
+
if (values.some((value) => !Number.isFinite(value))) {
|
|
2331
|
+
throw new BoardInputError(`${name} must contain only finite numbers.`);
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
function requireNonNegativeInput(name, value) {
|
|
2335
|
+
requireFiniteInput(name, value);
|
|
2336
|
+
if (value < 0) {
|
|
2337
|
+
throw new BoardInputError(`${name} must be greater than or equal to 0.`);
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
function createBoardEngine(options = {}) {
|
|
2341
|
+
const camera = { ...DEFAULT_CAMERA, ...options.camera };
|
|
2342
|
+
const zoom = { ...DEFAULT_ZOOM, ...options.zoom };
|
|
2343
|
+
let grid = { ...DEFAULT_GRID, ...options.grid };
|
|
2344
|
+
const boxSelectBehavior = options.boxSelect?.behavior ?? "autocad";
|
|
2345
|
+
const nodeConstraints = {
|
|
2346
|
+
...DEFAULT_NODE_CONSTRAINTS,
|
|
2347
|
+
...options.nodes
|
|
2348
|
+
};
|
|
2349
|
+
const diagnosticsEnabled = Boolean(options.diagnostics);
|
|
2350
|
+
const traceLimit = typeof options.diagnostics === "object" && options.diagnostics.traceLimit !== void 0 ? options.diagnostics.traceLimit : 500;
|
|
2351
|
+
validateBoardConfiguration({
|
|
2352
|
+
camera,
|
|
2353
|
+
zoom,
|
|
2354
|
+
grid,
|
|
2355
|
+
nodeConstraints,
|
|
2356
|
+
plugins: options.plugins ?? [],
|
|
2357
|
+
diagnostics: options.diagnostics,
|
|
2358
|
+
boxSelectBehavior
|
|
2359
|
+
});
|
|
2360
|
+
for (const plugin of options.plugins ?? []) {
|
|
2361
|
+
assertInternalBoardPlugin(plugin);
|
|
2362
|
+
}
|
|
2363
|
+
const eventBus = createEventBus({
|
|
2364
|
+
diagnosticsEnabled,
|
|
2365
|
+
traceLimit,
|
|
2366
|
+
onUnhandledError: options.onUnhandledError
|
|
2367
|
+
});
|
|
2368
|
+
const { emit, emitImmediate, on, once, off, reportUnhandledError } = eventBus;
|
|
2369
|
+
const commandGuards = createCommandGuardRegistry();
|
|
2370
|
+
const commitProjectors = /* @__PURE__ */ new Set();
|
|
2371
|
+
const nodeDeletedHooks = /* @__PURE__ */ new Set();
|
|
2372
|
+
const pluginCleanups = /* @__PURE__ */ new Map();
|
|
2373
|
+
let pluginStates = /* @__PURE__ */ new Map();
|
|
2374
|
+
const pluginPersistence = /* @__PURE__ */ new Map();
|
|
2375
|
+
function notifyNodeDeletedPlugins(nodeId) {
|
|
2376
|
+
for (const hook of nodeDeletedHooks) hook(nodeId);
|
|
2377
|
+
}
|
|
2378
|
+
const clipboard = [];
|
|
2379
|
+
const plugins = {};
|
|
2380
|
+
let viewportSize = { ...DEFAULT_VIEWPORT_SIZE };
|
|
2381
|
+
let destroyed = false;
|
|
2382
|
+
let finalizingCommitEffects = false;
|
|
2383
|
+
let activeGestureHistoryRoot = null;
|
|
2384
|
+
let activeBoxSelectionBefore = null;
|
|
2385
|
+
const nodeOverrides = /* @__PURE__ */ new Map();
|
|
2386
|
+
let indexedNodeRoot = null;
|
|
2387
|
+
let snapEdgeIndex = null;
|
|
2388
|
+
function getSnapEdgeIndex() {
|
|
2389
|
+
if (indexedNodeRoot !== state.nodes || !snapEdgeIndex) {
|
|
2390
|
+
indexedNodeRoot = state.nodes;
|
|
2391
|
+
snapEdgeIndex = buildSnapEdgeIndex(state.nodes.values());
|
|
2392
|
+
}
|
|
2393
|
+
return snapEdgeIndex;
|
|
2394
|
+
}
|
|
2395
|
+
function assertAlive() {
|
|
2396
|
+
if (destroyed) {
|
|
2397
|
+
throw new BoardDestroyedError();
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
function assertMutationAllowed() {
|
|
2401
|
+
if (finalizingCommitEffects) {
|
|
2402
|
+
throw new BoardConflictError(
|
|
2403
|
+
"Board mutations are unavailable during commit-effect finalization."
|
|
2404
|
+
);
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
function assertCommandReady() {
|
|
2408
|
+
assertAlive();
|
|
2409
|
+
assertMutationAllowed();
|
|
2410
|
+
}
|
|
2411
|
+
let state = {
|
|
2412
|
+
camera,
|
|
2413
|
+
nodes: /* @__PURE__ */ new Map(),
|
|
2414
|
+
selection: /* @__PURE__ */ new Set(),
|
|
2415
|
+
interaction: { mode: "idle" },
|
|
2416
|
+
snapGuides: [],
|
|
2417
|
+
nextZIndex: 1
|
|
2418
|
+
};
|
|
2419
|
+
const initialDocument = options.initialDocument ? normalizeDocumentForImport(options.initialDocument) : null;
|
|
2420
|
+
if (initialDocument) {
|
|
2421
|
+
const initial = documentToSnapshot(initialDocument);
|
|
2422
|
+
state.camera = { ...initial.camera };
|
|
2423
|
+
state.selection = /* @__PURE__ */ new Set();
|
|
2424
|
+
state.nextZIndex = initial.nextZIndex;
|
|
2425
|
+
Object.assign(grid, { ...initial.grid });
|
|
2426
|
+
for (const rawNode of materializeSnapshotNodes(initial)) {
|
|
2427
|
+
const normalized = normalizeExistingNode(rawNode);
|
|
2428
|
+
state.nodes.set(normalized.id, normalized);
|
|
2429
|
+
state.nextZIndex = Math.max(state.nextZIndex, normalized.zIndex + 1);
|
|
2430
|
+
}
|
|
2431
|
+
state.selection = new Set(
|
|
2432
|
+
initial.selection.filter((id) => state.nodes.has(id))
|
|
2433
|
+
);
|
|
2434
|
+
}
|
|
2435
|
+
for (const node of options.initialNodes ?? []) {
|
|
2436
|
+
const normalized = normalizeExistingNode(node);
|
|
2437
|
+
if (state.nodes.has(normalized.id)) {
|
|
2438
|
+
throw new BoardConflictError(
|
|
2439
|
+
`Cannot initialize board: node "${normalized.id}" is duplicated.`
|
|
2440
|
+
);
|
|
2441
|
+
}
|
|
2442
|
+
state.nodes.set(normalized.id, normalized);
|
|
2443
|
+
state.nextZIndex = Math.max(state.nextZIndex, normalized.zIndex + 1);
|
|
2444
|
+
}
|
|
2445
|
+
const reactive = createReactiveLayer({
|
|
2446
|
+
getState: () => state,
|
|
2447
|
+
getGrid: () => grid,
|
|
2448
|
+
emit,
|
|
2449
|
+
reportSubscriberError: (channel, error) => reportUnhandledError(error, { source: "subscriber", channel }),
|
|
2450
|
+
getEffectiveNodes: () => {
|
|
2451
|
+
if (nodeOverrides.size === 0) return state.nodes;
|
|
2452
|
+
const effective = new Map(state.nodes);
|
|
2453
|
+
for (const [id, node] of nodeOverrides) effective.set(id, node);
|
|
2454
|
+
return effective;
|
|
2455
|
+
}
|
|
2456
|
+
});
|
|
2457
|
+
const {
|
|
2458
|
+
batchCtrl,
|
|
2459
|
+
$camera,
|
|
2460
|
+
$grid,
|
|
2461
|
+
$nodes,
|
|
2462
|
+
$selection,
|
|
2463
|
+
$interaction,
|
|
2464
|
+
$snapGuides,
|
|
2465
|
+
getPublicNodeMap,
|
|
2466
|
+
invalidateNodeCache,
|
|
2467
|
+
notifyNodesChanged,
|
|
2468
|
+
notifyCameraChanged,
|
|
2469
|
+
notifyGridChanged,
|
|
2470
|
+
notifySelectionChanged,
|
|
2471
|
+
notifyInteractionChanged,
|
|
2472
|
+
notifySnapGuidesChanged,
|
|
2473
|
+
setCamera,
|
|
2474
|
+
setSelection,
|
|
2475
|
+
setInteraction,
|
|
2476
|
+
setSnapGuides,
|
|
2477
|
+
destroy: destroyReactiveLayer
|
|
2478
|
+
} = reactive;
|
|
2479
|
+
const cameraSession = createCameraSession({
|
|
2480
|
+
getCamera: () => state.camera,
|
|
2481
|
+
getNodes: () => state.nodes.values(),
|
|
2482
|
+
getViewportSize: () => viewportSize,
|
|
2483
|
+
setCamera,
|
|
2484
|
+
zoom
|
|
2485
|
+
});
|
|
2486
|
+
const batches = createBatchCommandController({
|
|
2487
|
+
batchCtrl,
|
|
2488
|
+
emitCommandBefore: (name, args, metadata) => emit("command:before", name, args, metadata),
|
|
2489
|
+
emitCommandAfter: (name, args, duration, metadata) => emit("command:after", name, args, duration, metadata),
|
|
2490
|
+
validate: (ctx) => validate(ctx)
|
|
2491
|
+
});
|
|
2492
|
+
function getGridSettings() {
|
|
2493
|
+
assertAlive();
|
|
2494
|
+
return freezeClone({ ...grid });
|
|
2495
|
+
}
|
|
2496
|
+
function resolveBoxSelectMode(startScreenPoint, currentScreenPoint) {
|
|
2497
|
+
if (boxSelectBehavior === "contain") {
|
|
2498
|
+
return "window";
|
|
2499
|
+
}
|
|
2500
|
+
if (boxSelectBehavior === "intersect") {
|
|
2501
|
+
return "crossing";
|
|
2502
|
+
}
|
|
2503
|
+
return currentScreenPoint.x >= startScreenPoint.x ? "window" : "crossing";
|
|
2504
|
+
}
|
|
2505
|
+
function getViewportSize() {
|
|
2506
|
+
assertAlive();
|
|
2507
|
+
return freezeClone({ ...viewportSize });
|
|
2508
|
+
}
|
|
2509
|
+
function materializeNode2(node) {
|
|
2510
|
+
return materializeNode(node);
|
|
2511
|
+
}
|
|
2512
|
+
function getState() {
|
|
2513
|
+
assertAlive();
|
|
2514
|
+
return buildPublicState(state, grid, getPublicNodeMap());
|
|
2515
|
+
}
|
|
2516
|
+
function beginPersistentTransaction() {
|
|
2517
|
+
const roots = { state, grid, pluginStates };
|
|
2518
|
+
const checkpoint = {
|
|
2519
|
+
roots,
|
|
2520
|
+
clipboard: [...clipboard],
|
|
2521
|
+
nodeOverrides: new Map(nodeOverrides),
|
|
2522
|
+
activeGestureHistoryRoot,
|
|
2523
|
+
activeBoxSelectionBefore
|
|
2524
|
+
};
|
|
2525
|
+
const candidate = stagePersistentRoots(roots);
|
|
2526
|
+
state = candidate.state;
|
|
2527
|
+
grid = candidate.grid;
|
|
2528
|
+
pluginStates = candidate.pluginStates;
|
|
2529
|
+
return checkpoint;
|
|
2530
|
+
}
|
|
2531
|
+
function rollbackPersistentTransaction(checkpoint) {
|
|
2532
|
+
state = checkpoint.roots.state;
|
|
2533
|
+
grid = checkpoint.roots.grid;
|
|
2534
|
+
pluginStates = checkpoint.roots.pluginStates;
|
|
2535
|
+
clipboard.splice(0, clipboard.length, ...checkpoint.clipboard);
|
|
2536
|
+
nodeOverrides.clear();
|
|
2537
|
+
for (const [id, node] of checkpoint.nodeOverrides) {
|
|
2538
|
+
nodeOverrides.set(id, node);
|
|
2539
|
+
}
|
|
2540
|
+
activeGestureHistoryRoot = checkpoint.activeGestureHistoryRoot;
|
|
2541
|
+
activeBoxSelectionBefore = checkpoint.activeBoxSelectionBefore;
|
|
2542
|
+
invalidateNodeCache();
|
|
2543
|
+
}
|
|
2544
|
+
function captureHistoryRoot() {
|
|
2545
|
+
return {
|
|
2546
|
+
nodes: state.nodes,
|
|
2547
|
+
grid: freezeClone({ ...grid }),
|
|
2548
|
+
selection: state.selection,
|
|
2549
|
+
nextZIndex: state.nextZIndex,
|
|
2550
|
+
pluginSlices: new Map(
|
|
2551
|
+
Array.from(pluginStates, ([name, pluginState]) => [
|
|
2552
|
+
name,
|
|
2553
|
+
pluginState.state
|
|
2554
|
+
])
|
|
2555
|
+
)
|
|
2556
|
+
};
|
|
2557
|
+
}
|
|
2558
|
+
function sameHistoryRoot(left, right) {
|
|
2559
|
+
if (left.nextZIndex !== right.nextZIndex) return false;
|
|
2560
|
+
if (left.grid.size !== right.grid.size || left.grid.majorEvery !== right.grid.majorEvery || left.grid.snap !== right.grid.snap || left.grid.edgeSnap !== right.grid.edgeSnap || left.grid.edgeSnapThreshold !== right.grid.edgeSnapThreshold || left.grid.pattern !== right.grid.pattern) {
|
|
2561
|
+
return false;
|
|
2562
|
+
}
|
|
2563
|
+
if (left.nodes.size !== right.nodes.size) return false;
|
|
2564
|
+
for (const [id, node] of left.nodes) {
|
|
2565
|
+
if (right.nodes.get(id) !== node) return false;
|
|
2566
|
+
}
|
|
2567
|
+
if (left.selection.size !== right.selection.size) return false;
|
|
2568
|
+
for (const id of left.selection) {
|
|
2569
|
+
if (!right.selection.has(id)) return false;
|
|
2570
|
+
}
|
|
2571
|
+
if (left.pluginSlices.size !== right.pluginSlices.size) return false;
|
|
2572
|
+
for (const [name, slice] of left.pluginSlices) {
|
|
2573
|
+
if (right.pluginSlices.get(name) !== slice) return false;
|
|
2574
|
+
}
|
|
2575
|
+
return true;
|
|
2576
|
+
}
|
|
2577
|
+
function prepareCommit(label, metadata, before) {
|
|
2578
|
+
const after = captureHistoryRoot();
|
|
2579
|
+
if (sameHistoryRoot(before, after)) return null;
|
|
2580
|
+
const commit = Object.freeze({
|
|
2581
|
+
label,
|
|
2582
|
+
timestamp: Date.now(),
|
|
2583
|
+
metadata,
|
|
2584
|
+
before,
|
|
2585
|
+
after
|
|
2586
|
+
});
|
|
2587
|
+
const effects = Array.from(commitProjectors, (project) => project(commit));
|
|
2588
|
+
return {
|
|
2589
|
+
label,
|
|
2590
|
+
finalize() {
|
|
2591
|
+
const errors = [];
|
|
2592
|
+
finalizingCommitEffects = true;
|
|
2593
|
+
try {
|
|
2594
|
+
for (const effect of effects) {
|
|
2595
|
+
try {
|
|
2596
|
+
effect();
|
|
2597
|
+
} catch (error) {
|
|
2598
|
+
errors.push(error);
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
} finally {
|
|
2602
|
+
finalizingCommitEffects = false;
|
|
2603
|
+
}
|
|
2604
|
+
return errors;
|
|
2605
|
+
}
|
|
2606
|
+
};
|
|
2607
|
+
}
|
|
2608
|
+
const validate = createValidator({
|
|
2609
|
+
getState: () => getState(),
|
|
2610
|
+
getGrid: () => grid,
|
|
2611
|
+
emitFailure: (failure) => emitImmediate("validation:failed", failure)
|
|
2612
|
+
});
|
|
2613
|
+
function discardActiveInteraction() {
|
|
2614
|
+
if (state.interaction.mode === "idle" && nodeOverrides.size === 0) return;
|
|
2615
|
+
const hadNodeOverrides = nodeOverrides.size > 0;
|
|
2616
|
+
const selectionBefore = state.interaction.mode === "box-select" ? activeBoxSelectionBefore : null;
|
|
2617
|
+
nodeOverrides.clear();
|
|
2618
|
+
activeGestureHistoryRoot = null;
|
|
2619
|
+
activeBoxSelectionBefore = null;
|
|
2620
|
+
setSnapGuides([]);
|
|
2621
|
+
if (selectionBefore) setSelection(selectionBefore);
|
|
2622
|
+
setInteraction({ mode: "idle" });
|
|
2623
|
+
if (hadNodeOverrides) notifyNodesChanged();
|
|
2624
|
+
}
|
|
2625
|
+
const { runCommand, runAsyncCommand } = createTransactionExecutor({
|
|
2626
|
+
assertAlive: assertCommandReady,
|
|
2627
|
+
runGuard: (name, args, metadata) => commandGuards.run(name, args, metadata),
|
|
2628
|
+
emitBlocked: (name, args, metadata) => emitImmediate("command:blocked", name, args, metadata),
|
|
2629
|
+
emitBefore: (name, args, metadata) => emit("command:before", name, args, metadata),
|
|
2630
|
+
emitAfter: (name, args, duration, metadata) => emit("command:after", name, args, duration, metadata),
|
|
2631
|
+
createBlockedError: (name, args, reason) => new CommandBlockedError(name, args, reason),
|
|
2632
|
+
isBatching: () => batches.isBatching(),
|
|
2633
|
+
canOwnEffects: () => batchCtrl.depth === 0,
|
|
2634
|
+
beginEffects: () => {
|
|
2635
|
+
batchCtrl.depth += 1;
|
|
2636
|
+
eventBus.beginTransaction();
|
|
2637
|
+
},
|
|
2638
|
+
commitEffects: () => {
|
|
2639
|
+
batchCtrl.depth -= 1;
|
|
2640
|
+
batches.flushBatchNotifications();
|
|
2641
|
+
eventBus.commitTransaction();
|
|
2642
|
+
},
|
|
2643
|
+
rollbackEffects: () => {
|
|
2644
|
+
batchCtrl.depth -= 1;
|
|
2645
|
+
batches.rollbackBatchNotifications();
|
|
2646
|
+
eventBus.rollbackTransaction();
|
|
2647
|
+
},
|
|
2648
|
+
markValidationPending: () => batches.markValidationPending(),
|
|
2649
|
+
captureHistoryRoot,
|
|
2650
|
+
beginPersistentTransaction,
|
|
2651
|
+
rollbackPersistentTransaction,
|
|
2652
|
+
beforeExecute: (name, metadata, historyBefore) => {
|
|
2653
|
+
const restoresBoxSelection = state.interaction.mode === "box-select" && activeBoxSelectionBefore !== null;
|
|
2654
|
+
if (metadata.history === "record" && name !== "endInteraction") {
|
|
2655
|
+
discardActiveInteraction();
|
|
2656
|
+
if (restoresBoxSelection && historyBefore) {
|
|
2657
|
+
return {
|
|
2658
|
+
...historyBefore,
|
|
2659
|
+
selection: new Set(state.selection)
|
|
2660
|
+
};
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
return null;
|
|
2664
|
+
},
|
|
2665
|
+
prepareCommit,
|
|
2666
|
+
reportCommitError: (label, error) => reportUnhandledError(error, {
|
|
2667
|
+
source: "commit-effect",
|
|
2668
|
+
commit: label
|
|
2669
|
+
}),
|
|
2670
|
+
validate,
|
|
2671
|
+
isCancellation: (error) => error instanceof AnimationCancelled
|
|
2672
|
+
});
|
|
2673
|
+
function assertBoardNode(id) {
|
|
2674
|
+
const node = nodeOverrides.get(id) ?? state.nodes.get(id);
|
|
2675
|
+
if (!node) {
|
|
2676
|
+
throw new BoardNotFoundError(`Node "${id}" does not exist.`);
|
|
2677
|
+
}
|
|
2678
|
+
return node;
|
|
2679
|
+
}
|
|
2680
|
+
function setNodeOverride(node) {
|
|
2681
|
+
nodeOverrides.set(node.id, node);
|
|
2682
|
+
return node;
|
|
2683
|
+
}
|
|
2684
|
+
function commitNodeOverrides(interaction) {
|
|
2685
|
+
for (const [id, next] of nodeOverrides) {
|
|
2686
|
+
const before = state.nodes.get(id);
|
|
2687
|
+
if (!before || before === next) continue;
|
|
2688
|
+
state.nodes.set(id, next);
|
|
2689
|
+
if (interaction.mode === "dragging-nodes") {
|
|
2690
|
+
emit("node:moved", materializeNode2(next), {
|
|
2691
|
+
x: next.x - before.x,
|
|
2692
|
+
y: next.y - before.y
|
|
2693
|
+
});
|
|
2694
|
+
emit("node:updated", materializeNode2(next), materializeNode2(before));
|
|
2695
|
+
} else if (id === interaction.nodeId) {
|
|
2696
|
+
emitNodeResize(before, next);
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
nodeOverrides.clear();
|
|
2700
|
+
notifyNodesChanged();
|
|
2701
|
+
}
|
|
2702
|
+
function normalizeNode(input) {
|
|
2703
|
+
const normalized = normalizeNodeInput(input, {
|
|
2704
|
+
nodes: state.nodes,
|
|
2705
|
+
grid,
|
|
2706
|
+
constraints: nodeConstraints,
|
|
2707
|
+
nextZIndex: state.nextZIndex
|
|
2708
|
+
});
|
|
2709
|
+
state.nextZIndex = normalized.nextZIndex;
|
|
2710
|
+
return normalized.node;
|
|
2711
|
+
}
|
|
2712
|
+
function applyNodePatch(node, patch) {
|
|
2713
|
+
return applyNodePatchToNode(node, patch, {
|
|
2714
|
+
nodes: state.nodes,
|
|
2715
|
+
grid,
|
|
2716
|
+
constraints: nodeConstraints
|
|
2717
|
+
});
|
|
2718
|
+
}
|
|
2719
|
+
function replaceBoardNodeWithoutNotify(node, next) {
|
|
2720
|
+
const stored = next;
|
|
2721
|
+
state.nodes.set(node.id, stored);
|
|
2722
|
+
invalidateNodeCache();
|
|
2723
|
+
return stored;
|
|
2724
|
+
}
|
|
2725
|
+
function replaceBoardNode(node, next) {
|
|
2726
|
+
const stored = replaceBoardNodeWithoutNotify(node, next);
|
|
2727
|
+
notifyNodesChanged();
|
|
2728
|
+
return stored;
|
|
2729
|
+
}
|
|
2730
|
+
function emitNodeResize(before, after) {
|
|
2731
|
+
const publicNode = materializeNode2(after);
|
|
2732
|
+
emit("node:resized", publicNode, {
|
|
2733
|
+
x: before.x,
|
|
2734
|
+
y: before.y,
|
|
2735
|
+
width: before.width,
|
|
2736
|
+
height: before.height
|
|
2737
|
+
});
|
|
2738
|
+
emit("node:updated", publicNode, materializeNode2(before));
|
|
2739
|
+
}
|
|
2740
|
+
function getPublicNode(id) {
|
|
2741
|
+
return materializeNode2(assertBoardNode(id));
|
|
2742
|
+
}
|
|
2743
|
+
function getDirectChildren(parentId) {
|
|
2744
|
+
return Array.from(state.nodes.values()).filter(
|
|
2745
|
+
(node) => node.parentId === parentId
|
|
2746
|
+
);
|
|
2747
|
+
}
|
|
2748
|
+
function collectSubtreeIdSet(rootId, into) {
|
|
2749
|
+
collectSubtreeIds(rootId, state.nodes, into);
|
|
2750
|
+
}
|
|
2751
|
+
function forestIdsFromSeeds(seedIds) {
|
|
2752
|
+
const out = /* @__PURE__ */ new Set();
|
|
2753
|
+
for (const id of seedIds) {
|
|
2754
|
+
if (state.nodes.has(id)) {
|
|
2755
|
+
collectSubtreeIdSet(id, out);
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
return out;
|
|
2759
|
+
}
|
|
2760
|
+
function deletionOrderPostOrder(ids) {
|
|
2761
|
+
const memo = /* @__PURE__ */ new Map();
|
|
2762
|
+
function depthOf(id) {
|
|
2763
|
+
const cached = memo.get(id);
|
|
2764
|
+
if (cached !== void 0) {
|
|
2765
|
+
return cached;
|
|
2766
|
+
}
|
|
2767
|
+
const node = state.nodes.get(id);
|
|
2768
|
+
if (!node?.parentId || !ids.has(node.parentId)) {
|
|
2769
|
+
memo.set(id, 0);
|
|
2770
|
+
return 0;
|
|
2771
|
+
}
|
|
2772
|
+
const depth = depthOf(node.parentId) + 1;
|
|
2773
|
+
memo.set(id, depth);
|
|
2774
|
+
return depth;
|
|
2775
|
+
}
|
|
2776
|
+
return Array.from(ids).sort((a, b) => depthOf(b) - depthOf(a));
|
|
2777
|
+
}
|
|
2778
|
+
function fixSubtreeZOrderAfter(parent, nodeId) {
|
|
2779
|
+
const node = state.nodes.get(nodeId);
|
|
2780
|
+
if (!node) {
|
|
2781
|
+
return;
|
|
2782
|
+
}
|
|
2783
|
+
let current = node;
|
|
2784
|
+
if (parent && current.zIndex <= parent.zIndex) {
|
|
2785
|
+
current = replaceBoardNode(node, {
|
|
2786
|
+
...node,
|
|
2787
|
+
zIndex: state.nextZIndex++
|
|
2788
|
+
});
|
|
2789
|
+
}
|
|
2790
|
+
for (const child of getDirectChildren(nodeId)) {
|
|
2791
|
+
fixSubtreeZOrderAfter(current, child.id);
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
function restackGroupDescendantsAbove(groupId) {
|
|
2795
|
+
const group = state.nodes.get(groupId);
|
|
2796
|
+
if (!group || group.type !== "group") {
|
|
2797
|
+
return;
|
|
2798
|
+
}
|
|
2799
|
+
for (const child of getDirectChildren(groupId)) {
|
|
2800
|
+
fixSubtreeZOrderAfter(group, child.id);
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
function reparentAfterDrag(movedIds) {
|
|
2804
|
+
const ordered = sortIdsByZIndex(
|
|
2805
|
+
movedIds,
|
|
2806
|
+
state.nodes
|
|
2807
|
+
);
|
|
2808
|
+
for (const id of ordered) {
|
|
2809
|
+
const node = state.nodes.get(id);
|
|
2810
|
+
if (!node) {
|
|
2811
|
+
continue;
|
|
2812
|
+
}
|
|
2813
|
+
const nextParent = findContainingGroup(
|
|
2814
|
+
node,
|
|
2815
|
+
state.nodes
|
|
2816
|
+
);
|
|
2817
|
+
if (nextParent === node.parentId) {
|
|
2818
|
+
continue;
|
|
2819
|
+
}
|
|
2820
|
+
const updated = replaceBoardNode(node, {
|
|
2821
|
+
...node,
|
|
2822
|
+
parentId: nextParent
|
|
2823
|
+
});
|
|
2824
|
+
emit("node:updated", materializeNode2(updated), materializeNode2(node));
|
|
2825
|
+
fixSubtreeZOrderAfter(nextParent ? assertBoardNode(nextParent) : null, id);
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
function reparentNodesCapturedByGroups(groupIds, excludeIds) {
|
|
2829
|
+
const exclude = new Set(excludeIds);
|
|
2830
|
+
const groups = groupIds.map((id) => state.nodes.get(id)).filter((node) => Boolean(node)).filter((node) => node.type === "group" && node.visible);
|
|
2831
|
+
if (groups.length === 0) {
|
|
2832
|
+
return;
|
|
2833
|
+
}
|
|
2834
|
+
const captured = [];
|
|
2835
|
+
for (const node of state.nodes.values()) {
|
|
2836
|
+
if (exclude.has(node.id) || node.locked || !node.visible) {
|
|
2837
|
+
continue;
|
|
2838
|
+
}
|
|
2839
|
+
if (groups.some(
|
|
2840
|
+
(group) => boundsContain(getBoundsFromNode(group), getBoundsFromNode(node))
|
|
2841
|
+
)) {
|
|
2842
|
+
captured.push(node.id);
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
if (captured.length > 0) {
|
|
2846
|
+
reparentAfterDrag(captured);
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2849
|
+
function reparentNodesCapturedByMovedGroups(movedIds) {
|
|
2850
|
+
reparentNodesCapturedByGroups(movedIds, movedIds);
|
|
2851
|
+
}
|
|
2852
|
+
function getSelectionNodes3() {
|
|
2853
|
+
return getSelectionNodes(state);
|
|
2854
|
+
}
|
|
2855
|
+
function getCopyClosureNodes2() {
|
|
2856
|
+
return getCopyClosureNodes(state);
|
|
2857
|
+
}
|
|
2858
|
+
function duplicateForest2(nodes, offset) {
|
|
2859
|
+
return duplicateForest(state, grid, nodes, offset);
|
|
2860
|
+
}
|
|
2861
|
+
function cleanupSelection() {
|
|
2862
|
+
setSelection(
|
|
2863
|
+
Array.from(state.selection.values()).filter((id) => state.nodes.has(id))
|
|
2864
|
+
);
|
|
2865
|
+
}
|
|
2866
|
+
function restoreSnapshot(snapshot, mode) {
|
|
2867
|
+
const snapshotNodes = materializeSnapshotNodes(snapshot);
|
|
2868
|
+
const idMap = /* @__PURE__ */ new Map();
|
|
2869
|
+
if (mode === "replace") {
|
|
2870
|
+
const existingIds = deletionOrderPostOrder(new Set(state.nodes.keys()));
|
|
2871
|
+
for (const id of existingIds) {
|
|
2872
|
+
const prevNode = state.nodes.get(id);
|
|
2873
|
+
if (!prevNode) {
|
|
2874
|
+
continue;
|
|
2875
|
+
}
|
|
2876
|
+
state.nodes.delete(id);
|
|
2877
|
+
notifyNodeDeletedPlugins(prevNode.id);
|
|
2878
|
+
emit("node:deleted", id, materializeNode2(prevNode));
|
|
2879
|
+
}
|
|
2880
|
+
state.nextZIndex = snapshot.nextZIndex ?? snapshotNodes.reduce((max, node) => Math.max(max, node.zIndex), 0) + 1;
|
|
2881
|
+
for (const rawNode of snapshotNodes) {
|
|
2882
|
+
const node = normalizeExistingNode(rawNode);
|
|
2883
|
+
state.nodes.set(node.id, node);
|
|
2884
|
+
idMap.set(rawNode.id, node.id);
|
|
2885
|
+
emit("node:created", materializeNode2(node));
|
|
2886
|
+
}
|
|
2887
|
+
state.selection = new Set(
|
|
2888
|
+
snapshot.selection.filter((id) => state.nodes.has(id))
|
|
2889
|
+
);
|
|
2890
|
+
state.interaction = { mode: "idle" };
|
|
2891
|
+
state.snapGuides = [];
|
|
2892
|
+
state.camera = { ...snapshot.camera };
|
|
2893
|
+
Object.assign(grid, { ...snapshot.grid });
|
|
2894
|
+
notifyCameraChanged();
|
|
2895
|
+
notifyNodesChanged();
|
|
2896
|
+
notifySelectionChanged();
|
|
2897
|
+
notifyInteractionChanged();
|
|
2898
|
+
notifySnapGuidesChanged();
|
|
2899
|
+
return idMap;
|
|
2900
|
+
}
|
|
2901
|
+
for (const rawNode of snapshotNodes) {
|
|
2902
|
+
const node = normalizeExistingNode(rawNode);
|
|
2903
|
+
const id = state.nodes.has(node.id) ? createNodeId() : node.id;
|
|
2904
|
+
state.nodes.set(id, { ...node, id, zIndex: state.nextZIndex++ });
|
|
2905
|
+
idMap.set(node.id, id);
|
|
2906
|
+
}
|
|
2907
|
+
notifyNodesChanged();
|
|
2908
|
+
return idMap;
|
|
2909
|
+
}
|
|
2910
|
+
function restorePluginDocuments(document, mode, idMap = /* @__PURE__ */ new Map()) {
|
|
2911
|
+
for (const entry of pluginPersistence.values()) {
|
|
2912
|
+
entry.hooks.loadDocument?.(entry.context, document, mode, idMap);
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
function assertCanRestoreDocument(document) {
|
|
2916
|
+
if (document.edges?.length && !pluginPersistence.has(CONNECTIONS_FEATURE_NAME)) {
|
|
2917
|
+
throw new BoardInputError(
|
|
2918
|
+
"Invalid board document: edges require the connections plugin."
|
|
2919
|
+
);
|
|
2920
|
+
}
|
|
2921
|
+
}
|
|
2922
|
+
function installPlugin(plugin) {
|
|
2923
|
+
if (plugin.slice) {
|
|
2924
|
+
pluginStates.set(plugin.name, {
|
|
2925
|
+
state: plugin.slice.initial
|
|
2926
|
+
});
|
|
2927
|
+
}
|
|
2928
|
+
const pluginCtx = Object.assign(
|
|
2929
|
+
Object.create(engine),
|
|
2930
|
+
{
|
|
2931
|
+
getPluginState: () => {
|
|
2932
|
+
assertAlive();
|
|
2933
|
+
const entry = pluginStates.get(plugin.name);
|
|
2934
|
+
if (!entry) {
|
|
2935
|
+
throw new Error(
|
|
2936
|
+
`Plugin "${plugin.name}" did not register a persistent slice.`
|
|
2937
|
+
);
|
|
2938
|
+
}
|
|
2939
|
+
return entry.state;
|
|
2940
|
+
},
|
|
2941
|
+
updatePluginState: (update) => {
|
|
2942
|
+
assertAlive();
|
|
2943
|
+
assertMutationAllowed();
|
|
2944
|
+
const entry = pluginStates.get(plugin.name);
|
|
2945
|
+
if (!entry) {
|
|
2946
|
+
throw new Error(
|
|
2947
|
+
`Plugin "${plugin.name}" did not register a persistent slice.`
|
|
2948
|
+
);
|
|
2949
|
+
}
|
|
2950
|
+
const next = update(entry.state);
|
|
2951
|
+
entry.state = next;
|
|
2952
|
+
return next;
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
);
|
|
2956
|
+
const cleanup = plugin.install(pluginCtx);
|
|
2957
|
+
if (plugin.nodeDeleted) {
|
|
2958
|
+
nodeDeletedHooks.add((nodeId) => plugin.nodeDeleted(pluginCtx, nodeId));
|
|
2959
|
+
}
|
|
2960
|
+
if (plugin.persistence) {
|
|
2961
|
+
pluginPersistence.set(plugin.name, {
|
|
2962
|
+
context: pluginCtx,
|
|
2963
|
+
hooks: plugin.persistence
|
|
2964
|
+
});
|
|
2965
|
+
}
|
|
2966
|
+
pluginCleanups.set(plugin.name, cleanup ?? (() => void 0));
|
|
2967
|
+
}
|
|
2968
|
+
const engine = {
|
|
2969
|
+
plugins,
|
|
2970
|
+
assertActive: assertAlive,
|
|
2971
|
+
isBatching: () => batches.isBatching(),
|
|
2972
|
+
$camera,
|
|
2973
|
+
$grid,
|
|
2974
|
+
$nodes,
|
|
2975
|
+
$selection,
|
|
2976
|
+
$interaction,
|
|
2977
|
+
$snapGuides,
|
|
2978
|
+
destroy() {
|
|
2979
|
+
if (destroyed) {
|
|
2980
|
+
return;
|
|
2981
|
+
}
|
|
2982
|
+
assertMutationAllowed();
|
|
2983
|
+
destroyed = true;
|
|
2984
|
+
cameraSession.cancelAnimations();
|
|
2985
|
+
nodeOverrides.clear();
|
|
2986
|
+
activeGestureHistoryRoot = null;
|
|
2987
|
+
activeBoxSelectionBefore = null;
|
|
2988
|
+
const cleanupErrors = [];
|
|
2989
|
+
for (const cleanup of pluginCleanups.values()) {
|
|
2990
|
+
try {
|
|
2991
|
+
cleanup();
|
|
2992
|
+
} catch (error) {
|
|
2993
|
+
cleanupErrors.push(error);
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2996
|
+
emit("destroy");
|
|
2997
|
+
pluginCleanups.clear();
|
|
2998
|
+
pluginStates.clear();
|
|
2999
|
+
pluginPersistence.clear();
|
|
3000
|
+
commitProjectors.clear();
|
|
3001
|
+
nodeDeletedHooks.clear();
|
|
3002
|
+
commandGuards.clear();
|
|
3003
|
+
eventBus.clear();
|
|
3004
|
+
destroyReactiveLayer();
|
|
3005
|
+
if (cleanupErrors.length > 0) {
|
|
3006
|
+
throw new AggregateError(
|
|
3007
|
+
cleanupErrors,
|
|
3008
|
+
"One or more board plugin cleanups failed."
|
|
3009
|
+
);
|
|
3010
|
+
}
|
|
3011
|
+
},
|
|
3012
|
+
extend(key, value) {
|
|
3013
|
+
assertMutationAllowed();
|
|
3014
|
+
plugins[key] = value;
|
|
3015
|
+
},
|
|
3016
|
+
batch(fn) {
|
|
3017
|
+
assertCommandReady();
|
|
3018
|
+
if (batches.isBatching()) {
|
|
3019
|
+
batches.batch(fn);
|
|
3020
|
+
return;
|
|
3021
|
+
}
|
|
3022
|
+
const originalHistoryBefore = captureHistoryRoot();
|
|
3023
|
+
const checkpoint = beginPersistentTransaction();
|
|
3024
|
+
eventBus.beginTransaction();
|
|
3025
|
+
let historyBefore = originalHistoryBefore;
|
|
3026
|
+
let commitErrors = [];
|
|
3027
|
+
try {
|
|
3028
|
+
batches.batch(
|
|
3029
|
+
() => {
|
|
3030
|
+
const restoresBoxSelection = state.interaction.mode === "box-select" && activeBoxSelectionBefore !== null;
|
|
3031
|
+
discardActiveInteraction();
|
|
3032
|
+
if (restoresBoxSelection) {
|
|
3033
|
+
historyBefore = {
|
|
3034
|
+
...originalHistoryBefore,
|
|
3035
|
+
selection: new Set(state.selection)
|
|
3036
|
+
};
|
|
3037
|
+
}
|
|
3038
|
+
fn();
|
|
3039
|
+
},
|
|
3040
|
+
() => {
|
|
3041
|
+
commitErrors = prepareCommit(
|
|
3042
|
+
"batch",
|
|
3043
|
+
RECORD_COMMAND,
|
|
3044
|
+
historyBefore
|
|
3045
|
+
)?.finalize() ?? [];
|
|
3046
|
+
}
|
|
3047
|
+
);
|
|
3048
|
+
eventBus.commitTransaction();
|
|
3049
|
+
for (const error of commitErrors) {
|
|
3050
|
+
reportUnhandledError(error, {
|
|
3051
|
+
source: "commit-effect",
|
|
3052
|
+
commit: "batch"
|
|
3053
|
+
});
|
|
3054
|
+
}
|
|
3055
|
+
} catch (error) {
|
|
3056
|
+
rollbackPersistentTransaction(checkpoint);
|
|
3057
|
+
eventBus.rollbackTransaction();
|
|
3058
|
+
throw error;
|
|
3059
|
+
}
|
|
3060
|
+
},
|
|
3061
|
+
getState,
|
|
3062
|
+
getGridSettings,
|
|
3063
|
+
getViewportSize,
|
|
3064
|
+
updateGridSettings(patch) {
|
|
3065
|
+
return runCommand("updateGridSettings", [patch], () => {
|
|
3066
|
+
const next = { ...grid, ...patch };
|
|
3067
|
+
validateGridSettings(next);
|
|
3068
|
+
Object.assign(grid, next);
|
|
3069
|
+
notifyGridChanged();
|
|
3070
|
+
return getGridSettings();
|
|
3071
|
+
});
|
|
3072
|
+
},
|
|
3073
|
+
setViewportSize(size) {
|
|
3074
|
+
assertAlive();
|
|
3075
|
+
assertMutationAllowed();
|
|
3076
|
+
if (!Number.isFinite(size.x) || !Number.isFinite(size.y)) {
|
|
3077
|
+
throw new BoardInputError(
|
|
3078
|
+
"Viewport width and height must be finite numbers."
|
|
3079
|
+
);
|
|
3080
|
+
}
|
|
3081
|
+
const next = {
|
|
3082
|
+
x: Math.max(1, size.x),
|
|
3083
|
+
y: Math.max(1, size.y)
|
|
3084
|
+
};
|
|
3085
|
+
if (next.x === viewportSize.x && next.y === viewportSize.y) {
|
|
3086
|
+
return;
|
|
3087
|
+
}
|
|
3088
|
+
const prev = { ...viewportSize };
|
|
3089
|
+
viewportSize = next;
|
|
3090
|
+
emit("viewport:change", freezeClone({ ...next }), freezeClone(prev));
|
|
3091
|
+
},
|
|
3092
|
+
emit,
|
|
3093
|
+
on(event, handler) {
|
|
3094
|
+
assertAlive();
|
|
3095
|
+
return on(event, handler);
|
|
3096
|
+
},
|
|
3097
|
+
once(event, handler) {
|
|
3098
|
+
assertAlive();
|
|
3099
|
+
return once(event, handler);
|
|
3100
|
+
},
|
|
3101
|
+
off(event, handler) {
|
|
3102
|
+
assertAlive();
|
|
3103
|
+
off(event, handler);
|
|
3104
|
+
},
|
|
3105
|
+
exportTrace() {
|
|
3106
|
+
assertAlive();
|
|
3107
|
+
return eventBus.exportTrace();
|
|
3108
|
+
},
|
|
3109
|
+
addCommandGuard(fn) {
|
|
3110
|
+
assertAlive();
|
|
3111
|
+
assertMutationAllowed();
|
|
3112
|
+
return commandGuards.add(fn);
|
|
3113
|
+
},
|
|
3114
|
+
runCommand(name, args, fn, metadata) {
|
|
3115
|
+
return runCommand(name, args, fn, metadata);
|
|
3116
|
+
},
|
|
3117
|
+
projectCommit(projector) {
|
|
3118
|
+
assertAlive();
|
|
3119
|
+
assertMutationAllowed();
|
|
3120
|
+
commitProjectors.add(projector);
|
|
3121
|
+
return () => commitProjectors.delete(projector);
|
|
3122
|
+
},
|
|
3123
|
+
restoreHistoryRoot(root) {
|
|
3124
|
+
runCommand(
|
|
3125
|
+
"history:restore",
|
|
3126
|
+
[],
|
|
3127
|
+
() => {
|
|
3128
|
+
nodeOverrides.clear();
|
|
3129
|
+
activeBoxSelectionBefore = null;
|
|
3130
|
+
state.nodes = new Map(root.nodes);
|
|
3131
|
+
state.selection = new Set(
|
|
3132
|
+
Array.from(root.selection).filter((id) => state.nodes.has(id))
|
|
3133
|
+
);
|
|
3134
|
+
state.nextZIndex = root.nextZIndex;
|
|
3135
|
+
Object.assign(grid, root.grid);
|
|
3136
|
+
for (const [name, pluginState] of pluginStates) {
|
|
3137
|
+
if (root.pluginSlices.has(name)) {
|
|
3138
|
+
pluginState.state = root.pluginSlices.get(name);
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
3141
|
+
setInteraction({ mode: "idle" });
|
|
3142
|
+
setSnapGuides([]);
|
|
3143
|
+
notifyGridChanged();
|
|
3144
|
+
notifyNodesChanged();
|
|
3145
|
+
notifySelectionChanged();
|
|
3146
|
+
},
|
|
3147
|
+
IGNORE_COMMAND
|
|
3148
|
+
);
|
|
3149
|
+
},
|
|
3150
|
+
getPluginState() {
|
|
3151
|
+
throw new Error(
|
|
3152
|
+
"getPluginState is only available inside an internal plugin install() context."
|
|
3153
|
+
);
|
|
3154
|
+
},
|
|
3155
|
+
updatePluginState(_update) {
|
|
3156
|
+
throw new Error(
|
|
3157
|
+
"updatePluginState is only available inside an internal plugin install() context."
|
|
3158
|
+
);
|
|
3159
|
+
},
|
|
3160
|
+
screenToWorld(point) {
|
|
3161
|
+
assertAlive();
|
|
3162
|
+
return screenToWorld(point, state.camera);
|
|
3163
|
+
},
|
|
3164
|
+
worldToScreen(point) {
|
|
3165
|
+
assertAlive();
|
|
3166
|
+
return worldToScreen(point, state.camera);
|
|
3167
|
+
},
|
|
3168
|
+
getVisibleBounds(width, height) {
|
|
3169
|
+
assertAlive();
|
|
3170
|
+
return getVisibleBounds(width, height, state.camera);
|
|
3171
|
+
},
|
|
3172
|
+
getNode(id) {
|
|
3173
|
+
assertAlive();
|
|
3174
|
+
return getPublicNode(id);
|
|
3175
|
+
},
|
|
3176
|
+
findNode(id) {
|
|
3177
|
+
assertAlive();
|
|
3178
|
+
return state.nodes.has(id) ? getPublicNode(id) : null;
|
|
3179
|
+
},
|
|
3180
|
+
hasNode(id) {
|
|
3181
|
+
assertAlive();
|
|
3182
|
+
return state.nodes.has(id);
|
|
3183
|
+
},
|
|
3184
|
+
getNodeAt(worldPoint) {
|
|
3185
|
+
assertAlive();
|
|
3186
|
+
let best = null;
|
|
3187
|
+
let bestZ = -Infinity;
|
|
3188
|
+
for (const node of state.nodes.values()) {
|
|
3189
|
+
if (node.visible && node.zIndex > bestZ && pointInBounds(worldPoint, getBoundsFromNode(node))) {
|
|
3190
|
+
best = node;
|
|
3191
|
+
bestZ = node.zIndex;
|
|
3192
|
+
}
|
|
3193
|
+
}
|
|
3194
|
+
return best ? materializeNode2(best) : null;
|
|
3195
|
+
},
|
|
3196
|
+
getNodesInBounds(bounds) {
|
|
3197
|
+
assertAlive();
|
|
3198
|
+
return Array.from(state.nodes.values()).filter(
|
|
3199
|
+
(node) => node.visible && boundsIntersect(getBoundsFromNode(node), bounds)
|
|
3200
|
+
).map((node) => materializeNode2(node));
|
|
3201
|
+
},
|
|
3202
|
+
panBy(dx, dy) {
|
|
3203
|
+
requireFiniteInput("panBy delta", dx, dy);
|
|
3204
|
+
runCommand(
|
|
3205
|
+
"panBy",
|
|
3206
|
+
[dx, dy],
|
|
3207
|
+
() => {
|
|
3208
|
+
setCamera({
|
|
3209
|
+
x: state.camera.x - dx / state.camera.z,
|
|
3210
|
+
y: state.camera.y - dy / state.camera.z,
|
|
3211
|
+
z: state.camera.z
|
|
3212
|
+
});
|
|
3213
|
+
},
|
|
3214
|
+
IGNORE_UNVALIDATED_COMMAND
|
|
3215
|
+
);
|
|
3216
|
+
},
|
|
3217
|
+
panTo(worldPoint, animated = false) {
|
|
3218
|
+
requireFiniteInput("panTo point", worldPoint.x, worldPoint.y);
|
|
3219
|
+
const target = { x: -worldPoint.x, y: -worldPoint.y, z: state.camera.z };
|
|
3220
|
+
return runAsyncCommand(
|
|
3221
|
+
"panTo",
|
|
3222
|
+
[worldPoint, animated],
|
|
3223
|
+
async () => {
|
|
3224
|
+
if (animated) {
|
|
3225
|
+
await cameraSession.animateTo(target);
|
|
3226
|
+
} else {
|
|
3227
|
+
setCamera(target);
|
|
3228
|
+
}
|
|
3229
|
+
},
|
|
3230
|
+
IGNORE_UNVALIDATED_COMMAND
|
|
3231
|
+
);
|
|
3232
|
+
},
|
|
3233
|
+
zoomAt(screenPoint, delta) {
|
|
3234
|
+
requireFiniteInput("zoomAt input", screenPoint.x, screenPoint.y, delta);
|
|
3235
|
+
runCommand(
|
|
3236
|
+
"zoomAt",
|
|
3237
|
+
[screenPoint, delta],
|
|
3238
|
+
() => {
|
|
3239
|
+
setCamera(
|
|
3240
|
+
zoomCameraAtScreenPoint(
|
|
3241
|
+
screenPoint,
|
|
3242
|
+
delta,
|
|
3243
|
+
state.camera,
|
|
3244
|
+
zoom.min,
|
|
3245
|
+
zoom.max
|
|
3246
|
+
)
|
|
3247
|
+
);
|
|
3248
|
+
},
|
|
3249
|
+
IGNORE_UNVALIDATED_COMMAND
|
|
3250
|
+
);
|
|
3251
|
+
},
|
|
3252
|
+
zoomTo(level, animated = false) {
|
|
3253
|
+
requireFiniteInput("zoomTo level", level);
|
|
3254
|
+
const clamped = clamp(level, zoom.min, zoom.max);
|
|
3255
|
+
const viewportCenter = { x: viewportSize.x / 2, y: viewportSize.y / 2 };
|
|
3256
|
+
const centerWorld = screenToWorld(viewportCenter, state.camera);
|
|
3257
|
+
const target = {
|
|
3258
|
+
x: viewportCenter.x / clamped - centerWorld.x,
|
|
3259
|
+
y: viewportCenter.y / clamped - centerWorld.y,
|
|
3260
|
+
z: clamped
|
|
3261
|
+
};
|
|
3262
|
+
return runAsyncCommand(
|
|
3263
|
+
"zoomTo",
|
|
3264
|
+
[level, animated],
|
|
3265
|
+
async () => {
|
|
3266
|
+
if (animated) {
|
|
3267
|
+
await cameraSession.animateTo(target);
|
|
3268
|
+
} else {
|
|
3269
|
+
setCamera(target);
|
|
3270
|
+
}
|
|
3271
|
+
},
|
|
3272
|
+
IGNORE_UNVALIDATED_COMMAND
|
|
3273
|
+
);
|
|
3274
|
+
},
|
|
3275
|
+
zoomToFit(padding = 40, animated = false) {
|
|
3276
|
+
requireNonNegativeInput("zoomToFit padding", padding);
|
|
3277
|
+
return runAsyncCommand(
|
|
3278
|
+
"zoomToFit",
|
|
3279
|
+
[padding, animated],
|
|
3280
|
+
async () => {
|
|
3281
|
+
const target = cameraSession.computeFit(null, padding);
|
|
3282
|
+
if (!target) {
|
|
3283
|
+
return;
|
|
3284
|
+
}
|
|
3285
|
+
if (animated) {
|
|
3286
|
+
await cameraSession.animateTo(target);
|
|
3287
|
+
} else {
|
|
3288
|
+
setCamera(target);
|
|
3289
|
+
}
|
|
3290
|
+
},
|
|
3291
|
+
IGNORE_UNVALIDATED_COMMAND
|
|
3292
|
+
);
|
|
3293
|
+
},
|
|
3294
|
+
zoomToNodes(ids, padding = 40, animated = false) {
|
|
3295
|
+
requireNonNegativeInput("zoomToNodes padding", padding);
|
|
3296
|
+
return runAsyncCommand(
|
|
3297
|
+
"zoomToNodes",
|
|
3298
|
+
[ids, padding, animated],
|
|
3299
|
+
async () => {
|
|
3300
|
+
const target = cameraSession.computeFit(ids, padding);
|
|
3301
|
+
if (!target) {
|
|
3302
|
+
return;
|
|
3303
|
+
}
|
|
3304
|
+
if (animated) {
|
|
3305
|
+
await cameraSession.animateTo(target);
|
|
3306
|
+
} else {
|
|
3307
|
+
setCamera(target);
|
|
3308
|
+
}
|
|
3309
|
+
},
|
|
3310
|
+
IGNORE_UNVALIDATED_COMMAND
|
|
3311
|
+
);
|
|
3312
|
+
},
|
|
3313
|
+
createNode(input) {
|
|
3314
|
+
return runCommand("createNode", [input], () => {
|
|
3315
|
+
const node = normalizeNode(input);
|
|
3316
|
+
state.nodes.set(node.id, node);
|
|
3317
|
+
notifyNodesChanged();
|
|
3318
|
+
if (input.select !== false) {
|
|
3319
|
+
setSelection([node.id]);
|
|
3320
|
+
}
|
|
3321
|
+
const publicNode = materializeNode2(node);
|
|
3322
|
+
emit("node:created", publicNode);
|
|
3323
|
+
return publicNode;
|
|
3324
|
+
});
|
|
3325
|
+
},
|
|
3326
|
+
updateNode(id, patch) {
|
|
3327
|
+
return runCommand("updateNode", [id, patch], () => {
|
|
3328
|
+
const current = assertBoardNode(id);
|
|
3329
|
+
const next = applyNodePatch(current, patch);
|
|
3330
|
+
const stored = replaceBoardNode(current, next);
|
|
3331
|
+
const publicNode = materializeNode2(stored);
|
|
3332
|
+
emit("node:updated", publicNode, materializeNode2(current));
|
|
3333
|
+
return publicNode;
|
|
3334
|
+
});
|
|
3335
|
+
},
|
|
3336
|
+
deleteNode(id) {
|
|
3337
|
+
runCommand("deleteNode", [id], () => {
|
|
3338
|
+
assertBoardNode(id);
|
|
3339
|
+
const toDelete = /* @__PURE__ */ new Set();
|
|
3340
|
+
collectSubtreeIdSet(id, toDelete);
|
|
3341
|
+
for (const deleteId of deletionOrderPostOrder(toDelete)) {
|
|
3342
|
+
const prevNode = state.nodes.get(deleteId);
|
|
3343
|
+
if (!prevNode) {
|
|
3344
|
+
continue;
|
|
3345
|
+
}
|
|
3346
|
+
state.nodes.delete(deleteId);
|
|
3347
|
+
notifyNodeDeletedPlugins(prevNode.id);
|
|
3348
|
+
emit("node:deleted", deleteId, materializeNode2(prevNode));
|
|
3349
|
+
}
|
|
3350
|
+
notifyNodesChanged();
|
|
3351
|
+
cleanupSelection();
|
|
3352
|
+
if (state.interaction.mode !== "idle") {
|
|
3353
|
+
setInteraction({ mode: "idle" });
|
|
3354
|
+
}
|
|
3355
|
+
});
|
|
3356
|
+
},
|
|
3357
|
+
moveNode(id, dx, dy) {
|
|
3358
|
+
return runCommand("moveNode", [id, dx, dy], () => {
|
|
3359
|
+
const node = assertBoardNode(id);
|
|
3360
|
+
if (node.locked) {
|
|
3361
|
+
return materializeNode2(node);
|
|
3362
|
+
}
|
|
3363
|
+
const targets = collectUniformTranslationTargets(
|
|
3364
|
+
[id],
|
|
3365
|
+
state.nodes
|
|
3366
|
+
);
|
|
3367
|
+
for (const targetId of targets) {
|
|
3368
|
+
const current = assertBoardNode(targetId);
|
|
3369
|
+
const next = {
|
|
3370
|
+
...current,
|
|
3371
|
+
x: grid.snap ? snapValue(current.x + dx, grid.size) : current.x + dx,
|
|
3372
|
+
y: grid.snap ? snapValue(current.y + dy, grid.size) : current.y + dy
|
|
3373
|
+
};
|
|
3374
|
+
const stored = replaceBoardNodeWithoutNotify(current, next);
|
|
3375
|
+
const publicNode = materializeNode2(stored);
|
|
3376
|
+
emit("node:moved", publicNode, {
|
|
3377
|
+
x: publicNode.x - current.x,
|
|
3378
|
+
y: publicNode.y - current.y
|
|
3379
|
+
});
|
|
3380
|
+
emit("node:updated", publicNode, materializeNode2(current));
|
|
3381
|
+
}
|
|
3382
|
+
if (targets.length > 0) {
|
|
3383
|
+
notifyNodesChanged();
|
|
3384
|
+
}
|
|
3385
|
+
reparentAfterDrag(targets);
|
|
3386
|
+
reparentNodesCapturedByMovedGroups(targets);
|
|
3387
|
+
return getPublicNode(id);
|
|
3388
|
+
});
|
|
3389
|
+
},
|
|
3390
|
+
translateSelectedNodes(dx, dy) {
|
|
3391
|
+
runCommand("translateSelectedNodes", [dx, dy], () => {
|
|
3392
|
+
const seeds = Array.from(state.selection.values()).filter((id) => {
|
|
3393
|
+
const node = state.nodes.get(id);
|
|
3394
|
+
return node && !node.locked;
|
|
3395
|
+
});
|
|
3396
|
+
if (seeds.length === 0) {
|
|
3397
|
+
return;
|
|
3398
|
+
}
|
|
3399
|
+
const targets = collectUniformTranslationTargets(
|
|
3400
|
+
seeds,
|
|
3401
|
+
state.nodes
|
|
3402
|
+
);
|
|
3403
|
+
for (const targetId of targets) {
|
|
3404
|
+
const current = assertBoardNode(targetId);
|
|
3405
|
+
const next = {
|
|
3406
|
+
...current,
|
|
3407
|
+
x: grid.snap ? snapValue(current.x + dx, grid.size) : current.x + dx,
|
|
3408
|
+
y: grid.snap ? snapValue(current.y + dy, grid.size) : current.y + dy
|
|
3409
|
+
};
|
|
3410
|
+
const stored = replaceBoardNodeWithoutNotify(current, next);
|
|
3411
|
+
const publicNode = materializeNode2(stored);
|
|
3412
|
+
emit("node:moved", publicNode, {
|
|
3413
|
+
x: publicNode.x - current.x,
|
|
3414
|
+
y: publicNode.y - current.y
|
|
3415
|
+
});
|
|
3416
|
+
emit("node:updated", publicNode, materializeNode2(current));
|
|
3417
|
+
}
|
|
3418
|
+
if (targets.length > 0) {
|
|
3419
|
+
notifyNodesChanged();
|
|
3420
|
+
}
|
|
3421
|
+
reparentAfterDrag(targets);
|
|
3422
|
+
reparentNodesCapturedByMovedGroups(targets);
|
|
3423
|
+
});
|
|
3424
|
+
},
|
|
3425
|
+
resizeNode(id, handle, dx, dy) {
|
|
3426
|
+
return runCommand("resizeNode", [id, handle, dx, dy], () => {
|
|
3427
|
+
const node = assertBoardNode(id);
|
|
3428
|
+
if (node.locked) {
|
|
3429
|
+
return materializeNode2(node);
|
|
3430
|
+
}
|
|
3431
|
+
const raw = applyResizeDelta(node, handle, dx, dy, {
|
|
3432
|
+
minWidth: nodeConstraints.minWidth,
|
|
3433
|
+
minHeight: nodeConstraints.minHeight
|
|
3434
|
+
});
|
|
3435
|
+
const nextBounds = grid.snap ? snapResizedBounds(raw, handle, grid.size, {
|
|
3436
|
+
minWidth: nodeConstraints.minWidth,
|
|
3437
|
+
minHeight: nodeConstraints.minHeight
|
|
3438
|
+
}) : raw;
|
|
3439
|
+
const stored = replaceBoardNode(node, {
|
|
3440
|
+
...node,
|
|
3441
|
+
...nextBounds
|
|
3442
|
+
});
|
|
3443
|
+
const publicNode = materializeNode2(stored);
|
|
3444
|
+
emit("node:resized", publicNode, {
|
|
3445
|
+
x: node.x,
|
|
3446
|
+
y: node.y,
|
|
3447
|
+
width: node.width,
|
|
3448
|
+
height: node.height
|
|
3449
|
+
});
|
|
3450
|
+
emit("node:updated", publicNode, materializeNode2(node));
|
|
3451
|
+
return publicNode;
|
|
3452
|
+
});
|
|
3453
|
+
},
|
|
3454
|
+
bringToFront(id) {
|
|
3455
|
+
runCommand("bringToFront", [id], () => {
|
|
3456
|
+
const node = assertBoardNode(id);
|
|
3457
|
+
const stored = replaceBoardNode(node, {
|
|
3458
|
+
...node,
|
|
3459
|
+
zIndex: state.nextZIndex++
|
|
3460
|
+
});
|
|
3461
|
+
emit("node:updated", materializeNode2(stored), materializeNode2(node));
|
|
3462
|
+
restackGroupDescendantsAbove(id);
|
|
3463
|
+
});
|
|
3464
|
+
},
|
|
3465
|
+
sendToBack(id) {
|
|
3466
|
+
runCommand("sendToBack", [id], () => {
|
|
3467
|
+
const node = assertBoardNode(id);
|
|
3468
|
+
const minZ = Math.min(
|
|
3469
|
+
...Array.from(state.nodes.values(), (entry) => entry.zIndex)
|
|
3470
|
+
);
|
|
3471
|
+
const stored = replaceBoardNode(node, {
|
|
3472
|
+
...node,
|
|
3473
|
+
zIndex: minZ - 1
|
|
3474
|
+
});
|
|
3475
|
+
emit("node:updated", materializeNode2(stored), materializeNode2(node));
|
|
3476
|
+
restackGroupDescendantsAbove(id);
|
|
3477
|
+
});
|
|
3478
|
+
},
|
|
3479
|
+
lockNode(id) {
|
|
3480
|
+
runCommand("lockNode", [id], () => {
|
|
3481
|
+
const node = assertBoardNode(id);
|
|
3482
|
+
const stored = replaceBoardNode(node, {
|
|
3483
|
+
...node,
|
|
3484
|
+
locked: true
|
|
3485
|
+
});
|
|
3486
|
+
emit("node:updated", materializeNode2(stored), materializeNode2(node));
|
|
3487
|
+
});
|
|
3488
|
+
},
|
|
3489
|
+
unlockNode(id) {
|
|
3490
|
+
runCommand("unlockNode", [id], () => {
|
|
3491
|
+
const node = assertBoardNode(id);
|
|
3492
|
+
const stored = replaceBoardNode(node, {
|
|
3493
|
+
...node,
|
|
3494
|
+
locked: false
|
|
3495
|
+
});
|
|
3496
|
+
emit("node:updated", materializeNode2(stored), materializeNode2(node));
|
|
3497
|
+
});
|
|
3498
|
+
},
|
|
3499
|
+
duplicateNodes(ids, offset = { x: grid.size, y: grid.size }) {
|
|
3500
|
+
return runCommand("duplicateNodes", [ids, offset], () => {
|
|
3501
|
+
const forest = forestIdsFromSeeds(ids);
|
|
3502
|
+
const source = Array.from(forest).map((id) => state.nodes.get(id)).filter((node) => Boolean(node)).sort((a, b) => a.zIndex - b.zIndex);
|
|
3503
|
+
const duplicated = duplicateForest2(source, offset);
|
|
3504
|
+
const created = duplicated.nodes;
|
|
3505
|
+
for (const node of created) {
|
|
3506
|
+
state.nodes.set(node.id, node);
|
|
3507
|
+
emit("node:created", materializeNode2(node));
|
|
3508
|
+
}
|
|
3509
|
+
notifyNodesChanged();
|
|
3510
|
+
setSelection(created.map((node) => node.id));
|
|
3511
|
+
return {
|
|
3512
|
+
nodes: created.map((node) => materializeNode2(node)),
|
|
3513
|
+
idMap: duplicated.idMap
|
|
3514
|
+
};
|
|
3515
|
+
});
|
|
3516
|
+
},
|
|
3517
|
+
copySelected() {
|
|
3518
|
+
return runCommand("copySelected", [], () => {
|
|
3519
|
+
clipboard.length = 0;
|
|
3520
|
+
for (const node of getCopyClosureNodes2()) {
|
|
3521
|
+
clipboard.push({ ...node });
|
|
3522
|
+
}
|
|
3523
|
+
return clipboard.map((node) => materializeNode2(node));
|
|
3524
|
+
});
|
|
3525
|
+
},
|
|
3526
|
+
pasteClipboard(offset = { x: grid.size, y: grid.size }) {
|
|
3527
|
+
return runCommand("pasteClipboard", [offset], () => {
|
|
3528
|
+
const created = duplicateForest2(clipboard, offset).nodes;
|
|
3529
|
+
for (const node of created) {
|
|
3530
|
+
state.nodes.set(node.id, node);
|
|
3531
|
+
emit("node:created", materializeNode2(node));
|
|
3532
|
+
}
|
|
3533
|
+
notifyNodesChanged();
|
|
3534
|
+
setSelection(created.map((node) => node.id));
|
|
3535
|
+
return created.map((node) => materializeNode2(node));
|
|
3536
|
+
});
|
|
3537
|
+
},
|
|
3538
|
+
select(ids, mode = "replace") {
|
|
3539
|
+
runCommand(
|
|
3540
|
+
"select",
|
|
3541
|
+
[ids, mode],
|
|
3542
|
+
() => {
|
|
3543
|
+
discardActiveInteraction();
|
|
3544
|
+
const resolved = Array.isArray(ids) ? ids : [ids];
|
|
3545
|
+
if (mode === "replace") {
|
|
3546
|
+
setSelection(resolved);
|
|
3547
|
+
return;
|
|
3548
|
+
}
|
|
3549
|
+
const next = new Set(state.selection);
|
|
3550
|
+
for (const id of resolved) {
|
|
3551
|
+
if (mode === "toggle") {
|
|
3552
|
+
if (next.has(id)) {
|
|
3553
|
+
next.delete(id);
|
|
3554
|
+
} else {
|
|
3555
|
+
next.add(id);
|
|
3556
|
+
}
|
|
3557
|
+
} else {
|
|
3558
|
+
next.add(id);
|
|
3559
|
+
}
|
|
3560
|
+
}
|
|
3561
|
+
setSelection(next);
|
|
3562
|
+
},
|
|
3563
|
+
IGNORE_COMMAND
|
|
3564
|
+
);
|
|
3565
|
+
},
|
|
3566
|
+
selectAll() {
|
|
3567
|
+
runCommand(
|
|
3568
|
+
"selectAll",
|
|
3569
|
+
[],
|
|
3570
|
+
() => {
|
|
3571
|
+
discardActiveInteraction();
|
|
3572
|
+
setSelection(
|
|
3573
|
+
Array.from(state.nodes.values()).filter((node) => node.visible).map((node) => node.id)
|
|
3574
|
+
);
|
|
3575
|
+
},
|
|
3576
|
+
IGNORE_COMMAND
|
|
3577
|
+
);
|
|
3578
|
+
},
|
|
3579
|
+
clearSelection() {
|
|
3580
|
+
runCommand(
|
|
3581
|
+
"clearSelection",
|
|
3582
|
+
[],
|
|
3583
|
+
() => {
|
|
3584
|
+
discardActiveInteraction();
|
|
3585
|
+
setSelection([]);
|
|
3586
|
+
},
|
|
3587
|
+
IGNORE_COMMAND
|
|
3588
|
+
);
|
|
3589
|
+
},
|
|
3590
|
+
deleteSelected() {
|
|
3591
|
+
runCommand("deleteSelected", [], () => {
|
|
3592
|
+
const deletingRoots = getSelectionNodes3().filter((node) => !node.locked);
|
|
3593
|
+
const toDelete = /* @__PURE__ */ new Set();
|
|
3594
|
+
for (const node of deletingRoots) {
|
|
3595
|
+
collectSubtreeIdSet(node.id, toDelete);
|
|
3596
|
+
}
|
|
3597
|
+
for (const deleteId of deletionOrderPostOrder(toDelete)) {
|
|
3598
|
+
const prevNode = state.nodes.get(deleteId);
|
|
3599
|
+
if (!prevNode) {
|
|
3600
|
+
continue;
|
|
3601
|
+
}
|
|
3602
|
+
state.nodes.delete(deleteId);
|
|
3603
|
+
notifyNodeDeletedPlugins(prevNode.id);
|
|
3604
|
+
emit("node:deleted", deleteId, materializeNode2(prevNode));
|
|
3605
|
+
}
|
|
3606
|
+
if (toDelete.size > 0) {
|
|
3607
|
+
notifyNodesChanged();
|
|
3608
|
+
}
|
|
3609
|
+
setSelection([]);
|
|
3610
|
+
setInteraction({ mode: "idle" });
|
|
3611
|
+
});
|
|
3612
|
+
},
|
|
3613
|
+
getSelection() {
|
|
3614
|
+
assertAlive();
|
|
3615
|
+
return Array.from(state.selection.values());
|
|
3616
|
+
},
|
|
3617
|
+
beginPan(pointerId, screenPoint) {
|
|
3618
|
+
runCommand(
|
|
3619
|
+
"beginPan",
|
|
3620
|
+
[pointerId, screenPoint],
|
|
3621
|
+
() => {
|
|
3622
|
+
discardActiveInteraction();
|
|
3623
|
+
setInteraction({
|
|
3624
|
+
mode: "panning",
|
|
3625
|
+
pointerId,
|
|
3626
|
+
lastScreenPoint: { ...screenPoint }
|
|
3627
|
+
});
|
|
3628
|
+
},
|
|
3629
|
+
IGNORE_UNVALIDATED_COMMAND
|
|
3630
|
+
);
|
|
3631
|
+
},
|
|
3632
|
+
beginNodeDrag(id, pointerId, screenPoint) {
|
|
3633
|
+
runCommand(
|
|
3634
|
+
"beginNodeDrag",
|
|
3635
|
+
[id, pointerId, screenPoint],
|
|
3636
|
+
() => {
|
|
3637
|
+
assertBoardNode(id);
|
|
3638
|
+
discardActiveInteraction();
|
|
3639
|
+
const node = assertBoardNode(id);
|
|
3640
|
+
if (node.locked) {
|
|
3641
|
+
return;
|
|
3642
|
+
}
|
|
3643
|
+
activeGestureHistoryRoot = captureHistoryRoot();
|
|
3644
|
+
const initialSelection = state.selection.has(id) ? getSelectionNodes3().filter((entry) => !entry.locked).map((entry) => entry.id) : [id];
|
|
3645
|
+
const nodeIds = collectUniformTranslationTargets(
|
|
3646
|
+
initialSelection,
|
|
3647
|
+
state.nodes
|
|
3648
|
+
);
|
|
3649
|
+
if (!state.selection.has(id)) {
|
|
3650
|
+
setSelection([id]);
|
|
3651
|
+
}
|
|
3652
|
+
const startNodePositions = Object.fromEntries(
|
|
3653
|
+
nodeIds.map((nodeId) => {
|
|
3654
|
+
const current = assertBoardNode(nodeId);
|
|
3655
|
+
return [nodeId, { x: current.x, y: current.y }];
|
|
3656
|
+
})
|
|
3657
|
+
);
|
|
3658
|
+
setInteraction({
|
|
3659
|
+
mode: "dragging-nodes",
|
|
3660
|
+
pointerId,
|
|
3661
|
+
nodeIds,
|
|
3662
|
+
startScreenPoint: { ...screenPoint },
|
|
3663
|
+
startNodePositions
|
|
3664
|
+
});
|
|
3665
|
+
},
|
|
3666
|
+
IGNORE_UNVALIDATED_COMMAND
|
|
3667
|
+
);
|
|
3668
|
+
},
|
|
3669
|
+
beginResize(id, handle, pointerId, screenPoint) {
|
|
3670
|
+
runCommand(
|
|
3671
|
+
"beginResize",
|
|
3672
|
+
[id, handle, pointerId, screenPoint],
|
|
3673
|
+
() => {
|
|
3674
|
+
assertBoardNode(id);
|
|
3675
|
+
discardActiveInteraction();
|
|
3676
|
+
const node = assertBoardNode(id);
|
|
3677
|
+
if (node.locked) {
|
|
3678
|
+
return;
|
|
3679
|
+
}
|
|
3680
|
+
activeGestureHistoryRoot = captureHistoryRoot();
|
|
3681
|
+
setSelection([id]);
|
|
3682
|
+
setInteraction({
|
|
3683
|
+
mode: "resizing-node",
|
|
3684
|
+
pointerId,
|
|
3685
|
+
nodeId: id,
|
|
3686
|
+
handle,
|
|
3687
|
+
startScreenPoint: { ...screenPoint },
|
|
3688
|
+
startNodeBounds: {
|
|
3689
|
+
x: node.x,
|
|
3690
|
+
y: node.y,
|
|
3691
|
+
width: node.width,
|
|
3692
|
+
height: node.height
|
|
3693
|
+
},
|
|
3694
|
+
aspectRatio: node.width / node.height
|
|
3695
|
+
});
|
|
3696
|
+
},
|
|
3697
|
+
IGNORE_UNVALIDATED_COMMAND
|
|
3698
|
+
);
|
|
3699
|
+
},
|
|
3700
|
+
beginBoxSelect(pointerId, screenPoint) {
|
|
3701
|
+
runCommand(
|
|
3702
|
+
"beginBoxSelect",
|
|
3703
|
+
[pointerId, screenPoint],
|
|
3704
|
+
() => {
|
|
3705
|
+
discardActiveInteraction();
|
|
3706
|
+
const worldPoint = engine.screenToWorld(screenPoint);
|
|
3707
|
+
activeBoxSelectionBefore = new Set(state.selection);
|
|
3708
|
+
setSelection([]);
|
|
3709
|
+
setInteraction({
|
|
3710
|
+
mode: "box-select",
|
|
3711
|
+
pointerId,
|
|
3712
|
+
selectionMode: resolveBoxSelectMode(screenPoint, screenPoint),
|
|
3713
|
+
startScreenPoint: { ...screenPoint },
|
|
3714
|
+
currentScreenPoint: { ...screenPoint },
|
|
3715
|
+
startWorldPoint: worldPoint,
|
|
3716
|
+
currentWorldPoint: worldPoint
|
|
3717
|
+
});
|
|
3718
|
+
},
|
|
3719
|
+
IGNORE_UNVALIDATED_COMMAND
|
|
3720
|
+
);
|
|
3721
|
+
},
|
|
3722
|
+
beginTextEdit(id) {
|
|
3723
|
+
runCommand(
|
|
3724
|
+
"beginTextEdit",
|
|
3725
|
+
[id],
|
|
3726
|
+
() => {
|
|
3727
|
+
const node = assertBoardNode(id);
|
|
3728
|
+
if (node.type !== "text") {
|
|
3729
|
+
throw new BoardInputError(
|
|
3730
|
+
`Cannot edit text for ${node.type} node "${id}".`
|
|
3731
|
+
);
|
|
3732
|
+
}
|
|
3733
|
+
discardActiveInteraction();
|
|
3734
|
+
setSelection([id]);
|
|
3735
|
+
setInteraction({ mode: "editing-text", nodeId: id });
|
|
3736
|
+
},
|
|
3737
|
+
IGNORE_UNVALIDATED_COMMAND
|
|
3738
|
+
);
|
|
3739
|
+
},
|
|
3740
|
+
commitTextEdit(id, text) {
|
|
3741
|
+
return runCommand("commitTextEdit", [id, text], () => {
|
|
3742
|
+
const node = assertBoardNode(id);
|
|
3743
|
+
if (node.type !== "text") {
|
|
3744
|
+
throw new BoardInputError(
|
|
3745
|
+
`Cannot edit text for ${node.type} node "${id}".`
|
|
3746
|
+
);
|
|
3747
|
+
}
|
|
3748
|
+
let stored = node;
|
|
3749
|
+
if (text !== void 0) {
|
|
3750
|
+
stored = replaceBoardNode(node, { ...node, text });
|
|
3751
|
+
emit("node:updated", materializeNode2(stored), materializeNode2(node));
|
|
3752
|
+
}
|
|
3753
|
+
setInteraction({ mode: "idle" });
|
|
3754
|
+
return materializeNode2(stored);
|
|
3755
|
+
});
|
|
3756
|
+
},
|
|
3757
|
+
cancelTextEdit() {
|
|
3758
|
+
if (state.interaction.mode !== "editing-text") return;
|
|
3759
|
+
runCommand(
|
|
3760
|
+
"cancelTextEdit",
|
|
3761
|
+
[],
|
|
3762
|
+
() => setInteraction({ mode: "idle" }),
|
|
3763
|
+
IGNORE_UNVALIDATED_COMMAND
|
|
3764
|
+
);
|
|
3765
|
+
},
|
|
3766
|
+
updatePointer(pointerId, screenPoint, modifiers) {
|
|
3767
|
+
const interaction = state.interaction;
|
|
3768
|
+
if (interaction.mode === "idle" || interaction.mode === "editing-text" || interaction.pointerId !== pointerId) {
|
|
3769
|
+
return;
|
|
3770
|
+
}
|
|
3771
|
+
if (interaction.mode === "panning") {
|
|
3772
|
+
runCommand(
|
|
3773
|
+
"updatePointer",
|
|
3774
|
+
[pointerId, screenPoint],
|
|
3775
|
+
() => {
|
|
3776
|
+
const deltaX = screenPoint.x - interaction.lastScreenPoint.x;
|
|
3777
|
+
const deltaY = screenPoint.y - interaction.lastScreenPoint.y;
|
|
3778
|
+
setCamera({
|
|
3779
|
+
x: state.camera.x + deltaX / state.camera.z,
|
|
3780
|
+
y: state.camera.y + deltaY / state.camera.z,
|
|
3781
|
+
z: state.camera.z
|
|
3782
|
+
});
|
|
3783
|
+
setInteraction({
|
|
3784
|
+
...interaction,
|
|
3785
|
+
lastScreenPoint: { ...screenPoint }
|
|
3786
|
+
});
|
|
3787
|
+
},
|
|
3788
|
+
IGNORE_UNVALIDATED_COMMAND
|
|
3789
|
+
);
|
|
3790
|
+
return;
|
|
3791
|
+
}
|
|
3792
|
+
if (interaction.mode === "dragging-nodes") {
|
|
3793
|
+
runCommand(
|
|
3794
|
+
"updatePointer",
|
|
3795
|
+
[pointerId, screenPoint, modifiers],
|
|
3796
|
+
() => {
|
|
3797
|
+
const rawDeltaX = (screenPoint.x - interaction.startScreenPoint.x) / state.camera.z;
|
|
3798
|
+
const rawDeltaY = (screenPoint.y - interaction.startScreenPoint.y) / state.camera.z;
|
|
3799
|
+
const axisLocked = Boolean(modifiers?.shift);
|
|
3800
|
+
const deltaX = axisLocked && Math.abs(rawDeltaY) > Math.abs(rawDeltaX) ? 0 : rawDeltaX;
|
|
3801
|
+
const deltaY = axisLocked && Math.abs(rawDeltaX) >= Math.abs(rawDeltaY) ? 0 : rawDeltaY;
|
|
3802
|
+
const bypassSnapping = Boolean(modifiers?.space);
|
|
3803
|
+
const snapToGrid = grid.snap && !bypassSnapping;
|
|
3804
|
+
const prelimBounds = {};
|
|
3805
|
+
let minX = Infinity;
|
|
3806
|
+
let minY = Infinity;
|
|
3807
|
+
let maxX = -Infinity;
|
|
3808
|
+
let maxY = -Infinity;
|
|
3809
|
+
for (const nodeId of interaction.nodeIds) {
|
|
3810
|
+
const node = assertBoardNode(nodeId);
|
|
3811
|
+
const origin = interaction.startNodePositions[nodeId];
|
|
3812
|
+
if (!origin) {
|
|
3813
|
+
continue;
|
|
3814
|
+
}
|
|
3815
|
+
const x = snapToGrid ? snapValue(origin.x + deltaX, grid.size) : origin.x + deltaX;
|
|
3816
|
+
const y = snapToGrid ? snapValue(origin.y + deltaY, grid.size) : origin.y + deltaY;
|
|
3817
|
+
prelimBounds[nodeId] = {
|
|
3818
|
+
x,
|
|
3819
|
+
y,
|
|
3820
|
+
width: node.width,
|
|
3821
|
+
height: node.height
|
|
3822
|
+
};
|
|
3823
|
+
minX = Math.min(minX, x);
|
|
3824
|
+
minY = Math.min(minY, y);
|
|
3825
|
+
maxX = Math.max(maxX, x + node.width);
|
|
3826
|
+
maxY = Math.max(maxY, y + node.height);
|
|
3827
|
+
}
|
|
3828
|
+
const snapResult = bypassSnapping || !grid.edgeSnap ? { dx: 0, dy: 0, guides: [] } : (() => {
|
|
3829
|
+
const excludeIds = new Set(interaction.nodeIds);
|
|
3830
|
+
const groupBounds = {
|
|
3831
|
+
x: minX,
|
|
3832
|
+
y: minY,
|
|
3833
|
+
width: maxX - minX,
|
|
3834
|
+
height: maxY - minY
|
|
3835
|
+
};
|
|
3836
|
+
return snapPositionToEdges(
|
|
3837
|
+
groupBounds,
|
|
3838
|
+
getSnapEdgeIndex(),
|
|
3839
|
+
grid.edgeSnapThreshold / state.camera.z,
|
|
3840
|
+
excludeIds
|
|
3841
|
+
);
|
|
3842
|
+
})();
|
|
3843
|
+
setSnapGuides(snapResult.guides);
|
|
3844
|
+
let movedNodeCount = 0;
|
|
3845
|
+
for (const nodeId of interaction.nodeIds) {
|
|
3846
|
+
const current = assertBoardNode(nodeId);
|
|
3847
|
+
const preliminary = prelimBounds[nodeId];
|
|
3848
|
+
if (!preliminary) {
|
|
3849
|
+
continue;
|
|
3850
|
+
}
|
|
3851
|
+
setNodeOverride({
|
|
3852
|
+
...current,
|
|
3853
|
+
x: preliminary.x + snapResult.dx,
|
|
3854
|
+
y: preliminary.y + snapResult.dy
|
|
3855
|
+
});
|
|
3856
|
+
movedNodeCount += 1;
|
|
3857
|
+
}
|
|
3858
|
+
if (movedNodeCount > 0) {
|
|
3859
|
+
notifyNodesChanged();
|
|
3860
|
+
}
|
|
3861
|
+
},
|
|
3862
|
+
IGNORE_UNVALIDATED_COMMAND
|
|
3863
|
+
);
|
|
3864
|
+
return;
|
|
3865
|
+
}
|
|
3866
|
+
if (interaction.mode === "resizing-node") {
|
|
3867
|
+
runCommand(
|
|
3868
|
+
"updatePointer",
|
|
3869
|
+
[pointerId, screenPoint, modifiers],
|
|
3870
|
+
() => {
|
|
3871
|
+
const node = assertBoardNode(interaction.nodeId);
|
|
3872
|
+
const deltaX = (screenPoint.x - interaction.startScreenPoint.x) / state.camera.z;
|
|
3873
|
+
const deltaY = (screenPoint.y - interaction.startScreenPoint.y) / state.camera.z;
|
|
3874
|
+
const constraints = {
|
|
3875
|
+
minWidth: nodeConstraints.minWidth,
|
|
3876
|
+
minHeight: nodeConstraints.minHeight
|
|
3877
|
+
};
|
|
3878
|
+
const locked = Boolean(modifiers?.shift);
|
|
3879
|
+
const bypassSnapping = Boolean(modifiers?.space);
|
|
3880
|
+
const raw = locked ? applyResizeDeltaLocked(
|
|
3881
|
+
interaction.startNodeBounds,
|
|
3882
|
+
interaction.handle,
|
|
3883
|
+
deltaX,
|
|
3884
|
+
deltaY,
|
|
3885
|
+
constraints,
|
|
3886
|
+
interaction.aspectRatio
|
|
3887
|
+
) : applyResizeDelta(
|
|
3888
|
+
interaction.startNodeBounds,
|
|
3889
|
+
interaction.handle,
|
|
3890
|
+
deltaX,
|
|
3891
|
+
deltaY,
|
|
3892
|
+
constraints
|
|
3893
|
+
);
|
|
3894
|
+
if (locked) {
|
|
3895
|
+
const nextBounds = !bypassSnapping && grid.snap ? snapResizedBoundsLocked(
|
|
3896
|
+
raw,
|
|
3897
|
+
interaction.startNodeBounds,
|
|
3898
|
+
interaction.handle,
|
|
3899
|
+
grid.size,
|
|
3900
|
+
constraints,
|
|
3901
|
+
interaction.aspectRatio
|
|
3902
|
+
) : raw;
|
|
3903
|
+
setSnapGuides([]);
|
|
3904
|
+
setNodeOverride({
|
|
3905
|
+
...node,
|
|
3906
|
+
...nextBounds
|
|
3907
|
+
});
|
|
3908
|
+
} else {
|
|
3909
|
+
const gridSnapped = !bypassSnapping && grid.snap ? snapResizedBounds(
|
|
3910
|
+
raw,
|
|
3911
|
+
interaction.handle,
|
|
3912
|
+
grid.size,
|
|
3913
|
+
constraints
|
|
3914
|
+
) : raw;
|
|
3915
|
+
if (bypassSnapping || !grid.edgeSnap) {
|
|
3916
|
+
setSnapGuides([]);
|
|
3917
|
+
setNodeOverride({
|
|
3918
|
+
...node,
|
|
3919
|
+
...gridSnapped
|
|
3920
|
+
});
|
|
3921
|
+
} else {
|
|
3922
|
+
const snapResult = snapBoundsToEdges(
|
|
3923
|
+
gridSnapped,
|
|
3924
|
+
interaction.handle,
|
|
3925
|
+
getSnapEdgeIndex(),
|
|
3926
|
+
grid.edgeSnapThreshold / state.camera.z,
|
|
3927
|
+
/* @__PURE__ */ new Set([interaction.nodeId])
|
|
3928
|
+
);
|
|
3929
|
+
setSnapGuides(snapResult.guides);
|
|
3930
|
+
setNodeOverride({
|
|
3931
|
+
...node,
|
|
3932
|
+
...snapResult.bounds
|
|
3933
|
+
});
|
|
3934
|
+
}
|
|
3935
|
+
}
|
|
3936
|
+
notifyNodesChanged();
|
|
3937
|
+
},
|
|
3938
|
+
IGNORE_UNVALIDATED_COMMAND
|
|
3939
|
+
);
|
|
3940
|
+
return;
|
|
3941
|
+
}
|
|
3942
|
+
runCommand(
|
|
3943
|
+
"updatePointer",
|
|
3944
|
+
[pointerId, screenPoint],
|
|
3945
|
+
() => {
|
|
3946
|
+
const currentWorldPoint = engine.screenToWorld(screenPoint);
|
|
3947
|
+
const bounds = getBoundsFromPoints(
|
|
3948
|
+
interaction.startWorldPoint,
|
|
3949
|
+
currentWorldPoint
|
|
3950
|
+
);
|
|
3951
|
+
const selectionMode = resolveBoxSelectMode(
|
|
3952
|
+
interaction.startScreenPoint,
|
|
3953
|
+
screenPoint
|
|
3954
|
+
);
|
|
3955
|
+
const matches = Array.from(state.nodes.values()).filter((node) => node.visible).filter(
|
|
3956
|
+
(node) => selectionMode === "window" ? boundsContain(bounds, getBoundsFromNode(node)) : boundsIntersect(getBoundsFromNode(node), bounds)
|
|
3957
|
+
).map((node) => node.id);
|
|
3958
|
+
setSelection(matches);
|
|
3959
|
+
setInteraction({
|
|
3960
|
+
...interaction,
|
|
3961
|
+
selectionMode,
|
|
3962
|
+
currentScreenPoint: { ...screenPoint },
|
|
3963
|
+
currentWorldPoint
|
|
3964
|
+
});
|
|
3965
|
+
},
|
|
3966
|
+
IGNORE_UNVALIDATED_COMMAND
|
|
3967
|
+
);
|
|
3968
|
+
},
|
|
3969
|
+
endInteraction(pointerId) {
|
|
3970
|
+
const interaction = state.interaction;
|
|
3971
|
+
if (interaction.mode === "idle") {
|
|
3972
|
+
return;
|
|
3973
|
+
}
|
|
3974
|
+
if ("pointerId" in interaction && pointerId !== void 0 && interaction.pointerId !== pointerId) {
|
|
3975
|
+
return;
|
|
3976
|
+
}
|
|
3977
|
+
const gestureCommit = activeGestureHistoryRoot && (interaction.mode === "dragging-nodes" || interaction.mode === "resizing-node") ? {
|
|
3978
|
+
before: activeGestureHistoryRoot,
|
|
3979
|
+
label: interaction.mode === "dragging-nodes" ? "moveNodes" : "resizeNode",
|
|
3980
|
+
metadata: RECORD_COMMAND
|
|
3981
|
+
} : void 0;
|
|
3982
|
+
runCommand(
|
|
3983
|
+
"endInteraction",
|
|
3984
|
+
[pointerId],
|
|
3985
|
+
() => {
|
|
3986
|
+
const previous = state.interaction;
|
|
3987
|
+
setSnapGuides([]);
|
|
3988
|
+
if (previous.mode === "dragging-nodes") {
|
|
3989
|
+
commitNodeOverrides(previous);
|
|
3990
|
+
reparentAfterDrag(previous.nodeIds);
|
|
3991
|
+
reparentNodesCapturedByMovedGroups(previous.nodeIds);
|
|
3992
|
+
}
|
|
3993
|
+
if (previous.mode === "resizing-node") {
|
|
3994
|
+
commitNodeOverrides(previous);
|
|
3995
|
+
reparentNodesCapturedByGroups([previous.nodeId], [previous.nodeId]);
|
|
3996
|
+
}
|
|
3997
|
+
setInteraction({ mode: "idle" });
|
|
3998
|
+
},
|
|
3999
|
+
gestureCommit ? RECORD_COMMAND : IGNORE_COMMAND,
|
|
4000
|
+
gestureCommit
|
|
4001
|
+
);
|
|
4002
|
+
activeGestureHistoryRoot = null;
|
|
4003
|
+
activeBoxSelectionBefore = null;
|
|
4004
|
+
},
|
|
4005
|
+
cancelInteraction(pointerId) {
|
|
4006
|
+
const interaction = state.interaction;
|
|
4007
|
+
if (interaction.mode === "idle") return;
|
|
4008
|
+
if ("pointerId" in interaction && pointerId !== void 0 && interaction.pointerId !== pointerId) {
|
|
4009
|
+
return;
|
|
4010
|
+
}
|
|
4011
|
+
runCommand(
|
|
4012
|
+
"cancelInteraction",
|
|
4013
|
+
[pointerId],
|
|
4014
|
+
() => {
|
|
4015
|
+
discardActiveInteraction();
|
|
4016
|
+
},
|
|
4017
|
+
IGNORE_UNVALIDATED_COMMAND
|
|
4018
|
+
);
|
|
4019
|
+
},
|
|
4020
|
+
getUniformTranslationTargets(seedIds) {
|
|
4021
|
+
assertAlive();
|
|
4022
|
+
return collectUniformTranslationTargets(
|
|
4023
|
+
seedIds,
|
|
4024
|
+
state.nodes
|
|
4025
|
+
);
|
|
4026
|
+
},
|
|
4027
|
+
syncGroupZOrder(groupId) {
|
|
4028
|
+
runCommand("syncGroupZOrder", [groupId], () => {
|
|
4029
|
+
assertBoardNode(groupId);
|
|
4030
|
+
restackGroupDescendantsAbove(groupId);
|
|
4031
|
+
});
|
|
4032
|
+
},
|
|
4033
|
+
exportDocument() {
|
|
4034
|
+
assertAlive();
|
|
4035
|
+
const pluginDocuments = Array.from(
|
|
4036
|
+
pluginPersistence.values(),
|
|
4037
|
+
(entry) => entry.hooks.exportDocument?.(entry.context) ?? {}
|
|
4038
|
+
);
|
|
4039
|
+
return toPersistedDocument(
|
|
4040
|
+
buildSnapshot(state, grid, buildPublicNodeMap(state)),
|
|
4041
|
+
pluginDocuments
|
|
4042
|
+
);
|
|
4043
|
+
},
|
|
4044
|
+
loadDocument(document, options2 = {}) {
|
|
4045
|
+
const mode = options2.mode ?? "replace";
|
|
4046
|
+
runCommand(
|
|
4047
|
+
"loadDocument",
|
|
4048
|
+
[mode],
|
|
4049
|
+
() => {
|
|
4050
|
+
const normalized = normalizeDocumentForImport(document);
|
|
4051
|
+
assertCanRestoreDocument(normalized);
|
|
4052
|
+
const snapshot = documentToSnapshot(normalized);
|
|
4053
|
+
nodeOverrides.clear();
|
|
4054
|
+
activeGestureHistoryRoot = null;
|
|
4055
|
+
activeBoxSelectionBefore = null;
|
|
4056
|
+
const idMap = restoreSnapshot(snapshot, mode);
|
|
4057
|
+
restorePluginDocuments(normalized, mode, idMap);
|
|
4058
|
+
},
|
|
4059
|
+
IGNORE_COMMAND
|
|
4060
|
+
);
|
|
4061
|
+
}
|
|
4062
|
+
};
|
|
4063
|
+
notifyNodesChanged();
|
|
4064
|
+
for (const plugin of options.plugins ?? []) {
|
|
4065
|
+
assertInternalBoardPlugin(plugin);
|
|
4066
|
+
installPlugin(plugin);
|
|
4067
|
+
}
|
|
4068
|
+
if (initialDocument) {
|
|
4069
|
+
assertCanRestoreDocument(initialDocument);
|
|
4070
|
+
runCommand(
|
|
4071
|
+
"initializeDocument",
|
|
4072
|
+
[],
|
|
4073
|
+
() => restorePluginDocuments(initialDocument, "replace"),
|
|
4074
|
+
IGNORE_COMMAND
|
|
4075
|
+
);
|
|
4076
|
+
}
|
|
4077
|
+
validate("createBoardEngine");
|
|
4078
|
+
const internalKeys = /* @__PURE__ */ new Set([
|
|
4079
|
+
"emit",
|
|
4080
|
+
"assertActive",
|
|
4081
|
+
"isBatching",
|
|
4082
|
+
"extend",
|
|
4083
|
+
"runCommand",
|
|
4084
|
+
"projectCommit",
|
|
4085
|
+
"restoreHistoryRoot",
|
|
4086
|
+
"getPluginState",
|
|
4087
|
+
"updatePluginState",
|
|
4088
|
+
"beginPan",
|
|
4089
|
+
"beginNodeDrag",
|
|
4090
|
+
"beginResize",
|
|
4091
|
+
"beginBoxSelect",
|
|
4092
|
+
"updatePointer",
|
|
4093
|
+
"endInteraction",
|
|
4094
|
+
"cancelInteraction",
|
|
4095
|
+
"getUniformTranslationTargets",
|
|
4096
|
+
"syncGroupZOrder"
|
|
4097
|
+
]);
|
|
4098
|
+
const publicEngine = Object.fromEntries(
|
|
4099
|
+
Object.entries(engine).filter(([key]) => !internalKeys.has(key))
|
|
4100
|
+
);
|
|
4101
|
+
registerBoardInteractionAdapter(publicEngine, engine);
|
|
4102
|
+
return publicEngine;
|
|
4103
|
+
}
|
|
4104
|
+
|
|
4105
|
+
// src/types.ts
|
|
4106
|
+
var asNodeId = (value) => value;
|
|
4107
|
+
var asEdgeId = (value) => value;
|
|
4108
|
+
|
|
4109
|
+
// src/selection.ts
|
|
4110
|
+
function getSelectionNodes2(engine) {
|
|
4111
|
+
const selected = new Set(engine.getSelection());
|
|
4112
|
+
return Array.from(engine.getState().nodes.values()).filter(
|
|
4113
|
+
(node) => selected.has(node.id)
|
|
4114
|
+
);
|
|
4115
|
+
}
|
|
4116
|
+
function getSelectionBounds(engine) {
|
|
4117
|
+
const nodes = getSelectionNodes2(engine);
|
|
4118
|
+
if (nodes.length === 0) {
|
|
4119
|
+
return null;
|
|
4120
|
+
}
|
|
4121
|
+
return {
|
|
4122
|
+
minX: Math.min(...nodes.map((node) => node.x)),
|
|
4123
|
+
minY: Math.min(...nodes.map((node) => node.y)),
|
|
4124
|
+
maxX: Math.max(...nodes.map((node) => node.x + node.width)),
|
|
4125
|
+
maxY: Math.max(...nodes.map((node) => node.y + node.height))
|
|
4126
|
+
};
|
|
4127
|
+
}
|
|
4128
|
+
function toggleIds(current, ids) {
|
|
4129
|
+
const next = new Set(current);
|
|
4130
|
+
for (const id of ids) {
|
|
4131
|
+
if (next.has(id)) {
|
|
4132
|
+
next.delete(id);
|
|
4133
|
+
} else {
|
|
4134
|
+
next.add(id);
|
|
4135
|
+
}
|
|
4136
|
+
}
|
|
4137
|
+
return Array.from(next);
|
|
4138
|
+
}
|
|
4139
|
+
export {
|
|
4140
|
+
BOARD_COLOR_PRESETS,
|
|
4141
|
+
BoardConflictError,
|
|
4142
|
+
BoardDestroyedError,
|
|
4143
|
+
BoardError,
|
|
4144
|
+
BoardInputError,
|
|
4145
|
+
BoardNotFoundError,
|
|
4146
|
+
CommandBlockedError,
|
|
4147
|
+
asEdgeId,
|
|
4148
|
+
asNodeId,
|
|
4149
|
+
boundsIntersect,
|
|
4150
|
+
clamp,
|
|
4151
|
+
colorForPreset,
|
|
4152
|
+
createBoardEngine,
|
|
4153
|
+
getBoundsFromPoints,
|
|
4154
|
+
getSelectionBounds,
|
|
4155
|
+
getSelectionNodes2 as getSelectionNodes,
|
|
4156
|
+
getVisibleBounds,
|
|
4157
|
+
isBoardColorPreset,
|
|
4158
|
+
toggleIds
|
|
4159
|
+
};
|