@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
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
 
@@ -40,6 +40,7 @@ export {
40
40
  constrainingFlatEdge, countConstraining, postorder,
41
41
  flatReorderBuildTemprank, flatReorderFixEdges, flatReorderRank, flatReorder,
42
42
  } from './mincross-flat.js';
43
+ import { registerRenderState } from '../../common/render-state.js';
43
44
 
44
45
  // allocate_ranks — @see lib/dotgen/mincross.c:allocate_ranks
45
46
  export function makeEmptyRank(): RankEntry {
@@ -99,6 +100,7 @@ export function allocateRanks(g: Graph): void {
99
100
  * distinct names rather than colliding/deduping by name).
100
101
  */
101
102
  let fillSeq = 0;
103
+ registerRenderState(() => { fillSeq = 0; });
102
104
 
103
105
  /**
104
106
  * Create a placeholder fill node in the `_new_rank` subgraph at `rank`.
@@ -18,12 +18,14 @@ import { CLUSTER } from './rank.js';
18
18
  import {
19
19
  MincrossContext, matrixGet, rankGet,
20
20
  } from './mincross-utils.js';
21
+ import { registerRenderState } from '../../common/render-state.js';
21
22
 
22
23
  /**
23
24
  * Mirror of C's file-global `ReMincross`. Set true only during the remincross
24
25
  * pass so left2right tightens the cluster constraint. @see mincross.c
25
26
  */
26
27
  let reMincross = false;
28
+ registerRenderState(() => { reMincross = false; });
27
29
  export function setReMincross(b: boolean): void { reMincross = b; }
28
30
 
29
31
  // ---------------------------------------------------------------------------
@@ -12,6 +12,7 @@ import { rank } from './ns.js';
12
12
  import { dot2Rank } from './rank-dot2.js';
13
13
  import { class1 } from './classify.js';
14
14
  import { pruneForeignClusterNodes } from './cluster.js';
15
+ import { registerRenderState } from '../../common/render-state.js';
15
16
 
16
17
  // ---------------------------------------------------------------------------
17
18
  // Constants @see lib/common/const.h
@@ -54,6 +55,7 @@ export const IGNORED = 6;
54
55
  /** Global cluster layout type. @see lib/common/globals.h:CL_type */
55
56
  export let clType = LOCAL;
56
57
  export function setClType(t: number): void { clType = t; }
58
+ registerRenderState(() => { clType = LOCAL; });
57
59
 
58
60
  // ---------------------------------------------------------------------------
59
61
  // Utility helpers
@@ -26,6 +26,7 @@ import { dispatchOrthoEdges } from './ortho-adapter.js';
26
26
  import { routeEdgeGroups } from './splines-groups.js';
27
27
  import { placePortLabels, placeRegularEdgeLabels, setEdgeLabelPos } from './splines-label.js';
28
28
  import { nodesInSeq } from './decomp.js';
29
+ import { agerrLineOn } from '../../common/agerr.js';
29
30
 
30
31
  // ---------------------------------------------------------------------------
31
32
  // Edge-type flag constants
@@ -473,7 +474,7 @@ function orthoDispatch(g: Graph): number {
473
474
  function curvedTop(g: Graph): void {
474
475
  resetRW(g);
475
476
  if (((g.root.info.has_labels ?? 0) & EDGE_LABEL) !== 0) {
476
- console.warn('edge labels with splines=curved not supported in dot - use xlabels\n');
477
+ agerrLineOn('warn', 'edge labels with splines=curved not supported in dot - use xlabels\n');
477
478
  }
478
479
  }
479
480
 
@@ -27,6 +27,7 @@ import { fdpLayout, mkClusters } from './layout.js';
27
27
  import { gdata } from './fdp-model.js';
28
28
  import { graphInit } from '../../common/graph-init.js';
29
29
  import { csrand } from '../../common/crand.js';
30
+ import { agerrLineOn } from '../../common/agerr.js';
30
31
 
31
32
  export { fdpLayout, mkClusters, evalPositions, setBB } from './layout.js';
32
33
  export { fdpTLayout } from './tlayout.js';
@@ -93,7 +94,7 @@ function fdpSplines(g: Graph): void {
93
94
  );
94
95
  }
95
96
  if (g.info.n_cluster_edges) { // HAS_CLUST_EDGE(g)
96
- console.warn(CLUSTER_EDGE_WARNING);
97
+ agerrLineOn('warn', CLUSTER_EDGE_WARNING);
97
98
  et = EDGETYPE_LINE;
98
99
  } else {
99
100
  splineEdges1(g, et);
@@ -15,6 +15,7 @@
15
15
  import type { Graph } from '../../model/graph.js';
16
16
  import { lateDouble, lateInt } from '../../common/nodeinit.js';
17
17
  import { type XParams, aggetGraph } from './fdp-model.js';
18
+ import { registerRenderState } from '../../common/render-state.js';
18
19
 
19
20
  // ---------------------------------------------------------------------------
20
21
  // Constants — @see lib/fdpgen/tlayout.c:96-101, lib/neatogen/neato.h:27-30
@@ -90,13 +91,19 @@ export interface TParms {
90
91
  loopcnt: number;
91
92
  }
92
93
 
93
- /** Module-level mutable state, mirroring the C static. @see tlayout.c:parms */
94
- export const parms: TParms = {
94
+ /** C's zero-initialized static parms_t. */
95
+ const PARMS_INIT: Readonly<TParms> = {
95
96
  useGrid: 0, useNew: 0, seed: 0, numIters: 0, maxIters: 0, unscaled: 0,
96
97
  C: 0, Tfact: 0, K: 0, T0: 0, smode: 0, Cell: 0, Wd: 0, Ht: 0,
97
98
  pass1: 0, loopcnt: 0,
98
99
  };
99
100
 
101
+ /** Module-level mutable state, mirroring the C static. @see tlayout.c:parms */
102
+ export const parms: TParms = { ...PARMS_INIT };
103
+ // fdpParms is not reset: it is the -L command-line configuration (args.c),
104
+ // and the one field a render writes (K) is set by fdpInitParams before any read.
105
+ registerRenderState(() => { Object.assign(parms, PARMS_INIT); });
106
+
100
107
  /** Linear cooling schedule. @see lib/fdpgen/tlayout.c:cool */
101
108
  export function cool(t: number): number {
102
109
  return parms.T0 * (parms.maxIters - t) / parms.maxIters;
@@ -36,6 +36,7 @@ import { libmHypot } from '../../common/libm-hypot.js';
36
36
  import { fma, fms } from '../../common/fma.js';
37
37
  import { normalizeG } from './normalize.js';
38
38
  import { simpleScale } from '../neato/simple-scale.js';
39
+ import { registerRenderState } from '../../common/render-state.js';
39
40
 
40
41
  /** @see lib/fdpgen/xlayout.c:DFLT_overlap */
41
42
  const DFLT_OVERLAP = '9:prism';
@@ -47,8 +48,8 @@ const PS2INCH = (x: number): number => x / 72;
47
48
  // xParams / X_marg — static state (C statics, persist across calls)
48
49
  // ---------------------------------------------------------------------------
49
50
 
50
- /** @see lib/fdpgen/xlayout.c:xParams */
51
- const xParams: XParams = {
51
+ /** C's static initializer of xParams. @see lib/fdpgen/xlayout.c:xParams */
52
+ const XPARAMS_INIT: Readonly<XParams> = {
52
53
  numIters: 60,
53
54
  T0: 0.0,
54
55
  K: 0.3,
@@ -56,8 +57,15 @@ const xParams: XParams = {
56
57
  loopcnt: 0,
57
58
  };
58
59
 
60
+ /** @see lib/fdpgen/xlayout.c:xParams */
61
+ const xParams: XParams = { ...XPARAMS_INIT };
62
+
59
63
  /** @see lib/fdpgen/xlayout.c:X_marg */
60
64
  let xMarg: ExpandT = { x: 0, y: 0, doAdd: false };
65
+ registerRenderState(() => {
66
+ Object.assign(xParams, XPARAMS_INIT);
67
+ xMarg = { x: 0, y: 0, doAdd: false };
68
+ });
61
69
 
62
70
  /** Overlap/non-overlap repulsion strengths for one try. */
63
71
  interface RepStrength {
@@ -354,14 +354,14 @@ class Cdt {
354
354
 
355
355
  if (this.inCircle(v1, v2, v3, v4) > 0) {
356
356
  const en = this.edgeOrNew(v3, v4);
357
- // f3 ring: (v4, v2, v3) with edges (e5, e2, en); e5 = v4–v2
358
- // f4 ring: (v1, v4, v3) with edges (e4, en, e3); e4 = v1–v4
359
- // (rings derived from C's gts_face_new(en,e5,e2)/(en,e3,e4) cycles,
360
- // matching the vertex triples C passes to the recursive calls)
357
+ // C: f3 = gts_face_new(en, e5, e2), f4 = gts_face_new(en, e3, e4).
358
+ // The stored ring starts at C's t->e1 (en) because gts_triangle_vertices
359
+ // reads (e1-only, e1∩e2, e2-only): f3 → (v3, v4, v2), f4 → (v4, v3, v1).
360
+ // That is the vertex order mkSurface emits and triCenter sums in.
361
361
  this.removeFace(f1);
362
362
  this.removeFace(f2);
363
- const f3 = this.addFace([v4, v2, v3], [e5, e2, en]);
364
- const f4 = this.addFace([v1, v4, v3], [e4, en, e3]);
363
+ const f3 = this.addFace([v3, v4, v2], [en, e5, e2]);
364
+ const f4 = this.addFace([v4, v3, v1], [en, e3, e4]);
365
365
 
366
366
  this.swapIfInCircle(f3, v4, v2, v3, e5, e2, en);
367
367
  this.swapIfInCircle(f4, v1, v4, v3, e4, en, e3);
@@ -9,6 +9,7 @@
9
9
 
10
10
  import type { Graph } from '../../model/graph.js';
11
11
  import type { Edge } from '../../model/edge.js';
12
+ import { agerrLineOn } from '../../common/agerr.js';
12
13
 
13
14
  /** `dfltlen` passed by scan_graph_mode. @see lib/neatogen/stuff.c:scan_graph_mode */
14
15
  export const DEFAULT_EDGE_LEN = 1.0;
@@ -24,7 +25,7 @@ export function lenattr(ep: Edge, g: Graph): number {
24
25
  const val = /^\s*[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?/.exec(s);
25
26
  const len = val === null ? Number.NaN : parseFloat(val[0]);
26
27
  if (Number.isNaN(len) || len < 0 || len === 0) {
27
- console.warn(`bad edge len "${s}" in ${g.name} - setting to ${DEFAULT_EDGE_LEN.toFixed(2)}`);
28
+ agerrLineOn('warn', `bad edge len "${s}" in ${g.name} - setting to ${DEFAULT_EDGE_LEN.toFixed(2)}`);
28
29
  return DEFAULT_EDGE_LEN;
29
30
  }
30
31
  return len;
@@ -26,6 +26,7 @@ import { vpscAdjust } from './vpsc-adjust.js';
26
26
  import { sepFactor } from './sep-factor.js';
27
27
  import { lateDouble } from '../../common/nodeinit.js';
28
28
  import { RenderError } from '../../errors.js';
29
+ import { agerrLineOn } from '../../common/agerr.js';
29
30
 
30
31
  /** @see lib/neatogen/adjust.c:DFLT_MARGIN (points) */
31
32
  const DFLT_MARGIN = 4;
@@ -161,7 +162,7 @@ export function warnUnrecognizedOverlap(flag: string | undefined): void {
161
162
  const s = flag.toLowerCase();
162
163
  if (s.startsWith('prism') || NAMED_MODES.has(s)) return;
163
164
  if (mapBoolDflt(flag, false) === mapBoolDflt(flag, true)) return;
164
- console.warn(`Unrecognized overlap value "${flag}" - using false`);
165
+ agerrLineOn('warn', `Unrecognized overlap value "${flag}" - using false`);
165
166
  }
166
167
 
167
168
  /**
@@ -203,7 +204,7 @@ function adjustLate(g: Graph, am: AdjustData): number {
203
204
  case AM_IPSEP: return 0;
204
205
  case AM_VPSC: return vpscAdjust(g);
205
206
  default:
206
- console.warn(`Unhandled adjust option ${am.print}`);
207
+ agerrLineOn('warn', `Unhandled adjust option ${am.print}`);
207
208
  return 0;
208
209
  }
209
210
  }
@@ -50,6 +50,7 @@ export const MODE_HIER = 2;
50
50
  export const MODE_IPSEP = 3;
51
51
  /** Stochastic gradient descent. @see lib/neatogen/neatoprocs.h */
52
52
  export const MODE_SGD = 4;
53
+ /** `MaxIter` under MODE_SGD. @see neatoinit.c:1285 */ const SGD_ITERATIONS = 30;
53
54
 
54
55
  // ---------------------------------------------------------------------------
55
56
  // Model constants — @see lib/neatogen/defs.h
@@ -321,6 +322,13 @@ export function mapModelToStress(model: number): number {
321
322
  // solveModel helpers
322
323
  // ---------------------------------------------------------------------------
323
324
 
325
+ /** `MaxIter` per mode; `maxiter` goes through C `atoi` (no digits -> 0). @see neatoinit.c:1280-1287 */
326
+ export function neatoMaxIter(maxiter: string | undefined, mode: number, n: number): number {
327
+ if (maxiter !== undefined) return parseInt(maxiter, 10) || 0;
328
+ if (mode === MODE_MAJOR) return DFLT_ITERATIONS;
329
+ return mode === MODE_SGD ? SGD_ITERATIONS : 100 * n;
330
+ }
331
+
324
332
  /** Run stress majorization and write positions back. @see lib/neatogen/neatoinit.c:majorization */
325
333
  export function runMajorization(g: Graph, mode: number, model: number): void {
326
334
  void model; // only MODEL_SHORTPATH distances are exercised by the suite
@@ -329,7 +337,7 @@ export function runMajorization(g: Graph, mode: number, model: number): void {
329
337
  const haveLen = graphHasLen(g);
330
338
  const vtx = makeGraphDataC(g, nodeList, haveLen, mode === MODE_HIER || mode === MODE_IPSEP);
331
339
  checkStart(g); // C: srand48(seed) immediately before initLayout draws
332
- const maxi = lateDouble(g.attrs.get('maxiter'), DFLT_ITERATIONS, 0);
340
+ const maxi = neatoMaxIter(g.attrs.get('maxiter'), mode, n);
333
341
  const epsilon = lateDouble(g.attrs.get('epsilon'), 1e-4, 0); // DFLT_TOLERANCE
334
342
  // C ipsep (stress_majorization_cola, constrained_majorization_ipsep.c:150) always uses
335
343
  // compute_apsp_packed; kD_mkernel uses the weighted APSP iff graph->ewgts (stress.c:855).
@@ -389,11 +397,7 @@ export function makeGraphDataC(
389
397
  return vtx;
390
398
  }
391
399
 
392
- /**
393
- * Dispatch to the appropriate layout solver for the given mode.
394
- *
395
- * @see lib/neatogen/neatoinit.c:neatoLayout
396
- */
400
+ /** Dispatch to the layout solver for the mode. @see lib/neatogen/neatoinit.c:neatoLayout */
397
401
  export function solveModel(g: Graph, mode: number, model: number): void {
398
402
  if (g.nodes.size < 2) return;
399
403
  assertSupported(g, mode, model);
@@ -411,11 +415,7 @@ export function solveModel(g: Graph, mode: number, model: number): void {
411
415
  // neatoTranslate helpers
412
416
  // ---------------------------------------------------------------------------
413
417
 
414
- /**
415
- * Find minimum X and Y across all node positions.
416
- *
417
- * @see lib/neatogen/neatoinit.c (translate logic)
418
- */
418
+ /** Minimum X and Y across all node positions. @see lib/neatogen/neatoinit.c (translate logic) */
419
419
  export function findMinPos(g: Graph): { minX: number; minY: number } {
420
420
  let minX = Infinity;
421
421
  let minY = Infinity;
@@ -27,6 +27,7 @@ import { MODEL_MDS } from './init.js';
27
27
  import { solve } from './kk-solve.js';
28
28
  import { lenattr } from './edge-len.js';
29
29
  import { newPathState, shortestPath, mdsModel, type Neighbour } from './kk-paths.js';
30
+ import { agerrLineOn } from '../../common/agerr.js';
30
31
 
31
32
  /** `Ndim`: layout dimension (this port lays out in 2-D only). */
32
33
  const NDIM = 2;
@@ -289,7 +290,7 @@ function moveNode(st: KkState, m: number): void {
289
290
  function solveModel(g: Graph, st: KkState): void {
290
291
  for (let m = chooseNode(st); m >= 0; m = chooseNode(st)) moveNode(st, m);
291
292
  if (st.move === st.maxIter) {
292
- console.warn(`Max. iterations (${st.maxIter}) reached on graph ${g.name}`);
293
+ agerrLineOn('warn', `Max. iterations (${st.maxIter}) reached on graph ${g.name}`);
293
294
  }
294
295
  }
295
296
 
@@ -19,6 +19,7 @@
19
19
  import type { Point, Poly } from '../../pathplan/types.js';
20
20
  import { wind } from '../../pathplan/visibility.js';
21
21
  import { mkSurface } from './cdt-surface.js';
22
+ import { dist2 } from '../../common/spline-midpoint.js';
22
23
  import { InternalError } from '../../errors.js';
23
24
 
24
25
  /** index pair. @see multispline.c:ipair */
@@ -87,9 +88,8 @@ function sharedEdge(faces: number[], p3: number, q3: number): Ipair {
87
88
 
88
89
  /** @see multispline.c:addTriEdge */
89
90
  export function addTriEdge(g: Tgraph, t: number, h: number, seg: Ipair): void {
90
- const dist = Math.hypot(
91
- g.nodes[t]!.ctr.x - g.nodes[h]!.ctr.x,
92
- g.nodes[t]!.ctr.y - g.nodes[h]!.ctr.y);
91
+ /* DIST = sqrt(fused DIST2): libgvplugin_neato_layout _addTriEdge fmul/fmadd/fsqrt */
92
+ const dist = Math.sqrt(dist2(g.nodes[t]!.ctr, g.nodes[h]!.ctr));
93
93
  const idx = g.edges.length;
94
94
  g.edges.push({ t, h, seg, dist });
95
95
  g.nodes[t]!.edges.push(idx);
@@ -25,6 +25,7 @@ import { clipAndInstall } from '../../common/splines-clip.js';
25
25
  import { makeStraightEdges, addEdgeLabels } from '../dot/straight-edges.js';
26
26
  import type { Router, Ipair } from './multispline-router.js';
27
27
  import { addEndpoint, edgeToSeg, triPath } from './multispline-router.js';
28
+ import { agwarningf } from '../../common/agerr.js';
28
29
 
29
30
  export type { Router } from './multispline-router.js';
30
31
  export { mkRouter } from './multispline-router.js';
@@ -339,8 +340,8 @@ function chainOf(e: Edge): Edge[] {
339
340
  * @see multispline.c:genroute
340
341
  */
341
342
  function warnNoControlPoints(e: Edge): number {
342
- console.warn(
343
- `Warning: Could not create control points for multiple spline for edge (${e.tail.name},${e.head.name})`,
343
+ agwarningf(
344
+ `Could not create control points for multiple spline for edge (${e.tail.name},${e.head.name})`,
344
345
  );
345
346
  return 1;
346
347
  }
@@ -19,6 +19,7 @@ import type { Node } from '../../model/node.js';
19
19
  import type { Box, Point } from '../../model/geom.js';
20
20
  import { ShapeKind } from '../../common/types.js';
21
21
  import type { PolygonT } from '../../common/types.js';
22
+ import { agerrorf } from '../../common/agerr.js';
22
23
 
23
24
  /** @see lib/neatogen/poly.c:BOX */
24
25
  export const POLY_BOX = 1;
@@ -258,7 +259,7 @@ export function makeAddPoly(n: Node, xmargin: number, ymargin: number): Poly | n
258
259
  case ShapeKind.SH_POINT:
259
260
  return finishPoly(genRound(n, xmargin, ymargin), POLY_CIRCLE);
260
261
  default:
261
- console.error(`Error: makeAddPoly: unknown shape type ${shapeName(n)}\n`);
262
+ agerrorf(`makeAddPoly: unknown shape type ${shapeName(n)}\n`);
262
263
  return null;
263
264
  }
264
265
  }
@@ -296,7 +297,7 @@ function baseShape(n: Node): { verts: Point[]; kind: number } | null {
296
297
  case ShapeKind.SH_POINT:
297
298
  return { verts: genRound(n, 0, 0), kind: POLY_CIRCLE };
298
299
  default:
299
- console.error(`Error: makePoly: unknown shape type ${shapeName(n)}\n`);
300
+ agerrorf(`makePoly: unknown shape type ${shapeName(n)}\n`);
300
301
  return null;
301
302
  }
302
303
  }
@@ -17,6 +17,7 @@ import type { RkState } from '../../util/mt19937.js';
17
17
  import { drand48 } from '../../common/random.js';
18
18
  import { lateDouble } from '../../common/nodeinit.js';
19
19
  import { checkStart, parseStart, INIT_RANDOM, INIT_REGULAR } from './start.js';
20
+ import { agerrLineOn } from '../../common/agerr.js';
20
21
 
21
22
  // ---------------------------------------------------------------------------
22
23
  // Model constants — @see lib/neatogen/defs.h
@@ -407,13 +408,15 @@ export function buildUnfixed(g: Graph, n: number): boolean[] {
407
408
  */
408
409
  export function resolveModel(model: number): number {
409
410
  if (model === MODEL_CIRCUIT) {
410
- console.warn(
411
+ agerrLineOn(
412
+ 'warn',
411
413
  'circuit model not yet supported in Gmode=sgd, reverting to shortpath model',
412
414
  );
413
415
  return MODEL_SHORTPATH;
414
416
  }
415
417
  if (model === MODEL_MDS) {
416
- console.warn(
418
+ agerrLineOn(
419
+ 'warn',
417
420
  'mds model not yet supported in Gmode=sgd, reverting to shortpath model',
418
421
  );
419
422
  return MODEL_SHORTPATH;