@knowvah/dot-engine 2.0.2 → 2.2.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.
- package/dist/async/render-async.d.ts.map +1 -1
- package/dist/common/arith.d.ts +6 -0
- package/dist/common/arith.d.ts.map +1 -1
- package/dist/common/libm-hypot.d.ts +3 -0
- package/dist/common/libm-hypot.d.ts.map +1 -0
- package/dist/common/poly-sizing.d.ts.map +1 -1
- package/dist/common/textmeasure-factory.d.ts +2 -1
- package/dist/common/textmeasure-factory.d.ts.map +1 -1
- package/dist/common/textmeasure.d.ts +1 -1
- package/dist/common/textmeasure.d.ts.map +1 -1
- package/dist/index.js +97 -59
- package/dist/index.js.map +4 -4
- package/dist/layout/dot/splines-label.d.ts.map +1 -1
- package/dist/layout/fdp/derive.d.ts.map +1 -1
- package/dist/layout/fdp/fdp-model.d.ts +4 -4
- package/dist/layout/fdp/normalize.d.ts.map +1 -1
- package/dist/layout/fdp/tlayout-init.d.ts.map +1 -1
- package/dist/layout/fdp/tlayout-parms.d.ts +4 -1
- package/dist/layout/fdp/tlayout-parms.d.ts.map +1 -1
- package/dist/layout/fdp/tlayout.d.ts +3 -4
- package/dist/layout/fdp/tlayout.d.ts.map +1 -1
- package/dist/layout/fdp/xlayout.d.ts.map +1 -1
- package/dist/render.js +95 -57
- package/dist/render.js.map +4 -4
- package/package.json +1 -1
- package/src/async/render-async.ts +6 -3
- package/src/common/arith.ts +9 -0
- package/src/common/libm-hypot.ts +40 -0
- package/src/common/poly-sizing.ts +12 -11
- package/src/common/textmeasure-factory.ts +20 -1
- package/src/common/textmeasure.ts +1 -1
- package/src/layout/dot/splines-label.ts +2 -1
- package/src/layout/fdp/derive.ts +5 -2
- package/src/layout/fdp/fdp-model.ts +4 -4
- package/src/layout/fdp/normalize.ts +2 -1
- package/src/layout/fdp/tlayout-init.ts +8 -6
- package/src/layout/fdp/tlayout-parms.ts +5 -2
- package/src/layout/fdp/tlayout.ts +18 -17
- package/src/layout/fdp/xlayout.ts +26 -22
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@knowvah/dot-engine",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.2.0",
|
|
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",
|
|
@@ -63,11 +63,14 @@ export interface AsyncSvgResult {
|
|
|
63
63
|
fontIssues: FontIssue[];
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
/**
|
|
66
|
+
/**
|
|
67
|
+
* `document.fonts` in a page, else `self.fonts` (WorkerGlobalScope.fonts) in a
|
|
68
|
+
* Worker; undefined in Node. @see plans/async-api/DESIGN.md D5
|
|
69
|
+
*/
|
|
67
70
|
function defaultFontSet(): FontSetLike | undefined {
|
|
68
71
|
const doc: unknown = Reflect.get(globalThis, 'document');
|
|
69
|
-
|
|
70
|
-
const fonts: unknown = Reflect.get(
|
|
72
|
+
const owner: unknown = typeof doc === 'object' && doc !== null ? doc : globalThis;
|
|
73
|
+
const fonts: unknown = Reflect.get(owner as object, 'fonts');
|
|
71
74
|
if (typeof fonts !== 'object' || fonts === null) return undefined;
|
|
72
75
|
return fonts as FontSetLike; // structural: FontFaceSet.load
|
|
73
76
|
}
|
package/src/common/arith.ts
CHANGED
|
@@ -37,3 +37,12 @@
|
|
|
37
37
|
export function cround(v: number): number {
|
|
38
38
|
return v >= 0 ? Math.floor(v + 0.5) : Math.ceil(v - 0.5);
|
|
39
39
|
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Degrees to radians in C's operation order, `(deg)/180.0 * M_PI`.
|
|
43
|
+
* `deg * Math.PI / 180` rounds differently for some angles (e.g. 33°).
|
|
44
|
+
* @see lib/common/arith.h:49 (RADIANS)
|
|
45
|
+
*/
|
|
46
|
+
export function RADIANS(deg: number): number {
|
|
47
|
+
return (deg / 180.0) * Math.PI;
|
|
48
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `hypot(a, b)` as the native build host's libm computes it, bit for bit.
|
|
5
|
+
*
|
|
6
|
+
* Graphviz calls libm `hypot`; V8's `Math.hypot` and `sqrt(a*a + b*b)` each
|
|
7
|
+
* round differently in the last bit for some inputs, and fdp's force loop
|
|
8
|
+
* amplifies one-ulp differences into visibly different layouts. The reference
|
|
9
|
+
* build's libm (macOS arm64 libSystem) matches this model exactly on 400k
|
|
10
|
+
* sampled inputs, including subnormal, near-overflow, zero, inf and NaN
|
|
11
|
+
* operands: order the magnitudes, rescale by 2^∓600 outside [2^-500, 2^500],
|
|
12
|
+
* then `sqrt(fma(small, small, big*big))`. It is within one ulp of the
|
|
13
|
+
* correctly rounded result (glibc's hypot can differ in that last bit).
|
|
14
|
+
*
|
|
15
|
+
* @see docs/known-divergences.md (host libm)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { fma } from './fma.js';
|
|
19
|
+
|
|
20
|
+
/** Rescale thresholds and factor of the reference libm. */
|
|
21
|
+
const BIG = 2 ** 500;
|
|
22
|
+
const SMALL = 2 ** -500;
|
|
23
|
+
const SCALE_UP = 2 ** 600;
|
|
24
|
+
const SCALE_DOWN = 2 ** -600;
|
|
25
|
+
|
|
26
|
+
/** libm `hypot(a, b)` of the native build host. */
|
|
27
|
+
export function libmHypot(a: number, b: number): number {
|
|
28
|
+
if (a === Infinity || a === -Infinity || b === Infinity || b === -Infinity) return Infinity;
|
|
29
|
+
let x = Math.abs(a);
|
|
30
|
+
let y = Math.abs(b);
|
|
31
|
+
if (x < y) [x, y] = [y, x];
|
|
32
|
+
if (x > BIG) return core(x * SCALE_DOWN, y * SCALE_DOWN) * SCALE_UP;
|
|
33
|
+
if (x < SMALL) return core(x * SCALE_UP, y * SCALE_UP) * SCALE_DOWN;
|
|
34
|
+
return core(x, y);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** sqrt(y·y + x·x) with the y·y product fused (x ≥ y ≥ 0). */
|
|
38
|
+
function core(x: number, y: number): number {
|
|
39
|
+
return Math.sqrt(fma(y, y, x * x));
|
|
40
|
+
}
|
|
@@ -22,6 +22,8 @@ import type { Point } from '../model/geom.js';
|
|
|
22
22
|
import { STAR, CYLINDER } from './shapeData.js';
|
|
23
23
|
import { starVertices } from './poly-vertices.js';
|
|
24
24
|
import { fma } from './fma.js';
|
|
25
|
+
import { RADIANS } from './arith.js';
|
|
26
|
+
import { libmHypot } from './libm-hypot.js';
|
|
25
27
|
|
|
26
28
|
/** Whitespace in points around labels / between peripheries. @see lib/common/const.h:GAP */
|
|
27
29
|
export const GAP = 4;
|
|
@@ -276,16 +278,15 @@ export interface PolyGeom {
|
|
|
276
278
|
|
|
277
279
|
/** Distort, skew, orient, and scale one unit vertex. @see shapes.c:poly_init */
|
|
278
280
|
function transformUnitVertex(R: Point, g: PolyGeom, c: { skewdist: number; gdistortion: number; gskew: number }, bb: Point): Point {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
//
|
|
284
|
-
//
|
|
285
|
-
//
|
|
286
|
-
//
|
|
287
|
-
|
|
288
|
-
const r = Math.sqrt(D.x * D.x + D.y * D.y);
|
|
281
|
+
// clang -ffp-contract=on fuses both multiply-adds of this one statement
|
|
282
|
+
// (the first product of the outer sum, as elsewhere in the port).
|
|
283
|
+
const D = { x: fma(R.x, fma(R.y, c.gdistortion, c.skewdist), R.y * c.gskew), y: R.y };
|
|
284
|
+
const alpha = RADIANS(g.orientation) + Math.atan2(D.y, D.x);
|
|
285
|
+
// C uses libm hypot(D.x, D.y); libmHypot reproduces the host libm bit for bit.
|
|
286
|
+
// Neither V8's Math.hypot (hypot(0.35355339059327379, same) = 0.5+1ULP, which once
|
|
287
|
+
// inflated ND_ht by ~7e-15 and swapped two packed circo/osage components) nor
|
|
288
|
+
// sqrt(x²+y²) (off on e.g. the orientation=20 box) does. @see shapes.c:poly_init
|
|
289
|
+
const r = libmHypot(D.x, D.y);
|
|
289
290
|
return { x: r * Math.cos(alpha) * bb.x, y: r * Math.sin(alpha) * bb.y };
|
|
290
291
|
}
|
|
291
292
|
|
|
@@ -302,7 +303,7 @@ export function polygonVertices(
|
|
|
302
303
|
const sectorangle = (2 * Math.PI) / g.sides;
|
|
303
304
|
const sidelength = Math.sin(sectorangle / 2);
|
|
304
305
|
const c = {
|
|
305
|
-
skewdist:
|
|
306
|
+
skewdist: libmHypot(Math.abs(g.distortion) + Math.abs(g.skew), 1),
|
|
306
307
|
gdistortion: (g.distortion * SQRT2) / Math.cos(sectorangle / 2),
|
|
307
308
|
gskew: g.skew / 2,
|
|
308
309
|
};
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* 1. explicit override via setTextMeasurer (tests, host-faithful Node opt-in)
|
|
7
7
|
* 2. GV_TEXT_MEASURER=estimate|lut — test/CI hook to force a specific measurer
|
|
8
8
|
* 3. browser (document present) — CanvasTextMeasurer over the page canvas
|
|
9
|
-
* (host-faithful: the same font the browser renders the SVG with)
|
|
9
|
+
* (host-faithful: the same font the browser renders the SVG with);
|
|
10
|
+
* Worker (no document) — the same over an OffscreenCanvas
|
|
10
11
|
* 4. Node default — EstimateTextMeasurer (deterministic; matches
|
|
11
12
|
* graphviz's own headless estimate_textspan_size)
|
|
12
13
|
*
|
|
@@ -77,6 +78,21 @@ function adviseHostFaithful(): void {
|
|
|
77
78
|
);
|
|
78
79
|
}
|
|
79
80
|
|
|
81
|
+
/**
|
|
82
|
+
* A CanvasTextMeasurer over an OffscreenCanvas 2d context, for Workers (no
|
|
83
|
+
* `document`); undefined where OffscreenCanvas is missing or unusable (Node).
|
|
84
|
+
* @see plans/async-api/DESIGN.md D5
|
|
85
|
+
*/
|
|
86
|
+
function offscreenMeasurer(): CanvasTextMeasurer | undefined {
|
|
87
|
+
if (typeof OffscreenCanvas === 'undefined') return undefined;
|
|
88
|
+
try {
|
|
89
|
+
const ctx2d = new OffscreenCanvas(0, 0).getContext('2d');
|
|
90
|
+
return ctx2d ? new CanvasTextMeasurer(ctx2d) : undefined;
|
|
91
|
+
} catch {
|
|
92
|
+
return undefined; // OffscreenCanvas present but no 2d context
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
80
96
|
/** Resolve the text measurer for this render (see resolution order above). */
|
|
81
97
|
export function createMeasurer(): TextMeasurer {
|
|
82
98
|
if (override) return override;
|
|
@@ -88,6 +104,9 @@ export function createMeasurer(): TextMeasurer {
|
|
|
88
104
|
const ctx2d = document.createElement('canvas').getContext('2d');
|
|
89
105
|
if (ctx2d) return new CanvasTextMeasurer(ctx2d);
|
|
90
106
|
} catch { /* canvas unavailable */ }
|
|
107
|
+
} else {
|
|
108
|
+
const offscreen = offscreenMeasurer(); // Worker: no document, same fonts API
|
|
109
|
+
if (offscreen) return offscreen;
|
|
91
110
|
}
|
|
92
111
|
adviseHostFaithful(); // Node, no host canvas → built-in deterministic metrics
|
|
93
112
|
// Node default = EstimateTextMeasurer: the deterministic reference that matches
|
|
@@ -285,7 +285,7 @@ export class CanvasTextMeasurer implements TextMeasurer {
|
|
|
285
285
|
*/
|
|
286
286
|
private readonly fonts = new Map<string, string>();
|
|
287
287
|
|
|
288
|
-
constructor(private readonly ctx: CanvasRenderingContext2D) {}
|
|
288
|
+
constructor(private readonly ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D) {}
|
|
289
289
|
|
|
290
290
|
measure(
|
|
291
291
|
text: string,
|
|
@@ -16,6 +16,7 @@ import { VIRTUAL } from './fastgr.js';
|
|
|
16
16
|
import { IGNORED } from './rank.js';
|
|
17
17
|
import { lateDouble } from '../../common/nodeinit.js';
|
|
18
18
|
import { nodesInSeq } from './decomp.js';
|
|
19
|
+
import { RADIANS } from '../../common/arith.js';
|
|
19
20
|
|
|
20
21
|
// ---------------------------------------------------------------------------
|
|
21
22
|
// TextLabel interface (minimal — full type deferred to Batch 5b)
|
|
@@ -286,7 +287,7 @@ function writePortlabelPos(
|
|
|
286
287
|
distanceStr: string,
|
|
287
288
|
): void {
|
|
288
289
|
const angleDeg = lateDouble(angleStr, PORT_LABEL_ANGLE, -180.0);
|
|
289
|
-
const angle = Math.atan2(pf.y - pe.y, pf.x - pe.x) + (
|
|
290
|
+
const angle = Math.atan2(pf.y - pe.y, pf.x - pe.x) + RADIANS(angleDeg);
|
|
290
291
|
const distMult = lateDouble(distanceStr || undefined, 1.0, 0.0);
|
|
291
292
|
const dist = PORT_LABEL_DISTANCE * distMult;
|
|
292
293
|
l.pos.x = pe.x + dist * Math.cos(angle);
|
package/src/layout/fdp/derive.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { Node } from '../../model/node.js';
|
|
|
21
21
|
import { Edge } from '../../model/edge.js';
|
|
22
22
|
import type { Box } from '../../model/geom.js';
|
|
23
23
|
import { doGraphLabel } from '../dot/graph-label.js';
|
|
24
|
+
import { nodesInSeq } from '../dot/decomp.js';
|
|
24
25
|
import { layoutMeasurer } from '../../common/nodeinit.js';
|
|
25
26
|
import { inputscaleDivisor } from '../../common/utils-inputscale.js';
|
|
26
27
|
import {
|
|
@@ -145,7 +146,9 @@ function deriveClusterNodes(g: Graph, dg: Graph, infop: LayoutInfo): void {
|
|
|
145
146
|
* @see layout.c:deriveGraph (remaining-nodes loop)
|
|
146
147
|
*/
|
|
147
148
|
function deriveRealNodes(g: Graph, dg: Graph): boolean {
|
|
148
|
-
|
|
149
|
+
// agfstnode/agnxtnode: a cluster subgraph iterates in node sequence order,
|
|
150
|
+
// not statement order; it fixes derived-node creation (and so id) order.
|
|
151
|
+
for (const n of nodesInSeq(g)) {
|
|
149
152
|
if (getDnode(n) !== null) continue;
|
|
150
153
|
const par = getParent(n);
|
|
151
154
|
if (par !== null && par !== gdata(g).parent) {
|
|
@@ -195,7 +198,7 @@ function deriveEdge(dg: Graph, byPair: Map<string, Edge>, tl: Node, hd: Node): E
|
|
|
195
198
|
|
|
196
199
|
/** Derived edges. @see layout.c:deriveGraph (edge loop) */
|
|
197
200
|
function deriveEdges(g: Graph, dg: Graph, byPair: Map<string, Edge>): void {
|
|
198
|
-
for (const n of g
|
|
201
|
+
for (const n of nodesInSeq(g)) { // agfstnode order, as in deriveRealNodes
|
|
199
202
|
const tl = getDnode(n)!;
|
|
200
203
|
for (const e of n.outEdges(g)) {
|
|
201
204
|
const hd = getDnode(e.head)!;
|
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
* fdp engine data model — gdata, dndata, bport_t, xparams, and the
|
|
5
5
|
* accessor macros of the C private interface.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* Matches Graphviz after 15.0.0 (fdpParms.Mlimit lives in
|
|
8
|
+
* tlayout-parms.ts).
|
|
9
9
|
*
|
|
10
|
-
* @see lib/fdpgen/fdp.h
|
|
11
|
-
* @see lib/fdpgen/xlayout.h
|
|
10
|
+
* @see lib/fdpgen/fdp.h
|
|
11
|
+
* @see lib/fdpgen/xlayout.h
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import type { Graph } from '../../model/graph.js';
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import type { Graph } from '../../model/graph.js';
|
|
15
15
|
import type { Node } from '../../model/node.js';
|
|
16
16
|
import { aggetGraph } from './fdp-model.js';
|
|
17
|
+
import { RADIANS } from '../../common/arith.js';
|
|
17
18
|
|
|
18
19
|
/** mapbool for the "normalize" attr. @see lib/common/utils.c:mapbool */
|
|
19
20
|
function mapbool(s: string): boolean {
|
|
@@ -34,7 +35,7 @@ function angleSet(g: Graph): number | null {
|
|
|
34
35
|
if (a === undefined || a === '') return null;
|
|
35
36
|
const ang = parseAngle(a);
|
|
36
37
|
if (ang === null) return null;
|
|
37
|
-
return (wrapDegrees(ang)
|
|
38
|
+
return RADIANS(wrapDegrees(ang));
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
/** strtod (leading whitespace, sign) with the mapbool fallback of angleSet. */
|
|
@@ -18,6 +18,7 @@ import type { Node } from '../../model/node.js';
|
|
|
18
18
|
import type { Edge } from '../../model/edge.js';
|
|
19
19
|
import type { Point } from '../../model/geom.js';
|
|
20
20
|
import { srand48, drand48 } from '../../common/random.js';
|
|
21
|
+
import { fma } from '../../common/fma.js';
|
|
21
22
|
import {
|
|
22
23
|
type Bport,
|
|
23
24
|
dndata,
|
|
@@ -53,8 +54,9 @@ function placeByNeighbors(g: Graph, np: Node, ctr: Point): void {
|
|
|
53
54
|
np.info.pos![0] = p.x;
|
|
54
55
|
np.info.pos![1] = p.y;
|
|
55
56
|
} else if (cnt === 1) {
|
|
56
|
-
|
|
57
|
-
np.info.pos![
|
|
57
|
+
// contracted (-ffp-contract=on): fmadd of the first product
|
|
58
|
+
np.info.pos![0] = fma(0.98, p.x, 0.1 * ctr.x);
|
|
59
|
+
np.info.pos![1] = fma(0.9, p.y, 0.1 * ctr.y);
|
|
58
60
|
} else {
|
|
59
61
|
const angle = Math.PI * 2.0 * drand48();
|
|
60
62
|
const radius = 0.9 * drand48();
|
|
@@ -76,8 +78,8 @@ function averagePlacedNeighbors(g: Graph, np: Node, p: Point): number {
|
|
|
76
78
|
const op = ep.head === np ? ep.tail : ep.head;
|
|
77
79
|
if (!hasPos(op)) continue;
|
|
78
80
|
if (cnt) {
|
|
79
|
-
p.x = (p.x
|
|
80
|
-
p.y = (p.y
|
|
81
|
+
p.x = fma(p.x, cnt, op.info.pos![0]!) / (cnt + 1); // contracted
|
|
82
|
+
p.y = fma(p.y, cnt, op.info.pos![1]!) / (cnt + 1);
|
|
81
83
|
} else {
|
|
82
84
|
p.x = op.info.pos![0]!;
|
|
83
85
|
p.y = op.info.pos![1]!;
|
|
@@ -162,8 +164,8 @@ function sizeBoundary(width: number, height: number): void {
|
|
|
162
164
|
function placeWithPorts(g: Graph, pp: Bport[], ctr: Point): void {
|
|
163
165
|
for (const port of pp) { /* position ports on ellipse */
|
|
164
166
|
const np = port.n;
|
|
165
|
-
np.info.pos![0] = parms.Wd
|
|
166
|
-
np.info.pos![1] = parms.Ht
|
|
167
|
+
np.info.pos![0] = fma(parms.Wd, Math.cos(port.alpha), ctr.x); // contracted
|
|
168
|
+
np.info.pos![1] = fma(parms.Ht, Math.sin(port.alpha), ctr.y);
|
|
167
169
|
dndata(np).pinned = P_SET;
|
|
168
170
|
}
|
|
169
171
|
for (const np of g.nodes.values()) {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* @see lib/fdpgen/tlayout.c (15.0.0) — parms_t, fdp_initParams,
|
|
11
11
|
* init_params, cool, reset_params
|
|
12
|
-
* @see lib/common/globals.c
|
|
12
|
+
* @see lib/common/globals.c — fdpParms defaults
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import type { Graph } from '../../model/graph.js';
|
|
@@ -41,7 +41,7 @@ export const INIT_RANDOM = 2;
|
|
|
41
41
|
const DFLT_SMODE = INIT_RANDOM;
|
|
42
42
|
|
|
43
43
|
// ---------------------------------------------------------------------------
|
|
44
|
-
// fdp_parms defaults — @see lib/common/globals.c
|
|
44
|
+
// fdp_parms defaults — @see lib/common/globals.c
|
|
45
45
|
// ---------------------------------------------------------------------------
|
|
46
46
|
|
|
47
47
|
/**
|
|
@@ -61,6 +61,9 @@ export const fdpParms = {
|
|
|
61
61
|
Tfact: 1.0,
|
|
62
62
|
K: -1.0,
|
|
63
63
|
T0: -1.0,
|
|
64
|
+
/** Distance beyond which repulsion is 0; HUGE_VAL unless `dot -Lm` (no library
|
|
65
|
+
* equivalent). @see lib/fdpgen/fdp.h:fdpParms_s.Mlimit */
|
|
66
|
+
Mlimit: Infinity,
|
|
64
67
|
};
|
|
65
68
|
|
|
66
69
|
// ---------------------------------------------------------------------------
|
|
@@ -4,9 +4,8 @@
|
|
|
4
4
|
* fdp initial layout — grid-accelerated Fruchterman–Reingold on point
|
|
5
5
|
* nodes with boundary ports.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
* (
|
|
9
|
-
* 15.0.0 output, so neither is ported.
|
|
7
|
+
* Repulsion follows Graphviz after 15.0.0: doRep takes the hypot distance
|
|
8
|
+
* (the host libm's, see libm-hypot.ts) and honours the Mlimit cutoff.
|
|
10
9
|
*
|
|
11
10
|
* All math is double precision (unlike neato's float32 stress kernel).
|
|
12
11
|
* Force-accumulation order is load-bearing: node iteration is the
|
|
@@ -16,13 +15,14 @@
|
|
|
16
15
|
* Parameter state lives in tlayout-parms.ts; initial placement in
|
|
17
16
|
* tlayout-init.ts.
|
|
18
17
|
*
|
|
19
|
-
* @see lib/fdpgen/tlayout.c
|
|
18
|
+
* @see lib/fdpgen/tlayout.c
|
|
20
19
|
*/
|
|
21
20
|
|
|
22
21
|
import type { Graph } from '../../model/graph.js';
|
|
23
22
|
import type { Node } from '../../model/node.js';
|
|
24
23
|
import type { Edge } from '../../model/edge.js';
|
|
25
24
|
import { fma, fms } from '../../common/fma.js';
|
|
25
|
+
import { libmHypot } from '../../common/libm-hypot.js';
|
|
26
26
|
import { Grid, type Cell } from './grid.js';
|
|
27
27
|
import {
|
|
28
28
|
type Bport,
|
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
gdata,
|
|
34
34
|
P_FIX,
|
|
35
35
|
} from './fdp-model.js';
|
|
36
|
-
import { parms, cool, initParams, resetParams } from './tlayout-parms.js';
|
|
36
|
+
import { parms, cool, initParams, resetParams, fdpParms } from './tlayout-parms.js';
|
|
37
37
|
import { initPositions } from './tlayout-init.js';
|
|
38
38
|
import { crand } from '../../common/crand.js';
|
|
39
39
|
|
|
@@ -66,25 +66,26 @@ export function coincidentDelta(): { xdelta: number; ydelta: number } {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
|
-
* Repulsive force = K²/d (useNew) or K²/d
|
|
69
|
+
* Repulsive force = K²/d² (useNew) or K²/d, zero beyond Mlimit.
|
|
70
70
|
* @see lib/fdpgen/tlayout.c:doRep
|
|
71
71
|
*/
|
|
72
72
|
function doRep(
|
|
73
|
-
p: Node, q: Node, xdelta: number, ydelta: number,
|
|
73
|
+
p: Node, q: Node, xdelta: number, ydelta: number, dist: number,
|
|
74
74
|
): void {
|
|
75
75
|
let force: number;
|
|
76
76
|
|
|
77
|
-
if (
|
|
78
|
-
const d = coincidentDelta();
|
|
77
|
+
if (!(dist > 0)) {
|
|
78
|
+
const d = coincidentDelta(); // C re-rolls rand() until hypot > 0
|
|
79
79
|
xdelta = d.xdelta;
|
|
80
80
|
ydelta = d.ydelta;
|
|
81
|
-
|
|
81
|
+
dist = libmHypot(xdelta, ydelta);
|
|
82
82
|
}
|
|
83
|
-
if (
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
if (dist > fdpParms.Mlimit) {
|
|
84
|
+
force = 0;
|
|
85
|
+
} else if (parms.useNew) {
|
|
86
|
+
force = parms.K * parms.K / (dist * dist * dist);
|
|
86
87
|
} else {
|
|
87
|
-
force = parms.K * parms.K /
|
|
88
|
+
force = parms.K * parms.K / (dist * dist);
|
|
88
89
|
}
|
|
89
90
|
if (isPort(p) && isPort(q)) force *= 10.0;
|
|
90
91
|
const dq = disp(q);
|
|
@@ -100,7 +101,7 @@ function doRep(
|
|
|
100
101
|
function applyRep(p: Node, q: Node): void {
|
|
101
102
|
const xdelta = q.info.pos![0]! - p.info.pos![0]!;
|
|
102
103
|
const ydelta = q.info.pos![1]! - p.info.pos![1]!;
|
|
103
|
-
doRep(p, q, xdelta, ydelta,
|
|
104
|
+
doRep(p, q, xdelta, ydelta, libmHypot(xdelta, ydelta));
|
|
104
105
|
}
|
|
105
106
|
|
|
106
107
|
/** @see lib/fdpgen/tlayout.c:doNeighbor */
|
|
@@ -111,8 +112,8 @@ function doNeighbor(grid: Grid, i: number, j: number, nodes: Node[]): void {
|
|
|
111
112
|
for (const q of cellp.nodes) {
|
|
112
113
|
const xdelta = q.info.pos![0]! - p.info.pos![0]!;
|
|
113
114
|
const ydelta = q.info.pos![1]! - p.info.pos![1]!;
|
|
114
|
-
const
|
|
115
|
-
if (
|
|
115
|
+
const dist = libmHypot(xdelta, ydelta);
|
|
116
|
+
if (dist < parms.Cell) doRep(p, q, xdelta, ydelta, dist);
|
|
116
117
|
}
|
|
117
118
|
}
|
|
118
119
|
}
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* fdp expansion layout: grow the point-node layout so sized nodes do
|
|
5
5
|
* not overlap, by force iteration with overlap-aware repulsion.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* Follows Graphviz after 15.0.0: repulsion uses the hypot distance (the
|
|
8
|
+
* host libm's, see libm-hypot.ts) and the Mlimit cutoff.
|
|
9
9
|
*
|
|
10
10
|
* The default overlap attribute is "9:prism": up to 9 x_layout tries,
|
|
11
11
|
* then removeOverlapAs with the remaining mode. x_layout does NOT always
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* fdpAdjust, the scale family via scAdjust. On the GTS reference build
|
|
15
15
|
* overlap=false resolves to AM_PRISM (value 1000), not a no-op.
|
|
16
16
|
*
|
|
17
|
-
* @see lib/fdpgen/xlayout.c
|
|
17
|
+
* @see lib/fdpgen/xlayout.c
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import { RenderError } from '../../errors.js';
|
|
@@ -31,6 +31,9 @@ import {
|
|
|
31
31
|
P_PIN,
|
|
32
32
|
} from './fdp-model.js';
|
|
33
33
|
import { coincidentDelta } from './tlayout.js';
|
|
34
|
+
import { fdpParms } from './tlayout-parms.js';
|
|
35
|
+
import { libmHypot } from '../../common/libm-hypot.js';
|
|
36
|
+
import { fma, fms } from '../../common/fma.js';
|
|
34
37
|
import { normalizeG } from './normalize.js';
|
|
35
38
|
import { simpleScale } from '../neato/simple-scale.js';
|
|
36
39
|
|
|
@@ -76,7 +79,7 @@ function ht2(n: Node): number {
|
|
|
76
79
|
|
|
77
80
|
/** Expanded radius of n. @see lib/fdpgen/xlayout.c:RAD */
|
|
78
81
|
function rad(n: Node): number {
|
|
79
|
-
return
|
|
82
|
+
return libmHypot(wd2(n), ht2(n));
|
|
80
83
|
}
|
|
81
84
|
|
|
82
85
|
/**
|
|
@@ -129,31 +132,32 @@ function cntOverlaps(g: Graph): number {
|
|
|
129
132
|
// ---------------------------------------------------------------------------
|
|
130
133
|
|
|
131
134
|
/**
|
|
132
|
-
* Overlap-aware repulsion; returns 1 if the pair overlaps.
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
* coincident nodes (coincidentDelta), shared with tlayout.
|
|
135
|
+
* Overlap-aware repulsion; returns 1 if the pair overlaps. Zero beyond
|
|
136
|
+
* Mlimit. C's applyRep (delta + hypot) is inlined; its doRep re-rolls rand()
|
|
137
|
+
* deltas while the distance is not positive (coincidentDelta).
|
|
136
138
|
* @see lib/fdpgen/xlayout.c:doRep
|
|
137
139
|
* @see lib/fdpgen/xlayout.c:applyRep
|
|
138
140
|
*/
|
|
139
141
|
function applyRep(p: Node, q: Node, rs: RepStrength): number {
|
|
140
142
|
let xdelta = q.info.pos![0]! - p.info.pos![0]!;
|
|
141
143
|
let ydelta = q.info.pos![1]! - p.info.pos![1]!;
|
|
142
|
-
let
|
|
143
|
-
if (
|
|
144
|
+
let dist = libmHypot(xdelta, ydelta);
|
|
145
|
+
if (!(dist > 0)) {
|
|
144
146
|
const d = coincidentDelta();
|
|
145
147
|
xdelta = d.xdelta;
|
|
146
148
|
ydelta = d.ydelta;
|
|
147
|
-
|
|
149
|
+
dist = libmHypot(xdelta, ydelta);
|
|
148
150
|
}
|
|
149
151
|
const ov = overlap(p, q) ? 1 : 0;
|
|
150
|
-
|
|
152
|
+
let force = ov ? rs.ov / (dist * dist) : rs.nonov / (dist * dist);
|
|
153
|
+
if (dist > fdpParms.Mlimit) force = 0;
|
|
151
154
|
const dq = disp(q);
|
|
152
155
|
const dp = disp(p);
|
|
153
|
-
|
|
154
|
-
dq[
|
|
155
|
-
|
|
156
|
-
dp[
|
|
156
|
+
// arm64 contraction (-ffp-contract=on): DISP ± delta·force are fmadd/fmsub
|
|
157
|
+
dq[0] = fma(xdelta, force, dq[0]!);
|
|
158
|
+
dq[1] = fma(ydelta, force, dq[1]!);
|
|
159
|
+
dp[0] = fms(xdelta, force, dp[0]!);
|
|
160
|
+
dp[1] = fms(ydelta, force, dp[1]!);
|
|
157
161
|
return ov;
|
|
158
162
|
}
|
|
159
163
|
|
|
@@ -165,16 +169,16 @@ function applyAttr(p: Node, q: Node): void {
|
|
|
165
169
|
if (overlap(p, q)) return;
|
|
166
170
|
const xdelta = q.info.pos![0]! - p.info.pos![0]!;
|
|
167
171
|
const ydelta = q.info.pos![1]! - p.info.pos![1]!;
|
|
168
|
-
const dist =
|
|
172
|
+
const dist = libmHypot(xdelta, ydelta);
|
|
169
173
|
const din = rad(p) + rad(q);
|
|
170
174
|
const dout = dist - din;
|
|
171
175
|
const force = dout * dout / ((xParams.K + din) * dist);
|
|
172
176
|
const dq = disp(q);
|
|
173
177
|
const dp = disp(p);
|
|
174
|
-
dq[0]
|
|
175
|
-
dq[1]
|
|
176
|
-
dp[0]
|
|
177
|
-
dp[1]
|
|
178
|
+
dq[0] = fms(xdelta, force, dq[0]!);
|
|
179
|
+
dq[1] = fms(ydelta, force, dq[1]!);
|
|
180
|
+
dp[0] = fma(xdelta, force, dp[0]!);
|
|
181
|
+
dp[1] = fma(ydelta, force, dp[1]!);
|
|
178
182
|
}
|
|
179
183
|
|
|
180
184
|
// ---------------------------------------------------------------------------
|
|
@@ -204,7 +208,7 @@ function moveNodes(g: Graph, temp: number): void {
|
|
|
204
208
|
if (dndata(n).pinned === P_PIN) continue;
|
|
205
209
|
const dx = disp(n)[0];
|
|
206
210
|
const dy = disp(n)[1];
|
|
207
|
-
const len2 = dx
|
|
211
|
+
const len2 = fma(dx, dx, dy * dy); // contracted, as in tlayout
|
|
208
212
|
if (len2 < temp2) {
|
|
209
213
|
n.info.pos![0]! += dx;
|
|
210
214
|
n.info.pos![1]! += dy;
|