@knowvah/dot-engine 2.3.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/common/arith.d.ts +12 -0
- package/dist/common/arith.d.ts.map +1 -1
- package/dist/common/edge-in-box.d.ts +31 -0
- package/dist/common/edge-in-box.d.ts.map +1 -0
- package/dist/common/htmltable-emit.d.ts +1 -1
- package/dist/common/htmltable-emit.d.ts.map +1 -1
- package/dist/common/make-label.d.ts +3 -1
- package/dist/common/make-label.d.ts.map +1 -1
- package/dist/common/poly-gencode.d.ts +12 -1
- package/dist/common/poly-gencode.d.ts.map +1 -1
- package/dist/gvc/device.d.ts.map +1 -1
- package/dist/gvc/emit-gates.d.ts +35 -0
- package/dist/gvc/emit-gates.d.ts.map +1 -0
- package/dist/gvc/job.d.ts +4 -0
- package/dist/gvc/job.d.ts.map +1 -1
- package/dist/gvc/viewport.d.ts +13 -0
- package/dist/gvc/viewport.d.ts.map +1 -1
- package/dist/index.js +1030 -742
- package/dist/index.js.map +4 -4
- package/dist/layout/circo/blockpath.d.ts +1 -38
- package/dist/layout/circo/blockpath.d.ts.map +1 -1
- package/dist/layout/circo/crossings.d.ts +59 -0
- package/dist/layout/circo/crossings.d.ts.map +1 -0
- package/dist/layout/dot/edge-route-chain.d.ts +5 -3
- package/dist/layout/dot/edge-route-chain.d.ts.map +1 -1
- package/dist/layout/dot/edge-route.d.ts.map +1 -1
- package/dist/layout/dot/index.d.ts +10 -2
- package/dist/layout/dot/index.d.ts.map +1 -1
- package/dist/layout/dot/ortho-adapter.d.ts +7 -3
- package/dist/layout/dot/ortho-adapter.d.ts.map +1 -1
- package/dist/layout/dot/pack-components.d.ts +4 -1
- package/dist/layout/dot/pack-components.d.ts.map +1 -1
- package/dist/layout/dot/splines-flat-multi.d.ts +7 -5
- package/dist/layout/dot/splines-flat-multi.d.ts.map +1 -1
- package/dist/layout/dot/splines-flat.d.ts +2 -13
- package/dist/layout/dot/splines-flat.d.ts.map +1 -1
- package/dist/layout/dot/splines-route.d.ts.map +1 -1
- package/dist/layout/neato/constraint-adjust.d.ts +32 -0
- package/dist/layout/neato/constraint-adjust.d.ts.map +1 -1
- package/dist/layout/neato/multispline.d.ts.map +1 -1
- package/dist/layout/neato/splines.d.ts.map +1 -1
- package/dist/layout/pack/poly-place.d.ts.map +1 -1
- package/dist/layout/sfdp/__fixtures__/pcp-rotate-native.d.ts +23 -0
- package/dist/layout/sfdp/__fixtures__/pcp-rotate-native.d.ts.map +1 -0
- package/dist/layout/sfdp/spring-driver.d.ts +6 -0
- package/dist/layout/sfdp/spring-driver.d.ts.map +1 -1
- package/dist/ortho/index.d.ts +4 -1
- package/dist/ortho/index.d.ts.map +1 -1
- package/dist/ortho/maze.d.ts +5 -2
- package/dist/ortho/maze.d.ts.map +1 -1
- package/dist/ortho/partition.d.ts +8 -1
- package/dist/ortho/partition.d.ts.map +1 -1
- package/dist/ortho/trap-segment.d.ts +2 -1
- package/dist/ortho/trap-segment.d.ts.map +1 -1
- package/dist/ortho/trapezoid.d.ts.map +1 -1
- package/dist/parser/index.d.ts +0 -7
- package/dist/parser/index.d.ts.map +1 -1
- package/dist/parser/number-scan.d.ts +31 -0
- package/dist/parser/number-scan.d.ts.map +1 -0
- package/dist/pathplan/route.d.ts.map +1 -1
- package/dist/pathplan/solvers.d.ts +15 -3
- package/dist/pathplan/solvers.d.ts.map +1 -1
- package/dist/render/dot/edge-draw.d.ts +12 -0
- package/dist/render/dot/edge-draw.d.ts.map +1 -1
- package/dist/render/svg-graph.d.ts +6 -5
- package/dist/render/svg-graph.d.ts.map +1 -1
- package/dist/render/svg.d.ts +2 -21
- package/dist/render/svg.d.ts.map +1 -1
- package/dist/render.js +1030 -742
- package/dist/render.js.map +4 -4
- package/dist/xdot/parse.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/common/arith.ts +25 -0
- package/src/common/edge-in-box.ts +54 -0
- package/src/common/graph-init.ts +3 -3
- package/src/common/htmltable-emit.ts +4 -2
- package/src/common/make-label.ts +3 -1
- package/src/common/poly-gencode.ts +18 -4
- package/src/gvc/device.ts +8 -41
- package/src/gvc/edge-labels.ts +30 -13
- package/src/gvc/emit-gates.ts +89 -0
- package/src/gvc/job.ts +4 -0
- package/src/gvc/viewport.ts +29 -0
- package/src/layout/circo/blockpath.ts +6 -125
- package/src/layout/circo/crossings.ts +171 -0
- package/src/layout/dot/__fixtures__/2784.dot +1 -0
- package/src/layout/dot/__fixtures__/2784.native-16.1.svg +48 -0
- package/src/layout/dot/edge-route-chain.ts +11 -5
- package/src/layout/dot/edge-route.ts +19 -8
- package/src/layout/dot/index.ts +14 -4
- package/src/layout/dot/ortho-adapter.ts +11 -6
- package/src/layout/dot/pack-components.ts +11 -4
- package/src/layout/dot/splines-flat-multi.ts +21 -8
- package/src/layout/dot/splines-flat.ts +8 -35
- package/src/layout/dot/splines-route.ts +10 -2
- package/src/layout/dot/splines.ts +6 -6
- package/src/layout/neato/constraint-adjust.ts +10 -4
- package/src/layout/neato/multispline.ts +26 -9
- package/src/layout/neato/splines.ts +7 -7
- package/src/layout/pack/poly-place.ts +9 -2
- package/src/layout/sfdp/__fixtures__/pcp-rotate-native.ts +94 -0
- package/src/layout/sfdp/spring-driver.ts +45 -13
- package/src/ortho/index.ts +22 -13
- package/src/ortho/maze.ts +8 -2
- package/src/ortho/partition.ts +17 -2
- package/src/ortho/trap-segment.ts +47 -24
- package/src/ortho/trapezoid.ts +24 -15
- package/src/parser/dot.js +345 -380
- package/src/parser/dot.pegjs +25 -10
- package/src/parser/index.ts +15 -0
- package/src/parser/number-scan.ts +118 -0
- package/src/pathplan/route.ts +79 -34
- package/src/pathplan/solvers.ts +78 -50
- package/src/render/dot/edge-draw.ts +25 -0
- package/src/render/svg-graph.ts +77 -47
- package/src/render/svg-helpers.ts +2 -2
- package/src/render/svg.ts +4 -41
- package/src/xdot/parse.ts +19 -5
package/src/parser/dot.pegjs
CHANGED
|
@@ -57,24 +57,36 @@ Stmt
|
|
|
57
57
|
// grammar.y: attrtype : T_graph | T_node | T_edge
|
|
58
58
|
// grammar.y: graphattrdefs : attrassignment (bare key=value at graph level)
|
|
59
59
|
AttrStmt
|
|
60
|
-
= t:AttrType _ attrs:
|
|
61
|
-
{
|
|
60
|
+
= t:AttrType _ m:OptMacroName attrs:AttrListReq
|
|
61
|
+
{
|
|
62
|
+
// grammar.y:attrstmt calls nomacros() when a macro name is present;
|
|
63
|
+
// the attributes are still applied unchanged.
|
|
64
|
+
if (m) console.error("Warning: attribute macros not implemented");
|
|
65
|
+
return { type: "attr", target: t, attrs: attrs };
|
|
66
|
+
}
|
|
62
67
|
/ a:AttrAssignment
|
|
63
68
|
{ return { type: "assign", key: a.key, value: a.value }; }
|
|
64
69
|
|
|
70
|
+
// grammar.y: optmacroname : atom '=' | /* empty */
|
|
71
|
+
// @see grammar.y:nomacros -- agwarningf("attribute macros not implemented")
|
|
72
|
+
OptMacroName
|
|
73
|
+
= Atom _ "=" _ { return true; }
|
|
74
|
+
/ "" { return false; }
|
|
75
|
+
|
|
65
76
|
// grammar.y: attrtype : T_graph | T_node | T_edge
|
|
66
77
|
AttrType
|
|
67
78
|
= GraphKw { return "graph"; }
|
|
68
79
|
/ NodeKw { return "node"; }
|
|
69
80
|
/ EdgeKw { return "edge"; }
|
|
70
81
|
|
|
71
|
-
// grammar.y:
|
|
72
|
-
|
|
73
|
-
AttrList
|
|
82
|
+
// grammar.y: attrlist : optattr '[' optattrdefs ']' (one or more bracket groups)
|
|
83
|
+
AttrListReq
|
|
74
84
|
= _ "[" _ defs:AttrDefs _ "]" rest:AttrList
|
|
75
85
|
{ return defs.concat(rest); }
|
|
76
|
-
|
|
77
|
-
|
|
86
|
+
|
|
87
|
+
// grammar.y: optattr : attrlist | /* empty */
|
|
88
|
+
AttrList
|
|
89
|
+
= AttrListReq
|
|
78
90
|
/ "" { return []; }
|
|
79
91
|
|
|
80
92
|
// grammar.y: optattrdefs : optattrdefs attrdefs | /* empty */
|
|
@@ -215,10 +227,13 @@ Name
|
|
|
215
227
|
= first:[A-Za-z_\x80-] rest:[A-Za-z_0-9\x80-]*
|
|
216
228
|
{ return first + rest.join(""); }
|
|
217
229
|
|
|
218
|
-
// scan.l: NUMBER = [-]?(({DIGIT}+(\.{DIGIT}*)?)|(\.{DIGIT}+))
|
|
219
|
-
//
|
|
230
|
+
// scan.l: NUMBER = [-]?(({DIGIT}+(\.{DIGIT}*)?)|(\.{DIGIT}+))(\.|{LETTER})?
|
|
231
|
+
// There is no exponent: `1e5` lexes as NUMBER `1e`, which chkNum splits into
|
|
232
|
+
// `1` and the NAME `e5`. The optional trailing '.'/LETTER is always split off
|
|
233
|
+
// by chkNum (yyless), so it is never part of the atom; its warning is emitted
|
|
234
|
+
// by number-scan.ts. Returned as string per spec requirement.
|
|
220
235
|
NumericLiteral
|
|
221
|
-
= s:( "-"? ( [0-9]+ ( "." [0-9]* )? / "." [0-9]+ )
|
|
236
|
+
= s:( "-"? ( [0-9]+ ( "." [0-9]* )? / "." [0-9]+ ) )
|
|
222
237
|
{ return Array.isArray(s) ? s.flat(Infinity).filter(x => x !== null && x !== undefined).join("") : String(s); }
|
|
223
238
|
|
|
224
239
|
// ── Case-insensitive keywords ─────────────────────────────────────────────────
|
package/src/parser/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ import type { GraphKind } from '../model/graph.js';
|
|
|
13
13
|
import type { Edge } from '../model/edge.js';
|
|
14
14
|
import { isHtmlValue, htmlValueContent } from '../common/html-string.js';
|
|
15
15
|
import { buildFromAst } from './builder.js';
|
|
16
|
+
import { scanNumberAmbiguities, numberAmbiguityMessage } from './number-scan.js';
|
|
16
17
|
import type { ParsedGraph } from './ast.js';
|
|
17
18
|
import type { GvErrorCode, GvExpectation } from '../errors.js';
|
|
18
19
|
import { DotEngineError, friendlyMessageFor, invalidArgType } from '../errors.js';
|
|
@@ -223,6 +224,18 @@ export function isPeggyError(err: unknown): err is {
|
|
|
223
224
|
* is too deep
|
|
224
225
|
* @throws TypeError `ERR_INVALID_ARG_TYPE` if `src` is not a string
|
|
225
226
|
*/
|
|
227
|
+
/**
|
|
228
|
+
* Emit scan.l's chkNum warnings for every badly delimited number the lexer
|
|
229
|
+
* reaches: all of them on success, those at or before a syntax error otherwise
|
|
230
|
+
* (C's lexer stops there). @see lib/cgraph/scan.l:chkNum
|
|
231
|
+
*/
|
|
232
|
+
function warnNumberAmbiguities(src: string, upTo: number): void {
|
|
233
|
+
for (const a of scanNumberAmbiguities(src, Stripper.strip(src))) {
|
|
234
|
+
if (a.offset > upTo) break;
|
|
235
|
+
console.error(numberAmbiguityMessage(a));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
226
239
|
export function parse(src: string): Graph {
|
|
227
240
|
if (typeof src !== 'string') {
|
|
228
241
|
// Runtime guard for JS callers (the TS signature already forbids this):
|
|
@@ -235,6 +248,7 @@ export function parse(src: string): Graph {
|
|
|
235
248
|
} catch (err: unknown) {
|
|
236
249
|
if (isPeggyError(err)) {
|
|
237
250
|
const start = err.location.start;
|
|
251
|
+
warnNumberAmbiguities(src, start.offset);
|
|
238
252
|
const code: GvErrorCode =
|
|
239
253
|
err.found === null ? 'SYNTAX_UNEXPECTED_EOF' : 'SYNTAX_ERROR';
|
|
240
254
|
throw new ParseError(
|
|
@@ -256,6 +270,7 @@ export function parse(src: string): Graph {
|
|
|
256
270
|
}
|
|
257
271
|
throw err;
|
|
258
272
|
}
|
|
273
|
+
warnNumberAmbiguities(src, src.length);
|
|
259
274
|
validateEdgeOperators(src, ast.directed);
|
|
260
275
|
return buildFromAst(ast);
|
|
261
276
|
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* scan.l's chkNum diagnostics. The NUMBER regexp allows one trailing '.' or
|
|
5
|
+
* LETTER so the lexer can catch a number run into a name (`00splines`) or a
|
|
6
|
+
* second dot (`1.2.3`); chkNum then warns and yyless() hands the last
|
|
7
|
+
* character back, splitting the text into two tokens. The PEG grammar already
|
|
8
|
+
* yields the split tokens; this pass reproduces the warnings, which a
|
|
9
|
+
* backtracking PEG action would emit unreliably (an alternative that fails
|
|
10
|
+
* after matching a number would re-run it).
|
|
11
|
+
*
|
|
12
|
+
* The walk runs over Stripper output (comments and string interiors blanked,
|
|
13
|
+
* same length, newlines kept), lexing the INITIAL-state tokens that can
|
|
14
|
+
* contain or abut a number: NAME, NUMBER, and the `--` / `->` edge operators.
|
|
15
|
+
*
|
|
16
|
+
* @see lib/cgraph/scan.l:106-110 (LETTER, DIGIT, NAME, NUMBER), :135, chkNum
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** One chkNum warning: the matched NUMBER text and its line. */
|
|
20
|
+
export interface NumberAmbiguity {
|
|
21
|
+
text: string;
|
|
22
|
+
line: number;
|
|
23
|
+
offset: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const ASCII_DEL = 0x7f;
|
|
27
|
+
|
|
28
|
+
/** scan.l LETTER `[A-Za-z_\200-\377]`: any non-ASCII character counts. */
|
|
29
|
+
function isLetter(c: string | undefined): boolean {
|
|
30
|
+
if (c === undefined) return false;
|
|
31
|
+
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c === '_'
|
|
32
|
+
|| c.charCodeAt(0) > ASCII_DEL;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isDigit(c: string | undefined): boolean {
|
|
36
|
+
return c !== undefined && c >= '0' && c <= '9';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** True when a NUMBER starts at i (sign, digits, or a dot before a digit). */
|
|
40
|
+
function startsNumber(s: string, i: number): boolean {
|
|
41
|
+
const c = s[i];
|
|
42
|
+
if (isDigit(c)) return true;
|
|
43
|
+
if (c === '.') return isDigit(s[i + 1]);
|
|
44
|
+
if (c !== '-') return false;
|
|
45
|
+
return isDigit(s[i + 1]) || (s[i + 1] === '.' && isDigit(s[i + 2]));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** End index of the longest NUMBER match starting at i (scan.l:109). */
|
|
49
|
+
function matchNumber(s: string, i: number): number {
|
|
50
|
+
let j = s[i] === '-' ? i + 1 : i;
|
|
51
|
+
if (s[j] === '.') {
|
|
52
|
+
j++;
|
|
53
|
+
while (isDigit(s[j])) j++;
|
|
54
|
+
} else {
|
|
55
|
+
while (isDigit(s[j])) j++;
|
|
56
|
+
if (s[j] === '.') {
|
|
57
|
+
j++;
|
|
58
|
+
while (isDigit(s[j])) j++;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (s[j] === '.' || isLetter(s[j])) j++;
|
|
62
|
+
return j;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** scan.l twoDots: the first '.' is not the last character. */
|
|
66
|
+
function twoDots(text: string): boolean {
|
|
67
|
+
const dot = text.indexOf('.');
|
|
68
|
+
return dot !== -1 && dot !== text.length - 1;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** scan.l chkNum: the last character is a letter, or a second '.'. */
|
|
72
|
+
function chkNum(text: string): boolean {
|
|
73
|
+
const c = text[text.length - 1]!;
|
|
74
|
+
return (!isDigit(c) && c !== '.') || (c === '.' && twoDots(text));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Lex one INITIAL token at i; returns the next index and, for a badly
|
|
79
|
+
* delimited NUMBER, its text (the next index is then yyless'd back by one).
|
|
80
|
+
*/
|
|
81
|
+
function lexToken(s: string, i: number): { next: number; bad: string | null } {
|
|
82
|
+
if (isLetter(s[i])) {
|
|
83
|
+
let j = i + 1;
|
|
84
|
+
while (isLetter(s[j]) || isDigit(s[j])) j++;
|
|
85
|
+
return { next: j, bad: null };
|
|
86
|
+
}
|
|
87
|
+
if (s[i] === '-' && (s[i + 1] === '-' || s[i + 1] === '>')) return { next: i + 2, bad: null };
|
|
88
|
+
if (!startsNumber(s, i)) return { next: i + 1, bad: null };
|
|
89
|
+
const j = matchNumber(s, i);
|
|
90
|
+
const text = s.slice(i, j);
|
|
91
|
+
return chkNum(text) ? { next: j - 1, bad: text } : { next: j, bad: null };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Every badly delimited NUMBER in `src`, in lexing order. `stripped` is
|
|
96
|
+
* Stripper.strip(src) (same length); tokens are lexed from it, lines are
|
|
97
|
+
* counted in `src` (the stripper blanks newlines inside comments and strings,
|
|
98
|
+
* which scan.l still counts). @see lib/cgraph/scan.l:chkNum
|
|
99
|
+
*/
|
|
100
|
+
export function scanNumberAmbiguities(src: string, stripped: string): NumberAmbiguity[] {
|
|
101
|
+
const out: NumberAmbiguity[] = [];
|
|
102
|
+
let line = 1;
|
|
103
|
+
let lineAt = 0;
|
|
104
|
+
for (let i = 0; i < stripped.length;) {
|
|
105
|
+
const { next, bad } = lexToken(stripped, i);
|
|
106
|
+
if (bad !== null) {
|
|
107
|
+
for (; lineAt < i; lineAt++) if (src[lineAt] === '\n') line++;
|
|
108
|
+
out.push({ text: bad, line, offset: i });
|
|
109
|
+
}
|
|
110
|
+
i = next;
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** agwarningf text for one chkNum ambiguity; a parsed string is "input". */
|
|
116
|
+
export function numberAmbiguityMessage(a: NumberAmbiguity): string {
|
|
117
|
+
return `Warning: syntax ambiguity - badly delimited number '${a.text}' in line ${a.line} of input splits into two tokens`;
|
|
118
|
+
}
|
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
|
}
|
|
@@ -18,6 +18,7 @@ import type { ArrowDrawOp } from '../../common/arrows-types.js';
|
|
|
18
18
|
import type { GVColor } from '../../common/color.js';
|
|
19
19
|
import type { RenderJob } from '../../gvc/job.js';
|
|
20
20
|
import { EmitState } from '../../gvc/job.js';
|
|
21
|
+
import { PenType } from '../../gvc/context.js';
|
|
21
22
|
import { resolveRenderColor } from '../color-resolve.js';
|
|
22
23
|
import { taper, taperfun } from '../../common/taper.js';
|
|
23
24
|
import { orthoRoundedRadius } from '../svg-helpers.js';
|
|
@@ -31,6 +32,9 @@ import { DotWriterBase } from './agwrite.js';
|
|
|
31
32
|
/** Pen colour when a colour-list entry is empty. @see lib/common/emit.c:2485 */
|
|
32
33
|
const DEFAULT_COLOR = 'black';
|
|
33
34
|
|
|
35
|
+
/** gvc->defaultlinestyle as style tokens. @see lib/common/emit.c:105 */
|
|
36
|
+
const DEFAULT_LINE_STYLE: readonly string[] = ['solid', 'setlinewidth(1)'];
|
|
37
|
+
|
|
34
38
|
/**
|
|
35
39
|
* One arrow primitive as an xdot op. Lifted out of emitArrows' loop unchanged —
|
|
36
40
|
* the four cases and their `filled` variants are exactly arrow_gen's.
|
|
@@ -64,6 +68,27 @@ export abstract class EdgeDrawBase extends DotWriterBase {
|
|
|
64
68
|
protected penwidth: number[] = new Array(12).fill(1);
|
|
65
69
|
protected abstract styleOp(job: RenderJob): string;
|
|
66
70
|
protected abstract penOp(job: RenderJob): string;
|
|
71
|
+
abstract polyline(pts: Point[], job: RenderJob): void;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* emit_attachment's draw calls (decorate=true): gvrender_set_style with the
|
|
75
|
+
* default line style sets the obj's rawstyle/pen/penwidth (setlinewidth 1),
|
|
76
|
+
* gvrender_set_pencolor sets the pen to the label fontcolor (first entry of
|
|
77
|
+
* a color list), then gvrender_polyline → xdot_polyline emits the style ops
|
|
78
|
+
* (`S 5 -solid`; setlinewidth is filtered), the pen color and `L 3`.
|
|
79
|
+
* @see lib/common/emit.c:emit_attachment (1886-1893)
|
|
80
|
+
* @see lib/gvc/gvrender.c:gvrender_set_style / gvrender_set_pencolor
|
|
81
|
+
*/
|
|
82
|
+
attachmentPolyline(pts: Point[], pencolor: string, job: RenderJob): void {
|
|
83
|
+
const obj = job.obj;
|
|
84
|
+
if (obj !== null) {
|
|
85
|
+
obj.rawStyle = [...DEFAULT_LINE_STYLE];
|
|
86
|
+
obj.pen = PenType.Solid;
|
|
87
|
+
obj.penWidth = 1;
|
|
88
|
+
obj.penColor = resolveRenderColor(pencolor.split(':')[0]!);
|
|
89
|
+
}
|
|
90
|
+
this.polyline(pts, job);
|
|
91
|
+
}
|
|
67
92
|
|
|
68
93
|
/**
|
|
69
94
|
* The plain single-colour spline branch of beginEdge — lifted verbatim so the
|