diff --git a/crates/sylpheed-formats/examples/envelope_screen.rs b/crates/sylpheed-formats/examples/envelope_screen.rs new file mode 100644 index 0000000..00884b0 --- /dev/null +++ b/crates/sylpheed-formats/examples/envelope_screen.rs @@ -0,0 +1,106 @@ +//! Does every part of a ship sit inside the envelope its siblings describe? +//! +//! `slab_screen` compared *scale* and could not see the `e106` slab. What the eye +//! used when the render exposed it was **relationship**: a blocky mass sitting +//! apart from the hull. This measures that — assemble a ship, and for each part +//! ask how far its world box protrudes beyond the box of all the OTHER parts, +//! relative to the ship's own size. A mis-anchored block sticks out; a real part, +//! however big, is part of the silhouette. +//! +//! Usage: envelope_screen [protrusion_fraction] +use sylpheed_formats::mesh::Xbg7Model; +use sylpheed_formats::ship::{assemble_ship, ship_id_of}; +use std::collections::{BTreeSet, HashSet}; + +fn main() { + let dir = std::env::args().nth(1).expect("resource3d dir"); + let limit: f32 = std::env::args().nth(2).and_then(|s| s.parse().ok()).unwrap_or(0.35); + 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 ids: BTreeSet = Xbg7Model::anchor_models_cancellable(&bytes, 0.0, &|| false) + .iter() + .filter_map(|m| ship_id_of(&m.name).map(|s| s.to_string())) + .collect(); + for id in &ids { + let placed = assemble_ship(&bytes, id, true); + if placed.len() < 3 { + continue; + } + let want: HashSet = placed.iter().map(|p| p.resource.clone()).collect(); + let models = Xbg7Model::models_named(&bytes, &want, &|| false); + // World box per placement. + let mut boxes: Vec<(String, [f32; 3], [f32; 3])> = Vec::new(); + for p in &placed { + let Some(m) = models.iter().find(|m| m.name == p.resource) else { continue }; + let (mut lo, mut hi) = ([f32::MAX; 3], [f32::MIN; 3]); + for s in &m.meshes { + for q in &s.positions { + let w = p.apply(*q); + for k in 0..3 { + lo[k] = lo[k].min(w[k]); + hi[k] = hi[k].max(w[k]); + } + } + } + if lo[0] != f32::MAX { + boxes.push((p.resource.clone(), lo, hi)); + } + } + if boxes.len() < 3 { + continue; + } + for i in 0..boxes.len() { + // Envelope of every OTHER part. + let (mut lo, mut hi) = ([f32::MAX; 3], [f32::MIN; 3]); + for (j, b) in boxes.iter().enumerate() { + if i == j { + continue; + } + for k in 0..3 { + lo[k] = lo[k].min(b.1[k]); + hi[k] = hi[k].max(b.2[k]); + } + } + // Per-AXIS: a bow legitimately extends the ship along its long + // axis, so protrusion only means something measured against the + // envelope's size IN THAT AXIS. The e106 slab stuck ~1 500 out in + // Y where its siblings spanned ~800. + let mut worst = 0.0f32; + let mut worst_out = 0.0f32; + for k in 0..3 { + let size_k = hi[k] - lo[k]; + if size_k <= 1.0 { + continue; + } + let out_k = (lo[k] - boxes[i].1[k]).max(boxes[i].2[k] - hi[k]).max(0.0); + if out_k / size_k > worst { + worst = out_k / size_k; + worst_out = out_k; + } + } + let (out, size) = (worst_out, 1.0f32); + let _ = size; + if worst > limit { + flagged += 1; + println!( + "{:<20} {:<22} protrudes {:>7.0} beyond its siblings ({:.0}% of the ship)", + f.file_name().unwrap().to_string_lossy(), + boxes[i].0, + out, + 100.0 * worst + ); + } + } + } + } + println!("{flagged} parts protrude more than {:.0}% of their ship's size", 100.0 * limit); +} diff --git a/docs/re/structures/xbg7-mesh.md b/docs/re/structures/xbg7-mesh.md index 6e8abe4..0d92309 100644 --- a/docs/re/structures/xbg7-mesh.md +++ b/docs/re/structures/xbg7-mesh.md @@ -1097,6 +1097,34 @@ that carry it) and `t901_e01_D` is 58×59×3013 (a mast). And `f101_bdy_01` flag at 6× while being **capture-verified exact** in the truth table — a useful reminder that a bulky hull is not a bug. +### The containment screen fails too — because the static placement is worse + +`examples/envelope_screen.rs` is the metric the eye actually used: assemble a +ship, and for each part measure how far its world box protrudes past the box of +all the *other* parts, **per axis** (a bow legitimately extends the long axis, so +protrusion only means something against that axis' own envelope). + +It does not flag `e106_bdy_03` either, before or after the coverage fix. Dumping +the static assembly shows why, and the reason is a bigger finding than the screen: + +``` +e106_bdy_03 X[ -300, 300] Y[ 0, 1600] Z[ 188, 1186] +e303_wep_01 X[-1179, 421] Y[ -996, 1104] Z[-2368, 2432] ← shared turret +e106_bdy_04 X[ -215, 215] Y[ -165, 234] Z[ -991, 389] +``` + +The **turret swallows the ship**: `e303_wep_01`'s world box is 1600×2100×4800 +around a hull of ~400×400×2000, so nothing can protrude past the envelope. Its +*decode* is not at fault — the resource comes out 49×23×42 in `Stage_S01` through +`Stage_S06` alike, consistent everywhere — so this is a **static-assembler** +defect: the placement blows a 49-unit turret up by 30–110× per axis (and +non-uniformly, so it is not a simple scale factor). + +Two conclusions. The screen is only meaningful once placement is trustworthy, and +**the static assembler has a worse defect than the decoder did** — one that the +capture-baked path for `e106` hides, because the baked table places the ship +correctly and is what the viewer prefers. + ### The anchor work has plateaued at 98.7 % — state and what is left Four evidence-driven changes took the decoder from 5 480 to **6 212 of 6 294**