@knowvah/dot-engine 2.6.11 → 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 (125) 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/htmltable-attr-warn.d.ts.map +1 -1
  12. package/dist/common/htmltable-emit-fill.d.ts +9 -0
  13. package/dist/common/htmltable-emit-fill.d.ts.map +1 -1
  14. package/dist/common/htmltable-emit-rules.d.ts.map +1 -1
  15. package/dist/common/htmltable-lex-expat.d.ts +1 -6
  16. package/dist/common/htmltable-lex-expat.d.ts.map +1 -1
  17. package/dist/common/htmltable.d.ts +1 -7
  18. package/dist/common/htmltable.d.ts.map +1 -1
  19. package/dist/common/poly-gencode.d.ts.map +1 -1
  20. package/dist/common/poly-init.d.ts.map +1 -1
  21. package/dist/common/postproc.d.ts.map +1 -1
  22. package/dist/common/random.d.ts.map +1 -1
  23. package/dist/common/render-state.d.ts +8 -0
  24. package/dist/common/render-state.d.ts.map +1 -0
  25. package/dist/common/splines-geom.d.ts +8 -0
  26. package/dist/common/splines-geom.d.ts.map +1 -1
  27. package/dist/common/splines-routespl.d.ts.map +1 -1
  28. package/dist/common/textmeasure-lookup.d.ts.map +1 -1
  29. package/dist/common/textmeasure.d.ts.map +1 -1
  30. package/dist/gvc/anchor.d.ts.map +1 -1
  31. package/dist/gvc/device-cluster.d.ts.map +1 -1
  32. package/dist/gvc/device-labels.d.ts +14 -0
  33. package/dist/gvc/device-labels.d.ts.map +1 -1
  34. package/dist/gvc/device.d.ts.map +1 -1
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +765 -451
  37. package/dist/index.js.map +4 -4
  38. package/dist/layout/dot/cluster.d.ts.map +1 -1
  39. package/dist/layout/dot/conc.d.ts.map +1 -1
  40. package/dist/layout/dot/decomp.d.ts.map +1 -1
  41. package/dist/layout/dot/edge-route-clip.d.ts +18 -10
  42. package/dist/layout/dot/edge-route-clip.d.ts.map +1 -1
  43. package/dist/layout/dot/mincross-build.d.ts.map +1 -1
  44. package/dist/layout/dot/mincross-cross.d.ts.map +1 -1
  45. package/dist/layout/dot/rank.d.ts.map +1 -1
  46. package/dist/layout/dot/splines.d.ts.map +1 -1
  47. package/dist/layout/fdp/index.d.ts.map +1 -1
  48. package/dist/layout/fdp/tlayout-parms.d.ts.map +1 -1
  49. package/dist/layout/fdp/xlayout.d.ts.map +1 -1
  50. package/dist/layout/neato/edge-len.d.ts.map +1 -1
  51. package/dist/layout/neato/fdp-adjust.d.ts.map +1 -1
  52. package/dist/layout/neato/init.d.ts +4 -10
  53. package/dist/layout/neato/init.d.ts.map +1 -1
  54. package/dist/layout/neato/kk.d.ts.map +1 -1
  55. package/dist/layout/neato/multispline-router.d.ts.map +1 -1
  56. package/dist/layout/neato/multispline.d.ts.map +1 -1
  57. package/dist/layout/neato/poly.d.ts.map +1 -1
  58. package/dist/layout/neato/sgd.d.ts.map +1 -1
  59. package/dist/layout/neato/start.d.ts.map +1 -1
  60. package/dist/ortho/index.d.ts.map +1 -1
  61. package/dist/parser/index.d.ts.map +1 -1
  62. package/dist/render/color-resolve.d.ts +49 -1
  63. package/dist/render/color-resolve.d.ts.map +1 -1
  64. package/dist/render/public.d.ts.map +1 -1
  65. package/dist/render/svg-edge-split.d.ts +2 -0
  66. package/dist/render/svg-edge-split.d.ts.map +1 -1
  67. package/dist/render/xdot-public.d.ts.map +1 -1
  68. package/dist/render.js +513 -200
  69. package/dist/render.js.map +4 -4
  70. package/package.json +1 -1
  71. package/src/async/render-async.ts +6 -0
  72. package/src/common/agerr.ts +96 -0
  73. package/src/common/arrows-shapes-poly.ts +19 -5
  74. package/src/common/color.ts +38 -10
  75. package/src/common/crand.ts +8 -2
  76. package/src/common/emit-once.ts +27 -0
  77. package/src/common/graph-init.ts +2 -1
  78. package/src/common/htmltable-attr-warn.ts +3 -4
  79. package/src/common/htmltable-emit-fill.ts +48 -0
  80. package/src/common/htmltable-emit-rules.ts +2 -0
  81. package/src/common/htmltable-lex-expat.ts +78 -23
  82. package/src/common/htmltable.ts +15 -16
  83. package/src/common/poly-gencode.ts +14 -2
  84. package/src/common/poly-init.ts +2 -1
  85. package/src/common/postproc.ts +4 -1
  86. package/src/common/random.ts +11 -1
  87. package/src/common/render-state.ts +36 -0
  88. package/src/common/splines-geom.ts +8 -4
  89. package/src/common/splines-routespl.ts +2 -1
  90. package/src/common/textmeasure-lookup.ts +10 -3
  91. package/src/common/textmeasure.ts +45 -2
  92. package/src/gvc/anchor.ts +1 -12
  93. package/src/gvc/device-cluster.ts +37 -9
  94. package/src/gvc/device-labels.ts +27 -0
  95. package/src/gvc/device.ts +18 -6
  96. package/src/index.ts +3 -0
  97. package/src/layout/dot/cluster.ts +2 -1
  98. package/src/layout/dot/conc.ts +6 -5
  99. package/src/layout/dot/decomp.ts +3 -0
  100. package/src/layout/dot/edge-route-clip.ts +27 -21
  101. package/src/layout/dot/mincross-build.ts +2 -0
  102. package/src/layout/dot/mincross-cross.ts +2 -0
  103. package/src/layout/dot/rank.ts +2 -0
  104. package/src/layout/dot/splines.ts +2 -1
  105. package/src/layout/fdp/index.ts +2 -1
  106. package/src/layout/fdp/tlayout-parms.ts +9 -2
  107. package/src/layout/fdp/xlayout.ts +10 -2
  108. package/src/layout/neato/cdt-surface.ts +6 -6
  109. package/src/layout/neato/edge-len.ts +2 -1
  110. package/src/layout/neato/fdp-adjust.ts +3 -2
  111. package/src/layout/neato/init.ts +11 -11
  112. package/src/layout/neato/kk.ts +2 -1
  113. package/src/layout/neato/multispline-router.ts +3 -3
  114. package/src/layout/neato/multispline.ts +3 -2
  115. package/src/layout/neato/poly.ts +3 -2
  116. package/src/layout/neato/sgd.ts +5 -2
  117. package/src/layout/neato/start.ts +14 -10
  118. package/src/ortho/index.ts +3 -1
  119. package/src/parser/dot.js +4 -1
  120. package/src/parser/dot.pegjs +5 -1
  121. package/src/parser/index.ts +4 -3
  122. package/src/render/color-resolve.ts +193 -4
  123. package/src/render/public.ts +2 -0
  124. package/src/render/svg-edge-split.ts +4 -2
  125. package/src/render/xdot-public.ts +2 -0
