Skip to main content

axiolid_mesh_boolean_contract/
contract.rs

1//! Portable mesh-boolean provider contract.
2
3use axiolid_contracts::{
4    Backend, CancellationGranularity, Determinism, ExecutionOptions, GeomResult, ScratchRequirement,
5};
6use axiolid_core::BooleanOperator;
7use axiolid_mesh::TriMesh;
8use axiolid_mesh_contracts::SolidRequirements;
9
10use crate::{merge_fates, 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    ///
23    /// The elements are the input triangles of every operand together, which
24    /// is what `axiolid-dispatch` charges. A provider that runs on a thread
25    /// pool declares what each worker costs as well
26    /// ([`ScratchRequirement::Affine`]), since that cost does not shrink with
27    /// the input (#226).
28    fn scratch_requirement(&self) -> ScratchRequirement {
29        ScratchRequirement::Unbounded
30    }
31
32    /// Reproducibility this provider guarantees for its results.
33    ///
34    /// Defaults to [`Determinism::BestEffort`]: the weakest level, so a
35    /// provider that has not audited its own reproducibility cannot silently
36    /// satisfy a stronger request. Overstating this is the dangerous
37    /// direction — `Plan::admit` refuses a step whose guarantee is weaker
38    /// than the caller asked for, and that refusal is only sound if the
39    /// declared level is honest.
40    ///
41    /// A provider whose output depends on thread scheduling, hash seeding, or
42    /// any other run-to-run variation must not claim [`Determinism::Bitwise`].
43    fn determinism(&self) -> Determinism {
44        Determinism::BestEffort
45    }
46
47    /// How finely this provider polls a cancellation token.
48    ///
49    /// Defaults to [`CancellationGranularity::None`]: a provider that has not
50    /// declared otherwise is assumed not to poll. Claiming responsiveness a
51    /// provider does not have is worse than admitting none.
52    fn cancellation_granularity(&self) -> CancellationGranularity {
53        CancellationGranularity::None
54    }
55
56    /// Admissibility this provider requires of its operands.
57    ///
58    /// Advisory only: the registry validates at the contract level before
59    /// dispatch. A provider declaring a *lower* level does not thereby get to
60    /// accept looser input, and one declaring a higher level is rejected by the
61    /// conformance suite for narrowing the contract.
62    fn solid_requirements(&self) -> SolidRequirements {
63        SolidRequirements::Oriented
64    }
65
66    /// Apply one regularized set operation.
67    ///
68    /// Operands are pre-validated by the registry. Returns a
69    /// [`BooleanOutcome`]: the mesh plus what was done to produce it. An empty
70    /// result mesh is a legitimate value, not an error.
71    fn boolean(
72        &self,
73        subject: &TriMesh,
74        tool: &TriMesh,
75        operation: BooleanOperator,
76        options: &ExecutionOptions,
77    ) -> GeomResult<BooleanOutcome>;
78
79    /// Subtract many tools in one batch so implementations can union or schedule
80    /// cutters efficiently. The default is correct but deliberately simple.
81    ///
82    /// The default polls cancellation between tools, which is why the default
83    /// granularity for an overriding provider must be declared honestly.
84    fn subtract_many(
85        &self,
86        subject: &TriMesh,
87        tools: &[TriMesh],
88        options: &ExecutionOptions,
89    ) -> GeomResult<BooleanOutcome> {
90        let mut evidence = BooleanEvidence {
91            subject_triangles: subject.triangle_count(),
92            tool_triangles: tools.iter().map(TriMesh::triangle_count).sum(),
93            output_triangles: subject.triangle_count(),
94            output_components: 1,
95            ..BooleanEvidence::default()
96        };
97        let mut result = subject.clone();
98        for tool in tools {
99            options.check_cancelled()?;
100            let outcome = self.boolean(&result, tool, BooleanOperator::Difference, options)?;
101            evidence.absorb(outcome.evidence);
102            result = outcome.mesh;
103        }
104        Ok(BooleanOutcome::new(result, evidence))
105    }
106
107    /// Union many solids in one batch so implementations can choose a
108    /// reduction order.
109    ///
110    /// The default folds left, which is correct but makes step `i` pay for
111    /// an accumulator holding `i` operands -- quadratic total work. A
112    /// provider that can do better should override this; see
113    /// `BoolmeshBoolean::union_tree` for a balanced reduction that measures
114    /// 28.9x faster on a 512-sphere grid.
115    ///
116    /// An empty slice yields an empty solid: the union of nothing is
117    /// nothing, which is a legitimate answer rather than an error.
118    ///
119    /// The default polls cancellation between operands, which is why the
120    /// declared granularity for an overriding provider must stay honest.
121    fn union_many(
122        &self,
123        solids: &[TriMesh],
124        options: &ExecutionOptions,
125    ) -> GeomResult<BooleanOutcome> {
126        let Some((first, rest)) = solids.split_first() else {
127            return Ok(BooleanOutcome::new(
128                TriMesh::default(),
129                BooleanEvidence::default(),
130            ));
131        };
132
133        let mut evidence = BooleanEvidence {
134            subject_triangles: first.triangle_count(),
135            tool_triangles: rest.iter().map(TriMesh::triangle_count).sum(),
136            output_triangles: first.triangle_count(),
137            output_components: 1,
138            ..BooleanEvidence::default()
139        };
140        let mut result = first.clone();
141        for solid in rest {
142            options.check_cancelled()?;
143            let outcome = self.boolean(&result, solid, BooleanOperator::Union, options)?;
144            evidence.absorb(outcome.evidence);
145            result = outcome.mesh;
146        }
147        Ok(BooleanOutcome::new(result, evidence))
148    }
149}
150
151/// Compose `A △ B` as `(A ∪ B) \ (A ∩ B)`.
152///
153/// Free-standing rather than a trait default so a provider cannot accidentally
154/// inherit a composed implementation while reporting `sub_operations: 1`. A
155/// native implementor overrides [`MeshBoolean::boolean`] and never calls this.
156///
157/// Composition is the reason `BooleanEvidence::sub_operations` exists: without
158/// it a caller cannot tell a three-pass emulation from a single-pass primitive,
159/// and the two have materially different numerical behaviour.
160pub fn symmetric_difference_via_composition<P>(
161    provider: &P,
162    subject: &TriMesh,
163    tool: &TriMesh,
164    options: &ExecutionOptions,
165) -> GeomResult<BooleanOutcome>
166where
167    P: MeshBoolean + ?Sized,
168{
169    options.check_cancelled()?;
170    let union = provider.boolean(subject, tool, BooleanOperator::Union, options)?;
171    options.check_cancelled()?;
172    let intersection = provider.boolean(subject, tool, BooleanOperator::Intersection, options)?;
173
174    // A ∩ B empty means the operands are disjoint, so A △ B == A ∪ B. Skipping
175    // the final difference is not just an optimisation: subtracting an empty
176    // solid is a degenerate operand many backends reject.
177    if intersection.mesh.indices.is_empty() {
178        let mut evidence = union.evidence;
179        evidence.sub_operations = 2;
180        evidence.coincident_faces_encountered |= intersection.evidence.coincident_faces_encountered;
181        return Ok(BooleanOutcome::new(union.mesh, evidence));
182    }
183
184    options.check_cancelled()?;
185    let difference = provider.boolean(
186        &union.mesh,
187        &intersection.mesh,
188        BooleanOperator::Difference,
189        options,
190    )?;
191
192    let mut evidence = difference.evidence;
193    // The result's channels came through union then difference: compose
194    // those fates. The difference alone would report `Preserved` for values
195    // the union already derived.
196    evidence.attribute_fates =
197        merge_fates(&union.evidence.attribute_fates, evidence.attribute_fates);
198    evidence.subject_triangles = subject.triangle_count();
199    evidence.tool_triangles = tool.triangle_count();
200    evidence.sub_operations = 3;
201    evidence.coincident_faces_encountered |= union.evidence.coincident_faces_encountered
202        || intersection.evidence.coincident_faces_encountered;
203    Ok(BooleanOutcome::new(difference.mesh, evidence))
204}