Engine unification: d3-force replaces forceAtlas2 everywhere

One engine owns the equilibrium: the merge settle and (next commit)
the live drag both run createGraphSimulation — d3-force link springs,
1/d charge, weak centroid gravity — so a drag's rest state IS the
server's rest state. The FA2-era anchor mechanism survives as a
post-tick POSITION-SPACE re-blend (retention 0.85): d3's forceLink is
a Hooke spring (strength*(d-L)*alpha), which overpowers any force-space
anchor at long range — the re-blend is alpha-independent and always
wins as the sim anneals (the 675-unit drift bug). Gravity retuned for
d3's 1/d many-body falloff (bootstrap 0.2 / incremental 0.12: cluster
spacing lands ~400 world units, the FA2-era density). Rim orphans seed
(and anchor) at 1.7x the CONNECTED core's mean radius — a max-span-based
rim fed back on itself and ran the layout away to 6000 units. Replay:
338 positions in 1.6s, old-node shift within a batch 4.8 (FA2: 23).
This commit is contained in:
amosblomqvist
2026-09-13 09:17:01 +03:00
parent 0c14ea425d
commit 7eceb58303
5 changed files with 186 additions and 73 deletions
+6 -2
View File
@@ -49,6 +49,7 @@ function main() {
console.log(`[replay] ${nodes.length} unique nodes, ${edges.length} edge lines, ${sources.length} sources`);
let maxOldShift = 0;
let shiftReport = "";
for (const source of sources) {
const batch = nodes.filter((n) => batchOf.get(n.id) === source.id);
const isLast = source.id === sources[sources.length - 1]!.id;
@@ -76,7 +77,10 @@ function main() {
for (const [id, prev] of before) {
const now = positions.get(id)!;
const shift = Math.hypot(prev.x - now.x, prev.y - now.y);
if (shift > maxOldShift) maxOldShift = shift;
if (shift > maxOldShift) {
maxOldShift = shift;
shiftReport = `${id} ${prev.x.toFixed(0)},${prev.y.toFixed(0)} -> ${now.x.toFixed(0)},${now.y.toFixed(0)} (batch ${batch.length} new, ${activeEdges.length} edges)`;
}
}
const label = source.filePath.split("/").pop() ?? source.id;
console.log(
@@ -87,7 +91,7 @@ function main() {
graphStore.writePositions(Object.fromEntries(positions));
console.log(
`[replay] done: ${positions.size} positions written in ${((Date.now() - t0) / 1000).toFixed(1)}s` +
` (max old-node shift within a batch: ${maxOldShift.toFixed(1)})`,
` (max old-node shift within a batch: ${maxOldShift.toFixed(1)}: ${shiftReport})`,
);
}