@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.
Files changed (131) hide show
  1. package/dist/async/render-async.d.ts.map +1 -1
  2. package/dist/common/agerr.d.ts +37 -0
  3. package/dist/common/agerr.d.ts.map +1 -0
  4. package/dist/common/arrows-shapes-poly.d.ts.map +1 -1
  5. package/dist/common/color.d.ts.map +1 -1
  6. package/dist/common/crand.d.ts +0 -20
  7. package/dist/common/crand.d.ts.map +1 -1
  8. package/dist/common/emit-once.d.ts +6 -0
  9. package/dist/common/emit-once.d.ts.map +1 -0
  10. package/dist/common/graph-init.d.ts.map +1 -1
  11. package/dist/common/html-entities.d.ts +13 -5
  12. package/dist/common/html-entities.d.ts.map +1 -1
  13. package/dist/common/htmltable-attr-warn.d.ts.map +1 -1
  14. package/dist/common/htmltable-emit-fill.d.ts +9 -0
  15. package/dist/common/htmltable-emit-fill.d.ts.map +1 -1
  16. package/dist/common/htmltable-emit-rules.d.ts.map +1 -1
  17. package/dist/common/htmltable-lex-chardata.d.ts +9 -0
  18. package/dist/common/htmltable-lex-chardata.d.ts.map +1 -1
  19. package/dist/common/htmltable-lex-expat.d.ts +1 -6
  20. package/dist/common/htmltable-lex-expat.d.ts.map +1 -1
  21. package/dist/common/htmltable.d.ts +1 -7
  22. package/dist/common/htmltable.d.ts.map +1 -1
  23. package/dist/common/poly-gencode.d.ts.map +1 -1
  24. package/dist/common/poly-init.d.ts.map +1 -1
  25. package/dist/common/postproc.d.ts.map +1 -1
  26. package/dist/common/random.d.ts.map +1 -1
  27. package/dist/common/render-state.d.ts +8 -0
  28. package/dist/common/render-state.d.ts.map +1 -0
  29. package/dist/common/splines-geom.d.ts +8 -0
  30. package/dist/common/splines-geom.d.ts.map +1 -1
  31. package/dist/common/splines-routespl.d.ts.map +1 -1
  32. package/dist/common/textmeasure-lookup.d.ts.map +1 -1
  33. package/dist/common/textmeasure.d.ts.map +1 -1
  34. package/dist/gvc/anchor.d.ts.map +1 -1
  35. package/dist/gvc/device-cluster.d.ts.map +1 -1
  36. package/dist/gvc/device-labels.d.ts +14 -0
  37. package/dist/gvc/device-labels.d.ts.map +1 -1
  38. package/dist/gvc/device.d.ts.map +1 -1
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +821 -471
  41. package/dist/index.js.map +4 -4
  42. package/dist/layout/dot/cluster.d.ts.map +1 -1
  43. package/dist/layout/dot/conc.d.ts.map +1 -1
  44. package/dist/layout/dot/decomp.d.ts.map +1 -1
  45. package/dist/layout/dot/edge-route-clip.d.ts +18 -10
  46. package/dist/layout/dot/edge-route-clip.d.ts.map +1 -1
  47. package/dist/layout/dot/mincross-build.d.ts.map +1 -1
  48. package/dist/layout/dot/mincross-cross.d.ts.map +1 -1
  49. package/dist/layout/dot/rank.d.ts.map +1 -1
  50. package/dist/layout/dot/splines.d.ts.map +1 -1
  51. package/dist/layout/fdp/index.d.ts.map +1 -1
  52. package/dist/layout/fdp/tlayout-parms.d.ts.map +1 -1
  53. package/dist/layout/fdp/xlayout.d.ts.map +1 -1
  54. package/dist/layout/neato/edge-len.d.ts.map +1 -1
  55. package/dist/layout/neato/fdp-adjust.d.ts.map +1 -1
  56. package/dist/layout/neato/init.d.ts +4 -10
  57. package/dist/layout/neato/init.d.ts.map +1 -1
  58. package/dist/layout/neato/kk.d.ts.map +1 -1
  59. package/dist/layout/neato/multispline-router.d.ts.map +1 -1
  60. package/dist/layout/neato/multispline.d.ts.map +1 -1
  61. package/dist/layout/neato/poly.d.ts.map +1 -1
  62. package/dist/layout/neato/sgd.d.ts.map +1 -1
  63. package/dist/layout/neato/start.d.ts.map +1 -1
  64. package/dist/ortho/index.d.ts.map +1 -1
  65. package/dist/parser/index.d.ts.map +1 -1
  66. package/dist/render/color-resolve.d.ts +49 -1
  67. package/dist/render/color-resolve.d.ts.map +1 -1
  68. package/dist/render/public.d.ts.map +1 -1
  69. package/dist/render/svg-edge-split.d.ts +2 -0
  70. package/dist/render/svg-edge-split.d.ts.map +1 -1
  71. package/dist/render/xdot-public.d.ts.map +1 -1
  72. package/dist/render.js +569 -220
  73. package/dist/render.js.map +4 -4
  74. package/package.json +1 -1
  75. package/src/async/render-async.ts +6 -0
  76. package/src/common/agerr.ts +96 -0
  77. package/src/common/arrows-shapes-poly.ts +19 -5
  78. package/src/common/color.ts +38 -10
  79. package/src/common/crand.ts +8 -2
  80. package/src/common/emit-once.ts +27 -0
  81. package/src/common/graph-init.ts +2 -1
  82. package/src/common/html-entities.ts +77 -25
  83. package/src/common/htmltable-attr-warn.ts +3 -4
  84. package/src/common/htmltable-emit-fill.ts +48 -0
  85. package/src/common/htmltable-emit-rules.ts +2 -0
  86. package/src/common/htmltable-lex-chardata.ts +42 -8
  87. package/src/common/htmltable-lex-expat.ts +78 -23
  88. package/src/common/htmltable.ts +15 -16
  89. package/src/common/poly-gencode.ts +14 -2
  90. package/src/common/poly-init.ts +2 -1
  91. package/src/common/postproc.ts +4 -1
  92. package/src/common/random.ts +11 -1
  93. package/src/common/render-state.ts +36 -0
  94. package/src/common/splines-geom.ts +8 -4
  95. package/src/common/splines-routespl.ts +2 -1
  96. package/src/common/textmeasure-lookup.ts +10 -3
  97. package/src/common/textmeasure.ts +45 -2
  98. package/src/gvc/anchor.ts +1 -12
  99. package/src/gvc/device-cluster.ts +37 -9
  100. package/src/gvc/device-labels.ts +27 -0
  101. package/src/gvc/device.ts +18 -6
  102. package/src/index.ts +3 -0
  103. package/src/layout/dot/cluster.ts +2 -1
  104. package/src/layout/dot/conc.ts +6 -5
  105. package/src/layout/dot/decomp.ts +3 -0
  106. package/src/layout/dot/edge-route-clip.ts +27 -21
  107. package/src/layout/dot/mincross-build.ts +2 -0
  108. package/src/layout/dot/mincross-cross.ts +2 -0
  109. package/src/layout/dot/rank.ts +2 -0
  110. package/src/layout/dot/splines.ts +2 -1
  111. package/src/layout/fdp/index.ts +2 -1
  112. package/src/layout/fdp/tlayout-parms.ts +9 -2
  113. package/src/layout/fdp/xlayout.ts +10 -2
  114. package/src/layout/neato/cdt-surface.ts +6 -6
  115. package/src/layout/neato/edge-len.ts +2 -1
  116. package/src/layout/neato/fdp-adjust.ts +3 -2
  117. package/src/layout/neato/init.ts +11 -11
  118. package/src/layout/neato/kk.ts +2 -1
  119. package/src/layout/neato/multispline-router.ts +3 -3
  120. package/src/layout/neato/multispline.ts +3 -2
  121. package/src/layout/neato/poly.ts +3 -2
  122. package/src/layout/neato/sgd.ts +5 -2
  123. package/src/layout/neato/start.ts +14 -10
  124. package/src/ortho/index.ts +3 -1
  125. package/src/parser/dot.js +4 -1
  126. package/src/parser/dot.pegjs +5 -1
  127. package/src/parser/index.ts +4 -3
  128. package/src/render/color-resolve.ts +193 -4
  129. package/src/render/public.ts +2 -0
  130. package/src/render/svg-edge-split.ts +4 -2
  131. 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
