@knowvah/dot-engine 2.3.0 → 2.4.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 (118) hide show
  1. package/dist/common/arith.d.ts +12 -0
  2. package/dist/common/arith.d.ts.map +1 -1
  3. package/dist/common/edge-in-box.d.ts +31 -0
  4. package/dist/common/edge-in-box.d.ts.map +1 -0
  5. package/dist/common/htmltable-emit.d.ts +1 -1
  6. package/dist/common/htmltable-emit.d.ts.map +1 -1
  7. package/dist/common/make-label.d.ts +3 -1
  8. package/dist/common/make-label.d.ts.map +1 -1
  9. package/dist/common/poly-gencode.d.ts +12 -1
  10. package/dist/common/poly-gencode.d.ts.map +1 -1
  11. package/dist/gvc/device.d.ts.map +1 -1
  12. package/dist/gvc/emit-gates.d.ts +35 -0
  13. package/dist/gvc/emit-gates.d.ts.map +1 -0
  14. package/dist/gvc/job.d.ts +4 -0
  15. package/dist/gvc/job.d.ts.map +1 -1
  16. package/dist/gvc/viewport.d.ts +13 -0
  17. package/dist/gvc/viewport.d.ts.map +1 -1
  18. package/dist/index.js +1030 -742
  19. package/dist/index.js.map +4 -4
  20. package/dist/layout/circo/blockpath.d.ts +1 -38
  21. package/dist/layout/circo/blockpath.d.ts.map +1 -1
  22. package/dist/layout/circo/crossings.d.ts +59 -0
  23. package/dist/layout/circo/crossings.d.ts.map +1 -0
  24. package/dist/layout/dot/edge-route-chain.d.ts +5 -3
  25. package/dist/layout/dot/edge-route-chain.d.ts.map +1 -1
  26. package/dist/layout/dot/edge-route.d.ts.map +1 -1
  27. package/dist/layout/dot/index.d.ts +10 -2
  28. package/dist/layout/dot/index.d.ts.map +1 -1
  29. package/dist/layout/dot/ortho-adapter.d.ts +7 -3
  30. package/dist/layout/dot/ortho-adapter.d.ts.map +1 -1
  31. package/dist/layout/dot/pack-components.d.ts +4 -1
  32. package/dist/layout/dot/pack-components.d.ts.map +1 -1
  33. package/dist/layout/dot/splines-flat-multi.d.ts +7 -5
  34. package/dist/layout/dot/splines-flat-multi.d.ts.map +1 -1
  35. package/dist/layout/dot/splines-flat.d.ts +2 -13
  36. package/dist/layout/dot/splines-flat.d.ts.map +1 -1
  37. package/dist/layout/dot/splines-route.d.ts.map +1 -1
  38. package/dist/layout/neato/constraint-adjust.d.ts +32 -0
  39. package/dist/layout/neato/constraint-adjust.d.ts.map +1 -1
  40. package/dist/layout/neato/multispline.d.ts.map +1 -1
  41. package/dist/layout/neato/splines.d.ts.map +1 -1
  42. package/dist/layout/pack/poly-place.d.ts.map +1 -1
  43. package/dist/layout/sfdp/__fixtures__/pcp-rotate-native.d.ts +23 -0
  44. package/dist/layout/sfdp/__fixtures__/pcp-rotate-native.d.ts.map +1 -0
  45. package/dist/layout/sfdp/spring-driver.d.ts +6 -0
  46. package/dist/layout/sfdp/spring-driver.d.ts.map +1 -1
  47. package/dist/ortho/index.d.ts +4 -1
  48. package/dist/ortho/index.d.ts.map +1 -1
  49. package/dist/ortho/maze.d.ts +5 -2
  50. package/dist/ortho/maze.d.ts.map +1 -1
  51. package/dist/ortho/partition.d.ts +8 -1
  52. package/dist/ortho/partition.d.ts.map +1 -1
  53. package/dist/ortho/trap-segment.d.ts +2 -1
  54. package/dist/ortho/trap-segment.d.ts.map +1 -1
  55. package/dist/ortho/trapezoid.d.ts.map +1 -1
  56. package/dist/parser/index.d.ts +0 -7
  57. package/dist/parser/index.d.ts.map +1 -1
  58. package/dist/parser/number-scan.d.ts +31 -0
  59. package/dist/parser/number-scan.d.ts.map +1 -0
  60. package/dist/pathplan/route.d.ts.map +1 -1
  61. package/dist/pathplan/solvers.d.ts +15 -3
  62. package/dist/pathplan/solvers.d.ts.map +1 -1
  63. package/dist/render/dot/edge-draw.d.ts +12 -0
  64. package/dist/render/dot/edge-draw.d.ts.map +1 -1
  65. package/dist/render/svg-graph.d.ts +6 -5
  66. package/dist/render/svg-graph.d.ts.map +1 -1
  67. package/dist/render/svg.d.ts +2 -21
  68. package/dist/render/svg.d.ts.map +1 -1
  69. package/dist/render.js +1030 -742
  70. package/dist/render.js.map +4 -4
  71. package/dist/xdot/parse.d.ts.map +1 -1
  72. package/package.json +1 -1
  73. package/src/common/arith.ts +25 -0
  74. package/src/common/edge-in-box.ts +54 -0
  75. package/src/common/graph-init.ts +3 -3
  76. package/src/common/htmltable-emit.ts +4 -2
  77. package/src/common/make-label.ts +3 -1
  78. package/src/common/poly-gencode.ts +18 -4
  79. package/src/gvc/device.ts +8 -41
  80. package/src/gvc/edge-labels.ts +30 -13
  81. package/src/gvc/emit-gates.ts +89 -0
  82. package/src/gvc/job.ts +4 -0
  83. package/src/gvc/viewport.ts +29 -0
  84. package/src/layout/circo/blockpath.ts +6 -125
  85. package/src/layout/circo/crossings.ts +171 -0
  86. package/src/layout/dot/__fixtures__/2784.dot +1 -0
  87. package/src/layout/dot/__fixtures__/2784.native-16.1.svg +48 -0
  88. package/src/layout/dot/edge-route-chain.ts +11 -5
  89. package/src/layout/dot/edge-route.ts +19 -8
  90. package/src/layout/dot/index.ts +14 -4
  91. package/src/layout/dot/ortho-adapter.ts +11 -6
  92. package/src/layout/dot/pack-components.ts +11 -4
  93. package/src/layout/dot/splines-flat-multi.ts +21 -8
  94. package/src/layout/dot/splines-flat.ts +8 -35
  95. package/src/layout/dot/splines-route.ts +10 -2
  96. package/src/layout/dot/splines.ts +6 -6
  97. package/src/layout/neato/constraint-adjust.ts +10 -4
  98. package/src/layout/neato/multispline.ts +26 -9
  99. package/src/layout/neato/splines.ts +7 -7
  100. package/src/layout/pack/poly-place.ts +9 -2
  101. package/src/layout/sfdp/__fixtures__/pcp-rotate-native.ts +94 -0
  102. package/src/layout/sfdp/spring-driver.ts +45 -13
  103. package/src/ortho/index.ts +22 -13
  104. package/src/ortho/maze.ts +8 -2
  105. package/src/ortho/partition.ts +17 -2
  106. package/src/ortho/trap-segment.ts +47 -24
  107. package/src/ortho/trapezoid.ts +24 -15
  108. package/src/parser/dot.js +345 -380
  109. package/src/parser/dot.pegjs +25 -10
  110. package/src/parser/index.ts +15 -0
  111. package/src/parser/number-scan.ts +118 -0
  112. package/src/pathplan/route.ts +79 -34
  113. package/src/pathplan/solvers.ts +78 -50
  114. package/src/render/dot/edge-draw.ts +25 -0
  115. package/src/render/svg-graph.ts +77 -47
  116. package/src/render/svg-helpers.ts +2 -2
  117. package/src/render/svg.ts +4 -41
  118. package/src/xdot/parse.ts +19 -5
