ship_capture: bake e106 COMPLETE from the 2026-07-26 F10 capture — all 8 parts
The second F10 capture (ship at _m LOD distance) closes e106 entirely: all 8 parts placed including the bridge both earlier captures missed, cross-validating the doc's 7 known translations to 0.1 units and adding brg_01 at the exact centreline (0, 153.3, -164.0). Matching hardened by what the real capture taught us: - LOD-aware: each part tries every variant vcount (base/_m/_l/_d) — a distant ship draws its LOD copies, same local frame. - Position-validated, SET-based, against the UNION of a part's variants: buffer order != decode order, and one draw's vcount equalled the _m count while its buffer held the FULL 1633-vert geometry. A coincidental vcount (foreign 51-vert mesh vs the bridge) is rejected by geometry. - Runtime mirror handled: twins share one file geometry; the engine uploads the starboard copy X-reflected. Mirror-validated draws bake diag(-1,1,1) and the viewer reverses triangle winding for det<0 so front faces stay outward. - Near-axis rotation entries snapped to exact 0/+-1 for a clean table; eng_01 keeps its genuine 30-degree nacelle rotation. data/ship_placements.txt now ships e106 (viewer renders via the captured table); embedded_e106_is_complete locks the baked data. part_pos + capture_match helper examples added. 10 ship_capture tests; 80 formats-lib tests green; viewer builds. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -36,7 +36,7 @@ type M3 = [[f64; 3]; 3];
|
||||
|
||||
/// One captured draw's rigid **WorldView**: rotation rows `r` + view-space
|
||||
/// translation `t`, recovered from the `c0..c2` WVP constants.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct CapturedDraw {
|
||||
/// Guest vertex-buffer base address (the draw's identity for de-duping).
|
||||
pub vbase: u32,
|
||||
@@ -46,6 +46,25 @@ pub struct CapturedDraw {
|
||||
pub r: M3,
|
||||
/// WorldView view-space translation.
|
||||
pub t: [f64; 3],
|
||||
/// First few LOCAL vertex positions dumped with the draw (buffer order).
|
||||
/// Used to disambiguate same-vcount twins (mirrored port/starboard parts).
|
||||
pub pos: Vec<[f32; 3]>,
|
||||
}
|
||||
|
||||
/// A ship part to match against the capture. `part` is the **base** part name
|
||||
/// (what goes in the placement table); `vcount`/`ref_pos` come from whichever
|
||||
/// resource variant is being tried (base or an `_m`/`_l` LOD copy — a LOD is the
|
||||
/// same part in the same local frame, so its captured transform is the part's).
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct PartKey {
|
||||
/// Base part name for the table, e.g. `e106_bdy_01`.
|
||||
pub part: String,
|
||||
/// The tried resource's vertex count (the draw match key).
|
||||
pub vcount: u32,
|
||||
/// The tried resource's decoded positions (any order — validation is
|
||||
/// set-based), used to validate a vcount hit and to route mirrored twins.
|
||||
/// Empty = match by vcount alone.
|
||||
pub ref_pos: Vec<[f32; 3]>,
|
||||
}
|
||||
|
||||
/// A part's ship-relative rigid placement (in the reference part's frame).
|
||||
@@ -79,9 +98,15 @@ pub fn parse_capture(text: &str) -> Vec<CapturedDraw> {
|
||||
let mut out = Vec::new();
|
||||
let mut vbase = 0u32;
|
||||
let mut vcount = 0u32;
|
||||
let mut pos: Vec<[f32; 3]> = Vec::new();
|
||||
let mut consts: Vec<(usize, [f64; 4])> = Vec::new();
|
||||
|
||||
let flush = |vbase: u32, vcount: u32, consts: &[(usize, [f64; 4])], out: &mut Vec<CapturedDraw>| {
|
||||
let flush = |vbase: u32,
|
||||
vcount: u32,
|
||||
pos: &mut Vec<[f32; 3]>,
|
||||
consts: &[(usize, [f64; 4])],
|
||||
out: &mut Vec<CapturedDraw>| {
|
||||
let pos = std::mem::take(pos);
|
||||
if vbase == 0 {
|
||||
return;
|
||||
}
|
||||
@@ -90,18 +115,20 @@ pub fn parse_capture(text: &str) -> Vec<CapturedDraw> {
|
||||
return; // no WorldView for this draw — skip it
|
||||
};
|
||||
if let Some((r, t)) = normalize_wvp([c0, c1, c2]) {
|
||||
out.push(CapturedDraw { vbase, vcount, r, t });
|
||||
out.push(CapturedDraw { vbase, vcount, r, t, pos });
|
||||
}
|
||||
};
|
||||
|
||||
for line in text.lines() {
|
||||
let l = line.trim();
|
||||
if let Some(rest) = l.strip_prefix("DRAW ") {
|
||||
flush(vbase, vcount, &consts, &mut out);
|
||||
flush(vbase, vcount, &mut pos, &consts, &mut out);
|
||||
consts.clear();
|
||||
let f = |k: &str| rest.split_whitespace().find_map(|t| t.strip_prefix(k));
|
||||
vbase = f("vbase=0x").and_then(|s| u32::from_str_radix(s, 16).ok()).unwrap_or(0);
|
||||
vcount = f("vcount=").and_then(|s| s.parse().ok()).unwrap_or(0);
|
||||
} else if l.starts_with("pos:") || l.starts_with("positions:") {
|
||||
pos = parse_pos_line(l, 8);
|
||||
} else if l.starts_with("vsconst") {
|
||||
for cap in l.split('c').skip(1) {
|
||||
let Some((idx, rest)) = cap.split_once('=') else { continue };
|
||||
@@ -120,7 +147,23 @@ pub fn parse_capture(text: &str) -> Vec<CapturedDraw> {
|
||||
}
|
||||
}
|
||||
}
|
||||
flush(vbase, vcount, &consts, &mut out);
|
||||
flush(vbase, vcount, &mut pos, &consts, &mut out);
|
||||
out
|
||||
}
|
||||
|
||||
/// Parse a `pos: (x,y,z) (x,y,z) …` dump line into up to `max` positions.
|
||||
fn parse_pos_line(l: &str, max: usize) -> Vec<[f32; 3]> {
|
||||
let mut out = Vec::new();
|
||||
for group in l.split('(').skip(1) {
|
||||
let Some(inner) = group.split(')').next() else { continue };
|
||||
let nums: Vec<f32> = inner.split(',').filter_map(|x| x.trim().parse().ok()).collect();
|
||||
if nums.len() == 3 {
|
||||
out.push([nums[0], nums[1], nums[2]]);
|
||||
if out.len() >= max {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
@@ -147,33 +190,38 @@ pub fn parse_drawlog(text: &str) -> Vec<CapturedDraw> {
|
||||
let mut base = 0u32;
|
||||
let mut stride = 0u32;
|
||||
let mut size = 0u32;
|
||||
let mut pos: Vec<[f32; 3]> = Vec::new();
|
||||
let mut consts: Vec<(usize, [f64; 4])> = Vec::new();
|
||||
|
||||
let mut flush = |base: u32,
|
||||
size: u32,
|
||||
stride: u32,
|
||||
pos: &mut Vec<[f32; 3]>,
|
||||
consts: &[(usize, [f64; 4])],
|
||||
seen: &mut std::collections::HashSet<u32>,
|
||||
out: &mut Vec<CapturedDraw>| {
|
||||
let pos = std::mem::take(pos);
|
||||
if base == 0 || stride == 0 || !seen.insert(base) {
|
||||
return;
|
||||
}
|
||||
let get = |i: usize| consts.iter().find(|(k, _)| *k == i).map(|(_, v)| *v);
|
||||
let (Some(c0), Some(c1), Some(c2)) = (get(0), get(1), get(2)) else { return };
|
||||
if let Some((r, t)) = normalize_wvp([c0, c1, c2]) {
|
||||
out.push(CapturedDraw { vbase: base, vcount: size / stride, r, t });
|
||||
out.push(CapturedDraw { vbase: base, vcount: size / stride, r, t, pos });
|
||||
}
|
||||
};
|
||||
|
||||
for line in text.lines() {
|
||||
let l = line.trim();
|
||||
if let Some(rest) = l.strip_prefix("DRAW ") {
|
||||
flush(base, size, stride, &consts, &mut seen, &mut out);
|
||||
flush(base, size, stride, &mut pos, &consts, &mut seen, &mut out);
|
||||
consts.clear();
|
||||
base = 0;
|
||||
stride = 0;
|
||||
size = 0;
|
||||
is_ship = rest.contains(&format!("vs={SHIP_VS_HASH}"));
|
||||
} else if is_ship && l.starts_with("positions:") {
|
||||
pos = parse_pos_line(l, 8);
|
||||
} else if is_ship && l.starts_with("stream ") {
|
||||
let f = |k: &str| l.split_whitespace().find_map(|t| t.strip_prefix(k));
|
||||
if let Some(b) = f("base=0x").and_then(|s| u32::from_str_radix(s, 16).ok()) {
|
||||
@@ -192,7 +240,7 @@ pub fn parse_drawlog(text: &str) -> Vec<CapturedDraw> {
|
||||
}
|
||||
}
|
||||
}
|
||||
flush(base, size, stride, &consts, &mut seen, &mut out);
|
||||
flush(base, size, stride, &mut pos, &consts, &mut seen, &mut out);
|
||||
out
|
||||
}
|
||||
|
||||
@@ -213,42 +261,105 @@ fn normalize_wvp(rows: [[f64; 4]; 3]) -> Option<(M3, [f64; 3])> {
|
||||
Some((r, t))
|
||||
}
|
||||
|
||||
/// Correlate captured draws to a ship's base parts and express each in the
|
||||
/// reference part's frame.
|
||||
/// Validate a vcount hit by the draw's dumped positions against the part's
|
||||
/// decoded position SET (order-independent — decode order can differ from buffer
|
||||
/// order). Returns `Some((hits, mirrored))` when at least half the dumped
|
||||
/// positions are found among the part's vertices, either directly or **all
|
||||
/// X-negated** — the engine uploads the second of a mirrored port/starboard pair
|
||||
/// as an X-reflection of the shared file geometry, so the guest buffer disagrees
|
||||
/// in X sign with every decoded copy. `None` = the dump belongs to a different
|
||||
/// model (a coincidental vcount).
|
||||
fn pos_validate(draw: &CapturedDraw, ref_pos: &[[f32; 3]]) -> Option<(usize, bool)> {
|
||||
if draw.pos.is_empty() || ref_pos.is_empty() {
|
||||
return Some((0, false)); // no data to validate with — accept neutrally
|
||||
}
|
||||
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 = 0usize;
|
||||
let mut mirror = 0usize;
|
||||
for p in &draw.pos {
|
||||
if ref_pos.iter().any(|v| near(p, v)) {
|
||||
direct += 1;
|
||||
}
|
||||
let pm = [-p[0], p[1], p[2]];
|
||||
if ref_pos.iter().any(|v| near(&pm, v)) {
|
||||
mirror += 1;
|
||||
}
|
||||
}
|
||||
let need = draw.pos.len().div_ceil(2);
|
||||
if direct >= need && direct >= mirror {
|
||||
Some((direct, false))
|
||||
} else if mirror >= need {
|
||||
Some((mirror, true))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Correlate captured draws to a ship's parts and express each in the reference
|
||||
/// part's frame.
|
||||
///
|
||||
/// `parts` is `(resource_name, vertex_count)` for the ship's base parts (from the
|
||||
/// XBG7 decoder); vertex count is the match key. `ref_sub` selects the reference
|
||||
/// part by substring (e.g. `bdy_04`); the first matched part is used if none
|
||||
/// contains it. Parts with no matching captured draw (culled at that camera
|
||||
/// angle) are omitted. Returns `None` if nothing matched.
|
||||
/// Each [`PartKey`]'s `vcount` is the match key; when several draws share the
|
||||
/// vcount (mirrored port/starboard twins) the draw whose dumped positions match
|
||||
/// the part's `ref_pos` validates best is chosen. `ref_sub` selects the reference part by
|
||||
/// substring (e.g. `bdy_04`); the first matched part is used if none contains it.
|
||||
/// Parts with no matching captured draw (culled at that camera angle) are
|
||||
/// omitted. Returns `None` if nothing matched.
|
||||
pub fn correlate(
|
||||
id: &str,
|
||||
draws: &[CapturedDraw],
|
||||
parts: &[(String, u32)],
|
||||
parts: &[PartKey],
|
||||
ref_sub: &str,
|
||||
) -> Option<ShipPlacement> {
|
||||
// Match each part to a distinct captured draw by vertex count.
|
||||
let mut matched: Vec<(String, M3, [f64; 3])> = Vec::new();
|
||||
let mut matched: Vec<(String, M3, [f64; 3], bool)> = Vec::new();
|
||||
let mut used: std::collections::HashSet<u32> = std::collections::HashSet::new();
|
||||
for (part, vc) in parts {
|
||||
if let Some(d) = draws.iter().find(|d| d.vcount == *vc && !used.contains(&d.vbase)) {
|
||||
for key in parts {
|
||||
// Several PartKeys may carry the same part (one per LOD-variant vcount);
|
||||
// the first that validates wins, the rest are skipped.
|
||||
if matched.iter().any(|(p, ..)| p == &key.part) {
|
||||
continue;
|
||||
}
|
||||
// A vcount hit alone can be a coincidence (small LODs share counts across
|
||||
// unrelated models — a 51-vert draw once matched the bridge but was a
|
||||
// different mesh). Candidates failing position validation are REJECTED;
|
||||
// among validated candidates the best hit count wins (routes twins), and
|
||||
// a mirror-validated match records the X-reflection.
|
||||
let mut best: Option<(&CapturedDraw, usize, bool)> = None;
|
||||
for d in draws.iter().filter(|d| d.vcount == key.vcount && !used.contains(&d.vbase)) {
|
||||
let Some((score, mirrored)) = pos_validate(d, &key.ref_pos) else {
|
||||
continue; // positions disagree — not this part
|
||||
};
|
||||
if best.map_or(true, |(_, s, _)| score > s) {
|
||||
best = Some((d, score, mirrored));
|
||||
}
|
||||
}
|
||||
if let Some((d, _, mirrored)) = best {
|
||||
used.insert(d.vbase);
|
||||
matched.push((part.clone(), d.r, d.t));
|
||||
matched.push((key.part.clone(), d.r, d.t, mirrored));
|
||||
}
|
||||
}
|
||||
let ref_idx = matched.iter().position(|(p, ..)| p.contains(ref_sub)).unwrap_or(0);
|
||||
let (ref_part, ref_r, ref_t) = matched.get(ref_idx)?.clone();
|
||||
let (ref_part, ref_r, ref_t, _) = matched.get(ref_idx)?.clone();
|
||||
let rt_ref = transpose(&ref_r);
|
||||
|
||||
let parts_out = matched
|
||||
.iter()
|
||||
.map(|(part, r, t)| {
|
||||
let rel_r = mmul(&rt_ref, r);
|
||||
.map(|(part, r, t, mirrored)| {
|
||||
let mut rel_r = mmul(&rt_ref, r);
|
||||
let dt = [t[0] - ref_t[0], t[1] - ref_t[1], t[2] - ref_t[2]];
|
||||
let rel_t = mat_vec(&rt_ref, dt);
|
||||
// The captured WorldView transforms the *uploaded* buffer; for the
|
||||
// mirrored twin that buffer is the X-reflection of the file geometry,
|
||||
// so the file-local placement is R·diag(−1,1,1) — negate column 0.
|
||||
if *mirrored {
|
||||
for row in &mut rel_r {
|
||||
row[0] = -row[0];
|
||||
}
|
||||
}
|
||||
PartPlacement {
|
||||
part: part.clone(),
|
||||
m: m3_to_f32(&rel_r),
|
||||
m: snap_m3(&rel_r),
|
||||
t: [rel_t[0] as f32, rel_t[1] as f32, rel_t[2] as f32],
|
||||
}
|
||||
})
|
||||
@@ -256,6 +367,27 @@ pub fn correlate(
|
||||
Some(ShipPlacement { id: id.to_string(), reference: ref_part, parts: parts_out })
|
||||
}
|
||||
|
||||
/// Snap near-axis rotation entries (float noise from the WV products) to exact
|
||||
/// 0/±1 so the checked-in table is clean; real rotations are untouched.
|
||||
fn snap_m3(m: &M3) -> [[f32; 3]; 3] {
|
||||
let snap = |v: f64| -> f32 {
|
||||
if v.abs() < 5e-4 {
|
||||
0.0
|
||||
} else if (v - 1.0).abs() < 5e-4 {
|
||||
1.0
|
||||
} else if (v + 1.0).abs() < 5e-4 {
|
||||
-1.0
|
||||
} else {
|
||||
v as f32
|
||||
}
|
||||
};
|
||||
[
|
||||
[snap(m[0][0]), snap(m[0][1]), snap(m[0][2])],
|
||||
[snap(m[1][0]), snap(m[1][1]), snap(m[1][2])],
|
||||
[snap(m[2][0]), snap(m[2][1]), snap(m[2][2])],
|
||||
]
|
||||
}
|
||||
|
||||
/// Convert a captured placement into viewer [`ScenePart`]s (rigid, unit scale).
|
||||
pub fn to_scene_parts(ship: &ShipPlacement) -> Vec<ScenePart> {
|
||||
ship.parts
|
||||
@@ -359,13 +491,6 @@ fn mat_vec(m: &M3, v: [f64; 3]) -> [f64; 3] {
|
||||
m[2][0] * v[0] + m[2][1] * v[1] + m[2][2] * v[2],
|
||||
]
|
||||
}
|
||||
fn m3_to_f32(m: &M3) -> [[f32; 3]; 3] {
|
||||
[
|
||||
[m[0][0] as f32, m[0][1] as f32, m[0][2] as f32],
|
||||
[m[1][0] as f32, m[1][1] as f32, m[1][2] as f32],
|
||||
[m[2][0] as f32, m[2][1] as f32, m[2][2] as f32],
|
||||
]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
@@ -381,6 +506,10 @@ mod tests {
|
||||
)
|
||||
}
|
||||
|
||||
fn key(part: &str, vcount: u32) -> PartKey {
|
||||
PartKey { part: part.to_string(), vcount, ref_pos: Vec::new() }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_recovers_worldview() {
|
||||
let log = draw_line(0x1000, 3, [10.0, 0.0, 0.0]);
|
||||
@@ -412,7 +541,7 @@ mod tests {
|
||||
draw_line(0x2000, 4, [10.0, 0.0, 50.0])
|
||||
);
|
||||
let draws = parse_capture(&log);
|
||||
let parts = vec![("e106_bdy_04".to_string(), 3), ("e106_eng_01".to_string(), 4)];
|
||||
let parts = vec![key("e106_bdy_04", 3), key("e106_eng_01", 4)];
|
||||
let ship = correlate("e106", &draws, &parts, "bdy_04").unwrap();
|
||||
assert_eq!(ship.reference, "e106_bdy_04");
|
||||
let refp = ship.parts.iter().find(|p| p.part == "e106_bdy_04").unwrap();
|
||||
@@ -442,12 +571,79 @@ mod tests {
|
||||
assert_eq!(draws[0].vcount, 1633); // 9798/6
|
||||
assert_eq!(draws[1].vcount, 815); // 4890/6
|
||||
// Correlate: part B sits 50 along Z from reference part A.
|
||||
let parts = vec![("e106_bdy_04".to_string(), 1633), ("e106_bdy_03".to_string(), 815)];
|
||||
let parts = vec![key("e106_bdy_04", 1633), key("e106_bdy_03", 815)];
|
||||
let ship = correlate("e106", &draws, &parts, "bdy_04").unwrap();
|
||||
let b = ship.parts.iter().find(|p| p.part == "e106_bdy_03").unwrap();
|
||||
assert_eq!(b.t, [0.0, 0.0, 50.0]);
|
||||
}
|
||||
|
||||
/// The real e106 bdy_01/bdy_02 case: both twin resources decode to
|
||||
/// IDENTICAL file geometry; the engine uploads the second instance as an
|
||||
/// X-reflection, so one captured buffer disagrees in X sign with the file.
|
||||
/// Both draws must be placed, and the mirrored one must bake the X-flip
|
||||
/// (negated first matrix column) so file-local geometry lands port-side.
|
||||
#[test]
|
||||
fn runtime_mirrored_twin_placed_with_reflection() {
|
||||
let log = "DRAW vbase=0x1000 stride=24 vcount=426 indices=21 prim=4 vs=0x1\n \
|
||||
pos: (134.4215,133.8319,-118.1757) (178.8384,85.3847,238.0463)\n \
|
||||
vsconst base=0: c0=(1,0,0,264) c1=(0,1,0,0) c2=(0,0,1,0)\n\
|
||||
DRAW vbase=0x2000 stride=24 vcount=426 indices=21 prim=4 vs=0x1\n \
|
||||
pos: (-134.4215,133.8319,-118.1757) (-178.8384,85.3847,238.0463)\n \
|
||||
vsconst base=0: c0=(1,0,0,-264) c1=(0,1,0,0) c2=(0,0,1,0)\n\
|
||||
DRAW vbase=0x3000 stride=24 vcount=558 indices=9 prim=4 vs=0x1\n \
|
||||
vsconst base=0: c0=(1,0,0,0) c1=(0,1,0,0) c2=(0,0,1,0)\n";
|
||||
let draws = parse_capture(log);
|
||||
assert_eq!(draws.len(), 3);
|
||||
assert_eq!(draws[0].pos.len(), 2);
|
||||
// Both twins carry the SAME (file) positions — +X side geometry.
|
||||
let file_pos = vec![[134.4215, 133.8319, -118.1757], [178.8384, 85.3847, 238.0463]];
|
||||
let parts = vec![
|
||||
PartKey { part: "e106_bdy_01".to_string(), vcount: 426, ref_pos: file_pos.clone() },
|
||||
PartKey { part: "e106_bdy_02".to_string(), vcount: 426, ref_pos: file_pos },
|
||||
key("e106_bdy_04", 558),
|
||||
];
|
||||
let ship = correlate("e106", &draws, &parts, "bdy_04").unwrap();
|
||||
assert_eq!(ship.parts.len(), 3, "both twins + reference placed");
|
||||
let get = |p: &str| ship.parts.iter().find(|x| x.part == p).unwrap().clone();
|
||||
// bdy_01 validated directly → the +264 draw, identity rotation.
|
||||
let a = get("e106_bdy_01");
|
||||
assert_eq!(a.t, [264.0, 0.0, 0.0]);
|
||||
assert_eq!(a.m[0][0], 1.0);
|
||||
// bdy_02 validated as the MIRROR → the −264 draw, X-flip baked in.
|
||||
let b = get("e106_bdy_02");
|
||||
assert_eq!(b.t, [-264.0, 0.0, 0.0]);
|
||||
assert_eq!(b.m[0][0], -1.0, "mirrored twin must negate the X column");
|
||||
assert_eq!(b.m[1][1], 1.0);
|
||||
}
|
||||
|
||||
/// A draw whose vcount matches but whose position dump disagrees must be
|
||||
/// REJECTED, not placed — the real false-positive: a foreign 51-vert model
|
||||
/// matched `e106_brg_01_l` by count alone and put the bridge 2 km off-hull.
|
||||
#[test]
|
||||
fn vcount_coincidence_rejected_by_positions() {
|
||||
let log = "DRAW vbase=0x1000 stride=24 vcount=51 indices=36 prim=4 vs=0x1\n \
|
||||
pos: (-0.0000,46.2359,-12.9454) (-0.0000,-6.1936,264.8687)\n \
|
||||
vsconst base=0: c0=(1,0,0,1975) c1=(0,1,0,0) c2=(0,0,1,0)\n\
|
||||
DRAW vbase=0x3000 stride=24 vcount=558 indices=9 prim=4 vs=0x1\n \
|
||||
vsconst base=0: c0=(1,0,0,0) c1=(0,1,0,0) c2=(0,0,1,0)\n";
|
||||
let draws = parse_capture(log);
|
||||
let parts = vec![
|
||||
PartKey {
|
||||
part: "e106_brg_01".to_string(),
|
||||
vcount: 51,
|
||||
// The REAL bridge LOD's vertices — disagree with the dump.
|
||||
ref_pos: vec![[35.2480, 26.0376, 49.2504], [6.0, 55.9632, 41.1370]],
|
||||
},
|
||||
key("e106_bdy_04", 558),
|
||||
];
|
||||
let ship = correlate("e106", &draws, &parts, "bdy_04").unwrap();
|
||||
assert!(
|
||||
!ship.parts.iter().any(|p| p.part == "e106_brg_01"),
|
||||
"coincidental vcount match must not place the bridge"
|
||||
);
|
||||
assert_eq!(ship.parts.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_round_trips() {
|
||||
let ship = ShipPlacement {
|
||||
@@ -476,4 +672,23 @@ mod tests {
|
||||
// The checked-in data file must always parse (even if empty).
|
||||
let _ = parse_table(EMBEDDED_TABLE);
|
||||
}
|
||||
|
||||
/// The baked e106 capture (2026-07-26 F10, Stage_S01): all 8 parts placed,
|
||||
/// port/starboard pair symmetric with the mirror on bdy_02, bridge on the
|
||||
/// centreline. Guards the checked-in data against accidental edits.
|
||||
#[test]
|
||||
fn embedded_e106_is_complete() {
|
||||
let e106 = embedded_placement("e106").expect("e106 baked in");
|
||||
assert_eq!(e106.reference, "e106_bdy_04");
|
||||
assert_eq!(e106.parts.len(), 8, "all 8 e106 parts placed");
|
||||
let get = |p: &str| e106.parts.iter().find(|x| x.part == p).unwrap();
|
||||
// Port/starboard hull pair: X = ∓264, the starboard copy mirrored.
|
||||
assert!((get("e106_bdy_01").t[0] + 264.0).abs() < 0.1);
|
||||
assert!((get("e106_bdy_02").t[0] - 264.0).abs() < 0.1);
|
||||
assert_eq!(get("e106_bdy_02").m[0][0], -1.0);
|
||||
assert_eq!(get("e106_bdy_01").m[0][0], 1.0);
|
||||
// Bridge: centreline, above and aft of the hull reference.
|
||||
let brg = get("e106_brg_01");
|
||||
assert!(brg.t[0].abs() < 0.1 && brg.t[1] > 150.0 && brg.t[2] < -160.0);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user