(geometry, faceParentId, maxEdgeLength, opts = {})
| 50 | * excludeWeight-marked faces so untextured surfaces are never modified (beta) |
| 51 | * @returns {{ geometry, faceParentId, collapseCount }} |
| 52 | */ |
| 53 | |
| 54 | import { THREE } from './threeCompat.js'; |
| 55 | import { QuantizedPointMap } from './meshIndex.js'; |
| 56 | |
| 57 | const QUANTISE = 1e5; |
| 58 | |
| 59 | export function regularizeMesh(geometry, faceParentId, maxEdgeLength, opts = {}) { |
| 60 | // Candidate filter — triangles with thinness ABOVE this become collapse |
| 61 | // candidates. Bumped from 3 → 5 when we switched from edge-ratio to |
| 62 | // thinness: the new metric is more sensitive (catches near-collinear |
| 63 | // tris), so 3 swept up moderate-shape fillet triangles and broke them. |
| 64 | // 5 still catches every meaningful sliver — chain slivers routinely have |
| 65 | // thinness > 50 and the user's reported case was ≈ 1800. |
| 66 | const aspectThreshold = opts.aspectThreshold ?? 5; |
| 67 | // Two-tier slack. The BASE (slack) is already loose so non-sliver boundary |
| 68 | // collapses still succeed — that's what gives sliver chains the topological |
| 69 | // room to dissolve. The AGGRESSIVE tier kicks in when at least one wing is |
| 70 | // an extreme sliver and adds extra leeway on top, letting tough chain ends |
| 71 | // clear that the base tier alone wouldn't reach. An earlier attempt with a |
| 72 | // tight base (1.2) BLOCKED the helper collapses and made chains worse — |
| 73 | // hence the loose base here. |
| 74 | const slack = opts.slack ?? 3.0; |
| 75 | const aggressiveSlack = opts.aggressiveSlack ?? 8.0; |
| 76 | // Note: extremeSliverAspect is measured in the new thinness metric (lmax/hmin), |
| 77 | // so a value of 8 means "longest edge is at least 8× the shortest altitude". |
| 78 | // This skips moderate-shape fillet triangles (typically thinness 2–5) so the |
| 79 | // loose aggressive normal cap doesn't bend fine fillets, while still catching |
| 80 | // every real chain sliver (thinness routinely > 50). |
| 81 | const extremeSliverAspect = opts.extremeSliverAspect ?? 8; |
| 82 | // Per-collapse normal swing is bounded by `maxNormalDeltaCos` BUT we measure |
| 83 | // the swing against each affected triangle's *original* normal (captured |
| 84 | // once before any collapse), not its current post-drift normal. Without |
| 85 | // this, multiple rounds of 15° drift each compound into >100° corner |
| 86 | // damage on 45° edges. |
| 87 | // |
| 88 | // Like edge-cap, normal-change is two-tier. The aggressive tier fires only |
| 89 | // when BOTH wings are extreme slivers (not just one) — that matches the |
| 90 | // chain-of-needles-on-a-curved-face shape but NOT the fillet-sliver-next-to- |
| 91 | // a-larger-fillet-face shape, so fine fillets keep their tight gate. |
| 92 | const maxNormalDeltaCos = opts.maxNormalDeltaCos ?? Math.cos(15 * Math.PI / 180); |
| 93 | const aggressiveNormalDeltaCos = opts.aggressiveNormalDeltaCos ?? Math.cos(25 * Math.PI / 180); |
| 94 | // Vertices on edges with dihedral > sharpEdgeAngle are frozen: they cannot |
| 95 | // be collapse endpoints, so 45°/90° feature edges keep every original |
| 96 | // vertex. Slivers in the interior of a flat face are still collapsible. |
| 97 | const sharpEdgeCos = opts.sharpEdgeCos ?? Math.cos(30 * Math.PI / 180); |
| 98 | const maxRounds = opts.maxRounds ?? 8; |
| 99 | |
| 100 | // Squared caps precomputed for both tiers; the per-collapse logic picks one. |
| 101 | const baseMaxLenSqAllowed = (maxEdgeLength * slack) * (maxEdgeLength * slack); |
| 102 | const aggressiveMaxLenSqAllowed = (maxEdgeLength * aggressiveSlack) * (maxEdgeLength * aggressiveSlack); |
| 103 | const extremeAspect2 = extremeSliverAspect * extremeSliverAspect; |
| 104 | |
| 105 | // ── Build indexed mesh ── |
| 106 | const pa = geometry.attributes.position.array; |
| 107 | const triCount = pa.length / 9; |
| 108 | |
| 109 | const posMap = new QuantizedPointMap(QUANTISE, Math.min(triCount * 3, 1 << 22)); |
no test coverage detected