@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.
- 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 +813 -542
- 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 +6 -2
- 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 +4 -4
- package/dist/layout/dot/splines-flat.d.ts +1 -11
- 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/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 +813 -542
- 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 +2 -1
- package/src/layout/dot/index.ts +14 -4
- package/src/layout/dot/ortho-adapter.ts +7 -3
- package/src/layout/dot/pack-components.ts +11 -4
- package/src/layout/dot/splines-flat-multi.ts +4 -4
- package/src/layout/dot/splines-flat.ts +5 -32
- 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 +334 -343
- package/src/parser/dot.pegjs +19 -5
- package/src/parser/index.ts +15 -0
- package/src/parser/number-scan.ts +118 -0
- 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
|
}
|
|
@@ -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
|
|
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);
|
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
|
|
|
@@ -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
|
-
/**
|
|
130
|
-
|
|
131
|
-
|
|
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):
|
|
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.
|
|
@@ -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
|
|
156
|
-
*
|
|
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
|
|
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:
|
|
423
|
+
// edge-route.ts:routeForwardEdge → makeFlatLabeledEdge. Unlabeled:
|
|
424
|
+
// routeFlatEdgeGroupFaithful (splines-flat-multi.ts).
|
|
425
425
|
return 0;
|
|
426
426
|
}
|
|
427
427
|
|
|
428
428
|
// ---------------------------------------------------------------------------
|
|
429
|
-
//
|
|
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
|
|
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-
|
|
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
|
-
|
|
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
|
-
/**
|
|
389
|
-
|
|
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 =
|
|
398
|
-
h2 =
|
|
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
|
-
|
|
89
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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:
|
|
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:
|
|
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;
|