@knowvah/dot-engine 2.3.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/dist/common/arith.d.ts +12 -0
  2. package/dist/common/arith.d.ts.map +1 -1
  3. package/dist/common/edge-in-box.d.ts +31 -0
  4. package/dist/common/edge-in-box.d.ts.map +1 -0
  5. package/dist/common/htmltable-emit.d.ts +1 -1
  6. package/dist/common/htmltable-emit.d.ts.map +1 -1
  7. package/dist/common/make-label.d.ts +3 -1
  8. package/dist/common/make-label.d.ts.map +1 -1
  9. package/dist/common/poly-gencode.d.ts +12 -1
  10. package/dist/common/poly-gencode.d.ts.map +1 -1
  11. package/dist/gvc/device.d.ts.map +1 -1
  12. package/dist/gvc/emit-gates.d.ts +35 -0
  13. package/dist/gvc/emit-gates.d.ts.map +1 -0
  14. package/dist/gvc/job.d.ts +4 -0
  15. package/dist/gvc/job.d.ts.map +1 -1
  16. package/dist/gvc/viewport.d.ts +13 -0
  17. package/dist/gvc/viewport.d.ts.map +1 -1
  18. package/dist/index.js +1030 -742
  19. package/dist/index.js.map +4 -4
  20. package/dist/layout/circo/blockpath.d.ts +1 -38
  21. package/dist/layout/circo/blockpath.d.ts.map +1 -1
  22. package/dist/layout/circo/crossings.d.ts +59 -0
  23. package/dist/layout/circo/crossings.d.ts.map +1 -0
  24. package/dist/layout/dot/edge-route-chain.d.ts +5 -3
  25. package/dist/layout/dot/edge-route-chain.d.ts.map +1 -1
  26. package/dist/layout/dot/edge-route.d.ts.map +1 -1
  27. package/dist/layout/dot/index.d.ts +10 -2
  28. package/dist/layout/dot/index.d.ts.map +1 -1
  29. package/dist/layout/dot/ortho-adapter.d.ts +7 -3
  30. package/dist/layout/dot/ortho-adapter.d.ts.map +1 -1
  31. package/dist/layout/dot/pack-components.d.ts +4 -1
  32. package/dist/layout/dot/pack-components.d.ts.map +1 -1
  33. package/dist/layout/dot/splines-flat-multi.d.ts +7 -5
  34. package/dist/layout/dot/splines-flat-multi.d.ts.map +1 -1
  35. package/dist/layout/dot/splines-flat.d.ts +2 -13
  36. package/dist/layout/dot/splines-flat.d.ts.map +1 -1
  37. package/dist/layout/dot/splines-route.d.ts.map +1 -1
  38. package/dist/layout/neato/constraint-adjust.d.ts +32 -0
  39. package/dist/layout/neato/constraint-adjust.d.ts.map +1 -1
  40. package/dist/layout/neato/multispline.d.ts.map +1 -1
  41. package/dist/layout/neato/splines.d.ts.map +1 -1
  42. package/dist/layout/pack/poly-place.d.ts.map +1 -1
  43. package/dist/layout/sfdp/__fixtures__/pcp-rotate-native.d.ts +23 -0
  44. package/dist/layout/sfdp/__fixtures__/pcp-rotate-native.d.ts.map +1 -0
  45. package/dist/layout/sfdp/spring-driver.d.ts +6 -0
  46. package/dist/layout/sfdp/spring-driver.d.ts.map +1 -1
  47. package/dist/ortho/index.d.ts +4 -1
  48. package/dist/ortho/index.d.ts.map +1 -1
  49. package/dist/ortho/maze.d.ts +5 -2
  50. package/dist/ortho/maze.d.ts.map +1 -1
  51. package/dist/ortho/partition.d.ts +8 -1
  52. package/dist/ortho/partition.d.ts.map +1 -1
  53. package/dist/ortho/trap-segment.d.ts +2 -1
  54. package/dist/ortho/trap-segment.d.ts.map +1 -1
  55. package/dist/ortho/trapezoid.d.ts.map +1 -1
  56. package/dist/parser/index.d.ts +0 -7
  57. package/dist/parser/index.d.ts.map +1 -1
  58. package/dist/parser/number-scan.d.ts +31 -0
  59. package/dist/parser/number-scan.d.ts.map +1 -0
  60. package/dist/pathplan/route.d.ts.map +1 -1
  61. package/dist/pathplan/solvers.d.ts +15 -3
  62. package/dist/pathplan/solvers.d.ts.map +1 -1
  63. package/dist/render/dot/edge-draw.d.ts +12 -0
  64. package/dist/render/dot/edge-draw.d.ts.map +1 -1
  65. package/dist/render/svg-graph.d.ts +6 -5
  66. package/dist/render/svg-graph.d.ts.map +1 -1
  67. package/dist/render/svg.d.ts +2 -21
  68. package/dist/render/svg.d.ts.map +1 -1
  69. package/dist/render.js +1030 -742
  70. package/dist/render.js.map +4 -4
  71. package/dist/xdot/parse.d.ts.map +1 -1
  72. package/package.json +1 -1
  73. package/src/common/arith.ts +25 -0
  74. package/src/common/edge-in-box.ts +54 -0
  75. package/src/common/graph-init.ts +3 -3
  76. package/src/common/htmltable-emit.ts +4 -2
  77. package/src/common/make-label.ts +3 -1
  78. package/src/common/poly-gencode.ts +18 -4
  79. package/src/gvc/device.ts +8 -41
  80. package/src/gvc/edge-labels.ts +30 -13
  81. package/src/gvc/emit-gates.ts +89 -0
  82. package/src/gvc/job.ts +4 -0
  83. package/src/gvc/viewport.ts +29 -0
  84. package/src/layout/circo/blockpath.ts +6 -125
  85. package/src/layout/circo/crossings.ts +171 -0
  86. package/src/layout/dot/__fixtures__/2784.dot +1 -0
  87. package/src/layout/dot/__fixtures__/2784.native-16.1.svg +48 -0
  88. package/src/layout/dot/edge-route-chain.ts +11 -5
  89. package/src/layout/dot/edge-route.ts +19 -8
  90. package/src/layout/dot/index.ts +14 -4
  91. package/src/layout/dot/ortho-adapter.ts +11 -6
  92. package/src/layout/dot/pack-components.ts +11 -4
  93. package/src/layout/dot/splines-flat-multi.ts +21 -8
  94. package/src/layout/dot/splines-flat.ts +8 -35
  95. package/src/layout/dot/splines-route.ts +10 -2
  96. package/src/layout/dot/splines.ts +6 -6
  97. package/src/layout/neato/constraint-adjust.ts +10 -4
  98. package/src/layout/neato/multispline.ts +26 -9
  99. package/src/layout/neato/splines.ts +7 -7
  100. package/src/layout/pack/poly-place.ts +9 -2
  101. package/src/layout/sfdp/__fixtures__/pcp-rotate-native.ts +94 -0
  102. package/src/layout/sfdp/spring-driver.ts +45 -13
  103. package/src/ortho/index.ts +22 -13
  104. package/src/ortho/maze.ts +8 -2
  105. package/src/ortho/partition.ts +17 -2
  106. package/src/ortho/trap-segment.ts +47 -24
  107. package/src/ortho/trapezoid.ts +24 -15
  108. package/src/parser/dot.js +345 -380
  109. package/src/parser/dot.pegjs +25 -10
  110. package/src/parser/index.ts +15 -0
  111. package/src/parser/number-scan.ts +118 -0
  112. package/src/pathplan/route.ts +79 -34
  113. package/src/pathplan/solvers.ts +78 -50
  114. package/src/render/dot/edge-draw.ts +25 -0
  115. package/src/render/svg-graph.ts +77 -47
  116. package/src/render/svg-helpers.ts +2 -2
  117. package/src/render/svg.ts +4 -41
  118. package/src/xdot/parse.ts +19 -5
