use acadrust::entities::acis::{SabReader, SatBody, SatDocument}; use acadrust::entities::{Body, Region, Solid3D}; use crate::scene::model::mesh_model::{MeshLodSet, MeshModel}; fn tessellate_acis( sat: &SatDocument, name: String, color: [f32; 4], facet_res: f64, chordal_deflection: Option, isolines: usize, ) -> Option { crate::scene::convert::acis_kernel::tessellate_sat( sat, name, color, facet_res, chordal_deflection, isolines, ) } pub(crate) fn body_transform( sat: &SatDocument, body_record: usize, ) -> Result, ()> { let body = SatBody::from_record(sat.record(body_record).ok_or(())?).ok_or(())?; let Some(transform_record) = body.transform().index() else { return Ok(None); }; let transform = sat.record(transform_record).ok_or(())?; if transform.entity_type != "transform" { return Err(()); } let mut values = Vec::with_capacity(13); for token in &transform.tokens { if values.len() >= 13 { break; } if let Some((components, len)) = token.coordinate_components() { 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 { break; }; values.push(value); if values.len() >= 13 { break; } } } } (values.len() >= 13 && values[..13].iter().all(|value| value.is_finite()) && values[12] > 0.0) .then(|| { ( [ values[0], values[1], values[2], values[3], values[4], values[5], values[6], values[7], values[8], ], [values[9], values[10], values[11]], values[12], ) }) .map(Some) .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]>, normals: Vec<[f32; 3]>, indices: Vec, triangle_material_handles: Vec>, triangle_colors: Vec>, color: [f32; 4], xform: Option<([f64; 9], [f64; 3], f64)>, ) -> MeshModel { let mut verts_high = Vec::with_capacity(verts.len()); let mut verts_low = Vec::with_capacity(verts.len()); for [x, y, z] in verts { let [x, y, z] = match xform { Some((matrix, translation, scale)) => [ scale * (x * matrix[0] + y * matrix[3] + z * matrix[6]) + translation[0], scale * (x * matrix[1] + y * matrix[4] + z * matrix[7]) + translation[1], scale * (x * matrix[2] + y * matrix[5] + z * matrix[8]) + translation[2], ], None => [x, y, z], }; let high = [x as f32, y as f32, z as f32]; verts_high.push(high); verts_low.push([ (x - high[0] as f64) as f32, (y - high[1] as f64) as f32, (z - high[2] as f64) as f32, ]); } let normals = match xform { Some((matrix, _, _)) => normals .iter() .map(|normal| { let [x, y, z] = [normal[0] as f64, normal[1] as f64, normal[2] as f64]; let transformed = [ x * matrix[0] + y * matrix[3] + z * matrix[6], x * matrix[1] + y * matrix[4] + z * matrix[7], x * matrix[2] + y * matrix[5] + z * matrix[8], ]; let length = (transformed[0] * transformed[0] + transformed[1] * transformed[1] + transformed[2] * transformed[2]) .sqrt(); if length > 1e-9 { [ (transformed[0] / length) as f32, (transformed[1] / length) as f32, (transformed[2] / length) as f32, ] } else { *normal } }) .collect(), None => normals, }; MeshModel { name, verts: verts_high, verts_low, normals, indices, triangle_material_handles, triangle_colors, color, selected: false, } } fn parse_acis( sat_fn: impl FnOnce() -> Option, is_binary: bool, sab_data: &[u8], ) -> Option { sat_fn().or_else(|| { (is_binary && !sab_data.is_empty()) .then(|| SabReader::read(sab_data).ok()) .flatten() }) } fn remap_acis_material_bindings( set: &mut MeshLodSet, acis: &acadrust::entities::AcisData, ) { for lod in &mut set.lods { for handle in lod.triangle_material_handles.iter_mut().flatten() { let reference = handle.value() as i32; if let Some(material_handle) = acis.materials.iter().find_map(|binding| { (binding.absolute_reference == reference || binding.array_index == reference) .then_some(binding.material_handle) .flatten() }) { *handle = material_handle; } } } } fn finish( sat: SatDocument, name: String, color: [f32; 4], facet_res: f64, chordal_deflection: Option, isolines: usize, acis: &acadrust::entities::AcisData, ) -> Option { let mut set = tessellate_acis( &sat, name, color, facet_res, chordal_deflection, isolines, )?; remap_acis_material_bindings(&mut set, acis); Some(set) } pub fn tessellate_region( region: &Region, color: [f32; 4], facet_res: f64, chordal_deflection: Option, isolines: usize, ) -> Option { let sat = parse_acis( || region.parse_sat(), region.acis_data.is_binary, ®ion.acis_data.sab_data, )?; finish( sat, region.common.handle.value().to_string(), color, facet_res, chordal_deflection, isolines, ®ion.acis_data, ) } pub fn tessellate_body( body: &Body, color: [f32; 4], facet_res: f64, chordal_deflection: Option, isolines: usize, ) -> Option { let sat = parse_acis( || body.parse_sat(), body.acis_data.is_binary, &body.acis_data.sab_data, )?; finish( sat, body.common.handle.value().to_string(), color, facet_res, chordal_deflection, isolines, &body.acis_data, ) } pub fn tessellate_surface( surface: &acadrust::entities::Surface, color: [f32; 4], facet_res: f64, chordal_deflection: Option, isolines: usize, ) -> Option { let sat = parse_acis( || surface.parse_sat(), surface.acis_data.is_binary, &surface.acis_data.sab_data, )?; finish( sat, surface.common.handle.value().to_string(), color, facet_res, chordal_deflection, isolines, &surface.acis_data, ) } pub fn tessellate_solid3d( solid: &Solid3D, color: [f32; 4], facet_res: f64, chordal_deflection: Option, isolines: usize, ) -> Option { let sat = parse_acis( || solid.parse_sat(), solid.acis_data.is_binary, &solid.acis_data.sab_data, )?; finish( sat, solid.common.handle.value().to_string(), color, facet_res, chordal_deflection, isolines, &solid.acis_data, ) }