axiolid_mesh_boolean_contract/contract.rs
1//! Portable mesh-boolean provider contract.
2
3use axiolid_contracts::{
4 Backend, CancellationGranularity, ExecutionOptions, GeomResult, ScratchRequirement,
5};
6use axiolid_core::BooleanOperator;
7use axiolid_mesh::TriMesh;
8use axiolid_mesh_contracts::SolidRequirements;
9
10use crate::{BooleanEvidence, BooleanOutcome};
11
12/// Mesh boolean provider.
13///
14/// Implementing this trait is the capability declaration. Providers that do not
15/// implement mesh booleans must not implement this trait.
16pub trait MeshBoolean: Backend {
17 /// Scratch this provider needs beyond its inputs and result.
18 ///
19 /// Callers budget against this before dispatch. Defaults to
20 /// [`ScratchRequirement::Unbounded`] so an unaudited provider is treated as
21 /// unbudgetable rather than silently assumed cheap.
22 fn scratch_requirement(&self) -> ScratchRequirement {
23 ScratchRequirement::Unbounded
24 }
25
26 /// How finely this provider polls a cancellation token.
27 ///
28 /// Defaults to [`CancellationGranularity::None`]: a provider that has not
29 /// declared otherwise is assumed not to poll. Claiming responsiveness a
30 /// provider does not have is worse than admitting none.
31 fn cancellation_granularity(&self) -> CancellationGranularity {
32 CancellationGranularity::None
33 }
34
35 /// Admissibility this provider requires of its operands.
36 ///
37 /// Advisory only: the registry validates at the contract level before
38 /// dispatch. A provider declaring a *lower* level does not thereby get to
39 /// accept looser input, and one declaring a higher level is rejected by the
40 /// conformance suite for narrowing the contract.
41 fn solid_requirements(&self) -> SolidRequirements {
42 SolidRequirements::Oriented
43 }
44
45 /// Apply one regularized set operation.
46 ///
47 /// Operands are pre-validated by the registry. Returns a
48 /// [`BooleanOutcome`]: the mesh plus what was done to produce it. An empty
49 /// result mesh is a legitimate value, not an error.
50 fn boolean(
51 &self,
52 subject: &TriMesh,
53 tool: &TriMesh,
54 operation: BooleanOperator,
55 options: &ExecutionOptions,
56 ) -> GeomResult<BooleanOutcome>;
57
58 /// Subtract many tools in one batch so implementations can union or schedule
59 /// cutters efficiently. The default is correct but deliberately simple.
60 ///
61 /// The default polls cancellation between tools, which is why the default
62 /// granularity for an overriding provider must be declared honestly.
63 fn subtract_many(
64 &self,
65 subject: &TriMesh,
66 tools: &[TriMesh],
67 options: &ExecutionOptions,
68 ) -> GeomResult<BooleanOutcome> {
69 let mut evidence = BooleanEvidence {
70 subject_triangles: subject.triangle_count(),
71 tool_triangles: tools.iter().map(TriMesh::triangle_count).sum(),
72 output_triangles: subject.triangle_count(),
73 output_components: 1,
74 ..BooleanEvidence::default()
75 };
76 let mut result = subject.clone();
77 for tool in tools {
78 options.check_cancelled()?;
79 let outcome = self.boolean(&result, tool, BooleanOperator::Difference, options)?;
80 evidence.absorb(outcome.evidence);
81 result = outcome.mesh;
82 }
83 Ok(BooleanOutcome::new(result, evidence))
84 }
85}
86
87/// Compose `A △ B` as `(A ∪ B) \ (A ∩ B)`.
88///
89/// Free-standing rather than a trait default so a provider cannot accidentally
90/// inherit a composed implementation while reporting `sub_operations: 1`. A
91/// native implementor overrides [`MeshBoolean::boolean`] and never calls this.
92///
93/// Composition is the reason `BooleanEvidence::sub_operations` exists: without
94/// it a caller cannot tell a three-pass emulation from a single-pass primitive,
95/// and the two have materially different numerical behaviour.
96pub fn symmetric_difference_via_composition<P>(
97 provider: &P,
98 subject: &TriMesh,
99 tool: &TriMesh,
100 options: &ExecutionOptions,
101) -> GeomResult<BooleanOutcome>
102where
103 P: MeshBoolean + ?Sized,
104{
105 options.check_cancelled()?;
106 let union = provider.boolean(subject, tool, BooleanOperator::Union, options)?;
107 options.check_cancelled()?;
108 let intersection = provider.boolean(subject, tool, BooleanOperator::Intersection, options)?;
109
110 // A ∩ B empty means the operands are disjoint, so A △ B == A ∪ B. Skipping
111 // the final difference is not just an optimisation: subtracting an empty
112 // solid is a degenerate operand many backends reject.
113 if intersection.mesh.indices.is_empty() {
114 let mut evidence = union.evidence;
115 evidence.sub_operations = 2;
116 evidence.coincident_faces_encountered |= intersection.evidence.coincident_faces_encountered;
117 return Ok(BooleanOutcome::new(union.mesh, evidence));
118 }
119
120 options.check_cancelled()?;
121 let difference = provider.boolean(
122 &union.mesh,
123 &intersection.mesh,
124 BooleanOperator::Difference,
125 options,
126 )?;
127
128 let mut evidence = difference.evidence;
129 evidence.subject_triangles = subject.triangle_count();
130 evidence.tool_triangles = tool.triangle_count();
131 evidence.sub_operations = 3;
132 evidence.coincident_faces_encountered |= union.evidence.coincident_faces_encountered
133 || intersection.evidence.coincident_faces_encountered;
134 Ok(BooleanOutcome::new(difference.mesh, evidence))
135}