1use super::GeometryRouter;
8use crate::csg::{ClippingProcessor, Plane, Triangle, TriangleVec};
9use crate::mesh::{SubMesh, SubMeshCollection};
10use crate::{Error, Mesh, Point3, Result, Vector3};
11use ifc_lite_core::{DecodedEntity, EntityDecoder, IfcType};
12use nalgebra::Matrix4;
13use rustc_hash::{FxHashMap, FxHashSet};
14
15const NORMALIZE_EPSILON: f64 = 1e-12;
17const MIN_OPENING_VOLUME: f64 = 0.0001;
20const CSG_TRIANGLE_RETENTION_DIVISOR: usize = 4;
24const MIN_VALID_TRIANGLES: usize = 4;
26const MAX_EXTRUSION_EXTRACT_DEPTH: usize = 32;
28
29fn extract_rotation_columns(m: &Matrix4<f64>) -> (Vector3<f64>, Vector3<f64>, Vector3<f64>) {
31 (
32 Vector3::new(m[(0, 0)], m[(1, 0)], m[(2, 0)]),
33 Vector3::new(m[(0, 1)], m[(1, 1)], m[(2, 1)]),
34 Vector3::new(m[(0, 2)], m[(1, 2)], m[(2, 2)]),
35 )
36}
37
38fn rotate_and_normalize(
40 rot: &(Vector3<f64>, Vector3<f64>, Vector3<f64>),
41 dir: &Vector3<f64>,
42) -> Result<Vector3<f64>> {
43 (rot.0 * dir.x + rot.1 * dir.y + rot.2 * dir.z)
44 .try_normalize(NORMALIZE_EPSILON)
45 .ok_or_else(|| Error::geometry("Zero-length direction vector".to_string()))
46}
47
48#[inline]
55fn determine_extrusion_axis(
56 extrusion_dir: Option<&Vector3<f64>>,
57 wall_min: &Point3<f64>,
58 wall_max: &Point3<f64>,
59) -> usize {
60 if let Some(dir) = extrusion_dir {
61 let ax = dir.x.abs();
62 let ay = dir.y.abs();
63 let az = dir.z.abs();
64 if ax >= ay && ax >= az {
65 0
66 } else if ay >= az {
67 1
68 } else {
69 2
70 }
71 } else {
72 let dx = (wall_max.x - wall_min.x).abs();
73 let dy = (wall_max.y - wall_min.y).abs();
74 let dz = (wall_max.z - wall_min.z).abs();
75 if dx <= dy && dx <= dz {
76 0
77 } else if dy <= dz {
78 1
79 } else {
80 2
81 }
82 }
83}
84
85#[inline(always)]
87fn axis_val(p: &Point3<f64>, axis: usize) -> f64 {
88 match axis {
89 0 => p.x,
90 1 => p.y,
91 _ => p.z,
92 }
93}
94
95#[inline(always)]
97fn point_from_axes(a: usize, va: f64, b: usize, vb: f64, c: usize, vc: f64) -> Point3<f64> {
98 let mut coords = [0.0_f64; 3];
99 coords[a] = va;
100 coords[b] = vb;
101 coords[c] = vc;
102 Point3::new(coords[0], coords[1], coords[2])
103}
104
105#[inline(always)]
107fn vec_along_axis(axis: usize, sign: f64) -> Vector3<f64> {
108 let mut coords = [0.0_f64; 3];
109 coords[axis] = sign;
110 Vector3::new(coords[0], coords[1], coords[2])
111}
112
113#[inline]
116fn add_reveal_quad(
117 mesh: &mut Mesh,
118 p0: Point3<f64>,
119 p1: Point3<f64>,
120 p2: Point3<f64>,
121 p3: Point3<f64>,
122 desired_normal: Vector3<f64>,
123) {
124 let edge1 = p1 - p0;
125 let edge2 = p2 - p0;
126 let computed = edge1.cross(&edge2);
127
128 let base = mesh.vertex_count() as u32;
129 mesh.add_vertex(p0, desired_normal);
130 mesh.add_vertex(p1, desired_normal);
131 mesh.add_vertex(p2, desired_normal);
132 mesh.add_vertex(p3, desired_normal);
133
134 if computed.dot(&desired_normal) >= 0.0 {
135 mesh.add_triangle(base, base + 1, base + 2);
136 mesh.add_triangle(base, base + 2, base + 3);
137 } else {
138 mesh.add_triangle(base, base + 2, base + 1);
139 mesh.add_triangle(base, base + 3, base + 2);
140 }
141}
142
143fn generate_reveal_quads(
152 mesh: &mut Mesh,
153 open_min: &Point3<f64>,
154 open_max: &Point3<f64>,
155 wall_min: &Point3<f64>,
156 wall_max: &Point3<f64>,
157 extrusion_dir: Option<&Vector3<f64>>,
158) {
159 let ea = determine_extrusion_axis(extrusion_dir, wall_min, wall_max);
160
161 let d_min = axis_val(wall_min, ea).max(axis_val(open_min, ea));
164 let d_max = axis_val(wall_max, ea).min(axis_val(open_max, ea));
165 if d_max - d_min < 1e-4 {
166 return; }
168
169 let cross: [usize; 2] = match ea {
171 0 => [1, 2],
172 1 => [0, 2],
173 _ => [0, 1],
174 };
175
176 for &ax in &cross {
181 let ov_min = axis_val(open_min, ax).max(axis_val(wall_min, ax));
182 let ov_max = axis_val(open_max, ax).min(axis_val(wall_max, ax));
183 if ov_max - ov_min < 1e-4 {
184 return;
185 }
186 }
187
188 for (i, &ca) in cross.iter().enumerate() {
189 let oa = cross[1 - i]; let o_min = axis_val(open_min, oa).max(axis_val(wall_min, oa));
193 let o_max = axis_val(open_max, oa).min(axis_val(wall_max, oa));
194
195 let face_lo = axis_val(open_min, ca);
197 if face_lo > axis_val(wall_min, ca) + 1e-4 {
198 add_reveal_quad(
199 mesh,
200 point_from_axes(ea, d_min, ca, face_lo, oa, o_min),
201 point_from_axes(ea, d_max, ca, face_lo, oa, o_min),
202 point_from_axes(ea, d_max, ca, face_lo, oa, o_max),
203 point_from_axes(ea, d_min, ca, face_lo, oa, o_max),
204 vec_along_axis(ca, 1.0),
205 );
206 }
207
208 let face_hi = axis_val(open_max, ca);
210 if face_hi < axis_val(wall_max, ca) - 1e-4 {
211 add_reveal_quad(
212 mesh,
213 point_from_axes(ea, d_min, ca, face_hi, oa, o_max),
214 point_from_axes(ea, d_max, ca, face_hi, oa, o_max),
215 point_from_axes(ea, d_max, ca, face_hi, oa, o_min),
216 point_from_axes(ea, d_min, ca, face_hi, oa, o_min),
217 vec_along_axis(ca, -1.0),
218 );
219 }
220 }
221}
222
223fn is_body_representation(rep_type: &str) -> bool {
225 matches!(
226 rep_type,
227 "Body"
228 | "SweptSolid"
229 | "Brep"
230 | "CSG"
231 | "Clipping"
232 | "Tessellation"
233 | "MappedRepresentation"
234 | "SolidModel"
235 | "SurfaceModel"
236 | "AdvancedSweptSolid"
237 | "AdvancedBrep"
238 )
239}
240
241#[derive(Clone)]
243enum OpeningType {
244 Rectangular(Point3<f64>, Point3<f64>, Option<Vector3<f64>>),
247 DiagonalRectangular(Mesh, Vector3<f64>),
250 NonRectangular(Mesh),
253}
254
255struct ClipBuffers {
260 result: TriangleVec,
262 remaining: TriangleVec,
264 next_remaining: TriangleVec,
266}
267
268impl ClipBuffers {
269 fn new() -> Self {
271 Self {
272 result: TriangleVec::new(),
273 remaining: TriangleVec::new(),
274 next_remaining: TriangleVec::new(),
275 }
276 }
277
278 #[inline]
280 fn clear(&mut self) {
281 self.result.clear();
282 self.remaining.clear();
283 self.next_remaining.clear();
284 }
285}
286
287pub(super) struct VoidContext {
297 openings: Vec<OpeningType>,
300 merged_openings: Vec<OpeningType>,
303 world_clipping_planes: Vec<(Point3<f64>, Vector3<f64>, bool)>,
305}
306
307impl VoidContext {
308 fn is_noop(&self) -> bool {
309 self.openings.is_empty() && self.world_clipping_planes.is_empty()
310 }
311}
312
313impl GeometryRouter {
314 fn extract_extrusion_direction_from_solid(
321 &self,
322 solid: &DecodedEntity,
323 decoder: &mut EntityDecoder,
324 ) -> Option<(Vector3<f64>, Option<Matrix4<f64>>)> {
325 let direction_attr = solid.get(2)?;
327 let direction_entity = decoder.resolve_ref(direction_attr).ok()??;
328 let local_dir = self.parse_direction(&direction_entity).ok()?;
329
330 let position_transform = if let Some(pos_attr) = solid.get(1) {
332 if !pos_attr.is_null() {
333 if let Ok(Some(pos_entity)) = decoder.resolve_ref(pos_attr) {
334 if pos_entity.ifc_type == IfcType::IfcAxis2Placement3D {
335 self.parse_axis2_placement_3d(&pos_entity, decoder).ok()
336 } else {
337 None
338 }
339 } else {
340 None
341 }
342 } else {
343 None
344 }
345 } else {
346 None
347 };
348
349 Some((local_dir, position_transform))
350 }
351
352 fn extract_extrusion_direction_recursive(
356 &self,
357 item: &DecodedEntity,
358 decoder: &mut EntityDecoder,
359 ) -> Option<(Vector3<f64>, Option<Matrix4<f64>>)> {
360 let mut current = item.clone();
361 let mut visited = FxHashSet::default();
362 let mut mapping_chain: Option<Matrix4<f64>> = None;
363
364 for _depth in 0..MAX_EXTRUSION_EXTRACT_DEPTH {
365 if !visited.insert(current.id) {
366 return None;
367 }
368
369 match current.ifc_type {
370 IfcType::IfcExtrudedAreaSolid => {
371 let (dir, position_transform) =
372 self.extract_extrusion_direction_from_solid(¤t, decoder)?;
373 let combined = match (mapping_chain.as_ref(), position_transform) {
374 (Some(chain), Some(pos)) => Some(chain * pos),
375 (Some(chain), None) => Some(chain.clone()),
376 (None, Some(pos)) => Some(pos),
377 (None, None) => None,
378 };
379 return Some((dir, combined));
380 }
381 IfcType::IfcBooleanClippingResult | IfcType::IfcBooleanResult => {
382 let first_attr = current.get(1)?;
384 current = decoder.resolve_ref(first_attr).ok()??;
385 }
386 IfcType::IfcMappedItem => {
387 let source_attr = current.get(0)?;
389 let source = decoder.resolve_ref(source_attr).ok()??;
390 let rep_attr = source.get(1)?;
392 let rep = decoder.resolve_ref(rep_attr).ok()??;
393
394 if let Some(target_attr) = current.get(1) {
396 if !target_attr.is_null() {
397 if let Ok(Some(target)) = decoder.resolve_ref(target_attr) {
398 if let Ok(map) =
399 self.parse_cartesian_transformation_operator(&target, decoder)
400 {
401 mapping_chain = Some(match mapping_chain.take() {
402 Some(chain) => chain * map,
403 None => map,
404 });
405 }
406 }
407 }
408 }
409
410 let items_attr = rep.get(3)?;
412 let items = decoder.resolve_ref_list(items_attr).ok()?;
413 current = items.first()?.clone();
414 }
415 _ => return None,
416 }
417 }
418
419 None
420 }
421
422 pub fn get_opening_item_meshes_world(
426 &self,
427 element: &DecodedEntity,
428 decoder: &mut EntityDecoder,
429 ) -> Result<Vec<Mesh>> {
430 let representation_attr = element.get(6).ok_or_else(|| {
431 Error::geometry("Element has no representation attribute".to_string())
432 })?;
433 if representation_attr.is_null() {
434 return Ok(vec![]);
435 }
436
437 let representation = decoder
438 .resolve_ref(representation_attr)?
439 .ok_or_else(|| Error::geometry("Failed to resolve representation".to_string()))?;
440 let representations_attr = representation.get(2).ok_or_else(|| {
441 Error::geometry("ProductDefinitionShape missing Representations".to_string())
442 })?;
443 let representations = decoder.resolve_ref_list(representations_attr)?;
444
445 let mut placement_transform = self
447 .get_placement_transform_from_element(element, decoder)
448 .unwrap_or_else(|_| Matrix4::identity());
449 self.scale_transform(&mut placement_transform);
450
451 let mut item_meshes = Vec::new();
452
453 for shape_rep in representations {
454 if shape_rep.ifc_type != IfcType::IfcShapeRepresentation {
455 continue;
456 }
457 if let Some(rep_type_attr) = shape_rep.get(2) {
458 if let Some(rep_type) = rep_type_attr.as_string() {
459 if !is_body_representation(rep_type) {
460 continue;
461 }
462 }
463 }
464 let items_attr = match shape_rep.get(3) {
465 Some(attr) => attr,
466 None => continue,
467 };
468 let items = match decoder.resolve_ref_list(items_attr) {
469 Ok(items) => items,
470 Err(_) => continue,
471 };
472
473 for item in items {
474 let mut mesh = match self.process_representation_item(&item, decoder) {
475 Ok(m) if !m.is_empty() => m,
476 _ => continue,
477 };
478
479 self.transform_mesh_world(&mut mesh, &placement_transform);
482
483 item_meshes.push(mesh);
484 }
485 }
486
487 Ok(item_meshes)
488 }
489
490 pub fn get_opening_item_bounds_with_direction(
493 &self,
494 element: &DecodedEntity,
495 decoder: &mut EntityDecoder,
496 ) -> Result<Vec<(Point3<f64>, Point3<f64>, Option<Vector3<f64>>)>> {
497 let representation_attr = element.get(6).ok_or_else(|| {
499 Error::geometry("Element has no representation attribute".to_string())
500 })?;
501
502 if representation_attr.is_null() {
503 return Ok(vec![]);
504 }
505
506 let representation = decoder
507 .resolve_ref(representation_attr)?
508 .ok_or_else(|| Error::geometry("Failed to resolve representation".to_string()))?;
509
510 let representations_attr = representation.get(2).ok_or_else(|| {
512 Error::geometry("ProductDefinitionShape missing Representations".to_string())
513 })?;
514
515 let representations = decoder.resolve_ref_list(representations_attr)?;
516
517 let mut placement_transform = self
519 .get_placement_transform_from_element(element, decoder)
520 .unwrap_or_else(|_| Matrix4::identity());
521 self.scale_transform(&mut placement_transform);
522
523 let mut bounds_list = Vec::new();
524
525 for shape_rep in representations {
526 if shape_rep.ifc_type != IfcType::IfcShapeRepresentation {
527 continue;
528 }
529
530 if let Some(rep_type_attr) = shape_rep.get(2) {
532 if let Some(rep_type) = rep_type_attr.as_string() {
533 if !is_body_representation(rep_type) {
534 continue;
535 }
536 }
537 }
538
539 let items_attr = match shape_rep.get(3) {
541 Some(attr) => attr,
542 None => continue,
543 };
544
545 let items = match decoder.resolve_ref_list(items_attr) {
546 Ok(items) => items,
547 Err(_) => continue,
548 };
549
550 for item in items {
552 let extrusion_direction = if let Some((local_dir, position_transform)) =
554 self.extract_extrusion_direction_recursive(&item, decoder)
555 {
556 if let Some(pos_transform) = position_transform {
558 let pos_rot = extract_rotation_columns(&pos_transform);
559 let world_dir = rotate_and_normalize(&pos_rot, &local_dir)?;
560
561 let element_rot = extract_rotation_columns(&placement_transform);
562 let final_dir = rotate_and_normalize(&element_rot, &world_dir)?;
563
564 Some(final_dir)
565 } else {
566 let element_rot = extract_rotation_columns(&placement_transform);
567 let final_dir = rotate_and_normalize(&element_rot, &local_dir)?;
568
569 Some(final_dir)
570 }
571 } else {
572 None
573 };
574
575 let mesh = match self.process_representation_item(&item, decoder) {
577 Ok(m) if !m.is_empty() => m,
578 _ => continue,
579 };
580
581 let (mesh_min, mesh_max) = mesh.bounds();
583
584 let corners = [
586 Point3::new(mesh_min.x as f64, mesh_min.y as f64, mesh_min.z as f64),
587 Point3::new(mesh_max.x as f64, mesh_min.y as f64, mesh_min.z as f64),
588 Point3::new(mesh_min.x as f64, mesh_max.y as f64, mesh_min.z as f64),
589 Point3::new(mesh_max.x as f64, mesh_max.y as f64, mesh_min.z as f64),
590 Point3::new(mesh_min.x as f64, mesh_min.y as f64, mesh_max.z as f64),
591 Point3::new(mesh_max.x as f64, mesh_min.y as f64, mesh_max.z as f64),
592 Point3::new(mesh_min.x as f64, mesh_max.y as f64, mesh_max.z as f64),
593 Point3::new(mesh_max.x as f64, mesh_max.y as f64, mesh_max.z as f64),
594 ];
595
596 let transformed: Vec<Point3<f64>> = corners
598 .iter()
599 .map(|p| placement_transform.transform_point(p))
600 .collect();
601
602 let world_min = Point3::new(
603 transformed
604 .iter()
605 .map(|p| p.x)
606 .fold(f64::INFINITY, f64::min),
607 transformed
608 .iter()
609 .map(|p| p.y)
610 .fold(f64::INFINITY, f64::min),
611 transformed
612 .iter()
613 .map(|p| p.z)
614 .fold(f64::INFINITY, f64::min),
615 );
616 let world_max = Point3::new(
617 transformed
618 .iter()
619 .map(|p| p.x)
620 .fold(f64::NEG_INFINITY, f64::max),
621 transformed
622 .iter()
623 .map(|p| p.y)
624 .fold(f64::NEG_INFINITY, f64::max),
625 transformed
626 .iter()
627 .map(|p| p.z)
628 .fold(f64::NEG_INFINITY, f64::max),
629 );
630
631 let rtc = self.rtc_offset;
634 let rtc_min = Point3::new(
635 world_min.x - rtc.0,
636 world_min.y - rtc.1,
637 world_min.z - rtc.2,
638 );
639 let rtc_max = Point3::new(
640 world_max.x - rtc.0,
641 world_max.y - rtc.1,
642 world_max.z - rtc.2,
643 );
644
645 bounds_list.push((rtc_min, rtc_max, extrusion_direction));
646 }
647 }
648
649 Ok(bounds_list)
650 }
651
652 #[inline]
660 pub fn process_element_with_voids(
678 &self,
679 element: &DecodedEntity,
680 decoder: &mut EntityDecoder,
681 void_index: &FxHashMap<u32, Vec<u32>>,
682 ) -> Result<Mesh> {
683 let opening_ids = match void_index.get(&element.id) {
684 Some(ids) if !ids.is_empty() => ids,
685 _ => {
686 return self.process_element(element, decoder);
687 }
688 };
689
690 let wall_mesh = match self.process_element(element, decoder) {
691 Ok(m) => m,
692 Err(_) => {
693 return self.process_element(element, decoder);
694 }
695 };
696
697 Ok(self.apply_voids_to_mesh(wall_mesh, element, opening_ids, decoder))
698 }
699
700 pub(super) fn apply_voids_to_mesh(
713 &self,
714 mesh: Mesh,
715 element: &DecodedEntity,
716 opening_ids: &[u32],
717 decoder: &mut EntityDecoder,
718 ) -> Mesh {
719 let ctx = self.build_void_context(element, opening_ids, decoder);
720 self.apply_void_context(mesh, &ctx)
721 }
722
723 pub(super) fn build_void_context(
731 &self,
732 element: &DecodedEntity,
733 opening_ids: &[u32],
734 decoder: &mut EntityDecoder,
735 ) -> VoidContext {
736 let world_clipping_planes: Vec<(Point3<f64>, Vector3<f64>, bool)> =
737 if self.has_clipping_planes(element, decoder) {
738 let mut object_placement_transform =
739 match self.get_placement_transform_from_element(element, decoder) {
740 Ok(t) => t,
741 Err(_) => Matrix4::identity(),
742 };
743 self.scale_transform(&mut object_placement_transform);
744
745 let clipping_planes = match self.extract_base_profile_and_clips(element, decoder) {
746 Ok((_profile, _depth, _axis, _origin, _transform, clips)) => clips,
747 Err(_) => Vec::new(),
748 };
749
750 clipping_planes
751 .iter()
752 .map(|(point, normal, agreement)| {
753 let world_point = object_placement_transform.transform_point(point);
754 let rotation = object_placement_transform.fixed_view::<3, 3>(0, 0);
755 let world_normal = (rotation * normal).normalize();
756 (world_point, world_normal, *agreement)
757 })
758 .collect()
759 } else {
760 Vec::new()
761 };
762
763 let openings = self.classify_openings(opening_ids, decoder);
764 let merged_openings = Self::merge_rectangular_openings(&openings);
765
766 VoidContext {
767 openings,
768 merged_openings,
769 world_clipping_planes,
770 }
771 }
772
773 pub(super) fn apply_void_context(&self, mesh: Mesh, ctx: &VoidContext) -> Mesh {
782 if ctx.is_noop() {
783 return mesh;
784 }
785
786 let clipper = ClippingProcessor::new();
787 let mut result = mesh;
788
789 let (wall_min_f32, wall_max_f32) = result.bounds();
790 let wall_min = Point3::new(
791 wall_min_f32.x as f64,
792 wall_min_f32.y as f64,
793 wall_min_f32.z as f64,
794 );
795 let wall_max = Point3::new(
796 wall_max_f32.x as f64,
797 wall_max_f32.y as f64,
798 wall_max_f32.z as f64,
799 );
800
801 let wall_valid = !result.is_empty()
802 && result.positions.iter().all(|&v| v.is_finite())
803 && result.triangle_count() >= 4;
804
805 if !wall_valid {
806 return result;
807 }
808
809 let mut csg_operation_count = 0;
810 const MAX_CSG_OPERATIONS: usize = 10;
811
812 self.apply_diagonal_openings(&mut result, &ctx.openings, &wall_min, &wall_max);
813
814 let mut rect_boxes: Vec<(Point3<f64>, Point3<f64>)> = Vec::new();
815 let mut rect_dirs: Vec<Option<Vector3<f64>>> = Vec::new();
817 let mut non_rect_openings: Vec<&OpeningType> = Vec::new();
818
819 for opening in &ctx.merged_openings {
820 match opening {
821 OpeningType::Rectangular(open_min, open_max, extrusion_dir) => {
822 let (final_min, final_max) = if let Some(dir) = extrusion_dir {
823 self.extend_opening_along_direction(
824 *open_min, *open_max, wall_min, wall_max, *dir,
825 )
826 } else {
827 (*open_min, *open_max)
828 };
829 rect_boxes.push((final_min, final_max));
830 rect_dirs.push(*extrusion_dir);
831 }
832 other => {
833 non_rect_openings.push(other);
834 }
835 }
836 }
837
838 if !rect_boxes.is_empty() {
839 let (new_result, processed) =
840 self.cut_multiple_rectangular_openings(&result, &rect_boxes);
841 result = new_result;
842
843 for (i, (open_min, open_max)) in rect_boxes.iter().enumerate().take(processed) {
849 generate_reveal_quads(
850 &mut result,
851 open_min,
852 open_max,
853 &wall_min,
854 &wall_max,
855 rect_dirs[i].as_ref(),
856 );
857 }
858 }
859
860 for opening in &non_rect_openings {
861 match *opening {
862 OpeningType::Rectangular(..) | OpeningType::DiagonalRectangular(..) => {}
863 OpeningType::NonRectangular(ref opening_mesh) => {
864 if csg_operation_count >= MAX_CSG_OPERATIONS {
865 continue;
866 }
867
868 let opening_valid = !opening_mesh.is_empty()
869 && opening_mesh.positions.iter().all(|&v| v.is_finite())
870 && opening_mesh.positions.len() >= 9;
871
872 if !opening_valid {
873 continue;
874 }
875
876 let (result_min, result_max) = result.bounds();
877 let (open_min_f32, open_max_f32) = opening_mesh.bounds();
878 let no_overlap = open_max_f32.x < result_min.x
879 || open_min_f32.x > result_max.x
880 || open_max_f32.y < result_min.y
881 || open_min_f32.y > result_max.y
882 || open_max_f32.z < result_min.z
883 || open_min_f32.z > result_max.z;
884 if no_overlap {
885 continue;
886 }
887
888 let open_vol = (open_max_f32.x - open_min_f32.x)
889 * (open_max_f32.y - open_min_f32.y)
890 * (open_max_f32.z - open_min_f32.z);
891 if open_vol < MIN_OPENING_VOLUME as f32 {
892 continue;
893 }
894
895 let tri_before = result.triangle_count();
896 match clipper.subtract_mesh(&result, opening_mesh) {
897 Ok(csg_result) => {
898 let min_tris = (tri_before / CSG_TRIANGLE_RETENTION_DIVISOR)
899 .max(MIN_VALID_TRIANGLES);
900 if !csg_result.is_empty() && csg_result.triangle_count() >= min_tris {
901 result = csg_result;
902 }
903 }
904 Err(_) => {}
905 }
906 csg_operation_count += 1;
907 }
908 }
909 }
910
911 if !ctx.world_clipping_planes.is_empty() {
912 for (plane_point, plane_normal, agreement) in &ctx.world_clipping_planes {
913 let clip_normal = if *agreement {
914 *plane_normal
915 } else {
916 -*plane_normal
917 };
918
919 let plane = Plane::new(*plane_point, clip_normal);
920 if let Ok(clipped) = clipper.clip_mesh(&result, &plane) {
921 if !clipped.is_empty() {
922 result = clipped;
923 }
924 }
925 }
926 }
927
928 result
929 }
930
931 pub fn process_element_with_submeshes_and_voids(
944 &self,
945 element: &DecodedEntity,
946 decoder: &mut EntityDecoder,
947 void_index: &FxHashMap<u32, Vec<u32>>,
948 ) -> Result<SubMeshCollection> {
949 if let Some(layered) = self.try_layered_sub_meshes(element, decoder, Some(void_index)) {
955 return Ok(layered);
956 }
957
958 let opening_ids = match void_index.get(&element.id) {
959 Some(ids) if !ids.is_empty() => ids.clone(),
960 _ => return Ok(SubMeshCollection::new()),
961 };
962
963 let sub_meshes = self.process_element_with_submeshes(element, decoder)?;
964 if sub_meshes.is_empty() {
965 return Ok(SubMeshCollection::new());
966 }
967
968 let ctx = self.build_void_context(element, &opening_ids, decoder);
974
975 let mut voided = SubMeshCollection::new();
976 for sub in sub_meshes.sub_meshes {
977 let geometry_id = sub.geometry_id;
978 let voided_mesh = self.apply_void_context(sub.mesh, &ctx);
979 if !voided_mesh.is_empty() {
980 voided.sub_meshes.push(SubMesh::new(geometry_id, voided_mesh));
981 }
982 }
983
984 Ok(voided)
985 }
986
987 fn classify_openings(
988 &self,
989 opening_ids: &[u32],
990 decoder: &mut EntityDecoder,
991 ) -> Vec<OpeningType> {
992 let mut openings: Vec<OpeningType> = Vec::new();
993 for &opening_id in opening_ids.iter() {
994 let opening_entity = match decoder.decode_by_id(opening_id) {
995 Ok(e) => e,
996 Err(_) => continue,
997 };
998
999 let opening_mesh = match self.process_element(&opening_entity, decoder) {
1000 Ok(m) if !m.is_empty() => m,
1001 _ => continue,
1002 };
1003
1004 let vertex_count = opening_mesh.positions.len() / 3;
1005
1006 if vertex_count > 100 {
1007 openings.push(OpeningType::NonRectangular(opening_mesh));
1008 } else {
1009 let item_bounds_with_dir = self
1010 .get_opening_item_bounds_with_direction(&opening_entity, decoder)
1011 .unwrap_or_default();
1012
1013 if !item_bounds_with_dir.is_empty() {
1014 let is_floor_opening = item_bounds_with_dir
1015 .iter()
1016 .any(|(_, _, dir)| dir.map(|d| d.z.abs() > 0.95).unwrap_or(false));
1017
1018 if is_floor_opening && vertex_count > 0 {
1019 openings.push(OpeningType::NonRectangular(opening_mesh.clone()));
1020 } else {
1021 let any_diagonal = item_bounds_with_dir.iter().any(|(_, _, dir)| {
1022 dir.map(|d| {
1023 const AXIS_THRESHOLD: f64 = 0.95;
1024 let abs_x = d.x.abs();
1025 let abs_y = d.y.abs();
1026 let abs_z = d.z.abs();
1027 !(abs_x > AXIS_THRESHOLD
1028 || abs_y > AXIS_THRESHOLD
1029 || abs_z > AXIS_THRESHOLD)
1030 })
1031 .unwrap_or(false)
1032 });
1033
1034 if any_diagonal {
1035 if let Some(dir) = item_bounds_with_dir.iter().find_map(|(_, _, d)| *d)
1038 {
1039 let item_meshes = self
1040 .get_opening_item_meshes_world(&opening_entity, decoder)
1041 .unwrap_or_default();
1042 if item_meshes.is_empty() {
1043 openings.push(OpeningType::DiagonalRectangular(
1044 opening_mesh.clone(),
1045 dir,
1046 ));
1047 } else {
1048 for item_mesh in item_meshes {
1049 openings
1050 .push(OpeningType::DiagonalRectangular(item_mesh, dir));
1051 }
1052 }
1053 } else {
1054 openings.push(OpeningType::NonRectangular(opening_mesh.clone()));
1056 }
1057 } else {
1058 for (min_pt, max_pt, extrusion_dir) in item_bounds_with_dir {
1059 openings.push(OpeningType::Rectangular(
1060 min_pt,
1061 max_pt,
1062 extrusion_dir,
1063 ));
1064 }
1065 }
1066 }
1067 } else {
1068 let (open_min, open_max) = opening_mesh.bounds();
1069 let min_f64 =
1070 Point3::new(open_min.x as f64, open_min.y as f64, open_min.z as f64);
1071 let max_f64 =
1072 Point3::new(open_max.x as f64, open_max.y as f64, open_max.z as f64);
1073
1074 openings.push(OpeningType::Rectangular(min_f64, max_f64, None));
1075 }
1076 }
1077 }
1078 openings
1079 }
1080
1081 fn merge_rectangular_openings(openings: &[OpeningType]) -> Vec<OpeningType> {
1086 const MERGE_TOLERANCE: f64 = 0.01; let mut rects: Vec<(Point3<f64>, Point3<f64>, Option<Vector3<f64>>)> = Vec::new();
1090 let mut others: Vec<OpeningType> = Vec::new();
1091
1092 for opening in openings {
1093 match opening {
1094 OpeningType::Rectangular(min, max, dir) => {
1095 rects.push((*min, *max, *dir));
1096 }
1097 other => others.push(other.clone()),
1098 }
1099 }
1100
1101 let mut merged = true;
1103 while merged {
1104 merged = false;
1105 let mut i = 0;
1106 while i < rects.len() {
1107 let mut j = i + 1;
1108 while j < rects.len() {
1109 let (a_min, a_max, _) = &rects[i];
1110 let (b_min, b_max, _) = &rects[j];
1111
1112 let overlaps_x = a_min.x <= b_max.x + MERGE_TOLERANCE
1114 && a_max.x >= b_min.x - MERGE_TOLERANCE;
1115 let overlaps_y = a_min.y <= b_max.y + MERGE_TOLERANCE
1116 && a_max.y >= b_min.y - MERGE_TOLERANCE;
1117 let overlaps_z = a_min.z <= b_max.z + MERGE_TOLERANCE
1118 && a_max.z >= b_min.z - MERGE_TOLERANCE;
1119
1120 let dirs_compatible = match (&rects[i].2, &rects[j].2) {
1122 (Some(a), Some(b)) => {
1123 let dot = a.x * b.x + a.y * b.y + a.z * b.z;
1124 dot.abs() > 0.99 }
1126 (None, None) => true,
1127 _ => false, };
1129
1130 if overlaps_x && overlaps_y && overlaps_z && dirs_compatible {
1131 let dir = rects[i].2;
1133 rects[i] = (
1134 Point3::new(
1135 a_min.x.min(b_min.x),
1136 a_min.y.min(b_min.y),
1137 a_min.z.min(b_min.z),
1138 ),
1139 Point3::new(
1140 a_max.x.max(b_max.x),
1141 a_max.y.max(b_max.y),
1142 a_max.z.max(b_max.z),
1143 ),
1144 dir,
1145 );
1146 rects.remove(j);
1147 merged = true;
1148 } else {
1149 j += 1;
1150 }
1151 }
1152 i += 1;
1153 }
1154 }
1155
1156 let mut result: Vec<OpeningType> = rects
1158 .into_iter()
1159 .map(|(min, max, dir)| OpeningType::Rectangular(min, max, dir))
1160 .collect();
1161 result.extend(others);
1162 result
1163 }
1164
1165 fn apply_diagonal_openings(
1166 &self,
1167 result: &mut Mesh,
1168 openings: &[OpeningType],
1169 wall_min: &Point3<f64>,
1170 wall_max: &Point3<f64>,
1171 ) {
1172 use nalgebra::Rotation3;
1173
1174 let diagonal_openings: Vec<(&Mesh, &Vector3<f64>)> = openings
1175 .iter()
1176 .filter_map(|o| match o {
1177 OpeningType::DiagonalRectangular(mesh, dir) => Some((mesh, dir)),
1178 _ => None,
1179 })
1180 .collect();
1181
1182 if diagonal_openings.is_empty() {
1183 return;
1184 }
1185
1186 const DIR_DOT_THRESHOLD: f64 = 0.9998; let mut groups: Vec<(Vector3<f64>, Vec<&Mesh>)> = Vec::new();
1191 for (mesh, dir) in &diagonal_openings {
1192 let d = *dir;
1193 if let Some(group) = groups
1194 .iter_mut()
1195 .find(|(g, _)| d.dot(g).abs() > DIR_DOT_THRESHOLD)
1196 {
1197 group.1.push(mesh);
1198 } else {
1199 groups.push((*d, vec![mesh]));
1200 }
1201 }
1202
1203 let wall_corners = [
1204 Point3::new(wall_min.x, wall_min.y, wall_min.z),
1205 Point3::new(wall_max.x, wall_min.y, wall_min.z),
1206 Point3::new(wall_min.x, wall_max.y, wall_min.z),
1207 Point3::new(wall_max.x, wall_max.y, wall_min.z),
1208 Point3::new(wall_min.x, wall_min.y, wall_max.z),
1209 Point3::new(wall_max.x, wall_min.y, wall_max.z),
1210 Point3::new(wall_min.x, wall_max.y, wall_max.z),
1211 Point3::new(wall_max.x, wall_max.y, wall_max.z),
1212 ];
1213
1214 for (extrusion_dir, group_meshes) in &groups {
1215 let target = Vector3::new(1.0, 0.0, 0.0);
1216 let rotation = Rotation3::rotation_between(extrusion_dir, &target)
1217 .unwrap_or(Rotation3::identity());
1218 let inv_rotation = rotation.inverse();
1219
1220 for chunk in result.positions.chunks_exact_mut(3) {
1222 let p = rotation * Point3::new(chunk[0] as f64, chunk[1] as f64, chunk[2] as f64);
1223 chunk[0] = p.x as f32;
1224 chunk[1] = p.y as f32;
1225 chunk[2] = p.z as f32;
1226 }
1227 for chunk in result.normals.chunks_exact_mut(3) {
1228 let n = rotation * Vector3::new(chunk[0] as f64, chunk[1] as f64, chunk[2] as f64);
1229 chunk[0] = n.x as f32;
1230 chunk[1] = n.y as f32;
1231 chunk[2] = n.z as f32;
1232 }
1233
1234 let mut rot_wall_min =
1236 Point3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
1237 let mut rot_wall_max =
1238 Point3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
1239 for wc in &wall_corners {
1240 let rwc = rotation * wc;
1241 rot_wall_min.x = rot_wall_min.x.min(rwc.x);
1242 rot_wall_min.y = rot_wall_min.y.min(rwc.y);
1243 rot_wall_min.z = rot_wall_min.z.min(rwc.z);
1244 rot_wall_max.x = rot_wall_max.x.max(rwc.x);
1245 rot_wall_max.y = rot_wall_max.y.max(rwc.y);
1246 rot_wall_max.z = rot_wall_max.z.max(rwc.z);
1247 }
1248
1249 for opening_mesh in group_meshes {
1250 let mut rot_min = Point3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
1251 let mut rot_max =
1252 Point3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
1253 for chunk in opening_mesh.positions.chunks_exact(3) {
1254 let p =
1255 rotation * Point3::new(chunk[0] as f64, chunk[1] as f64, chunk[2] as f64);
1256 rot_min.x = rot_min.x.min(p.x);
1257 rot_min.y = rot_min.y.min(p.y);
1258 rot_min.z = rot_min.z.min(p.z);
1259 rot_max.x = rot_max.x.max(p.x);
1260 rot_max.y = rot_max.y.max(p.y);
1261 rot_max.z = rot_max.z.max(p.z);
1262 }
1263 rot_min.x = rot_min.x.min(rot_wall_min.x);
1264 rot_max.x = rot_max.x.max(rot_wall_max.x);
1265
1266 *result = self.cut_rectangular_opening_no_faces(result, rot_min, rot_max);
1267
1268 let x_dir = Vector3::new(1.0, 0.0, 0.0);
1271 generate_reveal_quads(
1272 result,
1273 &rot_min,
1274 &rot_max,
1275 &rot_wall_min,
1276 &rot_wall_max,
1277 Some(&x_dir),
1278 );
1279 }
1280
1281 for chunk in result.positions.chunks_exact_mut(3) {
1283 let p =
1284 inv_rotation * Point3::new(chunk[0] as f64, chunk[1] as f64, chunk[2] as f64);
1285 chunk[0] = p.x as f32;
1286 chunk[1] = p.y as f32;
1287 chunk[2] = p.z as f32;
1288 }
1289 for chunk in result.normals.chunks_exact_mut(3) {
1290 let n =
1291 inv_rotation * Vector3::new(chunk[0] as f64, chunk[1] as f64, chunk[2] as f64);
1292 chunk[0] = n.x as f32;
1293 chunk[1] = n.y as f32;
1294 chunk[2] = n.z as f32;
1295 }
1296 }
1297 }
1298
1299 fn extend_opening_along_direction(
1310 &self,
1311 open_min: Point3<f64>,
1312 open_max: Point3<f64>,
1313 wall_min: Point3<f64>,
1314 wall_max: Point3<f64>,
1315 extrusion_direction: Vector3<f64>, ) -> (Point3<f64>, Point3<f64>) {
1317 let open_center = Point3::new(
1319 (open_min.x + open_max.x) * 0.5,
1320 (open_min.y + open_max.y) * 0.5,
1321 (open_min.z + open_max.z) * 0.5,
1322 );
1323
1324 let wall_corners = [
1326 Point3::new(wall_min.x, wall_min.y, wall_min.z),
1327 Point3::new(wall_max.x, wall_min.y, wall_min.z),
1328 Point3::new(wall_min.x, wall_max.y, wall_min.z),
1329 Point3::new(wall_max.x, wall_max.y, wall_min.z),
1330 Point3::new(wall_min.x, wall_min.y, wall_max.z),
1331 Point3::new(wall_max.x, wall_min.y, wall_max.z),
1332 Point3::new(wall_min.x, wall_max.y, wall_max.z),
1333 Point3::new(wall_max.x, wall_max.y, wall_max.z),
1334 ];
1335
1336 let mut wall_min_proj = f64::INFINITY;
1338 let mut wall_max_proj = f64::NEG_INFINITY;
1339
1340 for corner in &wall_corners {
1341 let proj = (corner - open_center).dot(&extrusion_direction);
1343 wall_min_proj = wall_min_proj.min(proj);
1344 wall_max_proj = wall_max_proj.max(proj);
1345 }
1346
1347 let open_corners = [
1349 Point3::new(open_min.x, open_min.y, open_min.z),
1350 Point3::new(open_max.x, open_min.y, open_min.z),
1351 Point3::new(open_min.x, open_max.y, open_min.z),
1352 Point3::new(open_max.x, open_max.y, open_min.z),
1353 Point3::new(open_min.x, open_min.y, open_max.z),
1354 Point3::new(open_max.x, open_min.y, open_max.z),
1355 Point3::new(open_min.x, open_max.y, open_max.z),
1356 Point3::new(open_max.x, open_max.y, open_max.z),
1357 ];
1358
1359 let mut open_min_proj = f64::INFINITY;
1360 let mut open_max_proj = f64::NEG_INFINITY;
1361
1362 for corner in &open_corners {
1363 let proj = (corner - open_center).dot(&extrusion_direction);
1364 open_min_proj = open_min_proj.min(proj);
1365 open_max_proj = open_max_proj.max(proj);
1366 }
1367
1368 let extend_backward = (open_min_proj - wall_min_proj).max(0.0); let extend_forward = (wall_max_proj - open_max_proj).max(0.0); let extended_min = open_min - extrusion_direction * extend_backward;
1375 let extended_max = open_max + extrusion_direction * extend_forward;
1376
1377 let all_points = [open_min, open_max, extended_min, extended_max];
1380
1381 let new_min = Point3::new(
1382 all_points.iter().map(|p| p.x).fold(f64::INFINITY, f64::min),
1383 all_points.iter().map(|p| p.y).fold(f64::INFINITY, f64::min),
1384 all_points.iter().map(|p| p.z).fold(f64::INFINITY, f64::min),
1385 );
1386 let new_max = Point3::new(
1387 all_points
1388 .iter()
1389 .map(|p| p.x)
1390 .fold(f64::NEG_INFINITY, f64::max),
1391 all_points
1392 .iter()
1393 .map(|p| p.y)
1394 .fold(f64::NEG_INFINITY, f64::max),
1395 all_points
1396 .iter()
1397 .map(|p| p.z)
1398 .fold(f64::NEG_INFINITY, f64::max),
1399 );
1400
1401 (new_min, new_max)
1402 }
1403
1404 fn cut_multiple_rectangular_openings(
1416 &self,
1417 mesh: &Mesh,
1418 boxes: &[(Point3<f64>, Point3<f64>)],
1419 ) -> (Mesh, usize) {
1420 let mut current = mesh.clone();
1421
1422 const MAX_TRIANGLES: usize = 500_000;
1436
1437 let mut processed = 0;
1438 for (open_min, open_max) in boxes.iter() {
1439 if current.indices.len() / 3 > MAX_TRIANGLES {
1440 break;
1441 }
1442 current = self.cut_rectangular_opening(¤t, *open_min, *open_max);
1443 processed += 1;
1444 }
1445
1446 (current, processed)
1447 }
1448
1449 pub(super) fn cut_rectangular_opening(
1450 &self,
1451 mesh: &Mesh,
1452 open_min: Point3<f64>,
1453 open_max: Point3<f64>,
1454 ) -> Mesh {
1455 self.cut_rectangular_opening_no_faces(mesh, open_min, open_max)
1456 }
1457
1458 fn cut_rectangular_opening_no_faces(
1461 &self,
1462 mesh: &Mesh,
1463 open_min: Point3<f64>,
1464 open_max: Point3<f64>,
1465 ) -> Mesh {
1466 use nalgebra::Vector3;
1467
1468 const EPSILON: f64 = 1e-6;
1469
1470 let mut result = Mesh::with_capacity(mesh.positions.len() / 3, mesh.indices.len() / 3);
1471
1472 let mut clip_buffers = ClipBuffers::new();
1473
1474 let num_vertices = mesh.positions.len() / 3;
1475 for chunk in mesh.indices.chunks_exact(3) {
1476 let i0 = chunk[0] as usize;
1477 let i1 = chunk[1] as usize;
1478 let i2 = chunk[2] as usize;
1479
1480 if i0 >= num_vertices || i1 >= num_vertices || i2 >= num_vertices {
1482 continue;
1483 }
1484
1485 let v0 = Point3::new(
1486 mesh.positions[i0 * 3] as f64,
1487 mesh.positions[i0 * 3 + 1] as f64,
1488 mesh.positions[i0 * 3 + 2] as f64,
1489 );
1490 let v1 = Point3::new(
1491 mesh.positions[i1 * 3] as f64,
1492 mesh.positions[i1 * 3 + 1] as f64,
1493 mesh.positions[i1 * 3 + 2] as f64,
1494 );
1495 let v2 = Point3::new(
1496 mesh.positions[i2 * 3] as f64,
1497 mesh.positions[i2 * 3 + 1] as f64,
1498 mesh.positions[i2 * 3 + 2] as f64,
1499 );
1500
1501 let n0 = if mesh.normals.len() >= mesh.positions.len() {
1502 Vector3::new(
1503 mesh.normals[i0 * 3] as f64,
1504 mesh.normals[i0 * 3 + 1] as f64,
1505 mesh.normals[i0 * 3 + 2] as f64,
1506 )
1507 } else {
1508 let edge1 = v1 - v0;
1509 let edge2 = v2 - v0;
1510 edge1
1511 .cross(&edge2)
1512 .try_normalize(1e-10)
1513 .unwrap_or(Vector3::new(0.0, 0.0, 1.0))
1514 };
1515
1516 let tri_min_x = v0.x.min(v1.x).min(v2.x);
1517 let tri_max_x = v0.x.max(v1.x).max(v2.x);
1518 let tri_min_y = v0.y.min(v1.y).min(v2.y);
1519 let tri_max_y = v0.y.max(v1.y).max(v2.y);
1520 let tri_min_z = v0.z.min(v1.z).min(v2.z);
1521 let tri_max_z = v0.z.max(v1.z).max(v2.z);
1522
1523 if tri_max_x <= open_min.x - EPSILON
1525 || tri_min_x >= open_max.x + EPSILON
1526 || tri_max_y <= open_min.y - EPSILON
1527 || tri_min_y >= open_max.y + EPSILON
1528 || tri_max_z <= open_min.z - EPSILON
1529 || tri_min_z >= open_max.z + EPSILON
1530 {
1531 let base = result.vertex_count() as u32;
1532 result.add_vertex(v0, n0);
1533 result.add_vertex(v1, n0);
1534 result.add_vertex(v2, n0);
1535 result.add_triangle(base, base + 1, base + 2);
1536 continue;
1537 }
1538
1539 if tri_min_x >= open_min.x + EPSILON
1541 && tri_max_x <= open_max.x - EPSILON
1542 && tri_min_y >= open_min.y + EPSILON
1543 && tri_max_y <= open_max.y - EPSILON
1544 && tri_min_z >= open_min.z + EPSILON
1545 && tri_max_z <= open_max.z - EPSILON
1546 {
1547 continue;
1548 }
1549
1550 if self.triangle_intersects_box(&v0, &v1, &v2, &open_min, &open_max) {
1552 self.clip_triangle_against_box(
1553 &mut result,
1554 &mut clip_buffers,
1555 &v0,
1556 &v1,
1557 &v2,
1558 &n0,
1559 &open_min,
1560 &open_max,
1561 );
1562 } else {
1563 let base = result.vertex_count() as u32;
1564 result.add_vertex(v0, n0);
1565 result.add_vertex(v1, n0);
1566 result.add_vertex(v2, n0);
1567 result.add_triangle(base, base + 1, base + 2);
1568 }
1569 }
1570
1571 result
1573 }
1574
1575 fn triangle_intersects_box(
1578 &self,
1579 v0: &Point3<f64>,
1580 v1: &Point3<f64>,
1581 v2: &Point3<f64>,
1582 box_min: &Point3<f64>,
1583 box_max: &Point3<f64>,
1584 ) -> bool {
1585 use nalgebra::Vector3;
1586
1587 let box_center = Point3::new(
1589 (box_min.x + box_max.x) * 0.5,
1590 (box_min.y + box_max.y) * 0.5,
1591 (box_min.z + box_max.z) * 0.5,
1592 );
1593 let box_half_extents = Vector3::new(
1594 (box_max.x - box_min.x) * 0.5,
1595 (box_max.y - box_min.y) * 0.5,
1596 (box_max.z - box_min.z) * 0.5,
1597 );
1598
1599 let t0 = v0 - box_center;
1601 let t1 = v1 - box_center;
1602 let t2 = v2 - box_center;
1603
1604 let e0 = t1 - t0;
1606 let e1 = t2 - t1;
1607 let e2 = t0 - t2;
1608
1609 for axis_idx in 0..3 {
1612 let axis = match axis_idx {
1613 0 => Vector3::new(1.0, 0.0, 0.0),
1614 1 => Vector3::new(0.0, 1.0, 0.0),
1615 2 => Vector3::new(0.0, 0.0, 1.0),
1616 _ => unreachable!(),
1617 };
1618
1619 let p0 = t0.dot(&axis);
1620 let p1 = t1.dot(&axis);
1621 let p2 = t2.dot(&axis);
1622
1623 let tri_min = p0.min(p1).min(p2);
1624 let tri_max = p0.max(p1).max(p2);
1625 let box_extent = box_half_extents[axis_idx];
1626
1627 if tri_max < -box_extent || tri_min > box_extent {
1628 return false; }
1630 }
1631
1632 let triangle_normal = e0.cross(&e2);
1634 let triangle_offset = t0.dot(&triangle_normal);
1635
1636 let mut box_projection = 0.0;
1638 for i in 0..3 {
1639 let axis = match i {
1640 0 => Vector3::new(1.0, 0.0, 0.0),
1641 1 => Vector3::new(0.0, 1.0, 0.0),
1642 2 => Vector3::new(0.0, 0.0, 1.0),
1643 _ => unreachable!(),
1644 };
1645 box_projection += box_half_extents[i] * triangle_normal.dot(&axis).abs();
1646 }
1647
1648 if triangle_offset.abs() > box_projection {
1649 return false; }
1651
1652 let box_axes = [
1654 Vector3::new(1.0, 0.0, 0.0),
1655 Vector3::new(0.0, 1.0, 0.0),
1656 Vector3::new(0.0, 0.0, 1.0),
1657 ];
1658 let tri_edges = [e0, e1, e2];
1659
1660 for box_axis in &box_axes {
1661 for tri_edge in &tri_edges {
1662 let axis = box_axis.cross(tri_edge);
1663
1664 if axis.norm_squared() < 1e-10 {
1666 continue;
1667 }
1668
1669 let axis_normalized = axis.normalize();
1670
1671 let p0 = t0.dot(&axis_normalized);
1673 let p1 = t1.dot(&axis_normalized);
1674 let p2 = t2.dot(&axis_normalized);
1675 let tri_min = p0.min(p1).min(p2);
1676 let tri_max = p0.max(p1).max(p2);
1677
1678 let mut box_projection = 0.0;
1680 for i in 0..3 {
1681 let box_axis_vec = box_axes[i];
1682 box_projection +=
1683 box_half_extents[i] * axis_normalized.dot(&box_axis_vec).abs();
1684 }
1685
1686 if tri_max < -box_projection || tri_min > box_projection {
1687 return false; }
1689 }
1690 }
1691
1692 true
1694 }
1695
1696 fn clip_triangle_against_box(
1713 &self,
1714 result: &mut Mesh,
1715 buffers: &mut ClipBuffers,
1716 v0: &Point3<f64>,
1717 v1: &Point3<f64>,
1718 v2: &Point3<f64>,
1719 normal: &Vector3<f64>,
1720 open_min: &Point3<f64>,
1721 open_max: &Point3<f64>,
1722 ) {
1723 let clipper = ClippingProcessor::new();
1724 let epsilon = clipper.epsilon;
1725
1726 buffers.clear();
1728
1729 let planes = [
1732 Plane::new(
1734 Point3::new(open_min.x, 0.0, 0.0),
1735 Vector3::new(1.0, 0.0, 0.0),
1736 ),
1737 Plane::new(
1739 Point3::new(open_max.x, 0.0, 0.0),
1740 Vector3::new(-1.0, 0.0, 0.0),
1741 ),
1742 Plane::new(
1744 Point3::new(0.0, open_min.y, 0.0),
1745 Vector3::new(0.0, 1.0, 0.0),
1746 ),
1747 Plane::new(
1749 Point3::new(0.0, open_max.y, 0.0),
1750 Vector3::new(0.0, -1.0, 0.0),
1751 ),
1752 Plane::new(
1754 Point3::new(0.0, 0.0, open_min.z),
1755 Vector3::new(0.0, 0.0, 1.0),
1756 ),
1757 Plane::new(
1759 Point3::new(0.0, 0.0, open_max.z),
1760 Vector3::new(0.0, 0.0, -1.0),
1761 ),
1762 ];
1763
1764 if !v0.x.is_finite()
1766 || !v0.y.is_finite()
1767 || !v0.z.is_finite()
1768 || !v1.x.is_finite()
1769 || !v1.y.is_finite()
1770 || !v1.z.is_finite()
1771 || !v2.x.is_finite()
1772 || !v2.y.is_finite()
1773 || !v2.z.is_finite()
1774 {
1775 let base = result.vertex_count() as u32;
1777 result.add_vertex(*v0, *normal);
1778 result.add_vertex(*v1, *normal);
1779 result.add_vertex(*v2, *normal);
1780 result.add_triangle(base, base + 1, base + 2);
1781 return;
1782 }
1783 buffers.remaining.push(Triangle::new(*v0, *v1, *v2));
1785
1786 for plane in &planes {
1788 buffers.next_remaining.clear();
1789
1790 for tri in &buffers.remaining {
1791 let d0 = plane.signed_distance(&tri.v0);
1793 let d1 = plane.signed_distance(&tri.v1);
1794 let d2 = plane.signed_distance(&tri.v2);
1795
1796 if !d0.is_finite() || !d1.is_finite() || !d2.is_finite() {
1798 buffers.result.push(tri.clone()); continue;
1800 }
1801
1802 let f0 = d0 >= -epsilon;
1803 let f1 = d1 >= -epsilon;
1804 let f2 = d2 >= -epsilon;
1805 let front_count = f0 as u8 + f1 as u8 + f2 as u8;
1806
1807 match front_count {
1808 3 => {
1809 buffers.next_remaining.push(tri.clone());
1810 }
1811 0 => {
1812 buffers.result.push(tri.clone());
1813 }
1814 1 => {
1815 let (front, back1, back2, d_f, d_b1, d_b2) = if f0 {
1816 (tri.v0, tri.v1, tri.v2, d0, d1, d2)
1817 } else if f1 {
1818 (tri.v1, tri.v2, tri.v0, d1, d2, d0)
1819 } else {
1820 (tri.v2, tri.v0, tri.v1, d2, d0, d1)
1821 };
1822
1823 let denom1 = d_f - d_b1;
1824 let denom2 = d_f - d_b2;
1825 if denom1.abs() < 1e-12 || denom2.abs() < 1e-12 {
1826 buffers.next_remaining.push(tri.clone());
1827 continue;
1828 }
1829 let t1 = (d_f / denom1).clamp(0.0, 1.0);
1830 let t2 = (d_f / denom2).clamp(0.0, 1.0);
1831 let p1 = front + (back1 - front) * t1;
1832 let p2 = front + (back2 - front) * t2;
1833
1834 if !p1.x.is_finite()
1836 || !p1.y.is_finite()
1837 || !p1.z.is_finite()
1838 || !p2.x.is_finite()
1839 || !p2.y.is_finite()
1840 || !p2.z.is_finite()
1841 {
1842 buffers.next_remaining.push(tri.clone());
1843 continue;
1844 }
1845
1846 buffers.next_remaining.push(Triangle::new(front, p1, p2));
1847 buffers.result.push(Triangle::new(p1, back1, back2));
1848 buffers.result.push(Triangle::new(p1, back2, p2));
1849 }
1850 2 => {
1851 let (front1, front2, back, d_f1, d_f2, d_b) = if !f0 {
1852 (tri.v1, tri.v2, tri.v0, d1, d2, d0)
1853 } else if !f1 {
1854 (tri.v2, tri.v0, tri.v1, d2, d0, d1)
1855 } else {
1856 (tri.v0, tri.v1, tri.v2, d0, d1, d2)
1857 };
1858
1859 let denom1 = d_f1 - d_b;
1860 let denom2 = d_f2 - d_b;
1861 if denom1.abs() < 1e-12 || denom2.abs() < 1e-12 {
1862 buffers.next_remaining.push(tri.clone());
1863 continue;
1864 }
1865 let t1 = (d_f1 / denom1).clamp(0.0, 1.0);
1866 let t2 = (d_f2 / denom2).clamp(0.0, 1.0);
1867 let p1 = front1 + (back - front1) * t1;
1868 let p2 = front2 + (back - front2) * t2;
1869
1870 if !p1.x.is_finite()
1872 || !p1.y.is_finite()
1873 || !p1.z.is_finite()
1874 || !p2.x.is_finite()
1875 || !p2.y.is_finite()
1876 || !p2.z.is_finite()
1877 {
1878 buffers.next_remaining.push(tri.clone());
1879 continue;
1880 }
1881
1882 buffers
1883 .next_remaining
1884 .push(Triangle::new(front1, front2, p1));
1885 buffers.next_remaining.push(Triangle::new(front2, p2, p1));
1886 buffers.result.push(Triangle::new(p1, p2, back));
1887 }
1888 _ => {
1889 buffers.result.push(tri.clone());
1891 }
1892 }
1893 }
1894
1895 std::mem::swap(&mut buffers.remaining, &mut buffers.next_remaining);
1897 }
1898
1899 for tri in &buffers.result {
1902 let base = result.vertex_count() as u32;
1903 result.add_vertex(tri.v0, *normal);
1904 result.add_vertex(tri.v1, *normal);
1905 result.add_vertex(tri.v2, *normal);
1906 result.add_triangle(base, base + 1, base + 2);
1907 }
1908 }
1909}
1910
1911#[cfg(test)]
1916mod reveal_tests {
1917 use super::*;
1918 use crate::Mesh;
1919
1920 #[allow(dead_code)]
1922 fn make_box_mesh(
1923 min: Point3<f64>,
1924 max: Point3<f64>,
1925 ) -> Mesh {
1926 let mut m = Mesh::with_capacity(24, 36);
1927
1928 let corners = [
1929 Point3::new(min.x, min.y, min.z), Point3::new(max.x, min.y, min.z), Point3::new(max.x, max.y, min.z), Point3::new(min.x, max.y, min.z), Point3::new(min.x, min.y, max.z), Point3::new(max.x, min.y, max.z), Point3::new(max.x, max.y, max.z), Point3::new(min.x, max.y, max.z), ];
1938
1939 let faces: [(Vector3<f64>, [usize; 4]); 6] = [
1941 (Vector3::new(0.0, 0.0, -1.0), [0, 2, 1, 3]), (Vector3::new(0.0, 0.0, 1.0), [4, 5, 6, 7]), (Vector3::new(0.0, -1.0, 0.0), [0, 1, 5, 4]), (Vector3::new(0.0, 1.0, 0.0), [2, 3, 7, 6]), (Vector3::new(-1.0, 0.0, 0.0), [0, 4, 7, 3]), (Vector3::new(1.0, 0.0, 0.0), [1, 2, 6, 5]), ];
1948 for (n, idx) in &faces {
1949 let b = m.vertex_count() as u32;
1950 m.add_vertex(corners[idx[0]], *n);
1951 m.add_vertex(corners[idx[1]], *n);
1952 m.add_vertex(corners[idx[2]], *n);
1953 m.add_vertex(corners[idx[3]], *n);
1954 m.add_triangle(b, b + 1, b + 2);
1955 m.add_triangle(b, b + 2, b + 3);
1956 }
1957 m
1958 }
1959
1960 fn reveal_normals(mesh: &Mesh, pre_tri_count: usize) -> Vec<Vector3<f64>> {
1963 let mut normals = Vec::new();
1964 let indices = &mesh.indices[pre_tri_count * 3..];
1965 for tri in indices.chunks_exact(3) {
1966 let i = tri[0] as usize;
1967 let nx = mesh.normals[i * 3] as f64;
1968 let ny = mesh.normals[i * 3 + 1] as f64;
1969 let nz = mesh.normals[i * 3 + 2] as f64;
1970 normals.push(Vector3::new(nx, ny, nz));
1971 }
1972 normals
1973 }
1974
1975 #[test]
1976 fn test_reveals_generated_for_axis_aligned_opening() {
1977 let wall_min = Point3::new(0.0, -0.15, 0.0);
1979 let wall_max = Point3::new(10.0, 0.15, 3.0);
1980
1981 let open_min = Point3::new(4.0, -0.3, 1.0);
1983 let open_max = Point3::new(6.0, 0.3, 2.5);
1984
1985 let mut mesh = Mesh::new();
1986 let extrusion_dir = Vector3::new(0.0, 1.0, 0.0); generate_reveal_quads(
1989 &mut mesh,
1990 &open_min,
1991 &open_max,
1992 &wall_min,
1993 &wall_max,
1994 Some(&extrusion_dir),
1995 );
1996
1997 assert_eq!(mesh.triangle_count(), 8, "Expected 4 reveal quads (8 triangles)");
1999 assert_eq!(mesh.vertex_count(), 16, "Expected 16 vertices (4 per quad)");
2000 }
2001
2002 #[test]
2003 fn test_reveal_normals_point_inward() {
2004 let wall_min = Point3::new(0.0, -0.15, 0.0);
2005 let wall_max = Point3::new(10.0, 0.15, 3.0);
2006 let open_min = Point3::new(4.0, -0.3, 1.0);
2007 let open_max = Point3::new(6.0, 0.3, 2.5);
2008
2009 let mut mesh = Mesh::new();
2010 let dir = Vector3::new(0.0, 1.0, 0.0);
2011 generate_reveal_quads(&mut mesh, &open_min, &open_max, &wall_min, &wall_max, Some(&dir));
2012
2013 let normals = reveal_normals(&mesh, 0);
2014
2015 let has_pos_x = normals.iter().any(|n| n.x > 0.5);
2021 let has_neg_x = normals.iter().any(|n| n.x < -0.5);
2022 let has_pos_z = normals.iter().any(|n| n.z > 0.5);
2023 let has_neg_z = normals.iter().any(|n| n.z < -0.5);
2024
2025 assert!(has_pos_x, "Should have +X normal (left reveal)");
2026 assert!(has_neg_x, "Should have −X normal (right reveal)");
2027 assert!(has_pos_z, "Should have +Z normal (bottom reveal)");
2028 assert!(has_neg_z, "Should have −Z normal (top reveal)");
2029 }
2030
2031 #[test]
2032 fn test_no_reveals_when_opening_at_wall_boundary() {
2033 let wall_min = Point3::new(0.0, -0.15, 0.0);
2035 let wall_max = Point3::new(10.0, 0.15, 3.0);
2036 let open_min = Point3::new(0.0, -0.3, 0.0);
2038 let open_max = Point3::new(10.0, 0.3, 2.1);
2039
2040 let mut mesh = Mesh::new();
2041 let dir = Vector3::new(0.0, 1.0, 0.0);
2042 generate_reveal_quads(&mut mesh, &open_min, &open_max, &wall_min, &wall_max, Some(&dir));
2043
2044 assert_eq!(mesh.triangle_count(), 2, "Only top reveal expected (1 quad = 2 tris)");
2048 }
2049
2050 #[test]
2051 fn test_reveals_with_extrusion_along_x() {
2052 let wall_min = Point3::new(-0.15, 0.0, 0.0);
2054 let wall_max = Point3::new(0.15, 10.0, 3.0);
2055 let open_min = Point3::new(-0.3, 4.0, 1.0);
2056 let open_max = Point3::new(0.3, 6.0, 2.5);
2057
2058 let mut mesh = Mesh::new();
2059 let dir = Vector3::new(1.0, 0.0, 0.0);
2060 generate_reveal_quads(&mut mesh, &open_min, &open_max, &wall_min, &wall_max, Some(&dir));
2061
2062 assert_eq!(mesh.triangle_count(), 8, "4 reveal quads for X-extrusion");
2063 }
2064
2065 #[test]
2066 fn test_reveals_with_extrusion_along_z() {
2067 let wall_min = Point3::new(0.0, 0.0, -0.15);
2069 let wall_max = Point3::new(10.0, 10.0, 0.15);
2070 let open_min = Point3::new(3.0, 3.0, -0.3);
2071 let open_max = Point3::new(5.0, 5.0, 0.3);
2072
2073 let mut mesh = Mesh::new();
2074 let dir = Vector3::new(0.0, 0.0, 1.0);
2075 generate_reveal_quads(&mut mesh, &open_min, &open_max, &wall_min, &wall_max, Some(&dir));
2076
2077 assert_eq!(mesh.triangle_count(), 8, "4 reveal quads for Z-extrusion");
2078 }
2079
2080 #[test]
2081 fn test_reveals_clamp_to_wall_depth() {
2082 let wall_min = Point3::new(0.0, 0.0, 0.0);
2084 let wall_max = Point3::new(10.0, 0.3, 3.0);
2085 let open_min = Point3::new(4.0, -1.0, 1.0);
2087 let open_max = Point3::new(6.0, 1.3, 2.5);
2088
2089 let mut mesh = Mesh::new();
2090 let dir = Vector3::new(0.0, 1.0, 0.0);
2091 generate_reveal_quads(&mut mesh, &open_min, &open_max, &wall_min, &wall_max, Some(&dir));
2092
2093 for chunk in mesh.positions.chunks_exact(3) {
2095 let y = chunk[1] as f64;
2096 assert!(
2097 y >= -1e-3 && y <= 0.3 + 1e-3,
2098 "Reveal vertex Y={y} should be within wall bounds [0.0, 0.3]"
2099 );
2100 }
2101 }
2102
2103 #[test]
2104 fn test_no_reveals_when_no_wall_thickness() {
2105 let wall_min = Point3::new(0.0, 0.0, 0.0);
2107 let wall_max = Point3::new(10.0, 0.0, 3.0);
2108 let open_min = Point3::new(4.0, -0.1, 1.0);
2109 let open_max = Point3::new(6.0, 0.1, 2.5);
2110
2111 let mut mesh = Mesh::new();
2112 let dir = Vector3::new(0.0, 1.0, 0.0);
2113 generate_reveal_quads(&mut mesh, &open_min, &open_max, &wall_min, &wall_max, Some(&dir));
2114
2115 assert_eq!(mesh.triangle_count(), 0, "No reveals for zero-thickness wall");
2116 }
2117
2118 #[test]
2119 fn test_no_reveals_when_opening_misses_submesh_on_cross_axis() {
2120 let wall_min = Point3::new(0.0, -0.15, 0.0);
2124 let wall_max = Point3::new(10.0, 0.15, 0.5);
2125 let open_min = Point3::new(4.0, -0.3, 1.0);
2126 let open_max = Point3::new(6.0, 0.3, 2.0);
2127
2128 let mut mesh = Mesh::new();
2129 let dir = Vector3::new(0.0, 1.0, 0.0);
2130 generate_reveal_quads(&mut mesh, &open_min, &open_max, &wall_min, &wall_max, Some(&dir));
2131
2132 assert_eq!(
2133 mesh.triangle_count(),
2134 0,
2135 "No reveals when opening lies outside sub-mesh on a cross-axis"
2136 );
2137 }
2138
2139 #[test]
2140 fn test_reveals_clamp_to_wall_on_orthogonal_cross_axis() {
2141 let wall_min = Point3::new(0.0, -0.15, 0.0);
2145 let wall_max = Point3::new(10.0, 0.15, 2.0);
2146 let open_min = Point3::new(4.0, -0.3, 1.0);
2147 let open_max = Point3::new(6.0, 0.3, 3.0);
2148
2149 let mut mesh = Mesh::new();
2150 let dir = Vector3::new(0.0, 1.0, 0.0);
2151 generate_reveal_quads(&mut mesh, &open_min, &open_max, &wall_min, &wall_max, Some(&dir));
2152
2153 for chunk in mesh.positions.chunks_exact(3) {
2154 let z = chunk[2] as f64;
2155 assert!(
2156 z >= -1e-3 && z <= 2.0 + 1e-3,
2157 "Reveal vertex Z={z} should stay within sub-mesh [0.0, 2.0]"
2158 );
2159 }
2160 }
2161
2162 #[test]
2163 fn test_determine_extrusion_axis() {
2164 let wmin = Point3::new(0.0, 0.0, 0.0);
2165 let wmax = Point3::new(10.0, 0.3, 3.0);
2166
2167 assert_eq!(
2168 determine_extrusion_axis(Some(&Vector3::new(1.0, 0.0, 0.0)), &wmin, &wmax),
2169 0
2170 );
2171 assert_eq!(
2172 determine_extrusion_axis(Some(&Vector3::new(0.0, 1.0, 0.0)), &wmin, &wmax),
2173 1
2174 );
2175 assert_eq!(
2176 determine_extrusion_axis(Some(&Vector3::new(0.0, 0.0, 1.0)), &wmin, &wmax),
2177 2
2178 );
2179 assert_eq!(
2181 determine_extrusion_axis(Some(&Vector3::new(0.7, 0.7, 0.0)), &wmin, &wmax),
2182 0 );
2184 assert_eq!(
2186 determine_extrusion_axis(None, &wmin, &wmax),
2187 1
2188 );
2189 }
2190}