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