diff --git a/crates/sylpheed-formats/examples/edge_cap_sweep.rs b/crates/sylpheed-formats/examples/edge_cap_sweep.rs index d874a5e..c948ba2 100644 --- a/crates/sylpheed-formats/examples/edge_cap_sweep.rs +++ b/crates/sylpheed-formats/examples/edge_cap_sweep.rs @@ -75,6 +75,6 @@ fn main() { } println!( "cap={} models={models} verts={verts} shared={shared} inconsistent={inconsistent}", - std::env::var("XBG7_EDGE_CAP").unwrap_or_else(|_| "0.42 (default)".into()) + std::env::var("XBG7_EDGE_CAP").unwrap_or_else(|_| "library default".into()) ); } diff --git a/crates/sylpheed-formats/src/mesh.rs b/crates/sylpheed-formats/src/mesh.rs index 3e9bb04..a0c659c 100644 --- a/crates/sylpheed-formats/src/mesh.rs +++ b/crates/sylpheed-formats/src/mesh.rs @@ -831,7 +831,10 @@ pub fn debug_best_rejection(bytes: &[u8], name: &str) -> Option<(usize, String)> if vb < idx_bytes + pad { continue; } - let mc = if pad == 0 { 0.0 } else { 0.85 }; + // Mirror production exactly: the pad-0 path now carries the winding + // floor too. Reporting at 0.0 would accept blocks the decoder + // rejects and point at the wrong gate. + let mc = if pad == 0 { pad0_consistency() } else { 0.85 }; if let Err(why) = validate_block_report( bytes, vb - idx_bytes - pad, @@ -966,6 +969,22 @@ fn edge_cap() -> f32 { std::env::var("XBG7_EDGE_CAP").ok().and_then(|v| v.parse().ok()).unwrap_or(1.0) } +/// Smallest bounding-box extent a block may have (default `0.5`). An absolute +/// floor on a format with no unit convention is a scale assumption, so it is a +/// knob: `XBG7_MIN_EXTENT`. +fn min_extent() -> f32 { + std::env::var("XBG7_MIN_EXTENT").ok().and_then(|v| v.parse().ok()).unwrap_or(0.5) +} + +/// Use a scale-free collinearity test for degeneracy instead of the absolute +/// triangle-area one (`XBG7_REL_DEGEN=1`). More principled in the abstract — an +/// absolute area threshold calls a small object's every triangle degenerate — +/// but measured on this disc it decodes **no more** resources and raises +/// cross-container inconsistency 39 → 44, so it is **not** the default. +fn rel_degen() -> bool { + std::env::var("XBG7_REL_DEGEN").is_ok() +} + /// Winding-consistency floor for the pad-0 single-block anchor. /// /// **0.70 since 2026-08-12.** A triangle's face normal should agree with its @@ -1278,7 +1297,21 @@ fn validate_block_report( u[2] * w[0] - u[0] * w[2], u[0] * w[1] - u[1] * w[0], ]; - if 0.5 * (cx[0] * cx[0] + cx[1] * cx[1] + cx[2] * cx[2]).sqrt() < 1.0e-9 { + // Degeneracy = collinear vertices, which is a SCALE-FREE property: + // compare the cross-product magnitude to the two edge lengths that + // produced it (i.e. sin of the angle between them). The old absolute + // `area < 1e-9` test called a small object's every triangle degenerate — + // `g005` spans 0.346 units and scored 7 of 8 — so it rejected tiny props + // for being tiny. `XBG7_ABS_DEGEN=1` restores the absolute test. + let cross = (cx[0] * cx[0] + cx[1] * cx[1] + cx[2] * cx[2]).sqrt(); + let un = (u[0] * u[0] + u[1] * u[1] + u[2] * u[2]).sqrt(); + let wn = (w[0] * w[0] + w[1] * w[1] + w[2] * w[2]).sqrt(); + let is_degenerate = if rel_degen() { + cross < 1.0e-6 * un * wn || un == 0.0 || wn == 0.0 + } else { + 0.5 * cross < 1.0e-9 + }; + if is_degenerate { degenerate += 1; } else if let Some(no) = decl.normal_offset { // Stored-normal agreement: the face normal should point the way @@ -1305,7 +1338,7 @@ fn validate_block_report( t += tstep; } let extent = (hi[0] - lo[0]).max(hi[1] - lo[1]).max(hi[2] - lo[2]); - if extent < 0.5 || sampled == 0 || degenerate * 10 > sampled * 3 { + if extent < min_extent() || sampled == 0 || degenerate * 10 > sampled * 3 { return Err(format!( "extent {extent:.3} (min 0.5), {degenerate}/{sampled} degenerate (max 30%)" )); diff --git a/docs/re/structures/xbg7-mesh.md b/docs/re/structures/xbg7-mesh.md index 8742876..e932511 100644 --- a/docs/re/structures/xbg7-mesh.md +++ b/docs/re/structures/xbg7-mesh.md @@ -1042,6 +1042,34 @@ not obviously right, and both points are one env var apart (`XBG7_PAD0_CONSISTENCY`, `XBG7_EDGE_CAP`) for anyone who wants the conservative end. +### Where the remaining 82 misses stand — and a refuted fix + +With the winding gate shipped, coverage is **6 212 / 6 294 = 98.7 %** and only +**82** resources never decode (was 225). Re-attributed: + +| furthest gate reached | count | +|---|---| +| degenerate / implausible positions (`extent < 0.5`, >30 % degenerate) | 42 | +| winding consistency | 31 | +| buffer not covered by indices | 9 | +| connectivity | 0 (inert) | + +**The biggest bucket is not the blocker** — which is exactly the caveat this +attribution carries. Both of its thresholds are *absolute*, which on a format +with no unit convention is a scale assumption: an area test of `< 1e-9` calls +every triangle of a small object degenerate (`g005` spans 0.346 units and scored +7 of 8), and `extent < 0.5` rejects it outright. Replacing the area test with a +**scale-free collinearity** test (`|u × w| < 1e-6·|u|·|w|`, i.e. sin of the angle +between the edges) is the principled version — and measured on this disc it +decodes **no more resources at all**, while raising cross-container inconsistency +39 → 44. Lowering the extent floor to 0.05 adds **two**. + +So the fix that the histogram appeared to point at is refuted: those 42 are +resources where some *wrong* candidate reached that gate, not where the true +block was rejected. Both are kept as knobs (`XBG7_REL_DEGEN`, `XBG7_MIN_EXTENT`), +neither is the default, and the measurement is recorded so the next reader does +not re-derive it. + ### Coverage has a denominator now, and the misses have a cause breakdown Coverage has been quoted as "resources decoded" with no total. `examples/undecoded.rs`