@knowvah/dot-engine 2.0.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/dist/index.js +101 -68
- package/dist/index.js.map +4 -4
- package/dist/layout/fdp/normalize.d.ts +2 -1
- package/dist/layout/fdp/normalize.d.ts.map +1 -1
- package/dist/layout/fdp/xlayout.d.ts.map +1 -1
- package/dist/layout/neato/fdp-adjust.d.ts +2 -2
- package/dist/layout/neato/fdp-adjust.d.ts.map +1 -1
- 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/simple-scale.d.ts +15 -0
- package/dist/layout/neato/simple-scale.d.ts.map +1 -0
- package/dist/render.js +101 -68
- package/dist/render.js.map +4 -4
- package/package.json +1 -1
- package/src/layout/fdp/normalize.ts +12 -8
- package/src/layout/fdp/xlayout.ts +5 -4
- package/src/layout/neato/fdp-adjust.ts +8 -5
- package/src/layout/neato/sc-adjust.ts +2 -16
- package/src/layout/neato/simple-scale.ts +54 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@knowvah/dot-engine",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "Faithful TypeScript port of Graphviz (dot, neato, fdp, sfdp, circo, twopi, osage, patchwork) — no C: no native binary, no WASM. Oracle-verified against the C implementation, runs in the browser.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"graphviz",
|
|
@@ -34,12 +34,12 @@ function angleSet(g: Graph): number | null {
|
|
|
34
34
|
if (a === undefined || a === '') return null;
|
|
35
35
|
const ang = parseAngle(a);
|
|
36
36
|
if (ang === null) return null;
|
|
37
|
-
return wrapDegrees(ang) *
|
|
37
|
+
return (wrapDegrees(ang) / 180.0) * Math.PI; // RADIANS(a) = a/180.0*M_PI
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
/** strtod with the mapbool fallback of angleSet. */
|
|
40
|
+
/** strtod (leading whitespace, sign) with the mapbool fallback of angleSet. */
|
|
41
41
|
function parseAngle(a: string): number | null {
|
|
42
|
-
const m =
|
|
42
|
+
const m = /^\s*[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?/.exec(a);
|
|
43
43
|
if (m !== null) return parseFloat(m[0]);
|
|
44
44
|
/* no number */
|
|
45
45
|
return mapbool(a) ? 0.0 : null;
|
|
@@ -55,34 +55,38 @@ function wrapDegrees(ang: number): number {
|
|
|
55
55
|
/**
|
|
56
56
|
* If "normalize" is set, move the first node to the origin, then
|
|
57
57
|
* rotate the layout so the first edge sits at the given angle.
|
|
58
|
+
* Returns 1 if nodes moved, else 0.
|
|
58
59
|
* @see lib/neatogen/adjust.c:normalize (15.0.0)
|
|
59
60
|
*/
|
|
60
|
-
export function normalizeG(g: Graph):
|
|
61
|
+
export function normalizeG(g: Graph): number {
|
|
61
62
|
const phi0 = angleSet(g);
|
|
62
|
-
if (phi0 === null) return;
|
|
63
|
+
if (phi0 === null) return 0;
|
|
63
64
|
|
|
64
65
|
const nodes = [...g.nodes.values()];
|
|
65
66
|
const first = nodes[0];
|
|
66
|
-
if (first === undefined) return;
|
|
67
|
+
if (first === undefined) return 0;
|
|
67
68
|
const px = first.info.pos![0]!;
|
|
68
69
|
const py = first.info.pos![1]!;
|
|
69
70
|
for (const v of nodes) {
|
|
70
71
|
v.info.pos![0]! -= px;
|
|
71
72
|
v.info.pos![1]! -= py;
|
|
72
73
|
}
|
|
74
|
+
const ret = px || py ? 1 : 0;
|
|
73
75
|
|
|
74
76
|
let e = null;
|
|
75
77
|
for (const v of nodes) {
|
|
76
78
|
const out = v.outEdges(g);
|
|
77
79
|
if (out.length > 0) { e = out[0]!; break; }
|
|
78
80
|
}
|
|
79
|
-
if (e === null) return;
|
|
81
|
+
if (e === null) return ret;
|
|
80
82
|
|
|
81
83
|
/* rotation necessary; pos => ccw */
|
|
82
84
|
const phi = phi0 - Math.atan2(
|
|
83
85
|
e.head.info.pos![1]! - e.tail.info.pos![1]!,
|
|
84
86
|
e.head.info.pos![0]! - e.tail.info.pos![0]!);
|
|
85
|
-
if (phi)
|
|
87
|
+
if (!phi) return ret;
|
|
88
|
+
rotateAll(nodes, e.tail, phi);
|
|
89
|
+
return 1;
|
|
86
90
|
}
|
|
87
91
|
|
|
88
92
|
/** Rotate all positions by phi about the orig node. */
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
} from './fdp-model.js';
|
|
33
33
|
import { coincidentDelta } from './tlayout.js';
|
|
34
34
|
import { normalizeG } from './normalize.js';
|
|
35
|
+
import { simpleScale } from '../neato/simple-scale.js';
|
|
35
36
|
|
|
36
37
|
/** @see lib/fdpgen/xlayout.c:DFLT_overlap */
|
|
37
38
|
const DFLT_OVERLAP = '9:prism';
|
|
@@ -314,14 +315,14 @@ export function fdpXLayout(g: Graph, xpms: XParams): void {
|
|
|
314
315
|
* scale family (scale/scalexy/compress) → scAdjust;
|
|
315
316
|
* AM_NONE ('' / 'true') and unported modes (voronoi) → no-op.
|
|
316
317
|
* normalize reads "normalize" and simpleScale reads "scale"; deriveGraph
|
|
317
|
-
* copies only overlap/sep/K, so both are faithfully dead here
|
|
318
|
-
*
|
|
319
|
-
* follows the project's decision to leave it unported.
|
|
318
|
+
* copies only overlap/sep/K, so both are faithfully dead here unless set on
|
|
319
|
+
* the derived chain (native fdp ignores both attributes).
|
|
320
320
|
* @see lib/neatogen/adjust.c:removeOverlapAs / removeOverlapWith / getAdjustMode
|
|
321
321
|
*/
|
|
322
322
|
function removeOverlapAs(g: Graph, flag: string): void {
|
|
323
323
|
if (g.nodes.size < 2) return; // removeOverlapWith: <2 nodes short-circuits
|
|
324
|
-
normalizeG(g); // removeOverlapWith runs normalize
|
|
324
|
+
normalizeG(g); // removeOverlapWith runs normalize + simpleScale first
|
|
325
|
+
simpleScale(g);
|
|
325
326
|
const ntry = overlapPrismTries(flag);
|
|
326
327
|
if (ntry !== null) {
|
|
327
328
|
fdpAdjust(g, ntry); // AM_PRISM
|
|
@@ -18,6 +18,8 @@ import {
|
|
|
18
18
|
} from '../sfdp/sparse-matrix.js';
|
|
19
19
|
import { removeOverlapPrism } from './overlap-prism.js';
|
|
20
20
|
import { scAdjust } from './sc-adjust.js';
|
|
21
|
+
import { simpleScale } from './simple-scale.js';
|
|
22
|
+
import { normalizeG } from '../fdp/normalize.js';
|
|
21
23
|
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
24
|
import { countOverlapIn, sAdjustGraph } from './adjust-info.js';
|
|
23
25
|
import { vpscAdjust } from './vpsc-adjust.js';
|
|
@@ -208,15 +210,16 @@ function adjustVoronoi(g: Graph, am: AdjustData): number {
|
|
|
208
210
|
|
|
209
211
|
/**
|
|
210
212
|
* Use `am` to decide if and how to remove node overlaps. normalize and
|
|
211
|
-
* simpleScale
|
|
212
|
-
*
|
|
213
|
+
* simpleScale run first, whatever the mode (the `normalize` / `scale`
|
|
214
|
+
* attributes); returns non-zero if nodes moved.
|
|
213
215
|
* @see lib/neatogen/adjust.c:removeOverlapWith
|
|
214
216
|
*/
|
|
215
217
|
export function removeOverlapWith(g: Graph, am: AdjustData): number {
|
|
216
218
|
if (g.nodes.size < 2) return 0;
|
|
217
|
-
|
|
218
|
-
if (am.mode
|
|
219
|
-
|
|
219
|
+
const nret = normalizeG(g) + simpleScale(g);
|
|
220
|
+
if (am.mode === AM_NONE) return nret;
|
|
221
|
+
if (am.mode > AM_SCALE) return nret + adjustLate(g, am);
|
|
222
|
+
return nret + (am.mode === AM_SCALE ? sAdjustGraph(g) : adjustVoronoi(g, am));
|
|
220
223
|
}
|
|
221
224
|
|
|
222
225
|
/**
|
|
@@ -11,9 +11,8 @@
|
|
|
11
11
|
* Using Constrained Optimization", Constraints 8(2):143-172, 2003 (per the
|
|
12
12
|
* C source comment).
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* no-op (normalize/simpleScale only fire on rare attrs, also unported).
|
|
14
|
+
* Dispatched from removeOverlapWith (fdp-adjust.ts) for overlap=scale,
|
|
15
|
+
* scalexy and compress.
|
|
17
16
|
*
|
|
18
17
|
* @see lib/neatogen/constraint.c:scAdjust
|
|
19
18
|
* @see lib/neatogen/adjust.c:removeOverlapWith / getAdjustMode
|
|
@@ -191,16 +190,3 @@ export function scAdjust(g: Graph, equal: number): number {
|
|
|
191
190
|
return 1;
|
|
192
191
|
}
|
|
193
192
|
|
|
194
|
-
/**
|
|
195
|
-
* The adjustNodes overlap-mode subset reaching scAdjust; other modes are
|
|
196
|
-
* no-ops (see module doc).
|
|
197
|
-
* @see lib/neatogen/adjust.c:999 adjustNodes, :776 adjustMode table
|
|
198
|
-
*/
|
|
199
|
-
export function adjustNodesScale(g: Graph): number {
|
|
200
|
-
// agget falls back to the root graph's attrs for component subgraphs.
|
|
201
|
-
const flag = (g.attrs.get('overlap') ?? g.root.attrs.get('overlap') ?? '').toLowerCase();
|
|
202
|
-
if (flag === 'scale') return scAdjust(g, 1); // AM_NSCALE
|
|
203
|
-
if (flag === 'scalexy') return scAdjust(g, 0); // AM_SCALEXY
|
|
204
|
-
if (flag === 'compress') return scAdjust(g, -1); // AM_COMPRESS
|
|
205
|
-
return 0; // unset -> AM_NONE; other modes unported
|
|
206
|
-
}
|
|
@@ -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
|
+
}
|