1use axiolid_brep::{ExactBRep, FaceName, Operand, SweptFace};
37use axiolid_contracts::{GeomError, GeomResult, Operation};
38use axiolid_core::{BooleanOperator, Frame2, Point2, Scalar, Tolerance, Vec2, Vec3};
39use axiolid_overlay::{
40 arc_overlay, overlay, validate_arc_ring, ArcPolygon, ArcRing, ArcVertex, FillRule,
41 OverlayInput, OverlayOperation, Polygon, Ring,
42};
43use axiolid_primitive::HalfSpace;
44
45use crate::boolean_column::{clip_columns, coaxial_columns, is_stepped, ColumnOperand};
46use crate::boolean_provenance::{name_side_fragment, OperandRings};
47use axiolid_brep_audit::geometric_audit;
48
49use crate::contour_lower::orient_arc_ring;
50use crate::extrude_arc::{arc_geometry, extrude_arc_rings_between, extrude_arc_rings_from, Level};
51use crate::extrude_exact::extrude_polygon_rings_named;
52use crate::BACKEND_ID;
53
54pub fn unsupported(input: &'static str) -> GeomError {
55 GeomError::UnsupportedInput {
56 backend: BACKEND_ID,
57 operation: Operation::MeshBoolean,
58 input,
59 }
60}
61
62#[derive(Debug, Clone, PartialEq)]
68pub struct Prism {
69 pub rings: Vec<Vec<Point2>>,
71 pub bottom: Scalar,
73 pub top: Scalar,
75}
76
77#[derive(Debug, Clone, PartialEq)]
84pub struct ArcPrism {
85 pub section: ArcRing,
87 pub bottom: Scalar,
89 pub top: Scalar,
91}
92
93pub fn boolean_prisms_exact(
104 subject: &Prism,
105 tool: &Prism,
106 operator: BooleanOperator,
107 tolerance: Tolerance,
108) -> GeomResult<ExactBRep> {
109 if let Some(solids) = prism_columns(subject, tool, operator, tolerance)? {
110 return one_solid(
111 solids,
112 "prism boolean produced an empty result",
113 "exact prism boolean producing disconnected components \
114 (boolean_prisms_exact_solids returns every piece)",
115 );
116 }
117 let (bottom, top) = prism_span(subject, tool, operator, tolerance)?;
118 let polygons = prism_sections(subject, tool, operator, tolerance)?;
119 single_solid(
120 polygons,
121 "prism boolean produced an empty cross-section",
122 "exact prism boolean producing disconnected components \
123 (boolean_prisms_exact_solids returns every piece)",
124 |polygon| prism_solid(polygon, subject, tool, (bottom, top), tolerance),
125 )
126}
127
128pub fn boolean_prisms_exact_solids(
140 subject: &Prism,
141 tool: &Prism,
142 operator: BooleanOperator,
143 tolerance: Tolerance,
144) -> GeomResult<Vec<ExactBRep>> {
145 if let Some(solids) = prism_columns(subject, tool, operator, tolerance)? {
146 return Ok(solids);
147 }
148 let Some((bottom, top)) = empty_span_is_none(prism_span(subject, tool, operator, tolerance))?
149 else {
150 return Ok(Vec::new());
151 };
152 let polygons = prism_sections(subject, tool, operator, tolerance)?;
155 polygons
156 .iter()
157 .map(|polygon| prism_solid(polygon, subject, tool, (bottom, top), tolerance))
158 .collect()
159}
160
161fn prism_span(
166 subject: &Prism,
167 tool: &Prism,
168 operator: BooleanOperator,
169 tolerance: Tolerance,
170) -> GeomResult<(Scalar, Scalar)> {
171 validate(subject, "subject")?;
172 validate(tool, "tool")?;
173 resolve_span(
174 (subject.bottom, subject.top),
175 (tool.bottom, tool.top),
176 operator,
177 tolerance,
178 )
179}
180
181fn prism_sections(
183 subject: &Prism,
184 tool: &Prism,
185 operator: BooleanOperator,
186 tolerance: Tolerance,
187) -> GeomResult<Vec<Polygon>> {
188 let operation = overlay_operation(operator)?;
189 let frame = Frame2 {
190 origin: Vec2::ZERO,
191 x: Vec2::X,
192 y: Vec2::Y,
193 };
194 let result = overlay(
195 &OverlayInput {
196 frame,
197 polygons: to_polygons(subject),
198 },
199 &OverlayInput {
200 frame,
201 polygons: to_polygons(tool),
202 },
203 operation,
204 FillRule::NonZero,
205 tolerance,
206 )
207 .map_err(|error| GeomError::BackendContractViolation {
208 backend: BACKEND_ID,
209 detail: format!("exact prism cross-section overlay failed: {error:?}"),
210 })?;
211 Ok(result.polygons)
212}
213
214fn prism_solid(
216 polygon: &Polygon,
217 subject: &Prism,
218 tool: &Prism,
219 (bottom, top): (Scalar, Scalar),
220 tolerance: Tolerance,
221) -> GeomResult<ExactBRep> {
222 let mut rings = Vec::with_capacity(1 + polygon.holes.len());
223 rings.push(polygon.outer.points.clone());
224 for hole in &polygon.holes {
225 rings.push(hole.points.clone());
226 }
227
228 if bottom.abs() > tolerance.linear() {
232 return Err(unsupported(
233 "exact prism boolean whose result does not start at z = 0",
234 ));
235 }
236 let subject_rings = OperandRings {
241 operand: Operand::Subject,
242 rings: &subject.rings,
243 };
244 let tool_rings = OperandRings {
245 operand: Operand::Tool,
246 rings: &tool.rings,
247 };
248 let operands = [subject_rings, tool_rings];
249 let mut solid =
250 extrude_polygon_rings_named(&rings, Vec3::Z * (top - bottom), &mut |(start, end)| {
251 name_side_fragment(start, end, &operands)
252 })?;
253
254 name_caps(&mut solid, subject, tool, bottom, top, tolerance);
257 gate_geometry(solid, tolerance)
258}
259
260fn single_solid<T>(
266 pieces: Vec<T>,
267 empty: &'static str,
268 disconnected: &'static str,
269 build: impl FnOnce(&T) -> GeomResult<ExactBRep>,
270) -> GeomResult<ExactBRep> {
271 match pieces.as_slice() {
272 [] => Err(GeomError::Degenerate(empty.to_owned())),
273 [only] => build(only),
274 _ => Err(unsupported(disconnected)),
275 }
276}
277
278fn empty_span_is_none(span: GeomResult<(Scalar, Scalar)>) -> GeomResult<Option<(Scalar, Scalar)>> {
283 match span {
284 Ok(span) => Ok(Some(span)),
285 Err(GeomError::Degenerate(_)) => Ok(None),
286 Err(error) => Err(error),
287 }
288}
289
290fn lowest_first(a: &[Point2], b: &[Point2]) -> std::cmp::Ordering {
292 let key = |ring: &[Point2]| {
293 ring.iter()
294 .copied()
295 .min_by(|p, q| p.x.total_cmp(&q.x).then(p.y.total_cmp(&q.y)))
296 };
297 match (key(a), key(b)) {
298 (Some(p), Some(q)) => p.x.total_cmp(&q.x).then(p.y.total_cmp(&q.y)),
299 (a, b) => a.is_some().cmp(&b.is_some()),
300 }
301}
302
303fn overlay_operation(operator: BooleanOperator) -> GeomResult<OverlayOperation> {
304 match operator {
305 BooleanOperator::Intersection => Ok(OverlayOperation::Intersection),
306 BooleanOperator::Union => Ok(OverlayOperation::Union),
307 BooleanOperator::Difference => Ok(OverlayOperation::Difference),
308 _ => Err(unsupported("unknown exact prism boolean operator")),
309 }
310}
311
312fn validate(prism: &Prism, role: &'static str) -> GeomResult<()> {
313 if prism.rings.is_empty() {
314 return Err(GeomError::InvalidInput(format!(
315 "{role} prism has no cross-section rings"
316 )));
317 }
318 for ring in &prism.rings {
319 if ring.len() < 3 {
320 return Err(GeomError::InvalidInput(format!(
321 "{role} prism ring needs at least three points"
322 )));
323 }
324 if !ring.iter().all(|p| p.x.is_finite() && p.y.is_finite()) {
325 return Err(GeomError::InvalidInput(format!(
326 "{role} prism ring has a non-finite point"
327 )));
328 }
329 }
330 if !prism.bottom.is_finite() || !prism.top.is_finite() {
331 return Err(GeomError::InvalidInput(format!(
332 "{role} prism heights must be finite"
333 )));
334 }
335 if prism.top <= prism.bottom {
336 return Err(GeomError::InvalidInput(format!(
337 "{role} prism top must lie above its bottom"
338 )));
339 }
340 Ok(())
341}
342
343fn to_polygons(prism: &Prism) -> Vec<Polygon> {
344 let mut rings = prism.rings.iter();
345 let outer = Ring {
346 points: rings.next().cloned().unwrap_or_default(),
347 };
348 let holes = rings.map(|r| Ring { points: r.clone() }).collect();
349 vec![Polygon { outer, holes }]
350}
351
352fn name_caps(
360 solid: &mut ExactBRep,
361 subject: &Prism,
362 tool: &Prism,
363 bottom: Scalar,
364 top: Scalar,
365 tolerance: Tolerance,
366) {
367 let start = cap_operand(subject.bottom, tool.bottom, bottom, tolerance);
368 let end = cap_operand(subject.top, tool.top, top, tolerance);
369 solid.name_caps(
370 start.map(|operand| FaceName::swept(SweptFace::StartCap).fragment(operand)),
371 end.map(|operand| FaceName::swept(SweptFace::EndCap).fragment(operand)),
372 );
373}
374
375fn cap_operand(
377 subject: Scalar,
378 tool: Scalar,
379 result: Scalar,
380 tolerance: Tolerance,
381) -> Option<Operand> {
382 if tolerance.eq(subject, result) {
383 Some(Operand::Subject)
384 } else if tolerance.eq(tool, result) {
385 Some(Operand::Tool)
386 } else {
387 None
388 }
389}
390
391pub fn boolean_arc_prisms_exact(
414 subject: &ArcPrism,
415 tool: &ArcPrism,
416 operator: BooleanOperator,
417 tolerance: Tolerance,
418) -> GeomResult<ExactBRep> {
419 if let Some(solids) = arc_prism_columns(subject, tool, operator, tolerance)? {
420 return one_solid(
421 solids,
422 "arc prism boolean produced an empty result",
423 "exact arc prism boolean producing disconnected components \
424 (boolean_arc_prisms_exact_solids returns every piece)",
425 );
426 }
427 let span = arc_prism_span(subject, tool, operator, tolerance)?;
428 let regions = arc_prism_sections(subject, tool, operator, tolerance)?;
429 single_solid(
430 regions,
431 "arc prism boolean produced an empty cross-section",
432 "exact arc prism boolean producing disconnected components \
433 (boolean_arc_prisms_exact_solids returns every piece)",
434 |region| arc_prism_solid(region, span, tolerance),
435 )
436}
437
438pub fn boolean_arc_prisms_exact_solids(
446 subject: &ArcPrism,
447 tool: &ArcPrism,
448 operator: BooleanOperator,
449 tolerance: Tolerance,
450) -> GeomResult<Vec<ExactBRep>> {
451 if let Some(solids) = arc_prism_columns(subject, tool, operator, tolerance)? {
452 return Ok(solids);
453 }
454 let Some(span) = empty_span_is_none(arc_prism_span(subject, tool, operator, tolerance))? else {
455 return Ok(Vec::new());
456 };
457 let mut regions = arc_prism_sections(subject, tool, operator, tolerance)?;
458 regions.sort_by(|a, b| lowest_first(&arc_points(&a.outer), &arc_points(&b.outer)));
459 regions
460 .iter()
461 .map(|region| arc_prism_solid(region, span, tolerance))
462 .collect()
463}
464
465pub fn clip_arc_prism_exact(
504 prism: &ArcPrism,
505 half_space: &HalfSpace,
506 tolerance: Tolerance,
507) -> GeomResult<ExactBRep> {
508 validate_arc_ring(&prism.section, tolerance)
509 .map_err(|error| GeomError::InvalidInput(format!("arc prism section: {error:?}")))?;
510 if !(prism.bottom.is_finite() && prism.top.is_finite()) {
511 return Err(GeomError::InvalidInput(
512 "arc prism heights must be finite".to_owned(),
513 ));
514 }
515 if prism.top <= prism.bottom {
516 return Err(GeomError::InvalidInput(
517 "arc prism top must lie above its bottom".to_owned(),
518 ));
519 }
520 let origin = half_space.boundary.origin;
521 let normal = half_space.boundary.normal;
522 if !(origin.is_finite() && normal.is_finite()) || normal.length_squared() == 0.0 {
523 return Err(GeomError::InvalidInput(
524 "half-space boundary must have a finite point and a non-zero normal".to_owned(),
525 ));
526 }
527 if normal.z == 0.0 {
528 return Err(unsupported(
529 "exact arc prism clip by a plane parallel to the extrusion axis",
530 ));
531 }
532 let level = Level {
534 height: normal.dot(origin) / normal.z,
535 gradient: Vec2::new(-normal.x / normal.z, -normal.y / normal.z),
536 };
537 if !(level.height.is_finite() && level.gradient.is_finite()) {
538 return Err(GeomError::Degenerate(
539 "half-space boundary is too steep to express as a height".to_owned(),
540 ));
541 }
542 let keeps_above = (normal.z > 0.0) == half_space.agreement;
545
546 let section = orient_arc_ring(&prism.section, true)?;
547 let (low, high) = level_range(§ion, level)?;
548 let linear = tolerance.linear();
549 let rings = [section];
550 let flat = |bottom, top| {
551 extrude_arc_rings_between(&rings, Level::flat(bottom), Level::flat(top), (true, true))
552 };
553 let solid = if keeps_above {
554 if high <= prism.bottom + linear {
555 flat(prism.bottom, prism.top)?
556 } else if low >= prism.top - linear {
557 return Err(GeomError::Degenerate(
558 "arc prism clip is empty: the plane lies above the prism".to_owned(),
559 ));
560 } else if low > prism.bottom + linear && high < prism.top - linear {
561 extrude_arc_rings_between(&rings, level, Level::flat(prism.top), (false, true))?
562 } else {
563 return clip_crossing(&rings[0], prism, level, keeps_above, tolerance);
564 }
565 } else if low >= prism.top - linear {
566 flat(prism.bottom, prism.top)?
567 } else if high <= prism.bottom + linear {
568 return Err(GeomError::Degenerate(
569 "arc prism clip is empty: the plane lies below the prism".to_owned(),
570 ));
571 } else if low > prism.bottom + linear && high < prism.top - linear {
572 extrude_arc_rings_between(&rings, Level::flat(prism.bottom), level, (true, false))?
573 } else {
574 return clip_crossing(&rings[0], prism, level, keeps_above, tolerance);
575 };
576 gate_geometry(solid, tolerance)
577}
578
579fn level_range(ring: &ArcRing, level: Level) -> GeomResult<(Scalar, Scalar)> {
587 let mut low = Scalar::INFINITY;
588 let mut high = Scalar::NEG_INFINITY;
589 let mut take = |p: Point2| {
590 let z = level.at(p);
591 low = low.min(z);
592 high = high.max(z);
593 };
594 let count = ring.vertices.len();
595 for index in 0..count {
596 let from = ring.vertices[index];
597 let to = ring.vertices[(index + 1) % count];
598 take(from.point);
599 if from.bulge == 0.0 || level.gradient == Vec2::ZERO {
600 continue;
601 }
602 let arc = arc_geometry(from.point, to.point, from.bulge)?;
603 let start = (from.point - arc.centre).to_angle();
604 let direction = level.gradient.to_angle();
605 for extreme in [direction, direction + core::f64::consts::PI] {
606 let turned = if arc.sweep > 0.0 {
609 (extreme - start).rem_euclid(core::f64::consts::TAU)
610 } else {
611 (start - extreme).rem_euclid(core::f64::consts::TAU)
612 };
613 if turned <= arc.sweep.abs() {
614 take(arc.centre + Vec2::from_angle(extreme) * arc.radius);
615 }
616 }
617 }
618 Ok((low, high))
619}
620
621fn arc_points(ring: &ArcRing) -> Vec<Point2> {
622 ring.vertices.iter().map(|vertex| vertex.point).collect()
623}
624
625fn one_solid(
628 mut solids: Vec<ExactBRep>,
629 empty: &'static str,
630 disconnected: &'static str,
631) -> GeomResult<ExactBRep> {
632 match solids.len() {
633 0 => Err(GeomError::Degenerate(empty.to_owned())),
634 1 => Ok(solids.remove(0)),
635 _ => Err(unsupported(disconnected)),
636 }
637}
638
639fn prism_columns(
642 subject: &Prism,
643 tool: &Prism,
644 operator: BooleanOperator,
645 tolerance: Tolerance,
646) -> GeomResult<Option<Vec<ExactBRep>>> {
647 validate(subject, "subject")?;
648 validate(tool, "tool")?;
649 if !is_stepped(
650 (subject.bottom, subject.top),
651 (tool.bottom, tool.top),
652 operator,
653 tolerance,
654 ) {
655 return Ok(None);
656 }
657 let operand = |prism: &Prism| ColumnOperand {
658 rings: prism
659 .rings
660 .iter()
661 .map(|ring| ArcRing::new(ring.iter().copied().map(ArcVertex::straight).collect()))
662 .collect(),
663 bottom: prism.bottom,
664 top: prism.top,
665 };
666 coaxial_columns(&operand(subject), &operand(tool), operator, tolerance).map(Some)
667}
668
669fn arc_prism_columns(
671 subject: &ArcPrism,
672 tool: &ArcPrism,
673 operator: BooleanOperator,
674 tolerance: Tolerance,
675) -> GeomResult<Option<Vec<ExactBRep>>> {
676 validate_arc_prisms(subject, tool, tolerance)?;
677 if !is_stepped(
678 (subject.bottom, subject.top),
679 (tool.bottom, tool.top),
680 operator,
681 tolerance,
682 ) {
683 return Ok(None);
684 }
685 let operand = |prism: &ArcPrism| ColumnOperand {
686 rings: vec![prism.section.clone()],
687 bottom: prism.bottom,
688 top: prism.top,
689 };
690 coaxial_columns(&operand(subject), &operand(tool), operator, tolerance).map(Some)
691}
692
693fn clip_crossing(
695 section: &ArcRing,
696 prism: &ArcPrism,
697 level: Level,
698 keeps_above: bool,
699 tolerance: Tolerance,
700) -> GeomResult<ExactBRep> {
701 let solids = clip_columns(
702 section,
703 (prism.bottom, prism.top),
704 level,
705 keeps_above,
706 tolerance,
707 )?;
708 one_solid(
709 solids,
710 "arc prism clip is empty",
711 "exact arc prism clip leaving disconnected pieces",
712 )
713}
714
715fn validate_arc_prisms(
716 subject: &ArcPrism,
717 tool: &ArcPrism,
718 tolerance: Tolerance,
719) -> GeomResult<()> {
720 for (section, role) in [(&subject.section, "subject"), (&tool.section, "tool")] {
721 validate_arc_ring(section, tolerance).map_err(|error| {
722 GeomError::InvalidInput(format!("{role} arc prism section: {error:?}"))
723 })?;
724 }
725 if !subject.bottom.is_finite()
726 || !subject.top.is_finite()
727 || !tool.bottom.is_finite()
728 || !tool.top.is_finite()
729 {
730 return Err(GeomError::InvalidInput(
731 "arc prism heights must be finite".to_owned(),
732 ));
733 }
734 if subject.top <= subject.bottom || tool.top <= tool.bottom {
735 return Err(GeomError::InvalidInput(
736 "arc prism top must lie above its bottom".to_owned(),
737 ));
738 }
739 Ok(())
740}
741
742fn arc_prism_span(
744 subject: &ArcPrism,
745 tool: &ArcPrism,
746 operator: BooleanOperator,
747 tolerance: Tolerance,
748) -> GeomResult<(Scalar, Scalar)> {
749 validate_arc_prisms(subject, tool, tolerance)?;
750 resolve_span(
751 (subject.bottom, subject.top),
752 (tool.bottom, tool.top),
753 operator,
754 tolerance,
755 )
756}
757
758fn arc_prism_sections(
760 subject: &ArcPrism,
761 tool: &ArcPrism,
762 operator: BooleanOperator,
763 tolerance: Tolerance,
764) -> GeomResult<Vec<ArcPolygon>> {
765 let operation = overlay_operation(operator)?;
766 let result =
767 arc_overlay(&subject.section, &tool.section, operation, tolerance).map_err(|error| {
768 GeomError::BackendContractViolation {
769 backend: BACKEND_ID,
770 detail: format!("arc prism cross-section overlay failed: {error:?}"),
771 }
772 })?;
773 Ok(result.regions)
774}
775
776fn arc_prism_solid(
778 region: &ArcPolygon,
779 (bottom, top): (Scalar, Scalar),
780 tolerance: Tolerance,
781) -> GeomResult<ExactBRep> {
782 let mut rings = Vec::with_capacity(1 + region.holes.len());
787 rings.push(region.outer.clone());
788 rings.extend(region.holes.iter().cloned());
789 let solid = extrude_arc_rings_from(&rings, bottom, Vec3::Z * (top - bottom))?;
790 gate_geometry(solid, tolerance)
791}
792
793fn resolve_span(
799 subject: (Scalar, Scalar),
800 tool: (Scalar, Scalar),
801 operator: BooleanOperator,
802 tolerance: Tolerance,
803) -> GeomResult<(Scalar, Scalar)> {
804 match operator {
805 BooleanOperator::Intersection => {
806 let bottom = subject.0.max(tool.0);
807 let top = subject.1.min(tool.1);
808 if top - bottom <= tolerance.linear() {
809 return Err(GeomError::Degenerate(
810 "prism intersection is empty along the extrusion axis".to_owned(),
811 ));
812 }
813 Ok((bottom, top))
814 }
815 BooleanOperator::Union => {
816 if !tolerance.eq(subject.0, tool.0) || !tolerance.eq(subject.1, tool.1) {
820 return Err(unsupported(
821 "exact prism union with differing extrusion spans",
822 ));
823 }
824 Ok(subject)
825 }
826 BooleanOperator::Difference => {
827 if tool.0 > subject.0 + tolerance.linear() || tool.1 < subject.1 - tolerance.linear() {
829 return Err(unsupported(
830 "exact prism difference with a tool shorter than the subject",
831 ));
832 }
833 Ok(subject)
834 }
835 _ => Err(unsupported("unknown exact prism boolean operator")),
836 }
837}
838
839pub(crate) fn gate_geometry(solid: ExactBRep, tolerance: Tolerance) -> GeomResult<ExactBRep> {
851 let health = geometric_audit(&solid, tolerance);
852 if health.is_consistent() {
853 return Ok(solid);
854 }
855 let detail = match health.worst_error() {
858 Some(error) => format!(
859 "boolean result failed its geometric audit: {} defect(s), worst deviation {error:e}",
860 health.defects().len()
861 ),
862 None => format!(
863 "boolean result failed its geometric audit: {} defect(s)",
864 health.defects().len()
865 ),
866 };
867 Err(GeomError::BackendContractViolation {
868 backend: BACKEND_ID,
869 detail,
870 })
871}