@kgdiff/layout 0.0.0-stage → 0.1.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.
Files changed (113) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +46 -2
  3. package/dist/components.d.ts +11 -0
  4. package/dist/components.d.ts.map +1 -0
  5. package/dist/components.js +56 -0
  6. package/dist/components.js.map +1 -0
  7. package/dist/engine.d.ts +20 -0
  8. package/dist/engine.d.ts.map +1 -0
  9. package/dist/engine.js +336 -0
  10. package/dist/engine.js.map +1 -0
  11. package/dist/extend.d.ts +7 -0
  12. package/dist/extend.d.ts.map +1 -0
  13. package/dist/extend.js +68 -0
  14. package/dist/extend.js.map +1 -0
  15. package/dist/forces/collide-rect.d.ts +50 -0
  16. package/dist/forces/collide-rect.d.ts.map +1 -0
  17. package/dist/forces/collide-rect.js +310 -0
  18. package/dist/forces/collide-rect.js.map +1 -0
  19. package/dist/forces/free-spot.d.ts +40 -0
  20. package/dist/forces/free-spot.d.ts.map +1 -0
  21. package/dist/forces/free-spot.js +189 -0
  22. package/dist/forces/free-spot.js.map +1 -0
  23. package/dist/forces/params.d.ts +38 -0
  24. package/dist/forces/params.d.ts.map +1 -0
  25. package/dist/forces/params.js +55 -0
  26. package/dist/forces/params.js.map +1 -0
  27. package/dist/forces/springs.d.ts +20 -0
  28. package/dist/forces/springs.d.ts.map +1 -0
  29. package/dist/forces/springs.js +34 -0
  30. package/dist/forces/springs.js.map +1 -0
  31. package/dist/geometry.d.ts +21 -0
  32. package/dist/geometry.d.ts.map +1 -0
  33. package/dist/geometry.js +39 -0
  34. package/dist/geometry.js.map +1 -0
  35. package/dist/graph.d.ts +38 -0
  36. package/dist/graph.d.ts.map +1 -0
  37. package/dist/graph.js +82 -0
  38. package/dist/graph.js.map +1 -0
  39. package/dist/index.d.ts +162 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +87 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/init.d.ts +28 -0
  44. package/dist/init.d.ts.map +1 -0
  45. package/dist/init.js +196 -0
  46. package/dist/init.js.map +1 -0
  47. package/dist/json.d.ts +4 -0
  48. package/dist/json.d.ts.map +1 -0
  49. package/dist/json.js +112 -0
  50. package/dist/json.js.map +1 -0
  51. package/dist/layout.d.ts +7 -0
  52. package/dist/layout.d.ts.map +1 -0
  53. package/dist/layout.js +70 -0
  54. package/dist/layout.js.map +1 -0
  55. package/dist/math.d.ts +29 -0
  56. package/dist/math.d.ts.map +1 -0
  57. package/dist/math.js +91 -0
  58. package/dist/math.js.map +1 -0
  59. package/dist/options.d.ts +43 -0
  60. package/dist/options.d.ts.map +1 -0
  61. package/dist/options.js +76 -0
  62. package/dist/options.js.map +1 -0
  63. package/dist/pack.d.ts +43 -0
  64. package/dist/pack.d.ts.map +1 -0
  65. package/dist/pack.js +168 -0
  66. package/dist/pack.js.map +1 -0
  67. package/dist/prng.d.ts +20 -0
  68. package/dist/prng.d.ts.map +1 -0
  69. package/dist/prng.js +53 -0
  70. package/dist/prng.js.map +1 -0
  71. package/dist/result.d.ts +20 -0
  72. package/dist/result.d.ts.map +1 -0
  73. package/dist/result.js +40 -0
  74. package/dist/result.js.map +1 -0
  75. package/dist/scope.d.ts +25 -0
  76. package/dist/scope.d.ts.map +1 -0
  77. package/dist/scope.js +71 -0
  78. package/dist/scope.js.map +1 -0
  79. package/dist/simulate.d.ts +64 -0
  80. package/dist/simulate.d.ts.map +1 -0
  81. package/dist/simulate.js +247 -0
  82. package/dist/simulate.js.map +1 -0
  83. package/dist/text-metrics.d.ts +32 -0
  84. package/dist/text-metrics.d.ts.map +1 -0
  85. package/dist/text-metrics.js +153 -0
  86. package/dist/text-metrics.js.map +1 -0
  87. package/dist/union.d.ts +32 -0
  88. package/dist/union.d.ts.map +1 -0
  89. package/dist/union.js +106 -0
  90. package/dist/union.js.map +1 -0
  91. package/package.json +52 -3
  92. package/src/components.ts +56 -0
  93. package/src/engine.ts +444 -0
  94. package/src/extend.ts +75 -0
  95. package/src/forces/collide-rect.ts +385 -0
  96. package/src/forces/free-spot.ts +243 -0
  97. package/src/forces/params.ts +88 -0
  98. package/src/forces/springs.ts +50 -0
  99. package/src/geometry.ts +66 -0
  100. package/src/graph.ts +120 -0
  101. package/src/index.ts +193 -0
  102. package/src/init.ts +246 -0
  103. package/src/json.ts +131 -0
  104. package/src/layout.ts +87 -0
  105. package/src/math.ts +91 -0
  106. package/src/options.ts +153 -0
  107. package/src/pack.ts +228 -0
  108. package/src/prng.ts +59 -0
  109. package/src/result.ts +63 -0
  110. package/src/scope.ts +86 -0
  111. package/src/simulate.ts +328 -0
  112. package/src/text-metrics.ts +157 -0
  113. package/src/union.ts +127 -0
