@knowvah/dot-engine 2.3.1 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/common/arith.d.ts +12 -0
- package/dist/common/arith.d.ts.map +1 -1
- package/dist/common/edge-in-box.d.ts +31 -0
- package/dist/common/edge-in-box.d.ts.map +1 -0
- package/dist/common/htmltable-emit.d.ts +1 -1
- package/dist/common/htmltable-emit.d.ts.map +1 -1
- package/dist/common/make-label.d.ts +3 -1
- package/dist/common/make-label.d.ts.map +1 -1
- package/dist/common/poly-gencode.d.ts +12 -1
- package/dist/common/poly-gencode.d.ts.map +1 -1
- package/dist/gvc/device.d.ts.map +1 -1
- package/dist/gvc/emit-gates.d.ts +35 -0
- package/dist/gvc/emit-gates.d.ts.map +1 -0
- package/dist/gvc/job.d.ts +4 -0
- package/dist/gvc/job.d.ts.map +1 -1
- package/dist/gvc/viewport.d.ts +13 -0
- package/dist/gvc/viewport.d.ts.map +1 -1
- package/dist/index.js +813 -542
- package/dist/index.js.map +4 -4
- package/dist/layout/circo/blockpath.d.ts +1 -38
- package/dist/layout/circo/blockpath.d.ts.map +1 -1
- package/dist/layout/circo/crossings.d.ts +59 -0
- package/dist/layout/circo/crossings.d.ts.map +1 -0
- package/dist/layout/dot/edge-route-chain.d.ts +5 -3
- package/dist/layout/dot/edge-route-chain.d.ts.map +1 -1
- package/dist/layout/dot/edge-route.d.ts.map +1 -1
- package/dist/layout/dot/index.d.ts +10 -2
- package/dist/layout/dot/index.d.ts.map +1 -1
- package/dist/layout/dot/ortho-adapter.d.ts +6 -2
- package/dist/layout/dot/ortho-adapter.d.ts.map +1 -1
- package/dist/layout/dot/pack-components.d.ts +4 -1
- package/dist/layout/dot/pack-components.d.ts.map +1 -1
- package/dist/layout/dot/splines-flat-multi.d.ts +4 -4
- package/dist/layout/dot/splines-flat.d.ts +1 -11
- package/dist/layout/dot/splines-flat.d.ts.map +1 -1
- package/dist/layout/dot/splines-route.d.ts.map +1 -1
- package/dist/layout/neato/constraint-adjust.d.ts +32 -0
- package/dist/layout/neato/constraint-adjust.d.ts.map +1 -1
- package/dist/layout/neato/multispline.d.ts.map +1 -1
- package/dist/layout/neato/splines.d.ts.map +1 -1
- package/dist/layout/pack/poly-place.d.ts.map +1 -1
- package/dist/layout/sfdp/__fixtures__/pcp-rotate-native.d.ts +23 -0
- package/dist/layout/sfdp/__fixtures__/pcp-rotate-native.d.ts.map +1 -0
- package/dist/layout/sfdp/spring-driver.d.ts +6 -0
- package/dist/layout/sfdp/spring-driver.d.ts.map +1 -1
- package/dist/ortho/index.d.ts +4 -1
- package/dist/ortho/index.d.ts.map +1 -1
- package/dist/ortho/maze.d.ts +5 -2
- package/dist/ortho/maze.d.ts.map +1 -1
- package/dist/ortho/partition.d.ts +8 -1
- package/dist/ortho/partition.d.ts.map +1 -1
- package/dist/ortho/trap-segment.d.ts +2 -1
- package/dist/ortho/trap-segment.d.ts.map +1 -1
- package/dist/ortho/trapezoid.d.ts.map +1 -1
- package/dist/parser/index.d.ts +0 -7
- package/dist/parser/index.d.ts.map +1 -1
- package/dist/parser/number-scan.d.ts +31 -0
- package/dist/parser/number-scan.d.ts.map +1 -0
- package/dist/render/dot/edge-draw.d.ts +12 -0
- package/dist/render/dot/edge-draw.d.ts.map +1 -1
- package/dist/render/svg-graph.d.ts +6 -5
- package/dist/render/svg-graph.d.ts.map +1 -1
- package/dist/render/svg.d.ts +2 -21
- package/dist/render/svg.d.ts.map +1 -1
- package/dist/render.js +813 -542
- package/dist/render.js.map +4 -4
- package/dist/xdot/parse.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/common/arith.ts +25 -0
- package/src/common/edge-in-box.ts +54 -0
- package/src/common/graph-init.ts +3 -3
- package/src/common/htmltable-emit.ts +4 -2
- package/src/common/make-label.ts +3 -1
- package/src/common/poly-gencode.ts +18 -4
- package/src/gvc/device.ts +8 -41
- package/src/gvc/edge-labels.ts +30 -13
- package/src/gvc/emit-gates.ts +89 -0
- package/src/gvc/job.ts +4 -0
- package/src/gvc/viewport.ts +29 -0
- package/src/layout/circo/blockpath.ts +6 -125
- package/src/layout/circo/crossings.ts +171 -0
- package/src/layout/dot/__fixtures__/2784.dot +1 -0
- package/src/layout/dot/__fixtures__/2784.native-16.1.svg +48 -0
- package/src/layout/dot/edge-route-chain.ts +11 -5
- package/src/layout/dot/edge-route.ts +2 -1
- package/src/layout/dot/index.ts +14 -4
- package/src/layout/dot/ortho-adapter.ts +7 -3
- package/src/layout/dot/pack-components.ts +11 -4
- package/src/layout/dot/splines-flat-multi.ts +4 -4
- package/src/layout/dot/splines-flat.ts +5 -32
- package/src/layout/dot/splines-route.ts +10 -2
- package/src/layout/dot/splines.ts +6 -6
- package/src/layout/neato/constraint-adjust.ts +10 -4
- package/src/layout/neato/multispline.ts +26 -9
- package/src/layout/neato/splines.ts +7 -7
- package/src/layout/pack/poly-place.ts +9 -2
- package/src/layout/sfdp/__fixtures__/pcp-rotate-native.ts +94 -0
- package/src/layout/sfdp/spring-driver.ts +45 -13
- package/src/ortho/index.ts +22 -13
- package/src/ortho/maze.ts +8 -2
- package/src/ortho/partition.ts +17 -2
- package/src/ortho/trap-segment.ts +47 -24
- package/src/ortho/trapezoid.ts +24 -15
- package/src/parser/dot.js +334 -343
- package/src/parser/dot.pegjs +19 -5
- package/src/parser/index.ts +15 -0
- package/src/parser/number-scan.ts +118 -0
- package/src/render/dot/edge-draw.ts +25 -0
- package/src/render/svg-graph.ts +77 -47
- package/src/render/svg-helpers.ts +2 -2
- package/src/render/svg.ts +4 -41
- package/src/xdot/parse.ts +19 -5
|
@@ -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
|
/**
|
|
@@ -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
|
-
):
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
234
|
-
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
275
|
-
|
|
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)) {
|
package/src/ortho/trapezoid.ts
CHANGED
|
@@ -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
|
-
|
|
47
|
-
|
|
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 {
|