From 4a61c54826e34ce9fe89dd0bbd32d2a451cf57d2 Mon Sep 17 00:00:00 2001 From: AI Assistant Date: Thu, 27 Aug 2026 17:00:04 +1000 Subject: [PATCH] Fix the 3D-solids save/reopen bug: body_transform() dropped SAB integer tokens Root cause of "reopen shows nothing, Zoom Extents finds nothing" for any solid saved to DWG and reopened: SabWriter round-trips exact transform values (0/1 in an identity matrix - the common case for a freshly drawn solid) as compact SAB integer tokens (tag 0x04), not doubles. body_transform()'s value-extraction loop only tried coordinate_components() -> as_float() -> as_string(), never as_integer(), so it silently parsed 0 of 13 values out of a perfectly valid transform record. tessellate_sat() then hit its own (already-instrumented) "no resolvable transform" bailout and aborted the whole document. Reproduced directly against a real saved/reopened file (a standalone harness against acadrust+cadkernel, bypassing the app's own broken native desktop build - unrelated pre-existing issues, not touched here): the transform record's tokens were confirmed to be Sab{tag:4,...}, and adding the as_integer() fallback resolved them to the expected identity matrix. Fix: fall back to token.as_integer() (cast to f64) alongside as_float() in body_transform(). Added regression coverage with a synthetic SAB-integer-encoded transform record. --- src/scene/convert/acis_kernel.rs | 1 - src/scene/convert/solid3d_tess.rs | 66 +++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) 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]>,