1use 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#[derive(Debug, Clone, Copy, PartialEq)]
45pub struct MeshMeasurements {
46 pub surface: SurfaceProperties,
47 pub volume: VolumeProperties,
48}
49
50#[derive(Debug, Clone, Copy, PartialEq)]
52pub struct CallContext {
53 pub operation: Operation,
54 pub provider: BackendId,
55 pub tolerance: Tolerance,
56}
57
58#[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#[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#[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#[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#[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 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 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#[derive(Debug, Clone)]
293pub struct Application {
294 #[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 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 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}