114 lines
2.8 KiB
TypeScript
114 lines
2.8 KiB
TypeScript
/**
|
|
* 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 };
|
|
}
|