/** * Quadtree that spatially sorts a particle array in-place at build time. * Stores only leaf index ranges [start, end) into the sorted array. * NO per-frame methods — this is purely a build-time spatial index. */ export interface Leaf { start: number; end: number; minX: number; minY: number; maxX: number; maxY: number; } /** * Spatially sort particles using a quadtree and return * the sorted array + leaf ranges. * * Takes raw Float32Arrays (no object allocation). * Uses in-place partitioning (zero temporary arrays). */ export function buildSpatialIndex( xs: Float32Array, ys: Float32Array ): { sorted: Float32Array; leaves: Leaf[]; order: Uint32Array } { const n = xs.length; const order = new Uint32Array(n); for (let i = 0; i < n; i++) order[i] = i; // Find bounds let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; for (let i = 0; i < n; i++) { const x = xs[i], y = ys[i]; if (x < minX) minX = x; if (y < minY) minY = y; if (x > maxX) maxX = x; if (y > maxY) maxY = y; } const leaves: Leaf[] = []; // In-place quicksort-style partitioning function partition( vals: Float32Array, start: number, end: number, mid: number ): number { let lo = start, hi = end - 1; while (lo <= hi) { while (lo <= hi && vals[order[lo]] < mid) lo++; while (lo <= hi && vals[order[hi]] >= mid) hi--; if (lo < hi) { const t = order[lo]; order[lo] = order[hi]; order[hi] = t; lo++; hi--; } } return lo; } function build( start: number, end: number, bMinX: number, bMinY: number, bMaxX: number, bMaxY: number, depth: number ): void { const count = end - start; if (count <= 0) return; // Leaf: stop subdividing if (count <= 4096 || depth >= 12) { leaves.push({ start, end, minX: bMinX, minY: bMinY, maxX: bMaxX, maxY: bMaxY }); return; } const midX = (bMinX + bMaxX) / 2; const midY = (bMinY + bMaxY) / 2; // Partition by X, then each half by Y const splitX = partition(xs, start, end, midX); const splitLeftY = partition(ys, start, splitX, midY); const splitRightY = partition(ys, splitX, end, midY); // BL, TL, BR, TR build(start, splitLeftY, bMinX, bMinY, midX, midY, depth + 1); build(splitLeftY, splitX, bMinX, midY, midX, bMaxY, depth + 1); build(splitX, splitRightY, midX, bMinY, bMaxX, midY, depth + 1); build(splitRightY, end, midX, midY, bMaxX, bMaxY, depth + 1); } build(0, n, minX, minY, maxX, maxY, 0); // Reorder particles to match tree layout const sorted = new Float32Array(n * 2); for (let i = 0; i < n; i++) { const src = order[i]; sorted[i * 2] = xs[src]; sorted[i * 2 + 1] = ys[src]; } return { sorted, leaves, order }; }