@knowvah/dot-engine 2.3.0 → 2.4.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.
Files changed (118) hide show
  1. package/dist/common/arith.d.ts +12 -0
  2. package/dist/common/arith.d.ts.map +1 -1
  3. package/dist/common/edge-in-box.d.ts +31 -0
  4. package/dist/common/edge-in-box.d.ts.map +1 -0
  5. package/dist/common/htmltable-emit.d.ts +1 -1
  6. package/dist/common/htmltable-emit.d.ts.map +1 -1
  7. package/dist/common/make-label.d.ts +3 -1
  8. package/dist/common/make-label.d.ts.map +1 -1
  9. package/dist/common/poly-gencode.d.ts +12 -1
  10. package/dist/common/poly-gencode.d.ts.map +1 -1
  11. package/dist/gvc/device.d.ts.map +1 -1
  12. package/dist/gvc/emit-gates.d.ts +35 -0
  13. package/dist/gvc/emit-gates.d.ts.map +1 -0
  14. package/dist/gvc/job.d.ts +4 -0
  15. package/dist/gvc/job.d.ts.map +1 -1
  16. package/dist/gvc/viewport.d.ts +13 -0
  17. package/dist/gvc/viewport.d.ts.map +1 -1
  18. package/dist/index.js +1030 -742
  19. package/dist/index.js.map +4 -4
  20. package/dist/layout/circo/blockpath.d.ts +1 -38
  21. package/dist/layout/circo/blockpath.d.ts.map +1 -1
  22. package/dist/layout/circo/crossings.d.ts +59 -0
  23. package/dist/layout/circo/crossings.d.ts.map +1 -0
  24. package/dist/layout/dot/edge-route-chain.d.ts +5 -3
  25. package/dist/layout/dot/edge-route-chain.d.ts.map +1 -1
  26. package/dist/layout/dot/edge-route.d.ts.map +1 -1
  27. package/dist/layout/dot/index.d.ts +10 -2
  28. package/dist/layout/dot/index.d.ts.map +1 -1
  29. package/dist/layout/dot/ortho-adapter.d.ts +7 -3
  30. package/dist/layout/dot/ortho-adapter.d.ts.map +1 -1
  31. package/dist/layout/dot/pack-components.d.ts +4 -1
  32. package/dist/layout/dot/pack-components.d.ts.map +1 -1
  33. package/dist/layout/dot/splines-flat-multi.d.ts +7 -5
  34. package/dist/layout/dot/splines-flat-multi.d.ts.map +1 -1
  35. package/dist/layout/dot/splines-flat.d.ts +2 -13
  36. package/dist/layout/dot/splines-flat.d.ts.map +1 -1
  37. package/dist/layout/dot/splines-route.d.ts.map +1 -1
  38. package/dist/layout/neato/constraint-adjust.d.ts +32 -0
  39. package/dist/layout/neato/constraint-adjust.d.ts.map +1 -1
  40. package/dist/layout/neato/multispline.d.ts.map +1 -1
  41. package/dist/layout/neato/splines.d.ts.map +1 -1
  42. package/dist/layout/pack/poly-place.d.ts.map +1 -1
  43. package/dist/layout/sfdp/__fixtures__/pcp-rotate-native.d.ts +23 -0
  44. package/dist/layout/sfdp/__fixtures__/pcp-rotate-native.d.ts.map +1 -0
  45. package/dist/layout/sfdp/spring-driver.d.ts +6 -0
  46. package/dist/layout/sfdp/spring-driver.d.ts.map +1 -1
  47. package/dist/ortho/index.d.ts +4 -1
  48. package/dist/ortho/index.d.ts.map +1 -1
  49. package/dist/ortho/maze.d.ts +5 -2
  50. package/dist/ortho/maze.d.ts.map +1 -1
  51. package/dist/ortho/partition.d.ts +8 -1
  52. package/dist/ortho/partition.d.ts.map +1 -1
  53. package/dist/ortho/trap-segment.d.ts +2 -1
  54. package/dist/ortho/trap-segment.d.ts.map +1 -1
  55. package/dist/ortho/trapezoid.d.ts.map +1 -1
  56. package/dist/parser/index.d.ts +0 -7
  57. package/dist/parser/index.d.ts.map +1 -1
  58. package/dist/parser/number-scan.d.ts +31 -0
  59. package/dist/parser/number-scan.d.ts.map +1 -0
  60. package/dist/pathplan/route.d.ts.map +1 -1
  61. package/dist/pathplan/solvers.d.ts +15 -3
  62. package/dist/pathplan/solvers.d.ts.map +1 -1
  63. package/dist/render/dot/edge-draw.d.ts +12 -0
  64. package/dist/render/dot/edge-draw.d.ts.map +1 -1
  65. package/dist/render/svg-graph.d.ts +6 -5
  66. package/dist/render/svg-graph.d.ts.map +1 -1
  67. package/dist/render/svg.d.ts +2 -21
  68. package/dist/render/svg.d.ts.map +1 -1
  69. package/dist/render.js +1030 -742
  70. package/dist/render.js.map +4 -4
  71. package/dist/xdot/parse.d.ts.map +1 -1
  72. package/package.json +1 -1
  73. package/src/common/arith.ts +25 -0
  74. package/src/common/edge-in-box.ts +54 -0
  75. package/src/common/graph-init.ts +3 -3
  76. package/src/common/htmltable-emit.ts +4 -2
  77. package/src/common/make-label.ts +3 -1
  78. package/src/common/poly-gencode.ts +18 -4
  79. package/src/gvc/device.ts +8 -41
  80. package/src/gvc/edge-labels.ts +30 -13
  81. package/src/gvc/emit-gates.ts +89 -0
  82. package/src/gvc/job.ts +4 -0
  83. package/src/gvc/viewport.ts +29 -0
  84. package/src/layout/circo/blockpath.ts +6 -125
  85. package/src/layout/circo/crossings.ts +171 -0
  86. package/src/layout/dot/__fixtures__/2784.dot +1 -0
  87. package/src/layout/dot/__fixtures__/2784.native-16.1.svg +48 -0
  88. package/src/layout/dot/edge-route-chain.ts +11 -5
  89. package/src/layout/dot/edge-route.ts +19 -8
  90. package/src/layout/dot/index.ts +14 -4
  91. package/src/layout/dot/ortho-adapter.ts +11 -6
  92. package/src/layout/dot/pack-components.ts +11 -4
  93. package/src/layout/dot/splines-flat-multi.ts +21 -8
  94. package/src/layout/dot/splines-flat.ts +8 -35
  95. package/src/layout/dot/splines-route.ts +10 -2
  96. package/src/layout/dot/splines.ts +6 -6
  97. package/src/layout/neato/constraint-adjust.ts +10 -4
  98. package/src/layout/neato/multispline.ts +26 -9
  99. package/src/layout/neato/splines.ts +7 -7
  100. package/src/layout/pack/poly-place.ts +9 -2
  101. package/src/layout/sfdp/__fixtures__/pcp-rotate-native.ts +94 -0
  102. package/src/layout/sfdp/spring-driver.ts +45 -13
  103. package/src/ortho/index.ts +22 -13
  104. package/src/ortho/maze.ts +8 -2
  105. package/src/ortho/partition.ts +17 -2
  106. package/src/ortho/trap-segment.ts +47 -24
  107. package/src/ortho/trapezoid.ts +24 -15
  108. package/src/parser/dot.js +345 -380
  109. package/src/parser/dot.pegjs +25 -10
  110. package/src/parser/index.ts +15 -0
  111. package/src/parser/number-scan.ts +118 -0
  112. package/src/pathplan/route.ts +79 -34
  113. package/src/pathplan/solvers.ts +78 -50
  114. package/src/render/dot/edge-draw.ts +25 -0
  115. package/src/render/svg-graph.ts +77 -47
  116. package/src/render/svg-helpers.ts +2 -2
  117. package/src/render/svg.ts +4 -41
  118. package/src/xdot/parse.ts +19 -5