@@ -16,12 +16,12 @@ import type { Box, Point } from '../model/geom.js';
16
16
  import type { RenderJob } from '../gvc/job.js';
17
17
  import { createObjState, ObjType } from '../gvc/job.js';
18
18
  import { FillType } from '../gvc/context.js';
19
- import { escapeXml, escapeXmlTitle } from './svg-helpers.js';
19
+ import { escapeXml, escapeXmlTitle, svgComment } from './svg-helpers.js';
20
20
  import { resolveRenderColor, colorPaint } from './color-resolve.js';
21
21
  import { svgGraphId, svgGraphClass } from './svg-id.js';
22
22
  import { parseGradientSpec } from '../common/htmltable-emit-fill.js';
23
23
  import { findStopColor, parseStyleFlags } from '../common/style-resolve.js';
24
- import { cround } from '../common/arith.js';
24
+ import { croundNarrow32 } from '../common/arith.js';
25
25
  import { FontnameKind } from '../model/layoutParams.js';
26
26
  import {
27
27
  emitLinearGradient,
@@ -66,7 +66,7 @@ function formatG(n: number): string {
66
66
  * (svgBeginPage). ViewBox starts at (round(margin.x), round(margin.y)) --
67
67
  * (0,0) only in the common no-margin case.
68
68
  */
69
- export function emitSvgTag(job: RenderJob): void {
69
+ export function emitSvgTag(job: RenderJob, svgid: string | undefined = ''): void {
70
70
  const bb = job.bb;
71
71
  // Size-fitted, UNROUNDED page-oriented view extent (init_job_pagination's
72
72
  // local `imageSize`/`pageSize`, emit.c:1198-1246): sz = bb extent plus pad,
@@ -87,8 +87,11 @@ export function emitSvgTag(job: RenderJob): void {
87
87
  const pageX = rotated ? viewY : viewX;
88
88
  const pageY = rotated ? viewX : viewY;
89
89
  // job->width/job->height (emit.c:1249-1250, dpi=72 so *dpi/72 is a no-op).
90
- const w = cround(pageX + 2 * job.margin.x);
91
- const h = cround(pageY + 2 * job.margin.y);
90
+ // The native build vectorises these ROUNDs (fcvtzs.2d + xtn: low 32 bits of
91
+ // an int64), so an out-of-range page size wraps exactly as native prints it
92
+ // (2784: 2.16e18 -> 1255669760). @see croundNarrow32
93
+ const w = croundNarrow32(pageX + 2 * job.margin.x);
94
+ const h = croundNarrow32(pageY + 2 * job.margin.y);
92
95
  job.write('<svg width="' + String(w) + 'pt" height="' + String(h) + 'pt"\n');
93
96
  // job->pageBoundingBox (canvasBox.LL = margin+centering, canvasBox.UR = LL +
94
97
  // imageSize, then ROUND -- emit.c:1282-1293). C's rotation-branch double
@@ -96,14 +99,20 @@ export function emitSvgTag(job: RenderJob): void {
96
99
  // swap-back, emit.c:1275-1298) cancels for LL (always plain margin) but NOT
97
100
  // for UR, which ends up paired with the page-oriented (rotation-swapped)
98
101
  // view extent -- i.e. the same `pageX`/`pageY` used for width/height above.
99
- const vbLLx = cround(job.margin.x);
100
- const vbLLy = cround(job.margin.y);
101
- const vbURx = cround(job.margin.x + pageX);
102
- const vbURy = cround(job.margin.y + pageY);
102
+ const vbLLx = croundNarrow32(job.margin.x);
103
+ const vbLLy = croundNarrow32(job.margin.y);
104
+ const vbURx = croundNarrow32(job.margin.x + pageX);
105
+ const vbURy = croundNarrow32(job.margin.y + pageY);
103
106
  job.write(' viewBox="' + String(vbLLx) + '.00 ' + String(vbLLy) + '.00 ' +
104
107
  String(vbURx) + '.00 ' + String(vbURy) + '.00"\n');
105
108
  job.write(' xmlns="http://www.w3.org/2000/svg"');
106
- job.write(' xmlns:xlink="http://www.w3.org/1999/xlink">\n');
109
+ job.write(' xmlns:xlink="http://www.w3.org/1999/xlink"');
110
+ // graphviz 16.1: `svgid` graph attribute -> id on the root <svg> (gvputs_xml).
111
+ // @see plugin/core/gvrender_core_svg.c:svg_begin_graph
112
+ if (svgid !== undefined && svgid.length > 0) {
113
+ job.write(' id="' + escapeXmlTitle(svgid) + '"');
114
+ }
115
+ job.write('>\n');
107
116
  }
108
117
 
109
118
  /**
@@ -175,22 +184,23 @@ export function resolveGraphBgcolor(bgcolorAttr: string | undefined): string {
175
184
  }
176
185
 
177
186
  /**
178
- * Emit background polygon covering the full padded content area.
187
+ * Emit background polygon covering the page clip box (C gvrender_box over
188
+ * job->clip — bb ± pad as setup_page evaluates it, see setupPageClip).
179
189
  *
180
190
  * Omits the polygon entirely when resolvedColor is the transparent sentinel.
181
191
  * Coordinates are in the group's local frame (y-flipped via devscale.y=-1):
182
- * x: [bb.ll.x - PAD, bb.ur.x + PAD]
183
- * y (flipped): [-(bb.ur.y + PAD), -(bb.ll.y - PAD)]
192
+ * x: [clip.ll.x, clip.ur.x]
193
+ * y (flipped): [-clip.ur.y, -clip.ll.y]
184
194
  *
185
195
  * @see plugin/core/gvrender_core_svg.c:svg_begin_page
186
196
  * @see lib/common/emit.c:emit_background:1476
187
197
  */
188
- export function emitGraphBackground(bb: Box, resolvedColor: string, job: RenderJob): void {
198
+ export function emitGraphBackground(clip: Box, resolvedColor: string, job: RenderJob): void {
189
199
  if (resolvedColor === BGCOLOR_TRANSPARENT) return;
190
- const left = bb.ll.x - job.pad.x;
191
- const right = bb.ur.x + job.pad.x;
192
- const top = -(bb.ur.y + job.pad.y);
193
- const bottom = -(bb.ll.y - job.pad.y);
200
+ const left = clip.ll.x;
201
+ const right = clip.ur.x;
202
+ const top = -clip.ur.y;
203
+ const bottom = -clip.ll.y;
194
204
  const corners: [number, number][] = [
195
205
  [left, bottom], [left, top], [right, top], [right, bottom], [left, bottom],
196
206
  ];
@@ -209,11 +219,11 @@ export function emitGraphBackground(bb: Box, resolvedColor: string, job: RenderJ
209
219
  * Passed to getGradientPoints inside emitLinearGradient.
210
220
  * @see lib/common/emit.c:emit_background:1517 (gvrender_box with clip)
211
221
  */
212
- function bgGradientPts(bb: Box, pad: Point): Point[] {
213
- const left = bb.ll.x - pad.x;
214
- const right = bb.ur.x + pad.x;
215
- const bottom = bb.ll.y - pad.y;
216
- const top = bb.ur.y + pad.y;
222
+ function bgGradientPts(clip: Box): Point[] {
223
+ const left = clip.ll.x;
224
+ const right = clip.ur.x;
225
+ const bottom = clip.ll.y;
226
+ const top = clip.ur.y;
217
227
  return [
218
228
  { x: left, y: bottom },
219
229
  { x: left, y: top },
@@ -227,11 +237,11 @@ function bgGradientPts(bb: Box, pad: Point): Point[] {
227
237
  * Coords identical to emitGraphBackground; only fill changes.
228
238
  * @see plugin/core/gvrender_core_svg.c:svg_polygon:686
229
239
  */
230
- function writeGradientPolygon(bb: Box, id: string, job: RenderJob): void {
231
- const left = bb.ll.x - job.pad.x;
232
- const right = bb.ur.x + job.pad.x;
233
- const top = -(bb.ur.y + job.pad.y);
234
- const bottom = -(bb.ll.y - job.pad.y);
240
+ function writeGradientPolygon(clip: Box, id: string, job: RenderJob): void {
241
+ const left = clip.ll.x;
242
+ const right = clip.ur.x;
243
+ const top = -clip.ur.y;
244
+ const bottom = -clip.ll.y;
235
245
  const corners: [number, number][] = [
236
246
  [left, bottom], [left, top], [right, top], [right, bottom], [left, bottom],
237
247
  ];
@@ -256,7 +266,7 @@ function writeGradientPolygon(bb: Box, id: string, job: RenderJob): void {
256
266
  * @see plugin/core/gvrender_core_svg.c:svg_gradstyle:572
257
267
  * @see plugin/core/gvrender_core_svg.c:svg_rgradstyle:608
258
268
  */
259
- function emitGradientBackground(bb: Box, spec: BgGradientSpec, job: RenderJob): void {
269
+ function emitGradientBackground(clip: Box, spec: BgGradientSpec, job: RenderJob): void {
260
270
  const obj = createObjState(ObjType.RootGraph);
261
271
  obj.id = 'graph0';
262
272
  obj.fillColor = resolveRenderColor(spec.fillColor);
@@ -271,10 +281,10 @@ function emitGradientBackground(bb: Box, spec: BgGradientSpec, job: RenderJob):
271
281
  emitRadialGradient(job, gid);
272
282
  } else {
273
283
  gid = gradientId('graph0', 'l', job.linearGradId++);
274
- emitLinearGradient(job, bgGradientPts(bb, job.pad), gid);
284
+ emitLinearGradient(job, bgGradientPts(clip), gid);
275
285
  }
276
286
  job.popObj();
277
- writeGradientPolygon(bb, gid, job);
287
+ writeGradientPolygon(clip, gid, job);
278
288
  }
279
289
 
280
290
  /**
@@ -296,17 +306,17 @@ function resolveBgGradientSpec(g: Graph, bgcolorAttr: string): BgGradientSpec |
296
306
  * Separated to keep svgBeginGraph under the 30-line body limit.
297
307
  * @see lib/common/emit.c:emit_background:1476
298
308
  */
299
- function emitBgcolorBackground(g: Graph, bb: Box, bgcolorAttr: string | undefined, job: RenderJob): void {
309
+ function emitBgcolorBackground(g: Graph, clip: Box, bgcolorAttr: string | undefined, job: RenderJob): void {
300
310
  if (!bgcolorAttr || bgcolorAttr.length === 0 || bgcolorAttr === 'transparent') {
301
- emitGraphBackground(bb, resolveGraphBgcolor(bgcolorAttr), job);
311
+ emitGraphBackground(clip, resolveGraphBgcolor(bgcolorAttr), job);
302
312
  return;
303
313
  }
304
314
  const spec = resolveBgGradientSpec(g, bgcolorAttr);
305
315
  if (spec !== null) {
306
- emitGradientBackground(bb, spec, job);
316
+ emitGradientBackground(clip, spec, job);
307
317
  return;
308
318
  }
309
- emitGraphBackground(bb, resolveGraphBgcolor(bgcolorAttr), job);
319
+ emitGraphBackground(clip, resolveGraphBgcolor(bgcolorAttr), job);
310
320
  }
311
321
 
312
322
  /**
@@ -344,7 +354,25 @@ export function svgBeginGraph(g: Graph, job: RenderJob): void {
344
354
  }
345
355
  job.write(SVG_DOCTYPE);
346
356
  job.write(SVG_GENERATOR_COMMENT);
347
- emitSvgTag(job);
357
+ // emit_graph: the root `comment` attribute, between svg_begin_job and
358
+ // emit_begin_graph. @see lib/common/emit.c:emit_graph
359
+ const comment = g.attrs.get('comment');
360
+ if (comment !== undefined && comment.length > 0) svgComment(comment, job);
361
+ emitTitlePagesComment(g, job);
362
+ emitSvgTag(job, g.attrs.get('svgid'));
363
+ }
364
+
365
+ /**
366
+ * svg_begin_graph's header comment: ` Title: <name>` for a user-named graph,
367
+ * then the page count (always 1 — the port emits a single page).
368
+ * @see plugin/core/gvrender_core_svg.c:svg_begin_graph
369
+ */
370
+ function emitTitlePagesComment(g: Graph, job: RenderJob): void {
371
+ job.write('<!--');
372
+ if (g.name.length > 0 && g.name[0] !== '%') {
373
+ job.write(' Title: ' + escapeXmlTitle(g.name));
374
+ }
375
+ job.write(' Pages: 1 -->\n');
348
376
  }
349
377
 
350
378
  /**
@@ -353,14 +381,16 @@ export function svgBeginGraph(g: Graph, job: RenderJob): void {
353
381
  * @see plugin/core/gvrender_core_svg.c:svg_begin_page
354
382
  */
355
383
  export function svgBeginPage(g: Graph, job: RenderJob): void {
356
- const bb = job.bb;
357
- // gvPostprocess zeroes bb.ll before render; tx is simply the padding.
384
+ const clip = job.clip;
385
+ // Portrait: translation.x = -clip.LL.x, with job->clip as setup_page builds
386
+ // it (setupPageClip: focus ∓ pageSize/2, i.e. bb ± pad up to rounding).
387
+ // gvPostprocess normally zeroes bb.ll, but a dot layout failure skips it
388
+ // (graphviz 16.1, corpus 2784: translate(31 ...)).
358
389
  // Landscape (ADR-3): setup_page's rotation branch (SVG = GVRENDER_Y_GOES_DOWN)
359
- // gives translation.x = -(focus.x + pageSize.x/2) = -(bb.ur.x + PAD) in the
360
- // zeroed-bb frame; translation.y is unchanged (focus.y + pageSize.y/2 =
361
- // bb.ur.y + PAD). pageSize = imageSize/zoom is Z-independent (outer scale
362
- // carries Z), so the formula holds for any size= fit. Pinned to b68 native
363
- // translate(-634 208.5). @see emit.c:setup_page; gvrender_core_svg.c:svg_begin_page
390
+ // gives translation.x = -clip.UR.x; translation.y = -clip.UR.y in both
391
+ // branches (printed negated). pageSize = imageSize/zoom is Z-independent
392
+ // (outer scale carries Z). Pinned to b68 native translate(-634 208.5).
393
+ // @see emit.c:setup_page; gvrender_core_svg.c:svg_begin_page
364
394
  //
365
395
  // margin (F6): setup_page adds `canvasBox.LL / job->zoom` on top of the
366
396
  // pad-only formula above -- canvasBox.LL is the FINAL (post swap-cancel)
@@ -374,9 +404,9 @@ export function svgBeginPage(g: Graph, job: RenderJob): void {
374
404
  // @see lib/common/emit.c:1566-1582 setup_page (CAUTION comment there)
375
405
  const z = job.scale.x;
376
406
  const tx = job.rotation !== 0
377
- ? -(bb.ur.x + job.pad.x) - job.margin.x / z
378
- : job.pad.x + job.margin.x / z;
379
- const ty = bb.ur.y + job.pad.y + job.margin.y / z;
407
+ ? -clip.ur.x - job.margin.x / z
408
+ : -clip.ll.x + job.margin.x / z;
409
+ const ty = clip.ur.y + job.margin.y / z;
380
410
  emitGraphGroupOpen(
381
411
  job.idLayerPrefix() + svgGraphId(g, job),
382
412
  svgGraphClass(g),
@@ -393,7 +423,7 @@ export function svgBeginPage(g: Graph, job: RenderJob): void {
393
423
  * @see lib/common/emit.c:emit_page (emit_background after begin_page)
394
424
  */
395
425
  export function svgPageBackground(g: Graph, job: RenderJob): void {
396
- emitBgcolorBackground(g, job.bb, g.attrs.get('bgcolor'), job);
426
+ emitBgcolorBackground(g, job.clip, g.attrs.get('bgcolor'), job);
397
427
  }
398
428
 
399
429
  /** Close the graph `<g>` group. @see svg_end_page */
@@ -421,8 +421,8 @@ export function svgPolyline(pts: Point[], job: RenderJob): void {
421
421
  job.write('"/>\n');
422
422
  }
423
423
 
424
- export function svgComment(text: string, job: RenderJob): void {
425
- job.write('<!-- ' + escapeXml(text) + ' -->\n');
424
+ export function svgComment(text: string, job: RenderJob): void { // svg_comment: gvputs_xml
425
+ job.write('<!-- ' + escapeXmlTitle(text) + ' -->\n');
426
426
  }
427
427
 
428
428
  // ---------------------------------------------------------------------------
package/src/render/svg.ts CHANGED
@@ -20,13 +20,12 @@ import type { Graph } from '../model/graph.js';
20
20
  import type { Node } from '../model/node.js';
21
21
  import type { Edge } from '../model/edge.js';
22
22
  import type { Point, Box } from '../model/geom.js';
23
- import { boxOverlap } from '../model/geom.js';
24
- import type { TextlabelT } from '../common/types.js';
25
23
  import type { TextSpan } from '../common/emit-types.js';
26
24
  import type { RendererPlugin, LabelType } from '../gvc/context.js';
27
25
  import type { RenderJob } from '../gvc/job.js';
28
26
  import { renderEdgeLabels } from '../gvc/edge-labels.js';
29
27
  import { beginAnchorIf } from '../gvc/anchor.js';
28
+ import { edgeHasDrawableContent } from '../common/edge-in-box.js';
30
29
  import {
31
30
  svgBeginGraph,
32
31
  svgEndGraph,
@@ -86,45 +85,9 @@ function emitMulticolorArrows(e: Edge, job: RenderJob, headColor: string, tailCo
86
85
  // @see plugin/core/gvrender_core_svg.c:svg_engine
87
86
  // ---------------------------------------------------------------------------
88
87
 
89
- /** overlap_label: does the label's box (pos ± dimen/2) overlap clip box b?
90
- * @see lib/common/utils.c:overlap_label */
91
- function overlapLabel(lp: TextlabelT, b: Box): boolean {
92
- const sx = lp.dimen.x / 2;
93
- const sy = lp.dimen.y / 2;
94
- const bb: Box = {
95
- ll: { x: lp.pos.x - sx, y: lp.pos.y - sy },
96
- ur: { x: lp.pos.x + sx, y: lp.pos.y + sy },
97
- };
98
- return boxOverlap(b, bb);
99
- }
100
-
101
- /**
102
- * Faithful-to-outcome `edge_in_box`: true when the edge has something to draw.
103
- * C's gvrender defers the `<g>` until a draw op, so an edge whose content falls
104
- * entirely outside the clip emits no group (emit.c:edge_in_box tests each piece
105
- * vs job->clip via boxf_overlap/overlap_label).
106
- *
107
- * The only piece that can legitimately fall OUTSIDE the clip on this corpus is a
108
- * degenerate labeled-flat label: it is spline-less, so map_edge leaves it at its
109
- * internal (un-normalized) x-frame position while the clip is the final frame.
110
- * The `overlapLabel` test on the main label reproduces C's draw-iff-overlap for
111
- * exactly that case — on-canvas (2368) → drawn; off-canvas (2368_1, negative
112
- * un-normalized x) → suppressed. A present spline always lies within the clip
113
- * (GD_bb contains it), and head/tail/xlabels are positioned at on-canvas
114
- * endpoints, so those keep their existing unconditional triggers — adding an
115
- * overlap test there only risks suppressing legitimately-drawn content (it
116
- * regressed neato/circo edges whose spl.bb is computed differently). The `set`
117
- * guard on the main label is kept: the port retains an unplaced (`pos=0,0`,
118
- * `set=false`) label on merged back-edges where C has none.
119
- * @see lib/common/emit.c:edge_in_box, lib/common/utils.c:overlap_label
120
- */
121
- export function edgeHasDrawableContent(e: Edge, clip: Box): boolean {
122
- const i = e.info;
123
- return i.spl !== undefined
124
- || (i.label !== undefined && i.label.set && overlapLabel(i.label, clip))
125
- || i.xlabel !== undefined
126
- || i.head_label !== undefined || i.tail_label !== undefined;
127
- }
88
+ // edge_in_box gate lives in common/edge-in-box.ts (shared with the device walk);
89
+ // re-exported for existing import sites.
90
+ export { edgeHasDrawableContent };
128
91
 
129
92
  export class SvgRenderer implements RendererPlugin {
130
93
  readonly type = 'svg';
package/src/xdot/parse.ts CHANGED
@@ -13,6 +13,9 @@ import type {
13
13
  } from "./types.js";
14
14
  import { XDOT_PARSE_ERROR } from "./types.js";
15
15
 
16
+ /** ULONG_MAX on an LP64 target: strtoul saturation value. */
17
+ const ULONG_MAX = 0xffff_ffff_ffff_ffffn;
18
+
16
19
  type PR<T> = { val: T; pos: number } | null;
17
20
  type OpResult = { op: XdotOp; pos: number } | "error";
18
21
  type OpMaybeResult = OpResult | null;
@@ -63,12 +66,23 @@ class XdotParser {
63
66
  return { val: parseInt(s.slice(start, i), 10), pos: i };
64
67
  }
65
68
 
69
+ /**
70
+ * strtoul(s, &endp, 10) cast to unsigned int: optional sign, a leading "-"
71
+ * negates modulo 2^64, magnitude overflow saturates to ULONG_MAX, and the
72
+ * result keeps the low 32 bits.
73
+ * @see lib/xdot/xdot.c:parseUInt
74
+ */
66
75
  static parseUInt_(s: string, pos: number): PR<number> {
67
76
  const i = XdotParser.skipWs(s, pos);
68
- const start = i;
69
- const end = XdotParser.consumeDigits(s, i);
77
+ let start = i;
78
+ const negative = i < s.length && s[i] === "-";
79
+ if (i < s.length && (s[i] === "-" || s[i] === "+")) start++;
80
+ const end = XdotParser.consumeDigits(s, start);
70
81
  if (end === start) return null;
71
- return { val: parseInt(s.slice(start, end), 10), pos: end };
82
+ const mag = BigInt(s.slice(start, end));
83
+ const clamped = mag > ULONG_MAX ? ULONG_MAX : mag;
84
+ const wrapped = negative && mag <= ULONG_MAX ? BigInt.asUintN(64, -clamped) : clamped;
85
+ return { val: Number(BigInt.asUintN(32, wrapped)), pos: end };
72
86
  }
73
87
 
74
88
  static parseRect(s: string, pos: number): PR<XdotRect> {
@@ -163,7 +177,7 @@ class XdotParser {
163
177
  const ry0 = XdotParser.parseReal(s, rx0.pos); if (!ry0) return null;
164
178
  const rx1 = XdotParser.parseReal(s, ry0.pos); if (!rx1) return null;
165
179
  const ry1 = XdotParser.parseReal(s, rx1.pos); if (!ry1) return null;
166
- const rn = XdotParser.parseInt_(s, ry1.pos); if (!rn) return null;
180
+ const rn = XdotParser.parseUInt_(s, ry1.pos); if (!rn) return null;
167
181
  const rs = XdotParser.parseStops(s, rn.pos, rn.val); if (!rs) return null;
168
182
  return { x0: rx0.val, y0: ry0.val, x1: rx1.val, y1: ry1.val, stops: rs.stops };
169
183
  }
@@ -175,7 +189,7 @@ class XdotParser {
175
189
  const rx1 = XdotParser.parseReal(s, rr0.pos); if (!rx1) return null;
176
190
  const ry1 = XdotParser.parseReal(s, rx1.pos); if (!ry1) return null;
177
191
  const rr1 = XdotParser.parseReal(s, ry1.pos); if (!rr1) return null;
178
- const rn = XdotParser.parseInt_(s, rr1.pos); if (!rn) return null;
192
+ const rn = XdotParser.parseUInt_(s, rr1.pos); if (!rn) return null;
179
193
  const rs = XdotParser.parseStops(s, rn.pos, rn.val); if (!rs) return null;
180
194
  return { x0: rx0.val, y0: ry0.val, r0: rr0.val, x1: rx1.val, y1: ry1.val, r1: rr1.val, stops: rs.stops };
181
195
  }