@knowvah/dot-engine 2.6.10 → 2.6.12
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/async/render-async.d.ts.map +1 -1
- package/dist/common/agerr.d.ts +37 -0
- package/dist/common/agerr.d.ts.map +1 -0
- package/dist/common/arrows-shapes-poly.d.ts.map +1 -1
- package/dist/common/color.d.ts.map +1 -1
- package/dist/common/crand.d.ts +0 -20
- package/dist/common/crand.d.ts.map +1 -1
- package/dist/common/emit-once.d.ts +6 -0
- package/dist/common/emit-once.d.ts.map +1 -0
- package/dist/common/graph-init.d.ts.map +1 -1
- package/dist/common/html-entities.d.ts +13 -5
- package/dist/common/html-entities.d.ts.map +1 -1
- package/dist/common/htmltable-attr-warn.d.ts.map +1 -1
- package/dist/common/htmltable-emit-fill.d.ts +9 -0
- package/dist/common/htmltable-emit-fill.d.ts.map +1 -1
- package/dist/common/htmltable-emit-rules.d.ts.map +1 -1
- package/dist/common/htmltable-lex-chardata.d.ts +9 -0
- package/dist/common/htmltable-lex-chardata.d.ts.map +1 -1
- package/dist/common/htmltable-lex-expat.d.ts +1 -6
- package/dist/common/htmltable-lex-expat.d.ts.map +1 -1
- package/dist/common/htmltable.d.ts +1 -7
- package/dist/common/htmltable.d.ts.map +1 -1
- package/dist/common/poly-gencode.d.ts.map +1 -1
- package/dist/common/poly-init.d.ts.map +1 -1
- package/dist/common/postproc.d.ts.map +1 -1
- package/dist/common/random.d.ts.map +1 -1
- package/dist/common/render-state.d.ts +8 -0
- package/dist/common/render-state.d.ts.map +1 -0
- package/dist/common/splines-geom.d.ts +8 -0
- package/dist/common/splines-geom.d.ts.map +1 -1
- package/dist/common/splines-routespl.d.ts.map +1 -1
- package/dist/common/textmeasure-lookup.d.ts.map +1 -1
- package/dist/common/textmeasure.d.ts.map +1 -1
- package/dist/gvc/anchor.d.ts.map +1 -1
- package/dist/gvc/device-cluster.d.ts.map +1 -1
- package/dist/gvc/device-labels.d.ts +14 -0
- package/dist/gvc/device-labels.d.ts.map +1 -1
- package/dist/gvc/device.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +821 -471
- package/dist/index.js.map +4 -4
- package/dist/layout/dot/cluster.d.ts.map +1 -1
- package/dist/layout/dot/conc.d.ts.map +1 -1
- package/dist/layout/dot/decomp.d.ts.map +1 -1
- package/dist/layout/dot/edge-route-clip.d.ts +18 -10
- package/dist/layout/dot/edge-route-clip.d.ts.map +1 -1
- package/dist/layout/dot/mincross-build.d.ts.map +1 -1
- package/dist/layout/dot/mincross-cross.d.ts.map +1 -1
- package/dist/layout/dot/rank.d.ts.map +1 -1
- package/dist/layout/dot/splines.d.ts.map +1 -1
- package/dist/layout/fdp/index.d.ts.map +1 -1
- package/dist/layout/fdp/tlayout-parms.d.ts.map +1 -1
- package/dist/layout/fdp/xlayout.d.ts.map +1 -1
- package/dist/layout/neato/edge-len.d.ts.map +1 -1
- package/dist/layout/neato/fdp-adjust.d.ts.map +1 -1
- package/dist/layout/neato/init.d.ts +4 -10
- package/dist/layout/neato/init.d.ts.map +1 -1
- package/dist/layout/neato/kk.d.ts.map +1 -1
- package/dist/layout/neato/multispline-router.d.ts.map +1 -1
- package/dist/layout/neato/multispline.d.ts.map +1 -1
- package/dist/layout/neato/poly.d.ts.map +1 -1
- package/dist/layout/neato/sgd.d.ts.map +1 -1
- package/dist/layout/neato/start.d.ts.map +1 -1
- package/dist/ortho/index.d.ts.map +1 -1
- package/dist/parser/index.d.ts.map +1 -1
- package/dist/render/color-resolve.d.ts +49 -1
- package/dist/render/color-resolve.d.ts.map +1 -1
- package/dist/render/public.d.ts.map +1 -1
- package/dist/render/svg-edge-split.d.ts +2 -0
- package/dist/render/svg-edge-split.d.ts.map +1 -1
- package/dist/render/xdot-public.d.ts.map +1 -1
- package/dist/render.js +569 -220
- package/dist/render.js.map +4 -4
- package/package.json +1 -1
- package/src/async/render-async.ts +6 -0
- package/src/common/agerr.ts +96 -0
- package/src/common/arrows-shapes-poly.ts +19 -5
- package/src/common/color.ts +38 -10
- package/src/common/crand.ts +8 -2
- package/src/common/emit-once.ts +27 -0
- package/src/common/graph-init.ts +2 -1
- package/src/common/html-entities.ts +77 -25
- package/src/common/htmltable-attr-warn.ts +3 -4
- package/src/common/htmltable-emit-fill.ts +48 -0
- package/src/common/htmltable-emit-rules.ts +2 -0
- package/src/common/htmltable-lex-chardata.ts +42 -8
- package/src/common/htmltable-lex-expat.ts +78 -23
- package/src/common/htmltable.ts +15 -16
- package/src/common/poly-gencode.ts +14 -2
- package/src/common/poly-init.ts +2 -1
- package/src/common/postproc.ts +4 -1
- package/src/common/random.ts +11 -1
- package/src/common/render-state.ts +36 -0
- package/src/common/splines-geom.ts +8 -4
- package/src/common/splines-routespl.ts +2 -1
- package/src/common/textmeasure-lookup.ts +10 -3
- package/src/common/textmeasure.ts +45 -2
- package/src/gvc/anchor.ts +1 -12
- package/src/gvc/device-cluster.ts +37 -9
- package/src/gvc/device-labels.ts +27 -0
- package/src/gvc/device.ts +18 -6
- package/src/index.ts +3 -0
- package/src/layout/dot/cluster.ts +2 -1
- package/src/layout/dot/conc.ts +6 -5
- package/src/layout/dot/decomp.ts +3 -0
- package/src/layout/dot/edge-route-clip.ts +27 -21
- package/src/layout/dot/mincross-build.ts +2 -0
- package/src/layout/dot/mincross-cross.ts +2 -0
- package/src/layout/dot/rank.ts +2 -0
- package/src/layout/dot/splines.ts +2 -1
- package/src/layout/fdp/index.ts +2 -1
- package/src/layout/fdp/tlayout-parms.ts +9 -2
- package/src/layout/fdp/xlayout.ts +10 -2
- package/src/layout/neato/cdt-surface.ts +6 -6
- package/src/layout/neato/edge-len.ts +2 -1
- package/src/layout/neato/fdp-adjust.ts +3 -2
- package/src/layout/neato/init.ts +11 -11
- package/src/layout/neato/kk.ts +2 -1
- package/src/layout/neato/multispline-router.ts +3 -3
- package/src/layout/neato/multispline.ts +3 -2
- package/src/layout/neato/poly.ts +3 -2
- package/src/layout/neato/sgd.ts +5 -2
- package/src/layout/neato/start.ts +14 -10
- package/src/ortho/index.ts +3 -1
- package/src/parser/dot.js +4 -1
- package/src/parser/dot.pegjs +5 -1
- package/src/parser/index.ts +4 -3
- package/src/render/color-resolve.ts +193 -4
- package/src/render/public.ts +2 -0
- package/src/render/svg-edge-split.ts +4 -2
- package/src/render/xdot-public.ts +2 -0
|
@@ -270,12 +270,19 @@ function scanCdata(text: string, p: number, base: number, dataOf: CdataText, tok
|
|
|
270
270
|
return close < 0 ? -1 : close + CDATA_CLOSE.length;
|
|
271
271
|
}
|
|
272
272
|
|
|
273
|
-
/**
|
|
274
|
-
|
|
273
|
+
/**
|
|
274
|
+
* Character data at `p` up to the next `<`: one `text` token, then the index it
|
|
275
|
+
* ends at. A reference cut off by the end of the data is not part of the token
|
|
276
|
+
* (`held`): expat waits for the characters that settle it.
|
|
277
|
+
* @see expat lib/xmltok_impl.c:PREFIX(contentTok) (XML_TOK_PARTIAL)
|
|
278
|
+
*/
|
|
279
|
+
function scanText(text: string, p: number, base: number, tokens: ExpatToken[]): { at: number; held: boolean } {
|
|
275
280
|
const e = text.indexOf('<', p);
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
281
|
+
const slice = text.slice(p, e < 0 ? text.length : e);
|
|
282
|
+
const from = e < 0 ? heldRefStart(slice) : undefined;
|
|
283
|
+
const end = from === undefined ? p + slice.length : p + from;
|
|
284
|
+
if (end > p) tokens.push({ kind: 'text', start: base + p, end: base + end });
|
|
285
|
+
return { at: end, held: from !== undefined };
|
|
279
286
|
}
|
|
280
287
|
|
|
281
288
|
/** The fed text of the label source between two offsets. */
|
|
@@ -295,7 +302,9 @@ function scanContent(text: string, base: number, inCdata: boolean, dataOf: Cdata
|
|
|
295
302
|
continue;
|
|
296
303
|
}
|
|
297
304
|
if (text[p] !== '<') {
|
|
298
|
-
|
|
305
|
+
const scanned = scanText(text, p, base, tokens);
|
|
306
|
+
if (scanned.held) return { tokens, consumed: scanned.at, inCdata: false };
|
|
307
|
+
p = scanned.at;
|
|
299
308
|
continue;
|
|
300
309
|
}
|
|
301
310
|
const { scan, kind } = scanMarkup(text, p);
|
|
@@ -408,33 +417,79 @@ export class ExpatFeed {
|
|
|
408
417
|
/** Entities expat knows without a DTD; `scanEntity` leaves them as written. */
|
|
409
418
|
const XML_PREDEFINED_ENTITIES = new Set(['lt', 'gt', 'amp', 'quot', 'apos']);
|
|
410
419
|
|
|
411
|
-
|
|
420
|
+
/** An `&name;` that `scanEntity` rewrites: 2 to 8 characters (alphanumeric) that name an HTML entity. */
|
|
421
|
+
const SCANNED_ENTITY = /^&([^;&<]*);/;
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* The reference the content tokenizer reads at the `&` at `t`: the index after
|
|
425
|
+
* its `;`, or the index of the character that makes it an invalid token (the
|
|
426
|
+
* end of `text` when the reference runs out: `</HTML>` or another tag follows).
|
|
427
|
+
* Runs the attribute value scanner over the reference alone, closed by a quote,
|
|
428
|
+
* which is scanRef on the same bytes.
|
|
429
|
+
* @see expat lib/xmltok_impl.c:PREFIX(contentTok) (BT_AMP)
|
|
430
|
+
* @see expat lib/xmltok_impl.c:PREFIX(scanRef)
|
|
431
|
+
* @see expat lib/xmltok_impl.c:PREFIX(scanCharRef)
|
|
432
|
+
* @see expat lib/xmltok_impl.c:PREFIX(scanHexCharRef)
|
|
433
|
+
*/
|
|
434
|
+
function contentRef(text: string, t: number): { end: number } | { invalid: number } {
|
|
435
|
+
const semi = text.indexOf(';', t);
|
|
436
|
+
const ref = text.slice(t, semi < 0 ? text.length : semi + 1);
|
|
437
|
+
const scan = scanAttValue(`"${ref}"`, 0);
|
|
438
|
+
if ('close' in scan) return { end: t + scan.close - 1 };
|
|
439
|
+
return { invalid: 'invalid' in scan ? t + scan.invalid - 1 : text.length };
|
|
440
|
+
}
|
|
412
441
|
|
|
413
|
-
|
|
442
|
+
/** A reference problem of character data: expat's error, where its event pointer is, and the `&` it began at. */
|
|
443
|
+
interface RefProblem {
|
|
444
|
+
message: string;
|
|
445
|
+
offset: number;
|
|
446
|
+
from: number;
|
|
447
|
+
}
|
|
414
448
|
|
|
415
449
|
/**
|
|
416
|
-
* The first reference of character data that expat rejects
|
|
417
|
-
*
|
|
418
|
-
* a character reference XML
|
|
419
|
-
*
|
|
450
|
+
* The first reference of character data that expat rejects: an invalid token
|
|
451
|
+
* (a bare `&`, `&#;`, `A`, `Aq`, ..), a `&name;` scanEntity leaves
|
|
452
|
+
* alone (neither an HTML entity nor predefined), or a character reference XML
|
|
453
|
+
* does not allow. An invalid token at the very end of `text` is a reference
|
|
454
|
+
* expat is still waiting on.
|
|
455
|
+
* @see lib/common/utils.c:scanEntity
|
|
456
|
+
* @see expat lib/xmlparse.c:doContent (XML_TOK_INVALID, XML_TOK_ENTITY_REF, XML_TOK_CHAR_REF)
|
|
420
457
|
*/
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
458
|
+
function firstRefProblem(text: string): RefProblem | undefined {
|
|
459
|
+
let t = text.indexOf('&');
|
|
460
|
+
while (t >= 0) {
|
|
461
|
+
if (text[t + 1] !== '#') {
|
|
462
|
+
const name = SCANNED_ENTITY.exec(text.slice(t))?.[1];
|
|
463
|
+
if (name !== undefined && name.length >= 2 && Object.hasOwn(HTML_ENTITIES, name)) {
|
|
464
|
+
t = text.indexOf('&', t + name.length + 2);
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
427
467
|
}
|
|
428
|
-
const
|
|
429
|
-
if (
|
|
430
|
-
const key = text.slice(t + 1,
|
|
431
|
-
if (
|
|
432
|
-
return { message:
|
|
468
|
+
const ref = contentRef(text, t);
|
|
469
|
+
if ('invalid' in ref) return { message: XML_ERROR_INVALID_TOKEN, offset: ref.invalid, from: t };
|
|
470
|
+
const key = text.slice(t + 1, ref.end - 1);
|
|
471
|
+
if (text[t + 1] === '#') {
|
|
472
|
+
if (charRefNumber(`&${key};`) < 0) return { message: XML_ERROR_BAD_CHAR_REF, offset: t, from: t };
|
|
473
|
+
} else if (!XML_PREDEFINED_ENTITIES.has(key)) {
|
|
474
|
+
return { message: XML_ERROR_UNDEFINED_ENTITY, offset: t, from: t };
|
|
433
475
|
}
|
|
476
|
+
t = text.indexOf('&', ref.end);
|
|
434
477
|
}
|
|
435
478
|
return undefined;
|
|
436
479
|
}
|
|
437
480
|
|
|
481
|
+
/** The first reference of character data that expat rejects, and the offset of its event pointer. */
|
|
482
|
+
export function textRefError(text: string): { message: string; offset: number } | undefined {
|
|
483
|
+
const problem = firstRefProblem(text);
|
|
484
|
+
return problem === undefined ? undefined : { message: problem.message, offset: problem.offset };
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/** Where a reference that runs to the end of `text` begins: expat holds it until more data arrives. */
|
|
488
|
+
function heldRefStart(text: string): number | undefined {
|
|
489
|
+
const problem = firstRefProblem(text);
|
|
490
|
+
return problem?.message === XML_ERROR_INVALID_TOKEN && problem.offset === text.length ? problem.from : undefined;
|
|
491
|
+
}
|
|
492
|
+
|
|
438
493
|
export interface TagChunk {
|
|
439
494
|
close: boolean;
|
|
440
495
|
selfClose: boolean;
|
package/src/common/htmltable.ts
CHANGED
|
@@ -169,25 +169,24 @@ const itemHasFlags = (i: HtmlTextItem): boolean =>
|
|
|
169
169
|
i.subscript === true || i.superscript === true;
|
|
170
170
|
|
|
171
171
|
/**
|
|
172
|
-
* C's "simple" test: one item per line, no font
|
|
173
|
-
* resolved face and size across all lines.
|
|
172
|
+
* C's "simple" test over the line list (C spans): one item per line, no font
|
|
173
|
+
* flags, uniform resolved face and size across all lines. It reads each
|
|
174
|
+
* line's first item, so the "" item appendFLineList gives an empty <BR/> line
|
|
175
|
+
* counts with its font: `k<U><BR/><BR/></U>` is not simple.
|
|
174
176
|
* @see lib/common/htmltable.c:size_html_txt (lines 946-986)
|
|
175
177
|
*/
|
|
176
|
-
|
|
178
|
+
const htmlTextIsSimple = (lines: SizedLine[], env: HtmlSizeEnv): boolean => {
|
|
177
179
|
let prevFs = -1;
|
|
178
180
|
let prevFace: string | null = null;
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if (prevFace === null) prevFace = face;
|
|
189
|
-
else if (face !== prevFace) return false;
|
|
190
|
-
}
|
|
181
|
+
for (const ln of lines) {
|
|
182
|
+
// splitSizedLines never yields an itemless line (C: nitems >= 1).
|
|
183
|
+
const item = ln.items[0]!;
|
|
184
|
+
if (ln.items.length > 1 || itemHasFlags(item)) return false;
|
|
185
|
+
const { fs, face } = itemFont(item, env);
|
|
186
|
+
if (prevFs < 0) prevFs = fs;
|
|
187
|
+
else if (fs !== prevFs) return false;
|
|
188
|
+
if (prevFace === null) prevFace = face;
|
|
189
|
+
else if (face !== prevFace) return false;
|
|
191
190
|
}
|
|
192
191
|
return true;
|
|
193
192
|
};
|
|
@@ -203,7 +202,7 @@ const sizeTextContent = (
|
|
|
203
202
|
env: HtmlSizeEnv = defaultSizeEnv(),
|
|
204
203
|
): { w: number; h: number } => {
|
|
205
204
|
const lines = splitSizedLines(texts, measurer, env);
|
|
206
|
-
const simple = htmlTextIsSimple(
|
|
205
|
+
const simple = htmlTextIsSimple(lines, env);
|
|
207
206
|
let w = 0;
|
|
208
207
|
let h = 0;
|
|
209
208
|
for (const ln of lines) {
|
|
@@ -33,7 +33,8 @@ import {
|
|
|
33
33
|
styleTokens,
|
|
34
34
|
} from './style-resolve.js';
|
|
35
35
|
import { stripedBox, wedgedEllipse } from '../render/svg-multicolor.js';
|
|
36
|
-
import { resolveRenderColor } from '../render/color-resolve.js';
|
|
36
|
+
import { resolveRenderColor, reportColorNames } from '../render/color-resolve.js';
|
|
37
|
+
import { htmlLabelColors } from './htmltable-emit-fill.js';
|
|
37
38
|
import { svgNodeId } from '../render/svg-id.js';
|
|
38
39
|
import { drawRoundCorners, mcircleHack, underlineDraw } from './poly-shapes.js';
|
|
39
40
|
|
|
@@ -191,6 +192,8 @@ export function renderLabel(
|
|
|
191
192
|
job: RenderJob,
|
|
192
193
|
): void {
|
|
193
194
|
if (label.u.kind !== 'txt' || label.u.nspans < 1) return;
|
|
195
|
+
// gvrender_set_pencolor(job, lp->fontcolor), once, before the spans.
|
|
196
|
+
reportColorNames([label.fontcolor]);
|
|
194
197
|
label.pos = coord;
|
|
195
198
|
label.set = true;
|
|
196
199
|
// First-span baseline per valign (labelloc). @see lib/common/labels.c:240-251
|
|
@@ -227,6 +230,7 @@ function renderNodeLabel(
|
|
|
227
230
|
// Route the HTML label's box/fill/text ops into the node LABEL emit-state
|
|
228
231
|
// (→ _ldraw_ for xdot), not the node DRAW state. SVG ignores emit_state.
|
|
229
232
|
const html = label.u.html as PlacedHtml;
|
|
233
|
+
reportColorNames(htmlLabelColors(html));
|
|
230
234
|
withLabelEmitState(job, () => emitHtmlLabel(html, coord, renderer, job));
|
|
231
235
|
} else {
|
|
232
236
|
renderLabel(label, coord, renderer, job);
|
|
@@ -367,8 +371,16 @@ export function applyNodeStyle(obj: ObjState, n: Node, job: RenderJob): boolean
|
|
|
367
371
|
// the whole node emission (emit_begin_node..emit_end_node), which covers both
|
|
368
372
|
// this color block and the label textspans. Both callers of this function are
|
|
369
373
|
// codefns, so they already run inside that window. @see gvc/device.ts
|
|
374
|
+
const penwidth = nodeAttr(n, n.root, 'penwidth');
|
|
375
|
+
// record_gencode resolves the pen (penColor) before the fill; poly_gencode
|
|
376
|
+
// and point_gencode resolve the fill first. Only the order in which an
|
|
377
|
+
// unknown colour is reported differs. @see shapes.c:3890-3892 vs 2985-3004
|
|
378
|
+
if ((n.info.shape as ShapeDesc | undefined)?.kind === ShapeKind.SH_RECORD) {
|
|
379
|
+
applyPenState(obj, styleAttr, colorAttr, penwidth);
|
|
380
|
+
return applyFillState(obj, n, job);
|
|
381
|
+
}
|
|
370
382
|
const filled = applyFillState(obj, n, job);
|
|
371
|
-
applyPenState(obj, styleAttr, colorAttr,
|
|
383
|
+
applyPenState(obj, styleAttr, colorAttr, penwidth);
|
|
372
384
|
return filled;
|
|
373
385
|
}
|
|
374
386
|
|
package/src/common/poly-init.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
DEFAULT_COLOR,
|
|
25
25
|
} from './make-label.js';
|
|
26
26
|
import { computeVertices } from './poly-vertices.js';
|
|
27
|
+
import { agwarningf } from './agerr.js';
|
|
27
28
|
|
|
28
29
|
// Default half-dimensions in points (72 dpi, 0.75 in wide, 0.5 in tall)
|
|
29
30
|
const DEFAULT_LW = 27;
|
|
@@ -117,7 +118,7 @@ export function polyInit(n: Node, g: Graph, measurer: TextMeasurer): void {
|
|
|
117
118
|
// C warns when the custom shape's image cannot be sized (headless: always).
|
|
118
119
|
// @see lib/common/shapes.c:2027-2033
|
|
119
120
|
if (shapefile !== undefined && shapefile !== '' && shapeName !== 'epsf') {
|
|
120
|
-
|
|
121
|
+
agwarningf(`No or improper shapefile="${shapefile}" for node "${n.name}"`, 'error');
|
|
121
122
|
}
|
|
122
123
|
if ((n.info.shape as ShapeDesc).kind === ShapeKind.SH_RECORD) {
|
|
123
124
|
if (n.info.shape_info === undefined) recordNodeInit(n, g, measurer);
|
package/src/common/postproc.ts
CHANGED
|
@@ -26,6 +26,8 @@ import {
|
|
|
26
26
|
RANKDIR_TB, RANKDIR_LR, RANKDIR_BT, RANKDIR_RL,
|
|
27
27
|
} from '../layout/dot/init.js';
|
|
28
28
|
import { addXLabels } from './xlabels-place.js';
|
|
29
|
+
import { registerRenderState } from './render-state.js';
|
|
30
|
+
import { agerrLineOn } from './agerr.js';
|
|
29
31
|
|
|
30
32
|
/** Concentrated multi-edge duplicate — never drawn. @see lib/common/const.h:IGNORED */
|
|
31
33
|
const IGNORED_EDGE = 6;
|
|
@@ -38,6 +40,7 @@ const IGNORED_EDGE = 6;
|
|
|
38
40
|
|
|
39
41
|
let Rankdir = 0;
|
|
40
42
|
let Offset: Point = { x: 0, y: 0 };
|
|
43
|
+
registerRenderState(() => { Rankdir = 0; Offset = { x: 0, y: 0 }; });
|
|
41
44
|
|
|
42
45
|
/** The bezier fields map_edge reads/writes. */
|
|
43
46
|
type MappedBezier = Pick<Bezier, 'list' | 'sflag' | 'eflag' | 'sp' | 'ep' | 'sArrowOps' | 'eArrowOps'>;
|
|
@@ -226,7 +229,7 @@ function translateDrawing(g: Graph): void {
|
|
|
226
229
|
// @see lib/common/postproc.c:map_edge ("lost %s %s edge")
|
|
227
230
|
if (e.info.spl === undefined) {
|
|
228
231
|
if (!(g.info.concentrate ?? false) && (e.info.edge_type ?? 0) !== IGNORED_EDGE) {
|
|
229
|
-
|
|
232
|
+
agerrLineOn('warn', `lost ${e.tail.name} ${e.head.name} edge`);
|
|
230
233
|
}
|
|
231
234
|
continue;
|
|
232
235
|
}
|
package/src/common/random.ts
CHANGED
|
@@ -14,12 +14,22 @@
|
|
|
14
14
|
* @see lib/neatogen/neatoinit.c:checkStart (srand48 call)
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
import { registerRenderState } from './render-state.js';
|
|
18
|
+
|
|
17
19
|
const A = BigInt('0x5deece66d');
|
|
18
20
|
const C = BigInt('0xb');
|
|
19
21
|
const MASK48 = (1n << 48n) - 1n;
|
|
20
22
|
const TWO48 = 2 ** 48;
|
|
23
|
+
/**
|
|
24
|
+
* Unseeded generator state: libc's X0 = 0x1234ABCD330E (rand48 seed words
|
|
25
|
+
* 0x1234, 0xABCD, 0x330E); verified on the reference machine — an unseeded
|
|
26
|
+
* drand48() returns 0.39646477376027534.
|
|
27
|
+
* @see POSIX drand48(3)
|
|
28
|
+
*/
|
|
29
|
+
const UNSEEDED_STATE = 0x1234abcd330en;
|
|
21
30
|
|
|
22
|
-
let state =
|
|
31
|
+
let state = UNSEEDED_STATE;
|
|
32
|
+
registerRenderState(() => { state = UNSEEDED_STATE; });
|
|
23
33
|
|
|
24
34
|
/**
|
|
25
35
|
* Seed the generator: X = (seed << 16) | 0x330E (low 32 bits of seed).
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Per-render scope for C process-level state (AD-4).
|
|
5
|
+
*
|
|
6
|
+
* Native Graphviz renders one graph per `dot` process, so every file static —
|
|
7
|
+
* RNG streams, sequence counters, the emit_once warning set — starts each
|
|
8
|
+
* graph at its C initial value (and gvFreeContext's emit_once_reset clears the
|
|
9
|
+
* warning set). The port's module statics outlive a render, so each module
|
|
10
|
+
* owning such state registers a reset that restores its C initial value, and
|
|
11
|
+
* every public render entry calls {@link resetRenderState} before any other
|
|
12
|
+
* work. Render output is then a function of its input alone.
|
|
13
|
+
*
|
|
14
|
+
* Not registered here: caller-installed hooks (setTextMeasurer,
|
|
15
|
+
* setImageSizer, setImageResolver) and debug hooks — they are configuration,
|
|
16
|
+
* not C process state.
|
|
17
|
+
*
|
|
18
|
+
* @see lib/gvc/gvcontext.c:gvFreeContext (emit_once_reset)
|
|
19
|
+
* @see lib/common/emit.c:emit_once_reset
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** Registered resets, run in registration (module evaluation) order. */
|
|
23
|
+
const resets: Array<() => void> = [];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Register a reset restoring one module's C process state to its initial
|
|
27
|
+
* value. Called once, at module evaluation.
|
|
28
|
+
*/
|
|
29
|
+
export function registerRenderState(reset: () => void): void {
|
|
30
|
+
resets.push(reset);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Restore every registered module's C process state; call first per render. */
|
|
34
|
+
export function resetRenderState(): void {
|
|
35
|
+
for (const reset of resets) reset();
|
|
36
|
+
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import type { Point, Box } from '../model/geom.js';
|
|
12
12
|
import { bezier as evalBezier } from './bezier.js';
|
|
13
|
+
import { fma } from './fma.js';
|
|
13
14
|
|
|
14
15
|
// ---------------------------------------------------------------------------
|
|
15
16
|
// Approximate equality
|
|
@@ -47,16 +48,19 @@ const HW = 2.0;
|
|
|
47
48
|
const SMALL = 0.0000000001;
|
|
48
49
|
|
|
49
50
|
/**
|
|
50
|
-
* Distance from point p to line a-b, squared.
|
|
51
|
+
* Distance from point p to line a-b, squared. Native (arm64, clang
|
|
52
|
+
* -ffp-contract=on) fuses the numerator as fmadd(p.y-a.y, dx, -(dy*(p.x-a.x)))
|
|
53
|
+
* and the denominator as fmadd(dx, dx, dy*dy): libgvc _ptToLine2 at
|
|
54
|
+
* 0x21214-0x21244, identically in inlined _update_bb_bz and _approx_bezier.
|
|
51
55
|
* @see lib/common/geom.c:ptToLine2
|
|
52
56
|
*/
|
|
53
|
-
function ptToLine2(a: Point, b: Point, p: Point): number {
|
|
57
|
+
export function ptToLine2(a: Point, b: Point, p: Point): number {
|
|
54
58
|
const dx = b.x - a.x;
|
|
55
59
|
const dy = b.y - a.y;
|
|
56
|
-
let a2 = (p.y - a.y
|
|
60
|
+
let a2 = fma(p.y - a.y, dx, -(dy * (p.x - a.x)));
|
|
57
61
|
a2 *= a2; // square — ensures positive
|
|
58
62
|
if (a2 < SMALL) return 0; // avoid 0/0 problems
|
|
59
|
-
return a2 / (dx
|
|
63
|
+
return a2 / fma(dx, dx, dy * dy);
|
|
60
64
|
}
|
|
61
65
|
|
|
62
66
|
/**
|
|
@@ -15,6 +15,7 @@ import { shortestPath, routeSpline, makePolyline } from '../pathplan/index.js';
|
|
|
15
15
|
import type { Poly } from '../pathplan/types.js';
|
|
16
16
|
import { INIT_DELTA, LOOP_TRIES, ROUTESPL_FUDGE } from './splines-constants.js';
|
|
17
17
|
import { fma } from './fma.js';
|
|
18
|
+
import { agerrLineOn } from './agerr.js';
|
|
18
19
|
|
|
19
20
|
// ---------------------------------------------------------------------------
|
|
20
21
|
// Stage-dump hook (diagnosis only; null in production — same pattern as
|
|
@@ -450,7 +451,7 @@ function routeSplinesInternal(pp: Path, polyline: boolean): Point[] | null {
|
|
|
450
451
|
if (pl === null) {
|
|
451
452
|
// C: agerrorf("in routesplines, Pshortestpath failed") — the caller then
|
|
452
453
|
// loses the edge. @see lib/common/routespl.c (Pshortestpath < 0 branch)
|
|
453
|
-
|
|
454
|
+
agerrLineOn('warn', 'in routesplines, Pshortestpath failed');
|
|
454
455
|
return null;
|
|
455
456
|
}
|
|
456
457
|
emitDump('S3_POLY', dumpNames, () => ({
|
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
ALL_FONT_METRICS,
|
|
9
9
|
type FontFamilyData,
|
|
10
10
|
} from "./textmeasure-lut-data.js";
|
|
11
|
+
import { registerRenderState } from "./render-state.js";
|
|
12
|
+
import { agwarningf } from './agerr.js';
|
|
11
13
|
|
|
12
14
|
/** Times is always present; used as the fallback family. */
|
|
13
15
|
export const TIMES_FALLBACK: FontFamilyData = ALL_FONT_METRICS[0]!;
|
|
@@ -30,8 +32,13 @@ export function normalizeFontName(name: string): string {
|
|
|
30
32
|
return result;
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* Font names that have already triggered the fallback warning: C's emit_once
|
|
37
|
+
* set, emptied per render (a native run is one process; emit_once_reset).
|
|
38
|
+
* @see lib/common/emit.c:emit_once
|
|
39
|
+
*/
|
|
34
40
|
const warnedFontNames = new Set<string>();
|
|
41
|
+
registerRenderState(() => warnedFontNames.clear());
|
|
35
42
|
|
|
36
43
|
/** @see lib/common/textspan_lut.c:get_metrics_for_font_family */
|
|
37
44
|
export function getFamilyMetrics(fontName: string): FontFamilyData {
|
|
@@ -41,8 +48,8 @@ export function getFamilyMetrics(fontName: string): FontFamilyData {
|
|
|
41
48
|
}
|
|
42
49
|
if (!warnedFontNames.has(fontName)) {
|
|
43
50
|
warnedFontNames.add(fontName);
|
|
44
|
-
|
|
45
|
-
`
|
|
51
|
+
agwarningf(
|
|
52
|
+
`no hard-coded metrics for '${fontName}'. ` +
|
|
46
53
|
`Falling back to 'Times' metrics`,
|
|
47
54
|
);
|
|
48
55
|
}
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
import { type FontFamilyData } from "./textmeasure-lut-data.js";
|
|
11
11
|
import { getFamilyMetrics, normalizeFontName } from "./textmeasure-lookup.js";
|
|
12
12
|
import { canvasFont } from "./css-font.js";
|
|
13
|
+
import { agwarningf } from "./agerr.js";
|
|
14
|
+
import { registerRenderState } from "./render-state.js";
|
|
13
15
|
|
|
14
16
|
/** Number of hard-coded font families in the LUT. */
|
|
15
17
|
export const LUT_FAMILY_COUNT = 11;
|
|
@@ -72,12 +74,53 @@ function getVariantWidths(
|
|
|
72
74
|
// ── Character width lookup ────────────────────────────────────────────────────
|
|
73
75
|
|
|
74
76
|
const SPACE_CHAR_CODE = 32;
|
|
77
|
+
const FIRST_NON_ASCII = 128;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* C's two `static bool warning_already_reported` flags, one per warning.
|
|
81
|
+
* Once per render (AD-4), as one native process renders one graph.
|
|
82
|
+
* @see lib/common/textspan_lut.c:estimate_character_width_canonical
|
|
83
|
+
*/
|
|
84
|
+
const warned = new Set<string>();
|
|
85
|
+
const NON_ASCII_FLAG = "non-ascii";
|
|
86
|
+
const ASCII_FLAG = "ascii";
|
|
87
|
+
|
|
88
|
+
registerRenderState(() => warned.clear());
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The C format strings already begin with `Warning: `, and agwarningf adds its
|
|
92
|
+
* own prefix, so native prints `Warning: Warning: ...`. The trailing newline
|
|
93
|
+
* of the C format is the line terminator `console.warn` supplies.
|
|
94
|
+
*/
|
|
95
|
+
function warnNonAscii(character: number): void {
|
|
96
|
+
if (warned.has(NON_ASCII_FLAG)) return;
|
|
97
|
+
warned.add(NON_ASCII_FLAG);
|
|
98
|
+
agwarningf(
|
|
99
|
+
`Warning: no value for width of non-ASCII character ${character}. Falling back to width of space character`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function warnAscii(character: number): void {
|
|
104
|
+
if (warned.has(ASCII_FLAG)) return;
|
|
105
|
+
warned.add(ASCII_FLAG);
|
|
106
|
+
agwarningf(
|
|
107
|
+
`Warning: no value for width of ASCII character ${character}. Falling back to 0`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
75
110
|
|
|
76
111
|
/** @see lib/common/textspan_lut.c:estimate_character_width_canonical */
|
|
77
112
|
function charWidthUnits(widths: readonly number[], charCode: number): number {
|
|
78
|
-
|
|
113
|
+
let code = charCode;
|
|
114
|
+
if (code >= FIRST_NON_ASCII) {
|
|
115
|
+
warnNonAscii(code);
|
|
116
|
+
code = SPACE_CHAR_CODE;
|
|
117
|
+
}
|
|
79
118
|
const w = widths[code] ?? -1;
|
|
80
|
-
|
|
119
|
+
if (w < 0) {
|
|
120
|
+
warnAscii(code);
|
|
121
|
+
return 0;
|
|
122
|
+
}
|
|
123
|
+
return w;
|
|
81
124
|
}
|
|
82
125
|
|
|
83
126
|
/**
|
package/src/gvc/anchor.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { substObjAnchor, preprocessTooltip } from '../common/subst.js';
|
|
|
21
21
|
import { parseStyleFlags } from '../common/style-resolve.js';
|
|
22
22
|
import { nodeAttr } from '../common/poly-init.js';
|
|
23
23
|
import { bezier } from '../common/bezier.js';
|
|
24
|
+
import { ptToLine2 } from '../common/splines-geom.js';
|
|
24
25
|
import type { RendererPlugin } from './context.js';
|
|
25
26
|
import { MapShape, type ObjState, type RenderJob } from './job.js';
|
|
26
27
|
|
|
@@ -231,8 +232,6 @@ export function beginAnchorIf(renderer: RendererPlugin, job: RenderJob): boolean
|
|
|
231
232
|
const DFLT_SAMPLE = 20;
|
|
232
233
|
/** HW: maximum distance from line, in points. @see lib/common/emit.c:739 */
|
|
233
234
|
const MAP_HW = 2.0;
|
|
234
|
-
/** SMALL: 0/0 guard in ptToLine2. @see lib/common/geom.c:176 */
|
|
235
|
-
const MAP_SMALL = 0.0000000001;
|
|
236
235
|
|
|
237
236
|
/**
|
|
238
237
|
* Coordinate-transform context for the imagemap device. `bb` is the unpadded
|
|
@@ -430,16 +429,6 @@ export function computeLabelRectMap(lab: TextlabelT, obj: ObjState, ctx: MapCtx)
|
|
|
430
429
|
|
|
431
430
|
// --- Edge spline hot spots (map_output_bspline) --------------------------
|
|
432
431
|
|
|
433
|
-
/** ptToLine2: squared distance from p to segment (a,b). @see lib/common/geom.c */
|
|
434
|
-
function ptToLine2(a: Point, b: Point, p: Point): number {
|
|
435
|
-
const dx = b.x - a.x;
|
|
436
|
-
const dy = b.y - a.y;
|
|
437
|
-
let a2 = (p.y - a.y) * dx - (p.x - a.x) * dy;
|
|
438
|
-
a2 *= a2;
|
|
439
|
-
if (a2 < MAP_SMALL) return 0;
|
|
440
|
-
return a2 / (dx * dx + dy * dy);
|
|
441
|
-
}
|
|
442
|
-
|
|
443
432
|
/** check_control_points: both handles within HW of the chord. @see emit.c */
|
|
444
433
|
function checkControlPoints(cp: Point[]): boolean {
|
|
445
434
|
return ptToLine2(cp[0]!, cp[3]!, cp[1]!) < MAP_HW * MAP_HW
|
|
@@ -13,7 +13,8 @@ import {
|
|
|
13
13
|
parseStyleFlags, resolvePenColor, resolvePenType, resolvePenWidth,
|
|
14
14
|
resolveClusterFillEx,
|
|
15
15
|
} from '../common/style-resolve.js';
|
|
16
|
-
import { resolveRenderColor, withColorScheme } from '../render/color-resolve.js';
|
|
16
|
+
import { resolveRenderColor, withColorScheme, reportColorNames } from '../render/color-resolve.js';
|
|
17
|
+
import { parseSegs } from '../common/multicolor.js';
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Copy gradient fields from a linear/radial ResolvedFill onto obj.
|
|
@@ -101,14 +102,41 @@ export function applyClusterObjState(sg: Graph, job: RenderJob): boolean {
|
|
|
101
102
|
const obj = job.obj;
|
|
102
103
|
// C wraps the cluster color block with setColorScheme. @see emit.c:3800/3943
|
|
103
104
|
return withColorScheme(clusterAttr(sg, 'colorscheme'), () => {
|
|
105
|
+
// C resolves the fill (set_fillcolor / gradient vals) before the pen,
|
|
106
|
+
// which is set just before gvrender_box. @see emit.c:3846-3868 vs 3907-3913
|
|
107
|
+
const filled = applyClusterFill(obj, fillRes);
|
|
104
108
|
applyClusterPenState(obj, sg);
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
obj.fill = FillType.Solid;
|
|
108
|
-
obj.fillColor = resolveRenderColor(fillRes.color);
|
|
109
|
-
return true;
|
|
110
|
-
}
|
|
111
|
-
applyClusterGradient(obj, fillRes);
|
|
112
|
-
return true;
|
|
109
|
+
reportColorNames(stripedClusterColors(sg));
|
|
110
|
+
return filled;
|
|
113
111
|
});
|
|
114
112
|
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The colours stripedBox sets for a `style=striped` cluster (not rounded): each
|
|
116
|
+
* segment of the fill spec with a positive width, after the pen.
|
|
117
|
+
* @see lib/common/emit.c:3896-3906 emit_clusters (striped branch)
|
|
118
|
+
* @see lib/common/emit.c:595 stripedBox
|
|
119
|
+
*/
|
|
120
|
+
function stripedClusterColors(sg: Graph): string[] {
|
|
121
|
+
const flags = parseStyleFlags(clusterStyle(sg));
|
|
122
|
+
if (!flags.striped || flags.rounded) return [];
|
|
123
|
+
const color = clusterAttr(sg, 'color') ?? '';
|
|
124
|
+
let fill = clusterAttr(sg, 'fillcolor') || color;
|
|
125
|
+
const bg = clusterAttr(sg, 'bgcolor') ?? '';
|
|
126
|
+
if ((!flags.filled || fill === '') && bg !== '') fill = bg;
|
|
127
|
+
const { segs, error } = parseSegs(fill === '' ? 'lightgrey' : fill);
|
|
128
|
+
if (error === 1 || error === 2) return [];
|
|
129
|
+
return segs.flatMap((seg) => (seg.color !== null && seg.t > 0 ? [seg.color] : []));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** The fill half of {@link applyClusterObjState}; true when the box is filled. */
|
|
133
|
+
function applyClusterFill(obj: ObjState, fillRes: ResolvedFill): boolean {
|
|
134
|
+
if (fillRes.kind === 'none') { obj.fill = FillType.None; return false; }
|
|
135
|
+
if (fillRes.kind === 'solid') {
|
|
136
|
+
obj.fill = FillType.Solid;
|
|
137
|
+
obj.fillColor = resolveRenderColor(fillRes.color);
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
applyClusterGradient(obj, fillRes);
|
|
141
|
+
return true;
|
|
142
|
+
}
|
package/src/gvc/device-labels.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import type { Graph } from '../model/graph.js';
|
|
12
12
|
import type { Node } from '../model/node.js';
|
|
13
|
+
import type { Edge } from '../model/edge.js';
|
|
13
14
|
import type { RendererPlugin } from './context.js';
|
|
14
15
|
import type { RenderJob } from './job.js';
|
|
15
16
|
import type { TextlabelT } from '../common/types.js';
|
|
@@ -17,6 +18,8 @@ import type { TextSpan } from '../common/emit-types.js';
|
|
|
17
18
|
import type { PlacedHtml } from '../common/htmltable-pos.js';
|
|
18
19
|
import { gvrenderTextspan, withLabelEmitState } from './textspan-emit.js';
|
|
19
20
|
import { emitHtmlLabel } from '../common/htmltable-emit.js';
|
|
21
|
+
import { reportColorNames } from '../render/color-resolve.js';
|
|
22
|
+
import { htmlLabelColors } from '../common/htmltable-emit-fill.js';
|
|
20
23
|
|
|
21
24
|
/** valign codes stored on textlabel_t. @see lib/common/types.h:textlabel_t.valign */
|
|
22
25
|
const VALIGN_TOP = 't'.charCodeAt(0);
|
|
@@ -38,6 +41,29 @@ function labelSpanX(lp: TextlabelT, just: 'l' | 'n' | 'r'): number {
|
|
|
38
41
|
return lp.pos.x;
|
|
39
42
|
}
|
|
40
43
|
|
|
44
|
+
/**
|
|
45
|
+
* The names emit_label hands to gvrender_resolve_color: an HTML label's walk,
|
|
46
|
+
* or a plain label's fontcolor (set once, before the spans, when there is a
|
|
47
|
+
* span).
|
|
48
|
+
* @see lib/common/labels.c:emit_label
|
|
49
|
+
*/
|
|
50
|
+
export function labelColorNames(lp: TextlabelT): string[] {
|
|
51
|
+
if (lp.html) return lp.u.kind === 'html' ? htmlLabelColors(lp.u.html as PlacedHtml) : [];
|
|
52
|
+
return lp.u.kind === 'txt' && lp.u.nspans >= 1 ? [lp.fontcolor] : [];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Report the unknown colours of an edge's four labels in emit_end_edge order
|
|
57
|
+
* (label, xlabel, head, tail), each only when placed.
|
|
58
|
+
* @see lib/common/emit.c:emit_edge_label
|
|
59
|
+
*/
|
|
60
|
+
export function reportEdgeLabelColors(e: Edge): void {
|
|
61
|
+
const labels = [e.info.label, e.info.xlabel, e.info.head_label, e.info.tail_label];
|
|
62
|
+
for (const lp of labels as Array<TextlabelT | undefined>) {
|
|
63
|
+
if (lp?.set) reportColorNames(labelColorNames(lp));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
41
67
|
/**
|
|
42
68
|
* Emit one label's text spans if present and placed.
|
|
43
69
|
* Shared by edge-label, node-xlabel, and graph-label slots.
|
|
@@ -59,6 +85,7 @@ export function renderOneLabel(
|
|
|
59
85
|
// default pos. Callers mirroring the existence-only sites pass false.
|
|
60
86
|
if (!lp) return;
|
|
61
87
|
if (requireSet && !lp.set) return;
|
|
88
|
+
reportColorNames(labelColorNames(lp));
|
|
62
89
|
// HTML branch: @see lib/common/labels.c:emit_label (226-230)
|
|
63
90
|
// C routes to emit_html_label(job, lp->u.html, lp) using lp->pos as anchor.
|
|
64
91
|
if (lp.html) {
|