@knowvah/dot-engine 1.9.0 → 2.0.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 (184) 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 +7682 -5535
  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/normalize.d.ts +2 -1
  60. package/dist/layout/fdp/normalize.d.ts.map +1 -1
  61. package/dist/layout/fdp/ports.d.ts +0 -10
  62. package/dist/layout/fdp/ports.d.ts.map +1 -1
  63. package/dist/layout/fdp/xlayout.d.ts +0 -16
  64. package/dist/layout/fdp/xlayout.d.ts.map +1 -1
  65. package/dist/layout/neato/adjust-info.d.ts +117 -0
  66. package/dist/layout/neato/adjust-info.d.ts.map +1 -0
  67. package/dist/layout/neato/cdt-surface.d.ts.map +1 -1
  68. package/dist/layout/neato/constraint-adjust.d.ts +40 -0
  69. package/dist/layout/neato/constraint-adjust.d.ts.map +1 -0
  70. package/dist/layout/neato/edge-len.d.ts +20 -0
  71. package/dist/layout/neato/edge-len.d.ts.map +1 -0
  72. package/dist/layout/neato/fdp-adjust.d.ts +32 -4
  73. package/dist/layout/neato/fdp-adjust.d.ts.map +1 -1
  74. package/dist/layout/neato/index.d.ts +9 -6
  75. package/dist/layout/neato/index.d.ts.map +1 -1
  76. package/dist/layout/neato/init.d.ts +4 -11
  77. package/dist/layout/neato/init.d.ts.map +1 -1
  78. package/dist/layout/neato/kk-paths.d.ts +50 -0
  79. package/dist/layout/neato/kk-paths.d.ts.map +1 -0
  80. package/dist/layout/neato/kk-solve.d.ts +14 -0
  81. package/dist/layout/neato/kk-solve.d.ts.map +1 -0
  82. package/dist/layout/neato/kk.d.ts +44 -0
  83. package/dist/layout/neato/kk.d.ts.map +1 -0
  84. package/dist/layout/neato/multispline-router.d.ts.map +1 -1
  85. package/dist/layout/neato/poly.d.ts +50 -0
  86. package/dist/layout/neato/poly.d.ts.map +1 -0
  87. package/dist/layout/neato/sc-adjust.d.ts +2 -9
  88. package/dist/layout/neato/sc-adjust.d.ts.map +1 -1
  89. package/dist/layout/neato/sgd-dijkstra.d.ts +20 -0
  90. package/dist/layout/neato/sgd-dijkstra.d.ts.map +1 -0
  91. package/dist/layout/neato/sgd.d.ts +14 -9
  92. package/dist/layout/neato/sgd.d.ts.map +1 -1
  93. package/dist/layout/neato/simple-scale.d.ts +15 -0
  94. package/dist/layout/neato/simple-scale.d.ts.map +1 -0
  95. package/dist/layout/neato/start.d.ts +51 -0
  96. package/dist/layout/neato/start.d.ts.map +1 -0
  97. package/dist/layout/neato/vpsc-adjust.d.ts +21 -0
  98. package/dist/layout/neato/vpsc-adjust.d.ts.map +1 -0
  99. package/dist/layout/sfdp/index.d.ts.map +1 -1
  100. package/dist/layout/sfdp/init.d.ts +0 -9
  101. package/dist/layout/sfdp/init.d.ts.map +1 -1
  102. package/dist/layout/sfdp/spring-driver.d.ts.map +1 -1
  103. package/dist/layout/twopi/circle.d.ts +0 -7
  104. package/dist/layout/twopi/circle.d.ts.map +1 -1
  105. package/dist/ortho/ortho-parallel.d.ts.map +1 -1
  106. package/dist/ortho/trap-query.d.ts.map +1 -1
  107. package/dist/parser/index.d.ts +9 -5
  108. package/dist/parser/index.d.ts.map +1 -1
  109. package/dist/render/index.d.ts +2 -0
  110. package/dist/render/index.d.ts.map +1 -1
  111. package/dist/render/public.d.ts +9 -3
  112. package/dist/render/public.d.ts.map +1 -1
  113. package/dist/render/xdot-public.d.ts +7 -2
  114. package/dist/render/xdot-public.d.ts.map +1 -1
  115. package/dist/render.js +7468 -5556
  116. package/dist/render.js.map +4 -4
  117. package/dist/util/xml.d.ts.map +1 -1
  118. package/dist/vpsc/Solver.d.ts +1 -0
  119. package/dist/vpsc/Solver.d.ts.map +1 -1
  120. package/package.json +1 -1
  121. package/src/api/builder.ts +73 -5
  122. package/src/api/edge-ops.ts +19 -0
  123. package/src/api/geometry.ts +22 -5
  124. package/src/async/collect.ts +204 -0
  125. package/src/async/fonts.ts +61 -0
  126. package/src/async/render-async.ts +205 -0
  127. package/src/async/render-into.ts +115 -0
  128. package/src/async/sanitize.ts +150 -0
  129. package/src/common/htmltable-types.ts +4 -5
  130. package/src/common/make-label.ts +10 -1
  131. package/src/common/poly-shapes.ts +4 -1
  132. package/src/common/textmeasure-factory.ts +11 -0
  133. package/src/common/utils-inputscale.ts +31 -0
  134. package/src/errors.ts +188 -5
  135. package/src/gvc/context.ts +96 -17
  136. package/src/gvc/device.ts +11 -6
  137. package/src/gvc/image-resolver.ts +25 -2
  138. package/src/gvc/job.ts +3 -1
  139. package/src/gvc/usershape.ts +6 -0
  140. package/src/index.ts +59 -43
  141. package/src/label/index.ts +6 -2
  142. package/src/label/node.ts +2 -1
  143. package/src/label/rectangle.ts +4 -2
  144. package/src/layout/circo/circular.ts +9 -6
  145. package/src/layout/dot/pack-components.ts +6 -2
  146. package/src/layout/dot/position.ts +21 -3
  147. package/src/layout/fdp/derive.ts +7 -6
  148. package/src/layout/fdp/index.ts +45 -5
  149. package/src/layout/fdp/init.ts +7 -6
  150. package/src/layout/fdp/layout.ts +2 -1
  151. package/src/layout/fdp/normalize.ts +12 -8
  152. package/src/layout/fdp/ports.ts +3 -2
  153. package/src/layout/fdp/xlayout.ts +8 -5
  154. package/src/layout/neato/adjust-info.ts +332 -0
  155. package/src/layout/neato/cdt-surface.ts +50 -30
  156. package/src/layout/neato/constraint-adjust.ts +466 -0
  157. package/src/layout/neato/edge-len.ts +36 -0
  158. package/src/layout/neato/fdp-adjust.ts +120 -12
  159. package/src/layout/neato/index.ts +36 -54
  160. package/src/layout/neato/init.ts +21 -36
  161. package/src/layout/neato/kk-paths.ts +146 -0
  162. package/src/layout/neato/kk-solve.ts +64 -0
  163. package/src/layout/neato/kk.ts +322 -0
  164. package/src/layout/neato/multispline-router.ts +4 -3
  165. package/src/layout/neato/poly.ts +493 -0
  166. package/src/layout/neato/sc-adjust.ts +2 -16
  167. package/src/layout/neato/sgd-dijkstra.ts +125 -0
  168. package/src/layout/neato/sgd.ts +63 -40
  169. package/src/layout/neato/simple-scale.ts +54 -0
  170. package/src/layout/neato/start.ts +222 -0
  171. package/src/layout/neato/vpsc-adjust.ts +93 -0
  172. package/src/layout/sfdp/index.ts +4 -5
  173. package/src/layout/sfdp/init.ts +40 -4
  174. package/src/layout/sfdp/spring-driver.ts +30 -1
  175. package/src/layout/twopi/circle.ts +2 -1
  176. package/src/ortho/ortho-parallel.ts +2 -1
  177. package/src/ortho/trap-query.ts +2 -1
  178. package/src/parser/index.ts +11 -11
  179. package/src/render/index.ts +5 -0
  180. package/src/render/public.ts +24 -24
  181. package/src/render/svg.ts +1 -1
  182. package/src/render/xdot-public.ts +19 -18
  183. package/src/util/xml.ts +24 -30
  184. package/src/vpsc/Solver.ts +5 -3
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * Neato layout engine entry point.
4
4
  *
