@knowvah/dot-engine 2.1.0 → 2.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knowvah/dot-engine",
3
- "version": "2.1.0",
3
+ "version": "2.3.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",
@@ -76,10 +76,12 @@
76
76
  "docs:brand": "node docs-site/copy-brand-assets.mjs",
77
77
  "docs:copy-reports": "node docs-site/copy-reports.mjs",
78
78
  "docs:copy-goldens": "node docs-site/copy-goldens.mjs",
79
+ "docs:i18n-status": "node scripts/docs-i18n-status.mjs",
80
+ "docs:i18n-hashes": "node scripts/docs-i18n-status.mjs --write-hashes",
79
81
  "docs:api": "typedoc",
80
- "docs:dev": "npm run docs:brand && npm run docs:copy-reports && npm run docs:copy-goldens && npm run docs:api && vitepress dev docs-site",
82
+ "docs:dev": "npm run docs:brand && npm run docs:copy-reports && npm run docs:copy-goldens && npm run docs:i18n-hashes && npm run docs:api && vitepress dev docs-site",
81
83
  "predocs:build": "npm run parser:regen",
82
- "docs:build": "npm run docs:brand && npm run docs:copy-reports && npm run docs:copy-goldens && npm run docs:api && vitepress build docs-site",
84
+ "docs:build": "npm run docs:brand && npm run docs:copy-reports && npm run docs:copy-goldens && npm run docs:i18n-hashes && npm run docs:api && vitepress build docs-site",
83
85
  "docs:preview": "vitepress preview docs-site",
84
86
  "release": "semantic-release",
85
87
  "publish:check": "npm run typecheck && npm run build && npm pack --dry-run",
@@ -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
+ }
@@ -23,6 +23,7 @@ import { STAR, CYLINDER } from './shapeData.js';
23
23
  import { starVertices } from './poly-vertices.js';
24
24
  import { fma } from './fma.js';
25
25
  import { RADIANS } from './arith.js';
26
+ import { libmHypot } from './libm-hypot.js';
26
27
 
27
28
  /** Whitespace in points around labels / between peripheries. @see lib/common/const.h:GAP */
28
29
  export const GAP = 4;
@@ -277,16 +278,15 @@ export interface PolyGeom {
277
278
 
278
279
  /** Distort, skew, orient, and scale one unit vertex. @see shapes.c:poly_init */
279
280
  function transformUnitVertex(R: Point, g: PolyGeom, c: { skewdist: number; gdistortion: number; gskew: number }, bb: Point): Point {
280
- const D = { x: R.x * (c.skewdist + R.y * c.gdistortion) + R.y * c.gskew, y: R.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 };
281
284
  const alpha = RADIANS(g.orientation) + Math.atan2(D.y, D.x);
282
- // C uses libm hypot(D.x, D.y). V8's Math.hypot is a scaled algorithm that
283
- // returns a result 1 ULP off from libm hypot for in-range polygon vertices
284
- // (e.g. hypot(0.35355…, 0.35355…): libm = 0.5 exactly, Math.hypot = 0.5+1ULP),
285
- // whereas the naive sqrt(x²+y²) reproduces libm's value bit-for-bit here.
286
- // That ULP inflated ND_ht by ~7e-15, and pack.c genPoly's GRID(=ceil) rounded
287
- // an exactly-on-boundary component height up a cell, reordering polyomino
288
- // packing and swapping two circo/osage components. @see shapes.c:poly_init
289
- const r = Math.sqrt(D.x * D.x + D.y * D.y);
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);
290
290
  return { x: r * Math.cos(alpha) * bb.x, y: r * Math.sin(alpha) * bb.y };
291
291
  }
292
292
 
@@ -303,7 +303,7 @@ export function polygonVertices(
303
303
  const sectorangle = (2 * Math.PI) / g.sides;
304
304
  const sidelength = Math.sin(sectorangle / 2);
305
305
  const c = {
306
- skewdist: Math.hypot(Math.abs(g.distortion) + Math.abs(g.skew), 1),
306
+ skewdist: libmHypot(Math.abs(g.distortion) + Math.abs(g.skew), 1),
307
307
  gdistortion: (g.distortion * SQRT2) / Math.cos(sectorangle / 2),
308
308
  gskew: g.skew / 2,
309
309
  };
@@ -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
- for (const n of g.nodes.values()) {
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.nodes.values()) {
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
- * Spec read at the 15.0.0 tag (post-tag fdpgen commits add an Mlimit
8
- * branch and float reorderings the golden refs do not have).
7
+ * Matches Graphviz after 15.0.0 (fdpParms.Mlimit lives in
8
+ * tlayout-parms.ts).
9
9
  *
10
- * @see lib/fdpgen/fdp.h (15.0.0)
11
- * @see lib/fdpgen/xlayout.h (15.0.0)
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';
@@ -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
- np.info.pos![0] = 0.98 * p.x + 0.1 * ctr.x;
57
- np.info.pos![1] = 0.9 * p.y + 0.1 * ctr.y;
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 * cnt + op.info.pos![0]!) / (cnt + 1);
80
- p.y = (p.y * cnt + op.info.pos![1]!) / (cnt + 1);
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 * Math.cos(port.alpha) + ctr.x;
166
- np.info.pos![1] = parms.Ht * Math.sin(port.alpha) + ctr.y;
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 (15.0.0) — fdpParms defaults
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 (15.0.0)
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
- * Spec read at the 15.0.0 tag: the post-tag tree reorders doRep floats
8
- * (hypot vs sqrt(x²·y²)) and adds an Mlimit cutoff; the golden refs are
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 (15.0.0)
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, dist2: number,
73
+ p: Node, q: Node, xdelta: number, ydelta: number, dist: number,
74
74
  ): void {
75
75
  let force: number;
76
76
 
77
- if (dist2 === 0.0) {
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
- dist2 = fma(xdelta, xdelta, ydelta * ydelta);
81
+ dist = libmHypot(xdelta, ydelta);
82
82
  }
83
- if (parms.useNew) {
84
- const dist = Math.sqrt(dist2);
85
- force = parms.K * parms.K / (dist * dist2);
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 / dist2;
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, fma(xdelta, xdelta, ydelta * 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 dist2 = fma(xdelta, xdelta, ydelta * ydelta);
115
- if (dist2 < parms.Cell * parms.Cell) doRep(p, q, xdelta, ydelta, dist2);
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
- * Spec read at the 15.0.0 tag (the post-tag Mlimit force cutoff is
8
- * deliberately NOT ported — the golden refs predate it).
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 (15.0.0)
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 Math.hypot(wd2(n), ht2(n));
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
- * The delta/dist2 computation of C's applyRep is inlined here; the
134
- * C doRep/applyRep split exists for the rand() re-roll loop on
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 dist2 = xdelta * xdelta + ydelta * ydelta;
143
- if (dist2 === 0.0) {
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
- dist2 = xdelta * xdelta + ydelta * ydelta;
149
+ dist = libmHypot(xdelta, ydelta);
148
150
  }
149
151
  const ov = overlap(p, q) ? 1 : 0;
150
- const force = ov ? rs.ov / dist2 : rs.nonov / dist2;
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
- dq[0] += xdelta * force;
154
- dq[1] += ydelta * force;
155
- dp[0] -= xdelta * force;
156
- dp[1] -= ydelta * force;
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 = Math.hypot(xdelta, ydelta);
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] -= xdelta * force;
175
- dq[1] -= ydelta * force;
176
- dp[0] += xdelta * force;
177
- dp[1] += ydelta * force;
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 * dx + dy * dy;
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;