1mod 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#[derive(Debug, Clone, Copy, PartialEq)]
56pub struct MeshMeasurements {
57 pub surface: SurfaceProperties,
58 pub volume: VolumeProperties,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq)]
63pub struct CallContext {
64 pub operation: Operation,
65 pub provider: BackendId,
66 pub tolerance: Tolerance,
67}
68
69#[derive(Debug)]
71#[non_exhaustive]
72pub enum ApplicationErrorSource {
73 Geometry(axiolid_contracts::GeomError),
74 HiddenLines(HiddenLineError),
76 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#[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#[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 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#[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#[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 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 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#[derive(Debug, Clone)]
348pub struct Application {
349 #[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 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 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 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}