1use axiolid_brep::{ExactBRep, FaceName, Operand, SweptFace};
33use axiolid_contracts::{GeomError, GeomResult, Operation};
34use axiolid_core::{BooleanOperator, Frame2, Point2, Scalar, Tolerance, Vec2, Vec3};
35use axiolid_overlay::{
36 arc_overlay, overlay, validate_arc_ring, ArcRing, FillRule, OverlayInput, OverlayOperation,
37 Polygon, Ring,
38};
39
40use crate::boolean_provenance::{name_side_fragment, OperandRings};
41use axiolid_brep_audit::geometric_audit;
42
43use crate::extrude_arc::extrude_arc_ring;
44use crate::extrude_exact::extrude_polygon_rings_named;
45use crate::BACKEND_ID;
46
47pub fn unsupported(input: &'static str) -> GeomError {
48 GeomError::UnsupportedInput {
49 backend: BACKEND_ID,
50 operation: Operation::MeshBoolean,
51 input,
52 }
53}
54
55#[derive(Debug, Clone, PartialEq)]
61pub struct Prism {
62 pub rings: Vec<Vec<Point2>>,
64 pub bottom: Scalar,
66 pub top: Scalar,
68}
69
70#[derive(Debug, Clone, PartialEq)]
77pub struct ArcPrism {
78 pub section: ArcRing,
80 pub bottom: Scalar,
82 pub top: Scalar,
84}
85
86pub fn boolean_prisms_exact(
91 subject: &Prism,
92 tool: &Prism,
93 operator: BooleanOperator,
94 tolerance: Tolerance,
95) -> GeomResult<ExactBRep> {
96 validate(subject, "subject")?;
97 validate(tool, "tool")?;
98
99 let (bottom, top) = resolve_span(
102 (subject.bottom, subject.top),
103 (tool.bottom, tool.top),
104 operator,
105 tolerance,
106 )?;
107 let operation = match operator {
108 BooleanOperator::Intersection => OverlayOperation::Intersection,
109 BooleanOperator::Union => OverlayOperation::Union,
110 BooleanOperator::Difference => OverlayOperation::Difference,
111 _ => return Err(unsupported("unknown exact prism boolean operator")),
112 };
113
114 let frame = Frame2 {
115 origin: Vec2::ZERO,
116 x: Vec2::X,
117 y: Vec2::Y,
118 };
119 let result = overlay(
120 &OverlayInput {
121 frame,
122 polygons: to_polygons(subject),
123 },
124 &OverlayInput {
125 frame,
126 polygons: to_polygons(tool),
127 },
128 operation,
129 FillRule::NonZero,
130 tolerance,
131 )
132 .map_err(|error| GeomError::BackendContractViolation {
133 backend: BACKEND_ID,
134 detail: format!("exact prism cross-section overlay failed: {error:?}"),
135 })?;
136
137 if result.polygons.is_empty() {
138 return Err(GeomError::Degenerate(
139 "prism boolean produced an empty cross-section".to_owned(),
140 ));
141 }
142 if result.polygons.len() > 1 {
145 return Err(unsupported(
146 "exact prism boolean producing disconnected components",
147 ));
148 }
149
150 let polygon = &result.polygons[0];
151 let mut rings = Vec::with_capacity(1 + polygon.holes.len());
152 rings.push(polygon.outer.points.clone());
153 for hole in &polygon.holes {
154 rings.push(hole.points.clone());
155 }
156
157 if bottom.abs() > tolerance.linear() {
161 return Err(unsupported(
162 "exact prism boolean whose result does not start at z = 0",
163 ));
164 }
165 let subject_rings = OperandRings {
170 operand: Operand::Subject,
171 rings: &subject.rings,
172 };
173 let tool_rings = OperandRings {
174 operand: Operand::Tool,
175 rings: &tool.rings,
176 };
177 let operands = [subject_rings, tool_rings];
178 let mut solid =
179 extrude_polygon_rings_named(&rings, Vec3::Z * (top - bottom), &mut |(start, end)| {
180 name_side_fragment(start, end, &operands)
181 })?;
182
183 name_caps(&mut solid, subject, tool, bottom, top, tolerance);
186 gate_geometry(solid, tolerance)
187}
188
189fn validate(prism: &Prism, role: &'static str) -> GeomResult<()> {
190 if prism.rings.is_empty() {
191 return Err(GeomError::InvalidInput(format!(
192 "{role} prism has no cross-section rings"
193 )));
194 }
195 for ring in &prism.rings {
196 if ring.len() < 3 {
197 return Err(GeomError::InvalidInput(format!(
198 "{role} prism ring needs at least three points"
199 )));
200 }
201 if !ring.iter().all(|p| p.x.is_finite() && p.y.is_finite()) {
202 return Err(GeomError::InvalidInput(format!(
203 "{role} prism ring has a non-finite point"
204 )));
205 }
206 }
207 if !prism.bottom.is_finite() || !prism.top.is_finite() {
208 return Err(GeomError::InvalidInput(format!(
209 "{role} prism heights must be finite"
210 )));
211 }
212 if prism.top <= prism.bottom {
213 return Err(GeomError::InvalidInput(format!(
214 "{role} prism top must lie above its bottom"
215 )));
216 }
217 Ok(())
218}
219
220fn to_polygons(prism: &Prism) -> Vec<Polygon> {
221 let mut rings = prism.rings.iter();
222 let outer = Ring {
223 points: rings.next().cloned().unwrap_or_default(),
224 };
225 let holes = rings.map(|r| Ring { points: r.clone() }).collect();
226 vec![Polygon { outer, holes }]
227}
228
229fn name_caps(
237 solid: &mut ExactBRep,
238 subject: &Prism,
239 tool: &Prism,
240 bottom: Scalar,
241 top: Scalar,
242 tolerance: Tolerance,
243) {
244 let start = cap_operand(subject.bottom, tool.bottom, bottom, tolerance);
245 let end = cap_operand(subject.top, tool.top, top, tolerance);
246 solid.name_caps(
247 start.map(|operand| FaceName::swept(SweptFace::StartCap).fragment(operand)),
248 end.map(|operand| FaceName::swept(SweptFace::EndCap).fragment(operand)),
249 );
250}
251
252fn cap_operand(
254 subject: Scalar,
255 tool: Scalar,
256 result: Scalar,
257 tolerance: Tolerance,
258) -> Option<Operand> {
259 if tolerance.eq(subject, result) {
260 Some(Operand::Subject)
261 } else if tolerance.eq(tool, result) {
262 Some(Operand::Tool)
263 } else {
264 None
265 }
266}
267
268pub fn boolean_arc_prisms_exact(
283 subject: &ArcPrism,
284 tool: &ArcPrism,
285 operator: BooleanOperator,
286 tolerance: Tolerance,
287) -> GeomResult<ExactBRep> {
288 for (section, role) in [(&subject.section, "subject"), (&tool.section, "tool")] {
289 validate_arc_ring(section, tolerance).map_err(|error| {
290 GeomError::InvalidInput(format!("{role} arc prism section: {error:?}"))
291 })?;
292 }
293 if !subject.bottom.is_finite()
294 || !subject.top.is_finite()
295 || !tool.bottom.is_finite()
296 || !tool.top.is_finite()
297 {
298 return Err(GeomError::InvalidInput(
299 "arc prism heights must be finite".to_owned(),
300 ));
301 }
302 if subject.top <= subject.bottom || tool.top <= tool.bottom {
303 return Err(GeomError::InvalidInput(
304 "arc prism top must lie above its bottom".to_owned(),
305 ));
306 }
307
308 let (bottom, top) = resolve_span(
309 (subject.bottom, subject.top),
310 (tool.bottom, tool.top),
311 operator,
312 tolerance,
313 )?;
314
315 let operation = match operator {
316 BooleanOperator::Intersection => OverlayOperation::Intersection,
317 BooleanOperator::Union => OverlayOperation::Union,
318 BooleanOperator::Difference => OverlayOperation::Difference,
319 _ => return Err(unsupported("unknown exact prism boolean operator")),
320 };
321
322 let result =
323 arc_overlay(&subject.section, &tool.section, operation, tolerance).map_err(|error| {
324 GeomError::BackendContractViolation {
325 backend: BACKEND_ID,
326 detail: format!("arc prism cross-section overlay failed: {error:?}"),
327 }
328 })?;
329
330 if result.regions.is_empty() {
331 return Err(GeomError::Degenerate(
332 "arc prism boolean produced an empty cross-section".to_owned(),
333 ));
334 }
335 if result.regions.len() > 1 {
338 return Err(unsupported(
339 "exact arc prism boolean producing disconnected components",
340 ));
341 }
342
343 let region = &result.regions[0];
344 if !region.holes.is_empty() {
348 return Err(unsupported(
349 "exact arc prism boolean whose result has an interior hole",
350 ));
351 }
352 if bottom.abs() > tolerance.linear() {
355 return Err(unsupported(
356 "exact arc prism boolean whose result does not start at z = 0",
357 ));
358 }
359 let solid = extrude_arc_ring(®ion.outer, Vec3::Z * (top - bottom))?;
360 gate_geometry(solid, tolerance)
361}
362
363fn resolve_span(
369 subject: (Scalar, Scalar),
370 tool: (Scalar, Scalar),
371 operator: BooleanOperator,
372 tolerance: Tolerance,
373) -> GeomResult<(Scalar, Scalar)> {
374 match operator {
375 BooleanOperator::Intersection => {
376 let bottom = subject.0.max(tool.0);
377 let top = subject.1.min(tool.1);
378 if top - bottom <= tolerance.linear() {
379 return Err(GeomError::Degenerate(
380 "prism intersection is empty along the extrusion axis".to_owned(),
381 ));
382 }
383 Ok((bottom, top))
384 }
385 BooleanOperator::Union => {
386 if !tolerance.eq(subject.0, tool.0) || !tolerance.eq(subject.1, tool.1) {
388 return Err(unsupported(
389 "exact prism union with differing extrusion spans",
390 ));
391 }
392 Ok(subject)
393 }
394 BooleanOperator::Difference => {
395 if tool.0 > subject.0 + tolerance.linear() || tool.1 < subject.1 - tolerance.linear() {
397 return Err(unsupported(
398 "exact prism difference with a tool shorter than the subject",
399 ));
400 }
401 Ok(subject)
402 }
403 _ => Err(unsupported("unknown exact prism boolean operator")),
404 }
405}
406
407fn gate_geometry(solid: ExactBRep, tolerance: Tolerance) -> GeomResult<ExactBRep> {
419 let health = geometric_audit(&solid, tolerance);
420 if health.is_consistent() {
421 return Ok(solid);
422 }
423 let detail = match health.worst_error() {
426 Some(error) => format!(
427 "boolean result failed its geometric audit: {} defect(s), worst deviation {error:e}",
428 health.defects().len()
429 ),
430 None => format!(
431 "boolean result failed its geometric audit: {} defect(s)",
432 health.defects().len()
433 ),
434 };
435 Err(GeomError::BackendContractViolation {
436 backend: BACKEND_ID,
437 detail,
438 })
439}