@knowvah/dot-engine 1.9.0 → 2.0.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 (175) hide show
  1. package/README.md +232 -30
  2. package/dist/api/builder.d.ts +3 -0
  3. package/dist/api/builder.d.ts.map +1 -1
  4. package/dist/api/edge-ops.d.ts +7 -0
  5. package/dist/api/edge-ops.d.ts.map +1 -1
  6. package/dist/api/geometry.d.ts +5 -2
  7. package/dist/api/geometry.d.ts.map +1 -1
  8. package/dist/api.js +159 -31
  9. package/dist/api.js.map +3 -3
  10. package/dist/async/collect.d.ts +49 -0
  11. package/dist/async/collect.d.ts.map +1 -0
  12. package/dist/async/fonts.d.ts +20 -0
  13. package/dist/async/fonts.d.ts.map +1 -0
  14. package/dist/async/render-async.d.ts +91 -0
  15. package/dist/async/render-async.d.ts.map +1 -0
  16. package/dist/async/render-into.d.ts +38 -0
  17. package/dist/async/render-into.d.ts.map +1 -0
  18. package/dist/async/sanitize.d.ts +28 -0
  19. package/dist/async/sanitize.d.ts.map +1 -0
  20. package/dist/common/htmltable-types.d.ts +3 -3
  21. package/dist/common/htmltable-types.d.ts.map +1 -1
  22. package/dist/common/make-label.d.ts.map +1 -1
  23. package/dist/common/poly-shapes.d.ts.map +1 -1
  24. package/dist/common/textmeasure-factory.d.ts +2 -0
  25. package/dist/common/textmeasure-factory.d.ts.map +1 -1
  26. package/dist/common/utils-inputscale.d.ts +19 -0
  27. package/dist/common/utils-inputscale.d.ts.map +1 -0
  28. package/dist/errors.d.ts +61 -5
  29. package/dist/errors.d.ts.map +1 -1
  30. package/dist/gvc/context.d.ts +36 -4
  31. package/dist/gvc/context.d.ts.map +1 -1
  32. package/dist/gvc/device.d.ts +5 -2
  33. package/dist/gvc/device.d.ts.map +1 -1
  34. package/dist/gvc/image-resolver.d.ts +16 -16
  35. package/dist/gvc/image-resolver.d.ts.map +1 -1
  36. package/dist/gvc/job.d.ts +1 -9
  37. package/dist/gvc/job.d.ts.map +1 -1
  38. package/dist/gvc/usershape.d.ts +2 -12
  39. package/dist/gvc/usershape.d.ts.map +1 -1
  40. package/dist/index.d.ts +27 -8
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +7560 -5446
  43. package/dist/index.js.map +4 -4
  44. package/dist/label/index.d.ts.map +1 -1
  45. package/dist/label/node.d.ts +0 -6
  46. package/dist/label/node.d.ts.map +1 -1
  47. package/dist/label/rectangle.d.ts +1 -7
  48. package/dist/label/rectangle.d.ts.map +1 -1
  49. package/dist/layout/circo/circular.d.ts +8 -5
  50. package/dist/layout/circo/circular.d.ts.map +1 -1
  51. package/dist/layout/dot/pack-components.d.ts +0 -19
  52. package/dist/layout/dot/pack-components.d.ts.map +1 -1
  53. package/dist/layout/dot/position.d.ts +7 -2
  54. package/dist/layout/dot/position.d.ts.map +1 -1
  55. package/dist/layout/fdp/derive.d.ts.map +1 -1
  56. package/dist/layout/fdp/index.d.ts.map +1 -1
  57. package/dist/layout/fdp/init.d.ts.map +1 -1
  58. package/dist/layout/fdp/layout.d.ts.map +1 -1
  59. package/dist/layout/fdp/ports.d.ts +0 -10
  60. package/dist/layout/fdp/ports.d.ts.map +1 -1
  61. package/dist/layout/fdp/xlayout.d.ts +0 -16
  62. package/dist/layout/fdp/xlayout.d.ts.map +1 -1
  63. package/dist/layout/neato/adjust-info.d.ts +117 -0
  64. package/dist/layout/neato/adjust-info.d.ts.map +1 -0
  65. package/dist/layout/neato/cdt-surface.d.ts.map +1 -1
  66. package/dist/layout/neato/constraint-adjust.d.ts +40 -0
  67. package/dist/layout/neato/constraint-adjust.d.ts.map +1 -0
  68. package/dist/layout/neato/edge-len.d.ts +20 -0
  69. package/dist/layout/neato/edge-len.d.ts.map +1 -0
  70. package/dist/layout/neato/fdp-adjust.d.ts +32 -4
  71. package/dist/layout/neato/fdp-adjust.d.ts.map +1 -1
  72. package/dist/layout/neato/index.d.ts +9 -6
  73. package/dist/layout/neato/index.d.ts.map +1 -1
  74. package/dist/layout/neato/init.d.ts +4 -11
  75. package/dist/layout/neato/init.d.ts.map +1 -1
  76. package/dist/layout/neato/kk-paths.d.ts +50 -0
  77. package/dist/layout/neato/kk-paths.d.ts.map +1 -0
  78. package/dist/layout/neato/kk-solve.d.ts +14 -0
  79. package/dist/layout/neato/kk-solve.d.ts.map +1 -0
  80. package/dist/layout/neato/kk.d.ts +44 -0
  81. package/dist/layout/neato/kk.d.ts.map +1 -0
  82. package/dist/layout/neato/multispline-router.d.ts.map +1 -1
  83. package/dist/layout/neato/poly.d.ts +50 -0
  84. package/dist/layout/neato/poly.d.ts.map +1 -0
  85. package/dist/layout/neato/sgd-dijkstra.d.ts +20 -0
  86. package/dist/layout/neato/sgd-dijkstra.d.ts.map +1 -0
  87. package/dist/layout/neato/sgd.d.ts +14 -9
  88. package/dist/layout/neato/sgd.d.ts.map +1 -1
  89. package/dist/layout/neato/start.d.ts +51 -0
  90. package/dist/layout/neato/start.d.ts.map +1 -0
  91. package/dist/layout/neato/vpsc-adjust.d.ts +21 -0
  92. package/dist/layout/neato/vpsc-adjust.d.ts.map +1 -0
  93. package/dist/layout/sfdp/index.d.ts.map +1 -1
  94. package/dist/layout/sfdp/init.d.ts +0 -9
  95. package/dist/layout/sfdp/init.d.ts.map +1 -1
  96. package/dist/layout/sfdp/spring-driver.d.ts.map +1 -1
  97. package/dist/layout/twopi/circle.d.ts +0 -7
  98. package/dist/layout/twopi/circle.d.ts.map +1 -1
  99. package/dist/ortho/ortho-parallel.d.ts.map +1 -1
  100. package/dist/ortho/trap-query.d.ts.map +1 -1
  101. package/dist/parser/index.d.ts +9 -5
  102. package/dist/parser/index.d.ts.map +1 -1
  103. package/dist/render/index.d.ts +2 -0
  104. package/dist/render/index.d.ts.map +1 -1
  105. package/dist/render/public.d.ts +9 -3
  106. package/dist/render/public.d.ts.map +1 -1
  107. package/dist/render/xdot-public.d.ts +7 -2
  108. package/dist/render/xdot-public.d.ts.map +1 -1
  109. package/dist/render.js +7355 -5476
  110. package/dist/render.js.map +4 -4
  111. package/dist/util/xml.d.ts.map +1 -1
  112. package/dist/vpsc/Solver.d.ts +1 -0
  113. package/dist/vpsc/Solver.d.ts.map +1 -1
  114. package/package.json +1 -1
  115. package/src/api/builder.ts +73 -5
  116. package/src/api/edge-ops.ts +19 -0
  117. package/src/api/geometry.ts +22 -5
  118. package/src/async/collect.ts +204 -0
  119. package/src/async/fonts.ts +61 -0
  120. package/src/async/render-async.ts +205 -0
  121. package/src/async/render-into.ts +115 -0
  122. package/src/async/sanitize.ts +150 -0
  123. package/src/common/htmltable-types.ts +4 -5
  124. package/src/common/make-label.ts +10 -1
  125. package/src/common/poly-shapes.ts +4 -1
  126. package/src/common/textmeasure-factory.ts +11 -0
  127. package/src/common/utils-inputscale.ts +31 -0
  128. package/src/errors.ts +188 -5
  129. package/src/gvc/context.ts +96 -17
  130. package/src/gvc/device.ts +11 -6
  131. package/src/gvc/image-resolver.ts +25 -2
  132. package/src/gvc/job.ts +3 -1
  133. package/src/gvc/usershape.ts +6 -0
  134. package/src/index.ts +59 -43
  135. package/src/label/index.ts +6 -2
  136. package/src/label/node.ts +2 -1
  137. package/src/label/rectangle.ts +4 -2
  138. package/src/layout/circo/circular.ts +9 -6
  139. package/src/layout/dot/pack-components.ts +6 -2
  140. package/src/layout/dot/position.ts +21 -3
  141. package/src/layout/fdp/derive.ts +7 -6
  142. package/src/layout/fdp/index.ts +45 -5
  143. package/src/layout/fdp/init.ts +7 -6
  144. package/src/layout/fdp/layout.ts +2 -1
  145. package/src/layout/fdp/ports.ts +3 -2
  146. package/src/layout/fdp/xlayout.ts +3 -1
  147. package/src/layout/neato/adjust-info.ts +332 -0
  148. package/src/layout/neato/cdt-surface.ts +50 -30
  149. package/src/layout/neato/constraint-adjust.ts +466 -0
  150. package/src/layout/neato/edge-len.ts +36 -0
  151. package/src/layout/neato/fdp-adjust.ts +117 -12
  152. package/src/layout/neato/index.ts +36 -54
  153. package/src/layout/neato/init.ts +21 -36
  154. package/src/layout/neato/kk-paths.ts +146 -0
  155. package/src/layout/neato/kk-solve.ts +64 -0
  156. package/src/layout/neato/kk.ts +322 -0
  157. package/src/layout/neato/multispline-router.ts +4 -3
  158. package/src/layout/neato/poly.ts +493 -0
  159. package/src/layout/neato/sgd-dijkstra.ts +125 -0
  160. package/src/layout/neato/sgd.ts +63 -40
  161. package/src/layout/neato/start.ts +222 -0
  162. package/src/layout/neato/vpsc-adjust.ts +93 -0
  163. package/src/layout/sfdp/index.ts +4 -5
  164. package/src/layout/sfdp/init.ts +40 -4
  165. package/src/layout/sfdp/spring-driver.ts +30 -1
  166. package/src/layout/twopi/circle.ts +2 -1
  167. package/src/ortho/ortho-parallel.ts +2 -1
  168. package/src/ortho/trap-query.ts +2 -1
  169. package/src/parser/index.ts +11 -11
  170. package/src/render/index.ts +5 -0
  171. package/src/render/public.ts +24 -24
  172. package/src/render/svg.ts +1 -1
  173. package/src/render/xdot-public.ts +19 -18
  174. package/src/util/xml.ts +24 -30
  175. package/src/vpsc/Solver.ts +5 -3
