Skip to main content

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//!
7//! Hidden-line drawings and visible faces (ADR 0090, ADR 0094) are part of
8//! this boundary (ADR 0105); the `drawing` module holds their builder calls,
9//! methods and the once-per-process conformance verification they rely on.
10
11mod drawing;
12
13use core::fmt;
14
15use axiolid_brep::ExactBRep;
16use axiolid_construct::extrude::extrude_profile_exact;
17use axiolid_contracts::{
18    capability_ids, BackendId, CapabilityDescriptor, Exactness, ExecutionOptions,
19    IntegrationDescriptor, Operation, Representation,
20};
21use axiolid_core::{BooleanOperator, Frame3, Ray3, Scalar, Tolerance, Vec3};
22use axiolid_dispatch::{
23    MeshBooleanRegistry, MeshHiddenLinesRegistry, MeshPlaneSectionRegistry,
24    MeshVisibleFacesRegistry,
25};
26use axiolid_measure::{
27    surface_properties, volume_properties, MeshMeasureError, SurfaceProperties, VolumeProperties,
28};
29use axiolid_mesh::{try_audit_mesh, MeshAuditError, MeshHealth, TriMesh};
30use axiolid_mesh_boolean_boolmesh::BoolmeshBoolean;
31use axiolid_mesh_boolean_contract::{
32    conformance::ConformanceReport as BooleanConformanceReport, BooleanOutcome,
33};
34use axiolid_mesh_hidden_lines_contract::{conformance as drawing_conformance, HiddenLineError};
35use axiolid_mesh_section_contract::{
36    conformance::ConformanceReport as SectionConformanceReport, PlaneSection, SectionLimits,
37    SectionOutcome, SectionRequest,
38};
39use axiolid_profile::Profile;
40#[cfg(not(all(feature = "ray-mesh", feature = "spatial")))]
41use axiolid_ray_mesh::nearest_hit;
42use axiolid_ray_mesh::{RayHit3, RayMeshError};
43use axiolid_reference::ScalarSection;
44
45const APPLICATION_ID: BackendId = BackendId::new("axiolid-application");
46const MEASURE_ID: BackendId = BackendId::new("scalar-measure");
47const RAY_MESH_ID: BackendId = BackendId::new("scalar-ray-mesh");
48
49static MESH_INPUT: &[Representation] = &[Representation::TriangleMesh];
50static MESH_PAIR_INPUT: &[Representation] =
51    &[Representation::TriangleMesh, Representation::TriangleMesh];
52static PROFILE_INPUT: &[Representation] = &[Representation::Profile2d];
53
54/// Surface and signed-volume properties computed under one tolerance policy.
55#[derive(Debug, Clone, Copy, PartialEq)]
56pub struct MeshMeasurements {
57    pub surface: SurfaceProperties,
58    pub volume: VolumeProperties,
59}
60
61/// Context retained by every application-level failure.
62#[derive(Debug, Clone, Copy, PartialEq)]
63pub struct CallContext {
64    pub operation: Operation,
65    pub provider: BackendId,
66    pub tolerance: Tolerance,
67}
68
69/// Typed lower-level reason an application call failed.
70#[derive(Debug)]
71#[non_exhaustive]
72pub enum ApplicationErrorSource {
73    Geometry(axiolid_contracts::GeomError),
74    /// A drawing refusal; keeps the mesh it names as data (ADR 0090).
75    HiddenLines(HiddenLineError),
76    /// A portable provider failed its conformance suite when first used, so
77    /// the call was refused without output.
78    ProviderConformance(ProviderConformanceReport),
79    MeshAudit(Box<MeshAuditError>),
80    Measurement(Box<MeshMeasureError>),
81    RayMesh(RayMeshError),
82}
83
84impl fmt::Display for ApplicationErrorSource {
85    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
86        match self {
87            Self::Geometry(error) => error.fmt(formatter),
88            Self::HiddenLines(error) => error.fmt(formatter),
89            Self::ProviderConformance(report) => report.fmt(formatter),
90            Self::MeshAudit(error) => error.fmt(formatter),
91            Self::Measurement(error) => error.fmt(formatter),
92            Self::RayMesh(error) => error.fmt(formatter),
93        }
94    }
95}
96
97impl std::error::Error for ApplicationErrorSource {
98    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
99        match self {
100            Self::Geometry(error) => Some(error),
101            Self::HiddenLines(error) => Some(error),
102            Self::ProviderConformance(_) => None,
103            Self::MeshAudit(error) => Some(error),
104            Self::Measurement(error) => Some(error),
105            Self::RayMesh(error) => Some(error),
106        }
107    }
108}
109
110/// Application failure with operation, selected provider, and tolerance intact.
111#[derive(Debug)]
112pub struct ApplicationError {
113    pub context: CallContext,
114    pub source: Box<ApplicationErrorSource>,
115}
116
117impl fmt::Display for ApplicationError {
118    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
119        write!(
120            formatter,
121            "{:?} via `{}` at tolerance {:?}: {}",
122            self.context.operation, self.context.provider, self.context.tolerance, self.source
123        )
124    }
125}
126
127impl std::error::Error for ApplicationError {
128    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
129        Some(self.source.as_ref())
130    }
131}
132
133/// Shared conformance report without provider implementation types.
134#[derive(Debug, Clone)]
135#[non_exhaustive]
136pub enum ProviderConformanceReport {
137    MeshBoolean(Box<BooleanConformanceReport>),
138    MeshSection(Box<SectionConformanceReport>),
139    MeshHiddenLines(Box<drawing_conformance::ConformanceReport>),
140    MeshVisibleFaces(Box<drawing_conformance::faces::FaceConformanceReport>),
141}
142
143impl ProviderConformanceReport {
144    /// Whether the suite found nothing to report.
145    fn is_success(&self) -> bool {
146        match self {
147            Self::MeshBoolean(report) => report.is_conformant(),
148            Self::MeshSection(report) => report.is_success(),
149            Self::MeshHiddenLines(report) => report.is_success(),
150            Self::MeshVisibleFaces(report) => report.is_success(),
151        }
152    }
153}
154
155impl fmt::Display for ProviderConformanceReport {
156    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
157        match self {
158            Self::MeshBoolean(report) => report.fmt(formatter),
159            Self::MeshSection(report) => report.fmt(formatter),
160            Self::MeshHiddenLines(report) => report.fmt(formatter),
161            Self::MeshVisibleFaces(report) => report.fmt(formatter),
162        }
163    }
164}
165
166/// Portable-provider conformance failed before the application became usable.
167#[derive(Debug, Clone)]
168pub struct ApplicationBuildError {
169    pub operation: Operation,
170    pub provider: BackendId,
171    pub report: ProviderConformanceReport,
172}
173
174impl fmt::Display for ApplicationBuildError {
175    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
176        write!(
177            formatter,
178            "provider `{}` failed {:?} registration: {}",
179            self.provider, self.operation, self.report
180        )
181    }
182}
183
184impl std::error::Error for ApplicationBuildError {}
185
186/// Explicit assembly of the supported application boundary.
187#[derive(Debug, Clone, Default)]
188pub struct ApplicationBuilder {
189    boolean: MeshBooleanRegistry,
190    section: MeshPlaneSectionRegistry,
191    hidden_lines: MeshHiddenLinesRegistry,
192    visible_faces: MeshVisibleFacesRegistry,
193    boolean_provider: Option<BackendId>,
194    section_provider: Option<BackendId>,
195    hidden_lines_provider: Option<BackendId>,
196    visible_faces_provider: Option<BackendId>,
197}
198
199impl ApplicationBuilder {
200    #[must_use]
201    pub const fn new() -> Self {
202        Self {
203            boolean: MeshBooleanRegistry::new(),
204            section: MeshPlaneSectionRegistry::new(),
205            hidden_lines: MeshHiddenLinesRegistry::new(),
206            visible_faces: MeshVisibleFacesRegistry::new(),
207            boolean_provider: None,
208            section_provider: None,
209            hidden_lines_provider: None,
210            visible_faces_provider: None,
211        }
212    }
213
214    /// Select the pure-Rust boolmesh provider after running shared conformance.
215    pub fn with_portable_boolean(mut self) -> Result<Self, ApplicationBuildError> {
216        let provider = BoolmeshBoolean::new();
217        self.boolean
218            .register_conformant(0, provider)
219            .map_err(|report| ApplicationBuildError {
220                operation: Operation::MeshBoolean,
221                provider: BoolmeshBoolean::ID,
222                report: ProviderConformanceReport::MeshBoolean(report),
223            })?;
224        self.boolean_provider = Some(BoolmeshBoolean::ID);
225        Ok(self)
226    }
227
228    /// Select the deterministic scalar mesh-section provider after shared conformance.
229    pub fn with_portable_section(mut self) -> Result<Self, ApplicationBuildError> {
230        let provider = ScalarSection::new();
231        self.section
232            .register_conformant(0, provider)
233            .map_err(|report| ApplicationBuildError {
234                operation: Operation::MeshPlaneSection,
235                provider: ScalarSection::ID,
236                report: ProviderConformanceReport::MeshSection(report),
237            })?;
238        self.section_provider = Some(ScalarSection::ID);
239        Ok(self)
240    }
241
242    #[must_use]
243    pub fn build(self) -> Application {
244        let mut descriptor = crate::integration::descriptor();
245        descriptor.representations.extend([
246            Representation::MeshHealth,
247            Representation::Measurements,
248            Representation::RayHit,
249        ]);
250        descriptor.representations.sort_unstable();
251        descriptor.representations.dedup();
252        descriptor.capabilities.extend(direct_capabilities());
253        if let Some(provider) = self.boolean_provider {
254            descriptor.capabilities.push(CapabilityDescriptor {
255                id: capability_ids::MESH_BOOLEAN,
256                operation: Operation::MeshBoolean,
257                provider,
258                required_feature: "application",
259                inputs: MESH_PAIR_INPUT,
260                output: Representation::TriangleMesh,
261                exactness: Exactness::ToleranceBounded,
262                deterministic: true,
263            });
264        }
265        if let Some(provider) = self.section_provider {
266            descriptor.capabilities.push(CapabilityDescriptor {
267                id: capability_ids::MESH_SECTION,
268                operation: Operation::MeshPlaneSection,
269                provider,
270                required_feature: "application",
271                inputs: MESH_INPUT,
272                output: Representation::Profile2d,
273                exactness: Exactness::ToleranceBounded,
274                deterministic: true,
275            });
276        }
277        drawing::push_capabilities(
278            &mut descriptor,
279            self.hidden_lines_provider,
280            self.visible_faces_provider,
281        );
282        descriptor
283            .capabilities
284            .sort_by_key(|capability| capability.id);
285        Application {
286            #[cfg(all(feature = "ray-mesh", feature = "spatial"))]
287            ray_indices: crate::ray_index::RayIndexCache::default(),
288            boolean: self.boolean,
289            section: self.section,
290            hidden_lines: self.hidden_lines,
291            visible_faces: self.visible_faces,
292            boolean_provider: self.boolean_provider,
293            section_provider: self.section_provider,
294            hidden_lines_provider: self.hidden_lines_provider,
295            visible_faces_provider: self.visible_faces_provider,
296            descriptor,
297        }
298    }
299}
300
301fn direct_capabilities() -> [CapabilityDescriptor; 4] {
302    [
303        CapabilityDescriptor {
304            id: capability_ids::MESH_VALIDATE,
305            operation: Operation::Healing,
306            provider: APPLICATION_ID,
307            required_feature: "application",
308            inputs: MESH_INPUT,
309            output: Representation::MeshHealth,
310            exactness: Exactness::ToleranceBounded,
311            deterministic: true,
312        },
313        CapabilityDescriptor {
314            id: capability_ids::MESH_MEASURE,
315            operation: Operation::Measurement,
316            provider: MEASURE_ID,
317            required_feature: "application",
318            inputs: MESH_INPUT,
319            output: Representation::Measurements,
320            exactness: Exactness::ToleranceBounded,
321            deterministic: true,
322        },
323        CapabilityDescriptor {
324            id: capability_ids::RAY_MESH,
325            operation: Operation::SpatialQuery,
326            provider: RAY_MESH_ID,
327            required_feature: "application",
328            inputs: MESH_INPUT,
329            output: Representation::RayHit,
330            exactness: Exactness::ToleranceBounded,
331            deterministic: true,
332        },
333        CapabilityDescriptor {
334            id: capability_ids::EXACT_EXTRUDE,
335            operation: Operation::Sweep,
336            provider: axiolid_construct::BACKEND_ID,
337            required_feature: "application",
338            inputs: PROFILE_INPUT,
339            output: Representation::ExactBrep,
340            exactness: Exactness::Exact,
341            deterministic: true,
342        },
343    ]
344}
345
346/// Coherent application boundary backed by explicitly selected providers.
347#[derive(Debug, Clone)]
348pub struct Application {
349    /// Broad-phase indices for repeat ray casts. Behind a lock so the
350    /// public `&self` methods keep working and `Application` stays `Sync`.
351    #[cfg(all(feature = "ray-mesh", feature = "spatial"))]
352    ray_indices: crate::ray_index::RayIndexCache,
353    boolean: MeshBooleanRegistry,
354    section: MeshPlaneSectionRegistry,
355    hidden_lines: MeshHiddenLinesRegistry,
356    visible_faces: MeshVisibleFacesRegistry,
357    boolean_provider: Option<BackendId>,
358    section_provider: Option<BackendId>,
359    hidden_lines_provider: Option<BackendId>,
360    visible_faces_provider: Option<BackendId>,
361    descriptor: IntegrationDescriptor,
362}
363
364impl Application {
365    /// Build the supported pure-Rust provider set explicitly.
366    pub fn portable() -> Result<Self, ApplicationBuildError> {
367        Ok(ApplicationBuilder::new()
368            .with_portable_boolean()?
369            .with_portable_section()?
370            .with_portable_hidden_lines()
371            .with_portable_visible_faces()
372            .build())
373    }
374
375    #[must_use]
376    pub fn descriptor(&self) -> &IntegrationDescriptor {
377        &self.descriptor
378    }
379
380    pub fn validate_mesh(
381        &self,
382        mesh: &TriMesh,
383        tolerance: Tolerance,
384    ) -> Result<MeshHealth, ApplicationError> {
385        try_audit_mesh(mesh, tolerance).map_err(|source| ApplicationError {
386            context: CallContext {
387                operation: Operation::Healing,
388                provider: APPLICATION_ID,
389                tolerance,
390            },
391            source: Box::new(ApplicationErrorSource::MeshAudit(Box::new(source))),
392        })
393    }
394
395    pub fn measure_mesh(
396        &self,
397        mesh: &TriMesh,
398        tolerance: Tolerance,
399    ) -> Result<MeshMeasurements, ApplicationError> {
400        let context = CallContext {
401            operation: Operation::Measurement,
402            provider: MEASURE_ID,
403            tolerance,
404        };
405        let surface = surface_properties(mesh, tolerance).map_err(|source| ApplicationError {
406            context,
407            source: Box::new(ApplicationErrorSource::Measurement(Box::new(source))),
408        })?;
409        let volume = volume_properties(mesh, tolerance).map_err(|source| ApplicationError {
410            context,
411            source: Box::new(ApplicationErrorSource::Measurement(Box::new(source))),
412        })?;
413        Ok(MeshMeasurements { surface, volume })
414    }
415
416    pub fn boolean(
417        &self,
418        subject: &TriMesh,
419        tool: &TriMesh,
420        operator: BooleanOperator,
421        options: &ExecutionOptions,
422    ) -> Result<BooleanOutcome, ApplicationError> {
423        self.boolean
424            .boolean(subject, tool, operator, options)
425            .map_err(|source| {
426                self.geometry_error(
427                    Operation::MeshBoolean,
428                    self.boolean_provider.unwrap_or(APPLICATION_ID),
429                    options.tolerance(),
430                    source,
431                )
432            })
433    }
434
435    pub fn subtract_many(
436        &self,
437        subject: &TriMesh,
438        tools: &[TriMesh],
439        options: &ExecutionOptions,
440    ) -> Result<BooleanOutcome, ApplicationError> {
441        self.boolean
442            .subtract_many(subject, tools, options)
443            .map_err(|source| {
444                self.geometry_error(
445                    Operation::MeshBoolean,
446                    self.boolean_provider.unwrap_or(APPLICATION_ID),
447                    options.tolerance(),
448                    source,
449                )
450            })
451    }
452
453    /// Union many solids in one batch.
454    ///
455    /// Providers may choose the reduction order; `boolmesh` reduces in a
456    /// balanced tree rather than folding left, which is materially faster
457    /// as the operand count grows.
458    pub fn union_many(
459        &self,
460        solids: &[TriMesh],
461        options: &ExecutionOptions,
462    ) -> Result<BooleanOutcome, ApplicationError> {
463        self.boolean.union_many(solids, options).map_err(|source| {
464            self.geometry_error(
465                Operation::MeshBoolean,
466                self.boolean_provider.unwrap_or(APPLICATION_ID),
467                options.tolerance(),
468                source,
469            )
470        })
471    }
472
473    pub fn section_mesh(
474        &self,
475        mesh: &TriMesh,
476        frame: Frame3,
477        limits: SectionLimits,
478        options: &ExecutionOptions,
479    ) -> Result<SectionOutcome, ApplicationError> {
480        self.section
481            .section(mesh, frame, limits, options)
482            .map_err(|source| {
483                self.geometry_error(
484                    Operation::MeshPlaneSection,
485                    self.section_provider.unwrap_or(APPLICATION_ID),
486                    options.tolerance(),
487                    source,
488                )
489            })
490    }
491
492    /// Section a closed oriented mesh, reporting faces that lie in the plane
493    /// as regions with a side and cut curves as transversal crossings only
494    /// (ADR 0091). The provider must implement `section_request`.
495    pub fn section_mesh_request(
496        &self,
497        mesh: &TriMesh,
498        request: &SectionRequest,
499        options: &ExecutionOptions,
500    ) -> Result<PlaneSection, ApplicationError> {
501        self.section
502            .section_request(mesh, request, options)
503            .map_err(|source| {
504                self.geometry_error(
505                    Operation::MeshPlaneSection,
506                    self.section_provider.unwrap_or(APPLICATION_ID),
507                    options.tolerance(),
508                    source,
509                )
510            })
511    }
512
513    pub fn nearest_mesh_hit(
514        &self,
515        mesh: &TriMesh,
516        ray: &Ray3,
517        tolerance: Tolerance,
518    ) -> Result<Option<RayHit3>, ApplicationError> {
519        #[cfg(all(feature = "ray-mesh", feature = "spatial"))]
520        let outcome = self.ray_indices.nearest_hit(mesh, ray, tolerance);
521        #[cfg(not(all(feature = "ray-mesh", feature = "spatial")))]
522        let outcome = nearest_hit(mesh, ray, tolerance);
523        outcome.map_err(|source| ApplicationError {
524            context: CallContext {
525                operation: Operation::SpatialQuery,
526                provider: RAY_MESH_ID,
527                tolerance,
528            },
529            source: Box::new(ApplicationErrorSource::RayMesh(source)),
530        })
531    }
532
533    pub fn extrude_profile_exact(
534        &self,
535        profile: &Profile,
536        direction: Vec3,
537        depth: Scalar,
538        tolerance: Tolerance,
539    ) -> Result<ExactBRep, ApplicationError> {
540        extrude_profile_exact(profile, direction, depth, tolerance).map_err(|source| {
541            self.geometry_error(
542                Operation::Sweep,
543                axiolid_construct::BACKEND_ID,
544                tolerance,
545                source,
546            )
547        })
548    }
549
550    fn geometry_error(
551        &self,
552        operation: Operation,
553        provider: BackendId,
554        tolerance: Tolerance,
555        source: axiolid_contracts::GeomError,
556    ) -> ApplicationError {
557        ApplicationError {
558            context: CallContext {
559                operation,
560                provider,
561                tolerance,
562            },
563            source: Box::new(ApplicationErrorSource::Geometry(source)),
564        }
565    }
566}
567
568#[cfg(test)]
569mod tests {
570    use super::*;
571
572    #[test]
573    fn a_report_with_a_failure_is_not_a_success_in_every_arm() {
574        use axiolid_mesh_hidden_lines_contract::conformance::{
575            faces::{FaceConformanceFailure, FaceConformanceReport},
576            ConformanceFailure, ConformanceReport,
577        };
578
579        let mut lines = ConformanceReport::default();
580        lines
581            .failures
582            .push(ConformanceFailure::NonDeterministic { case: "x" });
583        let mut faces = FaceConformanceReport::default();
584        faces
585            .failures
586            .push(FaceConformanceFailure::NonDeterministic { case: "x" });
587        assert!(ProviderConformanceReport::MeshHiddenLines(Box::default()).is_success());
588        assert!(ProviderConformanceReport::MeshVisibleFaces(Box::default()).is_success());
589        assert!(!ProviderConformanceReport::MeshHiddenLines(Box::new(lines)).is_success());
590        assert!(!ProviderConformanceReport::MeshVisibleFaces(Box::new(faces)).is_success());
591    }
592}