ship: STATIC assembly is now EXACT — node-TRS off-by-one cracked via the capture

The runtime capture served its true purpose: as the answer key that exposed the
static encoding. The XBG7 node joint table (node+0x44) holds NINE keyframe
channels [TX TY TZ RY RX RZ SX SY SZ] behind EIGHT pointer slots shifted one
track forward: channel k+1 = f64@(ptr[k]+8), and channel 0 -- TX, which no
pointer names -- sits at ptr[0]-0x48 (tracks are 0x50-byte records, value at
+8). The old 8-slot read took TY as X, RY as Y (usually 0 -- why both hulls
stacked on the centreline) and never saw TX at all (the +-264 hull offsets).
Euler order is Ry(ch3)*Rx(ch4)*Rz(ch5) with angles applied directly, pinned by
the captured engine nacelle Rx(-15)*Rz(30) decomposition. mesh.rs gains
read_trs9/node_rotation; scene_world_nodes and node_transforms both fixed
(saber fin overrides unaffected).

assemble_ship rewritten fully static and exact:
- every composite-node instance placed (alias duplicates collapsed, real
  instances kept) including cross-id turret mounts (rou_e303_wep_01_root x2)
- the e_rou_<id>_eng cluster rig mounts at GN_Engine_01 (two mirrored nacelles
  +-131 with -+30-degree cant + centre engine) -- world = frame o rig_local
- brg/sld at their exact GN frames (frames were always exact)
- shared-geometry twin pairs at +-TX: X-reflect the instance whose offset
  opposes the geometry's dominant side (viewer reverses winding on det<0)
- squeezed node names resolve (wep02 -> wep_02_01)

Ground-truth gate: ship::tests::static_assembly_matches_runtime_capture asserts
static == the baked e106 capture for all 8 parts (T<1.0, R<0.02) plus both
nacelles, both turrets, and the starboard-hull mirror. Viewer now uses pure
static assembly; ship_capture + the baked table remain as the verification
oracle. Renders show a coherent, bilaterally-symmetric destroyer.

81 formats-lib + all disc tests green; viewer builds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-26 19:00:49 +02:00
parent 015e49ac8a
commit 737b61242e
5 changed files with 433 additions and 81 deletions

View File

@@ -3952,15 +3952,11 @@ fn build_ship_model(
};
let should_cancel = || cancel.load(Ordering::Relaxed);
// Placement: prefer a runtime-captured ground-truth table (exact, baked from
// an F10 ship capture) when this ship has one; otherwise fall back to the
// static scene-graph assembler (hull exact, external parts approximate — the
// `external` toggle). A captured table already includes every captured part,
// so the toggle doesn't apply to it.
let placed = match sylpheed_formats::ship_capture::embedded_placement(id) {
Some(cap) => sylpheed_formats::ship_capture::to_scene_parts(&cap),
None => sylpheed_formats::ship::assemble_ship(&bytes, id, external),
};
// Placement: fully static scene-graph assembly — exact, validated
// part-for-part (translations + rotations + instances + mirror) against the
// e106 runtime capture (`ship::tests::static_assembly_matches_runtime_capture`).
// The captured table in ship_capture stays as the verification oracle only.
let placed = sylpheed_formats::ship::assemble_ship(&bytes, id, external);
if placed.is_empty() {
return PreparedXpr::Info(format!("No parts found for {label}."));
}