@knowvah/dot-engine 1.5.0 → 1.6.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 (45) hide show
  1. package/dist/api/geometry.d.ts +21 -3
  2. package/dist/api/geometry.d.ts.map +1 -1
  3. package/dist/api.js +4 -4
  4. package/dist/api.js.map +2 -2
  5. package/dist/cdt/splay.d.ts.map +1 -1
  6. package/dist/common/htmltable-emit-rules.d.ts.map +1 -1
  7. package/dist/common/shape-clip.d.ts +17 -0
  8. package/dist/common/shape-clip.d.ts.map +1 -0
  9. package/dist/index.js +601 -578
  10. package/dist/index.js.map +4 -4
  11. package/dist/layout/dot/edge-route.d.ts.map +1 -1
  12. package/dist/layout/dot/pack-components.d.ts.map +1 -1
  13. package/dist/layout/dot/sameport.d.ts +1 -13
  14. package/dist/layout/dot/sameport.d.ts.map +1 -1
  15. package/dist/layout/dot/self-loop.d.ts.map +1 -1
  16. package/dist/layout/dot/splines-groups.d.ts.map +1 -1
  17. package/dist/layout/neato/index.d.ts.map +1 -1
  18. package/dist/layout/neato/init.d.ts.map +1 -1
  19. package/dist/ortho/ortho-route.d.ts.map +1 -1
  20. package/dist/render/dot.d.ts.map +1 -1
  21. package/dist/render.js +1209 -1186
  22. package/dist/render.js.map +4 -4
  23. package/package.json +14 -5
  24. package/src/api/geometry.ts +25 -10
  25. package/src/cdt/splay.ts +0 -2
  26. package/src/common/arm-pow.ts +3 -3
  27. package/src/common/htmltable-emit-rules.ts +0 -10
  28. package/src/common/random.ts +2 -2
  29. package/src/common/shape-clip.ts +48 -0
  30. package/src/common/spline-midpoint.ts +1 -1
  31. package/src/layout/circo/circular.ts +1 -1
  32. package/src/layout/dot/edge-route.ts +18 -6
  33. package/src/layout/dot/ns.ts +1 -1
  34. package/src/layout/dot/pack-components.ts +0 -1
  35. package/src/layout/dot/sameport.ts +1 -25
  36. package/src/layout/dot/self-loop.ts +4 -2
  37. package/src/layout/dot/splines-flat-labeled.ts +1 -1
  38. package/src/layout/dot/splines-groups.ts +8 -1
  39. package/src/layout/neato/cdt-surface.ts +1 -1
  40. package/src/layout/neato/index.ts +0 -3
  41. package/src/layout/neato/init.ts +0 -1
  42. package/src/ortho/ortho-route.ts +1 -3
  43. package/src/ortho/trap-query.ts +1 -1
  44. package/src/render/dot.ts +3 -10
  45. package/src/vpsc/Solver.ts +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knowvah/dot-engine",
3
- "version": "1.5.0",
3
+ "version": "1.6.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",
@@ -51,12 +51,17 @@
51
51
  }
52
52
  },
