@knowvah/dot-engine 2.3.1 → 2.4.1

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 (115) 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 +821 -548
  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 +6 -2
  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 +4 -4
  34. package/dist/layout/dot/splines-flat.d.ts +1 -11
  35. package/dist/layout/dot/splines-flat.d.ts.map +1 -1
  36. package/dist/layout/dot/splines-route.d.ts.map +1 -1
  37. package/dist/layout/neato/constraint-adjust.d.ts +32 -0
  38. package/dist/layout/neato/constraint-adjust.d.ts.map +1 -1
  39. package/dist/layout/neato/multispline.d.ts.map +1 -1
  40. package/dist/layout/neato/splines.d.ts.map +1 -1
  41. package/dist/layout/pack/poly-pack.d.ts +3 -0
  42. package/dist/layout/pack/poly-pack.d.ts.map +1 -1
  43. package/dist/layout/pack/poly-place.d.ts.map +1 -1
  44. package/dist/layout/sfdp/__fixtures__/pcp-rotate-native.d.ts +23 -0
  45. package/dist/layout/sfdp/__fixtures__/pcp-rotate-native.d.ts.map +1 -0
  46. package/dist/layout/sfdp/spring-driver.d.ts +6 -0
  47. package/dist/layout/sfdp/spring-driver.d.ts.map +1 -1
  48. package/dist/ortho/index.d.ts +4 -1
  49. package/dist/ortho/index.d.ts.map +1 -1
  50. package/dist/ortho/maze.d.ts +5 -2
  51. package/dist/ortho/maze.d.ts.map +1 -1
  52. package/dist/ortho/partition.d.ts +8 -1
  53. package/dist/ortho/partition.d.ts.map +1 -1
  54. package/dist/ortho/trap-segment.d.ts +2 -1
  55. package/dist/ortho/trap-segment.d.ts.map +1 -1
  56. package/dist/ortho/trapezoid.d.ts.map +1 -1
  57. package/dist/parser/index.d.ts +0 -7
  58. package/dist/parser/index.d.ts.map +1 -1
  59. package/dist/parser/number-scan.d.ts +31 -0
  60. package/dist/parser/number-scan.d.ts.map +1 -0
  61. package/dist/render/dot/edge-draw.d.ts +12 -0
  62. package/dist/render/dot/edge-draw.d.ts.map +1 -1
  63. package/dist/render/svg-graph.d.ts +6 -5
  64. package/dist/render/svg-graph.d.ts.map +1 -1
  65. package/dist/render/svg.d.ts +2 -21
  66. package/dist/render/svg.d.ts.map +1 -1
  67. package/dist/render.js +821 -548
  68. package/dist/render.js.map +4 -4
  69. package/dist/xdot/parse.d.ts.map +1 -1
  70. package/package.json +1 -1
  71. package/src/common/arith.ts +25 -0
  72. package/src/common/edge-in-box.ts +54 -0
  73. package/src/common/graph-init.ts +3 -3
  74. package/src/common/htmltable-emit.ts +4 -2
  75. package/src/common/make-label.ts +3 -1
  76. package/src/common/poly-gencode.ts +18 -4
  77. package/src/gvc/device.ts +8 -41
  78. package/src/gvc/edge-labels.ts +30 -13
  79. package/src/gvc/emit-gates.ts +89 -0
  80. package/src/gvc/job.ts +4 -0
  81. package/src/gvc/viewport.ts +29 -0
  82. package/src/layout/circo/blockpath.ts +6 -125
  83. package/src/layout/circo/crossings.ts +171 -0
  84. package/src/layout/dot/__fixtures__/2784.dot +1 -0
  85. package/src/layout/dot/__fixtures__/2784.native-16.1.svg +48 -0
  86. package/src/layout/dot/edge-route-chain.ts +11 -5
  87. package/src/layout/dot/edge-route.ts +2 -1
  88. package/src/layout/dot/index.ts +14 -4
  89. package/src/layout/dot/ortho-adapter.ts +7 -3
  90. package/src/layout/dot/pack-components.ts +11 -4
  91. package/src/layout/dot/splines-flat-multi.ts +4 -4
  92. package/src/layout/dot/splines-flat.ts +5 -32
  93. package/src/layout/dot/splines-route.ts +10 -2
  94. package/src/layout/dot/splines.ts +6 -6
  95. package/src/layout/neato/constraint-adjust.ts +10 -4
  96. package/src/layout/neato/multispline.ts +26 -9
  97. package/src/layout/neato/splines.ts +7 -7
  98. package/src/layout/pack/poly-pack.ts +19 -6
  99. package/src/layout/pack/poly-place.ts +9 -2
  100. package/src/layout/sfdp/__fixtures__/pcp-rotate-native.ts +94 -0
  101. package/src/layout/sfdp/spring-driver.ts +45 -13
  102. package/src/ortho/index.ts +22 -13
  103. package/src/ortho/maze.ts +8 -2
  104. package/src/ortho/partition.ts +17 -2
  105. package/src/ortho/trap-segment.ts +47 -24
  106. package/src/ortho/trapezoid.ts +24 -15
  107. package/src/parser/dot.js +334 -343
  108. package/src/parser/dot.pegjs +19 -5
  109. package/src/parser/index.ts +15 -0
  110. package/src/parser/number-scan.ts +118 -0
  111. package/src/render/dot/edge-draw.ts +25 -0
  112. package/src/render/svg-graph.ts +77 -47
  113. package/src/render/svg-helpers.ts +2 -2
  114. package/src/render/svg.ts +4 -41
  115. package/src/xdot/parse.ts +19 -5
