(geometry, targetTriangles, onProgress, harvestFlat = true, harvestTol = DEFAULT_HARVEST_TOL)
| 104 | function _shouldYield() { |
| 105 | const now = performance.now(); |
| 106 | if (now - _lastYieldTime < 100) return false; |
| 107 | _lastYieldTime = now; |
| 108 | return true; |
| 109 | } |
| 110 | function _yieldFrame() { |
| 111 | return new Promise(r => setTimeout(r, 0)); |
| 112 | } |
| 113 | |
| 114 | // ── Public API ─────────────────────────────────────────────────────────────── |
| 115 | |
| 116 | export async function decimate(geometry, targetTriangles, onProgress, harvestFlat = true, harvestTol = DEFAULT_HARVEST_TOL, lockedFaces = null) { |
| 117 | const { positions, faces, vertCount, faceCount } = buildIndexed(geometry); |
| 118 | |
| 119 | // Already at/under the target: nothing to decimate. But if harvesting is on we |
| 120 | // still run — there may be flat faces collapsible for free even below the limit. |
| 121 | if (faceCount <= targetTriangles && !harvestFlat) return buildOutput(positions, faces, faceCount); |
| 122 | |
| 123 | // Preserve-untextured (beta): a vertex touching any locked (untextured) face |
| 124 | // may neither move nor be removed, so edges with a locked endpoint are never |
| 125 | // pushed onto the heap. This also pins the textured/untextured boundary ring, |
| 126 | // so no T-junctions can open against the locked region. lockedFaces uses the |
| 127 | // same face order as the non-indexed input geometry. |
| 128 | let lockedVert = null; |
| 129 | let lockedFaceCount = 0; |
| 130 | if (lockedFaces) { |
| 131 | lockedVert = new Uint8Array(vertCount); |
| 132 | for (let f = 0; f < faceCount; f++) { |
| 133 | if (!lockedFaces[f]) continue; |
| 134 | lockedFaceCount++; |
| 135 | lockedVert[faces[f * 3]] = 1; |
| 136 | lockedVert[faces[f * 3 + 1]] = 1; |
| 137 | lockedVert[faces[f * 3 + 2]] = 1; |
| 138 | } |
| 139 | } |
| 140 | // When the locked faces alone meet or exceed the triangle target, the target |
| 141 | // is unreachable without touching untextured geometry. Chasing it anyway |
| 142 | // would grind the textured region down to its guard limit, so instead drop |
| 143 | // straight to error-bounded harvesting (or bail entirely when harvesting is |
| 144 | // off) and flag the overrun so the caller can warn the user. |
| 145 | const lockedOverBudget = lockedVert !== null && faceCount > targetTriangles |
| 146 | && lockedFaceCount >= targetTriangles; |
| 147 | if (lockedOverBudget && !harvestFlat) { |
| 148 | if (onProgress) onProgress(1); |
| 149 | const out = buildOutput(positions, faces, faceCount); |
| 150 | out.userData.lockedOverBudget = true; |
| 151 | return out; |
| 152 | } |
| 153 | |
| 154 | // Per-vertex error quadrics (10 doubles = upper triangle of symmetric 4×4) |
| 155 | const quadrics = new Float64Array(vertCount * 10); |
| 156 | initQuadrics(quadrics, positions, faces, faceCount); |
| 157 | addCreaseQuadrics(quadrics, positions, faces, faceCount); |
| 158 | |
| 159 | // Doubly-linked vertex-face incidence (typed arrays — faster than Set<number>) |
| 160 | const { vfHead, slotFace, slotVert, slotNext, slotPrev, faceSlot } = |
| 161 | buildLinkedAdj(faces, faceCount, vertCount); |
| 162 | |
| 163 | const active = new Uint8Array(vertCount).fill(1); |
no test coverage detected