@@ -434,7 +434,7 @@ export function resetRW(g: Graph): void {
434
434
  }
435
435
 
436
436
  /**
437
- * splines=ortho dispatch — mirror dotsplines.c:251-259: resetRW, run the
437
+ * splines=ortho dispatch — mirror dotsplines.c:251-266: resetRW, run the
438
438
  * orthoEdges pipeline (before mark_lowclusters), then the finish semantics
439
439
  * (skip routesplinesterm — not ported in TS; set edgeLabelsDone; return 0).
440
440
  * Edge-label sub-case is T2.
@@ -443,15 +443,15 @@ export function resetRW(g: Graph): void {
443
443
  function orthoDispatch(g: Graph): number {
444
444
  resetRW(g);
445
445
  // C: if (GD_has_labels(g->root) & EDGE_LABEL) { setEdgeLabelPos(g);
446
- // orthoEdges(g,true); } else orthoEdges(g,false);
446
+ // rc = orthoEdges(g,true); } else rc = orthoEdges(g,false);
447
447
  // orthoEdges itself warns + downgrades useLbls (ortho/index.ts) — C never
448
448
  // routes edges around labels (ADR-2). We only POSITION the labels here.
449
+ let rc: number;
449
450
  if (((g.root.info.has_labels ?? 0) & EDGE_LABEL) !== 0) {
450
451
  setEdgeLabelPos(g);
451
- dispatchOrthoEdges(g, true);
452
- } else {
453
- dispatchOrthoEdges(g, false);
454
- }
452
+ rc = dispatchOrthoEdges(g, true);
453
+ } else rc = dispatchOrthoEdges(g, false);
454
+ if (rc !== 0) return rc; // 16.1 8fd4b59ef: skip port labels, EdgeLabelsDone
455
455
  // C's `goto finish` lands ON the port-label placement block
456
456
  // (dotsplines.c:436-458): head/tail labels with labelangle/labeldistance
457
457
  // are placed via place_portlabel for ortho too. Skipping this left the
@@ -26,6 +26,7 @@ import { Edge } from '../../model/edge.js';
26
26
  import type { EdgeList } from '../../model/nodeInfo.js';
27
27
  import { DtBag } from '../../cdt/index.js';
28
28
  import { cround } from '../../common/arith.js';
29
+ import { d2i } from '../../util/math.js';
29
30
  import { rank } from '../dot/ns.js';
30
31
  import { elistAppend } from '../dot/fastgr.js';
31
32
  import { nodesInSeq } from '../dot/decomp.js';
@@ -385,8 +386,13 @@ function overlaps(p: Nitem[]): boolean {
385
386
  return false;
386
387
  }
387
388
 
388
- /** @see lib/neatogen/constraint.c:initItem */
389
- function initItem(n: Node, margin: ExpandT): Nitem {
389
+ /**
390
+ * Additive margins convert with d2i (clamp to INT_MIN..INT_MAX, else
391
+ * truncate) since graphviz 16.1 (13e5f6ce8). Exported for tests.
392
+ * @see lib/neatogen/constraint.c:initItem
393
+ * @internal
394
+ */
395
+ export function initItem(n: Node, margin: ExpandT): Nitem {
390
396
  const x = cround(SCALE * (n.info.pos?.[0] ?? 0) * POINTS_PER_INCH);
391
397
  const y = cround(SCALE * (n.info.pos?.[1] ?? 0) * POINTS_PER_INCH);
392
398
  const width = n.info.width ?? 0;
@@ -394,8 +400,8 @@ function initItem(n: Node, margin: ExpandT): Nitem {
394
400
  let w2: number;
395
401
  let h2: number;
396
402
  if (margin.doAdd) {
397
- w2 = Math.trunc(SCALE * (cround((width / 2.0) * POINTS_PER_INCH) + margin.x));
398
- h2 = Math.trunc(SCALE * (cround((height / 2.0) * POINTS_PER_INCH) + margin.y));
403
+ w2 = d2i(SCALE * (cround((width / 2.0) * POINTS_PER_INCH) + margin.x));
404
+ h2 = d2i(SCALE * (cround((height / 2.0) * POINTS_PER_INCH) + margin.y));
399
405
  } else {
400
406
  w2 = cround(margin.x * SCALE2 * width * POINTS_PER_INCH);
401
407
  h2 = cround(margin.y * SCALE2 * height * POINTS_PER_INCH);
@@ -17,6 +17,7 @@ import type { Graph } from '../../model/graph.js';
17
17
  import type { Point } from '../../pathplan/types.js';
18
18
  import type { SplineInfo } from '../../common/types.js';
19
19
  import { wind } from '../../pathplan/visibility.js';
20
+ import { fma } from '../../common/fma.js';
20
21
  import { shortestPath } from '../../pathplan/shortest.js';
21
22
  import { routeSpline, makePolyline } from '../../pathplan/index.js';
22
23
  import type { Edge as PEdge } from '../../pathplan/types.js';
@@ -85,10 +86,12 @@ function raySegIntersect(v: Point, w: Point, a: Point, b: Point): Point | null {
85
86
  const mv = { x: w.x - v.x, y: w.y - v.y };
86
87
  const lv = { x: b.x - a.x, y: b.y - a.y };
87
88
  const ln = { x: -lv.y, y: lv.x }; // perp
88
- const lc = -(ln.x * a.x + ln.y * a.y);
89
- const dt = ln.x * mv.x + ln.y * mv.y;
89
+ // libgvc's line_intersect fuses the first product of each dot():
90
+ // fmsub(lv.y, p.x, lv.x·p.y). The final a - s·mv is NOT fused.
91
+ const lc = -fma(ln.x, a.x, ln.y * a.y);
92
+ const dt = fma(ln.x, mv.x, ln.y * mv.y);
90
93
  if (Math.abs(dt) < SMALL) return null;
91
- const s = (ln.x * v.x + ln.y * v.y + lc) / dt;
94
+ const s = (fma(ln.x, v.x, ln.y * v.y) + lc) / dt;
92
95
  return { x: v.x - s * mv.x, y: v.y - s * mv.y };
93
96
  }
94
97
 
@@ -115,6 +118,19 @@ function ctrlPtIdx(v: Point, ps: Point[]): number {
115
118
 
116
119
  const SEP = 15;
117
120
 
121
+ /** v + 100·(cos, sin), fused like the binary's fmadd. @see multispline.c:mkCtrlPts */
122
+ function bisectorTarget(v: Point, cosTheta: number, sinTheta: number): Point {
123
+ return { x: fma(cosTheta, 100, v.x), y: fma(sinTheta, 100, v.y) };
124
+ }
125
+
126
+ /** DIST(p, q) = sqrt(DIST2), the sum of squares fused (fmadd(dx, dx, dy·dy)) —
127
+ * not hypot. @see lib/common/geom.h:DIST */
128
+ function distFused(p: Point, q: Point): number {
129
+ const dx = p.x - q.x;
130
+ const dy = p.y - q.y;
131
+ return Math.sqrt(fma(dx, dx, dy * dy));
132
+ }
133
+
118
134
  /**
119
135
  * mult points on the bisector ray at corridor vertex v, spaced ≤ SEP,
120
136
  * ordered by side (s = index after which vertices are on the far side).
@@ -129,31 +145,32 @@ function mkCtrlPts(
129
145
  const theta = bisect(prev, v, nxt);
130
146
  let sinTheta = Math.sin(theta);
131
147
  let cosTheta = Math.cos(theta);
132
- let w = { x: v.x + 100 * cosTheta, y: v.y + 100 * sinTheta };
148
+ // The binary fuses v + 100·cos/sin and v + (i·sep)·cos/sin (fmadd/fmla).
149
+ let w = bisectorTarget(v, cosTheta, sinTheta);
133
150
  if (idx > s) {
134
151
  if (wind(prev, v, w) !== 1) {
135
152
  sinTheta *= -1;
136
153
  cosTheta *= -1;
137
- w = { x: v.x + 100 * cosTheta, y: v.y + 100 * sinTheta };
154
+ w = bisectorTarget(v, cosTheta, sinTheta);
138
155
  }
139
156
  } else if (wind(prev, v, w) !== -1) {
140
157
  sinTheta *= -1;
141
158
  cosTheta *= -1;
142
- w = { x: v.x + 100 * cosTheta, y: v.y + 100 * sinTheta };
159
+ w = bisectorTarget(v, cosTheta, sinTheta);
143
160
  }
144
161
  const q = triPoint(trip, idx, v, w);
145
162
  if (q === null) return null;
146
163
 
147
- const d = Math.hypot(q.x - v.x, q.y - v.y);
164
+ const d = distFused(q, v);
148
165
  const sep = d >= mult * SEP ? SEP : d / mult;
149
166
  const ps = new Array<Point>(mult);
150
167
  if (idx < s) {
151
168
  for (let i = 0; i < mult; i++) {
152
- ps[i] = { x: v.x + i * sep * cosTheta, y: v.y + i * sep * sinTheta };
169
+ ps[i] = { x: fma(i * sep, cosTheta, v.x), y: fma(i * sep, sinTheta, v.y) };
153
170
  }
154
171
  } else {
155
172
  for (let i = 0; i < mult; i++) {
156
- ps[mult - i - 1] = { x: v.x + i * sep * cosTheta, y: v.y + i * sep * sinTheta };
173
+ ps[mult - i - 1] = { x: fma(i * sep, cosTheta, v.x), y: fma(i * sep, sinTheta, v.y) };
157
174
  }
158
175
  }
159
176
  return ps;
@@ -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 * a - p.x * p.x));
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 circumscribed about the ellipse (a, b).
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 of the two tangents.
144
- const x = (m0 * p0.x - p0.y - m1 * p1.x + p1.y) / (m0 - m1);
145
- const y = p0.y + m0 * (x - p0.x);
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
- const root = Math.trunc((-b + r) / (2 * a));
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
- ctx.places[info.index] = { x: ctx.step * x - LL.x, y: ctx.step * y - LL.y };
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
- for (let i = 0; i < n; i++) {
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
- axis[0] = -(-y[0]! + y[3]! -
111
- Math.sqrt(y[0]! * y[0]! + 4 * y[1]! * y[1]! - 2 * y[0]! * y[3]! + y[3]! * y[3]!))
112
- / (2 * y[1]!);
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(1 + axis[0]! * axis[0]!);
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]! * axis[0]! + x[dim * i + 1]! * axis[1]!;
120
- const x1 = -x[dim * i]! * axis[1]! + x[dim * i + 1]! * axis[0]!;
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
  }
@@ -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: ReturnType<typeof mkMaze>,
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
- ): void {
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
- // ensure the two dummy nodes exist
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
- /** @see lib/ortho/maze.c:mkMaze */
274
- export function mkMaze(g: OrthoGraph): Maze {
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)[],
@@ -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
  /**