@knowvah/dot-engine 2.3.1 → 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 (112) 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 +813 -542
  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 +6 -2
  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 +4 -4
  34. package/dist/layout/dot/splines-flat.d.ts +1 -11
  35. package/dist/layout/dot/splines-flat.d.ts.map +1 -1
  36. package/dist/layout/dot/splines-route.d.ts.map +1 -1
  37. package/dist/layout/neato/constraint-adjust.d.ts +32 -0
  38. package/dist/layout/neato/constraint-adjust.d.ts.map +1 -1
  39. package/dist/layout/neato/multispline.d.ts.map +1 -1
  40. package/dist/layout/neato/splines.d.ts.map +1 -1
  41. package/dist/layout/pack/poly-place.d.ts.map +1 -1
  42. package/dist/layout/sfdp/__fixtures__/pcp-rotate-native.d.ts +23 -0
  43. package/dist/layout/sfdp/__fixtures__/pcp-rotate-native.d.ts.map +1 -0
  44. package/dist/layout/sfdp/spring-driver.d.ts +6 -0
  45. package/dist/layout/sfdp/spring-driver.d.ts.map +1 -1
  46. package/dist/ortho/index.d.ts +4 -1
  47. package/dist/ortho/index.d.ts.map +1 -1
  48. package/dist/ortho/maze.d.ts +5 -2
  49. package/dist/ortho/maze.d.ts.map +1 -1
  50. package/dist/ortho/partition.d.ts +8 -1
  51. package/dist/ortho/partition.d.ts.map +1 -1
  52. package/dist/ortho/trap-segment.d.ts +2 -1
  53. package/dist/ortho/trap-segment.d.ts.map +1 -1
  54. package/dist/ortho/trapezoid.d.ts.map +1 -1
  55. package/dist/parser/index.d.ts +0 -7
  56. package/dist/parser/index.d.ts.map +1 -1
  57. package/dist/parser/number-scan.d.ts +31 -0
  58. package/dist/parser/number-scan.d.ts.map +1 -0
  59. package/dist/render/dot/edge-draw.d.ts +12 -0
  60. package/dist/render/dot/edge-draw.d.ts.map +1 -1
  61. package/dist/render/svg-graph.d.ts +6 -5
  62. package/dist/render/svg-graph.d.ts.map +1 -1
  63. package/dist/render/svg.d.ts +2 -21
  64. package/dist/render/svg.d.ts.map +1 -1
  65. package/dist/render.js +813 -542
  66. package/dist/render.js.map +4 -4
  67. package/dist/xdot/parse.d.ts.map +1 -1
  68. package/package.json +1 -1
  69. package/src/common/arith.ts +25 -0
  70. package/src/common/edge-in-box.ts +54 -0
  71. package/src/common/graph-init.ts +3 -3
  72. package/src/common/htmltable-emit.ts +4 -2
  73. package/src/common/make-label.ts +3 -1
  74. package/src/common/poly-gencode.ts +18 -4
  75. package/src/gvc/device.ts +8 -41
  76. package/src/gvc/edge-labels.ts +30 -13
  77. package/src/gvc/emit-gates.ts +89 -0
  78. package/src/gvc/job.ts +4 -0
  79. package/src/gvc/viewport.ts +29 -0
  80. package/src/layout/circo/blockpath.ts +6 -125
  81. package/src/layout/circo/crossings.ts +171 -0
  82. package/src/layout/dot/__fixtures__/2784.dot +1 -0
  83. package/src/layout/dot/__fixtures__/2784.native-16.1.svg +48 -0
  84. package/src/layout/dot/edge-route-chain.ts +11 -5
  85. package/src/layout/dot/edge-route.ts +2 -1
  86. package/src/layout/dot/index.ts +14 -4
  87. package/src/layout/dot/ortho-adapter.ts +7 -3
  88. package/src/layout/dot/pack-components.ts +11 -4
  89. package/src/layout/dot/splines-flat-multi.ts +4 -4
  90. package/src/layout/dot/splines-flat.ts +5 -32
  91. package/src/layout/dot/splines-route.ts +10 -2
  92. package/src/layout/dot/splines.ts +6 -6
  93. package/src/layout/neato/constraint-adjust.ts +10 -4
  94. package/src/layout/neato/multispline.ts +26 -9
  95. package/src/layout/neato/splines.ts +7 -7
  96. package/src/layout/pack/poly-place.ts +9 -2
  97. package/src/layout/sfdp/__fixtures__/pcp-rotate-native.ts +94 -0
  98. package/src/layout/sfdp/spring-driver.ts +45 -13
  99. package/src/ortho/index.ts +22 -13
  100. package/src/ortho/maze.ts +8 -2
  101. package/src/ortho/partition.ts +17 -2
  102. package/src/ortho/trap-segment.ts +47 -24
  103. package/src/ortho/trapezoid.ts +24 -15
  104. package/src/parser/dot.js +334 -343
  105. package/src/parser/dot.pegjs +19 -5
  106. package/src/parser/index.ts +15 -0
  107. package/src/parser/number-scan.ts +118 -0
  108. package/src/render/dot/edge-draw.ts +25 -0
  109. package/src/render/svg-graph.ts +77 -47
  110. package/src/render/svg-helpers.ts +2 -2
  111. package/src/render/svg.ts +4 -41
  112. 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
  }
