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.
350 lines
10 KiB
Rust
350 lines
10 KiB
Rust
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<f64>,
|
|
isolines: usize,
|
|
) -> Option<MeshLodSet> {
|
|
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<Option<([f64; 9], [f64; 3], f64)>, ()> {
|
|
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::<f64>() 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<u32>,
|
|
triangle_material_handles: Vec<Option<acadrust::Handle>>,
|
|
triangle_colors: Vec<Option<[f32; 4]>>,
|
|
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<SatDocument>,
|
|
is_binary: bool,
|
|
sab_data: &[u8],
|
|
) -> Option<SatDocument> {
|
|
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<f64>,
|
|
isolines: usize,
|
|
acis: &acadrust::entities::AcisData,
|
|
) -> Option<MeshLodSet> {
|
|
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<f64>,
|
|
isolines: usize,
|
|
) -> Option<MeshLodSet> {
|
|
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<f64>,
|
|
isolines: usize,
|
|
) -> Option<MeshLodSet> {
|
|
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<f64>,
|
|
isolines: usize,
|
|
) -> Option<MeshLodSet> {
|
|
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<f64>,
|
|
isolines: usize,
|
|
) -> Option<MeshLodSet> {
|
|
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,
|
|
)
|
|
}
|