@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
|
@@ -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;
|
|
@@ -19,6 +19,7 @@ import type { Edge } from '../../model/edge.js';
|
|
|
19
19
|
import type { Point } from '../../model/geom.js';
|
|
20
20
|
import type { SplineInfo, PolygonT, TextlabelT } from '../../common/types.js';
|
|
21
21
|
import { polygonOutlineRing } from '../../common/poly-sizing.js';
|
|
22
|
+
import { fma, fms } from '../../common/fma.js';
|
|
22
23
|
import { ShapeKind } from '../../common/types.js';
|
|
23
24
|
import type { ExpandT } from './sep-factor.js';
|
|
24
25
|
import { esepFactor } from './sep-factor.js';
|
|
@@ -127,12 +128,11 @@ class ObstacleHelper {
|
|
|
127
128
|
|
|
128
129
|
/** Slope of the ellipse tangent at p. @see neatosplines.c:ellipse_tangent_slope */
|
|
129
130
|
function ellipseTangentSlope(a: number, b: number, p: Point): number {
|
|
130
|
-
const signY = p.y >= 0 ? 1 : -1;
|
|
131
|
-
return (-signY * (b * p.x)) / (a * Math.sqrt(a
|
|
131
|
+
const signY = p.y >= 0 ? 1 : -1; // radicand: the binary fuses a*a (fmla)
|
|
132
|
+
return (-signY * (b * p.x)) / (a * Math.sqrt(fma(a, a, -(p.x * p.x))));
|
|
132
133
|
}
|
|
133
134
|
|
|
134
|
-
/** Corner i of an nsides-gon
|
|
135
|
-
* @see neatosplines.c:circumscribed_polygon_corner_about_ellipse */
|
|
135
|
+
/** Corner i of an nsides-gon about ellipse (a, b). @see neatosplines.c:circumscribed_polygon_corner_about_ellipse */
|
|
136
136
|
function circumscribedCorner(a: number, b: number, i: number, nsides: number): Point {
|
|
137
137
|
const angle0 = (2 * Math.PI * (i - 0.5)) / nsides;
|
|
138
138
|
const angle1 = (2 * Math.PI * (i + 0.5)) / nsides;
|
|
@@ -140,9 +140,9 @@ function circumscribedCorner(a: number, b: number, i: number, nsides: number): P
|
|
|
140
140
|
const p1 = { x: a * Math.cos(angle1), y: b * Math.sin(angle1) };
|
|
141
141
|
const m0 = ellipseTangentSlope(a, b, p0);
|
|
142
142
|
const m1 = ellipseTangentSlope(a, b, p1);
|
|
143
|
-
// line_intersection
|
|
144
|
-
const x = (m0
|
|
145
|
-
const y =
|
|
143
|
+
// line_intersection, contracted as the binary: fnmsub, fmls, +; y = fmadd.
|
|
144
|
+
const x = (fms(p1.x, m1, fma(m0, p0.x, -p0.y)) + p1.y) / (m0 - m1);
|
|
145
|
+
const y = fma(m0, x - p0.x, p0.y);
|
|
146
146
|
return { x, y };
|
|
147
147
|
}
|
|
148
148
|
|
|
@@ -19,6 +19,7 @@ import type { Edge } from '../../model/edge.js';
|
|
|
19
19
|
import type { PackInfo } from './types.js';
|
|
20
20
|
import { PackMode } from './types.js';
|
|
21
21
|
import { cround } from '../../common/arith.js';
|
|
22
|
+
import { d2i } from '../../util/math.js';
|
|
22
23
|
import { gvQsort } from '../../util/bsd-qsort.js';
|
|
23
24
|
|
|
24
25
|
/** Max. avg. polyomino size. @see lib/pack/pack.c:C */
|
|
@@ -78,7 +79,10 @@ export function computeStep(bbs: Box[], margin: number): number {
|
|
|
78
79
|
c -= W * H;
|
|
79
80
|
}
|
|
80
81
|
const r = Math.sqrt(b * b - 4 * a * c);
|
|
81
|
-
|
|
82
|
+
// C `root = (int)l1`: the native arm64 build converts with `fcvtzs w`, which
|
|
83
|
+
// saturates at INT_MAX (objdump of libgvc `_computeStep`). Only graphs with
|
|
84
|
+
// extreme coordinates reach it (1314: l1 = 7.4e10 → step 2147483647).
|
|
85
|
+
const root = d2i((-b + r) / (2 * a));
|
|
82
86
|
return root === 0 ? 1 : root;
|
|
83
87
|
}
|
|
84
88
|
|
|
@@ -289,7 +293,10 @@ function fits(x: number, y: number, info: GInfo, ctx: PlaceCtx): boolean {
|
|
|
289
293
|
}
|
|
290
294
|
const bb = ctx.bbs[info.index]!;
|
|
291
295
|
const LL = { x: cround(bb.ll.x), y: cround(bb.ll.y) };
|
|
292
|
-
|
|
296
|
+
// C `step * x` is int * int: a 32-bit `mul w` that wraps before the result
|
|
297
|
+
// is widened to double (objdump of libgvc `_fits`). Matters only once
|
|
298
|
+
// computeStep has saturated at INT_MAX.
|
|
299
|
+
ctx.places[info.index] = { x: Math.imul(ctx.step, x) - LL.x, y: Math.imul(ctx.step, y) - LL.y };
|
|
293
300
|
for (const c of info.cells) ctx.ps.add(c.x + x, c.y + y);
|
|
294
301
|
return true;
|
|
295
302
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* pcp_rotate input/output pairs dumped from the native graphviz 16.1.0
|
|
5
|
+
* build (LTO on, Apple clang 21, arm64): the instrumented
|
|
6
|
+
* multilevel_spring_electrical_embedding printed x[] with %a immediately
|
|
7
|
+
* before and after the inlined pcp_rotate call (sfdp layout of each input).
|
|
8
|
+
* The instrumented plugin's disassembly of the inlined body was identical to
|
|
9
|
+
* the oracle's and its xdot for tests/42.dot was byte-identical.
|
|
10
|
+
*
|
|
11
|
+
* @see lib/sfdpgen/spring_electrical.c:pcp_rotate
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** One native pcp_rotate call (dim = 2): coordinates in and out. */
|
|
15
|
+
export interface PcpCase {
|
|
16
|
+
readonly n: number;
|
|
17
|
+
readonly input: readonly number[];
|
|
18
|
+
readonly output: readonly number[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** tests/42.dot (n = 25: 24 unfused main-loop points + 1 fused tail point). */
|
|
22
|
+
export const PCP_42: PcpCase = {
|
|
23
|
+
n: 25,
|
|
24
|
+
input: [
|
|
25
|
+
3.5872483107422566, -12.377777121200412, 3.4715918522980727,
|
|
26
|
+
-12.918978548323624, 4.121783883411746, -12.91831647385274,
|
|
27
|
+
3.795604961681946, -12.02670434921042, 3.475187937840741,
|
|
28
|
+
-12.116509880628376, 3.176953710079423, -11.474322726281912,
|
|
29
|
+
3.223320716000219, -12.50190058190682, 3.9427450081150517,
|
|
30
|
+
-12.609361218577487, 2.9320923020663012, -12.08466407155544,
|
|
31
|
+
2.647834118844372, -12.484128275830983, 2.485503508198676,
|
|
32
|
+
-12.052368284631822, 2.454104738007288, -11.543163215115078,
|
|
33
|
+
3.965624072051866, -12.301668238922174, 3.272695018831133,
|
|
34
|
+
-12.293622724126323, 4.277294098336774, -12.52763434032553,
|
|
35
|
+
2.6567536665021607, -11.78672436299522, 2.9194126389126986,
|
|
36
|
+
-10.94953631613356, 2.5489751513175936, -10.86946489644661,
|
|
37
|
+
2.6733650330275274, -11.224528211868394, 3.048965000467677,
|
|
38
|
+
-11.637154070809292, 4.317960366069153, -12.028182724742415,
|
|
39
|
+
3.1405903774739743, -10.735347387936093, 3.517649553441379,
|
|
40
|
+
-11.59828957557804, 2.889150038336242, -11.393037327449802,
|
|
41
|
+
3.8184483813156236, -11.736128083787474,
|
|
42
|
+
],
|
|
43
|
+
output: [
|
|
44
|
+
-0.5304877806920583, 0.08366486993416244, -0.8552660176992705,
|
|
45
|
+
0.5317658606344066, -1.289862780722194, 0.048158820532294325,
|
|
46
|
+
-0.409028142995645, -0.30609421159466693, -0.26135016442439085,
|
|
47
|
+
-0.007894082007798424, 0.41543462009487314, -0.21600547347691734,
|
|
48
|
+
-0.3791958356150653, 0.4371626422995346, -0.9404677592737167,
|
|
49
|
+
-0.02553912799220554, 0.1257378673267193, 0.374375300532583,
|
|
50
|
+
0.019109087106747153, 0.8529196228841546, 0.44858083486899103,
|
|
51
|
+
0.6846284019992013, 0.8479871604816006, 0.36721656754478493,
|
|
52
|
+
-0.7271280985453075, -0.24843957764647775, -0.257462324530711,
|
|
53
|
+
0.2610989035548756, -1.1036058128568746, -0.3288349437729226,
|
|
54
|
+
0.5313880036167653, 0.37960990645275156, 0.9777471494560837,
|
|
55
|
+
-0.37579533215854694, 1.285134511761471, -0.1541010066529222,
|
|
56
|
+
0.9380455904287646, -0.00893874806544041, 0.38007922650162834,
|
|
57
|
+
-0.01193403330818422, -0.7596711077227419, -0.693272291261024,
|
|
58
|
+
0.988907593770146, -0.6834833778804641, 0.09533034350091935,
|
|
59
|
+
-0.38622500677079324, 0.668428062586742, -0.05652949538534024,
|
|
60
|
+
-0.2083842264234602, -0.5175141883950642,
|
|
61
|
+
],
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** `graph { a -- b -- c -- d -- e -- f -- g; a -- d; c -- g }` (n = 7: 4 unfused + 3 fused). */
|
|
65
|
+
export const PCP_N7: PcpCase = {
|
|
66
|
+
n: 7,
|
|
67
|
+
input: [
|
|
68
|
+
-15.236266874412502, -14.141759136203413, -14.362447295439736,
|
|
69
|
+
-14.495631475674346, -14.72475387596594, -15.55293197670535,
|
|
70
|
+
-15.713464401249913, -15.151793337403825, -16.37705062226656,
|
|
71
|
+
-16.145225064687455, -15.922726903476539, -17.08665871053711,
|
|
72
|
+
-14.941732868825685, -16.728142530138104,
|
|
73
|
+
],
|
|
74
|
+
output: [
|
|
75
|
+
1.3982174906911033, 0.47136487209612654, 1.3990421407369296,
|
|
76
|
+
-0.47138939556383713, 0.2831108329282356, -0.533419420756337,
|
|
77
|
+
0.28286219933551654, 0.5335672937482661, -0.887224005839827,
|
|
78
|
+
0.7747136019415551, -1.5886065363823616, -0.0003798742551679708,
|
|
79
|
+
-0.887402121469613, -0.7744570772106122,
|
|
80
|
+
],
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/** `graph { a -- b; b -- c; a -- c [len=2] }` (n = 3: no main loop, all fused). */
|
|
84
|
+
export const PCP_N3: PcpCase = {
|
|
85
|
+
n: 3,
|
|
86
|
+
input: [
|
|
87
|
+
12.264744691446909, -0.43911661110249495, 13.621642438404422,
|
|
88
|
+
-0.3215742583017034, 12.841756307969007, 0.7944196810068545,
|
|
89
|
+
],
|
|
90
|
+
output: [
|
|
91
|
+
-0.7758786283883479, -0.1280321647860694, 0.4988649924591231,
|
|
92
|
+
-0.6076324165676953, 0.277013635929228, 0.7356645813537631,
|
|
93
|
+
],
|
|
94
|
+
};
|
|
@@ -84,8 +84,46 @@ export function powerLawGraph(A: SpMatrix): boolean {
|
|
|
84
84
|
return mask[1]! > 0.8 * max && mask[1]! > 0.3 * m;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Width of the vectorised main loop clang emits for pcp_rotate's
|
|
89
|
+
* covariance accumulation once LTO inlines it (dim == 2 constant).
|
|
90
|
+
* @see lib/sfdpgen/spring_electrical.c:pcp_rotate (16.1, 630465f82)
|
|
91
|
+
*/
|
|
92
|
+
const PCP_COVARIANCE_LANES = 4;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The covariance accumulation `y[dim*k+l] += x[i*dim+k]*x[i*dim+l]`, with
|
|
96
|
+
* the native 16.1 build's FMA contraction. 630465f82 made pcp_rotate
|
|
97
|
+
* static, so LTO inlines it into multilevel_spring_electrical_embedding
|
|
98
|
+
* and vectorises this loop 4 points at a time: the main loop multiplies
|
|
99
|
+
* and adds separately (fmul.2d + fadd.2d, two roundings) and only the
|
|
100
|
+
* scalar tail over the last n % 4 points is fused (fmla.2d/fmadd).
|
|
101
|
+
* Accumulation order is the C order in both parts. (The 15.x out-of-line
|
|
102
|
+
* pcp_rotate fused every point.)
|
|
103
|
+
* @see lib/sfdpgen/spring_electrical.c:pcp_rotate
|
|
104
|
+
*/
|
|
105
|
+
function pcpCovariance(n: number, dim: number, x: number[], y: number[]): void {
|
|
106
|
+
const nUnfused = n - (n % PCP_COVARIANCE_LANES);
|
|
107
|
+
for (let i = 0; i < n; i++) {
|
|
108
|
+
for (let k = 0; k < dim; k++) {
|
|
109
|
+
for (let l = 0; l < dim; l++) {
|
|
110
|
+
const a = x[i * dim + k]!;
|
|
111
|
+
const b = x[i * dim + l]!;
|
|
112
|
+
const acc = y[dim * k + l]!;
|
|
113
|
+
y[dim * k + l] = i < nUnfused ? acc + a * b : fma(a, b, acc);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
87
119
|
/**
|
|
88
120
|
* Principal-component rotation about the centroid.
|
|
121
|
+
*
|
|
122
|
+
* FMA contraction mirrors the native 16.1 build (objdump of the inlined
|
|
123
|
+
* body): the covariance loop per pcpCovariance; the eigenvector radicand
|
|
124
|
+
* fuses its y0*y0, -2*y0*y3 and y3*y3 terms (4*y1*y1 is rounded first);
|
|
125
|
+
* `1 + axis0*axis0` is fused; each rotated coordinate fuses its first
|
|
126
|
+
* product (x0 = fma(x, a0, y*a1), x1 = fma(-x, a1, y*a0)).
|
|
89
127
|
* @see lib/sfdpgen/spring_electrical.c:pcp_rotate
|
|
90
128
|
*/
|
|
91
129
|
export function pcpRotate(n: number, dim: number, x: number[]): void {
|
|
@@ -98,26 +136,20 @@ export function pcpRotate(n: number, dim: number, x: number[]): void {
|
|
|
98
136
|
for (let i = 0; i < n; i++) {
|
|
99
137
|
for (let k = 0; k < dim; k++) x[dim * i + k]! -= center[k]!;
|
|
100
138
|
}
|
|
101
|
-
|
|
102
|
-
for (let k = 0; k < dim; k++) {
|
|
103
|
-
for (let l = 0; l < dim; l++) {
|
|
104
|
-
y[dim * k + l] = fma(x[i * dim + k]!, x[i * dim + l]!, y[dim * k + l]!);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
139
|
+
pcpCovariance(n, dim, x, y);
|
|
108
140
|
const axis = [0, 1];
|
|
109
141
|
if (y[1] !== 0) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
142
|
+
const radicand = fma(y[3]!, y[3]!,
|
|
143
|
+
fma(-2 * y[0]!, y[3]!, fma(y[0]!, y[0]!, 4 * y[1]! * y[1]!)));
|
|
144
|
+
axis[0] = -(-y[0]! + y[3]! - Math.sqrt(radicand)) / (2 * y[1]!);
|
|
113
145
|
axis[1] = 1;
|
|
114
146
|
}
|
|
115
|
-
const dist = Math.sqrt(
|
|
147
|
+
const dist = Math.sqrt(fma(axis[0]!, axis[0]!, 1));
|
|
116
148
|
axis[0] = axis[0]! / dist;
|
|
117
149
|
axis[1] = axis[1]! / dist;
|
|
118
150
|
for (let i = 0; i < n; i++) {
|
|
119
|
-
const x0 = x[dim * i]
|
|
120
|
-
const x1 = -x[dim * i]
|
|
151
|
+
const x0 = fma(x[dim * i]!, axis[0]!, x[dim * i + 1]! * axis[1]!);
|
|
152
|
+
const x1 = fma(-x[dim * i]!, axis[1]!, x[dim * i + 1]! * axis[0]!);
|
|
121
153
|
x[dim * i] = x0;
|
|
122
154
|
x[dim * i + 1] = x1;
|
|
123
155
|
}
|
package/src/ortho/index.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import type {
|
|
11
11
|
OrthoGraph, OrthoEdge, OrthoNode, OrthoPoint,
|
|
12
|
-
Route,
|
|
12
|
+
Route, Maze, SGraph,
|
|
13
13
|
} from "./types.js";
|
|
14
14
|
import type { ClipAndInstallFn } from "./types.js";
|
|
15
15
|
import { mkMaze, freeMaze } from "./maze.js";
|
|
@@ -67,7 +67,7 @@ function edgeCmp(a: EdgePair, b: EdgePair): number {
|
|
|
67
67
|
function buildSpline(
|
|
68
68
|
e: OrthoEdge,
|
|
69
69
|
rte: Route,
|
|
70
|
-
mp:
|
|
70
|
+
mp: Maze,
|
|
71
71
|
): OrthoPoint[] {
|
|
72
72
|
const pts: OrthoPoint[] = [];
|
|
73
73
|
if (rte.segs.length === 0) return pts;
|
|
@@ -118,17 +118,31 @@ function buildSpline(
|
|
|
118
118
|
|
|
119
119
|
// ─── orthoEdges ───────────────────────────────────────────────────────────────
|
|
120
120
|
|
|
121
|
+
/** Ensure the two dummy search nodes (sn, dn) exist past the maze's nodes. */
|
|
122
|
+
function ensureDummyNodes(sg: SGraph, gstart: number): void {
|
|
123
|
+
while (sg.nodes.length < gstart + 2) {
|
|
124
|
+
sg.nodes.push({
|
|
125
|
+
nVal: 0, nIdx: 0, nDad: null, nEdge: null,
|
|
126
|
+
nAdj: 0, saveNAdj: 0, cells: [null, null],
|
|
127
|
+
adjEdgeList: [], index: sg.nodes.length, isVert: false, x: 0, y: 0,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
121
132
|
/**
|
|
122
133
|
* For edges without position information, construct an orthogonal routing.
|
|
123
134
|
* If useLbls is true, issues a warning and sets useLbls=false.
|
|
124
135
|
*
|
|
136
|
+
* @returns 0 on success; -1 when mkMaze fails (graphviz 16.1, 178fe6d2f). A
|
|
137
|
+
* shortPath or assignTracks failure still returns 0, as in C (`goto
|
|
138
|
+
* orthofinish` falls through to `return 0`).
|
|
125
139
|
* @see lib/ortho/ortho.c:orthoEdges
|
|
126
140
|
*/
|
|
127
141
|
export function orthoEdges(
|
|
128
142
|
g: OrthoGraph,
|
|
129
143
|
useLbls: boolean,
|
|
130
144
|
clipAndInstall?: ClipAndInstallFn,
|
|
131
|
-
):
|
|
145
|
+
): number {
|
|
132
146
|
if (useLbls) {
|
|
133
147
|
console.warn(
|
|
134
148
|
"Orthogonal edges do not currently handle edge labels. Try using xlabels.\n",
|
|
@@ -138,6 +152,7 @@ export function orthoEdges(
|
|
|
138
152
|
void useLbls; // suppress lint on intentional reassign
|
|
139
153
|
|
|
140
154
|
const mp = mkMaze(g);
|
|
155
|
+
if (mp === null) return -1; // C frees ps (Concentrate) and es first
|
|
141
156
|
const sg = mp.sg;
|
|
142
157
|
|
|
143
158
|
// collect and sort edges by length. C ortho.c sorts via qsort(edgecmp), which
|
|
@@ -153,14 +168,7 @@ export function orthoEdges(
|
|
|
153
168
|
const sn = sg.nodes[gstart];
|
|
154
169
|
const dn = sg.nodes[gstart + 1];
|
|
155
170
|
|
|
156
|
-
|
|
157
|
-
while (sg.nodes.length < gstart + 2) {
|
|
158
|
-
sg.nodes.push({
|
|
159
|
-
nVal: 0, nIdx: 0, nDad: null, nEdge: null,
|
|
160
|
-
nAdj: 0, saveNAdj: 0, cells: [null, null],
|
|
161
|
-
adjEdgeList: [], index: sg.nodes.length, isVert: false, x: 0, y: 0,
|
|
162
|
-
});
|
|
163
|
-
}
|
|
171
|
+
ensureDummyNodes(sg, gstart);
|
|
164
172
|
|
|
165
173
|
for (let i = 0; i < nEdges; i++) {
|
|
166
174
|
const e = es[i].e;
|
|
@@ -177,7 +185,7 @@ export function orthoEdges(
|
|
|
177
185
|
if (shortPath(pq, sg, dn, sn) !== 0) {
|
|
178
186
|
pqFree(pq);
|
|
179
187
|
freeMaze(mp);
|
|
180
|
-
return;
|
|
188
|
+
return 0;
|
|
181
189
|
}
|
|
182
190
|
|
|
183
191
|
routeList[i] = convertSPtoRoute(sg, sn, dn);
|
|
@@ -190,7 +198,7 @@ export function orthoEdges(
|
|
|
190
198
|
assignSegs(routeList, mp);
|
|
191
199
|
if (assignTracks(mp) !== 0) {
|
|
192
200
|
freeMaze(mp);
|
|
193
|
-
return;
|
|
201
|
+
return 0;
|
|
194
202
|
}
|
|
195
203
|
|
|
196
204
|
if (clipAndInstall) {
|
|
@@ -203,4 +211,5 @@ export function orthoEdges(
|
|
|
203
211
|
}
|
|
204
212
|
|
|
205
213
|
freeMaze(mp);
|
|
214
|
+
return 0;
|
|
206
215
|
}
|
package/src/ortho/maze.ts
CHANGED
|
@@ -270,8 +270,11 @@ function nodeBb(n: OrthoNode): OrthoBox {
|
|
|
270
270
|
return { LL: { x: cx - w2, y: cy - h2 }, UR: { x: cx + w2, y: cy + h2 } };
|
|
271
271
|
}
|
|
272
272
|
|
|
273
|
-
/**
|
|
274
|
-
|
|
273
|
+
/**
|
|
274
|
+
* @returns the maze, or null when partition fails (graphviz 16.1)
|
|
275
|
+
* @see lib/ortho/maze.c:mkMaze
|
|
276
|
+
*/
|
|
277
|
+
export function mkMaze(g: OrthoGraph): Maze | null {
|
|
275
278
|
const gcells: Cell[] = g.nodes.map((n) => ({
|
|
276
279
|
flags: MZ_ISNODE, nedges: 0,
|
|
277
280
|
edges: new Array(6).fill(null) as (SEdge | null)[],
|
|
@@ -292,6 +295,9 @@ export function mkMaze(g: OrthoGraph): Maze {
|
|
|
292
295
|
UR: { x: BB.UR.x + MARGIN, y: BB.UR.y + MARGIN },
|
|
293
296
|
};
|
|
294
297
|
const rects = partition(gcells, BB);
|
|
298
|
+
// C (16.1, 312e49714): partition failure → freeMaze(mp); return NULL.
|
|
299
|
+
// freeMaze is a no-op here (GC), so only the NULL return remains.
|
|
300
|
+
if (rects === null) return null;
|
|
295
301
|
const cells: Cell[] = rects.map((bb) => ({
|
|
296
302
|
flags: 0, nedges: 0,
|
|
297
303
|
edges: new Array(6).fill(null) as (SEdge | null)[],
|
package/src/ortho/partition.ts
CHANGED
|
@@ -258,9 +258,16 @@ class PartitionHelper {
|
|
|
258
258
|
|
|
259
259
|
/**
|
|
260
260
|
* Partition free space around cells into rectangular tiles.
|
|
261
|
+
*
|
|
262
|
+
* Returns null on failure (graphviz 16.1): an empty horizontal (057e8779d) or
|
|
263
|
+
* vertical (67bacf48e) trapezoid list — construct_trapezoids' failure signal —
|
|
264
|
+
* prints C's stderr line and returns NULL. C hands the result back through
|
|
265
|
+
* `LIST_DETACH(&rs, &ret, nrects)`, whose base pointer is NULL when no
|
|
266
|
+
* rectangle was appended, so an empty rectangle set is ALSO NULL to the caller
|
|
267
|
+
* (mkMaze's `rects == NULL` check), silently.
|
|
261
268
|
* @see lib/ortho/partition.c:partition
|
|
262
269
|
*/
|
|
263
|
-
export function partition(cells: Cell[], bb: OrthoBox): OrthoBox[] {
|
|
270
|
+
export function partition(cells: Cell[], bb: OrthoBox): OrthoBox[] | null {
|
|
264
271
|
const h = PartitionHelper;
|
|
265
272
|
const nsegs = 4 * (cells.length + 1);
|
|
266
273
|
|
|
@@ -271,11 +278,19 @@ export function partition(cells: Cell[], bb: OrthoBox): OrthoBox[] {
|
|
|
271
278
|
const hSeg = h.genSegments(cells, bb, false);
|
|
272
279
|
const hPerm = h.generateRandomOrdering(nsegs);
|
|
273
280
|
const hTr = constructTrapezoids(nsegs, hSeg, hPerm);
|
|
281
|
+
if (hTr.length === 0) {
|
|
282
|
+
console.error("horizontal trapezoid construction failed");
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
274
285
|
const hDecomp = h.monotonateTrapezoids(hTr, hSeg, false);
|
|
275
286
|
|
|
276
287
|
const vSeg = h.genSegments(cells, bb, true);
|
|
277
288
|
const vPerm = h.generateRandomOrdering(nsegs);
|
|
278
289
|
const vTr = constructTrapezoids(nsegs, vSeg, vPerm);
|
|
290
|
+
if (vTr.length === 0) {
|
|
291
|
+
console.error("vertical trapezoid construction failed");
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
279
294
|
const vDecomp = h.monotonateTrapezoids(vTr, vSeg, true);
|
|
280
295
|
|
|
281
296
|
const result: OrthoBox[] = [];
|
|
@@ -285,7 +300,7 @@ export function partition(cells: Cell[], bb: OrthoBox): OrthoBox[] {
|
|
|
285
300
|
if (isect) result.push(isect);
|
|
286
301
|
}
|
|
287
302
|
}
|
|
288
|
-
return result;
|
|
303
|
+
return result.length === 0 ? null : result;
|
|
289
304
|
}
|
|
290
305
|
|
|
291
306
|
/**
|