@adecore/diagram 0.21.1 → 0.21.2

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 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 = -(bandIndex + 1);
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
- * Deterministic layered layout with integer coordinates. Barycenter sweeps order unpinned nodes;
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
- document.nodes.forEach((node, index) => {
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
- document.groups.forEach((group, groupIndex) => {
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
- document.edges.forEach((edge, index) => {
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
- const layerCount = units.reduce((count, unit) => Math.max(count, unit.layer + 1), 0);
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
- const layering = { units, hops, bands, lines, items };
392
- orderLayers(layering);
393
- const top = placeAcross(layering);
394
- const centerOf = (unit) => top[unit] + units[unit].h / 2;
395
- // Ports: every hop leaves its box on the side facing the flow and enters on the side facing back.
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: centerOf(hop.to), edge: hop.edge, order: 0, apply: (y) => (hopStart[index] = y) });
404
- entries[hop.to].push({ key: centerOf(hop.from), edge: hop.edge, order: 0, apply: (y) => (hopEnd[index] = y) });
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 = centerOf(loop.unit);
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 = centerOf(unit);
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 - centerOf(unit), down);
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 - centerOf(unit), down);
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 alongAfter = GROUP_PADDING;
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 = bandAlongStart(band);
628
- const alongEnd = Math.max(...members.map((unit) => unitX[unit] + units[unit].w)) + alongAfter;
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
- // A label that no channel placed (a loop in the last layer, an edge with a pinned end) moves down until it is clear.
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 p = 0; p + 1 < count; p++) {
90
- const left = order[p];
91
- const right = order[p + 1];
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[p] = right;
94
- order[p + 1] = left;
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, p) => {
104
+ order.forEach((index, slot) => {
105
105
  let at = 0;
106
- for (let q = 0; q < p; q++) {
107
- const other = order[q];
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 r = Math.round(Math.min(radius, box.w / 2, box.h / 2));
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 + r} ${box.y}`,
14
- `H${right - r}`,
15
- `A${r} ${r} 0 0 1 ${right} ${box.y + r}`,
16
- `V${bottom - r}`,
17
- `A${r} ${r} 0 0 1 ${right - r} ${bottom}`,
18
- `H${box.x + r}`,
19
- `A${r} ${r} 0 0 1 ${box.x} ${bottom - r}`,
20
- `V${box.y + r}`,
21
- `A${r} ${r} 0 0 1 ${box.x + r} ${box.y}`,
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&apos;');
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.21.1",
3
+ "version": "0.21.2",
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.21.1",
47
+ "@adecore/drawing": "0.21.2",
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 = -(bandIndex + 1);
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
- * Deterministic layered layout with integer coordinates. Barycenter sweeps order unpinned nodes;
341
- * pinned nodes keep their position, and skipped layers give edges explicit routing points.
342
- */
343
- export function layoutOf(document: Pick<DiagramDocument, 'meta' | 'nodes' | 'groups' | 'edges'>): DiagramLayout {
344
- const down = document.meta.direction === 'down';
345
- const layers = layersOf(document.nodes, document.edges);
346
- const sizes = document.nodes.map((node) => sizeOfNode(node));
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
- // Every node that is not pinned is a unit, sized in the right-running frame.
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
- document.nodes.forEach((node, index) => {
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
- document.groups.forEach((group, groupIndex) => {
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
- document.edges.forEach((edge, index) => {
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
- const layerCount = units.reduce((count, unit) => Math.max(count, unit.layer + 1), 0);
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
- const layering = { units, hops, bands, lines, items };
488
- orderLayers(layering);
489
- const top = placeAcross(layering);
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: centerOf(hop.to), edge: hop.edge, order: 0, apply: (y) => (hopStart[index] = y) });
501
- entries[hop.to]!.push({ key: centerOf(hop.from), edge: hop.edge, order: 0, apply: (y) => (hopEnd[index] = y) });
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 = centerOf(loop.unit);
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 = centerOf(unit);
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
- const bandAlongStart = (band: Band): number => Math.min(...band.members[0]!.map((unit) => unitX[unit]!)) - alongBefore;
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 - centerOf(unit), down);
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 - centerOf(unit), down);
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 alongAfter = GROUP_PADDING;
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 = bandAlongStart(band);
736
- const alongEnd = Math.max(...members.map((unit) => unitX[unit]! + units[unit]!.w)) + alongAfter;
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
- // A label that no channel placed (a loop in the last layer, an edge with a pinned end) moves down until it is clear.
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 p = 0; p + 1 < count; p++) {
133
- const left = order[p]!;
134
- const right = order[p + 1]!;
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[p] = right;
137
- order[p + 1] = left;
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, p) => {
147
+ order.forEach((index, slot) => {
148
148
  let at = 0;
149
- for (let q = 0; q < p; q++) {
150
- const other = order[q]!;
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 r = Math.round(Math.min(radius, box.w / 2, box.h / 2));
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 + r} ${box.y}`,
23
- `H${right - r}`,
24
- `A${r} ${r} 0 0 1 ${right} ${box.y + r}`,
25
- `V${bottom - r}`,
26
- `A${r} ${r} 0 0 1 ${right - r} ${bottom}`,
27
- `H${box.x + r}`,
28
- `A${r} ${r} 0 0 1 ${box.x} ${bottom - r}`,
29
- `V${box.y + r}`,
30
- `A${r} ${r} 0 0 1 ${box.x + r} ${box.y}`,
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&apos;');
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;