53
53
  "scripts": {
54
+ "prebuild": "npm run parser:regen",
54
55
  "build": "npm run build:js && npm run build:types",
55
56
  "build:js": "esbuild src/index.ts --bundle --format=esm --sourcemap --outfile=dist/index.js && esbuild src/api/index.ts --bundle --format=esm --sourcemap --outfile=dist/api.js && esbuild src/render/index.ts --bundle --format=esm --sourcemap --outfile=dist/render.js",
56
57
  "build:types": "tsc -p tsconfig.build.json",
58
+ "pretest": "npm run parser:regen",
57
59
  "test": "vitest run",
60
+ "precoverage": "npm run parser:regen",
58
61
  "coverage": "vitest run --coverage",
62
+ "pretypecheck": "npm run parser:regen",
59
63
  "typecheck": "tsc --noEmit --stableTypeOrdering",
64
+ "pretypecheck:ts7": "npm run parser:regen",
60
65
  "typecheck:ts7": "tsgo --noEmit",
61
66
  "parser:regen": "peggy --format es --dts src/parser/dot.pegjs -o src/parser/dot.js",
62
67
  "survey:setup": "sh test/corpus/gen-headless-gvbindir.sh",
@@ -70,6 +75,7 @@
70
75
  "docs:copy-goldens": "node docs-site/copy-goldens.mjs",
71
76
  "docs:api": "typedoc",
72
77
  "docs:dev": "npm run docs:copy-reports && npm run docs:copy-goldens && npm run docs:api && vitepress dev docs-site",
78
+ "predocs:build": "npm run parser:regen",
73
79
  "docs:build": "npm run docs:copy-reports && npm run docs:copy-goldens && npm run docs:api && vitepress build docs-site",
74
80
  "docs:preview": "vitepress preview docs-site",
75
81
  "release": "semantic-release",
@@ -96,8 +102,8 @@
96
102
  "@types/fontkit": "^2.0.9",
97
103
  "@types/node": "^22.0.0",
98
104
  "@typescript/native-preview": "^7.0.0-dev.20260622.1",
99
- "@vitest/coverage-v8": "^4.1.9",
100
- "@xmldom/xmldom": "^0.9.10",
105
+ "@vitest/coverage-v8": "^4.1.11",
106
+ "@xmldom/xmldom": "^0.9.12",
101
107
  "esbuild": "^0.28.1",
102
108
  "fontkit": "^2.0.4",
103
109
  "peggy": "^5.0.0",
@@ -109,11 +115,14 @@
109
115
  "typedoc-vitepress-theme": "^1.1.3",
110
116
  "typescript": "^6.0.0",
111
117
  "vitepress": "^1.6.0",
112
- "vitest": "^4.1.9"
118
+ "vitest": "^4.1.11"
113
119
  },
114
120
  "overrides": {
115
121
  "postcss": "^8.5.25",
116
- "brace-expansion": "^5.0.9"
122
+ "brace-expansion": "^5.0.9",
123
+ "vitepress": {
124
+ "vite": "^6.4.3"
125
+ }
117
126
  },
