diff --git a/src/scene/convert/acis_kernel.rs b/src/scene/convert/acis_kernel.rs index 6ec12b7f..5b6a106e 100644 --- a/src/scene/convert/acis_kernel.rs +++ b/src/scene/convert/acis_kernel.rs @@ -289,5 +289,4 @@ mod tests { let moved = placed([0.0, 0.0, 0.0], quarter_turn()); assert_eq!(moved, [10.0, 20.0, 30.0]); } - } diff --git a/src/scene/convert/solid3d_tess.rs b/src/scene/convert/solid3d_tess.rs index d8c24af5..4729db5d 100644 --- a/src/scene/convert/solid3d_tess.rs +++ b/src/scene/convert/solid3d_tess.rs @@ -42,6 +42,16 @@ pub(crate) fn body_transform( values.extend_from_slice(&components[..len]); } else if let Some(value) = token.as_float() { values.push(value); + } else if let Some(value) = token.as_integer() { + // A SAB-encoded transform's exact values (0/1 in an identity + // matrix - the common case for a freshly drawn solid) round-trip + // as compact SAB integer tokens (tag 0x04), not doubles. Missing + // this fallback silently parsed 0 of 13 values on any such + // transform, which read as "no resolvable transform" and aborted + // tessellation of the whole document (CMMS incident, 2026-08-27: + // reproduced on a solid saved and reopened, body_transform() + // parsed a valid identity transform record down to zero values). + values.push(value as f64); } else if let Some(text) = token.as_string() { for word in text.split_ascii_whitespace() { let Ok(value) = word.parse::() else { @@ -71,6 +81,62 @@ pub(crate) fn body_transform( .ok_or(()) } +#[cfg(test)] +mod tests { + use super::*; + use acadrust::entities::acis::{SatPointer, SatRecord, SatToken}; + + fn body_record(index: i32, transform_index: i32) -> SatRecord { + let mut r = SatRecord::new(index, "body"); + r.tokens = vec![ + SatToken::Pointer(SatPointer::NULL), + SatToken::Pointer(SatPointer::NULL), + SatToken::Pointer(SatPointer::NULL), + SatToken::Pointer(SatPointer::new(transform_index)), + ]; + r + } + + /// A SAB-encoded transform round-trip through save/reopen writes its + /// exact 0/1 identity values as compact SAB integer tokens (tag 0x04), + /// not doubles - reproduces the "no resolvable transform" bug that + /// aborted tessellation of an entire document after any solid was saved + /// and reopened (CMMS incident, 2026-08-27). + fn sab_int_transform_record(index: i32) -> SatRecord { + let mut r = SatRecord::new(index, "transform"); + r.tokens.push(SatToken::Pointer(SatPointer::NULL)); + let values: [i32; 13] = [1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1]; + for v in values { + r.tokens.push(SatToken::Sab { + tag: 0x04, + data: v.to_le_bytes().to_vec(), + }); + } + r + } + + #[test] + fn resolves_a_sab_integer_encoded_identity_transform() { + let mut sat = SatDocument::new(); + sat.records.push(body_record(0, 1)); + sat.records.push(sab_int_transform_record(1)); + + let result = body_transform(&sat, 0).expect("should resolve, not error"); + let (matrix, translation, scale) = result.expect("should be Some, not identity-as-None"); + assert_eq!(matrix, [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]); + assert_eq!(translation, [0.0, 0.0, 0.0]); + assert_eq!(scale, 1.0); + } + + #[test] + fn a_body_with_a_null_transform_pointer_has_no_transform() { + let mut sat = SatDocument::new(); + sat.records.push(body_record(0, -1)); + + assert_eq!(body_transform(&sat, 0), Ok(None)); + } +} + pub(crate) fn finalize_mesh( name: String, verts: Vec<[f64; 3]>,