1#![forbid(unsafe_code)]
2
3mod assemble;
25mod classify;
26mod seams;
27mod section;
28mod split;
29mod support;
30
31pub use section::{section_edges, SectionEdge};
32pub use split::{split_face, Piece, PieceSource, Region};
33
34use axiolid_brep::ExactBRep;
35use axiolid_core::{BooleanOperator, Tolerance};
36use axiolid_evaluate::surface::{locate, normal};
37use axiolid_surface::Surface;
38use axiolid_topology::Orientation;
39
40pub fn boolean(
52 a: &ExactBRep,
53 b: &ExactBRep,
54 operator: BooleanOperator,
55 tolerance: Tolerance,
56) -> Result<ExactBRep, BooleanError> {
57 let seamed_a = seams::with_seams(a, tolerance)?;
59 let seamed_b = seams::with_seams(b, tolerance)?;
60 let a = seamed_a.as_ref().unwrap_or(a);
61 let b = seamed_b.as_ref().unwrap_or(b);
62 let edges = section_edges(a, b, tolerance)?;
63 let solid_a = classify::Solid::new(a, tolerance)?;
64 let solid_b = classify::Solid::new(b, tolerance)?;
65 let mut kept = Vec::new();
66 let cuts: Vec<axiolid_core::Point3> = edges.iter().flat_map(|e| [e.start, e.end]).collect();
67 for (operand, other, other_solid, first) in [(a, b, &solid_b, true), (b, a, &solid_a, false)] {
68 let topology = operand.topology();
69 for index in 0..topology.faces().len() {
70 let face = topology
71 .face_id_at(index)
72 .ok_or(BooleanError::DanglingReference)?;
73 let record = &topology.faces()[index];
74 let surface: Surface = record
75 .surface
76 .and_then(|id| operand.surfaces().get(id.index()))
77 .ok_or(BooleanError::DanglingReference)?
78 .clone();
79 let mine: Vec<SectionEdge> = edges
80 .iter()
81 .filter(|e| {
82 if first {
83 e.face_a == face
84 } else {
85 e.face_b == face
86 }
87 })
88 .cloned()
89 .collect();
90 let coincident: Vec<usize> = (0..other.topology().faces().len())
93 .filter(|&f| {
94 other.topology().faces()[f]
95 .surface
96 .and_then(|s| other.surfaces().get(s.index()))
97 .is_some_and(|s| support::same_support(&surface, s, tolerance))
98 })
99 .collect();
100 for region in split_face(operand, face, &mine, first, &cuts, tolerance)? {
101 let sign = match record.orientation {
102 Orientation::Forward if !region.against => 1.0,
103 Orientation::Reversed if region.against => 1.0,
104 _ => -1.0,
105 };
106 let (keep, flip) = decide(
107 ®ion,
108 &surface,
109 sign,
110 other_solid,
111 &coincident,
112 operator,
113 first,
114 tolerance,
115 )?;
116 if keep {
117 kept.push(assemble::Kept {
118 surface: surface.clone(),
119 orientation: record.orientation,
120 region,
121 flip,
122 });
123 }
124 }
125 }
126 }
127 assemble::assemble(&kept, tolerance)
128}
129
130#[allow(clippy::too_many_arguments)]
133fn decide(
134 region: &Region,
135 surface: &Surface,
136 sign: f64,
137 other_solid: &classify::Solid<'_>,
138 coincident: &[usize],
139 operator: BooleanOperator,
140 first: bool,
141 tolerance: Tolerance,
142) -> Result<(bool, bool), BooleanError> {
143 let mut last = BooleanError::Undecided;
144 for point in classify::interior_points(region, surface)? {
145 match classify_point(
146 point,
147 surface,
148 sign,
149 other_solid,
150 coincident,
151 operator,
152 first,
153 tolerance,
154 ) {
155 Err(BooleanError::Undecided) => last = BooleanError::Undecided,
156 other => return other,
157 }
158 }
159 Err(last)
160}
161
162#[allow(clippy::too_many_arguments)]
163fn classify_point(
164 point: axiolid_core::Point3,
165 surface: &Surface,
166 sign: f64,
167 other_solid: &classify::Solid<'_>,
168 coincident: &[usize],
169 operator: BooleanOperator,
170 first: bool,
171 tolerance: Tolerance,
172) -> Result<(bool, bool), BooleanError> {
173 if let Some(theirs) = other_solid.on_face(point, coincident, tolerance)? {
174 let (u, v) = locate(surface, point, tolerance).map_err(|_| BooleanError::Evaluation)?;
177 let ours = normal(surface, u, v).map_err(|_| BooleanError::Evaluation)? * sign;
178 let same = ours.dot(theirs) > 0.0;
179 let keep = first
180 && match operator {
181 BooleanOperator::Union | BooleanOperator::Intersection => same,
182 BooleanOperator::Difference => !same,
183 _ => return Err(BooleanError::UnsupportedSection),
184 };
185 return Ok((keep, false));
186 }
187 let inside = other_solid.contains(point, tolerance)?;
188 Ok(match (operator, first) {
189 (BooleanOperator::Union, _) => (!inside, false),
190 (BooleanOperator::Intersection, _) => (inside, false),
191 (BooleanOperator::Difference, true) => (!inside, false),
192 (BooleanOperator::Difference, false) => (inside, true),
193 _ => return Err(BooleanError::UnsupportedSection),
194 })
195}
196
197use axiolid_measure::ExactMeasureError;
198use core::fmt;
199
200#[non_exhaustive]
202#[derive(Debug, Clone, PartialEq, Eq)]
203pub enum BooleanError {
204 UnsupportedSection,
207 UnsupportedTrim,
210 Undecided,
212 Evaluation,
214 DanglingReference,
216 Measure(ExactMeasureError),
218 UnsupportedSplit,
221 UnclosedSplit,
223 TangentSplit,
226 EmptyResult,
230 AmbiguousCavity,
232 Assembly,
234}
235
236impl fmt::Display for BooleanError {
237 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
238 match self {
239 Self::UnsupportedSection => {
240 f.write_str("a face pair meets in a curve this stage does not build")
241 }
242 Self::UnsupportedTrim => {
243 f.write_str("a face is trimmed by a curve family this stage cannot intersect")
244 }
245 Self::Undecided => f.write_str("a point lies too close to a face boundary to classify"),
246 Self::Evaluation => f.write_str("a curve or surface could not be evaluated"),
247 Self::DanglingReference => f.write_str("a handle references missing geometry"),
248 Self::Measure(error) => write!(f, "a face domain could not be built: {error}"),
249 Self::UnsupportedSplit => {
250 f.write_str("a face or section has no exact pcurve in this stage")
251 }
252 Self::UnclosedSplit => f.write_str("the pieces of a split face do not close"),
253 Self::TangentSplit => {
254 f.write_str("two pieces leave a vertex of a split face in one direction")
255 }
256 Self::EmptyResult => f.write_str("the operation leaves nothing"),
257 Self::AmbiguousCavity => {
258 f.write_str("a cavity of the result lies inside none of its solids")
259 }
260 Self::Assembly => f.write_str("the kept faces did not sew into a valid exact B-rep"),
261 }
262 }
263}
264
265impl std::error::Error for BooleanError {}