1use crate::{calculate_normals, ClippingProcessor, Error, Mesh, Point2, Point3, Profile2D, Result, Vector3};
11use ifc_lite_core::{DecodedEntity, EntityDecoder, IfcSchema, IfcType};
12
13use crate::router::GeometryProcessor;
14use super::helpers::parse_axis2_placement_3d;
15use super::extrusion::ExtrudedAreaSolidProcessor;
16use super::tessellated::TriangulatedFaceSetProcessor;
17use super::brep::FacetedBrepProcessor;
18use super::swept::{SweptDiskSolidProcessor, RevolvedAreaSolidProcessor};
19
20const MAX_BOOLEAN_DEPTH: u32 = 20;
25
26pub struct BooleanClippingProcessor {
41 schema: IfcSchema,
42}
43
44impl BooleanClippingProcessor {
45 pub fn new() -> Self {
46 Self {
47 schema: IfcSchema::new(),
48 }
49 }
50
51 fn process_operand_with_depth(
53 &self,
54 operand: &DecodedEntity,
55 decoder: &mut EntityDecoder,
56 depth: u32,
57 ) -> Result<Mesh> {
58 match operand.ifc_type {
59 IfcType::IfcExtrudedAreaSolid => {
60 let processor = ExtrudedAreaSolidProcessor::new(self.schema.clone());
61 processor.process(operand, decoder, &self.schema)
62 }
63 IfcType::IfcFacetedBrep => {
64 let processor = FacetedBrepProcessor::new();
65 processor.process(operand, decoder, &self.schema)
66 }
67 IfcType::IfcTriangulatedFaceSet => {
68 let processor = TriangulatedFaceSetProcessor::new();
69 processor.process(operand, decoder, &self.schema)
70 }
71 IfcType::IfcSweptDiskSolid => {
72 let processor = SweptDiskSolidProcessor::new(self.schema.clone());
73 processor.process(operand, decoder, &self.schema)
74 }
75 IfcType::IfcRevolvedAreaSolid => {
76 let processor = RevolvedAreaSolidProcessor::new(self.schema.clone());
77 processor.process(operand, decoder, &self.schema)
78 }
79 IfcType::IfcBooleanResult | IfcType::IfcBooleanClippingResult => {
80 self.process_with_depth(operand, decoder, &self.schema, depth + 1)
82 }
83 _ => Ok(Mesh::new()),
84 }
85 }
86
87 fn parse_half_space_solid(
90 &self,
91 half_space: &DecodedEntity,
92 decoder: &mut EntityDecoder,
93 ) -> Result<(Point3<f64>, Vector3<f64>, bool)> {
94 let surface_attr = half_space
99 .get(0)
100 .ok_or_else(|| Error::geometry("HalfSpaceSolid missing BaseSurface".to_string()))?;
101
102 let surface = decoder
103 .resolve_ref(surface_attr)?
104 .ok_or_else(|| Error::geometry("Failed to resolve BaseSurface".to_string()))?;
105
106 let agreement = half_space
108 .get(1)
109 .map(|v| match v {
110 ifc_lite_core::AttributeValue::Enum(e) => e != "F" && e != ".F.",
112 _ => true,
113 })
114 .unwrap_or(true);
115
116 if surface.ifc_type != IfcType::IfcPlane {
118 return Err(Error::geometry(format!(
119 "Expected IfcPlane for HalfSpaceSolid, got {}",
120 surface.ifc_type
121 )));
122 }
123
124 let position_attr = surface
126 .get(0)
127 .ok_or_else(|| Error::geometry("IfcPlane missing Position".to_string()))?;
128
129 let position = decoder
130 .resolve_ref(position_attr)?
131 .ok_or_else(|| Error::geometry("Failed to resolve Plane position".to_string()))?;
132
133 let position_transform = parse_axis2_placement_3d(&position, decoder)?;
138
139 let location = Point3::new(
141 position_transform[(0, 3)],
142 position_transform[(1, 3)],
143 position_transform[(2, 3)],
144 );
145
146 let normal = Vector3::new(
149 position_transform[(0, 2)],
150 position_transform[(1, 2)],
151 position_transform[(2, 2)],
152 ).normalize();
153
154 Ok((location, normal, agreement))
155 }
156
157 fn clip_mesh_with_half_space(
159 &self,
160 mesh: &Mesh,
161 plane_point: Point3<f64>,
162 plane_normal: Vector3<f64>,
163 agreement: bool,
164 ) -> Result<Mesh> {
165 use crate::csg::{ClippingProcessor, Plane};
166
167 let clip_normal = if agreement {
174 plane_normal } else {
176 -plane_normal };
178
179 let plane = Plane::new(plane_point, clip_normal);
180 let processor = ClippingProcessor::new();
181 processor.clip_mesh(mesh, &plane)
182 }
183
184 fn parse_polygonal_boundary_2d(
185 &self,
186 boundary: &DecodedEntity,
187 decoder: &mut EntityDecoder,
188 ) -> Result<Vec<Point2<f64>>> {
189 if boundary.ifc_type != IfcType::IfcPolyline {
190 return Err(Error::geometry(format!(
191 "Expected IfcPolyline for PolygonalBoundary, got {}",
192 boundary.ifc_type
193 )));
194 }
195
196 let points_attr = boundary
197 .get(0)
198 .ok_or_else(|| Error::geometry("IfcPolyline missing Points".to_string()))?;
199 let points = decoder.resolve_ref_list(points_attr)?;
200
201 let mut contour = Vec::with_capacity(points.len());
202 for point in points {
203 if point.ifc_type != IfcType::IfcCartesianPoint {
204 return Err(Error::geometry(format!(
205 "Expected IfcCartesianPoint in PolygonalBoundary, got {}",
206 point.ifc_type
207 )));
208 }
209
210 let coords_attr = point
211 .get(0)
212 .ok_or_else(|| Error::geometry("IfcCartesianPoint missing coordinates".to_string()))?;
213 let coords = coords_attr
214 .as_list()
215 .ok_or_else(|| Error::geometry("Expected point coordinate list".to_string()))?;
216
217 let x = coords.first().and_then(|v| v.as_float()).unwrap_or(0.0);
218 let y = coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
219 contour.push(Point2::new(x, y));
220 }
221
222 if contour.len() > 1 {
223 let first = contour[0];
224 let last = contour[contour.len() - 1];
225 if (first.x - last.x).abs() < 1e-9 && (first.y - last.y).abs() < 1e-9 {
226 contour.pop();
227 }
228 }
229
230 if contour.len() < 3 {
231 return Err(Error::geometry(
232 "PolygonalBoundary must contain at least 3 distinct points".to_string(),
233 ));
234 }
235
236 Ok(contour)
237 }
238
239 fn polygon_normal(points: &[Point3<f64>]) -> Vector3<f64> {
240 let mut normal = Vector3::new(0.0, 0.0, 0.0);
241 for i in 0..points.len() {
242 let current = points[i];
243 let next = points[(i + 1) % points.len()];
244 normal.x += (current.y - next.y) * (current.z + next.z);
245 normal.y += (current.z - next.z) * (current.x + next.x);
246 normal.z += (current.x - next.x) * (current.y + next.y);
247 }
248
249 normal
250 .try_normalize(1e-12)
251 .unwrap_or_else(|| Vector3::new(0.0, 0.0, 1.0))
252 }
253
254 fn build_prism_mesh(
255 &self,
256 contour_2d: &[Point2<f64>],
257 origin: Point3<f64>,
258 x_axis: Vector3<f64>,
259 y_axis: Vector3<f64>,
260 extrusion_dir: Vector3<f64>,
261 depth: f64,
262 ) -> Result<Mesh> {
263 let profile = Profile2D::new(contour_2d.to_vec());
264 let triangulation = profile.triangulate()?;
265
266 let contour_world: Vec<Point3<f64>> = contour_2d
267 .iter()
268 .map(|p| origin + x_axis * p.x + y_axis * p.y)
269 .collect();
270 let tri_world: Vec<Point3<f64>> = triangulation
271 .points
272 .iter()
273 .map(|p| origin + x_axis * p.x + y_axis * p.y)
274 .collect();
275 let top_world: Vec<Point3<f64>> = tri_world
276 .iter()
277 .map(|p| *p + extrusion_dir * depth)
278 .collect();
279
280 let mut mesh = Mesh::with_capacity(
281 triangulation.points.len() * 2 + contour_world.len() * 4,
282 triangulation.indices.len() * 2 + contour_world.len() * 6,
283 );
284 let zero = Vector3::new(0.0, 0.0, 0.0);
285
286 let push_triangle = |mesh: &mut Mesh, a: Point3<f64>, b: Point3<f64>, c: Point3<f64>| {
287 let base = mesh.vertex_count() as u32;
288 mesh.add_vertex(a, zero);
289 mesh.add_vertex(b, zero);
290 mesh.add_vertex(c, zero);
291 mesh.indices.extend_from_slice(&[base, base + 1, base + 2]);
292 };
293
294 for indices in triangulation.indices.chunks_exact(3) {
295 let i0 = indices[0];
296 let i1 = indices[1];
297 let i2 = indices[2];
298
299 push_triangle(&mut mesh, tri_world[i2], tri_world[i1], tri_world[i0]);
301 push_triangle(&mut mesh, top_world[i0], top_world[i1], top_world[i2]);
303 }
304
305 let contour_top: Vec<Point3<f64>> = contour_world
306 .iter()
307 .map(|p| *p + extrusion_dir * depth)
308 .collect();
309
310 for i in 0..contour_world.len() {
311 let next = (i + 1) % contour_world.len();
312 let b0 = contour_world[i];
313 let b1 = contour_world[next];
314 let t0 = contour_top[i];
315 let t1 = contour_top[next];
316
317 push_triangle(&mut mesh, b0, b1, t1);
318 push_triangle(&mut mesh, b0, t1, t0);
319 }
320
321 calculate_normals(&mut mesh);
322 Ok(mesh)
323 }
324
325 fn build_polygonal_bounded_half_space_mesh(
326 &self,
327 half_space: &DecodedEntity,
328 decoder: &mut EntityDecoder,
329 host_mesh: &Mesh,
330 plane_normal: Vector3<f64>,
331 agreement: bool,
332 ) -> Result<Mesh> {
333 let position_attr = half_space
334 .get(2)
335 .ok_or_else(|| Error::geometry("PolygonalBoundedHalfSpace missing Position".to_string()))?;
336 let position = decoder
337 .resolve_ref(position_attr)?
338 .ok_or_else(|| Error::geometry("Failed to resolve bounded half-space Position".to_string()))?;
339 let transform = parse_axis2_placement_3d(&position, decoder)?;
340
341 let boundary_attr = half_space
342 .get(3)
343 .ok_or_else(|| Error::geometry("PolygonalBoundedHalfSpace missing PolygonalBoundary".to_string()))?;
344 let boundary = decoder
345 .resolve_ref(boundary_attr)?
346 .ok_or_else(|| Error::geometry("Failed to resolve PolygonalBoundary".to_string()))?;
347
348 let mut contour_2d = self.parse_polygonal_boundary_2d(&boundary, decoder)?;
349
350 let origin = Point3::new(transform[(0, 3)], transform[(1, 3)], transform[(2, 3)]);
351 let x_axis = Vector3::new(transform[(0, 0)], transform[(1, 0)], transform[(2, 0)]).normalize();
352 let y_axis = Vector3::new(transform[(0, 1)], transform[(1, 1)], transform[(2, 1)]).normalize();
353
354 let mut contour_world: Vec<Point3<f64>> = contour_2d
355 .iter()
356 .map(|p| origin + x_axis * p.x + y_axis * p.y)
357 .collect();
358
359 let extrusion_dir = if agreement {
361 -plane_normal
362 } else {
363 plane_normal
364 }
365 .normalize();
366
367 if Self::polygon_normal(&contour_world).dot(&extrusion_dir) < 0.0 {
368 contour_2d.reverse();
369 contour_world.reverse();
370 }
371
372 let (host_min, host_max) = host_mesh.bounds();
373 let host_corners = [
374 Point3::new(host_min.x as f64, host_min.y as f64, host_min.z as f64),
375 Point3::new(host_max.x as f64, host_min.y as f64, host_min.z as f64),
376 Point3::new(host_min.x as f64, host_max.y as f64, host_min.z as f64),
377 Point3::new(host_max.x as f64, host_max.y as f64, host_min.z as f64),
378 Point3::new(host_min.x as f64, host_min.y as f64, host_max.z as f64),
379 Point3::new(host_max.x as f64, host_min.y as f64, host_max.z as f64),
380 Point3::new(host_min.x as f64, host_max.y as f64, host_max.z as f64),
381 Point3::new(host_max.x as f64, host_max.y as f64, host_max.z as f64),
382 ];
383 let host_diag = ((host_max.x - host_min.x) as f64).hypot((host_max.y - host_min.y) as f64)
384 .hypot((host_max.z - host_min.z) as f64);
385 let max_projection = host_corners
386 .iter()
387 .map(|corner| (corner - origin).dot(&extrusion_dir))
388 .fold(0.0_f64, f64::max);
389 let depth = max_projection.max(host_diag) + 1.0;
390
391 self.build_prism_mesh(&contour_2d, origin, x_axis, y_axis, extrusion_dir, depth)
392 }
393
394 fn process_with_depth(
396 &self,
397 entity: &DecodedEntity,
398 decoder: &mut EntityDecoder,
399 _schema: &IfcSchema,
400 depth: u32,
401 ) -> Result<Mesh> {
402 if depth > MAX_BOOLEAN_DEPTH {
404 return Err(Error::geometry(format!(
405 "Boolean nesting depth {} exceeds limit {}",
406 depth, MAX_BOOLEAN_DEPTH
407 )));
408 }
409
410 let operator = entity
417 .get(0)
418 .and_then(|v| match v {
419 ifc_lite_core::AttributeValue::Enum(e) => Some(e.as_str()),
420 _ => None,
421 })
422 .unwrap_or(".DIFFERENCE.");
423
424 let first_operand_attr = entity
426 .get(1)
427 .ok_or_else(|| Error::geometry("BooleanResult missing FirstOperand".to_string()))?;
428
429 let first_operand = decoder
430 .resolve_ref(first_operand_attr)?
431 .ok_or_else(|| Error::geometry("Failed to resolve FirstOperand".to_string()))?;
432
433 let mesh = self.process_operand_with_depth(&first_operand, decoder, depth)?;
435
436 if mesh.is_empty() {
437 return Ok(mesh);
438 }
439
440 let second_operand_attr = entity
442 .get(2)
443 .ok_or_else(|| Error::geometry("BooleanResult missing SecondOperand".to_string()))?;
444
445 let second_operand = decoder
446 .resolve_ref(second_operand_attr)?
447 .ok_or_else(|| Error::geometry("Failed to resolve SecondOperand".to_string()))?;
448
449 if operator == ".DIFFERENCE." || operator == "DIFFERENCE" {
452 if second_operand.ifc_type == IfcType::IfcHalfSpaceSolid {
454 let (plane_point, plane_normal, agreement) =
456 self.parse_half_space_solid(&second_operand, decoder)?;
457 return self.clip_mesh_with_half_space(&mesh, plane_point, plane_normal, agreement);
458 }
459
460 if second_operand.ifc_type == IfcType::IfcPolygonalBoundedHalfSpace {
461 let (plane_point, plane_normal, agreement) =
462 self.parse_half_space_solid(&second_operand, decoder)?;
463 if let Ok(bound_mesh) = self.build_polygonal_bounded_half_space_mesh(
464 &second_operand,
465 decoder,
466 &mesh,
467 plane_normal,
468 agreement,
469 ) {
470 let clipper = ClippingProcessor::new();
471 if let Ok(clipped) = clipper.subtract_mesh(&mesh, &bound_mesh) {
472 return Ok(clipped);
473 }
474 }
475
476 return self.clip_mesh_with_half_space(&mesh, plane_point, plane_normal, agreement);
477 }
478
479 return Ok(mesh);
490 }
491
492 if operator == ".UNION." || operator == "UNION" {
494 let second_mesh = self.process_operand_with_depth(&second_operand, decoder, depth)?;
496 if !second_mesh.is_empty() {
497 let mut merged = mesh;
498 merged.merge(&second_mesh);
499 return Ok(merged);
500 }
501 return Ok(mesh);
502 }
503
504 if operator == ".INTERSECTION." || operator == "INTERSECTION" {
506 return Ok(Mesh::new());
509 }
510
511 #[cfg(debug_assertions)]
513 eprintln!("[WARN] Unknown CSG operator {}, returning first operand", operator);
514 Ok(mesh)
515 }
516}
517
518impl GeometryProcessor for BooleanClippingProcessor {
519 fn process(
520 &self,
521 entity: &DecodedEntity,
522 decoder: &mut EntityDecoder,
523 schema: &IfcSchema,
524 ) -> Result<Mesh> {
525 self.process_with_depth(entity, decoder, schema, 0)
526 }
527
528 fn supported_types(&self) -> Vec<IfcType> {
529 vec![IfcType::IfcBooleanResult, IfcType::IfcBooleanClippingResult]
530 }
531}
532
533impl Default for BooleanClippingProcessor {
534 fn default() -> Self {
535 Self::new()
536 }
537}