Skip to main content

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}