@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
package/src/parser/dot.pegjs
CHANGED
|
@@ -57,11 +57,22 @@ 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:AttrListReq
|
|
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"; }
|
|
@@ -216,10 +227,13 @@ Name
|
|
|
216
227
|
= first:[A-Za-z_\x80-] rest:[A-Za-z_0-9\x80-]*
|
|
217
228
|
{ return first + rest.join(""); }
|
|
218
229
|
|
|
219
|
-
// scan.l: NUMBER = [-]?(({DIGIT}+(\.{DIGIT}*)?)|(\.{DIGIT}+))
|
|
220
|
-
//
|
|
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.
|
|
221
235
|
NumericLiteral
|
|
222
|
-
= s:( "-"? ( [0-9]+ ( "." [0-9]* )? / "." [0-9]+ )
|
|
236
|
+
= s:( "-"? ( [0-9]+ ( "." [0-9]* )? / "." [0-9]+ ) )
|
|
223
237
|
{ return Array.isArray(s) ? s.flat(Infinity).filter(x => x !== null && x !== undefined).join("") : String(s); }
|
|
224
238
|
|
|
225
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
|
+
}
|
|
@@ -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
|
package/src/render/svg-graph.ts
CHANGED
|
@@ -16,12 +16,12 @@ import type { Box, Point } from '../model/geom.js';
|
|
|
16
16
|
import type { RenderJob } from '../gvc/job.js';
|
|
17
17
|
import { createObjState, ObjType } from '../gvc/job.js';
|
|
18
18
|
import { FillType } from '../gvc/context.js';
|
|
19
|
-
import { escapeXml, escapeXmlTitle } from './svg-helpers.js';
|
|
19
|
+
import { escapeXml, escapeXmlTitle, svgComment } from './svg-helpers.js';
|
|
20
20
|
import { resolveRenderColor, colorPaint } from './color-resolve.js';
|
|
21
21
|
import { svgGraphId, svgGraphClass } from './svg-id.js';
|
|
22
22
|
import { parseGradientSpec } from '../common/htmltable-emit-fill.js';
|
|
23
23
|
import { findStopColor, parseStyleFlags } from '../common/style-resolve.js';
|
|
24
|
-
import {
|
|
24
|
+
import { croundNarrow32 } from '../common/arith.js';
|
|
25
25
|
import { FontnameKind } from '../model/layoutParams.js';
|
|
26
26
|
import {
|
|
27
27
|
emitLinearGradient,
|
|
@@ -66,7 +66,7 @@ function formatG(n: number): string {
|
|
|
66
66
|
* (svgBeginPage). ViewBox starts at (round(margin.x), round(margin.y)) --
|
|
67
67
|
* (0,0) only in the common no-margin case.
|
|
68
68
|
*/
|
|
69
|
-
export function emitSvgTag(job: RenderJob): void {
|
|
69
|
+
export function emitSvgTag(job: RenderJob, svgid: string | undefined = ''): void {
|
|
70
70
|
const bb = job.bb;
|
|
71
71
|
// Size-fitted, UNROUNDED page-oriented view extent (init_job_pagination's
|
|
72
72
|
// local `imageSize`/`pageSize`, emit.c:1198-1246): sz = bb extent plus pad,
|
|
@@ -87,8 +87,11 @@ export function emitSvgTag(job: RenderJob): void {
|
|
|
87
87
|
const pageX = rotated ? viewY : viewX;
|
|
88
88
|
const pageY = rotated ? viewX : viewY;
|
|
89
89
|
// job->width/job->height (emit.c:1249-1250, dpi=72 so *dpi/72 is a no-op).
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
// The native build vectorises these ROUNDs (fcvtzs.2d + xtn: low 32 bits of
|
|
91
|
+
// an int64), so an out-of-range page size wraps exactly as native prints it
|
|
92
|
+
// (2784: 2.16e18 -> 1255669760). @see croundNarrow32
|
|
93
|
+
const w = croundNarrow32(pageX + 2 * job.margin.x);
|
|
94
|
+
const h = croundNarrow32(pageY + 2 * job.margin.y);
|
|
92
95
|
job.write('<svg width="' + String(w) + 'pt" height="' + String(h) + 'pt"\n');
|
|
93
96
|
// job->pageBoundingBox (canvasBox.LL = margin+centering, canvasBox.UR = LL +
|
|
94
97
|
// imageSize, then ROUND -- emit.c:1282-1293). C's rotation-branch double
|
|
@@ -96,14 +99,20 @@ export function emitSvgTag(job: RenderJob): void {
|
|
|
96
99
|
// swap-back, emit.c:1275-1298) cancels for LL (always plain margin) but NOT
|
|
97
100
|
// for UR, which ends up paired with the page-oriented (rotation-swapped)
|
|
98
101
|
// view extent -- i.e. the same `pageX`/`pageY` used for width/height above.
|
|
99
|
-
const vbLLx =
|
|
100
|
-
const vbLLy =
|
|
101
|
-
const vbURx =
|
|
102
|
-
const vbURy =
|
|
102
|
+
const vbLLx = croundNarrow32(job.margin.x);
|
|
103
|
+
const vbLLy = croundNarrow32(job.margin.y);
|
|
104
|
+
const vbURx = croundNarrow32(job.margin.x + pageX);
|
|
105
|
+
const vbURy = croundNarrow32(job.margin.y + pageY);
|
|
103
106
|
job.write(' viewBox="' + String(vbLLx) + '.00 ' + String(vbLLy) + '.00 ' +
|
|
104
107
|
String(vbURx) + '.00 ' + String(vbURy) + '.00"\n');
|
|
105
108
|
job.write(' xmlns="http://www.w3.org/2000/svg"');
|
|
106
|
-
job.write(' xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
109
|
+
job.write(' xmlns:xlink="http://www.w3.org/1999/xlink"');
|
|
110
|
+
// graphviz 16.1: `svgid` graph attribute -> id on the root <svg> (gvputs_xml).
|
|
111
|
+
// @see plugin/core/gvrender_core_svg.c:svg_begin_graph
|
|
112
|
+
if (svgid !== undefined && svgid.length > 0) {
|
|
113
|
+
job.write(' id="' + escapeXmlTitle(svgid) + '"');
|
|
114
|
+
}
|
|
115
|
+
job.write('>\n');
|
|
107
116
|
}
|
|
108
117
|
|
|
109
118
|
/**
|
|
@@ -175,22 +184,23 @@ export function resolveGraphBgcolor(bgcolorAttr: string | undefined): string {
|
|
|
175
184
|
}
|
|
176
185
|
|
|
177
186
|
/**
|
|
178
|
-
* Emit background polygon covering the
|
|
187
|
+
* Emit background polygon covering the page clip box (C gvrender_box over
|
|
188
|
+
* job->clip — bb ± pad as setup_page evaluates it, see setupPageClip).
|
|
179
189
|
*
|
|
180
190
|
* Omits the polygon entirely when resolvedColor is the transparent sentinel.
|
|
181
191
|
* Coordinates are in the group's local frame (y-flipped via devscale.y=-1):
|
|
182
|
-
* x: [
|
|
183
|
-
* y (flipped): [-
|
|
192
|
+
* x: [clip.ll.x, clip.ur.x]
|
|
193
|
+
* y (flipped): [-clip.ur.y, -clip.ll.y]
|
|
184
194
|
*
|
|
185
195
|
* @see plugin/core/gvrender_core_svg.c:svg_begin_page
|
|
186
196
|
* @see lib/common/emit.c:emit_background:1476
|
|
187
197
|
*/
|
|
188
|
-
export function emitGraphBackground(
|
|
198
|
+
export function emitGraphBackground(clip: Box, resolvedColor: string, job: RenderJob): void {
|
|
189
199
|
if (resolvedColor === BGCOLOR_TRANSPARENT) return;
|
|
190
|
-
const left =
|
|
191
|
-
const right =
|
|
192
|
-
const top = -
|
|
193
|
-
const bottom = -
|
|
200
|
+
const left = clip.ll.x;
|
|
201
|
+
const right = clip.ur.x;
|
|
202
|
+
const top = -clip.ur.y;
|
|
203
|
+
const bottom = -clip.ll.y;
|
|
194
204
|
const corners: [number, number][] = [
|
|
195
205
|
[left, bottom], [left, top], [right, top], [right, bottom], [left, bottom],
|
|
196
206
|
];
|
|
@@ -209,11 +219,11 @@ export function emitGraphBackground(bb: Box, resolvedColor: string, job: RenderJ
|
|
|
209
219
|
* Passed to getGradientPoints inside emitLinearGradient.
|
|
210
220
|
* @see lib/common/emit.c:emit_background:1517 (gvrender_box with clip)
|
|
211
221
|
*/
|
|
212
|
-
function bgGradientPts(
|
|
213
|
-
const left =
|
|
214
|
-
const right =
|
|
215
|
-
const bottom =
|
|
216
|
-
const top =
|
|
222
|
+
function bgGradientPts(clip: Box): Point[] {
|
|
223
|
+
const left = clip.ll.x;
|
|
224
|
+
const right = clip.ur.x;
|
|
225
|
+
const bottom = clip.ll.y;
|
|
226
|
+
const top = clip.ur.y;
|
|
217
227
|
return [
|
|
218
228
|
{ x: left, y: bottom },
|
|
219
229
|
{ x: left, y: top },
|
|
@@ -227,11 +237,11 @@ function bgGradientPts(bb: Box, pad: Point): Point[] {
|
|
|
227
237
|
* Coords identical to emitGraphBackground; only fill changes.
|
|
228
238
|
* @see plugin/core/gvrender_core_svg.c:svg_polygon:686
|
|
229
239
|
*/
|
|
230
|
-
function writeGradientPolygon(
|
|
231
|
-
const left =
|
|
232
|
-
const right =
|
|
233
|
-
const top = -
|
|
234
|
-
const bottom = -
|
|
240
|
+
function writeGradientPolygon(clip: Box, id: string, job: RenderJob): void {
|
|
241
|
+
const left = clip.ll.x;
|
|
242
|
+
const right = clip.ur.x;
|
|
243
|
+
const top = -clip.ur.y;
|
|
244
|
+
const bottom = -clip.ll.y;
|
|
235
245
|
const corners: [number, number][] = [
|
|
236
246
|
[left, bottom], [left, top], [right, top], [right, bottom], [left, bottom],
|
|
237
247
|
];
|
|
@@ -256,7 +266,7 @@ function writeGradientPolygon(bb: Box, id: string, job: RenderJob): void {
|
|
|
256
266
|
* @see plugin/core/gvrender_core_svg.c:svg_gradstyle:572
|
|
257
267
|
* @see plugin/core/gvrender_core_svg.c:svg_rgradstyle:608
|
|
258
268
|
*/
|
|
259
|
-
function emitGradientBackground(
|
|
269
|
+
function emitGradientBackground(clip: Box, spec: BgGradientSpec, job: RenderJob): void {
|
|
260
270
|
const obj = createObjState(ObjType.RootGraph);
|
|
261
271
|
obj.id = 'graph0';
|
|
262
272
|
obj.fillColor = resolveRenderColor(spec.fillColor);
|
|
@@ -271,10 +281,10 @@ function emitGradientBackground(bb: Box, spec: BgGradientSpec, job: RenderJob):
|
|
|
271
281
|
emitRadialGradient(job, gid);
|
|
272
282
|
} else {
|
|
273
283
|
gid = gradientId('graph0', 'l', job.linearGradId++);
|
|
274
|
-
emitLinearGradient(job, bgGradientPts(
|
|
284
|
+
emitLinearGradient(job, bgGradientPts(clip), gid);
|
|
275
285
|
}
|
|
276
286
|
job.popObj();
|
|
277
|
-
writeGradientPolygon(
|
|
287
|
+
writeGradientPolygon(clip, gid, job);
|
|
278
288
|
}
|
|
279
289
|
|
|
280
290
|
/**
|
|
@@ -296,17 +306,17 @@ function resolveBgGradientSpec(g: Graph, bgcolorAttr: string): BgGradientSpec |
|
|
|
296
306
|
* Separated to keep svgBeginGraph under the 30-line body limit.
|
|
297
307
|
* @see lib/common/emit.c:emit_background:1476
|
|
298
308
|
*/
|
|
299
|
-
function emitBgcolorBackground(g: Graph,
|
|
309
|
+
function emitBgcolorBackground(g: Graph, clip: Box, bgcolorAttr: string | undefined, job: RenderJob): void {
|
|
300
310
|
if (!bgcolorAttr || bgcolorAttr.length === 0 || bgcolorAttr === 'transparent') {
|
|
301
|
-
emitGraphBackground(
|
|
311
|
+
emitGraphBackground(clip, resolveGraphBgcolor(bgcolorAttr), job);
|
|
302
312
|
return;
|
|
303
313
|
}
|
|
304
314
|
const spec = resolveBgGradientSpec(g, bgcolorAttr);
|
|
305
315
|
if (spec !== null) {
|
|
306
|
-
emitGradientBackground(
|
|
316
|
+
emitGradientBackground(clip, spec, job);
|
|
307
317
|
return;
|
|
308
318
|
}
|
|
309
|
-
emitGraphBackground(
|
|
319
|
+
emitGraphBackground(clip, resolveGraphBgcolor(bgcolorAttr), job);
|
|
310
320
|
}
|
|
311
321
|
|
|
312
322
|
/**
|
|
@@ -344,7 +354,25 @@ export function svgBeginGraph(g: Graph, job: RenderJob): void {
|
|
|
344
354
|
}
|
|
345
355
|
job.write(SVG_DOCTYPE);
|
|
346
356
|
job.write(SVG_GENERATOR_COMMENT);
|
|
347
|
-
|
|
357
|
+
// emit_graph: the root `comment` attribute, between svg_begin_job and
|
|
358
|
+
// emit_begin_graph. @see lib/common/emit.c:emit_graph
|
|
359
|
+
const comment = g.attrs.get('comment');
|
|
360
|
+
if (comment !== undefined && comment.length > 0) svgComment(comment, job);
|
|
361
|
+
emitTitlePagesComment(g, job);
|
|
362
|
+
emitSvgTag(job, g.attrs.get('svgid'));
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* svg_begin_graph's header comment: ` Title: <name>` for a user-named graph,
|
|
367
|
+
* then the page count (always 1 — the port emits a single page).
|
|
368
|
+
* @see plugin/core/gvrender_core_svg.c:svg_begin_graph
|
|
369
|
+
*/
|
|
370
|
+
function emitTitlePagesComment(g: Graph, job: RenderJob): void {
|
|
371
|
+
job.write('<!--');
|
|
372
|
+
if (g.name.length > 0 && g.name[0] !== '%') {
|
|
373
|
+
job.write(' Title: ' + escapeXmlTitle(g.name));
|
|
374
|
+
}
|
|
375
|
+
job.write(' Pages: 1 -->\n');
|
|
348
376
|
}
|
|
349
377
|
|
|
350
378
|
/**
|
|
@@ -353,14 +381,16 @@ export function svgBeginGraph(g: Graph, job: RenderJob): void {
|
|
|
353
381
|
* @see plugin/core/gvrender_core_svg.c:svg_begin_page
|
|
354
382
|
*/
|
|
355
383
|
export function svgBeginPage(g: Graph, job: RenderJob): void {
|
|
356
|
-
const
|
|
357
|
-
//
|
|
384
|
+
const clip = job.clip;
|
|
385
|
+
// Portrait: translation.x = -clip.LL.x, with job->clip as setup_page builds
|
|
386
|
+
// it (setupPageClip: focus ∓ pageSize/2, i.e. bb ± pad up to rounding).
|
|
387
|
+
// gvPostprocess normally zeroes bb.ll, but a dot layout failure skips it
|
|
388
|
+
// (graphviz 16.1, corpus 2784: translate(31 ...)).
|
|
358
389
|
// Landscape (ADR-3): setup_page's rotation branch (SVG = GVRENDER_Y_GOES_DOWN)
|
|
359
|
-
// gives translation.x = -
|
|
360
|
-
//
|
|
361
|
-
//
|
|
362
|
-
//
|
|
363
|
-
// translate(-634 208.5). @see emit.c:setup_page; gvrender_core_svg.c:svg_begin_page
|
|
390
|
+
// gives translation.x = -clip.UR.x; translation.y = -clip.UR.y in both
|
|
391
|
+
// branches (printed negated). pageSize = imageSize/zoom is Z-independent
|
|
392
|
+
// (outer scale carries Z). Pinned to b68 native translate(-634 208.5).
|
|
393
|
+
// @see emit.c:setup_page; gvrender_core_svg.c:svg_begin_page
|
|
364
394
|
//
|
|
365
395
|
// margin (F6): setup_page adds `canvasBox.LL / job->zoom` on top of the
|
|
366
396
|
// pad-only formula above -- canvasBox.LL is the FINAL (post swap-cancel)
|
|
@@ -374,9 +404,9 @@ export function svgBeginPage(g: Graph, job: RenderJob): void {
|
|
|
374
404
|
// @see lib/common/emit.c:1566-1582 setup_page (CAUTION comment there)
|
|
375
405
|
const z = job.scale.x;
|
|
376
406
|
const tx = job.rotation !== 0
|
|
377
|
-
? -
|
|
378
|
-
:
|
|
379
|
-
const ty =
|
|
407
|
+
? -clip.ur.x - job.margin.x / z
|
|
408
|
+
: -clip.ll.x + job.margin.x / z;
|
|
409
|
+
const ty = clip.ur.y + job.margin.y / z;
|
|
380
410
|
emitGraphGroupOpen(
|
|
381
411
|
job.idLayerPrefix() + svgGraphId(g, job),
|
|
382
412
|
svgGraphClass(g),
|
|
@@ -393,7 +423,7 @@ export function svgBeginPage(g: Graph, job: RenderJob): void {
|
|
|
393
423
|
* @see lib/common/emit.c:emit_page (emit_background after begin_page)
|
|
394
424
|
*/
|
|
395
425
|
export function svgPageBackground(g: Graph, job: RenderJob): void {
|
|
396
|
-
emitBgcolorBackground(g, job.
|
|
426
|
+
emitBgcolorBackground(g, job.clip, g.attrs.get('bgcolor'), job);
|
|
397
427
|
}
|
|
398
428
|
|
|
399
429
|
/** Close the graph `<g>` group. @see svg_end_page */
|
|
@@ -421,8 +421,8 @@ export function svgPolyline(pts: Point[], job: RenderJob): void {
|
|
|
421
421
|
job.write('"/>\n');
|
|
422
422
|
}
|
|
423
423
|
|
|
424
|
-
export function svgComment(text: string, job: RenderJob): void {
|
|
425
|
-
job.write('<!-- ' +
|
|
424
|
+
export function svgComment(text: string, job: RenderJob): void { // svg_comment: gvputs_xml
|
|
425
|
+
job.write('<!-- ' + escapeXmlTitle(text) + ' -->\n');
|
|
426
426
|
}
|
|
427
427
|
|
|
428
428
|
// ---------------------------------------------------------------------------
|
package/src/render/svg.ts
CHANGED
|
@@ -20,13 +20,12 @@ import type { Graph } from '../model/graph.js';
|
|
|
20
20
|
import type { Node } from '../model/node.js';
|
|
21
21
|
import type { Edge } from '../model/edge.js';
|
|
22
22
|
import type { Point, Box } from '../model/geom.js';
|
|
23
|
-
import { boxOverlap } from '../model/geom.js';
|
|
24
|
-
import type { TextlabelT } from '../common/types.js';
|
|
25
23
|
import type { TextSpan } from '../common/emit-types.js';
|
|
26
24
|
import type { RendererPlugin, LabelType } from '../gvc/context.js';
|
|
27
25
|
import type { RenderJob } from '../gvc/job.js';
|
|
28
26
|
import { renderEdgeLabels } from '../gvc/edge-labels.js';
|
|
29
27
|
import { beginAnchorIf } from '../gvc/anchor.js';
|
|
28
|
+
import { edgeHasDrawableContent } from '../common/edge-in-box.js';
|
|
30
29
|
import {
|
|
31
30
|
svgBeginGraph,
|
|
32
31
|
svgEndGraph,
|
|
@@ -86,45 +85,9 @@ function emitMulticolorArrows(e: Edge, job: RenderJob, headColor: string, tailCo
|
|
|
86
85
|
// @see plugin/core/gvrender_core_svg.c:svg_engine
|
|
87
86
|
// ---------------------------------------------------------------------------
|
|
88
87
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const sx = lp.dimen.x / 2;
|
|
93
|
-
const sy = lp.dimen.y / 2;
|
|
94
|
-
const bb: Box = {
|
|
95
|
-
ll: { x: lp.pos.x - sx, y: lp.pos.y - sy },
|
|
96
|
-
ur: { x: lp.pos.x + sx, y: lp.pos.y + sy },
|
|
97
|
-
};
|
|
98
|
-
return boxOverlap(b, bb);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Faithful-to-outcome `edge_in_box`: true when the edge has something to draw.
|
|
103
|
-
* C's gvrender defers the `<g>` until a draw op, so an edge whose content falls
|
|
104
|
-
* entirely outside the clip emits no group (emit.c:edge_in_box tests each piece
|
|
105
|
-
* vs job->clip via boxf_overlap/overlap_label).
|
|
106
|
-
*
|
|
107
|
-
* The only piece that can legitimately fall OUTSIDE the clip on this corpus is a
|
|
108
|
-
* degenerate labeled-flat label: it is spline-less, so map_edge leaves it at its
|
|
109
|
-
* internal (un-normalized) x-frame position while the clip is the final frame.
|
|
110
|
-
* The `overlapLabel` test on the main label reproduces C's draw-iff-overlap for
|
|
111
|
-
* exactly that case — on-canvas (2368) → drawn; off-canvas (2368_1, negative
|
|
112
|
-
* un-normalized x) → suppressed. A present spline always lies within the clip
|
|
113
|
-
* (GD_bb contains it), and head/tail/xlabels are positioned at on-canvas
|
|
114
|
-
* endpoints, so those keep their existing unconditional triggers — adding an
|
|
115
|
-
* overlap test there only risks suppressing legitimately-drawn content (it
|
|
116
|
-
* regressed neato/circo edges whose spl.bb is computed differently). The `set`
|
|
117
|
-
* guard on the main label is kept: the port retains an unplaced (`pos=0,0`,
|
|
118
|
-
* `set=false`) label on merged back-edges where C has none.
|
|
119
|
-
* @see lib/common/emit.c:edge_in_box, lib/common/utils.c:overlap_label
|
|
120
|
-
*/
|
|
121
|
-
export function edgeHasDrawableContent(e: Edge, clip: Box): boolean {
|
|
122
|
-
const i = e.info;
|
|
123
|
-
return i.spl !== undefined
|
|
124
|
-
|| (i.label !== undefined && i.label.set && overlapLabel(i.label, clip))
|
|
125
|
-
|| i.xlabel !== undefined
|
|
126
|
-
|| i.head_label !== undefined || i.tail_label !== undefined;
|
|
127
|
-
}
|
|
88
|
+
// edge_in_box gate lives in common/edge-in-box.ts (shared with the device walk);
|
|
89
|
+
// re-exported for existing import sites.
|
|
90
|
+
export { edgeHasDrawableContent };
|
|
128
91
|
|
|
129
92
|
export class SvgRenderer implements RendererPlugin {
|
|
130
93
|
readonly type = 'svg';
|
package/src/xdot/parse.ts
CHANGED
|
@@ -13,6 +13,9 @@ import type {
|
|
|
13
13
|
} from "./types.js";
|
|
14
14
|
import { XDOT_PARSE_ERROR } from "./types.js";
|
|
15
15
|
|
|
16
|
+
/** ULONG_MAX on an LP64 target: strtoul saturation value. */
|
|
17
|
+
const ULONG_MAX = 0xffff_ffff_ffff_ffffn;
|
|
18
|
+
|
|
16
19
|
type PR<T> = { val: T; pos: number } | null;
|
|
17
20
|
type OpResult = { op: XdotOp; pos: number } | "error";
|
|
18
21
|
type OpMaybeResult = OpResult | null;
|
|
@@ -63,12 +66,23 @@ class XdotParser {
|
|
|
63
66
|
return { val: parseInt(s.slice(start, i), 10), pos: i };
|
|
64
67
|
}
|
|
65
68
|
|
|
69
|
+
/**
|
|
70
|
+
* strtoul(s, &endp, 10) cast to unsigned int: optional sign, a leading "-"
|
|
71
|
+
* negates modulo 2^64, magnitude overflow saturates to ULONG_MAX, and the
|
|
72
|
+
* result keeps the low 32 bits.
|
|
73
|
+
* @see lib/xdot/xdot.c:parseUInt
|
|
74
|
+
*/
|
|
66
75
|
static parseUInt_(s: string, pos: number): PR<number> {
|
|
67
76
|
const i = XdotParser.skipWs(s, pos);
|
|
68
|
-
|
|
69
|
-
const
|
|
77
|
+
let start = i;
|
|
78
|
+
const negative = i < s.length && s[i] === "-";
|
|
79
|
+
if (i < s.length && (s[i] === "-" || s[i] === "+")) start++;
|
|
80
|
+
const end = XdotParser.consumeDigits(s, start);
|
|
70
81
|
if (end === start) return null;
|
|
71
|
-
|
|
82
|
+
const mag = BigInt(s.slice(start, end));
|
|
83
|
+
const clamped = mag > ULONG_MAX ? ULONG_MAX : mag;
|
|
84
|
+
const wrapped = negative && mag <= ULONG_MAX ? BigInt.asUintN(64, -clamped) : clamped;
|
|
85
|
+
return { val: Number(BigInt.asUintN(32, wrapped)), pos: end };
|
|
72
86
|
}
|
|
73
87
|
|
|
74
88
|
static parseRect(s: string, pos: number): PR<XdotRect> {
|
|
@@ -163,7 +177,7 @@ class XdotParser {
|
|
|
163
177
|
const ry0 = XdotParser.parseReal(s, rx0.pos); if (!ry0) return null;
|
|
164
178
|
const rx1 = XdotParser.parseReal(s, ry0.pos); if (!rx1) return null;
|
|
165
179
|
const ry1 = XdotParser.parseReal(s, rx1.pos); if (!ry1) return null;
|
|
166
|
-
const rn = XdotParser.
|
|
180
|
+
const rn = XdotParser.parseUInt_(s, ry1.pos); if (!rn) return null;
|
|
167
181
|
const rs = XdotParser.parseStops(s, rn.pos, rn.val); if (!rs) return null;
|
|
168
182
|
return { x0: rx0.val, y0: ry0.val, x1: rx1.val, y1: ry1.val, stops: rs.stops };
|
|
169
183
|
}
|
|
@@ -175,7 +189,7 @@ class XdotParser {
|
|
|
175
189
|
const rx1 = XdotParser.parseReal(s, rr0.pos); if (!rx1) return null;
|
|
176
190
|
const ry1 = XdotParser.parseReal(s, rx1.pos); if (!ry1) return null;
|
|
177
191
|
const rr1 = XdotParser.parseReal(s, ry1.pos); if (!rr1) return null;
|
|
178
|
-
const rn = XdotParser.
|
|
192
|
+
const rn = XdotParser.parseUInt_(s, rr1.pos); if (!rn) return null;
|
|
179
193
|
const rs = XdotParser.parseStops(s, rn.pos, rn.val); if (!rs) return null;
|
|
180
194
|
return { x0: rx0.val, y0: ry0.val, r0: rr0.val, x1: rx1.val, y1: ry1.val, r1: rr1.val, stops: rs.stops };
|
|
181
195
|
}
|