@weasel-js/diagram 1.4.3 → 1.5.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/README.md +69 -2
- package/dist/index.d.ts +1100 -5
- package/dist/index.js +1339 -17
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { rectPath, polygonFromPoints, PathBuilder,
|
|
1
|
+
import { AUTO_POSE_DESCRIPTOR, pointAlongPath, translatePoseViaDescriptor, polylineFromPoints, rotatePoint, rectPath, polygonFromPoints, PATH_M, PATH_L, PATH_C, flattenCubic, PATH_Q, PATH_Z, PathBuilder, effectivePose, composeAffordanceLayer, createSimulation, usePoseRun, RECT_POSE_DESCRIPTOR, registerNodeShape } from '@weasel-js/core';
|
|
2
|
+
import { useRef, useCallback, useEffect, useMemo } from 'react';
|
|
2
3
|
|
|
3
4
|
// src/outline.ts
|
|
4
5
|
var LEAN = 0.2;
|
|
@@ -49,6 +50,136 @@ function stadiumPath(x, y, w, h) {
|
|
|
49
50
|
const midX = x + w / 2;
|
|
50
51
|
return b.moveTo(right, top).lineTo(right, bottom).curveTo(right, bottom + k, midX + k, y + h, midX, y + h).curveTo(midX - k, y + h, left, bottom + k, left, bottom).lineTo(left, top).curveTo(left, top - k, midX - k, y, midX, y).curveTo(midX + k, y, right, top - k, right, top).close().build();
|
|
51
52
|
}
|
|
53
|
+
function contentBox(outline, bounds) {
|
|
54
|
+
if (typeof outline !== "string") return bounds;
|
|
55
|
+
const { x, y, width: w, height: h } = bounds;
|
|
56
|
+
switch (outline) {
|
|
57
|
+
case "rect":
|
|
58
|
+
return bounds;
|
|
59
|
+
case "diamond":
|
|
60
|
+
return { x: x + w / 4, y: y + h / 4, width: w / 2, height: h / 2 };
|
|
61
|
+
case "parallelogram": {
|
|
62
|
+
const lean = Math.min(w * LEAN, w / 2);
|
|
63
|
+
return { x: x + lean, y, width: Math.max(w - lean * 2, 0), height: h };
|
|
64
|
+
}
|
|
65
|
+
case "stadium": {
|
|
66
|
+
const r = Math.min(w, h) / 2;
|
|
67
|
+
return w >= h ? { x: x + r, y, width: Math.max(w - r * 2, 0), height: h } : { x, y: y + r, width: w, height: Math.max(h - r * 2, 0) };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function boxForContent(outline, content) {
|
|
72
|
+
if (typeof outline !== "string") return content;
|
|
73
|
+
switch (outline) {
|
|
74
|
+
case "rect":
|
|
75
|
+
return content;
|
|
76
|
+
case "diamond":
|
|
77
|
+
return { width: content.width * 2, height: content.height * 2 };
|
|
78
|
+
case "parallelogram":
|
|
79
|
+
return { width: content.width / (1 - LEAN * 2), height: content.height };
|
|
80
|
+
case "stadium":
|
|
81
|
+
return { width: content.width + content.height, height: content.height };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
var TOLERANCE = 0.25;
|
|
85
|
+
function outlinePolyline(outline, bounds) {
|
|
86
|
+
const path = outlinePath(outline, bounds);
|
|
87
|
+
return flattenPath(path);
|
|
88
|
+
}
|
|
89
|
+
function flattenPath(path) {
|
|
90
|
+
if (path.kind === "rect") {
|
|
91
|
+
const { x, y, width: w, height: h } = path;
|
|
92
|
+
return [x, y, x + w, y, x + w, y + h, x, y + h, x, y];
|
|
93
|
+
}
|
|
94
|
+
const out = [];
|
|
95
|
+
const { commands, coords } = path;
|
|
96
|
+
let ci = 0;
|
|
97
|
+
let cx = 0;
|
|
98
|
+
let cy = 0;
|
|
99
|
+
let sx = 0;
|
|
100
|
+
let sy = 0;
|
|
101
|
+
for (const cmd of commands) {
|
|
102
|
+
if (cmd === PATH_M) {
|
|
103
|
+
cx = coords[ci];
|
|
104
|
+
cy = coords[ci + 1];
|
|
105
|
+
ci += 2;
|
|
106
|
+
sx = cx;
|
|
107
|
+
sy = cy;
|
|
108
|
+
out.push(cx, cy);
|
|
109
|
+
} else if (cmd === PATH_L) {
|
|
110
|
+
cx = coords[ci];
|
|
111
|
+
cy = coords[ci + 1];
|
|
112
|
+
ci += 2;
|
|
113
|
+
out.push(cx, cy);
|
|
114
|
+
} else if (cmd === PATH_C) {
|
|
115
|
+
const x1 = coords[ci], y1 = coords[ci + 1];
|
|
116
|
+
const x2 = coords[ci + 2], y2 = coords[ci + 3];
|
|
117
|
+
const x3 = coords[ci + 4], y3 = coords[ci + 5];
|
|
118
|
+
ci += 6;
|
|
119
|
+
flattenCubic(cx, cy, x1, y1, x2, y2, x3, y3, TOLERANCE, out);
|
|
120
|
+
cx = x3;
|
|
121
|
+
cy = y3;
|
|
122
|
+
} else if (cmd === PATH_Q) {
|
|
123
|
+
const qx = coords[ci], qy = coords[ci + 1];
|
|
124
|
+
const x3 = coords[ci + 2], y3 = coords[ci + 3];
|
|
125
|
+
ci += 4;
|
|
126
|
+
flattenCubic(
|
|
127
|
+
cx,
|
|
128
|
+
cy,
|
|
129
|
+
cx + 2 / 3 * (qx - cx),
|
|
130
|
+
cy + 2 / 3 * (qy - cy),
|
|
131
|
+
x3 + 2 / 3 * (qx - x3),
|
|
132
|
+
y3 + 2 / 3 * (qy - y3),
|
|
133
|
+
x3,
|
|
134
|
+
y3,
|
|
135
|
+
TOLERANCE,
|
|
136
|
+
out
|
|
137
|
+
);
|
|
138
|
+
cx = x3;
|
|
139
|
+
cy = y3;
|
|
140
|
+
} else if (cmd === PATH_Z) {
|
|
141
|
+
out.push(sx, sy);
|
|
142
|
+
cx = sx;
|
|
143
|
+
cy = sy;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (out.length >= 2 && (out[0] !== out[out.length - 2] || out[1] !== out[out.length - 1])) {
|
|
147
|
+
out.push(out[0], out[1]);
|
|
148
|
+
}
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
function rayHit(poly, center2, point) {
|
|
152
|
+
const dx = point.x - center2.x;
|
|
153
|
+
const dy = point.y - center2.y;
|
|
154
|
+
if (dx === 0 && dy === 0) return null;
|
|
155
|
+
let bestT = -Infinity;
|
|
156
|
+
let hit = null;
|
|
157
|
+
for (let i = 0; i + 3 < poly.length; i += 2) {
|
|
158
|
+
const ax = poly[i], ay = poly[i + 1];
|
|
159
|
+
const bx = poly[i + 2], by = poly[i + 3];
|
|
160
|
+
const ex = bx - ax;
|
|
161
|
+
const ey = by - ay;
|
|
162
|
+
const denom = dx * ey - dy * ex;
|
|
163
|
+
if (denom === 0) continue;
|
|
164
|
+
const t = ((ax - center2.x) * ey - (ay - center2.y) * ex) / denom;
|
|
165
|
+
const u = ((ax - center2.x) * dy - (ay - center2.y) * dx) / denom;
|
|
166
|
+
if (t < 0 || u < 0 || u > 1) continue;
|
|
167
|
+
if (t > bestT) {
|
|
168
|
+
bestT = t;
|
|
169
|
+
hit = { x: center2.x + dx * t, y: center2.y + dy * t };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return hit;
|
|
173
|
+
}
|
|
174
|
+
function portsOnOutline(ports, outline, bounds) {
|
|
175
|
+
if (bounds.width <= 0 || bounds.height <= 0) return [...ports];
|
|
176
|
+
const poly = outlinePolyline(outline, bounds);
|
|
177
|
+
const center2 = { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 };
|
|
178
|
+
return ports.map((port) => {
|
|
179
|
+
const hit = rayHit(poly, center2, port.point);
|
|
180
|
+
return hit === null ? port : { ...port, point: hit };
|
|
181
|
+
});
|
|
182
|
+
}
|
|
52
183
|
|
|
53
184
|
// src/trait.ts
|
|
54
185
|
var DIAGRAM_TRAIT_KEY = "diagram";
|
|
@@ -56,7 +187,9 @@ var dataKeyReader = (node) => {
|
|
|
56
187
|
const data = node.data;
|
|
57
188
|
if (data === null || typeof data !== "object") return null;
|
|
58
189
|
const trait = data[DIAGRAM_TRAIT_KEY];
|
|
59
|
-
|
|
190
|
+
if (trait === null || typeof trait !== "object") return null;
|
|
191
|
+
if ("from" in trait && "to" in trait) return null;
|
|
192
|
+
return trait;
|
|
60
193
|
};
|
|
61
194
|
function createDiagramNodes(entries) {
|
|
62
195
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -115,11 +248,13 @@ function portsOf(node, pose, opts = {}) {
|
|
|
115
248
|
const rotation = geometry.getRotation?.(pose) ?? 0;
|
|
116
249
|
const cx = bounds.x + bounds.width / 2;
|
|
117
250
|
const cy = bounds.y + bounds.height / 2;
|
|
251
|
+
const poly = trait.outline === void 0 || bounds.width <= 0 || bounds.height <= 0 ? null : outlinePolyline(trait.outline, bounds);
|
|
118
252
|
return (trait.ports ?? DEFAULT_PORTS).map((spec) => {
|
|
119
|
-
const
|
|
253
|
+
const anchored = {
|
|
120
254
|
x: bounds.x + spec.at.u * bounds.width,
|
|
121
255
|
y: bounds.y + spec.at.v * bounds.height
|
|
122
256
|
};
|
|
257
|
+
const flat = (poly === null ? null : rayHit(poly, { x: cx, y: cy }, anchored)) ?? anchored;
|
|
123
258
|
const normal = spec.normal === void 0 ? normalOf(spec.at) : spec.normal;
|
|
124
259
|
if (rotation === 0) {
|
|
125
260
|
return { id: spec.id, nodeId: node.id, point: flat, normal, ...typeOf(spec) };
|
|
@@ -184,7 +319,11 @@ function measureBody(spec, measure) {
|
|
|
184
319
|
width = Math.max(width, m.width);
|
|
185
320
|
height += m.height + (i > 0 ? gap : 0);
|
|
186
321
|
}
|
|
187
|
-
|
|
322
|
+
const box = boxForContent(spec.outline, {
|
|
323
|
+
width: width + padding * 2,
|
|
324
|
+
height: height + padding * 2
|
|
325
|
+
});
|
|
326
|
+
return { minWidth: box.width, minHeight: box.height };
|
|
188
327
|
}
|
|
189
328
|
function sizeToBody(pose, floor) {
|
|
190
329
|
const width = Math.max(pose.width, floor.minWidth);
|
|
@@ -195,34 +334,65 @@ function sizeToBody(pose, floor) {
|
|
|
195
334
|
function layoutBody(spec, bounds, measure) {
|
|
196
335
|
const padding = spec.padding ?? DEFAULT_PADDING;
|
|
197
336
|
const gap = spec.gap ?? DEFAULT_GAP;
|
|
198
|
-
const
|
|
199
|
-
|
|
337
|
+
const inner = contentBox(spec.outline, bounds);
|
|
338
|
+
const width = Math.max(inner.width - padding * 2, 0);
|
|
339
|
+
let y = inner.y + padding;
|
|
200
340
|
return spec.rows.map((row, index) => {
|
|
201
341
|
const h = measureRow(row, measure).height;
|
|
202
|
-
const box = { row, index, x:
|
|
342
|
+
const box = { row, index, x: inner.x + padding, y, width, height: h };
|
|
203
343
|
y += h + gap;
|
|
204
344
|
return box;
|
|
205
345
|
});
|
|
206
346
|
}
|
|
347
|
+
function layoutRowPorts(row, box, measure) {
|
|
348
|
+
const width = (label) => label === void 0 ? 0 : measureRow({ kind: "label", text: label }, measure).width;
|
|
349
|
+
const slots = Math.max(row.left?.length ?? 0, row.right?.length ?? 0, 1);
|
|
350
|
+
const slotHeight = box.height / slots;
|
|
351
|
+
const out = [];
|
|
352
|
+
const side = (ports, which) => {
|
|
353
|
+
for (const [index, port] of (ports ?? []).entries()) {
|
|
354
|
+
const w = width(port.label);
|
|
355
|
+
out.push({
|
|
356
|
+
port,
|
|
357
|
+
side: which,
|
|
358
|
+
index,
|
|
359
|
+
x: which === "left" ? box.x : box.x + box.width - w,
|
|
360
|
+
y: box.y + index * slotHeight,
|
|
361
|
+
width: w,
|
|
362
|
+
height: slotHeight
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
side(row.left, "left");
|
|
367
|
+
side(row.right, "right");
|
|
368
|
+
return out;
|
|
369
|
+
}
|
|
207
370
|
function bodyTrait(spec, bounds, measure) {
|
|
208
|
-
const
|
|
371
|
+
const outline = spec.outline;
|
|
372
|
+
const compass = [
|
|
209
373
|
{ id: "n", at: COMPASS.n },
|
|
210
374
|
{ id: "e", at: COMPASS.e },
|
|
211
375
|
{ id: "s", at: COMPASS.s },
|
|
212
376
|
{ id: "w", at: COMPASS.w }
|
|
213
377
|
];
|
|
214
|
-
if (bounds.height <= 0) return { ports };
|
|
378
|
+
if (bounds.height <= 0) return { outline, ports: compass };
|
|
379
|
+
const rowPorts = [];
|
|
380
|
+
let hasLeft = false;
|
|
381
|
+
let hasRight = false;
|
|
215
382
|
for (const box of layoutBody(spec, bounds, measure)) {
|
|
216
383
|
if (box.row.kind !== "ports") continue;
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
384
|
+
for (const pb of layoutRowPorts(box.row, box, measure)) {
|
|
385
|
+
if (pb.side === "left") hasLeft = true;
|
|
386
|
+
else hasRight = true;
|
|
387
|
+
const v = (pb.y + pb.height / 2 - bounds.y) / bounds.height;
|
|
388
|
+
rowPorts.push(withType(
|
|
389
|
+
{ id: pb.port.id, at: { u: pb.side === "left" ? 0 : 1, v } },
|
|
390
|
+
pb.port.type
|
|
391
|
+
));
|
|
223
392
|
}
|
|
224
393
|
}
|
|
225
|
-
|
|
394
|
+
const kept = compass.filter((p) => !(p.id === "w" && hasLeft) && !(p.id === "e" && hasRight));
|
|
395
|
+
return { outline, ports: [...kept, ...rowPorts] };
|
|
226
396
|
}
|
|
227
397
|
function withType(spec, type) {
|
|
228
398
|
return type === void 0 ? spec : { ...spec, type };
|
|
@@ -230,7 +400,1159 @@ function withType(spec, type) {
|
|
|
230
400
|
function bodyOutline(spec, bounds) {
|
|
231
401
|
return outlinePath(spec.outline, bounds);
|
|
232
402
|
}
|
|
403
|
+
function buildBody(spec, at, opts) {
|
|
404
|
+
const pose = sizeToBody(at, measureBody(spec, opts.measure));
|
|
405
|
+
const specs = [{
|
|
406
|
+
kind: "container",
|
|
407
|
+
layer: opts.layer,
|
|
408
|
+
pose,
|
|
409
|
+
data: opts.body(bodyTrait(spec, pose, opts.measure), pose),
|
|
410
|
+
...opts.id === void 0 ? {} : { id: opts.id }
|
|
411
|
+
}];
|
|
412
|
+
const leaf = (box, data) => {
|
|
413
|
+
specs.push({
|
|
414
|
+
kind: "leaf",
|
|
415
|
+
layer: opts.layer,
|
|
416
|
+
pose: { ...pose, x: box.x, y: box.y, width: box.width, height: box.height },
|
|
417
|
+
data,
|
|
418
|
+
pickable: false,
|
|
419
|
+
...opts.id === void 0 ? {} : { parent: opts.id }
|
|
420
|
+
});
|
|
421
|
+
};
|
|
422
|
+
const portLabel = opts.portLabel;
|
|
423
|
+
for (const box of layoutBody(spec, pose, opts.measure)) {
|
|
424
|
+
const data = opts.row(rowText(box.row), box);
|
|
425
|
+
if (data !== null) leaf(box, data);
|
|
426
|
+
if (portLabel === void 0 || box.row.kind !== "ports") continue;
|
|
427
|
+
for (const pb of layoutRowPorts(box.row, box, opts.measure)) {
|
|
428
|
+
if (pb.port.label === void 0) continue;
|
|
429
|
+
const labelData = portLabel(pb.port.label, pb);
|
|
430
|
+
if (labelData !== null) leaf(pb, labelData);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return { pose, specs };
|
|
434
|
+
}
|
|
435
|
+
function rowText(row) {
|
|
436
|
+
if (row.kind === "label") return row.text;
|
|
437
|
+
if (row.kind === "field") return `${row.label}: ${row.value}`;
|
|
438
|
+
return "";
|
|
439
|
+
}
|
|
440
|
+
var NAMED = { start: 0, mid: 0.5, end: 1 };
|
|
441
|
+
function diagramLabelOf(node) {
|
|
442
|
+
const data = node.data;
|
|
443
|
+
if (data === null || typeof data !== "object") return null;
|
|
444
|
+
const trait = data.diagram;
|
|
445
|
+
if (trait === null || typeof trait !== "object") return null;
|
|
446
|
+
const label = trait.label;
|
|
447
|
+
if (label === null || typeof label !== "object") return null;
|
|
448
|
+
return "at" in label ? label : null;
|
|
449
|
+
}
|
|
450
|
+
function labelDerivePose(opts = {}) {
|
|
451
|
+
const geometry = opts.geometry ?? AUTO_POSE_DESCRIPTOR;
|
|
452
|
+
return (node, deps) => {
|
|
453
|
+
const label = diagramLabelOf(node);
|
|
454
|
+
if (label === null) return null;
|
|
455
|
+
const path = deps[0]?.path ?? null;
|
|
456
|
+
if (path === null) return null;
|
|
457
|
+
const t = typeof label.at === "number" ? label.at : NAMED[label.at] ?? 0.5;
|
|
458
|
+
const station = pointAlongPath(path, t);
|
|
459
|
+
if (station === null) return null;
|
|
460
|
+
const offset = label.offset ?? 0;
|
|
461
|
+
const x = station.point.x + station.tangent.y * offset;
|
|
462
|
+
const y = station.point.y - station.tangent.x * offset;
|
|
463
|
+
const bounds = geometry.getBounds(node.pose);
|
|
464
|
+
return translatePoseViaDescriptor(
|
|
465
|
+
node.pose,
|
|
466
|
+
x - (bounds.x + bounds.width / 2),
|
|
467
|
+
y - (bounds.y + bounds.height / 2),
|
|
468
|
+
geometry
|
|
469
|
+
);
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
var DIAGRAM_LABEL = "diagram:label";
|
|
473
|
+
var LABEL_DERIVE_POSE = labelDerivePose();
|
|
474
|
+
|
|
475
|
+
// src/edge.ts
|
|
476
|
+
var straight = (req) => [req.from.point, ...req.waypoints, req.to.point];
|
|
477
|
+
var orthogonal = (req) => {
|
|
478
|
+
const stops = [req.from.point, ...req.waypoints, req.to.point];
|
|
479
|
+
const out = [stops[0]];
|
|
480
|
+
for (let i = 1; i < stops.length; i++) {
|
|
481
|
+
const a = out[out.length - 1];
|
|
482
|
+
const b = stops[i];
|
|
483
|
+
if (a.x === b.x || a.y === b.y) {
|
|
484
|
+
out.push(b);
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
const leaveHorizontal = i === 1 ? isHorizontal(req.from.normal) : out.length > 1 && out[out.length - 2].y === a.y;
|
|
488
|
+
const arriving = i === stops.length - 1 ? req.to.normal : null;
|
|
489
|
+
const arriveHorizontal = arriving == null ? !leaveHorizontal : isHorizontal(arriving);
|
|
490
|
+
if (leaveHorizontal !== arriveHorizontal) {
|
|
491
|
+
out.push(leaveHorizontal ? { x: b.x, y: a.y } : { x: a.x, y: b.y });
|
|
492
|
+
out.push(b);
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
if (leaveHorizontal) {
|
|
496
|
+
const mid = (a.x + b.x) / 2;
|
|
497
|
+
out.push({ x: mid, y: a.y }, { x: mid, y: b.y }, b);
|
|
498
|
+
} else {
|
|
499
|
+
const mid = (a.y + b.y) / 2;
|
|
500
|
+
out.push({ x: a.x, y: mid }, { x: b.x, y: mid }, b);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return out;
|
|
504
|
+
};
|
|
505
|
+
function isHorizontal(normal) {
|
|
506
|
+
return Math.abs(normal?.x ?? 1) >= Math.abs(normal?.y ?? 0);
|
|
507
|
+
}
|
|
508
|
+
var BEZIER_REACH = 0.4;
|
|
509
|
+
var bezier = (req) => {
|
|
510
|
+
const stops = [req.from.point, ...req.waypoints, req.to.point];
|
|
511
|
+
const out = [stops[0]];
|
|
512
|
+
for (let i = 1; i < stops.length; i++) {
|
|
513
|
+
const a = stops[i - 1];
|
|
514
|
+
const b = stops[i];
|
|
515
|
+
const reach = Math.hypot(b.x - a.x, b.y - a.y) * BEZIER_REACH;
|
|
516
|
+
const outN = i === 1 ? req.from.normal : null;
|
|
517
|
+
const inN = i === stops.length - 1 ? req.to.normal : null;
|
|
518
|
+
const c1 = outN === null || outN === void 0 ? { x: a.x + (b.x - a.x) * BEZIER_REACH, y: a.y + (b.y - a.y) * BEZIER_REACH } : { x: a.x + outN.x * reach, y: a.y + outN.y * reach };
|
|
519
|
+
const c2 = inN === null || inN === void 0 ? { x: b.x - (b.x - a.x) * BEZIER_REACH, y: b.y - (b.y - a.y) * BEZIER_REACH } : { x: b.x + inN.x * reach, y: b.y + inN.y * reach };
|
|
520
|
+
for (let s = 1; s <= BEZIER_SAMPLES; s++) {
|
|
521
|
+
out.push(cubicAt(a, c1, c2, b, s / BEZIER_SAMPLES));
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return out;
|
|
525
|
+
};
|
|
526
|
+
var BEZIER_SAMPLES = 16;
|
|
527
|
+
function cubicAt(p0, p1, p2, p3, t) {
|
|
528
|
+
const u = 1 - t;
|
|
529
|
+
const a = u * u * u;
|
|
530
|
+
const b = 3 * u * u * t;
|
|
531
|
+
const c = 3 * u * t * t;
|
|
532
|
+
const d = t * t * t;
|
|
533
|
+
return {
|
|
534
|
+
x: a * p0.x + b * p1.x + c * p2.x + d * p3.x,
|
|
535
|
+
y: a * p0.y + b * p1.y + c * p2.y + d * p3.y
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
var ROUTERS = Object.freeze({
|
|
539
|
+
straight,
|
|
540
|
+
orthogonal,
|
|
541
|
+
bezier
|
|
542
|
+
});
|
|
543
|
+
function diagramEdgeOf(node) {
|
|
544
|
+
const data = node.data;
|
|
545
|
+
if (data === null || typeof data !== "object") return null;
|
|
546
|
+
const edge = data.diagram;
|
|
547
|
+
if (edge === null || typeof edge !== "object") return null;
|
|
548
|
+
return "from" in edge && "to" in edge ? edge : null;
|
|
549
|
+
}
|
|
550
|
+
function resolveEnd(end, dep, toward, opts, allPorts) {
|
|
551
|
+
if (end.port !== void 0) {
|
|
552
|
+
const named = portOf(dep.node, dep.pose, end.port, opts);
|
|
553
|
+
if (named !== void 0) return named;
|
|
554
|
+
}
|
|
555
|
+
let best;
|
|
556
|
+
let bestScore = -Infinity;
|
|
557
|
+
for (const port of allPorts(dep)) {
|
|
558
|
+
const dx = toward.x - port.point.x;
|
|
559
|
+
const dy = toward.y - port.point.y;
|
|
560
|
+
const len = Math.hypot(dx, dy) || 1;
|
|
561
|
+
const facing = port.normal === null ? 0 : (port.normal.x * dx + port.normal.y * dy) / len;
|
|
562
|
+
const score = facing - len / 1e6;
|
|
563
|
+
if (score > bestScore) {
|
|
564
|
+
bestScore = score;
|
|
565
|
+
best = port;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return best;
|
|
569
|
+
}
|
|
570
|
+
function edgeDerivePath(opts = {}) {
|
|
571
|
+
const geometry = opts.geometry ?? AUTO_POSE_DESCRIPTOR;
|
|
572
|
+
const portOpts = { geometry, ...opts.read ? { read: opts.read } : {} };
|
|
573
|
+
const allPorts = (d) => portsOf(d.node, d.pose, portOpts);
|
|
574
|
+
const centerOf = (d) => {
|
|
575
|
+
const b = geometry.getBounds(d.pose);
|
|
576
|
+
return { x: b.x + b.width / 2, y: b.y + b.height / 2 };
|
|
577
|
+
};
|
|
578
|
+
return (node, deps) => {
|
|
579
|
+
const edge = diagramEdgeOf(node);
|
|
580
|
+
if (edge === null) return null;
|
|
581
|
+
const [fromDep, toDep] = deps;
|
|
582
|
+
if (fromDep === void 0 || toDep === void 0) return null;
|
|
583
|
+
const from = resolveEnd(edge.from, fromDep, centerOf(toDep), portOpts, allPorts);
|
|
584
|
+
const to = resolveEnd(edge.to, toDep, centerOf(fromDep), portOpts, allPorts);
|
|
585
|
+
if (from === void 0 || to === void 0) return null;
|
|
586
|
+
const routers = opts.routers ?? ROUTERS;
|
|
587
|
+
const router = edge.router !== void 0 && routers[edge.router] || straight;
|
|
588
|
+
const points = router({ from, to, waypoints: edge.waypoints ?? [] });
|
|
589
|
+
return points.length < 2 ? null : polylineFromPoints(points);
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
var DIAGRAM_EDGE = "diagram:edge";
|
|
593
|
+
var EDGE_DERIVE_PATH = edgeDerivePath();
|
|
594
|
+
function withDiagramRegistry(registry = {}) {
|
|
595
|
+
return {
|
|
596
|
+
...registry,
|
|
597
|
+
derivePath: {
|
|
598
|
+
[DIAGRAM_EDGE]: EDGE_DERIVE_PATH,
|
|
599
|
+
...registry.derivePath
|
|
600
|
+
},
|
|
601
|
+
derivePose: {
|
|
602
|
+
[DIAGRAM_LABEL]: LABEL_DERIVE_POSE,
|
|
603
|
+
...registry.derivePose
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// src/portAffordance.ts
|
|
609
|
+
var PORT_LAYER_ID = "diagram-ports";
|
|
610
|
+
var PORT_AFFORDANCE_KIND = `layer:${PORT_LAYER_ID}`;
|
|
611
|
+
var DEFAULT_HIT_PX = 8;
|
|
612
|
+
var DEFAULT_SIZE_PX = 7;
|
|
613
|
+
function portScratchOf(hit) {
|
|
614
|
+
const payload = hit?.payload;
|
|
615
|
+
return payload !== void 0 && typeof payload.portId === "string" ? payload : null;
|
|
616
|
+
}
|
|
617
|
+
function createPortAffordance(participants, opts = {}) {
|
|
618
|
+
const {
|
|
619
|
+
id = PORT_LAYER_ID,
|
|
620
|
+
hitRadiusPx = DEFAULT_HIT_PX,
|
|
621
|
+
sizePx = DEFAULT_SIZE_PX,
|
|
622
|
+
strength = "exclusive"
|
|
623
|
+
} = opts;
|
|
624
|
+
const portOpts = {
|
|
625
|
+
...opts.geometry ? { geometry: opts.geometry } : {},
|
|
626
|
+
...opts.read ? { read: opts.read } : {}
|
|
627
|
+
};
|
|
628
|
+
const paint = sizePx <= 0 ? void 0 : {
|
|
629
|
+
kind: "square",
|
|
630
|
+
sizePx,
|
|
631
|
+
...opts.fill ? { fill: opts.fill } : {},
|
|
632
|
+
...opts.stroke ? { stroke: opts.stroke } : {}
|
|
633
|
+
};
|
|
634
|
+
return {
|
|
635
|
+
id,
|
|
636
|
+
regions(state) {
|
|
637
|
+
const out = [];
|
|
638
|
+
for (const { node, pose } of participants()) {
|
|
639
|
+
if (opts.shows !== void 0 && !opts.shows(node, state)) continue;
|
|
640
|
+
for (const port of portsOf(node, pose, portOpts)) {
|
|
641
|
+
out.push({
|
|
642
|
+
id: `${node.id}:${port.id}`,
|
|
643
|
+
targetId: null,
|
|
644
|
+
shape: { kind: "point", x: port.point.x, y: port.point.y, hitRadiusPx },
|
|
645
|
+
...paint ? { paint } : {},
|
|
646
|
+
...opts.cursor !== void 0 ? { cursor: opts.cursor } : {},
|
|
647
|
+
strength,
|
|
648
|
+
claimedKinds: ["pointer"],
|
|
649
|
+
bind: () => ({
|
|
650
|
+
initialScratch: {
|
|
651
|
+
targetId: node.id,
|
|
652
|
+
nodeId: node.id,
|
|
653
|
+
portId: port.id,
|
|
654
|
+
port
|
|
655
|
+
}
|
|
656
|
+
})
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
return out;
|
|
661
|
+
}
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// src/connect.ts
|
|
666
|
+
var CONNECT_ACTION_ID = "diagram.connect";
|
|
667
|
+
var defaultCanConnect = (from, to) => {
|
|
668
|
+
if (from.nodeId === to.nodeId && from.id === to.id) return false;
|
|
669
|
+
if (from.type === void 0 || to.type === void 0) return true;
|
|
670
|
+
return from.type === to.type;
|
|
671
|
+
};
|
|
672
|
+
var DEFAULT_SNAP = 16;
|
|
673
|
+
var DEFAULT_EDGE_STROKE = {
|
|
674
|
+
paint: { color: "#7ba7c7" },
|
|
675
|
+
width: 2,
|
|
676
|
+
markerEnd: "arrow"
|
|
677
|
+
};
|
|
678
|
+
var DEFAULT_ROUTER = "straight";
|
|
679
|
+
function connectBinding(actionId = CONNECT_ACTION_ID) {
|
|
680
|
+
return {
|
|
681
|
+
spec: { kind: "drag", target: `affordance:${PORT_AFFORDANCE_KIND}` },
|
|
682
|
+
actionId
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
var GRAB_PORT_ACTION_ID = "diagram.grabPort";
|
|
686
|
+
function grabPortAction(id = GRAB_PORT_ACTION_ID) {
|
|
687
|
+
return {
|
|
688
|
+
id,
|
|
689
|
+
label: "Grab port",
|
|
690
|
+
group: "diagram",
|
|
691
|
+
invoker: { timing: "immediate", run: () => {
|
|
692
|
+
} },
|
|
693
|
+
enabled: () => true
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
function grabPortBindings(actionId = GRAB_PORT_ACTION_ID) {
|
|
697
|
+
return [
|
|
698
|
+
{ spec: { kind: "pointerDown", target: `affordance:${PORT_AFFORDANCE_KIND}` }, actionId },
|
|
699
|
+
{ spec: { kind: "click", target: `affordance:${PORT_AFFORDANCE_KIND}` }, actionId }
|
|
700
|
+
];
|
|
701
|
+
}
|
|
702
|
+
function commitEdgeToScene(edge, ctx) {
|
|
703
|
+
const scene = ctx.deps["scene"];
|
|
704
|
+
if (scene === void 0) return;
|
|
705
|
+
const trait = {
|
|
706
|
+
from: { port: edge.from.id },
|
|
707
|
+
to: { port: edge.to.id },
|
|
708
|
+
router: edge.router
|
|
709
|
+
};
|
|
710
|
+
const source = scene.get(edge.from.nodeId);
|
|
711
|
+
scene.add({
|
|
712
|
+
kind: "leaf",
|
|
713
|
+
layer: source?.layer ?? scene.layers[0].id,
|
|
714
|
+
pose: { x: 0, y: 0, width: 0, height: 0 },
|
|
715
|
+
// An edge runs *from* one node *to* another, and a plain line does not say
|
|
716
|
+
// so. The arrowhead is an ordinary stroke marker, which is why nothing in
|
|
717
|
+
// this package draws one.
|
|
718
|
+
data: { diagram: trait, stroke: DEFAULT_EDGE_STROKE },
|
|
719
|
+
dependsOn: [edge.from.nodeId, edge.to.nodeId],
|
|
720
|
+
// Without this the edge re-resolves on every dependency move and paints
|
|
721
|
+
// nothing. `EDGE_DERIVE_PATH` rather than a fresh `edgeDerivePath()` so the
|
|
722
|
+
// scene can find its registry key and the edge survives `toJSON`.
|
|
723
|
+
derivePath: EDGE_DERIVE_PATH
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
function createConnectAction(opts) {
|
|
727
|
+
const {
|
|
728
|
+
id = CONNECT_ACTION_ID,
|
|
729
|
+
canConnect = defaultCanConnect,
|
|
730
|
+
snapRadius = DEFAULT_SNAP,
|
|
731
|
+
router: routerKey = DEFAULT_ROUTER,
|
|
732
|
+
commit = commitEdgeToScene
|
|
733
|
+
} = opts;
|
|
734
|
+
const routers = opts.routers ?? ROUTERS;
|
|
735
|
+
const portOpts = {
|
|
736
|
+
...opts.geometry ? { geometry: opts.geometry } : {},
|
|
737
|
+
...opts.read ? { read: opts.read } : {}
|
|
738
|
+
};
|
|
739
|
+
const route = routers[routerKey] ?? routers["straight"] ?? ROUTERS["straight"];
|
|
740
|
+
function targetPort(from, at) {
|
|
741
|
+
let best = null;
|
|
742
|
+
let bestD2 = snapRadius * snapRadius;
|
|
743
|
+
for (const { node, pose } of opts.participants()) {
|
|
744
|
+
for (const port of portsOf(node, pose, portOpts)) {
|
|
745
|
+
if (!canConnect(from, port)) continue;
|
|
746
|
+
const d2 = (port.point.x - at.x) ** 2 + (port.point.y - at.y) ** 2;
|
|
747
|
+
if (d2 <= bestD2) {
|
|
748
|
+
bestD2 = d2;
|
|
749
|
+
best = port;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
return best;
|
|
754
|
+
}
|
|
755
|
+
function endAt(from, at) {
|
|
756
|
+
const port = targetPort(from, at);
|
|
757
|
+
return {
|
|
758
|
+
port,
|
|
759
|
+
end: port ?? { id: "", nodeId: "", point: at, normal: null }
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
return {
|
|
763
|
+
id,
|
|
764
|
+
label: "Connect",
|
|
765
|
+
group: "diagram",
|
|
766
|
+
requires: ["scene"],
|
|
767
|
+
invoker: {
|
|
768
|
+
timing: "ongoing",
|
|
769
|
+
start(ctx) {
|
|
770
|
+
const pressed = portScratchOf(ctx.drag?.affordance);
|
|
771
|
+
if (pressed === null) return {};
|
|
772
|
+
const scratch = {
|
|
773
|
+
from: pressed.port,
|
|
774
|
+
current: { x: ctx.world.x, y: ctx.world.y },
|
|
775
|
+
open: true,
|
|
776
|
+
deps: ctx.deps
|
|
777
|
+
};
|
|
778
|
+
return {
|
|
779
|
+
kind: "diagram-connect",
|
|
780
|
+
onMove(moveCtx) {
|
|
781
|
+
scratch.current = { x: moveCtx.world.x, y: moveCtx.world.y };
|
|
782
|
+
},
|
|
783
|
+
overlay() {
|
|
784
|
+
if (!scratch.open) return null;
|
|
785
|
+
const { end } = endAt(scratch.from, scratch.current);
|
|
786
|
+
const points = route({ from: scratch.from, to: end, waypoints: [] });
|
|
787
|
+
if (points.length < 2) return null;
|
|
788
|
+
return { kind: "polyline", points, role: "connector" };
|
|
789
|
+
},
|
|
790
|
+
onEnd(endCtx, reason) {
|
|
791
|
+
scratch.open = false;
|
|
792
|
+
if (reason === "cancel") return;
|
|
793
|
+
const { port } = endAt(scratch.from, { x: endCtx.world.x, y: endCtx.world.y });
|
|
794
|
+
if (port === null) return;
|
|
795
|
+
commit(
|
|
796
|
+
{ from: scratch.from, to: port, router: routerKey },
|
|
797
|
+
{ ...endCtx, deps: scratch.deps }
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
},
|
|
803
|
+
enabled: () => true
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
function sceneParticipants(scene) {
|
|
807
|
+
return function* () {
|
|
808
|
+
for (const node of scene.renderOrderNodes()) {
|
|
809
|
+
yield {
|
|
810
|
+
node,
|
|
811
|
+
pose: effectivePose(scene, node)
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
function portLayer(participants, opts = {}) {
|
|
817
|
+
return composeAffordanceLayer(
|
|
818
|
+
opts.id ?? PORT_LAYER_ID,
|
|
819
|
+
"Diagram ports",
|
|
820
|
+
[createPortAffordance(participants, opts)]
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// src/contribution.ts
|
|
825
|
+
function createDiagramContribution(opts) {
|
|
826
|
+
const actionId = opts.id ?? CONNECT_ACTION_ID;
|
|
827
|
+
return {
|
|
828
|
+
id: "weasel-diagram",
|
|
829
|
+
eligibility: { claimed: true },
|
|
830
|
+
actions: [createConnectAction({ ...opts, id: actionId }), grabPortAction()],
|
|
831
|
+
bindings: [connectBinding(actionId), ...grabPortBindings()]
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
function diagramPorts(opts) {
|
|
835
|
+
return {
|
|
836
|
+
layer: portLayer(opts.participants, opts),
|
|
837
|
+
contribution: createDiagramContribution(opts)
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
function buildGraph(source, opts = {}) {
|
|
841
|
+
const geometry = opts.geometry ?? AUTO_POSE_DESCRIPTOR;
|
|
842
|
+
const nodes = [];
|
|
843
|
+
const byId = /* @__PURE__ */ new Map();
|
|
844
|
+
const candidates = [];
|
|
845
|
+
for (const { node, pose } of source()) {
|
|
846
|
+
if (diagramEdgeOf(node) !== null) {
|
|
847
|
+
const deps = node.dependsOn;
|
|
848
|
+
if (Array.isArray(deps) && deps.length === 2) {
|
|
849
|
+
candidates.push({ id: node.id, from: deps[0], to: deps[1] });
|
|
850
|
+
}
|
|
851
|
+
continue;
|
|
852
|
+
}
|
|
853
|
+
const trait = diagramNodeOf(node, opts.read);
|
|
854
|
+
if (trait === null) continue;
|
|
855
|
+
const entry = {
|
|
856
|
+
id: node.id,
|
|
857
|
+
bounds: geometry.getBounds(pose),
|
|
858
|
+
pinned: trait.pinned === true
|
|
859
|
+
};
|
|
860
|
+
nodes.push(entry);
|
|
861
|
+
byId.set(entry.id, entry);
|
|
862
|
+
}
|
|
863
|
+
const edges = candidates.filter((e) => byId.has(e.from) && byId.has(e.to));
|
|
864
|
+
const out = /* @__PURE__ */ new Map();
|
|
865
|
+
const into = /* @__PURE__ */ new Map();
|
|
866
|
+
for (const edge of edges) {
|
|
867
|
+
push(out, edge.from, edge);
|
|
868
|
+
push(into, edge.to, edge);
|
|
869
|
+
}
|
|
870
|
+
const NONE = Object.freeze([]);
|
|
871
|
+
return {
|
|
872
|
+
nodes,
|
|
873
|
+
edges,
|
|
874
|
+
node: (id) => byId.get(id),
|
|
875
|
+
outgoing: (id) => out.get(id) ?? NONE,
|
|
876
|
+
incoming: (id) => into.get(id) ?? NONE
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
function push(index, key, edge) {
|
|
880
|
+
const list = index.get(key);
|
|
881
|
+
if (list === void 0) index.set(key, [edge]);
|
|
882
|
+
else list.push(edge);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// src/layout.ts
|
|
886
|
+
var DEFAULT_NODE_GAP = 48;
|
|
887
|
+
var DEFAULT_RANK_GAP = 96;
|
|
888
|
+
var DEFAULT_TOLERANCE = 1e-6;
|
|
889
|
+
function axesFor(direction = "down") {
|
|
890
|
+
switch (direction) {
|
|
891
|
+
case "up":
|
|
892
|
+
return { rank: "y", cross: "x", sign: -1 };
|
|
893
|
+
case "right":
|
|
894
|
+
return { rank: "x", cross: "y", sign: 1 };
|
|
895
|
+
case "left":
|
|
896
|
+
return { rank: "x", cross: "y", sign: -1 };
|
|
897
|
+
default:
|
|
898
|
+
return { rank: "y", cross: "x", sign: 1 };
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
function extent(bounds, axis) {
|
|
902
|
+
return axis === "x" ? bounds.width : bounds.height;
|
|
903
|
+
}
|
|
904
|
+
function center(bounds, axis) {
|
|
905
|
+
return axis === "x" ? bounds.x + bounds.width / 2 : bounds.y + bounds.height / 2;
|
|
906
|
+
}
|
|
907
|
+
function pinnedSet(graph, opts = {}) {
|
|
908
|
+
const pinned = new Set(opts.pin ?? []);
|
|
909
|
+
for (const node of graph.nodes) {
|
|
910
|
+
if (node.pinned) pinned.add(node.id);
|
|
911
|
+
}
|
|
912
|
+
return pinned;
|
|
913
|
+
}
|
|
914
|
+
function settle(graph, slots, opts = {}) {
|
|
915
|
+
const axes = axesFor(opts.direction);
|
|
916
|
+
const pinned = pinnedSet(graph, opts);
|
|
917
|
+
const tolerance = opts.tolerance ?? DEFAULT_TOLERANCE;
|
|
918
|
+
const placed = /* @__PURE__ */ new Map();
|
|
919
|
+
for (const node of graph.nodes) {
|
|
920
|
+
const slot = slots.get(node.id);
|
|
921
|
+
if (slot === void 0) continue;
|
|
922
|
+
const along = axes.sign === 1 ? slot.rank : -(slot.rank + extent(node.bounds, axes.rank));
|
|
923
|
+
placed.set(
|
|
924
|
+
node.id,
|
|
925
|
+
axes.rank === "y" ? { x: slot.cross, y: along } : { x: along, y: slot.cross }
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
return translated(graph, placed, pinned, tolerance);
|
|
929
|
+
}
|
|
930
|
+
function translated(graph, placed, pinned, tolerance = DEFAULT_TOLERANCE) {
|
|
931
|
+
const anchor = anchorDelta(graph, placed, pinned);
|
|
932
|
+
const out = /* @__PURE__ */ new Map();
|
|
933
|
+
for (const node of graph.nodes) {
|
|
934
|
+
const at = placed.get(node.id);
|
|
935
|
+
if (at === void 0 || pinned.has(node.id)) continue;
|
|
936
|
+
const x = at.x + anchor.x;
|
|
937
|
+
const y = at.y + anchor.y;
|
|
938
|
+
if (Math.abs(x - node.bounds.x) <= tolerance && Math.abs(y - node.bounds.y) <= tolerance) {
|
|
939
|
+
continue;
|
|
940
|
+
}
|
|
941
|
+
out.set(node.id, { x, y });
|
|
942
|
+
}
|
|
943
|
+
return out;
|
|
944
|
+
}
|
|
945
|
+
function anchorDelta(graph, placed, pinned) {
|
|
946
|
+
for (const node of graph.nodes) {
|
|
947
|
+
if (!pinned.has(node.id)) continue;
|
|
948
|
+
const at = placed.get(node.id);
|
|
949
|
+
if (at !== void 0) return { x: node.bounds.x - at.x, y: node.bounds.y - at.y };
|
|
950
|
+
}
|
|
951
|
+
let minCurX = Infinity;
|
|
952
|
+
let minCurY = Infinity;
|
|
953
|
+
let minNewX = Infinity;
|
|
954
|
+
let minNewY = Infinity;
|
|
955
|
+
for (const node of graph.nodes) {
|
|
956
|
+
const at = placed.get(node.id);
|
|
957
|
+
if (at === void 0) continue;
|
|
958
|
+
minCurX = Math.min(minCurX, node.bounds.x);
|
|
959
|
+
minCurY = Math.min(minCurY, node.bounds.y);
|
|
960
|
+
minNewX = Math.min(minNewX, at.x);
|
|
961
|
+
minNewY = Math.min(minNewY, at.y);
|
|
962
|
+
}
|
|
963
|
+
if (!Number.isFinite(minCurX)) return { x: 0, y: 0 };
|
|
964
|
+
return { x: minCurX - minNewX, y: minCurY - minNewY };
|
|
965
|
+
}
|
|
966
|
+
function packAcross(nodes, axis, gap) {
|
|
967
|
+
const at = /* @__PURE__ */ new Map();
|
|
968
|
+
let cursor = 0;
|
|
969
|
+
for (const node of nodes) {
|
|
970
|
+
at.set(node.id, cursor);
|
|
971
|
+
cursor += extent(node.bounds, axis) + gap;
|
|
972
|
+
}
|
|
973
|
+
return { at, span: nodes.length === 0 ? 0 : cursor - gap };
|
|
974
|
+
}
|
|
975
|
+
function seededOrder(nodes, axis, indexOf) {
|
|
976
|
+
return [...nodes].sort((a, b) => {
|
|
977
|
+
const d = center(a.bounds, axis) - center(b.bounds, axis);
|
|
978
|
+
return d !== 0 ? d : indexOf(a.id) - indexOf(b.id);
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
function graphOrder(graph) {
|
|
982
|
+
const index = /* @__PURE__ */ new Map();
|
|
983
|
+
graph.nodes.forEach((node, i) => index.set(node.id, i));
|
|
984
|
+
return (id) => index.get(id) ?? Number.MAX_SAFE_INTEGER;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// src/force.ts
|
|
988
|
+
var DEFAULTS = {
|
|
989
|
+
iterations: 300,
|
|
990
|
+
linkDistance: 160,
|
|
991
|
+
linkStrength: 0.35,
|
|
992
|
+
charge: -3e3,
|
|
993
|
+
gravity: 0.02,
|
|
994
|
+
padding: 24
|
|
995
|
+
};
|
|
996
|
+
var GOLDEN = Math.PI * (3 - Math.sqrt(5));
|
|
997
|
+
var NUDGE = 0.5;
|
|
998
|
+
function charge(bodies, strength) {
|
|
999
|
+
return (alpha) => {
|
|
1000
|
+
for (let i = 0; i < bodies.length; i++) {
|
|
1001
|
+
const a = bodies[i];
|
|
1002
|
+
for (let j = i + 1; j < bodies.length; j++) {
|
|
1003
|
+
const b = bodies[j];
|
|
1004
|
+
const dx = b.x - a.x;
|
|
1005
|
+
const dy = b.y - a.y;
|
|
1006
|
+
const d2 = dx * dx + dy * dy;
|
|
1007
|
+
if (d2 === 0) continue;
|
|
1008
|
+
const w = strength * alpha / d2;
|
|
1009
|
+
a.vx += dx * w;
|
|
1010
|
+
a.vy += dy * w;
|
|
1011
|
+
b.vx -= dx * w;
|
|
1012
|
+
b.vy -= dy * w;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
function links(bodies, graph, distance, strength) {
|
|
1018
|
+
return (alpha) => {
|
|
1019
|
+
for (const edge of graph.edges) {
|
|
1020
|
+
const a = bodies.get(edge.from);
|
|
1021
|
+
const b = bodies.get(edge.to);
|
|
1022
|
+
if (a === void 0 || b === void 0 || a === b) continue;
|
|
1023
|
+
const dx = b.x - a.x;
|
|
1024
|
+
const dy = b.y - a.y;
|
|
1025
|
+
const d = Math.hypot(dx, dy);
|
|
1026
|
+
if (d === 0) continue;
|
|
1027
|
+
const pull = (d - distance) / d * alpha * strength;
|
|
1028
|
+
a.vx += dx * pull;
|
|
1029
|
+
a.vy += dy * pull;
|
|
1030
|
+
b.vx -= dx * pull;
|
|
1031
|
+
b.vy -= dy * pull;
|
|
1032
|
+
}
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
function separateBoxes(bodies, padding) {
|
|
1036
|
+
return () => {
|
|
1037
|
+
for (let i = 0; i < bodies.length; i++) {
|
|
1038
|
+
const a = bodies[i];
|
|
1039
|
+
for (let j = i + 1; j < bodies.length; j++) {
|
|
1040
|
+
const b = bodies[j];
|
|
1041
|
+
const dx = b.x - a.x;
|
|
1042
|
+
const dy = b.y - a.y;
|
|
1043
|
+
const gapX = a.hw + b.hw + padding - Math.abs(dx);
|
|
1044
|
+
const gapY = a.hh + b.hh + padding - Math.abs(dy);
|
|
1045
|
+
if (gapX <= 0 || gapY <= 0) continue;
|
|
1046
|
+
if (gapX < gapY) {
|
|
1047
|
+
const push2 = (Math.sign(dx) || 1) * gapX * 0.5;
|
|
1048
|
+
a.vx -= push2;
|
|
1049
|
+
b.vx += push2;
|
|
1050
|
+
} else {
|
|
1051
|
+
const push2 = (Math.sign(dy) || 1) * gapY * 0.5;
|
|
1052
|
+
a.vy -= push2;
|
|
1053
|
+
b.vy += push2;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
function gravity(bodies, at, strength) {
|
|
1060
|
+
return (alpha) => {
|
|
1061
|
+
for (const body of bodies) {
|
|
1062
|
+
body.vx += (at.x - body.x) * strength * alpha;
|
|
1063
|
+
body.vy += (at.y - body.y) * strength * alpha;
|
|
1064
|
+
}
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
function forceRelaxation(graph, opts = {}) {
|
|
1068
|
+
const {
|
|
1069
|
+
linkDistance = DEFAULTS.linkDistance,
|
|
1070
|
+
linkStrength = DEFAULTS.linkStrength,
|
|
1071
|
+
charge: chargeStrength = DEFAULTS.charge,
|
|
1072
|
+
gravity: gravityStrength = DEFAULTS.gravity,
|
|
1073
|
+
padding = DEFAULTS.padding
|
|
1074
|
+
} = opts;
|
|
1075
|
+
const pinned = pinnedSet(graph, opts);
|
|
1076
|
+
const bodies = graph.nodes.map((node) => {
|
|
1077
|
+
const body = {
|
|
1078
|
+
id: node.id,
|
|
1079
|
+
x: center(node.bounds, "x"),
|
|
1080
|
+
y: center(node.bounds, "y"),
|
|
1081
|
+
hw: node.bounds.width / 2,
|
|
1082
|
+
hh: node.bounds.height / 2
|
|
1083
|
+
};
|
|
1084
|
+
if (pinned.has(node.id)) {
|
|
1085
|
+
body.fx = body.x;
|
|
1086
|
+
body.fy = body.y;
|
|
1087
|
+
}
|
|
1088
|
+
return body;
|
|
1089
|
+
});
|
|
1090
|
+
separate(bodies);
|
|
1091
|
+
const byId = new Map(bodies.map((b) => [b.id, b]));
|
|
1092
|
+
const centroid = {
|
|
1093
|
+
x: bodies.reduce((sum, b) => sum + b.x, 0) / bodies.length,
|
|
1094
|
+
y: bodies.reduce((sum, b) => sum + b.y, 0) / bodies.length
|
|
1095
|
+
};
|
|
1096
|
+
const sim = createSimulation({
|
|
1097
|
+
nodes: bodies,
|
|
1098
|
+
forces: [
|
|
1099
|
+
charge(bodies, chargeStrength),
|
|
1100
|
+
links(byId, graph, linkDistance, linkStrength),
|
|
1101
|
+
gravity(bodies, centroid, gravityStrength),
|
|
1102
|
+
separateBoxes(bodies, padding)
|
|
1103
|
+
]
|
|
1104
|
+
});
|
|
1105
|
+
return {
|
|
1106
|
+
bodies,
|
|
1107
|
+
byId,
|
|
1108
|
+
pinned,
|
|
1109
|
+
sim,
|
|
1110
|
+
placed() {
|
|
1111
|
+
const placed = /* @__PURE__ */ new Map();
|
|
1112
|
+
for (const node of graph.nodes) {
|
|
1113
|
+
const body = byId.get(node.id);
|
|
1114
|
+
placed.set(node.id, {
|
|
1115
|
+
x: body.x - node.bounds.width / 2,
|
|
1116
|
+
y: body.y - node.bounds.height / 2
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
return placed;
|
|
1120
|
+
}
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
var force = (graph, opts = {}) => {
|
|
1124
|
+
if (graph.nodes.length === 0) return /* @__PURE__ */ new Map();
|
|
1125
|
+
const relaxation = forceRelaxation(graph, opts);
|
|
1126
|
+
relaxation.sim.tick(opts.iterations ?? DEFAULTS.iterations);
|
|
1127
|
+
return translated(graph, relaxation.placed(), relaxation.pinned, opts.tolerance);
|
|
1128
|
+
};
|
|
1129
|
+
function separate(bodies) {
|
|
1130
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1131
|
+
bodies.forEach((body, i) => {
|
|
1132
|
+
const key = `${body.x},${body.y}`;
|
|
1133
|
+
const nth = seen.get(key) ?? 0;
|
|
1134
|
+
seen.set(key, nth + 1);
|
|
1135
|
+
if (nth === 0 || body.fx != null || body.fy != null) return;
|
|
1136
|
+
const angle = GOLDEN * i;
|
|
1137
|
+
const radius = NUDGE * nth;
|
|
1138
|
+
body.x += Math.cos(angle) * radius;
|
|
1139
|
+
body.y += Math.sin(angle) * radius;
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// src/layered.ts
|
|
1144
|
+
function backEdges(graph) {
|
|
1145
|
+
const back = /* @__PURE__ */ new Set();
|
|
1146
|
+
const done = /* @__PURE__ */ new Set();
|
|
1147
|
+
const onStack = /* @__PURE__ */ new Set();
|
|
1148
|
+
const walk = (id) => {
|
|
1149
|
+
if (done.has(id)) return;
|
|
1150
|
+
onStack.add(id);
|
|
1151
|
+
for (const edge of graph.outgoing(id)) {
|
|
1152
|
+
if (onStack.has(edge.to)) back.add(edge.id);
|
|
1153
|
+
else walk(edge.to);
|
|
1154
|
+
}
|
|
1155
|
+
onStack.delete(id);
|
|
1156
|
+
done.add(id);
|
|
1157
|
+
};
|
|
1158
|
+
for (const node of graph.nodes) walk(node.id);
|
|
1159
|
+
return back;
|
|
1160
|
+
}
|
|
1161
|
+
function ranksOf(graph, back) {
|
|
1162
|
+
const ranks = /* @__PURE__ */ new Map();
|
|
1163
|
+
const resolving = /* @__PURE__ */ new Set();
|
|
1164
|
+
const rankOf = (id) => {
|
|
1165
|
+
const known = ranks.get(id);
|
|
1166
|
+
if (known !== void 0) return known;
|
|
1167
|
+
if (resolving.has(id)) return 0;
|
|
1168
|
+
resolving.add(id);
|
|
1169
|
+
let rank = 0;
|
|
1170
|
+
for (const edge of graph.incoming(id)) {
|
|
1171
|
+
if (back.has(edge.id)) continue;
|
|
1172
|
+
rank = Math.max(rank, rankOf(edge.from) + 1);
|
|
1173
|
+
}
|
|
1174
|
+
resolving.delete(id);
|
|
1175
|
+
ranks.set(id, rank);
|
|
1176
|
+
return rank;
|
|
1177
|
+
};
|
|
1178
|
+
for (const node of graph.nodes) rankOf(node.id);
|
|
1179
|
+
return ranks;
|
|
1180
|
+
}
|
|
1181
|
+
var layered = (graph, opts = {}) => {
|
|
1182
|
+
const axes = axesFor(opts.direction);
|
|
1183
|
+
const nodeGap = opts.nodeGap ?? DEFAULT_NODE_GAP;
|
|
1184
|
+
const rankGap = opts.rankGap ?? DEFAULT_RANK_GAP;
|
|
1185
|
+
const order = graphOrder(graph);
|
|
1186
|
+
const ranks = ranksOf(graph, backEdges(graph));
|
|
1187
|
+
const byRank = /* @__PURE__ */ new Map();
|
|
1188
|
+
for (const node of graph.nodes) {
|
|
1189
|
+
const rank = ranks.get(node.id) ?? 0;
|
|
1190
|
+
const list = byRank.get(rank);
|
|
1191
|
+
if (list === void 0) byRank.set(rank, [node]);
|
|
1192
|
+
else list.push(node);
|
|
1193
|
+
}
|
|
1194
|
+
const rows = [...byRank.keys()].sort((a, b) => a - b);
|
|
1195
|
+
const packed = /* @__PURE__ */ new Map();
|
|
1196
|
+
let widest = 0;
|
|
1197
|
+
for (const rank of rows) {
|
|
1198
|
+
const row = seededOrder(byRank.get(rank), axes.cross, order);
|
|
1199
|
+
const p = packAcross(row, axes.cross, nodeGap);
|
|
1200
|
+
packed.set(rank, p);
|
|
1201
|
+
widest = Math.max(widest, p.span);
|
|
1202
|
+
}
|
|
1203
|
+
const slots = /* @__PURE__ */ new Map();
|
|
1204
|
+
let along = 0;
|
|
1205
|
+
for (const rank of rows) {
|
|
1206
|
+
const row = byRank.get(rank);
|
|
1207
|
+
const p = packed.get(rank);
|
|
1208
|
+
const indent = (widest - p.span) / 2;
|
|
1209
|
+
const depth = Math.max(0, ...row.map((n) => extent(n.bounds, axes.rank)));
|
|
1210
|
+
for (const node of row) {
|
|
1211
|
+
slots.set(node.id, {
|
|
1212
|
+
cross: indent + p.at.get(node.id),
|
|
1213
|
+
// Centered in its rank's band, so a tall node does not push its
|
|
1214
|
+
// neighbors' edges off the line they read along.
|
|
1215
|
+
rank: along + (depth - extent(node.bounds, axes.rank)) / 2
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
along += depth + rankGap;
|
|
1219
|
+
}
|
|
1220
|
+
return settle(graph, slots, opts);
|
|
1221
|
+
};
|
|
1222
|
+
|
|
1223
|
+
// src/tree.ts
|
|
1224
|
+
function forestOf(graph, crossAxis) {
|
|
1225
|
+
const order = graphOrder(graph);
|
|
1226
|
+
const children = /* @__PURE__ */ new Map();
|
|
1227
|
+
const depth = /* @__PURE__ */ new Map();
|
|
1228
|
+
const roots = [];
|
|
1229
|
+
const claim = (node, at) => {
|
|
1230
|
+
depth.set(node.id, at);
|
|
1231
|
+
const kids = [];
|
|
1232
|
+
for (const edge of graph.outgoing(node.id)) {
|
|
1233
|
+
if (edge.to === node.id || depth.has(edge.to)) continue;
|
|
1234
|
+
const child = graph.node(edge.to);
|
|
1235
|
+
if (child === void 0) continue;
|
|
1236
|
+
depth.set(child.id, at + 1);
|
|
1237
|
+
kids.push(child);
|
|
1238
|
+
}
|
|
1239
|
+
const ordered = seededOrder(kids, crossAxis, order);
|
|
1240
|
+
children.set(node.id, ordered);
|
|
1241
|
+
for (const child of ordered) claim(child, at + 1);
|
|
1242
|
+
};
|
|
1243
|
+
const rootish = graph.nodes.filter((node) => graph.incoming(node.id).every((edge) => edge.from === node.id));
|
|
1244
|
+
for (const node of seededOrder(rootish, crossAxis, order)) {
|
|
1245
|
+
if (depth.has(node.id)) continue;
|
|
1246
|
+
roots.push(node);
|
|
1247
|
+
claim(node, 0);
|
|
1248
|
+
}
|
|
1249
|
+
for (const node of graph.nodes) {
|
|
1250
|
+
if (depth.has(node.id)) continue;
|
|
1251
|
+
roots.push(node);
|
|
1252
|
+
claim(node, 0);
|
|
1253
|
+
}
|
|
1254
|
+
return { roots, children, depth };
|
|
1255
|
+
}
|
|
1256
|
+
var tree = (graph, opts = {}) => {
|
|
1257
|
+
const axes = axesFor(opts.direction);
|
|
1258
|
+
const nodeGap = opts.nodeGap ?? DEFAULT_NODE_GAP;
|
|
1259
|
+
const rankGap = opts.rankGap ?? DEFAULT_RANK_GAP;
|
|
1260
|
+
const { roots, children, depth } = forestOf(graph, axes.cross);
|
|
1261
|
+
const bands = /* @__PURE__ */ new Map();
|
|
1262
|
+
for (const node of graph.nodes) {
|
|
1263
|
+
const at = depth.get(node.id) ?? 0;
|
|
1264
|
+
bands.set(at, Math.max(bands.get(at) ?? 0, extent(node.bounds, axes.rank)));
|
|
1265
|
+
}
|
|
1266
|
+
const bandStart = /* @__PURE__ */ new Map();
|
|
1267
|
+
let along = 0;
|
|
1268
|
+
for (const at of [...bands.keys()].sort((a, b) => a - b)) {
|
|
1269
|
+
bandStart.set(at, along);
|
|
1270
|
+
along += bands.get(at) + rankGap;
|
|
1271
|
+
}
|
|
1272
|
+
const slots = /* @__PURE__ */ new Map();
|
|
1273
|
+
const measured = /* @__PURE__ */ new Map();
|
|
1274
|
+
const measure = (node) => {
|
|
1275
|
+
const known = measured.get(node.id);
|
|
1276
|
+
if (known !== void 0) return known;
|
|
1277
|
+
const kids = children.get(node.id) ?? [];
|
|
1278
|
+
let width = 0;
|
|
1279
|
+
for (const child of kids) width += measure(child);
|
|
1280
|
+
if (kids.length > 0) width += nodeGap * (kids.length - 1);
|
|
1281
|
+
const block = Math.max(extent(node.bounds, axes.cross), width);
|
|
1282
|
+
measured.set(node.id, block);
|
|
1283
|
+
return block;
|
|
1284
|
+
};
|
|
1285
|
+
const place = (node, from) => {
|
|
1286
|
+
const kids = children.get(node.id) ?? [];
|
|
1287
|
+
const block = measure(node);
|
|
1288
|
+
const width = kids.reduce((sum, k) => sum + measure(k), 0) + Math.max(0, kids.length - 1) * nodeGap;
|
|
1289
|
+
let cursor2 = from + (block - width) / 2;
|
|
1290
|
+
for (const child of kids) {
|
|
1291
|
+
place(child, cursor2);
|
|
1292
|
+
cursor2 += measure(child) + nodeGap;
|
|
1293
|
+
}
|
|
1294
|
+
const own = extent(node.bounds, axes.cross);
|
|
1295
|
+
const at = depth.get(node.id) ?? 0;
|
|
1296
|
+
slots.set(node.id, {
|
|
1297
|
+
cross: from + (block - own) / 2,
|
|
1298
|
+
rank: bandStart.get(at) + (bands.get(at) - extent(node.bounds, axes.rank)) / 2
|
|
1299
|
+
});
|
|
1300
|
+
};
|
|
1301
|
+
let cursor = 0;
|
|
1302
|
+
for (const root of roots) {
|
|
1303
|
+
place(root, cursor);
|
|
1304
|
+
cursor += measure(root) + nodeGap;
|
|
1305
|
+
}
|
|
1306
|
+
return settle(graph, slots, opts);
|
|
1307
|
+
};
|
|
1308
|
+
|
|
1309
|
+
// src/layoutAction.ts
|
|
1310
|
+
var LAYOUTS = Object.freeze({
|
|
1311
|
+
layered,
|
|
1312
|
+
tree,
|
|
1313
|
+
force
|
|
1314
|
+
});
|
|
1315
|
+
function applyLayout(scene, result, opts = {}) {
|
|
1316
|
+
const poses = layoutPoses(scene, result, opts.geometry);
|
|
1317
|
+
if (poses.size === 0) return 0;
|
|
1318
|
+
scene.batch(opts.label ?? "Layout", () => {
|
|
1319
|
+
for (const [id, pose] of poses) scene.setPose(id, pose);
|
|
1320
|
+
});
|
|
1321
|
+
let moved = 0;
|
|
1322
|
+
for (const id of result.keys()) if (poses.has(id)) moved++;
|
|
1323
|
+
return moved;
|
|
1324
|
+
}
|
|
1325
|
+
function layoutPoses(scene, result, geometry = AUTO_POSE_DESCRIPTOR) {
|
|
1326
|
+
const out = /* @__PURE__ */ new Map();
|
|
1327
|
+
for (const [id, at] of result) {
|
|
1328
|
+
const node = scene.get(id);
|
|
1329
|
+
if (node === void 0) continue;
|
|
1330
|
+
const bounds = geometry.getBounds(node.pose);
|
|
1331
|
+
const dx = at.x - bounds.x;
|
|
1332
|
+
const dy = at.y - bounds.y;
|
|
1333
|
+
if (dx === 0 && dy === 0) continue;
|
|
1334
|
+
out.set(id, translatePoseViaDescriptor(node.pose, dx, dy, geometry));
|
|
1335
|
+
for (const descendant of subtreeOf(scene, id)) {
|
|
1336
|
+
const child = scene.get(descendant);
|
|
1337
|
+
if (child === void 0) continue;
|
|
1338
|
+
out.set(descendant, translatePoseViaDescriptor(child.pose, dx, dy, geometry));
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
return out;
|
|
1342
|
+
}
|
|
1343
|
+
function subtreeOf(scene, id) {
|
|
1344
|
+
const out = [];
|
|
1345
|
+
const stack = [...scene.childrenOf(id)];
|
|
1346
|
+
while (stack.length > 0) {
|
|
1347
|
+
const next = stack.pop();
|
|
1348
|
+
out.push(next);
|
|
1349
|
+
stack.push(...scene.childrenOf(next));
|
|
1350
|
+
}
|
|
1351
|
+
return out;
|
|
1352
|
+
}
|
|
1353
|
+
var LAYOUT_ACTION_ID = "diagram.layout";
|
|
1354
|
+
function createLayoutAction(opts) {
|
|
1355
|
+
const algorithm = typeof opts.algorithm === "function" ? opts.algorithm : LAYOUTS[opts.algorithm ?? "layered"] ?? layered;
|
|
1356
|
+
const build = {
|
|
1357
|
+
...opts.read ? { read: opts.read } : {},
|
|
1358
|
+
...opts.geometry ? { geometry: opts.geometry } : {}
|
|
1359
|
+
};
|
|
1360
|
+
return {
|
|
1361
|
+
id: opts.id ?? LAYOUT_ACTION_ID,
|
|
1362
|
+
label: opts.label ?? "Lay out diagram",
|
|
1363
|
+
group: opts.group ?? "diagram",
|
|
1364
|
+
requires: ["scene"],
|
|
1365
|
+
invoker: {
|
|
1366
|
+
timing: "immediate",
|
|
1367
|
+
run: (deps) => {
|
|
1368
|
+
const scene = deps["scene"];
|
|
1369
|
+
if (scene === void 0) return;
|
|
1370
|
+
const graph = buildGraph(opts.source, build);
|
|
1371
|
+
applyLayout(scene, algorithm(graph, opts.layout), {
|
|
1372
|
+
...opts.geometry ? { geometry: opts.geometry } : {},
|
|
1373
|
+
...opts.label ? { label: opts.label } : {}
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
},
|
|
1377
|
+
enabled: () => true
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
// src/live.ts
|
|
1382
|
+
var EASE_FRAMES = 24;
|
|
1383
|
+
var easeInOut = (t) => t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2;
|
|
1384
|
+
function forceProducer(graph, opts = {}) {
|
|
1385
|
+
const geometry = opts.geometry ?? AUTO_POSE_DESCRIPTOR;
|
|
1386
|
+
const relaxation = forceRelaxation(graph, opts);
|
|
1387
|
+
const dragAlpha = opts.dragAlpha ?? 0.3;
|
|
1388
|
+
return ({ pinned }) => {
|
|
1389
|
+
for (const node of graph.nodes) {
|
|
1390
|
+
const body = relaxation.byId.get(node.id);
|
|
1391
|
+
if (body === void 0) continue;
|
|
1392
|
+
const held = pinned.get(node.id);
|
|
1393
|
+
if (held !== void 0) {
|
|
1394
|
+
const bounds = geometry.getBounds(held);
|
|
1395
|
+
body.fx = center(bounds, "x");
|
|
1396
|
+
body.fy = center(bounds, "y");
|
|
1397
|
+
} else if (!relaxation.pinned.has(node.id)) {
|
|
1398
|
+
delete body.fx;
|
|
1399
|
+
delete body.fy;
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
relaxation.sim.alphaTarget(pinned.size > 0 ? dragAlpha : 0);
|
|
1403
|
+
relaxation.sim.tick();
|
|
1404
|
+
return { result: relaxation.placed(), done: relaxation.sim.isSettled() };
|
|
1405
|
+
};
|
|
1406
|
+
}
|
|
1407
|
+
function easedProducer(graph, algorithm, opts = {}) {
|
|
1408
|
+
const frames = opts.frames ?? EASE_FRAMES;
|
|
1409
|
+
const easing = opts.easing ?? easeInOut;
|
|
1410
|
+
const target = algorithm(graph, opts.layout);
|
|
1411
|
+
const from = /* @__PURE__ */ new Map();
|
|
1412
|
+
for (const node of graph.nodes) from.set(node.id, { x: node.bounds.x, y: node.bounds.y });
|
|
1413
|
+
return ({ pinned, frame }) => {
|
|
1414
|
+
const t = Math.min((frame + 1) / frames, 1);
|
|
1415
|
+
const u = easing(t);
|
|
1416
|
+
const result = /* @__PURE__ */ new Map();
|
|
1417
|
+
for (const [id, to] of target) {
|
|
1418
|
+
if (pinned.has(id)) continue;
|
|
1419
|
+
const start = from.get(id);
|
|
1420
|
+
if (start === void 0) continue;
|
|
1421
|
+
result.set(id, {
|
|
1422
|
+
x: start.x + (to.x - start.x) * u,
|
|
1423
|
+
y: start.y + (to.y - start.y) * u
|
|
1424
|
+
});
|
|
1425
|
+
}
|
|
1426
|
+
return { result, done: t >= 1 };
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1429
|
+
function shapeOf(graph) {
|
|
1430
|
+
const nodes = graph.nodes.map((n) => n.id).sort().join(",");
|
|
1431
|
+
const edges = graph.edges.map((e) => `${e.from}>${e.to}`).sort().join(",");
|
|
1432
|
+
return `${nodes}|${edges}`;
|
|
1433
|
+
}
|
|
1434
|
+
function useLiveLayout(opts) {
|
|
1435
|
+
const optsRef = useRef(opts);
|
|
1436
|
+
optsRef.current = opts;
|
|
1437
|
+
const producer = useRef(null);
|
|
1438
|
+
const producerFrame = useRef(0);
|
|
1439
|
+
const shape = useRef("");
|
|
1440
|
+
const build = useCallback(() => {
|
|
1441
|
+
const o = optsRef.current;
|
|
1442
|
+
const graph = buildGraph(o.source, {
|
|
1443
|
+
...o.read ? { read: o.read } : {},
|
|
1444
|
+
...o.geometry ? { geometry: o.geometry } : {}
|
|
1445
|
+
});
|
|
1446
|
+
shape.current = shapeOf(graph);
|
|
1447
|
+
producerFrame.current = 0;
|
|
1448
|
+
const algorithm = o.algorithm ?? "force";
|
|
1449
|
+
if (algorithm === "force") {
|
|
1450
|
+
producer.current = forceProducer(graph, {
|
|
1451
|
+
...o.layout,
|
|
1452
|
+
...o.force,
|
|
1453
|
+
...o.geometry ? { geometry: o.geometry } : {}
|
|
1454
|
+
});
|
|
1455
|
+
return;
|
|
1456
|
+
}
|
|
1457
|
+
const fn = typeof algorithm === "function" ? algorithm : LAYOUTS[algorithm] ?? layered;
|
|
1458
|
+
producer.current = easedProducer(graph, fn, {
|
|
1459
|
+
...o.layout ? { layout: o.layout } : {},
|
|
1460
|
+
...o.frames === void 0 ? {} : { frames: o.frames },
|
|
1461
|
+
...o.easing ? { easing: o.easing } : {}
|
|
1462
|
+
});
|
|
1463
|
+
}, []);
|
|
1464
|
+
const step = useCallback((ctx) => {
|
|
1465
|
+
const o = optsRef.current;
|
|
1466
|
+
if (producer.current === null) return { poses: [], done: true };
|
|
1467
|
+
const { result, done } = producer.current({
|
|
1468
|
+
pinned: ctx.pinned,
|
|
1469
|
+
frame: producerFrame.current++
|
|
1470
|
+
});
|
|
1471
|
+
let placed = result;
|
|
1472
|
+
if (ctx.pinned.size > 0) {
|
|
1473
|
+
const held = new Map(result);
|
|
1474
|
+
for (const id of ctx.pinned.keys()) held.delete(id);
|
|
1475
|
+
placed = held;
|
|
1476
|
+
}
|
|
1477
|
+
const poses = layoutPoses(o.scene, placed, o.geometry);
|
|
1478
|
+
return { poses: [...poses], done };
|
|
1479
|
+
}, []);
|
|
1480
|
+
const run = usePoseRun({
|
|
1481
|
+
scene: opts.scene,
|
|
1482
|
+
step,
|
|
1483
|
+
onCommit: (moved) => optsRef.current.onSettle?.(moved),
|
|
1484
|
+
...opts.label ? { label: opts.label } : {},
|
|
1485
|
+
...opts.requestFrame ? { requestFrame: opts.requestFrame } : {},
|
|
1486
|
+
...opts.cancelFrame ? { cancelFrame: opts.cancelFrame } : {}
|
|
1487
|
+
});
|
|
1488
|
+
useEffect(() => {
|
|
1489
|
+
if (opts.reheatOnGraphChange === false) return;
|
|
1490
|
+
return opts.scene.subscribe(() => {
|
|
1491
|
+
if (!run.isRunning()) return;
|
|
1492
|
+
const o = optsRef.current;
|
|
1493
|
+
const graph = buildGraph(o.source, {
|
|
1494
|
+
...o.read ? { read: o.read } : {},
|
|
1495
|
+
...o.geometry ? { geometry: o.geometry } : {}
|
|
1496
|
+
});
|
|
1497
|
+
if (shapeOf(graph) === shape.current) return;
|
|
1498
|
+
build();
|
|
1499
|
+
});
|
|
1500
|
+
}, [opts.scene, opts.reheatOnGraphChange, run, build]);
|
|
1501
|
+
return useMemo(() => ({
|
|
1502
|
+
start() {
|
|
1503
|
+
if (run.isRunning()) return;
|
|
1504
|
+
build();
|
|
1505
|
+
run.start();
|
|
1506
|
+
},
|
|
1507
|
+
stop: () => {
|
|
1508
|
+
run.stop();
|
|
1509
|
+
},
|
|
1510
|
+
cancel: () => {
|
|
1511
|
+
run.cancel();
|
|
1512
|
+
},
|
|
1513
|
+
restart() {
|
|
1514
|
+
run.cancel();
|
|
1515
|
+
build();
|
|
1516
|
+
run.start();
|
|
1517
|
+
},
|
|
1518
|
+
isRunning: () => run.isRunning()
|
|
1519
|
+
}), [run, build]);
|
|
1520
|
+
}
|
|
1521
|
+
var EASED_LAYOUTS = Object.freeze({
|
|
1522
|
+
layered,
|
|
1523
|
+
tree
|
|
1524
|
+
});
|
|
1525
|
+
function diagramShape(opts = {}) {
|
|
1526
|
+
const geometry = opts.geometry ?? RECT_POSE_DESCRIPTOR;
|
|
1527
|
+
const outlineOf = (node) => diagramNodeOf(node, opts.read)?.outline;
|
|
1528
|
+
return {
|
|
1529
|
+
id: opts.id ?? "diagram:outline",
|
|
1530
|
+
matches: (node) => outlineOf(node) !== void 0,
|
|
1531
|
+
paint: (node, pose) => {
|
|
1532
|
+
const outline = outlineOf(node);
|
|
1533
|
+
if (outline === void 0) return [];
|
|
1534
|
+
const d = node.data;
|
|
1535
|
+
const fill = d.fill === void 0 ? opts.defaultFill : d.fill;
|
|
1536
|
+
const stroke = d.stroke === void 0 ? opts.defaultStroke : d.stroke;
|
|
1537
|
+
return [{
|
|
1538
|
+
kind: "path",
|
|
1539
|
+
path: outlinePath(outline, geometry.getBounds(pose)),
|
|
1540
|
+
...fill ? { fill } : {},
|
|
1541
|
+
...stroke ? { stroke } : {}
|
|
1542
|
+
}];
|
|
1543
|
+
},
|
|
1544
|
+
// The drawn boundary, so picking and clipping follow the diamond rather
|
|
1545
|
+
// than its bounding box.
|
|
1546
|
+
silhouette: (node, pose) => {
|
|
1547
|
+
const outline = outlineOf(node);
|
|
1548
|
+
return outline === void 0 ? null : outlinePath(outline, geometry.getBounds(pose));
|
|
1549
|
+
}
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1552
|
+
function registerDiagramShape(opts = {}) {
|
|
1553
|
+
return registerNodeShape(diagramShape(opts), { priority: "high" });
|
|
1554
|
+
}
|
|
233
1555
|
|
|
234
|
-
export { COMPASS, DEFAULT_PORTS, DIAGRAM_TRAIT_KEY, bodyOutline, bodyTrait, canvasMeasure, createDiagramNodes, dataKeyReader, diagramNodeOf, isPinned, layoutBody, measureBody, outlinePath, portOf, portsOf, sizeToBody };
|
|
1556
|
+
export { COMPASS, CONNECT_ACTION_ID, DEFAULT_EDGE_STROKE, DEFAULT_NODE_GAP, DEFAULT_PORTS, DEFAULT_RANK_GAP, DIAGRAM_EDGE, DIAGRAM_LABEL, DIAGRAM_TRAIT_KEY, EASED_LAYOUTS, EDGE_DERIVE_PATH, GRAB_PORT_ACTION_ID, LABEL_DERIVE_POSE, LAYOUTS, LAYOUT_ACTION_ID, PORT_AFFORDANCE_KIND, PORT_LAYER_ID, ROUTERS, applyLayout, axesFor, backEdges, bezier, bodyOutline, bodyTrait, boxForContent, buildBody, buildGraph, canvasMeasure, commitEdgeToScene, connectBinding, contentBox, createConnectAction, createDiagramContribution, createDiagramNodes, createLayoutAction, createPortAffordance, dataKeyReader, defaultCanConnect, diagramEdgeOf, diagramLabelOf, diagramNodeOf, diagramPorts, diagramShape, easedProducer, edgeDerivePath, force, forceProducer, forceRelaxation, forestOf, grabPortAction, grabPortBindings, graphOrder, isPinned, labelDerivePose, layered, layoutBody, layoutPoses, layoutRowPorts, measureBody, orthogonal, outlinePath, outlinePolyline, packAcross, pinnedSet, portLayer, portOf, portScratchOf, portsOf, portsOnOutline, ranksOf, rayHit, registerDiagramShape, resolveEnd, sceneParticipants, seededOrder, settle, sizeToBody, straight, translated, tree, useLiveLayout, withDiagramRegistry };
|
|
235
1557
|
//# sourceMappingURL=index.js.map
|
|
236
1558
|
//# sourceMappingURL=index.js.map
|