@@ -0,0 +1,466 @@
1
+ // SPDX-License-Identifier: EPL-2.0
2
+
3
+ /**
4
+ * Orthogonal-ordering overlap removal — cAdjust (overlap=ortho*, portho*).
5
+ *
6
+ * Builds horizontal/vertical constraint graphs over the node boxes and solves
7
+ * them with network simplex (a linear objective, so `rank` is reused). Based
8
+ * on Marriott, Stuckey, Tam and He, "Removing Node Overlapping in Graph Layout
9
+ * Using Constrained Optimization", Constraints 8(2):143-172, 2003 (per the C
10
+ * source comment). All geometry is integer points scaled by SCALE (10).
11
+ *
12
+ * Deviations from C, all structural (results identical):
13
+ * - the visibility graph `vg` of mkConstraintG is a per-item adjacency array
14
+ * (C builds a strict cgraph whose out-edge order — by head sequence — equals
15
+ * the ascending list order used here);
16
+ * - cAdjust's `switch` is a mode -> plan table (same eight cases plus the
17
+ * PORTHO default);
18
+ * - constrainX/constrainY share `buildConstraintGraph` for the ortho choice.
19
+ *
20
+ * @see lib/neatogen/constraint.c:cAdjust
21
+ */
22
+
23
+ import { Graph } from '../../model/graph.js';
24
+ import { Node } from '../../model/node.js';
25
+ import { Edge } from '../../model/edge.js';
26
+ import type { EdgeList } from '../../model/nodeInfo.js';
27
+ import { DtBag } from '../../cdt/index.js';
28
+ import { cround } from '../../common/arith.js';
29
+ import { rank } from '../dot/ns.js';
30
+ import { elistAppend } from '../dot/fastgr.js';
31
+ import { nodesInSeq } from '../dot/decomp.js';
32
+ import { sepFactor } from './sep-factor.js';
33
+ import type { ExpandT } from './sep-factor.js';
34
+
35
+ /** @see lib/neatogen/adjust.h:adjust_mode */
36
+ export const AM_ORTHO = 5;
37
+ export const AM_ORTHO_YX = 6;
38
+ export const AM_ORTHOXY = 7;
39
+ export const AM_ORTHOYX = 8;
40
+ export const AM_PORTHO = 9;
41
+ export const AM_PORTHO_YX = 10;
42
+ export const AM_PORTHOXY = 11;
43
+ export const AM_PORTHOYX = 12;
44
+
45
+ /** For precision, scale up before the algorithms, then scale down. */
46
+ const SCALE = 10;
47
+ const SCALE2 = SCALE / 2;
48
+ /** C `INT_MAX` (rank's maxiter) and `-INT_MAX` (mkConstraintG's sentinel). */
49
+ const INT_MAX = 2147483647;
50
+ /** @see lib/common/geom.h:POINTS_PER_INCH */
51
+ const POINTS_PER_INCH = 72;
52
+
53
+ interface Pt { x: number; y: number }
54
+ interface Box { llx: number; lly: number; urx: number; ury: number }
55
+
56
+ /** @see lib/neatogen/constraint.c:nitem */
57
+ interface Nitem {
58
+ /** Sort key (the dtdisc key, `offsetof(nitem, val)`). */
59
+ val: number;
60
+ /** Position for sorting (integer scaled points). */
61
+ pos: Pt;
62
+ /** Base node. */
63
+ np: Node;
64
+ /** Corresponding node in the constraint graph. */
65
+ cnode: Node | undefined;
66
+ bb: Box;
67
+ }
68
+
69
+ type DistFn = (b1: Box, b2: Box) => number;
70
+ type IntersectFn = (p: Nitem, q: Nitem) => boolean;
71
+ type Items = DtBag<Nitem, number>;
72
+
73
+ /** Constraint graph plus the strict-graph edge index (agedge find/create). */
74
+ interface ConstraintGraph {
75
+ g: Graph;
76
+ edges: Map<Node, Map<Node, Edge>>;
77
+ }
78
+
79
+ /** @see lib/neatogen/constraint.c:cmpitem */
80
+ function cmpitem(a: number, b: number): number {
81
+ if (a < b) return -1;
82
+ if (a > b) return 1;
83
+ return 0;
84
+ }
85
+
86
+ /** C int division truncates toward zero. */
87
+ function idiv2(v: number): number {
88
+ return Math.trunc(v / 2);
89
+ }
90
+
91
+ /** @see lib/neatogen/constraint.c:distY */
92
+ function distY(b1: Box, b2: Box): number {
93
+ return idiv2(b1.ury - b1.lly + (b2.ury - b2.lly));
94
+ }
95
+
96
+ /** @see lib/neatogen/constraint.c:distX */
97
+ function distX(b1: Box, b2: Box): number {
98
+ return idiv2(b1.urx - b1.llx + (b2.urx - b2.llx));
99
+ }
100
+
101
+ /**
102
+ * True if boxes could overlap if shifted in y but don't, or if they overlap and
103
+ * a y move is smallest. Assumes q pos above p pos.
104
+ * @see lib/neatogen/constraint.c:intersectX0
105
+ */
106
+ function intersectX0(p: Nitem, q: Nitem): boolean {
107
+ if (!(p.bb.llx <= q.bb.urx && q.bb.llx <= p.bb.urx)) return false;
108
+ if (p.bb.ury < q.bb.lly) return true;
109
+ const ydelta = distY(p.bb, q.bb) - (q.pos.y - p.pos.y);
110
+ const dx = q.pos.x >= p.pos.x ? q.pos.x - p.pos.x : p.pos.x - q.pos.x;
111
+ const xdelta = distX(p.bb, q.bb) - dx;
112
+ return ydelta <= xdelta;
113
+ }
114
+
115
+ /**
116
+ * True if boxes could overlap if shifted in x but don't, or if they overlap and
117
+ * an x move is smallest. Assumes q pos right of p pos.
118
+ * @see lib/neatogen/constraint.c:intersectY0
119
+ */
120
+ function intersectY0(p: Nitem, q: Nitem): boolean {
121
+ if (!(p.bb.lly <= q.bb.ury && q.bb.lly <= p.bb.ury)) return false;
122
+ if (p.bb.urx < q.bb.llx) return true;
123
+ const xdelta = distX(p.bb, q.bb) - (q.pos.x - p.pos.x);
124
+ const dy = q.pos.y >= p.pos.y ? q.pos.y - p.pos.y : p.pos.y - q.pos.y;
125
+ const ydelta = distY(p.bb, q.bb) - dy;
126
+ return xdelta <= ydelta;
127
+ }
128
+
129
+ /** @see lib/neatogen/constraint.c:intersectY */
130
+ function intersectY(p: Nitem, q: Nitem): boolean {
131
+ return p.bb.lly <= q.bb.ury && q.bb.lly <= p.bb.ury;
132
+ }
133
+
134
+ /** @see lib/neatogen/constraint.c:intersectX */
135
+ function intersectX(p: Nitem, q: Nitem): boolean {
136
+ return p.bb.llx <= q.bb.urx && q.bb.llx <= p.bb.urx;
137
+ }
138
+
139
+ /** dtflatten order: ascending key, bag-internal order among equal keys. */
140
+ function flatten(list: Items): Nitem[] {
141
+ const out: Nitem[] = [];
142
+ for (let p = list.first(); p !== undefined; p = list.next(p)) out.push(p);
143
+ return out;
144
+ }
145
+
146
+ function newCg(): ConstraintGraph {
147
+ return { g: new Graph('cg', 'strict-directed'), edges: new Map() };
148
+ }
149
+
150
+ /** agnode + agbindrec + alloc_elist for a constraint-graph node. */
151
+ function newCgNode(cg: ConstraintGraph, name: string): Node {
152
+ const n = new Node(cg.g.nodes.size, name, cg.g);
153
+ cg.g.nodes.set(name, n);
154
+ n.info.in = { list: [], size: 0 };
155
+ n.info.out = { list: [], size: 0 };
156
+ return n;
157
+ }
158
+
159
+ /** Append n to GD_nlist(cg). */
160
+ function linkNode(cg: ConstraintGraph, last: Node | undefined, n: Node): Node {
161
+ if (last === undefined) cg.g.info.nlist = n;
162
+ else last.info.next = n;
163
+ return n;
164
+ }
165
+
166
+ /** agedge(cg, t, h, NULL, 1): strict, returns the existing edge if any. */
167
+ function cgEdge(cg: ConstraintGraph, t: Node, h: Node): Edge {
168
+ let heads = cg.edges.get(t);
169
+ if (heads === undefined) {
170
+ heads = new Map();
171
+ cg.edges.set(t, heads);
172
+ }
173
+ let e = heads.get(h);
174
+ if (e === undefined) {
175
+ e = new Edge(t, h, '');
176
+ e.info.minlen = 0;
177
+ heads.set(h, e);
178
+ }
179
+ return e;
180
+ }
181
+
182
+ function listOf(l: EdgeList | undefined): EdgeList {
183
+ if (l === undefined) throw new Error('constraint graph node has no edge list');
184
+ return l;
185
+ }
186
+
187
+ /** elist_append(e, ND_out(t)); elist_append(e, ND_in(h)). */
188
+ function linkEdge(e: Edge): void {
189
+ elistAppend(listOf(e.tail.info.out), e);
190
+ elistAppend(listOf(e.head.info.in), e);
191
+ }
192
+
193
+ function cnodeOf(p: Nitem): Node {
194
+ if (p.cnode === undefined) throw new Error('nitem has no constraint node');
195
+ return p.cnode;
196
+ }
197
+
198
+ /**
199
+ * agfindedge(g, a, b): an a->b edge, or b->a when g is undirected.
200
+ * @see lib/cgraph/edge.c:agedge
201
+ */
202
+ function makeHasEdge(g: Graph): (a: Node, b: Node) => boolean {
203
+ const undirected = g.root.kind === 'undirected' || g.root.kind === 'strict-undirected';
204
+ const pairs = new Map<Node, Set<Node>>();
205
+ for (const e of g.edges) {
206
+ const s = pairs.get(e.tail) ?? new Set<Node>();
207
+ s.add(e.head);
208
+ pairs.set(e.tail, s);
209
+ }
210
+ return (a, b) => pairs.get(a)?.has(b) === true || (undirected && pairs.get(b)?.has(a) === true);
211
+ }
212
+
213
+ /**
214
+ * For every item pair, add a constraint when `intersect` holds; no orthogonal
215
+ * ordering is enforced. Edges that exist in `g` get weight 100.
216
+ * @see lib/neatogen/constraint.c:mkNConstraintG
217
+ */
218
+ function mkNConstraintG(g: Graph, list: Items, intersect: IntersectFn, dist: DistFn): ConstraintGraph {
219
+ const cg = newCg();
220
+ const flat = flatten(list);
221
+ let lastn: Node | undefined;
222
+ for (const p of flat) {
223
+ const n = newCgNode(cg, p.np.name);
224
+ p.cnode = n;
225
+ lastn = linkNode(cg, lastn, n);
226
+ }
227
+ const hasEdge = makeHasEdge(g);
228
+ const created: Edge[] = [];
229
+ for (let i = 0; i < flat.length; i++) {
230
+ const p = flat[i]!;
231
+ for (let j = i + 1; j < flat.length; j++) {
232
+ const nxp = flat[j]!;
233
+ if (!intersect(p, nxp)) continue;
234
+ const e = cgEdge(cg, cnodeOf(p), cnodeOf(nxp));
235
+ e.info.minlen = dist(p.bb, nxp.bb);
236
+ e.info.weight = hasEdge(p.np, nxp.np) ? 100 : 1;
237
+ created.push(e);
238
+ }
239
+ }
240
+ // C walks each node's cgraph out-edges (by head sequence); creation order
241
+ // above is already tail-major and head-ascending, which is that order.
242
+ for (const e of created) linkEdge(e);
243
+ return cg;
244
+ }
245
+
246
+ /**
247
+ * Chain one node per distinct val, in order, joined by SCALE-length edges.
248
+ * @see lib/neatogen/constraint.c:mkConstraintG (basic chain)
249
+ */
250
+ function buildChain(cg: ConstraintGraph, flat: Nitem[]): void {
251
+ let oldval = -INT_MAX;
252
+ let prev: Node | undefined;
253
+ let n: Node | undefined;
254
+ let lastn: Node | undefined;
255
+ for (const p of flat) {
256
+ if (oldval !== p.val) {
257
+ oldval = p.val;
258
+ n = newCgNode(cg, p.np.name);
259
+ lastn = linkNode(cg, lastn, n);
260
+ if (prev !== undefined) {
261
+ const e = cgEdge(cg, prev, n);
262
+ e.info.minlen = SCALE;
263
+ e.info.weight = 1;
264
+ linkEdge(e);
265
+ }
266
+ prev = n;
267
+ }
268
+ p.cnode = n;
269
+ }
270
+ }
271
+
272
+ /**
273
+ * Visibility graph as per-item adjacency (head indices ascending).
274
+ * @see lib/neatogen/constraint.c:mkConstraintG (immediate right neighbours)
275
+ */
276
+ function buildVisibility(flat: Nitem[], intersect: IntersectFn): number[][] {
277
+ const vg: number[][] = flat.map(() => []);
278
+ let oldval = -INT_MAX;
279
+ let nxt = flat.length;
280
+ for (let i = 0; i < flat.length; i++) {
281
+ const p = flat[i]!;
282
+ if (oldval !== p.val) {
283
+ oldval = p.val;
284
+ nxt = i + 1;
285
+ while (nxt < flat.length && flat[nxt]!.val === oldval) nxt++;
286
+ if (nxt >= flat.length) break;
287
+ }
288
+ for (let j = nxt; j < flat.length; j++) {
289
+ if (intersect(p, flat[j]!)) vg[i]!.push(j);
290
+ }
291
+ }
292
+ return vg;
293
+ }
294
+
295
+ /**
296
+ * Add a constraint edge per visibility edge, with minlen = dist.
297
+ * @see lib/neatogen/constraint.c:mapGraphs
298
+ */
299
+ function mapGraphs(vg: number[][], flat: Nitem[], cg: ConstraintGraph, dist: DistFn): void {
300
+ for (let i = 0; i < flat.length; i++) {
301
+ const tp = flat[i]!;
302
+ for (const j of vg[i]!) {
303
+ const hp = flat[j]!;
304
+ const delta = dist(tp.bb, hp.bb);
305
+ const ce = cgEdge(cg, cnodeOf(tp), cnodeOf(hp));
306
+ ce.info.weight = 1;
307
+ if ((ce.info.minlen ?? 0) < delta) {
308
+ if (ce.info.minlen === 0) linkEdge(ce);
309
+ ce.info.minlen = delta;
310
+ }
311
+ }
312
+ }
313
+ }
314
+
315
+ /** @see lib/neatogen/constraint.c:mkConstraintG */
316
+ function mkConstraintG(list: Items, intersect: IntersectFn, dist: DistFn): ConstraintGraph {
317
+ const cg = newCg();
318
+ const flat = flatten(list);
319
+ buildChain(cg, flat);
320
+ mapGraphs(buildVisibility(flat, intersect), flat, cg, dist);
321
+ return cg;
322
+ }
323
+
324
+ function buildConstraintGraph(
325
+ g: Graph, list: Items, ifn: IntersectFn, dist: DistFn, ortho: boolean,
326
+ ): ConstraintGraph {
327
+ return ortho ? mkConstraintG(list, ifn, dist) : mkNConstraintG(g, list, ifn, dist);
328
+ }
329
+
330
+ function newItems(): Items {
331
+ return new DtBag<Nitem, number>((p) => p.val, cmpitem);
332
+ }
333
+
334
+ /**
335
+ * Create the X constraints and solve (linear objective, so network simplex).
336
+ * @see lib/neatogen/constraint.c:constrainX
337
+ */
338
+ function constrainX(g: Graph, nlist: Nitem[], ifn: IntersectFn, ortho: boolean): void {
339
+ const list = newItems();
340
+ for (const p of nlist) {
341
+ p.val = p.pos.x;
342
+ list.insert(p);
343
+ }
344
+ const cg = buildConstraintGraph(g, list, ifn, distX, ortho);
345
+ rank(cg.g, 2, INT_MAX);
346
+ for (const p of nlist) {
347
+ const newpos = cnodeOf(p).info.rank ?? 0;
348
+ const delta = newpos - p.pos.x;
349
+ p.pos.x = newpos;
350
+ p.bb.llx += delta;
351
+ p.bb.urx += delta;
352
+ }
353
+ }
354
+
355
+ /**
356
+ * See constrainX.
357
+ * @see lib/neatogen/constraint.c:constrainY
358
+ */
359
+ function constrainY(g: Graph, nlist: Nitem[], ifn: IntersectFn, ortho: boolean): void {
360
+ const list = newItems();
361
+ for (const p of nlist) {
362
+ p.val = p.pos.y;
363
+ list.insert(p);
364
+ }
365
+ const cg = buildConstraintGraph(g, list, ifn, distY, ortho);
366
+ rank(cg.g, 2, INT_MAX);
367
+ for (const p of nlist) {
368
+ const newpos = cnodeOf(p).info.rank ?? 0;
369
+ const delta = newpos - p.pos.y;
370
+ p.pos.y = newpos;
371
+ p.bb.lly += delta;
372
+ p.bb.ury += delta;
373
+ }
374
+ }
375
+
376
+ /** @see lib/neatogen/constraint.c:overlaps (geom.h OVERLAP) */
377
+ function overlaps(p: Nitem[]): boolean {
378
+ for (let i = 0; i < p.length - 1; i++) {
379
+ const a = p[i]!.bb;
380
+ for (let j = i + 1; j < p.length; j++) {
381
+ const b = p[j]!.bb;
382
+ if (a.urx >= b.llx && b.urx >= a.llx && a.ury >= b.lly && b.ury >= a.lly) return true;
383
+ }
384
+ }
385
+ return false;
386
+ }
387
+
388
+ /** @see lib/neatogen/constraint.c:initItem */
389
+ function initItem(n: Node, margin: ExpandT): Nitem {
390
+ const x = cround(SCALE * (n.info.pos?.[0] ?? 0) * POINTS_PER_INCH);
391
+ const y = cround(SCALE * (n.info.pos?.[1] ?? 0) * POINTS_PER_INCH);
392
+ const width = n.info.width ?? 0;
393
+ const height = n.info.height ?? 0;
394
+ let w2: number;
395
+ let h2: number;
396
+ if (margin.doAdd) {
397
+ w2 = Math.trunc(SCALE * (cround((width / 2.0) * POINTS_PER_INCH) + margin.x));
398
+ h2 = Math.trunc(SCALE * (cround((height / 2.0) * POINTS_PER_INCH) + margin.y));
399
+ } else {
400
+ w2 = cround(margin.x * SCALE2 * width * POINTS_PER_INCH);
401
+ h2 = cround(margin.y * SCALE2 * height * POINTS_PER_INCH);
402
+ }
403
+ return {
404
+ val: 0,
405
+ pos: { x, y },
406
+ np: n,
407
+ cnode: undefined,
408
+ bb: { llx: x - w2, lly: y - h2, urx: x + w2, ury: y + h2 },
409
+ };
410
+ }
411
+
412
+ /** One cAdjust mode: which axis goes first, the first pass's test, and ortho. */
413
+ interface Plan {
414
+ xFirst: boolean;
415
+ firstTest: IntersectFn;
416
+ ortho: boolean;
417
+ }
418
+
419
+ /** @see lib/neatogen/constraint.c:cAdjust (switch) */
420
+ const PLANS: ReadonlyMap<number, Plan> = new Map([
421
+ [AM_ORTHOXY, { xFirst: true, firstTest: intersectY, ortho: true }],
422
+ [AM_ORTHOYX, { xFirst: false, firstTest: intersectX, ortho: true }],
423
+ [AM_ORTHO, { xFirst: true, firstTest: intersectY0, ortho: true }],
424
+ [AM_ORTHO_YX, { xFirst: false, firstTest: intersectX0, ortho: true }],
425
+ [AM_PORTHOXY, { xFirst: true, firstTest: intersectY, ortho: false }],
426
+ [AM_PORTHOYX, { xFirst: false, firstTest: intersectX, ortho: false }],
427
+ [AM_PORTHO_YX, { xFirst: false, firstTest: intersectX0, ortho: false }],
428
+ [AM_PORTHO, { xFirst: true, firstTest: intersectY0, ortho: false }],
429
+ ]);
430
+
431
+ function runPlan(g: Graph, nlist: Nitem[], plan: Plan): void {
432
+ if (plan.xFirst) {
433
+ constrainX(g, nlist, plan.firstTest, plan.ortho);
434
+ constrainY(g, nlist, intersectX, plan.ortho);
435
+ } else {
436
+ constrainY(g, nlist, plan.firstTest, plan.ortho);
437
+ constrainX(g, nlist, intersectY, plan.ortho);
438
+ }
439
+ }
440
+
441
+ /**
442
+ * Remove node overlaps with orthogonal-ordering constraints. mode is one of
443
+ * the eight AM_ORTHO and AM_PORTHO values (else AM_PORTHO). Returns 1
444
+ * if nodes were moved, 0 if no boxes overlapped.
445
+ *
446
+ * ND_pos is in inches; positions are rounded to integer points (x SCALE) while
447
+ * solving, then written back.
448
+ * @see lib/neatogen/constraint.c:cAdjust
449
+ */
450
+ export function cAdjust(g: Graph, mode: number): number {
451
+ const margin = sepFactor(g);
452
+ const nlist = nodesInSeq(g).map((n) => initItem(n, margin));
453
+ if (!overlaps(nlist)) return 0;
454
+ runPlan(g, nlist, PLANS.get(mode) ?? PLANS.get(AM_PORTHO)!);
455
+ for (const p of nlist) {
456
+ const x = p.pos.x / POINTS_PER_INCH / SCALE;
457
+ const y = p.pos.y / POINTS_PER_INCH / SCALE;
458
+ if (p.np.info.pos) {
459
+ p.np.info.pos[0] = x;
460
+ p.np.info.pos[1] = y;
461
+ } else {
462
+ p.np.info.pos = [x, y];
463
+ }
464
+ }
465
+ return 1;
466
+ }
@@ -0,0 +1,36 @@
1
+ // SPDX-License-Identifier: EPL-2.0
2
+ /**
3
+ * Edge lengths: scan_graph_mode's MODE_KK and MODE_SGD branches call
4
+ * setEdgeLen on every node, storing `len` (default 1.0) in ED_dist.
5
+ *
6
+ * @see lib/neatogen/stuff.c:setEdgeLen
7
+ * @see lib/neatogen/stuff.c:lenattr
8
+ */
9
+
10
+ import type { Graph } from '../../model/graph.js';
11
+ import type { Edge } from '../../model/edge.js';
12
+
13
+ /** `dfltlen` passed by scan_graph_mode. @see lib/neatogen/stuff.c:scan_graph_mode */
14
+ export const DEFAULT_EDGE_LEN = 1.0;
15
+
16
+ /**
17
+ * The edge's `len`, or the default when it is absent or empty; a value that
18
+ * does not parse as a positive number warns and falls back to the default.
19
+ * @see lib/neatogen/stuff.c:lenattr
20
+ */
21
+ export function lenattr(ep: Edge, g: Graph): number {
22
+ const s = ep.attrs.get('len');
23
+ if (s === undefined || s === '') return DEFAULT_EDGE_LEN;
24
+ const val = /^\s*[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?/.exec(s);
25
+ const len = val === null ? Number.NaN : parseFloat(val[0]);
26
+ if (Number.isNaN(len) || len < 0 || len === 0) {
27
+ console.warn(`bad edge len "${s}" in ${g.name} - setting to ${DEFAULT_EDGE_LEN.toFixed(2)}`);
28
+ return DEFAULT_EDGE_LEN;
29
+ }
30
+ return len;
31
+ }
32
+
33
+ /** Store every edge's length in `info.dist` (ED_dist). @see lib/neatogen/stuff.c:setEdgeLen */
34
+ export function setEdgeLen(g: Graph): void {
35
+ for (const ep of g.edges) ep.info.dist = lenattr(ep, g);
36
+ }
@@ -17,9 +17,13 @@ import {
17
17
  MATRIX_TYPE_REAL,
18
18
  } from '../sfdp/sparse-matrix.js';
19
19
  import { removeOverlapPrism } from './overlap-prism.js';
20
- import { adjustNodesScale } from './sc-adjust.js';
20
+ import { scAdjust } from './sc-adjust.js';
21
+ import { AM_ORTHO, AM_ORTHO_YX, AM_ORTHOXY, AM_ORTHOYX, AM_PORTHO, AM_PORTHO_YX, AM_PORTHOXY, AM_PORTHOYX, cAdjust } from './constraint-adjust.js';
22
+ import { countOverlapIn, sAdjustGraph } from './adjust-info.js';
23
+ import { vpscAdjust } from './vpsc-adjust.js';
21
24
  import { sepFactor } from './sep-factor.js';
22
25
  import { lateDouble } from '../../common/nodeinit.js';
26
+ import { RenderError } from '../../errors.js';
23
27
 
24
28
  /** @see lib/neatogen/adjust.c:DFLT_MARGIN (points) */
25
29
  const DFLT_MARGIN = 4;
@@ -106,19 +110,120 @@ export function fdpAdjust(g: Graph, ntry: number): void {
106
110
  }
107
111
  }