@@ -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) {
package/src/gvc/device.ts CHANGED
@@ -42,11 +42,14 @@ import {
42
42
  styleTokens,
43
43
  } from '../common/style-resolve.js';
44
44
  import { isPointNode } from '../common/poly-gencode.js';
45
- import { resolveRenderColor, withColorScheme } from '../render/color-resolve.js';
45
+ import {
46
+ resolveRenderColor, withColorScheme, reportEdgeColors, reportColorNames, backgroundColors,
47
+ setRendererKnownColors,
48
+ } from '../render/color-resolve.js';
46
49
  import { emitRoundedBezier } from '../common/poly-shapes.js';
47
50
  import { applyClusterObjState, clusterStyle, clusterPeripheries } from './device-cluster.js';
48
51
  import { transformPoint } from './device-transform.js';
49
- import { renderOneLabel, renderNodeXLabel, renderClusterLabel } from './device-labels.js';
52
+ import { renderOneLabel, renderNodeXLabel, renderClusterLabel, reportEdgeLabelColors } from './device-labels.js';
50
53
  export { transformPoint, applyRotation, applyScale, buildPoint } from './device-transform.js';
51
54
  export { renderOneLabel, renderNodeXLabel, renderClusterLabel } from './device-labels.js';
52
55
  import { svgNodeId, svgEdgeId, svgClusterId, svgGraphId } from '../render/svg-id.js';
@@ -156,7 +159,7 @@ function setEdgePen(e: Edge, job: RenderJob): void {
156
159
  const obj = job.obj;
157
160
  const flags = parseStyleFlags(e.attrs.get('style'));
158
161
  obj.penColor = withColorScheme(e.attrs.get('colorscheme'),
159
- () => resolveRenderColor(resolvePenColor(e.attrs.get('color'))));
162
+ () => resolveRenderColor(resolvePenColor(e.attrs.get('color')), false));
160
163
  obj.pen = resolvePenType(flags);
161
164
  obj.penWidth = resolvePenWidth(flags, e.attrs.get('penwidth'));
162
165
  // Raw style tokens for xdot's `S` ops (e.g. explicit style="solid"); SVG
@@ -189,6 +192,10 @@ export function renderEdge(e: Edge, renderer: RendererPlugin, job: RenderJob): v
189
192
  // @see lib/common/emit.c:emit_edge (begin/end color context)
190
193
  withColorScheme(e.attrs.get('colorscheme'), () => {
191
194
  renderer.beginEdge(e, job);
195
+ // emit_edge_graphics, then emit_end_edge's labels: C resolves these names
196
+ // here whatever the renderer draws (the non-drawing ones included).
197
+ reportEdgeColors(e);
198
+ reportEdgeLabelColors(e);
192
199
  renderer.endEdge(e, job);
193
200
  });
194
201
  } finally {
@@ -351,9 +358,13 @@ function renderPage(g: Graph, renderer: RendererPlugin, job: RenderJob, info: La
351
358
  resolveObjAnchor(g, labelTextOf(g.info.label), svgGraphId(g, job), job.obj);
352
359
  }
353
360
  const anchored = beginAnchorIf(renderer, job);
354
- renderer.pageBackground?.(g, job);
355
- // emit root-graph label before clusters/nodes @see lib/common/emit.c:emit_page
356
- renderGraphLabel(g, renderer, job);
361
+ // emit_page opens the graph's colorscheme around the background and label.
362
+ withColorScheme(g.attrs.get('colorscheme'), () => {
363
+ reportColorNames(backgroundColors(g.attrs.get('bgcolor'), renderer.type === 'svg'));
364
+ renderer.pageBackground?.(g, job);
365
+ // emit root-graph label before clusters/nodes @see lib/common/emit.c:emit_page
366
+ renderGraphLabel(g, renderer, job);
367
+ });
357
368
  if (anchored) renderer.endAnchor?.(job);
358
369
  // emit_view: clusters are drawn BEFORE nodes/edges normally, but the map
359
370
  // device (EMIT_CLUSTERS_LAST) emits their <area> AFTER, so a node/edge area
@@ -459,6 +470,7 @@ export function render(ctx: GvcContext, g: Graph, format: string, inlineImages =
459
470
  // @see lib/common/emit.c:3260 (gvc->rotation=90); :3390 (job->rotation)
460
471
  job.rotation = parseLandscape(g) ? 90 : 0;
461
472
  job.renderer = renderer;
473
+ setRendererKnownColors(renderer.type === 'svg'); // svg_knowncolors; others have none
462
474
  resetHtmlAnchorIds(); // C: fresh anchorId per dot invocation
463
475
  for (const n of g.nodes.values()) polyInit(n, g, job.measurer);
464
476
  return renderGraph(g, job, renderer);
package/src/index.ts CHANGED
@@ -22,6 +22,7 @@ import type { GvError, RenderResult } from './errors.js';
22
22
  import type { EngineName } from './gvc/context.js';
23
23
  import { render as deviceRender } from './gvc/device.js';
24
24
  import { createDefaultContext } from './gvc/default-context.js';
25
+ import { resetRenderState } from './common/render-state.js';
25
26
 
26
27
 
27
28
 
@@ -90,6 +91,7 @@ function classifyError(err: unknown): GvError {
90
91
  */
91
92
  export function renderSvg(dotSource: string, engine: EngineName): string {
92
93
  checkRenderArgs(dotSource, engine);
94
+ resetRenderState(); // AD-4: one render = one native process
93
95
  const ctx = createDefaultContext();
94
96
  try {
95
97
  // parse() is inside the try so any non-ParseError throw (e.g. a raw
@@ -122,6 +124,7 @@ export function renderSvg(dotSource: string, engine: EngineName): string {
122
124
  export function tryRenderSvg(dotSource: string, engine: EngineName): RenderResult {
123
125
  checkRenderArgs(dotSource, engine);
124
126
  try {
127
+ // renderSvg calls resetRenderState first (AD-4).
125
128
  return { svg: renderSvg(dotSource, engine) };
126
129
  } catch (err: unknown) {
127
130
  if (isUsageError(err)) throw err;
@@ -27,6 +27,7 @@ import { CLUSTER } from './rank.js';
27
27
  import { makeSlots, safeOtherEdge, makeInterclustChain } from './cluster-path.js';
28
28
  // mergeChain lives in classify.ts (circular: classify ↔ cluster — safe for function refs)
29
29
  import { mergeChain, portsEq } from './classify.js';
30
+ import { agwarningf } from '../../common/agerr.js';
30
31
 
31
32
  export { CLUSTER_EDGE } from './cluster-path.js';
32
33
  export { mapInterclustNode } from './cluster-path.js';
@@ -297,7 +298,7 @@ export function markClusterNode(clust: Graph, n: Node, g: Graph): void {
297
298
  if ((n.info.ranktype ?? 0) !== 0) {
298
299
  // C warns with the ENCLOSING graph's name, not the cluster's.
299
300
  // @see lib/dotgen/cluster.c:317-320
300
- console.error(`Warning: ${n.name} was already in a rankset, deleted from cluster ${g.name}`);
301
+ agwarningf(`${n.name} was already in a rankset, deleted from cluster ${g.name}`, 'error');
301
302
  agDeleteFromCluster(clust, n);
302
303
  return;
303
304
  }
@@ -20,6 +20,7 @@ import { dotScanRanks } from './rank.js';
20
20
  import { dotRoot, rankGet } from './mincross-utils.js';
21
21
  import { nodesInSeq } from './decomp.js';
22
22
  import { graphMinrank, graphMaxrank } from './position-aux.js';
23
+ import { agerr, agerrorf, agwarningf } from '../../common/agerr.js';
23
24
 
24
25
  const UP = 0;
25
26
  const DOWN = 1;
@@ -263,13 +264,13 @@ export function fillRankVlist(g: Graph, r: number, root: Graph): number {
263
264
  const rankArr = g.info.rank!;
264
265
  const lead = g.info.rankleader![r];
265
266
  if (lead === undefined || lead === null) {
266
- console.error(`Error: rebuild_vlists: lead is null for rank ${r}`);
267
+ agerrorf(`rebuild_vlists: lead is null for rank ${r}`);
267
268
  return -1;
268
269
  }
269
270
  const rootRank = root.info.rank![r];
270
271
  if (rootRank.v[lead.info.order ?? 0] !== lead) {
271
- console.error(
272
- `Error: rebuild_vlists: rank lead ${lead.name} not in order ` +
272
+ agerrorf(
273
+ `rebuild_vlists: rank lead ${lead.name} not in order ` +
273
274
  `${lead.info.order ?? 0} of rank ${r}`,
274
275
  );
275
276
  return -1;
@@ -292,7 +293,7 @@ export function fillRankVlist(g: Graph, r: number, root: Graph): number {
292
293
  rankArr[r].v = rootRank.v;
293
294
  rankArr[r].vStart = lead.info.order ?? 0;
294
295
  const maxi = computeMaxi(g, r);
295
- if (maxi === -1) console.error(`Warning: degenerate concentrated rank ${g.name},${r}`);
296
+ if (maxi === -1) agwarningf(`degenerate concentrated rank ${g.name},${r}`, 'error');
296
297
  rankArr[r].n = maxi + 1;
297
298
  return 0;
298
299
  }
@@ -425,7 +426,7 @@ export function dotConcentrate(g: Graph): number {
425
426
  const clust = g.info.clust ?? [];
426
427
  for (let c = 0; c < nCluster; c++) {
427
428
  if (rebuildVlists(clust[c]) !== 0) {
428
- console.error('concentrate=true may not work correctly.');
429
+ agerr('prev', 'concentrate=true may not work correctly.');
429
430
  return -1;
430
431
  }
431
432
  }
@@ -3,6 +3,7 @@
3
3
  import type { Node } from '../../model/node.js';
4
4
  import type { Graph } from '../../model/graph.js';
5
5
  import type { EdgeList } from '../../model/nodeInfo.js';
6
+ import { registerRenderState } from '../../common/render-state.js';
6
7
 
7
8
  // ---------------------------------------------------------------------------
8
9
  // AGSEQ-order node iteration (agfstnode/agnxtnode mirror)
@@ -135,7 +136,9 @@ export function decompSearch(g: Graph, start: Node, cmark: number): void {
135
136
  }
136
137
 
137
138
  // Cmark as const-object property avoids module-level let that confuses Lizard.
139
+ /** @see lib/dotgen/decomp.c:Cmark (C static, 0 at process start) */
138
140
  const _dc = { cmark: 0 };
141
+ registerRenderState(() => { _dc.cmark = 0; });
139
142
 
140
143
  // ONE private function immediately before decompose — the acyclic.ts pattern.
141
144
  function resolveNode(n: Node, pass: 0 | 1): Node | undefined {
@@ -12,6 +12,7 @@ import type { Point } from '../../model/geom.js';
12
12
  import { parseArrow, resolveArrowType } from '../../common/arrows.js';
13
13
  import { arrowLength } from '../../common/arrows-shapes.js';
14
14
  import { bezier } from '../../common/bezier.js';
15
+ import { fma } from '../../common/fma.js';
15
16
 
16
17
  // ---------------------------------------------------------------------------
17
18
  // De Casteljau subdivision
@@ -115,17 +116,30 @@ export function bezierClipNode(
115
116
  }
116
117
 
117
118
  /**
118
- * Clip the path endpoint to the arrowhead BASE (not node boundary).
119
- *
120
- * Ports lib/common/arrows.c:arrowEndClip.
121
- * The path ends at arrowhead-BASE (arrowTip + elen toward tail), not at the
122
- * node boundary. The arrowTip (spl.ep) is the node-boundary clip result.
119
+ * DIST(p, q) = sqrt(SQR(dx) + SQR(dy)) as the oracle compiles it
120
+ * (libgvc 16.1 `_arrowEndClip`: `fmul dy*dy; fmadd dx, dx; fsqrt`), i.e.
121
+ * sqrt(fma(dx, dx, dy * dy)) -- not Math.hypot.
122
+ * @see lib/common/geom.h:DIST
123
+ */
124
+ export function distFused(p: Point, q: Point): number {
125
+ const dx = p.x - q.x;
126
+ const dy = p.y - q.y;
127
+ return Math.sqrt(fma(dx, dx, dy * dy));
128
+ }
129
+
130
+ /**
131
+ * Clip the path endpoint to the arrowhead BASE (not the node boundary).
123
132
  *
124
- * Steps (matching C arrowEndClip):
125
- * 1. Reverse the last Bezier segment: sp[0]=arrowTip, sp[1..3]=reversed path
126
- * 2. inside = DIST(p, arrowTip) <= elen (arrowhead sphere)
127
- * 3. bezierClip with left_inside=true (sp[0]=arrowTip is "inside" the sphere)
128
- * 4. Un-reverse the clipped right subdivision → path ends at arrowhead BASE
133
+ * Ports the bezier part of lib/common/arrows.c:arrowEndClip (:285-330); the
134
+ * `spl->eflag/ep` bookkeeping and the `endp -= 3` back-up (:288-296) live in
135
+ * the callers (compound.ts:arrowEndClipIdx). Line map (C -> here):
136
+ * :299-302 sp[3..0] = ps[endp..], sp[0] = spl->ep -> `sp` below
137
+ * :304 if (elen > 0) -> `elen > 0`
138
+ * :305-307 inside_context {p = &sp[0], r = &elen} -> `inside` (centre = tip)
139
+ * :308 bezier_clip(&ctx, inside, sp, true) -> bezierClipNode, left
140
+ * inside, in ABSOLUTE coordinates (centre 0,0), as C does
141
+ * :311-314 ps[endp..endp+3] = sp[3..0] -> returned array
142
+ * :281-283 inside(): DIST(p, a.p[0]) <= a.r[0] -> distFused <= elen
129
143
  *
130
144
  * @see lib/common/arrows.c:arrowEndClip
131
145
  */
@@ -134,18 +148,10 @@ export function arrowEndClip(
134
148
  arrowTip: Point,
135
149
  elen: number,
136
150
  ): Point[] {
137
- if (elen <= 0) return path4;
138
- // Reverse: sp[0]=tip (inside sphere), sp[1..3]=reversed last 3 path points
139
151
  const sp: Point[] = [arrowTip, path4[2]!, path4[1]!, path4[0]!];
140
- const insideSphere = (lx: number, ly: number): boolean =>
141
- Math.hypot(lx, ly) <= elen;
142
- // bezierClipLocal with sp in RELATIVE coords (tip=origin)
143
- const spRel: Point[] = sp.map(p => ({ x: p.x - arrowTip.x, y: p.y - arrowTip.y }));
144
- const clippedRel = bezierClipLocal(spRel, insideSphere, true);
145
- const clipped = clippedRel.map(p => ({ x: p.x + arrowTip.x, y: p.y + arrowTip.y }));
146
- // Un-reverse: the clipped right subdivision = [arrowBase, ..., arrowTip]
147
- // The path endpoint becomes arrowBase = clipped[0]
148
- // Un-reverse the full clipped result; all 4 points update via de Casteljau
152
+ const clipped = elen > 0
153
+ ? bezierClipNode(sp, 0, 0, (x, y) => distFused({ x, y }, arrowTip) <= elen, true)
154
+ : sp;
149
155
  return [clipped[3]!, clipped[2]!, clipped[1]!, clipped[0]!];
150
156
  }
151
157