dsh-model-everything 0.5.0 → 0.5.1
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.en-US.md +12 -0
- package/README.md +4 -0
- package/lib/client.js +2437 -216
- package/lib/render/channels.js +243 -0
- package/lib/render/collapse.js +12 -20
- package/lib/render/components.js +99 -0
- package/lib/render/coords.js +288 -0
- package/lib/render/cycle-break.js +147 -0
- package/lib/render/dummy.js +43 -0
- package/lib/render/from-mermaid.js +34 -10
- package/lib/render/from-plantuml.js +318 -46
- package/lib/render/layer.js +91 -0
- package/lib/render/layout.js +27 -11
- package/lib/render/network-simplex.js +272 -0
- package/lib/render/order.js +122 -0
- package/lib/render/paint.js +764 -67
- package/lib/render/place.js +393 -121
- package/lib/render/route.js +225 -0
- package/lib/render/svg.js +143 -21
- package/lib/render/symbol.js +108 -0
- package/lib/render/text.js +74 -0
- package/lib/render/wrap.js +87 -0
- package/lib/types/render/channels.d.ts +63 -0
- package/lib/types/render/components.d.ts +57 -0
- package/lib/types/render/coords.d.ts +64 -0
- package/lib/types/render/cycle-break.d.ts +45 -0
- package/lib/types/render/dummy.d.ts +32 -0
- package/lib/types/render/from-plantuml.d.ts +12 -6
- package/lib/types/render/layer.d.ts +28 -0
- package/lib/types/render/layout.d.ts +43 -11
- package/lib/types/render/model.d.ts +17 -0
- package/lib/types/render/network-simplex.d.ts +54 -0
- package/lib/types/render/order.d.ts +27 -0
- package/lib/types/render/paint.d.ts +42 -9
- package/lib/types/render/place.d.ts +35 -19
- package/lib/types/render/route.d.ts +77 -0
- package/lib/types/render/svg.d.ts +1 -1
- package/lib/types/render/symbol.d.ts +73 -0
- package/lib/types/render/text.d.ts +38 -0
- package/lib/types/render/wrap.d.ts +27 -0
- package/package.json +148 -148
- package/skills/model-everything-diagram/SKILL.md +84 -82
- package/src/render/channels.ts +309 -0
- package/src/render/collapse.ts +150 -155
- package/src/render/components.ts +126 -0
- package/src/render/coords.ts +328 -0
- package/src/render/cycle-break.ts +164 -0
- package/src/render/dummy.ts +65 -0
- package/src/render/from-mermaid.ts +261 -236
- package/src/render/from-plantuml.ts +469 -178
- package/src/render/layer.ts +98 -0
- package/src/render/layout.ts +101 -62
- package/src/render/model.ts +86 -67
- package/src/render/network-simplex.ts +290 -0
- package/src/render/order.ts +139 -0
- package/src/render/paint.ts +1006 -290
- package/src/render/place.ts +527 -234
- package/src/render/route.ts +269 -0
- package/src/render/svg.ts +270 -127
- package/src/render/symbol.ts +137 -0
- package/src/render/text.ts +86 -0
- package/src/render/wrap.ts +103 -0
package/lib/client.js
CHANGED
|
@@ -37,17 +37,844 @@ window.__ModuleLoader__.load({
|
|
|
37
37
|
});
|
|
38
38
|
module.exports = __toCommonJS(client_exports);
|
|
39
39
|
|
|
40
|
+
// src/render/components.ts
|
|
41
|
+
var PAGE_ASPECT = 1.6;
|
|
42
|
+
function weakComponents(nodes, edges) {
|
|
43
|
+
const known = new Set(nodes.map((node) => node.id));
|
|
44
|
+
const neighbours = /* @__PURE__ */ new Map();
|
|
45
|
+
const link = (from, to) => {
|
|
46
|
+
neighbours.set(from, [...neighbours.get(from) ?? [], to]);
|
|
47
|
+
};
|
|
48
|
+
for (const edge of edges) {
|
|
49
|
+
if (!known.has(edge.from) || !known.has(edge.to) || edge.from === edge.to) continue;
|
|
50
|
+
link(edge.from, edge.to);
|
|
51
|
+
link(edge.to, edge.from);
|
|
52
|
+
}
|
|
53
|
+
const seen = /* @__PURE__ */ new Set();
|
|
54
|
+
const components = [];
|
|
55
|
+
for (const node of nodes) {
|
|
56
|
+
if (seen.has(node.id)) continue;
|
|
57
|
+
const members = [];
|
|
58
|
+
const queue = [node.id];
|
|
59
|
+
seen.add(node.id);
|
|
60
|
+
for (let head = 0; head < queue.length; head += 1) {
|
|
61
|
+
const id = queue[head];
|
|
62
|
+
members.push(id);
|
|
63
|
+
for (const next of neighbours.get(id) ?? []) {
|
|
64
|
+
if (seen.has(next)) continue;
|
|
65
|
+
seen.add(next);
|
|
66
|
+
queue.push(next);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
components.push(members);
|
|
70
|
+
}
|
|
71
|
+
return components;
|
|
72
|
+
}
|
|
73
|
+
function packRectangles(rectangles, options = {}) {
|
|
74
|
+
const gap = options.gap ?? 30;
|
|
75
|
+
const aspect = options.aspect ?? PAGE_ASPECT;
|
|
76
|
+
if (rectangles.length === 0) return { placements: [], width: 0, height: 0 };
|
|
77
|
+
const area = rectangles.reduce((total, rectangle) => total + (rectangle.width + gap) * (rectangle.height + gap), 0);
|
|
78
|
+
const targetWidth = Math.max(gap, Math.sqrt(area * aspect));
|
|
79
|
+
const order2 = rectangles.map((rectangle, index) => ({ rectangle, index })).sort((left, right) => right.rectangle.height - left.rectangle.height || right.rectangle.width - left.rectangle.width || left.index - right.index);
|
|
80
|
+
const placements = rectangles.map((rectangle) => ({ x: 0, y: 0, width: rectangle.width, height: rectangle.height }));
|
|
81
|
+
let x = 0;
|
|
82
|
+
let y = 0;
|
|
83
|
+
let shelfHeight = 0;
|
|
84
|
+
let width = 0;
|
|
85
|
+
for (const entry of order2) {
|
|
86
|
+
const rectangle = entry.rectangle;
|
|
87
|
+
if (x > 0 && x + rectangle.width > targetWidth) {
|
|
88
|
+
y += shelfHeight + gap;
|
|
89
|
+
x = 0;
|
|
90
|
+
shelfHeight = 0;
|
|
91
|
+
}
|
|
92
|
+
placements[entry.index] = { x, y, width: rectangle.width, height: rectangle.height };
|
|
93
|
+
x += rectangle.width + gap;
|
|
94
|
+
width = Math.max(width, x - gap);
|
|
95
|
+
shelfHeight = Math.max(shelfHeight, rectangle.height);
|
|
96
|
+
}
|
|
97
|
+
return { placements, width, height: y + shelfHeight };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/render/channels.ts
|
|
101
|
+
var PORT_INSET = 10;
|
|
102
|
+
function spanOf(box) {
|
|
103
|
+
const lo = box.x + PORT_INSET;
|
|
104
|
+
const hi = box.x + box.width - PORT_INSET;
|
|
105
|
+
return hi > lo ? { lo, hi } : { lo: box.x + box.width / 2, hi: box.x + box.width / 2 };
|
|
106
|
+
}
|
|
107
|
+
function clamp(value, span) {
|
|
108
|
+
return Math.min(span.hi, Math.max(span.lo, value));
|
|
109
|
+
}
|
|
110
|
+
function inside(value, span) {
|
|
111
|
+
return value >= span.lo - 1e-3 && value <= span.hi + 1e-3;
|
|
112
|
+
}
|
|
113
|
+
function cut(free, blocked) {
|
|
114
|
+
const out = [];
|
|
115
|
+
for (const span of free) {
|
|
116
|
+
if (blocked.hi <= span.lo || blocked.lo >= span.hi) {
|
|
117
|
+
out.push(span);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (blocked.lo > span.lo) out.push({ lo: span.lo, hi: blocked.lo });
|
|
121
|
+
if (blocked.hi < span.hi) out.push({ lo: blocked.hi, hi: span.hi });
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
function atOrAfter(value, free) {
|
|
126
|
+
let found = value;
|
|
127
|
+
for (const span of free) {
|
|
128
|
+
if (span.hi < value) continue;
|
|
129
|
+
found = Math.max(value, span.lo);
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
return found;
|
|
133
|
+
}
|
|
134
|
+
function choose(entry) {
|
|
135
|
+
const candidates = [entry.proposal, entry.source.lo, entry.source.hi, entry.target.lo, entry.target.hi];
|
|
136
|
+
candidates.push((clamp(entry.proposal, entry.source) + clamp(entry.proposal, entry.target)) / 2);
|
|
137
|
+
let best = entry.proposal;
|
|
138
|
+
let bestBends = Infinity;
|
|
139
|
+
let bestFar = Infinity;
|
|
140
|
+
let bestReach = Infinity;
|
|
141
|
+
for (const span of entry.free) {
|
|
142
|
+
for (const value of candidates) {
|
|
143
|
+
const x = clamp(value, span);
|
|
144
|
+
const bends = (inside(x, entry.source) ? 0 : 1) + (inside(x, entry.target) ? 0 : 1);
|
|
145
|
+
const far = Math.abs(x - entry.proposal);
|
|
146
|
+
const reach = Math.abs(x - clamp(x, entry.source)) + Math.abs(x - clamp(x, entry.target));
|
|
147
|
+
const better = bends < bestBends || bends === bestBends && (far < bestFar - 1e-3 || Math.abs(far - bestFar) <= 1e-3 && reach < bestReach);
|
|
148
|
+
if (!better) continue;
|
|
149
|
+
best = x;
|
|
150
|
+
bestBends = bends;
|
|
151
|
+
bestFar = far;
|
|
152
|
+
bestReach = reach;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return best;
|
|
156
|
+
}
|
|
157
|
+
function adjacentPorts(source, target) {
|
|
158
|
+
const fromMiddle = source.x + source.width / 2;
|
|
159
|
+
const toMiddle = target.x + target.width / 2;
|
|
160
|
+
const fromSpan = spanOf(source);
|
|
161
|
+
const toSpan = spanOf(target);
|
|
162
|
+
const chosen = inside(fromMiddle, toSpan) ? fromMiddle : clamp(toMiddle, fromSpan);
|
|
163
|
+
return { from: clamp(chosen, fromSpan), to: clamp(chosen, toSpan) };
|
|
164
|
+
}
|
|
165
|
+
function assignChannels(input, options) {
|
|
166
|
+
const { layers, chains, chainOf, boxes, layerY, layerHeight, laneX } = input;
|
|
167
|
+
const rowOf = /* @__PURE__ */ new Map();
|
|
168
|
+
layers.forEach((row, depth) => {
|
|
169
|
+
for (const id of row) rowOf.set(id, depth);
|
|
170
|
+
});
|
|
171
|
+
const long = [];
|
|
172
|
+
for (const chain of chains) {
|
|
173
|
+
if (chain.path.length <= 2) continue;
|
|
174
|
+
const head = chain.path[0];
|
|
175
|
+
const tail = chain.path[chain.path.length - 1];
|
|
176
|
+
const from = rowOf.get(head);
|
|
177
|
+
const to = rowOf.get(tail);
|
|
178
|
+
if (from === void 0 || to === void 0 || to <= from) continue;
|
|
179
|
+
const lane = chainOf.get(chain.path[1]);
|
|
180
|
+
const source = boxes.get(head);
|
|
181
|
+
const target = boxes.get(tail);
|
|
182
|
+
if (lane === void 0 || source === void 0 || target === void 0) continue;
|
|
183
|
+
let free = [{ lo: -Infinity, hi: Infinity }];
|
|
184
|
+
for (let depth = from + 1; depth < to; depth += 1) {
|
|
185
|
+
for (const id of layers[depth]) {
|
|
186
|
+
const box = boxes.get(id);
|
|
187
|
+
if (box === void 0) continue;
|
|
188
|
+
free = cut(free, { lo: box.x - options.clearance, hi: box.x + box.width + options.clearance });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const centre = (source.x + source.width / 2 + target.x + target.width / 2) / 2;
|
|
192
|
+
long.push({
|
|
193
|
+
edge: chain.index,
|
|
194
|
+
lane,
|
|
195
|
+
path: chain.path,
|
|
196
|
+
from,
|
|
197
|
+
to,
|
|
198
|
+
free,
|
|
199
|
+
source: spanOf(source),
|
|
200
|
+
target: spanOf(target),
|
|
201
|
+
proposal: laneX.get(lane) ?? centre,
|
|
202
|
+
x: 0
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
for (const entry of long) entry.x = choose(entry);
|
|
206
|
+
const byGap = /* @__PURE__ */ new Map();
|
|
207
|
+
for (const entry of long) {
|
|
208
|
+
for (let gap = entry.from; gap < entry.to; gap += 1) {
|
|
209
|
+
const row = gap + 1 < entry.to ? gap + 1 : gap;
|
|
210
|
+
const id = entry.path[row - entry.from];
|
|
211
|
+
const index = layers[row].indexOf(id);
|
|
212
|
+
byGap.set(gap, [...byGap.get(gap) ?? [], { entry, index }]);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
for (const [, list] of byGap) list.sort((left, right) => left.index - right.index);
|
|
216
|
+
for (let round = 0; round < 4; round += 1) {
|
|
217
|
+
let moved = false;
|
|
218
|
+
for (const [, list] of byGap) {
|
|
219
|
+
let previous = -Infinity;
|
|
220
|
+
for (const item of list) {
|
|
221
|
+
const x = item.entry.x < previous + options.spacing ? atOrAfter(previous + options.spacing, item.entry.free) : item.entry.x;
|
|
222
|
+
if (x !== item.entry.x) {
|
|
223
|
+
item.entry.x = x;
|
|
224
|
+
moved = true;
|
|
225
|
+
}
|
|
226
|
+
previous = item.entry.x;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (!moved) break;
|
|
230
|
+
}
|
|
231
|
+
const stacks = /* @__PURE__ */ new Map();
|
|
232
|
+
const want = (gap, edge, left) => {
|
|
233
|
+
stacks.set(gap, [...stacks.get(gap) ?? [], { edge, gap, left }]);
|
|
234
|
+
};
|
|
235
|
+
for (const entry of long) {
|
|
236
|
+
const head = clamp(entry.x, entry.source);
|
|
237
|
+
const tail = clamp(entry.x, entry.target);
|
|
238
|
+
if (Math.abs(head - entry.x) > 0.5) want(entry.from, entry.edge, Math.min(head, entry.x));
|
|
239
|
+
if (Math.abs(tail - entry.x) > 0.5) want(entry.to - 1, entry.edge, Math.min(tail, entry.x));
|
|
240
|
+
}
|
|
241
|
+
for (const chain of chains) {
|
|
242
|
+
if (chain.path.length > 2) continue;
|
|
243
|
+
const head = chain.path[0];
|
|
244
|
+
const tail = chain.path[chain.path.length - 1];
|
|
245
|
+
const from = rowOf.get(head);
|
|
246
|
+
const to = rowOf.get(tail);
|
|
247
|
+
if (from === void 0 || to === void 0 || Math.abs(from - to) !== 1) continue;
|
|
248
|
+
const source = boxes.get(head);
|
|
249
|
+
const target = boxes.get(tail);
|
|
250
|
+
if (source === void 0 || target === void 0) continue;
|
|
251
|
+
const ports = adjacentPorts(source, target);
|
|
252
|
+
want(Math.min(from, to), chain.index, Math.min(ports.from, ports.to));
|
|
253
|
+
}
|
|
254
|
+
const midOf = (gap) => (layerY[gap] + layerHeight[gap] + layerY[gap + 1]) / 2;
|
|
255
|
+
const heightOf = /* @__PURE__ */ new Map();
|
|
256
|
+
for (const [gap, list] of stacks) {
|
|
257
|
+
const top = layerY[gap] + layerHeight[gap];
|
|
258
|
+
const bottom = layerY[gap + 1];
|
|
259
|
+
const pad = Math.min(8, Math.max(1, (bottom - top) / 3));
|
|
260
|
+
const lo = top + pad;
|
|
261
|
+
const hi = Math.max(lo, bottom - pad);
|
|
262
|
+
const sorted = [...list].sort((left, right) => left.left - right.left || left.edge - right.edge);
|
|
263
|
+
sorted.forEach((item, index) => {
|
|
264
|
+
const y = sorted.length === 1 ? (lo + hi) / 2 : lo + (hi - lo) * (index + 1) / (sorted.length + 1);
|
|
265
|
+
const heights = heightOf.get(item.edge) ?? /* @__PURE__ */ new Map();
|
|
266
|
+
heights.set(item.gap, y);
|
|
267
|
+
heightOf.set(item.edge, heights);
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
const lanes2 = /* @__PURE__ */ new Map();
|
|
271
|
+
for (const entry of long) {
|
|
272
|
+
const y = [];
|
|
273
|
+
for (let gap = entry.from; gap < entry.to; gap += 1) y.push(heightOf.get(entry.edge)?.get(gap) ?? midOf(gap));
|
|
274
|
+
lanes2.set(entry.edge, { x: entry.x, y });
|
|
275
|
+
}
|
|
276
|
+
const runs = /* @__PURE__ */ new Map();
|
|
277
|
+
for (const [edge, heights] of heightOf) {
|
|
278
|
+
if (lanes2.has(edge)) continue;
|
|
279
|
+
runs.set(edge, [...heights.values()][0]);
|
|
280
|
+
}
|
|
281
|
+
return { lanes: lanes2, runs };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// src/render/coords.ts
|
|
285
|
+
function positions(rows) {
|
|
286
|
+
const at = /* @__PURE__ */ new Map();
|
|
287
|
+
for (const row of rows) row.forEach((id, index) => at.set(id, index));
|
|
288
|
+
return at;
|
|
289
|
+
}
|
|
290
|
+
function alignBlocks(frame, at) {
|
|
291
|
+
const root = /* @__PURE__ */ new Map();
|
|
292
|
+
const joined = /* @__PURE__ */ new Map();
|
|
293
|
+
for (const row of frame.rows) {
|
|
294
|
+
for (const id of row) {
|
|
295
|
+
root.set(id, id);
|
|
296
|
+
joined.set(id, id);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
for (let depth = 1; depth < frame.rows.length; depth += 1) {
|
|
300
|
+
let previous = -1;
|
|
301
|
+
for (const id of frame.rows[depth]) {
|
|
302
|
+
const close = frame.near.get(id) ?? [];
|
|
303
|
+
if (close.length === 0) continue;
|
|
304
|
+
const sorted = [...close].sort((left, right) => at.get(left) - at.get(right));
|
|
305
|
+
const middle = (sorted.length - 1) / 2;
|
|
306
|
+
for (let cursor = Math.floor(middle); cursor <= Math.ceil(middle); cursor += 1) {
|
|
307
|
+
if (joined.get(id) !== id) break;
|
|
308
|
+
const target = sorted[cursor];
|
|
309
|
+
if (previous >= at.get(target)) continue;
|
|
310
|
+
joined.set(target, id);
|
|
311
|
+
root.set(id, root.get(target));
|
|
312
|
+
joined.set(id, root.get(id));
|
|
313
|
+
previous = at.get(target);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return root;
|
|
318
|
+
}
|
|
319
|
+
function compactBlocks(rows, root, widthOf, gap) {
|
|
320
|
+
const clears = /* @__PURE__ */ new Map();
|
|
321
|
+
for (const row of rows) {
|
|
322
|
+
for (let index = 1; index < row.length; index += 1) {
|
|
323
|
+
const left = row[index - 1];
|
|
324
|
+
const right = row[index];
|
|
325
|
+
const leftBlock = root.get(left);
|
|
326
|
+
const rightBlock = root.get(right);
|
|
327
|
+
const space = (widthOf(left) + widthOf(right)) / 2 + gap;
|
|
328
|
+
clears.set(rightBlock, [...clears.get(rightBlock) ?? [], { block: leftBlock, space }]);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
const placed = /* @__PURE__ */ new Map();
|
|
332
|
+
const place2 = (block) => {
|
|
333
|
+
const known = placed.get(block);
|
|
334
|
+
if (known !== void 0) return known;
|
|
335
|
+
let value = 0;
|
|
336
|
+
for (const step of clears.get(block) ?? []) value = Math.max(value, place2(step.block) + step.space);
|
|
337
|
+
placed.set(block, value);
|
|
338
|
+
return value;
|
|
339
|
+
};
|
|
340
|
+
const x = /* @__PURE__ */ new Map();
|
|
341
|
+
for (const row of rows) {
|
|
342
|
+
for (const id of row) {
|
|
343
|
+
const block = root.get(id);
|
|
344
|
+
if (!placed.has(block)) place2(block);
|
|
345
|
+
x.set(id, placed.get(block));
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return x;
|
|
349
|
+
}
|
|
350
|
+
function extentOf(x, rows, widthOf) {
|
|
351
|
+
let left = Infinity;
|
|
352
|
+
let right = -Infinity;
|
|
353
|
+
for (const row of rows) {
|
|
354
|
+
for (const id of row) {
|
|
355
|
+
const value = x.get(id);
|
|
356
|
+
left = Math.min(left, value - widthOf(id) / 2);
|
|
357
|
+
right = Math.max(right, value + widthOf(id) / 2);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return { left, right };
|
|
361
|
+
}
|
|
362
|
+
function onePass(frame, widthOf, gap) {
|
|
363
|
+
const at = positions(frame.rows);
|
|
364
|
+
const root = alignBlocks(frame, at);
|
|
365
|
+
const x = compactBlocks(frame.rows, root, widthOf, gap);
|
|
366
|
+
if (!frame.mirrored) return x;
|
|
367
|
+
const mirrored = /* @__PURE__ */ new Map();
|
|
368
|
+
for (const [id, value] of x) mirrored.set(id, -value);
|
|
369
|
+
return mirrored;
|
|
370
|
+
}
|
|
371
|
+
function frames(layers, above, below) {
|
|
372
|
+
const fromTop = layers;
|
|
373
|
+
const fromBottom = [...layers].reverse();
|
|
374
|
+
const flip = (rows) => rows.map((row) => [...row].reverse());
|
|
375
|
+
return [
|
|
376
|
+
{ rows: fromTop, near: above, mirrored: false },
|
|
377
|
+
{ rows: flip(fromTop), near: above, mirrored: true },
|
|
378
|
+
{ rows: fromBottom, near: below, mirrored: false },
|
|
379
|
+
{ rows: flip(fromBottom), near: below, mirrored: true }
|
|
380
|
+
];
|
|
381
|
+
}
|
|
382
|
+
function assignCoordinates(input, options) {
|
|
383
|
+
const { layers, chains, sizeOf, chainOf } = input;
|
|
384
|
+
const gap = options.gap;
|
|
385
|
+
const laneIds = new Set(chainOf.values());
|
|
386
|
+
const isDummy = (id) => chainOf.has(id);
|
|
387
|
+
const isPoint = (id) => isDummy(id) || laneIds.has(id);
|
|
388
|
+
const widthOf = (id) => isPoint(id) ? 0 : sizeOf(id).width;
|
|
389
|
+
const heightOf = (id) => isPoint(id) ? 0 : sizeOf(id).height;
|
|
390
|
+
const laneOf = (id) => chainOf.get(id) ?? id;
|
|
391
|
+
const x = /* @__PURE__ */ new Map();
|
|
392
|
+
const read = (id) => x.get(laneOf(id));
|
|
393
|
+
const above = /* @__PURE__ */ new Map();
|
|
394
|
+
const below = /* @__PURE__ */ new Map();
|
|
395
|
+
const laneEnds = /* @__PURE__ */ new Map();
|
|
396
|
+
const link = (into, id, other) => {
|
|
397
|
+
into.set(id, [...into.get(id) ?? [], other]);
|
|
398
|
+
};
|
|
399
|
+
for (const chain of chains) {
|
|
400
|
+
for (let cursor = 0; cursor + 1 < chain.path.length; cursor += 1) {
|
|
401
|
+
const top2 = chain.path[cursor];
|
|
402
|
+
const bottom = chain.path[cursor + 1];
|
|
403
|
+
if (!isDummy(top2) && !isDummy(bottom)) {
|
|
404
|
+
link(below, top2, bottom);
|
|
405
|
+
link(above, bottom, top2);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if (chain.path.length > 2) {
|
|
409
|
+
const lane = chainOf.get(chain.path[1]);
|
|
410
|
+
if (lane !== void 0) laneEnds.set(lane, [chain.path[0], chain.path[chain.path.length - 1]]);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
const candidates = frames(layers, above, below).map((frame) => onePass(frame, widthOf, gap));
|
|
414
|
+
const leftOrigin = extentOf(candidates[0], layers, widthOf).left;
|
|
415
|
+
const rightOrigin = extentOf(candidates[1], layers, widthOf).right;
|
|
416
|
+
for (const [index, candidate] of candidates.entries()) {
|
|
417
|
+
const extent = extentOf(candidate, layers, widthOf);
|
|
418
|
+
const shift = index % 2 === 0 ? leftOrigin - extent.left : rightOrigin - extent.right;
|
|
419
|
+
for (const [id, value] of candidate) candidate.set(id, value + shift);
|
|
420
|
+
}
|
|
421
|
+
for (const row of layers) {
|
|
422
|
+
for (const id of row) {
|
|
423
|
+
let total = 0;
|
|
424
|
+
for (const candidate of candidates) total += candidate.get(id);
|
|
425
|
+
x.set(id, total / candidates.length);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
const reserved = layers.map((layer2) => {
|
|
429
|
+
let lowest = Infinity;
|
|
430
|
+
let highest = -Infinity;
|
|
431
|
+
for (const id of layer2) {
|
|
432
|
+
const value = x.get(id);
|
|
433
|
+
lowest = Math.min(lowest, value - widthOf(id) / 2);
|
|
434
|
+
highest = Math.max(highest, value + widthOf(id) / 2);
|
|
435
|
+
}
|
|
436
|
+
return { lowest, highest };
|
|
437
|
+
});
|
|
438
|
+
for (const [lane, ends] of laneEnds) x.set(lane, (x.get(ends[0]) + x.get(ends[1])) / 2);
|
|
439
|
+
for (const id of chainOf.keys()) x.delete(id);
|
|
440
|
+
for (const row of layers) {
|
|
441
|
+
let cursor = -Infinity;
|
|
442
|
+
for (const id of row) {
|
|
443
|
+
if (isDummy(id)) {
|
|
444
|
+
cursor = Math.max(cursor, read(id));
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
const width2 = widthOf(id);
|
|
448
|
+
const centre = Math.max(x.get(id), cursor + width2 / 2);
|
|
449
|
+
x.set(id, centre);
|
|
450
|
+
cursor = centre + width2 / 2 + gap;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
let left = 0;
|
|
454
|
+
for (const [id, value] of x) left = Math.min(left, value - widthOf(id) / 2);
|
|
455
|
+
for (const [id, value] of x) x.set(id, value - left);
|
|
456
|
+
const layerHeight = layers.map((layer2) => Math.max(0, ...layer2.map((id) => heightOf(id))));
|
|
457
|
+
const layerWidth = layers.map((layer2, depth) => {
|
|
458
|
+
let lowest = reserved[depth].lowest - left;
|
|
459
|
+
let highest = reserved[depth].highest - left;
|
|
460
|
+
for (const id of layer2) {
|
|
461
|
+
const value = read(id);
|
|
462
|
+
lowest = Math.min(lowest, value - widthOf(id) / 2);
|
|
463
|
+
highest = Math.max(highest, value + widthOf(id) / 2);
|
|
464
|
+
}
|
|
465
|
+
return highest - lowest;
|
|
466
|
+
});
|
|
467
|
+
const layerY = [];
|
|
468
|
+
let top = 0;
|
|
469
|
+
for (const [depth, height] of layerHeight.entries()) {
|
|
470
|
+
layerY.push(top);
|
|
471
|
+
top += height + options.layerGap;
|
|
472
|
+
}
|
|
473
|
+
let width = 0;
|
|
474
|
+
for (const [id, value] of x) width = Math.max(width, value + widthOf(id) / 2);
|
|
475
|
+
return {
|
|
476
|
+
x,
|
|
477
|
+
layerY,
|
|
478
|
+
layerHeight,
|
|
479
|
+
layerWidth,
|
|
480
|
+
width,
|
|
481
|
+
height: layerY.length === 0 ? 0 : layerY[layerY.length - 1] + layerHeight[layerHeight.length - 1]
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// src/render/cycle-break.ts
|
|
486
|
+
function tarjan(nodes, outgoing) {
|
|
487
|
+
let counter = 0;
|
|
488
|
+
const index = /* @__PURE__ */ new Map();
|
|
489
|
+
const low = /* @__PURE__ */ new Map();
|
|
490
|
+
const stack = [];
|
|
491
|
+
const onStack = /* @__PURE__ */ new Set();
|
|
492
|
+
const found = [];
|
|
493
|
+
const visit = (node) => {
|
|
494
|
+
index.set(node, counter);
|
|
495
|
+
low.set(node, counter);
|
|
496
|
+
counter += 1;
|
|
497
|
+
stack.push(node);
|
|
498
|
+
onStack.add(node);
|
|
499
|
+
for (const next of outgoing.get(node) ?? []) {
|
|
500
|
+
if (!index.has(next)) {
|
|
501
|
+
visit(next);
|
|
502
|
+
low.set(node, Math.min(low.get(node), low.get(next)));
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
if (onStack.has(next)) low.set(node, Math.min(low.get(node), index.get(next)));
|
|
506
|
+
}
|
|
507
|
+
if (low.get(node) !== index.get(node)) return;
|
|
508
|
+
const component = [];
|
|
509
|
+
let member;
|
|
510
|
+
do {
|
|
511
|
+
member = stack.pop();
|
|
512
|
+
onStack.delete(member);
|
|
513
|
+
component.push(member);
|
|
514
|
+
} while (member !== node);
|
|
515
|
+
found.push(component);
|
|
516
|
+
};
|
|
517
|
+
for (const node of nodes) if (!index.has(node)) visit(node);
|
|
518
|
+
return found;
|
|
519
|
+
}
|
|
520
|
+
function breakCycles(graph) {
|
|
521
|
+
const known = new Set(graph.nodes.map((node) => node.id));
|
|
522
|
+
const position = new Map(graph.nodes.map((node, offset) => [node.id, offset]));
|
|
523
|
+
const edges = graph.edges.map((edge) => ({ ...edge }));
|
|
524
|
+
const reversed = /* @__PURE__ */ new Set();
|
|
525
|
+
const flip = (index) => {
|
|
526
|
+
const edge = edges[index];
|
|
527
|
+
const next = edge.from;
|
|
528
|
+
edge.from = edge.to;
|
|
529
|
+
edge.to = next;
|
|
530
|
+
};
|
|
531
|
+
if (graph.init !== void 0 && known.has(graph.init)) {
|
|
532
|
+
for (const [index, edge] of edges.entries()) {
|
|
533
|
+
if (edge.to === graph.init && edge.from !== graph.init) flip(index);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
const outgoing = /* @__PURE__ */ new Map();
|
|
537
|
+
for (const edge of edges) {
|
|
538
|
+
if (!known.has(edge.from) || !known.has(edge.to) || edge.from === edge.to) continue;
|
|
539
|
+
outgoing.set(edge.from, [...outgoing.get(edge.from) ?? [], edge.to]);
|
|
540
|
+
}
|
|
541
|
+
const nodes = graph.nodes.map((node) => node.id);
|
|
542
|
+
const raw = tarjan(nodes, outgoing);
|
|
543
|
+
raw.reverse();
|
|
544
|
+
const componentOf = /* @__PURE__ */ new Map();
|
|
545
|
+
const components = raw.map((members, id) => {
|
|
546
|
+
for (const member of members) componentOf.set(member, id);
|
|
547
|
+
return { id, members };
|
|
548
|
+
});
|
|
549
|
+
const condensation = [];
|
|
550
|
+
const seen = /* @__PURE__ */ new Set();
|
|
551
|
+
for (const edge of edges) {
|
|
552
|
+
const from = componentOf.get(edge.from);
|
|
553
|
+
const to = componentOf.get(edge.to);
|
|
554
|
+
if (from === void 0 || to === void 0 || from === to) continue;
|
|
555
|
+
const key = String(from) + ">" + String(to);
|
|
556
|
+
if (seen.has(key)) continue;
|
|
557
|
+
seen.add(key);
|
|
558
|
+
condensation.push({ from, to });
|
|
559
|
+
}
|
|
560
|
+
const rank = /* @__PURE__ */ new Map();
|
|
561
|
+
for (const component of components) {
|
|
562
|
+
if (component.members.length < 2) {
|
|
563
|
+
rank.set(component.members[0], 0);
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
const inside2 = new Set(component.members);
|
|
567
|
+
const score = /* @__PURE__ */ new Map();
|
|
568
|
+
for (const member of component.members) score.set(member, 0);
|
|
569
|
+
for (const edge of edges) {
|
|
570
|
+
if (!inside2.has(edge.from) || !inside2.has(edge.to) || edge.from === edge.to) continue;
|
|
571
|
+
score.set(edge.from, score.get(edge.from) + 1);
|
|
572
|
+
score.set(edge.to, score.get(edge.to) - 1);
|
|
573
|
+
}
|
|
574
|
+
const ordered = [...component.members].sort((left, right) => {
|
|
575
|
+
const difference = score.get(right) - score.get(left);
|
|
576
|
+
return difference !== 0 ? difference : position.get(left) - position.get(right);
|
|
577
|
+
});
|
|
578
|
+
ordered.forEach((member, offset) => rank.set(member, offset));
|
|
579
|
+
}
|
|
580
|
+
for (const [index, edge] of edges.entries()) {
|
|
581
|
+
const from = rank.get(edge.from);
|
|
582
|
+
const to = rank.get(edge.to);
|
|
583
|
+
if (from === void 0 || to === void 0 || edge.from === edge.to) continue;
|
|
584
|
+
if (componentOf.get(edge.from) !== componentOf.get(edge.to)) continue;
|
|
585
|
+
if (from > to) flip(index);
|
|
586
|
+
}
|
|
587
|
+
edges.forEach((edge, index) => {
|
|
588
|
+
if (graph.edges[index].from !== edge.from) reversed.add(edge.index);
|
|
589
|
+
});
|
|
590
|
+
return { edges, reversed, components, componentOf, rank, condensation };
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// src/render/dummy.ts
|
|
594
|
+
function addDummies(layering, edges) {
|
|
595
|
+
const layers = layering.layers.map((layer2) => [...layer2]);
|
|
596
|
+
const chains = [];
|
|
597
|
+
const chainOf = /* @__PURE__ */ new Map();
|
|
598
|
+
const laneOf = /* @__PURE__ */ new Map();
|
|
599
|
+
for (const edge of edges) {
|
|
600
|
+
if (edge.from === edge.to) {
|
|
601
|
+
chains.push({ index: edge.index, path: [edge.from, edge.to] });
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
const from = layering.layerOf.get(edge.from);
|
|
605
|
+
const to = layering.layerOf.get(edge.to);
|
|
606
|
+
if (from === void 0 || to === void 0 || to <= from) {
|
|
607
|
+
chains.push({ index: edge.index, path: [edge.from, edge.to] });
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
const path = [edge.from];
|
|
611
|
+
for (let cursor = from + 1; cursor < to; cursor += 1) {
|
|
612
|
+
const instance = "#d" + String(edge.index) + "_" + String(cursor);
|
|
613
|
+
const lane = "#e" + String(edge.index);
|
|
614
|
+
chainOf.set(instance, lane);
|
|
615
|
+
laneOf.set(edge.index, lane);
|
|
616
|
+
layers[cursor].push(instance);
|
|
617
|
+
path.push(instance);
|
|
618
|
+
}
|
|
619
|
+
path.push(edge.to);
|
|
620
|
+
chains.push({ index: edge.index, path });
|
|
621
|
+
}
|
|
622
|
+
return { layers, chains, chainOf, laneOf };
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// src/render/network-simplex.ts
|
|
626
|
+
function slackOf(edge, rank) {
|
|
627
|
+
return rank[edge.to] - rank[edge.from] - edge.minLength;
|
|
628
|
+
}
|
|
629
|
+
function componentsOf(count, edges) {
|
|
630
|
+
const neighbours = [];
|
|
631
|
+
for (let node = 0; node < count; node += 1) neighbours.push([]);
|
|
632
|
+
for (const edge of edges) {
|
|
633
|
+
neighbours[edge.from].push(edge.to);
|
|
634
|
+
neighbours[edge.to].push(edge.from);
|
|
635
|
+
}
|
|
636
|
+
const seen = new Array(count).fill(false);
|
|
637
|
+
const components = [];
|
|
638
|
+
for (let node = 0; node < count; node += 1) {
|
|
639
|
+
if (seen[node]) continue;
|
|
640
|
+
seen[node] = true;
|
|
641
|
+
const members = [node];
|
|
642
|
+
for (let head = 0; head < members.length; head += 1) {
|
|
643
|
+
for (const next of neighbours[members[head]]) {
|
|
644
|
+
if (seen[next]) continue;
|
|
645
|
+
seen[next] = true;
|
|
646
|
+
members.push(next);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
members.sort((left, right) => left - right);
|
|
650
|
+
components.push(members);
|
|
651
|
+
}
|
|
652
|
+
return components;
|
|
653
|
+
}
|
|
654
|
+
function buildTightTree(members, edges, rank, tree) {
|
|
655
|
+
const inside2 = /* @__PURE__ */ new Set([members[0]]);
|
|
656
|
+
while (inside2.size < members.length) {
|
|
657
|
+
let tight = -1;
|
|
658
|
+
for (const [index, edge] of edges.entries()) {
|
|
659
|
+
if (tree.has(index)) continue;
|
|
660
|
+
if (inside2.has(edge.from) === inside2.has(edge.to)) continue;
|
|
661
|
+
if (slackOf(edge, rank) === 0) {
|
|
662
|
+
tight = index;
|
|
663
|
+
break;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
if (tight >= 0) {
|
|
667
|
+
tree.add(tight);
|
|
668
|
+
inside2.add(edges[tight].from);
|
|
669
|
+
inside2.add(edges[tight].to);
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
let lowest = -Infinity;
|
|
673
|
+
let highest = Infinity;
|
|
674
|
+
for (const edge of edges) {
|
|
675
|
+
const from = inside2.has(edge.from);
|
|
676
|
+
const to = inside2.has(edge.to);
|
|
677
|
+
if (from === to) continue;
|
|
678
|
+
const slack = slackOf(edge, rank);
|
|
679
|
+
if (from) lowest = Math.max(lowest, -slack);
|
|
680
|
+
else highest = Math.min(highest, slack);
|
|
681
|
+
}
|
|
682
|
+
const step = lowest > -Infinity ? lowest : highest;
|
|
683
|
+
for (const node of members) if (!inside2.has(node)) rank[node] += step;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
function walkTree(members, edges, tree) {
|
|
687
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
688
|
+
const link = (node, other) => {
|
|
689
|
+
adjacency.set(node, [...adjacency.get(node) ?? [], { node: other }]);
|
|
690
|
+
};
|
|
691
|
+
for (const index of tree) {
|
|
692
|
+
const edge = edges[index];
|
|
693
|
+
link(edge.from, edge.to);
|
|
694
|
+
link(edge.to, edge.from);
|
|
695
|
+
}
|
|
696
|
+
const parent = /* @__PURE__ */ new Map();
|
|
697
|
+
const first = /* @__PURE__ */ new Map();
|
|
698
|
+
const size = /* @__PURE__ */ new Map();
|
|
699
|
+
const order2 = [];
|
|
700
|
+
const seen = /* @__PURE__ */ new Set();
|
|
701
|
+
const root = members[0];
|
|
702
|
+
const stack = [root];
|
|
703
|
+
seen.add(root);
|
|
704
|
+
parent.set(root, -1);
|
|
705
|
+
while (stack.length > 0) {
|
|
706
|
+
const node = stack.pop();
|
|
707
|
+
first.set(node, order2.length);
|
|
708
|
+
order2.push(node);
|
|
709
|
+
for (const step of adjacency.get(node)) {
|
|
710
|
+
if (seen.has(step.node)) continue;
|
|
711
|
+
seen.add(step.node);
|
|
712
|
+
parent.set(step.node, node);
|
|
713
|
+
stack.push(step.node);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
for (let index = order2.length - 1; index >= 0; index -= 1) {
|
|
717
|
+
const node = order2[index];
|
|
718
|
+
size.set(node, (size.get(node) ?? 0) + 1);
|
|
719
|
+
const above = parent.get(node);
|
|
720
|
+
if (above !== void 0 && above >= 0) size.set(above, (size.get(above) ?? 0) + size.get(node));
|
|
721
|
+
}
|
|
722
|
+
return { parent, first, size, order: order2 };
|
|
723
|
+
}
|
|
724
|
+
function solveComponent(size, members, edges, rank) {
|
|
725
|
+
if (members.length < 2) {
|
|
726
|
+
if (members.length === 1) rank[members[0]] = 0;
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
const tree = /* @__PURE__ */ new Set();
|
|
730
|
+
buildTightTree(members, edges, rank, tree);
|
|
731
|
+
const spanning = tree.size === members.length - 1;
|
|
732
|
+
if (spanning) improve(size, members, edges, tree, rank);
|
|
733
|
+
let top = Infinity;
|
|
734
|
+
for (const node of members) top = Math.min(top, rank[node]);
|
|
735
|
+
for (const node of members) rank[node] -= top;
|
|
736
|
+
}
|
|
737
|
+
function improve(size, members, edges, tree, rank) {
|
|
738
|
+
const incoming = new Float64Array(size);
|
|
739
|
+
const outgoing = new Float64Array(size);
|
|
740
|
+
for (const edge of edges) {
|
|
741
|
+
outgoing[edge.from] += edge.weight;
|
|
742
|
+
incoming[edge.to] += edge.weight;
|
|
743
|
+
}
|
|
744
|
+
const cap = Math.max(50, 8 * members.length + 4 * edges.length);
|
|
745
|
+
for (let iteration = 0; iteration < cap; iteration += 1) {
|
|
746
|
+
const walk = walkTree(members, edges, tree);
|
|
747
|
+
const entering = /* @__PURE__ */ new Map();
|
|
748
|
+
const leaving = /* @__PURE__ */ new Map();
|
|
749
|
+
for (let index = walk.order.length - 1; index >= 0; index -= 1) {
|
|
750
|
+
const node = walk.order[index];
|
|
751
|
+
entering.set(node, incoming[node] + (entering.get(node) ?? 0));
|
|
752
|
+
leaving.set(node, outgoing[node] + (leaving.get(node) ?? 0));
|
|
753
|
+
const above = walk.parent.get(node);
|
|
754
|
+
if (above === void 0 || above < 0) continue;
|
|
755
|
+
entering.set(above, (entering.get(above) ?? 0) + entering.get(node));
|
|
756
|
+
leaving.set(above, (leaving.get(above) ?? 0) + leaving.get(node));
|
|
757
|
+
}
|
|
758
|
+
const holds = (node, child) => {
|
|
759
|
+
const start = walk.first.get(child);
|
|
760
|
+
const at = walk.first.get(node);
|
|
761
|
+
return at >= start && at < start + walk.size.get(child);
|
|
762
|
+
};
|
|
763
|
+
let worst = -1;
|
|
764
|
+
let worstEdge = -1;
|
|
765
|
+
let worstChild = -1;
|
|
766
|
+
for (const index of tree) {
|
|
767
|
+
const edge = edges[index];
|
|
768
|
+
const child = walk.parent.get(edge.from) === edge.to ? edge.from : edge.to;
|
|
769
|
+
const tailInside = holds(edge.from, child);
|
|
770
|
+
const value = tailInside ? leaving.get(child) - entering.get(child) : entering.get(child) - leaving.get(child);
|
|
771
|
+
if (value < 0 && (worstEdge < 0 || value < worst)) {
|
|
772
|
+
worst = value;
|
|
773
|
+
worstEdge = index;
|
|
774
|
+
worstChild = child;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
if (worstEdge < 0) return;
|
|
778
|
+
const leavingEdge = edges[worstEdge];
|
|
779
|
+
const headInside = !holds(leavingEdge.from, worstChild);
|
|
780
|
+
const inHead = (node) => headInside ? holds(node, worstChild) : !holds(node, worstChild);
|
|
781
|
+
let enteringEdge = -1;
|
|
782
|
+
let step = Infinity;
|
|
783
|
+
for (const [index, edge] of edges.entries()) {
|
|
784
|
+
if (tree.has(index)) continue;
|
|
785
|
+
if (!inHead(edge.from) || inHead(edge.to)) continue;
|
|
786
|
+
const slack = slackOf(edge, rank);
|
|
787
|
+
if (slack < step || slack === step && (enteringEdge < 0 || index < enteringEdge)) {
|
|
788
|
+
step = slack;
|
|
789
|
+
enteringEdge = index;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
if (enteringEdge < 0 || step === Infinity) return;
|
|
793
|
+
if (step !== 0) {
|
|
794
|
+
for (const node of members) if (inHead(node)) rank[node] += step;
|
|
795
|
+
}
|
|
796
|
+
tree.delete(worstEdge);
|
|
797
|
+
tree.add(enteringEdge);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
function minimumLength(problem) {
|
|
801
|
+
const rank = problem.start.slice();
|
|
802
|
+
for (const members of componentsOf(problem.count, problem.edges)) {
|
|
803
|
+
const inside2 = new Set(members);
|
|
804
|
+
const local = problem.edges.filter((edge) => inside2.has(edge.from) && inside2.has(edge.to));
|
|
805
|
+
const pinned = problem.pinned !== void 0 && inside2.has(problem.pinned) ? problem.pinned : void 0;
|
|
806
|
+
const pinnedRank = pinned === void 0 ? void 0 : rank[pinned];
|
|
807
|
+
if (pinned !== void 0 && pinnedRank !== void 0 && members.every((node) => rank[node] >= pinnedRank)) {
|
|
808
|
+
for (const node of members) if (node !== pinned) local.push({ from: pinned, to: node, minLength: 0, weight: 0 });
|
|
809
|
+
}
|
|
810
|
+
solveComponent(problem.count, members, local, rank);
|
|
811
|
+
}
|
|
812
|
+
return rank;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// src/render/layer.ts
|
|
816
|
+
function longestPath(ids, edges, index) {
|
|
817
|
+
const rank = new Array(ids.length).fill(0);
|
|
818
|
+
const pending = new Array(ids.length).fill(0);
|
|
819
|
+
const outgoing = ids.map(() => []);
|
|
820
|
+
for (const edge of edges) {
|
|
821
|
+
outgoing[index.get(edge.from)].push(index.get(edge.to));
|
|
822
|
+
pending[index.get(edge.to)] += 1;
|
|
823
|
+
}
|
|
824
|
+
const queue = [];
|
|
825
|
+
for (let node = 0; node < ids.length; node += 1) if (pending[node] === 0) queue.push(node);
|
|
826
|
+
for (let head = 0; head < queue.length; head += 1) {
|
|
827
|
+
const node = queue[head];
|
|
828
|
+
for (const target of outgoing[node]) {
|
|
829
|
+
rank[target] = Math.max(rank[target], rank[node] + 1);
|
|
830
|
+
pending[target] -= 1;
|
|
831
|
+
if (pending[target] === 0) queue.push(target);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
return rank;
|
|
835
|
+
}
|
|
836
|
+
function layer(graph, edges) {
|
|
837
|
+
const ids = [];
|
|
838
|
+
const seen = /* @__PURE__ */ new Set();
|
|
839
|
+
for (const node of graph.nodes) {
|
|
840
|
+
if (seen.has(node.id)) continue;
|
|
841
|
+
seen.add(node.id);
|
|
842
|
+
ids.push(node.id);
|
|
843
|
+
}
|
|
844
|
+
const index = new Map(ids.map((id, offset) => [id, offset]));
|
|
845
|
+
const forward = edges.filter((edge) => index.has(edge.from) && index.has(edge.to) && edge.from !== edge.to);
|
|
846
|
+
const rankEdges = forward.map((edge) => ({
|
|
847
|
+
from: index.get(edge.from),
|
|
848
|
+
to: index.get(edge.to),
|
|
849
|
+
minLength: 1,
|
|
850
|
+
weight: 1
|
|
851
|
+
}));
|
|
852
|
+
let pinned;
|
|
853
|
+
if (graph.init !== void 0 && index.has(graph.init)) {
|
|
854
|
+
const start = index.get(graph.init);
|
|
855
|
+
const forced = forward.some((edge) => edge.to === graph.init);
|
|
856
|
+
if (!forced) pinned = start;
|
|
857
|
+
}
|
|
858
|
+
const rank = minimumLength({ count: ids.length, edges: rankEdges, start: longestPath(ids, forward, index), ...pinned === void 0 ? {} : { pinned } });
|
|
859
|
+
const layers = [];
|
|
860
|
+
for (const [offset, id] of ids.entries()) {
|
|
861
|
+
const depth = rank[offset];
|
|
862
|
+
while (layers.length <= depth) layers.push([]);
|
|
863
|
+
layers[depth].push(id);
|
|
864
|
+
}
|
|
865
|
+
const layerOf = new Map(ids.map((id, offset) => [id, rank[offset]]));
|
|
866
|
+
return { layers, layerOf };
|
|
867
|
+
}
|
|
868
|
+
|
|
40
869
|
// src/render/layout.ts
|
|
41
870
|
function rankStates(nodes, edges, init) {
|
|
42
871
|
const ids = nodes.map((node) => node.id);
|
|
43
872
|
const known = new Set(ids);
|
|
44
873
|
const start = init !== void 0 && known.has(init) ? init : ids[0];
|
|
45
874
|
const outgoing = /* @__PURE__ */ new Map();
|
|
46
|
-
const incoming = /* @__PURE__ */ new Map();
|
|
47
875
|
for (const edge of edges) {
|
|
48
876
|
if (!known.has(edge.from) || !known.has(edge.to)) continue;
|
|
49
877
|
outgoing.set(edge.from, [...outgoing.get(edge.from) ?? [], edge.to]);
|
|
50
|
-
incoming.set(edge.to, [...incoming.get(edge.to) ?? [], edge.from]);
|
|
51
878
|
}
|
|
52
879
|
const depth = /* @__PURE__ */ new Map();
|
|
53
880
|
if (start !== void 0) {
|
|
@@ -72,40 +899,224 @@ window.__ModuleLoader__.load({
|
|
|
72
899
|
}
|
|
73
900
|
return { rows, unreachable: ids.filter((id) => !depth.has(id)) };
|
|
74
901
|
}
|
|
902
|
+
function seedOrder(graph) {
|
|
903
|
+
const ranked = rankStates(graph.nodes, graph.edges, graph.init);
|
|
904
|
+
const order2 = /* @__PURE__ */ new Map();
|
|
905
|
+
let index = 0;
|
|
906
|
+
for (const row of ranked.rows) for (const id of row) order2.set(id, index++);
|
|
907
|
+
for (const id of ranked.unreachable) order2.set(id, index++);
|
|
908
|
+
return order2;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// src/render/order.ts
|
|
912
|
+
function locate(layers) {
|
|
913
|
+
const found = /* @__PURE__ */ new Map();
|
|
914
|
+
layers.forEach((layer2, depth) => layer2.forEach((id, index) => found.set(id, { layer: depth, index })));
|
|
915
|
+
return found;
|
|
916
|
+
}
|
|
917
|
+
function medianOf(id, neighbours, reference2, positions2) {
|
|
918
|
+
const found = (neighbours.get(id) ?? []).filter((other) => positions2.get(other)?.layer === reference2).map((other) => positions2.get(other).index).sort((left, right) => left - right);
|
|
919
|
+
if (found.length === 0) return positions2.get(id).index;
|
|
920
|
+
const middle = Math.floor(found.length / 2);
|
|
921
|
+
return found.length % 2 === 1 ? found[middle] : (found[middle - 1] + found[middle]) / 2;
|
|
922
|
+
}
|
|
923
|
+
function countCrossings(layers, chains) {
|
|
924
|
+
const positions2 = locate(layers);
|
|
925
|
+
const segments = /* @__PURE__ */ new Map();
|
|
926
|
+
for (const chain of chains) {
|
|
927
|
+
for (let cursor = 0; cursor + 1 < chain.path.length; cursor += 1) {
|
|
928
|
+
const top = positions2.get(chain.path[cursor]);
|
|
929
|
+
const bottom = positions2.get(chain.path[cursor + 1]);
|
|
930
|
+
if (top === void 0 || bottom === void 0 || bottom.layer !== top.layer + 1) continue;
|
|
931
|
+
const list = segments.get(top.layer) ?? [];
|
|
932
|
+
list.push({ top: top.index, bottom: bottom.index });
|
|
933
|
+
segments.set(top.layer, list);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
let crossings = 0;
|
|
937
|
+
for (const list of segments.values()) {
|
|
938
|
+
for (let left = 0; left < list.length; left += 1) {
|
|
939
|
+
for (let right = left + 1; right < list.length; right += 1) {
|
|
940
|
+
if ((list[left].top - list[right].top) * (list[left].bottom - list[right].bottom) < 0) crossings += 1;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
return crossings;
|
|
945
|
+
}
|
|
946
|
+
function order(seedLayers, chains, seed) {
|
|
947
|
+
const layers = seedLayers.map(
|
|
948
|
+
(layer2) => layer2.map((id, index) => ({ id, index })).sort((left, right) => (seed?.get(left.id) ?? Infinity) - (seed?.get(right.id) ?? Infinity) || left.index - right.index).map((entry) => entry.id)
|
|
949
|
+
);
|
|
950
|
+
const down = /* @__PURE__ */ new Map();
|
|
951
|
+
const up = /* @__PURE__ */ new Map();
|
|
952
|
+
for (const chain of chains) {
|
|
953
|
+
for (let cursor = 0; cursor + 1 < chain.path.length; cursor += 1) {
|
|
954
|
+
const top = chain.path[cursor];
|
|
955
|
+
const bottom = chain.path[cursor + 1];
|
|
956
|
+
down.set(top, [...down.get(top) ?? [], bottom]);
|
|
957
|
+
up.set(bottom, [...up.get(bottom) ?? [], top]);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
let positions2 = locate(layers);
|
|
961
|
+
for (let round = 0; round < 4; round += 1) {
|
|
962
|
+
for (let depth = 1; depth < layers.length; depth += 1) {
|
|
963
|
+
const layer2 = layers[depth];
|
|
964
|
+
const sorted = layer2.map((id, index) => ({ id, index, median: medianOf(id, up, depth - 1, positions2) })).sort((left, right) => left.median - right.median || left.index - right.index).map((entry) => entry.id);
|
|
965
|
+
layers[depth] = sorted;
|
|
966
|
+
positions2 = locate(layers);
|
|
967
|
+
}
|
|
968
|
+
for (let depth = layers.length - 2; depth >= 0; depth -= 1) {
|
|
969
|
+
const layer2 = layers[depth];
|
|
970
|
+
const sorted = layer2.map((id, index) => ({ id, index, median: medianOf(id, down, depth + 1, positions2) })).sort((left, right) => left.median - right.median || left.index - right.index).map((entry) => entry.id);
|
|
971
|
+
layers[depth] = sorted;
|
|
972
|
+
positions2 = locate(layers);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
let crossings = countCrossings(layers, chains);
|
|
976
|
+
for (let pass = 0; pass < 4; pass += 1) {
|
|
977
|
+
let improved = false;
|
|
978
|
+
for (const layer2 of layers) {
|
|
979
|
+
for (let index = 0; index + 1 < layer2.length; index += 1) {
|
|
980
|
+
const before = layer2[index];
|
|
981
|
+
layer2[index] = layer2[index + 1];
|
|
982
|
+
layer2[index + 1] = before;
|
|
983
|
+
const next = countCrossings(layers, chains);
|
|
984
|
+
if (next < crossings) {
|
|
985
|
+
crossings = next;
|
|
986
|
+
improved = true;
|
|
987
|
+
} else {
|
|
988
|
+
layer2[index + 1] = layer2[index];
|
|
989
|
+
layer2[index] = before;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
if (!improved) break;
|
|
994
|
+
}
|
|
995
|
+
return { layers, crossings };
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// src/render/text.ts
|
|
999
|
+
var CHAR_WIDTH = 6.6;
|
|
1000
|
+
var MAX_LINE_CHARS = 30;
|
|
1001
|
+
var LINE_HEIGHT = 15;
|
|
1002
|
+
var TEXT_PADDING = 12;
|
|
1003
|
+
var MIN_NODE_WIDTH = 88;
|
|
1004
|
+
var MAX_NODE_WIDTH = 260;
|
|
1005
|
+
var BREAK = /<br\s*\/?>|\r?\n/gi;
|
|
1006
|
+
function explicitLines(label) {
|
|
1007
|
+
return label.split(BREAK).map((line) => line.trim());
|
|
1008
|
+
}
|
|
1009
|
+
function wrapLine(line, limit = MAX_LINE_CHARS) {
|
|
1010
|
+
if (line === "") return [""];
|
|
1011
|
+
const out = [];
|
|
1012
|
+
let current = "";
|
|
1013
|
+
for (const word of line.split(/\s+/).filter((part) => part !== "")) {
|
|
1014
|
+
const candidate = current === "" ? word : current + " " + word;
|
|
1015
|
+
if (candidate.length <= limit) {
|
|
1016
|
+
current = candidate;
|
|
1017
|
+
continue;
|
|
1018
|
+
}
|
|
1019
|
+
if (current !== "") out.push(current);
|
|
1020
|
+
if (word.length <= limit) {
|
|
1021
|
+
current = word;
|
|
1022
|
+
continue;
|
|
1023
|
+
}
|
|
1024
|
+
let rest = word;
|
|
1025
|
+
while (rest.length > limit) {
|
|
1026
|
+
out.push(rest.slice(0, limit));
|
|
1027
|
+
rest = rest.slice(limit);
|
|
1028
|
+
}
|
|
1029
|
+
current = rest;
|
|
1030
|
+
}
|
|
1031
|
+
if (current !== "") out.push(current);
|
|
1032
|
+
return out.length === 0 ? [""] : out;
|
|
1033
|
+
}
|
|
1034
|
+
function measure(label) {
|
|
1035
|
+
const lines = explicitLines(label).flatMap((line) => wrapLine(line));
|
|
1036
|
+
const longest = lines.reduce((max, line) => Math.max(max, line.length), 0);
|
|
1037
|
+
const width = Math.min(MAX_NODE_WIDTH, Math.max(MIN_NODE_WIDTH, longest * CHAR_WIDTH + TEXT_PADDING * 2));
|
|
1038
|
+
const height = lines.length * LINE_HEIGHT + TEXT_PADDING;
|
|
1039
|
+
return { lines, width, height };
|
|
1040
|
+
}
|
|
1041
|
+
function textWidth(text2) {
|
|
1042
|
+
return text2.length * CHAR_WIDTH;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// src/render/wrap.ts
|
|
1046
|
+
var PAGE_ASPECT2 = 1.6;
|
|
1047
|
+
var MIN_LAYERS_TO_WRAP = 4;
|
|
1048
|
+
var STRIP_RATIO = 2;
|
|
1049
|
+
function runHeight(layerHeight, first, last, gap) {
|
|
1050
|
+
let total = 0;
|
|
1051
|
+
for (let index = first; index <= last; index += 1) {
|
|
1052
|
+
total += layerHeight[index];
|
|
1053
|
+
if (index < last) total += gap;
|
|
1054
|
+
}
|
|
1055
|
+
return total;
|
|
1056
|
+
}
|
|
1057
|
+
function runWidth(layerWidth, first, last) {
|
|
1058
|
+
let widest = 0;
|
|
1059
|
+
for (let index = first; index <= last; index += 1) widest = Math.max(widest, layerWidth[index]);
|
|
1060
|
+
return widest;
|
|
1061
|
+
}
|
|
1062
|
+
function shapeError(width, height, aspect) {
|
|
1063
|
+
if (width <= 0 || height <= 0) return Infinity;
|
|
1064
|
+
const ratio = Math.max(width, height) / Math.min(width, height);
|
|
1065
|
+
return Math.abs(Math.log(ratio / aspect));
|
|
1066
|
+
}
|
|
1067
|
+
function planBands(layerWidth, layerHeight, gap, aspect = PAGE_ASPECT2) {
|
|
1068
|
+
const count = layerWidth.length;
|
|
1069
|
+
if (count === 0) return { bands: [], width: 0, height: 0 };
|
|
1070
|
+
const single = {
|
|
1071
|
+
bands: [{ first: 0, last: count - 1, offsetX: 0 }],
|
|
1072
|
+
width: runWidth(layerWidth, 0, count - 1),
|
|
1073
|
+
height: runHeight(layerHeight, 0, count - 1, gap)
|
|
1074
|
+
};
|
|
1075
|
+
if (count < MIN_LAYERS_TO_WRAP || single.height <= single.width * STRIP_RATIO) return single;
|
|
1076
|
+
const columnGap = gap * 2;
|
|
1077
|
+
let best = single;
|
|
1078
|
+
let bestError = shapeError(single.width, single.height, aspect);
|
|
1079
|
+
for (let bands = 2; bands < count; bands += 1) {
|
|
1080
|
+
const base = Math.floor(count / bands);
|
|
1081
|
+
const extra = count % bands;
|
|
1082
|
+
const ranges = [];
|
|
1083
|
+
let cursor = 0;
|
|
1084
|
+
for (let band = 0; band < bands; band += 1) {
|
|
1085
|
+
const size = base + (band < extra ? 1 : 0);
|
|
1086
|
+
ranges.push({ first: cursor, last: cursor + size - 1 });
|
|
1087
|
+
cursor += size;
|
|
1088
|
+
}
|
|
1089
|
+
let width = columnGap * (bands - 1);
|
|
1090
|
+
let height = 0;
|
|
1091
|
+
for (const range of ranges) {
|
|
1092
|
+
width += runWidth(layerWidth, range.first, range.last);
|
|
1093
|
+
height = Math.max(height, runHeight(layerHeight, range.first, range.last, gap));
|
|
1094
|
+
}
|
|
1095
|
+
if (shapeError(width, height, aspect) >= bestError) continue;
|
|
1096
|
+
const planned = [];
|
|
1097
|
+
let offsetX = 0;
|
|
1098
|
+
for (const range of ranges) {
|
|
1099
|
+
planned.push({ first: range.first, last: range.last, offsetX });
|
|
1100
|
+
offsetX += runWidth(layerWidth, range.first, range.last) + columnGap;
|
|
1101
|
+
}
|
|
1102
|
+
best = { bands: planned, width, height };
|
|
1103
|
+
bestError = shapeError(width, height, aspect);
|
|
1104
|
+
}
|
|
1105
|
+
return best;
|
|
1106
|
+
}
|
|
75
1107
|
|
|
76
1108
|
// src/render/place.ts
|
|
77
1109
|
var MARGIN = 20;
|
|
78
|
-
var
|
|
79
|
-
var
|
|
80
|
-
var
|
|
81
|
-
var
|
|
82
|
-
var
|
|
83
|
-
var
|
|
1110
|
+
var LAYER_GAP = 44;
|
|
1111
|
+
var LABELLED_LAYER_GAP = 54;
|
|
1112
|
+
var NODE_GAP = 30;
|
|
1113
|
+
var LANE_CLEARANCE = 12;
|
|
1114
|
+
var LANE_SPACING = 14;
|
|
1115
|
+
var COMPONENT_GAP = 30;
|
|
84
1116
|
var GROUP_PADDING = 10;
|
|
85
1117
|
var GROUP_HEADER = 16;
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
function placeByRanks(diagram, nodes, top) {
|
|
89
|
-
const ranked = rankStates(diagram.nodes, diagram.edges, diagram.init);
|
|
90
|
-
const rows = [...ranked.rows];
|
|
91
|
-
if (ranked.unreachable.length > 0) rows.push(ranked.unreachable);
|
|
92
|
-
const boxes = /* @__PURE__ */ new Map();
|
|
93
|
-
let widest = 0;
|
|
94
|
-
rows.forEach((row, index) => {
|
|
95
|
-
const widths = row.map((id) => nodeWidth(id));
|
|
96
|
-
widest = Math.max(widest, widths.reduce((total, width) => total + width, 0) + Math.max(0, row.length - 1) * COLUMN_GAP);
|
|
97
|
-
let x = 0;
|
|
98
|
-
row.forEach((id, position) => {
|
|
99
|
-
boxes.set(id, { x, y: top + index * (NODE_HEIGHT + ROW_GAP), width: widths[position], height: NODE_HEIGHT });
|
|
100
|
-
x += widths[position] + COLUMN_GAP;
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
rows.forEach((row) => {
|
|
104
|
-
const rowWidth = row.reduce((total, id) => total + boxes.get(id).width, 0) + Math.max(0, row.length - 1) * COLUMN_GAP;
|
|
105
|
-
const offset = (widest - rowWidth) / 2;
|
|
106
|
-
for (const id of row) boxes.get(id).x += offset + MARGIN;
|
|
107
|
-
});
|
|
108
|
-
return { boxes, groupRects: /* @__PURE__ */ new Map(), width: widest + MARGIN * 2 };
|
|
1118
|
+
function textOf(node) {
|
|
1119
|
+
return measure(node.label !== void 0 && node.label !== "" ? node.label : node.id);
|
|
109
1120
|
}
|
|
110
1121
|
function groupTree(groups) {
|
|
111
1122
|
const byId = new Map(groups.map((group) => [group.id, group]));
|
|
@@ -162,69 +1173,511 @@ window.__ModuleLoader__.load({
|
|
|
162
1173
|
if (parent === void 0) roots.push(group.id);
|
|
163
1174
|
else children.set(parent, [...children.get(parent) ?? [], group.id]);
|
|
164
1175
|
}
|
|
165
|
-
return { owner, children, roots };
|
|
1176
|
+
return { owner, parentOf, children, roots };
|
|
166
1177
|
}
|
|
167
|
-
function
|
|
168
|
-
const groups = diagram.groups;
|
|
169
|
-
const
|
|
170
|
-
const
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
1178
|
+
function place(diagram, top) {
|
|
1179
|
+
const groups = diagram.groups ?? [];
|
|
1180
|
+
const nodes = [];
|
|
1181
|
+
const seenNode = /* @__PURE__ */ new Set();
|
|
1182
|
+
for (const node of diagram.nodes) {
|
|
1183
|
+
if (seenNode.has(node.id)) continue;
|
|
1184
|
+
seenNode.add(node.id);
|
|
1185
|
+
nodes.push(node);
|
|
1186
|
+
}
|
|
1187
|
+
const nodeById = new Map(nodes.map((node) => [node.id, node]));
|
|
1188
|
+
const groupById = new Map(groups.map((group) => [group.id, group]));
|
|
1189
|
+
const tree = groupTree(groups);
|
|
1190
|
+
const appearance = /* @__PURE__ */ new Map();
|
|
1191
|
+
for (const node of nodes) if (!appearance.has(node.id)) appearance.set(node.id, appearance.size);
|
|
1192
|
+
for (const group of groups) if (!appearance.has(group.id)) appearance.set(group.id, appearance.size);
|
|
1193
|
+
const visible = /* @__PURE__ */ new Set();
|
|
1194
|
+
const asked = /* @__PURE__ */ new Map();
|
|
1195
|
+
const isVisible = (id) => {
|
|
1196
|
+
const known = asked.get(id);
|
|
1197
|
+
if (known !== void 0) return known;
|
|
1198
|
+
const group = groupById.get(id);
|
|
1199
|
+
let found = false;
|
|
1200
|
+
for (const member of group.members) {
|
|
1201
|
+
if (tree.owner.get(member) === id && nodeById.has(member)) {
|
|
1202
|
+
found = true;
|
|
1203
|
+
break;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
if (!found) {
|
|
1207
|
+
for (const child of tree.children.get(id) ?? []) {
|
|
1208
|
+
if (isVisible(child)) {
|
|
1209
|
+
found = true;
|
|
1210
|
+
break;
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
asked.set(id, found);
|
|
1215
|
+
if (found) visible.add(id);
|
|
1216
|
+
return found;
|
|
179
1217
|
};
|
|
180
|
-
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
1218
|
+
for (const group of groups) isVisible(group.id);
|
|
1219
|
+
const directNodes = (scope) => nodes.filter((node) => tree.owner.get(node.id) === scope).map((node) => node.id);
|
|
1220
|
+
const directGroups = (scope) => (tree.children.get(scope) ?? []).filter((child) => visible.has(child));
|
|
1221
|
+
const firstIndexCache = /* @__PURE__ */ new Map();
|
|
1222
|
+
const firstIndexOf = (id) => {
|
|
1223
|
+
const cached = firstIndexCache.get(id);
|
|
1224
|
+
if (cached !== void 0) return cached;
|
|
1225
|
+
if (nodeById.has(id)) return appearance.get(id);
|
|
1226
|
+
const members = groupById.get(id).members.filter((member) => tree.owner.get(member) === id && nodeById.has(member)).map((member) => appearance.get(member));
|
|
1227
|
+
const children = directGroups(id).map(firstIndexOf);
|
|
1228
|
+
const all = [...members, ...children];
|
|
1229
|
+
const found = Math.min(...all);
|
|
1230
|
+
firstIndexCache.set(id, found);
|
|
1231
|
+
return found;
|
|
1232
|
+
};
|
|
1233
|
+
const entityAt = (scope, id) => {
|
|
1234
|
+
const boundary = groupById.get(id);
|
|
1235
|
+
if (boundary !== void 0) {
|
|
1236
|
+
if (!visible.has(id)) return void 0;
|
|
1237
|
+
if (scope === void 0) {
|
|
1238
|
+
let cursor = id;
|
|
1239
|
+
while (tree.parentOf.has(cursor)) cursor = tree.parentOf.get(cursor);
|
|
1240
|
+
return cursor;
|
|
1241
|
+
}
|
|
1242
|
+
return tree.parentOf.get(id) === scope ? id : void 0;
|
|
1243
|
+
}
|
|
1244
|
+
if (!nodeById.has(id)) return void 0;
|
|
1245
|
+
const group = tree.owner.get(id);
|
|
1246
|
+
if (scope === void 0) {
|
|
1247
|
+
if (group === void 0 || !visible.has(group)) return id;
|
|
1248
|
+
let cursor = group;
|
|
1249
|
+
while (tree.parentOf.has(cursor)) cursor = tree.parentOf.get(cursor);
|
|
1250
|
+
return cursor;
|
|
1251
|
+
}
|
|
1252
|
+
if (group === scope) return id;
|
|
1253
|
+
if (group === void 0) return void 0;
|
|
1254
|
+
return tree.parentOf.get(group) === scope && visible.has(group) ? group : void 0;
|
|
1255
|
+
};
|
|
1256
|
+
const groupLayouts = /* @__PURE__ */ new Map();
|
|
1257
|
+
const measureGroup = (id) => {
|
|
1258
|
+
const inside2 = buildLevel(id);
|
|
1259
|
+
const size = {
|
|
1260
|
+
width: Math.max(MIN_NODE_WIDTH, inside2.width) + GROUP_PADDING * 2,
|
|
1261
|
+
height: inside2.height + GROUP_HEADER + GROUP_PADDING * 2
|
|
1262
|
+
};
|
|
1263
|
+
groupLayouts.set(id, inside2);
|
|
1264
|
+
return size;
|
|
1265
|
+
};
|
|
1266
|
+
const entityOf = (id) => {
|
|
1267
|
+
const node = nodeById.get(id);
|
|
1268
|
+
if (node !== void 0) {
|
|
1269
|
+
const text2 = textOf(node);
|
|
1270
|
+
return { id, group: false, width: text2.width, height: text2.height };
|
|
1271
|
+
}
|
|
1272
|
+
const size = measureGroup(id);
|
|
1273
|
+
return { id, group: true, width: size.width, height: size.height };
|
|
1274
|
+
};
|
|
1275
|
+
const entitiesIn = (scope) => {
|
|
1276
|
+
const found = [];
|
|
1277
|
+
if (scope === void 0) {
|
|
1278
|
+
for (const node of nodes) {
|
|
1279
|
+
const group = tree.owner.get(node.id);
|
|
1280
|
+
if (group === void 0 || !visible.has(group)) found.push(entityOf(node.id));
|
|
1281
|
+
}
|
|
1282
|
+
for (const root2 of tree.roots) if (visible.has(root2)) found.push(entityOf(root2));
|
|
1283
|
+
} else {
|
|
1284
|
+
for (const id of directNodes(scope)) found.push(entityOf(id));
|
|
1285
|
+
for (const id of directGroups(scope)) found.push(entityOf(id));
|
|
1286
|
+
}
|
|
1287
|
+
return found.sort((left, right2) => firstIndexOf(left.id) - firstIndexOf(right2.id));
|
|
1288
|
+
};
|
|
1289
|
+
const edgesIn = (scope) => {
|
|
1290
|
+
const found = [];
|
|
1291
|
+
diagram.edges.forEach((edge, index) => {
|
|
1292
|
+
if (edge.from === edge.to) return;
|
|
1293
|
+
const from = entityAt(scope, edge.from);
|
|
1294
|
+
const to = entityAt(scope, edge.to);
|
|
1295
|
+
if (from === void 0 || to === void 0 || from === to) return;
|
|
1296
|
+
found.push({ index, from, to });
|
|
1297
|
+
});
|
|
1298
|
+
return found;
|
|
1299
|
+
};
|
|
1300
|
+
const pinnedAt = (scope) => {
|
|
1301
|
+
if (diagram.init === void 0) return void 0;
|
|
1302
|
+
const entity = entityAt(scope, diagram.init);
|
|
1303
|
+
return entity === diagram.init || entity !== void 0 && groupById.has(entity) ? entity : void 0;
|
|
1304
|
+
};
|
|
1305
|
+
const layoutPiece = (entities, edges, pinned, seed) => {
|
|
1306
|
+
const byId = new Map(entities.map((entity) => [entity.id, entity]));
|
|
1307
|
+
const graph = {
|
|
1308
|
+
nodes: entities.map((entity) => ({ id: entity.id, width: entity.width, height: entity.height })),
|
|
1309
|
+
edges,
|
|
1310
|
+
...pinned === void 0 ? {} : { init: pinned }
|
|
188
1311
|
};
|
|
1312
|
+
const broken = breakCycles(graph);
|
|
1313
|
+
const layered = layer(graph, broken.edges);
|
|
1314
|
+
const augmented = addDummies(layered, broken.edges);
|
|
1315
|
+
const ordered = order(augmented.layers, augmented.chains, seed);
|
|
1316
|
+
const gap = edges.some((edge) => diagram.edges[edge.index].label !== "") ? LABELLED_LAYER_GAP : LAYER_GAP;
|
|
1317
|
+
const coordinates = assignCoordinates(
|
|
1318
|
+
{
|
|
1319
|
+
layers: ordered.layers,
|
|
1320
|
+
chains: augmented.chains,
|
|
1321
|
+
sizeOf: (id) => {
|
|
1322
|
+
const entity = byId.get(id);
|
|
1323
|
+
return { width: entity.width, height: entity.height };
|
|
1324
|
+
},
|
|
1325
|
+
chainOf: augmented.chainOf
|
|
1326
|
+
},
|
|
1327
|
+
{ gap: NODE_GAP, layerGap: gap }
|
|
1328
|
+
);
|
|
1329
|
+
const bands = planBands(coordinates.layerWidth, coordinates.layerHeight, gap);
|
|
1330
|
+
const bandOfLayer = new Array(coordinates.layerHeight.length).fill(0);
|
|
1331
|
+
bands.bands.forEach((band, index) => {
|
|
1332
|
+
for (let depth = band.first; depth <= band.last; depth += 1) bandOfLayer[depth] = index;
|
|
1333
|
+
});
|
|
1334
|
+
const layerTop = [];
|
|
1335
|
+
for (const band of bands.bands) {
|
|
1336
|
+
let y = 0;
|
|
1337
|
+
for (let depth = band.first; depth <= band.last; depth += 1) {
|
|
1338
|
+
layerTop[depth] = y;
|
|
1339
|
+
y += coordinates.layerHeight[depth] + gap;
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
const items = /* @__PURE__ */ new Map();
|
|
1343
|
+
ordered.layers.forEach((row, depth) => {
|
|
1344
|
+
const shift = bands.bands[bandOfLayer[depth]].offsetX;
|
|
1345
|
+
const top2 = layerTop[depth];
|
|
1346
|
+
const rowHeight = coordinates.layerHeight[depth];
|
|
1347
|
+
for (const id of row) {
|
|
1348
|
+
const entity = byId.get(id);
|
|
1349
|
+
if (entity === void 0) continue;
|
|
1350
|
+
const centre = coordinates.x.get(id);
|
|
1351
|
+
items.set(id, {
|
|
1352
|
+
box: {
|
|
1353
|
+
x: centre + shift - entity.width / 2,
|
|
1354
|
+
y: top2 + (rowHeight - entity.height) / 2,
|
|
1355
|
+
width: entity.width,
|
|
1356
|
+
height: entity.height
|
|
1357
|
+
},
|
|
1358
|
+
group: entity.group
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1361
|
+
});
|
|
1362
|
+
const across2 = /* @__PURE__ */ new Set();
|
|
1363
|
+
for (const edge of edges) {
|
|
1364
|
+
const from = layered.layerOf.get(edge.from);
|
|
1365
|
+
const to = layered.layerOf.get(edge.to);
|
|
1366
|
+
if (bandOfLayer[from] !== bandOfLayer[to]) across2.add(edge.index);
|
|
1367
|
+
}
|
|
1368
|
+
const laneProposal = /* @__PURE__ */ new Map();
|
|
1369
|
+
for (const chain of augmented.chains) {
|
|
1370
|
+
if (chain.path.length <= 2) continue;
|
|
1371
|
+
const lane = augmented.chainOf.get(chain.path[1]);
|
|
1372
|
+
const fromLayer = layered.layerOf.get(chain.path[0]);
|
|
1373
|
+
const proposed = coordinates.x.get(lane);
|
|
1374
|
+
laneProposal.set(lane, proposed + bands.bands[bandOfLayer[fromLayer]].offsetX);
|
|
1375
|
+
}
|
|
1376
|
+
const itemBoxes = /* @__PURE__ */ new Map();
|
|
1377
|
+
for (const [id, item] of items) itemBoxes.set(id, item.box);
|
|
1378
|
+
const channels = assignChannels(
|
|
1379
|
+
{
|
|
1380
|
+
layers: ordered.layers,
|
|
1381
|
+
chains: augmented.chains,
|
|
1382
|
+
chainOf: augmented.chainOf,
|
|
1383
|
+
boxes: itemBoxes,
|
|
1384
|
+
layerY: layerTop,
|
|
1385
|
+
layerHeight: coordinates.layerHeight,
|
|
1386
|
+
laneX: laneProposal
|
|
1387
|
+
},
|
|
1388
|
+
{ clearance: LANE_CLEARANCE, spacing: LANE_SPACING }
|
|
1389
|
+
);
|
|
1390
|
+
const lanes3 = /* @__PURE__ */ new Map();
|
|
1391
|
+
for (const chain of augmented.chains) {
|
|
1392
|
+
if (chain.path.length <= 2) continue;
|
|
1393
|
+
const start = byId.get(chain.path[0]);
|
|
1394
|
+
const end = byId.get(chain.path[chain.path.length - 1]);
|
|
1395
|
+
if (start.group || end.group) continue;
|
|
1396
|
+
const fromLayer = layered.layerOf.get(chain.path[0]);
|
|
1397
|
+
const toLayer = layered.layerOf.get(chain.path[chain.path.length - 1]);
|
|
1398
|
+
if (bandOfLayer[fromLayer] !== bandOfLayer[toLayer]) continue;
|
|
1399
|
+
const channel = channels.lanes.get(chain.index);
|
|
1400
|
+
lanes3.set(chain.index, {
|
|
1401
|
+
points: channel.y.map((y) => ({ x: channel.x, y })),
|
|
1402
|
+
reversed: broken.reversed.has(chain.index)
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1405
|
+
const runs2 = /* @__PURE__ */ new Map();
|
|
1406
|
+
for (const [edgeIndex, y] of channels.runs) if (!across2.has(edgeIndex)) runs2.set(edgeIndex, y);
|
|
1407
|
+
let right2 = 0;
|
|
1408
|
+
let bottom = 0;
|
|
1409
|
+
for (const entry of items.values()) {
|
|
1410
|
+
right2 = Math.max(right2, entry.box.x + entry.box.width);
|
|
1411
|
+
bottom = Math.max(bottom, entry.box.y + entry.box.height);
|
|
1412
|
+
}
|
|
1413
|
+
return { width: right2, height: bottom, items, lanes: lanes3, runs: runs2, across: across2 };
|
|
189
1414
|
};
|
|
1415
|
+
function buildLevel(scope) {
|
|
1416
|
+
const entities = entitiesIn(scope);
|
|
1417
|
+
if (entities.length === 0) {
|
|
1418
|
+
return { width: 0, height: 0, items: /* @__PURE__ */ new Map(), lanes: /* @__PURE__ */ new Map(), runs: /* @__PURE__ */ new Map(), across: /* @__PURE__ */ new Set() };
|
|
1419
|
+
}
|
|
1420
|
+
const edges = edgesIn(scope);
|
|
1421
|
+
const pinned = pinnedAt(scope);
|
|
1422
|
+
const seed = seedOrder({ nodes: entities.map((entity) => ({ id: entity.id, width: entity.width, height: entity.height })), edges, ...pinned === void 0 ? {} : { init: pinned } });
|
|
1423
|
+
const components = weakComponents(entities, edges);
|
|
1424
|
+
const pieces = [];
|
|
1425
|
+
for (const members of components) {
|
|
1426
|
+
const inside2 = new Set(members);
|
|
1427
|
+
const pieceEntities = entities.filter((entity) => inside2.has(entity.id));
|
|
1428
|
+
const pieceEdges = edges.filter((edge) => inside2.has(edge.from) && inside2.has(edge.to));
|
|
1429
|
+
const pin = pinned !== void 0 && inside2.has(pinned) ? pinned : void 0;
|
|
1430
|
+
pieces.push(layoutPiece(pieceEntities, pieceEdges, pin, seed));
|
|
1431
|
+
}
|
|
1432
|
+
const packed = packRectangles(pieces.map((piece) => ({ width: piece.width, height: piece.height })), { gap: COMPONENT_GAP });
|
|
1433
|
+
const layout = { width: packed.width, height: packed.height, items: /* @__PURE__ */ new Map(), lanes: /* @__PURE__ */ new Map(), runs: /* @__PURE__ */ new Map(), across: /* @__PURE__ */ new Set() };
|
|
1434
|
+
pieces.forEach((piece, index) => {
|
|
1435
|
+
for (const edgeIndex of piece.across) layout.across.add(edgeIndex);
|
|
1436
|
+
const at = packed.placements[index];
|
|
1437
|
+
for (const [id, item] of piece.items) {
|
|
1438
|
+
layout.items.set(id, { box: { x: item.box.x + at.x, y: item.box.y + at.y, width: item.box.width, height: item.box.height }, group: item.group });
|
|
1439
|
+
}
|
|
1440
|
+
for (const [edgeIndex, lane] of piece.lanes) {
|
|
1441
|
+
layout.lanes.set(edgeIndex, { points: lane.points.map((point) => ({ x: point.x + at.x, y: point.y + at.y })), reversed: lane.reversed });
|
|
1442
|
+
}
|
|
1443
|
+
for (const [edgeIndex, y] of piece.runs) layout.runs.set(edgeIndex, y + at.y);
|
|
1444
|
+
});
|
|
1445
|
+
return layout;
|
|
1446
|
+
}
|
|
1447
|
+
const root = buildLevel(void 0);
|
|
190
1448
|
const boxes = /* @__PURE__ */ new Map();
|
|
191
1449
|
const groupRects = /* @__PURE__ */ new Map();
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
1450
|
+
const lanes2 = /* @__PURE__ */ new Map();
|
|
1451
|
+
const runs = /* @__PURE__ */ new Map();
|
|
1452
|
+
const across = /* @__PURE__ */ new Set();
|
|
1453
|
+
const placeLevel = (layout, originX, originY) => {
|
|
1454
|
+
for (const edgeIndex of layout.across) across.add(edgeIndex);
|
|
1455
|
+
for (const [id, item] of layout.items) {
|
|
1456
|
+
const box = { x: originX + item.box.x, y: originY + item.box.y, width: item.box.width, height: item.box.height };
|
|
1457
|
+
if (item.group) {
|
|
1458
|
+
groupRects.set(id, box);
|
|
1459
|
+
const inside2 = groupLayouts.get(id);
|
|
1460
|
+
placeLevel(inside2, box.x + (box.width - inside2.width) / 2, box.y + GROUP_HEADER + GROUP_PADDING);
|
|
1461
|
+
continue;
|
|
1462
|
+
}
|
|
1463
|
+
boxes.set(id, box);
|
|
206
1464
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
if (!byId.has(id)) return order.get(id);
|
|
212
|
-
return Math.min(...contents(id).map(firstIndex));
|
|
1465
|
+
for (const [edgeIndex, lane] of layout.lanes) {
|
|
1466
|
+
lanes2.set(edgeIndex, { points: lane.points.map((point) => ({ x: point.x + originX, y: point.y + originY })), reversed: lane.reversed });
|
|
1467
|
+
}
|
|
1468
|
+
for (const [edgeIndex, y] of layout.runs) runs.set(edgeIndex, y + originY);
|
|
213
1469
|
};
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
1470
|
+
placeLevel(root, MARGIN, top);
|
|
1471
|
+
let right = MARGIN;
|
|
1472
|
+
for (const box of boxes.values()) right = Math.max(right, box.x + box.width);
|
|
1473
|
+
for (const box of groupRects.values()) right = Math.max(right, box.x + box.width);
|
|
1474
|
+
return { boxes, groupRects, width: right + MARGIN, lanes: lanes2, runs, across };
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
// src/render/route.ts
|
|
1478
|
+
var LANE_GAP = 10;
|
|
1479
|
+
var ROUTE_STUB = 16;
|
|
1480
|
+
function normalOf(side) {
|
|
1481
|
+
switch (side) {
|
|
1482
|
+
case "top":
|
|
1483
|
+
return { x: 0, y: -1 };
|
|
1484
|
+
case "bottom":
|
|
1485
|
+
return { x: 0, y: 1 };
|
|
1486
|
+
case "left":
|
|
1487
|
+
return { x: -1, y: 0 };
|
|
1488
|
+
case "right":
|
|
1489
|
+
return { x: 1, y: 0 };
|
|
222
1490
|
}
|
|
223
|
-
return { boxes, groupRects, width: width + MARGIN * 2 };
|
|
224
1491
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
1492
|
+
var BEND_COST = 24;
|
|
1493
|
+
function crossesBox(x1, y1, x2, y2, box) {
|
|
1494
|
+
const minX = box.x + 0.5;
|
|
1495
|
+
const maxX = box.x + box.width - 0.5;
|
|
1496
|
+
const minY = box.y + 0.5;
|
|
1497
|
+
const maxY = box.y + box.height - 0.5;
|
|
1498
|
+
if (Math.abs(y1 - y2) < 1e-3) {
|
|
1499
|
+
const y = y1;
|
|
1500
|
+
if (y <= minY || y >= maxY) return false;
|
|
1501
|
+
return Math.max(x1, x2) > minX && Math.min(x1, x2) < maxX;
|
|
1502
|
+
}
|
|
1503
|
+
if (Math.abs(x1 - x2) < 1e-3) {
|
|
1504
|
+
const x = x1;
|
|
1505
|
+
if (x <= minX || x >= maxX) return false;
|
|
1506
|
+
return Math.max(y1, y2) > minY && Math.min(y1, y2) < maxY;
|
|
1507
|
+
}
|
|
1508
|
+
const steps = Math.max(8, Math.round(Math.hypot(x2 - x1, y2 - y1)));
|
|
1509
|
+
for (let index = 0; index <= steps; index += 1) {
|
|
1510
|
+
const x = x1 + (x2 - x1) * index / steps;
|
|
1511
|
+
const y = y1 + (y2 - y1) * index / steps;
|
|
1512
|
+
if (x > minX && x < maxX && y > minY && y < maxY) return true;
|
|
1513
|
+
}
|
|
1514
|
+
return false;
|
|
1515
|
+
}
|
|
1516
|
+
function anchor(box, side) {
|
|
1517
|
+
switch (side) {
|
|
1518
|
+
case "top":
|
|
1519
|
+
return { x: box.x + box.width / 2, y: box.y };
|
|
1520
|
+
case "bottom":
|
|
1521
|
+
return { x: box.x + box.width / 2, y: box.y + box.height };
|
|
1522
|
+
case "left":
|
|
1523
|
+
return { x: box.x, y: box.y + box.height / 2 };
|
|
1524
|
+
case "right":
|
|
1525
|
+
return { x: box.x + box.width, y: box.y + box.height / 2 };
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
function approachSides(from, to) {
|
|
1529
|
+
const fromCy = from.y + from.height / 2;
|
|
1530
|
+
const toCy = to.y + to.height / 2;
|
|
1531
|
+
if (Math.abs(toCy - fromCy) > 0.5) return toCy > fromCy ? { exit: "bottom", entry: "top" } : { exit: "top", entry: "bottom" };
|
|
1532
|
+
const fromCx = from.x + from.width / 2;
|
|
1533
|
+
const toCx = to.x + to.width / 2;
|
|
1534
|
+
return toCx >= fromCx ? { exit: "right", entry: "left" } : { exit: "left", entry: "right" };
|
|
1535
|
+
}
|
|
1536
|
+
function lanes(obstacles, extra) {
|
|
1537
|
+
const values = new Set(extra);
|
|
1538
|
+
for (const box of obstacles) {
|
|
1539
|
+
values.add(box.x - LANE_GAP);
|
|
1540
|
+
values.add(box.x + box.width + LANE_GAP);
|
|
1541
|
+
values.add(box.y - LANE_GAP);
|
|
1542
|
+
values.add(box.y + box.height + LANE_GAP);
|
|
1543
|
+
}
|
|
1544
|
+
return [...values].sort((left, right) => left - right);
|
|
1545
|
+
}
|
|
1546
|
+
function facesAway(side, port, other) {
|
|
1547
|
+
const normal = normalOf(side);
|
|
1548
|
+
return normal.x * (other.x + other.width / 2 - port.x) + normal.y * (other.y + other.height / 2 - port.y) < 0;
|
|
1549
|
+
}
|
|
1550
|
+
function routeEdge(from, to, obstacles, anchors) {
|
|
1551
|
+
const sides = approachSides(from, to);
|
|
1552
|
+
const exitSide = anchors?.exitSide ?? sides.exit;
|
|
1553
|
+
const entrySide = anchors?.entrySide ?? sides.entry;
|
|
1554
|
+
const exit = anchors?.exit ?? anchor(from, exitSide);
|
|
1555
|
+
const entry = anchors?.entry ?? anchor(to, entrySide);
|
|
1556
|
+
const others = obstacles.filter((box) => box !== from && box !== to);
|
|
1557
|
+
const own = [];
|
|
1558
|
+
if (facesAway(exitSide, exit, to)) own.push(from);
|
|
1559
|
+
if (facesAway(entrySide, entry, from)) own.push(to);
|
|
1560
|
+
const blockers = own.length === 0 ? others : [...others, ...own];
|
|
1561
|
+
const blockedStub = (point) => blockers.some((box) => point.x > box.x && point.x < box.x + box.width && point.y > box.y && point.y < box.y + box.height);
|
|
1562
|
+
const outNormal = normalOf(exitSide);
|
|
1563
|
+
const inNormal = normalOf(entrySide);
|
|
1564
|
+
let exitStub = { x: exit.x + outNormal.x * ROUTE_STUB, y: exit.y + outNormal.y * ROUTE_STUB };
|
|
1565
|
+
let entryStub = { x: entry.x + inNormal.x * ROUTE_STUB, y: entry.y + inNormal.y * ROUTE_STUB };
|
|
1566
|
+
if (blockedStub(exitStub) || blockedStub(entryStub)) {
|
|
1567
|
+
exitStub = exit;
|
|
1568
|
+
entryStub = entry;
|
|
1569
|
+
}
|
|
1570
|
+
const straight = [exit, exitStub, entryStub, entry];
|
|
1571
|
+
const axisAligned = Math.abs(exitStub.x - entryStub.x) < 0.5 || Math.abs(exitStub.y - entryStub.y) < 0.5;
|
|
1572
|
+
if (axisAligned && !blockers.some((box) => crossesBox(exitStub.x, exitStub.y, entryStub.x, entryStub.y, box))) {
|
|
1573
|
+
return { points: simplify(straight), routed: false };
|
|
1574
|
+
}
|
|
1575
|
+
const xs = lanes(blockers, [exitStub.x, entryStub.x]);
|
|
1576
|
+
const ys = lanes(blockers, [exitStub.y, entryStub.y]);
|
|
1577
|
+
const blocked = (x1, y1, x2, y2) => blockers.some((box) => crossesBox(x1, y1, x2, y2, box));
|
|
1578
|
+
const start = { point: exitStub, axis: -1, cost: 0, bends: 0, path: exitStub === exit ? [exit] : [exit, exitStub] };
|
|
1579
|
+
const queue = [start];
|
|
1580
|
+
const best = /* @__PURE__ */ new Map();
|
|
1581
|
+
const key = (point) => String(Math.round(point.x)) + "," + String(Math.round(point.y));
|
|
1582
|
+
while (queue.length > 0) {
|
|
1583
|
+
let index = 0;
|
|
1584
|
+
for (let cursor = 1; cursor < queue.length; cursor += 1) if (queue[cursor].cost < queue[index].cost) index = cursor;
|
|
1585
|
+
const current = queue.splice(index, 1)[0];
|
|
1586
|
+
if (current.cost > (best.get(key(current.point)) ?? Infinity)) continue;
|
|
1587
|
+
if (Math.abs(current.point.x - entryStub.x) < 0.5 && Math.abs(current.point.y - entryStub.y) < 0.5) {
|
|
1588
|
+
return { points: simplify([...current.path, entryStub, entry]), routed: true };
|
|
1589
|
+
}
|
|
1590
|
+
const options = [];
|
|
1591
|
+
for (const x of xs) if (Math.abs(x - current.point.x) > 0.5) options.push({ x, y: current.point.y });
|
|
1592
|
+
for (const y of ys) if (Math.abs(y - current.point.y) > 0.5) options.push({ x: current.point.x, y });
|
|
1593
|
+
for (const next of options) {
|
|
1594
|
+
if (blocked(current.point.x, current.point.y, next.x, next.y)) continue;
|
|
1595
|
+
const axis = Math.abs(next.y - current.point.y) < 0.5 ? 0 : 1;
|
|
1596
|
+
const bend = current.axis === -1 || current.axis === axis ? 0 : 1;
|
|
1597
|
+
const cost = current.cost + Math.abs(next.x - current.point.x) + Math.abs(next.y - current.point.y) + bend * BEND_COST;
|
|
1598
|
+
const state = key(next);
|
|
1599
|
+
if (cost >= (best.get(state) ?? Infinity)) continue;
|
|
1600
|
+
best.set(state, cost);
|
|
1601
|
+
queue.push({ point: next, axis, cost, bends: current.bends + bend, path: [...current.path, next] });
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
return { points: simplify(straight), routed: false };
|
|
1605
|
+
}
|
|
1606
|
+
function simplify(points) {
|
|
1607
|
+
if (points.length <= 2) return points;
|
|
1608
|
+
const out = [points[0]];
|
|
1609
|
+
for (let index = 1; index < points.length - 1; index += 1) {
|
|
1610
|
+
const previous = out[out.length - 1];
|
|
1611
|
+
const current = points[index];
|
|
1612
|
+
const next = points[index + 1];
|
|
1613
|
+
const collinear = Math.abs(previous.x - current.x) < 0.5 && Math.abs(current.x - next.x) < 0.5 || Math.abs(previous.y - current.y) < 0.5 && Math.abs(current.y - next.y) < 0.5;
|
|
1614
|
+
if (!collinear) out.push(current);
|
|
1615
|
+
}
|
|
1616
|
+
out.push(points[points.length - 1]);
|
|
1617
|
+
return out;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
// src/render/symbol.ts
|
|
1621
|
+
function cubicPoint(curve, t) {
|
|
1622
|
+
const u = 1 - t;
|
|
1623
|
+
const a = u * u * u;
|
|
1624
|
+
const b = 3 * u * u * t;
|
|
1625
|
+
const c = 3 * u * t * t;
|
|
1626
|
+
const d = t * t * t;
|
|
1627
|
+
return {
|
|
1628
|
+
x: a * curve.from.x + b * curve.c1.x + c * curve.c2.x + d * curve.to.x,
|
|
1629
|
+
y: a * curve.from.y + b * curve.c1.y + c * curve.c2.y + d * curve.to.y
|
|
1630
|
+
};
|
|
1631
|
+
}
|
|
1632
|
+
function diamondPoints(box) {
|
|
1633
|
+
return [
|
|
1634
|
+
{ x: box.x + box.width / 2, y: box.y },
|
|
1635
|
+
{ x: box.x + box.width, y: box.y + box.height / 2 },
|
|
1636
|
+
{ x: box.x + box.width / 2, y: box.y + box.height },
|
|
1637
|
+
{ x: box.x, y: box.y + box.height / 2 }
|
|
1638
|
+
];
|
|
1639
|
+
}
|
|
1640
|
+
function hasDiscretePorts(shape) {
|
|
1641
|
+
return shape === "diamond" || shape === "circle";
|
|
1642
|
+
}
|
|
1643
|
+
function hexagonCut(width) {
|
|
1644
|
+
return Math.min(12, width / 4);
|
|
1645
|
+
}
|
|
1646
|
+
function hexagonPoints(box) {
|
|
1647
|
+
const cut2 = hexagonCut(box.width);
|
|
1648
|
+
return [
|
|
1649
|
+
{ x: box.x + cut2, y: box.y },
|
|
1650
|
+
{ x: box.x + box.width - cut2, y: box.y },
|
|
1651
|
+
{ x: box.x + box.width, y: box.y + box.height / 2 },
|
|
1652
|
+
{ x: box.x + box.width - cut2, y: box.y + box.height },
|
|
1653
|
+
{ x: box.x + cut2, y: box.y + box.height },
|
|
1654
|
+
{ x: box.x, y: box.y + box.height / 2 }
|
|
1655
|
+
];
|
|
1656
|
+
}
|
|
1657
|
+
function stadiumRadius(box) {
|
|
1658
|
+
return Math.min(box.height / 2, box.width / 2);
|
|
1659
|
+
}
|
|
1660
|
+
function cylinderCaps(box) {
|
|
1661
|
+
const cap = Math.min(10, box.height / 3);
|
|
1662
|
+
return {
|
|
1663
|
+
cap,
|
|
1664
|
+
top: {
|
|
1665
|
+
from: { x: box.x, y: box.y + cap },
|
|
1666
|
+
c1: { x: box.x, y: box.y },
|
|
1667
|
+
c2: { x: box.x + box.width, y: box.y },
|
|
1668
|
+
to: { x: box.x + box.width, y: box.y + cap }
|
|
1669
|
+
},
|
|
1670
|
+
bottom: {
|
|
1671
|
+
from: { x: box.x + box.width, y: box.y + box.height - cap },
|
|
1672
|
+
c1: { x: box.x + box.width, y: box.y + box.height },
|
|
1673
|
+
c2: { x: box.x, y: box.y + box.height },
|
|
1674
|
+
to: { x: box.x, y: box.y + box.height - cap }
|
|
1675
|
+
}
|
|
1676
|
+
};
|
|
1677
|
+
}
|
|
1678
|
+
function cylinderApex(box, side) {
|
|
1679
|
+
const caps = cylinderCaps(box);
|
|
1680
|
+
return cubicPoint(side === "top" ? caps.top : caps.bottom, 0.5);
|
|
228
1681
|
}
|
|
229
1682
|
|
|
230
1683
|
// src/render/palette.ts
|
|
@@ -327,12 +1780,28 @@ window.__ModuleLoader__.load({
|
|
|
327
1780
|
|
|
328
1781
|
// src/render/paint.ts
|
|
329
1782
|
var TITLE_HEIGHT = 26;
|
|
330
|
-
var NODE_HEIGHT2 = 34;
|
|
331
|
-
var CHANNEL_GAP = 16;
|
|
332
1783
|
var CAPTION_CHARS = 78;
|
|
333
1784
|
var CAPTION_LINE = 15;
|
|
334
1785
|
var CAPTION_GAP = 16;
|
|
335
1786
|
var CAPTION_CHAR_WIDTH = 6.2;
|
|
1787
|
+
var PORT_SEPARATION = 14;
|
|
1788
|
+
var PORT_SIDES = ["top", "right", "bottom", "left"];
|
|
1789
|
+
var isVerticalSide = (side) => side === "top" || side === "bottom";
|
|
1790
|
+
var MULTI_EDGE_SPACING = 20;
|
|
1791
|
+
function packPorts(wanted, low, high) {
|
|
1792
|
+
const placed = [];
|
|
1793
|
+
let previous = low - PORT_SEPARATION;
|
|
1794
|
+
for (const value of wanted) {
|
|
1795
|
+
previous = Math.min(high, Math.max(value, previous + PORT_SEPARATION));
|
|
1796
|
+
placed.push(previous);
|
|
1797
|
+
}
|
|
1798
|
+
let next = high + PORT_SEPARATION;
|
|
1799
|
+
for (let index = placed.length - 1; index >= 0; index -= 1) {
|
|
1800
|
+
next = Math.max(low, Math.min(placed[index], next - PORT_SEPARATION));
|
|
1801
|
+
placed[index] = next;
|
|
1802
|
+
}
|
|
1803
|
+
return placed;
|
|
1804
|
+
}
|
|
336
1805
|
function roleOfNode(node) {
|
|
337
1806
|
if (node.role === "error") return "fail";
|
|
338
1807
|
if (node.role === "warning") return "findings";
|
|
@@ -363,6 +1832,76 @@ window.__ModuleLoader__.load({
|
|
|
363
1832
|
function captionRole(caption) {
|
|
364
1833
|
return roleForVerdict(caption.split(" \u2014 ")[0]);
|
|
365
1834
|
}
|
|
1835
|
+
var LABEL_HEIGHT = 12;
|
|
1836
|
+
var LABEL_PAD = 2;
|
|
1837
|
+
var LABEL_STEP = 3;
|
|
1838
|
+
function placeLabel(label, from, to, boxes, placed, segments) {
|
|
1839
|
+
const width = textWidth(label);
|
|
1840
|
+
const height = LABEL_HEIGHT;
|
|
1841
|
+
const spotAt = (x, y) => ({ x, y, width, height });
|
|
1842
|
+
const hitsBox = (spot, box) => spot.x - spot.width / 2 < box.x + box.width + LABEL_PAD && spot.x + spot.width / 2 > box.x - LABEL_PAD && spot.y - spot.height / 2 < box.y + box.height + LABEL_PAD && spot.y + spot.height / 2 > box.y - LABEL_PAD;
|
|
1843
|
+
const hitsSpot = (spot, other) => spot.x - spot.width / 2 < other.x + other.width / 2 + LABEL_PAD && spot.x + spot.width / 2 > other.x - other.width / 2 - LABEL_PAD && spot.y - spot.height / 2 < other.y + other.height / 2 + LABEL_PAD && spot.y + spot.height / 2 > other.y - other.height / 2 - LABEL_PAD;
|
|
1844
|
+
const hitsSegment = (spot, segment) => spot.x + spot.width / 2 > Math.min(segment.x1, segment.x2) - LABEL_PAD && spot.x - spot.width / 2 < Math.max(segment.x1, segment.x2) + LABEL_PAD && spot.y + spot.height / 2 > Math.min(segment.y1, segment.y2) - LABEL_PAD && spot.y - spot.height / 2 < Math.max(segment.y1, segment.y2) + LABEL_PAD;
|
|
1845
|
+
const clearOfBoxes = (spot) => {
|
|
1846
|
+
for (const box of boxes) if (hitsBox(spot, box)) return false;
|
|
1847
|
+
for (const earlier of placed) if (hitsSpot(spot, earlier)) return false;
|
|
1848
|
+
return true;
|
|
1849
|
+
};
|
|
1850
|
+
const clearOfEdges = (spot) => {
|
|
1851
|
+
for (const segment of segments) if (hitsSegment(spot, segment)) return false;
|
|
1852
|
+
return true;
|
|
1853
|
+
};
|
|
1854
|
+
const middle = { x: (from.x + to.x) / 2, y: (from.y + to.y) / 2 };
|
|
1855
|
+
const dx = to.x - from.x;
|
|
1856
|
+
const dy = to.y - from.y;
|
|
1857
|
+
const length = Math.abs(dx) + Math.abs(dy);
|
|
1858
|
+
const ux = dx / length;
|
|
1859
|
+
const uy = dy / length;
|
|
1860
|
+
const along = Math.abs(ux) * width / 2 + Math.abs(uy) * height / 2;
|
|
1861
|
+
const bound = Math.max(0, length / 2 - along);
|
|
1862
|
+
const walk = [0];
|
|
1863
|
+
for (let step = LABEL_STEP; step <= bound + 1e-3; step += LABEL_STEP) walk.push(step, -step);
|
|
1864
|
+
const at = (offset) => spotAt(middle.x + ux * offset, middle.y + uy * offset);
|
|
1865
|
+
for (const offset of walk) {
|
|
1866
|
+
const spot = at(offset);
|
|
1867
|
+
if (clearOfBoxes(spot) && clearOfEdges(spot)) return spot;
|
|
1868
|
+
}
|
|
1869
|
+
for (const offset of walk) {
|
|
1870
|
+
const spot = at(offset);
|
|
1871
|
+
if (clearOfBoxes(spot)) return spot;
|
|
1872
|
+
}
|
|
1873
|
+
return at(0);
|
|
1874
|
+
}
|
|
1875
|
+
function drawnExtent(shapes) {
|
|
1876
|
+
let width = 0;
|
|
1877
|
+
let height = 0;
|
|
1878
|
+
const consider = (x, y) => {
|
|
1879
|
+
if (x > width) width = x;
|
|
1880
|
+
if (y > height) height = y;
|
|
1881
|
+
};
|
|
1882
|
+
for (const shape of shapes) {
|
|
1883
|
+
switch (shape.kind) {
|
|
1884
|
+
case "box":
|
|
1885
|
+
case "group":
|
|
1886
|
+
consider(shape.x + shape.width, shape.y + shape.height);
|
|
1887
|
+
break;
|
|
1888
|
+
case "link":
|
|
1889
|
+
consider(Math.max(shape.x1, shape.x2), Math.max(shape.y1, shape.y2));
|
|
1890
|
+
if (shape.label !== "") consider(shape.labelX + textWidth(shape.label) / 2, shape.labelY + LABEL_HEIGHT / 2);
|
|
1891
|
+
break;
|
|
1892
|
+
case "path": {
|
|
1893
|
+
for (const point of shape.points) consider(point.x, point.y);
|
|
1894
|
+
if (shape.label !== "") consider(shape.labelX + textWidth(shape.label) / 2, shape.labelY + LABEL_HEIGHT / 2);
|
|
1895
|
+
break;
|
|
1896
|
+
}
|
|
1897
|
+
case "caption":
|
|
1898
|
+
case "title":
|
|
1899
|
+
consider(textWidth(shape.lines?.[0] ?? "") + shape.x, shape.y + 12);
|
|
1900
|
+
break;
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
return { width, height };
|
|
1904
|
+
}
|
|
366
1905
|
function paintWith(diagram, overrides = {}, options = {}) {
|
|
367
1906
|
const colours = palette(overrides, options);
|
|
368
1907
|
const shapes = [];
|
|
@@ -370,16 +1909,23 @@ window.__ModuleLoader__.load({
|
|
|
370
1909
|
const topOffset = MARGIN + TITLE_HEIGHT;
|
|
371
1910
|
const placement = place(diagram, topOffset);
|
|
372
1911
|
const { boxes, groupRects } = placement;
|
|
1912
|
+
const endBox = (id) => boxes.get(id) ?? groupRects.get(id);
|
|
1913
|
+
const shapeById = /* @__PURE__ */ new Map();
|
|
1914
|
+
for (const node of diagram.nodes) if (node.shape !== void 0 && !shapeById.has(node.id)) shapeById.set(node.id, node.shape);
|
|
1915
|
+
const shapeOf = (id) => shapeById.get(id) ?? "rect";
|
|
373
1916
|
for (const group of groups) {
|
|
374
1917
|
const rect = groupRects.get(group.id);
|
|
375
1918
|
if (rect === void 0) continue;
|
|
376
1919
|
shapes.push({ kind: "group", id: group.id, x: rect.x, y: rect.y, width: rect.width, height: rect.height, label: group.label, colour: "muted" });
|
|
377
1920
|
}
|
|
378
1921
|
const drawn = /* @__PURE__ */ new Set();
|
|
1922
|
+
const boxList = Array.from(boxes.values());
|
|
1923
|
+
const routes = [];
|
|
379
1924
|
for (const node of diagram.nodes) {
|
|
380
1925
|
if (drawn.has(node.id)) continue;
|
|
381
1926
|
const box = boxes.get(node.id);
|
|
382
1927
|
drawn.add(node.id);
|
|
1928
|
+
const text2 = textOf(node);
|
|
383
1929
|
shapes.push({
|
|
384
1930
|
kind: "box",
|
|
385
1931
|
x: box.x,
|
|
@@ -387,19 +1933,370 @@ window.__ModuleLoader__.load({
|
|
|
387
1933
|
width: box.width,
|
|
388
1934
|
height: box.height,
|
|
389
1935
|
label: labelOf(node),
|
|
1936
|
+
lines: text2.lines,
|
|
1937
|
+
...node.shape === void 0 ? {} : { shape: node.shape },
|
|
390
1938
|
colour: roleOfNode(node),
|
|
391
1939
|
initial: diagram.init === node.id
|
|
392
1940
|
});
|
|
393
1941
|
}
|
|
394
1942
|
const roleById = new Map(diagram.nodes.map((node) => [node.id, roleOfNode(node)]));
|
|
395
|
-
|
|
396
|
-
|
|
1943
|
+
const multiEdgeSets = /* @__PURE__ */ new Map();
|
|
1944
|
+
diagram.edges.forEach((edge, index) => {
|
|
1945
|
+
if (edge.from === edge.to) return;
|
|
1946
|
+
if (endBox(edge.from) === void 0 || endBox(edge.to) === void 0) return;
|
|
1947
|
+
const key = edge.from < edge.to ? edge.from + "\0" + edge.to : edge.to + "\0" + edge.from;
|
|
1948
|
+
const list = multiEdgeSets.get(key);
|
|
1949
|
+
if (list === void 0) multiEdgeSets.set(key, [index]);
|
|
1950
|
+
else list.push(index);
|
|
1951
|
+
});
|
|
1952
|
+
const fanOf = /* @__PURE__ */ new Map();
|
|
1953
|
+
for (const list of multiEdgeSets.values()) {
|
|
1954
|
+
if (list.length < 2) continue;
|
|
1955
|
+
list.forEach((index, position) => {
|
|
1956
|
+
if (position === 0) return;
|
|
1957
|
+
fanOf.set(index, (position % 2 === 1 ? 1 : -1) * Math.ceil(position / 2) * MULTI_EDGE_SPACING);
|
|
1958
|
+
});
|
|
1959
|
+
}
|
|
1960
|
+
const endKey = (index, end) => String(index) + ":" + end;
|
|
1961
|
+
const endsAt = /* @__PURE__ */ new Map();
|
|
397
1962
|
for (const edge of diagram.edges) {
|
|
398
|
-
|
|
399
|
-
|
|
1963
|
+
if (edge.from === edge.to) continue;
|
|
1964
|
+
if (endBox(edge.from) === void 0 || endBox(edge.to) === void 0) continue;
|
|
1965
|
+
endsAt.set(edge.from, (endsAt.get(edge.from) ?? 0) + 1);
|
|
1966
|
+
endsAt.set(edge.to, (endsAt.get(edge.to) ?? 0) + 1);
|
|
1967
|
+
}
|
|
1968
|
+
const claimed = /* @__PURE__ */ new Map();
|
|
1969
|
+
const claimedAt = /* @__PURE__ */ new Map();
|
|
1970
|
+
const claim = (id, index, end, side) => {
|
|
1971
|
+
claimed.set(endKey(index, end), side);
|
|
1972
|
+
claimedAt.set(id, [...claimedAt.get(id) ?? [], side]);
|
|
1973
|
+
};
|
|
1974
|
+
const bestSide = (id, other) => {
|
|
1975
|
+
const box = endBox(id);
|
|
1976
|
+
const centre = { x: box.x + box.width / 2, y: box.y + box.height / 2 };
|
|
1977
|
+
const toward = { x: other.x + other.width / 2 - centre.x, y: other.y + other.height / 2 - centre.y };
|
|
1978
|
+
const reach = Math.hypot(toward.x, toward.y);
|
|
1979
|
+
const taken = claimedAt.get(id) ?? [];
|
|
1980
|
+
let best;
|
|
1981
|
+
for (const side of PORT_SIDES) {
|
|
1982
|
+
if (taken.includes(side)) continue;
|
|
1983
|
+
const normal = normalOf(side);
|
|
1984
|
+
const score = reach === 0 ? 0 : (normal.x * toward.x + normal.y * toward.y) / reach;
|
|
1985
|
+
const point = anchor(box, side);
|
|
1986
|
+
const near = Math.hypot(
|
|
1987
|
+
Math.max(other.x - point.x, 0, point.x - (other.x + other.width)),
|
|
1988
|
+
Math.max(other.y - point.y, 0, point.y - (other.y + other.height))
|
|
1989
|
+
);
|
|
1990
|
+
const better = best === void 0 || score > best.score + 1e-9 || Math.abs(score - best.score) <= 1e-9 && near < best.near - 1e-9;
|
|
1991
|
+
if (better) best = { side, score, near };
|
|
1992
|
+
}
|
|
1993
|
+
return best?.side;
|
|
1994
|
+
};
|
|
1995
|
+
diagram.edges.forEach((edge, index) => {
|
|
1996
|
+
if (edge.from === edge.to) return;
|
|
1997
|
+
const source = endBox(edge.from);
|
|
1998
|
+
const target = endBox(edge.to);
|
|
1999
|
+
if (source === void 0 || target === void 0) return;
|
|
2000
|
+
if (placement.across.has(index)) {
|
|
2001
|
+
claim(edge.from, index, "exit", "right");
|
|
2002
|
+
claim(edge.to, index, "entry", "left");
|
|
2003
|
+
return;
|
|
2004
|
+
}
|
|
2005
|
+
const lane = placement.lanes.get(index);
|
|
2006
|
+
if (lane === void 0 || lane.points.length === 0) return;
|
|
2007
|
+
const down = target.y > source.y;
|
|
2008
|
+
claim(edge.from, index, "exit", down ? "bottom" : "top");
|
|
2009
|
+
claim(edge.to, index, "entry", down ? "top" : "bottom");
|
|
2010
|
+
});
|
|
2011
|
+
diagram.edges.forEach((edge, index) => {
|
|
2012
|
+
if (edge.from === edge.to) return;
|
|
2013
|
+
const source = endBox(edge.from);
|
|
2014
|
+
const target = endBox(edge.to);
|
|
2015
|
+
if (source === void 0 || target === void 0) return;
|
|
2016
|
+
for (const end of ["exit", "entry"]) {
|
|
2017
|
+
if (claimed.has(endKey(index, end))) continue;
|
|
2018
|
+
const id = end === "exit" ? edge.from : edge.to;
|
|
2019
|
+
if ((endsAt.get(id) ?? 0) < 2) continue;
|
|
2020
|
+
if (!hasDiscretePorts(shapeOf(id))) continue;
|
|
2021
|
+
const side = bestSide(id, end === "exit" ? target : source);
|
|
2022
|
+
if (side === void 0) continue;
|
|
2023
|
+
claim(id, index, end, side);
|
|
2024
|
+
}
|
|
2025
|
+
});
|
|
2026
|
+
const facing = diagram.edges.map((edge, index) => {
|
|
2027
|
+
const source = endBox(edge.from);
|
|
2028
|
+
const target = endBox(edge.to);
|
|
2029
|
+
if (source === void 0 || target === void 0 || edge.from === edge.to) return void 0;
|
|
2030
|
+
const sides2 = placement.across.has(index) ? { exit: "right", entry: "left" } : approachSides(source, target);
|
|
2031
|
+
const lane = placement.lanes.get(index);
|
|
2032
|
+
if (lane === void 0 || lane.points.length === 0) {
|
|
2033
|
+
return {
|
|
2034
|
+
exit: claimed.get(endKey(index, "exit")) ?? sides2.exit,
|
|
2035
|
+
entry: claimed.get(endKey(index, "entry")) ?? sides2.entry
|
|
2036
|
+
};
|
|
2037
|
+
}
|
|
2038
|
+
const down = target.y > source.y;
|
|
2039
|
+
return {
|
|
2040
|
+
exit: claimed.get(endKey(index, "exit")) ?? (down ? "bottom" : "top"),
|
|
2041
|
+
entry: claimed.get(endKey(index, "entry")) ?? (down ? "top" : "bottom"),
|
|
2042
|
+
at: lane.points[0].x
|
|
2043
|
+
};
|
|
2044
|
+
});
|
|
2045
|
+
const laneClear = (points, x, from, to) => {
|
|
2046
|
+
for (let at = 0; at + 1 < points.length; at += 1) {
|
|
2047
|
+
for (const box of boxList) {
|
|
2048
|
+
if (box === from || box === to) continue;
|
|
2049
|
+
if (crossesBox(x, points[at].y, x, points[at + 1].y, box)) return false;
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
return true;
|
|
2053
|
+
};
|
|
2054
|
+
const laneShift = /* @__PURE__ */ new Map();
|
|
2055
|
+
for (const list of multiEdgeSets.values()) {
|
|
2056
|
+
if (list.length < 2) continue;
|
|
2057
|
+
const lead = placement.lanes.get(list[0]);
|
|
2058
|
+
if (lead === void 0 || lead.points.length === 0) continue;
|
|
2059
|
+
const base = lead.points[0].x;
|
|
2060
|
+
const taken = /* @__PURE__ */ new Set();
|
|
2061
|
+
for (const index of list) {
|
|
2062
|
+
const lane = placement.lanes.get(index);
|
|
2063
|
+
const own = lane.points[0].x;
|
|
2064
|
+
const offset = fanOf.get(index) ?? 0;
|
|
2065
|
+
const edge = diagram.edges[index];
|
|
2066
|
+
const from = endBox(edge.from);
|
|
2067
|
+
const to = endBox(edge.to);
|
|
2068
|
+
let target = own;
|
|
2069
|
+
if (offset !== 0) {
|
|
2070
|
+
for (const candidate of [base + offset, own + offset]) {
|
|
2071
|
+
if (taken.has(candidate) || !laneClear(lane.points, candidate, from, to)) continue;
|
|
2072
|
+
target = candidate;
|
|
2073
|
+
break;
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
taken.add(target);
|
|
2077
|
+
if (target !== own) laneShift.set(index, target - own);
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
const wanted = /* @__PURE__ */ new Map();
|
|
2081
|
+
const slotKey = (boxId, side, index) => boxId + ":" + side + ":" + String(index);
|
|
2082
|
+
const spanOf2 = (boxId, side, corner) => {
|
|
2083
|
+
const box = endBox(boxId);
|
|
2084
|
+
const vertical = side === "top" || side === "bottom";
|
|
2085
|
+
const middle = vertical ? box.x + box.width / 2 : box.y + box.height / 2;
|
|
2086
|
+
const within = (low, high) => ({ low, high: Math.max(low, high) });
|
|
2087
|
+
switch (shapeOf(boxId)) {
|
|
2088
|
+
// The four vertices of a diamond, and the four cardinal points of an ellipse, are the middles of its sides.
|
|
2089
|
+
case "diamond":
|
|
2090
|
+
case "circle":
|
|
2091
|
+
return within(middle, middle);
|
|
2092
|
+
case "hexagon":
|
|
2093
|
+
return vertical ? within(box.x + hexagonCut(box.width), box.x + box.width - hexagonCut(box.width)) : within(middle, middle);
|
|
2094
|
+
case "stadium": {
|
|
2095
|
+
const radius = stadiumRadius(box);
|
|
2096
|
+
return vertical ? within(box.x + radius, box.x + box.width - radius) : within(middle, middle);
|
|
2097
|
+
}
|
|
2098
|
+
case "cylinder": {
|
|
2099
|
+
if (vertical) return within(middle, middle);
|
|
2100
|
+
const cap = cylinderCaps(box).cap;
|
|
2101
|
+
return within(box.y + cap, box.y + box.height - cap);
|
|
2102
|
+
}
|
|
2103
|
+
default: {
|
|
2104
|
+
const inset = corner ? 2 : vertical ? PORT_INSET : 8;
|
|
2105
|
+
return vertical ? within(box.x + inset, box.x + box.width - inset) : within(box.y + inset, box.y + box.height - inset);
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
};
|
|
2109
|
+
const clampTo = (boxId, side, value, corner) => {
|
|
2110
|
+
const span = spanOf2(boxId, side, corner);
|
|
2111
|
+
return Math.min(span.high, Math.max(span.low, value));
|
|
2112
|
+
};
|
|
2113
|
+
const pointOn = (boxId, side, along) => {
|
|
2114
|
+
const box = endBox(boxId);
|
|
2115
|
+
if (side === "left") return { x: box.x, y: along };
|
|
2116
|
+
if (side === "right") return { x: box.x + box.width, y: along };
|
|
2117
|
+
if (side === "top") return { x: along, y: shapeOf(boxId) === "cylinder" ? cylinderApex(box, "top").y : box.y };
|
|
2118
|
+
return { x: along, y: shapeOf(boxId) === "cylinder" ? cylinderApex(box, "bottom").y : box.y + box.height };
|
|
2119
|
+
};
|
|
2120
|
+
for (const [index, entry] of facing.entries()) {
|
|
2121
|
+
if (entry === void 0) continue;
|
|
2122
|
+
const edge = diagram.edges[index];
|
|
2123
|
+
const from = endBox(edge.from);
|
|
2124
|
+
const to = endBox(edge.to);
|
|
2125
|
+
const middle = (box, side) => isVerticalSide(side) ? box.x + box.width / 2 : box.y + box.height / 2;
|
|
2126
|
+
const fromMiddle = middle(from, entry.exit);
|
|
2127
|
+
const toMiddle = middle(to, entry.entry);
|
|
2128
|
+
const corner = entry.at !== void 0;
|
|
2129
|
+
const offset = fanOf.get(index) ?? 0;
|
|
2130
|
+
let exitWanted;
|
|
2131
|
+
let entryWanted;
|
|
2132
|
+
if (entry.at !== void 0) {
|
|
2133
|
+
const chosen = entry.at + (laneShift.get(index) ?? 0);
|
|
2134
|
+
exitWanted = clampTo(edge.from, entry.exit, chosen, corner);
|
|
2135
|
+
entryWanted = clampTo(edge.to, entry.entry, chosen, corner);
|
|
2136
|
+
} else if (isVerticalSide(entry.exit) === isVerticalSide(entry.entry)) {
|
|
2137
|
+
const target = spanOf2(edge.to, entry.entry, false);
|
|
2138
|
+
const chosen = (fromMiddle >= target.low && fromMiddle <= target.high ? fromMiddle : clampTo(edge.from, entry.exit, toMiddle, false)) + offset;
|
|
2139
|
+
exitWanted = clampTo(edge.from, entry.exit, chosen, corner);
|
|
2140
|
+
entryWanted = clampTo(edge.to, entry.entry, chosen, corner);
|
|
2141
|
+
} else {
|
|
2142
|
+
exitWanted = hasDiscretePorts(shapeOf(edge.from)) ? fromMiddle : clampTo(edge.from, entry.exit, anchor(to, entry.entry).x + normalOf(entry.entry).x * ROUTE_STUB, false) + offset;
|
|
2143
|
+
entryWanted = hasDiscretePorts(shapeOf(edge.to)) ? toMiddle : clampTo(edge.to, entry.entry, anchor(from, entry.exit).x + normalOf(entry.exit).x * ROUTE_STUB, false) + offset;
|
|
2144
|
+
}
|
|
2145
|
+
wanted.set(slotKey(edge.from, entry.exit, index), exitWanted);
|
|
2146
|
+
wanted.set(slotKey(edge.to, entry.entry, index), entryWanted);
|
|
2147
|
+
}
|
|
2148
|
+
const sides = /* @__PURE__ */ new Map();
|
|
2149
|
+
for (const [index, entry] of facing.entries()) {
|
|
2150
|
+
if (entry === void 0) continue;
|
|
2151
|
+
const edge = diagram.edges[index];
|
|
2152
|
+
sides.set(edge.from + ":" + entry.exit, [
|
|
2153
|
+
...sides.get(edge.from + ":" + entry.exit) ?? [],
|
|
2154
|
+
{ index, wanted: wanted.get(slotKey(edge.from, entry.exit, index)) }
|
|
2155
|
+
]);
|
|
2156
|
+
sides.set(edge.to + ":" + entry.entry, [
|
|
2157
|
+
...sides.get(edge.to + ":" + entry.entry) ?? [],
|
|
2158
|
+
{ index, wanted: wanted.get(slotKey(edge.to, entry.entry, index)) }
|
|
2159
|
+
]);
|
|
2160
|
+
}
|
|
2161
|
+
let placedPort = /* @__PURE__ */ new Map();
|
|
2162
|
+
for (let round = 0; round < 3; round += 1) {
|
|
2163
|
+
placedPort = /* @__PURE__ */ new Map();
|
|
2164
|
+
for (const [key, list] of sides) {
|
|
2165
|
+
const [boxId, side] = key.split(":");
|
|
2166
|
+
const span = spanOf2(boxId, side, true);
|
|
2167
|
+
const order2 = [...list].sort((left, right) => left.wanted - right.wanted || left.index - right.index);
|
|
2168
|
+
const packed = packPorts(
|
|
2169
|
+
order2.map((entry) => clampTo(boxId, side, entry.wanted, true)),
|
|
2170
|
+
span.low,
|
|
2171
|
+
span.high
|
|
2172
|
+
);
|
|
2173
|
+
order2.forEach((entry, at) => placedPort.set(slotKey(boxId, side, entry.index), packed[at]));
|
|
2174
|
+
}
|
|
2175
|
+
let moved = false;
|
|
2176
|
+
for (const [index, entry] of facing.entries()) {
|
|
2177
|
+
if (entry === void 0) continue;
|
|
2178
|
+
const edge = diagram.edges[index];
|
|
2179
|
+
const vertical = entry.exit === "bottom" || entry.exit === "top";
|
|
2180
|
+
if (!vertical) continue;
|
|
2181
|
+
const exit = placedPort.get(slotKey(edge.from, entry.exit, index));
|
|
2182
|
+
if (Math.abs(exit - placedPort.get(slotKey(edge.to, entry.entry, index))) < 0.01) continue;
|
|
2183
|
+
if (Math.abs(clampTo(edge.to, entry.entry, exit, true) - exit) > 0.01) continue;
|
|
2184
|
+
for (const slot of sides.get(edge.to + ":" + entry.entry)) {
|
|
2185
|
+
if (slot.index !== index) continue;
|
|
2186
|
+
slot.wanted = exit;
|
|
2187
|
+
moved = true;
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
if (!moved) break;
|
|
2191
|
+
}
|
|
2192
|
+
const portOf = (boxId, side, edgeIndex) => {
|
|
2193
|
+
const coordinate = placedPort.get(slotKey(boxId, side, edgeIndex));
|
|
2194
|
+
return pointOn(boxId, side, coordinate);
|
|
2195
|
+
};
|
|
2196
|
+
const anchorsFor = /* @__PURE__ */ new Map();
|
|
2197
|
+
for (const [index, entry] of facing.entries()) {
|
|
2198
|
+
if (entry === void 0) continue;
|
|
2199
|
+
const edge = diagram.edges[index];
|
|
2200
|
+
anchorsFor.set(index, {
|
|
2201
|
+
exit: portOf(edge.from, entry.exit, index),
|
|
2202
|
+
entry: portOf(edge.to, entry.entry, index),
|
|
2203
|
+
exitSide: entry.exit,
|
|
2204
|
+
entrySide: entry.entry
|
|
2205
|
+
});
|
|
2206
|
+
}
|
|
2207
|
+
const longestRun = (points) => {
|
|
2208
|
+
let best = { from: points[0], to: points[points.length - 1], length: -1 };
|
|
2209
|
+
for (let index = 0; index + 1 < points.length; index += 1) {
|
|
2210
|
+
const from = points[index];
|
|
2211
|
+
const to = points[index + 1];
|
|
2212
|
+
const length = Math.abs(to.x - from.x) + Math.abs(to.y - from.y);
|
|
2213
|
+
if (length > best.length) best = { from, to, length };
|
|
2214
|
+
}
|
|
2215
|
+
return best;
|
|
2216
|
+
};
|
|
2217
|
+
const pointsOfRoute = (shape) => shape.kind === "path" ? shape.points : [{ x: shape.x1, y: shape.y1 }, { x: shape.x2, y: shape.y2 }];
|
|
2218
|
+
const segmentsOfRoute = (shape) => {
|
|
2219
|
+
const points = pointsOfRoute(shape);
|
|
2220
|
+
const out = [];
|
|
2221
|
+
for (let index = 0; index + 1 < points.length; index += 1) {
|
|
2222
|
+
const from = points[index];
|
|
2223
|
+
const to = points[index + 1];
|
|
2224
|
+
out.push({ x1: from.x, y1: from.y, x2: to.x, y2: to.y });
|
|
2225
|
+
}
|
|
2226
|
+
return out;
|
|
2227
|
+
};
|
|
2228
|
+
const paintPath = (edge, points, colour) => {
|
|
2229
|
+
const shape = {
|
|
2230
|
+
kind: "path",
|
|
2231
|
+
points,
|
|
2232
|
+
label: edge.label,
|
|
2233
|
+
labelX: 0,
|
|
2234
|
+
labelY: 0,
|
|
2235
|
+
colour,
|
|
2236
|
+
...edge.dashed === void 0 ? {} : { dashed: edge.dashed },
|
|
2237
|
+
...edge.arrow === void 0 ? {} : { arrow: edge.arrow }
|
|
2238
|
+
};
|
|
2239
|
+
shapes.push(shape);
|
|
2240
|
+
routes.push(shape);
|
|
2241
|
+
};
|
|
2242
|
+
const paintRoute = (edge, points, colour) => {
|
|
2243
|
+
if (points.length > 2) {
|
|
2244
|
+
paintPath(edge, points, colour);
|
|
2245
|
+
return;
|
|
2246
|
+
}
|
|
2247
|
+
const exit = points[0];
|
|
2248
|
+
const entry = points[points.length - 1];
|
|
2249
|
+
const shape = {
|
|
2250
|
+
kind: "link",
|
|
2251
|
+
x1: exit.x,
|
|
2252
|
+
y1: exit.y,
|
|
2253
|
+
x2: entry.x,
|
|
2254
|
+
y2: entry.y,
|
|
2255
|
+
label: edge.label,
|
|
2256
|
+
labelX: 0,
|
|
2257
|
+
labelY: 0,
|
|
2258
|
+
colour,
|
|
2259
|
+
...edge.dashed === void 0 ? {} : { dashed: edge.dashed },
|
|
2260
|
+
...edge.arrow === void 0 ? {} : { arrow: edge.arrow }
|
|
2261
|
+
};
|
|
2262
|
+
shapes.push(shape);
|
|
2263
|
+
routes.push(shape);
|
|
2264
|
+
};
|
|
2265
|
+
const runRoute = (ports, runY) => {
|
|
2266
|
+
const out = isVerticalSide(ports.exitSide) ? 0 : normalOf(ports.exitSide).x * ROUTE_STUB;
|
|
2267
|
+
const into = isVerticalSide(ports.entrySide) ? 0 : normalOf(ports.entrySide).x * ROUTE_STUB;
|
|
2268
|
+
const points = [ports.exit];
|
|
2269
|
+
if (out !== 0) points.push({ x: ports.exit.x + out, y: ports.exit.y });
|
|
2270
|
+
points.push({ x: ports.exit.x + out, y: runY }, { x: ports.entry.x + into, y: runY });
|
|
2271
|
+
if (into !== 0) points.push({ x: ports.entry.x + into, y: ports.entry.y });
|
|
2272
|
+
points.push(ports.entry);
|
|
2273
|
+
return simplify(points);
|
|
2274
|
+
};
|
|
2275
|
+
const runFits = (ports, runY) => {
|
|
2276
|
+
if (isVerticalSide(ports.exitSide) && Math.abs(runY - ports.exit.y) > 0.5) {
|
|
2277
|
+
if (Math.sign(runY - ports.exit.y) !== Math.sign(normalOf(ports.exitSide).y)) return false;
|
|
2278
|
+
}
|
|
2279
|
+
if (isVerticalSide(ports.entrySide) && Math.abs(ports.entry.y - runY) > 0.5) {
|
|
2280
|
+
if (Math.sign(ports.entry.y - runY) !== -Math.sign(normalOf(ports.entrySide).y)) return false;
|
|
2281
|
+
}
|
|
2282
|
+
return true;
|
|
2283
|
+
};
|
|
2284
|
+
const clearsBoxes = (points, from, to) => {
|
|
2285
|
+
for (let at = 0; at + 1 < points.length; at += 1) {
|
|
2286
|
+
const a = points[at];
|
|
2287
|
+
const b = points[at + 1];
|
|
2288
|
+
for (const box of boxList) {
|
|
2289
|
+
if (box === from || box === to) continue;
|
|
2290
|
+
if (crossesBox(a.x, a.y, b.x, b.y, box)) return false;
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
return true;
|
|
2294
|
+
};
|
|
2295
|
+
for (const [edgeIndex, edge] of diagram.edges.entries()) {
|
|
2296
|
+
const source = endBox(edge.from);
|
|
2297
|
+
const target = endBox(edge.to);
|
|
400
2298
|
if (source === void 0 || target === void 0) continue;
|
|
401
|
-
const colour = roleById.get(edge.from);
|
|
402
|
-
const sameRow = Math.abs(target.y - source.y) < NODE_HEIGHT2 / 2;
|
|
2299
|
+
const colour = roleById.get(edge.from) ?? "muted";
|
|
403
2300
|
if (edge.from === edge.to) {
|
|
404
2301
|
const x = source.x + source.width;
|
|
405
2302
|
const y = source.y + source.height / 2;
|
|
@@ -412,57 +2309,57 @@ window.__ModuleLoader__.load({
|
|
|
412
2309
|
{ x, y: y + 9 }
|
|
413
2310
|
],
|
|
414
2311
|
label: edge.label,
|
|
415
|
-
|
|
2312
|
+
// The one label that is not on its own edge: a self loop's longest run is eighteen points, and its name
|
|
2313
|
+
// does not fit on that. It sits beside the loop, where it belongs to the edge and touches nothing.
|
|
2314
|
+
labelX: x + 20 + textWidth(edge.label) / 2,
|
|
416
2315
|
labelY: y,
|
|
417
|
-
colour
|
|
2316
|
+
colour,
|
|
2317
|
+
...edge.dashed === void 0 ? {} : { dashed: edge.dashed },
|
|
2318
|
+
...edge.arrow === void 0 ? {} : { arrow: edge.arrow }
|
|
418
2319
|
});
|
|
419
2320
|
continue;
|
|
420
2321
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
const
|
|
425
|
-
const
|
|
426
|
-
|
|
2322
|
+
const anchors = anchorsFor.get(edgeIndex);
|
|
2323
|
+
const lane = placement.lanes.get(edgeIndex);
|
|
2324
|
+
if (lane !== void 0 && lane.points.length > 0 && anchors !== void 0) {
|
|
2325
|
+
const shift = laneShift.get(edgeIndex) ?? 0;
|
|
2326
|
+
const ordered = lane.reversed ? [...lane.points].reverse() : lane.points;
|
|
2327
|
+
const waypoints = shift === 0 ? ordered : ordered.map((point) => ({ x: point.x + shift, y: point.y }));
|
|
2328
|
+
const points = [anchors.exit];
|
|
2329
|
+
let previous = anchors.exit;
|
|
2330
|
+
for (const waypoint of waypoints) {
|
|
2331
|
+
if (Math.abs(previous.x - waypoint.x) > 0.5 && Math.abs(previous.y - waypoint.y) > 0.5) points.push({ x: previous.x, y: waypoint.y });
|
|
2332
|
+
points.push(waypoint);
|
|
2333
|
+
previous = waypoint;
|
|
2334
|
+
}
|
|
2335
|
+
if (Math.abs(previous.x - anchors.entry.x) > 0.5 && Math.abs(previous.y - anchors.entry.y) > 0.5) points.push({ x: anchors.entry.x, y: previous.y });
|
|
2336
|
+
points.push(anchors.entry);
|
|
2337
|
+
paintPath(edge, simplify(points), colour);
|
|
427
2338
|
continue;
|
|
428
2339
|
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
const
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
y1: y,
|
|
438
|
-
x2: forward ? left : target.x + target.width,
|
|
439
|
-
y2: y,
|
|
440
|
-
label: edge.label,
|
|
441
|
-
labelX: (right + left) / 2,
|
|
442
|
-
labelY: y - 7,
|
|
443
|
-
colour
|
|
444
|
-
});
|
|
445
|
-
continue;
|
|
2340
|
+
const runY = placement.runs.get(edgeIndex);
|
|
2341
|
+
if (runY !== void 0 && anchors !== void 0 && runFits(anchors, runY)) {
|
|
2342
|
+
const points = runRoute(anchors, runY);
|
|
2343
|
+
const mixed = isVerticalSide(anchors.exitSide) !== isVerticalSide(anchors.entrySide);
|
|
2344
|
+
if (!mixed || clearsBoxes(points, source, target)) {
|
|
2345
|
+
paintRoute(edge, points, colour);
|
|
2346
|
+
continue;
|
|
2347
|
+
}
|
|
446
2348
|
}
|
|
447
|
-
|
|
448
|
-
channelUses += 1;
|
|
449
|
-
channel = Math.max(channel, lane);
|
|
450
|
-
const startY = source.y + source.height / 2;
|
|
451
|
-
const endY = target.y + target.height / 2;
|
|
452
|
-
shapes.push({
|
|
453
|
-
kind: "path",
|
|
454
|
-
points: [
|
|
455
|
-
{ x: source.x + source.width, y: startY },
|
|
456
|
-
{ x: lane, y: startY },
|
|
457
|
-
{ x: lane, y: endY },
|
|
458
|
-
{ x: target.x + target.width, y: endY }
|
|
459
|
-
],
|
|
460
|
-
label: edge.label,
|
|
461
|
-
labelX: lane + 6,
|
|
462
|
-
labelY: (startY + endY) / 2,
|
|
463
|
-
colour
|
|
464
|
-
});
|
|
2349
|
+
paintRoute(edge, routeEdge(source, target, boxList, anchors).points, colour);
|
|
465
2350
|
}
|
|
2351
|
+
const routeSegments = routes.map((shape) => segmentsOfRoute(shape));
|
|
2352
|
+
const placedLabels = [];
|
|
2353
|
+
routes.forEach((shape, index) => {
|
|
2354
|
+
const run = longestRun(pointsOfRoute(shape));
|
|
2355
|
+
let spot = { x: (run.from.x + run.to.x) / 2, y: (run.from.y + run.to.y) / 2, width: textWidth(shape.label), height: LABEL_HEIGHT };
|
|
2356
|
+
if (shape.label !== "") {
|
|
2357
|
+
spot = placeLabel(shape.label, run.from, run.to, boxList, placedLabels, routeSegments.flatMap((list, at) => at === index ? [] : list));
|
|
2358
|
+
placedLabels.push(spot);
|
|
2359
|
+
}
|
|
2360
|
+
shape.labelX = spot.x;
|
|
2361
|
+
shape.labelY = spot.y;
|
|
2362
|
+
});
|
|
466
2363
|
shapes.push({ kind: "title", x: MARGIN, y: MARGIN + 12, text: diagram.title, colour: "text" });
|
|
467
2364
|
const contentRight = Math.max(
|
|
468
2365
|
MARGIN,
|
|
@@ -471,7 +2368,7 @@ window.__ModuleLoader__.load({
|
|
|
471
2368
|
);
|
|
472
2369
|
const captionLines = wrap(diagram.caption, CAPTION_CHARS);
|
|
473
2370
|
const captionWidth = Math.max(...captionLines.map((line) => line.length * CAPTION_CHAR_WIDTH)) + MARGIN * 2;
|
|
474
|
-
const width = Math.max(contentRight + MARGIN
|
|
2371
|
+
const width = Math.max(contentRight + MARGIN, captionWidth, 240);
|
|
475
2372
|
const contentBottom = Math.max(
|
|
476
2373
|
topOffset,
|
|
477
2374
|
...Array.from(boxes.values(), (box) => box.y + box.height),
|
|
@@ -480,7 +2377,10 @@ window.__ModuleLoader__.load({
|
|
|
480
2377
|
const captionTop = contentBottom + CAPTION_GAP;
|
|
481
2378
|
shapes.push({ kind: "caption", x: MARGIN, y: captionTop, lines: captionLines, colour: captionRole(diagram.caption) });
|
|
482
2379
|
const height = captionTop + captionLines.length * CAPTION_LINE + MARGIN / 2;
|
|
483
|
-
|
|
2380
|
+
const extent = drawnExtent(shapes);
|
|
2381
|
+
const canvasWidth = Math.max(width, extent.width + MARGIN);
|
|
2382
|
+
const canvasHeight = Math.max(height, extent.height + MARGIN);
|
|
2383
|
+
return { canvas: { width: Math.round(canvasWidth), height: Math.round(canvasHeight), shapes }, colours };
|
|
484
2384
|
}
|
|
485
2385
|
|
|
486
2386
|
// src/render/from-mermaid.ts
|
|
@@ -505,10 +2405,15 @@ window.__ModuleLoader__.load({
|
|
|
505
2405
|
"requirementdiagram",
|
|
506
2406
|
"sankey-beta"
|
|
507
2407
|
]);
|
|
508
|
-
function ensure(builder, id, label) {
|
|
2408
|
+
function ensure(builder, id, label, shape) {
|
|
509
2409
|
const existing = builder.nodes.get(id);
|
|
510
2410
|
if (existing === void 0) {
|
|
511
|
-
builder.nodes.set(id, {
|
|
2411
|
+
builder.nodes.set(id, {
|
|
2412
|
+
id,
|
|
2413
|
+
role: "normal",
|
|
2414
|
+
...label === void 0 ? {} : { label },
|
|
2415
|
+
...shape === void 0 ? {} : { shape }
|
|
2416
|
+
});
|
|
512
2417
|
const innermost = builder.open[builder.open.length - 1];
|
|
513
2418
|
if (innermost !== void 0 && !innermost.members.includes(id)) innermost.members.push(id);
|
|
514
2419
|
return;
|
|
@@ -520,7 +2425,12 @@ window.__ModuleLoader__.load({
|
|
|
520
2425
|
if (match === null) return void 0;
|
|
521
2426
|
const id = match[1];
|
|
522
2427
|
const label = [match[2], match[3], match[4], match[5], match[6], match[7]].find((value) => value !== void 0 && value.trim() !== "");
|
|
523
|
-
|
|
2428
|
+
const shape = match[2] !== void 0 ? "cylinder" : match[3] !== void 0 ? "subroutine" : match[4] !== void 0 ? "rect" : match[5] !== void 0 ? "stadium" : match[6] !== void 0 ? "diamond" : match[7] !== void 0 ? "hexagon" : void 0;
|
|
2429
|
+
return {
|
|
2430
|
+
id,
|
|
2431
|
+
...label === void 0 ? {} : { label: label.trim().replace(/^["']|["']$/g, "") },
|
|
2432
|
+
...shape === void 0 ? {} : { shape }
|
|
2433
|
+
};
|
|
524
2434
|
}
|
|
525
2435
|
function fromMermaid(source, title = "mermaid") {
|
|
526
2436
|
const lines = source.split(/\r?\n/);
|
|
@@ -537,13 +2447,13 @@ window.__ModuleLoader__.load({
|
|
|
537
2447
|
if (line === "") return;
|
|
538
2448
|
const where = "line " + String(index + 1) + ": ";
|
|
539
2449
|
const lower = line.toLowerCase();
|
|
540
|
-
const keyword = lower.split(/[\s{}]/)[0]
|
|
2450
|
+
const keyword = lower.split(/[\s{}]/)[0];
|
|
541
2451
|
if (UNSUPPORTED.has(keyword)) {
|
|
542
2452
|
refused.push(where + "`" + keyword + "` is not a diagram this renderer draws");
|
|
543
2453
|
return;
|
|
544
2454
|
}
|
|
545
2455
|
if (DIRECTIVE.test(line)) {
|
|
546
|
-
refused.push(where + "`" +
|
|
2456
|
+
refused.push(where + "`" + line.split(/\s/)[0] + "` is a directive this renderer ignores");
|
|
547
2457
|
return;
|
|
548
2458
|
}
|
|
549
2459
|
if (/^(graph|flowchart)\b/.test(lower)) {
|
|
@@ -615,8 +2525,8 @@ window.__ModuleLoader__.load({
|
|
|
615
2525
|
refused.push(where + "`" + line + "` does not name two nodes");
|
|
616
2526
|
return;
|
|
617
2527
|
}
|
|
618
|
-
ensure(builder, left.id, left.label);
|
|
619
|
-
ensure(builder, right.id, right.label);
|
|
2528
|
+
ensure(builder, left.id, left.label, left.shape);
|
|
2529
|
+
ensure(builder, right.id, right.label, right.shape);
|
|
620
2530
|
push({ from: left.id, to: right.id, label: labelled[2].trim() });
|
|
621
2531
|
return;
|
|
622
2532
|
}
|
|
@@ -628,8 +2538,8 @@ window.__ModuleLoader__.load({
|
|
|
628
2538
|
refused.push(where + "`" + line + "` does not name two nodes");
|
|
629
2539
|
return;
|
|
630
2540
|
}
|
|
631
|
-
ensure(builder, left.id, left.label);
|
|
632
|
-
ensure(builder, right.id, right.label);
|
|
2541
|
+
ensure(builder, left.id, left.label, left.shape);
|
|
2542
|
+
ensure(builder, right.id, right.label, right.shape);
|
|
633
2543
|
push({
|
|
634
2544
|
from: left.id,
|
|
635
2545
|
to: right.id,
|
|
@@ -640,7 +2550,7 @@ window.__ModuleLoader__.load({
|
|
|
640
2550
|
}
|
|
641
2551
|
const single = nodeFrom(line);
|
|
642
2552
|
if (single !== void 0) {
|
|
643
|
-
ensure(builder, single.id, single.label);
|
|
2553
|
+
ensure(builder, single.id, single.label, single.shape);
|
|
644
2554
|
return;
|
|
645
2555
|
}
|
|
646
2556
|
refused.push(where + "`" + line + "` is not something this renderer draws");
|
|
@@ -664,17 +2574,15 @@ window.__ModuleLoader__.load({
|
|
|
664
2574
|
}
|
|
665
2575
|
|
|
666
2576
|
// src/render/collapse.ts
|
|
667
|
-
function subtreeSize(
|
|
2577
|
+
function subtreeSize(groups, groupId, seen) {
|
|
668
2578
|
if (seen.has(groupId)) return 0;
|
|
669
2579
|
seen.add(groupId);
|
|
670
|
-
const groups = diagram.groups ?? [];
|
|
671
2580
|
const group = groups.find((entry) => entry.id === groupId);
|
|
672
|
-
if (group === void 0) return 0;
|
|
673
2581
|
const children = groups.filter((entry) => entry.parent === groupId);
|
|
674
|
-
return group.members.length + children.reduce((total, child) => total + subtreeSize(
|
|
2582
|
+
return group.members.length + children.reduce((total, child) => total + subtreeSize(groups, child.id, seen), 0);
|
|
675
2583
|
}
|
|
676
|
-
function ownerOf(
|
|
677
|
-
return
|
|
2584
|
+
function ownerOf(groups, nodeId) {
|
|
2585
|
+
return groups.find((group) => group.members.includes(nodeId))?.id;
|
|
678
2586
|
}
|
|
679
2587
|
function collapse(diagram, folded) {
|
|
680
2588
|
const asked = new Set(folded);
|
|
@@ -684,7 +2592,7 @@ window.__ModuleLoader__.load({
|
|
|
684
2592
|
const targets = /* @__PURE__ */ new Set();
|
|
685
2593
|
for (const id of asked) {
|
|
686
2594
|
const group = byId.get(id);
|
|
687
|
-
if (group !== void 0 && subtreeSize(
|
|
2595
|
+
if (group !== void 0 && subtreeSize(groups, id, /* @__PURE__ */ new Set()) > 0) targets.add(id);
|
|
688
2596
|
}
|
|
689
2597
|
if (targets.size === 0) return diagram;
|
|
690
2598
|
for (const id of [...targets]) {
|
|
@@ -701,16 +2609,13 @@ window.__ModuleLoader__.load({
|
|
|
701
2609
|
}
|
|
702
2610
|
const removedNodes = /* @__PURE__ */ new Set();
|
|
703
2611
|
const removedGroups = /* @__PURE__ */ new Set();
|
|
704
|
-
const descend = (groupId
|
|
705
|
-
if (seen.has(groupId)) return;
|
|
706
|
-
seen.add(groupId);
|
|
2612
|
+
const descend = (groupId) => {
|
|
707
2613
|
removedGroups.add(groupId);
|
|
708
2614
|
const group = byId.get(groupId);
|
|
709
|
-
if (group === void 0) return;
|
|
710
2615
|
for (const member of group.members) removedNodes.add(member);
|
|
711
|
-
for (const child of groups.filter((entry) => entry.parent === groupId)) descend(child.id
|
|
2616
|
+
for (const child of groups.filter((entry) => entry.parent === groupId)) descend(child.id);
|
|
712
2617
|
};
|
|
713
|
-
for (const id of targets) descend(id
|
|
2618
|
+
for (const id of targets) descend(id);
|
|
714
2619
|
const foldOf = (id) => {
|
|
715
2620
|
for (const target of targets) {
|
|
716
2621
|
const inSubtree = (groupId, seen) => {
|
|
@@ -720,7 +2625,7 @@ window.__ModuleLoader__.load({
|
|
|
720
2625
|
return groups.filter((entry) => entry.parent === groupId).some((child) => inSubtree(child.id, seen));
|
|
721
2626
|
};
|
|
722
2627
|
const group = byId.get(target);
|
|
723
|
-
if (group.members.includes(id) || inSubtree(ownerOf(
|
|
2628
|
+
if (group.members.includes(id) || inSubtree(ownerOf(groups, id), /* @__PURE__ */ new Set())) return target;
|
|
724
2629
|
}
|
|
725
2630
|
return void 0;
|
|
726
2631
|
};
|
|
@@ -735,12 +2640,12 @@ window.__ModuleLoader__.load({
|
|
|
735
2640
|
if (fold === void 0 || emitted.has(fold)) continue;
|
|
736
2641
|
emitted.add(fold);
|
|
737
2642
|
const group = byId.get(fold);
|
|
738
|
-
nodes.push({ id: fold, label: group.label + " (" + String(subtreeSize(
|
|
2643
|
+
nodes.push({ id: fold, label: group.label + " (" + String(subtreeSize(groups, fold, /* @__PURE__ */ new Set())) + ")", role: "normal" });
|
|
739
2644
|
}
|
|
740
2645
|
for (const id of targets) {
|
|
741
2646
|
if (emitted.has(id)) continue;
|
|
742
2647
|
const group = byId.get(id);
|
|
743
|
-
nodes.push({ id, label: group.label + " (" + String(subtreeSize(
|
|
2648
|
+
nodes.push({ id, label: group.label + " (" + String(subtreeSize(groups, id, /* @__PURE__ */ new Set())) + ")", role: "normal" });
|
|
744
2649
|
}
|
|
745
2650
|
const edges = [];
|
|
746
2651
|
const seenEdges = /* @__PURE__ */ new Set();
|
|
@@ -757,7 +2662,7 @@ window.__ModuleLoader__.load({
|
|
|
757
2662
|
const adopted = /* @__PURE__ */ new Map();
|
|
758
2663
|
for (const id of targets) {
|
|
759
2664
|
const parent = byId.get(id)?.parent;
|
|
760
|
-
if (parent === void 0
|
|
2665
|
+
if (parent === void 0) continue;
|
|
761
2666
|
adopted.set(parent, [...adopted.get(parent) ?? [], id]);
|
|
762
2667
|
}
|
|
763
2668
|
const remaining = groups.filter((group) => !removedGroups.has(group.id) && targets.has(group.id) === false).map((group) => {
|
|
@@ -776,38 +2681,139 @@ window.__ModuleLoader__.load({
|
|
|
776
2681
|
}
|
|
777
2682
|
|
|
778
2683
|
// src/render/from-plantuml.ts
|
|
779
|
-
var
|
|
780
|
-
|
|
2684
|
+
var SHAPES = {
|
|
2685
|
+
component: "rect",
|
|
2686
|
+
node: "rect",
|
|
2687
|
+
rectangle: "rect",
|
|
2688
|
+
artifact: "rect",
|
|
2689
|
+
state: "rect",
|
|
2690
|
+
database: "cylinder",
|
|
2691
|
+
// PlantUML draws an entity as a cylinder in a component diagram; `entity` in an ER diagram is a different
|
|
2692
|
+
// symbol, and that diagram family is not one this renderer accepts.
|
|
2693
|
+
entity: "cylinder",
|
|
2694
|
+
// A use case is an ellipse. The `circle` symbol is written as an ellipse whose radii follow the box, so it
|
|
2695
|
+
// reads as the familiar use-case oval; `stadium` is the pill a terminal state is drawn as.
|
|
2696
|
+
usecase: "circle"
|
|
2697
|
+
};
|
|
2698
|
+
var BOUNDARY = /* @__PURE__ */ new Set(["package", "folder", "node", "component", "rectangle", "artifact"]);
|
|
2699
|
+
var NOT_DRAWN = {
|
|
2700
|
+
actor: "an actor is a stick figure, not one of the shapes this renderer draws",
|
|
2701
|
+
participant: "a participant is a lifeline, not one of the shapes this renderer draws",
|
|
2702
|
+
class: "a class box has compartments this renderer does not draw",
|
|
2703
|
+
object: "an object box has compartments this renderer does not draw",
|
|
2704
|
+
interface: "an interface is a circle or a boxed stereotype, neither of which this renderer draws",
|
|
2705
|
+
enum: "an enum box has compartments this renderer does not draw",
|
|
2706
|
+
annotation: "an annotation is not one of the shapes this renderer draws",
|
|
2707
|
+
cloud: "a cloud outline is not one of the shapes this renderer draws",
|
|
2708
|
+
frame: "a frame tab is not one of the shapes this renderer draws",
|
|
2709
|
+
card: "a card is not one of the shapes this renderer draws",
|
|
2710
|
+
file: "a file outline is not one of the shapes this renderer draws",
|
|
2711
|
+
agent: "an agent is not one of the shapes this renderer draws",
|
|
2712
|
+
boundary: "the boundary symbol is not one of the shapes this renderer draws",
|
|
2713
|
+
control: "the control symbol is not one of the shapes this renderer draws",
|
|
2714
|
+
collections: "a collections symbol is not one of the shapes this renderer draws",
|
|
2715
|
+
queue: "a queue is not one of the shapes this renderer draws",
|
|
2716
|
+
stack: "a stack is not one of the shapes this renderer draws",
|
|
2717
|
+
storage: "a storage symbol is not one of the shapes this renderer draws",
|
|
2718
|
+
note: "a note is not one of the shapes this renderer draws",
|
|
2719
|
+
legend: "a legend is not one of the shapes this renderer draws"
|
|
2720
|
+
};
|
|
2721
|
+
var FRAGMENT = /* @__PURE__ */ new Set(["sequence", "alt", "opt", "loop", "par", "group", "ref", "break", "critical", "activate", "deactivate", "destroy", "create", "newpage", "autonumber"]);
|
|
2722
|
+
var ARROW = /^(.+?)\s*(<\|?|[*o])?(-{2,}|\.{2,})(\|?>|[*o])?\s*(.+?)(?:\s*:\s*(.+))?$/;
|
|
2723
|
+
var SHAPE_KEYWORDS = Object.keys(SHAPES);
|
|
2724
|
+
var DECLARED = new RegExp("^(" + SHAPE_KEYWORDS.join("|") + ")\\s+(.+)$");
|
|
781
2725
|
var QUOTED_ALIAS = /^["'](.+?)["']\s+as\s+([A-Za-z_][\w-]*)$/;
|
|
782
2726
|
var BRACKET_ALIAS = /^\[(.+?)\]\s+as\s+([A-Za-z_][\w-]*)$/;
|
|
2727
|
+
var PLAIN_ALIAS = /^([A-Za-z_][\w.-]*)\s+as\s+([A-Za-z_][\w-]*)$/;
|
|
783
2728
|
var BRACKET = /^\[(.+?)\]$/;
|
|
784
|
-
var
|
|
785
|
-
|
|
786
|
-
function ensure2(builder, id, label) {
|
|
2729
|
+
var IGNORED = /^(skinparam|!theme|!define|!include|!pragma|hide|show|scale|left to right direction|top to bottom direction|together|allowmixing)\b/;
|
|
2730
|
+
function ensure2(builder, id, label, shape, implicit = false) {
|
|
787
2731
|
const existing = builder.nodes.get(id);
|
|
788
2732
|
if (existing === void 0) {
|
|
789
|
-
builder.nodes.set(id, {
|
|
2733
|
+
builder.nodes.set(id, {
|
|
2734
|
+
id,
|
|
2735
|
+
role: "normal",
|
|
2736
|
+
...label === void 0 ? {} : { label },
|
|
2737
|
+
...shape === void 0 ? {} : { shape }
|
|
2738
|
+
});
|
|
2739
|
+
if (implicit) builder.implicit.add(id);
|
|
790
2740
|
const innermost = builder.open[builder.open.length - 1];
|
|
791
2741
|
if (innermost !== void 0 && !innermost.members.includes(id)) innermost.members.push(id);
|
|
792
2742
|
return;
|
|
793
2743
|
}
|
|
2744
|
+
if (!implicit) builder.implicit.delete(id);
|
|
794
2745
|
if (label !== void 0 && (existing.label === void 0 || existing.label === "")) existing.label = label;
|
|
2746
|
+
if (shape !== void 0 && existing.shape === void 0) existing.shape = shape;
|
|
795
2747
|
}
|
|
796
2748
|
function reference(token) {
|
|
797
2749
|
const text2 = token.trim();
|
|
798
2750
|
const bracket = BRACKET.exec(text2);
|
|
799
2751
|
if (bracket !== null) {
|
|
800
2752
|
const label = bracket[1].trim();
|
|
801
|
-
return { id: label, label };
|
|
2753
|
+
return { id: label, label, shape: "rect" };
|
|
802
2754
|
}
|
|
803
2755
|
const quoted = /^["'](.+?)["']$/.exec(text2);
|
|
804
2756
|
if (quoted !== null) return { id: quoted[1], label: quoted[1] };
|
|
805
2757
|
return { id: text2 };
|
|
806
2758
|
}
|
|
2759
|
+
function keywordOf(line) {
|
|
2760
|
+
return line.split(/[\s{]/)[0].toLowerCase();
|
|
2761
|
+
}
|
|
2762
|
+
function boundaryOf(declaration, keyword, ordinal) {
|
|
2763
|
+
const rest = declaration.slice(keyword.length).trim();
|
|
2764
|
+
const alias = QUOTED_ALIAS.exec(rest) ?? BRACKET_ALIAS.exec(rest) ?? PLAIN_ALIAS.exec(rest);
|
|
2765
|
+
if (alias !== null) return { id: alias[2], label: alias[1] };
|
|
2766
|
+
const label = rest.replace(/^["']|["']$/g, "");
|
|
2767
|
+
return { id: "group-" + String(ordinal), label: label === "" ? keyword : label };
|
|
2768
|
+
}
|
|
2769
|
+
function readArrow(line) {
|
|
2770
|
+
const match = ARROW.exec(line);
|
|
2771
|
+
if (match === null) return void 0;
|
|
2772
|
+
const left = reference(match[1]);
|
|
2773
|
+
const right = reference(match[5]);
|
|
2774
|
+
const leftMark = match[2];
|
|
2775
|
+
const rightMark = match[4];
|
|
2776
|
+
const dotted = match[3].startsWith(".");
|
|
2777
|
+
const label = (match[6] ?? "").trim();
|
|
2778
|
+
if (leftMark !== void 0 && rightMark !== void 0) {
|
|
2779
|
+
return "an edge marked at both ends is not one this renderer draws";
|
|
2780
|
+
}
|
|
2781
|
+
if (leftMark === "*" || leftMark === "o" || rightMark === "*" || rightMark === "o") {
|
|
2782
|
+
if (dotted) return "a dotted composition or aggregation is not one this renderer draws";
|
|
2783
|
+
const atRight = rightMark === "*" || rightMark === "o";
|
|
2784
|
+
const mark = atRight ? rightMark : leftMark;
|
|
2785
|
+
return {
|
|
2786
|
+
from: atRight ? right : left,
|
|
2787
|
+
to: atRight ? left : right,
|
|
2788
|
+
label,
|
|
2789
|
+
dashed: false,
|
|
2790
|
+
arrow: mark === "*" ? "composition" : "aggregation"
|
|
2791
|
+
};
|
|
2792
|
+
}
|
|
2793
|
+
if (leftMark !== void 0) {
|
|
2794
|
+
return {
|
|
2795
|
+
from: right,
|
|
2796
|
+
to: left,
|
|
2797
|
+
label,
|
|
2798
|
+
dashed: dotted,
|
|
2799
|
+
arrow: leftMark === "<|" ? dotted ? "realization" : "generalization" : dotted ? "dependency" : "association"
|
|
2800
|
+
};
|
|
2801
|
+
}
|
|
2802
|
+
if (rightMark !== void 0) {
|
|
2803
|
+
return {
|
|
2804
|
+
from: left,
|
|
2805
|
+
to: right,
|
|
2806
|
+
label,
|
|
2807
|
+
dashed: dotted,
|
|
2808
|
+
arrow: rightMark === "|>" ? dotted ? "realization" : "generalization" : dotted ? "dependency" : "association"
|
|
2809
|
+
};
|
|
2810
|
+
}
|
|
2811
|
+
return "a line with no arrow end is not one this renderer draws; name the end it points at";
|
|
2812
|
+
}
|
|
807
2813
|
function fromPlantUml(source, title = "plantuml") {
|
|
808
2814
|
const lines = source.split(/\r?\n/);
|
|
809
2815
|
const refused = [];
|
|
810
|
-
const builder = { nodes: /* @__PURE__ */ new Map(), edges: [], groups: [], open: [] };
|
|
2816
|
+
const builder = { nodes: /* @__PURE__ */ new Map(), edges: [], edgeLines: [], edgeScopes: [], groups: [], open: [], implicit: /* @__PURE__ */ new Set() };
|
|
811
2817
|
let startId;
|
|
812
2818
|
let seenAny = false;
|
|
813
2819
|
let groupCount = 0;
|
|
@@ -819,8 +2825,14 @@ window.__ModuleLoader__.load({
|
|
|
819
2825
|
seenAny = true;
|
|
820
2826
|
if (/^title\b/.test(line)) return;
|
|
821
2827
|
if (IGNORED.test(line)) return;
|
|
822
|
-
|
|
823
|
-
|
|
2828
|
+
const keyword = keywordOf(line);
|
|
2829
|
+
const notDrawn = NOT_DRAWN[keyword];
|
|
2830
|
+
if (notDrawn !== void 0) {
|
|
2831
|
+
refused.push(where + "`" + keyword + "`: " + notDrawn);
|
|
2832
|
+
return;
|
|
2833
|
+
}
|
|
2834
|
+
if (FRAGMENT.has(keyword)) {
|
|
2835
|
+
refused.push(where + "`" + keyword + "`: a sequence statement is not part of a drawing this renderer makes");
|
|
824
2836
|
return;
|
|
825
2837
|
}
|
|
826
2838
|
if (line === "}") {
|
|
@@ -834,56 +2846,176 @@ window.__ModuleLoader__.load({
|
|
|
834
2846
|
}
|
|
835
2847
|
if (line.endsWith("{")) {
|
|
836
2848
|
const declaration = line.slice(0, -1).trim();
|
|
837
|
-
const
|
|
838
|
-
|
|
2849
|
+
const opener = keywordOf(declaration);
|
|
2850
|
+
if (!BOUNDARY.has(opener)) {
|
|
2851
|
+
const reason = SHAPES[opener] === void 0 ? "`" + declaration + "` is not a boundary this renderer draws" : "a `" + opener + "` block is not a boundary this renderer draws";
|
|
2852
|
+
refused.push(where + reason);
|
|
2853
|
+
return;
|
|
2854
|
+
}
|
|
839
2855
|
groupCount += 1;
|
|
840
|
-
|
|
2856
|
+
const boundary = boundaryOf(declaration, opener, groupCount);
|
|
2857
|
+
builder.open.push({ id: boundary.id, label: boundary.label, members: [] });
|
|
2858
|
+
return;
|
|
2859
|
+
}
|
|
2860
|
+
if (line === "[*] --> [*]") return;
|
|
2861
|
+
const startMarker = /^\[\*\]\s*-->\s*(.+)$/.exec(line);
|
|
2862
|
+
if (startMarker !== null) {
|
|
2863
|
+
const target = reference(startMarker[1]);
|
|
2864
|
+
ensure2(builder, target.id, target.label, target.shape);
|
|
2865
|
+
if (startId === void 0) startId = target.id;
|
|
841
2866
|
return;
|
|
842
2867
|
}
|
|
843
2868
|
const declared = DECLARED.exec(line);
|
|
844
2869
|
if (declared !== null) {
|
|
2870
|
+
const shape = SHAPES[declared[1]];
|
|
845
2871
|
const rest = declared[2].trim();
|
|
846
2872
|
const quoted = QUOTED_ALIAS.exec(rest);
|
|
847
2873
|
if (quoted !== null) {
|
|
848
|
-
ensure2(builder, quoted[2], quoted[1]);
|
|
2874
|
+
ensure2(builder, quoted[2], quoted[1], shape);
|
|
849
2875
|
return;
|
|
850
2876
|
}
|
|
851
2877
|
const bracketed = BRACKET_ALIAS.exec(rest);
|
|
852
2878
|
if (bracketed !== null) {
|
|
853
|
-
ensure2(builder, bracketed[2], bracketed[1]);
|
|
2879
|
+
ensure2(builder, bracketed[2], bracketed[1], shape);
|
|
854
2880
|
return;
|
|
855
2881
|
}
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
if (startMarker !== null) {
|
|
864
|
-
const target = reference(startMarker[1]);
|
|
865
|
-
ensure2(builder, target.id, target.label);
|
|
866
|
-
if (startId === void 0) startId = target.id;
|
|
2882
|
+
const plain = PLAIN_ALIAS.exec(rest);
|
|
2883
|
+
if (plain !== null) {
|
|
2884
|
+
ensure2(builder, plain[2], plain[1], shape);
|
|
2885
|
+
return;
|
|
2886
|
+
}
|
|
2887
|
+
const written = reference(rest);
|
|
2888
|
+
ensure2(builder, written.id, written.label, shape);
|
|
867
2889
|
return;
|
|
868
2890
|
}
|
|
869
|
-
const arrow =
|
|
870
|
-
if (arrow !==
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
ensure2(builder, from.id, from.label);
|
|
876
|
-
ensure2(builder, to.id, to.label);
|
|
877
|
-
builder.edges.push({
|
|
2891
|
+
const arrow = readArrow(line);
|
|
2892
|
+
if (arrow !== void 0) {
|
|
2893
|
+
if (typeof arrow === "string") {
|
|
2894
|
+
refused.push(where + arrow);
|
|
2895
|
+
return;
|
|
2896
|
+
}
|
|
2897
|
+
ensure2(builder, arrow.from.id, arrow.from.label, arrow.from.shape, true);
|
|
2898
|
+
ensure2(builder, arrow.to.id, arrow.to.label, arrow.to.shape, true);
|
|
2899
|
+
builder.edges.push({
|
|
2900
|
+
from: arrow.from.id,
|
|
2901
|
+
to: arrow.to.id,
|
|
2902
|
+
label: arrow.label,
|
|
2903
|
+
...arrow.dashed ? { dashed: true } : {},
|
|
2904
|
+
arrow: arrow.arrow
|
|
2905
|
+
});
|
|
2906
|
+
builder.edgeLines.push(index + 1);
|
|
2907
|
+
builder.edgeScopes.push(builder.open[builder.open.length - 1]?.id);
|
|
878
2908
|
return;
|
|
879
2909
|
}
|
|
880
2910
|
const single = BRACKET.exec(line);
|
|
881
2911
|
if (single !== null) {
|
|
882
|
-
ensure2(builder, single[1].trim(), single[1].trim());
|
|
2912
|
+
ensure2(builder, single[1].trim(), single[1].trim(), "rect");
|
|
883
2913
|
return;
|
|
884
2914
|
}
|
|
885
2915
|
refused.push(where + "`" + line + "` is not something this renderer draws");
|
|
886
2916
|
});
|
|
2917
|
+
const boundaryNames = /* @__PURE__ */ new Map();
|
|
2918
|
+
const disputedNames = /* @__PURE__ */ new Set();
|
|
2919
|
+
for (const group of builder.groups) {
|
|
2920
|
+
for (const name of [group.id, group.label]) {
|
|
2921
|
+
const existing = boundaryNames.get(name);
|
|
2922
|
+
if (existing !== void 0 && existing !== group.id) disputedNames.add(name);
|
|
2923
|
+
else boundaryNames.set(name, group.id);
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
for (const name of disputedNames) boundaryNames.delete(name);
|
|
2927
|
+
const boundaryAt = /* @__PURE__ */ new Map();
|
|
2928
|
+
const ambiguousAt = /* @__PURE__ */ new Set();
|
|
2929
|
+
for (const id of builder.implicit) {
|
|
2930
|
+
if (disputedNames.has(id)) {
|
|
2931
|
+
ambiguousAt.add(id);
|
|
2932
|
+
builder.nodes.delete(id);
|
|
2933
|
+
} else {
|
|
2934
|
+
const groupId = boundaryNames.get(id);
|
|
2935
|
+
if (groupId === void 0) continue;
|
|
2936
|
+
boundaryAt.set(id, groupId);
|
|
2937
|
+
builder.nodes.delete(id);
|
|
2938
|
+
}
|
|
2939
|
+
for (const group of builder.groups) {
|
|
2940
|
+
const at = group.members.indexOf(id);
|
|
2941
|
+
if (at >= 0) group.members.splice(at, 1);
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
const groupById = new Map(builder.groups.map((group) => [group.id, group]));
|
|
2945
|
+
const childrenOf = /* @__PURE__ */ new Map();
|
|
2946
|
+
for (const group of builder.groups) {
|
|
2947
|
+
if (group.parent === void 0) continue;
|
|
2948
|
+
childrenOf.set(group.parent, [...childrenOf.get(group.parent) ?? [], group.id]);
|
|
2949
|
+
}
|
|
2950
|
+
const drawable = /* @__PURE__ */ new Map();
|
|
2951
|
+
const hasContents = (id) => {
|
|
2952
|
+
const known = drawable.get(id);
|
|
2953
|
+
if (known !== void 0) return known;
|
|
2954
|
+
const group = groupById.get(id);
|
|
2955
|
+
let found = false;
|
|
2956
|
+
for (const member of group.members) {
|
|
2957
|
+
if (builder.nodes.has(member)) {
|
|
2958
|
+
found = true;
|
|
2959
|
+
break;
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2962
|
+
if (!found) {
|
|
2963
|
+
for (const child of childrenOf.get(id) ?? []) {
|
|
2964
|
+
if (hasContents(child)) {
|
|
2965
|
+
found = true;
|
|
2966
|
+
break;
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
2969
|
+
}
|
|
2970
|
+
drawable.set(id, found);
|
|
2971
|
+
return found;
|
|
2972
|
+
};
|
|
2973
|
+
const boundaryLabel = (id) => groupById.get(id).label;
|
|
2974
|
+
const within = (scope, boundary) => {
|
|
2975
|
+
let cursor = scope;
|
|
2976
|
+
while (cursor !== void 0) {
|
|
2977
|
+
if (cursor === boundary) return true;
|
|
2978
|
+
cursor = groupById.get(cursor).parent;
|
|
2979
|
+
}
|
|
2980
|
+
return false;
|
|
2981
|
+
};
|
|
2982
|
+
const keptEdges = [];
|
|
2983
|
+
builder.edges.forEach((edge, index) => {
|
|
2984
|
+
const where = "line " + String(builder.edgeLines[index]) + ": ";
|
|
2985
|
+
const fromBoundary = boundaryAt.get(edge.from);
|
|
2986
|
+
const toBoundary = boundaryAt.get(edge.to);
|
|
2987
|
+
if (ambiguousAt.has(edge.from) || ambiguousAt.has(edge.to)) {
|
|
2988
|
+
const name = ambiguousAt.has(edge.from) ? edge.from : edge.to;
|
|
2989
|
+
refused.push(where + "`" + name + "` is the name of more than one block; an edge cannot say which boundary it means");
|
|
2990
|
+
return;
|
|
2991
|
+
}
|
|
2992
|
+
if (fromBoundary !== void 0 && toBoundary !== void 0) {
|
|
2993
|
+
refused.push(where + "`" + boundaryLabel(fromBoundary) + "` and `" + boundaryLabel(toBoundary) + "` are boundaries, not elements; an edge between two boundaries is not one this renderer draws");
|
|
2994
|
+
return;
|
|
2995
|
+
}
|
|
2996
|
+
const boundary = fromBoundary ?? toBoundary;
|
|
2997
|
+
if (boundary !== void 0) {
|
|
2998
|
+
const group = groupById.get(boundary);
|
|
2999
|
+
if (group.parent !== void 0) {
|
|
3000
|
+
refused.push(where + "`" + group.label + "` is a block inside `" + boundaryLabel(group.parent) + "`, not an element; an edge to a nested boundary would have to enter another boundary");
|
|
3001
|
+
return;
|
|
3002
|
+
}
|
|
3003
|
+
if (within(builder.edgeScopes[index], boundary)) {
|
|
3004
|
+
refused.push(where + "`" + group.label + "` is the block this edge is written inside, not an element; an edge cannot leave a boundary and arrive at the same boundary");
|
|
3005
|
+
return;
|
|
3006
|
+
}
|
|
3007
|
+
if (!hasContents(boundary)) {
|
|
3008
|
+
refused.push(where + "`" + group.label + "` is a block with nothing drawn inside it, so there is no rectangle to attach the edge to");
|
|
3009
|
+
return;
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
keptEdges.push({
|
|
3013
|
+
...edge,
|
|
3014
|
+
...fromBoundary === void 0 ? {} : { from: fromBoundary },
|
|
3015
|
+
...toBoundary === void 0 ? {} : { to: toBoundary }
|
|
3016
|
+
});
|
|
3017
|
+
});
|
|
3018
|
+
builder.edges = keptEdges;
|
|
887
3019
|
if (!seenAny || builder.nodes.size === 0) {
|
|
888
3020
|
return { diagram: emptyDiagram(title, "no nodes found"), refused };
|
|
889
3021
|
}
|
|
@@ -900,6 +3032,13 @@ window.__ModuleLoader__.load({
|
|
|
900
3032
|
}
|
|
901
3033
|
|
|
902
3034
|
// src/render/svg.ts
|
|
3035
|
+
function boxText(shape, stroke) {
|
|
3036
|
+
const lines = shape.lines === void 0 || shape.lines.length === 0 ? [shape.label] : shape.lines;
|
|
3037
|
+
const lineHeight = 15;
|
|
3038
|
+
const first = shape.y + shape.height / 2 - (lines.length - 1) * lineHeight / 2 + 4;
|
|
3039
|
+
const spans = lines.map((line, index) => '<tspan x="' + n(shape.x + shape.width / 2) + '" y="' + n(first + index * lineHeight) + '">' + escape(line) + "</tspan>").join("");
|
|
3040
|
+
return '<text font-size="11" text-anchor="middle" fill="' + stroke + '">' + spans + "</text>";
|
|
3041
|
+
}
|
|
903
3042
|
var n = (value) => {
|
|
904
3043
|
const rounded = Math.round(value * 10) / 10;
|
|
905
3044
|
return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1);
|
|
@@ -914,19 +3053,59 @@ window.__ModuleLoader__.load({
|
|
|
914
3053
|
box: (shape, colours) => {
|
|
915
3054
|
const stroke = colours[shape.colour];
|
|
916
3055
|
const width = shape.initial ? 2.5 : 1;
|
|
917
|
-
|
|
3056
|
+
const tail = '" fill="' + colours.surface + '" stroke="' + stroke + '" stroke-width="' + n(width) + '" />';
|
|
3057
|
+
const x = shape.x;
|
|
3058
|
+
const y = shape.y;
|
|
3059
|
+
const w = shape.width;
|
|
3060
|
+
const h = shape.height;
|
|
3061
|
+
const at = (point) => n(point.x) + " " + n(point.y);
|
|
3062
|
+
const points = (list) => list.map((point) => n(point.x) + "," + n(point.y)).join(",");
|
|
3063
|
+
const curve = (segment) => " C" + at(segment.c1) + " " + at(segment.c2) + " " + at(segment.to);
|
|
3064
|
+
switch (shape.shape) {
|
|
3065
|
+
// A decision: the notation wrote `{…}`, and a rounded rectangle would say "step".
|
|
3066
|
+
case "diamond":
|
|
3067
|
+
return '<polygon points="' + points(diamondPoints(shape)) + tail + boxText(shape, stroke);
|
|
3068
|
+
case "hexagon":
|
|
3069
|
+
return '<polygon points="' + points(hexagonPoints(shape)) + tail + boxText(shape, stroke);
|
|
3070
|
+
// A datastore: the cylinder UML draws for a stored collection.
|
|
3071
|
+
case "cylinder": {
|
|
3072
|
+
const caps = cylinderCaps(shape);
|
|
3073
|
+
const path = "M" + at(caps.top.from) + curve(caps.top) + " L" + at(caps.bottom.from) + curve(caps.bottom) + " Z";
|
|
3074
|
+
return '<path d="' + path + tail + boxText(shape, stroke);
|
|
3075
|
+
}
|
|
3076
|
+
// A start or end state: the stadium a terminal node is drawn as.
|
|
3077
|
+
case "stadium":
|
|
3078
|
+
return '<rect x="' + n(x) + '" y="' + n(y) + '" width="' + n(w) + '" height="' + n(h) + '" rx="' + n(stadiumRadius(shape)) + tail + boxText(shape, stroke);
|
|
3079
|
+
case "circle":
|
|
3080
|
+
return '<ellipse cx="' + n(x + w / 2) + '" cy="' + n(y + h / 2) + '" rx="' + n(w / 2) + '" ry="' + n(h / 2) + tail + boxText(shape, stroke);
|
|
3081
|
+
// A subroutine carries its double side lines.
|
|
3082
|
+
case "subroutine":
|
|
3083
|
+
return '<rect x="' + n(x) + '" y="' + n(y) + '" width="' + n(w) + '" height="' + n(h) + tail + '<line x1="' + n(x + 6) + '" y1="' + n(y) + '" x2="' + n(x + 6) + '" y2="' + n(y + h) + '" stroke="' + stroke + '" stroke-width="1" /><line x1="' + n(x + w - 6) + '" y1="' + n(y) + '" x2="' + n(x + w - 6) + '" y2="' + n(y + h) + '" stroke="' + stroke + '" stroke-width="1" />' + boxText(shape, stroke);
|
|
3084
|
+
default:
|
|
3085
|
+
return '<rect x="' + n(x) + '" y="' + n(y) + '" width="' + n(w) + '" height="' + n(h) + '" rx="6' + tail + boxText(shape, stroke);
|
|
3086
|
+
}
|
|
918
3087
|
},
|
|
919
3088
|
link: (shape, colours) => {
|
|
920
3089
|
const stroke = colours[shape.colour];
|
|
921
|
-
const
|
|
922
|
-
|
|
3090
|
+
const marks = edgeMarks(
|
|
3091
|
+
{ x: shape.x1, y: shape.y1 },
|
|
3092
|
+
{ x: shape.x2, y: shape.y2 },
|
|
3093
|
+
{ x: shape.x1, y: shape.y1 },
|
|
3094
|
+
{ x: shape.x2, y: shape.y2 },
|
|
3095
|
+
shape.arrow,
|
|
3096
|
+
stroke,
|
|
3097
|
+
colours.background
|
|
3098
|
+
);
|
|
3099
|
+
return '<line x1="' + n(shape.x1) + '" y1="' + n(shape.y1) + '" x2="' + n(shape.x2) + '" y2="' + n(shape.y2) + '" stroke="' + stroke + '" stroke-width="1"' + dash(shape) + " />" + marks + edgeLabel(shape.label, shape.labelX, shape.labelY, colours);
|
|
923
3100
|
},
|
|
924
3101
|
path: (shape, colours) => {
|
|
925
3102
|
const stroke = colours[shape.colour];
|
|
926
3103
|
const points = shape.points.map((point) => n(point.x) + "," + n(point.y)).join(" ");
|
|
927
3104
|
const last = shape.points[shape.points.length - 1];
|
|
928
3105
|
const before = shape.points[shape.points.length - 2] ?? last;
|
|
929
|
-
|
|
3106
|
+
const first = shape.points[0];
|
|
3107
|
+
const after = shape.points[1] ?? first;
|
|
3108
|
+
return '<polyline points="' + points + '" fill="none" stroke="' + stroke + '" stroke-width="1" stroke-linejoin="round"' + dash(shape) + " />" + edgeMarks(first, after, before, last, shape.arrow, stroke, colours.background) + edgeLabel(shape.label, shape.labelX, shape.labelY, colours);
|
|
930
3109
|
},
|
|
931
3110
|
title: (shape, colours) => {
|
|
932
3111
|
return '<text x="' + n(shape.x) + '" y="' + n(shape.y) + '" font-size="12" fill="' + colours[shape.colour] + '">' + escape(shape.text) + "</text>";
|
|
@@ -934,25 +3113,67 @@ window.__ModuleLoader__.load({
|
|
|
934
3113
|
caption: (shape, colours) => {
|
|
935
3114
|
const colour = colours[shape.colour];
|
|
936
3115
|
const lines = shape.lines.map((line, index) => '<tspan x="' + n(shape.x) + '" dy="' + (index === 0 ? 0 : 15) + '">' + escape(line) + "</tspan>").join("");
|
|
937
|
-
return '<text x="' + n(shape.x) + '" y="' + n(shape.y + 11) + '" font-size="10" fill="' + colour + '">' + lines + "</text>";
|
|
3116
|
+
return '<text x="' + n(shape.x) + '" y="' + n(shape.y + 11) + '" font-size="10" text-anchor="middle" fill="' + colour + '">' + lines + "</text>";
|
|
938
3117
|
}
|
|
939
3118
|
};
|
|
940
|
-
function
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
3119
|
+
function dash(shape) {
|
|
3120
|
+
return shape.dashed === true ? ' stroke-dasharray="4 3"' : "";
|
|
3121
|
+
}
|
|
3122
|
+
function edgeMarks(source, sourceNext, targetPrev, target, arrow, stroke, background) {
|
|
3123
|
+
if (arrow === "composition" || arrow === "aggregation") {
|
|
3124
|
+
return diamond(source, sourceNext, stroke, arrow === "composition" ? stroke : background);
|
|
3125
|
+
}
|
|
3126
|
+
if (arrow === "generalization" || arrow === "realization") {
|
|
3127
|
+
return hollowTriangle(targetPrev, target, stroke, background);
|
|
3128
|
+
}
|
|
3129
|
+
return arrowHead(targetPrev.x, targetPrev.y, target.x, target.y, stroke);
|
|
3130
|
+
}
|
|
3131
|
+
function tipAndBase(from, to) {
|
|
3132
|
+
const length = Math.hypot(to.x - from.x, to.y - from.y);
|
|
3133
|
+
if (length < 1) return void 0;
|
|
3134
|
+
const ux = (to.x - from.x) / length;
|
|
3135
|
+
const uy = (to.y - from.y) / length;
|
|
945
3136
|
const size = 7;
|
|
946
3137
|
const half = 3.2;
|
|
947
|
-
const baseX =
|
|
948
|
-
const baseY =
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
3138
|
+
const baseX = to.x - ux * size;
|
|
3139
|
+
const baseY = to.y - uy * size;
|
|
3140
|
+
return {
|
|
3141
|
+
tip: n(to.x) + "," + n(to.y),
|
|
3142
|
+
left: n(baseX - uy * half) + "," + n(baseY + ux * half),
|
|
3143
|
+
right: n(baseX + uy * half) + "," + n(baseY - ux * half)
|
|
3144
|
+
};
|
|
3145
|
+
}
|
|
3146
|
+
function arrowHead(x1, y1, x2, y2, stroke) {
|
|
3147
|
+
const points = tipAndBase({ x: x1, y: y1 }, { x: x2, y: y2 });
|
|
3148
|
+
if (points === void 0) return "";
|
|
3149
|
+
return '<polygon points="' + points.tip + " " + points.left + " " + points.right + '" fill="' + stroke + '" />';
|
|
3150
|
+
}
|
|
3151
|
+
function hollowTriangle(from, to, stroke, background) {
|
|
3152
|
+
const points = tipAndBase(from, to);
|
|
3153
|
+
if (points === void 0) return "";
|
|
3154
|
+
return '<polygon points="' + points.tip + " " + points.left + " " + points.right + '" fill="' + background + '" stroke="' + stroke + '" stroke-width="1" />';
|
|
3155
|
+
}
|
|
3156
|
+
function diamond(at, next, stroke, fill) {
|
|
3157
|
+
const length = Math.hypot(next.x - at.x, next.y - at.y);
|
|
3158
|
+
if (length < 1) return "";
|
|
3159
|
+
const ux = (next.x - at.x) / length;
|
|
3160
|
+
const uy = (next.y - at.y) / length;
|
|
3161
|
+
const size = 12;
|
|
3162
|
+
const half = 4;
|
|
3163
|
+
const back = { x: at.x + ux * size, y: at.y + uy * size };
|
|
3164
|
+
const middle = { x: at.x + ux * size / 2, y: at.y + uy * size / 2 };
|
|
3165
|
+
const left = { x: middle.x - uy * half, y: middle.y + ux * half };
|
|
3166
|
+
const right = { x: middle.x + uy * half, y: middle.y - ux * half };
|
|
3167
|
+
const points = [at, left, back, right].map((point) => n(point.x) + "," + n(point.y)).join(" ");
|
|
3168
|
+
return '<polygon points="' + points + '" fill="' + fill + '" stroke="' + stroke + '" stroke-width="1" />';
|
|
952
3169
|
}
|
|
953
3170
|
function edgeLabel(label, x, y, colours) {
|
|
954
3171
|
if (typeof label !== "string" || label === "") return "";
|
|
955
|
-
|
|
3172
|
+
const width = textWidth(label);
|
|
3173
|
+
const left = x - width / 2;
|
|
3174
|
+
const top = y - LABEL_HEIGHT / 2;
|
|
3175
|
+
const baseline = y + 3.5;
|
|
3176
|
+
return '<rect x="' + n(left) + '" y="' + n(top) + '" width="' + n(width) + '" height="' + n(LABEL_HEIGHT) + '" fill="' + colours.background + '" /><text x="' + n(x) + '" y="' + n(baseline) + '" font-size="10" text-anchor="middle" fill="' + colours.muted + '">' + escape(label) + "</text>";
|
|
956
3177
|
}
|
|
957
3178
|
function toSvg(canvas, colours, label) {
|
|
958
3179
|
let dropped = 0;
|