@@ -1 +1 @@
1
- {"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../../src/xdot/parse.ts"],"names":[],"mappings":"AACA;;;;;GAKG;AAEH,OAAO,KAAK,EACV,IAAI,EAAU,QAAQ,EAAE,YAAY,EACf,SAAS,EACC,WAAW,EAC3C,MAAM,YAAY,CAAC;AAGpB,KAAK,EAAE,CAAC,CAAC,IAAI;IAAE,GAAG,EAAE,CAAC,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC;AAoU5C,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,EAAE,CAAC,QAAQ,CAAC,CAAyC;AACxG,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,EAAE,CAAC,YAAY,CAAC,CAA6C;AACpH,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,EAAE,CAAC,MAAM,CAAC,CAA2C;AAC1G,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,EAAE,CAAC,SAAS,CAAC,CAAgD;AACvH,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,WAAW,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAE5G;AACD,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,WAAW,CAAC,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAE1F;AACD,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAAoC;AACrF,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAAyC"}
1
+ {"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../../src/xdot/parse.ts"],"names":[],"mappings":"AACA;;;;;GAKG;AAEH,OAAO,KAAK,EACV,IAAI,EAAU,QAAQ,EAAE,YAAY,EACf,SAAS,EACC,WAAW,EAC3C,MAAM,YAAY,CAAC;AAMpB,KAAK,EAAE,CAAC,CAAC,IAAI;IAAE,GAAG,EAAE,CAAC,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC;AA+U5C,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,EAAE,CAAC,QAAQ,CAAC,CAAyC;AACxG,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,EAAE,CAAC,YAAY,CAAC,CAA6C;AACpH,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,EAAE,CAAC,MAAM,CAAC,CAA2C;AAC1G,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,EAAE,CAAC,SAAS,CAAC,CAAgD;AACvH,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,WAAW,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAE5G;AACD,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,WAAW,CAAC,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAE1F;AACD,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAAoC;AACrF,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAAyC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knowvah/dot-engine",
3
- "version": "2.3.1",
3
+ "version": "2.4.1",
4
4
  "description": "Faithful TypeScript port of Graphviz (dot, neato, fdp, sfdp, circo, twopi, osage, patchwork) — no C: no native binary, no WASM. Oracle-verified against the C implementation, runs in the browser.",
