@adecore/diagram 0.21.1 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/across.js +2 -2
- package/dist/layout.js +84 -51
- package/dist/route.js +8 -8
- package/dist/shapes.js +10 -10
- package/dist/svg.js +1 -5
- package/package.json +2 -2
- package/src/across.ts +2 -2
- package/src/layout.ts +119 -56
- package/src/route.ts +8 -8
- package/src/shapes.ts +10 -10
- package/src/svg.ts +1 -5
package/dist/across.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BAND_MARGIN, bandOfCode } from './graph.js';
|
|
1
|
+
import { BAND_MARGIN, bandCode, bandOfCode } from './graph.js';
|
|
2
2
|
/* Rounds that pull every unit towards the units it is joined to, alternating the direction of the sweep. */
|
|
3
3
|
const ROUNDS = 8;
|
|
4
4
|
function clamp(value, low, high) {
|
|
@@ -165,7 +165,7 @@ export function placeAcross(layering) {
|
|
|
165
165
|
if (shifts.length === 0) {
|
|
166
166
|
return;
|
|
167
167
|
}
|
|
168
|
-
const code =
|
|
168
|
+
const code = bandCode(bandIndex);
|
|
169
169
|
let low = -Infinity;
|
|
170
170
|
let high = Infinity;
|
|
171
171
|
for (let layer = band.first; layer <= band.last; layer++) {
|
package/dist/layout.js
CHANGED
|
@@ -248,20 +248,12 @@ function looseRoute(source, target, loop, down) {
|
|
|
248
248
|
{ x: to.x, y: end }
|
|
249
249
|
]);
|
|
250
250
|
}
|
|
251
|
-
/*
|
|
252
|
-
|
|
253
|
-
* pinned nodes keep their position, and skipped layers give edges explicit routing points.
|
|
254
|
-
*/
|
|
255
|
-
export function layoutOf(document) {
|
|
256
|
-
const down = document.meta.direction === 'down';
|
|
257
|
-
const layers = layersOf(document.nodes, document.edges);
|
|
258
|
-
const sizes = document.nodes.map((node) => sizeOfNode(node));
|
|
259
|
-
const knownIds = new Set(document.nodes.map((node) => node.id));
|
|
260
|
-
// Every node that is not pinned is a unit, sized in the right-running frame.
|
|
251
|
+
/* Every node that is not pinned is a unit, sized in the right-running frame. */
|
|
252
|
+
function unitsOf(nodes, layers, sizes, down) {
|
|
261
253
|
const units = [];
|
|
262
254
|
const unitOf = new Map();
|
|
263
255
|
const unitOfNode = new Map();
|
|
264
|
-
|
|
256
|
+
nodes.forEach((node, index) => {
|
|
265
257
|
if (node.pos) {
|
|
266
258
|
return;
|
|
267
259
|
}
|
|
@@ -280,9 +272,12 @@ export function layoutOf(document) {
|
|
|
280
272
|
down: []
|
|
281
273
|
});
|
|
282
274
|
});
|
|
275
|
+
return { units, unitOf, unitOfNode };
|
|
276
|
+
}
|
|
277
|
+
function bandsOf(groups, units, unitOf, down) {
|
|
283
278
|
const bands = [];
|
|
284
279
|
const bandOfGroup = new Map();
|
|
285
|
-
|
|
280
|
+
groups.forEach((group, groupIndex) => {
|
|
286
281
|
const inside = group.wraps.flatMap((id) => {
|
|
287
282
|
const unit = unitOf.get(id);
|
|
288
283
|
return unit === undefined || units[unit].band >= 0 ? [] : [unit];
|
|
@@ -309,12 +304,16 @@ export function layoutOf(document) {
|
|
|
309
304
|
labelWidth: estimateTextWidth(group.label, SUB_SIZE, true)
|
|
310
305
|
});
|
|
311
306
|
});
|
|
307
|
+
return { bands, bandOfGroup };
|
|
308
|
+
}
|
|
309
|
+
/* Splits every edge between two units into hops of one layer each, adding a point to `units` for every layer it skips. */
|
|
310
|
+
function edgesOf(edges, units, unitOf, knownIds) {
|
|
312
311
|
const hops = [];
|
|
313
312
|
const chains = [];
|
|
314
313
|
const loops = [];
|
|
315
314
|
const loose = [];
|
|
316
315
|
const lines = [];
|
|
317
|
-
|
|
316
|
+
edges.forEach((edge, index) => {
|
|
318
317
|
if (!knownIds.has(edge.from) || !knownIds.has(edge.to)) {
|
|
319
318
|
return;
|
|
320
319
|
}
|
|
@@ -358,7 +357,10 @@ export function layoutOf(document) {
|
|
|
358
357
|
}
|
|
359
358
|
chains.push({ edge: index, hops: chain, reversed });
|
|
360
359
|
});
|
|
361
|
-
|
|
360
|
+
return { hops, chains, loops, loose, lines };
|
|
361
|
+
}
|
|
362
|
+
/* The top-level entries of every layer, with the members of each band filled in and its height measured. */
|
|
363
|
+
function itemsOf(units, bands, layerCount, down) {
|
|
362
364
|
const items = Array.from({ length: layerCount }, () => []);
|
|
363
365
|
units.forEach((unit, index) => {
|
|
364
366
|
if (unit.band < 0) {
|
|
@@ -388,11 +390,11 @@ export function layoutOf(document) {
|
|
|
388
390
|
band.height = Math.max(band.height, band.labelWidth + GROUP_LABEL_INSET * 2);
|
|
389
391
|
}
|
|
390
392
|
});
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
393
|
+
return items;
|
|
394
|
+
}
|
|
395
|
+
/* Every hop leaves its box on the side facing the flow and enters on the side facing back. */
|
|
396
|
+
function portsOf(units, top, hops, loops) {
|
|
397
|
+
const middleOf = (unit) => top[unit] + units[unit].h / 2;
|
|
396
398
|
const hopStart = new Array(hops.length).fill(0);
|
|
397
399
|
const hopEnd = new Array(hops.length).fill(0);
|
|
398
400
|
const loopStart = new Array(loops.length).fill(0);
|
|
@@ -400,11 +402,11 @@ export function layoutOf(document) {
|
|
|
400
402
|
const exits = units.map(() => []);
|
|
401
403
|
const entries = units.map(() => []);
|
|
402
404
|
hops.forEach((hop, index) => {
|
|
403
|
-
exits[hop.from].push({ key:
|
|
404
|
-
entries[hop.to].push({ key:
|
|
405
|
+
exits[hop.from].push({ key: middleOf(hop.to), edge: hop.edge, order: 0, apply: (y) => (hopStart[index] = y) });
|
|
406
|
+
entries[hop.to].push({ key: middleOf(hop.from), edge: hop.edge, order: 0, apply: (y) => (hopEnd[index] = y) });
|
|
405
407
|
});
|
|
406
408
|
loops.forEach((loop, index) => {
|
|
407
|
-
const key =
|
|
409
|
+
const key = middleOf(loop.unit);
|
|
408
410
|
exits[loop.unit].push({ key, edge: loop.edge, order: 0, apply: (y) => (loopStart[index] = y) }, { key, edge: loop.edge, order: 1, apply: (y) => (loopEnd[index] = y) });
|
|
409
411
|
});
|
|
410
412
|
const spread = (unit, ports) => {
|
|
@@ -413,7 +415,7 @@ export function layoutOf(document) {
|
|
|
413
415
|
}
|
|
414
416
|
// In the order of the other ends, so two hops leaving one box do not cross on the way out.
|
|
415
417
|
ports.sort((left, right) => left.key - right.key || left.edge - right.edge || left.order - right.order);
|
|
416
|
-
const middle =
|
|
418
|
+
const middle = middleOf(unit);
|
|
417
419
|
const room = Math.max(0, units[unit].h - PORT_INSET * 2);
|
|
418
420
|
const step = ports.length > 1 ? Math.min(PORT_GAP, Math.floor(room / (ports.length - 1))) : 0;
|
|
419
421
|
ports.forEach((port, index) => port.apply(Math.round(middle + (index - (ports.length - 1) / 2) * step)));
|
|
@@ -435,6 +437,12 @@ export function layoutOf(document) {
|
|
|
435
437
|
hopStart[index] = hopEnd[index];
|
|
436
438
|
}
|
|
437
439
|
});
|
|
440
|
+
return { hopStart, hopEnd, loopStart, loopEnd };
|
|
441
|
+
}
|
|
442
|
+
/* Places the layers along the flow, each followed by the channel its hops and labels are routed through. */
|
|
443
|
+
function placeAlong(layering, edges, ports, labelSizes, layerCount, down) {
|
|
444
|
+
const { units, bands } = layering;
|
|
445
|
+
const { hops, chains, loops } = edges;
|
|
438
446
|
const byLayer = Array.from({ length: layerCount }, () => []);
|
|
439
447
|
units.forEach((unit, index) => byLayer[unit.layer].push(index));
|
|
440
448
|
const columnWidth = byLayer.map((column) => even(column.reduce((widest, unit) => Math.max(widest, units[unit].w), 0)));
|
|
@@ -442,7 +450,6 @@ export function layoutOf(document) {
|
|
|
442
450
|
const unitX = new Array(units.length).fill(0);
|
|
443
451
|
const hopsByLayer = Array.from({ length: layerCount }, () => []);
|
|
444
452
|
hops.forEach((hop, index) => hopsByLayer[units[hop.from].layer].push(index));
|
|
445
|
-
const labelSizes = document.edges.map((edge) => (edge.label ? sizeOfEdgeLabel(edge.label) : null));
|
|
446
453
|
const labelHop = new Map();
|
|
447
454
|
for (const chain of chains) {
|
|
448
455
|
if (labelSizes[chain.edge]) {
|
|
@@ -452,7 +459,6 @@ export function layoutOf(document) {
|
|
|
452
459
|
const frameLabels = new Map();
|
|
453
460
|
const trackX = new Map();
|
|
454
461
|
const alongBefore = GROUP_PADDING + (down ? GROUP_LABEL_BAND : 0);
|
|
455
|
-
const bandAlongStart = (band) => Math.min(...band.members[0].map((unit) => unitX[unit])) - alongBefore;
|
|
456
462
|
for (let layer = 0; layer < layerCount; layer++) {
|
|
457
463
|
for (const unit of byLayer[layer]) {
|
|
458
464
|
unitX[unit] = columnX[layer] + Math.floor((columnWidth[layer] - units[unit].w) / 2);
|
|
@@ -461,19 +467,19 @@ export function layoutOf(document) {
|
|
|
461
467
|
break;
|
|
462
468
|
}
|
|
463
469
|
const start = columnX[layer] + columnWidth[layer];
|
|
464
|
-
const channelHops = hopsByLayer[layer].map((hop) => ({ hop, ya: hopStart[hop], yb: hopEnd[hop] }));
|
|
470
|
+
const channelHops = hopsByLayer[layer].map((hop) => ({ hop, ya: ports.hopStart[hop], yb: ports.hopEnd[hop] }));
|
|
465
471
|
const channelLabels = [];
|
|
466
472
|
for (const hop of hopsByLayer[layer]) {
|
|
467
473
|
const edge = labelHop.get(hop);
|
|
468
474
|
const size = edge === undefined ? null : labelSizes[edge];
|
|
469
475
|
if (edge !== undefined && size) {
|
|
470
|
-
channelLabels.push({ edge, hop, y: hopStart[hop], along: down ? size.h : size.w, across: down ? size.w : size.h });
|
|
476
|
+
channelLabels.push({ edge, hop, y: ports.hopStart[hop], along: down ? size.h : size.w, across: down ? size.w : size.h });
|
|
471
477
|
}
|
|
472
478
|
}
|
|
473
479
|
loops.forEach((loop, index) => {
|
|
474
480
|
const size = labelSizes[loop.edge];
|
|
475
481
|
if (size && units[loop.unit].layer === layer) {
|
|
476
|
-
channelLabels.push({ edge: loop.edge, hop: -1, y: loopEnd[index], along: down ? size.h : size.w, across: down ? size.w : size.h });
|
|
482
|
+
channelLabels.push({ edge: loop.edge, hop: -1, y: ports.loopEnd[index], along: down ? size.h : size.w, across: down ? size.w : size.h });
|
|
477
483
|
}
|
|
478
484
|
});
|
|
479
485
|
channelLabels.sort((left, right) => left.edge - right.edge);
|
|
@@ -499,6 +505,46 @@ export function layoutOf(document) {
|
|
|
499
505
|
channel.labels.forEach((rect, edge) => frameLabels.set(edge, rect));
|
|
500
506
|
columnX[layer + 1] = start + channel.width;
|
|
501
507
|
}
|
|
508
|
+
return { columnX, columnWidth, unitX, trackX, frameLabels };
|
|
509
|
+
}
|
|
510
|
+
/* A label that no channel placed (a loop in the last layer, an edge with a pinned end) moves down until it is clear. */
|
|
511
|
+
function settleLabels(edges, unplaced, nodes, groups) {
|
|
512
|
+
const settled = edges.flatMap((edge) => (edge.label && !unplaced.has(edge.index) ? [edge.label] : []));
|
|
513
|
+
for (const edge of edges) {
|
|
514
|
+
const label = edge.label;
|
|
515
|
+
if (!label || !unplaced.has(edge.index)) {
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
const taken = () => [...nodes, ...groups.map((group) => group.labelBox)].some((box) => intersects(box, label)) ||
|
|
519
|
+
settled.some((other) => intersects({ x: other.x - 4, y: other.y - 4, w: other.w + 8, h: other.h + 8 }, label));
|
|
520
|
+
for (let step = 0; step < 1000 && taken(); step++) {
|
|
521
|
+
label.y += 4;
|
|
522
|
+
}
|
|
523
|
+
settled.push(label);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
/*
|
|
527
|
+
* Deterministic layered layout with integer coordinates. Barycenter sweeps order unpinned nodes;
|
|
528
|
+
* pinned nodes keep their position, and skipped layers give edges explicit routing points.
|
|
529
|
+
*/
|
|
530
|
+
export function layoutOf(document) {
|
|
531
|
+
const down = document.meta.direction === 'down';
|
|
532
|
+
const layers = layersOf(document.nodes, document.edges);
|
|
533
|
+
const sizes = document.nodes.map((node) => sizeOfNode(node));
|
|
534
|
+
const knownIds = new Set(document.nodes.map((node) => node.id));
|
|
535
|
+
const { units, unitOf, unitOfNode } = unitsOf(document.nodes, layers, sizes, down);
|
|
536
|
+
const { bands, bandOfGroup } = bandsOf(document.groups, units, unitOf, down);
|
|
537
|
+
const graphEdges = edgesOf(document.edges, units, unitOf, knownIds);
|
|
538
|
+
const { hops, chains, loops, loose } = graphEdges;
|
|
539
|
+
const layerCount = units.reduce((count, unit) => Math.max(count, unit.layer + 1), 0);
|
|
540
|
+
const items = itemsOf(units, bands, layerCount, down);
|
|
541
|
+
const layering = { units, hops, bands, lines: graphEdges.lines, items };
|
|
542
|
+
orderLayers(layering);
|
|
543
|
+
const top = placeAcross(layering);
|
|
544
|
+
const middleOf = (unit) => top[unit] + units[unit].h / 2;
|
|
545
|
+
const ports = portsOf(units, top, hops, loops);
|
|
546
|
+
const labelSizes = document.edges.map((edge) => (edge.label ? sizeOfEdgeLabel(edge.label) : null));
|
|
547
|
+
const { columnX, columnWidth, unitX, trackX, frameLabels } = placeAlong(layering, graphEdges, ports, labelSizes, layerCount, down);
|
|
502
548
|
const frameBoxOf = (unit) => ({ x: unitX[unit], y: top[unit], w: units[unit].w, h: units[unit].h });
|
|
503
549
|
const shapeOf = (unit) => document.nodes[units[unit].node].shape;
|
|
504
550
|
const exitX = (unit, y) => {
|
|
@@ -507,7 +553,7 @@ export function layoutOf(document) {
|
|
|
507
553
|
return columnX[self.layer] + columnWidth[self.layer];
|
|
508
554
|
}
|
|
509
555
|
const box = frameBoxOf(unit);
|
|
510
|
-
return box.x + box.w - insetOf(shapeOf(unit), box, y -
|
|
556
|
+
return box.x + box.w - insetOf(shapeOf(unit), box, y - middleOf(unit), down);
|
|
511
557
|
};
|
|
512
558
|
const entryX = (unit, y) => {
|
|
513
559
|
const self = units[unit];
|
|
@@ -515,7 +561,7 @@ export function layoutOf(document) {
|
|
|
515
561
|
return columnX[self.layer];
|
|
516
562
|
}
|
|
517
563
|
const box = frameBoxOf(unit);
|
|
518
|
-
return box.x + insetOf(shapeOf(unit), box, y -
|
|
564
|
+
return box.x + insetOf(shapeOf(unit), box, y - middleOf(unit), down);
|
|
519
565
|
};
|
|
520
566
|
const toReal = (point) => (down ? flipPoint(point) : point);
|
|
521
567
|
const labelOf = (edge, frame) => {
|
|
@@ -553,8 +599,8 @@ export function layoutOf(document) {
|
|
|
553
599
|
const points = [];
|
|
554
600
|
for (const index of chain.hops) {
|
|
555
601
|
const hop = hops[index];
|
|
556
|
-
const ya = hopStart[index];
|
|
557
|
-
const yb = hopEnd[index];
|
|
602
|
+
const ya = ports.hopStart[index];
|
|
603
|
+
const yb = ports.hopEnd[index];
|
|
558
604
|
points.push({ x: exitX(hop.from, ya), y: ya });
|
|
559
605
|
if (ya !== yb) {
|
|
560
606
|
const x = trackX.get(index);
|
|
@@ -575,8 +621,8 @@ export function layoutOf(document) {
|
|
|
575
621
|
loops.forEach((loop, index) => {
|
|
576
622
|
const box = frameBoxOf(loop.unit);
|
|
577
623
|
const reach = box.x + box.w + LOOP;
|
|
578
|
-
const startY = loopStart[index];
|
|
579
|
-
const endY = loopEnd[index];
|
|
624
|
+
const startY = ports.loopStart[index];
|
|
625
|
+
const endY = ports.loopEnd[index];
|
|
580
626
|
const points = [
|
|
581
627
|
{ x: exitX(loop.unit, startY), y: startY },
|
|
582
628
|
{ x: reach, y: startY },
|
|
@@ -617,15 +663,15 @@ export function layoutOf(document) {
|
|
|
617
663
|
routes[index] = { index, from: edge.from, to: edge.to, points, label };
|
|
618
664
|
}
|
|
619
665
|
const edges = routes.filter((route) => route !== undefined);
|
|
620
|
-
const
|
|
666
|
+
const alongBefore = GROUP_PADDING + (down ? GROUP_LABEL_BAND : 0);
|
|
621
667
|
const groups = document.groups.flatMap((group, groupIndex) => {
|
|
622
668
|
const rects = [];
|
|
623
669
|
const bandIndex = bandOfGroup.get(groupIndex);
|
|
624
670
|
if (bandIndex !== undefined) {
|
|
625
671
|
const band = bands[bandIndex];
|
|
626
672
|
const members = band.members.flat().filter((unit) => units[unit].node >= 0);
|
|
627
|
-
const alongStart =
|
|
628
|
-
const alongEnd = Math.max(...members.map((unit) => unitX[unit] + units[unit].w)) +
|
|
673
|
+
const alongStart = Math.min(...band.members[0].map((unit) => unitX[unit])) - alongBefore;
|
|
674
|
+
const alongEnd = Math.max(...members.map((unit) => unitX[unit] + units[unit].w)) + GROUP_PADDING;
|
|
629
675
|
const frame = { x: alongStart, y: band.top, w: alongEnd - alongStart, h: band.height };
|
|
630
676
|
rects.push(down ? flip(frame) : frame);
|
|
631
677
|
}
|
|
@@ -647,20 +693,7 @@ export function layoutOf(document) {
|
|
|
647
693
|
const labelBox = { x: rect.x + GROUP_LABEL_INSET, y: rect.y + 4, w: estimateTextWidth(group.label, SUB_SIZE, true), h: SUB_LINE };
|
|
648
694
|
return [{ id: group.id, ...rect, labelBox }];
|
|
649
695
|
});
|
|
650
|
-
|
|
651
|
-
const settled = edges.flatMap((edge) => (edge.label && !unplaced.has(edge.index) ? [edge.label] : []));
|
|
652
|
-
for (const edge of edges) {
|
|
653
|
-
const label = edge.label;
|
|
654
|
-
if (!label || !unplaced.has(edge.index)) {
|
|
655
|
-
continue;
|
|
656
|
-
}
|
|
657
|
-
const taken = () => [...nodes, ...groups.map((group) => group.labelBox)].some((box) => intersects(box, label)) ||
|
|
658
|
-
settled.some((other) => intersects({ x: other.x - 4, y: other.y - 4, w: other.w + 8, h: other.h + 8 }, label));
|
|
659
|
-
for (let step = 0; step < 1000 && taken(); step++) {
|
|
660
|
-
label.y += 4;
|
|
661
|
-
}
|
|
662
|
-
settled.push(label);
|
|
663
|
-
}
|
|
696
|
+
settleLabels(edges, unplaced, nodes, groups);
|
|
664
697
|
const corners = edges.flatMap((edge) => edge.points.map((point) => ({ ...point, w: 0, h: 0 })));
|
|
665
698
|
const labels = edges.flatMap((edge) => (edge.label ? [edge.label] : []));
|
|
666
699
|
const groupLabels = groups.map((group) => group.labelBox);
|
package/dist/route.js
CHANGED
|
@@ -86,12 +86,12 @@ export function routeChannel(input) {
|
|
|
86
86
|
const order = vertical.map((_, i) => i).sort((left, right) => score[left] - score[right] || left - right);
|
|
87
87
|
for (let pass = 0; pass < count; pass++) {
|
|
88
88
|
let swapped = false;
|
|
89
|
-
for (let
|
|
90
|
-
const left = order[
|
|
91
|
-
const right = order[
|
|
89
|
+
for (let slot = 0; slot + 1 < count; slot++) {
|
|
90
|
+
const left = order[slot];
|
|
91
|
+
const right = order[slot + 1];
|
|
92
92
|
if (cost[right * count + left] < cost[left * count + right]) {
|
|
93
|
-
order[
|
|
94
|
-
order[
|
|
93
|
+
order[slot] = right;
|
|
94
|
+
order[slot + 1] = left;
|
|
95
95
|
swapped = true;
|
|
96
96
|
}
|
|
97
97
|
}
|
|
@@ -101,10 +101,10 @@ export function routeChannel(input) {
|
|
|
101
101
|
}
|
|
102
102
|
const track = new Array(count).fill(0);
|
|
103
103
|
let tracks = 0;
|
|
104
|
-
order.forEach((index,
|
|
104
|
+
order.forEach((index, slot) => {
|
|
105
105
|
let at = 0;
|
|
106
|
-
for (let
|
|
107
|
-
const other = order[
|
|
106
|
+
for (let earlier = 0; earlier < slot; earlier++) {
|
|
107
|
+
const other = order[earlier];
|
|
108
108
|
if (near(vertical[other], vertical[index])) {
|
|
109
109
|
at = Math.max(at, track[other] + 1);
|
|
110
110
|
}
|
package/dist/shapes.js
CHANGED
|
@@ -6,19 +6,19 @@ const LID_RATIO = 0.18;
|
|
|
6
6
|
const HEAD_LENGTH = 10;
|
|
7
7
|
const HEAD_WIDTH = 6;
|
|
8
8
|
function roundedRect(box, radius) {
|
|
9
|
-
const
|
|
9
|
+
const corner = Math.round(Math.min(radius, box.w / 2, box.h / 2));
|
|
10
10
|
const right = box.x + box.w;
|
|
11
11
|
const bottom = box.y + box.h;
|
|
12
12
|
return [
|
|
13
|
-
`M${box.x +
|
|
14
|
-
`H${right -
|
|
15
|
-
`A${
|
|
16
|
-
`V${bottom -
|
|
17
|
-
`A${
|
|
18
|
-
`H${box.x +
|
|
19
|
-
`A${
|
|
20
|
-
`V${box.y +
|
|
21
|
-
`A${
|
|
13
|
+
`M${box.x + corner} ${box.y}`,
|
|
14
|
+
`H${right - corner}`,
|
|
15
|
+
`A${corner} ${corner} 0 0 1 ${right} ${box.y + corner}`,
|
|
16
|
+
`V${bottom - corner}`,
|
|
17
|
+
`A${corner} ${corner} 0 0 1 ${right - corner} ${bottom}`,
|
|
18
|
+
`H${box.x + corner}`,
|
|
19
|
+
`A${corner} ${corner} 0 0 1 ${box.x} ${bottom - corner}`,
|
|
20
|
+
`V${box.y + corner}`,
|
|
21
|
+
`A${corner} ${corner} 0 0 1 ${box.x + corner} ${box.y}`,
|
|
22
22
|
'Z'
|
|
23
23
|
].join(' ');
|
|
24
24
|
}
|
package/dist/svg.js
CHANGED
|
@@ -13,11 +13,7 @@ const NOT_XML = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\uFFFE\uFFFF]/g;
|
|
|
13
13
|
function escapeXml(value) {
|
|
14
14
|
return value.replace(NOT_XML, '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
15
15
|
}
|
|
16
|
-
/*
|
|
17
|
-
* The whole diagram as one SVG string, for an export and for what the daemon hands an agent. The
|
|
18
|
-
* view in the app draws the same layout and the same paths as elements of its own, so the two only
|
|
19
|
-
* differ in where the colors come from.
|
|
20
|
-
*/
|
|
16
|
+
/* The whole diagram as one SVG string, from the same layout and paths a view draws with its own elements. */
|
|
21
17
|
export function toSvg(document, options = {}) {
|
|
22
18
|
const palette = options.palette ?? DEFAULT_PALETTE;
|
|
23
19
|
const paper = options.paper ?? DEFAULT_PAPER;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adecore/diagram",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "Directed graph schemas, layered layout, SVG export and reading order.",
|
|
5
5
|
"license": "FSL-1.1-MIT",
|
|
6
6
|
"author": "Bas Milius",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"typescript": "^7.0.2"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@adecore/drawing": "0.
|
|
47
|
+
"@adecore/drawing": "0.22.0",
|
|
48
48
|
"zod": "^4.6.5"
|
|
49
49
|
}
|
|
50
50
|
}
|
package/src/across.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BAND_MARGIN, bandOfCode, type Layering } from './graph.ts';
|
|
1
|
+
import { BAND_MARGIN, bandCode, bandOfCode, type Layering } from './graph.ts';
|
|
2
2
|
|
|
3
3
|
/* Rounds that pull every unit towards the units it is joined to, alternating the direction of the sweep. */
|
|
4
4
|
const ROUNDS = 8;
|
|
@@ -175,7 +175,7 @@ export function placeAcross(layering: Layering): number[] {
|
|
|
175
175
|
if (shifts.length === 0) {
|
|
176
176
|
return;
|
|
177
177
|
}
|
|
178
|
-
const code =
|
|
178
|
+
const code = bandCode(bandIndex);
|
|
179
179
|
let low = -Infinity;
|
|
180
180
|
let high = Infinity;
|
|
181
181
|
for (let layer = band.first; layer <= band.last; layer++) {
|
package/src/layout.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { DiagramDocument, DiagramEdge, DiagramNode, DiagramShape } from './protocol.ts';
|
|
1
|
+
import type { DiagramDocument, DiagramEdge, DiagramGroup, DiagramNode, DiagramShape } from './protocol.ts';
|
|
2
2
|
import { centerOf, intersects, roundPoint, type Point, type Rect } from '@adecore/drawing';
|
|
3
3
|
import { placeAcross } from './across.ts';
|
|
4
|
-
import { DUMMY_MARGIN, NODE_MARGIN, bandCode, type Band, type Hop, type Unit } from './graph.ts';
|
|
4
|
+
import { DUMMY_MARGIN, NODE_MARGIN, bandCode, type Band, type Hop, type Layering, type Unit } from './graph.ts';
|
|
5
5
|
import { orderLayers } from './order.ts';
|
|
6
6
|
import { routeChannel, type ChannelHop, type ChannelLabel } from './route.ts';
|
|
7
7
|
import { LABEL_LINE, LABEL_SIZE, SUB_LINE, SUB_SIZE, estimateTextWidth, widestLine, wrapText } from './text.ts';
|
|
@@ -336,21 +336,30 @@ interface Port {
|
|
|
336
336
|
apply: (y: number) => void;
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
const knownIds = new Set(document.nodes.map((node) => node.id));
|
|
339
|
+
interface Edges {
|
|
340
|
+
hops: Hop[];
|
|
341
|
+
chains: Chain[];
|
|
342
|
+
loops: Loop[];
|
|
343
|
+
/* Edges with a pinned end, or with both ends in one layer: routed straight, outside the layering. */
|
|
344
|
+
loose: number[];
|
|
345
|
+
lines: number[][];
|
|
346
|
+
}
|
|
348
347
|
|
|
349
|
-
|
|
348
|
+
interface Ports {
|
|
349
|
+
hopStart: number[];
|
|
350
|
+
hopEnd: number[];
|
|
351
|
+
loopStart: number[];
|
|
352
|
+
loopEnd: number[];
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
type LabelSize = ReturnType<typeof sizeOfEdgeLabel>;
|
|
356
|
+
|
|
357
|
+
/* Every node that is not pinned is a unit, sized in the right-running frame. */
|
|
358
|
+
function unitsOf(nodes: readonly DiagramNode[], layers: ReadonlyMap<string, number>, sizes: readonly NodeSize[], down: boolean) {
|
|
350
359
|
const units: Unit[] = [];
|
|
351
360
|
const unitOf = new Map<string, number>();
|
|
352
361
|
const unitOfNode = new Map<number, number>();
|
|
353
|
-
|
|
362
|
+
nodes.forEach((node, index) => {
|
|
354
363
|
if (node.pos) {
|
|
355
364
|
return;
|
|
356
365
|
}
|
|
@@ -369,10 +378,13 @@ export function layoutOf(document: Pick<DiagramDocument, 'meta' | 'nodes' | 'gro
|
|
|
369
378
|
down: []
|
|
370
379
|
});
|
|
371
380
|
});
|
|
381
|
+
return { units, unitOf, unitOfNode };
|
|
382
|
+
}
|
|
372
383
|
|
|
384
|
+
function bandsOf(groups: readonly DiagramGroup[], units: Unit[], unitOf: ReadonlyMap<string, number>, down: boolean) {
|
|
373
385
|
const bands: Band[] = [];
|
|
374
386
|
const bandOfGroup = new Map<number, number>();
|
|
375
|
-
|
|
387
|
+
groups.forEach((group, groupIndex) => {
|
|
376
388
|
const inside = group.wraps.flatMap((id) => {
|
|
377
389
|
const unit = unitOf.get(id);
|
|
378
390
|
return unit === undefined || units[unit]!.band >= 0 ? [] : [unit];
|
|
@@ -399,13 +411,17 @@ export function layoutOf(document: Pick<DiagramDocument, 'meta' | 'nodes' | 'gro
|
|
|
399
411
|
labelWidth: estimateTextWidth(group.label, SUB_SIZE, true)
|
|
400
412
|
});
|
|
401
413
|
});
|
|
414
|
+
return { bands, bandOfGroup };
|
|
415
|
+
}
|
|
402
416
|
|
|
417
|
+
/* Splits every edge between two units into hops of one layer each, adding a point to `units` for every layer it skips. */
|
|
418
|
+
function edgesOf(edges: readonly DiagramEdge[], units: Unit[], unitOf: ReadonlyMap<string, number>, knownIds: ReadonlySet<string>): Edges {
|
|
403
419
|
const hops: Hop[] = [];
|
|
404
420
|
const chains: Chain[] = [];
|
|
405
421
|
const loops: Loop[] = [];
|
|
406
422
|
const loose: number[] = [];
|
|
407
423
|
const lines: number[][] = [];
|
|
408
|
-
|
|
424
|
+
edges.forEach((edge, index) => {
|
|
409
425
|
if (!knownIds.has(edge.from) || !knownIds.has(edge.to)) {
|
|
410
426
|
return;
|
|
411
427
|
}
|
|
@@ -449,8 +465,11 @@ export function layoutOf(document: Pick<DiagramDocument, 'meta' | 'nodes' | 'gro
|
|
|
449
465
|
}
|
|
450
466
|
chains.push({ edge: index, hops: chain, reversed });
|
|
451
467
|
});
|
|
468
|
+
return { hops, chains, loops, loose, lines };
|
|
469
|
+
}
|
|
452
470
|
|
|
453
|
-
|
|
471
|
+
/* The top-level entries of every layer, with the members of each band filled in and its height measured. */
|
|
472
|
+
function itemsOf(units: readonly Unit[], bands: readonly Band[], layerCount: number, down: boolean): number[][] {
|
|
454
473
|
const items: number[][] = Array.from({ length: layerCount }, () => []);
|
|
455
474
|
units.forEach((unit, index) => {
|
|
456
475
|
if (unit.band < 0) {
|
|
@@ -483,13 +502,12 @@ export function layoutOf(document: Pick<DiagramDocument, 'meta' | 'nodes' | 'gro
|
|
|
483
502
|
band.height = Math.max(band.height, band.labelWidth + GROUP_LABEL_INSET * 2);
|
|
484
503
|
}
|
|
485
504
|
});
|
|
505
|
+
return items;
|
|
506
|
+
}
|
|
486
507
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
const
|
|
490
|
-
const centerOf = (unit: number): number => top[unit]! + units[unit]!.h / 2;
|
|
491
|
-
|
|
492
|
-
// Ports: every hop leaves its box on the side facing the flow and enters on the side facing back.
|
|
508
|
+
/* Every hop leaves its box on the side facing the flow and enters on the side facing back. */
|
|
509
|
+
function portsOf(units: readonly Unit[], top: readonly number[], hops: readonly Hop[], loops: readonly Loop[]): Ports {
|
|
510
|
+
const middleOf = (unit: number): number => top[unit]! + units[unit]!.h / 2;
|
|
493
511
|
const hopStart = new Array<number>(hops.length).fill(0);
|
|
494
512
|
const hopEnd = new Array<number>(hops.length).fill(0);
|
|
495
513
|
const loopStart = new Array<number>(loops.length).fill(0);
|
|
@@ -497,11 +515,11 @@ export function layoutOf(document: Pick<DiagramDocument, 'meta' | 'nodes' | 'gro
|
|
|
497
515
|
const exits: Port[][] = units.map(() => []);
|
|
498
516
|
const entries: Port[][] = units.map(() => []);
|
|
499
517
|
hops.forEach((hop, index) => {
|
|
500
|
-
exits[hop.from]!.push({ key:
|
|
501
|
-
entries[hop.to]!.push({ key:
|
|
518
|
+
exits[hop.from]!.push({ key: middleOf(hop.to), edge: hop.edge, order: 0, apply: (y) => (hopStart[index] = y) });
|
|
519
|
+
entries[hop.to]!.push({ key: middleOf(hop.from), edge: hop.edge, order: 0, apply: (y) => (hopEnd[index] = y) });
|
|
502
520
|
});
|
|
503
521
|
loops.forEach((loop, index) => {
|
|
504
|
-
const key =
|
|
522
|
+
const key = middleOf(loop.unit);
|
|
505
523
|
exits[loop.unit]!.push(
|
|
506
524
|
{ key, edge: loop.edge, order: 0, apply: (y) => (loopStart[index] = y) },
|
|
507
525
|
{ key, edge: loop.edge, order: 1, apply: (y) => (loopEnd[index] = y) }
|
|
@@ -513,7 +531,7 @@ export function layoutOf(document: Pick<DiagramDocument, 'meta' | 'nodes' | 'gro
|
|
|
513
531
|
}
|
|
514
532
|
// In the order of the other ends, so two hops leaving one box do not cross on the way out.
|
|
515
533
|
ports.sort((left, right) => left.key - right.key || left.edge - right.edge || left.order - right.order);
|
|
516
|
-
const middle =
|
|
534
|
+
const middle = middleOf(unit);
|
|
517
535
|
const room = Math.max(0, units[unit]!.h - PORT_INSET * 2);
|
|
518
536
|
const step = ports.length > 1 ? Math.min(PORT_GAP, Math.floor(room / (ports.length - 1))) : 0;
|
|
519
537
|
ports.forEach((port, index) => port.apply(Math.round(middle + (index - (ports.length - 1) / 2) * step)));
|
|
@@ -535,7 +553,23 @@ export function layoutOf(document: Pick<DiagramDocument, 'meta' | 'nodes' | 'gro
|
|
|
535
553
|
hopStart[index] = hopEnd[index]!;
|
|
536
554
|
}
|
|
537
555
|
});
|
|
556
|
+
return { hopStart, hopEnd, loopStart, loopEnd };
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
interface Columns {
|
|
560
|
+
columnX: number[];
|
|
561
|
+
columnWidth: number[];
|
|
562
|
+
unitX: number[];
|
|
563
|
+
/* Per hop that bends, where its run across the flow sits in the channel after its layer. */
|
|
564
|
+
trackX: Map<number, number>;
|
|
565
|
+
/* Per edge, its label in the right-running frame. */
|
|
566
|
+
frameLabels: Map<number, Rect>;
|
|
567
|
+
}
|
|
538
568
|
|
|
569
|
+
/* Places the layers along the flow, each followed by the channel its hops and labels are routed through. */
|
|
570
|
+
function placeAlong(layering: Layering, edges: Edges, ports: Ports, labelSizes: readonly (LabelSize | null)[], layerCount: number, down: boolean): Columns {
|
|
571
|
+
const { units, bands } = layering;
|
|
572
|
+
const { hops, chains, loops } = edges;
|
|
539
573
|
const byLayer: number[][] = Array.from({ length: layerCount }, () => []);
|
|
540
574
|
units.forEach((unit, index) => byLayer[unit.layer]!.push(index));
|
|
541
575
|
const columnWidth = byLayer.map((column) => even(column.reduce((widest, unit) => Math.max(widest, units[unit]!.w), 0)));
|
|
@@ -544,7 +578,6 @@ export function layoutOf(document: Pick<DiagramDocument, 'meta' | 'nodes' | 'gro
|
|
|
544
578
|
const hopsByLayer: number[][] = Array.from({ length: layerCount }, () => []);
|
|
545
579
|
hops.forEach((hop, index) => hopsByLayer[units[hop.from]!.layer]!.push(index));
|
|
546
580
|
|
|
547
|
-
const labelSizes = document.edges.map((edge) => (edge.label ? sizeOfEdgeLabel(edge.label) : null));
|
|
548
581
|
const labelHop = new Map<number, number>();
|
|
549
582
|
for (const chain of chains) {
|
|
550
583
|
if (labelSizes[chain.edge]) {
|
|
@@ -553,9 +586,8 @@ export function layoutOf(document: Pick<DiagramDocument, 'meta' | 'nodes' | 'gro
|
|
|
553
586
|
}
|
|
554
587
|
const frameLabels = new Map<number, Rect>();
|
|
555
588
|
const trackX = new Map<number, number>();
|
|
556
|
-
|
|
557
589
|
const alongBefore = GROUP_PADDING + (down ? GROUP_LABEL_BAND : 0);
|
|
558
|
-
|
|
590
|
+
|
|
559
591
|
for (let layer = 0; layer < layerCount; layer++) {
|
|
560
592
|
for (const unit of byLayer[layer]!) {
|
|
561
593
|
unitX[unit] = columnX[layer]! + Math.floor((columnWidth[layer]! - units[unit]!.w) / 2);
|
|
@@ -564,19 +596,19 @@ export function layoutOf(document: Pick<DiagramDocument, 'meta' | 'nodes' | 'gro
|
|
|
564
596
|
break;
|
|
565
597
|
}
|
|
566
598
|
const start = columnX[layer]! + columnWidth[layer]!;
|
|
567
|
-
const channelHops: ChannelHop[] = hopsByLayer[layer]!.map((hop) => ({ hop, ya: hopStart[hop]!, yb: hopEnd[hop]! }));
|
|
599
|
+
const channelHops: ChannelHop[] = hopsByLayer[layer]!.map((hop) => ({ hop, ya: ports.hopStart[hop]!, yb: ports.hopEnd[hop]! }));
|
|
568
600
|
const channelLabels: ChannelLabel[] = [];
|
|
569
601
|
for (const hop of hopsByLayer[layer]!) {
|
|
570
602
|
const edge = labelHop.get(hop);
|
|
571
603
|
const size = edge === undefined ? null : labelSizes[edge];
|
|
572
604
|
if (edge !== undefined && size) {
|
|
573
|
-
channelLabels.push({ edge, hop, y: hopStart[hop]!, along: down ? size.h : size.w, across: down ? size.w : size.h });
|
|
605
|
+
channelLabels.push({ edge, hop, y: ports.hopStart[hop]!, along: down ? size.h : size.w, across: down ? size.w : size.h });
|
|
574
606
|
}
|
|
575
607
|
}
|
|
576
608
|
loops.forEach((loop, index) => {
|
|
577
609
|
const size = labelSizes[loop.edge];
|
|
578
610
|
if (size && units[loop.unit]!.layer === layer) {
|
|
579
|
-
channelLabels.push({ edge: loop.edge, hop: -1, y: loopEnd[index]!, along: down ? size.h : size.w, across: down ? size.w : size.h });
|
|
611
|
+
channelLabels.push({ edge: loop.edge, hop: -1, y: ports.loopEnd[index]!, along: down ? size.h : size.w, across: down ? size.w : size.h });
|
|
580
612
|
}
|
|
581
613
|
});
|
|
582
614
|
channelLabels.sort((left, right) => left.edge - right.edge);
|
|
@@ -602,6 +634,51 @@ export function layoutOf(document: Pick<DiagramDocument, 'meta' | 'nodes' | 'gro
|
|
|
602
634
|
channel.labels.forEach((rect, edge) => frameLabels.set(edge, rect));
|
|
603
635
|
columnX[layer + 1] = start + channel.width;
|
|
604
636
|
}
|
|
637
|
+
return { columnX, columnWidth, unitX, trackX, frameLabels };
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/* A label that no channel placed (a loop in the last layer, an edge with a pinned end) moves down until it is clear. */
|
|
641
|
+
function settleLabels(edges: readonly EdgeRoute[], unplaced: ReadonlySet<number>, nodes: readonly NodeBox[], groups: readonly GroupBox[]): void {
|
|
642
|
+
const settled = edges.flatMap((edge) => (edge.label && !unplaced.has(edge.index) ? [edge.label] : []));
|
|
643
|
+
for (const edge of edges) {
|
|
644
|
+
const label = edge.label;
|
|
645
|
+
if (!label || !unplaced.has(edge.index)) {
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
const taken = (): boolean =>
|
|
649
|
+
[...nodes, ...groups.map((group) => group.labelBox)].some((box) => intersects(box, label)) ||
|
|
650
|
+
settled.some((other) => intersects({ x: other.x - 4, y: other.y - 4, w: other.w + 8, h: other.h + 8 }, label));
|
|
651
|
+
for (let step = 0; step < 1000 && taken(); step++) {
|
|
652
|
+
label.y += 4;
|
|
653
|
+
}
|
|
654
|
+
settled.push(label);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/*
|
|
659
|
+
* Deterministic layered layout with integer coordinates. Barycenter sweeps order unpinned nodes;
|
|
660
|
+
* pinned nodes keep their position, and skipped layers give edges explicit routing points.
|
|
661
|
+
*/
|
|
662
|
+
export function layoutOf(document: Pick<DiagramDocument, 'meta' | 'nodes' | 'groups' | 'edges'>): DiagramLayout {
|
|
663
|
+
const down = document.meta.direction === 'down';
|
|
664
|
+
const layers = layersOf(document.nodes, document.edges);
|
|
665
|
+
const sizes = document.nodes.map((node) => sizeOfNode(node));
|
|
666
|
+
const knownIds = new Set(document.nodes.map((node) => node.id));
|
|
667
|
+
|
|
668
|
+
const { units, unitOf, unitOfNode } = unitsOf(document.nodes, layers, sizes, down);
|
|
669
|
+
const { bands, bandOfGroup } = bandsOf(document.groups, units, unitOf, down);
|
|
670
|
+
const graphEdges = edgesOf(document.edges, units, unitOf, knownIds);
|
|
671
|
+
const { hops, chains, loops, loose } = graphEdges;
|
|
672
|
+
const layerCount = units.reduce((count, unit) => Math.max(count, unit.layer + 1), 0);
|
|
673
|
+
const items = itemsOf(units, bands, layerCount, down);
|
|
674
|
+
|
|
675
|
+
const layering: Layering = { units, hops, bands, lines: graphEdges.lines, items };
|
|
676
|
+
orderLayers(layering);
|
|
677
|
+
const top = placeAcross(layering);
|
|
678
|
+
const middleOf = (unit: number): number => top[unit]! + units[unit]!.h / 2;
|
|
679
|
+
const ports = portsOf(units, top, hops, loops);
|
|
680
|
+
const labelSizes = document.edges.map((edge) => (edge.label ? sizeOfEdgeLabel(edge.label) : null));
|
|
681
|
+
const { columnX, columnWidth, unitX, trackX, frameLabels } = placeAlong(layering, graphEdges, ports, labelSizes, layerCount, down);
|
|
605
682
|
|
|
606
683
|
const frameBoxOf = (unit: number): Rect => ({ x: unitX[unit]!, y: top[unit]!, w: units[unit]!.w, h: units[unit]!.h });
|
|
607
684
|
const shapeOf = (unit: number): DiagramShape | undefined => document.nodes[units[unit]!.node]!.shape;
|
|
@@ -611,7 +688,7 @@ export function layoutOf(document: Pick<DiagramDocument, 'meta' | 'nodes' | 'gro
|
|
|
611
688
|
return columnX[self.layer]! + columnWidth[self.layer]!;
|
|
612
689
|
}
|
|
613
690
|
const box = frameBoxOf(unit);
|
|
614
|
-
return box.x + box.w - insetOf(shapeOf(unit), box, y -
|
|
691
|
+
return box.x + box.w - insetOf(shapeOf(unit), box, y - middleOf(unit), down);
|
|
615
692
|
};
|
|
616
693
|
const entryX = (unit: number, y: number): number => {
|
|
617
694
|
const self = units[unit]!;
|
|
@@ -619,7 +696,7 @@ export function layoutOf(document: Pick<DiagramDocument, 'meta' | 'nodes' | 'gro
|
|
|
619
696
|
return columnX[self.layer]!;
|
|
620
697
|
}
|
|
621
698
|
const box = frameBoxOf(unit);
|
|
622
|
-
return box.x + insetOf(shapeOf(unit), box, y -
|
|
699
|
+
return box.x + insetOf(shapeOf(unit), box, y - middleOf(unit), down);
|
|
623
700
|
};
|
|
624
701
|
const toReal = (point: Point): Point => (down ? flipPoint(point) : point);
|
|
625
702
|
const labelOf = (edge: number, frame: Rect | undefined): EdgeLabelBox | null => {
|
|
@@ -659,8 +736,8 @@ export function layoutOf(document: Pick<DiagramDocument, 'meta' | 'nodes' | 'gro
|
|
|
659
736
|
const points: Point[] = [];
|
|
660
737
|
for (const index of chain.hops) {
|
|
661
738
|
const hop = hops[index]!;
|
|
662
|
-
const ya = hopStart[index]!;
|
|
663
|
-
const yb = hopEnd[index]!;
|
|
739
|
+
const ya = ports.hopStart[index]!;
|
|
740
|
+
const yb = ports.hopEnd[index]!;
|
|
664
741
|
points.push({ x: exitX(hop.from, ya), y: ya });
|
|
665
742
|
if (ya !== yb) {
|
|
666
743
|
const x = trackX.get(index)!;
|
|
@@ -681,8 +758,8 @@ export function layoutOf(document: Pick<DiagramDocument, 'meta' | 'nodes' | 'gro
|
|
|
681
758
|
loops.forEach((loop, index) => {
|
|
682
759
|
const box = frameBoxOf(loop.unit);
|
|
683
760
|
const reach = box.x + box.w + LOOP;
|
|
684
|
-
const startY = loopStart[index]!;
|
|
685
|
-
const endY = loopEnd[index]!;
|
|
761
|
+
const startY = ports.loopStart[index]!;
|
|
762
|
+
const endY = ports.loopEnd[index]!;
|
|
686
763
|
const points = [
|
|
687
764
|
{ x: exitX(loop.unit, startY), y: startY },
|
|
688
765
|
{ x: reach, y: startY },
|
|
@@ -725,15 +802,15 @@ export function layoutOf(document: Pick<DiagramDocument, 'meta' | 'nodes' | 'gro
|
|
|
725
802
|
}
|
|
726
803
|
const edges = routes.filter((route): route is EdgeRoute => route !== undefined);
|
|
727
804
|
|
|
728
|
-
const
|
|
805
|
+
const alongBefore = GROUP_PADDING + (down ? GROUP_LABEL_BAND : 0);
|
|
729
806
|
const groups: GroupBox[] = document.groups.flatMap((group, groupIndex) => {
|
|
730
807
|
const rects: Rect[] = [];
|
|
731
808
|
const bandIndex = bandOfGroup.get(groupIndex);
|
|
732
809
|
if (bandIndex !== undefined) {
|
|
733
810
|
const band = bands[bandIndex]!;
|
|
734
811
|
const members = band.members.flat().filter((unit) => units[unit]!.node >= 0);
|
|
735
|
-
const alongStart =
|
|
736
|
-
const alongEnd = Math.max(...members.map((unit) => unitX[unit]! + units[unit]!.w)) +
|
|
812
|
+
const alongStart = Math.min(...band.members[0]!.map((unit) => unitX[unit]!)) - alongBefore;
|
|
813
|
+
const alongEnd = Math.max(...members.map((unit) => unitX[unit]! + units[unit]!.w)) + GROUP_PADDING;
|
|
737
814
|
const frame = { x: alongStart, y: band.top, w: alongEnd - alongStart, h: band.height };
|
|
738
815
|
rects.push(down ? flip(frame) : frame);
|
|
739
816
|
}
|
|
@@ -756,21 +833,7 @@ export function layoutOf(document: Pick<DiagramDocument, 'meta' | 'nodes' | 'gro
|
|
|
756
833
|
return [{ id: group.id, ...rect, labelBox }];
|
|
757
834
|
});
|
|
758
835
|
|
|
759
|
-
|
|
760
|
-
const settled = edges.flatMap((edge) => (edge.label && !unplaced.has(edge.index) ? [edge.label] : []));
|
|
761
|
-
for (const edge of edges) {
|
|
762
|
-
const label = edge.label;
|
|
763
|
-
if (!label || !unplaced.has(edge.index)) {
|
|
764
|
-
continue;
|
|
765
|
-
}
|
|
766
|
-
const taken = (): boolean =>
|
|
767
|
-
[...nodes, ...groups.map((group) => group.labelBox)].some((box) => intersects(box, label)) ||
|
|
768
|
-
settled.some((other) => intersects({ x: other.x - 4, y: other.y - 4, w: other.w + 8, h: other.h + 8 }, label));
|
|
769
|
-
for (let step = 0; step < 1000 && taken(); step++) {
|
|
770
|
-
label.y += 4;
|
|
771
|
-
}
|
|
772
|
-
settled.push(label);
|
|
773
|
-
}
|
|
836
|
+
settleLabels(edges, unplaced, nodes, groups);
|
|
774
837
|
const corners = edges.flatMap((edge) => edge.points.map((point) => ({ ...point, w: 0, h: 0 })));
|
|
775
838
|
const labels = edges.flatMap((edge) => (edge.label ? [edge.label] : []));
|
|
776
839
|
const groupLabels = groups.map((group) => group.labelBox);
|
package/src/route.ts
CHANGED
|
@@ -129,12 +129,12 @@ export function routeChannel(input: ChannelInput): ChannelResult {
|
|
|
129
129
|
const order = vertical.map((_, i) => i).sort((left, right) => score[left]! - score[right]! || left - right);
|
|
130
130
|
for (let pass = 0; pass < count; pass++) {
|
|
131
131
|
let swapped = false;
|
|
132
|
-
for (let
|
|
133
|
-
const left = order[
|
|
134
|
-
const right = order[
|
|
132
|
+
for (let slot = 0; slot + 1 < count; slot++) {
|
|
133
|
+
const left = order[slot]!;
|
|
134
|
+
const right = order[slot + 1]!;
|
|
135
135
|
if (cost[right * count + left]! < cost[left * count + right]!) {
|
|
136
|
-
order[
|
|
137
|
-
order[
|
|
136
|
+
order[slot] = right;
|
|
137
|
+
order[slot + 1] = left;
|
|
138
138
|
swapped = true;
|
|
139
139
|
}
|
|
140
140
|
}
|
|
@@ -144,10 +144,10 @@ export function routeChannel(input: ChannelInput): ChannelResult {
|
|
|
144
144
|
}
|
|
145
145
|
const track = new Array<number>(count).fill(0);
|
|
146
146
|
let tracks = 0;
|
|
147
|
-
order.forEach((index,
|
|
147
|
+
order.forEach((index, slot) => {
|
|
148
148
|
let at = 0;
|
|
149
|
-
for (let
|
|
150
|
-
const other = order[
|
|
149
|
+
for (let earlier = 0; earlier < slot; earlier++) {
|
|
150
|
+
const other = order[earlier]!;
|
|
151
151
|
if (near(vertical[other]!, vertical[index]!)) {
|
|
152
152
|
at = Math.max(at, track[other]! + 1);
|
|
153
153
|
}
|
package/src/shapes.ts
CHANGED
|
@@ -15,19 +15,19 @@ export interface ShapePaths {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
function roundedRect(box: Rect, radius: number): string {
|
|
18
|
-
const
|
|
18
|
+
const corner = Math.round(Math.min(radius, box.w / 2, box.h / 2));
|
|
19
19
|
const right = box.x + box.w;
|
|
20
20
|
const bottom = box.y + box.h;
|
|
21
21
|
return [
|
|
22
|
-
`M${box.x +
|
|
23
|
-
`H${right -
|
|
24
|
-
`A${
|
|
25
|
-
`V${bottom -
|
|
26
|
-
`A${
|
|
27
|
-
`H${box.x +
|
|
28
|
-
`A${
|
|
29
|
-
`V${box.y +
|
|
30
|
-
`A${
|
|
22
|
+
`M${box.x + corner} ${box.y}`,
|
|
23
|
+
`H${right - corner}`,
|
|
24
|
+
`A${corner} ${corner} 0 0 1 ${right} ${box.y + corner}`,
|
|
25
|
+
`V${bottom - corner}`,
|
|
26
|
+
`A${corner} ${corner} 0 0 1 ${right - corner} ${bottom}`,
|
|
27
|
+
`H${box.x + corner}`,
|
|
28
|
+
`A${corner} ${corner} 0 0 1 ${box.x} ${bottom - corner}`,
|
|
29
|
+
`V${box.y + corner}`,
|
|
30
|
+
`A${corner} ${corner} 0 0 1 ${box.x + corner} ${box.y}`,
|
|
31
31
|
'Z'
|
|
32
32
|
].join(' ');
|
|
33
33
|
}
|
package/src/svg.ts
CHANGED
|
@@ -34,11 +34,7 @@ function escapeXml(value: string): string {
|
|
|
34
34
|
return value.replace(NOT_XML, '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
/*
|
|
38
|
-
* The whole diagram as one SVG string, for an export and for what the daemon hands an agent. The
|
|
39
|
-
* view in the app draws the same layout and the same paths as elements of its own, so the two only
|
|
40
|
-
* differ in where the colors come from.
|
|
41
|
-
*/
|
|
37
|
+
/* The whole diagram as one SVG string, from the same layout and paths a view draws with its own elements. */
|
|
42
38
|
export function toSvg(document: Pick<DiagramDocument, 'meta' | 'nodes' | 'groups' | 'edges'>, options: DiagramSvgOptions = {}): string {
|
|
43
39
|
const palette = options.palette ?? DEFAULT_PALETTE;
|
|
44
40
|
const paper = options.paper ?? DEFAULT_PAPER;
|