@knowvah/dot-engine 2.3.1 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/dist/common/arith.d.ts +12 -0
  2. package/dist/common/arith.d.ts.map +1 -1
  3. package/dist/common/edge-in-box.d.ts +31 -0
  4. package/dist/common/edge-in-box.d.ts.map +1 -0
  5. package/dist/common/htmltable-emit.d.ts +1 -1
  6. package/dist/common/htmltable-emit.d.ts.map +1 -1
  7. package/dist/common/make-label.d.ts +3 -1
  8. package/dist/common/make-label.d.ts.map +1 -1
  9. package/dist/common/poly-gencode.d.ts +12 -1
  10. package/dist/common/poly-gencode.d.ts.map +1 -1
  11. package/dist/gvc/device.d.ts.map +1 -1
  12. package/dist/gvc/emit-gates.d.ts +35 -0
  13. package/dist/gvc/emit-gates.d.ts.map +1 -0
  14. package/dist/gvc/job.d.ts +4 -0
  15. package/dist/gvc/job.d.ts.map +1 -1
  16. package/dist/gvc/viewport.d.ts +13 -0
  17. package/dist/gvc/viewport.d.ts.map +1 -1
  18. package/dist/index.js +813 -542
  19. package/dist/index.js.map +4 -4
  20. package/dist/layout/circo/blockpath.d.ts +1 -38
  21. package/dist/layout/circo/blockpath.d.ts.map +1 -1
  22. package/dist/layout/circo/crossings.d.ts +59 -0
  23. package/dist/layout/circo/crossings.d.ts.map +1 -0
  24. package/dist/layout/dot/edge-route-chain.d.ts +5 -3
  25. package/dist/layout/dot/edge-route-chain.d.ts.map +1 -1
  26. package/dist/layout/dot/edge-route.d.ts.map +1 -1
  27. package/dist/layout/dot/index.d.ts +10 -2
  28. package/dist/layout/dot/index.d.ts.map +1 -1
  29. package/dist/layout/dot/ortho-adapter.d.ts +6 -2
  30. package/dist/layout/dot/ortho-adapter.d.ts.map +1 -1
  31. package/dist/layout/dot/pack-components.d.ts +4 -1
  32. package/dist/layout/dot/pack-components.d.ts.map +1 -1
  33. package/dist/layout/dot/splines-flat-multi.d.ts +4 -4
  34. package/dist/layout/dot/splines-flat.d.ts +1 -11
  35. package/dist/layout/dot/splines-flat.d.ts.map +1 -1
  36. package/dist/layout/dot/splines-route.d.ts.map +1 -1
  37. package/dist/layout/neato/constraint-adjust.d.ts +32 -0
  38. package/dist/layout/neato/constraint-adjust.d.ts.map +1 -1
  39. package/dist/layout/neato/multispline.d.ts.map +1 -1
  40. package/dist/layout/neato/splines.d.ts.map +1 -1
  41. package/dist/layout/pack/poly-place.d.ts.map +1 -1
  42. package/dist/layout/sfdp/__fixtures__/pcp-rotate-native.d.ts +23 -0
  43. package/dist/layout/sfdp/__fixtures__/pcp-rotate-native.d.ts.map +1 -0
  44. package/dist/layout/sfdp/spring-driver.d.ts +6 -0
  45. package/dist/layout/sfdp/spring-driver.d.ts.map +1 -1
  46. package/dist/ortho/index.d.ts +4 -1
  47. package/dist/ortho/index.d.ts.map +1 -1
  48. package/dist/ortho/maze.d.ts +5 -2
  49. package/dist/ortho/maze.d.ts.map +1 -1
  50. package/dist/ortho/partition.d.ts +8 -1
  51. package/dist/ortho/partition.d.ts.map +1 -1
  52. package/dist/ortho/trap-segment.d.ts +2 -1
  53. package/dist/ortho/trap-segment.d.ts.map +1 -1
  54. package/dist/ortho/trapezoid.d.ts.map +1 -1
  55. package/dist/parser/index.d.ts +0 -7
  56. package/dist/parser/index.d.ts.map +1 -1
  57. package/dist/parser/number-scan.d.ts +31 -0
  58. package/dist/parser/number-scan.d.ts.map +1 -0
  59. package/dist/render/dot/edge-draw.d.ts +12 -0
  60. package/dist/render/dot/edge-draw.d.ts.map +1 -1
  61. package/dist/render/svg-graph.d.ts +6 -5
  62. package/dist/render/svg-graph.d.ts.map +1 -1
  63. package/dist/render/svg.d.ts +2 -21
  64. package/dist/render/svg.d.ts.map +1 -1
  65. package/dist/render.js +813 -542
  66. package/dist/render.js.map +4 -4
  67. package/dist/xdot/parse.d.ts.map +1 -1
  68. package/package.json +1 -1
  69. package/src/common/arith.ts +25 -0
  70. package/src/common/edge-in-box.ts +54 -0
  71. package/src/common/graph-init.ts +3 -3
  72. package/src/common/htmltable-emit.ts +4 -2
  73. package/src/common/make-label.ts +3 -1
  74. package/src/common/poly-gencode.ts +18 -4
  75. package/src/gvc/device.ts +8 -41
  76. package/src/gvc/edge-labels.ts +30 -13
  77. package/src/gvc/emit-gates.ts +89 -0
  78. package/src/gvc/job.ts +4 -0
  79. package/src/gvc/viewport.ts +29 -0
  80. package/src/layout/circo/blockpath.ts +6 -125
  81. package/src/layout/circo/crossings.ts +171 -0
  82. package/src/layout/dot/__fixtures__/2784.dot +1 -0
  83. package/src/layout/dot/__fixtures__/2784.native-16.1.svg +48 -0
  84. package/src/layout/dot/edge-route-chain.ts +11 -5
  85. package/src/layout/dot/edge-route.ts +2 -1
  86. package/src/layout/dot/index.ts +14 -4
  87. package/src/layout/dot/ortho-adapter.ts +7 -3
  88. package/src/layout/dot/pack-components.ts +11 -4
  89. package/src/layout/dot/splines-flat-multi.ts +4 -4
  90. package/src/layout/dot/splines-flat.ts +5 -32
  91. package/src/layout/dot/splines-route.ts +10 -2
  92. package/src/layout/dot/splines.ts +6 -6
  93. package/src/layout/neato/constraint-adjust.ts +10 -4
  94. package/src/layout/neato/multispline.ts +26 -9
  95. package/src/layout/neato/splines.ts +7 -7
  96. package/src/layout/pack/poly-place.ts +9 -2
  97. package/src/layout/sfdp/__fixtures__/pcp-rotate-native.ts +94 -0
  98. package/src/layout/sfdp/spring-driver.ts +45 -13
  99. package/src/ortho/index.ts +22 -13
  100. package/src/ortho/maze.ts +8 -2
  101. package/src/ortho/partition.ts +17 -2
  102. package/src/ortho/trap-segment.ts +47 -24
  103. package/src/ortho/trapezoid.ts +24 -15
  104. package/src/parser/dot.js +334 -343
  105. package/src/parser/dot.pegjs +19 -5
  106. package/src/parser/index.ts +15 -0
  107. package/src/parser/number-scan.ts +118 -0
  108. package/src/render/dot/edge-draw.ts +25 -0
  109. package/src/render/svg-graph.ts +77 -47
  110. package/src/render/svg-helpers.ts +2 -2
  111. package/src/render/svg.ts +4 -41
  112. package/src/xdot/parse.ts +19 -5