@@ -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
  }
@@ -377,7 +378,7 @@ function routeFaithfulSidePort(e: GraphEdge, g: Graph): boolean {
377
378
  // Non-adjacent flats: C groups identical-port siblings into ONE make_flat_edge
378
379
  // call and nests their splines (cnt-loop). Collect the group and route once;
379
380
  // the group router clip-installs every sibling, so the main loop skips them.
380
- // 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).
381
382
  // @see lib/dotgen/dotsplines.c:make_flat_edge, make_flat_bottom_edges
382
383
  if (sameRank) {
383
384
  const group = collectNonAdjacentFlatGroup(e, g);
@@ -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
 
@@ -126,9 +126,13 @@ export function installOrthoResult(oe: OrthoEdge, pts: OrthoPoint[]): void {
126
126
  clipAndInstall(origEdge, origEdge.head, pts, pts.length, ORTHO_SINFO);
127
127
  }
128
128
 
129
- /** Run the ortho pipeline over the dot graph, installing results (no labels). */
130
- export function dispatchOrthoEdges(g: Graph, useLbls: boolean): void {
131
- 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) =>
132
136
  installOrthoResult(oe, pts),
133
137
  );
134
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.
@@ -152,8 +152,8 @@ function routeGroupEdge(e: Edge, i: number, c: GroupRouteCtx): boolean {
152
152
  /**
153
153
  * Faithful make_flat_edge / make_flat_bottom_edges cnt-loop: ONE shared
154
154
  * makeFlatEnd tail+head; for i in 0..cnt-1 route edge i nested by (i+1)*step.
155
- * Installs spl on every edges[i]; returns true iff all installed. cnt=1 reduces
156
- * 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
157
  * `et == EDGETYPE_LINE` short-circuits to makeSimpleFlat (C 1535-1537); the
158
158
  * corridor router is routesplines only for EDGETYPE_SPLINE, else routepolylines.
159
159
  * @see lib/dotgen/dotsplines.c:make_flat_edge, make_flat_bottom_edges
@@ -21,11 +21,10 @@ import { dotRank } from './rank.js';
21
21
  import { dotMincross } from './mincross.js';
22
22
  import { dotPosition } from './position.js';
23
23
  import { dotSameports } from './sameport.js';
24
- import { dotSplines_, swapEndsP, swapEdgeSpline, EDGETYPE_SPLINE } from './splines.js';
24
+ 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, routePolylines } 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 {
@@ -421,12 +420,14 @@ export function makeFlatEdge(
421
420
  const isAdj = edges.some(e => (e.info.adjacent ?? 0) !== 0);
422
421
  if (isAdj) return makeFlatAdjEdges(g, edges, cnt, et);
423
422
  // C's labeled-flat dispatch lives in the live router here (AD-3): see
424
- // edge-route.ts:routeForwardEdge → makeFlatLabeledEdge. Unlabeled: routeFlatEdgeFaithful.
423
+ // edge-route.ts:routeForwardEdge → makeFlatLabeledEdge. Unlabeled:
424
+ // routeFlatEdgeGroupFaithful (splines-flat-multi.ts).
425
425
  return 0;
426
426
  }
427
427
 
428
428
  // ---------------------------------------------------------------------------
429
- // 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.
430
431
  // @see lib/dotgen/dotsplines.c:make_flat_edge (non-adjacent, non-labeled,
431
432
  // EDGETYPE_SPLINE: the top-routing branch + make_flat_bottom_edges),
432
433
  // makeFlatEnd / makeBottomFlatEnd
@@ -568,31 +569,3 @@ export function assembleFlatPath(P: Path, tend: PathendT, hend: PathendT, mid: B
568
569
  for (const b of mid) addBox(P, b);
569
570
  for (let i = hend.boxn - 1; i >= 0; i--) addBox(P, hend.boxes[i]);
570
571
  }
571
-
572
- /**
573
- * Route a single same-rank (flat) edge carrying a side-mask port through the
574
- * faithful `beginPath → routeSplines → endPath → clipAndInstall` pipeline.
575
- * Returns spline control points in graphviz-internal (y-up) coordinates, or
576
- * null when the edge is not a same-rank edge. Does NOT clip or install — the
577
- * caller (routeOneEdge) does that via clipAndInstall (as the regular path).
578
- *
579
- * @see lib/dotgen/dotsplines.c:make_flat_edge (cnt=1, non-adjacent spline)
580
- */
581
- export function routeFlatEdgeFaithful(g: Graph, e: Edge, et: number): Point[] | null {
582
- const tn = e.tail;
583
- const r = tn.info.rank;
584
- if (g.info.rank === undefined || r === undefined || e.head.info.rank !== r) return null;
585
- const { bottom, side } = flatSide(e);
586
- const ctx = flatBboxCtx(g);
587
- const ranksep = graphRanksep(g);
588
- const P = freshFlatPath();
589
- const tend = makeFlatEndBox({ ctx, P, e, n: tn, side, ranksep, isBegin: true });
590
- const hend = makeFlatEndBox({ ctx, P, e, n: e.head, side, ranksep, isBegin: false });
591
- const stepx = (g.info.nodesep ?? 18) / 2;
592
- const stepy = flatVspace(g, tn, !bottom) / 2;
593
- const [tlast, hlast] = [tend.boxes[tend.boxn - 1], hend.boxes[hend.boxn - 1]];
594
- const mid = (bottom ? bottomBoxes : topBoxes)(tlast, hlast, stepx, stepy, stepy);
595
- assembleFlatPath(P, tend, hend, mid);
596
- // @see lib/dotgen/dotsplines.c:1603-1607
597
- return et === EDGETYPE_SPLINE ? routeSplines(P) : routePolylines(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.
@@ -434,7 +434,7 @@ export function resetRW(g: Graph): void {
434
434
  }
435
435
 
436
436
  /**
437
- * splines=ortho dispatch — mirror dotsplines.c:251-259: resetRW, run the
437
+ * splines=ortho dispatch — mirror dotsplines.c:251-266: resetRW, run the
438
438
  * orthoEdges pipeline (before mark_lowclusters), then the finish semantics
439
439
  * (skip routesplinesterm — not ported in TS; set edgeLabelsDone; return 0).
440
440
  * Edge-label sub-case is T2.
@@ -443,15 +443,15 @@ export function resetRW(g: Graph): void {
443
443
  function orthoDispatch(g: Graph): number {
444
444
  resetRW(g);
445
445
  // C: if (GD_has_labels(g->root) & EDGE_LABEL) { setEdgeLabelPos(g);
446
- // orthoEdges(g,true); } else orthoEdges(g,false);
446
+ // rc = orthoEdges(g,true); } else rc = orthoEdges(g,false);
447
447
  // orthoEdges itself warns + downgrades useLbls (ortho/index.ts) — C never
448
448
  // routes edges around labels (ADR-2). We only POSITION the labels here.
449
+ let rc: number;
449
450
  if (((g.root.info.has_labels ?? 0) & EDGE_LABEL) !== 0) {
450
451
  setEdgeLabelPos(g);
451
- dispatchOrthoEdges(g, true);
452
- } else {
453
- dispatchOrthoEdges(g, false);
454
- }
452
+ rc = dispatchOrthoEdges(g, true);
453
+ } else rc = dispatchOrthoEdges(g, false);
454
+ if (rc !== 0) return rc; // 16.1 8fd4b59ef: skip port labels, EdgeLabelsDone
455
455
  // C's `goto finish` lands ON the port-label placement block
456
456
  // (dotsplines.c:436-458): head/tail labels with labelangle/labeldistance
457
457
  // are placed via place_portlabel for ortho too. Skipping this left the
@@ -26,6 +26,7 @@ import { Edge } from '../../model/edge.js';
26
26
  import type { EdgeList } from '../../model/nodeInfo.js';
27
27
  import { DtBag } from '../../cdt/index.js';
28
28
  import { cround } from '../../common/arith.js';
29
+ import { d2i } from '../../util/math.js';
29
30
  import { rank } from '../dot/ns.js';
30
31
  import { elistAppend } from '../dot/fastgr.js';
31
32
  import { nodesInSeq } from '../dot/decomp.js';
@@ -385,8 +386,13 @@ function overlaps(p: Nitem[]): boolean {
385
386
  return false;
386
387
  }
387
388
 
388
- /** @see lib/neatogen/constraint.c:initItem */
389
- function initItem(n: Node, margin: ExpandT): Nitem {
389
+ /**
390
+ * Additive margins convert with d2i (clamp to INT_MIN..INT_MAX, else
391
+ * truncate) since graphviz 16.1 (13e5f6ce8). Exported for tests.
392
+ * @see lib/neatogen/constraint.c:initItem
393
+ * @internal
394
+ */
395
+ export function initItem(n: Node, margin: ExpandT): Nitem {
390
396
  const x = cround(SCALE * (n.info.pos?.[0] ?? 0) * POINTS_PER_INCH);
391
397
  const y = cround(SCALE * (n.info.pos?.[1] ?? 0) * POINTS_PER_INCH);
392
398
  const width = n.info.width ?? 0;
@@ -394,8 +400,8 @@ function initItem(n: Node, margin: ExpandT): Nitem {
394
400
  let w2: number;
395
401
  let h2: number;
396
402
  if (margin.doAdd) {
397
- w2 = Math.trunc(SCALE * (cround((width / 2.0) * POINTS_PER_INCH) + margin.x));
398
- h2 = Math.trunc(SCALE * (cround((height / 2.0) * POINTS_PER_INCH) + margin.y));
403
+ w2 = d2i(SCALE * (cround((width / 2.0) * POINTS_PER_INCH) + margin.x));
404
+ h2 = d2i(SCALE * (cround((height / 2.0) * POINTS_PER_INCH) + margin.y));
399
405
  } else {
400
406
  w2 = cround(margin.x * SCALE2 * width * POINTS_PER_INCH);
401
407
  h2 = cround(margin.y * SCALE2 * height * POINTS_PER_INCH);
@@ -17,6 +17,7 @@ import type { Graph } from '../../model/graph.js';
17
17
  import type { Point } from '../../pathplan/types.js';
18
18
  import type { SplineInfo } from '../../common/types.js';
19
19
  import { wind } from '../../pathplan/visibility.js';
20
+ import { fma } from '../../common/fma.js';
20
21
  import { shortestPath } from '../../pathplan/shortest.js';
21
22
  import { routeSpline, makePolyline } from '../../pathplan/index.js';
22
23
  import type { Edge as PEdge } from '../../pathplan/types.js';
@@ -85,10 +86,12 @@ function raySegIntersect(v: Point, w: Point, a: Point, b: Point): Point | null {
85
86
  const mv = { x: w.x - v.x, y: w.y - v.y };
86
87
  const lv = { x: b.x - a.x, y: b.y - a.y };
87
88
  const ln = { x: -lv.y, y: lv.x }; // perp
88
- const lc = -(ln.x * a.x + ln.y * a.y);
89
- const dt = ln.x * mv.x + ln.y * mv.y;
89
+ // libgvc's line_intersect fuses the first product of each dot():
90
+ // fmsub(lv.y, p.x, lv.x·p.y). The final a - s·mv is NOT fused.
91
+ const lc = -fma(ln.x, a.x, ln.y * a.y);
92
+ const dt = fma(ln.x, mv.x, ln.y * mv.y);
90
93
  if (Math.abs(dt) < SMALL) return null;
91
- const s = (ln.x * v.x + ln.y * v.y + lc) / dt;
94
+ const s = (fma(ln.x, v.x, ln.y * v.y) + lc) / dt;
92
95
  return { x: v.x - s * mv.x, y: v.y - s * mv.y };
93
96
  }
94
97
 
@@ -115,6 +118,19 @@ function ctrlPtIdx(v: Point, ps: Point[]): number {
115
118
 
116
119
  const SEP = 15;
117
120
 
121
+ /** v + 100·(cos, sin), fused like the binary's fmadd. @see multispline.c:mkCtrlPts */
122
+ function bisectorTarget(v: Point, cosTheta: number, sinTheta: number): Point {
123
+ return { x: fma(cosTheta, 100, v.x), y: fma(sinTheta, 100, v.y) };
124
+ }
125
+
126
+ /** DIST(p, q) = sqrt(DIST2), the sum of squares fused (fmadd(dx, dx, dy·dy)) —
127
+ * not hypot. @see lib/common/geom.h:DIST */
128
+ function distFused(p: Point, q: Point): number {
129
+ const dx = p.x - q.x;
130
+ const dy = p.y - q.y;
131
+ return Math.sqrt(fma(dx, dx, dy * dy));
132
+ }
133
+
118
134
  /**
119
135
  * mult points on the bisector ray at corridor vertex v, spaced ≤ SEP,
120
136
  * ordered by side (s = index after which vertices are on the far side).
@@ -129,31 +145,32 @@ function mkCtrlPts(
129
145
  const theta = bisect(prev, v, nxt);
130
146
  let sinTheta = Math.sin(theta);
131
147
  let cosTheta = Math.cos(theta);
132
- let w = { x: v.x + 100 * cosTheta, y: v.y + 100 * sinTheta };
148
+ // The binary fuses v + 100·cos/sin and v + (i·sep)·cos/sin (fmadd/fmla).
149
+ let w = bisectorTarget(v, cosTheta, sinTheta);
133
150
  if (idx > s) {
134
151
  if (wind(prev, v, w) !== 1) {
135
152
  sinTheta *= -1;
136
153
  cosTheta *= -1;
137
- w = { x: v.x + 100 * cosTheta, y: v.y + 100 * sinTheta };
154
+ w = bisectorTarget(v, cosTheta, sinTheta);
138
155
  }
139
156
  } else if (wind(prev, v, w) !== -1) {
140
157
  sinTheta *= -1;
141
158
  cosTheta *= -1;
142
- w = { x: v.x + 100 * cosTheta, y: v.y + 100 * sinTheta };
159
+ w = bisectorTarget(v, cosTheta, sinTheta);
143
160
  }
144
161
  const q = triPoint(trip, idx, v, w);
145
162
  if (q === null) return null;
146
163
 
147
- const d = Math.hypot(q.x - v.x, q.y - v.y);
164
+ const d = distFused(q, v);
148
165
  const sep = d >= mult * SEP ? SEP : d / mult;
149
166
  const ps = new Array<Point>(mult);
150
167
  if (idx < s) {
151
168
  for (let i = 0; i < mult; i++) {
152
- ps[i] = { x: v.x + i * sep * cosTheta, y: v.y + i * sep * sinTheta };
169
+ ps[i] = { x: fma(i * sep, cosTheta, v.x), y: fma(i * sep, sinTheta, v.y) };
153
170
  }
154
171
  } else {
155
172
  for (let i = 0; i < mult; i++) {
156
- ps[mult - i - 1] = { x: v.x + i * sep * cosTheta, y: v.y + i * sep * sinTheta };
173
+ ps[mult - i - 1] = { x: fma(i * sep, cosTheta, v.x), y: fma(i * sep, sinTheta, v.y) };
157
174
  }
158
175
  }
159
176
  return ps;