fix(acis): place a lifted solid where its body transform says
Solids in a BIM export collapsed onto the origin — every one the right shape, all in the same place. ACIS keeps a body's geometry in its own local frame and records where it sits in a separate `transform` record. Both paths that drew ACIS before this applied it; the kernel path I put in their place did not, so every solid drew at its local origin. A file authored in absolute coordinates carries no transform and looked fine, which is why it took a placed one to show. The mesh now goes through `finalize_mesh`, which already applied the placement for the fallback sampler and already split positions into the coarse and fine pair. That fixes the fault and deletes the hand-rolled copy of the splitting next to it. The convention is the part worth testing and the part that invites a mistake: ACIS treats points as row vectors, `p' = scale·(p·M) + T`, so the stored 3×3 is indexed transposed from a column-vector multiply and the translation is not scaled. Getting the first wrong mirrors a solid instead of moving it; the second puts it at twice its offset, which reads as a plausible position and is the wrong one. Both are covered. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
273f482aee
commit
c668b2f665
1 changed files with 81 additions and 23 deletions
|
|
@ -21,7 +21,8 @@ use acadrust::acis::lift;
|
||||||
use acadrust::entities::acis::SatDocument;
|
use acadrust::entities::acis::SatDocument;
|
||||||
use acadrust::kernel::brep;
|
use acadrust::kernel::brep;
|
||||||
|
|
||||||
use crate::scene::model::mesh_model::{MeshLodSet, MeshModel};
|
use crate::scene::convert::solid3d_tess::{body_transform, finalize_mesh};
|
||||||
|
use crate::scene::model::mesh_model::MeshLodSet;
|
||||||
|
|
||||||
/// How far a triangle may sit from the surface it lies on.
|
/// How far a triangle may sit from the surface it lies on.
|
||||||
const SAG: f64 = 0.05;
|
const SAG: f64 = 0.05;
|
||||||
|
|
@ -46,6 +47,8 @@ pub fn tessellate_sat(
|
||||||
}
|
}
|
||||||
let sag = if sag > 0.0 { sag } else { SAG };
|
let sag = if sag > 0.0 { sag } else { SAG };
|
||||||
|
|
||||||
|
// Positions stay f64 until `finalize_mesh` splits them into the coarse
|
||||||
|
// and fine pair, so a solid at survey coordinates keeps its millimetres.
|
||||||
let mut positions: Vec<[f64; 3]> = Vec::new();
|
let mut positions: Vec<[f64; 3]> = Vec::new();
|
||||||
let mut normals: Vec<[f32; 3]> = Vec::new();
|
let mut normals: Vec<[f32; 3]> = Vec::new();
|
||||||
let mut indices: Vec<u32> = Vec::new();
|
let mut indices: Vec<u32> = Vec::new();
|
||||||
|
|
@ -77,32 +80,20 @@ pub fn tessellate_sat(
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The renderer holds each position as a coarse float plus a fine
|
// ACIS keeps a body's geometry in its own local frame and records where it
|
||||||
// correction, so a solid at survey coordinates keeps its last millimetres
|
// sits in a separate `transform` record. Skipping that leaves every solid
|
||||||
// instead of losing them to f32.
|
// stacked at the origin — which is what a BIM file looks like when each
|
||||||
let mut verts = Vec::with_capacity(positions.len());
|
// component is placed rather than authored in world coordinates.
|
||||||
let mut verts_low = Vec::with_capacity(positions.len());
|
let mut set = MeshLodSet::from_single(finalize_mesh(
|
||||||
for point in &positions {
|
|
||||||
let high = [point[0] as f32, point[1] as f32, point[2] as f32];
|
|
||||||
verts.push(high);
|
|
||||||
verts_low.push([
|
|
||||||
(point[0] - high[0] as f64) as f32,
|
|
||||||
(point[1] - high[1] as f64) as f32,
|
|
||||||
(point[2] - high[2] as f64) as f32,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut set = MeshLodSet::from_single(MeshModel {
|
|
||||||
name,
|
name,
|
||||||
verts,
|
positions,
|
||||||
verts_low,
|
|
||||||
normals,
|
normals,
|
||||||
indices,
|
indices,
|
||||||
triangle_material_handles: Vec::new(),
|
Vec::new(),
|
||||||
triangle_colors: Vec::new(),
|
Vec::new(),
|
||||||
color,
|
color,
|
||||||
selected: false,
|
body_transform(document),
|
||||||
});
|
));
|
||||||
set.complete = loss.is_empty() && undrawn == 0;
|
set.complete = loss.is_empty() && undrawn == 0;
|
||||||
Some(set)
|
Some(set)
|
||||||
}
|
}
|
||||||
|
|
@ -121,8 +112,75 @@ pub fn tessellate_sat(
|
||||||
pub fn edge_polylines(document: &SatDocument, sag: f64) -> Vec<Vec<[f64; 3]>> {
|
pub fn edge_polylines(document: &SatDocument, sag: f64) -> Vec<Vec<[f64; 3]>> {
|
||||||
let (bodies, _) = lift(document);
|
let (bodies, _) = lift(document);
|
||||||
let sag = if sag > 0.0 { sag } else { SAG };
|
let sag = if sag > 0.0 { sag } else { SAG };
|
||||||
|
let placement = body_transform(document);
|
||||||
bodies
|
bodies
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|body| brep::edge_polylines(body, sag))
|
.flat_map(|body| brep::edge_polylines(body, sag))
|
||||||
|
.map(|polyline| {
|
||||||
|
polyline
|
||||||
|
.into_iter()
|
||||||
|
.map(|point| placed(point, placement))
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A body-local point moved to where the body sits.
|
||||||
|
///
|
||||||
|
/// ACIS treats points as row vectors — `p' = scale·(p·M) + T` — so the stored
|
||||||
|
/// 3×3 is indexed transposed from a column-vector multiply. Getting that the
|
||||||
|
/// wrong way round mirrors a placed solid rather than moving it.
|
||||||
|
fn placed(point: [f64; 3], xform: Option<([f64; 9], [f64; 3], f64)>) -> [f64; 3] {
|
||||||
|
let Some((m, translation, scale)) = xform else {
|
||||||
|
return point;
|
||||||
|
};
|
||||||
|
let [x, y, z] = point;
|
||||||
|
[
|
||||||
|
scale * (x * m[0] + y * m[3] + z * m[6]) + translation[0],
|
||||||
|
scale * (x * m[1] + y * m[4] + z * m[7]) + translation[1],
|
||||||
|
scale * (x * m[2] + y * m[5] + z * m[8]) + translation[2],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// A quarter turn about z, written the way ACIS writes it: row-major, and
|
||||||
|
/// applied to points as row vectors.
|
||||||
|
fn quarter_turn() -> Option<([f64; 9], [f64; 3], f64)> {
|
||||||
|
Some((
|
||||||
|
[0.0, 1.0, 0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 1.0],
|
||||||
|
[10.0, 20.0, 30.0],
|
||||||
|
2.0,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_placement_turns_the_way_acis_means_it_to() {
|
||||||
|
// Deliberately asymmetric: a transposed multiply turns the other way,
|
||||||
|
// so this catches the one mistake the convention invites.
|
||||||
|
let moved = placed([1.0, 0.0, 0.0], quarter_turn());
|
||||||
|
assert!((moved[0] - 10.0).abs() < 1e-12, "{moved:?}");
|
||||||
|
assert!((moved[1] - 22.0).abs() < 1e-12, "{moved:?}");
|
||||||
|
assert!((moved[2] - 30.0).abs() < 1e-12, "{moved:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_body_with_no_transform_stays_where_it_is() {
|
||||||
|
// Many solids store absolute geometry and carry no transform record.
|
||||||
|
// Treating that as anything but identity moves them off their own
|
||||||
|
// coordinates.
|
||||||
|
let point = [3.0, -4.0, 5.0];
|
||||||
|
assert_eq!(placed(point, None), point);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_scale_reaches_the_translation_only_once() {
|
||||||
|
// `p' = scale·(p·M) + T`: the translation is not scaled. Folding the
|
||||||
|
// scale into it as well puts a placed solid at twice its offset,
|
||||||
|
// which reads as a plausible position and is the wrong one.
|
||||||
|
let moved = placed([0.0, 0.0, 0.0], quarter_turn());
|
||||||
|
assert_eq!(moved, [10.0, 20.0, 30.0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue