re: the other four ships are clean; automating the slab check failed
Rendered f101/f105/f106/e105 statically -- all coherent, no stray masses, so the e106 slab was specific. slab_screen.rs tries to automate the check by comparing each part's min-axis extent to its ship median, but it produces identical flags with and without the coverage fix at both 4x and 2.5x: it never sees e106_bdy_03, the part it was built for. The eye used relationship (a mass apart from the hull), not scale; a containment test is the right analogue and is not built. Kept for what it does show: f002_bdy_22's 100000-unit tether and t901_e01_D's mast are legitimate, and capture-verified f101_bdy_01 flags at 6x. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
74
crates/sylpheed-formats/examples/slab_screen.rs
Normal file
74
crates/sylpheed-formats/examples/slab_screen.rs
Normal file
@@ -0,0 +1,74 @@
|
||||
//! Screen every ship family for a part that is wildly out of scale with its
|
||||
//! siblings — the "slab" signature of a mis-anchored block.
|
||||
//!
|
||||
//! Rendering `e106` found such a part (`bdy_03`, 600×1600×998 beside parts of
|
||||
//! ~250) that coverage, cross-container consistency, the capture oracle and the
|
||||
//! twin invariant were all blind to. Eyeballing does not scale to 166 containers;
|
||||
//! this does the same comparison numerically.
|
||||
//!
|
||||
//! Usage: slab_screen <resource3d_dir> [factor]
|
||||
use sylpheed_formats::mesh::Xbg7Model;
|
||||
use sylpheed_formats::ship::{is_base_part, ship_id_of};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
fn main() {
|
||||
let dir = std::env::args().nth(1).expect("resource3d dir");
|
||||
let factor: f32 = std::env::args().nth(2).and_then(|s| s.parse().ok()).unwrap_or(4.0);
|
||||
let mut files: Vec<_> = std::fs::read_dir(&dir)
|
||||
.unwrap()
|
||||
.flatten()
|
||||
.map(|e| e.path())
|
||||
.filter(|p| p.extension().and_then(|s| s.to_str()) == Some("xpr"))
|
||||
.collect();
|
||||
files.sort();
|
||||
|
||||
let mut flagged = 0usize;
|
||||
for f in &files {
|
||||
let Ok(bytes) = std::fs::read(f) else { continue };
|
||||
let mut by_ship: BTreeMap<String, Vec<(String, f32)>> = BTreeMap::new();
|
||||
for m in Xbg7Model::anchor_models_cancellable(&bytes, 0.0, &|| false) {
|
||||
if !is_base_part(&m.name) {
|
||||
continue;
|
||||
}
|
||||
let Some(id) = ship_id_of(&m.name) else { continue };
|
||||
let (mut lo, mut hi) = ([f32::MAX; 3], [f32::MIN; 3]);
|
||||
for s in &m.meshes {
|
||||
for q in &s.positions {
|
||||
for k in 0..3 {
|
||||
lo[k] = lo[k].min(q[k]);
|
||||
hi[k] = hi[k].max(q[k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if lo[0] == f32::MAX {
|
||||
continue;
|
||||
}
|
||||
// Compare the part's SMALLEST axis span, not its diagonal. A long
|
||||
// thin part (an antenna, a 100 000-unit tether on `f002`) is
|
||||
// legitimately huge in one axis and would swamp a diagonal test; a
|
||||
// mis-anchored block is bulky in all three, which is what the e106
|
||||
// slab looked like (600×1600×998 beside siblings of ~250).
|
||||
let thin = (hi[0] - lo[0]).min(hi[1] - lo[1]).min(hi[2] - lo[2]);
|
||||
by_ship.entry(id.to_string()).or_default().push((m.name.clone(), thin));
|
||||
}
|
||||
for (id, parts) in &by_ship {
|
||||
if parts.len() < 3 {
|
||||
continue; // no meaningful median
|
||||
}
|
||||
let mut d: Vec<f32> = parts.iter().map(|(_, x)| *x).collect();
|
||||
d.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
let median = d[d.len() / 2];
|
||||
for (name, diag) in parts {
|
||||
if *diag > median * factor {
|
||||
flagged += 1;
|
||||
println!(
|
||||
"{:<22} {name:<22} min-axis {diag:>8.0} vs ship median {median:>8.0} ({:.1}×)",
|
||||
f.file_name().unwrap().to_string_lossy(),
|
||||
diag / median
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("{flagged} parts flagged at {factor}× the ship median");
|
||||
}
|
||||
Reference in New Issue
Block a user