@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.
- package/README.md +232 -30
- package/dist/api/builder.d.ts +3 -0
- package/dist/api/builder.d.ts.map +1 -1
- package/dist/api/edge-ops.d.ts +7 -0
- package/dist/api/edge-ops.d.ts.map +1 -1
- package/dist/api/geometry.d.ts +5 -2
- package/dist/api/geometry.d.ts.map +1 -1
- package/dist/api.js +159 -31
- package/dist/api.js.map +3 -3
- package/dist/async/collect.d.ts +49 -0
- package/dist/async/collect.d.ts.map +1 -0
- package/dist/async/fonts.d.ts +20 -0
- package/dist/async/fonts.d.ts.map +1 -0
- package/dist/async/render-async.d.ts +91 -0
- package/dist/async/render-async.d.ts.map +1 -0
- package/dist/async/render-into.d.ts +38 -0
- package/dist/async/render-into.d.ts.map +1 -0
- package/dist/async/sanitize.d.ts +28 -0
- package/dist/async/sanitize.d.ts.map +1 -0
- package/dist/common/htmltable-types.d.ts +3 -3
- package/dist/common/htmltable-types.d.ts.map +1 -1
- package/dist/common/make-label.d.ts.map +1 -1
- package/dist/common/poly-shapes.d.ts.map +1 -1
- package/dist/common/textmeasure-factory.d.ts +2 -0
- package/dist/common/textmeasure-factory.d.ts.map +1 -1
- package/dist/common/utils-inputscale.d.ts +19 -0
- package/dist/common/utils-inputscale.d.ts.map +1 -0
- package/dist/errors.d.ts +61 -5
- package/dist/errors.d.ts.map +1 -1
- package/dist/gvc/context.d.ts +36 -4
- package/dist/gvc/context.d.ts.map +1 -1
- package/dist/gvc/device.d.ts +5 -2
- package/dist/gvc/device.d.ts.map +1 -1
- package/dist/gvc/image-resolver.d.ts +16 -16
- package/dist/gvc/image-resolver.d.ts.map +1 -1
- package/dist/gvc/job.d.ts +1 -9
- package/dist/gvc/job.d.ts.map +1 -1
- package/dist/gvc/usershape.d.ts +2 -12
- package/dist/gvc/usershape.d.ts.map +1 -1
- package/dist/index.d.ts +27 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7682 -5535
- package/dist/index.js.map +4 -4
- package/dist/label/index.d.ts.map +1 -1
- package/dist/label/node.d.ts +0 -6
- package/dist/label/node.d.ts.map +1 -1
- package/dist/label/rectangle.d.ts +1 -7
- package/dist/label/rectangle.d.ts.map +1 -1
- package/dist/layout/circo/circular.d.ts +8 -5
- package/dist/layout/circo/circular.d.ts.map +1 -1
- package/dist/layout/dot/pack-components.d.ts +0 -19
- package/dist/layout/dot/pack-components.d.ts.map +1 -1
- package/dist/layout/dot/position.d.ts +7 -2
- package/dist/layout/dot/position.d.ts.map +1 -1
- package/dist/layout/fdp/derive.d.ts.map +1 -1
- package/dist/layout/fdp/index.d.ts.map +1 -1
- package/dist/layout/fdp/init.d.ts.map +1 -1
- package/dist/layout/fdp/layout.d.ts.map +1 -1
- package/dist/layout/fdp/normalize.d.ts +2 -1
- package/dist/layout/fdp/normalize.d.ts.map +1 -1
- package/dist/layout/fdp/ports.d.ts +0 -10
- package/dist/layout/fdp/ports.d.ts.map +1 -1
- package/dist/layout/fdp/xlayout.d.ts +0 -16
- package/dist/layout/fdp/xlayout.d.ts.map +1 -1
- package/dist/layout/neato/adjust-info.d.ts +117 -0
- package/dist/layout/neato/adjust-info.d.ts.map +1 -0
- package/dist/layout/neato/cdt-surface.d.ts.map +1 -1
- package/dist/layout/neato/constraint-adjust.d.ts +40 -0
- package/dist/layout/neato/constraint-adjust.d.ts.map +1 -0
- package/dist/layout/neato/edge-len.d.ts +20 -0
- package/dist/layout/neato/edge-len.d.ts.map +1 -0
- package/dist/layout/neato/fdp-adjust.d.ts +32 -4
- package/dist/layout/neato/fdp-adjust.d.ts.map +1 -1
- package/dist/layout/neato/index.d.ts +9 -6
- package/dist/layout/neato/index.d.ts.map +1 -1
- package/dist/layout/neato/init.d.ts +4 -11
- package/dist/layout/neato/init.d.ts.map +1 -1
- package/dist/layout/neato/kk-paths.d.ts +50 -0
- package/dist/layout/neato/kk-paths.d.ts.map +1 -0
- package/dist/layout/neato/kk-solve.d.ts +14 -0
- package/dist/layout/neato/kk-solve.d.ts.map +1 -0
- package/dist/layout/neato/kk.d.ts +44 -0
- package/dist/layout/neato/kk.d.ts.map +1 -0
- package/dist/layout/neato/multispline-router.d.ts.map +1 -1
- package/dist/layout/neato/poly.d.ts +50 -0
- package/dist/layout/neato/poly.d.ts.map +1 -0
- package/dist/layout/neato/sc-adjust.d.ts +2 -9
- package/dist/layout/neato/sc-adjust.d.ts.map +1 -1
- package/dist/layout/neato/sgd-dijkstra.d.ts +20 -0
- package/dist/layout/neato/sgd-dijkstra.d.ts.map +1 -0
- package/dist/layout/neato/sgd.d.ts +14 -9
- package/dist/layout/neato/sgd.d.ts.map +1 -1
- package/dist/layout/neato/simple-scale.d.ts +15 -0
- package/dist/layout/neato/simple-scale.d.ts.map +1 -0
- package/dist/layout/neato/start.d.ts +51 -0
- package/dist/layout/neato/start.d.ts.map +1 -0
- package/dist/layout/neato/vpsc-adjust.d.ts +21 -0
- package/dist/layout/neato/vpsc-adjust.d.ts.map +1 -0
- package/dist/layout/sfdp/index.d.ts.map +1 -1
- package/dist/layout/sfdp/init.d.ts +0 -9
- package/dist/layout/sfdp/init.d.ts.map +1 -1
- package/dist/layout/sfdp/spring-driver.d.ts.map +1 -1
- package/dist/layout/twopi/circle.d.ts +0 -7
- package/dist/layout/twopi/circle.d.ts.map +1 -1
- package/dist/ortho/ortho-parallel.d.ts.map +1 -1
- package/dist/ortho/trap-query.d.ts.map +1 -1
- package/dist/parser/index.d.ts +9 -5
- package/dist/parser/index.d.ts.map +1 -1
- package/dist/render/index.d.ts +2 -0
- package/dist/render/index.d.ts.map +1 -1
- package/dist/render/public.d.ts +9 -3
- package/dist/render/public.d.ts.map +1 -1
- package/dist/render/xdot-public.d.ts +7 -2
- package/dist/render/xdot-public.d.ts.map +1 -1
- package/dist/render.js +7468 -5556
- package/dist/render.js.map +4 -4
- package/dist/util/xml.d.ts.map +1 -1
- package/dist/vpsc/Solver.d.ts +1 -0
- package/dist/vpsc/Solver.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/api/builder.ts +73 -5
- package/src/api/edge-ops.ts +19 -0
- package/src/api/geometry.ts +22 -5
- package/src/async/collect.ts +204 -0
- package/src/async/fonts.ts +61 -0
- package/src/async/render-async.ts +205 -0
- package/src/async/render-into.ts +115 -0
- package/src/async/sanitize.ts +150 -0
- package/src/common/htmltable-types.ts +4 -5
- package/src/common/make-label.ts +10 -1
- package/src/common/poly-shapes.ts +4 -1
- package/src/common/textmeasure-factory.ts +11 -0
- package/src/common/utils-inputscale.ts +31 -0
- package/src/errors.ts +188 -5
- package/src/gvc/context.ts +96 -17
- package/src/gvc/device.ts +11 -6
- package/src/gvc/image-resolver.ts +25 -2
- package/src/gvc/job.ts +3 -1
- package/src/gvc/usershape.ts +6 -0
- package/src/index.ts +59 -43
- package/src/label/index.ts +6 -2
- package/src/label/node.ts +2 -1
- package/src/label/rectangle.ts +4 -2
- package/src/layout/circo/circular.ts +9 -6
- package/src/layout/dot/pack-components.ts +6 -2
- package/src/layout/dot/position.ts +21 -3
- package/src/layout/fdp/derive.ts +7 -6
- package/src/layout/fdp/index.ts +45 -5
- package/src/layout/fdp/init.ts +7 -6
- package/src/layout/fdp/layout.ts +2 -1
- package/src/layout/fdp/normalize.ts +12 -8
- package/src/layout/fdp/ports.ts +3 -2
- package/src/layout/fdp/xlayout.ts +8 -5
- package/src/layout/neato/adjust-info.ts +332 -0
- package/src/layout/neato/cdt-surface.ts +50 -30
- package/src/layout/neato/constraint-adjust.ts +466 -0
- package/src/layout/neato/edge-len.ts +36 -0
- package/src/layout/neato/fdp-adjust.ts +120 -12
- package/src/layout/neato/index.ts +36 -54
- package/src/layout/neato/init.ts +21 -36
- package/src/layout/neato/kk-paths.ts +146 -0
- package/src/layout/neato/kk-solve.ts +64 -0
- package/src/layout/neato/kk.ts +322 -0
- package/src/layout/neato/multispline-router.ts +4 -3
- package/src/layout/neato/poly.ts +493 -0
- package/src/layout/neato/sc-adjust.ts +2 -16
- package/src/layout/neato/sgd-dijkstra.ts +125 -0
- package/src/layout/neato/sgd.ts +63 -40
- package/src/layout/neato/simple-scale.ts +54 -0
- package/src/layout/neato/start.ts +222 -0
- package/src/layout/neato/vpsc-adjust.ts +93 -0
- package/src/layout/sfdp/index.ts +4 -5
- package/src/layout/sfdp/init.ts +40 -4
- package/src/layout/sfdp/spring-driver.ts +30 -1
- package/src/layout/twopi/circle.ts +2 -1
- package/src/ortho/ortho-parallel.ts +2 -1
- package/src/ortho/trap-query.ts +2 -1
- package/src/parser/index.ts +11 -11
- package/src/render/index.ts +5 -0
- package/src/render/public.ts +24 -24
- package/src/render/svg.ts +1 -1
- package/src/render/xdot-public.ts +19 -18
- package/src/util/xml.ts +24 -30
- package/src/vpsc/Solver.ts +5 -3
package/src/layout/neato/sgd.ts
CHANGED
|
@@ -8,10 +8,15 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { Graph } from '../../model/graph.js';
|
|
11
|
+
import type { Node } from '../../model/node.js';
|
|
11
12
|
import type { TermSgd } from './dijkstra.js';
|
|
12
|
-
import { dijkstraSgd } from './dijkstra.js';
|
|
13
|
+
import { dijkstraSgd } from './sgd-dijkstra.js';
|
|
14
|
+
import { setEdgeLen } from './edge-len.js';
|
|
13
15
|
import { rkNewState, rkSeed, rkInterval } from '../../util/mt19937.js';
|
|
14
16
|
import type { RkState } from '../../util/mt19937.js';
|
|
17
|
+
import { drand48 } from '../../common/random.js';
|
|
18
|
+
import { lateDouble } from '../../common/nodeinit.js';
|
|
19
|
+
import { checkStart, parseStart, INIT_RANDOM, INIT_REGULAR } from './start.js';
|
|
15
20
|
|
|
16
21
|
// ---------------------------------------------------------------------------
|
|
17
22
|
// Model constants — @see lib/neatogen/defs.h
|
|
@@ -26,11 +31,14 @@ export const MODEL_SUBSET = 2;
|
|
|
26
31
|
/** MDS distance model (falls back to MODEL_SHORTPATH). */
|
|
27
32
|
export const MODEL_MDS = 3;
|
|
28
33
|
|
|
29
|
-
/**
|
|
30
|
-
const
|
|
34
|
+
/** Epsilon under MODE_SGD: a minimum step size, not the 1e-4 energy tolerance. @see lib/neatogen/stuff.c:scan_graph_mode */
|
|
35
|
+
const SGD_EPSILON = 0.01;
|
|
31
36
|
|
|
32
|
-
/**
|
|
33
|
-
const
|
|
37
|
+
/** MaxIter when `maxiter` is unset under MODE_SGD. @see lib/neatogen/neatoinit.c:1288 */
|
|
38
|
+
const SGD_DFLT_ITERATIONS = 30;
|
|
39
|
+
|
|
40
|
+
/** C's rk_seed(0, &rstate) in sgd(): the shuffle seed is fixed. @see lib/neatogen/sgd.c:215 */
|
|
41
|
+
const SHUFFLE_SEED = 0;
|
|
34
42
|
|
|
35
43
|
// ---------------------------------------------------------------------------
|
|
36
44
|
// GraphSgd — CSR sparse-graph for Dijkstra
|
|
@@ -78,27 +86,31 @@ export function assignNodeIds(g: Graph): { nNodes: number; nEdges: number } {
|
|
|
78
86
|
}
|
|
79
87
|
|
|
80
88
|
/**
|
|
81
|
-
* Append CSR entries for all non-self-loop edges incident on node `np
|
|
89
|
+
* Append CSR entries for all non-self-loop edges incident on node `np`
|
|
90
|
+
* (out-edges first, then in-edges, as agfstedge/agnxtedge).
|
|
82
91
|
* Returns the number of edges appended.
|
|
83
92
|
* @internal
|
|
84
93
|
*/
|
|
85
94
|
export function fillNodeEdges(
|
|
86
|
-
np:
|
|
95
|
+
np: Node,
|
|
87
96
|
g: Graph,
|
|
88
97
|
graph: GraphSgd,
|
|
89
98
|
edgeOffset: number,
|
|
90
99
|
): number {
|
|
91
100
|
let nEdges = edgeOffset;
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
101
|
+
// agfstedge/agnxtedge: every out-edge, then every in-edge, each dict ordered
|
|
102
|
+
// by (other endpoint's seq, edge seq). The order is load-bearing: dijkstra
|
|
103
|
+
// tie-breaking sets the term order that the shuffle then permutes.
|
|
104
|
+
// ND_id is assigned and setEdgeLen sets ED_dist on every edge before
|
|
105
|
+
// extractAdjacency runs (sgd() below), so both are defined here.
|
|
106
|
+
for (const ep of np.outEdges(g)) {
|
|
107
|
+
if (ep.head === np) continue;
|
|
108
|
+
graph.targets[nEdges] = ep.head.info.id!;
|
|
109
|
+
graph.weights[nEdges++] = ep.info.dist!;
|
|
110
|
+
}
|
|
111
|
+
for (const ep of np.inEdges(g)) {
|
|
112
|
+
graph.targets[nEdges] = ep.tail.info.id!;
|
|
113
|
+
graph.weights[nEdges++] = ep.info.dist!;
|
|
102
114
|
}
|
|
103
115
|
return nEdges;
|
|
104
116
|
}
|
|
@@ -252,21 +264,25 @@ export function fisheryatesShuffle(terms: TermSgd[], state: RkState): void {
|
|
|
252
264
|
// Initial positions
|
|
253
265
|
// ---------------------------------------------------------------------------
|
|
254
266
|
|
|
267
|
+
/** C hasPos: ND_pinned > 0 (P_SET from `pos=`, or P_PIN). @see lib/neatogen/stress.c:hasPos */
|
|
268
|
+
function hasPos(np: Node): boolean {
|
|
269
|
+
return np.info.posSet === true || np.info.pinned === true;
|
|
270
|
+
}
|
|
271
|
+
|
|
255
272
|
/**
|
|
256
|
-
*
|
|
257
|
-
*
|
|
273
|
+
* Seed drand48 from `start` (checkStart) and give every node without a
|
|
274
|
+
* position a random one. start=regular keeps the regular placement that
|
|
275
|
+
* solveModel already made (checkStart's INIT_REGULAR early return).
|
|
258
276
|
*
|
|
259
277
|
* @see lib/neatogen/stuff.c:initial_positions
|
|
260
278
|
* @see lib/neatogen/stuff.c:randompos
|
|
261
279
|
*/
|
|
262
|
-
export function initialPositions(g: Graph,
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
np
|
|
267
|
-
|
|
268
|
-
rkInterval(0xffff, state) / 0x10000,
|
|
269
|
-
];
|
|
280
|
+
export function initialPositions(g: Graph, nG: number = g.nodes.size): void {
|
|
281
|
+
if (parseStart(g.root.attrs.get('start'), INIT_RANDOM).init === INIT_REGULAR) return;
|
|
282
|
+
checkStart(g, nG, INIT_RANDOM);
|
|
283
|
+
for (const np of g.nodes.values()) {
|
|
284
|
+
if (hasPos(np)) continue;
|
|
285
|
+
np.info.pos = [drand48(), drand48()];
|
|
270
286
|
}
|
|
271
287
|
}
|
|
272
288
|
|
|
@@ -277,7 +293,7 @@ export function initialPositions(g: Graph, state: RkState): void {
|
|
|
277
293
|
/**
|
|
278
294
|
* Compute the annealing schedule parameters from the term weights.
|
|
279
295
|
*
|
|
280
|
-
* eta_max = 1/w_min; eta_min =
|
|
296
|
+
* eta_max = 1/w_min; eta_min = Epsilon/w_max
|
|
281
297
|
* lambda = log(eta_max/eta_min) / (maxIter - 1)
|
|
282
298
|
*
|
|
283
299
|
* @see lib/neatogen/sgd.c:sgd (initialise annealing schedule)
|
|
@@ -285,6 +301,7 @@ export function initialPositions(g: Graph, state: RkState): void {
|
|
|
285
301
|
export function computeSchedule(
|
|
286
302
|
terms: TermSgd[],
|
|
287
303
|
maxIter: number,
|
|
304
|
+
epsilon: number = SGD_EPSILON,
|
|
288
305
|
): { etaMax: number; lambda: number } {
|
|
289
306
|
let wMin = terms[0].w;
|
|
290
307
|
let wMax = terms[0].w;
|
|
@@ -293,7 +310,7 @@ export function computeSchedule(
|
|
|
293
310
|
if (terms[ij].w > wMax) wMax = terms[ij].w;
|
|
294
311
|
}
|
|
295
312
|
const etaMax = 1 / wMin;
|
|
296
|
-
const etaMin =
|
|
313
|
+
const etaMin = epsilon / wMax;
|
|
297
314
|
const lambda = Math.log(etaMax / etaMin) / (maxIter - 1);
|
|
298
315
|
return { etaMax, lambda };
|
|
299
316
|
}
|
|
@@ -404,15 +421,15 @@ export function resolveModel(model: number): number {
|
|
|
404
421
|
return model;
|
|
405
422
|
}
|
|
406
423
|
|
|
407
|
-
/** Optimisation loop: shuffle and step for
|
|
424
|
+
/** Optimisation loop: shuffle and step for `sched.maxIter` iterations. @see lib/neatogen/sgd.c:sgd */
|
|
408
425
|
export function runSgdLoop(
|
|
409
426
|
pos: Float64Array,
|
|
410
427
|
terms: TermSgd[],
|
|
411
428
|
unfixed: boolean[],
|
|
412
|
-
sched: { etaMax: number; lambda: number },
|
|
429
|
+
sched: { etaMax: number; lambda: number; maxIter: number },
|
|
413
430
|
state: RkState,
|
|
414
431
|
): void {
|
|
415
|
-
for (let t = 0; t <
|
|
432
|
+
for (let t = 0; t < sched.maxIter; t++) {
|
|
416
433
|
fisheryatesShuffle(terms, state);
|
|
417
434
|
const eta = sched.etaMax * Math.exp(-sched.lambda * t);
|
|
418
435
|
sgdIteration(pos, terms, unfixed, eta);
|
|
@@ -431,7 +448,8 @@ export function runSgdLoop(
|
|
|
431
448
|
*
|
|
432
449
|
* Node positions are written to `node.info.pos` ([x, y]).
|
|
433
450
|
* Nodes with `node.info.pinned === true` are not moved.
|
|
434
|
-
*
|
|
451
|
+
* The shuffle RNG is seeded with 0 as in C; `start` seeds only the initial
|
|
452
|
+
* positions (drand48, via checkStart).
|
|
435
453
|
*
|
|
436
454
|
* @see lib/neatogen/sgd.c:sgd
|
|
437
455
|
*/
|
|
@@ -440,22 +458,27 @@ export function sgdLayout(g: Graph, model: number): void {
|
|
|
440
458
|
const n = g.nodes.size;
|
|
441
459
|
if (n === 0) return;
|
|
442
460
|
|
|
443
|
-
const state = rkNewState();
|
|
444
|
-
rkSeed(g.info.seed ?? 0, state);
|
|
445
|
-
|
|
446
461
|
let idx = 0;
|
|
447
462
|
for (const [, node] of g.nodes) { node.info.id = idx++; }
|
|
448
463
|
|
|
449
|
-
|
|
450
|
-
|
|
464
|
+
setEdgeLen(g);
|
|
451
465
|
const graph = extractAdjacency(g, resolvedModel);
|
|
452
466
|
const terms = sgdBuildTerms(graph);
|
|
453
467
|
if (terms.length === 0) return;
|
|
454
468
|
|
|
455
|
-
|
|
469
|
+
// C: MaxIter = atoi(maxiter) else 30; Epsilon = late_double(epsilon, 1e-4)
|
|
470
|
+
// @see lib/neatogen/neatoinit.c:1283-1288
|
|
471
|
+
const maxIterAttr = g.attrs.get('maxiter');
|
|
472
|
+
const maxIter = maxIterAttr !== undefined ? parseInt(maxIterAttr, 10) || 0 : SGD_DFLT_ITERATIONS;
|
|
473
|
+
const epsilon = lateDouble(g.attrs.get('epsilon'), SGD_EPSILON, 0);
|
|
474
|
+
const { etaMax, lambda } = computeSchedule(terms, maxIter, epsilon);
|
|
475
|
+
|
|
476
|
+
initialPositions(g, n);
|
|
456
477
|
const pos = posToArray(g, n);
|
|
457
478
|
const unfixed = buildUnfixed(g, n);
|
|
458
479
|
|
|
459
|
-
|
|
480
|
+
const state = rkNewState();
|
|
481
|
+
rkSeed(SHUFFLE_SEED, state);
|
|
482
|
+
runSgdLoop(pos, terms, unfixed, { etaMax, lambda, maxIter }, state);
|
|
460
483
|
posFromArray(g, pos);
|
|
461
484
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The `scale` graph attribute: multiply every node position by (sx, sy)
|
|
5
|
+
* before overlap removal. removeOverlapWith runs it right after normalize,
|
|
6
|
+
* whatever the overlap mode.
|
|
7
|
+
*
|
|
8
|
+
* @see lib/neatogen/adjust.c:simpleScale
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Graph } from '../../model/graph.js';
|
|
12
|
+
import { aggetGraph } from '../fdp/fdp-model.js';
|
|
13
|
+
|
|
14
|
+
/** @see lib/neatogen/adjust.c:ISZERO */
|
|
15
|
+
const ISZERO = (d: number): boolean => Math.abs(d) < 0.000000001;
|
|
16
|
+
|
|
17
|
+
/** One C `%lf` conversion: optional leading whitespace, then a strtod number. */
|
|
18
|
+
const LF = /^\s*([+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?)/;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* C `sscanf(p, "%lf,%lf", &x, &y)`: the values converted, in order (0, 1 or
|
|
22
|
+
* 2 of them). An empty string returns none — C's EOF (-1) there reads an
|
|
23
|
+
* uninitialised pointf, undefined behaviour the port defines as a no-op.
|
|
24
|
+
*/
|
|
25
|
+
function scanPair(p: string): number[] {
|
|
26
|
+
const m1 = LF.exec(p);
|
|
27
|
+
if (m1 === null) return [];
|
|
28
|
+
const rest = p.slice(m1[0].length);
|
|
29
|
+
if (!rest.startsWith(',')) return [parseFloat(m1[1]!)];
|
|
30
|
+
const m2 = LF.exec(rest.slice(1));
|
|
31
|
+
return m2 === null ? [parseFloat(m1[1]!)] : [parseFloat(m1[1]!), parseFloat(m2[1]!)];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Scale node positions by the `scale` attribute ("s" or "sx,sy"). Returns 1
|
|
36
|
+
* if nodes moved, else 0 (unset, unparsable, a zero factor, or 1,1).
|
|
37
|
+
* @see lib/neatogen/adjust.c:simpleScale
|
|
38
|
+
*/
|
|
39
|
+
export function simpleScale(g: Graph): number {
|
|
40
|
+
const p = aggetGraph(g, 'scale');
|
|
41
|
+
if (p === undefined) return 0;
|
|
42
|
+
const sc = scanPair(p);
|
|
43
|
+
if (sc.length === 0) return 0;
|
|
44
|
+
const x = sc[0]!;
|
|
45
|
+
if (ISZERO(x)) return 0;
|
|
46
|
+
const y = sc.length === 1 ? x : sc[1]!;
|
|
47
|
+
if (ISZERO(y)) return 0;
|
|
48
|
+
if (y === 1 && x === 1) return 0;
|
|
49
|
+
for (const n of g.nodes.values()) {
|
|
50
|
+
n.info.pos![0] *= x;
|
|
51
|
+
n.info.pos![1] *= y;
|
|
52
|
+
}
|
|
53
|
+
return 1;
|
|
54
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EPL-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Neato `start` handling (setSeed / initRegular / checkStart) and the
|
|
4
|
+
* fail-loud checks for the neato init features this port does not have
|
|
5
|
+
* (smart init, the subset/circuit distance models, hier/ipsep modes).
|
|
6
|
+
*
|
|
7
|
+
* Each loud check sits where neato's C branches into the unported code and
|
|
8
|
+
* fires only for the modes whose C path reaches it (ADR-3).
|
|
9
|
+
*
|
|
10
|
+
* @see lib/neatogen/neatoinit.c
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { RenderError } from '../../errors.js';
|
|
14
|
+
import type { Graph } from '../../model/graph.js';
|
|
15
|
+
import { srand48 } from '../../common/random.js';
|
|
16
|
+
import { mapbool, isACluster } from '../dot/rank.js';
|
|
17
|
+
import { getPack } from '../pack/index.js';
|
|
18
|
+
import {
|
|
19
|
+
MODE_KK, MODE_MAJOR, MODE_HIER, MODE_IPSEP, MODE_SGD,
|
|
20
|
+
MODEL_CIRCUIT, MODEL_SUBSET,
|
|
21
|
+
} from './init.js';
|
|
22
|
+
|
|
23
|
+
/** @see lib/neatogen/neato.h:INIT_SELF */
|
|
24
|
+
export const INIT_SELF = 0;
|
|
25
|
+
/** @see lib/neatogen/neato.h:INIT_REGULAR */
|
|
26
|
+
export const INIT_REGULAR = 1;
|
|
27
|
+
/** @see lib/neatogen/neato.h:INIT_RANDOM */
|
|
28
|
+
export const INIT_RANDOM = 2;
|
|
29
|
+
|
|
30
|
+
/** @see lib/common/const.h:Spring_coeff */
|
|
31
|
+
const SPRING_COEFF = 1.0;
|
|
32
|
+
|
|
33
|
+
/** `start` keyword prefixes. @see lib/neatogen/neatoinit.c:SMART,REGULAR,RANDOM */
|
|
34
|
+
const START_KEYWORDS: readonly (readonly [string, number])[] = [
|
|
35
|
+
['self', INIT_SELF],
|
|
36
|
+
['regular', INIT_REGULAR],
|
|
37
|
+
['random', INIT_RANDOM],
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
const WARN_POS_IGNORED = 'node positions are ignored unless start=random';
|
|
41
|
+
const WARN_SELF_IGNORED = 'start=0 not supported with mode=self - ignored';
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Roots already warned about start=self under KK/SGD. C warns once per process
|
|
45
|
+
* (stuff.c:330 static atomic_flag); one render is one C process here, so the
|
|
46
|
+
* flag is per root graph rather than module state (multi-diagram safety).
|
|
47
|
+
*/
|
|
48
|
+
const selfWarned = new WeakSet<object>();
|
|
49
|
+
/** Roots already warned about the KK circuit fallback (one warning per graph). */
|
|
50
|
+
const circuitWarned = new WeakSet<object>();
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Classify the `start` attribute: keyword prefix, digit-led (random), else the
|
|
54
|
+
* default. A random seed comes from the digits following the keyword; with
|
|
55
|
+
* none, C seeds from time(), which this port keeps at the default seed 1.
|
|
56
|
+
* @see lib/neatogen/neatoinit.c:setSeed
|
|
57
|
+
*/
|
|
58
|
+
export function parseStart(
|
|
59
|
+
p: string | undefined, dflt: number,
|
|
60
|
+
): { init: number; seed: number } {
|
|
61
|
+
let init = dflt;
|
|
62
|
+
let rest = p ?? '';
|
|
63
|
+
if (rest === '') return { init, seed: 1 };
|
|
64
|
+
if (/^[A-Za-z]/.test(rest)) {
|
|
65
|
+
const kw = START_KEYWORDS.find(([k]) => rest.startsWith(k));
|
|
66
|
+
if (kw !== undefined) { init = kw[1]; rest = rest.slice(kw[0].length); }
|
|
67
|
+
} else if (/^\d/.test(rest)) {
|
|
68
|
+
init = INIT_RANDOM;
|
|
69
|
+
}
|
|
70
|
+
const seed = init === INIT_RANDOM && /^\d/.test(rest) ? parseInt(rest, 10) : 1;
|
|
71
|
+
return { init, seed };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Place the nodes on a regular polygon and mark them P_SET (positioned, not
|
|
76
|
+
* pinned), so the stress kernel's initLayout starts from them.
|
|
77
|
+
* @see lib/neatogen/neatoinit.c:initRegular
|
|
78
|
+
*/
|
|
79
|
+
export function initRegular(g: Graph, nG: number): void {
|
|
80
|
+
let a = 0.0;
|
|
81
|
+
const da = (2 * Math.PI) / nG;
|
|
82
|
+
for (const np of g.nodes.values()) {
|
|
83
|
+
np.info.pos = [nG * SPRING_COEFF * Math.cos(a), nG * SPRING_COEFF * Math.sin(a)];
|
|
84
|
+
np.info.posSet = true;
|
|
85
|
+
np.info.pinned = false; // C: ND_pinned = P_SET overrides P_PIN
|
|
86
|
+
a += da;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Analyse `start`: place regular nodes, seed the RNG, return the init kind.
|
|
92
|
+
* @see lib/neatogen/neatoinit.c:checkStart
|
|
93
|
+
*/
|
|
94
|
+
export function checkStart(
|
|
95
|
+
g: Graph, nG: number = g.nodes.size, dflt: number = INIT_RANDOM,
|
|
96
|
+
): number {
|
|
97
|
+
const { init, seed } = parseStart(g.root.attrs.get('start'), dflt);
|
|
98
|
+
if (init !== INIT_RANDOM && [...g.nodes.values()].some((n) => n.attrs.has('pos'))) {
|
|
99
|
+
console.warn(WARN_POS_IGNORED);
|
|
100
|
+
}
|
|
101
|
+
if (init === INIT_REGULAR) initRegular(g, nG);
|
|
102
|
+
srand48(seed);
|
|
103
|
+
return init;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function unsupported(attr: string, value: string, what: string): never {
|
|
107
|
+
throw new RenderError(`${attr}=${value}: ${what} is not supported yet`, 'UNSUPPORTED_FEATURE');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Does C's ipsep branch build constraints? @see neatoinit.c:1132-1150 */
|
|
111
|
+
function ipsepHasConstraints(g: Graph): boolean {
|
|
112
|
+
const dir = g.root.attrs.get('diredgeconstraints') ?? '';
|
|
113
|
+
if (mapbool(dir) || /^hier/i.test(dir)) return true;
|
|
114
|
+
if ((g.root.attrs.get('overlap') ?? '').toLowerCase() === 'ipsep') return true;
|
|
115
|
+
return [...g.root.subgraphs.values()].some((s) => s !== g.root && isACluster(s));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** @see lib/neatogen/neatoinit.c:1290 (MaxIter < 0 returns before any solver) */
|
|
119
|
+
function maxIterNegative(g: Graph): boolean {
|
|
120
|
+
const v = g.attrs.get('maxiter') ?? g.root.attrs.get('maxiter');
|
|
121
|
+
if (v === undefined) return false;
|
|
122
|
+
const n = parseInt(v, 10);
|
|
123
|
+
return !Number.isNaN(n) && n < 0;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** @see lib/neatogen/neatoinit.c:neatoMode (DIGCOLA + IPSEPCOLA builds) */
|
|
127
|
+
// A function, not a constant: init.ts and start.ts import each other, so the
|
|
128
|
+
// MODE_* values are not yet initialised when this module body runs.
|
|
129
|
+
const modeByName = (): ReadonlyMap<string, number> => new Map([
|
|
130
|
+
['KK', MODE_KK], ['major', MODE_MAJOR], ['sgd', MODE_SGD],
|
|
131
|
+
['hier', MODE_HIER], ['ipsep', MODE_IPSEP],
|
|
132
|
+
]);
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Read the root `mode` attribute: empty/absent is MODE_MAJOR, an unknown value
|
|
136
|
+
* warns with C's text and is ignored. Case sensitive, as C's streq.
|
|
137
|
+
* @see lib/neatogen/neatoinit.c:neatoMode
|
|
138
|
+
*/
|
|
139
|
+
export function neatoMode(g: Graph): number {
|
|
140
|
+
const str = g.root.attrs.get('mode');
|
|
141
|
+
if (str === undefined || str === '') return MODE_MAJOR;
|
|
142
|
+
const mode = modeByName().get(str);
|
|
143
|
+
if (mode !== undefined) return mode;
|
|
144
|
+
console.warn(`Illegal value ${str} for attribute "mode" in graph ${g.root.name} - ignored`);
|
|
145
|
+
return MODE_MAJOR;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function checkMode(g: Graph, mode: number): void {
|
|
149
|
+
const value = g.root.attrs.get('mode') || (mode === MODE_HIER ? 'hier' : 'ipsep');
|
|
150
|
+
// @see lib/neatogen/neatoinit.c:1117 (stress_majorization_with_hierarchy)
|
|
151
|
+
if (mode === MODE_HIER) unsupported('mode', value, 'hierarchical stress majorization');
|
|
152
|
+
// @see lib/neatogen/neatoinit.c:1121-1170 (stress_majorization_cola)
|
|
153
|
+
if (mode === MODE_IPSEP && ipsepHasConstraints(g)) {
|
|
154
|
+
unsupported('mode', value, 'constrained (ipsep) majorization');
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function checkStartAttr(g: Graph, mode: number): void {
|
|
159
|
+
const value = g.root.attrs.get('start');
|
|
160
|
+
if (parseStart(value, INIT_RANDOM).init !== INIT_SELF) return;
|
|
161
|
+
// @see lib/neatogen/neatoinit.c:1095, lib/neatogen/stress.c:884 (smart_ini)
|
|
162
|
+
if (mode === MODE_MAJOR || mode === MODE_IPSEP) unsupported('start', value ?? '', 'smart initialisation');
|
|
163
|
+
// @see lib/neatogen/stuff.c:330 (KK and SGD warn once and ignore it)
|
|
164
|
+
if ((mode === MODE_KK || mode === MODE_SGD) && !selfWarned.has(g.root)) {
|
|
165
|
+
selfWarned.add(g.root);
|
|
166
|
+
console.warn(WARN_SELF_IGNORED);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function isConnected(g: Graph): boolean {
|
|
171
|
+
const nodes = [...g.nodes.values()];
|
|
172
|
+
if (nodes.length === 0) return true;
|
|
173
|
+
const adj = new Map<unknown, unknown[]>(nodes.map((n) => [n, []]));
|
|
174
|
+
for (const e of g.edges) { adj.get(e.tail)?.push(e.head); adj.get(e.head)?.push(e.tail); }
|
|
175
|
+
const seen = new Set<unknown>([nodes[0]]);
|
|
176
|
+
const stack: unknown[] = [nodes[0]];
|
|
177
|
+
for (let n = stack.pop(); n !== undefined; n = stack.pop()) {
|
|
178
|
+
for (const m of adj.get(n) ?? []) if (!seen.has(m)) { seen.add(m); stack.push(m); }
|
|
179
|
+
}
|
|
180
|
+
return seen.size === nodes.length;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** KK runs unpacked unless `pack` or `packmode` is set. @see neatoinit.c:1380-1389 */
|
|
184
|
+
function kkCircuitFallsBack(g: Graph): boolean {
|
|
185
|
+
const packed = getPack(g.root, -1, 0) >= 0 || (g.root.attrs.get('packmode') ?? '') !== '';
|
|
186
|
+
return !packed && !isConnected(g.root);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function warnCircuitDisconnected(g: Graph): void {
|
|
190
|
+
if (circuitWarned.has(g.root)) return;
|
|
191
|
+
circuitWarned.add(g.root);
|
|
192
|
+
console.warn(
|
|
193
|
+
`graph ${g.root.name} is disconnected. Hence, the circuit model\n`
|
|
194
|
+
+ 'is undefined. Reverting to the shortest path model.\n'
|
|
195
|
+
+ 'Alternatively, consider running neato using -Gpack=true or decomposing\n'
|
|
196
|
+
+ 'the graph into connected components.',
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function checkModel(g: Graph, mode: number, model: number): void {
|
|
201
|
+
const value = g.root.attrs.get('model') ?? '';
|
|
202
|
+
const stressPath = mode === MODE_MAJOR || mode === MODE_KK;
|
|
203
|
+
// @see lib/neatogen/stress.c:846, lib/neatogen/neatoinit.c:1246
|
|
204
|
+
if (model === MODEL_SUBSET && stressPath) unsupported('model', value, 'the subset distance model');
|
|
205
|
+
if (model !== MODEL_CIRCUIT || !stressPath) return;
|
|
206
|
+
// @see lib/neatogen/neatoinit.c:1248-1257 (disconnected: warn + shortpath)
|
|
207
|
+
if (mode === MODE_KK && kkCircuitFallsBack(g)) { warnCircuitDisconnected(g); return; }
|
|
208
|
+
// @see lib/neatogen/stress.c:851, lib/neatogen/circuit.c:circuit_model
|
|
209
|
+
unsupported('model', value, 'the circuit distance model');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Throw UNSUPPORTED_FEATURE where neato's C would run unported init code for
|
|
214
|
+
* this mode/model/start. Call after the nG < 2 guard.
|
|
215
|
+
* @see lib/neatogen/neatoinit.c:neatoLayout
|
|
216
|
+
*/
|
|
217
|
+
export function assertSupported(g: Graph, mode: number, model: number): void {
|
|
218
|
+
if (maxIterNegative(g)) return;
|
|
219
|
+
checkMode(g, mode);
|
|
220
|
+
checkStartAttr(g, mode);
|
|
221
|
+
checkModel(g, mode, model);
|
|
222
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `overlap=vpsc`: the VPSC adjuster of removeOverlapWith, moved out of
|
|
5
|
+
* neato/index.ts so every engine reaches it through the shared dispatch.
|
|
6
|
+
*
|
|
7
|
+
* C works on `float` coordinates (`float *coords[2]`), so positions are rounded
|
|
8
|
+
* to float on entry and after each pass: that decides exact-touch verdicts
|
|
9
|
+
* between the x and y passes.
|
|
10
|
+
*
|
|
11
|
+
* @see lib/neatogen/adjust.c:vpscAdjust
|
|
12
|
+
* @see lib/neatogen/quad_prog_vpsc.c:removeoverlaps
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { Graph } from '../../model/graph.js';
|
|
16
|
+
import {
|
|
17
|
+
newVariable,
|
|
18
|
+
deleteVariable,
|
|
19
|
+
deleteVPSC,
|
|
20
|
+
deleteConstraints,
|
|
21
|
+
solveVPSC,
|
|
22
|
+
genXConstraints,
|
|
23
|
+
genYConstraints,
|
|
24
|
+
VPSC,
|
|
25
|
+
Rectangle,
|
|
26
|
+
type Variable,
|
|
27
|
+
} from '../../vpsc/index.js';
|
|
28
|
+
import { sepFactor, DFLT_MARGIN } from './sep-factor.js';
|
|
29
|
+
|
|
30
|
+
const POINTS_PER_INCH = 72;
|
|
31
|
+
/** Widen nodes in the x pass so a horizontally resolved pair does not count as
|
|
32
|
+
* vertically overlapping. @see quad_prog_vpsc.c:generateNonoverlapConstraints */
|
|
33
|
+
const X_PASS_SCALE = Math.fround(1.0001);
|
|
34
|
+
|
|
35
|
+
interface Gap { x: number; y: number }
|
|
36
|
+
interface VpscBox { w: number; h: number }
|
|
37
|
+
type Coords = readonly [Float32Array, Float32Array];
|
|
38
|
+
|
|
39
|
+
/** One Rectangle per node: size scaled by `scale`, plus half the gap per side.
|
|
40
|
+
* @see lib/neatogen/quad_prog_vpsc.c:generateNonoverlapConstraints */
|
|
41
|
+
function buildRects(c: Coords, size: VpscBox[], gap: Gap, scale: number): Rectangle[] {
|
|
42
|
+
return size.map((s, i) => {
|
|
43
|
+
const hw = (scale * s.w) / 2.0 + gap.x / 2.0;
|
|
44
|
+
const hh = (scale * s.h) / 2.0 + gap.y / 2.0;
|
|
45
|
+
return new Rectangle(c[0][i]! - hw, c[0][i]! + hw, c[1][i]! - hh, c[1][i]! + hh);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Solve one axis and store the result (rounded to float) into `c[axis]`.
|
|
50
|
+
* @see lib/neatogen/quad_prog_vpsc.c:removeoverlaps */
|
|
51
|
+
function solveAxis(c: Coords, size: VpscBox[], gap: Gap, axis: 0 | 1): void {
|
|
52
|
+
const rects = buildRects(c, size, gap, axis === 0 ? X_PASS_SCALE : 1);
|
|
53
|
+
const vars: Variable[] = size.map((_, i) => newVariable(i, 1.0, 1.0));
|
|
54
|
+
const cs = axis === 0 ? genXConstraints(rects, vars, true) : genYConstraints(rects, vars);
|
|
55
|
+
const vpsc = new VPSC(vars, cs);
|
|
56
|
+
solveVPSC(vpsc);
|
|
57
|
+
// solveVPSC places every variable in a block. @see quad_prog_vpsc.c:568 getVariablePos
|
|
58
|
+
vars.forEach((v, i) => {
|
|
59
|
+
c[axis][i] = v.position();
|
|
60
|
+
});
|
|
61
|
+
deleteVPSC(vpsc);
|
|
62
|
+
deleteConstraints(cs);
|
|
63
|
+
for (const v of vars) deleteVariable(v);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Remove overlaps with two VPSC passes (x then y). The gap handed to
|
|
68
|
+
* removeoverlaps is twice the separation margin (opt.gap is the gap size, not
|
|
69
|
+
* the margin): `2 * sep` when `sep` is additive, else twice DFLT_MARGIN.
|
|
70
|
+
* Returns 0 as C does.
|
|
71
|
+
* @see lib/neatogen/adjust.c:vpscAdjust
|
|
72
|
+
*/
|
|
73
|
+
export function vpscAdjust(g: Graph): number {
|
|
74
|
+
const margin = sepFactor(g);
|
|
75
|
+
const gap: Gap = {
|
|
76
|
+
x: (2 * (margin.doAdd ? margin.x : DFLT_MARGIN)) / POINTS_PER_INCH,
|
|
77
|
+
y: (2 * (margin.doAdd ? margin.y : DFLT_MARGIN)) / POINTS_PER_INCH,
|
|
78
|
+
};
|
|
79
|
+
const nodes = Array.from(g.nodes.values());
|
|
80
|
+
const c: Coords = [new Float32Array(nodes.length), new Float32Array(nodes.length)];
|
|
81
|
+
// Layout has set ND_pos and node init ND_width/ND_height before any adjuster.
|
|
82
|
+
nodes.forEach((n, i) => {
|
|
83
|
+
c[0][i] = n.info.pos![0];
|
|
84
|
+
c[1][i] = n.info.pos![1];
|
|
85
|
+
});
|
|
86
|
+
const size = nodes.map((n) => ({ w: n.info.width!, h: n.info.height! }));
|
|
87
|
+
solveAxis(c, size, gap, 0);
|
|
88
|
+
solveAxis(c, size, gap, 1);
|
|
89
|
+
nodes.forEach((n, i) => {
|
|
90
|
+
n.info.pos = [c[0][i]!, c[1][i]!];
|
|
91
|
+
});
|
|
92
|
+
return 0;
|
|
93
|
+
}
|
package/src/layout/sfdp/index.ts
CHANGED
|
@@ -31,8 +31,7 @@ import { CL_OFFSET } from '../twopi/pipeline.js';
|
|
|
31
31
|
import { placeGraphLabel } from '../dot/position-bbox.js';
|
|
32
32
|
import { gvPostprocess } from '../../common/postproc.js';
|
|
33
33
|
import { aggetGraph } from '../fdp/fdp-model.js';
|
|
34
|
-
import { overlapPrismTries } from '../neato/fdp-adjust.js';
|
|
35
|
-
import { adjustNodesScale } from '../neato/sc-adjust.js';
|
|
34
|
+
import { overlapPrismTries, adjustNodesFull } from '../neato/fdp-adjust.js';
|
|
36
35
|
import { lateDouble } from '../../common/nodeinit.js';
|
|
37
36
|
import {
|
|
38
37
|
type SpringElectricalControl,
|
|
@@ -85,7 +84,7 @@ export function sfdpLayoutComponent(
|
|
|
85
84
|
* for prism/false), ctrl.initialScaling = overlap_scaling (default −4), and
|
|
86
85
|
* doAdjust is false. Otherwise (scale family, AM_NONE, unported modes) sfdp's
|
|
87
86
|
* in-layout removal is off (ctrl.overlap = −1) and doAdjust drives a
|
|
88
|
-
* post-layout removeOverlapWith (
|
|
87
|
+
* post-layout removeOverlapWith (adjustNodesFull).
|
|
89
88
|
* @see lib/neatogen/adjust.c:graphAdjustMode / getAdjustMode (15.0.0)
|
|
90
89
|
* @see lib/sfdpgen/sfdpinit.c:sfdp_layout (HAVE_GTS branch)
|
|
91
90
|
*/
|
|
@@ -139,7 +138,7 @@ function layoutComponents(
|
|
|
139
138
|
for (const sg of comps) {
|
|
140
139
|
// ccomps already node-induces edges (graphviz_node_induce).
|
|
141
140
|
sfdpLayoutComponent(sg, ctrl, pad);
|
|
142
|
-
if (doAdjust)
|
|
141
|
+
if (doAdjust) adjustNodesFull(sg); // removeOverlapWith (non-PRISM modes)
|
|
143
142
|
// @see lib/sfdpgen/sfdpinit.c:284 (setEdgeType FUNCTION, per component)
|
|
144
143
|
setEdgeTypeFromAttr(sg, EDGETYPE_LINE);
|
|
145
144
|
splineEdgesShifted(sg);
|
|
@@ -213,7 +212,7 @@ export function sfdpLayout(g: Graph): void {
|
|
|
213
212
|
singleComponent = comps.length === 1;
|
|
214
213
|
if (singleComponent) {
|
|
215
214
|
sfdpLayoutComponent(g, ctrl, pad);
|
|
216
|
-
if (doAdjust)
|
|
215
|
+
if (doAdjust) adjustNodesFull(g); // removeOverlapWith (non-PRISM modes)
|
|
217
216
|
splineEdgesShifted(g); // C spline_edges: shift + coord sync + route
|
|
218
217
|
} else {
|
|
219
218
|
layoutComponents(g, comps, ctrl, pad, doAdjust);
|