@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knowvah/dot-engine",
3
- "version": "2.0.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) * (Math.PI / 180);
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 = /^-?[\d.]+(?:[eE][-+]?\d+)?/.exec(a);
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): void {
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) rotateAll(nodes, e.tail, 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 (as noted in
318
- * normalize.ts / sc-adjust.ts) unless set on the derived chain — simpleScale
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 before the mode switch
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 (only active with the `normalize`/`scale` attributes) are not
212
- * ported (see sc-adjust module doc).
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
- 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);
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
- * Other overlap modes (voronoi, prism, vpsc, ortho*) remain unported; with
15
- * the attr unset C's getAdjustMode resolves to AM_NONE and the pass is a
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
+ }