@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.
- package/dist/common/arith.d.ts +12 -0
- package/dist/common/arith.d.ts.map +1 -1
- package/dist/common/edge-in-box.d.ts +31 -0
- package/dist/common/edge-in-box.d.ts.map +1 -0
- package/dist/common/htmltable-emit.d.ts +1 -1
- package/dist/common/htmltable-emit.d.ts.map +1 -1
- package/dist/common/make-label.d.ts +3 -1
- package/dist/common/make-label.d.ts.map +1 -1
- package/dist/common/poly-gencode.d.ts +12 -1
- package/dist/common/poly-gencode.d.ts.map +1 -1
- package/dist/gvc/device.d.ts.map +1 -1
- package/dist/gvc/emit-gates.d.ts +35 -0
- package/dist/gvc/emit-gates.d.ts.map +1 -0
- package/dist/gvc/job.d.ts +4 -0
- package/dist/gvc/job.d.ts.map +1 -1
- package/dist/gvc/viewport.d.ts +13 -0
- package/dist/gvc/viewport.d.ts.map +1 -1
- package/dist/index.js +1030 -742
- package/dist/index.js.map +4 -4
- package/dist/layout/circo/blockpath.d.ts +1 -38
- package/dist/layout/circo/blockpath.d.ts.map +1 -1
- package/dist/layout/circo/crossings.d.ts +59 -0
- package/dist/layout/circo/crossings.d.ts.map +1 -0
- package/dist/layout/dot/edge-route-chain.d.ts +5 -3
- package/dist/layout/dot/edge-route-chain.d.ts.map +1 -1
- package/dist/layout/dot/edge-route.d.ts.map +1 -1
- package/dist/layout/dot/index.d.ts +10 -2
- package/dist/layout/dot/index.d.ts.map +1 -1
- package/dist/layout/dot/ortho-adapter.d.ts +7 -3
- package/dist/layout/dot/ortho-adapter.d.ts.map +1 -1
- package/dist/layout/dot/pack-components.d.ts +4 -1
- package/dist/layout/dot/pack-components.d.ts.map +1 -1
- package/dist/layout/dot/splines-flat-multi.d.ts +7 -5
- package/dist/layout/dot/splines-flat-multi.d.ts.map +1 -1
- package/dist/layout/dot/splines-flat.d.ts +2 -13
- package/dist/layout/dot/splines-flat.d.ts.map +1 -1
- package/dist/layout/dot/splines-route.d.ts.map +1 -1
- package/dist/layout/neato/constraint-adjust.d.ts +32 -0
- package/dist/layout/neato/constraint-adjust.d.ts.map +1 -1
- package/dist/layout/neato/multispline.d.ts.map +1 -1
- package/dist/layout/neato/splines.d.ts.map +1 -1
- package/dist/layout/pack/poly-place.d.ts.map +1 -1
- package/dist/layout/sfdp/__fixtures__/pcp-rotate-native.d.ts +23 -0
- package/dist/layout/sfdp/__fixtures__/pcp-rotate-native.d.ts.map +1 -0
- package/dist/layout/sfdp/spring-driver.d.ts +6 -0
- package/dist/layout/sfdp/spring-driver.d.ts.map +1 -1
- package/dist/ortho/index.d.ts +4 -1
- package/dist/ortho/index.d.ts.map +1 -1
- package/dist/ortho/maze.d.ts +5 -2
- package/dist/ortho/maze.d.ts.map +1 -1
- package/dist/ortho/partition.d.ts +8 -1
- package/dist/ortho/partition.d.ts.map +1 -1
- package/dist/ortho/trap-segment.d.ts +2 -1
- package/dist/ortho/trap-segment.d.ts.map +1 -1
- package/dist/ortho/trapezoid.d.ts.map +1 -1
- package/dist/parser/index.d.ts +0 -7
- package/dist/parser/index.d.ts.map +1 -1
- package/dist/parser/number-scan.d.ts +31 -0
- package/dist/parser/number-scan.d.ts.map +1 -0
- package/dist/pathplan/route.d.ts.map +1 -1
- package/dist/pathplan/solvers.d.ts +15 -3
- package/dist/pathplan/solvers.d.ts.map +1 -1
- package/dist/render/dot/edge-draw.d.ts +12 -0
- package/dist/render/dot/edge-draw.d.ts.map +1 -1
- package/dist/render/svg-graph.d.ts +6 -5
- package/dist/render/svg-graph.d.ts.map +1 -1
- package/dist/render/svg.d.ts +2 -21
- package/dist/render/svg.d.ts.map +1 -1
- package/dist/render.js +1030 -742
- package/dist/render.js.map +4 -4
- package/dist/xdot/parse.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/common/arith.ts +25 -0
- package/src/common/edge-in-box.ts +54 -0
- package/src/common/graph-init.ts +3 -3
- package/src/common/htmltable-emit.ts +4 -2
- package/src/common/make-label.ts +3 -1
- package/src/common/poly-gencode.ts +18 -4
- package/src/gvc/device.ts +8 -41
- package/src/gvc/edge-labels.ts +30 -13
- package/src/gvc/emit-gates.ts +89 -0
- package/src/gvc/job.ts +4 -0
- package/src/gvc/viewport.ts +29 -0
- package/src/layout/circo/blockpath.ts +6 -125
- package/src/layout/circo/crossings.ts +171 -0
- package/src/layout/dot/__fixtures__/2784.dot +1 -0
- package/src/layout/dot/__fixtures__/2784.native-16.1.svg +48 -0
- package/src/layout/dot/edge-route-chain.ts +11 -5
- package/src/layout/dot/edge-route.ts +19 -8
- package/src/layout/dot/index.ts +14 -4
- package/src/layout/dot/ortho-adapter.ts +11 -6
- package/src/layout/dot/pack-components.ts +11 -4
- package/src/layout/dot/splines-flat-multi.ts +21 -8
- package/src/layout/dot/splines-flat.ts +8 -35
- package/src/layout/dot/splines-route.ts +10 -2
- package/src/layout/dot/splines.ts +6 -6
- package/src/layout/neato/constraint-adjust.ts +10 -4
- package/src/layout/neato/multispline.ts +26 -9
- package/src/layout/neato/splines.ts +7 -7
- package/src/layout/pack/poly-place.ts +9 -2
- package/src/layout/sfdp/__fixtures__/pcp-rotate-native.ts +94 -0
- package/src/layout/sfdp/spring-driver.ts +45 -13
- package/src/ortho/index.ts +22 -13
- package/src/ortho/maze.ts +8 -2
- package/src/ortho/partition.ts +17 -2
- package/src/ortho/trap-segment.ts +47 -24
- package/src/ortho/trapezoid.ts +24 -15
- package/src/parser/dot.js +345 -380
- package/src/parser/dot.pegjs +25 -10
- package/src/parser/index.ts +15 -0
- package/src/parser/number-scan.ts +118 -0
- package/src/pathplan/route.ts +79 -34
- package/src/pathplan/solvers.ts +78 -50
- package/src/render/dot/edge-draw.ts +25 -0
- package/src/render/svg-graph.ts +77 -47
- package/src/render/svg-helpers.ts +2 -2
- package/src/render/svg.ts +4 -41
- 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,
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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 &&
|
|
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,
|
|
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
|
|
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;
|
package/src/layout/dot/index.ts
CHANGED
|
@@ -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):
|
|
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
|
|
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 (
|
|
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
|
-
/**
|
|
129
|
-
|
|
130
|
-
|
|
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):
|
|
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
|
-
|
|
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
|
|
12
|
-
* (`Multisep/2 = nodesep/2`, `(0+1)*step = step`)
|
|
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
|
-
|
|
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
|
|
151
|
-
*
|
|
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(
|
|
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,
|
|
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:
|
|
423
|
+
// edge-route.ts:routeForwardEdge → makeFlatLabeledEdge. Unlabeled:
|
|
424
|
+
// routeFlatEdgeGroupFaithful (splines-flat-multi.ts).
|
|
422
425
|
return 0;
|
|
423
426
|
}
|
|
424
427
|
|
|
425
428
|
// ---------------------------------------------------------------------------
|
|
426
|
-
//
|
|
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
|
|
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.
|