axiolid/
application.rs

1//! Supported application boundary for the portable v0.4 workflows.
2//!
3//! Concrete providers stay private. Selection is explicit on
4//! [`ApplicationBuilder`](crate::application::ApplicationBuilder),
5//! while results and errors use only public contract and representation types.
6
7use core::fmt;
8
9use axiolid_brep::ExactBRep;
10use axiolid_construct::extrude::extrude_profile_exact;
11use axiolid_contracts::{
12    capability_ids, BackendId, CapabilityDescriptor, Exactness, ExecutionOptions,
13    IntegrationDescriptor, Operation, Representation,
14};
15use axiolid_core::{BooleanOperator, Frame3, Ray3, Scalar, Tolerance, Vec3};
16use axiolid_dispatch::{MeshBooleanRegistry, MeshPlaneSectionRegistry};
17use axiolid_measure::{
18    surface_properties, volume_properties, MeshMeasureError, SurfaceProperties, VolumeProperties,
19};
20use axiolid_mesh::{try_audit_mesh, MeshAuditError, MeshHealth, TriMesh};
21use axiolid_mesh_boolean_boolmesh::BoolmeshBoolean;
22use axiolid_mesh_boolean_contract::{
23    conformance::ConformanceReport as BooleanConformanceReport, BooleanOutcome,
24};
25use axiolid_mesh_section_contract::{
26    conformance::ConformanceReport as SectionConformanceReport, SectionLimits, SectionOutcome,
27};
28use axiolid_profile::Profile;
29#[cfg(not(all(feature = "ray-mesh", feature = "spatial")))]
30use axiolid_ray_mesh::nearest_hit;
31use axiolid_ray_mesh::{RayHit3, RayMeshError};
32use axiolid_reference::ScalarSection;
33
34const APPLICATION_ID: BackendId = BackendId::new("axiolid-application");
35const MEASURE_ID: BackendId = BackendId::new("scalar-measure");
36const RAY_MESH_ID: BackendId = BackendId::new("scalar-ray-mesh");
37
38static MESH_INPUT: &[Representation] = &[Representation::TriangleMesh];
39static MESH_PAIR_INPUT: &[Representation] =
40    &[Representation::TriangleMesh, Representation::TriangleMesh];
41static PROFILE_INPUT: &[Representation] = &[Representation::Profile2d];
42
43/// Surface and signed-volume properties computed under one tolerance policy.
44#[derive(Debug, Clone, Copy, PartialEq)]
45pub struct MeshMeasurements {
46    pub surface: SurfaceProperties,
47    pub volume: VolumeProperties,
48}
49
50/// Context retained by every application-level failure.
51#[derive(Debug, Clone, Copy, PartialEq)]
52pub struct CallContext {
53    pub operation: Operation,
54    pub provider: BackendId,
55    pub tolerance: Tolerance,
56}
57
58/// Typed lower-level reason an application call failed.
59#[derive(Debug)]
60pub enum ApplicationErrorSource {
61    Geometry(axiolid_contracts::GeomError),
62    MeshAudit(Box<MeshAuditError>),
63    Measurement(Box<MeshMeasureError>),
64    RayMesh(RayMeshError),
65}
66
67impl fmt::Display for ApplicationErrorSource {
68    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
69        match self {
70            Self::Geometry(error) => error.fmt(formatter),
71            Self::MeshAudit(error) => error.fmt(formatter),
72            Self::Measurement(error) => error.fmt(formatter),
73            Self::RayMesh(error) => error.fmt(formatter),
74        }
75    }
76}
77
78impl std::error::Error for ApplicationErrorSource {
79    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
80        match self {
81            Self::Geometry(error) => Some(error),
82            Self::MeshAudit(error) => Some(error),
83            Self::Measurement(error) => Some(error),
84            Self::RayMesh(error) => Some(error),
85        }
86    }
87}
88
89/// Application failure with operation, selected provider, and tolerance intact.
90#[derive(Debug)]
91pub struct ApplicationError {
92    pub context: CallContext,
93    pub source: Box<ApplicationErrorSource>,
94}
95
96impl fmt::Display for ApplicationError {
97    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
98        write!(
99            formatter,
100            "{:?} via `{}` at tolerance {:?}: {}",
101            self.context.operation, self.context.provider, self.context.tolerance, self.source
102        )
103    }
104}
105
106impl std::error::Error for ApplicationError {
107    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
108        Some(self.source.as_ref())
109    }
110}
111
112/// Shared conformance report without provider implementation types.
113#[derive(Debug, Clone)]
114pub enum ProviderConformanceReport {
115    MeshBoolean(Box<BooleanConformanceReport>),
116    MeshSection(Box<SectionConformanceReport>),
117}
118
119impl fmt::Display for ProviderConformanceReport {
120    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
121        match self {
122            Self::MeshBoolean(report) => report.fmt(formatter),
123            Self::MeshSection(report) => report.fmt(formatter),
124        }
125    }
126}
127
128/// Portable-provider conformance failed before the application became usable.
129#[derive(Debug, Clone)]
130pub struct ApplicationBuildError {
131    pub operation: Operation,
132    pub provider: BackendId,
133    pub report: ProviderConformanceReport,
134}
135
136impl fmt::Display for ApplicationBuildError {
137    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
138        write!(
139            formatter,
140            "provider `{}` failed {:?} registration: {}",
141            self.provider, self.operation, self.report
142        )
143    }
144}
145
146impl std::error::Error for ApplicationBuildError {}
147
148/// Explicit assembly of the supported application boundary.
149#[derive(Debug, Clone, Default)]
150pub struct ApplicationBuilder {
151    boolean: MeshBooleanRegistry,
152    section: MeshPlaneSectionRegistry,
153    boolean_provider: Option<BackendId>,
154    section_provider: Option<BackendId>,
155}
156
157impl ApplicationBuilder {
158    #[must_use]
159    pub const fn new() -> Self {
160        Self {
161            boolean: MeshBooleanRegistry::new(),
162            section: MeshPlaneSectionRegistry::new(),
163            boolean_provider: None,
164            section_provider: None,
165        }
166    }
167
168    /// Select the pure-Rust boolmesh provider after running shared conformance.
169    pub fn with_portable_boolean(mut self) -> Result<Self, ApplicationBuildError> {
170        let provider = BoolmeshBoolean::new();
171        self.boolean
172            .register_conformant(0, provider)
173            .map_err(|report| ApplicationBuildError {
174                operation: Operation::MeshBoolean,
175                provider: BoolmeshBoolean::ID,
176                report: ProviderConformanceReport::MeshBoolean(report),
177            })?;
178        self.boolean_provider = Some(BoolmeshBoolean::ID);
179        Ok(self)
180    }
181
182    /// Select the deterministic scalar mesh-section provider after shared conformance.
183    pub fn with_portable_section(mut self) -> Result<Self, ApplicationBuildError> {
184        let provider = ScalarSection::new();
185        self.section
186            .register_conformant(0, provider)
187            .map_err(|report| ApplicationBuildError {
188                operation: Operation::MeshPlaneSection,
189                provider: ScalarSection::ID,
190                report: ProviderConformanceReport::MeshSection(report),
191            })?;
192        self.section_provider = Some(ScalarSection::ID);
193        Ok(self)
194    }
195
196    #[must_use]
197    pub fn build(self) -> Application {
198        let mut descriptor = crate::integration::descriptor();
199        descriptor.representations.extend([
200            Representation::MeshHealth,
201            Representation::Measurements,
202            Representation::RayHit,
203        ]);
204        descriptor.representations.sort_unstable();
205        descriptor.representations.dedup();
206        descriptor.capabilities.extend(direct_capabilities());
207        if let Some(provider) = self.boolean_provider {
208            descriptor.capabilities.push(CapabilityDescriptor {
209                id: capability_ids::MESH_BOOLEAN,
210                operation: Operation::MeshBoolean,
211                provider,
212                required_feature: "application",
213                inputs: MESH_PAIR_INPUT,
214                output: Representation::TriangleMesh,
215                exactness: Exactness::ToleranceBounded,
216                deterministic: true,
217            });
218        }
219        if let Some(provider) = self.section_provider {
220            descriptor.capabilities.push(CapabilityDescriptor {
221                id: capability_ids::MESH_SECTION,
222                operation: Operation::MeshPlaneSection,
223                provider,
224                required_feature: "application",
225                inputs: MESH_INPUT,
226                output: Representation::Profile2d,
227                exactness: Exactness::ToleranceBounded,
228                deterministic: true,
229            });
230        }
231        descriptor
232            .capabilities
233            .sort_by_key(|capability| capability.id);
234        Application {
235            #[cfg(all(feature = "ray-mesh", feature = "spatial"))]
236            ray_indices: crate::ray_index::RayIndexCache::default(),
237            boolean: self.boolean,
238            section: self.section,
239            boolean_provider: self.boolean_provider,
240            section_provider: self.section_provider,
241            descriptor,
242        }
243    }
244}
245
246fn direct_capabilities() -> [CapabilityDescriptor; 4] {
247    [
248        CapabilityDescriptor {
249            id: capability_ids::MESH_VALIDATE,
250            operation: Operation::Healing,
251            provider: APPLICATION_ID,
252            required_feature: "application",
253            inputs: MESH_INPUT,
254            output: Representation::MeshHealth,
255            exactness: Exactness::ToleranceBounded,
256            deterministic: true,
257        },
258        CapabilityDescriptor {
259            id: capability_ids::MESH_MEASURE,
260            operation: Operation::Measurement,
261            provider: MEASURE_ID,
262            required_feature: "application",
263            inputs: MESH_INPUT,
264            output: Representation::Measurements,
265            exactness: Exactness::ToleranceBounded,
266            deterministic: true,
267        },
268        CapabilityDescriptor {
269            id: capability_ids::RAY_MESH,
270            operation: Operation::SpatialQuery,
271            provider: RAY_MESH_ID,
272            required_feature: "application",
273            inputs: MESH_INPUT,
274            output: Representation::RayHit,
275            exactness: Exactness::ToleranceBounded,
276            deterministic: true,
277        },
278        CapabilityDescriptor {
279            id: capability_ids::EXACT_EXTRUDE,
280            operation: Operation::Sweep,
281            provider: axiolid_construct::BACKEND_ID,
282            required_feature: "application",
283            inputs: PROFILE_INPUT,
284            output: Representation::ExactBrep,
285            exactness: Exactness::Exact,
286            deterministic: true,
287        },
288    ]
289}
290
291/// Coherent application boundary backed by explicitly selected providers.
292#[derive(Debug, Clone)]
293pub struct Application {
294    /// Broad-phase indices for repeat ray casts. Behind a lock so the
295    /// public `&self` methods keep working and `Application` stays `Sync`.
296    #[cfg(all(feature = "ray-mesh", feature = "spatial"))]
297    ray_indices: crate::ray_index::RayIndexCache,
298    boolean: MeshBooleanRegistry,
299    section: MeshPlaneSectionRegistry,
300    boolean_provider: Option<BackendId>,
301    section_provider: Option<BackendId>,
302    descriptor: IntegrationDescriptor,
303}
304
305impl Application {
306    /// Build the supported pure-Rust provider set explicitly.
307    pub fn portable() -> Result<Self, ApplicationBuildError> {
308        Ok(ApplicationBuilder::new()
309            .with_portable_boolean()?
310            .with_portable_section()?
311            .build())
312    }
313
314    #[must_use]
315    pub fn descriptor(&self) -> &IntegrationDescriptor {
316        &self.descriptor
317    }
318
319    pub fn validate_mesh(
320        &self,
321        mesh: &TriMesh,
322        tolerance: Tolerance,
323    ) -> Result<MeshHealth, ApplicationError> {
324        try_audit_mesh(mesh, tolerance).map_err(|source| ApplicationError {
325            context: CallContext {
326                operation: Operation::Healing,
327                provider: APPLICATION_ID,
328                tolerance,
329            },
330            source: Box::new(ApplicationErrorSource::MeshAudit(Box::new(source))),
331        })
332    }
333
334    pub fn measure_mesh(
335        &self,
336        mesh: &TriMesh,
337        tolerance: Tolerance,
338    ) -> Result<MeshMeasurements, ApplicationError> {
339        let context = CallContext {
340            operation: Operation::Measurement,
341            provider: MEASURE_ID,
342            tolerance,
343        };
344        let surface = surface_properties(mesh, tolerance).map_err(|source| ApplicationError {
345            context,
346            source: Box::new(ApplicationErrorSource::Measurement(Box::new(source))),
347        })?;
348        let volume = volume_properties(mesh, tolerance).map_err(|source| ApplicationError {
349            context,
350            source: Box::new(ApplicationErrorSource::Measurement(Box::new(source))),
351        })?;
352        Ok(MeshMeasurements { surface, volume })
353    }
354
355    pub fn boolean(
356        &self,
357        subject: &TriMesh,
358        tool: &TriMesh,
359        operator: BooleanOperator,
360        options: &ExecutionOptions,
361    ) -> Result<BooleanOutcome, ApplicationError> {
362        self.boolean
363            .boolean(subject, tool, operator, options)
364            .map_err(|source| {
365                self.geometry_error(
366                    Operation::MeshBoolean,
367                    self.boolean_provider.unwrap_or(APPLICATION_ID),
368                    options.tolerance(),
369                    source,
370                )
371            })
372    }
373
374    pub fn subtract_many(
375        &self,
376        subject: &TriMesh,
377        tools: &[TriMesh],
378        options: &ExecutionOptions,
379    ) -> Result<BooleanOutcome, ApplicationError> {
380        self.boolean
381            .subtract_many(subject, tools, options)
382            .map_err(|source| {
383                self.geometry_error(
384                    Operation::MeshBoolean,
385                    self.boolean_provider.unwrap_or(APPLICATION_ID),
386                    options.tolerance(),
387                    source,
388                )
389            })
390    }
391
392    /// Union many solids in one batch.
393    ///
394    /// Providers may choose the reduction order; `boolmesh` reduces in a
395    /// balanced tree rather than folding left, which is materially faster
396    /// as the operand count grows.
397    pub fn union_many(
398        &self,
399        solids: &[TriMesh],
400        options: &ExecutionOptions,
401    ) -> Result<BooleanOutcome, ApplicationError> {
402        self.boolean.union_many(solids, options).map_err(|source| {
403            self.geometry_error(
404                Operation::MeshBoolean,
405                self.boolean_provider.unwrap_or(APPLICATION_ID),
406                options.tolerance(),
407                source,
408            )
409        })
410    }
411
412    pub fn section_mesh(
413        &self,
414        mesh: &TriMesh,
415        frame: Frame3,
416        limits: SectionLimits,
417        options: &ExecutionOptions,
418    ) -> Result<SectionOutcome, ApplicationError> {
419        self.section
420            .section(mesh, frame, limits, options)
421            .map_err(|source| {
422                self.geometry_error(
423                    Operation::MeshPlaneSection,
424                    self.section_provider.unwrap_or(APPLICATION_ID),
425                    options.tolerance(),
426                    source,
427                )
428            })
429    }
430
431    pub fn nearest_mesh_hit(
432        &self,
433        mesh: &TriMesh,
434        ray: &Ray3,
435        tolerance: Tolerance,
436    ) -> Result<Option<RayHit3>, ApplicationError> {
437        #[cfg(all(feature = "ray-mesh", feature = "spatial"))]
438        let outcome = self.ray_indices.nearest_hit(mesh, ray, tolerance);
439        #[cfg(not(all(feature = "ray-mesh", feature = "spatial")))]
440        let outcome = nearest_hit(mesh, ray, tolerance);
441        outcome.map_err(|source| ApplicationError {
442            context: CallContext {
443                operation: Operation::SpatialQuery,
444                provider: RAY_MESH_ID,
445                tolerance,
446            },
447            source: Box::new(ApplicationErrorSource::RayMesh(source)),
448        })
449    }
450
451    pub fn extrude_profile_exact(
452        &self,
453        profile: &Profile,
454        direction: Vec3,
455        depth: Scalar,
456        tolerance: Tolerance,
457    ) -> Result<ExactBRep, ApplicationError> {
458        extrude_profile_exact(profile, direction, depth, tolerance).map_err(|source| {
459            self.geometry_error(
460                Operation::Sweep,
461                axiolid_construct::BACKEND_ID,
462                tolerance,
463                source,
464            )
465        })
466    }
467
468    fn geometry_error(
469        &self,
470        operation: Operation,
471        provider: BackendId,
472        tolerance: Tolerance,
473        source: axiolid_contracts::GeomError,
474    ) -> ApplicationError {
475        ApplicationError {
476            context: CallContext {
477                operation,
478                provider,
479                tolerance,
480            },
481            source: Box::new(ApplicationErrorSource::Geometry(source)),
482        }
483    }
484}