// **GPU-compute force-directed layout** (feature `gpu`) — the VRAM n-body layout.
// One compute entry `cs_layout` advances every node one Fruchterman-Reingold step,
// reading `src`, writing `dst` (ping-pong, no read/write hazard). It MIRRORS
// `gpu_layout::step_cpu` line-for-line (same loop order, same math) so the CPU
// fallback and the GPU path are the same deterministic function (FC-7 parity).
//
// node : a = (pos.x, pos.y, vel.x, vel.y), b = (weight, _, _, _)
// forces: inverse-square repulsion over ALL nodes (O(n²)) + Hooke springs along
// the streamed CSR neighbours + centring gravity, velocity-integrated
// with mass = weight (heavy nodes move slower).
struct LNode {
a: vec4<f32>,
b: vec4<f32>,
};
struct U {
// x = n, y = repulsion, z = attraction, w = ideal_len
a: vec4<f32>,
// x = gravity, y = damping, z = dt, w = min_dist
b: vec4<f32>,
};
@group(0) @binding(0) var<uniform> P: U;
@group(0) @binding(1) var<storage, read> src: array<LNode>;
@group(0) @binding(2) var<storage, read_write> dst: array<LNode>;
@group(0) @binding(3) var<storage, read> offsets: array<u32>;
@group(0) @binding(4) var<storage, read> targets: array<u32>;
@compute @workgroup_size(64)
fn cs_layout(@builtin(global_invocation_id) gid: vec3<u32>) {
let i = gid.x;
let n = u32(P.a.x);
if (i >= n) { return; }
let rep = P.a.y;
let attr = P.a.z;
let ideal = P.a.w;
let grav = P.b.x;
let damp = P.b.y;
let dt = P.b.z;
let mind = P.b.w;
let min2 = mind * mind;
let me = src[i];
let pos_i = me.a.xy;
let wi = me.b.x;
var force = vec2<f32>(0.0, 0.0);
// ── all-pairs inverse-square repulsion ──
for (var j = 0u; j < n; j = j + 1u) {
if (j == i) { continue; }
let oj = src[j];
let d = pos_i - oj.a.xy;
let dist2 = max(dot(d, d), min2);
force = force + d * (rep * wi * oj.b.x / dist2);
}
// ── spring attraction along CSR neighbours ──
let start = offsets[i];
let end = offsets[i + 1u];
for (var k = start; k < end; k = k + 1u) {
let t = targets[k];
let d = src[t].a.xy - pos_i;
let dist = max(length(d), 1e-4);
force = force + d * (attr * (dist - ideal) / dist);
}
// ── centring gravity ──
force = force - pos_i * grav;
// ── integrate (mass = wi) ──
let inv_m = 1.0 / max(wi, 1e-4);
let vel = (me.a.zw + force * inv_m * dt) * damp;
let pos = pos_i + vel * dt;
dst[i].a = vec4<f32>(pos.x, pos.y, vel.x, vel.y);
dst[i].b = me.b;
}