5
- * Wires together neatoInitNode, setSeed, solveModel, removeOverlap,
5
+ * Wires together neatoInitNode, setSeed, solveModel, adjustNodes,
6
6
  * splineEdges, neatoTranslate, and neatoSetAspect into the full neato
7
7
  * layout pipeline, and exports NEATO_LAYOUT_ENGINE for registration with
8
8
  * GvcContext.
@@ -14,28 +14,26 @@
14
14
  import type { Graph } from '../../model/graph.js';
15
15
  import type { LayoutEngine } from '../../gvc/context.js';
16
16
  import {
17
+ MODE_KK,
17
18
  neatoInitNode,
18
19
  userPos,
19
20
  solveModel,
20
21
  neatoCleanup,
21
- MODE_KK,
22
- MODE_MAJOR,
23
- MODE_HIER,
24
- MODE_IPSEP,
25
- MODE_SGD,
26
22
  MODEL_SHORTPATH,
27
23
  MODEL_CIRCUIT,
28
24
  MODEL_SUBSET,
29
25
  MODEL_MDS,
30
26
  } from './init.js';
31
- import { removeOverlap } from './overlap.js';
27
+ import { neatoMode } from './start.js';
32
28
  import { adjustNodesFull } from './fdp-adjust.js';
33
29
  import { splineEdgesShifted, EDGETYPE_LINE } from './splines.js';
