1use axiolid_contracts::{
8 Backend, BackendDescriptor, BackendId, ExecutionOptions, ExecutionTarget, GeomError,
9 GeomResult, Operation, ScratchRequirement,
10};
11use axiolid_core::{PlaneFrame, Point3, Scalar, Tolerance, Transform3};
12use axiolid_mesh::TriMesh;
13use axiolid_mesh_boolean_contract::MeshBoolean;
14use axiolid_mesh_compile_contract::{CompileOutcome, MeshCompiler};
15use axiolid_model::{GeometryGraph, GeometryNode, NodeId, SolidOperation};
16
17use axiolid_construct::extrude::extrude_profile;
18use axiolid_construct::profile::profile_rings;
19
20use crate::channels::{self, Built};
21
22#[derive(Debug, Clone)]
28pub struct ReferenceMeshCompiler<B> {
29 boolean: B,
30}
31
32impl<B> ReferenceMeshCompiler<B> {
33 pub const fn new(boolean: B) -> Self {
35 Self { boolean }
36 }
37
38 pub const fn boolean(&self) -> &B {
40 &self.boolean
41 }
42}
43
44impl<B: MeshBoolean> Backend for ReferenceMeshCompiler<B> {
45 fn descriptor(&self) -> BackendDescriptor {
46 BackendDescriptor::new(
47 BackendId::new("scalar-compile"),
48 ExecutionTarget::PortableCpu,
49 )
50 }
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
58struct EvalKey {
59 id: NodeId,
60 linear_bits: u64,
61 angular_bits: u64,
62 chord_bits: u64,
66}
67
68#[derive(Debug, Clone, Copy, PartialEq)]
72struct Budget {
73 tolerance: Tolerance,
74 chord: Scalar,
75}
76
77impl Budget {
78 fn of(options: &ExecutionOptions) -> Self {
79 let tolerance = options.tolerance();
80 Self {
81 tolerance,
82 chord: options.chord_error().unwrap_or(tolerance.linear()),
83 }
84 }
85}
86
87impl EvalKey {
88 fn new(id: NodeId, budget: Budget) -> Self {
89 let bits = |value: Scalar| if value == 0.0 { 0 } else { value.to_bits() };
90 Self {
91 id,
92 linear_bits: bits(budget.tolerance.linear()),
93 angular_bits: bits(budget.tolerance.angular()),
94 chord_bits: bits(budget.chord),
95 }
96 }
97
98 fn budget(self) -> Budget {
99 Budget {
100 tolerance: self.tolerance(),
101 chord: Scalar::from_bits(self.chord_bits),
102 }
103 }
104
105 fn tolerance(self) -> Tolerance {
106 Tolerance::new(
107 Scalar::from_bits(self.linear_bits),
108 Scalar::from_bits(self.angular_bits),
109 )
110 .expect("evaluation keys only contain validated tolerances")
111 }
112}
113
114#[derive(Debug, Clone, Copy)]
115enum Step {
116 Enter(EvalKey),
117 Exit(EvalKey),
118}
119
120type Cache = std::collections::HashMap<EvalKey, Built>;
125
126impl<B: MeshBoolean> ReferenceMeshCompiler<B> {
127 fn boundary_rings(
134 &self,
135 graph: &GeometryGraph,
136 id: NodeId,
137 ) -> GeomResult<axiolid_construct::profile::Rings> {
138 let node = self.node(graph, id)?;
139 let GeometryNode::Curve2(curve) = node else {
140 return Err(GeomError::InvalidInput(format!(
141 "half-space boundary {id:?} is not a Curve2 node"
142 )));
143 };
144 match curve {
145 axiolid_curve::Curve2::Polyline(p) => {
146 let mut pts = p.points.clone();
147 if pts.len() >= 2 && pts[0] == pts[pts.len() - 1] {
150 pts.pop();
151 }
152 if pts.len() < 3 {
153 return Err(GeomError::InvalidInput(
154 "half-space boundary needs at least 3 distinct points".to_owned(),
155 ));
156 }
157 Ok(axiolid_construct::profile::Rings {
158 outer: pts,
159 holes: Vec::new(),
160 })
161 }
162 _ => Err(GeomError::Unsupported {
163 backend: self.descriptor().id,
164 operation: Operation::CurveEvaluation,
165 }),
166 }
167 }
168
169 fn surface_of(
175 graph: &GeometryGraph,
176 id: axiolid_model::NodeId,
177 ) -> GeomResult<&axiolid_surface::Surface> {
178 match graph.get(id) {
179 Some(GeometryNode::Surface(surface)) => Ok(surface),
180 Some(_) => Err(GeomError::InvalidInput(format!(
181 "reference surface {id:?} is not a Surface node"
182 ))),
183 None => Err(GeomError::InvalidInput(format!(
184 "reference surface {id:?} does not belong to this graph"
185 ))),
186 }
187 }
188
189 fn surface_normals(
190 &self,
191 graph: &GeometryGraph,
192 id: NodeId,
193 path: &[Point3],
194 options: &ExecutionOptions,
195 ) -> GeomResult<Vec<axiolid_core::Vec3>> {
196 if let Some(GeometryNode::SurfaceRelation(
197 axiolid_model::SurfaceRelation::LinearExtrusion { direction, .. },
198 )) = graph.get(id)
199 {
200 return axiolid_construct::sweep::linear_extrusion_normals(path, *direction);
201 }
202 let surface = Self::surface_of(graph, id)?;
203 path.iter()
204 .map(|point| {
205 let (u, v) =
206 axiolid_reference::surface::invert(surface, *point, options.tolerance())?;
207 axiolid_reference::surface::normal(surface, u, v)
208 })
209 .collect()
210 }
211
212 fn rings_of(
213 &self,
214 graph: &GeometryGraph,
215 id: NodeId,
216 options: &ExecutionOptions,
217 what: &str,
218 ) -> GeomResult<axiolid_construct::profile::Rings> {
219 let node = self.node(graph, id)?;
220 let GeometryNode::Profile(shape) = node else {
221 return Err(GeomError::InvalidInput(format!(
222 "{what} {id:?} is not a Profile node"
223 )));
224 };
225 profile_rings(shape, chord_error(options), options.tolerance())
226 }
227
228 fn directrix_points(
236 &self,
237 graph: &GeometryGraph,
238 id: NodeId,
239 range: Option<(Scalar, Scalar)>,
240 options: &ExecutionOptions,
241 ) -> GeomResult<Vec<Point3>> {
242 crate::directrix::points(graph, id, range, options)
243 }
244
245 fn node<'g>(&self, graph: &'g GeometryGraph, id: NodeId) -> GeomResult<&'g GeometryNode> {
246 graph.get(id).ok_or_else(|| {
247 GeomError::InvalidInput(format!("node {id:?} does not belong to this graph"))
248 })
249 }
250
251 fn mesh_dependencies(
257 &self,
258 graph: &GeometryGraph,
259 node: &GeometryNode,
260 key: EvalKey,
261 ) -> GeomResult<Vec<EvalKey>> {
262 let budget = key.budget();
263 let same = |id| EvalKey::new(id, budget);
264 Ok(match node {
265 GeometryNode::Instance(instance) => vec![EvalKey::new(
266 instance.source,
267 instance_local_budget(instance.transform, budget)?,
268 )],
269 GeometryNode::Collection(members) => members.iter().copied().map(same).collect(),
270 GeometryNode::SolidOperation(
271 operation @ SolidOperation::Boolean { left, right, .. },
272 ) => {
273 if is_subject_bounded_half_space_boolean(graph, operation) {
274 vec![same(*left)]
275 } else {
276 vec![same(*left), same(*right)]
277 }
278 }
279 _ => Vec::new(),
280 })
281 }
282
283 fn evaluate(
285 &self,
286 graph: &GeometryGraph,
287 root: NodeId,
288 options: &ExecutionOptions,
289 cache: &mut Cache,
290 ) -> GeomResult<Built> {
291 let root_key = EvalKey::new(root, Budget::of(options));
292 let mut stack = vec![Step::Enter(root_key)];
293 while let Some(step) = stack.pop() {
294 match step {
295 Step::Enter(key) => {
296 if cache.contains_key(&key) {
297 continue;
298 }
299 let node = self.node(graph, key.id)?;
300 let deps = self.mesh_dependencies(graph, node, key)?;
301 stack.push(Step::Exit(key));
303 for dep in deps {
304 if !cache.contains_key(&dep) {
305 stack.push(Step::Enter(dep));
306 }
307 }
308 }
309 Step::Exit(key) => {
310 if cache.contains_key(&key) {
311 continue;
312 }
313 let budget = key.budget();
314 let local_options = options
315 .clone()
316 .with_tolerance(budget.tolerance)
317 .with_chord_error(budget.chord)
318 .ok_or_else(|| {
319 GeomError::InvalidInput(format!(
320 "chord budget {} is not positive and finite",
321 budget.chord
322 ))
323 })?;
324 let built = self.build(graph, key, &local_options, cache)?;
325 cache.insert(key, built);
326 }
327 }
328 }
329 cache
330 .get(&root_key)
331 .cloned()
332 .ok_or_else(|| GeomError::InvalidInput(format!("root {root:?} produced no mesh")))
333 }
334
335 fn compile_authored_polygons(
349 &self,
350 mesh: &axiolid_mesh::PolygonMesh,
351 options: &ExecutionOptions,
352 ) -> GeomResult<TriMesh> {
353 let position_count = mesh.positions.len();
354 if let Some(index) = mesh
355 .faces
356 .iter()
357 .flat_map(|face| face_rings(face).flatten().copied())
358 .find(|&index| index as usize >= position_count)
359 {
360 return Err(GeomError::InvalidInput(format!(
361 "authored polygon index {index} exceeds position count {position_count}"
362 )));
363 }
364
365 let mut positions = Vec::new();
366 positions
367 .try_reserve_exact(position_count)
368 .map_err(|_| GeomError::BudgetExceeded {
369 resource: "authored polygon positions",
370 })?;
371 positions.extend_from_slice(&mesh.positions);
372
373 let index_bound = mesh
376 .faces
377 .iter()
378 .try_fold(0_usize, |total, face| {
379 let corners = face_rings(face).map(Vec::len).sum::<usize>();
380 corners
381 .checked_add(2 * face.holes.len())
382 .and_then(|triangles| triangles.checked_mul(3))
383 .and_then(|indices| total.checked_add(indices))
384 })
385 .ok_or(GeomError::BudgetExceeded {
386 resource: "authored polygon indices",
387 })?;
388 let mut indices = Vec::new();
389 indices
390 .try_reserve_exact(index_bound)
391 .map_err(|_| GeomError::BudgetExceeded {
392 resource: "authored polygon indices",
393 })?;
394
395 let linear = options.tolerance().linear();
396 for (face_index, face) in mesh.faces.iter().enumerate() {
397 if face.outer.len() == 3 && face.holes.is_empty() {
398 indices.extend_from_slice(&face.outer);
399 continue;
400 }
401 let rings: Vec<&Vec<u32>> = face_rings(face).collect();
405 let points: Vec<Vec<axiolid_core::Point3>> = rings
406 .iter()
407 .map(|ring| ring.iter().map(|&i| mesh.positions[i as usize]).collect())
408 .collect();
409 let views: Vec<&[axiolid_core::Point3]> = points.iter().map(Vec::as_slice).collect();
410 let local = crate::planar::triangulate_polygon(&views, linear)
411 .map_err(|refusal| crate::planar::face_error(face_index, refusal))?;
412 let corners: Vec<u32> = rings.into_iter().flatten().copied().collect();
413 indices.extend(local.into_iter().map(|corner| corners[corner]));
414 }
415
416 let triangles = TriMesh::new(positions, indices);
417 triangles.validate_structure().map_err(|error| {
418 GeomError::InvalidInput(format!("invalid authored polygon mesh: {error}"))
419 })?;
420 Ok(triangles)
421 }
422
423 fn build(
425 &self,
426 graph: &GeometryGraph,
427 key: EvalKey,
428 options: &ExecutionOptions,
429 cache: &Cache,
430 ) -> GeomResult<Built> {
431 let id = key.id;
432 let node = self.node(graph, id)?;
433 match node {
434 GeometryNode::TriMesh(mesh) => Ok(authored_mesh(mesh.clone(), options)),
435 GeometryNode::PolygonMesh(mesh) => self
436 .compile_authored_polygons(mesh, options)
437 .map(|mesh| authored_mesh(mesh, options)),
438 GeometryNode::Instance(instance) => {
439 let source_budget = instance_local_budget(instance.transform, Budget::of(options))?;
440 let source = self.cached(cache, instance.source, source_budget)?;
441 Ok(channels::transform(source, instance.transform))
442 }
443 GeometryNode::Collection(members) => {
444 let members = members
445 .iter()
446 .map(|&member| self.cached(cache, member, Budget::of(options)))
447 .collect::<GeomResult<Vec<_>>>()?;
448 Ok(channels::merge(&members))
449 }
450 GeometryNode::SolidOperation(SolidOperation::Boolean {
451 left,
452 right,
453 operator,
454 }) => self.build_boolean(graph, *left, *right, *operator, options, cache),
455 GeometryNode::SolidOperation(operation) => {
456 self.build_solid(graph, operation, options).map(Built::leaf)
457 }
458 GeometryNode::BRep(brep) => {
459 crate::brep::tessellate(brep, graph, options.tolerance(), chord_error(options))
460 .map(|(mesh, closure)| Built::with_closure(mesh, closure))
461 }
462 GeometryNode::SurfaceRelation(axiolid_model::SurfaceRelation::CurveBounded {
464 basis,
465 boundaries,
466 implicit_outer,
467 }) => crate::bounded::curve_bounded(
468 self.descriptor().id,
469 graph,
470 *basis,
471 boundaries,
472 *implicit_outer,
473 chord_error(options),
474 options.tolerance(),
475 )
476 .map(|mesh| {
477 Built::with_closure(mesh, axiolid_mesh_compile_contract::MeshClosure::Surface)
478 }),
479 GeometryNode::Primitive(primitive) => {
482 axiolid_reference::primitive::tessellate_primitive(
483 primitive,
484 chord_tolerance(options)?,
485 )
486 .map(Built::leaf)
487 }
488 other => Err(GeomError::Unsupported {
489 backend: self.descriptor().id,
490 operation: unsupported_operation(other),
491 }),
492 }
493 }
494
495 fn cached<'c>(&self, cache: &'c Cache, id: NodeId, budget: Budget) -> GeomResult<&'c Built> {
497 cache.get(&EvalKey::new(id, budget)).ok_or_else(|| {
498 GeomError::InvalidInput(format!(
499 "dependency {id:?} was not evaluated first at {budget:?}"
500 ))
501 })
502 }
503
504 fn build_boolean(
507 &self,
508 graph: &GeometryGraph,
509 left: NodeId,
510 right: NodeId,
511 operator: axiolid_core::BooleanOperator,
512 options: &ExecutionOptions,
513 cache: &Cache,
514 ) -> GeomResult<Built> {
515 let subject = self.cached(cache, left, Budget::of(options))?;
516 refuse_surface_operand(subject, "subject")?;
517 let bounded_tool = match self.node(graph, right)? {
518 GeometryNode::HalfSpace(hs) => Some(axiolid_construct::half_space::for_subject(
519 &subject.mesh,
520 *hs,
521 options.tolerance(),
522 )?),
523 _ => None,
524 };
525 let (tool, tool_fates) = match bounded_tool.as_ref() {
528 Some(tool) => (tool, None),
529 None => {
530 let built = self.cached(cache, right, Budget::of(options))?;
531 refuse_surface_operand(built, "tool")?;
532 (&built.mesh, Some(&built.fates))
533 }
534 };
535 let outcome = self
536 .boolean
537 .boolean(&subject.mesh, tool, operator, options)?;
538 if let Some(pinch) = crate::pinch::find(&outcome.mesh) {
539 return Err(GeomError::Degenerate(match pinch {
540 crate::pinch::Pinch::Edge { from, to } => format!(
541 "{operator:?} result touches itself along the edge {from:?} to {to:?}: \
542 the operands meet tangentially there (a void tangent to its host's face), \
543 leaving no material between two faces"
544 ),
545 crate::pinch::Pinch::Vertex { at } => format!(
546 "{operator:?} result touches itself at the point {at:?}: the operands \
547 meet tangentially there, leaving no material between two faces"
548 ),
549 }));
550 }
551 Ok(channels::after_boolean(
552 outcome.mesh,
553 &subject.fates,
554 tool_fates,
555 outcome.evidence.attribute_fates,
556 ))
557 }
558
559 fn build_solid(
563 &self,
564 graph: &GeometryGraph,
565 operation: &SolidOperation,
566 options: &ExecutionOptions,
567 ) -> GeomResult<TriMesh> {
568 match operation {
569 SolidOperation::Extrusion {
570 profile,
571 direction,
572 depth,
573 } => {
574 let node = self.node(graph, *profile)?;
575 let GeometryNode::Profile(shape) = node else {
576 return Err(GeomError::InvalidInput(format!(
577 "extrusion profile {profile:?} is not a Profile node"
578 )));
579 };
580 let rings = profile_rings(shape, chord_error(options), options.tolerance())?;
581 extrude_profile(&rings, *direction, *depth, options.tolerance())
582 }
583 SolidOperation::Revolution {
584 profile,
585 axis_origin,
586 axis_direction,
587 angle,
588 } => {
589 let node = self.node(graph, *profile)?;
590 let GeometryNode::Profile(shape) = node else {
591 return Err(GeomError::InvalidInput(format!(
592 "revolution profile {profile:?} is not a Profile node"
593 )));
594 };
595 let rings = profile_rings(shape, chord_error(options), options.tolerance())?;
596 axiolid_construct::revolve::revolve(
597 &rings,
598 *axis_origin,
599 *axis_direction,
600 *angle,
601 options.tolerance(),
602 )
603 }
604 SolidOperation::TaperedExtrusion {
605 start_profile,
606 end_profile,
607 direction,
608 depth,
609 } => {
610 let a = self.rings_of(graph, *start_profile, options, "taper start profile")?;
611 let b = self.rings_of(graph, *end_profile, options, "taper end profile")?;
612 axiolid_construct::sweep::tapered_extrude(&a, &b, *direction, *depth)
613 }
614 SolidOperation::TaperedRevolution {
615 start_profile,
616 end_profile,
617 axis_origin,
618 axis_direction,
619 angle,
620 } => {
621 let a = self.rings_of(graph, *start_profile, options, "taper start profile")?;
622 let b = self.rings_of(graph, *end_profile, options, "taper end profile")?;
623 axiolid_construct::sweep::tapered_revolve(
624 &a,
625 &b,
626 *axis_origin,
627 *axis_direction,
628 *angle,
629 options.tolerance(),
630 )
631 }
632 SolidOperation::SweptDisk {
633 directrix,
634 radius,
635 inner_radius,
636 parameter_range,
637 fillet_radius,
638 } => {
639 let path = self.directrix_points(graph, *directrix, *parameter_range, options)?;
640 axiolid_construct::sweep::swept_disk(
641 &path,
642 *radius,
643 *inner_radius,
644 *fillet_radius,
645 options.tolerance(),
646 )
647 }
648 SolidOperation::FixedReferenceSweep {
649 profile,
650 directrix,
651 reference_direction,
652 parameter_range,
653 } => {
654 let rings = self.rings_of(graph, *profile, options, "sweep profile")?;
655 let path = self.directrix_points(graph, *directrix, *parameter_range, options)?;
656 axiolid_construct::sweep::fixed_reference_sweep(&rings, &path, *reference_direction)
657 }
658 SolidOperation::SurfaceCurveSweep {
659 profile,
660 directrix,
661 reference_surface,
662 parameter_range,
663 } => {
664 let rings =
665 self.rings_of(graph, *profile, options, "surface curve sweep profile")?;
666 let path = self.directrix_points(graph, *directrix, *parameter_range, options)?;
667 let normals = self.surface_normals(graph, *reference_surface, &path, options)?;
668 axiolid_construct::sweep::surface_curve_sweep(&rings, &path, &normals)
669 }
670 SolidOperation::SectionedSpine { spine, sections } => {
671 let path = self.directrix_points(graph, *spine, None, options)?;
672 if sections.len() != path.len() {
673 return Err(GeomError::InvalidInput(format!(
674 "a sectioned spine needs one section per spine point: {} sections, {} points",
675 sections.len(),
676 path.len()
677 )));
678 }
679 let mut placed = Vec::with_capacity(sections.len());
680 for (section, origin) in sections.iter().zip(&path) {
681 let rings =
682 self.rings_of(graph, section.profile, options, "spine section profile")?;
683 let pts = rings
686 .outer
687 .iter()
688 .chain(rings.holes.iter().flatten())
689 .map(|p| {
690 section
691 .placement
692 .transform_point3(Point3::new(p.x, p.y, 0.0))
693 + *origin
694 })
695 .collect();
696 placed.push((rings, pts));
697 }
698 axiolid_construct::sweep::sectioned_spine(&placed)
699 }
700 SolidOperation::BoundedHalfSpace {
701 half_space,
702 boundary,
703 placement,
704 } => {
705 let node = self.node(graph, *half_space)?;
706 let GeometryNode::HalfSpace(hs) = node else {
707 return Err(GeomError::InvalidInput(format!(
708 "half-space {half_space:?} is not a HalfSpace node"
709 )));
710 };
711 let rings = self.boundary_rings(graph, *boundary)?;
712 let margin = axiolid_primitive::ClipMargin::new(2.0)
715 .expect("2.0 is a valid positive clip margin");
716 let frame = PlaneFrame::new(
723 placement.translation,
724 placement.matrix3.x_axis.normalize_or_zero(),
725 placement.matrix3.y_axis.normalize_or_zero(),
726 options.tolerance(),
727 )
728 .map_err(|error| {
729 GeomError::InvalidInput(format!(
730 "bounded half-space placement is not a usable boundary frame: {error}"
731 ))
732 })?;
733 let mesh = axiolid_construct::half_space::bounded_half_space_in_frame(
734 &rings,
735 hs.boundary,
736 frame,
737 hs.agreement,
738 margin,
739 options.tolerance(),
740 )?;
741 Ok(mesh)
742 }
743 SolidOperation::Boolean { .. } => Err(GeomError::InvalidInput(
745 "boolean must be built through build_boolean".into(),
746 )),
747 other => Err(GeomError::UnsupportedInput {
755 backend: self.descriptor().id,
756 operation: Operation::Sweep,
757 input: crate::exact::solid_operation_family(other),
758 }),
759 }
760 }
761}
762
763fn instance_local_budget(transform: Transform3, budget: Budget) -> GeomResult<Budget> {
770 let (tolerance, stretch) = instance_local_tolerance(transform, budget.tolerance)?;
771 Ok(Budget {
772 tolerance,
773 chord: budget.chord / stretch,
774 })
775}
776
777fn instance_local_tolerance(
778 transform: Transform3,
779 tolerance: Tolerance,
780) -> GeomResult<(Tolerance, Scalar)> {
781 let m = transform.matrix3;
782 let sx = m.x_axis.length();
783 let sy = m.y_axis.length();
784 let sz = m.z_axis.length();
785 let max_scale = sx.max(sy).max(sz);
786 if !max_scale.is_finite() || max_scale == 0.0 {
787 return Err(GeomError::InvalidInput(
788 "instance transform has no finite scale".into(),
789 ));
790 }
791 let eps = 32.0 * f64::EPSILON;
792 let orthogonal = m.x_axis.dot(m.y_axis).abs() <= eps * sx * sy
793 && m.x_axis.dot(m.z_axis).abs() <= eps * sx * sz
794 && m.y_axis.dot(m.z_axis).abs() <= eps * sy * sz;
795 let stretch = if orthogonal {
796 max_scale * (1.0 + 3.0 * eps)
797 } else {
798 (sx * sx + sy * sy + sz * sz).sqrt()
799 };
800 if !stretch.is_finite() {
801 return Err(GeomError::InvalidInput(
802 "instance transform scale overflow".into(),
803 ));
804 }
805 Tolerance::new(tolerance.linear() / stretch, tolerance.angular())
806 .map(|local| (local, stretch))
807 .map_err(|error| GeomError::InvalidInput(error.to_string()))
808}
809
810pub(crate) fn chord_error(options: &ExecutionOptions) -> Scalar {
817 options
818 .chord_error()
819 .unwrap_or_else(|| options.tolerance().linear())
820}
821
822fn chord_tolerance(options: &ExecutionOptions) -> GeomResult<Tolerance> {
825 Tolerance::new(chord_error(options), options.tolerance().angular())
826 .map_err(|error| GeomError::InvalidInput(error.to_string()))
827}
828
829fn is_subject_bounded_half_space_boolean(
830 graph: &GeometryGraph,
831 operation: &SolidOperation,
832) -> bool {
833 let SolidOperation::Boolean {
834 right, operator, ..
835 } = operation
836 else {
837 return false;
838 };
839 matches!(
843 operator,
844 axiolid_core::BooleanOperator::Difference | axiolid_core::BooleanOperator::Intersection
845 ) && matches!(graph.get(*right), Some(GeometryNode::HalfSpace(_)))
846}
847
848fn unsupported_operation(node: &GeometryNode) -> Operation {
853 match node {
854 GeometryNode::Curve2(_) | GeometryNode::Curve3(_) | GeometryNode::OpenProfile(_) => {
855 Operation::CurveEvaluation
856 }
857 GeometryNode::Surface(_) => Operation::SurfaceEvaluation,
858 GeometryNode::Profile(_) => Operation::ProfileTriangulation,
859 GeometryNode::BRep(_) | GeometryNode::PolygonMesh(_) => Operation::Tessellation,
860 _ => Operation::GraphCompilation,
861 }
862}
863
864impl<B: MeshBoolean> MeshCompiler for ReferenceMeshCompiler<B> {
865 fn scratch_requirement(&self) -> ScratchRequirement {
868 ScratchRequirement::Unbounded
869 }
870
871 fn compile_mesh(
872 &self,
873 graph: &GeometryGraph,
874 root: NodeId,
875 options: &ExecutionOptions,
876 ) -> GeomResult<TriMesh> {
877 self.admit_budget(options)?;
878 let mut cache = Cache::new();
879 self.evaluate(graph, root, options, &mut cache)
880 .map(|built| built.mesh)
881 }
882
883 fn compile_mesh_reported(
886 &self,
887 graph: &GeometryGraph,
888 root: NodeId,
889 options: &ExecutionOptions,
890 ) -> GeomResult<CompileOutcome> {
891 self.admit_budget(options)?;
892 let mut cache = Cache::new();
893 let built = self.evaluate(graph, root, options, &mut cache)?;
894 Ok(CompileOutcome::tracked(built.mesh, built.fates.into_vec()).with_closure(built.closure))
895 }
896
897 fn compile_mesh_batch_into(
900 &self,
901 graph: &GeometryGraph,
902 roots: &[NodeId],
903 options: &ExecutionOptions,
904 destination: &mut Vec<TriMesh>,
905 ) -> GeomResult<()> {
906 self.admit_budget(options)?;
907 destination.reserve(roots.len());
908 let mut cache = Cache::new();
909 for &root in roots {
910 destination.push(self.evaluate(graph, root, options, &mut cache)?.mesh);
911 }
912 Ok(())
913 }
914}
915
916impl<B: MeshBoolean> ReferenceMeshCompiler<B> {
917 fn admit_budget(&self, options: &ExecutionOptions) -> GeomResult<()> {
929 if self.scratch_requirement().fits_budget(options, 0) {
930 return Ok(());
931 }
932 Err(GeomError::BudgetExceeded {
933 resource: "graph compilation intermediate meshes",
934 })
935 }
936
937 pub const fn boolean_provider(&self) -> &B {
943 &self.boolean
944 }
945}
946
947fn face_rings(face: &axiolid_mesh::PolygonFace) -> impl Iterator<Item = &Vec<u32>> {
949 std::iter::once(&face.outer).chain(face.holes.iter())
950}
951
952fn refuse_surface_operand(built: &Built, role: &'static str) -> GeomResult<()> {
958 if built.closure == axiolid_mesh_compile_contract::MeshClosure::Solid {
959 return Ok(());
960 }
961 Err(GeomError::InvalidInput(format!(
962 "boolean {role} is a surface model: it encloses no volume to combine"
963 )))
964}
965
966fn authored_mesh(mesh: TriMesh, _options: &ExecutionOptions) -> Built {
982 let closure = if authored_mesh_is_closed(&mesh) {
983 axiolid_mesh_compile_contract::MeshClosure::Solid
984 } else {
985 axiolid_mesh_compile_contract::MeshClosure::Surface
986 };
987 Built::with_closure(mesh, closure)
988}
989
990fn authored_mesh_is_closed(mesh: &TriMesh) -> bool {
994 if mesh.indices.is_empty() || mesh.positions.iter().any(|p| !p.is_finite()) {
995 return false;
996 }
997 let adjacency = axiolid_mesh::EdgeAdjacency::build(mesh);
998 adjacency.edge_count() > 0 && adjacency.is_closed_two_manifold()
999}