re: containment screen fails too -- the static assembler inflates a shared turret

envelope_screen measures per-axis protrusion past the sibling envelope, the
relationship the eye used. It still does not flag e106_bdy_03, and dumping the
static assembly shows why: e303_wep_01's world box is 1600x2100x4800 around a
~400x400x2000 hull, so nothing can protrude. The decode is innocent -- that
resource is 49x23x42 in every container -- so the static assembler is inflating
it 30-110x per axis, non-uniformly. The screen is only meaningful once placement
is trustworthy, and the assembler now has a worse defect than the decoder had.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-12 17:20:52 +00:00
parent 85ad540a7e
commit 27fd9f186f
2 changed files with 134 additions and 0 deletions

View File

@@ -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 <resource3d_dir> [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<String> = 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<String> = 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);
}

View File

@@ -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 30110× 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**