34
30
  import { setEdgeTypeFromAttr } from '../dot/index.js';
35
31
  import {
36
32
  pccomps,
37
33
  computeSubgraphBB,
34
+ getPack,
38
35
  getPackInfo,
36
+ getPackModeInfo,
39
37
  packGraphs,
40
38
  PackMode,
41
39
  type PackInfo,
@@ -43,7 +41,7 @@ import {
43
41
  import { CL_OFFSET } from '../twopi/pipeline.js';
44
42
  import { isACluster } from '../dot/rank.js';
45
43
  import { doGraphLabel } from '../dot/graph-label.js';
46
- import { graphInit, DEFAULT_NODESEP_POINTS } from '../../common/graph-init.js';
44
+ import { graphInit } from '../../common/graph-init.js';
47
45
  import { placeGraphLabel } from '../dot/position-bbox.js';
48
46
  import { gvPostprocess } from '../../common/postproc.js';
49
47
  import { layoutMeasurer } from '../../common/nodeinit.js';
@@ -62,25 +60,14 @@ export {
62
60
  // Mode parsing
63
61
  // ---------------------------------------------------------------------------
64
62
 
65
- /** Map from mode string to numeric constant. @see lib/neatogen/neato.h */
66
- const MODE_MAP: Record<string, number> = {
67
- KK: MODE_KK,
68
- major: MODE_MAJOR,
69
- hier: MODE_HIER,
70
- ipsep: MODE_IPSEP,
71
- sgd: MODE_SGD,
72
- };
73
-
74
63
  /**
75
- * Parse `g.info.mode` string into a numeric mode constant.
76
- * Defaults to MODE_MAJOR when unset or unrecognised.
64
+ * Parse the root `mode` attribute into a numeric mode constant (C neatoMode).
65
+ * Defaults to MODE_MAJOR when unset; warns and defaults on an unknown value.
77
66
  *
78
- * @see lib/neatogen/neato.h:neatoMode
67
+ * @see lib/neatogen/neatoinit.c:neatoMode
79
68
  */
80
69
  export function parseMode(g: Graph): number {
81
- const s = g.info.mode;
82
- if (!s) return MODE_MAJOR;
83
- return MODE_MAP[s] ?? MODE_MAJOR;
70
+ return neatoMode(g);
84
71
  }
85
72
 
86
73
  // ---------------------------------------------------------------------------
@@ -113,39 +100,15 @@ export function parseModel(g: Graph): number {
113
100
  // ---------------------------------------------------------------------------
114
101
 
115
102
  /**
116
- * Apply VPSC overlap removal unless `g.info.overlap === 'false'`.
103
+ * Remove node overlaps per the `overlap` attribute: C's removeOverlapWith
104
+ * dispatch (prism, scale family, oscale, ortho*, portho*, vpsc, voronoi) for
105
+ * every mode, as neato_layout's adjustNodes call does.
117
106
  *
118
- * @see lib/neatogen/neatoinit.c:neato_layout (removeOverlapWith call)
107
+ * @see lib/neatogen/neatoinit.c:neato_layout (adjustNodes call)
108
+ * @see lib/neatogen/adjust.c:removeOverlapWith
119
109
  */
120
110
  export function maybeRemoveOverlap(g: Graph): void {
121
- // C: graphAdjustMode defaults to AM_NONE — no overlap attr means no
122
- // overlap removal ("overlap: none"). VPSC runs only on request.
123
- const overlap = g.attrs.get('overlap');
124
- if (overlap === undefined || overlap === 'true') return;
125
- // C removeOverlapWith dispatches by adjust mode. `overlap=false` resolves to
126
- // AM_PRISM (getAdjustMode's boolean fallback lands on adjustMode[1] on a
127
- // GTS+SFDP build), `overlap=scale/scalexy/compress` to the scale family, only
128
- // `overlap=vpsc` to VPSC. Previously neato hardcoded VPSC for ALL of these,
129
- // under-scaling every overlap=false graph (bb ~0.4-0.8x the oracle) and
130
- // scaling scale-mode graphs wrong. adjustNodesFull is the ported
131
- // removeOverlapWith body (PRISM via fdpAdjust + scale via scAdjust); VPSC is
132
- // the one mode it does not cover. @see lib/neatogen/adjust.c:removeOverlapWith
133
- if (overlap !== 'vpsc') {
134
- adjustNodesFull(g);
135
- return;
136
- }
137
- const nodes = Array.from(g.nodes.values());
138
- // Separation is DELIBERATELY the *default* nodesep, not GD_nodesep(g). C's
139
- // overlap removal derives its padding from `sep`/DFLT_MARGIN (adjust.c:591-600
140
- // sepFactor) and never reads GD_nodesep — that field is used only by
141
- // makeSelfArcs (neatosplines.c:673) and routespl.c:1006. Before graph_init was
142
- // consolidated, GD_nodesep was unset under neato at this point, so this site
143
- // always saw the 18pt default; now that graphInit parses `nodesep` for every
144
- // engine (as C does), reading it here would silently change the VPSC
145
- // separation on graphs that set both `nodesep` and `overlap` (corpus: 1554,
146
- // 2242) — a divergence C does not have. Pinned to the default it always used.
147
- const nodesep = DEFAULT_NODESEP_POINTS / 72; // points → inches
148
- removeOverlap(nodes, { x: nodesep / 2, y: nodesep / 2 });
111
+ adjustNodesFull(g);
149
112
  }
150
113
 
151
114
  // ---------------------------------------------------------------------------
@@ -205,7 +168,9 @@ export function neatoLayout(g: Graph): void {
205
168
 
206
169
  // C uses pccomps (pin-aware): components with a pinned node are collected
207
170
  // first (index 0) so the packer can hold them fixed. @see neatoinit.c:1391
208
- const { graphs: comps, pinned } = pccomps(g, '_neato_cc');
171
+ const { graphs: comps, pinned } = neatoPack(g, mode) >= 0
172
+ ? pccomps(g, '_neato_cc')
173
+ : { graphs: [g], pinned: false };
209
174
  if (comps.length > 1) {
210
175
  layoutComponents(g, comps, mode, model, pinned);
211
176
  } else {
@@ -227,6 +192,23 @@ export function neatoLayout(g: Graph): void {
227
192
  gvPostprocess(g);
228
193
  }
229
194
 
195
+ /**
196
+ * C's Pack decision: unset `pack` with no `packmode` packs (CL_OFFSET) unless
197
+ * layoutMode is MODE_KK (0); a set `packmode` always packs. Negative means the
198
+ * whole graph is laid out at once.
199
+ * @see lib/neatogen/neatoinit.c:1371-1383 (neato_layout)
200
+ */
201
+ function neatoPack(g: Graph, layoutMode: number): number {
202
+ const pinfo: PackInfo = {
203
+ aspect: 1, sz: 0, margin: CL_OFFSET, doSplines: false,
204
+ mode: PackMode.Undef, fixed: null, vals: null, flags: 0,
205
+ };
206
+ const mode = getPackModeInfo(g, PackMode.Undef, pinfo);
207
+ const pack = getPack(g, -1, CL_OFFSET);
208
+ if (mode === PackMode.Undef) return pack < 0 && layoutMode !== MODE_KK ? CL_OFFSET : pack;
209
+ return pack < 0 ? CL_OFFSET : pack;
210
+ }
211
+
230
212
  /**
231
213
  * Register direct cluster subgraphs: build their labels and tight
232
214
  * member bounding boxes for emission.
@@ -21,7 +21,10 @@ import {
21
21
  MODEL_SUBSET as STRESS_MODEL_SUBSET,
22
22
  } from './stress.js';
23
23
  import { sgdLayout } from './sgd.js';
24
- import { srand48 } from '../../common/random.js';
24
+ import { kkNeato } from './kk.js';
25
+ import { checkStart, assertSupported, parseStart, INIT_RANDOM, INIT_REGULAR } from './start.js';
26
+
27
+ export { checkStart };
25
28
  import { mapbool } from '../dot/rank.js';
26
29
  import {
27
30
  computeApspPacked,
@@ -30,6 +33,7 @@ import {
30
33
  OPT_EXP_FLAG,
31
34
  } from './stress-kernel.js';
32
35
  import { lateDouble } from '../../common/nodeinit.js';
36
+ import { inputscaleDivisor } from '../../common/utils-inputscale.js';
33
37
  import { parseNeatoDrawing, neatoSetAspectRatio } from './set-aspect.js';
34
38
 
35
39
  // ---------------------------------------------------------------------------
@@ -85,15 +89,10 @@ export function neatoInitNode(n: Node, dim = DFLT_DIM): void {
85
89
  n.info.pos = pos;
86
90
  }
87
91
  if (n.info.UF_size === undefined) n.info.UF_size = 1;
88
- // C neato_init_node has NO size defaulting (neatoinit.c:60-66) — after
89
- // common_init_node, ND_width is always written (late_double clamps a 0
90
- // attr to the 0.75 default; every initfn assigns). The port's NodeInfo
91
- // zero-initializes width/height (calloc mirror) and two port-only paths
92
- // leave them at that unset-0 (no-measurer initNodeDefaults; poly-null
93
- // custom/epsf shapes — C gives customs box geometry, a modeling gap
94
- // flagged in the journal), so a fallback is still needed. The ONE case
95
- // where 0 is a legitimate post-init size is shape=plain (poly_init
96
- // IS_PLAIN, shapes.c:1962) — never clobber it.
92
+ // C neato_init_node has no size defaulting (neatoinit.c:60-66); the port's
93
+ // zero-initialised width/height need a fallback on two port-only paths
94
+ // (no-measurer initNodeDefaults, poly-null custom/epsf shapes). The one
95
+ // legitimate post-init 0 is shape=plain (poly_init IS_PLAIN) — never clobber it.
97
96
  const plainShaped = (n.info.shape as ShapeDesc | undefined)?.name === 'plain';
98
97
  if (!n.info.width && !plainShaped) n.info.width = 0.75;
99
98
  if (!n.info.height && !plainShaped) n.info.height = 0.5;
@@ -109,10 +108,8 @@ export function neatoInitNode(n: Node, dim = DFLT_DIM): void {
109
108
  * majorization starts from the input layout instead of a random init; a `!`
110
109
  * suffix or `pin=true` additionally sets info.pinned (P_PIN → isFixed).
111
110
  *
112
- * PSinputscale (the `inputscale` attr) is not tracked — the suite has
113
- * PSinputscale <= 0, so positions stay in points exactly as native leaves them
114
- * (a `pos="27,42"` seeds (27,42), which orthog1 then centres). Must run AFTER
115
- * neatoInitNode, which zeroes info.pos.
111
+ * `pos` is divided by PSinputscale when > 0 (neatoinit.c:90-95). Must run
112
+ * AFTER neatoInitNode, which zeroes info.pos.
116
113
  * @see lib/neatogen/neatoinit.c:user_pos
117
114
  */
118
115
  export function userPos(n: Node): boolean {
@@ -122,7 +119,8 @@ export function userPos(n: Node): boolean {
122
119
  const num = '(-?\\d+(?:\\.\\d+)?(?:[eE][+-]?\\d+)?)';
123
120
  const m = new RegExp(`^\\s*${num}\\s*,\\s*${num}(.?)`).exec(p);
124
121
  if (m === null) return false; // C agerrorf: malformed pos, non-fatal
125
- n.info.pos = [Number(m[1]), Number(m[2])];
122
+ const div = inputscaleDivisor(n.root);
123
+ n.info.pos = [Number(m[1]) / div, Number(m[2]) / div];
126
124
  n.info.posSet = true;
127
125
  const pin = n.attrs.get('pin');
128
126
  if (m[3] === '!' || (pin !== undefined && mapbool(pin))) n.info.pinned = true;
@@ -335,7 +333,7 @@ export function runMajorization(g: Graph, mode: number, model: number): void {
335
333
  const haveLen = graphHasLen(g);
336
334
  const vtx = makeGraphDataC(g, nodeList, haveLen);
337
335
  checkStart(g); // C: srand48(seed) immediately before initLayout draws
338
- const maxi = mode === MODE_KK ? 0 : lateDouble(g.attrs.get('maxiter'), DFLT_ITERATIONS, 0);
336
+ const maxi = lateDouble(g.attrs.get('maxiter'), DFLT_ITERATIONS, 0);
339
337
  const epsilon = lateDouble(g.attrs.get('epsilon'), 1e-4, 0); // DFLT_TOLERANCE
340
338
  const Dij = haveLen ? computeWeightedApspPacked(vtx, n) : computeApspPacked(vtx, n);
341
339
  const dCoords = [new Float64Array(n), new Float64Array(n)];
@@ -355,22 +353,6 @@ export function graphHasLen(g: Graph): boolean {
355
353
  return false;
356
354
  }
357
355
 
358
- /**
359
- * Seed the RNG from the start attr; no attr means the default seed 1.
360
- * start=regular/self are not ported (no suite input uses them).
361
- * @see lib/neatogen/neatoinit.c:setSeed
362
- * @see lib/neatogen/neatoinit.c:checkStart
363
- */
364
- export function checkStart(g: Graph): void {
365
- const p = g.root.attrs.get('start');
366
- let seed = 1;
367
- if (p !== undefined && /^\d/.test(p)) {
368
- const v = parseInt(p, 10);
369
- if (!Number.isNaN(v)) seed = v;
370
- }
371
- srand48(seed);
372
- }
373
-
374
356
  /**
375
357
  * Build the vtx_data adjacency in C's makeGraphData shape: per node,
376
358
  * out-edges then in-edges (agfstedge order), self entry at index 0,
@@ -414,10 +396,13 @@ export function makeGraphDataC(g: Graph, nodeList: Node[], haveLen: boolean): Vt
414
396
  */
415
397
  export function solveModel(g: Graph, mode: number, model: number): void {
416
398
  if (g.nodes.size < 2) return;
417
- if (mode === MODE_SGD) { sgdLayout(g, model); return; }
418
- if (mode === MODE_HIER || mode === MODE_IPSEP) {
419
- const name = mode === MODE_HIER ? 'hier' : 'ipsep';
420
- console.warn(`neato: mode ${name} not fully implemented; falling back to majorization`);
399
+ assertSupported(g, mode, model);
400
+ if (mode === MODE_KK) return kkNeato(g, g.nodes.size, model);
401
+ if (mode === MODE_SGD) {
402
+ // C initial_positions -> checkStart: regular positions are kept
403
+ if (parseStart(g.root.attrs.get('start'), INIT_RANDOM).init === INIT_REGULAR) checkStart(g);
404
+ sgdLayout(g, model);
405
+ return;
421
406
  }
422
407
  runMajorization(g, mode, model);
423
408
  }
@@ -0,0 +1,146 @@
1
+ // SPDX-License-Identifier: EPL-2.0
2
+ /**
3
+ * Kamada-Kawai ideal-distance setup: Dijkstra over edge lengths with C's
4
+ * binary heap (`neato_enqueue`/`neato_dequeue`, `shortest_path`, `s1`,
5
+ * `make_spring`) and `mds_model`.
6
+ *
7
+ * The C keeps the heap, its size and the source node in file-statics and the
8
+ * per-node `ND_dist`/`ND_hops`/`ND_heapindex` in node records; here they live
9
+ * in one {@link PathState} per run (no module state), indexed by `ND_id`.
10
+ *
11
+ * @see lib/neatogen/stuff.c
12
+ */
13
+
14
+ /** One neighbour of a node: its id and the edge's `ED_dist`. */
15
+ export interface Neighbour {
16
+ readonly id: number;
17
+ readonly len: number;
18
+ }
19
+
20
+ /** Heap + per-node Dijkstra state. @see lib/neatogen/stuff.c (Heap, Heapsize) */
21
+ export interface PathState {
22
+ readonly n: number;
23
+ /** `GD_dist` as a flat n*n matrix. */
24
+ readonly dist: Float64Array;
25
+ /** `agfstedge` neighbours of each node, in C's iteration order. */
26
+ readonly adj: readonly (readonly Neighbour[])[];
27
+ /** `Initial_dist`. */
28
+ readonly initialDist: number;
29
+ readonly heap: Int32Array;
30
+ heapSize: number;
31
+ /** `ND_heapindex` (-1 = not queued). */
32
+ readonly heapIndex: Int32Array;
33
+ /** `ND_dist`. */
34
+ readonly nd: Float64Array;
35
+ /** `ND_hops`. */
36
+ readonly hops: Int32Array;
37
+ }
38
+
39
+ /** Fresh path state for `n` nodes. @see lib/neatogen/stuff.c:shortest_path */
40
+ export function newPathState(
41
+ n: number, dist: Float64Array, adj: readonly (readonly Neighbour[])[], initialDist: number,
42
+ ): PathState {
43
+ return {
44
+ n, dist, adj, initialDist,
45
+ heap: new Int32Array(n + 1), heapSize: 0,
46
+ heapIndex: new Int32Array(n).fill(-1),
47
+ nd: new Float64Array(n), hops: new Int32Array(n),
48
+ };
49
+ }
50
+
51
+ /** @see lib/neatogen/stuff.c:heapup */
52
+ function heapUp(s: PathState, v: number): void {
53
+ for (let i = s.heapIndex[v]!, par: number; i > 0; i = par) {
54
+ par = (i - 1) >> 1;
55
+ const u = s.heap[par]!;
56
+ if (s.nd[u]! <= s.nd[v]!) break;
57
+ s.heap[par] = v;
58
+ s.heapIndex[v] = par;
59
+ s.heap[i] = u;
60
+ s.heapIndex[u] = i;
61
+ }
62
+ }
63
+
64
+ /** @see lib/neatogen/stuff.c:heapdown */
65
+ function heapDown(s: PathState, v: number): void {
66
+ let i = s.heapIndex[v]!;
67
+ for (let left = 2 * i + 1; left < s.heapSize; left = 2 * i + 1) {
68
+ const right = left + 1;
69
+ const c = right < s.heapSize && s.nd[s.heap[right]!]! < s.nd[s.heap[left]!]! ? right : left;
70
+ const u = s.heap[c]!;
71
+ if (s.nd[v]! <= s.nd[u]!) break;
72
+ s.heap[c] = v;
73
+ s.heapIndex[v] = c;
74
+ s.heap[i] = u;
75
+ s.heapIndex[u] = i;
76
+ i = c;
77
+ }
78
+ }
79
+
80
+ /** @see lib/neatogen/stuff.c:neato_enqueue */
81
+ function enqueue(s: PathState, v: number): void {
82
+ const i = s.heapSize++;
83
+ s.heapIndex[v] = i;
84
+ s.heap[i] = v;
85
+ if (i > 0) heapUp(s, v);
86
+ }
87
+
88
+ /** Returns -1 when empty (C: NULL). @see lib/neatogen/stuff.c:neato_dequeue */
89
+ function dequeue(s: PathState): number {
90
+ if (s.heapSize === 0) return -1;
91
+ const rv = s.heap[0]!;
92
+ const i = --s.heapSize;
93
+ const v = s.heap[i]!;
94
+ s.heap[0] = v;
95
+ s.heapIndex[v] = 0;
96
+ if (i > 1) heapDown(s, v);
97
+ s.heapIndex[rv] = -1;
98
+ return rv;
99
+ }
100
+
101
+ /** @see lib/neatogen/stuff.c:make_spring */
102
+ function makeSpring(s: PathState, u: number, v: number, f: number): void {
103
+ s.dist[u * s.n + v] = f;
104
+ s.dist[v * s.n + u] = f;
105
+ }
106
+
107
+ /** Single-source shortest paths from `src`. @see lib/neatogen/stuff.c:s1 */
108
+ function s1(s: PathState, src: number): void {
109
+ s.nd.fill(s.initialDist);
110
+ s.nd[src] = 0;
111
+ s.hops[src] = 0;
112
+ enqueue(s, src);
113
+ for (let v = dequeue(s); v >= 0; v = dequeue(s)) {
114
+ if (v !== src) makeSpring(s, src, v, s.nd[v]!);
115
+ for (const { id: u, len } of s.adj[v]!) {
116
+ const f = s.nd[v]! + len;
117
+ if (s.nd[u]! <= f) continue;
118
+ s.nd[u] = f;
119
+ if (s.heapIndex[u]! >= 0) heapUp(s, u);
120
+ else {
121
+ s.hops[u] = s.hops[v]! + 1;
122
+ enqueue(s, u);
123
+ }
124
+ }
125
+ }
126
+ }
127
+
128
+ /** All-pairs shortest paths into `GD_dist`. @see lib/neatogen/stuff.c:shortest_path */
129
+ export function shortestPath(s: PathState): void {
130
+ for (let v = 0; v < s.n; v++) s1(s, v);
131
+ }
132
+
133
+ /**
134
+ * Overwrite `GD_dist` with edge lengths. C indexes the matrix with `AGSEQ`
135
+ * (1-based), so the last node's row falls outside the n*n allocation (heap
136
+ * overflow in C); out-of-range writes are dropped here.
137
+ * @see lib/neatogen/neatoinit.c:mds_model
138
+ */
139
+ export function mdsModel(
140
+ dist: Float64Array, n: number, edges: readonly { tailSeq: number; headSeq: number; len: number }[],
141
+ ): void {
142
+ for (const { tailSeq: i, headSeq: j, len } of edges) {
143
+ if (i === j) continue;
144
+ for (const k of [i * n + j, j * n + i]) if (k < dist.length) dist[k] = len;
145
+ }
146
+ }
@@ -0,0 +1,64 @@
1
+ // SPDX-License-Identifier: EPL-2.0
2
+ /**
3
+ * Gaussian elimination with partial pivoting for the small Newton systems of
4
+ * Kamada-Kawai (`move_node`).
5
+ *
6
+ * @see lib/neatogen/solve.c
7
+ */
8
+
9
+ /** Pivots below this make the system ill-conditioned. @see solve.c:solve */
10
+ const MIN_PIVOT = 1.0e-10;
11
+
12
+ /** @see lib/neatogen/solve.c:SWAP */
13
+ function swap(x: Float64Array, i: number, j: number): void {
14
+ const t = x[i]!;
15
+ x[i] = x[j]!;
16
+ x[j] = t;
17
+ }
18
+
19
+ /** Returns false on an unusable pivot. Eliminates unknown `i`. */
20
+ function eliminate(a: Float64Array, c: Float64Array, n: number, i: number): boolean {
21
+ let amax = 0.0;
22
+ let istar = 0;
23
+ for (let ii = i; ii < n; ii++) {
24
+ const dum = Math.abs(a[ii * n + i]!);
25
+ if (dum < amax) continue;
26
+ istar = ii;
27
+ amax = dum;
28
+ }
29
+ if (amax < MIN_PIVOT) return false;
30
+ for (let j = i; j < n; j++) swap(a, istar * n + j, i * n + j);
31
+ swap(c, istar, i);
32
+ for (let ii = i + 1; ii < n; ii++) {
33
+ const pivot = a[ii * n + i]! / a[i * n + i]!;
34
+ c[ii]! -= pivot * c[i]!;
35
+ for (let j = 0; j < n; j++) a[ii * n + j] = a[ii * n + j]! - pivot * a[i * n + j]!;
36
+ }
37
+ return true;
38
+ }
39
+
40
+ /**
41
+ * Solve a*b = c (a is n*n row-major). On success `a` and `c` are restored and
42
+ * `b` holds the solution; when ill-conditioned `b` is untouched. C prints
43
+ * "ill-conditioned" to stdout; a library has no stdout, so it is a warning.
44
+ * @see lib/neatogen/solve.c:solve
45
+ */
46
+ export function solve(a: Float64Array, b: Float64Array, c: Float64Array, n: number): void {
47
+ const asave = Float64Array.from(a);
48
+ const csave = Float64Array.from(c);
49
+ let ok = true;
50
+ for (let i = 0; i < n - 1 && ok; i++) ok = eliminate(a, c, n, i);
51
+ if (!ok || Math.abs(a[n * n - 1]!) < MIN_PIVOT) {
52
+ console.warn('ill-conditioned');
53
+ return;
54
+ }
55
+ b[n - 1] = c[n - 1]! / a[n * n - 1]!;
56
+ for (let k = 0; k < n - 1; k++) {
57
+ const m = n - k - 2;
58
+ b[m] = c[m]!;
59
+ for (let j = m + 1; j < n; j++) b[m]! -= a[m * n + j]! * b[j]!;
60
+ b[m]! /= a[m * n + m]!;
61
+ }
62
+ c.set(csave);
63
+ a.set(asave);
64
+ }