//! Verify static ship assembly against multi-snapshot, multi-instance F10 //! captures: cluster every part's ship-relative transform across ALL ships in //! ALL snapshots, and compare the dominant clusters with `assemble_ship`. //! //! cargo run --release --example capture_verify -- [log…] //! //! Every draw is position-validated against the ship's parts (any LOD, set //! based). Each `bdy_04`(-LOD) draw acts as a ship reference; every validated //! part draw within ship range contributes `rel = WV_ref⁻¹ ∘ WV_part`. The true //! per-part placements appear as tight clusters repeated once PER SHIP per //! snapshot; cross-ship pairings scatter and stay below the cluster threshold. use std::collections::HashSet; use sylpheed_formats::mesh::{xbg7_resource_names, Xbg7Model}; use sylpheed_formats::ship::{assemble_ship, is_base_part, ship_id_of}; use sylpheed_formats::ship_capture::{parse_capture, CapturedDraw}; type M3 = [[f64; 3]; 3]; fn transpose(m: &M3) -> M3 { [ [m[0][0], m[1][0], m[2][0]], [m[0][1], m[1][1], m[2][1]], [m[0][2], m[1][2], m[2][2]], ] } fn mmul(a: &M3, b: &M3) -> M3 { let mut o = [[0.0; 3]; 3]; for i in 0..3 { for j in 0..3 { o[i][j] = (0..3).map(|k| a[i][k] * b[k][j]).sum(); } } o } fn mv(m: &M3, v: [f64; 3]) -> [f64; 3] { [ m[0][0] * v[0] + m[0][1] * v[1] + m[0][2] * v[2], m[1][0] * v[0] + m[1][1] * v[1] + m[1][2] * v[2], m[2][0] * v[0] + m[2][1] * v[1] + m[2][2] * v[2], ] } fn main() { let a: Vec = std::env::args().collect(); let bytes = std::fs::read(&a[1]).unwrap(); let id = &a[2]; // Part position sets (base + LODs share the local frame) + per-variant vcounts. let names = xbg7_resource_names(&bytes); let base_parts: Vec = names .iter() .filter(|n| is_base_part(n) && ship_id_of(n) == Some(id.as_str())) .cloned() .collect(); let mut want: HashSet = base_parts.iter().cloned().collect(); for p in &base_parts { for suf in ["_m", "_l", "_d"] { let c = format!("{p}{suf}"); if names.contains(&c) { want.insert(c); } } } let models = Xbg7Model::models_named(&bytes, &want, &|| false); struct Part { base: String, vcounts: Vec, pos: Vec<[f32; 3]>, // union of variants } let mut parts: Vec = Vec::new(); for p in &base_parts { let mut vcounts = Vec::new(); let mut pos = Vec::new(); for cand in [p.clone(), format!("{p}_m"), format!("{p}_l"), format!("{p}_d")] { if let Some(m) = models.iter().find(|m| m.name == cand) { let mp: Vec<[f32; 3]> = m.meshes.iter().flat_map(|s| s.positions.iter().copied()).collect(); vcounts.push(mp.len() as u32); pos.extend(mp); } } parts.push(Part { base: p.clone(), vcounts, pos }); } // Validate a draw against a part (direct or X-mirrored), like ship_capture. let validate = |d: &CapturedDraw, p: &Part| -> Option { if !p.vcounts.contains(&d.vcount) || d.pos.is_empty() { return None; } let near = |a: &[f32; 3], b: &[f32; 3]| { (a[0] - b[0]).abs() <= 1e-2 && (a[1] - b[1]).abs() <= 1e-2 && (a[2] - b[2]).abs() <= 1e-2 }; let (mut direct, mut mirror) = (0usize, 0usize); for q in &d.pos { if p.pos.iter().any(|v| near(q, v)) { direct += 1; } let qm = [-q[0], q[1], q[2]]; if p.pos.iter().any(|v| near(&qm, v)) { mirror += 1; } } let need = d.pos.len().div_ceil(2); if direct >= need && direct >= mirror { Some(false) } else if mirror >= need { Some(true) } else { None } }; // Collect rel samples per part across all logs. let mut samples: std::collections::BTreeMap> = Default::default(); for log in &a[3..] { let text = std::fs::read_to_string(log).unwrap(); let draws = parse_capture(&text); // label validated draws let mut labeled: Vec<(usize, usize, bool)> = Vec::new(); // (draw, part, mirrored) for (di, d) in draws.iter().enumerate() { for (pi, p) in parts.iter().enumerate() { if let Some(mir) = validate(d, p) { labeled.push((di, pi, mir)); break; } } } let refs: Vec = labeled .iter() .filter(|(_, pi, _)| parts[*pi].base.ends_with("bdy_04")) .map(|(di, ..)| *di) .collect(); for &ri in &refs { let rf = &draws[ri]; let rt = transpose(&rf.r); for &(di, pi, _mir) in &labeled { let d = &draws[di]; let dt = [d.t[0] - rf.t[0], d.t[1] - rf.t[1], d.t[2] - rf.t[2]]; let rel = mv(&rt, dt); if rel.iter().map(|v| v * v).sum::().sqrt() < 2600.0 { // also require the rel rotation be finite-sane let _rm = mmul(&rt, &d.r); samples.entry(parts[pi].base.clone()).or_default().push(rel); } } } eprintln!("{log}: {} validated draws, {} bdy_04 refs", labeled.len(), refs.len()); } // Cluster per part (greedy, 8-unit radius), print clusters with ≥3 samples. println!("\n== dominant ship-relative placements (clusters ≥3 samples) =="); for (part, mut v) in samples { v.sort_by(|x, y| x[0].partial_cmp(&y[0]).unwrap()); let mut clusters: Vec<([f64; 3], usize)> = Vec::new(); for s in &v { match clusters.iter_mut().find(|(c, _)| { (c[0] - s[0]).abs() < 8.0 && (c[1] - s[1]).abs() < 8.0 && (c[2] - s[2]).abs() < 8.0 }) { Some((c, n)) => { for k in 0..3 { c[k] = (c[k] * *n as f64 + s[k]) / (*n as f64 + 1.0); } *n += 1; } None => clusters.push((*s, 1)), } } clusters.sort_by(|x, y| y.1.cmp(&x.1)); let tops: Vec = clusters .iter() .filter(|(_, n)| *n >= 3) .take(6) .map(|(c, n)| format!("[{:7.1} {:7.1} {:7.1}]×{n}", c[0], c[1], c[2])) .collect(); println!(" {part:20} {}", tops.join(" ")); } // Static assembly rel bdy_04 for comparison. println!("\n== static assemble_ship rel bdy_04 =="); let placed = assemble_ship(&bytes, id, true); if let Some(r4) = placed.iter().find(|p| p.resource == format!("{id}_bdy_04")) { for p in &placed { println!( " {:20} [{:7.1} {:7.1} {:7.1}]", p.resource, p.t[0] - r4.t[0], p.t[1] - r4.t[1], p.t[2] - r4.t[2] ); } } }