118
127
  "volta": {
119
128
  "node": "26.3.1"
@@ -97,7 +97,10 @@ export interface NodeGeometry {
97
97
  *
98
98
  * `points` concatenates all bezier control points from the edge spline,
99
99
  * in order. An edge with no routed spline produces an empty `points` array.
100
- * `label` is present only when the edge carries a centre label.
100
+ * `label` is present only when the edge carries a centre label that layout
101
+ * placed. It carries the same `lp->set` gate as the labels below: dot's
102
+ * label search can leave a centre label unplaced (sentinel pos), and
103
+ * `render()` then declines to draw it.
101
104
  *
102
105
  * `tailLabel`/`headLabel` are the `taillabel`/`headlabel` port labels. They
103
106
  * are present only once the layout has actually *placed* them (C `lp->set`),
@@ -106,6 +109,12 @@ export interface NodeGeometry {
106
109
  * type) still holds calloc-zero coordinates, so it is reported as absent
107
110
  * rather than as a label at the origin.
108
111
  *
112
+ * `xlabel` is the `xlabel` external label, positioned by the force-placement
113
+ * search rather than by spline midpoint arithmetic, so it is not derivable
114
+ * from `label` or `points`. It carries the same `lp->set` gate: a declared
115
+ * xlabel the search could not fit is reported as absent, exactly as
116
+ * `render()` declines to draw it.
117
+ *
109
118
  * `sp`/`ep` are the arrow attachment points. When an end carries an arrow the
110
119
  * spline is shortened to leave room for it, and the arrow spans from the
111
120
  * terminal control point out to this point — so a consumer drawing its own
@@ -119,7 +128,8 @@ export interface NodeGeometry {
119
128
  * (measured: ~1.5pt at `penwidth=1`, ~6.2pt at `penwidth=5`), so `ep` is the
120
129
  * point to draw an arrow *to*, not a copy of the rendered polygon's tip.
121
130
  *
122
- * @see lib/common/types.h:ED_spl, ED_label, ED_tail_label, ED_head_label
131
+ * @see lib/common/types.h:ED_spl, ED_label, ED_tail_label, ED_head_label,
132
+ * ED_xlabel
123
133
  * @see lib/common/types.h:bezier (sflag/eflag, sp/ep)
124
134
  */
125
135
  export interface EdgeGeometry {
@@ -137,7 +147,7 @@ export interface EdgeGeometry {
137
147
  * @see lib/common/types.h:bezier.ep
138
148
  */
139
149
  ep?: { x: number; y: number };
140
- /** Centre edge label position, if present. @see lib/common/types.h:ED_label */
150
+ /** Centre edge label position, if placed. @see lib/common/types.h:ED_label */
141
151
  label?: { x: number; y: number };
142
152
  /**
143
153
  * `taillabel` position, if placed.
@@ -149,6 +159,11 @@ export interface EdgeGeometry {
149
159
  * @see lib/common/types.h:ED_head_label
150
160
  */
151
161
  headLabel?: { x: number; y: number };
162
+ /**
163
+ * `xlabel` (external label) position, if placed.
164
+ * @see lib/common/types.h:ED_xlabel
165
+ */
166
+ xlabel?: { x: number; y: number };
152
167
  }
153
168
 
154
169
  /**
@@ -261,7 +276,7 @@ function collectEdgePoints(
261
276
  }
262
277
 
263
278
  /**
264
- * Position of a port label that layout actually placed, else undefined.
279
+ * Position of an edge label that layout actually placed, else undefined.
265
280
  * `set` is C's own "this label has coordinates" flag, and the gate
266
281
  * emit_edge_label uses before drawing; an unplaced label still holds the
267
282
  * calloc-zero pos and must not be published as geometry.
@@ -301,8 +316,8 @@ function arrowAttachPoints(
301
316
 
302
317
  /**
303
318
  * Snapshot one edge's geometry.
304
- * @see lib/common/types.h:ED_spl, ED_label, ED_tail_label, ED_head_label
305
- * (textlabel_t.pos), bezier.sp/ep
319
+ * @see lib/common/types.h:ED_spl, ED_label, ED_tail_label, ED_head_label,
320
+ * ED_xlabel (textlabel_t.pos), bezier.sp/ep
306
321
  */
307
322
  function snapshotEdge(edge: Edge, flipY: (y: number) => number): EdgeGeometry {
308
323
  const geom: EdgeGeometry = {
@@ -310,14 +325,14 @@ function snapshotEdge(edge: Edge, flipY: (y: number) => number): EdgeGeometry {
310
325
  head: edge.head.name,
311
326
  points: collectEdgePoints(edge, flipY),
312
327
  };
313
- const lbl = edge.info.label;
314
- if (lbl !== undefined) {
315
- geom.label = { x: lbl.pos.x, y: flipY(lbl.pos.y) };
316
- }
328
+ const label = placedLabelPos(edge.info.label, flipY);
329
+ if (label !== undefined) geom.label = label;
317
330
  const tailLabel = placedLabelPos(edge.info.tail_label, flipY);
318
331
  if (tailLabel !== undefined) geom.tailLabel = tailLabel;
319
332
  const headLabel = placedLabelPos(edge.info.head_label, flipY);
320
333
  if (headLabel !== undefined) geom.headLabel = headLabel;
334
+ const xlabel = placedLabelPos(edge.info.xlabel, flipY);
335
+ if (xlabel !== undefined) geom.xlabel = xlabel;
321
336
  const { sp, ep } = arrowAttachPoints(edge, flipY);
322
337
  if (sp !== undefined) geom.sp = sp;
323
338
  if (ep !== undefined) geom.ep = ep;
package/src/cdt/splay.ts CHANGED
@@ -10,8 +10,6 @@
10
10
 
11
11
  import type { Comparator, KeyOf } from "./types.js";
12
12
  import {
13
- lrotate,
14
- rrotate,
15
13
  splay,
16
14
  splayMax,
17
15
  splayMin,
@@ -85,11 +85,11 @@ const POW_LOG_TABLE_BITS = 7;
85
85
  const N_LOG = 1 << POW_LOG_TABLE_BITS;
86
86
  const EXP_TABLE_BITS = 7;
87
87
  const N_EXP = 1 << EXP_TABLE_BITS;
88
- const OFF = 0x3fe6955500000000n;
88
+ const OFF = BigInt('0x3fe6955500000000');
89
89
 
90
90
  /** Bit patterns and sign bias for the pow special-case branch. */
91
- const INF_BITS = 0x7ff0000000000000n;
92
- const ONE_BITS = 0x3ff0000000000000n; // asuint64(1.0)
91
+ const INF_BITS = BigInt('0x7ff0000000000000');
92
+ const ONE_BITS = BigInt('0x3ff0000000000000'); // asuint64(1.0)
93
93
  /** SIGN_BIAS = 0x800 << EXP_TABLE_BITS. @see ARM pow.c */
94
94
  const SIGN_BIAS = 0x800 << EXP_TABLE_BITS;
95
95
 
@@ -218,16 +218,6 @@ export function resetHtmlAnchorIds(): void {
218
218
  anchorEnv.imgscale = 'false';
219
219
  }
220
220
 
221
- /** Normalise AnchorData to plain strings (empty when absent). */
222
- function normaliseAnchor(data: AnchorData): { h: string; t: string; tg: string; i: string } {
223
- return {
224
- h: data.href !== undefined ? data.href : '',
225
- t: data.title !== undefined ? data.title : '',
226
- tg: data.target !== undefined ? data.target : '',
227
- i: data.id !== undefined ? data.id : '',
228
- };
229
- }
230
-
231
221
  /**
232
222
  * Open an anchor when the data carries href/title/target.
233
223
  *
@@ -14,8 +14,8 @@
14
14
  * @see lib/neatogen/neatoinit.c:checkStart (srand48 call)
15
15
  */
16
16
 
17
- const A = 0x5deece66dn;
18
- const C = 0xbn;
17
+ const A = BigInt('0x5deece66d');
18
+ const C = BigInt('0xb');
19
19
  const MASK48 = (1n << 48n) - 1n;
20
20
  const TWO48 = 2 ** 48;
21
21
 
@@ -0,0 +1,48 @@
1
+ // SPDX-License-Identifier: EPL-2.0
2
+
3
+ /**
4
+ * shape_clip: clip a Bezier to a node's own shape boundary.
5
+ *
6
+ * Separate from splines-clip.ts (clip_and_install's home) only for file size;
7
+ * both drive the same node insidefn through bezierClip.
8
+ *
9
+ * @see lib/common/splines.c:shape_clip
10
+ */
11
+
12
+ import type { Point } from '../model/geom.js';
13
+ import type { Node } from '../model/node.js';
14
+ import { bezierClip } from './splines-geom.js';
15
+ import type { InsideContext } from './splines-geom.js';
16
+
17
+ type InsideFn = (c: InsideContext, p: Point) => boolean;
18
+ type ShapeHost = { shape?: { fns?: { insidefn?: InsideFn | null } | null } | null };
19
+
20
+ /**
21
+ * Clip curve to n's boundary in node-relative coordinates, then translate
22
+ * back; ND_rw is restored because some insidefns (record) scribble on it.
23
+ * @see lib/common/splines.c:shape_clip0
24
+ */
25
+ function shapeClip0(ctx: InsideContext, insidefn: InsideFn, n: Node, curve: Point[], leftInside: boolean): void {
26
+ const saveRealSize = n.info.rw;
27
+ const c = curve.map((p) => ({ x: p.x - n.info.coord.x, y: p.y - n.info.coord.y }));
28
+ bezierClip(ctx, insidefn, c, leftInside);
29
+ for (let i = 0; i < 4; i++) {
30
+ curve[i] = { x: c[i].x + n.info.coord.x, y: c[i].y + n.info.coord.y };
31
+ }
32
+ n.info.rw = saveRealSize;
33
+ }
34
+
35
+ /**
36
+ * Clip curve (4 control points, graph coordinates) to node n's shape, in
37
+ * place. Nodes whose shape has no insidefn are left untouched.
38
+ * @see lib/common/splines.c:shape_clip
39
+ */
40
+ export function shapeClip(n: Node, curve: Point[]): void {
41
+ const insidefn = (n.info as unknown as ShapeHost).shape?.fns?.insidefn ?? null;
42
+ if (insidefn === null) return;
43
+ const ctx: InsideContext = { nodeCoord: n.info.coord, rw: n.info.rw, bp: null, node: n };
44
+ const saveRealSize = n.info.rw;
45
+ const leftInside = insidefn(ctx, { x: curve[0].x - n.info.coord.x, y: curve[0].y - n.info.coord.y });
46
+ n.info.rw = saveRealSize;
47
+ shapeClip0(ctx, insidefn, n, curve, leftInside);
48
+ }
@@ -52,7 +52,7 @@ export function dotneatoClosest(spl: SplLike, pt: Point): Point {
52
52
  const c = [bz.list[j], bz.list[j + 1], bz.list[j + 2], bz.list[j + 3]];
53
53
  let lo = 0.0; let hi = 1.0;
54
54
  let dlo2 = dist2(c[0], pt); let dhi2 = dist2(c[3], pt);
55
- let pt2 = c[0];
55
+ let pt2: Point;
56
56
  for (;;) {
57
57
  const t = (lo + hi) / 2.0;
58
58
  pt2 = evalBez4(c, t);
@@ -182,7 +182,7 @@ export function circoLayout(g: Graph): void {
182
182
  dg.components = comps;
183
183
  let blockCount = 0;
184
184
  if (comps.length === 1) {
185
- blockCount = circularLayout(comps[0]!, g, blockCount, allEdges);
185
+ circularLayout(comps[0]!, g, blockCount, allEdges);
186
186
  copyPositions(comps[0]!);
187
187
  // C: adjustNodes(g) on the REAL graph — overlap=false resolves to
188
188
  // PRISM on the GTS reference build. @see circularinit.c:203
@@ -298,15 +298,27 @@ function hasSidePort(e: GraphEdge): boolean {
298
298
  return (tp.side ?? 0) !== 0 || (hp.side ?? 0) !== 0;
299
299
  }
300
300
 
301
- /** True when x is an unrouted adjacent same-rank side-port flat edge. */
301
+ /** C's make_flat_adj_edges port test: either end has a defined port, of any
302
+ * side (an interior HTML cell's port has side 0).
303
+ * @see lib/dotgen/dotsplines.c:make_flat_adj_edges (`ports = true`) */
304
+ function hasDefinedPort(e: GraphEdge): boolean {
305
+ return e.info.tail_port.defined || e.info.head_port.defined;
306
+ }
307
+
308
+ /** True when e is a same-rank adjacent flat edge with a defined port — the
309
+ * make_flat_adj_edges rotated-aux case. */
310
+ function isAdjacentPortFlat(e: GraphEdge, g: Graph): boolean {
311
+ return e.tail.info.rank !== undefined && e.tail.info.rank === e.head.info.rank
312
+ && isFlatAdjacent(g, e) && hasDefinedPort(e);
313
+ }
314
+
315
+ /** True when x is an unrouted adjacent same-rank flat edge with a port. */
302
316
  function isGroupableFlat(x: GraphEdge, g: Graph): boolean {
303
- return x.info.spl === undefined
304
- && x.tail.info.rank === x.head.info.rank
305
- && isFlatAdjacent(g, x) && hasSidePort(x);
317
+ return x.info.spl === undefined && isAdjacentPortFlat(x, g);
306
318
  }
307
319
 
308
320
  /**
309
- * Collect all unrouted adjacent same-rank side-port flat edges between e's two
321
+ * Collect all unrouted adjacent same-rank ported flat edges between e's two
310
322
  * endpoints (either direction), ordered so group[0].tail is the lower-order
311
323
  * (left) node. C's dispatch loop sorts edges by edgecmp (equivalent edges
312
324
  * contiguous) and groups a run while `getmainedge(e0) === getmainedge(e1)`
@@ -399,7 +411,7 @@ function routeFlatEdge(e: GraphEdge, g: Graph): boolean {
399
411
  // other edge, so only labeled flats are diverted. @see dotsplines.c:1527-1533
400
412
  if (makeFlatLabeledEdge(g, e)) return true;
401
413
  if (makeAdjFlatNoPortEdge(g, e)) return true;
402
- if (hasSidePort(e) && routeFaithfulSidePort(e, g)) return true;
414
+ if ((hasSidePort(e) || isAdjacentPortFlat(e, g)) && routeFaithfulSidePort(e, g)) return true;
403
415
  // Non-adjacent unlabeled flat with no side port (e.g. a same-rank cross-cluster
404
416
  // edge with an intervening node): C make_flat_edge routes it UP through the box
405
417
  // corridor, not straight through. @see lib/dotgen/dotsplines.c:make_flat_edge
@@ -98,7 +98,7 @@ export function leaveEdge(ctx: NsCtx): Edge | undefined {
98
98
  }
99
99
  ctx.sI++;
100
100
  }
101
- if (j > 0) { ctx.sI = 0; [rv, cnt] = leaveEdgeScan(ctx, j, cnt, rv); }
101
+ if (j > 0) { ctx.sI = 0; [rv] = leaveEdgeScan(ctx, j, cnt, rv); }
102
102
  return rv;
103
103
  }
104
104
 
@@ -37,7 +37,6 @@ import { dotRank, isACluster } from './rank.js';
37
37
  import { dotMincross } from './mincross.js';
38
38
  import { dotPosition } from './position.js';
39
39
  import { gvPostprocess } from '../../common/postproc.js';
40
- import { mapArrowOpPoints } from '../../common/arrows-shapes-util.js';
41
40
  import type { ArrowDrawOp } from '../../common/arrows-types.js';
42
41
 
43
42
  // ---------------------------------------------------------------------------
@@ -13,9 +13,7 @@ import type { Node } from '../../model/node.js';
13
13
  import type { Edge } from '../../model/edge.js';
14
14
  import type { Point, Port } from '../../model/geom.js';
15
15
  import { VIRTUAL, MC_SCALE } from './fastgr.js';
16
- import { nodeBoxOf } from './edge-route-helpers.js';
17
- import { bezierClipNode } from './edge-route-clip.js';
18
- import { nodeInsideFn } from './edge-route-routing.js';
16
+ import { shapeClip } from '../../common/shape-clip.js';
19
17
  import { nodesInSeq } from './decomp.js';
20
18
  import { cround } from '../../common/arith.js';
21
19
 
@@ -51,28 +49,6 @@ export function sameedge(same: SameGroup[], e: Edge, id: string): void {
51
49
  same.push({ id, edges: [e] });
52
50
  }
53
51
 
54
- // ---------------------------------------------------------------------------
55
- // shapeClip — move curve[0] to the node boundary
56
- // @see lib/dotgen/sameport.c — calls shape_clip(u, curve)
57
- // ---------------------------------------------------------------------------
58
-
59
- /**
60
- * Move curve[0] to node u's shape boundary along the Bézier, exactly as C's
61
- * shape_clip: build the node's shape inside-function (ellipse vs polygon, pen-
62
- * width aware) and bezier-clip with left_inside = insidefn(curve[0]). curve[0]
63
- * is the node center, which is always inside, so left_inside is true.
64
- *
65
- * Replaces an earlier rectangle approximation, which was wrong for ellipse
66
- * nodes (every honda head is an ellipse) and shifted the shared port ~17px.
67
- *
68
- * @see lib/common/splines.c:shape_clip / shape_clip0
69
- */
70
- export function shapeClip(u: Node, curve: Point[]): void {
71
- const box = nodeBoxOf(u, u.root);
72
- const clipped = bezierClipNode(curve, box.center.x, box.center.y, nodeInsideFn(box), true);
73
- for (let i = 0; i < curve.length && i < clipped.length; i++) curve[i] = clipped[i];
74
- }
75
-
76
52
  // ---------------------------------------------------------------------------
77
53
  // buildPort — construct the shared port for a group
78
54
  // @see lib/dotgen/sameport.c:sameport
@@ -79,9 +79,11 @@ function rankY(g: Graph, r: number): number {
79
79
  return g.info.rank![r].v[0].info.coord?.y ?? 0;
80
80
  }
81
81
 
82
- /** sizey when n is at the maximum rank. @see lib/dotgen/dotsplines.c:392-396 */
82
+ /** sizey when n is at the maximum rank. C's `r > 0` is a literal index test;
83
+ * after abomination C's rank is r - abomShift (AD-2 renumbers +1 where C uses
84
+ * index -1). @see lib/dotgen/dotsplines.c:392-396 */
83
85
  function sizeyAtMaxrank(g: Graph, n: Node, r: number): number {
84
- if (r > 0) return rankY(g, r - 1) - (n.info.coord?.y ?? 0);
86
+ if (r - (g.info.abomShift ?? 0) > 0) return rankY(g, r - 1) - (n.info.coord?.y ?? 0);
85
87
  return n.info.ht ?? 0;
86
88
  }
87
89
 
@@ -130,7 +130,7 @@ export function makeFlatLabeledEdge(g: Graph, e: Edge): boolean {
130
130
  const et = edgeType(g);
131
131
  if (et === EDGETYPE_LINE) {
132
132
  const ps = flatLabeledLinePoints(e.tail, e.head, e);
133
- if (ps !== null && ps.length !== 0) clipAndInstall(e, e.head, ps, ps.length, buildDotSinfo());
133
+ clipAndInstall(e, e.head, ps, ps.length, buildDotSinfo());
134
134
  return true;
135
135
  }
136
136
  // C make_flat_edge forward-normalizes a backward (tail right of head by node
@@ -112,8 +112,15 @@ function dispatchEdgeGroup(g: Graph, group: Edge[], ctx: GroupRouteCtx): void {
112
112
  }
113
113
  return;
114
114
  }
115
- if (nodeRankOf(e0.tail) === nodeRankOf(e0.head)) return;
116
115
  const uniq = dedupByOrig(group);
116
+ // Flat group: route HERE at its edgecmp position, as C's make_flat_edge call
117
+ // does, so it reads vnodes before later regular edges' recover_slack moves
118
+ // them. The flat routers gather their own group and skip routed members.
119
+ // @see lib/dotgen/dotsplines.c:dot_splines_ (410-411)
120
+ if (nodeRankOf(e0.tail) === nodeRankOf(e0.head)) {
121
+ for (const e of uniq) routeLoneEdge(e, g);
122
+ return;
123
+ }
117
124
  // Lone edge: route HERE at its edgecmp position (interleaved with groups), as C
118
125
  // does, so it reads recover_slack-moved vnodes correctly. @see root-cause.md
119
126
  if (uniq.length <= 1) {
@@ -286,7 +286,7 @@ class Cdt {
286
286
  let e2: CEdge | null = null; let e3: CEdge | null = null;
287
287
  if (f !== null) {
288
288
  const r = this.ringFrom(f, prev);
289
- v1 = r.v1; v2 = r.v2; v3 = r.v3; prev = r.e1; e2 = r.e2; e3 = r.e3;
289
+ v1 = r.v1; v2 = r.v2; v3 = r.v3; e2 = r.e2; e3 = r.e3;
290
290
  }
291
291
  while (f !== null) {
292
292
  const orient = this.orientPt(ox, oy, v3, x, y);
@@ -16,10 +16,7 @@ import type { LayoutEngine } from '../../gvc/context.js';
16
16
  import {
17
17
  neatoInitNode,
18
18
  userPos,
19
- setSeed,
20
19
  solveModel,
21
- neatoTranslate,
22
- neatoSetAspect,
23
20
  neatoCleanup,
24
21
  MODE_KK,
25
22
  MODE_MAJOR,
@@ -15,7 +15,6 @@ import type { Node } from '../../model/node.js';
15
15
  import type { Edge } from '../../model/edge.js';
16
16
  import type { VtxData } from './dijkstra.js';
17
17
  import {
18
- stressMajorizationKD,
19
18
  DFLT_ITERATIONS,
20
19
  MODEL_MDS as STRESS_MODEL_MDS,
21
20
  MODEL_CIRCUIT as STRESS_MODEL_CIRCUIT,
@@ -12,7 +12,7 @@ import type {
12
12
  OrthoSegment, Route, Cell, SGraph, SNode, SEdge,
13
13
  Channel, ChanDict, Maze,
14
14
  } from "./types.js";
15
- import { Bend, M_LEFT, M_RIGHT, M_TOP, M_BOTTOM } from "./types.js";
15
+ import { Bend } from "./types.js";
16
16
  import { chanSearch, chansInOrder } from "./maze-channels.js";
17
17
  import { makeGraph } from "./rawgraph.js";
18
18
  import { insertEdge, edgeExists, removeRedge, topSort } from "./rawgraph.js";
@@ -22,8 +22,6 @@ import { cround } from "../common/arith.js";
22
22
 
23
23
  // ─── weight constants ─────────────────────────────────────────────────────────
24
24
 
25
- const delta = 1; // weight per unit length
26
- const mu = 500; // bend penalty per bend edge
27
25
  export const BIG = 16384; // near-infinite weight
28
26
 
29
27
  // ─── Segment construction ─────────────────────────────────────────────────────
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import {
10
- T_X, T_Y, T_SINK, FIRSTPT, LASTPT,
10
+ T_X, T_Y, T_SINK, FIRSTPT,
11
11
  S_LEFT,
12
12
  fpEqual, equalTo, greaterThan, isValidTrap,
13
13
  cross, TRAP_MAX,
package/src/render/dot.ts CHANGED
@@ -19,14 +19,8 @@ import type { ArrowDrawOp } from '../common/arrows-types.js';
19
19
  import type { GVColor } from '../common/color.js';
20
20
  import { resolveRenderColor } from './color-resolve.js';
21
21
  import { edgeIsTapered } from './svg-tapered-edge.js';
22
- import { taper, taperfun } from '../common/taper.js';
23
- import { orthoRoundedRadius } from './svg-helpers.js';
24
- import { orthoRoundedPolylines } from './svg-edge-ortho-radius.js';
25
22
  import { findStopColor, parseStyleFlags } from '../common/style-resolve.js';
26
23
  import { parseGraphPad } from '../gvc/viewport.js';
27
- import { parseSegs } from '../common/multicolor.js';
28
- import { splitSplineByColor } from './svg-edge-split.js';
29
- import { buildOffsetLists, advanceTmpList } from '../common/edge-offset.js';
30
24
  import { edgeHasDrawableContent } from './svg.js';
31
25
  import type { Bezier } from '../model/geom.js';
32
26
  import type { RendererPlugin } from '../gvc/context.js';
@@ -41,11 +35,10 @@ import { renderEdgeLabels } from '../gvc/edge-labels.js';
41
35
  export * from './dot/xdot-ops.js';
42
36
  export * from './dot/attrs.js';
43
37
  import {
44
- NON_LINE_STYLES, XDOT_VERSION, agcanonEscape, escBackslash, gfmt5, linearGradientOp,
45
- lpStr, makeXbufs, radialGradientOp, trimFixed3, utf8Len, xdotFillColor, xdotFont,
46
- xdotId, xdotNum, xdotPenColor, xdotPoint, xdotPoints, xdotStrOp,
38
+ NON_LINE_STYLES, escBackslash, linearGradientOp,
39
+ makeXbufs, radialGradientOp, trimFixed3, utf8Len, xdotFillColor, xdotFont,
40
+ xdotNum, xdotPenColor, xdotPoint, xdotPoints, xdotStrOp,
47
41
  } from './dot/xdot-ops.js';
48
- import { isDirected } from './dot/attrs.js';
49
42
 
50
43
 
51
44
  // ---------------------------------------------------------------------------
@@ -237,7 +237,7 @@ export class IncVPSC extends VPSC {
237
237
  * @see lib/vpsc/solve_VPSC.cpp: IncVPSC::solve()
238
238
  */
239
239
  override solve(): void {
240
- let lastCost = Infinity;
240
+ let lastCost: number;
241
241
  let cost = this.bs.cost();
242
242
  do {
243
243
  lastCost = cost;