@@ -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
  /**
@@ -198,6 +198,7 @@ export function swapSegEndpoints(s: SegmentT): void {
198
198
 
199
199
  /**
200
200
  * Insert one segment into the trapezoidation.
201
+ * @returns 0 on success, -1 when threading met an invalid trapezoid (16.1)
201
202
  * @see lib/ortho/trapezoid.c:add_segment
202
203
  */
203
204
  export function addSegment(
@@ -205,7 +206,7 @@ export function addSegment(
205
206
  seg: SegmentT[],
206
207
  tr: TrapT[],
207
208
  qs: QNode[],
208
- ): void {
209
+ ): number {
209
210
  const s = { ...seg[segnum] };
210
211
  let isSwapped = false;
211
212
  if (greaterThanOrEq(s.v1, s.v0) && !equalTo(s.v1, s.v0)) {
@@ -214,31 +215,49 @@ export function addSegment(
214
215
  isSwapped = true;
215
216
  }
216
217
 
217
- let tfirst: number;
218
- if (!inserted(segnum, seg, isSwapped ? LASTPT : FIRSTPT)) {
219
- tfirst = insertV0(s, seg, tr, qs, segnum);
220
- } else {
221
- tfirst = locateEndpoint(s.v0, s.v1, s.root0, seg, qs);
222
- }
218
+ const ctx: ThreadCtx = { s, seg, tr, qs, segnum, tribot: false, isSwapped };
219
+ const tfirst = firstTrap(ctx);
220
+ const tlast = lastTrap(ctx);
221
+ if (threadSegment(ctx, tfirst, tlast) !== 0) return -1;
222
+ seg[segnum].isInserted = true;
223
+ return 0;
224
+ }
223
225
 
224
- let tlast: number;
225
- let tribot = false;
226
- if (!inserted(segnum, seg, isSwapped ? FIRSTPT : LASTPT)) {
227
- tlast = insertV1(s, seg, tr, qs, segnum);
228
- } else {
229
- tlast = locateEndpoint(s.v1, s.v0, s.root1, seg, qs);
230
- tribot = true;
226
+ /** add_segment locals the threading loop reads (one C stack frame). */
227
+ interface ThreadCtx {
228
+ s: SegmentT; seg: SegmentT[]; tr: TrapT[]; qs: QNode[];
229
+ segnum: number; tribot: boolean; isSwapped: boolean;
230
+ }
231
+
232
+ /** add_segment: insert v0 in the tree, or locate it if already present. */
233
+ function firstTrap(c: ThreadCtx): number {
234
+ if (!inserted(c.segnum, c.seg, c.isSwapped ? LASTPT : FIRSTPT)) {
235
+ return insertV0(c.s, c.seg, c.tr, c.qs, c.segnum);
231
236
  }
237
+ return locateEndpoint(c.s.v0, c.s.v1, c.s.root0, c.seg, c.qs);
238
+ }
232
239
 
233
- threadSegment(s, seg, tr, qs, segnum, tfirst, tlast, tribot, isSwapped);
234
- seg[segnum].isInserted = true;
240
+ /**
241
+ * add_segment: insert v1 in the tree, or (v1 already present) get the
242
+ * lowermost intersecting trapezoid and mark the bottom as a triangle.
243
+ */
244
+ function lastTrap(c: ThreadCtx): number {
245
+ if (!inserted(c.segnum, c.seg, c.isSwapped ? FIRSTPT : LASTPT)) {
246
+ return insertV1(c.s, c.seg, c.tr, c.qs, c.segnum);
247
+ }
248
+ c.tribot = true;
249
+ return locateEndpoint(c.s.v1, c.s.v0, c.s.root1, c.seg, c.qs);
235
250
  }
236
251
 
237
- function threadSegment(
238
- s: SegmentT, seg: SegmentT[], tr: TrapT[], qs: QNode[],
239
- segnum: number, tfirst: number, tlast: number,
240
- tribot: boolean, isSwapped: boolean,
241
- ): void {
252
+ /**
253
+ * Split every trapezoid s crosses, top to bottom, then merge both sides.
254
+ * Returns -1 (after C's stderr line) when a crossed trapezoid has no valid
255
+ * lower neighbour — graphviz 16.1 (3c01aaeb4) returns from add_segment there;
256
+ * 15.x printed "add_segment: error" and broke out of the loop.
257
+ * @see lib/ortho/trapezoid.c:add_segment (threading loop)
258
+ */
259
+ function threadSegment(c: ThreadCtx, tfirst: number, tlast: number): number {
260
+ const { s, seg, tr, qs, segnum, tribot, isSwapped } = c;
242
261
  let tfirstr = INVALID_TRAP;
243
262
  let tlastr = INVALID_TRAP;
244
263
  let t = tfirst;
@@ -258,12 +277,17 @@ function threadSegment(
258
277
  if (equalTo(tr[t].lo, tr[tlast].lo)) tlastr = tn;
259
278
 
260
279
  const tSav = t; const tnSav = tn;
280
+ if (!isValidTrap(tr[t].d0) && !isValidTrap(tr[t].d1)) { // error
281
+ console.error("trapezoid segment construction failed");
282
+ return -1;
283
+ }
261
284
  t = advanceT(s, seg, tr, t, tn, tlast, segnum, isSwapped, tribot);
262
285
  tr[tSav].rseg = segnum; tr[tnSav].lseg = segnum;
263
286
  }
264
287
 
265
288
  mergeTrapezoids(segnum, tfirst, tlast, S_LEFT, tr, qs);
266
289
  mergeTrapezoids(segnum, tfirstr, tlastr, S_RIGHT, tr, qs);
290
+ return 0;
267
291
  }
268
292
 
269
293
  function advanceT(
@@ -271,9 +295,8 @@ function advanceT(
271
295
  t: number, tn: number, tlast: number,
272
296
  segnum: number, isSwapped: boolean, tribot: boolean,
273
297
  ): number {
274
- if (!isValidTrap(tr[t].d0) && !isValidTrap(tr[t].d1)) {
275
- return INVALID_TRAP; // error
276
- } else if (isValidTrap(tr[t].d0) && !isValidTrap(tr[t].d1)) {
298
+ // The "neither below is valid" error case returns from threadSegment first.
299
+ if (isValidTrap(tr[t].d0) && !isValidTrap(tr[t].d1)) {
277
300
  handleD0Only(s, seg, tr, t, tn, tlast, segnum, isSwapped, tribot);
278
301
  return tr[t].d0;
279
302
  } else if (!isValidTrap(tr[t].d0) && isValidTrap(tr[t].d1)) {
@@ -31,29 +31,20 @@ export function constructTrapezoids(
31
31
  permute: number[],
32
32
  ): TrapT[] {
33
33
  const qs: QNode[] = [];
34
- const tr: TrapT[] = [];
35
- // index 0 is a sentinel (invalid trap)
36
- tr.push({
37
- lseg: 0, rseg: 0,
38
- hi: { x: 0, y: 0 }, lo: { x: 0, y: 0 },
39
- u0: 0, u1: 0, d0: 0, d1: 0,
40
- sink: 0, usave: 0, uside: 0, isValid: false,
41
- });
34
+ const tr: TrapT[] = [sentinelTrap()];
42
35
 
43
36
  let segi = 0;
44
37
  const root = initQueryStructure(permute[segi++], seg, tr, qs);
38
+ seedRoots(nseg, seg, root);
45
39
 
46
- for (let i = 1; i <= nseg; i++) {
47
- seg[i].root0 = root;
48
- seg[i].root1 = root;
49
- }
50
-
40
+ // C (16.1, 3660b260e): an add_segment failure LIST_FREEs the trapezoid list
41
+ // and jumps to `done`, so the caller receives an EMPTY list.
51
42
  const logstar = mathLogstarN(nseg);
52
43
  for (let h = 1; h <= logstar; h++) {
53
44
  const lo = mathN(nseg, h - 1) + 1;
54
45
  const hi = mathN(nseg, h);
55
46
  for (let i = lo; i <= hi; i++) {
56
- addSegment(permute[segi++], seg, tr, qs);
47
+ if (addSegment(permute[segi++], seg, tr, qs) !== 0) return [];
57
48
  }
58
49
  for (let i = 1; i <= nseg; i++) {
59
50
  findNewRoots(i, seg, tr, qs);
@@ -62,12 +53,30 @@ export function constructTrapezoids(
62
53
 
63
54
  const remaining = mathN(nseg, logstar) + 1;
64
55
  for (let i = remaining; i <= nseg; i++) {
65
- addSegment(permute[segi++], seg, tr, qs);
56
+ if (addSegment(permute[segi++], seg, tr, qs) !== 0) return [];
66
57
  }
67
58
 
68
59
  return tr;
69
60
  }
70
61
 
62
+ /** Index 0 of the trapezoid list is a sentinel (invalid trap). */
63
+ function sentinelTrap(): TrapT {
64
+ return {
65
+ lseg: 0, rseg: 0,
66
+ hi: { x: 0, y: 0 }, lo: { x: 0, y: 0 },
67
+ u0: 0, u1: 0, d0: 0, d1: 0,
68
+ sink: 0, usave: 0, uside: 0, isValid: false,
69
+ };
70
+ }
71
+
72
+ /** construct_trapezoids: every segment starts at the initial query root. */
73
+ function seedRoots(nseg: number, seg: SegmentT[], root: number): void {
74
+ for (let i = 1; i <= nseg; i++) {
75
+ seg[i].root0 = root;
76
+ seg[i].root1 = root;
77
+ }
78
+ }
79
+
71
80
  // ─── init_query_structure ─────────────────────────────────────────────────────
72
81
 
73
82
  import {