use crate::{
calculate_normals, ClippingProcessor, Error, Mesh, Point2, Point3, Profile2D, Result, Vector3,
};
use ifc_lite_core::{DecodedEntity, EntityDecoder, IfcSchema, IfcType};
use super::brep::FacetedBrepProcessor;
use super::extrusion::ExtrudedAreaSolidProcessor;
use super::helpers::parse_axis2_placement_3d;
use super::swept::{RevolvedAreaSolidProcessor, SweptDiskSolidProcessor};
use super::tessellated::TriangulatedFaceSetProcessor;
use crate::router::GeometryProcessor;
const MAX_BOOLEAN_DEPTH: u32 = 10;
pub struct BooleanClippingProcessor {
schema: IfcSchema,
}
impl BooleanClippingProcessor {
pub fn new() -> Self {
Self {
schema: IfcSchema::new(),
}
}
fn process_operand_with_depth(
&self,
operand: &DecodedEntity,
decoder: &mut EntityDecoder,
depth: u32,
) -> Result<Mesh> {
match operand.ifc_type {
IfcType::IfcExtrudedAreaSolid => {
let processor = ExtrudedAreaSolidProcessor::new(self.schema.clone());
processor.process(operand, decoder, &self.schema)
}
IfcType::IfcFacetedBrep => {
let processor = FacetedBrepProcessor::new();
processor.process(operand, decoder, &self.schema)
}
IfcType::IfcTriangulatedFaceSet => {
let processor = TriangulatedFaceSetProcessor::new();
processor.process(operand, decoder, &self.schema)
}
IfcType::IfcSweptDiskSolid => {
let processor = SweptDiskSolidProcessor::new(self.schema.clone());
processor.process(operand, decoder, &self.schema)
}
IfcType::IfcRevolvedAreaSolid => {
let processor = RevolvedAreaSolidProcessor::new(self.schema.clone());
processor.process(operand, decoder, &self.schema)
}
IfcType::IfcBooleanResult | IfcType::IfcBooleanClippingResult => {
self.process_with_depth(operand, decoder, &self.schema, depth + 1)
}
_ => Ok(Mesh::new()),
}
}
fn parse_half_space_solid(
&self,
half_space: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> Result<(Point3<f64>, Vector3<f64>, bool)> {
let surface_attr = half_space
.get(0)
.ok_or_else(|| Error::geometry("HalfSpaceSolid missing BaseSurface".to_string()))?;
let surface = decoder
.resolve_ref(surface_attr)?
.ok_or_else(|| Error::geometry("Failed to resolve BaseSurface".to_string()))?;
let agreement = half_space
.get(1)
.map(|v| match v {
ifc_lite_core::AttributeValue::Enum(e) => e != "F" && e != ".F.",
_ => true,
})
.unwrap_or(true);
if surface.ifc_type != IfcType::IfcPlane {
return Err(Error::geometry(format!(
"Expected IfcPlane for HalfSpaceSolid, got {}",
surface.ifc_type
)));
}
let position_attr = surface
.get(0)
.ok_or_else(|| Error::geometry("IfcPlane missing Position".to_string()))?;
let position = decoder
.resolve_ref(position_attr)?
.ok_or_else(|| Error::geometry("Failed to resolve Plane position".to_string()))?;
let position_transform = parse_axis2_placement_3d(&position, decoder)?;
let location = Point3::new(
position_transform[(0, 3)],
position_transform[(1, 3)],
position_transform[(2, 3)],
);
let normal = Vector3::new(
position_transform[(0, 2)],
position_transform[(1, 2)],
position_transform[(2, 2)],
)
.normalize();
Ok((location, normal, agreement))
}
fn clip_mesh_with_half_space(
&self,
mesh: &Mesh,
plane_point: Point3<f64>,
plane_normal: Vector3<f64>,
agreement: bool,
) -> Result<Mesh> {
use crate::csg::{ClippingProcessor, Plane};
let clip_normal = if agreement {
plane_normal } else {
-plane_normal };
let plane = Plane::new(plane_point, clip_normal);
let processor = ClippingProcessor::new();
processor.clip_mesh(mesh, &plane)
}
fn parse_polygonal_boundary_2d(
&self,
boundary: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> Result<Vec<Point2<f64>>> {
if boundary.ifc_type != IfcType::IfcPolyline {
return Err(Error::geometry(format!(
"Expected IfcPolyline for PolygonalBoundary, got {}",
boundary.ifc_type
)));
}
let points_attr = boundary
.get(0)
.ok_or_else(|| Error::geometry("IfcPolyline missing Points".to_string()))?;
let points = decoder.resolve_ref_list(points_attr)?;
let mut contour = Vec::with_capacity(points.len());
for point in points {
if point.ifc_type != IfcType::IfcCartesianPoint {
return Err(Error::geometry(format!(
"Expected IfcCartesianPoint in PolygonalBoundary, got {}",
point.ifc_type
)));
}
let coords_attr = point.get(0).ok_or_else(|| {
Error::geometry("IfcCartesianPoint missing coordinates".to_string())
})?;
let coords = coords_attr
.as_list()
.ok_or_else(|| Error::geometry("Expected point coordinate list".to_string()))?;
let x = coords.first().and_then(|v| v.as_float()).unwrap_or(0.0);
let y = coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
contour.push(Point2::new(x, y));
}
if contour.len() > 1 {
let first = contour[0];
let last = contour[contour.len() - 1];
if (first.x - last.x).abs() < 1e-9 && (first.y - last.y).abs() < 1e-9 {
contour.pop();
}
}
if contour.len() < 3 {
return Err(Error::geometry(
"PolygonalBoundary must contain at least 3 distinct points".to_string(),
));
}
Ok(contour)
}
fn polygon_normal(points: &[Point3<f64>]) -> Vector3<f64> {
let mut normal = Vector3::new(0.0, 0.0, 0.0);
for i in 0..points.len() {
let current = points[i];
let next = points[(i + 1) % points.len()];
normal.x += (current.y - next.y) * (current.z + next.z);
normal.y += (current.z - next.z) * (current.x + next.x);
normal.z += (current.x - next.x) * (current.y + next.y);
}
normal
.try_normalize(1e-12)
.unwrap_or_else(|| Vector3::new(0.0, 0.0, 1.0))
}
fn build_prism_mesh(
&self,
contour_2d: &[Point2<f64>],
origin: Point3<f64>,
x_axis: Vector3<f64>,
y_axis: Vector3<f64>,
extrusion_dir: Vector3<f64>,
depth: f64,
) -> Result<Mesh> {
let profile = Profile2D::new(contour_2d.to_vec());
let triangulation = profile.triangulate()?;
let contour_world: Vec<Point3<f64>> = contour_2d
.iter()
.map(|p| origin + x_axis * p.x + y_axis * p.y)
.collect();
let tri_world: Vec<Point3<f64>> = triangulation
.points
.iter()
.map(|p| origin + x_axis * p.x + y_axis * p.y)
.collect();
let top_world: Vec<Point3<f64>> = tri_world
.iter()
.map(|p| *p + extrusion_dir * depth)
.collect();
let mut mesh = Mesh::with_capacity(
triangulation.points.len() * 2 + contour_world.len() * 4,
triangulation.indices.len() * 2 + contour_world.len() * 6,
);
let zero = Vector3::new(0.0, 0.0, 0.0);
let push_triangle = |mesh: &mut Mesh, a: Point3<f64>, b: Point3<f64>, c: Point3<f64>| {
let base = mesh.vertex_count() as u32;
mesh.add_vertex(a, zero);
mesh.add_vertex(b, zero);
mesh.add_vertex(c, zero);
mesh.indices.extend_from_slice(&[base, base + 1, base + 2]);
};
for indices in triangulation.indices.chunks_exact(3) {
let i0 = indices[0];
let i1 = indices[1];
let i2 = indices[2];
push_triangle(&mut mesh, tri_world[i2], tri_world[i1], tri_world[i0]);
push_triangle(&mut mesh, top_world[i0], top_world[i1], top_world[i2]);
}
let contour_top: Vec<Point3<f64>> = contour_world
.iter()
.map(|p| *p + extrusion_dir * depth)
.collect();
for i in 0..contour_world.len() {
let next = (i + 1) % contour_world.len();
let b0 = contour_world[i];
let b1 = contour_world[next];
let t0 = contour_top[i];
let t1 = contour_top[next];
push_triangle(&mut mesh, b0, b1, t1);
push_triangle(&mut mesh, b0, t1, t0);
}
calculate_normals(&mut mesh);
Ok(mesh)
}
fn build_polygonal_bounded_half_space_mesh(
&self,
half_space: &DecodedEntity,
decoder: &mut EntityDecoder,
host_mesh: &Mesh,
plane_normal: Vector3<f64>,
agreement: bool,
) -> Result<Mesh> {
let position_attr = half_space.get(2).ok_or_else(|| {
Error::geometry("PolygonalBoundedHalfSpace missing Position".to_string())
})?;
let position = decoder.resolve_ref(position_attr)?.ok_or_else(|| {
Error::geometry("Failed to resolve bounded half-space Position".to_string())
})?;
let transform = parse_axis2_placement_3d(&position, decoder)?;
let boundary_attr = half_space.get(3).ok_or_else(|| {
Error::geometry("PolygonalBoundedHalfSpace missing PolygonalBoundary".to_string())
})?;
let boundary = decoder
.resolve_ref(boundary_attr)?
.ok_or_else(|| Error::geometry("Failed to resolve PolygonalBoundary".to_string()))?;
let mut contour_2d = self.parse_polygonal_boundary_2d(&boundary, decoder)?;
let origin = Point3::new(transform[(0, 3)], transform[(1, 3)], transform[(2, 3)]);
let x_axis =
Vector3::new(transform[(0, 0)], transform[(1, 0)], transform[(2, 0)]).normalize();
let y_axis =
Vector3::new(transform[(0, 1)], transform[(1, 1)], transform[(2, 1)]).normalize();
let mut contour_world: Vec<Point3<f64>> = contour_2d
.iter()
.map(|p| origin + x_axis * p.x + y_axis * p.y)
.collect();
let extrusion_dir = if agreement {
-plane_normal
} else {
plane_normal
}
.normalize();
if Self::polygon_normal(&contour_world).dot(&extrusion_dir) < 0.0 {
contour_2d.reverse();
contour_world.reverse();
}
let (host_min, host_max) = host_mesh.bounds();
let host_corners = [
Point3::new(host_min.x as f64, host_min.y as f64, host_min.z as f64),
Point3::new(host_max.x as f64, host_min.y as f64, host_min.z as f64),
Point3::new(host_min.x as f64, host_max.y as f64, host_min.z as f64),
Point3::new(host_max.x as f64, host_max.y as f64, host_min.z as f64),
Point3::new(host_min.x as f64, host_min.y as f64, host_max.z as f64),
Point3::new(host_max.x as f64, host_min.y as f64, host_max.z as f64),
Point3::new(host_min.x as f64, host_max.y as f64, host_max.z as f64),
Point3::new(host_max.x as f64, host_max.y as f64, host_max.z as f64),
];
let host_diag = ((host_max.x - host_min.x) as f64)
.hypot((host_max.y - host_min.y) as f64)
.hypot((host_max.z - host_min.z) as f64);
let max_projection = host_corners
.iter()
.map(|corner| (corner - origin).dot(&extrusion_dir))
.fold(0.0_f64, f64::max);
let depth = max_projection.max(host_diag) + 1.0;
self.build_prism_mesh(&contour_2d, origin, x_axis, y_axis, extrusion_dir, depth)
}
fn process_with_depth(
&self,
entity: &DecodedEntity,
decoder: &mut EntityDecoder,
_schema: &IfcSchema,
depth: u32,
) -> Result<Mesh> {
if depth > MAX_BOOLEAN_DEPTH {
return Err(Error::geometry(format!(
"Boolean nesting depth {} exceeds limit {}",
depth, MAX_BOOLEAN_DEPTH
)));
}
let operator = entity
.get(0)
.and_then(|v| match v {
ifc_lite_core::AttributeValue::Enum(e) => Some(e.as_str()),
_ => None,
})
.unwrap_or(".DIFFERENCE.");
let first_operand_attr = entity
.get(1)
.ok_or_else(|| Error::geometry("BooleanResult missing FirstOperand".to_string()))?;
let first_operand = decoder
.resolve_ref(first_operand_attr)?
.ok_or_else(|| Error::geometry("Failed to resolve FirstOperand".to_string()))?;
let mesh = self.process_operand_with_depth(&first_operand, decoder, depth)?;
if mesh.is_empty() {
return Ok(mesh);
}
let second_operand_attr = entity
.get(2)
.ok_or_else(|| Error::geometry("BooleanResult missing SecondOperand".to_string()))?;
let second_operand = decoder
.resolve_ref(second_operand_attr)?
.ok_or_else(|| Error::geometry("Failed to resolve SecondOperand".to_string()))?;
if operator == ".DIFFERENCE." || operator == "DIFFERENCE" {
if second_operand.ifc_type == IfcType::IfcHalfSpaceSolid {
let (plane_point, plane_normal, agreement) =
self.parse_half_space_solid(&second_operand, decoder)?;
return self.clip_mesh_with_half_space(&mesh, plane_point, plane_normal, agreement);
}
if second_operand.ifc_type == IfcType::IfcPolygonalBoundedHalfSpace {
let (plane_point, plane_normal, agreement) =
self.parse_half_space_solid(&second_operand, decoder)?;
if let Ok(bound_mesh) = self.build_polygonal_bounded_half_space_mesh(
&second_operand,
decoder,
&mesh,
plane_normal,
agreement,
) {
let clipper = ClippingProcessor::new();
if let Ok(clipped) = clipper.subtract_mesh(&mesh, &bound_mesh) {
return Ok(clipped);
}
}
return self.clip_mesh_with_half_space(&mesh, plane_point, plane_normal, agreement);
}
return Ok(mesh);
}
if operator == ".UNION." || operator == "UNION" {
let second_mesh = self.process_operand_with_depth(&second_operand, decoder, depth)?;
if !second_mesh.is_empty() {
let mut merged = mesh;
merged.merge(&second_mesh);
return Ok(merged);
}
return Ok(mesh);
}
if operator == ".INTERSECTION." || operator == "INTERSECTION" {
return Ok(Mesh::new());
}
#[cfg(debug_assertions)]
eprintln!(
"[WARN] Unknown CSG operator {}, returning first operand",
operator
);
Ok(mesh)
}
}
impl GeometryProcessor for BooleanClippingProcessor {
fn process(
&self,
entity: &DecodedEntity,
decoder: &mut EntityDecoder,
schema: &IfcSchema,
) -> Result<Mesh> {
self.process_with_depth(entity, decoder, schema, 0)
}
fn supported_types(&self) -> Vec<IfcType> {
vec![IfcType::IfcBooleanResult, IfcType::IfcBooleanClippingResult]
}
}
impl Default for BooleanClippingProcessor {
fn default() -> Self {
Self::new()
}
}