@knowvah/dot-engine 2.7.1 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/dist/api.js.map +2 -2
  2. package/dist/common/agerr.d.ts +8 -1
  3. package/dist/common/agerr.d.ts.map +1 -1
  4. package/dist/common/fma.d.ts.map +1 -1
  5. package/dist/common/htmltable-emit-fill.d.ts.map +1 -1
  6. package/dist/common/htmltable-label.d.ts +3 -0
  7. package/dist/common/htmltable-label.d.ts.map +1 -1
  8. package/dist/common/htmltable-lex-attvalue.d.ts.map +1 -1
  9. package/dist/common/htmltable.d.ts +5 -0
  10. package/dist/common/htmltable.d.ts.map +1 -1
  11. package/dist/common/layers.d.ts +12 -0
  12. package/dist/common/layers.d.ts.map +1 -1
  13. package/dist/common/layout-state.d.ts +8 -0
  14. package/dist/common/layout-state.d.ts.map +1 -0
  15. package/dist/common/make-label.d.ts.map +1 -1
  16. package/dist/common/multicolor.d.ts +6 -3
  17. package/dist/common/multicolor.d.ts.map +1 -1
  18. package/dist/common/nodeinit.d.ts.map +1 -1
  19. package/dist/common/poly-init.d.ts +11 -0
  20. package/dist/common/poly-init.d.ts.map +1 -1
  21. package/dist/common/poly-sizing.d.ts +1 -0
  22. package/dist/common/poly-sizing.d.ts.map +1 -1
  23. package/dist/common/postproc.d.ts.map +1 -1
  24. package/dist/common/style-resolve.d.ts +11 -9
  25. package/dist/common/style-resolve.d.ts.map +1 -1
  26. package/dist/gvc/device-cluster.d.ts +5 -6
  27. package/dist/gvc/device-cluster.d.ts.map +1 -1
  28. package/dist/gvc/device.d.ts.map +1 -1
  29. package/dist/gvc/first-layer.d.ts +21 -5
  30. package/dist/gvc/first-layer.d.ts.map +1 -1
  31. package/dist/gvc/gvrender-set-style.d.ts.map +1 -1
  32. package/dist/gvc/usershape.d.ts +20 -0
  33. package/dist/gvc/usershape.d.ts.map +1 -1
  34. package/dist/index.js +494 -275
  35. package/dist/index.js.map +4 -4
  36. package/dist/layout/circo/index.d.ts.map +1 -1
  37. package/dist/layout/dot/init.d.ts +10 -0
  38. package/dist/layout/dot/init.d.ts.map +1 -1
  39. package/dist/layout/dot/splines.d.ts.map +1 -1
  40. package/dist/layout/fdp/init.d.ts.map +1 -1
  41. package/dist/layout/neato/conjgrad-mkernel.d.ts.map +1 -1
  42. package/dist/layout/neato/constrained-majorization.d.ts.map +1 -1
  43. package/dist/layout/neato/edge-len.d.ts +6 -2
  44. package/dist/layout/neato/edge-len.d.ts.map +1 -1
  45. package/dist/layout/neato/init.d.ts.map +1 -1
  46. package/dist/layout/neato/kk.d.ts.map +1 -1
  47. package/dist/layout/neato/make-graph-data.d.ts.map +1 -1
  48. package/dist/layout/neato/matrix-ops.d.ts +0 -6
  49. package/dist/layout/neato/matrix-ops.d.ts.map +1 -1
  50. package/dist/layout/neato/smart-ini-x.d.ts.map +1 -1
  51. package/dist/layout/neato/splines.d.ts.map +1 -1
  52. package/dist/layout/neato/start.d.ts.map +1 -1
  53. package/dist/layout/neato/stress-kernel.d.ts +2 -5
  54. package/dist/layout/neato/stress-kernel.d.ts.map +1 -1
  55. package/dist/layout/neato/stress.d.ts +6 -0
  56. package/dist/layout/neato/stress.d.ts.map +1 -1
  57. package/dist/layout/osage/index.d.ts.map +1 -1
  58. package/dist/layout/patchwork/index.d.ts.map +1 -1
  59. package/dist/layout/sfdp/init.d.ts.map +1 -1
  60. package/dist/layout/twopi/init.d.ts +12 -6
  61. package/dist/layout/twopi/init.d.ts.map +1 -1
  62. package/dist/model/cgraph-ops.d.ts +9 -0
  63. package/dist/model/cgraph-ops.d.ts.map +1 -1
  64. package/dist/render/color-resolve.d.ts +7 -0
  65. package/dist/render/color-resolve.d.ts.map +1 -1
  66. package/dist/render/json.d.ts +17 -2
  67. package/dist/render/json.d.ts.map +1 -1
  68. package/dist/render/map.d.ts +3 -5
  69. package/dist/render/map.d.ts.map +1 -1
  70. package/dist/render.js +500 -281
  71. package/dist/render.js.map +4 -4
  72. package/package.json +1 -1
  73. package/src/common/agerr.ts +69 -8
  74. package/src/common/fma.ts +11 -1
  75. package/src/common/htmltable-emit-fill.ts +11 -7
  76. package/src/common/htmltable-label.ts +6 -3
  77. package/src/common/htmltable-lex-attvalue.ts +17 -1
  78. package/src/common/htmltable.ts +10 -5
  79. package/src/common/layers.ts +31 -2
  80. package/src/common/layout-state.ts +35 -0
  81. package/src/common/make-label.ts +6 -2
  82. package/src/common/multicolor.ts +12 -6
  83. package/src/common/nodeinit.ts +2 -1
  84. package/src/common/poly-init.ts +18 -1
  85. package/src/common/poly-sizing.ts +4 -4
  86. package/src/common/postproc.ts +4 -1
  87. package/src/common/style-resolve.ts +118 -50
  88. package/src/gvc/device-cluster.ts +31 -39
  89. package/src/gvc/device.ts +55 -43
  90. package/src/gvc/first-layer.ts +60 -11
  91. package/src/gvc/gvrender-set-style.ts +1 -6
  92. package/src/gvc/usershape.ts +40 -0
  93. package/src/layout/circo/index.ts +4 -0
  94. package/src/layout/dot/init.ts +32 -0
  95. package/src/layout/dot/splines.ts +3 -0
  96. package/src/layout/fdp/init.ts +17 -0
  97. package/src/layout/neato/conjgrad-mkernel.ts +2 -3
  98. package/src/layout/neato/constrained-majorization.ts +2 -1
  99. package/src/layout/neato/edge-len.ts +39 -7
  100. package/src/layout/neato/init.ts +8 -3
  101. package/src/layout/neato/kk.ts +2 -1
  102. package/src/layout/neato/make-graph-data.ts +1 -2
  103. package/src/layout/neato/matrix-ops.ts +1 -11
  104. package/src/layout/neato/smart-ini-x.ts +1 -1
  105. package/src/layout/neato/splines.ts +3 -1
  106. package/src/layout/neato/start.ts +3 -2
  107. package/src/layout/neato/stress-kernel.ts +45 -86
  108. package/src/layout/neato/stress.ts +19 -0
  109. package/src/layout/osage/index.ts +8 -1
  110. package/src/layout/patchwork/index.ts +4 -0
  111. package/src/layout/sfdp/init.ts +3 -0
  112. package/src/layout/twopi/init.ts +21 -9
  113. package/src/layout/twopi/pipeline.ts +3 -3
  114. package/src/model/cgraph-ops.ts +19 -0
  115. package/src/render/color-resolve.ts +17 -2
  116. package/src/render/json.ts +9 -10
  117. package/src/render/map.ts +5 -6