- /** Character data at `p` up to the next `<`: one `text` token, then the index it ends at. */
274
- function scanText(text: string, p: number, base: number, tokens: ExpatToken[]): number {
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 end = e < 0 ? text.length : e;
277
- tokens.push({ kind: 'text', start: base + p, end: base + end });
278
- return end;
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
- p = scanText(text, p, base, tokens);
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
- const ENTITY_NAME = /^[A-Za-z_:][\w.:-]*$/;
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
- const CHAR_REF = /^&#(?:x[0-9A-Fa-f]+|[0-9]+);/;
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, at its offset: a
417
- * `&name;` scanEntity leaves alone (neither an HTML entity nor predefined), or
418
- * a character reference XML does not allow.
419
- * @see expat lib/xmlparse.c:doContent (XML_TOK_ENTITY_REF, XML_TOK_CHAR_REF)
450
+ * The first reference of character data that expat rejects: an invalid token
451
+ * (a bare `&`, `&#;`, `&#X41;`, `&#65q`, ..), 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
- export function textRefError(text: string): { message: string; offset: number } | undefined {
422
- for (let t = text.indexOf('&'); t >= 0; t = text.indexOf('&', t + 1)) {
423
- if (text[t + 1] === '#') {
424
- const ref = CHAR_REF.exec(text.slice(t))?.[0];
425
- if (ref !== undefined && charRefNumber(ref) < 0) return { message: XML_ERROR_BAD_CHAR_REF, offset: t };
426
- continue;
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 semi = text.indexOf(';', t + 1);
429
- if (semi < 0) continue;
430
- const key = text.slice(t + 1, semi);
431
- if (ENTITY_NAME.test(key) && !Object.hasOwn(HTML_ENTITIES, key) && !XML_PREDEFINED_ENTITIES.has(key)) {
432
- return { message: XML_ERROR_UNDEFINED_ENTITY, offset: t };
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;
@@ -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 flags, uniform
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
- export const htmlTextIsSimple = (texts: HtmlText[], env: HtmlSizeEnv): boolean => {
178
+ const htmlTextIsSimple = (lines: SizedLine[], env: HtmlSizeEnv): boolean => {
177
179
  let prevFs = -1;
178
180
  let prevFace: string | null = null;
179
- let count = 0;
180
- for (const txt of texts) {
181
- for (const item of txt.items) {
182
- if (item.br) { count = 0; continue; }
183
- if (item.text === undefined) continue;
184
- if (++count > 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;
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(texts, env);
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, nodeAttr(n, n.root, 'penwidth'));
383
+ applyPenState(obj, styleAttr, colorAttr, penwidth);
372
384
  return filled;
373
385
  }
374
386
 
@@ -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
- console.error(`Warning: No or improper shapefile="${shapefile}" for node "${n.name}"`);
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);
@@ -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
- console.warn(`lost ${e.tail.name} ${e.head.name} edge`);
232
+ agerrLineOn('warn', `lost ${e.tail.name} ${e.head.name} edge`);
230
233
  }
231
234
  continue;
232
235
  }
@@ -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 = 0x330en; // POSIX default: X0 = 0x1234ABCD330E... see srand48 note
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) * dx - (p.x - a.x) * dy;
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 * dx + dy * dy);
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
- console.warn('in routesplines, Pshortestpath failed');
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
- /** Set of font names that have already triggered the fallback warning. */
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
- console.warn(
45
- `Warning: no hard-coded metrics for '${fontName}'. ` +
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
- const code = charCode >= 128 ? SPACE_CHAR_CODE : charCode;
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
- return w < 0 ? 0 : w;
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
- if (fillRes.kind === 'none') { obj.fill = FillType.None; return false; }
106
- if (fillRes.kind === 'solid') {
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
+ }
@@ -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) {