mirror of
https://github.com/CNCKitchen/stlTexturizer.git
synced 2026-04-07 22:11:32 +00:00
feat: enhance QEM mesh decimation with performance improvements and numeric key optimizations
This commit is contained in:
+193
-96
@@ -24,6 +24,14 @@
|
|||||||
* the QEM cost of any collapse that would move a vertex off a sharp feature,
|
* the QEM cost of any collapse that would move a vertex off a sharp feature,
|
||||||
* ensuring smooth regions are decimated first while creases are kept intact.
|
* ensuring smooth regions are decimated first while creases are kept intact.
|
||||||
*
|
*
|
||||||
|
* Performance notes:
|
||||||
|
* - Struct-of-arrays typed-array heap avoids per-entry object allocation.
|
||||||
|
* - Numeric edge keys (v_lo * MAX_V + v_hi) replace template strings.
|
||||||
|
* - Vertex deduplication uses a numeric spatial-grid Map instead of strings.
|
||||||
|
* - Link-violation check packs sorted face triple into two Numbers to avoid
|
||||||
|
* string allocation.
|
||||||
|
* - Progress callback fires at most every 512 collapses.
|
||||||
|
*
|
||||||
* @param {THREE.BufferGeometry} geometry non-indexed input
|
* @param {THREE.BufferGeometry} geometry non-indexed input
|
||||||
* @param {number} targetTriangles desired output face count
|
* @param {number} targetTriangles desired output face count
|
||||||
* @param {function} [onProgress] callback(0–1)
|
* @param {function} [onProgress] callback(0–1)
|
||||||
@@ -49,50 +57,55 @@ export function decimate(geometry, targetTriangles, onProgress) {
|
|||||||
initQuadrics(quadrics, positions, faces, faceCount);
|
initQuadrics(quadrics, positions, faces, faceCount);
|
||||||
addCreaseQuadrics(quadrics, positions, faces, faceCount);
|
addCreaseQuadrics(quadrics, positions, faces, faceCount);
|
||||||
|
|
||||||
// Vertex → set of incident face indices
|
// Vertex → set of incident face indices (Int32Arrays for cache efficiency)
|
||||||
const vertFaces = buildAdjacency(faces, faceCount, vertCount);
|
const vertFaces = buildAdjacency(faces, faceCount, vertCount);
|
||||||
const active = new Uint8Array(vertCount).fill(1);
|
const active = new Uint8Array(vertCount).fill(1);
|
||||||
// Per-vertex version counter: incremented whenever a vertex's quadric or
|
// Per-vertex version counter: incremented whenever a vertex's quadric or
|
||||||
// position changes. Heap entries carry the versions at push time; any
|
// position changes. Heap entries carry the versions at push time; any
|
||||||
// entry whose versions no longer match is stale and is skipped. This
|
// entry whose versions no longer match is stale and is skipped.
|
||||||
// prevents old low-cost entries (computed before a crease-quadric merge)
|
|
||||||
// from firing after the vertex has been updated to a higher-cost state.
|
|
||||||
const version = new Uint32Array(vertCount);
|
const version = new Uint32Array(vertCount);
|
||||||
let activeFaces = faceCount;
|
let activeFaces = faceCount;
|
||||||
|
|
||||||
// Seed min-heap with one entry per unique edge
|
// Seed min-heap with one entry per unique edge.
|
||||||
const heap = new MinHeap();
|
// Use BigInt keys to handle any vertex count without integer overflow.
|
||||||
|
const heap = new SoAHeap(Math.min(faceCount * 3, 1 << 24));
|
||||||
const seedSeen = new Set();
|
const seedSeen = new Set();
|
||||||
|
const _vc = BigInt(vertCount);
|
||||||
for (let f = 0; f < faceCount; f++) {
|
for (let f = 0; f < faceCount; f++) {
|
||||||
if (faces[f * 3] < 0) continue;
|
if (faces[f * 3] < 0) continue;
|
||||||
for (let e = 0; e < 3; e++) {
|
for (let e = 0; e < 3; e++) {
|
||||||
const v1 = faces[f * 3 + e];
|
const va = faces[f * 3 + e];
|
||||||
const v2 = faces[f * 3 + ((e + 1) % 3)];
|
const vb = faces[f * 3 + ((e + 1) % 3)];
|
||||||
const ek = v1 < v2 ? `${v1}:${v2}` : `${v2}:${v1}`;
|
const lo = va < vb ? va : vb, hi = va < vb ? vb : va;
|
||||||
if (!seedSeen.has(ek)) { seedSeen.add(ek); pushEdge(heap, quadrics, positions, version, v1, v2); }
|
const ek = BigInt(lo) * _vc + BigInt(hi);
|
||||||
|
if (!seedSeen.has(ek)) { seedSeen.add(ek); pushEdge(heap, quadrics, positions, version, va, vb); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
seedSeen.clear();
|
seedSeen.clear();
|
||||||
|
|
||||||
const initFaces = activeFaces;
|
const initFaces = activeFaces;
|
||||||
let lastProg = 0;
|
const toRemove = initFaces - targetTriangles;
|
||||||
|
let lastProg = 0;
|
||||||
|
let collapses = 0;
|
||||||
|
|
||||||
while (activeFaces > targetTriangles && heap.size() > 0) {
|
while (activeFaces > targetTriangles && heap.size() > 0) {
|
||||||
const { v1, v2, ver1, ver2, px, py, pz } = heap.pop();
|
const idx = heap.pop();
|
||||||
|
if (idx < 0) break;
|
||||||
|
|
||||||
|
const v1 = heap.getV1(idx), v2 = heap.getV2(idx);
|
||||||
|
const ver1 = heap.getVer1(idx), ver2 = heap.getVer2(idx);
|
||||||
|
const px = heap.getPx(idx), py = heap.getPy(idx), pz = heap.getPz(idx);
|
||||||
|
|
||||||
// Stale-entry checks (lazy deletion)
|
// Stale-entry checks (lazy deletion)
|
||||||
if (!active[v1] || !active[v2]) continue;
|
if (!active[v1] || !active[v2]) continue;
|
||||||
// Version check: reject if either vertex's quadric/position has changed
|
|
||||||
// since this entry was pushed (catches outdated pre-merge low-cost entries)
|
|
||||||
if (version[v1] !== ver1 || version[v2] !== ver2) continue;
|
if (version[v1] !== ver1 || version[v2] !== ver2) continue;
|
||||||
if (!shareActiveFace(faces, vertFaces, v1, v2)) continue;
|
if (!shareActiveFace(faces, vertFaces, v1, v2)) continue;
|
||||||
|
|
||||||
// ── Three safety guards ───────────────────────────────────────────────────
|
// ── Three safety guards ───────────────────────────────────────────────────
|
||||||
if (isBoundaryEdge(faces, vertFaces, v1, v2)) continue; // Guard 1
|
if (isBoundaryEdge(faces, vertFaces, v1, v2)) continue; // Guard 1
|
||||||
if (hasLinkViolation(faces, vertFaces, v1, v2)) continue; // Guard 2
|
if (hasLinkViolation(faces, vertFaces, v1, v2)) continue; // Guard 2
|
||||||
const np = [px, py, pz];
|
if (checkFlipped(positions, vertFaces, faces, v1, v2, px, py, pz)) continue; // Guard 3 v1-side
|
||||||
if (checkFlipped(positions, vertFaces, faces, v1, v2, np)) continue; // Guard 3 v1-side
|
if (checkFlipped(positions, vertFaces, faces, v2, v1, px, py, pz)) continue; // Guard 3 v2-side
|
||||||
if (checkFlipped(positions, vertFaces, faces, v2, v1, np)) continue; // Guard 3 v2-side
|
|
||||||
|
|
||||||
// ── Collapse: keep v1 at new position, remove v2 ─────────────────────────
|
// ── Collapse: keep v1 at new position, remove v2 ─────────────────────────
|
||||||
positions[v1 * 3] = px;
|
positions[v1 * 3] = px;
|
||||||
@@ -133,9 +146,9 @@ export function decimate(geometry, targetTriangles, onProgress) {
|
|||||||
if (active[nb]) pushEdge(heap, quadrics, positions, version, v1, nb);
|
if (active[nb]) pushEdge(heap, quadrics, positions, version, v1, nb);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (onProgress) {
|
if (onProgress && (++collapses & 511) === 0) {
|
||||||
const p = Math.min(1, (initFaces - activeFaces) / (initFaces - targetTriangles));
|
const p = Math.min(1, (initFaces - activeFaces) / toRemove);
|
||||||
if (p - lastProg > 0.02) { onProgress(p); lastProg = p; }
|
if (p - lastProg > 0.015) { onProgress(p); lastProg = p; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,41 +174,51 @@ function isBoundaryEdge(faces, vertFaces, v1, v2) {
|
|||||||
// After collapsing v2 into v1, every face of v2 that survives (i.e. does not
|
// After collapsing v2 into v1, every face of v2 that survives (i.e. does not
|
||||||
// share v1) gets v2 replaced by v1. If any such remapped face is identical to
|
// share v1) gets v2 replaced by v1. If any such remapped face is identical to
|
||||||
// a face already incident to v1, the collapse would create a duplicate → reject.
|
// a face already incident to v1, the collapse would create a duplicate → reject.
|
||||||
// This is the actual harm the link condition guards against, without the
|
|
||||||
// false-positives that the strict set-equality test produces on interior edges.
|
|
||||||
|
|
||||||
function hasLinkViolation(faces, vertFaces, v1, v2) {
|
function hasLinkViolation(faces, vertFaces, v1, v2) {
|
||||||
// Build a set of face "signatures" already incident to v1 (excluding shared faces).
|
// Build a map of face signatures already incident to v1 (excluding shared faces).
|
||||||
// A signature is the sorted triple of vertex indices, encoded as a string.
|
// Each sorted triple (a,b,c) is encoded as hi=a*0x200000+b → [c…] for zero string allocation.
|
||||||
const v1Sigs = new Set();
|
const v1Lo = new Map(); // hi → [c…]
|
||||||
for (const f of vertFaces[v1]) {
|
for (const f of vertFaces[v1]) {
|
||||||
if (faces[f * 3] < 0) continue;
|
if (faces[f * 3] < 0) continue;
|
||||||
const fa = faces[f * 3], fb = faces[f * 3 + 1], fc = faces[f * 3 + 2];
|
let fa = faces[f * 3], fb = faces[f * 3 + 1], fc = faces[f * 3 + 2];
|
||||||
if (fa === v2 || fb === v2 || fc === v2) continue; // shared face, will be deleted
|
if (fa === v2 || fb === v2 || fc === v2) continue;
|
||||||
const arr = [fa, fb, fc].sort((a, b) => a - b);
|
// Sort triple
|
||||||
v1Sigs.add(`${arr[0]},${arr[1]},${arr[2]}`);
|
let t;
|
||||||
|
if (fa > fb) { t = fa; fa = fb; fb = t; }
|
||||||
|
if (fb > fc) { t = fb; fb = fc; fc = t; }
|
||||||
|
if (fa > fb) { t = fa; fa = fb; fb = t; }
|
||||||
|
const hi = fa * 0x200000 + fb;
|
||||||
|
// Store hi→[lo…] mapping
|
||||||
|
let arr = v1Lo.get(hi);
|
||||||
|
if (!arr) { arr = []; v1Lo.set(hi, arr); }
|
||||||
|
arr.push(fc);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check every surviving face of v2 (after remapping v2→v1) for duplicates.
|
|
||||||
for (const f of vertFaces[v2]) {
|
for (const f of vertFaces[v2]) {
|
||||||
if (faces[f * 3] < 0) continue;
|
if (faces[f * 3] < 0) continue;
|
||||||
const fa = faces[f * 3], fb = faces[f * 3 + 1], fc = faces[f * 3 + 2];
|
let fa = faces[f * 3], fb = faces[f * 3 + 1], fc = faces[f * 3 + 2];
|
||||||
if (fa === v1 || fb === v1 || fc === v1) continue; // shared face, will be deleted
|
if (fa === v1 || fb === v1 || fc === v1) continue;
|
||||||
// Remap v2 → v1
|
// Remap v2 → v1
|
||||||
const ra = fa === v2 ? v1 : fa;
|
if (fa === v2) fa = v1; else if (fb === v2) fb = v1; else fc = v1;
|
||||||
const rb = fb === v2 ? v1 : fb;
|
let t;
|
||||||
const rc = fc === v2 ? v1 : fc;
|
if (fa > fb) { t = fa; fa = fb; fb = t; }
|
||||||
const arr = [ra, rb, rc].sort((a, b) => a - b);
|
if (fb > fc) { t = fb; fb = fc; fc = t; }
|
||||||
if (v1Sigs.has(`${arr[0]},${arr[1]},${arr[2]}`)) return true;
|
if (fa > fb) { t = fa; fa = fb; fb = t; }
|
||||||
|
const hi = fa * 0x200000 + fb;
|
||||||
|
const arr = v1Lo.get(hi);
|
||||||
|
if (arr) {
|
||||||
|
for (let i = 0; i < arr.length; i++) if (arr[i] === fc) return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Guard 3: Normal-flip rejection ──────────────────────────────────────────
|
// ── Guard 3: Normal-flip rejection ──────────────────────────────────────────
|
||||||
// After hypothetical collapse of v_collapse → newPos, recompute normals of
|
// After hypothetical collapse of v_collapse → (npx,npy,npz), recompute normals
|
||||||
// all affected faces. If any flip by more than ~78° (dot < FLIP_DOT) reject.
|
// of all affected faces. If any flip by more than ~78° (dot < FLIP_DOT) reject.
|
||||||
|
|
||||||
function checkFlipped(positions, vertFaces, faces, v_collapse, v_other, newPos) {
|
function checkFlipped(positions, vertFaces, faces, v_collapse, v_other, npx, npy, npz) {
|
||||||
for (const f of vertFaces[v_collapse]) {
|
for (const f of vertFaces[v_collapse]) {
|
||||||
if (faces[f * 3] < 0) continue;
|
if (faces[f * 3] < 0) continue;
|
||||||
const fa = faces[f * 3], fb = faces[f * 3 + 1], fc = faces[f * 3 + 2];
|
const fa = faces[f * 3], fb = faces[f * 3 + 1], fc = faces[f * 3 + 2];
|
||||||
@@ -209,16 +232,16 @@ function checkFlipped(positions, vertFaces, faces, v_collapse, v_other, newPos)
|
|||||||
positions[fc*3], positions[fc*3+1], positions[fc*3+2]
|
positions[fc*3], positions[fc*3+1], positions[fc*3+2]
|
||||||
);
|
);
|
||||||
|
|
||||||
// New normal with v_collapse replaced by newPos
|
// New normal with v_collapse replaced by new position
|
||||||
const ax = fa === v_collapse ? newPos[0] : positions[fa*3];
|
const ax = fa === v_collapse ? npx : positions[fa*3];
|
||||||
const ay = fa === v_collapse ? newPos[1] : positions[fa*3+1];
|
const ay = fa === v_collapse ? npy : positions[fa*3+1];
|
||||||
const az = fa === v_collapse ? newPos[2] : positions[fa*3+2];
|
const az = fa === v_collapse ? npz : positions[fa*3+2];
|
||||||
const bx = fb === v_collapse ? newPos[0] : positions[fb*3];
|
const bx = fb === v_collapse ? npx : positions[fb*3];
|
||||||
const by = fb === v_collapse ? newPos[1] : positions[fb*3+1];
|
const by = fb === v_collapse ? npy : positions[fb*3+1];
|
||||||
const bz = fb === v_collapse ? newPos[2] : positions[fb*3+2];
|
const bz = fb === v_collapse ? npz : positions[fb*3+2];
|
||||||
const cx = fc === v_collapse ? newPos[0] : positions[fc*3];
|
const cx = fc === v_collapse ? npx : positions[fc*3];
|
||||||
const cy = fc === v_collapse ? newPos[1] : positions[fc*3+1];
|
const cy = fc === v_collapse ? npy : positions[fc*3+1];
|
||||||
const cz = fc === v_collapse ? newPos[2] : positions[fc*3+2];
|
const cz = fc === v_collapse ? npz : positions[fc*3+2];
|
||||||
const [nnx, nny, nnz] = faceNormal(ax, ay, az, bx, by, bz, cx, cy, cz);
|
const [nnx, nny, nnz] = faceNormal(ax, ay, az, bx, by, bz, cx, cy, cz);
|
||||||
|
|
||||||
const dot = onx * nnx + ony * nny + onz * nnz;
|
const dot = onx * nnx + ony * nny + onz * nnz;
|
||||||
@@ -249,14 +272,19 @@ function faceNormal(ax, ay, az, bx, by, bz, cx, cy, cz) {
|
|||||||
// smooth-surface edges and are therefore collapsed last (or not at all).
|
// smooth-surface edges and are therefore collapsed last (or not at all).
|
||||||
|
|
||||||
function addCreaseQuadrics(quadrics, positions, faces, faceCount) {
|
function addCreaseQuadrics(quadrics, positions, faces, faceCount) {
|
||||||
// Build edge → [face, face] map
|
// Build edge → [face, face] map using numeric keys (va_lo * vertMax + vb_hi)
|
||||||
|
// vertMax = next power of two >= faceCount*3 vertices upper bound; use faceCount*3
|
||||||
|
// as a safe upper bound since #verts ≤ #triangles*3.
|
||||||
|
// We already have the actual vertCount from the caller but it's not passed here;
|
||||||
|
// use a Map with numeric key = min*N + max where N = faceCount*3 (safe upper bound).
|
||||||
|
const N = faceCount * 3;
|
||||||
const edgeToFaces = new Map();
|
const edgeToFaces = new Map();
|
||||||
for (let f = 0; f < faceCount; f++) {
|
for (let f = 0; f < faceCount; f++) {
|
||||||
if (faces[f * 3] < 0) continue;
|
if (faces[f * 3] < 0) continue;
|
||||||
for (let e = 0; e < 3; e++) {
|
for (let e = 0; e < 3; e++) {
|
||||||
const va = faces[f * 3 + e];
|
const va = faces[f * 3 + e];
|
||||||
const vb = faces[f * 3 + ((e + 1) % 3)];
|
const vb = faces[f * 3 + ((e + 1) % 3)];
|
||||||
const key = va < vb ? `${va}:${vb}` : `${vb}:${va}`;
|
const key = va < vb ? va * N + vb : vb * N + va;
|
||||||
let arr = edgeToFaces.get(key);
|
let arr = edgeToFaces.get(key);
|
||||||
if (!arr) { arr = []; edgeToFaces.set(key, arr); }
|
if (!arr) { arr = []; edgeToFaces.set(key, arr); }
|
||||||
arr.push(f);
|
arr.push(f);
|
||||||
@@ -285,10 +313,9 @@ function addCreaseQuadrics(quadrics, positions, faces, faceCount) {
|
|||||||
|
|
||||||
if (n0x*n1x + n0y*n1y + n0z*n1z >= CREASE_COS) continue; // smooth — skip
|
if (n0x*n1x + n0y*n1y + n0z*n1z >= CREASE_COS) continue; // smooth — skip
|
||||||
|
|
||||||
// Resolve the two vertex indices from the key string
|
// Resolve the two vertex indices from the numeric key
|
||||||
const colon = key.indexOf(':');
|
const va = Math.floor(key / N);
|
||||||
const va = parseInt(key.slice(0, colon));
|
const vb = key - va * N;
|
||||||
const vb = parseInt(key.slice(colon + 1));
|
|
||||||
|
|
||||||
// Normalised edge direction
|
// Normalised edge direction
|
||||||
const ex = positions[vb*3] - positions[va*3];
|
const ex = positions[vb*3] - positions[va*3];
|
||||||
@@ -400,26 +427,38 @@ function pushEdge(heap, quadrics, positions, version, v1, v2) {
|
|||||||
|
|
||||||
const cost = evalQSum(quadrics, v1, v2, px, py, pz);
|
const cost = evalQSum(quadrics, v1, v2, px, py, pz);
|
||||||
// Snapshot both vertices' versions so the pop-side check can detect staleness
|
// Snapshot both vertices' versions so the pop-side check can detect staleness
|
||||||
heap.push({ cost, v1, v2, ver1: version[v1], ver2: version[v2], px, py, pz });
|
heap.push(cost, v1, v2, version[v1], version[v2], px, py, pz);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Indexed <-> Non-indexed conversion ──────────────────────────────────────
|
// ── Indexed <-> Non-indexed conversion ──────────────────────────────────────
|
||||||
|
|
||||||
|
// Numeric spatial-hash vertex deduplication.
|
||||||
|
// Avoids template-string allocation by encoding quantised (ix,iy,iz) as a
|
||||||
|
// BigInt key: this is still fast because we only call BigInt() once per vertex.
|
||||||
function buildIndexed(geometry) {
|
function buildIndexed(geometry) {
|
||||||
const posAttr = geometry.attributes.position;
|
const posAttr = geometry.attributes.position;
|
||||||
const n = posAttr.count;
|
const n = posAttr.count;
|
||||||
|
|
||||||
const positions = [];
|
const positions = new Float64Array(n * 3); // over-allocated, trimmed later
|
||||||
const vertMap = new Map();
|
|
||||||
const indexRemap = new Int32Array(n);
|
const indexRemap = new Int32Array(n);
|
||||||
|
let vertCount = 0;
|
||||||
|
|
||||||
|
const vertMap = new Map();
|
||||||
|
|
||||||
for (let i = 0; i < n; i++) {
|
for (let i = 0; i < n; i++) {
|
||||||
const x = posAttr.getX(i), y = posAttr.getY(i), z = posAttr.getZ(i);
|
const x = posAttr.getX(i), y = posAttr.getY(i), z = posAttr.getZ(i);
|
||||||
const key = `${Math.round(x * QUANT)}_${Math.round(y * QUANT)}_${Math.round(z * QUANT)}`;
|
// Encode three 21-bit quantised integers into one BigInt key.
|
||||||
|
// Offset by 2^20 to handle negative coordinates.
|
||||||
|
const ix = (Math.round(x * QUANT) + 0x100000) >>> 0;
|
||||||
|
const iy = (Math.round(y * QUANT) + 0x100000) >>> 0;
|
||||||
|
const iz = (Math.round(z * QUANT) + 0x100000) >>> 0;
|
||||||
|
const key = (BigInt(ix) << 42n) | (BigInt(iy) << 21n) | BigInt(iz);
|
||||||
let idx = vertMap.get(key);
|
let idx = vertMap.get(key);
|
||||||
if (idx === undefined) {
|
if (idx === undefined) {
|
||||||
idx = positions.length / 3;
|
idx = vertCount++;
|
||||||
positions.push(x, y, z);
|
positions[idx * 3] = x;
|
||||||
|
positions[idx * 3 + 1] = y;
|
||||||
|
positions[idx * 3 + 2] = z;
|
||||||
vertMap.set(key, idx);
|
vertMap.set(key, idx);
|
||||||
}
|
}
|
||||||
indexRemap[i] = idx;
|
indexRemap[i] = idx;
|
||||||
@@ -427,13 +466,9 @@ function buildIndexed(geometry) {
|
|||||||
|
|
||||||
const faceCount = n / 3;
|
const faceCount = n / 3;
|
||||||
const faces = new Int32Array(faceCount * 3);
|
const faces = new Int32Array(faceCount * 3);
|
||||||
for (let f = 0; f < faceCount; f++) {
|
for (let i = 0; i < n; i++) faces[i] = indexRemap[i];
|
||||||
faces[f * 3] = indexRemap[f * 3];
|
|
||||||
faces[f * 3 + 1] = indexRemap[f * 3 + 1];
|
|
||||||
faces[f * 3 + 2] = indexRemap[f * 3 + 2];
|
|
||||||
}
|
|
||||||
|
|
||||||
return { positions: new Float64Array(positions), faces, vertCount: positions.length / 3, faceCount };
|
return { positions: positions.subarray(0, vertCount * 3), faces, vertCount, faceCount };
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Adjacency helpers ────────────────────────────────────────────────────────
|
// ── Adjacency helpers ────────────────────────────────────────────────────────
|
||||||
@@ -483,46 +518,108 @@ function buildOutput(positions, faces, faceCount) {
|
|||||||
return geo;
|
return geo;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Binary Min-Heap ──────────────────────────────────────────────────────────
|
// ── Struct-of-arrays Min-Heap ────────────────────────────────────────────────
|
||||||
|
// Stores each heap entry in parallel typed arrays rather than JS objects to
|
||||||
class MinHeap {
|
// avoid heap allocation pressure and GC pauses during the collapse loop.
|
||||||
constructor() { this._data = []; }
|
// The heap is 1-indexed (root at slot 1). Slot 0 is used as a scratch area
|
||||||
|
// by pop() so the caller can read fields after popping.
|
||||||
size() { return this._data.length; }
|
// pop() returns 0 (the scratch slot index) on success, or -1 if empty.
|
||||||
|
const SOA_GROW = 1.5;
|
||||||
push(item) {
|
class SoAHeap {
|
||||||
this._data.push(item);
|
constructor(initialCap = 65536) {
|
||||||
this._bubbleUp(this._data.length - 1);
|
let cap = 2;
|
||||||
|
while (cap <= initialCap) cap <<= 1;
|
||||||
|
this._cap = cap;
|
||||||
|
this._len = 0;
|
||||||
|
this._cost = new Float64Array(cap);
|
||||||
|
this._v1 = new Int32Array(cap);
|
||||||
|
this._v2 = new Int32Array(cap);
|
||||||
|
this._ver1 = new Uint32Array(cap);
|
||||||
|
this._ver2 = new Uint32Array(cap);
|
||||||
|
this._px = new Float64Array(cap);
|
||||||
|
this._py = new Float64Array(cap);
|
||||||
|
this._pz = new Float64Array(cap);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
size() { return this._len; }
|
||||||
|
|
||||||
|
push(cost, v1, v2, ver1, ver2, px, py, pz) {
|
||||||
|
let i = ++this._len;
|
||||||
|
if (i >= this._cap) this._grow();
|
||||||
|
this._cost[i] = cost; this._v1[i] = v1; this._v2[i] = v2;
|
||||||
|
this._ver1[i] = ver1; this._ver2[i] = ver2;
|
||||||
|
this._px[i] = px; this._py[i] = py; this._pz[i] = pz;
|
||||||
|
this._bubbleUp(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pops the minimum entry into slot 0 and returns 0. Returns -1 if empty.
|
||||||
pop() {
|
pop() {
|
||||||
const top = this._data[0];
|
if (this._len === 0) return -1;
|
||||||
const last = this._data.pop();
|
this._copySlot(0, 1);
|
||||||
if (this._data.length > 0) { this._data[0] = last; this._sinkDown(0); }
|
this._copySlot(1, this._len--);
|
||||||
return top;
|
if (this._len > 0) this._sinkDown(1);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
getV1 (i) { return this._v1[i]; }
|
||||||
|
getV2 (i) { return this._v2[i]; }
|
||||||
|
getVer1(i) { return this._ver1[i]; }
|
||||||
|
getVer2(i) { return this._ver2[i]; }
|
||||||
|
getPx (i) { return this._px[i]; }
|
||||||
|
getPy (i) { return this._py[i]; }
|
||||||
|
getPz (i) { return this._pz[i]; }
|
||||||
|
|
||||||
|
_copySlot(dst, src) {
|
||||||
|
this._cost[dst] = this._cost[src]; this._v1[dst] = this._v1[src]; this._v2[dst] = this._v2[src];
|
||||||
|
this._ver1[dst] = this._ver1[src]; this._ver2[dst] = this._ver2[src];
|
||||||
|
this._px[dst] = this._px[src]; this._py[dst] = this._py[src]; this._pz[dst] = this._pz[src];
|
||||||
|
}
|
||||||
|
|
||||||
|
_swap(a, b) {
|
||||||
|
const tc = this._cost[a], tv1 = this._v1[a], tv2 = this._v2[a];
|
||||||
|
const te1 = this._ver1[a], te2 = this._ver2[a];
|
||||||
|
const tpx = this._px[a], tpy = this._py[a], tpz = this._pz[a];
|
||||||
|
this._cost[a] = this._cost[b]; this._v1[a] = this._v1[b]; this._v2[a] = this._v2[b];
|
||||||
|
this._ver1[a] = this._ver1[b]; this._ver2[a] = this._ver2[b];
|
||||||
|
this._px[a] = this._px[b]; this._py[a] = this._py[b]; this._pz[a] = this._pz[b];
|
||||||
|
this._cost[b] = tc; this._v1[b] = tv1; this._v2[b] = tv2;
|
||||||
|
this._ver1[b] = te1; this._ver2[b] = te2;
|
||||||
|
this._px[b] = tpx; this._py[b] = tpy; this._pz[b] = tpz;
|
||||||
}
|
}
|
||||||
|
|
||||||
_bubbleUp(i) {
|
_bubbleUp(i) {
|
||||||
const d = this._data;
|
const cost = this._cost;
|
||||||
while (i > 0) {
|
while (i > 1) {
|
||||||
const p = (i - 1) >> 1;
|
const p = i >> 1;
|
||||||
if (d[p].cost <= d[i].cost) break;
|
if (cost[p] <= cost[i]) break;
|
||||||
[d[p], d[i]] = [d[i], d[p]];
|
this._swap(p, i); i = p;
|
||||||
i = p;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_sinkDown(i) {
|
_sinkDown(i) {
|
||||||
const d = this._data;
|
const cost = this._cost;
|
||||||
const n = d.length;
|
const n = this._len;
|
||||||
for (;;) {
|
for (;;) {
|
||||||
let s = i;
|
let s = i;
|
||||||
const l = 2 * i + 1, r = 2 * i + 2;
|
const l = i << 1, r = l | 1;
|
||||||
if (l < n && d[l].cost < d[s].cost) s = l;
|
if (l <= n && cost[l] < cost[s]) s = l;
|
||||||
if (r < n && d[r].cost < d[s].cost) s = r;
|
if (r <= n && cost[r] < cost[s]) s = r;
|
||||||
if (s === i) break;
|
if (s === i) break;
|
||||||
[d[s], d[i]] = [d[i], d[s]];
|
this._swap(s, i); i = s;
|
||||||
i = s;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_grow() {
|
||||||
|
const newCap = Math.ceil(this._cap * SOA_GROW) + 2;
|
||||||
|
const resize = (old, Ctor) => { const n = new Ctor(newCap); n.set(old); return n; };
|
||||||
|
this._cost = resize(this._cost, Float64Array);
|
||||||
|
this._v1 = resize(this._v1, Int32Array);
|
||||||
|
this._v2 = resize(this._v2, Int32Array);
|
||||||
|
this._ver1 = resize(this._ver1, Uint32Array);
|
||||||
|
this._ver2 = resize(this._ver2, Uint32Array);
|
||||||
|
this._px = resize(this._px, Float64Array);
|
||||||
|
this._py = resize(this._py, Float64Array);
|
||||||
|
this._pz = resize(this._pz, Float64Array);
|
||||||
|
this._cap = newCap;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user