@@ -19,31 +19,24 @@ import type { Node } from '../../model/node.js';
19
19
  import { drand48 } from '../../common/random.js';
20
20
  import { bfs } from './bfs.js';
21
21
  import { dijkstra, type VtxData } from './dijkstra.js';
22
+ import {
23
+ conjugateGradientMkernelFused, rightMultWithVectorFFFused, type CgMkernelOptions,
24
+ } from './conjgrad-mkernel.js';
22
25
  import {
23
26
  copyVectorF,
24
27
  invertSqrtVec,
25
28
  invertVec,
26
- maxAbsF,
27
29
  orthog1,
28
- orthog1f,
29
30
  rightMultWithVectorFF,
30
31
  sqrtVecF,
31
32
  squareVec,
32
33
  vectorsInnerProductF,
33
- vectorsMultAdditionF,
34
- vectorsSubtractionF,
35
34
  } from './matrix-ops.js';
35
+ import { fmaf } from './quad-prog-solve.js';
36
+ import { OPT_EXP_FLAG, TOLERANCE_CG } from './stress.js';
36
37
 
37
38
  const fr = Math.fround;
38
39
 
39
- /** @see lib/neatogen/stress.h:tolerance_cg */
40
- const TOLERANCE_CG = 1e-3;
41
-
42
- /** Stress options bit: weight exponent (1 or 2). @see lib/neatogen/stress.h */
43
- export const OPT_EXP_FLAG = 2;
44
- /** Stress options bit: smart initialisation. @see lib/neatogen/stress.h */
45
- export const OPT_SMART_INIT = 1;
46
-
47
40
  /**
48
41
  * Packed all-pairs shortest paths over unit weights (BFS hops).
49
42
  * @see lib/neatogen/stress.c:compute_apsp_packed
@@ -115,71 +108,32 @@ export function initLayout(
115
108
  return pinned;
116
109
  }
117
110
 
118
- /**
119
- * Conjugate gradients on a packed symmetric float matrix.
120
- * Returns negative on error.
121
- * @see lib/neatogen/conjgrad.c:conjugate_gradient_mkernel
122
- */
123
- /** Mutable CG iteration state. */
124
- interface CgState {
125
- A: Float32Array;
126
- x: Float32Array;
127
- b: Float32Array;
128
- n: number;
129
- r: Float32Array;
130
- p: Float32Array;
131
- Ap: Float32Array;
132
- rR: number;
133
- }
134
-
135
- /** One CG step; returns false on a zero-length residual error. */
136
- function cgStep(st: CgState, last: boolean): boolean {
137
- const { n } = st;
138
- orthog1f(n, st.p);
139
- orthog1f(n, st.x);
140
- orthog1f(n, st.r);
141
- rightMultWithVectorFF(st.A, n, st.p, st.Ap);
142
- orthog1f(n, st.Ap);
143
- const pAp = vectorsInnerProductF(n, st.p, st.Ap);
144
- if (pAp === 0) return true; // C: break
145
- const alpha = st.rR / pAp;
146
- vectorsMultAdditionF(n, st.x, alpha, st.p);
147
- if (!last) {
148
- vectorsMultAdditionF(n, st.r, -alpha, st.Ap);
149
- const rRNew = vectorsInnerProductF(n, st.r, st.r);
150
- if (st.rR === 0) return false;
151
- const betaF = fr(rRNew / st.rR);
152
- st.rR = rRNew;
153
- for (let j = 0; j < n; j++) st.p[j] = fr(betaF * st.p[j]!) + st.r[j]!;
154
- }
155
- return true;
111
+ /** The right-multiply and conjugate-gradient kernels one caller runs. */
112
+ interface KernelOps {
113
+ /** `acc + tmp*tmp`, the distance accumulation of the Laplacian build. */
114
+ squareAdd(tmp: number, acc: number): number;
115
+ rightMult(m: Float32Array, n: number, vector: Float32Array, result: Float32Array): void;
116
+ /** Returns nonzero on an unexpected zero residual. */
117
+ cg(A: Float32Array, x: Float32Array, b: Float32Array, n: number, o: CgMkernelOptions): number;
156
118
  }
157
119
 
158
- export function conjugateGradientMkernel(
159
- A: Float32Array, x: Float32Array, b: Float32Array, n: number,
160
- tol: number, maxIterations: number,
161
- ): number {
162
- const st: CgState = {
163
- A, x, b, n,
164
- r: new Float32Array(n),
165
- p: new Float32Array(n),
166
- Ap: new Float32Array(n),
167
- rR: 0,
168
- };
169
- orthog1f(n, x);
170
- orthog1f(n, b);
171
- const Ax = new Float32Array(n);
172
- rightMultWithVectorFF(A, n, x, Ax);
173
- orthog1f(n, Ax);
174
- vectorsSubtractionF(n, b, Ax, st.r);
175
- copyVectorF(n, st.r, st.p);
176
- st.rR = vectorsInnerProductF(n, st.r, st.r);
177
-
178
- for (let i = 0; i < maxIterations && maxAbsF(n, st.r) > tol; i++) {
179
- if (!cgStep(st, i >= maxIterations - 1)) return -1;
180
- }
181
- return 0;
182
- }
120
+ /** The oracle's fused kernels, as `stress_majorization_kD_mkernel` runs them. */
121
+ const FUSED_OPS: KernelOps = {
122
+ squareAdd: (tmp, acc) => fmaf(tmp, tmp, acc),
123
+ rightMult: rightMultWithVectorFFFused, cg: conjugateGradientMkernelFused,
124
+ };
125
+ /**
126
+ * The kernels as `stress_majorization_cola` (mode=ipsep) runs them: its squared
127
+ * distances are `square_vec` then `vectors_additionf` (two loops, so the product
128
+ * is rounded before the add), its inlined `right_mult_with_vector_ff` copy is not
129
+ * read from the disassembly (kept unfused), and its conjugate gradient is the same
130
+ * out-of-line `conjugate_gradient_mkernel` the kD kernel calls.
131
+ * @see lib/neatogen/constrained_majorization_ipsep.c:stress_majorization_cola
132
+ */
133
+ const COLA_OPS: KernelOps = {
134
+ squareAdd: (tmp, acc) => acc + fr(tmp * tmp),
135
+ rightMult: rightMultWithVectorFF, cg: conjugateGradientMkernelFused,
136
+ };
183
137
 
184
138
  /** Laplacian off-diagonal prep + diagonal degrees. @see stress.c (Laplacian computation) */
185
139
  function buildLap2(lap2: Float32Array, n: number, exp: number): void {
@@ -207,9 +161,9 @@ function buildLap2(lap2: Float32Array, n: number, exp: number): void {
207
161
  /** Per-iteration Laplacian of 1/(d_ij*|p_i-p_j|). @see stress.c (main loop head) */
208
162
  function buildLap1(
209
163
  lap1: Float32Array, lap2: Float32Array, coords: Float32Array[],
210
- shape: { n: number; dim: number; exp: number },
164
+ shape: { n: number; dim: number; exp: number; ops: KernelOps },
211
165
  ): void {
212
- const { n, dim, exp } = shape;
166
+ const { n, dim, exp, ops } = shape;
213
167
  const lapLength = (n * (n + 1)) / 2;
214
168
  const degrees = new Float64Array(n);
215
169
  const distAccumulator = new Float32Array(n);
@@ -222,7 +176,7 @@ function buildLap1(
222
176
  const ck = coords[k]!;
223
177
  for (let x = 0; x < len; x++) {
224
178
  const tmp = fr(ck[i]! + fr(-1 * ck[i + 1 + x]!));
225
- distAccumulator[x] = distAccumulator[x]! + fr(tmp * tmp);
179
+ distAccumulator[x] = ops.squareAdd(tmp, distAccumulator[x]!);
226
180
  }
227
181
  }
228
182
  invertSqrtVec(len, distAccumulator);
@@ -264,32 +218,34 @@ interface KernelState {
264
218
  b: Float32Array[];
265
219
  tmpCoords: Float32Array;
266
220
  constantTerm: number;
221
+ ops: KernelOps;
267
222
  }
268
223
 
269
224
  /** One stress evaluation. @see stress.c (compute new stress block) */
270
225
  function computeStress(st: KernelState): number {
271
226
  let stress = 0;
272
227
  for (let k = 0; k < st.dim; k++) {
273
- rightMultWithVectorFF(st.lap1, st.n, st.coords[k]!, st.b[k]!);
228
+ st.ops.rightMult(st.lap1, st.n, st.coords[k]!, st.b[k]!);
274
229
  stress += vectorsInnerProductF(st.n, st.coords[k]!, st.b[k]!);
275
230
  }
276
231
  stress *= 2;
277
232
  stress += st.constantTerm;
278
233
  for (let k = 0; k < st.dim; k++) {
279
- rightMultWithVectorFF(st.lap2, st.n, st.coords[k]!, st.tmpCoords);
234
+ st.ops.rightMult(st.lap2, st.n, st.coords[k]!, st.tmpCoords);
280
235
  stress -= vectorsInnerProductF(st.n, st.coords[k]!, st.tmpCoords);
281
236
  }
282
237
  return stress;
283
238
  }
284
239
 
285
- /** Solve one axis, respecting pinned nodes. @see stress.c (CG block) */
240
+ /** Solve one axis, respecting pinned nodes; nonzero on a CG error. @see stress.c (CG block) */
286
241
  function solveAxis(st: KernelState, k: number, havePinned: boolean, nodes: Node[]): number {
242
+ const cg = { tol: TOLERANCE_CG, maxIterations: st.n };
287
243
  if (!havePinned) {
288
- return conjugateGradientMkernel(st.lap2, st.coords[k]!, st.b[k]!, st.n, TOLERANCE_CG, st.n);
244
+ return st.ops.cg(st.lap2, st.coords[k]!, st.b[k]!, st.n, cg);
289
245
  }
290
246
  copyVectorF(st.n, st.coords[k]!, st.tmpCoords);
291
- const rc = conjugateGradientMkernel(st.lap2, st.tmpCoords, st.b[k]!, st.n, TOLERANCE_CG, st.n);
292
- if (rc < 0) return rc;
247
+ const rc = st.ops.cg(st.lap2, st.tmpCoords, st.b[k]!, st.n, cg);
248
+ if (rc !== 0) return rc;
293
249
  for (let i = 0; i < st.n; i++) {
294
250
  if (!isFixed(nodes[i]!)) st.coords[k]![i] = st.tmpCoords[i]!;
295
251
  }
@@ -304,6 +260,8 @@ export interface StressOpts {
304
260
  /** MODEL_* — only the resulting Dij matters here; pass it in. */
305
261
  maxi: number;
306
262
  epsilon: number;
263
+ /** Run the kernels as `stress_majorization_cola` does (mode=ipsep). */
264
+ cola?: boolean;
307
265
  }
308
266
 
309
267
  /**
@@ -338,20 +296,21 @@ export function stressMajorizationKDMkernel(
338
296
  b: Array.from({ length: dim }, () => new Float32Array(n)),
339
297
  tmpCoords: new Float32Array(n),
340
298
  constantTerm,
299
+ ops: o.cola === true ? COLA_OPS : FUSED_OPS,
341
300
  };
342
301
 
343
302
  let oldStress = Number.MAX_VALUE;
344
303
  let converged = false;
345
304
  let iterations = 0;
346
305
  for (; iterations < maxi && !converged; iterations++) {
347
- buildLap1(st.lap1, lap2, coords, { n, dim, exp });
306
+ buildLap1(st.lap1, lap2, coords, { n, dim, exp, ops: st.ops });
348
307
  const newStress = computeStress(st);
349
308
  if (typeof process !== 'undefined' && process.env['STRESS_DEBUG'] && iterations % 5 === 0) console.error(newStress.toFixed(3));
350
309
  const change = Math.abs(oldStress - newStress);
351
310
  converged = change / oldStress < o.epsilon || newStress < o.epsilon;
352
311
  oldStress = newStress;
353
312
  for (let k = 0; k < dim; k++) {
354
- if (solveAxis(st, k, havePinned, nodes) < 0) return -1;
313
+ if (solveAxis(st, k, havePinned, nodes) !== 0) return -1;
355
314
  }
356
315
  }
357
316
 
@@ -14,6 +14,9 @@
14
14
  * @see lib/neatogen/stress.h
15
15
  */
16
16
 
17
+ import { agwarningf } from '../../common/agerr.js';
18
+ import { lateInt } from '../../common/nodeinit.js';
19
+ import type { Graph } from '../../model/graph.js';
17
20
  import type { VtxData } from './dijkstra.js';
18
21
  import {
19
22
  computeApspPacked,
@@ -41,6 +44,22 @@ export const OPT_SMART_INIT = 0x4;
41
44
  /** Exponent flag in opts bitmask (exp=2 when set, exp=1 when clear). */
42
45
  export const OPT_EXP_FLAG = 0x3;
43
46
 
47
+ /** Name of the stress weight-exponent graph attribute. @see lib/neatogen/neatoinit.c:exp_name */
48
+ const EXP_NAME = 'stresswt';
49
+
50
+ /**
51
+ * The `stresswt` exponent, 1 or 2 (default 2).
52
+ * @see lib/neatogen/neatoinit.c:checkExp
53
+ */
54
+ export function checkExp(g: Graph): number {
55
+ const exp = lateInt(g.attrs.get(EXP_NAME), 2, 0);
56
+ if (exp === 0 || exp > 2) {
57
+ agwarningf(`${EXP_NAME} attribute value must be 1 or 2 - ignoring`);
58
+ return 2;
59
+ }
60
+ return exp;
61
+ }
62
+
44
63
  /** Model: MDS (user-supplied edge lengths). */
45
64
  export const MODEL_MDS = 1;
46
65
 
@@ -11,6 +11,7 @@
11
11
  */
12
12
 
13
13
  import type { Graph } from '../../model/graph.js';
14
+ import { gvCleanupEdgeLabels, gvCleanupNodeLabels } from '../dot/init.js';
14
15
  import type { Node } from '../../model/node.js';
15
16
  import type { Box } from '../../model/geom.js';
16
17
  import type { LayoutEngine } from '../../gvc/context.js';
@@ -507,7 +508,11 @@ export function osageLayout(g: Graph): void {
507
508
  /** Recursively clear cluster arrays from g and all subclusters. */
508
509
  export function cleanupGraphs(g: Graph): void {
509
510
  const nClust = g.info.n_cluster ?? 0;
510
- for (let i = 1; i <= nClust; i++) cleanupGraphs(g.info.clust![i - 1]);
511
+ for (let i = 1; i <= nClust; i++) {
512
+ const subg = g.info.clust![i - 1];
513
+ subg.info.label = undefined; // free_label(GD_label(subg))
514
+ cleanupGraphs(subg);
515
+ }
511
516
  g.info.clust = undefined;
512
517
  g.info.n_cluster = undefined;
513
518
  }
@@ -525,6 +530,8 @@ export function cleanupGraphs(g: Graph): void {
525
530
  * @see lib/osage/osageinit.c:osage_cleanup
526
531
  */
527
532
  export function osageCleanup(g: Graph): void {
533
+ for (const e of g.edges) gvCleanupEdgeLabels(e); // gv_cleanup_edge: per-node agfstout, all edges of root g
534
+ for (const n of g.nodes.values()) gvCleanupNodeLabels(n); // gv_cleanup_node
528
535
  for (const n of g.nodes.values()) {
529
536
  if (n.info.alg?.kind === 'osage') n.info.alg = undefined;
530
537
  }
@@ -15,6 +15,7 @@
15
15
  */
16
16
 
17
17
  import type { Graph } from '../../model/graph.js';
18
+ import { gvCleanupEdgeLabels, gvCleanupNodeLabels } from '../dot/init.js';
18
19
  import type { Node } from '../../model/node.js';
19
20
  import type { LayoutEngine } from '../../gvc/context.js';
20
21
  import { ps2inch } from '../pack/index.js';
@@ -425,6 +426,9 @@ export function patchworkEngineLayout(g: Graph): void {
425
426
 
426
427
  /** @see lib/patchwork/patchworkinit.c:patchwork_cleanup */
427
428
  export function patchworkEngineCleanup(g: Graph): void {
429
+ if (g.nodes.size === 0) return; // C: if (!n) return
430
+ for (const e of g.edges) gvCleanupEdgeLabels(e); // gv_cleanup_edge: per-node agfstout, all edges of root g
431
+ for (const n of g.nodes.values()) gvCleanupNodeLabels(n); // gv_cleanup_node
428
432
  g.info.clust = undefined;
429
433
  g.info.n_cluster = undefined;
430
434
  for (const n of g.nodes.values()) { n.info.alg = undefined; }
@@ -12,6 +12,7 @@
12
12
 
13
13
  import { RenderError } from '../../errors.js';
14
14
  import type { Graph } from '../../model/graph.js';
15
+ import { gvCleanupEdgeLabels, gvCleanupNodeLabels } from '../dot/init.js';
15
16
  import { setEdgeTypeFromAttr } from '../dot/index.js';
16
17
  import { EDGETYPE_LINE } from '../neato/splines.js';
17
18
  import { neatoInitNode } from '../neato/init.js';
@@ -264,6 +265,8 @@ export function getPos(g: Graph, dim: number): number[] {
264
265
  * @see lib/sfdpgen/sfdpinit.c:sfdp_cleanup
265
266
  */
266
267
  export function sfdpCleanup(g: Graph): void {
268
+ for (const e of g.edges) gvCleanupEdgeLabels(e); // gv_cleanup_edge: per-node agfstout, all edges of root g
269
+ for (const n of g.nodes.values()) gvCleanupNodeLabels(n); // gv_cleanup_node
267
270
  for (const n of g.nodes.values()) {
268
271
  n.info.pos = undefined;
269
272
  n.info.id = undefined;
@@ -10,6 +10,7 @@
10
10
 
11
11
  import type { ShapeDesc } from '../../common/types.js';
12
12
  import type { Graph } from '../../model/graph.js';
13
+ import { gvCleanupEdgeLabels, gvCleanupNodeLabels } from '../dot/init.js';
13
14
  import type { Node } from '../../model/node.js';
14
15
  import type { Edge } from '../../model/edge.js';
15
16
  import type { TwopiAlgData } from '../../model/nodeInfo.js';
@@ -157,23 +158,34 @@ export function adjustNodes(g: Graph): void {
157
158
  }
158
159
 
159
160
  /**
160
- * Release twopi-specific per-node state after layout.
161
+ * Free ND_alg of the first node, as twopi_layout does before spline routing.
161
162
  *
162
- * Mirrors the C twopi_layout cleanup: free(ND_alg(first_node)) / ND_alg = NULL
163
- * on the FIRST node only (the contiguous block was allocated as a single unit).
164
- * Other nodes' ND_alg pointers were interior offsets into that block so they
165
- * do not need to be freed separately.
163
+ * Mirrors the C twopi_layout: free(ND_alg(n)) / ND_alg(n) = NULL on the FIRST
164
+ * node only (the contiguous block was allocated as a single unit). Other
165
+ * nodes' ND_alg pointers were interior offsets into that block so they do
166
+ * not need to be freed separately.
166
167
  *
167
168
  * ORDERING: must be called BEFORE splineEdges because edge routing may
168
169
  * store its own data in n.info.alg.
169
170
  *
170
- * @see lib/twopigen/twopiinit.c:twopi_layout (free(ND_alg) placement note)
171
- * @see lib/twopigen/twopiinit.c:twopi_cleanup
171
+ * @see lib/twopigen/twopiinit.c:twopi_layout (lines 113-115)
172
172
  */
173
- export function twopiCleanup(g: Graph): void {
173
+ export function twopiFreeAlg(g: Graph): void {
174
174
  const first = g.nodes.values().next().value as Node | undefined;
175
175
  if (first !== undefined && first.info.alg?.kind === 'twopi') {
176
176
  first.info.alg = undefined;
177
177
  }
178
- g.info.neato_nlist = undefined;
178
+ }
179
+
180
+ /**
181
+ * Release twopi layout state after rendering (the engine's `cleanup`).
182
+ * ND_alg is freed earlier, in twopi_layout (see twopiFreeAlg).
183
+ *
184
+ * @see lib/twopigen/twopiinit.c:twopi_cleanup
185
+ */
186
+ export function twopiCleanup(g: Graph): void {
187
+ if (g.nodes.size === 0) return; // C: empty graph
188
+ for (const e of g.edges) gvCleanupEdgeLabels(e); // gv_cleanup_edge
189
+ for (const n of g.nodes.values()) gvCleanupNodeLabels(n); // gv_cleanup_node
190
+ g.info.neato_nlist = undefined; // twopi_cleanup_graph
179
191
  }
@@ -11,7 +11,7 @@ import type { Graph } from '../../model/graph.js';
11
11
  import type { Node } from '../../model/node.js';
12
12
  import type { PackInfo } from '../pack/index.js';
13
13
  import {
14
- twopiCleanup,
14
+ twopiFreeAlg,
15
15
  findRootNode,
16
16
  adjustNodes,
17
17
  finaliseCoords,
@@ -71,7 +71,7 @@ export function layoutSingle(g: Graph, comps: Graph[], globalRoot: Center, setRo
71
71
  if (setRoot && globalRoot === null && center !== null) {
72
72
  g.attrs.set('root', center.name);
73
73
  }
74
- twopiCleanup(g); // ORDERING: before spline routing
74
+ twopiFreeAlg(g); // ORDERING: before spline routing
75
75
  // C: spline_edges(g) — shifts pos to the origin, syncs coord, routes.
76
76
  splineEdgesShifted(g);
77
77
  finaliseCoords(g);
@@ -90,7 +90,7 @@ export function layoutMulti(g: Graph, comps: Graph[], globalRoot: Center, pinfo:
90
90
  layoutComponent(g, sg, globalRoot);
91
91
  finaliseCoords(sg);
92
92
  }
93
- twopiCleanup(g); // ORDERING: before spline routing
93
+ twopiFreeAlg(g); // ORDERING: before spline routing
94
94
  packSubgraphs(comps.length, comps, g, pinfo);
95
95
  // C: spline_edges(g) — shifts pos to the origin, syncs coord, routes.
96
96
  splineEdgesShifted(g);
@@ -26,6 +26,25 @@ import { Node } from './node.js';
26
26
  */
27
27
  const LOCALNAMEPREFIX = '%';
28
28
 
29
+ /**
30
+ * cgraph's id for the first anonymous object, which an anonymous root graph is
31
+ * (agopen maps its NULL name first; idmap hands out odd ids from 1).
32
+ * @see lib/cgraph/id.c:idmap
33
+ */
34
+ const ANONYMOUS_ROOT_ID = 1;
35
+
36
+ /**
37
+ * Graph name as `agnameof` returns it: the DOT name, or `%<id>` for an
38
+ * anonymous graph. The port stores `''` for an anonymous root (dot/xdot
39
+ * re-serialization must not print the `%` name), so it is restored here.
40
+ * Anonymous subgraph ids are not tracked; callers pass the root, as C's
41
+ * neato message sites do.
42
+ * @see lib/cgraph/id.c:agnameof
43
+ */
44
+ export function agNameOf(g: Graph): string {
45
+ return g.anonymous ? `${LOCALNAMEPREFIX}${ANONYMOUS_ROOT_ID}` : g.name;
46
+ }
47
+
29
48
  /**
30
49
  * Allocate a fresh node id unique within `root.nodes`. Mirrors C's
31
50
  * agnextseq/agmapnametoid id reservation: at layout time the per-parse
@@ -27,7 +27,7 @@ import type { Edge } from '../model/edge.js';
27
27
  import { registerRenderState } from '../common/render-state.js';
28
28
  import { agerrorf, agwarningf } from '../common/agerr.js';
29
29
  import { parseSegsSilent } from '../common/multicolor.js';
30
- import { findStopColor, styleTokens } from '../common/style-resolve.js';
30
+ import { findStopColor, styleTokensQuiet } from '../common/style-resolve.js';
31
31
  import { emitOnce } from '../common/emit-once.js';
32
32
 
33
33
  /**
@@ -122,6 +122,21 @@ export function firstColor(name: string): string {
122
122
  */
123
123
  export function resolveRenderColor(raw: string, report = true): GVColor {
124
124
  if (raw === '' || raw === 'none') return { type: 'none' };
125
+ return resolveColorName(raw, report);
126
+ }
127
+
128
+ /**
129
+ * {@link resolveRenderColor} without the empty-name shortcut: C hands an empty
130
+ * name (a leading ':' cut at the first colon) to colorxlate, which rejects it
131
+ * (`Warning: is not a known color.`) and leaves opaque black.
132
+ * @see lib/gvc/gvrender.c:456 gvrender_set_fillcolor
133
+ */
134
+ export function resolveFillColorName(raw: string, report = true): GVColor {
135
+ if (raw === 'none') return { type: 'none' };
136
+ return resolveColorName(raw, report);
137
+ }
138
+
139
+ function resolveColorName(raw: string, report: boolean): GVColor {
125
140
  // C keeps a known name verbatim (case preserved); only the compare is
126
141
  // case-insensitive (bsearch over a lowercase table).
127
142
  const known = KNOWN_COLORS.has(raw.toLowerCase());
@@ -338,7 +353,7 @@ export function reportEdgeColors(e: Edge): void {
338
353
  reportColorNames(edgeGraphicsColors({
339
354
  color,
340
355
  fillcolor: e.attrs.get('fillcolor') ?? '',
341
- tapered: styleTokens(e.attrs.get('style')).includes('tapered'),
356
+ tapered: styleTokensQuiet(e.attrs.get('style')).includes('tapered'),
342
357
  arrows: spl.list.map((bz) => ({ sflag: bz.sflag !== 0, eflag: bz.eflag !== 0 })),
343
358
  }));
344
359
  }
@@ -239,12 +239,8 @@ function applyDraws(attrs: Map<string, string> | undefined, d: XdotDraws | undef
239
239
  * hand-built graph with no real layout state) so callers fall back cleanly. */
240
240
  function collectDrawLookup(g: Graph): DrawLookup | undefined {
241
241
  let xg: Graph;
242
- // Inject our own XdotRenderer (last-registered wins bestRenderer) so we can
243
- // read its per-object draw strings AFTER the render: the DOT-text re-parse
244
- // below is faithful for coordinates but LOSSY for draw text containing `"`
245
- // (a DOT quoted string cannot hold `\` before `"`; a cluster label's `\"`
246
- // round-trips to `"`, id 2239). The draws map holds the pre-serialization
247
- // strings C's -Tjson reads directly, so we overlay them onto the re-parse.
242
+ // Our own XdotRenderer (last-registered wins) exposes raw draw strings: the
243
+ // DOT re-parse is LOSSY for draw text containing `"` (id 2239); overlay them.
248
244
  const xr = new XdotRenderer();
249
245
  try {
250
246
  const ctx = createDefaultContext();
@@ -650,8 +646,8 @@ function latin1Reencode(s: string): string {
650
646
  return out;
651
647
  }
652
648
 
653
- export function buildJson(g: Graph, doXDot: boolean): string {
654
- const draws = doXDot ? collectDrawLookup(g) : undefined;
649
+ export function buildJson(g: Graph, doXDot: boolean, preDraws?: DrawLookup): string {
650
+ const draws = doXDot ? (preDraws ?? collectDrawLookup(g)) : undefined;
655
651
  const ctx = assignIds(g, doXDot, draws);
656
652
  const root = buildGraphObject(g, true, ctx);
657
653
  return JSON.stringify(root, null, 2) + '\n';
@@ -702,10 +698,13 @@ export class JsonRenderer implements RendererPlugin {
702
698
  /** @see plugin/core/gvplugin_core.c registration table */
703
699
  readonly quality = 0;
704
700
 
705
- beginGraph(_g: Graph, _job: RenderJob): void { /* no-op */ }
701
+ private draws: DrawLookup | undefined; // set by beginGraph, cleared by endGraph
702
+ /** @see plugin/core/gvrender_core_json.c:json_begin_graph */
703
+ beginGraph(g: Graph, _job: RenderJob): void { this.draws = collectDrawLookup(g); }
706
704
 
707
705
  endGraph(g: Graph, job: RenderJob): void {
708
- job.write(buildJson(g, true));
706
+ job.write(buildJson(g, true, this.draws));
707
+ this.draws = undefined;
709
708
  }
710
709
 
711
710
  beginNode(_n: Node, _job: RenderJob): void { /* no-op */ }
package/src/render/map.ts CHANGED
@@ -11,6 +11,7 @@
11
11
  */
12
12
 
13
13
  import type { Graph } from '../model/graph.js';
14
+ import { agNameOf } from '../model/cgraph-ops.js';
14
15
  import type { Node } from '../model/node.js';
15
16
  import type { Edge } from '../model/edge.js';
16
17
  import type { Point, Box } from '../model/geom.js';
@@ -583,13 +584,11 @@ export class PlainExtRenderer implements RendererPlugin {
583
584
  // Imagemap renderers (cmapx / imap and their no-polygon `_np` variants)
584
585
  // ---------------------------------------------------------------------------
585
586
 
586
- /** Graph name as `agnameof` returns it: the DOT name, or `%1` for an anonymous
587
- * root (cgraph's first anonymous id). The port stores `''` for an anonymous
588
- * root (dot/xdot re-serialization must not print a `%`-name — see dot.ts), so
589
- * the imagemap layer restores the internal name only here.
590
- * @see lib/cgraph/id.c:agnameof ; lib/cgraph/id.c:idmap (anon → `%1`) */
587
+ /** Graph name as `agnameof` returns it (`%1` for an anonymous root; the port
588
+ * stores `''` there so dot/xdot re-serialization stays unchanged).
589
+ * @see lib/cgraph/id.c:agnameof */
591
590
  export function mapGraphName(g: Graph): string {
592
- return g.anonymous ? '%1' : g.name;
591
+ return agNameOf(g);
593
592
  }
594
593
 
595
594
  /** First non-empty attr walking parent scopes (agget inheritance). */