@@ -0,0 +1,171 @@
1
+ // SPDX-License-Identifier: EPL-2.0
2
+
3
+ /**
4
+ * Crossing reduction for one block's circle order: count_all_crossings (with
5
+ * its open-edge-list quirks), insertNodelist and the reduce passes. Split out
6
+ * of blockpath.ts for file size; bodies unchanged.
7
+ *
8
+ * @see lib/circogen/blockpath.c:count_all_crossings / reduce / reduce_edge_crossings
9
+ * @see lib/circogen/nodelist.c:insertNodelist
10
+ */
11
+
12
+ import type { Node } from '../../model/node.js';
13
+ import type { DerivedNode, DerivedEdge } from './blocks.js';
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Crossing counting helpers (each branch extracted to keep CCN low)
17
+ // ---------------------------------------------------------------------------
18
+
19
+ export function derivedOf(n: Node): DerivedNode | null {
20
+ return (n.info.alg as { derivedNode?: DerivedNode } | undefined)?.derivedNode ?? null;
21
+ }
22
+
23
+ export function closeOpenEdge(e: DerivedEdge, d: DerivedNode, opened: Iterable<DerivedEdge>): number {
24
+ let cross = 0;
25
+ for (const o of opened) {
26
+ if (o.order > e.order && o.tail !== d && o.head !== d) cross++;
27
+ }
28
+ return cross;
29
+ }
30
+
31
+ /**
32
+ * Every node's incident edges in agfstedge order: out-edges by head, then
33
+ * in-edges by tail (node creation order, ties by edge sequence) — the order
34
+ * spanIncident uses, keyed by orig.id because layoutBlock sorts the block's
35
+ * nodes that way. @see lib/cgraph/edge.c:agfstedge
36
+ */
37
+ export function agfstedgeLists(edges: DerivedEdge[]): Map<DerivedNode, DerivedEdge[]> {
38
+ const seq = new Map<DerivedEdge, number>();
39
+ edges.forEach((e, i) => seq.set(e, i));
40
+ const by = (end: (e: DerivedEdge) => DerivedNode) => (a: DerivedEdge, b: DerivedEdge): number =>
41
+ (end(a).orig.id - end(b).orig.id) || (seq.get(a)! - seq.get(b)!);
42
+ const out = new Map<DerivedNode, DerivedEdge[]>();
43
+ const inn = new Map<DerivedNode, DerivedEdge[]>();
44
+ for (const e of edges) {
45
+ out.set(e.tail, [...(out.get(e.tail) ?? []), e]);
46
+ inn.set(e.head, [...(inn.get(e.head) ?? []), e]);
47
+ }
48
+ const lists = new Map<DerivedNode, DerivedEdge[]>();
49
+ for (const d of new Set([...out.keys(), ...inn.keys()])) {
50
+ const o = (out.get(d) ?? []).sort(by((e) => e.head));
51
+ const i = (inn.get(d) ?? []).sort(by((e) => e.tail));
52
+ lists.set(d, [...o, ...i]);
53
+ }
54
+ return lists;
55
+ }
56
+
57
+ /**
58
+ * C's count_all_crossings — INCLUDING its load-bearing bug: remove_edge keys
59
+ * the open-edge dict by the half-edge POINTER handed to it, and add_edge
60
+ * stored the half that the OPENING node's agfstedge yielded. Closing at the
61
+ * other endpoint yields the opposite half, so dtdelete misses and the edge
62
+ * stays open; each close therefore counts EVERY edge opened after it, minus
63
+ * edges incident to the closing node. Only when a node is on the list twice
64
+ * (a place_residual_nodes quirk) does its second visit yield the stored half,
65
+ * so the edges it opened ARE removed — in agfstedge order, between the counts.
66
+ * Do not "fix" either behaviour: the final circle order depends on both.
67
+ * @see lib/circogen/blockpath.c:count_all_crossings
68
+ * @see lib/circogen/edgelist.c:remove_edge
69
+ */
70
+ export function countCrossings(list: Node[], edges: DerivedEdge[]): number {
71
+ for (const e of edges) e.order = 0;
72
+ const incident = agfstedgeLists(edges);
73
+ const opened = new Map<DerivedEdge, DerivedNode>(); // edge → node whose half add_edge stored
74
+ let crossings = 0; let order = 1;
75
+ for (const n of list) {
76
+ const d = derivedOf(n);
77
+ if (!d) continue;
78
+ const es = incident.get(d) ?? [];
79
+ crossings += closeEdgesAt(d, es, opened);
80
+ for (const e of es) {
81
+ if (e.order === 0) { e.order = order; opened.set(e, d); }
82
+ }
83
+ order++;
84
+ }
85
+ return crossings;
86
+ }
87
+
88
+ /**
89
+ * count_all_crossings' close loop at node d: count against the open list,
90
+ * then remove_edge — a hit only for edges add_edge stored from d itself.
91
+ */
92
+ export function closeEdgesAt(d: DerivedNode, es: DerivedEdge[], opened: Map<DerivedEdge, DerivedNode>): number {
93
+ let crossings = 0;
94
+ for (const e of es) {
95
+ if (e.order === 0) continue;
96
+ crossings += closeOpenEdge(e, d, opened.keys());
97
+ if (opened.get(e) === d) opened.delete(e); // remove_edge: same half → dtdelete hits
98
+ }
99
+ return crossings;
100
+ }
101
+
102
+ // ---------------------------------------------------------------------------
103
+ // Crossing reduction
104
+ // ---------------------------------------------------------------------------
105
+
106
+ export function insertNode(list: Node[], cn: Node, neighbor: Node, pos: number): void {
107
+ const i = list.indexOf(cn); if (i !== -1) list.splice(i, 1);
108
+ const ni = list.indexOf(neighbor);
109
+ if (ni === -1) { list.push(cn); return; }
110
+ list.splice(pos === 0 ? ni : ni + 1, 0, cn);
111
+ }
112
+
113
+ /**
114
+ * One reduce pass, mirroring C's reduce (blockpath.c:439): iterate the
115
+ * SUBGRAPH's nodes in agfstnode (root-seq) order — NOT the current list
116
+ * order — and each node's incident edges in agfstedge order; the neighbour
117
+ * is agtail(e) unless that is curnode. The first-improving-move order is
118
+ * load-bearing for the final arrangement.
119
+ */
120
+ export function reducePass(
121
+ list: Node[], edges: DerivedEdge[], cnt: { v: number }, nodes: DerivedNode[],
122
+ ): void {
123
+ const ord = new Map<DerivedNode, number>();
124
+ nodes.forEach((dn, i) => ord.set(dn, i));
125
+ const seq = new Map<DerivedEdge, number>();
126
+ edges.forEach((e, i) => seq.set(e, i));
127
+ for (const dn of nodes) {
128
+ for (const e of spanIncident(dn, edges, ord, seq)) {
129
+ let neighbor = e.tail;
130
+ if (neighbor === dn) neighbor = e.head;
131
+ for (const pos of [0, 1] as const) {
132
+ const copy = [...list];
133
+ insertNode(list, dn.orig, neighbor.orig, pos);
134
+ const nc = countCrossings(list, edges);
135
+ if (nc < cnt.v) { cnt.v = nc; if (cnt.v === 0) return; }
136
+ else { list.length = 0; list.push(...copy); }
137
+ }
138
+ }
139
+ }
140
+ }
141
+
142
+ const CROSS_ITER = 10;
143
+
144
+ /** @see lib/circogen/blockpath.c:reduce_edge_crossings */
145
+ export function reduceEdgeCrossings(
146
+ list: Node[], edges: DerivedEdge[], nodes: DerivedNode[],
147
+ ): Node[] {
148
+ const cnt = { v: countCrossings(list, edges) };
149
+ for (let i = 0; i < CROSS_ITER && cnt.v > 0; i++) {
150
+ const prev = cnt.v; reducePass(list, edges, cnt, nodes);
151
+ if (prev === cnt.v) break;
152
+ }
153
+ return list;
154
+ }
155
+
156
+ /**
157
+ * Incident edges of dn in agfstedge order over the (possibly pruned)
158
+ * subgraph edge set: out-edges (by head order, seq) then in-edges (by tail
159
+ * order, seq). `ord`/`seq` maps carry the derived root's node and edge
160
+ * creation orders. @see lib/cgraph/edge.c:agfstedge
161
+ */
162
+ export function spanIncident(
163
+ dn: DerivedNode, edges: DerivedEdge[],
164
+ ord: Map<DerivedNode, number>, seq: Map<DerivedEdge, number>,
165
+ ): DerivedEdge[] {
166
+ const out = edges.filter((e) => e.tail === dn)
167
+ .sort((a, b) => ((ord.get(a.head) ?? 0) - (ord.get(b.head) ?? 0)) || ((seq.get(a) ?? 0) - (seq.get(b) ?? 0)));
168
+ const inn = edges.filter((e) => e.head === dn)
169
+ .sort((a, b) => ((ord.get(a.tail) ?? 0) - (ord.get(b.tail) ?? 0)) || ((seq.get(a) ?? 0) - (seq.get(b) ?? 0)));
170
+ return [...out, ...inn];
171
+ }
@@ -0,0 +1 @@
1
+ graph{00splines="ortho"0e[0=""]2[0=""height="30000000000000000"]1[0=0]6[0=0]}
@@ -0,0 +1,48 @@
1
+ <?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+ <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
3
+ "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
4
+ <!-- Generated by graphviz version 16.1.0 (20261012.1431)
5
+ -->
6
+ <!-- Pages: 1 -->
7
+ <svg width="422pt" height="1255669760pt"
8
+ viewBox="0.00 0.00 422.00 1255669760.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
9
+ <g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(31 2160000000000000000)">
10
+ <polygon fill="white" stroke="none" points="-31,0 -31,-2160000000000000000 391,-2160000000000000000 391,0 -31,0"/>
11
+ <!-- 00 -->
12
+ <g id="node1" class="node">
13
+ <title>00</title>
14
+ <ellipse fill="none" stroke="black" cx="0" cy="-1080000000000000000" rx="27" ry="0"/>
15
+ <text xml:space="preserve" text-anchor="middle" x="0" y="-1080000000000000000" font-family="Times,serif" font-size="14.00">00</text>
16
+ </g>
17
+ <!-- 0 -->
18
+ <g id="node2" class="node">
19
+ <title>0</title>
20
+ <ellipse fill="none" stroke="black" cx="72" cy="-1080000000000000000" rx="27" ry="0"/>
21
+ <text xml:space="preserve" text-anchor="middle" x="72" y="-1080000000000000000" font-family="Times,serif" font-size="14.00">0</text>
22
+ </g>
23
+ <!-- e -->
24
+ <g id="node3" class="node">
25
+ <title>e</title>
26
+ <ellipse fill="none" stroke="black" cx="144" cy="-1080000000000000000" rx="27" ry="0"/>
27
+ <text xml:space="preserve" text-anchor="middle" x="144" y="-1080000000000000000" font-family="Times,serif" font-size="14.00">e</text>
28
+ </g>
29
+ <!-- 2 -->
30
+ <g id="node4" class="node">
31
+ <title>2</title>
32
+ <ellipse fill="none" stroke="black" cx="216" cy="-1080000000000000000" rx="27" ry="1080000000000000000"/>
33
+ <text xml:space="preserve" text-anchor="middle" x="216" y="-1080000000000000000" font-family="Times,serif" font-size="14.00">2</text>
34
+ </g>
35
+ <!-- 1 -->
36
+ <g id="node5" class="node">
37
+ <title>1</title>
38
+ <ellipse fill="none" stroke="black" cx="288" cy="-1080000000000000000" rx="27" ry="0"/>
39
+ <text xml:space="preserve" text-anchor="middle" x="288" y="-1080000000000000000" font-family="Times,serif" font-size="14.00">1</text>
40
+ </g>
41
+ <!-- 6 -->
42
+ <g id="node6" class="node">
43
+ <title>6</title>
44
+ <ellipse fill="none" stroke="black" cx="360" cy="-1080000000000000000" rx="27" ry="0"/>
45
+ <text xml:space="preserve" text-anchor="middle" x="360" y="-1080000000000000000" font-family="Times,serif" font-size="14.00">6</text>
46
+ </g>
47
+ </g>
48
+ </svg>
@@ -233,11 +233,13 @@ function routeChainSegmented(
233
233
  * Route a forward edge spanning ≥2 ranks via its virtual-node chain through the
234
234
  * faithful pipeline (REGULAREDGE side boxes steer the port, routeSplines over
235
235
  * the chain; adjustregularpath inter-rank widening finally fires). Returns
236
- * control points in graphviz-internal y-up, or null when the edge is not a
237
- * multi-rank chain.
236
+ * control points in graphviz-internal y-up, null when the edge is not a
237
+ * multi-rank chain, or 'lost' when the chain walk was attempted and failed
238
+ * (completeregularpath / routesplines failure): C's make_regular_edge returns
239
+ * with no spline and the edge is never routed again.
238
240
  * @see lib/dotgen/dotsplines.c:make_regular_edge (hackflag forward path)
239
241
  */
240
- export function routeMultiRankEdgeFaithful(g: Graph, e: GraphEdge): Point[] | null {
242
+ export function routeMultiRankEdgeFaithful(g: Graph, e: GraphEdge): Point[] | null | 'lost' {
241
243
  const r = e.tail.info.rank;
242
244
  const rh = e.head.info.rank;
243
245
  if (g.info.rank === undefined || r === undefined || rh === undefined || rh <= r + 1) return null;
@@ -260,7 +262,7 @@ export function routeMultiRankEdgeFaithful(g: Graph, e: GraphEdge): Point[] | nu
260
262
  // record port resolves toward the adjacent-rank vnode, not the far real
261
263
  // endpoint). @see lib/dotgen/dotsplines.c:make_regular_edge (tn/hn walk),
262
264
  // lib/dotgen/fastgr.c:new_virtual_edge (port copies)
263
- return routeChainSegmented(g, segs[0], segs[segs.length - 1], segs);
265
+ return routeChainSegmented(g, segs[0], segs[segs.length - 1], segs) ?? 'lost';
264
266
  }
265
267
 
266
268
  /**
@@ -414,7 +416,11 @@ export function routeBackEdge(e: GraphEdge, _tailBox: NodeBox, _headBox: NodeBox
414
416
  // bezier_clip keeps the load-bearing forward direction (1644).
415
417
  // @see lib/dotgen/dotsplines.c:make_regular_edge (segfirst/tn/hn walk)
416
418
  const fwd = routeChainSegmented(g, segs[0], segs[segs.length - 1], segs);
417
- if (fwd === null) return;
419
+ // Chain routing failed: C's make_regular_edge returns with no spline and
420
+ // dot_splines_ never revisits the edge — mark it consumed so the backstop
421
+ // sweep does not re-route (and re-report) it.
422
+ // @see lib/dotgen/dotsplines.c:make_regular_edge (pn == 0 return)
423
+ if (fwd === null) { e.info.lost = true; return; }
418
424
  clipAndInstall(fwdEdge, fwdEdge.head, fwd, fwd.length, buildDotSinfo());
419
425
  if (swapEndsP(e)) swapEdgeSpline(e);
420
426
  }
@@ -33,8 +33,8 @@ import { ShapeKind, type ShapeDesc } from '../../common/types.js';
33
33
  import {
34
34
  collectNonAdjacentFlatGroup, routeFlatEdgeGroupFaithful,
35
35
  } from './splines-flat-multi.js';
36
- import { makeFlatLabeledEdge, makeAdjFlatNoPortEdge } from './splines-flat-labeled.js';
37
- import { EDGETYPE_SPLINE, swapEndsP, swapEdgeSpline, getMainEdge } from './splines.js';
36
+ import { makeFlatLabeledEdge, makeAdjFlatNoPortEdge, isAdjFlatCandidate } from './splines-flat-labeled.js';
37
+ import { edgeType, swapEndsP, swapEdgeSpline, getMainEdge } from './splines.js';
38
38
  import { FLATORDER } from './fastgr.js';
39
39
  import { IGNORED } from './rank.js';
40
40
  import { buildDotSinfo } from './self-loop.js';
@@ -281,6 +281,7 @@ export function routeOneEdge(e: GraphEdge, g: Graph): void {
281
281
  function routeFaithfulMultiRank(e: GraphEdge, g: Graph): boolean {
282
282
  const pts = routeMultiRankEdgeFaithful(g, e);
283
283
  if (pts === null) return false;
284
+ if (pts === 'lost') { e.info.lost = true; return true; } // C: edge lost, no spline
284
285
  clipAndInstall(e, e.head, pts, pts.length, buildDotSinfo());
285
286
  return true;
286
287
  }
@@ -312,9 +313,18 @@ function isAdjacentPortFlat(e: GraphEdge, g: Graph): boolean {
312
313
  && isFlatAdjacent(g, e) && hasDefinedPort(e);
313
314
  }
314
315
 
315
- /** True when x is an unrouted adjacent same-rank flat edge with a port. */
316
+ /** True when x is an unrouted adjacent same-rank flat edge (port or not): C
317
+ * groups by main edge only, so a port-less sibling joins the aux call. */
316
318
  function isGroupableFlat(x: GraphEdge, g: Graph): boolean {
317
- return x.info.spl === undefined && isAdjacentPortFlat(x, g);
319
+ return x.info.spl === undefined && x.tail.info.rank !== undefined
320
+ && x.tail.info.rank === x.head.info.rank && isFlatAdjacent(g, x);
321
+ }
322
+
323
+ /** True when a port-less adjacent flat edge shares its group with a ported one:
324
+ * C's `ports` flag covers the whole group, so it routes via the aux graph. */
325
+ function joinsPortedFlatGroup(e: GraphEdge, g: Graph): boolean {
326
+ return isAdjFlatCandidate(e) && isGroupableFlat(e, g)
327
+ && collectAdjacentFlatGroup(e, g).some(hasDefinedPort);
318
328
  }
319
329
 
320
330
  /**
@@ -362,17 +372,17 @@ function routeFaithfulSidePort(e: GraphEdge, g: Graph): boolean {
362
372
  // @see lib/dotgen/dotsplines.c:dot_splines_ (ED_adjacent edge-group loop)
363
373
  if (sameRank && isFlatAdjacent(g, e)) {
364
374
  const group = collectAdjacentFlatGroup(e, g);
365
- return makeFlatAdjEdges(g, group, group.length, EDGETYPE_SPLINE) === 0
375
+ return makeFlatAdjEdges(g, group, group.length, edgeType(g)) === 0
366
376
  && e.info.spl !== undefined;
367
377
  }
368
378
  // Non-adjacent flats: C groups identical-port siblings into ONE make_flat_edge
369
379
  // call and nests their splines (cnt-loop). Collect the group and route once;
370
380
  // the group router clip-installs every sibling, so the main loop skips them.
371
- // cnt=1 reduces to the former single routeFlatEdgeFaithful path (AD-1).
381
+ // cnt=1 is the single-edge case of C make_flat_edge's cnt loop (AD-1).
372
382
  // @see lib/dotgen/dotsplines.c:make_flat_edge, make_flat_bottom_edges
373
383
  if (sameRank) {
374
384
  const group = collectNonAdjacentFlatGroup(e, g);
375
- return routeFlatEdgeGroupFaithful(g, group, group.length) && e.info.spl !== undefined;
385
+ return routeFlatEdgeGroupFaithful(g, group, group.length, edgeType(g)) && e.info.spl !== undefined;
376
386
  }
377
387
  const pts = routeRegularEdgeFaithful(g, e);
378
388
  if (pts === null) return false;
@@ -410,6 +420,7 @@ function routeFlatEdge(e: GraphEdge, g: Graph): boolean {
410
420
  // above (make_flat_adj_edges → makeSimpleFlatLabels). Both decline for every
411
421
  // other edge, so only labeled flats are diverted. @see dotsplines.c:1527-1533
412
422
  if (makeFlatLabeledEdge(g, e)) return true;
423
+ if (joinsPortedFlatGroup(e, g) && routeFaithfulSidePort(e, g)) return true;
413
424
  if (makeAdjFlatNoPortEdge(g, e)) return true;
414
425
  if ((hasSidePort(e) || isAdjacentPortFlat(e, g)) && routeFaithfulSidePort(e, g)) return true;
415
426
  // Non-adjacent unlabeled flat with no side port (e.g. a same-rank cross-cluster
@@ -419,7 +430,7 @@ function routeFlatEdge(e: GraphEdge, g: Graph): boolean {
419
430
  && e.info.label === undefined && !isFlatAdjacent(g, e)) {
420
431
  const group = collectNonAdjacentFlatGroup(e, g);
421
432
  if (group.length > 0
422
- && routeFlatEdgeGroupFaithful(g, group, group.length)
433
+ && routeFlatEdgeGroupFaithful(g, group, group.length, edgeType(g))
423
434
  && e.info.spl !== undefined) return true;
424
435
  }
425
436
  return false;
@@ -135,28 +135,38 @@ export function dotPhaseInit(g: Graph): void {
135
135
  * removeFill, sameports, splines, compound. This is C's static `dotLayout`
136
136
  * tail; the per-component pack branch runs exactly this (gvPostprocess is run
137
137
  * once on the root afterwards, not per component).
138
+ *
139
+ * Returns dot_splines' status: C's `if (r != 0) return r;` skips
140
+ * dot_compoundEdges, and the caller's doDot/dot_layout then skip
141
+ * dotneato_postprocess. Since graphviz 16.1 (8fd4b59ef) this is non-zero when
142
+ * splines=ortho cannot build its maze (tests/2784.dot).
138
143
  * @see lib/dotgen/dotinit.c:dotLayout (static)
139
144
  */
140
- export function dotPhasePostNoFinish(g: Graph): void {
145
+ export function dotPhasePostNoFinish(g: Graph): number {
141
146
  removeFill(g);
142
147
  dotSameports(g);
143
- dotSplines(g);
148
+ const r = dotSplines(g);
149
+ if (r !== 0) return r;
144
150
  // C: if (mapbool(agget(g, "compound"))) dot_compoundEdges(g);
145
151
  // agget on a pack-branch component subgraph (_cc_N) falls back to the
146
152
  // attribute default declared by the root's `compound=true`, so C's gate
147
153
  // passes per component; mirror that with a root fallback (local wins).
148
154
  // @see lib/dotgen/dotinit.c:338
149
155
  if (mapbool(g.attrs.get('compound') ?? g.root.attrs.get('compound'))) dotCompoundEdges(g);
156
+ return 0;
150
157
  }
151
158
 
152
159
  /**
153
160
  * Phase 4: post-position passes — the no-finish tail plus gvPostprocess to
154
- * rotate/translate coordinates per rankdir.
161
+ * rotate/translate coordinates per rankdir. A splines failure is a doDot
162
+ * error, so `dot_layout` returns WITHOUT dotneato_postprocess: native 16.1
163
+ * still renders (exit 0) from the untranslated layout coordinates.
155
164
  * @see lib/dotgen/dotinit.c:dotLayout (static)
165
+ * @see lib/dotgen/dotinit.c:dot_layout (`if (doDot(g) != 0) return;`)
156
166
  * @see lib/common/postproc.c:dotneato_postprocess
157
167
  */
158
168
  export function dotPhasePost(g: Graph): void {
159
- dotPhasePostNoFinish(g);
169
+ if (dotPhasePostNoFinish(g) !== 0) return;
160
170
  gvPostprocess(g);
161
171
  }
162
172
 
@@ -58,7 +58,9 @@ function buildNodes(nodeArr: Node[]): OrthoNodeArr {
58
58
  * parallel multi-edge) is skipped. This is ortho's own concentrate mechanism,
59
59
  * distinct from class2's `edge_type == IGNORED` marking, and the keep-first
60
60
  * decision depends on this iteration order matching C's.
61
- * Self-loops (tail === head) are routed elsewhere and excluded here.
61
+ * Self-loops (tail === head) are gathered like any other edge; orthoEdges
62
+ * routes them via addLoop. Only Concentrate dedup skips edges.
63
+ * @see lib/ortho/ortho.c:orthoEdges
62
64
  * @see lib/ortho/ortho.c:1207-1228 (orthoEdges edge gather + Concentrate PS)
63
65
  */
64
66
  function buildEdges(
@@ -74,7 +76,6 @@ function buildEdges(
74
76
  const oes = outIndex.get(n);
75
77
  if (oes === undefined) continue;
76
78
  for (const e of oes) {
77
- if (e.tail === e.head) continue;
78
79
  if (concentrate) {
79
80
  const ti = e.tail.id;
80
81
  const hi = e.head.id;
@@ -103,7 +104,7 @@ function buildEdges(
103
104
  return result;
104
105
  }
105
106
 
106
- /** Build an OrthoGraph from the dot Graph (tail≠head edges only). */
107
+ /** Build an OrthoGraph from the dot Graph (every out-edge, self-loops included). */
107
108
  export function buildOrthoGraph(g: Graph): OrthoGraph {
108
109
  const nodeArr = nodesInSeq(g);
109
110
  const orthoNodes = buildNodes(nodeArr);
@@ -125,9 +126,13 @@ export function installOrthoResult(oe: OrthoEdge, pts: OrthoPoint[]): void {
125
126
  clipAndInstall(origEdge, origEdge.head, pts, pts.length, ORTHO_SINFO);
126
127
  }
127
128
 
128
- /** Run the ortho pipeline over the dot graph, installing results (no labels). */
129
- export function dispatchOrthoEdges(g: Graph, useLbls: boolean): void {
130
- orthoEdges(buildOrthoGraph(g), useLbls, (_og, oe, pts) =>
129
+ /**
130
+ * Run the ortho pipeline over the dot graph, installing results (no labels).
131
+ * @returns orthoEdges' status: 0 on success, non-zero when the maze could not
132
+ * be built (graphviz 16.1) — dot_splines_ returns it (8fd4b59ef)
133
+ */
134
+ export function dispatchOrthoEdges(g: Graph, useLbls: boolean): number {
135
+ return orthoEdges(buildOrthoGraph(g), useLbls, (_og, oe, pts) =>
131
136
  installOrthoResult(oe, pts),
132
137
  );
133
138
  }
@@ -216,9 +216,12 @@ export function initSubg(sg: Graph, root: Graph): void {
216
216
  * ranking reads them. Node sizing in `dotPhaseInit` reads `sg.root.info.flip`
217
217
  * (the real root), so it is correct regardless of this ordering.
218
218
  *
219
+ * Returns the component's dot_splines status (non-zero = C's dotLayout(sg)
220
+ * failing, which makes doDot return before packing). The mincross/position
221
+ * statuses are not consulted on this arm.
219
222
  * @see lib/dotgen/dotinit.c:dotLayout (static)
220
223
  */
221
- export function dotLayoutComponent(sg: Graph, root: Graph): void {
224
+ export function dotLayoutComponent(sg: Graph, root: Graph): number {
222
225
  // C: dotLayout(sg) begins with dot_init_subg(sg, sg), which sets
223
226
  // GD_dotroot(agroot(sg)) = sg so dot_root() returns the COMPONENT during its
224
227
  // layout (so mincross/position treat it as their root, not as a cluster).
@@ -246,7 +249,7 @@ export function dotLayoutComponent(sg: Graph, root: Graph): void {
246
249
  dotRank(sg);
247
250
  dotMincross(sg);
248
251
  dotPosition(sg);
249
- dotPhasePostNoFinish(sg);
252
+ return dotPhasePostNoFinish(sg);
250
253
  }
251
254
 
252
255
  // ---------------------------------------------------------------------------
@@ -416,8 +419,12 @@ export function layoutAndPack(
416
419
  ): void {
417
420
  // Seed the root's drawing-info (C: graph_init before doDot set GD_rankdir2 etc).
418
421
  dotGraphInit(root);
419
- // C: for each component { initSubg(sg, g); dotLayout(sg); }
420
- for (const sg of comps) dotLayoutComponent(sg, root);
422
+ // C: for each component { initSubg(sg, g); rc = dotLayout(sg); if (rc != 0)
423
+ // { free(ccs); return rc; } } — a failure skips packing, cluster carry
424
+ // and the root dotneato_postprocess (dot_layout returns on doDot error).
425
+ for (const sg of comps) {
426
+ if (dotLayoutComponent(sg, root) !== 0) return;
427
+ }
421
428
  // C: attachPos(g) [points: unneeded]; packSubgraphs(ncc, ccs, g, &pinfo);
422
429
  // resetCoord(g) [points: unneeded]. Cluster-inclusive bboxes so packed
423
430
  // clusters get native spacing (the pack module's bbox is node-only).
@@ -8,8 +8,8 @@
8
8
  * flat edge between a same-rank node pair into ONE call and NEST their splines:
9
9
  * `stepx = Multisep/(cnt+1)`, `stepy = vspace/(cnt+1)`; ONE shared makeFlatEnd
10
10
  * tail+head; then edge `i` is offset by `(i+1)*step` on the two END boxes while
11
- * the MIDDLE box keeps plain `stepy`. cnt=1 reduces to routeFlatEdgeFaithful
12
- * (`Multisep/2 = nodesep/2`, `(0+1)*step = step`) — identical (AD-1).
11
+ * the MIDDLE box keeps plain `stepy`. cnt=1 is the single-edge case
12
+ * of C make_flat_edge (`Multisep/2 = nodesep/2`, `(0+1)*step = step`) (AD-1).
13
13
  *
14
14
  * The port routes each non-adjacent flat independently at `nodesep/2`, so cnt>=2
15
15
  * edges come out identical/overlapping; this module restores the nesting.
@@ -22,10 +22,12 @@ import type { Edge } from '../../model/edge.js';
22
22
  import type { Node } from '../../model/node.js';
23
23
  import type { Port, Box } from '../../model/geom.js';
24
24
  import type { Path, PathendT } from '../../common/types.js';
25
- import { routeSplines } from '../../common/splines-routespl.js';
25
+ import { routeSplines, routePolylines } from '../../common/splines-routespl.js';
26
26
  import { clipAndInstall } from '../../common/splines-clip.js';
27
27
  import { graphRanksep } from './position-aux.js';
28
28
  import { buildDotSinfo } from './self-loop.js';
29
+ import { EDGETYPE_LINE, EDGETYPE_SPLINE } from './splines.js';
30
+ import { makeSimpleFlat } from './splines-flat-labeled.js';
29
31
  import {
30
32
  isFlatAdjacent, makeFlatEndBox, topBoxes, bottomBoxes, flatSide, flatVspace,
31
33
  freshFlatPath, assembleFlatPath, flatBboxCtx,
@@ -41,6 +43,8 @@ interface GroupRouteCtx {
41
43
  tlast: Box;
42
44
  hlast: Box;
43
45
  bottom: boolean;
46
+ /** Edge type: SPLINE routes with routesplines, anything else routepolylines. */
47
+ et: number;
44
48
  stepx: number;
45
49
  stepy: number;
46
50
  /** Geometric left end node of the channel (lower order). routeSplines yields
@@ -132,7 +136,8 @@ function routeGroupEdge(e: Edge, i: number, c: GroupRouteCtx): boolean {
132
136
  ? bottomBoxes(c.tlast, c.hlast, endStepX, endStepY, c.stepy)
133
137
  : topBoxes(c.tlast, c.hlast, endStepX, endStepY, c.stepy);
134
138
  assembleFlatPath(c.P, copyPathEnd(c.tend), copyPathEnd(c.hend), mid);
135
- const pts = routeSplines(c.P);
139
+ // @see lib/dotgen/dotsplines.c:1603-1607 (et == EDGETYPE_SPLINE ? routesplines : routepolylines)
140
+ const pts = c.et === EDGETYPE_SPLINE ? routeSplines(c.P) : routePolylines(c.P);
136
141
  if (pts === null || pts.length === 0) return false;
137
142
  // routeSplines returns points left→right (tend = leftNode). Reverse for an edge
138
143
  // whose tail is the right node so the points run tail→head; pass ignoreSwap so
@@ -147,14 +152,22 @@ function routeGroupEdge(e: Edge, i: number, c: GroupRouteCtx): boolean {
147
152
  /**
148
153
  * Faithful make_flat_edge / make_flat_bottom_edges cnt-loop: ONE shared
149
154
  * makeFlatEnd tail+head; for i in 0..cnt-1 route edge i nested by (i+1)*step.
150
- * Installs spl on every edges[i]; returns true iff all installed. cnt=1 reduces
151
- * to routeFlatEdgeFaithful (AD-1). `edges` must be group-ordered (collect...).
155
+ * Installs spl on every edges[i]; returns true iff all installed. cnt=1 is the
156
+ * single-edge case of C make_flat_edge (AD-1). `edges` must be group-ordered (collect...).
157
+ * `et == EDGETYPE_LINE` short-circuits to makeSimpleFlat (C 1535-1537); the
158
+ * corridor router is routesplines only for EDGETYPE_SPLINE, else routepolylines.
152
159
  * @see lib/dotgen/dotsplines.c:make_flat_edge, make_flat_bottom_edges
153
160
  */
154
- export function routeFlatEdgeGroupFaithful(g: Graph, edges: Edge[], cnt: number): boolean {
161
+ export function routeFlatEdgeGroupFaithful(
162
+ g: Graph, edges: Edge[], cnt: number, et: number,
163
+ ): boolean {
155
164
  const lead = edges[0];
156
165
  if (g.info.rank === undefined || lead.tail.info.rank === undefined
157
166
  || lead.head.info.rank !== lead.tail.info.rank) return false;
167
+ if (et === EDGETYPE_LINE) {
168
+ makeSimpleFlat(edges, et);
169
+ return true;
170
+ }
158
171
  // C makefwdedge: build the channel left→right by node order, else the middle
159
172
  // box inverts for a right-to-left (tail right of head) flat edge.
160
173
  const tailIsLeft = (lead.tail.info.order ?? 0) <= (lead.head.info.order ?? 0);
@@ -179,7 +192,7 @@ export function routeFlatEdgeGroupFaithful(g: Graph, edges: Edge[], cnt: number)
179
192
  const stepy = flatVspace(g, leftNode, !bottom) / (cnt + 1);
180
193
  const rc: GroupRouteCtx = {
181
194
  P, tend, hend, tlast: tend.boxes[tend.boxn - 1], hlast: hend.boxes[hend.boxn - 1],
182
- bottom, stepx, stepy, leftNode,
195
+ bottom, et, stepx, stepy, leftNode,
183
196
  };
184
197
  for (let i = 0; i < cnt; i++) {
185
198
  if (!routeGroupEdge(edges[i], i, rc)) return false;
@@ -25,7 +25,6 @@ import { dotSplines_, swapEndsP, swapEdgeSpline } from './splines.js';
25
25
  import { makePort } from '../../model/edgeInfo.js';
26
26
  import { beginPath } from '../../common/splines-path-begin.js';
27
27
  import { endPath } from '../../common/splines-path-end.js';
28
- import { routeSplines } from '../../common/splines-routespl.js';
29
28
  import { addBox, resolvePort } from '../../common/splines-path-shared.js';
30
29
  import { TOP, BOTTOM, FLATEDGE } from '../../common/splines-constants.js';
31
30
  import {
@@ -363,8 +362,11 @@ function normalizeCopiedFlatSpline(orig: Edge, auxe: Edge): void {
363
362
  * makeSimpleFlat branch are out of scope (callers gate on a side port).
364
363
  * @see lib/dotgen/dotsplines.c:make_flat_adj_edges
365
364
  */
366
- export function makeFlatAdjEdges(g: Graph, edges: Edge[], cnt: number, _et: number): number {
365
+ export function makeFlatAdjEdges(g: Graph, edges: Edge[], cnt: number, et: number): number {
367
366
  const aux = buildFlatAux(g, edges, cnt);
367
+ // C setEdgeType(auxg, et); inlined (index.ts's setEdgeType would be an import cycle).
368
+ // @see lib/dotgen/dotsplines.c:make_flat_adj_edges
369
+ aux.auxg.info.flags = (aux.auxg.info.flags & ~0xf) | (et & 0xf);
368
370
  dotInitNodeEdge(aux.auxg);
369
371
  dotRank(aux.auxg);
370
372
  if (dotMincross(aux.auxg) !== 0) return 1;
@@ -418,12 +420,14 @@ export function makeFlatEdge(
418
420
  const isAdj = edges.some(e => (e.info.adjacent ?? 0) !== 0);
419
421
  if (isAdj) return makeFlatAdjEdges(g, edges, cnt, et);
420
422
  // C's labeled-flat dispatch lives in the live router here (AD-3): see
421
- // edge-route.ts:routeForwardEdge → makeFlatLabeledEdge. Unlabeled: routeFlatEdgeFaithful.
423
+ // edge-route.ts:routeForwardEdge → makeFlatLabeledEdge. Unlabeled:
424
+ // routeFlatEdgeGroupFaithful (splines-flat-multi.ts).
422
425
  return 0;
423
426
  }
424
427
 
425
428
  // ---------------------------------------------------------------------------
426
- // routeFlatEdgeFaithful — faithful side-port routing for a single flat edge
429
+ // Flat-edge routing building blocks shared by routeFlatEdgeGroupFaithful
430
+ // (splines-flat-multi.ts) and makeFlatLabeledEdge.
427
431
  // @see lib/dotgen/dotsplines.c:make_flat_edge (non-adjacent, non-labeled,
428
432
  // EDGETYPE_SPLINE: the top-routing branch + make_flat_bottom_edges),
429
433
  // makeFlatEnd / makeBottomFlatEnd
@@ -565,34 +569,3 @@ export function assembleFlatPath(P: Path, tend: PathendT, hend: PathendT, mid: B
565
569
  for (const b of mid) addBox(P, b);
566
570
  for (let i = hend.boxn - 1; i >= 0; i--) addBox(P, hend.boxes[i]);
567
571
  }
568
-
569
- /**
570
- * Route a single same-rank (flat) edge carrying a side-mask port through the
571
- * faithful `beginPath → routeSplines → endPath → clipAndInstall` pipeline.
572
- * Returns spline control points in graphviz-internal (y-up) coordinates, or
573
- * null when the edge is not a same-rank edge. Does NOT clip or install — the
574
- * caller (routeOneEdge) does that via clipAndInstall, mirroring how SR3 wires
575
- * the regular faithful path.
576
- *
577
- * @see lib/dotgen/dotsplines.c:make_flat_edge (cnt=1, non-adjacent spline)
578
- */
579
- export function routeFlatEdgeFaithful(g: Graph, e: Edge): Point[] | null {
580
- const tn = e.tail;
581
- const r = tn.info.rank;
582
- if (g.info.rank === undefined || r === undefined || e.head.info.rank !== r) return null;
583
- const { bottom, side } = flatSide(e);
584
- const ctx = flatBboxCtx(g);
585
- const ranksep = graphRanksep(g);
586
- const P = freshFlatPath();
587
- const tend = makeFlatEndBox({ ctx, P, e, n: tn, side, ranksep, isBegin: true });
588
- const hend = makeFlatEndBox({ ctx, P, e, n: e.head, side, ranksep, isBegin: false });
589
- const stepx = (g.info.nodesep ?? 18) / 2;
590
- const stepy = flatVspace(g, tn, !bottom) / 2;
591
- const tlast = tend.boxes[tend.boxn - 1];
592
- const hlast = hend.boxes[hend.boxn - 1];
593
- const mid = bottom
594
- ? bottomBoxes(tlast, hlast, stepx, stepy, stepy)
595
- : topBoxes(tlast, hlast, stepx, stepy, stepy);
596
- assembleFlatPath(P, tend, hend, mid);
597
- return routeSplines(P);
598
- }
@@ -296,13 +296,13 @@ function isInterior(i: number, len: number): boolean {
296
296
  * unchanged.
297
297
  * @see lib/dotgen/dotsplines.c:make_regular_edge (realedge resolve + VIRTUAL chain walk)
298
298
  */
299
- function baseSplineForGroup(g: Graph, e0: Edge): Point[] | null {
299
+ function baseSplineForGroup(g: Graph, e0: Edge): Point[] | null | 'lost' {
300
300
  const orig = resolveOrigEdge(e0);
301
301
  const fe = isBackEdgeMember(orig) ? makeFwdEdge(orig) : orig;
302
302
  const pts = routeRegularEdgeFaithful(g, fe);
303
303
  // routesplines failure on the group base: C's make_regular_edge returns
304
304
  // before any clip_and_install — every member of the group is lost.
305
- if (pts === 'lost') return null;
305
+ if (pts === 'lost') return 'lost';
306
306
  return pts ?? routeMultiRankEdgeFaithful(g, fe);
307
307
  }
308
308
 
@@ -386,6 +386,14 @@ export function routeParallelEdgeGroup(
386
386
  return o.info.head_port?.defined || o.info.tail_port?.defined;
387
387
  }) ?? edges[0]!;
388
388
  const base = baseSplineForGroup(g, baseEdge);
389
+ // A lost group base consumes every member: C's dot_splines_ calls
390
+ // make_regular_edge once per group and never re-routes its edges, so the
391
+ // routeDotEdges backstop must not re-attempt (and re-report) them.
392
+ // @see lib/dotgen/dotsplines.c:dot_splines_ (group loop, one call per group)
393
+ if (base === 'lost') {
394
+ for (const e of edges) resolveOrigEdge(e).info.lost = true;
395
+ return;
396
+ }
389
397
  if (base === null) return;
390
398
  if (cnt === 1) { installShiftedEdge(edges[0]!, base); return; }
391
399
  // dx = Multisep * (cnt-1) / 2 centres the fan on the base path.