108
112
 
113
+ /** Adjust modes, in C's enum order (modes above AM_SCALE take the early switch).
114
+ * @see lib/neatogen/adjust.h:AM_NONE */
115
+ export const AM_NONE = 0;
116
+ export const AM_VOR = 1;
117
+ export const AM_SCALE = 2;
118
+ export const AM_NSCALE = 3;
119
+ export const AM_SCALEXY = 4;
120
+ export const AM_COMPRESS = 13;
121
+ export const AM_VPSC = 14;
122
+ export const AM_IPSEP = 15;
123
+ export const AM_PRISM = 16;
124
+
125
+ /** @see lib/neatogen/adjust.h:adjust_data (mode, print, value) */
126
+ export interface AdjustData {
127
+ mode: number;
128
+ print: string;
129
+ value: number;
130
+ }
131
+
132
+ /** `overlap` value (lowercase) to mode and print string.
133
+ * @see lib/neatogen/adjust.c:adjustMode */
134
+ const ADJUST_MODES = new Map<string, readonly [number, string]>([
135
+ ['voronoi', [AM_VOR, 'Voronoi']],
136
+ ['scale', [AM_NSCALE, 'scaling']],
137
+ ['compress', [AM_COMPRESS, 'compress']],
138
+ ['vpsc', [AM_VPSC, 'vpsc']],
139
+ ['ipsep', [AM_IPSEP, 'ipsep']],
140
+ ['oscale', [AM_SCALE, 'old scaling']],
141
+ ['scalexy', [AM_SCALEXY, 'x and y scaling']],
142
+ ['ortho', [AM_ORTHO, 'orthogonal constraints']],
143
+ ['ortho_yx', [AM_ORTHO_YX, 'orthogonal constraints']],
144
+ ['orthoxy', [AM_ORTHOXY, 'xy orthogonal constraints']],
145
+ ['orthoyx', [AM_ORTHOYX, 'yx orthogonal constraints']],
146
+ ['portho', [AM_PORTHO, 'pseudo-orthogonal constraints']],
147
+ ['portho_yx', [AM_PORTHO_YX, 'pseudo-orthogonal constraints']],
148
+ ['porthoxy', [AM_PORTHOXY, 'xy pseudo-orthogonal constraints']],
149
+ ['porthoyx', [AM_PORTHOYX, 'yx pseudo-orthogonal constraints']],
150
+ ]);
151
+
109
152
  /**
110
- * Full adjustNodes: scale-family via scAdjust, PRISM via fdpAdjust,
111
- * everything else a no-op (AM_NONE, or a mode with no divergent corpus
112
- * coverage — see sc-adjust module doc).
113
- * @see lib/neatogen/adjust.c:adjustNodes / removeOverlapWith
153
+ * Resolve an `overlap` value to its adjust mode (the PRISM try count lands in
154
+ * `value`).
155
+ * @see lib/neatogen/adjust.c:getAdjustMode
114
156
  */
