Files
OneCAD-Draft/src/scene/convert/acis_kernel.rs
T

262 lines
9.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Drawing an ACIS solid by lifting it into the geometry kernel.
//!
//! A DWG or DXF carries its solids as ACIS records — analytic surfaces and the
//! topology joining them — and the kernel holds exactly that. So the shortest
//! path from a file to a picture is to lift the document into a `Body` and ask
//! the kernel for triangles, rather than to re-derive each surface's extent by
//! sampling it.
//!
//! Lift and tessellation failures are reported as an incomplete result.
use cadkernel::acis::lift;
use acadrust::entities::acis::SatDocument;
use cadkernel::brep;
use crate::scene::convert::solid3d_tess::{body_transform, finalize_mesh};
use crate::scene::model::mesh_model::{CurvedGen, MeshLodSet};
const DEFAULT_FIT_TOLERANCE: f64 = 1e-6;
/// Tessellate an ACIS document through the kernel.
pub fn tessellate_sat(
document: &SatDocument,
name: String,
color: [f32; 4],
facet_res: f64,
chordal_deflection: Option<f64>,
isolines: [usize; 2],
planar_isolines: bool,
) -> Option<MeshLodSet> {
let (bodies, loss) = lift(document);
if bodies.is_empty() {
return None;
}
let mut placed_bodies = Vec::with_capacity(bodies.len());
for body in bodies {
let source = body.provenance.source()?;
let transform = body_transform(document, source.index() as usize).ok()?;
let placement_scale = transform.map_or(1.0, |(_, _, scale)| scale.abs());
let placed = if let Some((matrix, translation, scale)) = transform {
let placement = brep::Placement {
x_axis: [scale * matrix[0], scale * matrix[1], scale * matrix[2]],
y_axis: [scale * matrix[3], scale * matrix[4], scale * matrix[5]],
z_axis: [scale * matrix[6], scale * matrix[7], scale * matrix[8]],
origin: translation,
};
brep::transform(&body, &placement)?
} else {
body
};
placed_bodies.push((placed, placement_scale));
}
let bodies = placed_bodies;
let resolution = if facet_res.is_finite() && facet_res > 0.0 {
facet_res.clamp(0.01, 10.0)
} else {
1.0
};
let max_angle = chordal_deflection.map_or_else(
|| cadkernel::tessellation::angle_for_resolution(resolution),
|_| cadkernel::tessellation::display_angle_for_resolution(resolution),
);
// 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 normals: Vec<[f32; 3]> = Vec::new();
let mut indices: Vec<u32> = Vec::new();
let mut edges: Vec<[f64; 3]> = Vec::new();
let mut triangle_materials = Vec::new();
let mut triangle_colors = Vec::new();
let mut curved_gens = Vec::new();
let face_materials: std::collections::HashMap<i32, acadrust::Handle> = document
.records
.iter()
.filter(|record| record.entity_type == "material-adesk-attrib")
.filter_map(|record| {
let owner = record.token_pointer(2)?.0;
let handle = record.token(3)?.as_integer()?;
(owner >= 0 && handle > 0)
.then(|| (owner, acadrust::Handle::new(handle as u64)))
})
.collect();
let face_colors: std::collections::HashMap<i32, [f32; 4]> = document
.records
.iter()
.filter(|record| record.entity_type == "color-adesk-attrib")
.filter_map(|record| {
let owner = record.token_pointer(2)?.0;
let value = record.token(3)?.as_integer()?;
let source = if (1..=255).contains(&value) {
acadrust::Color::from_index(value as i16)
} else if value > 257 {
acadrust::Color::from_true_color_value(value as i32)
} else {
return None;
};
let mut rgba = crate::scene::convert::tess_util::aci_to_rgba(&source);
rgba[3] = color[3];
Some((owner, rgba))
})
.collect();
// A face the kernel holds but cannot express in its surface's own
// parameters leaves a hole, the same as one that never lifted — so both
// are counted before calling the mesh whole.
let mut undrawn = 0usize;
let source_fit = if document.header.spatial_resolution.is_finite()
&& document.header.spatial_resolution > 0.0
{
document.header.spatial_resolution
} else {
DEFAULT_FIT_TOLERANCE
};
for (body, placement_scale) in &bodies {
let mut tolerance = brep::mesh::TessellationTolerance::new(
max_angle,
source_fit * placement_scale,
)
.with_uv_isolines(isolines[0], isolines[1])
.with_planar_isolines(planar_isolines);
if let Some(deflection) = chordal_deflection {
tolerance = tolerance.with_chordal_deflection(deflection);
}
let tessellation = brep::mesh::tessellate(body, tolerance);
undrawn += tessellation.missing_faces.len();
for face in &tessellation.triangle_faces {
let record = body
.faces
.get(*face)
.and_then(|face| face.provenance.source())
.map(|source| source.index() as i32);
triangle_materials.push(record.and_then(|record| face_materials.get(&record).copied()));
triangle_colors.push(record.and_then(|record| face_colors.get(&record).copied()));
}
curved_gens.push(CurvedGen {
source: tessellation.silhouette_source(),
});
let base = positions.len() as u32;
positions.extend_from_slice(&tessellation.mesh.positions);
normals.extend(
tessellation
.mesh
.normals
.iter()
.map(|n| [n[0] as f32, n[1] as f32, n[2] as f32]),
);
indices.extend(tessellation.mesh.triangles.iter().flat_map(|triangle| {
[
base + triangle[0] as u32,
base + triangle[1] as u32,
base + triangle[2] as u32,
]
}));
for edge in tessellation.edges {
for segment in edge.positions.windows(2) {
edges.extend_from_slice(segment);
}
}
for isoline in tessellation.isolines {
for segment in isoline.positions.windows(2) {
edges.extend_from_slice(segment);
}
}
}
if indices.is_empty() {
return None;
}
// ACIS keeps a body's geometry in its own local frame and records where it
// sits in a separate `transform` record. Skipping that leaves every solid
// stacked at the origin — which is what a BIM file looks like when each
// component is placed rather than authored in world coordinates.
let mut set = MeshLodSet::from_single(finalize_mesh(
name,
positions,
normals,
indices,
triangle_materials,
triangle_colors,
color,
None,
));
if let [single] = bodies.as_slice() {
if let Some(properties) = brep::analytic_mass_properties(&single.0) {
set.apply_mass_properties(properties);
}
}
set.curved_gens = curved_gens;
for point in edges {
let high = [point[0] as f32, point[1] as f32, point[2] as f32];
set.edge_verts.push(high);
set.edge_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,
]);
}
set.complete = loss.is_empty() && undrawn == 0;
Some(set)
}
/// 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.
#[cfg(test)]
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]);
}
}