use super::GeometryRouter;
use crate::csg::{ClippingProcessor, Plane};
use crate::material_layer_index::{LayerAxis, LayerBuildup, LayerInfo};
use crate::mesh::{SubMesh, SubMeshCollection};
use crate::{Mesh, Point3, Result, Vector3};
use ifc_lite_core::{DecodedEntity, EntityDecoder, IfcType};
use nalgebra::Matrix4;
use rustc_hash::FxHashMap;
const MIN_SLICEABLE_THICKNESS_M: f64 = 0.002;
impl GeometryRouter {
pub(crate) fn try_layered_sub_meshes(
&self,
element: &DecodedEntity,
decoder: &mut EntityDecoder,
void_index: Option<&FxHashMap<u32, Vec<u32>>>,
) -> Option<SubMeshCollection> {
let index = self.material_layer_index()?;
let buildup = index.get(element.id)?;
if !buildup.is_sliceable() {
return None;
}
let empty: FxHashMap<u32, Vec<u32>> = FxHashMap::default();
let voids = void_index.unwrap_or(&empty);
let collection = self
.process_element_with_material_layers(element, decoder, buildup, voids)
.ok()
.flatten()?;
if collection.sub_meshes.len() < 2 {
return None;
}
let mut collection = collection;
for sub in &mut collection.sub_meshes {
sub.mesh.clean_degenerate();
}
Some(collection)
}
pub fn process_element_with_material_layers(
&self,
element: &DecodedEntity,
decoder: &mut EntityDecoder,
buildup: &LayerBuildup,
void_index: &FxHashMap<u32, Vec<u32>>,
) -> Result<Option<SubMeshCollection>> {
let (layers, axis, direction_sense, offset) = match buildup {
LayerBuildup::Sliceable {
layers,
axis,
direction_sense,
offset_from_reference_line,
} => (layers, *axis, *direction_sense, *offset_from_reference_line),
LayerBuildup::NotSliceable => return Ok(None),
};
if layers.len() < 2 {
return Ok(None);
}
if !element_is_single_unshifted_item(element, decoder) {
return Ok(None);
}
let visual_layers = merge_thin_layers(&layers, self.unit_scale);
if visual_layers.len() < 2 {
return Ok(None);
}
let base_mesh = self.process_element_with_voids(element, decoder, void_index)?;
if base_mesh.is_empty() {
return Ok(None);
}
let planes = match self.build_layer_planes(
element,
decoder,
&visual_layers,
axis,
direction_sense,
offset,
) {
Some(p) => p,
None => return Ok(None),
};
if planes.is_empty() {
return Ok(None);
}
Ok(Some(slice_mesh_into_layers(
&base_mesh,
&visual_layers,
&planes,
)))
}
fn build_layer_planes(
&self,
element: &DecodedEntity,
decoder: &mut EntityDecoder,
visual_layers: &[VisualLayer],
axis: LayerAxis,
direction_sense: f64,
offset: f64,
) -> Option<Vec<Plane>> {
let mut placement = self.get_placement_transform_from_element(element, decoder).ok()?;
self.scale_transform(&mut placement);
let scale = self.unit_scale;
let rtc = self.rtc_offset;
let axis_local = {
let v = axis.unit_vector();
Vector3::new(v[0], v[1], v[2])
};
let rotation = placement.fixed_view::<3, 3>(0, 0);
let world_normal = (rotation * axis_local)
.try_normalize(1e-12)?
* direction_sense;
let offset_m = offset * scale;
let mut planes = Vec::with_capacity(visual_layers.len().saturating_sub(1));
let mut cumulative_m = 0.0_f64;
for (i, layer) in visual_layers.iter().enumerate() {
cumulative_m += layer.thickness_m;
if i + 1 == visual_layers.len() {
break;
}
let d = offset_m + direction_sense * cumulative_m;
let local_origin = Point3::new(
axis_local.x * d,
axis_local.y * d,
axis_local.z * d,
);
let world_origin = placement.transform_point(&local_origin);
let rtc_origin = Point3::new(
world_origin.x - rtc.0,
world_origin.y - rtc.1,
world_origin.z - rtc.2,
);
planes.push(Plane::new(rtc_origin, world_normal));
}
Some(planes)
}
}
#[derive(Debug, Clone)]
pub(crate) struct VisualLayer {
pub(crate) material_id: u32,
pub(crate) thickness_m: f64,
}
pub(crate) fn merge_thin_layers(layers: &[LayerInfo], unit_scale: f64) -> Vec<VisualLayer> {
let thresh = MIN_SLICEABLE_THICKNESS_M;
let mut slabs: Vec<VisualLayer> = layers
.iter()
.map(|l| VisualLayer {
material_id: l.material_id,
thickness_m: l.thickness * unit_scale,
})
.collect();
loop {
if slabs.len() <= 1 {
break;
}
let mut victim: Option<usize> = None;
let mut victim_thickness = thresh;
for (i, s) in slabs.iter().enumerate() {
if s.thickness_m < victim_thickness {
victim = Some(i);
victim_thickness = s.thickness_m;
}
}
let Some(v) = victim else { break };
let prev = if v > 0 { Some(v - 1) } else { None };
let next = if v + 1 < slabs.len() {
Some(v + 1)
} else {
None
};
let target = match (prev, next) {
(Some(p), Some(n)) => {
if slabs[p].thickness_m >= slabs[n].thickness_m {
p
} else {
n
}
}
(Some(p), None) => p,
(None, Some(n)) => n,
(None, None) => break,
};
slabs[target].thickness_m += slabs[v].thickness_m;
slabs.remove(v);
}
slabs
}
fn element_is_single_unshifted_item(
element: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> bool {
let rep_attr = match element.get(6) {
Some(a) if !a.is_null() => a,
_ => return false,
};
let rep = match decoder.resolve_ref(rep_attr) {
Ok(Some(r)) => r,
_ => return false,
};
if rep.ifc_type != IfcType::IfcProductDefinitionShape {
return false;
}
let reps_attr = match rep.get(2) {
Some(a) => a,
None => return false,
};
let reps = match decoder.resolve_ref_list(reps_attr) {
Ok(r) => r,
Err(_) => return false,
};
for shape_rep in &reps {
if shape_rep.ifc_type != IfcType::IfcShapeRepresentation {
continue;
}
let is_body = shape_rep
.get(2)
.and_then(|a| a.as_string())
.map(|s| {
matches!(
s,
"Body"
| "SweptSolid"
| "SolidModel"
| "Brep"
| "CSG"
| "Clipping"
| "SurfaceModel"
| "Tessellation"
| "AdvancedSweptSolid"
| "AdvancedBrep"
)
})
.unwrap_or(false);
if !is_body {
continue;
}
let items = match shape_rep.get(3).and_then(|a| a.as_list()) {
Some(l) => l,
None => return false,
};
if items.len() != 1 {
return false;
}
let item_id = match items.first().and_then(|v| v.as_entity_ref()) {
Some(id) => id,
None => return false,
};
let item = match decoder.decode_by_id(item_id) {
Ok(e) => e,
Err(_) => return false,
};
return item_has_identity_position(&item, decoder);
}
false
}
fn item_has_identity_position(item: &DecodedEntity, decoder: &mut EntityDecoder) -> bool {
match item.ifc_type {
IfcType::IfcExtrudedAreaSolid
| IfcType::IfcRevolvedAreaSolid
| IfcType::IfcSurfaceCurveSweptAreaSolid
| IfcType::IfcFixedReferenceSweptAreaSolid => {
attribute_placement_is_identity(item, 1, decoder)
}
IfcType::IfcBooleanClippingResult | IfcType::IfcBooleanResult => {
let first_operand_id = match item.get_ref(1) {
Some(id) => id,
None => return false,
};
match decoder.decode_by_id(first_operand_id) {
Ok(inner) => item_has_identity_position(&inner, decoder),
Err(_) => false,
}
}
IfcType::IfcMappedItem => false,
IfcType::IfcFacetedBrep
| IfcType::IfcFacetedBrepWithVoids
| IfcType::IfcAdvancedBrep
| IfcType::IfcAdvancedBrepWithVoids
| IfcType::IfcTriangulatedFaceSet
| IfcType::IfcTriangulatedIrregularNetwork
| IfcType::IfcPolygonalFaceSet
| IfcType::IfcFaceBasedSurfaceModel
| IfcType::IfcShellBasedSurfaceModel => true,
_ => false,
}
}
fn attribute_placement_is_identity(
entity: &DecodedEntity,
attr_index: usize,
decoder: &mut EntityDecoder,
) -> bool {
let attr = match entity.get(attr_index) {
Some(a) => a,
None => return true,
};
if attr.is_null() {
return true;
}
let placement_id = match attr.as_entity_ref() {
Some(id) => id,
None => return false,
};
match crate::transform::parse_axis2_placement_3d_from_id(placement_id, decoder) {
Ok(m) => matrix_is_identity(&m),
Err(_) => false,
}
}
#[inline]
fn matrix_is_identity(m: &Matrix4<f64>) -> bool {
const EPS: f64 = 1e-9;
let id = Matrix4::<f64>::identity();
for i in 0..4 {
for j in 0..4 {
if (m[(i, j)] - id[(i, j)]).abs() > EPS {
return false;
}
}
}
true
}
fn slice_mesh_into_layers(
mesh: &Mesh,
visual_layers: &[VisualLayer],
planes: &[Plane],
) -> SubMeshCollection {
debug_assert_eq!(planes.len() + 1, visual_layers.len());
let clipper = ClippingProcessor::new();
let mut out = SubMeshCollection::new();
for (i, layer) in visual_layers.iter().enumerate() {
let after_prev: Option<&Plane> = if i == 0 { None } else { planes.get(i - 1) };
let before_next: Option<&Plane> = if i + 1 == visual_layers.len() {
None
} else {
planes.get(i)
};
let mut slab = mesh.clone();
if let Some(plane) = after_prev {
if let Ok(clipped) = clipper.clip_mesh(&slab, plane) {
slab = clipped;
}
}
if let Some(plane) = before_next {
let flipped = Plane::new(plane.point, -plane.normal);
if let Ok(clipped) = clipper.clip_mesh(&slab, &flipped) {
slab = clipped;
}
}
if !slab.is_empty() {
out.sub_meshes.push(SubMesh::new(layer.material_id, slab));
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn li(material: u32, thickness: f64) -> LayerInfo {
LayerInfo { material_id: material, thickness }
}
#[test]
fn thin_middle_layer_folded_into_thicker_neighbour() {
let layers = vec![li(1, 100.0), li(2, 1.0), li(3, 50.0)];
let merged = merge_thin_layers(&layers, 0.001);
assert_eq!(merged.len(), 2, "3-layer stack with a sub-mm middle should collapse to 2 slabs");
assert_eq!(merged[0].material_id, 1);
assert!((merged[0].thickness_m - 0.101).abs() < 1e-9);
assert_eq!(merged[1].material_id, 3);
assert!((merged[1].thickness_m - 0.050).abs() < 1e-9);
}
#[test]
fn all_thick_layers_stay_separate() {
let layers = vec![li(1, 50.0), li(2, 80.0), li(3, 30.0)];
let merged = merge_thin_layers(&layers, 0.001);
assert_eq!(merged.len(), 3);
assert_eq!(merged[0].material_id, 1);
assert_eq!(merged[1].material_id, 2);
assert_eq!(merged[2].material_id, 3);
}
#[test]
fn trailing_thin_layer_folds_into_previous_slab() {
let layers = vec![li(1, 50.0), li(2, 80.0), li(3, 1.0)];
let merged = merge_thin_layers(&layers, 0.001);
assert_eq!(merged.len(), 2, "sub-mm trailing layer merges into the previous slab");
assert_eq!(merged[1].material_id, 2);
assert!((merged[1].thickness_m - 0.081).abs() < 1e-9);
}
#[test]
fn leading_thin_layer_folds_into_next_slab() {
let layers = vec![li(1, 1.0), li(2, 80.0), li(3, 50.0)];
let merged = merge_thin_layers(&layers, 0.001);
assert_eq!(merged.len(), 2);
assert_eq!(merged[0].material_id, 2);
assert!((merged[0].thickness_m - 0.081).abs() < 1e-9);
assert_eq!(merged[1].material_id, 3);
}
}