5
5
  "keywords": [
6
6
  "graphviz",
@@ -38,6 +38,31 @@ export function cround(v: number): number {
38
38
  return v >= 0 ? Math.floor(v + 0.5) : Math.ceil(v - 0.5);
39
39
  }
40
40
 
41
+ const INT64_MAX = 2n ** 63n - 1n;
42
+ const INT64_MIN = -(2n ** 63n);
43
+ const TWO_POW_63 = 2 ** 63;
44
+
45
+ /**
46
+ * `ROUND(f)` as the native arm64 build computes it where clang vectorises the
47
+ * conversion: `fcvtzs.2d` (double → int64, saturating, NaN → 0) followed by
48
+ * `xtn.2s` / `uzp1.4s`, which keep the LOW 32 bits. The `(int)` cast of an
49
+ * out-of-range double is undefined in C; this is the oracle binary's
50
+ * behaviour (objdump of libgvc `_gvRenderJobs`, the inlined
51
+ * init_job_pagination: job->width/height and pageBoundingBox). Identical to
52
+ * {@link cround} whenever the result fits in an int; only these vectorised
53
+ * sites use it — scalar ROUND sites compile to `fcvtzs w` (saturate at int32).
54
+ * @see lib/common/emit.c:init_job_pagination (job->width, pageBoundingBox)
55
+ */
56
+ export function croundNarrow32(v: number): number {
57
+ const r = cround(v);
58
+ let i64: bigint;
59
+ if (Number.isNaN(r)) i64 = 0n;
60
+ else if (r >= TWO_POW_63) i64 = INT64_MAX;
61
+ else if (r < -TWO_POW_63) i64 = INT64_MIN;
62
+ else i64 = BigInt(r);
63
+ return Number(BigInt.asIntN(32, i64));
64
+ }
65
+
41
66
  /**
42
67
  * Degrees to radians in C's operation order, `(deg)/180.0 * M_PI`.
43
68
  * `deg * Math.PI / 180` rounds differently for some angles (e.g. 33°).
@@ -0,0 +1,54 @@
1
+ // SPDX-License-Identifier: EPL-2.0
2
+
3
+ /**
4
+ * emit_edge's edge_in_box gate, shared by the device walk (edge comments) and
5
+ * the SVG / xdot renderers (edge group and draw ops). Moved out of
6
+ * render/svg.ts unchanged.
7
+ *
8
+ * @see lib/common/emit.c:edge_in_box, lib/common/utils.c:overlap_label
9
+ */
10
+
11
+ import type { Box } from '../model/geom.js';
12
+ import { boxOverlap } from '../model/geom.js';
13
+ import type { Edge } from '../model/edge.js';
14
+ import type { TextlabelT } from './types.js';
15
+
16
+ /** overlap_label: does the label's box (pos ± dimen/2) overlap clip box b?
17
+ * @see lib/common/utils.c:overlap_label */
18
+ function overlapLabel(lp: TextlabelT, b: Box): boolean {
19
+ const sx = lp.dimen.x / 2;
20
+ const sy = lp.dimen.y / 2;
21
+ const bb: Box = {
22
+ ll: { x: lp.pos.x - sx, y: lp.pos.y - sy },
23
+ ur: { x: lp.pos.x + sx, y: lp.pos.y + sy },
24
+ };
25
+ return boxOverlap(b, bb);
26
+ }
27
+
28
+ /**
29
+ * Faithful-to-outcome `edge_in_box`: true when the edge has something to draw.
30
+ * C's gvrender defers the `<g>` until a draw op, so an edge whose content falls
31
+ * entirely outside the clip emits no group (emit.c:edge_in_box tests each piece
32
+ * vs job->clip via boxf_overlap/overlap_label).
33
+ *
34
+ * The only piece that can legitimately fall OUTSIDE the clip on this corpus is a
35
+ * degenerate labeled-flat label: it is spline-less, so map_edge leaves it at its
36
+ * internal (un-normalized) x-frame position while the clip is the final frame.
37
+ * The `overlapLabel` test on the main label reproduces C's draw-iff-overlap for
38
+ * exactly that case — on-canvas (2368) → drawn; off-canvas (2368_1, negative
39
+ * un-normalized x) → suppressed. A present spline always lies within the clip
40
+ * (GD_bb contains it), and head/tail/xlabels are positioned at on-canvas
41
+ * endpoints, so those keep their existing unconditional triggers — adding an
42
+ * overlap test there only risks suppressing legitimately-drawn content (it
43
+ * regressed neato/circo edges whose spl.bb is computed differently). The `set`
44
+ * guard on the main label is kept: the port retains an unplaced (`pos=0,0`,
45
+ * `set=false`) label on merged back-edges where C has none.
46
+ * @see lib/common/emit.c:edge_in_box, lib/common/utils.c:overlap_label
47
+ */
48
+ export function edgeHasDrawableContent(e: Edge, clip: Box): boolean {
49
+ const i = e.info;
50
+ return i.spl !== undefined
51
+ || (i.label !== undefined && i.label.set && overlapLabel(i.label, clip))
52
+ || i.xlabel !== undefined
53
+ || i.head_label !== undefined || i.tail_label !== undefined;
54
+ }
@@ -264,9 +264,9 @@ export function graphInit(g: Graph, useRankdir: boolean): void {
264
264
  // allocated unconditionally; see parseRatioDrawing's note (it would change
265
265
  // dot output by activating the unported ratio modes).
266
266
 
267
- // SKIPPED — `postaction` reparse (input.c:611-617): needs agmemconcat plus
268
- // parser re-entry to splice a second graph into g. Neither exists in the
269
- // port, and no corpus input sets `postaction`.
267
+ // `postaction` reparse: removed upstream in 16.1.0 (41e482146 reverted the
268
+ // feature and dropped agmemconcat), so graph_init no longer reads it. The
269
+ // port never implemented it; nothing to mirror.
270
270
 
271
271
  // SKIPPED — fontpath / DOTFONTPATH setenv (input.c:619-629): browser-
272
272
  // impossible (no environment, no GDFONTPATH).
@@ -330,7 +330,7 @@ function emitTableRules(
330
330
  * Emit a fully positioned HTML table label.
331
331
  *
332
332
  * Draw order mirrors C emit_html_tbl:
333
- * 1. Open anchor 2. Table BGCOLOR 3. Cells 4. Rules 5. Border 6. Close anchor
333
+ * 1. Open anchor 2. Table BGCOLOR 3. Rules 4. Border 5. Cells 6. Close anchor
334
334
  *
335
335
  * @see lib/common/htmltable.c:emit_html_tbl
336
336
  */
@@ -360,11 +360,13 @@ export function emitHtmlLabel(
360
360
  emitBgFill({ bgcolor: placed.bgcolor, box: placed.box, pos, border: placed.border,
361
361
  renderer, job, gradientangle: placed.gradientangle, style: placed.style });
362
362
  }
363
- for (const cell of placed.cells) emitHtmlCell(cell, pos, renderer, job);
363
+ // Graphviz 16.1 (f0d07786b): rules and border precede the cells so cell
364
+ // text is painted above table-level shapes.
364
365
  emitTableRules(placed, pos, renderer, job);
365
366
  if (placed.border > 0) {
366
367
  emitBorder({ box: placed.box, pos, border: placed.border, color: placed.color, sides: placed.sides, style: placed.style }, renderer, job);
367
368
  }
369
+ for (const cell of placed.cells) emitHtmlCell(cell, pos, renderer, job);
368
370
  }
369
371
  if (inAnchor) endHtmlAnchor(renderer, job);
370
372
  if (clustersLast && initHtmlAnchor(anchorData, tblBox, renderer, job)) {
@@ -19,7 +19,9 @@ import type { Graph } from '../model/graph.js';
19
19
  import { htmlEntityUTF8 } from './html-entities.js';
20
20
 
21
21
  export const DEFAULT_FONTSIZE = 14.0;
22
- export const DEFAULT_FONTNAME = 'Times,serif';
22
+ /** C's default fontname (non-Windows); renderers alias it, e.g. SVG
23
+ * font-family="Times,serif". @see lib/common/const.h:67 */
24
+ export const DEFAULT_FONTNAME = 'Times-Roman';
23
25
  export const DEFAULT_COLOR = 'black';
24
26
 
25
27
  // htmlEntityUTF8 moved to html-entities.ts (a leaf module) so the HTML lexer
@@ -53,7 +53,18 @@ function isMulticolor(fillcolor: string): boolean {
53
53
  // Periphery ring renderers
54
54
  // ---------------------------------------------------------------------------
55
55
 
56
- /** Render one ellipse periphery ring for a node. */
56
+ /**
57
+ * Render one ellipse periphery ring for a node. poly_gencode hands
58
+ * gvrender_ellipse the two ABSOLUTE corners AF = vertex + ND_coord; it passes
59
+ * the renderer the centre mid_pointf(AF[0], AF[1]) and the corner AF[1], and
60
+ * the renderer prints rx/ry as corner - centre. Computing the radii in that
61
+ * order (not from the relative vertices) keeps C's rounding: at |coord| where
62
+ * the double spacing exceeds the radius, coord ± r rounds back to coord and the
63
+ * radius collapses to 0 (corpus 2784, y = 1.08e18).
64
+ * @see lib/common/shapes.c:poly_gencode (AF[i] = P + ND_coord, gvrender_ellipse)
65
+ * @see lib/gvc/gvrender.c:gvrender_ellipse (af[0] = mid_pointf, af[1] = pf[1])
66
+ * @see plugin/core/gvrender_core_svg.c:svg_ellipse (rx = A[1].x - A[0].x)
67
+ */
57
68
  export function renderEllipse(
58
69
  ring: Point[],
59
70
  coord: Point,
@@ -61,9 +72,12 @@ export function renderEllipse(
61
72
  job: RenderJob,
62
73
  filled: boolean,
63
74
  ): void {
64
- const rx = Math.abs((ring[1]!.x - ring[0]!.x) / 2);
65
- const ry = Math.abs((ring[1]!.y - ring[0]!.y) / 2);
66
- const center = transformPoint(coord, job);
75
+ const af0 = { x: ring[0]!.x + coord.x, y: ring[0]!.y + coord.y };
76
+ const af1 = { x: ring[1]!.x + coord.x, y: ring[1]!.y + coord.y };
77
+ const mid = { x: (af0.x + af1.x) / 2.0, y: (af0.y + af1.y) / 2.0 };
78
+ const rx = af1.x - mid.x;
79
+ const ry = af1.y - mid.y;
80
+ const center = transformPoint(mid, job);
67
81
  renderer.ellipse(center, rx, ry, filled, job);
68
82
  }
69
83
 
package/src/gvc/device.ts CHANGED
@@ -12,10 +12,9 @@
12
12
  * @see lib/gvc/gvdevice.c
13
13
  */
14
14
 
15
- import type { Box } from '../model/geom.js';
16
- import { boxOverlap, POINTS_PER_INCH } from '../model/geom.js';
15
+ import { POINTS_PER_INCH } from '../model/geom.js';
17
16
  import { lateDouble } from '../common/nodeinit.js';
18
- import { parseDrawingSize, initJobViewportZoom, parseLandscape, parseGraphPad, parseGraphMargin } from './viewport.js';
17
+ import { parseDrawingSize, initJobViewportZoom, parseLandscape, parseGraphPad, parseGraphMargin, setupPageClip } from './viewport.js';
19
18
  import type { Graph } from '../model/graph.js';
20
19
  import type { Node } from '../model/node.js';
21
20
  import type { Edge } from '../model/edge.js';
@@ -51,6 +50,7 @@ import { renderOneLabel, renderNodeXLabel, renderClusterLabel } from './device-l
51
50
  export { transformPoint, applyRotation, applyScale, buildPoint } from './device-transform.js';
52
51
  export { renderOneLabel, renderNodeXLabel, renderClusterLabel } from './device-labels.js';
53
52
  import { svgNodeId, svgEdgeId, svgClusterId, svgGraphId } from '../render/svg-id.js';
53
+ import { nodeInBox, emitNodeComments, emitEdgeComments } from './emit-gates.js';
54
54
 
55
55
  // ---------------------------------------------------------------------------
56
56
  // AD-1 (image-api): `inlineImages` on RenderJob via module augmentation
@@ -78,43 +78,6 @@ function labelTextOf(lp: unknown): string | null {
78
78
  return (lp as TextlabelT | undefined)?.text ?? null;
79
79
  }
80
80
 
81
- /**
82
- * A node's own layout bounding box: coord ± (lw/rw, ht/2).
83
- * @see lib/common/types.h:ND_bb
84
- * @see lib/common/emit.c:4140-4143 init_bb_node
85
- */
86
- function nodeBBox(n: Node): Box {
87
- const { coord, lw, rw, ht } = n.info;
88
- return {
89
- ll: { x: coord.x - lw, y: coord.y - ht / 2 },
90
- ur: { x: coord.x + rw, y: coord.y + ht / 2 },
91
- };
92
- }
93
-
94
- /**
95
- * node_in_box: is the node's own bbox within job's page/view clip?
96
- *
97
- * C's `job->clip` is derived from `job->bb` (= `GD_bb(g)`) expanded by
98
- * `job->pad` on every side (init_job_viewport's `job->bb = gvc->bb ±
99
- * job->pad`, feeding `job->view`/`job->focus`, which setup_page's `job->clip`
100
- * algebraically reduces back to for the single-page, unpaginated case this
101
- * port implements — Z and rotation cancel out of that derivation). This port
102
- * has no separate `page=` pagination model, so `job.bb` padded by `job.pad`
103
- * IS `job->clip` here.
104
- *
105
- * @see lib/common/emit.c:1636-1639 node_in_box
106
- * @see lib/common/emit.c:1808 emit_node gate
107
- * @see lib/common/emit.c:3356 init_job_viewport (job->bb = gvc->bb ± pad)
108
- * @see lib/common/emit.c:1553-1564 setup_page (job->clip from focus/view)
109
- */
110
- function nodeInBox(n: Node, job: RenderJob): boolean {
111
- const clip: Box = {
112
- ll: { x: job.bb.ll.x - job.pad.x, y: job.bb.ll.y - job.pad.y },
113
- ur: { x: job.bb.ur.x + job.pad.x, y: job.bb.ur.y + job.pad.y },
114
- };
115
- return boxOverlap(nodeBBox(n), clip);
116
- }
117
-
118
81
  /**
119
82
  * Inner body of renderNode — runs inside the push/pop try block.
120
83
  * Extracted to keep renderNode under the 30-line hook limit.
@@ -150,6 +113,7 @@ export function renderNode(n: Node, renderer: RendererPlugin, job: RenderJob, do
150
113
  done.add(n);
151
114
  // "and is in page/view" gate. @see lib/common/emit.c:1808 node_in_box
152
115
  if (!nodeInBox(n, job)) return;
116
+ emitNodeComments(n, renderer, job); // before the invis shortcircuit, as C
153
117
  // Shortcircuit invisible nodes: C omits the whole node before emit_begin_node.
154
118
  // C reads late_string(n, N_style, ""), i.e. agxget — which resolves the
155
119
  // node's DECLARATION-SCOPE default, so `{ node [style=invis] p q }` hides p
@@ -201,6 +165,7 @@ function setEdgePen(e: Edge, job: RenderJob): void {
201
165
  }
202
166
 
203
167
  export function renderEdge(e: Edge, renderer: RendererPlugin, job: RenderJob): void {
168
+ emitEdgeComments(e, renderer, job); // before the invis shortcircuit, as C
204
169
  // Shortcircuit invisible edges: C omits the whole edge (no group/title/path)
205
170
  // before emit_begin_edge. @see lib/common/emit.c:emit_edge (style "invis" return)
206
171
  // The "invisible" alias likewise suppresses all edge draw ops via PEN_NONE
@@ -467,7 +432,9 @@ export function render(ctx: GvcContext, g: Graph, format: string, inlineImages =
467
432
  // altering the ptf path, carry Z in job.scale instead — which is exactly what
468
433
  // C's SVG group emits (svg_begin_page prints job->scale.x/y = zoom*dpi/72),
469
434
  // not job->zoom. job.scale is read only by the SVG group/dims.
470
- const z = initJobViewportZoom(job.bb, parseDrawingSize(g.attrs.get('size')), job.pad);
435
+ const drawingSize = parseDrawingSize(g.attrs.get('size'));
436
+ const z = initJobViewportZoom(job.bb, drawingSize, job.pad);
437
+ job.clip = setupPageClip(job.bb, drawingSize, job.pad, z); // @see emit.c:setup_page
471
438
  // init_job_dpi (emit.c:3333): GD_drawing->dpi = late_double(dpi, resolution, 0)
472
439
  // (input.c:713). When >0 it becomes job->dpi; otherwise the SVG device's
473
440
  // default_dpi = 72 (gvrender_core_svg.c:814) applies. init_job_viewport then
@@ -77,19 +77,36 @@ function emitEdgeLabel(
77
77
  emitState?: EmitState,
78
78
  ): void {
79
79
  if (!lp?.set) return; // emit_edge_label: lbl == NULL || !lbl->set
80
- const open = hs.url !== null || hs.explicit;
81
- if (open) openAnchorWith(renderer, job, hs.url, hs.tooltip, hs.target, id);
82
- // Head/tail labels route to EMIT_HLABEL/EMIT_TLABEL (→ _hldraw_/_tldraw_);
83
- // the center label keeps its default EMIT_ELABEL. @see emit.c:emit_edge
84
- const savedOverride = job.labelEmitOverride;
85
- if (emitState !== undefined) job.labelEmitOverride = emitState;
86
- try {
87
- renderOneLabel(lp, renderer, job);
88
- } finally {
89
- job.labelEmitOverride = savedOverride;
90
- }
91
- if (spl) emitAttachment(lp, spl, renderer, job); // emit.c:2918
92
- if (open) renderer.endAnchor?.(job);
80
+ withObjEmitState(job, emitState ?? EmitState.ELabel, () => {
81
+ const open = hs.url !== null || hs.explicit;
82
+ if (open) openAnchorWith(renderer, job, hs.url, hs.tooltip, hs.target, id);
83
+ // Head/tail labels route to EMIT_HLABEL/EMIT_TLABEL (→ _hldraw_/_tldraw_);
84
+ // the center label keeps its default EMIT_ELABEL. @see emit.c:emit_edge
85
+ const savedOverride = job.labelEmitOverride;
86
+ if (emitState !== undefined) job.labelEmitOverride = emitState;
87
+ try {
88
+ renderOneLabel(lp, renderer, job);
89
+ } finally {
90
+ job.labelEmitOverride = savedOverride;
91
+ }
92
+ if (spl) emitAttachment(lp, spl, renderer, job); // emit.c:2918
93
+ if (open) renderer.endAnchor?.(job);
94
+ });
95
+ }
96
+
97
+ /**
98
+ * emit_edge_label's emit-state window: C sets obj->emit_state = lkind before
99
+ * the anchor/label/attachment and restores the old state at the end, so the
100
+ * decorate attachment polyline lands in the label xbuf (_ldraw_), not _draw_.
101
+ * @see lib/common/emit.c:emit_edge_label (2909-2910, 2925)
102
+ */
103
+ function withObjEmitState(job: RenderJob, lkind: EmitState, fn: () => void): void {
104
+ const obj = job.obj;
105
+ if (obj === null) { fn(); return; }
106
+ const oldEmitState = obj.emitState;
107
+ obj.emitState = lkind;
108
+ fn();
109
+ obj.emitState = oldEmitState;
93
110
  }
94
111
 
95
112
  /**
@@ -0,0 +1,89 @@
1
+ // SPDX-License-Identifier: EPL-2.0
2
+
3
+ /**
4
+ * emit_node / emit_edge preambles: the node_in_box gate and the
5
+ * gvrender_comment calls C makes before an object's style=invis shortcircuit.
6
+ * Split from device.ts to keep that file under the size cap.
7
+ *
8
+ * @see lib/common/emit.c:emit_node, emit_edge
9
+ */
10
+
11
+ import type { Box } from '../model/geom.js';
12
+ import { boxOverlap } from '../model/geom.js';
13
+ import type { Node } from '../model/node.js';
14
+ import type { Edge } from '../model/edge.js';
15
+ import type { RendererPlugin } from './context.js';
16
+ import type { RenderJob } from './job.js';
17
+ import { nodeAttr } from '../common/poly-init.js';
18
+ import { edgeHasDrawableContent } from '../common/edge-in-box.js';
19
+
20
+ /**
21
+ * A node's own layout bounding box: coord ± (lw/rw, ht/2).
22
+ * @see lib/common/types.h:ND_bb
23
+ * @see lib/common/emit.c:4140-4143 init_bb_node
24
+ */
25
+ function nodeBBox(n: Node): Box {
26
+ const { coord, lw, rw, ht } = n.info;
27
+ return {
28
+ ll: { x: coord.x - lw, y: coord.y - ht / 2 },
29
+ ur: { x: coord.x + rw, y: coord.y + ht / 2 },
30
+ };
31
+ }
32
+
33
+ /**
34
+ * node_in_box: is the node's own bbox within job's page/view clip?
35
+ *
36
+ * C's `job->clip` is derived from `job->bb` (= `GD_bb(g)`) expanded by
37
+ * `job->pad` on every side (init_job_viewport's `job->bb = gvc->bb ±
38
+ * job->pad`, feeding `job->view`/`job->focus`, which setup_page's `job->clip`
39
+ * algebraically reduces back to for the single-page, unpaginated case this
40
+ * port implements — Z and rotation cancel out of that derivation). This port
41
+ * has no separate `page=` pagination model, so `job.bb` padded by `job.pad`
42
+ * IS `job->clip` here.
43
+ *
44
+ * @see lib/common/emit.c:1636-1639 node_in_box
45
+ * @see lib/common/emit.c:1808 emit_node gate
46
+ * @see lib/common/emit.c:3356 init_job_viewport (job->bb = gvc->bb ± pad)
47
+ * @see lib/common/emit.c:1553-1564 setup_page (job->clip from focus/view)
48
+ */
49
+ export function nodeInBox(n: Node, job: RenderJob): boolean {
50
+ const clip: Box = {
51
+ ll: { x: job.bb.ll.x - job.pad.x, y: job.bb.ll.y - job.pad.y },
52
+ ur: { x: job.bb.ur.x + job.pad.x, y: job.bb.ur.y + job.pad.y },
53
+ };
54
+ return boxOverlap(nodeBBox(n), clip);
55
+ }
56
+
57
+ /**
58
+ * gvrender_comment: pass a non-empty string to the renderer's comment hook
59
+ * (SVG is the only ported renderer that has one).
60
+ * @see lib/gvc/gvrender.c:gvrender_comment
61
+ */
62
+ function gvrenderComment(renderer: RendererPlugin, job: RenderJob, str: string | undefined): void {
63
+ if (str === undefined || str.length === 0) return;
64
+ renderer.comment?.(str, job);
65
+ }
66
+
67
+ /**
68
+ * The node's name and `comment` attribute, emitted after node_in_box and
69
+ * before the invis shortcircuit (an invisible node still gets its comments).
70
+ * @see lib/common/emit.c:emit_node (gvrender_comment x2)
71
+ */
72
+ export function emitNodeComments(n: Node, renderer: RendererPlugin, job: RenderJob): void {
73
+ gvrenderComment(renderer, job, n.name);
74
+ gvrenderComment(renderer, job, nodeAttr(n, n.root, 'comment'));
75
+ }
76
+
77
+ /**
78
+ * When edge_in_box passes: the `tail->head` name (`--` when the head's graph
79
+ * is undirected) and the `comment` attribute, before the invis shortcircuit.
80
+ * The renderers re-test the same gate for the edge group.
81
+ * @see lib/common/emit.c:emit_edge (gvrender_comment x2)
82
+ */
83
+ export function emitEdgeComments(e: Edge, renderer: RendererPlugin, job: RenderJob): void {
84
+ if (!edgeHasDrawableContent(e, job.bb)) return;
85
+ const kind = e.head.root.kind; // agisdirected(agraphof(aghead(e)))
86
+ const op = kind === 'directed' || kind === 'strict-directed' ? '->' : '--';
87
+ gvrenderComment(renderer, job, e.tail.name + op + e.head.name);
88
+ gvrenderComment(renderer, job, e.attrs.get('comment'));
89
+ }
package/src/gvc/job.ts CHANGED
@@ -275,6 +275,10 @@ export class RenderJob {
275
275
 
276
276
  bb: Box = { ll: { x: 0, y: 0 }, ur: { x: 0, y: 0 } };
277
277
  pad: Point = { x: 0, y: 0 };
278
+ /** Current page clip in graph units, as setup_page computes it from the
279
+ * viewport focus/view (see setupPageClip). Read by the SVG page translate and
280
+ * background. @see lib/gvc/gvcjob.h:GVJ_s.clip; lib/common/emit.c:setup_page */
281
+ clip: Box = { ll: { x: 0, y: 0 }, ur: { x: 0, y: 0 } };
278
282
  /** Graph-level `margin=` (init_job_margin); page-pagination margin, distinct
279
283
  * from node/cluster label margin. @see lib/common/emit.c:3309-3331 */
280
284
  margin: Point = { x: 0, y: 0 };
@@ -201,3 +201,32 @@ export function initJobViewportZoom(bb: Box, size: DrawingSize | null, pad: Poin
201
201
  if (drawingNeedsFit(size, szx, szy)) return Math.min(size.x / szx, size.y / szy);
202
202
  return 1.0;
203
203
  }
204
+
205
+ /**
206
+ * job->clip for the single-page devices this port drives, evaluated in C's
207
+ * order: init_job_viewport pads the bb (job->bb = bb ± pad, sz = job->bb.UR -
208
+ * job->bb.LL, size= substituting a degenerate sz axis), takes the focus as the
209
+ * centre of the UNPADDED bb and view = Z * sz; init_job_pagination sets
210
+ * pageSize = imageSize / zoom (imageSize = view, rotated to page orientation
211
+ * and back); setup_page sets clip.LL = focus + pageSize * (elem - size / 2.)
212
+ * with pagesArrayElem 0 / pagesArraySize 1 and clip.UR = clip.LL + pageSize.
213
+ * Algebraically bb ± pad, but not in floating point: where the pad is below the
214
+ * double spacing (corpus 2784, y = 2.16e18) clip.LL.y is 0, not -pad.
215
+ * @see lib/common/emit.c:init_job_viewport, init_job_pagination, setup_page
216
+ */
217
+ export function setupPageClip(bb: Box, size: DrawingSize | null, pad: Point, z: number): Box {
218
+ const jbLL = { x: bb.ll.x - pad.x, y: bb.ll.y - pad.y };
219
+ const jbUR = { x: bb.ur.x + pad.x, y: bb.ur.y + pad.y };
220
+ const sz = { x: jbUR.x - jbLL.x, y: jbUR.y - jbLL.y };
221
+ if (size !== null && size.x > 0.001 && size.y > 0.001) {
222
+ if (sz.x <= 0.001) sz.x = size.x;
223
+ if (sz.y <= 0.001) sz.y = size.y;
224
+ }
225
+ const focus = { x: 0.5 * (bb.ll.x + bb.ur.x), y: 0.5 * (bb.ll.y + bb.ur.y) };
226
+ const pageSize = { x: (z * sz.x) / z, y: (z * sz.y) / z };
227
+ const ll = {
228
+ x: focus.x + pageSize.x * (0 - 1 / 2.0),
229
+ y: focus.y + pageSize.y * (0 - 1 / 2.0),
230
+ };
231
+ return { ll, ur: { x: ll.x + pageSize.x, y: ll.y + pageSize.y } };
232
+ }
@@ -14,6 +14,12 @@ import type { Node } from '../../model/node.js';
14
14
  import type { Block, SubGraph, CircState, DerivedNode, DerivedEdge } from './blocks.js';
15
15
  import { FLAGS_ONPATH, FLAGS_ISPARENT } from './blocks.js';
16
16
  import { gvQsort } from '../../util/bsd-qsort.js';
17
+ import { derivedOf, spanIncident, reduceEdgeCrossings } from './crossings.js';
18
+
19
+ export {
20
+ derivedOf, closeOpenEdge, agfstedgeLists, countCrossings, insertNode, reducePass,
21
+ reduceEdgeCrossings, spanIncident,
22
+ } from './crossings.js';
17
23
 
18
24
  export type SpanNode = {
19
25
  dn: DerivedNode; tparent: SpanNode | null; visited: boolean;
@@ -35,135 +41,10 @@ export function blockGraph(allEdges: DerivedEdge[], sn: Block): void {
35
41
  }
36
42
  }
37
43
 
38
- // ---------------------------------------------------------------------------
39
- // Crossing counting helpers (each branch extracted to keep CCN low)
40
- // ---------------------------------------------------------------------------
41
-
42
- export function derivedOf(n: Node): DerivedNode | null {
43
- return (n.info.alg as { derivedNode?: DerivedNode } | undefined)?.derivedNode ?? null;
44
- }
45
-
46
- export function closeOpenEdge(e: DerivedEdge, d: DerivedNode, opened: DerivedEdge[]): number {
47
- let cross = 0;
48
- for (const o of opened) {
49
- if (o.order > e.order && o.tail !== d && o.head !== d) cross++;
50
- }
51
- return cross;
52
- }
53
-
54
- export function openEdgesAt(d: DerivedNode, edges: DerivedEdge[], opened: DerivedEdge[], order: number): void {
55
- for (const e of edges) {
56
- if (e.order === 0 && (e.tail === d || e.head === d)) { e.order = order; opened.push(e); }
57
- }
58
- }
59
-
60
- /**
61
- * C's count_all_crossings — INCLUDING its load-bearing bug: remove_edge keys
62
- * the open-edge dict by the half-edge POINTER handed to it, and the closing
63
- * node's agfstedge always yields the OPPOSITE half from the one stored when
64
- * the edge was opened, so dtdelete never matches and the list only grows
65
- * (circogen/edgelist.c:cmpItem on Agedge_t*). Each edge's close therefore
66
- * counts EVERY edge opened after it (nested as well as interleaved spans),
67
- * minus edges incident to the closing node — an inflated count that reduce()
68
- * uses to accept moves. Do not "fix" this to true interleave counting: the
69
- * final circle order depends on it.
70
- * @see lib/circogen/blockpath.c:count_all_crossings
71
- * @see lib/circogen/edgelist.c:remove_edge
72
- */
73
- export function countCrossings(list: Node[], edges: DerivedEdge[]): number {
74
- for (const e of edges) e.order = 0;
75
- const opened: DerivedEdge[] = [];
76
- let crossings = 0; let order = 1;
77
- for (const n of list) {
78
- const d = derivedOf(n);
79
- if (!d) continue;
80
- // Close pass: incident edges already opened at their other endpoint
81
- // (edges opened AT this node still have order 0 — the open pass below
82
- // runs after, so each edge closes exactly once).
83
- for (const e of edges) {
84
- if (e.order > 0 && (e.tail === d || e.head === d))
85
- crossings += closeOpenEdge(e, d, opened);
86
- }
87
- openEdgesAt(d, edges, opened, order++);
88
- }
89
- return crossings;
90
- }
91
-
92
- // ---------------------------------------------------------------------------
93
- // Crossing reduction
94
- // ---------------------------------------------------------------------------
95
-
96
- export function insertNode(list: Node[], cn: Node, neighbor: Node, pos: number): void {
97
- const i = list.indexOf(cn); if (i !== -1) list.splice(i, 1);
98
- const ni = list.indexOf(neighbor);
99
- if (ni === -1) { list.push(cn); return; }
100
- list.splice(pos === 0 ? ni : ni + 1, 0, cn);
101
- }
102
-
103
- /**
104
- * One reduce pass, mirroring C's reduce (blockpath.c:439): iterate the
105
- * SUBGRAPH's nodes in agfstnode (root-seq) order — NOT the current list
106
- * order — and each node's incident edges in agfstedge order; the neighbour
107
- * is agtail(e) unless that is curnode. The first-improving-move order is
108
- * load-bearing for the final arrangement.
109
- */
110
- export function reducePass(
111
- list: Node[], edges: DerivedEdge[], cnt: { v: number }, nodes: DerivedNode[],
112
- ): void {
113
- const ord = new Map<DerivedNode, number>();
114
- nodes.forEach((dn, i) => ord.set(dn, i));
115
- const seq = new Map<DerivedEdge, number>();
116
- edges.forEach((e, i) => seq.set(e, i));
117
- for (const dn of nodes) {
118
- for (const e of spanIncident(dn, edges, ord, seq)) {
119
- let neighbor = e.tail;
120
- if (neighbor === dn) neighbor = e.head;
121
- for (const pos of [0, 1] as const) {
122
- const copy = [...list];
123
- insertNode(list, dn.orig, neighbor.orig, pos);
124
- const nc = countCrossings(list, edges);
125
- if (nc < cnt.v) { cnt.v = nc; if (cnt.v === 0) return; }
126
- else { list.length = 0; list.push(...copy); }
127
- }
128
- }
129
- }
130
- }
131
-
132
- const CROSS_ITER = 10;
133
-
134
- /** @see lib/circogen/blockpath.c:reduce_edge_crossings */
135
- export function reduceEdgeCrossings(
136
- list: Node[], edges: DerivedEdge[], nodes: DerivedNode[],
137
- ): Node[] {
138
- const cnt = { v: countCrossings(list, edges) };
139
- for (let i = 0; i < CROSS_ITER && cnt.v > 0; i++) {
140
- const prev = cnt.v; reducePass(list, edges, cnt, nodes);
141
- if (prev === cnt.v) break;
142
- }
143
- return list;
144
- }
145
-
146
44
  // ---------------------------------------------------------------------------
147
45
  // Spanning tree
148
46
  // ---------------------------------------------------------------------------
149
47
 
150
- /**
151
- * Incident edges of dn in agfstedge order over the (possibly pruned)
152
- * subgraph edge set: out-edges (by head order, seq) then in-edges (by tail
153
- * order, seq). `ord`/`seq` maps carry the derived root's node and edge
154
- * creation orders. @see lib/cgraph/edge.c:agfstedge
155
- */
156
- export function spanIncident(
157
- dn: DerivedNode, edges: DerivedEdge[],
158
- ord: Map<DerivedNode, number>, seq: Map<DerivedEdge, number>,
159
- ): DerivedEdge[] {
160
- const out = edges.filter((e) => e.tail === dn)
161
- .sort((a, b) => ((ord.get(a.head) ?? 0) - (ord.get(b.head) ?? 0)) || ((seq.get(a) ?? 0) - (seq.get(b) ?? 0)));
162
- const inn = edges.filter((e) => e.head === dn)
163
- .sort((a, b) => ((ord.get(a.tail) ?? 0) - (ord.get(b.tail) ?? 0)) || ((seq.get(a) ?? 0) - (seq.get(b) ?? 0)));
164
- return [...out, ...inn];
165
- }
166
-
167
48
  export function dfsSpan(
168
49
  sn: SpanNode, map: Map<DerivedNode, SpanNode>, edges: DerivedEdge[],
169
50
  ord: Map<DerivedNode, number>, seq: Map<DerivedEdge, number>,