@knowvah/dot-engine 2.3.0 → 2.3.1
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/index.js +274 -257
- package/dist/index.js.map +4 -4
- package/dist/layout/dot/edge-route.d.ts.map +1 -1
- package/dist/layout/dot/ortho-adapter.d.ts +1 -1
- package/dist/layout/dot/ortho-adapter.d.ts.map +1 -1
- package/dist/layout/dot/splines-flat-multi.d.ts +3 -1
- package/dist/layout/dot/splines-flat-multi.d.ts.map +1 -1
- package/dist/layout/dot/splines-flat.d.ts +3 -4
- package/dist/layout/dot/splines-flat.d.ts.map +1 -1
- package/dist/pathplan/route.d.ts.map +1 -1
- package/dist/pathplan/solvers.d.ts +15 -3
- package/dist/pathplan/solvers.d.ts.map +1 -1
- package/dist/render.js +274 -257
- package/dist/render.js.map +4 -4
- package/package.json +1 -1
- package/src/layout/dot/edge-route.ts +17 -7
- package/src/layout/dot/ortho-adapter.ts +4 -3
- package/src/layout/dot/splines-flat-multi.ts +17 -4
- package/src/layout/dot/splines-flat.ts +12 -12
- package/src/parser/dot.js +68 -94
- package/src/parser/dot.pegjs +7 -6
- package/src/pathplan/route.ts +79 -34
- package/src/pathplan/solvers.ts +78 -50
package/src/pathplan/route.ts
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
import type { Point, Poly, Edge } from './types.js';
|
|
5
5
|
import { solve3 } from './solvers.js';
|
|
6
|
+
import { fma, fms } from '../common/fma.js';
|
|
7
|
+
import { libmHypot } from '../common/libm-hypot.js';
|
|
6
8
|
|
|
7
9
|
const EPSILON1 = 1e-3;
|
|
8
10
|
const EPSILON2 = 1e-6;
|
|
@@ -18,28 +20,53 @@ class RouteHelper {
|
|
|
18
20
|
static B1(t: number): number { const u = 1-t; return 3*t*u*u; }
|
|
19
21
|
static B2(t: number): number { const u = 1-t; return 3*t*t*u; }
|
|
20
22
|
static B3(t: number): number { return t*t*t; }
|
|
21
|
-
|
|
22
|
-
static
|
|
23
|
+
/** @see lib/pathplan/route.c:B01 */
|
|
24
|
+
static B01(t: number): number {
|
|
25
|
+
const u = 1-t;
|
|
26
|
+
// clang -ffp-contract=on: fmadd at route.c:488 (`tmp + 3*t`)
|
|
27
|
+
return (u*u) * fma(3, t, u);
|
|
28
|
+
}
|
|
29
|
+
/** @see lib/pathplan/route.c:B23 */
|
|
30
|
+
static B23(t: number): number {
|
|
31
|
+
const u = 1-t;
|
|
32
|
+
// clang -ffp-contract=on: fmadd at route.c:494 (`3*tmp + t`)
|
|
33
|
+
return (t*t) * fma(3, u, t);
|
|
34
|
+
}
|
|
23
35
|
|
|
36
|
+
/** @see lib/pathplan/route.c:normv */
|
|
24
37
|
static normv(v: Point): Point {
|
|
25
|
-
|
|
38
|
+
// clang -ffp-contract=on: fmadd at route.c:413 (v.x*v.x fused, v.y*v.y rounded)
|
|
39
|
+
const d = fma(v.x, v.x, v.y*v.y);
|
|
26
40
|
if (d > 1e-6) { const s = Math.sqrt(d); return { x: v.x/s, y: v.y/s }; }
|
|
27
41
|
return v;
|
|
28
42
|
}
|
|
29
43
|
static ptAdd(a: Point, b: Point): Point { return { x: a.x+b.x, y: a.y+b.y }; }
|
|
30
44
|
static ptSub(a: Point, b: Point): Point { return { x: a.x-b.x, y: a.y-b.y }; }
|
|
31
45
|
static ptScale(p: Point, c: number): Point { return { x: p.x*c, y: p.y*c }; }
|
|
32
|
-
|
|
33
|
-
static
|
|
46
|
+
/** @see lib/pathplan/route.c:dot */
|
|
47
|
+
static ptDot(a: Point, b: Point): number {
|
|
48
|
+
// clang -ffp-contract=on: fmadd at route.c:459 (p1.x*p2.x fused, p1.y*p2.y rounded)
|
|
49
|
+
return fma(a.x, b.x, a.y*b.y);
|
|
50
|
+
}
|
|
51
|
+
/** @see lib/pathplan/route.c:dist (libm hypot, no contraction) */
|
|
52
|
+
static ptDist(a: Point, b: Point): number { return libmHypot(a.x-b.x, a.y-b.y); }
|
|
34
53
|
static distN(pts: Point[]): number {
|
|
35
54
|
let rv = 0;
|
|
36
55
|
for (let i = 1; i < pts.length; i++) rv += RouteHelper.ptDist(pts[i], pts[i-1]);
|
|
37
56
|
return rv;
|
|
38
57
|
}
|
|
39
58
|
|
|
59
|
+
/**
|
|
60
|
+
* The cubic's coefficients come out of catastrophic cancellation when the
|
|
61
|
+
* spline is tangent to a barrier (tests/2646.dot), so clang's contraction
|
|
62
|
+
* here decides solve3's root count; mirror it exactly.
|
|
63
|
+
* @see lib/pathplan/route.c:points2coeff
|
|
64
|
+
*/
|
|
40
65
|
static points2coeff(v0: number, v1: number, v2: number, v3: number, c: number[]): void {
|
|
41
|
-
|
|
42
|
-
c[
|
|
66
|
+
// clang -ffp-contract=on: fmadd x2 at route.c:397 (`v3 + 3*v1`, `v0 + 3*v2`; outer sub rounded)
|
|
67
|
+
c[3] = fma(3, v1, v3) - fma(3, v2, v0);
|
|
68
|
+
// clang -ffp-contract=on: fmadd at route.c:398 (`3*v0` fused onto rounded `3*v2`, then `- 6*v1` fused)
|
|
69
|
+
c[2] = fma(-6, v1, fma(3, v0, 3*v2));
|
|
43
70
|
c[1] = 3*(v1 - v0);
|
|
44
71
|
c[0] = v0;
|
|
45
72
|
}
|
|
@@ -79,7 +106,8 @@ class RouteHelper {
|
|
|
79
106
|
const tv = xr[i];
|
|
80
107
|
if (tv >= 0 && tv <= 1) {
|
|
81
108
|
RouteHelper.points2coeff(sps[0].y, sps[1].y, sps[2].y, sps[3].y, sc);
|
|
82
|
-
|
|
109
|
+
// clang -ffp-contract=on: fmadd x3 at route.c:360-361 (Horner `sv`; `(sv - ycoeff[0]) / ycoeff[1]` plain)
|
|
110
|
+
const sv = (fma(tv, fma(tv, fma(tv, sc[3], sc[2]), sc[1]), sc[0]) - lc.yc0) / lc.yc1;
|
|
83
111
|
if (sv >= 0 && sv <= 1) RouteHelper.addRoot(tv, acc);
|
|
84
112
|
}
|
|
85
113
|
}
|
|
@@ -89,23 +117,27 @@ class RouteHelper {
|
|
|
89
117
|
static silGeneral(sps: Point[], lc: LineCoeff, acc: RootAccum): number {
|
|
90
118
|
const sc = [0,0,0,0], xr = [0,0,0];
|
|
91
119
|
const rat = lc.yc1 / lc.xc1;
|
|
120
|
+
// clang -ffp-contract=on: fmsub x4 at route.c:371-372 (`sps[k].y - rat*sps[k].x`)
|
|
92
121
|
RouteHelper.points2coeff(
|
|
93
|
-
sps[0].
|
|
94
|
-
sps[2].
|
|
95
|
-
|
|
122
|
+
fms(rat, sps[0].x, sps[0].y), fms(rat, sps[1].x, sps[1].y),
|
|
123
|
+
fms(rat, sps[2].x, sps[2].y), fms(rat, sps[3].x, sps[3].y), sc);
|
|
124
|
+
// clang -ffp-contract=on: fnmsub at route.c:374 (`rat*xcoeff[0] - ycoeff[0]`; `+=` rounded)
|
|
125
|
+
sc[0] += fma(rat, lc.xc0, -lc.yc0);
|
|
96
126
|
const xn = solve3(sc, xr);
|
|
97
127
|
if (xn === 4) return 4;
|
|
98
128
|
for (let i = 0; i < xn; i++) {
|
|
99
129
|
const tv = xr[i];
|
|
100
130
|
if (tv >= 0 && tv <= 1) {
|
|
101
131
|
RouteHelper.points2coeff(sps[0].x, sps[1].x, sps[2].x, sps[3].x, sc);
|
|
102
|
-
|
|
132
|
+
// clang -ffp-contract=on: fmadd x3 at route.c:383-384 (Horner `sv`; `(sv - xcoeff[0]) / xcoeff[1]` plain)
|
|
133
|
+
const sv = (fma(tv, fma(tv, fma(tv, sc[3], sc[2]), sc[1]), sc[0]) - lc.xc0) / lc.xc1;
|
|
103
134
|
if (sv >= 0 && sv <= 1) RouteHelper.addRoot(tv, acc);
|
|
104
135
|
}
|
|
105
136
|
}
|
|
106
137
|
return acc.n;
|
|
107
138
|
}
|
|
108
139
|
|
|
140
|
+
/** @see lib/pathplan/route.c:splineintersectsline */
|
|
109
141
|
static splineIntersectsLine(sps: Point[], lps: Point[], roots: number[]): number {
|
|
110
142
|
const lc: LineCoeff = {
|
|
111
143
|
xc0: lps[0].x, xc1: lps[1].x - lps[0].x,
|
|
@@ -156,8 +188,13 @@ class RouteHelper {
|
|
|
156
188
|
x0 += RouteHelper.ptDot(tnas[i].a[0], tmp);
|
|
157
189
|
x1 += RouteHelper.ptDot(tnas[i].a[1], tmp);
|
|
158
190
|
}
|
|
159
|
-
|
|
160
|
-
|
|
191
|
+
// C's c[1][0] is a copy of c[0][1] (route.c:174), so c10 == c01 here.
|
|
192
|
+
// clang -ffp-contract=on: fnmul+fmadd at route.c:181 (c00*c11 fused, c10*c01 rounded)
|
|
193
|
+
const det01 = fma(c00, c11, -(c01*c01));
|
|
194
|
+
// clang -ffp-contract=on: fnmul+fmadd at route.c:182 (c00*x1 fused, c01*x0 rounded)
|
|
195
|
+
const det0X = fma(c00, x1, -(c01*x0));
|
|
196
|
+
// clang -ffp-contract=on: fnmul+fmadd at route.c:183 (x0*c11 fused, x1*c01 rounded)
|
|
197
|
+
const detX1 = fma(x0, c11, -(x1*c01));
|
|
161
198
|
if (Math.abs(det01) >= 1e-6) { scale0 = detX1/det01; scale3 = det0X/det01; }
|
|
162
199
|
if (Math.abs(det01) < 1e-6 || scale0 <= 0 || scale3 <= 0) {
|
|
163
200
|
const d = RouteHelper.ptDist(inps[0], inps[n-1]) / 3;
|
|
@@ -179,6 +216,27 @@ class RouteHelper {
|
|
|
179
216
|
return tnas;
|
|
180
217
|
}
|
|
181
218
|
|
|
219
|
+
/**
|
|
220
|
+
* The max-deviation split of `reallyroutespline` (route.c:130-143): the
|
|
221
|
+
* interior polyline point farthest from the fitted bezier.
|
|
222
|
+
*
|
|
223
|
+
* Faithful contract. On a geometrically symmetric channel two interior
|
|
224
|
+
* deviations tie to within an ulp, so the split point (and the whole
|
|
225
|
+
* spline) is decided by C's exact arithmetic, which this mirrors bit for
|
|
226
|
+
* bit against the native arm64 oracle:
|
|
227
|
+
* - `dist` is libm `hypot` (route.c:448) -> `libmHypot` via `ptDist`;
|
|
228
|
+
* - the test is C's strict `(d = dist(p, inps[i])) > maxd` (route.c:139),
|
|
229
|
+
* so an exact tie keeps the first index;
|
|
230
|
+
* - clang's default `-ffp-contract=on` fuses the bezier eval at
|
|
231
|
+
* route.c:137-138 as `fma(B3,p2, fma(B2,cp2, fma(B0,p1, B1*cp1)))`; the
|
|
232
|
+
* B0..B3 weights and cp1/cp2 (route.c:130-131) are not fused. Fusing the
|
|
233
|
+
* other product first is NOT equivalent (flips pt-issue-23-w9 and 32).
|
|
234
|
+
* C is therefore not translation-equivariant on such ties (the winner
|
|
235
|
+
* depends on absolute-coordinate rounding), and neither is this port.
|
|
236
|
+
* This supersedes the former tolerant tie-break (accepted divergence A3).
|
|
237
|
+
* Contraction map: .agent-notes/pathplan-fma-contraction-map.md.
|
|
238
|
+
* @see lib/pathplan/route.c:reallyroutespline
|
|
239
|
+
*/
|
|
182
240
|
static findMaxDev(inps: Point[], tnas: Tna[], ends: SplineEndpoints): number {
|
|
183
241
|
const { p0, v0, p1, v1 } = ends;
|
|
184
242
|
const cp1 = RouteHelper.ptAdd(p0, RouteHelper.ptScale(v0, 1/3));
|
|
@@ -186,27 +244,14 @@ class RouteHelper {
|
|
|
186
244
|
let maxi = -1, maxd = -1;
|
|
187
245
|
for (let i = 1; i < inps.length - 1; i++) {
|
|
188
246
|
const t = tnas[i].t;
|
|
189
|
-
const
|
|
190
|
-
const
|
|
247
|
+
const b0 = RouteHelper.B0(t), b1 = RouteHelper.B1(t);
|
|
248
|
+
const b2 = RouteHelper.B2(t), b3 = RouteHelper.B3(t);
|
|
249
|
+
// clang -ffp-contract=on: fmadd at route.c:137 (x3; B1*cp1.x rounded first)
|
|
250
|
+
const px = fma(b3, p1.x, fma(b2, cp2.x, fma(b0, p0.x, b1*cp1.x)));
|
|
251
|
+
// clang -ffp-contract=on: fmadd at route.c:138 (x3; B1*cp1.y rounded first)
|
|
252
|
+
const py = fma(b3, p1.y, fma(b2, cp2.y, fma(b0, p0.y, b1*cp1.y)));
|
|
191
253
|
const d = RouteHelper.ptDist({ x: px, y: py }, inps[i]);
|
|
192
|
-
|
|
193
|
-
// SYMMETRIC channel the two interior deviations are an exact tie, and the
|
|
194
|
-
// winner is then decided by ~1e-14 cancellation noise in the
|
|
195
|
-
// absolute-coordinate bezier eval above — whose sign depends on absolute
|
|
196
|
-
// position. So C is NOT consistent: it splits two translation-congruent
|
|
197
|
-
// arcs toward opposite corners (e.g. 2368 `376->76` mirrors `256->436`),
|
|
198
|
-
// because Apple's macOS libm `hypot` (which generated the oracle) is a
|
|
199
|
-
// proprietary ~1-ULP-divergent implementation that no portable hypot
|
|
200
|
-
// reproduces. We deliberately keep the tolerant tie-break: a true tie
|
|
201
|
-
// always resolves to the first index, so the port is TRANSLATION-
|
|
202
|
-
// EQUIVARIANT (same channel anywhere -> same spline). This matches C where
|
|
203
|
-
// C's noise also keeps-first (#241_0 5:ne->8:nw, and the cnt=3 oracle
|
|
204
|
-
// case) and consciously diverges where C's noise keeps-second (2368
|
|
205
|
-
// `376->76`, the lone surviving residual: structural-match, maxΔ ~10pt on
|
|
206
|
-
// one flat-edge arc). See .agent-notes/2368-residual-flat-label-ranksep.md
|
|
207
|
-
// and the pow precedent (src/common/arm-pow.ts) for the same Apple-libm
|
|
208
|
-
// boundary. @see lib/pathplan/route.c:reallyroutespline (max-deviation split)
|
|
209
|
-
if (d > maxd * (1 + 1e-10) + 1e-10) { maxd = d; maxi = i; }
|
|
254
|
+
if (d > maxd) { maxd = d; maxi = i; }
|
|
210
255
|
}
|
|
211
256
|
return maxi;
|
|
212
257
|
}
|
package/src/pathplan/solvers.ts
CHANGED
|
@@ -1,65 +1,93 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EPL-2.0
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* @see lib/pathplan/solvers.c
|
|
4
|
+
*
|
|
5
|
+
* The native oracle's libpathplan is compiled by clang with the default
|
|
6
|
+
* `-ffp-contract=on`, which fuses five multiply-adds in this file into single
|
|
7
|
+
* fmadd/fmsub/fnmsub instructions (solve2 is inlined into solve3). Those sites
|
|
8
|
+
* are mirrored with the software `fma`; every other operation is plain IEEE
|
|
9
|
+
* double, in C's evaluation order. Contraction map:
|
|
10
|
+
* .agent-notes/pathplan-fma-contraction-map.md.
|
|
11
|
+
*/
|
|
3
12
|
|
|
13
|
+
import { fma } from '../common/fma.js';
|
|
14
|
+
|
|
15
|
+
/** @see lib/pathplan/solvers.c:EPS */
|
|
4
16
|
const EPS = 1e-7;
|
|
5
17
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
18
|
+
/** @see lib/pathplan/solvers.c:AEQ0 */
|
|
19
|
+
function aeq0(x: number): boolean {
|
|
20
|
+
return x < EPS && x > -EPS;
|
|
21
|
+
}
|
|
10
22
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Solve cubic coeff[3]*x^3 + coeff[2]*x^2 + coeff[1]*x + coeff[0] = 0.
|
|
25
|
+
* Returns the root count; 4 means every x satisfies.
|
|
26
|
+
* @see lib/pathplan/solvers.c:solve3
|
|
27
|
+
*/
|
|
28
|
+
export function solve3(coeff: number[], roots: number[]): number {
|
|
29
|
+
const a = coeff[3], b = coeff[2], c = coeff[1], d = coeff[0];
|
|
30
|
+
if (aeq0(a)) return solve2(coeff, roots);
|
|
31
|
+
const bOver3a = b / (3 * a), cOverA = c / a, dOverA = d / a;
|
|
16
32
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
r[1] = -2 * b2a - r[0];
|
|
25
|
-
return 2;
|
|
26
|
-
}
|
|
27
|
-
r[0] = -b2a;
|
|
28
|
-
return 1;
|
|
29
|
-
}
|
|
33
|
+
let p = bOver3a * bOver3a;
|
|
34
|
+
// clang -ffp-contract=on: fnmul+fmadd at solvers.c:41 (`2*b_over_3a*p` fused,
|
|
35
|
+
// `b_over_3a*c_over_a` rounded; `+ d_over_a` is a separate rounded add)
|
|
36
|
+
const q = dOverA + fma(2 * bOver3a, p, -(bOver3a * cOverA));
|
|
37
|
+
p = cOverA / 3 - p;
|
|
38
|
+
// clang -ffp-contract=on: fmadd at solvers.c:43 (q*q fused, 4*p*p*p rounded)
|
|
39
|
+
const disc = fma(q, q, 4 * p * p * p);
|
|
30
40
|
|
|
31
|
-
|
|
32
|
-
|
|
41
|
+
let rootn: number;
|
|
42
|
+
if (disc < 0) {
|
|
43
|
+
// clang -ffp-contract=on: fnmsub at solvers.c:46 (`-disc + q*q`)
|
|
44
|
+
const r = 0.5 * Math.sqrt(fma(q, q, -disc));
|
|
33
45
|
const theta = Math.atan2(Math.sqrt(-disc), -q);
|
|
34
|
-
const temp = 2 * Math.cbrt(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
46
|
+
const temp = 2 * Math.cbrt(r);
|
|
47
|
+
roots[0] = temp * Math.cos(theta / 3);
|
|
48
|
+
roots[1] = temp * Math.cos((theta + Math.PI + Math.PI) / 3);
|
|
49
|
+
roots[2] = temp * Math.cos((theta - Math.PI - Math.PI) / 3);
|
|
50
|
+
rootn = 3;
|
|
51
|
+
} else {
|
|
52
|
+
const alpha = 0.5 * (Math.sqrt(disc) - q);
|
|
53
|
+
const beta = -q - alpha;
|
|
54
|
+
roots[0] = Math.cbrt(alpha) + Math.cbrt(beta);
|
|
55
|
+
if (disc > 0) rootn = 1;
|
|
56
|
+
else { roots[1] = roots[2] = -0.5 * roots[0]; rootn = 3; }
|
|
39
57
|
}
|
|
40
58
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
59
|
+
for (let i = 0; i < rootn; i++) roots[i] -= bOver3a;
|
|
60
|
+
return rootn;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** @see lib/pathplan/solvers.c:solve2 */
|
|
64
|
+
function solve2(coeff: number[], roots: number[]): number {
|
|
65
|
+
const a = coeff[2], b = coeff[1], c = coeff[0];
|
|
66
|
+
if (aeq0(a)) return solve1(coeff, roots);
|
|
67
|
+
const bOver2a = b / (2 * a);
|
|
68
|
+
const cOverA = c / a;
|
|
48
69
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
70
|
+
// clang -ffp-contract=on: fmadd at solvers.c:80 (`b_over_2a*b_over_2a - c_over_a`)
|
|
71
|
+
const disc = fma(bOver2a, bOver2a, -cOverA);
|
|
72
|
+
if (disc < 0) {
|
|
73
|
+
return 0;
|
|
74
|
+
} else if (disc > 0) {
|
|
75
|
+
roots[0] = -bOver2a + Math.sqrt(disc);
|
|
76
|
+
// clang -ffp-contract=on: fnmsub at solvers.c:85 (`-2*b_over_2a - roots[0]`)
|
|
77
|
+
roots[1] = fma(-2, bOver2a, -roots[0]);
|
|
78
|
+
return 2;
|
|
58
79
|
}
|
|
80
|
+
roots[0] = -bOver2a;
|
|
81
|
+
return 1;
|
|
59
82
|
}
|
|
60
83
|
|
|
61
|
-
/**
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
84
|
+
/** @see lib/pathplan/solvers.c:solve1 (no contraction) */
|
|
85
|
+
function solve1(coeff: number[], roots: number[]): number {
|
|
86
|
+
const a = coeff[1], b = coeff[0];
|
|
87
|
+
if (aeq0(a)) {
|
|
88
|
+
if (aeq0(b)) return 4;
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
roots[0] = -b / a;
|
|
92
|
+
return 1;
|
|
65
93
|
}
|