115
- export function adjustNodesFull(g: Graph): number {
116
- const flag = g.attrs.get('overlap') ?? g.root.attrs.get('overlap');
157
+ export function getAdjustMode(flag: string | undefined): AdjustData {
117
158
  const ntry = overlapPrismTries(flag);
118
- // C removeOverlapWith: fewer than 2 nodes short-circuits every mode.
119
- if (ntry !== null && g.nodes.size >= 2) {
120
- fdpAdjust(g, ntry);
121
- return 1;
159
+ if (ntry !== null) return { mode: AM_PRISM, print: 'prism', value: ntry };
160
+ const named = ADJUST_MODES.get((flag ?? '').toLowerCase());
161
+ if (named !== undefined) return { mode: named[0], print: named[1], value: 0 };
162
+ return { mode: AM_NONE, print: 'none', value: 0 };
163
+ }
164
+
165
+ /** Modes handled by cAdjust. @see lib/neatogen/adjust.c:removeOverlapWith */
166
+ function isConstraintMode(mode: number): boolean {
167
+ return mode >= AM_ORTHO && mode <= AM_PORTHOYX;
168
+ }
169
+
170
+ /**
171
+ * The `am->mode > AM_SCALE` switch of removeOverlapWith. Under IPSEPCOLA (on in
172
+ * the reference build) ipsep is handled during layout; anything else warns.
173
+ * @see lib/neatogen/adjust.c:removeOverlapWith
174
+ */
175
+ function adjustLate(g: Graph, am: AdjustData): number {
176
+ if (isConstraintMode(am.mode)) {
177
+ cAdjust(g, am.mode);
178
+ return 0;
179
+ }
180
+ switch (am.mode) {
181
+ case AM_NSCALE: return scAdjust(g, 1);
182
+ case AM_SCALEXY: return scAdjust(g, 0);
183
+ case AM_COMPRESS: return scAdjust(g, -1);
184
+ case AM_PRISM:
185
+ fdpAdjust(g, am.value);
186
+ return 0;
187
+ case AM_IPSEP: return 0;
188
+ case AM_VPSC: return vpscAdjust(g);
189
+ default:
190
+ console.warn(`Unhandled adjust option ${am.print}`);
191
+ return 0;
122
192
  }
123
- return adjustNodesScale(g);
193
+ }
194
+
195
+ /**
196
+ * AM_VOR (`overlap=voronoi`) is vAdjust: it returns 0 when countOverlap finds
197
+ * nothing, otherwise runs the Voronoi adjuster, which is not ported (loud).
198
+ * @see lib/neatogen/adjust.c:vAdjust
199
+ */
200
+ function adjustVoronoi(g: Graph, am: AdjustData): number {
201
+ if (countOverlapIn(g) === 0) return 0;
202
+ throw new RenderError(
203
+ `overlap=${(g.attrs.get('overlap') ?? g.root.attrs.get('overlap')) ?? am.print}: ` +
204
+ 'Voronoi overlap removal is not supported yet',
205
+ 'UNSUPPORTED_FEATURE',
206
+ );
207
+ }
208
+
209
+ /**
210
+ * Use `am` to decide if and how to remove node overlaps. normalize and
211
+ * simpleScale (only active with the `normalize`/`scale` attributes) are not
212
+ * ported (see sc-adjust module doc).
213
+ * @see lib/neatogen/adjust.c:removeOverlapWith
214
+ */
215
+ export function removeOverlapWith(g: Graph, am: AdjustData): number {
216
+ if (g.nodes.size < 2) return 0;
217
+ if (am.mode === AM_NONE) return 0;
218
+ if (am.mode > AM_SCALE) return adjustLate(g, am);
219
+ return am.mode === AM_SCALE ? sAdjustGraph(g) : adjustVoronoi(g, am);
220
+ }
221
+
222
+ /**
223
+ * Remove node overlap relying on the graph's `overlap` attribute.
224
+ * @see lib/neatogen/adjust.c:adjustNodes / removeOverlapAs
225
+ */
226
+ export function adjustNodesFull(g: Graph): number {
227
+ const flag = g.attrs.get('overlap') ?? g.root.attrs.get('overlap');
228
+ return removeOverlapWith(g, getAdjustMode(flag));
124
229
  }