package/dist/union.js ADDED
@@ -0,0 +1,106 @@
1
+ const byCodeUnit = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
2
+ function sortedById(items) {
3
+ for (let i = 1; i < items.length; i++) {
4
+ const prev = items[i - 1];
5
+ const cur = items[i];
6
+ if (prev !== undefined && cur !== undefined && byCodeUnit(prev.id, cur.id) > 0) {
7
+ return [...items].sort((a, b) => byCodeUnit(a.id, b.id));
8
+ }
9
+ }
10
+ return items;
11
+ }
12
+ /** Build CSR adjacency (ascending neighbour lists) from flat undirected pairs. */
13
+ export function buildAdjacency(n, pairs) {
14
+ const offsets = new Int32Array(n + 1);
15
+ for (let k = 0; k < pairs.length; k++)
16
+ offsets[(pairs[k] ?? 0) + 1]++;
17
+ for (let i = 0; i < n; i++)
18
+ offsets[i + 1] += offsets[i];
19
+ const fill = offsets.slice(0, n);
20
+ const adjacency = new Int32Array(pairs.length);
21
+ for (let k = 0; k < pairs.length; k += 2) {
22
+ const a = pairs[k];
23
+ const b = pairs[k + 1];
24
+ adjacency[fill[a]++] = b;
25
+ adjacency[fill[b]++] = a;
26
+ }
27
+ for (let i = 0; i < n; i++)
28
+ adjacency.subarray(offsets[i], offsets[i + 1]).sort();
29
+ return { offsets, adjacency };
30
+ }
31
+ export function buildUnionGraph(diff) {
32
+ const nodes = sortedById(diff.nodes);
33
+ const n = nodes.length;
34
+ const indexOf = new Map();
35
+ for (let i = 0; i < n; i++)
36
+ indexOf.set(nodes[i].id, i);
37
+ const changed = new Uint8Array(n);
38
+ for (let i = 0; i < n; i++)
39
+ if (nodes[i].status !== 'unchanged')
40
+ changed[i] = 1;
41
+ const seen = new Set();
42
+ const flat = [];
43
+ const addPair = (a, b) => {
44
+ if (a === undefined || b === undefined || a === b)
45
+ return;
46
+ const lo = a < b ? a : b;
47
+ const hi = a < b ? b : a;
48
+ const key = lo * n + hi;
49
+ if (seen.has(key))
50
+ return;
51
+ seen.add(key);
52
+ flat.push(lo, hi);
53
+ };
54
+ for (const edge of sortedById(diff.edges)) {
55
+ const s = indexOf.get(edge.source);
56
+ const t = indexOf.get(edge.target);
57
+ addPair(s, t);
58
+ const sb = edge.sourceBefore === undefined ? s : indexOf.get(edge.sourceBefore);
59
+ const tb = edge.targetBefore === undefined ? t : indexOf.get(edge.targetBefore);
60
+ if (sb !== s || tb !== t)
61
+ addPair(sb, tb);
62
+ if (edge.status !== 'unchanged') {
63
+ for (const end of [s, t, sb, tb])
64
+ if (end !== undefined)
65
+ changed[end] = 1;
66
+ }
67
+ }
68
+ const pairs = Int32Array.from(flat);
69
+ const { offsets, adjacency } = buildAdjacency(n, pairs);
70
+ const partners = [];
71
+ const partnerKeys = new Set();
72
+ const addPartner = (mover, anchor) => {
73
+ if (anchor === undefined || anchor === mover)
74
+ return;
75
+ const key = mover * n + anchor;
76
+ if (partnerKeys.has(key))
77
+ return;
78
+ partnerKeys.add(key);
79
+ partners.push({ mover, anchor });
80
+ };
81
+ for (let i = 0; i < n; i++) {
82
+ for (const facet of nodes[i].facets) {
83
+ if (facet.kind === 'mergedInto')
84
+ addPartner(i, indexOf.get(facet.target));
85
+ else if (facet.kind === 'splitFrom')
86
+ addPartner(i, indexOf.get(facet.source));
87
+ else if (facet.kind === 'mergeTarget') {
88
+ for (const source of facet.sources) {
89
+ const s = indexOf.get(source);
90
+ if (s !== undefined)
91
+ addPartner(s, i);
92
+ }
93
+ }
94
+ else if (facet.kind === 'splitInto') {
95
+ for (const target of facet.targets) {
96
+ const t = indexOf.get(target);
97
+ if (t !== undefined)
98
+ addPartner(t, i);
99
+ }
100
+ }
101
+ }
102
+ }
103
+ partners.sort((a, b) => a.mover - b.mover || a.anchor - b.anchor);
104
+ return { nodes, indexOf, pairs, offsets, adjacency, partners, changed };
105
+ }
106
+ //# sourceMappingURL=union.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"union.js","sourceRoot":"","sources":["../src/union.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,55 @@
1
1
  {
2
2
  "name": "@kgdiff/layout",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "Stable, deterministic, label-aware layout for graph diffs: both versions share one mental map.",
5
+ "keywords": [
6
+ "kgdiff",
7
+ "layout",
8
+ "force-directed"
9
+ ],
10
+ "homepage": "https://github.com/iiwish/kgdiff/tree/main/packages/layout#readme",
11
+ "bugs": {
12
+ "url": "https://github.com/iiwish/kgdiff/issues"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/iiwish/kgdiff.git",
17
+ "directory": "packages/layout"
18
+ },
19
+ "license": "MIT",
20
+ "author": "iiwish",
21
+ "type": "module",
22
+ "sideEffects": false,
23
+ "exports": {
24
+ ".": {
25
+ "types": "./dist/index.d.ts",
26
+ "default": "./dist/index.js"
27
+ }
28
+ },
29
+ "files": [
30
+ "dist",
31
+ "src",
32
+ "!src/**/*.test.ts"
33
+ ],
34
+ "engines": {
35
+ "node": ">=22"
36
+ },
37
+ "publishConfig": {
38
+ "access": "public",
39
+ "registry": "https://registry.npmjs.org/"
40
+ },
41
+ "dependencies": {
42
+ "@kgdiff/core": "0.1.0",
43
+ "d3-force": "3.0.0",
44
+ "d3-quadtree": "3.0.1"
45
+ },
46
+ "devDependencies": {
47
+ "@types/d3-force": "3.0.10",
48
+ "@types/d3-quadtree": "3.0.6"
49
+ },
50
+ "scripts": {
51
+ "build": "tsc -p tsconfig.build.json",
52
+ "typecheck": "tsc -p tsconfig.json",
53
+ "test": "vitest run"
54
+ }
6
55
  }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Connected components over links AND merge / split partners (a merge source with no edges of
3
+ * its own still belongs next to its target).
4
+ */
5
+ import type { LayoutGraph } from './graph.js';
6
+
7
+ function find(parent: Int32Array, i: number): number {
8
+ let root = i;
9
+ while (parent[root]! !== root) root = parent[root]!;
10
+ let cur = i;
11
+ while (parent[cur]! !== root) {
12
+ const next = parent[cur]!;
13
+ parent[cur] = root;
14
+ cur = next;
15
+ }
16
+ return root;
17
+ }
18
+
19
+ function union(parent: Int32Array, a: number, b: number): void {
20
+ const ra = find(parent, a);
21
+ const rb = find(parent, b);
22
+ // Smaller index becomes the root: the result does not depend on edge order.
23
+ if (ra < rb) parent[rb] = ra;
24
+ else if (rb < ra) parent[ra] = rb;
25
+ }
26
+
27
+ /**
28
+ * Components as ascending index lists, largest first (ties: smallest first index, i.e. id order).
29
+ * `members` restricts the computation to a subset (others are ignored).
30
+ */
31
+ export function connectedComponents(graph: LayoutGraph, members?: Uint8Array): number[][] {
32
+ const { n } = graph;
33
+ const parent = new Int32Array(n);
34
+ for (let i = 0; i < n; i++) parent[i] = i;
35
+ const inSet = (i: number): boolean => members === undefined || members[i] === 1;
36
+ for (let k = 0; k < graph.pairs.length; k += 2) {
37
+ const a = graph.pairs[k]!;
38
+ const b = graph.pairs[k + 1]!;
39
+ if (inSet(a) && inSet(b)) union(parent, a, b);
40
+ }
41
+ for (const { mover, anchor } of graph.partners) {
42
+ if (inSet(mover) && inSet(anchor)) union(parent, mover, anchor);
43
+ }
44
+ const byRoot = new Map<number, number[]>();
45
+ for (let i = 0; i < n; i++) {
46
+ if (!inSet(i)) continue;
47
+ const root = find(parent, i);
48
+ let list = byRoot.get(root);
49
+ if (list === undefined) {
50
+ list = [];
51
+ byRoot.set(root, list);
52
+ }
53
+ list.push(i);
54
+ }
55
+ return [...byRoot.values()].sort((a, b) => b.length - a.length || a[0]! - b[0]!);
56
+ }
package/src/engine.ts ADDED
@@ -0,0 +1,444 @@
1
+ /**
2
+ * Placement strategies over a `LayoutGraph` (docs/algorithms.md §12 "Strategy", "Components").
3
+ *
4
+ * - `placeFree`: every component laid out on its own, packed, centered at the origin.
5
+ * - `placeAnchored`: known nodes never move; unknown nodes connected to them are seeded next to
6
+ * their placed neighbours and refined by a simulation in which the known nodes are fixed;
7
+ * components without any known node are laid out freely and packed beside the anchored region.
8
+ */
9
+ import { type QuadtreeLeaf, quadtree } from 'd3-quadtree';
10
+ import { connectedComponents } from './components.js';
11
+ import { type CollisionBoxes, removeOverlaps } from './forces/collide-rect.js';
12
+ import { settleOverlapping } from './forces/free-spot.js';
13
+ import {
14
+ anchoredChargeRange,
15
+ CHARGE_RANGE,
16
+ chargeStrength,
17
+ gravityFor,
18
+ linkDistance,
19
+ linkStrength,
20
+ } from './forces/params.js';
21
+ import type { Spring } from './forces/springs.js';
22
+ import { BOX_PADDING, type Extent, extentOf, isEmpty } from './geometry.js';
23
+ import type { LayoutGraph } from './graph.js';
24
+ import { partnerLists, placeAroundFixed, placeOnSpiral } from './init.js';
25
+ import { defaultIterations, type ResolvedLayoutOptions } from './options.js';
26
+ import { pack } from './pack.js';
27
+ import { mulberry32, type Random } from './prng.js';
28
+ import { type SimLink, type SimNode, simulate } from './simulate.js';
29
+
30
+ /** Merge / split partner springs (docs/algorithms.md §12), rest length 0.5·L. */
31
+ const SPRING_STRENGTH = 0.08;
32
+ const SPRING_LENGTH_RATIO = 0.5;
33
+ /** Spiral spacing relative to the link distance: starts a bit denser than the final layout. */
34
+ const SPIRAL_SPACING_RATIO = 0.4;
35
+ const TARGET_ASPECT = 1.6;
36
+ /** Pushing passes before wedged nodes are moved to free spots instead. */
37
+ const SETTLE_PASSES = 24;
38
+
39
+ export interface EngineContext {
40
+ readonly graph: LayoutGraph;
41
+ readonly options: ResolvedLayoutOptions;
42
+ /** Positions by layout index, filled in place. */
43
+ readonly x: Float64Array;
44
+ readonly y: Float64Array;
45
+ }
46
+
47
+ interface Workspace {
48
+ readonly partners: readonly (readonly number[])[];
49
+ /** layout index → sim index, −1 outside the current simulation. */
50
+ readonly slot: Int32Array;
51
+ readonly random: Random;
52
+ }
53
+
54
+ const workspace = (ctx: EngineContext): Workspace => ({
55
+ partners: partnerLists(ctx.graph),
56
+ slot: new Int32Array(ctx.graph.n).fill(-1),
57
+ random: mulberry32(ctx.options.seed),
58
+ });
59
+
60
+ /** Ticks for a simulation over `size` nodes: the option, else `defaultIterations(size)`. */
61
+ const ticksFor = (ctx: EngineContext, size: number): number =>
62
+ ctx.options.iterations ?? defaultIterations(size);
63
+
64
+ /**
65
+ * Lay out everything freely. Returns the ticks of the largest simulation run (0 when none ran,
66
+ * e.g. only isolated nodes).
67
+ */
68
+ export function placeFree(ctx: EngineContext): number {
69
+ const ws = workspace(ctx);
70
+ const components = connectedComponents(ctx.graph);
71
+ const block = layoutBlock(ctx, ws, components);
72
+ const all = components.flat();
73
+ const e = extentOf(ctx.graph, all, ctx.x, ctx.y);
74
+ if (!isEmpty(e)) translate(ctx, all, -(e.minX + e.maxX) / 2, -(e.minY + e.maxY) / 2);
75
+ return block.ticks;
76
+ }
77
+
78
+ /**
79
+ * Lay out around `fixed` nodes, whose positions must already be in `ctx.x` / `ctx.y`.
80
+ * Returns the ticks of the largest simulation run (0 when none ran).
81
+ */
82
+ export function placeAnchored(ctx: EngineContext, fixed: Uint8Array): number {
83
+ const { graph } = ctx;
84
+ const ws = workspace(ctx);
85
+ const components = connectedComponents(graph);
86
+ const anchoredComponents: number[][] = [];
87
+ const freeComponents: number[][] = [];
88
+ for (const component of components) {
89
+ (component.some((i) => fixed[i] === 1) ? anchoredComponents : freeComponents).push(component);
90
+ }
91
+
92
+ const members = anchoredComponents.flat().sort((a, b) => a - b);
93
+ const placed = Uint8Array.from(fixed);
94
+ placeAroundFixed(
95
+ graph,
96
+ ws.partners,
97
+ members,
98
+ placed,
99
+ ctx.x,
100
+ ctx.y,
101
+ ctx.options.linkDistance,
102
+ ctx.options.seed,
103
+ );
104
+
105
+ const free = members.filter((i) => fixed[i] !== 1);
106
+ let ticks = 0;
107
+ let largest = 0;
108
+ if (free.length > 0) {
109
+ // Refine with an energy (and a charge range) matching how much of the picture is new.
110
+ const share = free.length / members.length;
111
+ const window = simulationWindow(ctx, members, free, fixed);
112
+ ticks = ticksFor(ctx, window.length);
113
+ largest = window.length;
114
+ runSimulation(
115
+ ctx,
116
+ ws,
117
+ window,
118
+ fixed,
119
+ ticks,
120
+ 0.3 + 0.7 * share,
121
+ false,
122
+ 0,
123
+ anchoredChargeRange(share, ctx.options.linkDistance),
124
+ );
125
+ settle(ctx, members, fixed);
126
+ }
127
+
128
+ if (freeComponents.length > 0) {
129
+ const block = layoutBlock(ctx, ws, freeComponents);
130
+ if (block.largest > largest) ticks = block.ticks;
131
+ const anchoredExtent = extentOf(graph, members, ctx.x, ctx.y);
132
+ const nodes = freeComponents.flat();
133
+ const gap = ctx.options.componentGap;
134
+ const right = { dx: anchoredExtent.maxX + gap, dy: anchoredExtent.minY };
135
+ const below = { dx: anchoredExtent.minX, dy: anchoredExtent.maxY + gap };
136
+ const cost = (dx: number, dy: number): number => {
137
+ const w = Math.max(anchoredExtent.maxX, dx + block.width) - Math.min(anchoredExtent.minX, dx);
138
+ const h =
139
+ Math.max(anchoredExtent.maxY, dy + block.height) - Math.min(anchoredExtent.minY, dy);
140
+ const side = Math.max(w, h * TARGET_ASPECT);
141
+ return side * side;
142
+ };
143
+ const at = cost(below.dx, below.dy) < cost(right.dx, right.dy) - 1e-6 ? below : right;
144
+ translate(ctx, nodes, at.dx, at.dy);
145
+ }
146
+ return ticks;
147
+ }
148
+
149
+ /**
150
+ * The nodes worth simulating around `free` ones: all of them, their graph neighbours, and the
151
+ * fixed nodes within reach of where any of them starts (a quadtree range query per free node).
152
+ * Far-away fixed nodes cannot interact with the new ones in a refinement run, and skipping them
153
+ * keeps `extendLayout` interactive on large graphs.
154
+ */
155
+ function simulationWindow(
156
+ ctx: EngineContext,
157
+ members: readonly number[],
158
+ free: readonly number[],
159
+ fixed: Uint8Array,
160
+ ): number[] {
161
+ const { graph, x, y } = ctx;
162
+ if (free.length * 2 >= members.length) return [...members];
163
+ const inWindow = new Uint8Array(graph.n);
164
+ let reach = 4 * ctx.options.linkDistance;
165
+ for (const i of free) {
166
+ reach = Math.max(reach, 2 * graph.boxHalfWidth[i]!, 2 * graph.boxBottom[i]!);
167
+ inWindow[i] = 1;
168
+ for (let k = graph.offsets[i]!; k < graph.offsets[i + 1]!; k++)
169
+ inWindow[graph.adjacency[k]!] = 1;
170
+ }
171
+ const tree = quadtree<number>()
172
+ .x((i) => x[i]!)
173
+ .y((i) => y[i]!)
174
+ .addAll(members.filter((i) => fixed[i] === 1));
175
+ for (const i of free) {
176
+ const x0 = x[i]! - reach;
177
+ const x1 = x[i]! + reach;
178
+ const y0 = y[i]! - reach;
179
+ const y1 = y[i]! + reach;
180
+ tree.visit((cell, cx0, cy0, cx1, cy1) => {
181
+ if (!cell.length) {
182
+ let leaf: QuadtreeLeaf<number> | undefined = cell as QuadtreeLeaf<number>;
183
+ while (leaf !== undefined) {
184
+ const j = leaf.data;
185
+ if (x[j]! >= x0 && x[j]! <= x1 && y[j]! >= y0 && y[j]! <= y1) inWindow[j] = 1;
186
+ leaf = leaf.next;
187
+ }
188
+ }
189
+ return cx0 > x1 || cx1 < x0 || cy0 > y1 || cy1 < y0;
190
+ });
191
+ }
192
+ return members.filter((i) => inWindow[i] === 1);
193
+ }
194
+
195
+ interface Block {
196
+ readonly width: number;
197
+ readonly height: number;
198
+ /** Ticks of the largest component simulated (0 when none was). */
199
+ readonly ticks: number;
200
+ /** Size of that component. */
201
+ readonly largest: number;
202
+ }
203
+
204
+ /**
205
+ * Simulate each multi-node component around the origin, then pack all of them (and the
206
+ * isolated nodes) into one block whose visual extent starts at (0, 0).
207
+ */
208
+ function layoutBlock(ctx: EngineContext, ws: Workspace, components: readonly number[][]): Block {
209
+ const { graph, x, y, options } = ctx;
210
+ const visited = new Uint8Array(graph.n);
211
+ const groups: number[][] = [];
212
+ const isolated: number[] = [];
213
+ const extents: Extent[] = [];
214
+ let ticks = 0;
215
+ let largest = 0;
216
+
217
+ for (const component of components) {
218
+ if (component.length === 1) {
219
+ isolated.push(component[0]!);
220
+ continue;
221
+ }
222
+ placeOnSpiral(
223
+ graph,
224
+ ws.partners,
225
+ component,
226
+ options.linkDistance * SPIRAL_SPACING_RATIO,
227
+ ws.random,
228
+ visited,
229
+ x,
230
+ y,
231
+ );
232
+ const componentTicks = ticksFor(ctx, component.length);
233
+ // Components come largest first.
234
+ if (component.length > largest) {
235
+ largest = component.length;
236
+ ticks = componentTicks;
237
+ }
238
+ runSimulation(
239
+ ctx,
240
+ ws,
241
+ component,
242
+ undefined,
243
+ componentTicks,
244
+ 1,
245
+ true,
246
+ gravityFor(component.length),
247
+ CHARGE_RANGE,
248
+ );
249
+ groups.push(component);
250
+ extents.push(extentOf(graph, component, x, y));
251
+ }
252
+
253
+ isolated.sort((a, b) => a - b);
254
+ const packed = pack({
255
+ components: extents.map((e) => ({ width: e.maxX - e.minX, height: e.maxY - e.minY })),
256
+ isolated: isolated.map((i) => ({
257
+ halfWidth: graph.boxHalfWidth[i]!,
258
+ top: graph.boxTop[i]!,
259
+ bottom: graph.boxBottom[i]!,
260
+ })),
261
+ componentGap: options.componentGap,
262
+ gridGapX: options.fontSize * 1.5,
263
+ gridGapY: options.fontSize,
264
+ aspect: TARGET_ASPECT,
265
+ });
266
+ for (let c = 0; c < groups.length; c++) {
267
+ const at = packed.components[c]!;
268
+ const e = extents[c]!;
269
+ translate(ctx, groups[c]!, at.x - e.minX, at.y - e.minY);
270
+ }
271
+ for (let k = 0; k < isolated.length; k++) {
272
+ const i = isolated[k]!;
273
+ const at = packed.isolated[k]!;
274
+ x[i] = at.x;
275
+ y[i] = at.y;
276
+ }
277
+ return { width: packed.width, height: packed.height, ticks, largest };
278
+ }
279
+
280
+ function translate(ctx: EngineContext, nodes: readonly number[], dx: number, dy: number): void {
281
+ for (const i of nodes) {
282
+ ctx.x[i]! += dx;
283
+ ctx.y[i]! += dy;
284
+ }
285
+ }
286
+
287
+ interface SimSetup {
288
+ readonly nodes: SimNode[];
289
+ readonly nodeBoxes: CollisionBoxes;
290
+ readonly labelBoxes: CollisionBoxes;
291
+ readonly charge: Float64Array;
292
+ }
293
+
294
+ /** Sim nodes (index k ↔ `list[k]`), collision boxes and charges for a list of layout indices. */
295
+ function setup(
296
+ ctx: EngineContext,
297
+ list: readonly number[],
298
+ fixed: Uint8Array | undefined,
299
+ ): SimSetup {
300
+ const { graph, x, y, options } = ctx;
301
+ const m = list.length;
302
+ const nodeBoxes: CollisionBoxes = {
303
+ halfWidth: new Float64Array(m),
304
+ halfHeight: new Float64Array(m),
305
+ offsetY: new Float64Array(m),
306
+ invMass: new Float64Array(m),
307
+ };
308
+ const labelBoxes: CollisionBoxes = options.labelAware
309
+ ? {
310
+ halfWidth: new Float64Array(m),
311
+ halfHeight: new Float64Array(m),
312
+ offsetY: new Float64Array(m),
313
+ invMass: nodeBoxes.invMass,
314
+ }
315
+ : nodeBoxes;
316
+ const charge = new Float64Array(m);
317
+ const nodes: SimNode[] = [];
318
+ for (let k = 0; k < m; k++) {
319
+ const i = list[k]!;
320
+ const r = graph.radius[i]!;
321
+ const isFixed = fixed !== undefined && fixed[i] === 1;
322
+ const node: SimNode = { index: k, x: x[i]!, y: y[i]!, vx: 0, vy: 0 };
323
+ if (isFixed) {
324
+ node.fx = node.x;
325
+ node.fy = node.y;
326
+ }
327
+ nodes.push(node);
328
+ nodeBoxes.halfWidth[k] = r;
329
+ nodeBoxes.halfHeight[k] = r;
330
+ // Hubs are heavy: in a collision the leaf steps aside. Fixed nodes never move.
331
+ nodeBoxes.invMass[k] = isFixed ? 0 : 1 / (1 + graph.degree[i]!);
332
+ if (labelBoxes !== nodeBoxes) {
333
+ labelBoxes.halfWidth[k] = graph.boxHalfWidth[i]!;
334
+ labelBoxes.halfHeight[k] = (graph.boxBottom[i]! - graph.boxTop[i]!) / 2;
335
+ labelBoxes.offsetY[k] = (graph.boxBottom[i]! + graph.boxTop[i]!) / 2;
336
+ }
337
+ charge[k] = chargeStrength(graph, i, options.linkDistance, options.labelAware);
338
+ }
339
+ return { nodes, nodeBoxes, labelBoxes, charge };
340
+ }
341
+
342
+ function writeBack(ctx: EngineContext, list: readonly number[], nodes: readonly SimNode[]): void {
343
+ for (let k = 0; k < list.length; k++) {
344
+ const node = nodes[k]!;
345
+ if (node.fx == null) {
346
+ ctx.x[list[k]!] = node.x;
347
+ ctx.y[list[k]!] = node.y;
348
+ }
349
+ }
350
+ }
351
+
352
+ /** Simulate `list` (ascending layout indices) in place; fixed nodes keep their position. */
353
+ function runSimulation(
354
+ ctx: EngineContext,
355
+ ws: Workspace,
356
+ list: readonly number[],
357
+ fixed: Uint8Array | undefined,
358
+ ticks: number,
359
+ alpha: number,
360
+ orient: boolean,
361
+ gravity: number,
362
+ chargeRange: number,
363
+ ): void {
364
+ const { graph, options } = ctx;
365
+ const { slot } = ws;
366
+ for (let k = 0; k < list.length; k++) slot[list[k]!] = k;
367
+ const { nodes, nodeBoxes, labelBoxes, charge } = setup(ctx, list, fixed);
368
+ const isFixed = (i: number): boolean => fixed !== undefined && fixed[i] === 1;
369
+
370
+ const L = options.linkDistance;
371
+ const links: SimLink[] = [];
372
+ for (let k = 0; k < list.length; k++) {
373
+ const i = list[k]!;
374
+ for (let e = graph.offsets[i]!; e < graph.offsets[i + 1]!; e++) {
375
+ const v = graph.adjacency[e]!;
376
+ const kv = slot[v]!;
377
+ if (v <= i || kv < 0 || (isFixed(i) && isFixed(v))) continue;
378
+ const distance = linkDistance(L, Math.max(1, Math.min(graph.degree[i]!, graph.degree[v]!)));
379
+ links.push({
380
+ source: nodes[k]!,
381
+ target: nodes[kv]!,
382
+ distance,
383
+ strength: linkStrength(graph, i, v, distance, options.labelAware),
384
+ });
385
+ }
386
+ }
387
+ const springs: Spring[] = [];
388
+ for (const { mover, anchor } of graph.partners) {
389
+ const km = slot[mover]!;
390
+ const ka = slot[anchor]!;
391
+ if (km >= 0 && ka >= 0) springs.push({ mover: km, anchor: ka });
392
+ }
393
+ for (const i of list) slot[i] = -1;
394
+
395
+ simulate({
396
+ nodes,
397
+ links,
398
+ springs,
399
+ charge,
400
+ nodeBoxes,
401
+ labelBoxes,
402
+ ticks,
403
+ alpha,
404
+ gravity,
405
+ orient,
406
+ springStrength: SPRING_STRENGTH,
407
+ springLength: L * SPRING_LENGTH_RATIO,
408
+ dockDistance: L,
409
+ chargeRange,
410
+ padding: BOX_PADDING,
411
+ random: ws.random,
412
+ });
413
+ writeBack(ctx, list, nodes);
414
+ }
415
+
416
+ /**
417
+ * Final overlap removal over the whole anchored region (`list`), fixed nodes being immovable
418
+ * obstacles: pushes first (new nodes may have drifted next to fixed nodes outside the simulation
419
+ * window), then free spots for nodes that pushing cannot free.
420
+ */
421
+ function settle(ctx: EngineContext, list: readonly number[], fixed: Uint8Array): void {
422
+ const { graph, x, y } = ctx;
423
+ const { nodes, labelBoxes } = setup(ctx, list, fixed);
424
+ removeOverlaps(nodes, labelBoxes, BOX_PADDING, SETTLE_PASSES);
425
+ const slotOf = new Map<number, number>();
426
+ list.forEach((i, k) => {
427
+ slotOf.set(i, k);
428
+ });
429
+ settleOverlapping(nodes, labelBoxes, BOX_PADDING, (k) => {
430
+ const i = list[k]!;
431
+ let sx = 0;
432
+ let sy = 0;
433
+ let count = 0;
434
+ for (let e = graph.offsets[i]!; e < graph.offsets[i + 1]!; e++) {
435
+ const node = nodes[slotOf.get(graph.adjacency[e]!) ?? -1];
436
+ if (node === undefined) continue;
437
+ sx += node.x;
438
+ sy += node.y;
439
+ count++;
440
+ }
441
+ return count > 0 ? { x: sx / count, y: sy / count } : { x: x[i]!, y: y[i]! };
442
+ });
443
+ writeBack(ctx, list, nodes);
444
+ }