1use std::collections::{HashMap, HashSet};
50
51use axiolid_brep::{ExactBRep, TransformError};
52use axiolid_brep_boolean::BooleanReport;
53use axiolid_construct::extrude::extrude_profile_exact;
54use axiolid_construct::revolve_exact::revolve_profile_exact;
55use axiolid_construct::swept_disk_exact::{
56 swept_disk_along_arc_exact, swept_disk_along_line_exact,
57};
58use axiolid_contracts::{
59 Backend, BackendDescriptor, BackendId, ExecutionOptions, ExecutionTarget, GeomError,
60 GeomResult, Operation,
61};
62use axiolid_core::Scalar;
63use axiolid_exact_compile_contract::ExactCompiler;
64use axiolid_model::{GeometryGraph, GeometryNode, NodeId, SolidOperation};
65
66mod boolean;
67mod clip;
68pub(crate) mod polyhedral;
69
70#[derive(Debug, Clone, Copy, Default)]
72pub struct ReferenceExactCompiler;
73
74impl ReferenceExactCompiler {
75 pub const ID: BackendId = BackendId::new("scalar-exact-compile");
77
78 pub const fn new() -> Self {
80 Self
81 }
82
83 pub fn compile_exact_with_report(
106 &self,
107 graph: &GeometryGraph,
108 root: NodeId,
109 options: &ExecutionOptions,
110 ) -> GeomResult<(ExactBRep, BooleanReport)> {
111 let mut compilation = ExactCompilation::new(graph, options);
112 let body = compilation.compile(root)?;
113 Ok((body, compilation.report_of(root)))
114 }
115
116 pub(crate) fn compile_flattened_with_report(
128 &self,
129 graph: &GeometryGraph,
130 root: NodeId,
131 options: &ExecutionOptions,
132 ) -> GeomResult<(Option<ExactBRep>, BooleanReport, Scalar)> {
133 let mut compilation = ExactCompilation::new(graph, options);
134 compilation.flatten = true;
135 let body = match compilation.body(root)? {
136 Body::Solid(solid) => Some(*solid),
137 Body::Empty(_) => None,
138 };
139 Ok((
140 body,
141 compilation.report_of(root),
142 compilation.flattened_of(root),
143 ))
144 }
145
146 pub fn compile_exact_batch_with_reports(
153 &self,
154 graph: &GeometryGraph,
155 roots: &[NodeId],
156 options: &ExecutionOptions,
157 ) -> GeomResult<Vec<(ExactBRep, BooleanReport)>> {
158 let mut compilation = ExactCompilation::new(graph, options);
159 let mut out = Vec::with_capacity(roots.len());
160 for &root in roots {
161 let body = compilation.compile(root)?;
162 out.push((body, compilation.report_of(root)));
163 }
164 Ok(out)
165 }
166}
167
168impl Backend for ReferenceExactCompiler {
169 fn descriptor(&self) -> BackendDescriptor {
170 BackendDescriptor::new(Self::ID, ExecutionTarget::PortableCpu)
171 }
172}
173
174impl ExactCompiler for ReferenceExactCompiler {
175 fn compile_exact(
176 &self,
177 graph: &GeometryGraph,
178 root: NodeId,
179 options: &ExecutionOptions,
180 ) -> GeomResult<ExactBRep> {
181 ExactCompilation::new(graph, options).compile(root)
182 }
183
184 fn compile_exact_batch_into(
185 &self,
186 graph: &GeometryGraph,
187 roots: &[NodeId],
188 options: &ExecutionOptions,
189 destination: &mut Vec<ExactBRep>,
190 ) -> GeomResult<()> {
191 destination.reserve(roots.len());
192 let mut compilation = ExactCompilation::new(graph, options);
193 for &root in roots {
194 destination.push(compilation.compile(root)?);
195 }
196 Ok(())
197 }
198}
199
200#[derive(Debug, Clone)]
204enum Body {
205 Solid(Box<ExactBRep>),
206 Empty(&'static str),
208}
209
210impl Body {
211 fn new(solid: ExactBRep) -> Self {
212 Self::Solid(Box::new(solid))
213 }
214
215 fn solid(self) -> GeomResult<ExactBRep> {
217 match self {
218 Self::Solid(solid) => Ok(*solid),
219 Self::Empty(why) => Err(GeomError::Degenerate(why.to_owned())),
220 }
221 }
222
223 fn is_empty(&self) -> bool {
224 matches!(self, Self::Empty(_))
225 }
226}
227
228struct ExactCompilation<'a> {
229 graph: &'a GeometryGraph,
230 options: &'a ExecutionOptions,
231 cache: HashMap<NodeId, Body>,
232 active: HashSet<NodeId>,
233 cache_hits: usize,
234 evaluated_nodes: usize,
235 reports: HashMap<NodeId, BooleanReport>,
239 boolean_report: Option<BooleanReport>,
241 flatten: bool,
245 flattened: HashMap<NodeId, Scalar>,
248 regions: crate::regions::RegionMemo,
250}
251
252impl<'a> ExactCompilation<'a> {
253 fn new(graph: &'a GeometryGraph, options: &'a ExecutionOptions) -> Self {
254 Self {
255 graph,
256 options,
257 cache: HashMap::new(),
258 active: HashSet::new(),
259 cache_hits: 0,
260 evaluated_nodes: 0,
261 reports: HashMap::new(),
262 boolean_report: None,
263 flatten: false,
264 flattened: HashMap::new(),
265 regions: crate::regions::RegionMemo::default(),
266 }
267 }
268
269 fn flattened_of(&self, node: NodeId) -> Scalar {
271 self.flattened.get(&node).copied().unwrap_or(0.0)
272 }
273
274 fn set_flattened(&mut self, node: NodeId, width: Scalar) {
276 if width > 0.0 {
277 self.flattened.insert(node, width);
278 }
279 }
280
281 fn flattened_mesh(&self, node: &GeometryNode) -> GeomResult<(ExactBRep, Scalar)> {
284 match node {
285 GeometryNode::PolygonMesh(mesh) => {
286 let (triangles, warp, steiner_offset) =
290 crate::compiler::compile_authored_polygons(mesh, self.options)?;
291 let width = warp.unwrap_or(0.0).max(steiner_offset.unwrap_or(0.0));
292 Ok((polyhedral::from_triangles(&triangles)?, width))
293 }
294 GeometryNode::TriMesh(mesh) => Ok((polyhedral::from_triangles(mesh)?, 0.0)),
295 GeometryNode::BRep(brep) => {
296 let (triangles, closure, deviation) = crate::brep::tessellate(
297 brep,
298 self.graph,
299 self.options.tolerance(),
300 crate::compiler::chord_error(self.options),
301 )?;
302 if closure != axiolid_mesh_compile_contract::MeshClosure::Solid {
303 return Err(unsupported(
304 "exact boolean over a B-rep operand that bounds no solid",
305 ));
306 }
307 let warp = deviation.polyhedral_warp().ok_or_else(|| {
308 unsupported(
309 "exact boolean over a B-rep operand with a face that is neither a plane \
310 with straight edges nor a warped face without a surface",
311 )
312 })?;
313 Ok((polyhedral::from_triangles(&triangles)?, warp))
314 }
315 other => Err(unsupported(exact_input_family(other))),
316 }
317 }
318
319 fn report_of(&self, node: NodeId) -> BooleanReport {
321 self.reports.get(&node).cloned().unwrap_or_default()
322 }
323
324 fn set_report(&mut self, node: NodeId, report: BooleanReport) {
327 if !report.is_exact() || report.rounding_floor() > 0.0 {
328 self.reports.insert(node, report);
329 }
330 }
331
332 fn compile(&mut self, root: NodeId) -> GeomResult<ExactBRep> {
333 self.body(root)?.solid()
334 }
335
336 fn body(&mut self, root: NodeId) -> GeomResult<Body> {
337 if let Some(cached) = self.cache.get(&root) {
338 self.cache_hits += 1;
339 return Ok(cached.clone());
340 }
341 if !self.active.insert(root) {
342 return Err(GeomError::InvalidInput(format!(
343 "exact compilation cycle reached at node {root:?}"
344 )));
345 }
346
347 self.evaluated_nodes += 1;
348 let result = self.body_uncached(root);
349 self.active.remove(&root);
350 let exact = result?;
351 self.cache.insert(root, exact.clone());
352 Ok(exact)
353 }
354
355 fn body_uncached(&mut self, root: NodeId) -> GeomResult<Body> {
356 let node = self.graph.get(root).ok_or_else(|| {
357 GeomError::InvalidInput(format!("node {root:?} does not belong to this graph"))
358 })?;
359
360 match node {
361 GeometryNode::SolidOperation(SolidOperation::Extrusion {
362 profile,
363 direction,
364 depth,
365 }) => {
366 let profile_node = self.graph.get(*profile).ok_or_else(|| {
367 GeomError::InvalidInput(format!(
368 "extrusion profile {profile:?} does not belong to this graph"
369 ))
370 })?;
371 let GeometryNode::Profile(profile) = profile_node else {
372 return Err(unsupported("extrusion profile reference"));
373 };
374 extrude_profile_exact(profile, *direction, *depth, self.options.tolerance())
375 .map(Body::new)
376 .map_err(remap_construction_error)
377 }
378 GeometryNode::SolidOperation(SolidOperation::Revolution {
379 profile,
380 axis_origin,
381 axis_direction,
382 angle,
383 }) => {
384 let profile_node = self.graph.get(*profile).ok_or_else(|| {
385 GeomError::InvalidInput(format!(
386 "revolution profile {profile:?} does not belong to this graph"
387 ))
388 })?;
389 let GeometryNode::Profile(profile) = profile_node else {
390 return Err(unsupported("revolution profile reference"));
391 };
392 revolve_profile_exact(
393 profile,
394 *axis_origin,
395 *axis_direction,
396 *angle,
397 self.options.tolerance(),
398 )
399 .map(Body::new)
400 .map_err(remap_construction_error)
401 }
402 GeometryNode::Instance(instance) => {
403 let instance = *instance;
404 let source = self.body(instance.source)?;
405 self.set_report(root, self.report_of(instance.source));
406 self.set_flattened(root, self.flattened_of(instance.source));
407 let Body::Solid(source) = source else {
409 return Ok(source);
410 };
411 source
412 .transformed(&instance.transform)
413 .map(Body::new)
414 .map_err(|error| match error {
415 TransformError::NotRigid => {
416 unsupported("exact instance under a scaled or sheared transform")
417 }
418 TransformError::Unsupported(what) => unsupported(what),
419 other => GeomError::InvalidInput(format!("instance transform: {other}")),
420 })
421 }
422 GeometryNode::InstanceAtStation(placed) => {
426 let source_id = placed.source;
427 let placement = crate::station::placement(self.graph, root)?;
428 if !placement.exact {
429 return Err(unsupported(
430 "exact instance at a station whose frame is not exact: the basis is \
431 not a line, so its distance is read by an estimate",
432 ));
433 }
434 let source = self.body(source_id)?;
435 self.set_report(root, self.report_of(source_id));
436 self.set_flattened(root, self.flattened_of(source_id));
437 let Body::Solid(source) = source else {
438 return Ok(source);
439 };
440 source
441 .transformed(&placement.transform)
442 .map(Body::new)
443 .map_err(|error| match error {
444 TransformError::Unsupported(what) => unsupported(what),
445 other => GeomError::InvalidInput(format!("instance at a station: {other}")),
446 })
447 }
448 GeometryNode::SolidOperation(SolidOperation::SweptDisk {
451 directrix,
452 radius,
453 inner_radius,
454 parameter_range,
455 fillet_radius: _,
456 }) => {
457 let tolerance = self.options.tolerance();
458 match crate::directrix::exact(
459 self.graph,
460 *directrix,
461 *parameter_range,
462 self.options,
463 unsupported,
464 )? {
465 crate::directrix::ExactDirectrix::Segment(start, end) => {
466 swept_disk_along_line_exact(start, end, *radius, *inner_radius, tolerance)
467 }
468 crate::directrix::ExactDirectrix::Arc(circle, span) => {
469 swept_disk_along_arc_exact(&circle, span, *radius, *inner_radius, tolerance)
470 }
471 }
472 .map(Body::new)
473 .map_err(remap_construction_error)
474 }
475 GeometryNode::SolidOperation(SolidOperation::Boolean {
476 left,
477 right,
478 operator,
479 }) => {
480 let (left, right, operator) = (*left, *right, *operator);
481 let body = self.compile_boolean(left, right, operator)?;
482 let own = self.boolean_report.take().unwrap_or_default();
483 let report = self
484 .report_of(left)
485 .merged(&self.report_of(right))
486 .merged(&own);
487 self.set_report(root, report);
488 let width = self.flattened_of(left).max(self.flattened_of(right));
489 self.set_flattened(root, width);
490 Ok(body)
491 }
492 GeometryNode::PolygonMesh(_) | GeometryNode::TriMesh(_) | GeometryNode::BRep(_)
495 if self.flatten =>
496 {
497 let (body, width) = self.flattened_mesh(node)?;
498 self.set_flattened(root, width);
499 Ok(Body::new(body))
500 }
501 GeometryNode::SolidOperation(SolidOperation::StationedSpine {
504 directrix,
505 sections,
506 frame,
507 }) => {
508 let sections: Vec<_> = sections.iter().copied().map(Into::into).collect();
509 crate::station::stationed_spine_exact(
510 self.graph,
511 *directrix,
512 §ions,
513 *frame,
514 self.options,
515 unsupported,
516 )
517 .map(Body::new)
518 }
519 GeometryNode::SolidOperation(SolidOperation::SectionsAtStations {
520 directrix,
521 sections,
522 frame,
523 }) => crate::station::stationed_spine_exact(
524 self.graph,
525 *directrix,
526 sections,
527 *frame,
528 self.options,
529 unsupported,
530 )
531 .map(Body::new),
532 _ => Err(unsupported(exact_input_family(node))),
533 }
534 }
535}
536
537fn remap_construction_error(error: GeomError) -> GeomError {
538 match error {
539 GeomError::UnsupportedInput { input, .. } => unsupported(input),
540 error => error,
541 }
542}
543
544pub fn exact_directrix(
557 graph: &GeometryGraph,
558 directrix: NodeId,
559 parameter_range: Option<(Scalar, Scalar)>,
560 options: &ExecutionOptions,
561) -> GeomResult<crate::directrix::ExactDirectrix> {
562 crate::directrix::exact(graph, directrix, parameter_range, options, unsupported)
563}
564
565fn unsupported(input: &'static str) -> GeomError {
566 GeomError::UnsupportedInput {
567 backend: ReferenceExactCompiler::ID,
568 operation: Operation::GraphCompilation,
569 input,
570 }
571}
572
573fn exact_input_family(node: &GeometryNode) -> &'static str {
574 match node {
575 GeometryNode::Point2(_) => "2D point",
576 GeometryNode::Point3(_) => "3D point",
577 GeometryNode::Vector2(_) => "2D vector",
578 GeometryNode::Vector3(_) => "3D vector",
579 GeometryNode::Frame2(_) => "2D frame",
580 GeometryNode::Frame3(_) => "3D frame",
581 GeometryNode::Transform(_) => "transform",
582 GeometryNode::PointList2(_) => "2D point list",
583 GeometryNode::PointList3(_) => "3D point list",
584 GeometryNode::Primitive(_) => "primitive",
585 GeometryNode::Curve2(_) => "2D curve",
586 GeometryNode::Curve3(_) => "3D curve",
587 GeometryNode::CurveRelation(_) => "curve relation",
588 GeometryNode::PointOnCurve(_) => "point on curve",
589 GeometryNode::Surface(_) => "surface",
590 GeometryNode::SurfaceRelation(_) => "surface relation",
591 GeometryNode::PointOnSurface(_) => "point on surface",
592 GeometryNode::Profile(_) => "profile",
593 GeometryNode::OpenProfile(_) => "open profile",
594 GeometryNode::HalfSpace(_) => "half-space",
595 GeometryNode::BRep(_) => "B-rep",
596 GeometryNode::PolygonMesh(_) => "polygon mesh",
597 GeometryNode::TriMesh(_) => "triangle mesh",
598 GeometryNode::BoundingBox(_) => "bounding box",
599 GeometryNode::SolidOperation(operation) => solid_operation_family(operation),
600 GeometryNode::Instance(_) => "instance",
601 GeometryNode::Collection(_) => "collection",
602 GeometryNode::CurveStation(_) => "curve station",
603 GeometryNode::OrientedCurveStation(_) => "oriented curve station",
604 GeometryNode::InstanceAtStation(_) => "instance at a station",
605 _ => "unknown geometry node",
606 }
607}
608
609pub(crate) fn solid_operation_family(operation: &SolidOperation) -> &'static str {
610 match operation {
611 SolidOperation::Extrusion { .. } => "extrusion",
612 SolidOperation::TaperedExtrusion { .. } => "tapered extrusion",
613 SolidOperation::Revolution { .. } => "revolution",
614 SolidOperation::TaperedRevolution { .. } => "tapered revolution",
615 SolidOperation::SweptDisk { .. } => "swept disk",
616 SolidOperation::FixedReferenceSweep { .. } => "fixed-reference sweep",
617 SolidOperation::SurfaceCurveSweep { .. } => "surface-curve sweep",
618 SolidOperation::SectionedSpine { .. } => "sectioned spine",
619 SolidOperation::StationedSpine { .. } | SolidOperation::SectionsAtStations { .. } => {
620 "station-placed spine"
621 }
622 SolidOperation::Boolean { .. } => "boolean",
623 SolidOperation::BoundedHalfSpace { .. } => "bounded half-space",
624 SolidOperation::BoundedHalfSpaceAlongZ { .. } => "bounded half-space along z",
625 _ => "unknown solid operation",
626 }
627}
628
629#[cfg(test)]
630mod family_name_tests {
631 use super::{solid_operation_family, SOLID_FAMILY_NAMES};
632 use axiolid_core::Vec3;
633 use axiolid_model::{GeometryGraphBuilder, GeometryNode, SolidOperation};
634 use axiolid_profile::{Profile, RectangleProfile};
635
636 #[test]
643 fn the_exported_table_matches_the_naming_function() {
644 assert_eq!(
645 SOLID_FAMILY_NAMES.len(),
646 12,
647 "the declared family count changed; update the table"
648 );
649 for name in SOLID_FAMILY_NAMES {
650 assert!(!name.is_empty());
651 assert_ne!(*name, "unknown solid operation");
652 }
653
654 let mut builder = GeometryGraphBuilder::new();
656 let profile = builder
657 .push(GeometryNode::Profile(Profile::Rectangle(
658 RectangleProfile {
659 x: 1.0,
660 y: 1.0,
661 thickness: None,
662 outer_radius: None,
663 inner_radius: None,
664 },
665 )))
666 .expect("a rectangle profile is a valid node");
667
668 let extrusion = SolidOperation::Extrusion {
669 profile,
670 direction: Vec3::Z,
671 depth: 1.0,
672 };
673 assert!(
674 SOLID_FAMILY_NAMES.contains(&solid_operation_family(&extrusion)),
675 "the function returned a name the table does not carry"
676 );
677 }
678}
679
680#[cfg(test)]
681mod tests {
682 use axiolid_core::{Tolerance, Vec3};
683 use axiolid_model::GeometryGraphBuilder;
684 use axiolid_profile::{Profile, RectangleProfile};
685
686 use super::*;
687
688 #[test]
689 fn duplicate_roots_hit_the_exact_result_cache() {
690 let mut builder = GeometryGraphBuilder::new();
691 let profile = builder
692 .push(GeometryNode::Profile(Profile::Rectangle(
693 RectangleProfile {
694 x: 2.0,
695 y: 1.0,
696 thickness: None,
697 outer_radius: None,
698 inner_radius: None,
699 },
700 )))
701 .unwrap();
702 let root = builder
703 .push(GeometryNode::SolidOperation(SolidOperation::Extrusion {
704 profile,
705 direction: Vec3::Z,
706 depth: 1.0,
707 }))
708 .unwrap();
709 let graph = builder.finish(vec![root]).unwrap();
710 let options = ExecutionOptions::new(Tolerance::METRE);
711 let mut compilation = ExactCompilation::new(&graph, &options);
712
713 let first = compilation.compile(root).unwrap();
714 let second = compilation.compile(root).unwrap();
715
716 assert_eq!(first, second);
717 assert_eq!(compilation.evaluated_nodes, 1);
718 assert_eq!(compilation.cache_hits, 1);
719 assert_eq!(compilation.cache.len(), 1);
720 }
721}
722
723pub const SOLID_FAMILY_NAMES: &[&str] = &[
729 "extrusion",
730 "tapered extrusion",
731 "revolution",
732 "tapered revolution",
733 "swept disk",
734 "fixed-reference sweep",
735 "surface-curve sweep",
736 "sectioned spine",
737 "station-placed spine",
738 "boolean",
739 "bounded half-space",
740 "bounded half-space along z",
741];