1use axiolid_contracts::{
46 Backend, BackendDescriptor, BackendId, CancellationGranularity, ExecutionOptions,
47 ExecutionTarget, GeomError, GeomResult, Operation, ScratchRequirement, Sign,
48};
49use axiolid_core::BooleanOperator;
50use axiolid_core::Point3;
51use axiolid_mesh::TriMesh;
52use axiolid_mesh_boolean_contract::{BooleanEvidence, BooleanOutcome, MeshBoolean};
53
54use crate::orient3d;
55
56#[derive(Debug, Default, Clone, Copy)]
62pub struct ScalarBoolean;
63
64impl ScalarBoolean {
65 pub const ID: BackendId = BackendId::new("scalar-reference");
67
68 #[must_use]
70 pub fn new() -> Self {
71 Self
72 }
73}
74
75impl Backend for ScalarBoolean {
76 fn descriptor(&self) -> BackendDescriptor {
77 BackendDescriptor::new(Self::ID, ExecutionTarget::PortableCpu)
78 }
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83enum Arrangement {
84 Disjoint,
86 SubjectInsideTool,
88 ToolInsideSubject,
90 Identical,
92}
93
94impl MeshBoolean for ScalarBoolean {
95 fn scratch_requirement(&self) -> ScratchRequirement {
97 ScratchRequirement::None
98 }
99
100 fn cancellation_granularity(&self) -> CancellationGranularity {
102 CancellationGranularity::Incremental
103 }
104
105 fn boolean(
106 &self,
107 subject: &TriMesh,
108 tool: &TriMesh,
109 operation: BooleanOperator,
110 options: &ExecutionOptions,
111 ) -> GeomResult<BooleanOutcome> {
112 let arrangement = classify(subject, tool, options)?;
113 let mesh = match (operation, arrangement) {
114 (BooleanOperator::Union | BooleanOperator::Intersection, Arrangement::Identical) => {
116 subject.clone()
117 }
118 (
119 BooleanOperator::Difference | BooleanOperator::SymmetricDifference,
120 Arrangement::Identical,
121 ) => empty(),
122
123 (
125 BooleanOperator::Union | BooleanOperator::SymmetricDifference,
126 Arrangement::Disjoint,
127 ) => concatenate(subject, tool),
128 (BooleanOperator::Intersection, Arrangement::Disjoint) => empty(),
129 (BooleanOperator::Difference, Arrangement::Disjoint) => subject.clone(),
130
131 (BooleanOperator::Union, Arrangement::SubjectInsideTool) => tool.clone(),
133 (BooleanOperator::Intersection, Arrangement::SubjectInsideTool) => subject.clone(),
134 (BooleanOperator::Difference, Arrangement::SubjectInsideTool) => empty(),
135 (BooleanOperator::SymmetricDifference, Arrangement::SubjectInsideTool) => {
138 concatenate(tool, &reversed(subject))
139 }
140
141 (BooleanOperator::Union, Arrangement::ToolInsideSubject) => subject.clone(),
143 (BooleanOperator::Intersection, Arrangement::ToolInsideSubject) => tool.clone(),
144 (
145 BooleanOperator::Difference | BooleanOperator::SymmetricDifference,
146 Arrangement::ToolInsideSubject,
147 ) => concatenate(subject, &reversed(tool)),
148
149 _ => {
151 return Err(GeomError::Unsupported {
152 backend: Self::ID,
153 operation: Operation::MeshBoolean,
154 })
155 }
156 };
157
158 let evidence = BooleanEvidence::record(
159 subject.triangle_count(),
160 tool.triangle_count(),
161 mesh.triangle_count(),
162 components(&mesh),
163 )
164 .with_disjoint_tools(usize::from(arrangement == Arrangement::Disjoint));
165 Ok(BooleanOutcome::new(mesh, evidence))
166 }
167}
168
169fn exact_sign(certified: axiolid_contracts::Certified) -> Sign {
176 certified.sign().unwrap_or(Sign::Zero)
177}
178
179fn empty() -> TriMesh {
181 TriMesh::new(Vec::new(), Vec::new())
182}
183
184fn concatenate(a: &TriMesh, b: &TriMesh) -> TriMesh {
186 let offset = a.positions.len() as u32;
187 let mut positions = a.positions.clone();
188 positions.extend_from_slice(&b.positions);
189 let mut indices = a.indices.clone();
190 indices.extend(b.indices.iter().map(|i| i + offset));
191 TriMesh::new(positions, indices)
192}
193
194fn reversed(mesh: &TriMesh) -> TriMesh {
196 let mut indices = mesh.indices.clone();
197 for triangle in indices.chunks_exact_mut(3) {
198 triangle.swap(0, 1);
199 }
200 TriMesh::new(mesh.positions.clone(), indices)
201}
202
203fn components(mesh: &TriMesh) -> usize {
205 if mesh.positions.is_empty() {
206 return 0;
207 }
208 let mut parent: Vec<usize> = (0..mesh.positions.len()).collect();
209
210 fn find(parent: &mut [usize], mut node: usize) -> usize {
211 while parent[node] != node {
212 parent[node] = parent[parent[node]];
213 node = parent[node];
214 }
215 node
216 }
217
218 for triangle in mesh.indices.chunks_exact(3) {
219 let root = find(&mut parent, triangle[0] as usize);
220 for corner in &triangle[1..] {
221 let other = find(&mut parent, *corner as usize);
222 if root != other {
223 parent[other] = root;
224 }
225 }
226 }
227
228 let mut roots = std::collections::BTreeSet::new();
229 for index in &mesh.indices {
230 let root = find(&mut parent, *index as usize);
231 roots.insert(root);
232 }
233 roots.len()
234}
235
236fn classify(
238 subject: &TriMesh,
239 tool: &TriMesh,
240 options: &ExecutionOptions,
241) -> GeomResult<Arrangement> {
242 for (mesh, role) in [(subject, "subject"), (tool, "tool")] {
247 if mesh.positions.is_empty() || mesh.indices.is_empty() {
248 return Err(GeomError::InvalidInput(format!(
249 "{role}: an empty mesh has no interior and cannot be a boolean operand"
250 )));
251 }
252 }
253
254 if same_geometry(subject, tool) {
255 return Ok(Arrangement::Identical);
256 }
257
258 if surfaces_intersect(subject, tool, options)? {
262 return Err(GeomError::Unsupported {
263 backend: ScalarBoolean::ID,
264 operation: Operation::MeshBoolean,
265 });
266 }
267
268 let subject_in_tool = contains_point(tool, subject.positions[0]);
271 let tool_in_subject = contains_point(subject, tool.positions[0]);
272
273 Ok(match (subject_in_tool, tool_in_subject) {
274 (true, false) => Arrangement::SubjectInsideTool,
275 (false, true) => Arrangement::ToolInsideSubject,
276 (false, false) => Arrangement::Disjoint,
277 (true, true) => {
279 return Err(GeomError::Degenerate(
280 "operands report mutual containment, which is geometrically impossible".into(),
281 ))
282 }
283 })
284}
285
286fn same_geometry(a: &TriMesh, b: &TriMesh) -> bool {
288 if a.positions.len() != b.positions.len() || a.indices.len() != b.indices.len() {
289 return false;
290 }
291 if a.positions
292 .iter()
293 .zip(&b.positions)
294 .any(|(p, q)| p.x != q.x || p.y != q.y || p.z != q.z)
295 {
296 return false;
297 }
298 let mut left: Vec<[u32; 3]> = a
299 .indices
300 .chunks_exact(3)
301 .map(|t| {
302 let mut v = [t[0], t[1], t[2]];
303 v.sort_unstable();
304 v
305 })
306 .collect();
307 let mut right: Vec<[u32; 3]> = b
308 .indices
309 .chunks_exact(3)
310 .map(|t| {
311 let mut v = [t[0], t[1], t[2]];
312 v.sort_unstable();
313 v
314 })
315 .collect();
316 left.sort_unstable();
317 right.sort_unstable();
318 left == right
319}
320
321fn surfaces_intersect(a: &TriMesh, b: &TriMesh, options: &ExecutionOptions) -> GeomResult<bool> {
327 for left in a.indices.chunks_exact(3) {
328 options.check_cancelled()?;
329 let triangle_a = [
330 a.positions[left[0] as usize],
331 a.positions[left[1] as usize],
332 a.positions[left[2] as usize],
333 ];
334 for right in b.indices.chunks_exact(3) {
335 let triangle_b = [
336 b.positions[right[0] as usize],
337 b.positions[right[1] as usize],
338 b.positions[right[2] as usize],
339 ];
340 if edges_cross_triangle(&triangle_a, &triangle_b)
341 || edges_cross_triangle(&triangle_b, &triangle_a)
342 {
343 return Ok(true);
344 }
345 }
346 }
347 Ok(false)
348}
349
350fn edges_cross_triangle(edges: &[Point3; 3], face: &[Point3; 3]) -> bool {
352 let [p, q, r] = *face;
353 for (start, end) in [
354 (edges[0], edges[1]),
355 (edges[1], edges[2]),
356 (edges[2], edges[0]),
357 ] {
358 let side_start = exact_sign(orient3d(p, q, r, start));
359 let side_end = exact_sign(orient3d(p, q, r, end));
360 if side_start == Sign::Zero || side_end == Sign::Zero || side_start == side_end {
363 continue;
364 }
365 let signs = [
374 exact_sign(orient3d(start, end, p, q)),
375 exact_sign(orient3d(start, end, q, r)),
376 exact_sign(orient3d(start, end, r, p)),
377 ];
378 let positive = signs.contains(&Sign::Positive);
379 let negative = signs.contains(&Sign::Negative);
380 if !(positive && negative) {
381 return true;
382 }
383 }
384 false
385}
386
387fn contains_point(mesh: &TriMesh, point: Point3) -> bool {
393 const DIRECTIONS: [[f64; 3]; 4] = [
396 [1.0, 0.0, 0.0],
397 [1.0, 0.125, 0.0625],
398 [0.5, 1.0, 0.25],
399 [0.25, 0.5, 1.0],
400 ];
401
402 for direction in DIRECTIONS {
403 if let Some(inside) = parity_along(mesh, point, direction) {
404 return inside;
405 }
406 }
407 false
410}
411
412fn parity_along(mesh: &TriMesh, origin: Point3, direction: [f64; 3]) -> Option<bool> {
414 let bounds = mesh.bounds();
417 let span = (bounds.max.x - bounds.min.x)
418 .max(bounds.max.y - bounds.min.y)
419 .max(bounds.max.z - bounds.min.z)
420 .max(1.0)
421 * 8.0;
422 let far = Point3::new(
423 origin.x + direction[0] * span,
424 origin.y + direction[1] * span,
425 origin.z + direction[2] * span,
426 );
427
428 let mut crossings = 0usize;
429 for triangle in mesh.indices.chunks_exact(3) {
430 let p = mesh.positions[triangle[0] as usize];
431 let q = mesh.positions[triangle[1] as usize];
432 let r = mesh.positions[triangle[2] as usize];
433
434 let side_origin = exact_sign(orient3d(p, q, r, origin));
435 let side_far = exact_sign(orient3d(p, q, r, far));
436 if side_origin == Sign::Zero {
437 return Some(false);
439 }
440 if side_far == Sign::Zero || side_origin == side_far {
441 continue;
442 }
443
444 let a = exact_sign(orient3d(origin, far, p, q));
445 let b = exact_sign(orient3d(origin, far, q, r));
446 let c = exact_sign(orient3d(origin, far, r, p));
447 if a == Sign::Zero || b == Sign::Zero || c == Sign::Zero {
450 return None;
451 }
452 if a == b && b == c {
453 crossings += 1;
454 }
455 }
456 Some(crossings % 2 == 1)
457}