Skip to main content

axiolid_mesh_compile/
compiler.rs

1//! The `MeshCompiler` implementation: graph in, meshes out.
2//!
3//! Evaluation is iterative. The graph forbids non-prior references,
4//! so cycles are structurally impossible, but depth is unbounded and
5//! recursion would risk a stack overflow on adversarial input.
6
7use axiolid_contracts::{
8    Backend, BackendDescriptor, BackendId, ExecutionOptions, ExecutionTarget, GeomError,
9    GeomResult, Operation, ScratchRequirement,
10};
11use axiolid_core::{Point3, Scalar, Tolerance, Transform3, Vec3};
12use axiolid_mesh::TriMesh;
13use axiolid_mesh_boolean_contract::MeshBoolean;
14use axiolid_mesh_compile_contract::{
15    CompileOutcome, MeshCompiler, PolylineCompiler, PolylineOutcome,
16};
17use axiolid_model::{GeometryGraph, GeometryNode, NodeId, SolidOperation};
18
19use crate::half_space_boundary::PrismAxis;
20
21use axiolid_construct::extrude::extrude_profile;
22use axiolid_construct::profile::profile_rings;
23
24use crate::channels::{self, Built};
25use crate::station::{PlacementAccuracy, UNCERTIFIED_STATION_FRAME};
26
27/// Scalar reference compiler.
28///
29/// Generic over the boolean provider so this crate never depends on a
30/// particular one: `axiolid-mesh-boolean-boolmesh` is an adapter, and a different provider
31/// swaps in without touching this code.
32#[derive(Debug, Clone)]
33pub struct ReferenceMeshCompiler<B> {
34    boolean: B,
35}
36
37impl<B> ReferenceMeshCompiler<B> {
38    /// Bind a boolean provider.
39    pub const fn new(boolean: B) -> Self {
40        Self { boolean }
41    }
42
43    /// The bound provider.
44    pub const fn boolean(&self) -> &B {
45        &self.boolean
46    }
47}
48
49impl<B: MeshBoolean> Backend for ReferenceMeshCompiler<B> {
50    fn descriptor(&self) -> BackendDescriptor {
51        BackendDescriptor::new(
52            BackendId::new("scalar-compile"),
53            ExecutionTarget::PortableCpu,
54        )
55    }
56}
57
58/// Why the mesh cut of a boolean was refused.
59#[derive(Debug)]
60enum CutRefusal {
61    /// The result touches itself (#194) or holds two bodies within the
62    /// tolerance (ADR 0096, D2): the operands meet where the mesh boolean,
63    /// which has no tolerance, cannot decide. The exact referee may.
64    Contact(GeomError),
65    /// Any other refusal.
66    Other(GeomError),
67}
68
69impl CutRefusal {
70    fn into_error(self) -> GeomError {
71        match self {
72            Self::Contact(error) | Self::Other(error) => error,
73        }
74    }
75}
76
77/// A boolean's mesh cut, and whether its faces include a pair that nearly
78/// share a plane ([`axiolid_inspect::near_coplanar`]): contact the mesh
79/// boolean did not decide, whatever it returned (#391).
80struct MeshCut {
81    built: Built,
82    unsettled: bool,
83    /// The result holds a skin thinner than the rounding reach (#400, ADR
84    /// 0102): the faces a clip left a residue apart, which the snap could
85    /// not close.
86    skin: Option<PendingSkin>,
87}
88
89/// A skin within rounding a cut's result holds: the refusal that names it
90/// and the rounding reach it was read at.
91#[derive(Debug, Clone)]
92pub(crate) struct PendingSkin {
93    pub error: GeomError,
94    pub reach: Scalar,
95}
96
97/// Who decided a boolean's result.
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99enum Decided {
100    /// The mesh boolean.
101    Mesh,
102    /// The exact compiler, tessellated ([`crate::referee`]).
103    Referee,
104}
105
106/// One entry in the explicit evaluation stack.
107///
108/// `Enter` schedules dependency discovery; `Exit` runs after every dependency
109/// already has a mesh, which is what replaces the recursive call.
110#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
111struct EvalKey {
112    id: NodeId,
113    linear_bits: u64,
114    angular_bits: u64,
115    /// The chord budget in this node's local space (#165). Part of the
116    /// key because an instance scales it with its transform, and two
117    /// budgets tessellate one node differently.
118    chord_bits: u64,
119    /// The placed coordinate scale (#312): part of the key because a
120    /// boolean's snap reaches the rounding of the coordinates its result is
121    /// placed at, so a source placed near and far compiles once per scale.
122    placed_bits: u64,
123}
124
125/// The evaluation budgets that vary per node: the tolerance, and the chord
126/// budget curved geometry is flattened to (#165). Both rescale together
127/// through an instance transform. And the placed coordinate scale a
128/// boolean's snap reaches (#312, [`crate::snap::placed_scale`]): `0` where
129/// nothing places the node, else the world coordinate magnitude its
130/// placement adds, in the node's own units.
131#[derive(Debug, Clone, Copy, PartialEq)]
132struct Budget {
133    tolerance: Tolerance,
134    chord: Scalar,
135    placed: Scalar,
136}
137
138impl Budget {
139    fn of(options: &ExecutionOptions) -> Self {
140        let tolerance = options.tolerance();
141        Self {
142            tolerance,
143            chord: options.chord_error().unwrap_or(tolerance.linear()),
144            placed: 0.0,
145        }
146    }
147}
148
149impl EvalKey {
150    fn new(id: NodeId, budget: Budget) -> Self {
151        let bits = |value: Scalar| if value == 0.0 { 0 } else { value.to_bits() };
152        Self {
153            id,
154            linear_bits: bits(budget.tolerance.linear()),
155            angular_bits: bits(budget.tolerance.angular()),
156            chord_bits: bits(budget.chord),
157            placed_bits: bits(budget.placed),
158        }
159    }
160
161    fn budget(self) -> Budget {
162        Budget {
163            tolerance: self.tolerance(),
164            chord: Scalar::from_bits(self.chord_bits),
165            placed: Scalar::from_bits(self.placed_bits),
166        }
167    }
168
169    fn tolerance(self) -> Tolerance {
170        Tolerance::new(
171            Scalar::from_bits(self.linear_bits),
172            Scalar::from_bits(self.angular_bits),
173        )
174        .expect("evaluation keys only contain validated tolerances")
175    }
176}
177
178#[derive(Debug, Clone, Copy)]
179enum Step {
180    Enter(EvalKey),
181    Exit(EvalKey),
182}
183
184/// Whether a compilation measures what no construction proves: a
185/// boolean's deviation against its exact result (#235) costs an exact
186/// compilation and a certified search, so only a deviation report pays it,
187/// and only for the booleans whose result is emitted: an operand of
188/// another boolean is replaced by that boolean's own measurement.
189#[derive(Debug, Clone, Copy, PartialEq, Eq)]
190enum Measure<'m> {
191    Skip,
192    /// Measure the booleans in this set
193    /// ([`crate::deviation::emitted_booleans`]).
194    Deviation(&'m std::collections::HashSet<NodeId>),
195}
196
197/// Cached meshes keyed by node and effective local tolerance.
198///
199/// A transformed instance changes the local chord budget. Keying only by node
200/// would incorrectly reuse a coarse source mesh for a larger instance.
201///
202/// It also remembers which nodes are regions ([`Regions`]), so a batch of
203/// roots decides each node once.
204#[derive(Debug, Default)]
205struct Cache {
206    built: std::collections::HashMap<EvalKey, Built>,
207    regions: crate::regions::RegionMemo,
208}
209
210impl Cache {
211    fn new() -> Self {
212        Self::default()
213    }
214}
215
216impl std::ops::Deref for Cache {
217    type Target = std::collections::HashMap<EvalKey, Built>;
218
219    fn deref(&self) -> &Self::Target {
220        &self.built
221    }
222}
223
224impl std::ops::DerefMut for Cache {
225    fn deref_mut(&mut self) -> &mut Self::Target {
226        &mut self.built
227    }
228}
229
230impl<B: MeshBoolean> ReferenceMeshCompiler<B> {
231    /// Resolve a bounded half-space's boundary into rings.
232    ///
233    /// A closed polyline's points ARE the ring, taken as wound. A profile
234    /// (#277) is flattened as an extrusion's is, every arc chorded within
235    /// `chord` by the certified flattener, the rings oriented outer
236    /// counter-clockwise; an open, self-crossing or degenerate one is
237    /// refused there or by the cap triangulation, by name. Any other curve
238    /// needs the curve evaluator, and an open one does not bound anything,
239    /// so both are refused rather than guessed at.
240    fn boundary_rings(
241        &self,
242        graph: &GeometryGraph,
243        id: NodeId,
244        chord: Scalar,
245        options: &ExecutionOptions,
246    ) -> GeomResult<axiolid_construct::profile::Rings> {
247        let node = self.node(graph, id)?;
248        let curve = match node {
249            GeometryNode::Curve2(curve) => curve,
250            GeometryNode::Profile(shape) => {
251                crate::half_space_boundary::check(shape, options.tolerance())?;
252                return profile_rings(shape, chord, options.tolerance());
253            }
254            _ => {
255                return Err(GeomError::InvalidInput(format!(
256                    "half-space boundary {id:?} is neither a Curve2 nor a Profile node"
257                )))
258            }
259        };
260        match curve {
261            axiolid_curve::Curve2::Polyline(p) => {
262                let mut pts = p.points.clone();
263                // A closed polyline may or may not repeat its first point.
264                // Dropping the duplicate keeps the ring's edge count honest.
265                if pts.len() >= 2 && pts[0] == pts[pts.len() - 1] {
266                    pts.pop();
267                }
268                if pts.len() < 3 {
269                    return Err(GeomError::InvalidInput(
270                        "half-space boundary needs at least 3 distinct points".to_owned(),
271                    ));
272                }
273                Ok(axiolid_construct::profile::Rings {
274                    outer: pts,
275                    holes: Vec::new(),
276                })
277            }
278            _ => Err(GeomError::Unsupported {
279                backend: self.descriptor().id,
280                operation: Operation::CurveEvaluation,
281            }),
282        }
283    }
284
285    /// The slab of a bounded half-space, `depth` deep on its kept side.
286    ///
287    /// `placement` is the boundary's OWN frame, independent of the clip
288    /// plane: the profile is authored in it, so it orients the profile
289    /// before the slab is built. Applying it only to the finished mesh
290    /// cannot express an in-plane rotation, because by then the boundary has
291    /// already been framed against the plane normal. The whole frame is
292    /// honoured (#322): along the plane normal its `x` is projected into the
293    /// plane and its scale, shear and handedness kept
294    /// ([`BoundaryPrism::along_plane_normal`]); along the placement's `z`
295    /// (#321) it is read as written ([`BoundaryPrism::along_placement_z`]),
296    /// the prism at any angle to the plane. Either way the boundary is
297    /// chorded within the budget over the frame's longest axis, so the
298    /// chords stay within it once placed.
299    ///
300    /// [`BoundaryPrism::along_plane_normal`]: axiolid_construct::half_space::BoundaryPrism::along_plane_normal
301    /// [`BoundaryPrism::along_placement_z`]: axiolid_construct::half_space::BoundaryPrism::along_placement_z
302    fn bounded_half_space(
303        &self,
304        graph: &GeometryGraph,
305        (half_space, boundary): (NodeId, NodeId),
306        placement: Transform3,
307        prism_axis: PrismAxis,
308        depth: axiolid_construct::half_space::SlabDepth,
309        options: &ExecutionOptions,
310    ) -> GeomResult<TriMesh> {
311        use axiolid_construct::half_space::{bounded_half_space_prism, BoundaryPrism};
312        let node = self.node(graph, half_space)?;
313        let GeometryNode::HalfSpace(hs) = node else {
314            return Err(GeomError::InvalidInput(format!(
315                "half-space {half_space:?} is not a HalfSpace node"
316            )));
317        };
318        let tolerance = options.tolerance();
319        let prism = match prism_axis {
320            PrismAxis::PlaneNormal => {
321                BoundaryPrism::along_plane_normal(placement, hs.boundary, tolerance)?
322            }
323            PrismAxis::PlacementZ => BoundaryPrism::along_placement_z(placement, tolerance)?,
324        };
325        let stretch = prism.x.length().max(prism.y.length());
326        let chord = chord_error(options) / stretch.max(1.0);
327        let rings = self.boundary_rings(graph, boundary, chord, options)?;
328        bounded_half_space_prism(&rings, hs.boundary, prism, hs.agreement, depth, tolerance)
329    }
330
331    /// The finite stand-in for a half-space `tool`, placed by `placement`,
332    /// over `subject`: the tool is unbounded on its kept side, and a
333    /// difference or an intersection with it stays within the subject, so
334    /// a slab reaching past the subject is the half-space for them.
335    ///
336    /// The subject is brought into the tool's own frame. An unbounded
337    /// half-space covers it as before (`for_subject`); a bounded one is cut
338    /// off past the subject's farthest point on its kept side, by a tenth of
339    /// the subject's span plus the tolerance (#325): sized by its boundary
340    /// alone, a small boundary over a tall subject ended inside it and cut
341    /// it too little, with no error. The slab is placed back, its boundary
342    /// chorded within the budget over the placement's stretch.
343    fn clip_tool_mesh(
344        &self,
345        graph: &GeometryGraph,
346        tool: ClipTool,
347        placement: Transform3,
348        subject: &TriMesh,
349        options: &ExecutionOptions,
350    ) -> GeomResult<TriMesh> {
351        let tolerance = options.tolerance();
352        let placed = placement != Transform3::IDENTITY;
353        let inverse = placement.inverse();
354        if placed
355            && !(placement.matrix3.determinant() != 0.0
356                && inverse.matrix3.is_finite()
357                && inverse.translation.is_finite())
358        {
359            return Err(GeomError::InvalidInput(
360                "half-space placed by a singular transform".to_owned(),
361            ));
362        }
363        let local_subject;
364        let subject = if placed {
365            local_subject = TriMesh::new(
366                subject
367                    .positions
368                    .iter()
369                    .map(|&p| inverse.transform_point3(p))
370                    .collect(),
371                subject.indices.clone(),
372            );
373            &local_subject
374        } else {
375            subject
376        };
377        let local = match tool {
378            ClipTool::Unbounded(half_space) => {
379                axiolid_construct::half_space::for_subject(subject, half_space, tolerance)?
380            }
381            ClipTool::Bounded {
382                half_space,
383                boundary,
384                placement: frame,
385                prism_axis,
386            } => {
387                let Some(GeometryNode::HalfSpace(hs)) = graph.get(half_space) else {
388                    return Err(GeomError::InvalidInput(format!(
389                        "half-space {half_space:?} is not a HalfSpace node"
390                    )));
391                };
392                let normal = hs.boundary.normal.normalize_or_zero();
393                if normal == Vec3::ZERO || subject.positions.is_empty() {
394                    return Err(GeomError::InvalidInput(
395                        "bounded half-space boolean needs a plane and a subject with bounds".into(),
396                    ));
397                }
398                let kept = if hs.agreement { normal } else { -normal };
399                let (mut lo, mut hi) = (
400                    Vec3::splat(Scalar::INFINITY),
401                    Vec3::splat(Scalar::NEG_INFINITY),
402                );
403                let mut farthest = Scalar::NEG_INFINITY;
404                for &point in &subject.positions {
405                    if !point.is_finite() {
406                        return Err(GeomError::InvalidInput(
407                            "bounded half-space subject contains non-finite points".into(),
408                        ));
409                    }
410                    lo = lo.min(point);
411                    hi = hi.max(point);
412                    farthest = farthest.max((point - hs.boundary.origin).dot(kept));
413                }
414                let span = (hi - lo).max_element().max(tolerance.linear());
415                let reach = farthest.max(0.0) + 0.1 * span + tolerance.linear();
416                let local_options;
417                let options = if placed {
418                    let chord = chord_error(options) / crate::deviation::stretch(placement);
419                    local_options = options.clone().with_chord_error(chord).ok_or_else(|| {
420                        GeomError::InvalidInput(format!(
421                            "chord budget {chord} is not positive and finite"
422                        ))
423                    })?;
424                    &local_options
425                } else {
426                    options
427                };
428                self.bounded_half_space(
429                    graph,
430                    (half_space, boundary),
431                    frame,
432                    prism_axis,
433                    axiolid_construct::half_space::SlabDepth::Reach(reach),
434                    options,
435                )?
436            }
437        };
438        Ok(if placed {
439            channels::transform(&Built::leaf(local), placement, 0.0).mesh
440        } else {
441            local
442        })
443    }
444
445    /// The finite stand-in for a region over `subject` (#326): the first
446    /// leaf's slab is sized by the subject, as a single clip's is, and each
447    /// further leaf's by the stand-in so far, then cut together. `None`
448    /// when the leaves share nothing within the subject's reach: an empty
449    /// tool. With one leaf it is `clip_tool_mesh`, unchanged.
450    fn region_tool_mesh(
451        &self,
452        graph: &GeometryGraph,
453        leaves: &[(ClipTool, Transform3)],
454        subject: &TriMesh,
455        options: &ExecutionOptions,
456        placed: Scalar,
457    ) -> GeomResult<Option<Built>> {
458        let mut tool: Option<Built> = None;
459        for &(leaf, placement) in leaves {
460            let around = tool.as_ref().map_or(subject, |built| &built.mesh);
461            let side = self.clip_tool_mesh(graph, leaf, placement, around, options)?;
462            tool = Some(match tool {
463                None => Built::leaf(side),
464                Some(so_far) => {
465                    let cut = self
466                        .boolean_of(
467                            &so_far,
468                            (&side, None, None),
469                            axiolid_core::BooleanOperator::Intersection,
470                            options,
471                            placed,
472                        )
473                        .map_err(CutRefusal::into_error)?;
474                    if let Some(skin) = cut.skin {
475                        return Err(skin.error);
476                    }
477                    if cut.built.mesh.indices.is_empty() {
478                        return Ok(None);
479                    }
480                    cut.built
481                }
482            });
483        }
484        Ok(tool)
485    }
486
487    /// Resolve a node that must be a profile, into flattened rings.
488    /// Resolve a node that must be a surface.
489    ///
490    /// Reported by node id rather than by position so a malformed graph
491    /// names the offending node instead of the operation that reached it.
492    fn surface_of(
493        graph: &GeometryGraph,
494        id: axiolid_model::NodeId,
495    ) -> GeomResult<&axiolid_surface::Surface> {
496        match graph.get(id) {
497            Some(GeometryNode::Surface(surface)) => Ok(surface),
498            // A relation is a model node with no `Surface` value to
499            // evaluate; a refusal by name, not a malformed graph (#402).
500            Some(GeometryNode::SurfaceRelation(_)) => Err(GeomError::UnsupportedInput {
501                backend: crate::BACKEND_ID,
502                operation: axiolid_contracts::Operation::SurfaceEvaluation,
503                input: "a surface relation as a reference surface",
504            }),
505            Some(_) => Err(GeomError::InvalidInput(format!(
506                "reference surface {id:?} is not a Surface node"
507            ))),
508            None => Err(GeomError::InvalidInput(format!(
509                "reference surface {id:?} does not belong to this graph"
510            ))),
511        }
512    }
513
514    fn surface_normals(
515        &self,
516        graph: &GeometryGraph,
517        id: NodeId,
518        path: &[Point3],
519        options: &ExecutionOptions,
520    ) -> GeomResult<Vec<axiolid_core::Vec3>> {
521        if let Some(GeometryNode::SurfaceRelation(
522            axiolid_model::SurfaceRelation::LinearExtrusion { direction, .. },
523        )) = graph.get(id)
524        {
525            return axiolid_construct::sweep::linear_extrusion_normals(path, *direction);
526        }
527        let surface = Self::surface_of(graph, id)?;
528        path.iter()
529            .map(|point| {
530                let (u, v) =
531                    axiolid_reference::surface::invert(surface, *point, options.tolerance())?;
532                axiolid_reference::surface::normal(surface, u, v)
533            })
534            .collect()
535    }
536
537    /// A profile's rings, chorded to `chord`.
538    fn rings_of(
539        &self,
540        graph: &GeometryGraph,
541        id: NodeId,
542        chord: Scalar,
543        options: &ExecutionOptions,
544        what: &str,
545    ) -> GeomResult<axiolid_construct::profile::Rings> {
546        let node = self.node(graph, id)?;
547        let GeometryNode::Profile(shape) = node else {
548            return Err(GeomError::InvalidInput(format!(
549                "{what} {id:?} is not a Profile node"
550            )));
551        };
552        profile_rings(shape, chord, options.tolerance())
553    }
554
555    /// A directrix sampled to the chord budget `chord`, with its end
556    /// tangents when it is one smooth conic (#231).
557    fn directrix_sampled(
558        &self,
559        graph: &GeometryGraph,
560        id: NodeId,
561        range: Option<(Scalar, Scalar)>,
562        options: &ExecutionOptions,
563        chord: Scalar,
564    ) -> GeomResult<axiolid_construct::sweep::SampledPath> {
565        let options = options.clone().with_chord_error(chord).ok_or_else(|| {
566            GeomError::InvalidInput(format!(
567                "directrix chord budget {chord} is not positive and finite"
568            ))
569        })?;
570        crate::directrix::sampled(graph, id, range, &options)
571    }
572
573    /// Sample a directrix curve into a polyline.
574    ///
575    /// Only a polyline directrix is handled here: its points ARE the samples,
576    /// so no evaluator is involved. An analytic directrix needs the curve
577    /// provider, which the compiler does not yet hold, and is refused with the
578    /// named capability rather than silently approximated by its control
579    /// points.
580    fn directrix_points(
581        &self,
582        graph: &GeometryGraph,
583        id: NodeId,
584        range: Option<(Scalar, Scalar)>,
585        options: &ExecutionOptions,
586    ) -> GeomResult<Vec<Point3>> {
587        crate::directrix::points(graph, id, range, options)
588    }
589
590    fn node<'g>(&self, graph: &'g GeometryGraph, id: NodeId) -> GeomResult<&'g GeometryNode> {
591        graph.get(id).ok_or_else(|| {
592            GeomError::InvalidInput(format!("node {id:?} does not belong to this graph"))
593        })
594    }
595
596    /// Nodes that must have meshes before `id` can be evaluated.
597    ///
598    /// Only mesh-producing dependencies are listed. A profile referenced by an
599    /// extrusion is consumed as 2D data, not as a mesh, so it is deliberately
600    /// absent: compiling it standalone would be meaningless.
601    fn mesh_dependencies(
602        &self,
603        graph: &GeometryGraph,
604        regions: &Regions<'_>,
605        node: &GeometryNode,
606        key: EvalKey,
607    ) -> GeomResult<Vec<EvalKey>> {
608        let budget = key.budget();
609        let same = |id| EvalKey::new(id, budget);
610        // Placed by a transform, or in a station's frame (#264): a rigid
611        // motion resolved from the graph. The source is meshed with the
612        // initial room for the placement's rounding (#295); a retry with
613        // more is scheduled by `evaluate`.
614        if let Some((source, transform)) = placed_source(graph, node, key.id)? {
615            return Ok(vec![EvalKey::new(
616                source,
617                instance_local_budget(transform, budget, 0.0)?,
618            )]);
619        }
620        Ok(match node {
621            GeometryNode::Collection(members) => members.iter().copied().map(same).collect(),
622            GeometryNode::SolidOperation(SolidOperation::Boolean {
623                left,
624                right,
625                operator,
626            }) => {
627                if let Some(clip) = clip_of(graph, regions, (*left, *right), *operator)? {
628                    // The region is built from its subject in `build_boolean`.
629                    vec![same(clip.subject)]
630                } else if regions.has(*left) || regions.has(*right) {
631                    // A region has no mesh of its own: `build_boolean` refuses
632                    // the boolean by name (#326).
633                    Vec::new()
634                } else {
635                    vec![same(*left), same(*right)]
636                }
637            }
638            _ => Vec::new(),
639        })
640    }
641
642    /// Iterative post-order evaluation of one root.
643    fn evaluate(
644        &self,
645        graph: &GeometryGraph,
646        root: NodeId,
647        options: &ExecutionOptions,
648        cache: &mut Cache,
649        measure: Measure<'_>,
650    ) -> GeomResult<Built> {
651        let root_key = EvalKey::new(root, Budget::of(options));
652        let mut stack = vec![Step::Enter(root_key)];
653        // The room an instance's source was re-meshed with for its
654        // placement's rounding (#295), once its first mesh rounded by more
655        // than the initial room left.
656        let mut rooms: std::collections::HashMap<EvalKey, Scalar> =
657            std::collections::HashMap::new();
658        // How far each node placed at a station lies from its exact
659        // placement (#405), read once: the retry and the build see one.
660        let mut accuracies: std::collections::HashMap<EvalKey, PlacementAccuracy> =
661            std::collections::HashMap::new();
662        while let Some(step) = stack.pop() {
663            match step {
664                Step::Enter(key) => {
665                    if cache.contains_key(&key) {
666                        continue;
667                    }
668                    let node = self.node(graph, key.id)?;
669                    let deps = self.mesh_dependencies(
670                        graph,
671                        &Regions::new(graph, &cache.regions),
672                        node,
673                        key,
674                    )?;
675                    // Exit runs after every dependency, so push it first.
676                    stack.push(Step::Exit(key));
677                    for dep in deps {
678                        if !cache.contains_key(&dep) {
679                            stack.push(Step::Enter(dep));
680                        }
681                    }
682                }
683                Step::Exit(key) => {
684                    if cache.contains_key(&key) {
685                        continue;
686                    }
687                    let budget = key.budget();
688                    let node = self.node(graph, key.id)?;
689                    if matches!(node, GeometryNode::InstanceAtStation(_)) {
690                        if let std::collections::hash_map::Entry::Vacant(slot) =
691                            accuracies.entry(key)
692                        {
693                            slot.insert(crate::station::placement_accuracy(
694                                graph,
695                                key.id,
696                                crate::station::CERTIFICATION_SHARE * budget.chord,
697                            )?);
698                        }
699                    }
700                    let accuracy = accuracies.get(&key).copied();
701                    if let std::collections::hash_map::Entry::Vacant(slot) = rooms.entry(key) {
702                        if let Some((source, transform, room)) =
703                            self.placement_retry(graph, node, key, cache, accuracy)?
704                        {
705                            slot.insert(room);
706                            stack.push(Step::Exit(key));
707                            stack.push(Step::Enter(EvalKey::new(
708                                source,
709                                instance_local_budget(transform, budget, room)?,
710                            )));
711                            continue;
712                        }
713                    }
714                    let room = rooms.get(&key).copied().unwrap_or(0.0);
715                    let local_options = options
716                        .clone()
717                        .with_tolerance(budget.tolerance)
718                        .with_chord_error(budget.chord)
719                        .ok_or_else(|| {
720                            GeomError::InvalidInput(format!(
721                                "chord budget {} is not positive and finite",
722                                budget.chord
723                            ))
724                        })?;
725                    let built = self.build(
726                        graph,
727                        (key, room, accuracy),
728                        &local_options,
729                        (cache, &Regions::new(graph, &cache.regions)),
730                        (measure, root),
731                    )?;
732                    cache.insert(key, built);
733                }
734            }
735        }
736        cache
737            .get(&root_key)
738            .cloned()
739            .ok_or_else(|| GeomError::InvalidInput(format!("root {root:?} produced no mesh")))
740    }
741
742    /// The room for its placement's rounding an instance's source needs
743    /// beyond the initial one (#295), with the source and the transform:
744    /// `Some` when the source, meshed with the initial room
745    /// ([`placement_room`]), rounds by more once placed and its bound would
746    /// carry the instance over the chord budget. The room is an eighth more
747    /// than the rounding shown, kept below the budget: a mesh finer by
748    /// that little has nearly the same coordinates. `None` for any other
749    /// node, an unbounded source, or one that fits. A source whose placed
750    /// vertices round by the whole budget is refused
751    /// ([`placed_rounding`]).
752    fn placement_retry(
753        &self,
754        graph: &GeometryGraph,
755        node: &GeometryNode,
756        key: EvalKey,
757        cache: &Cache,
758        accuracy: Option<PlacementAccuracy>,
759    ) -> GeomResult<Option<(NodeId, Transform3, Scalar)>> {
760        let Some((source_id, transform)) = placed_source(graph, node, key.id)? else {
761            return Ok(None);
762        };
763        let budget = key.budget();
764        let source = self.cached(
765            cache,
766            source_id,
767            instance_local_budget(transform, budget, 0.0)?,
768        )?;
769        // What the placement adds to every bound: its rounding, and where
770        // the station's frame has a certified error (#405), that.
771        let rounding = placed_rounding(&source.mesh, transform, budget.chord)?;
772        let shown = rounding + frame_term(accuracy, &source.mesh).unwrap_or(0.0);
773        if shown <= placement_room(transform, budget.chord, 0.0)? || shown >= budget.chord {
774            return Ok(None);
775        }
776        let Some(bound) = source.deviation.bound() else {
777            return Ok(None);
778        };
779        let factor = crate::deviation::stretch(transform);
780        if crate::deviation::grown(bound, factor, shown) <= budget.chord {
781            return Ok(None);
782        }
783        let room = (shown * 1.125).min(shown + (budget.chord - shown) * 0.5);
784        Ok(Some((source_id, transform, room)))
785    }
786
787    /// Build one node, assuming its mesh dependencies are already cached.
788    /// `room` is the room an instance's source was re-meshed with for its
789    /// placement's rounding (#295), `0` for the initial one, and `accuracy`
790    /// how far a node placed at a station lies from its exact placement
791    /// (#405).
792    fn build(
793        &self,
794        graph: &GeometryGraph,
795        (key, room, accuracy): (EvalKey, Scalar, Option<PlacementAccuracy>),
796        options: &ExecutionOptions,
797        (cache, regions): (&Cache, &Regions<'_>),
798        (measure, root): (Measure<'_>, NodeId),
799    ) -> GeomResult<Built> {
800        let id = key.id;
801        // The key's budget, placed scale included: what the dependencies
802        // were evaluated at (`mesh_dependencies`).
803        let budget = key.budget();
804        let node = self.node(graph, id)?;
805        match node {
806            GeometryNode::TriMesh(mesh) => Ok(authored_mesh(mesh.clone(), options)),
807            GeometryNode::PolygonMesh(mesh) => {
808                compile_authored_polygons(mesh, options).map(|(mesh, warp, steiner_offset)| {
809                    let built = authored_mesh(mesh, options);
810                    let mut deviation = built.deviation.clone();
811                    for (detail, bound) in [
812                        (crate::deviation::WARPED_AUTHORED_FACE, warp),
813                        (crate::deviation::KEYHOLE_STEINER_POINT, steiner_offset),
814                    ] {
815                        if let Some(bound) = bound {
816                            deviation.add(
817                                crate::deviation::DeviationPath::AuthoredMesh,
818                                detail,
819                                crate::deviation::DeviationBound::Certified(bound),
820                            );
821                        }
822                    }
823                    built.with_deviation(deviation)
824                })
825            }
826            GeometryNode::Instance(instance) => {
827                let source_budget = instance_local_budget(instance.transform, budget, room)?;
828                let source = self.cached(cache, instance.source, source_budget)?;
829                let rounding = placed_rounding(&source.mesh, instance.transform, budget.chord)?;
830                Ok(channels::transform(source, instance.transform, rounding))
831            }
832            // An instance in a station's frame (#264): exact on a line;
833            // elsewhere its frame's certified error grows every bound
834            // (#405), or its placement is named unbounded.
835            GeometryNode::InstanceAtStation(placed) => {
836                let placement = crate::station::placement(graph, id)?;
837                let source_budget = instance_local_budget(placement.transform, budget, room)?;
838                let source = self.cached(cache, placed.source, source_budget)?;
839                let rounding = placed_rounding(&source.mesh, placement.transform, budget.chord)?;
840                let accuracy = accuracy.ok_or_else(|| {
841                    GeomError::InvalidInput(format!(
842                        "node {id:?} placed at a station was built before its accuracy was read"
843                    ))
844                })?;
845                let unbounded = |reason| {
846                    let mut built = channels::transform(source, placement.transform, rounding);
847                    built.deviation.add(
848                        crate::deviation::DeviationPath::StationPlacement,
849                        crate::deviation::NUMERICAL_STATION_FRAME,
850                        crate::deviation::DeviationBound::Unbounded(reason),
851                    );
852                    built
853                };
854                Ok(match accuracy {
855                    PlacementAccuracy::Exact => {
856                        channels::transform(source, placement.transform, rounding)
857                    }
858                    PlacementAccuracy::Certified(_) => {
859                        match frame_term(Some(accuracy), &source.mesh) {
860                            Some(frame) => channels::transform_within(
861                                source,
862                                placement.transform,
863                                rounding,
864                                frame,
865                            ),
866                            None => unbounded(UNCERTIFIED_STATION_FRAME),
867                        }
868                    }
869                    PlacementAccuracy::Unbounded(reason) => unbounded(reason),
870                })
871            }
872            GeometryNode::Collection(members) => {
873                let members = members
874                    .iter()
875                    .map(|&member| self.cached(cache, member, budget))
876                    .collect::<GeomResult<Vec<_>>>()?;
877                Ok(channels::merge(&members))
878            }
879            GeometryNode::SolidOperation(SolidOperation::Boolean {
880                left,
881                right,
882                operator,
883            }) => {
884                let (built, decided) = self.build_boolean(
885                    graph,
886                    (id, (*left, *right)),
887                    *operator,
888                    options,
889                    budget,
890                    (cache, regions, root),
891                )?;
892                Ok(match measure {
893                    // The exact compiler's own result states its bound
894                    // (#391): measuring it against itself would be circular.
895                    Measure::Deviation(_) | Measure::Skip if decided == Decided::Referee => built,
896                    // Measured against the exact result only when a report
897                    // asks for it: it compiles the exact boolean (#235).
898                    // Operands snapped within tolerance stay reported (#276).
899                    Measure::Deviation(emitted) if emitted.contains(&id) => {
900                        let mut deviation =
901                            crate::deviation::of_boolean(graph, id, options, &built.mesh);
902                        deviation.carry_perturbations(&built.deviation);
903                        built.with_deviation(deviation)
904                    }
905                    Measure::Deviation(_) | Measure::Skip => built,
906                })
907            }
908            GeometryNode::SolidOperation(operation) => {
909                let (mesh, deviation) = self.build_solid_within(graph, operation, options)?;
910                Ok(Built::leaf(mesh).with_deviation(deviation))
911            }
912            GeometryNode::BRep(brep) => {
913                crate::brep::tessellate(brep, graph, options.tolerance(), chord_error(options)).map(
914                    |(mesh, closure, deviation)| {
915                        Built::with_closure(mesh, closure).with_deviation(deviation)
916                    },
917                )
918            }
919            // A plane trimmed by boundary curves: a surface (#192).
920            GeometryNode::SurfaceRelation(axiolid_model::SurfaceRelation::CurveBounded {
921                basis,
922                boundaries,
923                implicit_outer,
924            }) => crate::bounded::curve_bounded(
925                self.descriptor().id,
926                graph,
927                *basis,
928                boundaries,
929                *implicit_outer,
930                options,
931            )
932            .map(|(mesh, merged)| {
933                Built::with_closure(mesh, axiolid_mesh_compile_contract::MeshClosure::Surface)
934                    .with_deviation(crate::deviation::of_curve_bounded(
935                        graph, *basis, boundaries, options, merged,
936                    ))
937            }),
938            // Open sections at stations along a directrix: a sheet (#241).
939            GeometryNode::SurfaceRelation(axiolid_model::SurfaceRelation::SectionedSurface {
940                directrix,
941                sections,
942                frame,
943            }) => {
944                let sections: Vec<_> = sections.iter().cloned().map(Into::into).collect();
945                crate::station::sectioned_surface(graph, *directrix, &sections, *frame, options)
946                    .map(|(mesh, bound)| {
947                        Built::with_closure(
948                            mesh,
949                            axiolid_mesh_compile_contract::MeshClosure::Surface,
950                        )
951                        .with_deviation(crate::deviation::of_sectioned_surface(bound))
952                    })
953            }
954            // The general form: oriented sections, joined by tag (#246).
955            GeometryNode::SurfaceRelation(
956                axiolid_model::SurfaceRelation::OpenSectionsAtStations {
957                    directrix,
958                    sections,
959                    frame,
960                },
961            ) => crate::station::sectioned_surface(graph, *directrix, sections, *frame, options)
962                .map(|(mesh, bound)| {
963                    Built::with_closure(mesh, axiolid_mesh_compile_contract::MeshClosure::Surface)
964                        .with_deviation(crate::deviation::of_sectioned_surface(bound))
965                }),
966            // A surface bounded by a parameter rectangle: a sheet (#311).
967            GeometryNode::SurfaceRelation(axiolid_model::SurfaceRelation::RectangularTrimmed {
968                basis,
969                u,
970                v,
971                u_sense,
972                v_sense,
973            }) => crate::trimmed::rectangular(
974                graph,
975                *basis,
976                crate::trimmed::Trim {
977                    u: *u,
978                    v: *v,
979                    u_sense: *u_sense,
980                    v_sense: *v_sense,
981                },
982                options,
983            )
984            .map(|(mesh, family, bound)| {
985                Built::with_closure(mesh, axiolid_mesh_compile_contract::MeshClosure::Surface)
986                    .with_deviation(crate::deviation::Deviation::one(
987                        crate::deviation::DeviationPath::TrimmedSurface,
988                        family,
989                        bound,
990                    ))
991            }),
992            // A surface ruled between two sides: a sheet (#402).
993            GeometryNode::SurfaceRelation(axiolid_model::SurfaceRelation::Ruled {
994                first,
995                second,
996            }) => crate::ruled::ruled(graph, first, second, options).map(|(mesh, bound)| {
997                Built::with_closure(mesh, axiolid_mesh_compile_contract::MeshClosure::Surface)
998                    .with_deviation(crate::deviation::Deviation::one(
999                        crate::deviation::DeviationPath::RuledSurface,
1000                        "ruled",
1001                        bound,
1002                    ))
1003            }),
1004            // CSG primitives are analytic solids: no surface evaluation,
1005            // no trim curves, just a closed mesh at the caller's tolerance.
1006            GeometryNode::Primitive(primitive) => {
1007                axiolid_reference::primitive::tessellate_primitive(
1008                    primitive,
1009                    chord_tolerance(options)?,
1010                )
1011                .map(|mesh| {
1012                    let deviation = crate::deviation::of_primitive(primitive, chord_error(options));
1013                    Built::leaf(mesh).with_deviation(deviation)
1014                })
1015            }
1016            // Unbounded on its kept side, a half-space has a finite stand-in
1017            // only against a subject (#326): it is a clip's tool.
1018            GeometryNode::HalfSpace(_) => Err(unsupported_input(HALF_SPACE_WITHOUT_SUBJECT)),
1019            other => Err(GeomError::Unsupported {
1020                backend: self.descriptor().id,
1021                operation: unsupported_operation(other),
1022            }),
1023        }
1024    }
1025
1026    /// Read an already-built dependency.
1027    fn cached<'c>(&self, cache: &'c Cache, id: NodeId, budget: Budget) -> GeomResult<&'c Built> {
1028        cache.get(&EvalKey::new(id, budget)).ok_or_else(|| {
1029            GeomError::InvalidInput(format!(
1030                "dependency {id:?} was not evaluated first at {budget:?}"
1031            ))
1032        })
1033    }
1034
1035    /// A boolean, with the operands' channel fates composed onto the
1036    /// provider's (#115): a channel lost upstream stays reported lost.
1037    /// `budget` is the boolean's own, the placed scale included (#312).
1038    ///
1039    /// Operands placed by the same linear part are cut in the frame they
1040    /// share and the result is placed (#356,
1041    /// [`Self::boolean_in_shared_frame`]): placing each operand rounds the
1042    /// vertices of faces that lie on one plane in exact arithmetic onto
1043    /// planes a residue apart, which the snap, landing only on axis-aligned
1044    /// planes, cannot close under a general rotation. That cut is never
1045    /// worse: where it is refused, for any reason (the unmerged-contact
1046    /// guard included), the operands are cut in the world frame as before.
1047    ///
1048    /// A region (a half-space, or an intersection of half-spaces, read
1049    /// through instances) is a clip's tool (#326): of a difference or an
1050    /// intersection, or, an intersection commuting, on the left of one with
1051    /// a solid. It has no mesh of its own and is built here from its
1052    /// subject ([`Self::region_tool_mesh`]); any other boolean with one is
1053    /// refused by name.
1054    ///
1055    /// An operand with no triangles (a clip that left nothing, an
1056    /// intersection of disjoint solids) decides the boolean by an identity
1057    /// before any frame or cut (#326, [`crate::empty`]): the subject, the
1058    /// tool or nothing, as built, with no deviation of a cut. A skin the
1059    /// subject has pending passes through to the root of its chain.
1060    ///
1061    /// A boolean the world-frame mesh cut cannot settle is decided by the
1062    /// exact compiler (#391, #423, [`crate::referee`]): when faces of its operands
1063    /// nearly share a plane within the linear tolerance, or when the cut is
1064    /// refused as a pinch or as unmerged contact. The referee is never
1065    /// worse than the mesh path: where it refuses, a valid mesh result is
1066    /// returned as before, and a contact refusal keeps its message and names
1067    /// the referee's reason.
1068    fn build_boolean(
1069        &self,
1070        graph: &GeometryGraph,
1071        (id, (left, right)): (NodeId, (NodeId, NodeId)),
1072        operator: axiolid_core::BooleanOperator,
1073        options: &ExecutionOptions,
1074        budget: Budget,
1075        (cache, regions, root): (&Cache, &Regions<'_>, NodeId),
1076    ) -> GeomResult<(Built, Decided)> {
1077        // A region is the tool of a clip, or the left operand of an
1078        // intersection with a solid, whose operands are then read the other
1079        // way round; any other boolean with one is unbounded (#326).
1080        let clip = clip_of(graph, regions, (left, right), operator)?;
1081        if clip.is_none() && (regions.has(left) || regions.has(right)) {
1082            return Err(unsupported_input(UNCLIPPED_HALF_SPACE));
1083        }
1084        let (left, right) = clip
1085            .as_ref()
1086            .map_or((left, right), |clip| (clip.subject, clip.tool));
1087        let subject = self.cached(cache, left, budget)?;
1088        refuse_surface_operand(subject, "subject")?;
1089        // Any other tool is a node of its own, built before this one.
1090        let built_tool = match &clip {
1091            Some(_) => None,
1092            None => {
1093                let built = self.cached(cache, right, budget)?;
1094                refuse_surface_operand(built, "tool")?;
1095                Some(built)
1096            }
1097        };
1098        // An empty operand decides the boolean by an identity, before any
1099        // frame or cut (#326, `crate::empty`).
1100        let identity = crate::empty::identity(
1101            operator,
1102            subject.mesh.indices.is_empty(),
1103            built_tool.is_some_and(|built| built.mesh.indices.is_empty()),
1104        );
1105        if identity.is_none() {
1106            if let Ok(Some(built)) = self.boolean_in_shared_frame(
1107                graph,
1108                (left, right),
1109                operator,
1110                options,
1111                budget,
1112                (cache, regions),
1113            ) {
1114                return Ok((built, Decided::Mesh));
1115            }
1116            // A clip is a constructed stand-in with no coordinates of its
1117            // own: cut in its subject's frame, never worse than the world cut
1118            // below (#400, ADR 0102).
1119            if let Ok(Some(built)) = self.clip_in_subject_frame(
1120                graph,
1121                (left, right),
1122                operator,
1123                options,
1124                budget,
1125                (cache, regions),
1126            ) {
1127                return Ok((built, Decided::Mesh));
1128            }
1129        }
1130        let region_tool = match &clip {
1131            Some(clip) if identity.is_none() => {
1132                self.region_tool_mesh(graph, &clip.leaves, &subject.mesh, options, budget.placed)?
1133            }
1134            _ => None,
1135        };
1136        // A region that leaves nothing of the subject's reach is an empty
1137        // tool (#326).
1138        let identity = match (&clip, &region_tool, identity) {
1139            (Some(_), None, None) => crate::empty::identity(operator, false, true),
1140            (.., identity) => identity,
1141        };
1142        // A half-space is built here from the subject, so it has no
1143        // upstream fates of its own; the deviation of a tool cut from
1144        // several leaves is carried (#326).
1145        let nothing = TriMesh::default();
1146        let (tool, tool_fates, tool_deviation) = match (&region_tool, built_tool, &clip) {
1147            (Some(built), _, Some(clip)) => (
1148                &built.mesh,
1149                None,
1150                (clip.leaves.len() > 1).then_some(&built.deviation),
1151            ),
1152            (None, Some(built), _) => (&built.mesh, Some(&built.fates), Some(&built.deviation)),
1153            _ => (&nothing, None, None),
1154        };
1155        let cut = match identity {
1156            Some(identity) => {
1157                identity_cut(identity, subject, built_tool).map_err(CutRefusal::Other)
1158            }
1159            None => self.boolean_of(
1160                subject,
1161                (tool, tool_fates, tool_deviation),
1162                operator,
1163                options,
1164                budget.placed,
1165            ),
1166        };
1167        // A boolean the mesh cut cannot settle is decided by the exact
1168        // compiler (#391, #423, ADR 0098). It is never worse than the mesh path:
1169        // where the referee refuses, an unsettled but valid mesh result is
1170        // returned as before, and a contact refusal keeps its message and
1171        // names the referee's reason.
1172        let half_space_tool = clip.is_some();
1173        let referee = |reason: &mut Option<String>,
1174                       against: Option<crate::referee::Against<'_>>,
1175                       rounding: Option<Scalar>| {
1176            // A half-space is clipped to the subject here and never built as
1177            // a node of its own, so it has no built operand to hand the
1178            // referee. The extra-shell trigger of #423 leaves it to the mesh
1179            // result; a skin within rounding is decided for it too (#400,
1180            // ADR 0102).
1181            if half_space_tool && rounding.is_none() {
1182                *reason = Some("the tool is a half-space clipped to the subject".to_owned());
1183                return Ok(None);
1184            }
1185            match crate::referee::boolean(
1186                graph,
1187                id,
1188                operator,
1189                options,
1190                (subject, built_tool),
1191                against,
1192                rounding,
1193            ) {
1194                Ok(built) => Ok::<_, GeomError>(Some(built)),
1195                Err(why) => {
1196                    *reason = Some(why);
1197                    Ok(None)
1198                }
1199            }
1200        };
1201        let mut reason = None;
1202        match cut {
1203            // A skin within rounding is decided by the exact referee, once,
1204            // at the root of the chain of clips it sits in: a chain defers
1205            // it, and one exact compile of the whole chain decides it
1206            // (#400, ADR 0102).
1207            Ok(MeshCut {
1208                mut built,
1209                skin: Some(skin),
1210                ..
1211            }) => {
1212                if clip_chain_continues(graph, regions, id, root) {
1213                    built.pending_skin = Some(skin);
1214                    return Ok((built, Decided::Mesh));
1215                }
1216                match referee(&mut reason, None, Some(skin.reach))? {
1217                    Some(decided) => Ok((decided, Decided::Referee)),
1218                    None => Err(refused_by_referee(skin.error, reason)),
1219                }
1220            }
1221            Ok(cut) if !cut.unsettled => Ok((cut.built, Decided::Mesh)),
1222            Ok(cut) => Ok(
1223                match referee(
1224                    &mut reason,
1225                    (operator != axiolid_core::BooleanOperator::Union).then(|| {
1226                        crate::referee::Against {
1227                            mesh: &cut.built.mesh,
1228                            reach: crate::snap::reach(
1229                                &subject.mesh,
1230                                tool,
1231                                options.tolerance(),
1232                                budget.placed,
1233                            ),
1234                        }
1235                    }),
1236                    None,
1237                )? {
1238                    Some(built) => (built, Decided::Referee),
1239                    None => (cut.built, Decided::Mesh),
1240                },
1241            ),
1242            // A pinch against the mesh a deferred skin left (#400) is decided
1243            // by the referee, at the rounding reach.
1244            Err(CutRefusal::Contact(error))
1245                if operator != axiolid_core::BooleanOperator::Union
1246                    && subject.pending_skin.is_some() =>
1247            {
1248                let reach =
1249                    crate::snap::reach(&subject.mesh, tool, options.tolerance(), budget.placed);
1250                match referee(&mut reason, None, Some(reach))? {
1251                    Some(decided) => Ok((decided, Decided::Referee)),
1252                    None => Err(refused_by_referee(error, reason)),
1253                }
1254            }
1255            // A pinched difference or an intersection has no mesh result to
1256            // hold a void: its refusal stands (ADR 0096, D7).
1257            Err(CutRefusal::Contact(error)) if operator != axiolid_core::BooleanOperator::Union => {
1258                Err(error)
1259            }
1260            Err(CutRefusal::Contact(error)) => match referee(&mut reason, None, None)? {
1261                Some(built) => Ok((built, Decided::Referee)),
1262                None => Err(refused_by_referee(error, reason)),
1263            },
1264            Err(CutRefusal::Other(error)) => Err(error),
1265        }
1266    }
1267
1268    /// The operand of a boolean as a frame sees it (#356): an `Instance`'s
1269    /// transform with its source as cached for it, or the frame a boolean
1270    /// was itself cut in with its unplaced result. `None` for any other
1271    /// node, and for an instance whose source is re-meshed for its
1272    /// placement's rounding ([`Self::placement_retry`]): the world frame
1273    /// handles that.
1274    fn frame_of<'c>(
1275        &self,
1276        graph: &GeometryGraph,
1277        id: NodeId,
1278        budget: Budget,
1279        cache: &'c Cache,
1280    ) -> GeomResult<Option<(Transform3, &'c Built)>> {
1281        let node = self.node(graph, id)?;
1282        if let GeometryNode::Instance(instance) = node {
1283            if self
1284                .placement_retry(graph, node, EvalKey::new(id, budget), cache, None)?
1285                .is_some()
1286            {
1287                return Ok(None);
1288            }
1289            let source_budget = instance_local_budget(instance.transform, budget, 0.0)?;
1290            let source = self.cached(cache, instance.source, source_budget)?;
1291            return Ok(Some((instance.transform, source)));
1292        }
1293        Ok(self
1294            .cached(cache, id, budget)?
1295            .local
1296            .as_deref()
1297            .map(|local| (local.transform, &local.built)))
1298    }
1299
1300    /// The boolean cut in the frame its operands share and placed by the
1301    /// subject's transform (#356): `None` when they share none, that is
1302    /// when either is not framed ([`Self::frame_of`]) or their linear parts
1303    /// differ in a bit ([`crate::frame::shared_linear`]; a union is then
1304    /// refereed, [`crate::referee`]), or
1305    /// when the tool is a half-space clip, or when the instance chains
1306    /// beneath the frames compose to linear parts that differ in a bit
1307    /// (the sources were themselves placed by differently composed turns):
1308    /// the world path re-reads it and the referee decides. The tool is moved by its offset
1309    /// in the subject's frame, which is exactly zero when the translations
1310    /// are equal.
1311    ///
1312    /// The result keeps its unplaced form in [`Built::local`], so an
1313    /// enclosing boolean in the same frame cuts against it: a chain of
1314    /// corbels on a column stays in the column's frame.
1315    ///
1316    /// Its operands are the caller's, already refused if they are surface
1317    /// models and not empty (#326).
1318    fn boolean_in_shared_frame(
1319        &self,
1320        graph: &GeometryGraph,
1321        (left, right): (NodeId, NodeId),
1322        operator: axiolid_core::BooleanOperator,
1323        options: &ExecutionOptions,
1324        budget: Budget,
1325        (cache, regions): (&Cache, &Regions<'_>),
1326    ) -> GeomResult<Option<Built>> {
1327        if clip_of(graph, regions, (left, right), operator)?.is_some() {
1328            return Ok(None);
1329        }
1330        let Some((subject_frame, subject)) = self.frame_of(graph, left, budget, cache)? else {
1331            return Ok(None);
1332        };
1333        let Some((tool_frame, tool)) = self.frame_of(graph, right, budget, cache)? else {
1334            return Ok(None);
1335        };
1336        if !crate::frame::shared_linear(subject_frame, tool_frame) {
1337            return Ok(None);
1338        }
1339        // The outer frames agree, but a source placed beneath them by turns
1340        // computed differently leaves faces that nearly share a plane in
1341        // the frame as well: the world path re-reads and referees it (#391).
1342        if let (Some(a), Some(b)) = (
1343            chain_linear(graph, left, subject_frame),
1344            chain_linear(graph, right, tool_frame),
1345        ) {
1346            if !crate::frame::shared_linear(a, b) {
1347                return Ok(None);
1348            }
1349        }
1350        let offset = crate::frame::relative_offset(subject_frame, tool_frame).ok_or_else(|| {
1351            GeomError::InvalidInput("operand translations have no finite offset".into())
1352        })?;
1353        let shifted;
1354        let tool = if offset == Vec3::ZERO {
1355            tool
1356        } else {
1357            let shift = Transform3::from_translation(offset);
1358            let rounding = crate::deviation::placement_rounding(&tool.mesh.positions, shift);
1359            shifted = channels::transform(tool, shift, rounding);
1360            &shifted
1361        };
1362        let local_budget = instance_local_budget(subject_frame, budget, 0.0)?;
1363        let tool_budget = instance_local_budget(tool_frame, budget, 0.0)?;
1364        let local_options = local_options(options, local_budget)?;
1365        let local = self
1366            .boolean_of(
1367                subject,
1368                (&tool.mesh, Some(&tool.fates), Some(&tool.deviation)),
1369                operator,
1370                &local_options,
1371                local_budget.placed.max(tool_budget.placed),
1372            )
1373            .map_err(CutRefusal::into_error)?;
1374        // A skin within rounding is the world cut's to decide (#400).
1375        if let Some(skin) = local.skin {
1376            return Err(skin.error);
1377        }
1378        let local = local.built;
1379        let rounding = placed_rounding(&local.mesh, subject_frame, budget.chord)?;
1380        let mut placed = channels::transform(&local, subject_frame, rounding);
1381        // Defence in depth: the local tolerance is the world one over the
1382        // frame's stretch, so a union that still leaves bodies within the
1383        // world tolerance once placed is refused here, and `build_boolean`
1384        // cuts it in the world frame instead.
1385        if operator == axiolid_core::BooleanOperator::Union {
1386            let eps = options.tolerance().linear();
1387            if let Some(found) = crate::contact::unmerged(&placed.mesh, eps) {
1388                return Err(crate::contact::refusal(&found, eps));
1389            }
1390        }
1391        placed.local = Some(Box::new(channels::LocalBoolean {
1392            transform: subject_frame,
1393            built: local,
1394        }));
1395        Ok(Some(placed))
1396    }
1397
1398    /// A difference or intersection with a half-space clip, cut in the frame
1399    /// its subject is placed by (#400): `None` when the operator does not
1400    /// clip, the tool is no clip, the subject is not framed
1401    /// ([`Self::frame_of`]) or not a similarity, or the tool's placement has
1402    /// no finite form in that frame ([`crate::frame::relative`]).
1403    ///
1404    /// Unlike [`Self::boolean_in_shared_frame`] the linear parts need not be
1405    /// bit-equal (ADR 0096, D3): a clip has no coordinates of its own to
1406    /// keep. The tool is rebuilt against the subject's local mesh, where the
1407    /// subject's faces are exactly axis-aligned; where the linear parts
1408    /// differ, its faces lie a residue off them and the snap lands them. The
1409    /// result is placed by the frame and keeps its unplaced form in
1410    /// [`Built::local`]. A region (#326) is placed leaf by leaf, each
1411    /// relative to the frame; one with a leaf that has no finite form there,
1412    /// or that leaves nothing of the subject, is the world cut's. The subject
1413    /// is the caller's, already refused if it is a surface model and not
1414    /// empty.
1415    fn clip_in_subject_frame(
1416        &self,
1417        graph: &GeometryGraph,
1418        (left, right): (NodeId, NodeId),
1419        operator: axiolid_core::BooleanOperator,
1420        options: &ExecutionOptions,
1421        budget: Budget,
1422        (cache, regions): (&Cache, &Regions<'_>),
1423    ) -> GeomResult<Option<Built>> {
1424        let Some(clip) = clip_of(graph, regions, (left, right), operator)? else {
1425            return Ok(None);
1426        };
1427        let Some((frame, subject)) = self.frame_of(graph, left, budget, cache)? else {
1428            return Ok(None);
1429        };
1430        // Each leaf of a region (#326) is placed in the subject's frame; one
1431        // that has no finite form there leaves the world cut to decide.
1432        let mut leaves = Vec::with_capacity(clip.leaves.len());
1433        for &(leaf, chain) in &clip.leaves {
1434            let Some(relative) = crate::frame::relative(frame, chain) else {
1435                return Ok(None);
1436            };
1437            leaves.push((leaf, relative));
1438        }
1439        let local_budget = instance_local_budget(frame, budget, 0.0)?;
1440        let local_options = local_options(options, local_budget)?;
1441        // A region that leaves nothing of the subject is an empty tool: the
1442        // world cut decides it by an identity.
1443        let Some(tool) = self.region_tool_mesh(
1444            graph,
1445            &leaves,
1446            &subject.mesh,
1447            &local_options,
1448            local_budget.placed,
1449        )?
1450        else {
1451            return Ok(None);
1452        };
1453        let local = self
1454            .boolean_of(
1455                subject,
1456                (
1457                    &tool.mesh,
1458                    None,
1459                    (leaves.len() > 1).then_some(&tool.deviation),
1460                ),
1461                operator,
1462                &local_options,
1463                local_budget.placed,
1464            )
1465            .map_err(CutRefusal::into_error)?;
1466        if let Some(skin) = local.skin {
1467            return Err(skin.error);
1468        }
1469        let local = local.built;
1470        let rounding = placed_rounding(&local.mesh, frame, budget.chord)?;
1471        let mut placed = channels::transform(&local, frame, rounding);
1472        placed.local = Some(Box::new(channels::LocalBoolean {
1473            transform: frame,
1474            built: local,
1475        }));
1476        Ok(Some(placed))
1477    }
1478
1479    /// The cut of two operands in one frame: settled onto each other
1480    /// ([`crate::snap`]), cut, and reported. `placed` is the scale the
1481    /// result is placed at (#312).
1482    fn boolean_of(
1483        &self,
1484        subject: &Built,
1485        (tool, tool_fates, tool_deviation): (
1486            &TriMesh,
1487            Option<&channels::Fates>,
1488            Option<&crate::deviation::Deviation>,
1489        ),
1490        operator: axiolid_core::BooleanOperator,
1491        options: &ExecutionOptions,
1492        placed: Scalar,
1493    ) -> Result<MeshCut, CutRefusal> {
1494        // Faces of the two operands a rounding residue apart are moved
1495        // together first, so a skin or sliver that thin is not cut as one
1496        // (#276, narrowed to rounding in #291; `crate::snap`): the rounding
1497        // of the operands' coordinates, or of the coordinates the result is
1498        // placed at, whichever is coarser (#312).
1499        // A union reads contact at the tolerance (#357), the others at the
1500        // rounding.
1501        let reach = if operator == axiolid_core::BooleanOperator::Union {
1502            crate::snap::contact_reach(options.tolerance())
1503        } else {
1504            crate::snap::reach(&subject.mesh, tool, options.tolerance(), placed)
1505        };
1506        let settled = crate::snap::settle(
1507            &subject.mesh,
1508            tool,
1509            reach,
1510            operator != axiolid_core::BooleanOperator::Union,
1511        );
1512        let snapped = if settled.moved > 0.0 {
1513            // A snap never turns a working boolean into a refusal (#291):
1514            // where the snapped operands are refused, or their result touches
1515            // itself or holds a skin, they are cut as given below, and no
1516            // snap is reported.
1517            self.cut(
1518                settled.subject.as_ref().unwrap_or(&subject.mesh),
1519                settled.tool.as_ref().unwrap_or(tool),
1520                operator,
1521                options,
1522                reach,
1523            )
1524            .ok()
1525            .filter(|(_, skin)| skin.is_none())
1526        } else {
1527            None
1528        };
1529        let ((outcome, skin), moved) = match snapped {
1530            Some(cut) => (cut, settled.moved),
1531            None => (
1532                self.cut(&subject.mesh, tool, operator, options, reach)?,
1533                0.0,
1534            ),
1535        };
1536        // Faces of the pair actually cut that nearly share a plane, a
1537        // residue off exact contact, are not decided by a boolean without a
1538        // tolerance whatever its result looks like (#391): the exact
1539        // compiler referees them.
1540        let snapped_pair = moved > 0.0;
1541        let cut_subject = settled
1542            .subject
1543            .as_ref()
1544            .filter(|_| snapped_pair)
1545            .unwrap_or(&subject.mesh);
1546        let cut_tool = settled
1547            .tool
1548            .as_ref()
1549            .filter(|_| snapped_pair)
1550            .unwrap_or(tool);
1551        let eps = options.tolerance().linear();
1552        // Near contact calls the referee. A union is decided by the exact
1553        // compiler then; a difference or an intersection only where its mesh
1554        // holds a shell the exact result lacks (#423), as a skin thinner
1555        // than the tolerance stays as authored (ADR 0096, D7, #291).
1556        // The cheap conditions come first: the exact compile of the whole
1557        // subtree is dear, and a mesh of one component holds no void.
1558        let unsettled = eps > 0.0
1559            && (operator == axiolid_core::BooleanOperator::Union
1560                || crate::deviation::geometric_components(&outcome.mesh) > 1)
1561            && axiolid_inspect::near_coplanar(cut_subject, cut_tool, eps).is_some();
1562        let mut built = channels::after_boolean(
1563            outcome.mesh,
1564            &subject.fates,
1565            tool_fates,
1566            outcome.evidence.attribute_fates,
1567        );
1568        built.deviation.carry_perturbations(&subject.deviation);
1569        if let Some(tool) = tool_deviation {
1570            built.deviation.carry_perturbations(tool);
1571        }
1572        if moved > 0.0 {
1573            built.deviation.add(
1574                crate::deviation::DeviationPath::Boolean,
1575                crate::deviation::SNAPPED_OPERANDS,
1576                crate::deviation::DeviationBound::Certified(moved),
1577            );
1578        }
1579        Ok(MeshCut {
1580            built,
1581            unsettled,
1582            skin,
1583        })
1584    }
1585
1586    /// The mesh boolean of `subject` and `tool`, refused where its result
1587    /// touches itself (#194), and, for a union, where it holds two bodies
1588    /// within the linear tolerance of each other (#356, #357): the operands
1589    /// meet there and the boolean, which has no tolerance, left them apart.
1590    /// That union is refused by name, [`crate::UNMERGED_UNION_CONTACT`]. A
1591    /// difference or an intersection is refused where its result holds a
1592    /// skin thinner than the rounding `reach`, [`crate::SKIN_WITHIN_ROUNDING`]
1593    /// (#400).
1594    fn cut(
1595        &self,
1596        subject: &TriMesh,
1597        tool: &TriMesh,
1598        operator: axiolid_core::BooleanOperator,
1599        options: &ExecutionOptions,
1600        reach: Scalar,
1601    ) -> Result<
1602        (
1603            axiolid_mesh_boolean_contract::BooleanOutcome,
1604            Option<PendingSkin>,
1605        ),
1606        CutRefusal,
1607    > {
1608        let outcome = self
1609            .boolean
1610            .boolean(subject, tool, operator, options)
1611            .map_err(CutRefusal::Other)?;
1612        if let Some(pinch) = crate::pinch::find(&outcome.mesh) {
1613            return Err(CutRefusal::Contact(GeomError::Degenerate(match pinch {
1614                crate::pinch::Pinch::Edge { from, to } => format!(
1615                    "{operator:?} result touches itself along the edge {from:?} to {to:?}: \
1616                     the operands meet tangentially there (a void tangent to its host's face), \
1617                     leaving no material between two faces"
1618                ),
1619                crate::pinch::Pinch::Vertex { at } => format!(
1620                    "{operator:?} result touches itself at the point {at:?}: the operands \
1621                     meet tangentially there, leaving no material between two faces"
1622                ),
1623            })));
1624        }
1625        if operator == axiolid_core::BooleanOperator::Union {
1626            let eps = options.tolerance().linear();
1627            if let Some(found) = crate::contact::unmerged(&outcome.mesh, eps) {
1628                return Err(CutRefusal::Contact(crate::contact::refusal(&found, eps)));
1629            }
1630        }
1631        // A difference or an intersection leaves no skin its coordinates
1632        // cannot carry (#400): two faces facing each other within the
1633        // rounding are residue, and a Solid with them is not decided here.
1634        let skin = if matches!(
1635            operator,
1636            axiolid_core::BooleanOperator::Difference | axiolid_core::BooleanOperator::Intersection
1637        ) {
1638            axiolid_inspect::thin_sheet(&outcome.mesh, reach).map(|found| PendingSkin {
1639                error: crate::skin::refusal(&found, &outcome.mesh, reach, operator),
1640                reach,
1641            })
1642        } else {
1643            None
1644        };
1645        Ok((outcome, skin))
1646    }
1647
1648    /// A non-boolean solid and its deviation. A swept disk along a smooth
1649    /// directrix places its stations by a second-order estimate, so its
1650    /// certified bound can land a little over the budget: it is rebuilt
1651    /// with the construction's chord shrunk by the overshoot, and
1652    /// certified again against the requested budget, at most
1653    /// [`REFINE_ROUNDS`] times. A bound still over it is reported as it
1654    /// is, never trimmed (#252).
1655    fn build_solid_within(
1656        &self,
1657        graph: &GeometryGraph,
1658        operation: &SolidOperation,
1659        options: &ExecutionOptions,
1660    ) -> GeomResult<(TriMesh, crate::deviation::Deviation)> {
1661        // A run of sections at stations is bounded as it is sampled (#247).
1662        if let Some(run) = crate::deviation::stationed_spine(graph, operation, options) {
1663            return run;
1664        }
1665        let mut mesh = self.build_solid(graph, operation, options)?;
1666        let mut deviation = crate::deviation::of_solid(graph, operation, options, options, &mesh)?;
1667        if !matches!(operation, SolidOperation::SweptDisk { .. }) {
1668            return Ok((mesh, deviation));
1669        }
1670        let budget = chord_error(options);
1671        let mut chord = budget;
1672        for _ in 0..REFINE_ROUNDS {
1673            let Some(over) = deviation.certified_over(budget) else {
1674                break;
1675            };
1676            chord *= REFINE_MARGIN * budget / over;
1677            let Some(finer) = options.clone().with_chord_error(chord) else {
1678                break;
1679            };
1680            let refined = self.build_solid(graph, operation, &finer)?;
1681            let certified =
1682                crate::deviation::of_solid(graph, operation, options, &finer, &refined)?;
1683            match certified.certified_over(budget) {
1684                // No better: the overshoot is not the stations' to remove.
1685                Some(next) if next >= over => break,
1686                _ => (mesh, deviation) = (refined, certified),
1687            }
1688        }
1689        Ok((mesh, deviation))
1690    }
1691
1692    /// Every non-boolean solid family; unsupported ones are explicitly
1693    /// refused. Booleans go through [`Self::build_boolean`], which needs the
1694    /// operands' cached fates.
1695    fn build_solid(
1696        &self,
1697        graph: &GeometryGraph,
1698        operation: &SolidOperation,
1699        options: &ExecutionOptions,
1700    ) -> GeomResult<TriMesh> {
1701        match operation {
1702            SolidOperation::Extrusion {
1703                profile,
1704                direction,
1705                depth,
1706            } => {
1707                let node = self.node(graph, *profile)?;
1708                let GeometryNode::Profile(shape) = node else {
1709                    return Err(GeomError::InvalidInput(format!(
1710                        "extrusion profile {profile:?} is not a Profile node"
1711                    )));
1712                };
1713                let rings = profile_rings(shape, chord_error(options), options.tolerance())?;
1714                extrude_profile(&rings, *direction, *depth, options.tolerance())
1715            }
1716            SolidOperation::Revolution {
1717                profile,
1718                axis_origin,
1719                axis_direction,
1720                angle,
1721            } => {
1722                let node = self.node(graph, *profile)?;
1723                let GeometryNode::Profile(shape) = node else {
1724                    return Err(GeomError::InvalidInput(format!(
1725                        "revolution profile {profile:?} is not a Profile node"
1726                    )));
1727                };
1728                // Doubly curved (#231): half the budget across the profile,
1729                // half round the axis, so the two chordings add to at most
1730                // the whole (see `axiolid_construct::revolve`).
1731                let half = 0.5 * chord_error(options);
1732                let rings = profile_rings(shape, half, options.tolerance())?;
1733                axiolid_construct::revolve::revolve(
1734                    &rings,
1735                    *axis_origin,
1736                    *axis_direction,
1737                    *angle,
1738                    half_tolerance(options)?,
1739                )
1740            }
1741            SolidOperation::TaperedExtrusion {
1742                start_profile,
1743                end_profile,
1744                direction,
1745                depth,
1746            } => {
1747                let chord = chord_error(options);
1748                let a =
1749                    self.rings_of(graph, *start_profile, chord, options, "taper start profile")?;
1750                let b = self.rings_of(graph, *end_profile, chord, options, "taper end profile")?;
1751                axiolid_construct::sweep::tapered_extrude(&a, &b, *direction, *depth)
1752            }
1753            SolidOperation::TaperedRevolution {
1754                start_profile,
1755                end_profile,
1756                axis_origin,
1757                axis_direction,
1758                angle,
1759            } => {
1760                let half = 0.5 * chord_error(options);
1761                let a =
1762                    self.rings_of(graph, *start_profile, half, options, "taper start profile")?;
1763                let b = self.rings_of(graph, *end_profile, half, options, "taper end profile")?;
1764                axiolid_construct::sweep::tapered_revolve(
1765                    &a,
1766                    &b,
1767                    *axis_origin,
1768                    *axis_direction,
1769                    *angle,
1770                    half_tolerance(options)?,
1771                )
1772            }
1773            // Sweeps along a directrix are doubly curved too: the
1774            // construction splits the budget between the section and the
1775            // directrix and refines the directrix until its walls fit.
1776            // Lines, polylines and composites of lines and arcs are swept
1777            // piece by piece, corners mitred (#245, beside an arc #248) or
1778            // filleted (#232); a closed polyline is swept as a loop (#248).
1779            SolidOperation::SweptDisk {
1780                directrix,
1781                radius,
1782                inner_radius,
1783                parameter_range,
1784                fillet_radius,
1785            } => match crate::directrix::pieces(graph, *directrix, *parameter_range, options)? {
1786                Some(_) if fillet_radius.is_some() && parameter_range.is_some() => {
1787                    Err(GeomError::UnsupportedInput {
1788                        backend: self.descriptor().id,
1789                        operation: axiolid_contracts::Operation::Sweep,
1790                        input: "a parameter range on a swept disk with a fillet radius",
1791                    })
1792                }
1793                Some(path) if path.closed => axiolid_construct::pipe::swept_disk_along_loop(
1794                    &path.pieces,
1795                    *radius,
1796                    *inner_radius,
1797                    *fillet_radius,
1798                    chord_error(options),
1799                ),
1800                Some(path) => axiolid_construct::pipe::swept_disk_along_pieces(
1801                    &path.pieces,
1802                    *radius,
1803                    *inner_radius,
1804                    *fillet_radius,
1805                    chord_error(options),
1806                ),
1807                None => {
1808                    // The conic path checks only the radii; a disk wider
1809                    // than a lone arc's bend would mesh into a tube that
1810                    // folds through the bend's axis (#122).
1811                    if let Some(path) = crate::directrix::exact_pieces(
1812                        graph,
1813                        *directrix,
1814                        *parameter_range,
1815                        options,
1816                    )? {
1817                        if let [arc @ axiolid_construct::pipe::PathPiece::Arc { .. }] =
1818                            path.pieces.as_slice()
1819                        {
1820                            axiolid_construct::pipe::check_disk_along(arc, *radius)?;
1821                        }
1822                    }
1823                    axiolid_construct::sweep::swept_disk_within(
1824                        |chord| {
1825                            self.directrix_sampled(
1826                                graph,
1827                                *directrix,
1828                                *parameter_range,
1829                                options,
1830                                chord,
1831                            )
1832                        },
1833                        *radius,
1834                        *inner_radius,
1835                        *fillet_radius,
1836                        chord_error(options),
1837                    )
1838                }
1839            },
1840            SolidOperation::FixedReferenceSweep {
1841                profile,
1842                directrix,
1843                reference_direction,
1844                parameter_range,
1845            } => {
1846                let half = 0.5 * chord_error(options);
1847                let rings = self.rings_of(graph, *profile, half, options, "sweep profile")?;
1848                // A tangent-continuous chain of segments and arcs is swept
1849                // piece by piece with a proven bound, a corner refused (#122);
1850                // an arc oblique to the reference, or any other family, is
1851                // swept as sampled.
1852                if let Some(path) = crate::directrix::fixed_reference_pieces(
1853                    graph,
1854                    *directrix,
1855                    *parameter_range,
1856                    options,
1857                )? {
1858                    let plan = axiolid_construct::pipe::fixed_reference_plan(
1859                        &path.pieces,
1860                        path.closed,
1861                        *reference_direction,
1862                        axiolid_construct::pipe::ring_reach(&rings),
1863                        chord_error(options),
1864                        options.tolerance(),
1865                    )?;
1866                    if let axiolid_construct::pipe::FixedReferencePlan::Untwisted(chain) = plan {
1867                        return axiolid_construct::pipe::fixed_reference_along_pieces(
1868                            &rings, &chain,
1869                        );
1870                    }
1871                }
1872                axiolid_construct::sweep::fixed_reference_sweep_within(
1873                    &rings,
1874                    |chord| {
1875                        self.directrix_sampled(graph, *directrix, *parameter_range, options, chord)
1876                    },
1877                    *reference_direction,
1878                    half,
1879                )
1880            }
1881            SolidOperation::SurfaceCurveSweep {
1882                profile,
1883                directrix,
1884                reference_surface,
1885                parameter_range,
1886            } => {
1887                let half = 0.5 * chord_error(options);
1888                let rings = self.rings_of(
1889                    graph,
1890                    *profile,
1891                    half,
1892                    options,
1893                    "surface curve sweep profile",
1894                )?;
1895                axiolid_construct::sweep::surface_curve_sweep_within(
1896                    &rings,
1897                    |chord| {
1898                        let path = self.directrix_sampled(
1899                            graph,
1900                            *directrix,
1901                            *parameter_range,
1902                            options,
1903                            chord,
1904                        )?;
1905                        let normals =
1906                            self.surface_normals(graph, *reference_surface, &path.points, options)?;
1907                        Ok((path, normals))
1908                    },
1909                    half,
1910                )
1911            }
1912            SolidOperation::SectionedSpine { spine, sections } => {
1913                let path = self.directrix_points(graph, *spine, None, options)?;
1914                if sections.len() != path.len() {
1915                    return Err(GeomError::InvalidInput(format!(
1916                        "a sectioned spine needs one section per spine point: {} sections, {} points",
1917                        sections.len(),
1918                        path.len()
1919                    )));
1920                }
1921                let mut placed = Vec::with_capacity(sections.len());
1922                for (section, origin) in sections.iter().zip(&path) {
1923                    let rings = self.rings_of(
1924                        graph,
1925                        section.profile,
1926                        chord_error(options),
1927                        options,
1928                        "spine section profile",
1929                    )?;
1930                    // The section's own placement positions its profile;
1931                    // the spine point supplies the station origin.
1932                    let pts = rings
1933                        .outer
1934                        .iter()
1935                        .chain(rings.holes.iter().flatten())
1936                        .map(|p| {
1937                            section
1938                                .placement
1939                                .transform_point3(Point3::new(p.x, p.y, 0.0))
1940                                + *origin
1941                        })
1942                        .collect();
1943                    placed.push((rings, pts));
1944                }
1945                axiolid_construct::sweep::sectioned_spine(&placed)
1946            }
1947            // Sections at stations along the directrix (#241).
1948            SolidOperation::StationedSpine {
1949                directrix,
1950                sections,
1951                frame,
1952            } => {
1953                let sections: Vec<_> = sections.iter().copied().map(Into::into).collect();
1954                crate::station::stationed_spine(graph, *directrix, &sections, *frame, options)
1955            }
1956            // The general form: oriented sections, matched by tag (#246).
1957            SolidOperation::SectionsAtStations {
1958                directrix,
1959                sections,
1960                frame,
1961            } => crate::station::stationed_spine(graph, *directrix, sections, *frame, options),
1962            // Unbounded on its kept side, a bounded half-space has a finite
1963            // stand-in only against a subject (#325): a slab sized by its
1964            // boundary alone ends wherever it ends, and a subject reaching
1965            // further would be cut too little, silently. Its booleans are
1966            // routed to `build_boolean`, which sizes it; here it has none.
1967            SolidOperation::BoundedHalfSpace { .. }
1968            | SolidOperation::BoundedHalfSpaceAlongZ { .. } => Err(GeomError::UnsupportedInput {
1969                backend: self.descriptor().id,
1970                operation: Operation::GraphCompilation,
1971                input: "bounded half-space without a subject to size it by: it is unbounded \
1972                        on its kept side, and is compiled only as the tool of a difference \
1973                        or an intersection",
1974            }),
1975            // Routed to `build_boolean` by `build`; unreachable here.
1976            SolidOperation::Boolean { .. } => Err(GeomError::InvalidInput(
1977                "boolean must be built through build_boolean".into(),
1978            )),
1979            // Naming the capability lets a caller register a provider for it
1980            // rather than guess. `Unsupported` names only the operation, which
1981            // collapses revolution, swept disk, fixed-reference sweep and the
1982            // rest into one indistinguishable answer -- a caller learns that
1983            // "a sweep" failed but not WHICH provider is missing, so it cannot
1984            // act. `UnsupportedInput` carries the family, matching what the
1985            // exact path already reports.
1986            other => Err(GeomError::UnsupportedInput {
1987                backend: self.descriptor().id,
1988                operation: Operation::Sweep,
1989                input: crate::exact::solid_operation_family(other),
1990            }),
1991        }
1992    }
1993}
1994
1995/// The cut of a boolean with an empty operand (#326): the subject or the
1996/// tool as it is, or nothing, by the identity table. Nothing is cut, so no
1997/// snap, skin or contact is read; a skin the subject carries stays pending,
1998/// for the root of its chain to decide.
1999fn identity_cut(
2000    identity: crate::empty::Identity,
2001    subject: &Built,
2002    tool: Option<&Built>,
2003) -> GeomResult<MeshCut> {
2004    use crate::empty::Identity;
2005    let built = match identity {
2006        Identity::Subject => {
2007            let mut built = subject.clone();
2008            let skin = built.pending_skin.take();
2009            return Ok(MeshCut {
2010                built,
2011                unsettled: false,
2012                skin,
2013            });
2014        }
2015        Identity::Tool => tool
2016            .ok_or_else(|| {
2017                GeomError::InvalidInput("boolean identity names a tool that was not built".into())
2018            })?
2019            .clone(),
2020        Identity::Empty => channels::after_boolean(
2021            TriMesh::default(),
2022            &subject.fates,
2023            tool.map(|built| &built.fates),
2024            Vec::new(),
2025        ),
2026    };
2027    Ok(MeshCut {
2028        built,
2029        unsettled: false,
2030        skin: None,
2031    })
2032}
2033
2034/// `error` with the reason the exact referee refused to decide it.
2035fn refused_by_referee(error: GeomError, reason: Option<String>) -> GeomError {
2036    match error {
2037        GeomError::Degenerate(text) => GeomError::Degenerate(format!(
2038            "{text}; the exact referee refused: {}",
2039            reason.unwrap_or_default()
2040        )),
2041        other => other,
2042    }
2043}
2044
2045/// Whether the difference or intersection `id` is only the subject of
2046/// further differences and intersections, and no root asked for: a chain of
2047/// clips goes on past it, so a skin within rounding in its result waits for
2048/// the root of the chain (#400, ADR 0102). A node any other node reads, as a
2049/// tool, in a union or an instance, ends its chain.
2050fn clip_chain_continues(
2051    graph: &GeometryGraph,
2052    regions: &Regions<'_>,
2053    id: NodeId,
2054    root: NodeId,
2055) -> bool {
2056    if id == root || graph.roots().contains(&id) {
2057        return false;
2058    }
2059    let mut continues = false;
2060    for (_, node) in graph.iter() {
2061        if !node.references().contains(&id) {
2062            continue;
2063        }
2064        match node {
2065            GeometryNode::SolidOperation(SolidOperation::Boolean {
2066                left,
2067                right,
2068                operator:
2069                    axiolid_core::BooleanOperator::Difference
2070                    | axiolid_core::BooleanOperator::Intersection,
2071            }) if *left == id && *right != id => continues = true,
2072            // The subject of an intersection with a region on its left (#326).
2073            GeometryNode::SolidOperation(SolidOperation::Boolean {
2074                left,
2075                right,
2076                operator: axiolid_core::BooleanOperator::Intersection,
2077            }) if *right == id && regions.has(*left) => {
2078                continues = true;
2079            }
2080            _ => return false,
2081        }
2082    }
2083    continues
2084}
2085
2086/// `options` read in the frame a boolean is cut in (#356, #400): its
2087/// tolerance and chord budget are the frame's `local_budget`.
2088fn local_options(options: &ExecutionOptions, local_budget: Budget) -> GeomResult<ExecutionOptions> {
2089    options
2090        .clone()
2091        .with_tolerance(local_budget.tolerance)
2092        .with_chord_error(local_budget.chord)
2093        .ok_or_else(|| {
2094            GeomError::InvalidInput(format!(
2095                "chord budget {} is not positive and finite",
2096                local_budget.chord
2097            ))
2098        })
2099}
2100
2101/// Convert world-space budgets into conservative instance-local space.
2102///
2103/// The tolerance and the chord budget are both lengths, so both shrink by
2104/// the transform's stretch bound ([`crate::deviation::stretch`], the same
2105/// factor the instance's deviation report grows by): a chord within `c` of
2106/// the curve in local space is within `stretch * c` of it in world space.
2107/// The chord budget is shrunk until that product, rounded, is within the
2108/// world budget ([`crate::deviation::shrunk_budget`]), so a source meshed
2109/// to its local budget reports at most the world one (#258). The chord
2110/// budget also leaves room for the rounding of placing the source's
2111/// vertices ([`placement_room`], at least `room`), which the instance's
2112/// bound adds (#295), and a budget at or below the floor at the
2113/// translation's scale is refused. The placed coordinate scale grows by
2114/// the translation, in the source's units ([`crate::snap::placed_scale`],
2115/// #312).
2116fn instance_local_budget(
2117    transform: Transform3,
2118    budget: Budget,
2119    room: Scalar,
2120) -> GeomResult<Budget> {
2121    let (tolerance, stretch) = instance_local_tolerance(transform, budget.tolerance)?;
2122    let room = placement_room(transform, budget.chord, room)?;
2123    Ok(Budget {
2124        tolerance,
2125        chord: crate::deviation::shrunk_budget_within(budget.chord, stretch, room),
2126        placed: crate::snap::placed_scale(transform, budget.placed).ok_or_else(|| {
2127            GeomError::InvalidInput("instance translation has no finite magnitude".into())
2128        })?,
2129    })
2130}
2131
2132/// The share of what the floor leaves of a chord budget an instance's
2133/// source first leaves for the rounding of its own coordinates once placed
2134/// (#295): `2^-20`, enough for a source within about `10^9` budgets of the
2135/// origin, so most sources are meshed once.
2136const ROOM_SHARE: Scalar = 1.0 / 1_048_576.0;
2137
2138/// The room an instance's source leaves in `chord` for its placement's
2139/// rounding (#295): the floor every placement by `transform` reaches, the
2140/// rounding at the origin ([`crate::deviation::placement_rounding`]), plus
2141/// [`ROOM_SHARE`] of what it leaves, and at least `shown`, the rounding a
2142/// first mesh of the source showed. A `chord` at or below the floor is
2143/// refused by name.
2144pub(crate) fn placement_room(
2145    transform: Transform3,
2146    chord: Scalar,
2147    shown: Scalar,
2148) -> GeomResult<Scalar> {
2149    let floor = crate::deviation::placement_rounding(&[Point3::ZERO], transform);
2150    refuse_below_rounding(chord, floor)?;
2151    Ok((floor + (chord - floor) * ROOM_SHARE).max(shown))
2152}
2153
2154/// How far a source lies from its exact placement at a station whose frame
2155/// has a certified error (#405): the bound's displacement of the source's
2156/// farthest vertex, `point_error + axis_error R`, `R` the largest `|x| +
2157/// |y| + |z|` of its vertices, which covers every point of the mesh (`|q|_1`
2158/// is convex). `None` for any other accuracy, or a term that is not finite.
2159fn frame_term(accuracy: Option<PlacementAccuracy>, source: &TriMesh) -> Option<Scalar> {
2160    let Some(PlacementAccuracy::Certified(bound)) = accuracy else {
2161        return None;
2162    };
2163    let reach = source
2164        .positions
2165        .iter()
2166        .map(|p| p.x.abs() + p.y.abs() + p.z.abs())
2167        .fold(0.0, Scalar::max)
2168        * (1.0 + 1e-12);
2169    let frame = bound.displacement(Point3::new(reach, 0.0, 0.0));
2170    frame.is_finite().then_some(frame)
2171}
2172
2173/// The rounding of placing `source`'s vertices with `transform` (#295),
2174/// refused by name when it reaches the chord budget: no mesh placed there
2175/// is within it.
2176fn placed_rounding(source: &TriMesh, transform: Transform3, chord: Scalar) -> GeomResult<Scalar> {
2177    let rounding = crate::deviation::placement_rounding(&source.positions, transform);
2178    refuse_below_rounding(chord, rounding)?;
2179    Ok(rounding)
2180}
2181
2182/// [`crate::deviation::BUDGET_BELOW_PLACEMENT_ROUNDING`] unless `rounding`
2183/// is below `chord`.
2184pub(crate) fn refuse_below_rounding(chord: Scalar, rounding: Scalar) -> GeomResult<()> {
2185    if rounding < chord {
2186        return Ok(());
2187    }
2188    Err(GeomError::UnsupportedInput {
2189        backend: BackendId::new("scalar-compile"),
2190        operation: Operation::GraphCompilation,
2191        input: crate::deviation::BUDGET_BELOW_PLACEMENT_ROUNDING,
2192    })
2193}
2194
2195/// The source and the transform of a node that places one: an `Instance`,
2196/// or an `InstanceAtStation` with its placement resolved (#264). `None` for
2197/// any other node.
2198fn placed_source(
2199    graph: &GeometryGraph,
2200    node: &GeometryNode,
2201    id: NodeId,
2202) -> GeomResult<Option<(NodeId, Transform3)>> {
2203    Ok(match node {
2204        GeometryNode::Instance(instance) => Some((instance.source, instance.transform)),
2205        GeometryNode::InstanceAtStation(placed) => Some((
2206            placed.source,
2207            crate::station::placement(graph, id)?.transform,
2208        )),
2209        _ => None,
2210    })
2211}
2212
2213pub(crate) fn instance_local_tolerance(
2214    transform: Transform3,
2215    tolerance: Tolerance,
2216) -> GeomResult<(Tolerance, Scalar)> {
2217    let m = transform.matrix3;
2218    let max_scale = m
2219        .x_axis
2220        .length()
2221        .max(m.y_axis.length())
2222        .max(m.z_axis.length());
2223    if !max_scale.is_finite() || max_scale == 0.0 {
2224        return Err(GeomError::InvalidInput(
2225            "instance transform has no finite scale".into(),
2226        ));
2227    }
2228    let stretch = crate::deviation::stretch(transform);
2229    if !stretch.is_finite() {
2230        return Err(GeomError::InvalidInput(
2231            "instance transform scale overflow".into(),
2232        ));
2233    }
2234    Tolerance::new(tolerance.linear() / stretch, tolerance.angular())
2235        .map(|local| (local, stretch))
2236        .map_err(|error| GeomError::InvalidInput(error.to_string()))
2237}
2238
2239/// How far a chord may sit from the curve it replaces (#165).
2240///
2241/// The caller's `ExecutionOptions::with_chord_error`, else the linear
2242/// tolerance. Kept separate from the tolerance because the tolerance is a
2243/// coincidence test: at `Tolerance::MILLIMETRE` it leaves a 5 mm arc four
2244/// chords per half turn, percent-level area error on a slot or a gutter.
2245/// Rebuilds a certified sweep may take to bring its bound within budget.
2246const REFINE_ROUNDS: usize = 3;
2247
2248/// Each rebuild's chord is the last one shrunk by the overshoot and this
2249/// margin, so one round usually settles it.
2250const REFINE_MARGIN: Scalar = 0.9;
2251
2252pub(crate) fn chord_error(options: &ExecutionOptions) -> Scalar {
2253    options
2254        .chord_error()
2255        .unwrap_or_else(|| options.tolerance().linear())
2256}
2257
2258/// The tolerance with the chord budget as its linear part, for providers
2259/// that take one number for both (the CSG primitive tessellator).
2260fn chord_tolerance(options: &ExecutionOptions) -> GeomResult<Tolerance> {
2261    Tolerance::new(chord_error(options), options.tolerance().angular())
2262        .map_err(|error| GeomError::InvalidInput(error.to_string()))
2263}
2264
2265/// The tolerance with HALF the chord budget as its linear part: the share
2266/// a revolution's steps round the axis get, the other half having chorded
2267/// its profile (#231).
2268fn half_tolerance(options: &ExecutionOptions) -> GeomResult<Tolerance> {
2269    Tolerance::new(0.5 * chord_error(options), options.tolerance().angular())
2270        .map_err(|error| GeomError::InvalidInput(error.to_string()))
2271}
2272
2273/// The message of a half-space built on its own (#326): it has a finite
2274/// stand-in only against a subject.
2275const HALF_SPACE_WITHOUT_SUBJECT: &str = "half-space without a subject to size it by: it is \
2276     unbounded on its kept side, and is compiled only as the tool of a difference or an \
2277     intersection";
2278
2279/// The message of a boolean with a region operand that is no clip (#326).
2280const UNCLIPPED_HALF_SPACE: &str = "boolean of a half-space that is not a clip: a half-space, or \
2281     an intersection of half-spaces, is unbounded, and is compiled only as the tool of a \
2282     difference or an intersection, or as one operand of an intersection with a solid";
2283
2284fn unsupported_input(input: &'static str) -> GeomError {
2285    GeomError::UnsupportedInput {
2286        backend: crate::BACKEND_ID,
2287        operation: Operation::GraphCompilation,
2288        input,
2289    }
2290}
2291
2292/// A clip: a difference or an intersection with a region as its tool
2293/// (#326), the operands read in the order that makes the region the tool.
2294struct Clip {
2295    subject: NodeId,
2296    tool: NodeId,
2297    leaves: Vec<(ClipTool, Transform3)>,
2298}
2299
2300/// Which nodes of `graph` are regions (#326): a half-space, an instance of a
2301/// region, or an intersection of regions, decided on demand and remembered
2302/// in the compilation's cache ([`crate::regions`]).
2303struct Regions<'a> {
2304    graph: &'a GeometryGraph,
2305    memo: &'a crate::regions::RegionMemo,
2306}
2307
2308impl<'a> Regions<'a> {
2309    fn new(graph: &'a GeometryGraph, memo: &'a crate::regions::RegionMemo) -> Self {
2310        Self { graph, memo }
2311    }
2312
2313    fn has(&self, id: NodeId) -> bool {
2314        self.memo
2315            .has(self.graph, id, |leaf| clip_tool(self.graph, leaf).is_some())
2316    }
2317}
2318
2319/// The clip a boolean is, or `None`. These operators stay within the
2320/// finite subject, so a slab covering its bounds is an exact finite
2321/// stand-in for the region (#325). Union and XOR are unbounded and remain
2322/// unsupported. An intersection commutes, so a region on its left clips the
2323/// solid on its right.
2324fn clip_of(
2325    graph: &GeometryGraph,
2326    regions: &Regions<'_>,
2327    (left, right): (NodeId, NodeId),
2328    operator: axiolid_core::BooleanOperator,
2329) -> GeomResult<Option<Clip>> {
2330    use axiolid_core::BooleanOperator::{Difference, Intersection};
2331    if !matches!(operator, Difference | Intersection) {
2332        return Ok(None);
2333    }
2334    let (subject, tool) = match (regions.has(left), regions.has(right)) {
2335        (false, true) => (left, right),
2336        (true, false) if operator == Intersection => (right, left),
2337        _ => return Ok(None),
2338    };
2339    let leaves = region(graph, tool)?;
2340    Ok(Some(Clip {
2341        subject,
2342        tool,
2343        leaves,
2344    }))
2345}
2346
2347/// The half-spaces of the region `id` (#326), each with the placement above
2348/// it, left operand first. `id` must be a region ([`Regions`]). Iterative,
2349/// with the leaves bounded by the graph's size: a graph that shares a
2350/// sub-region is read through many paths.
2351fn region(graph: &GeometryGraph, id: NodeId) -> GeomResult<Vec<(ClipTool, Transform3)>> {
2352    let mut leaves = Vec::new();
2353    let mut work = vec![(id, Transform3::IDENTITY)];
2354    while let Some((mut id, mut outer)) = work.pop() {
2355        loop {
2356            let node = graph.get(id).ok_or_else(|| {
2357                GeomError::InvalidInput(format!("operand {id:?} does not belong to this graph"))
2358            })?;
2359            match node {
2360                GeometryNode::Instance(instance) => {
2361                    outer *= instance.transform;
2362                    id = instance.source;
2363                }
2364                GeometryNode::SolidOperation(SolidOperation::Boolean {
2365                    left,
2366                    right,
2367                    operator: axiolid_core::BooleanOperator::Intersection,
2368                }) => {
2369                    work.push((*right, outer));
2370                    work.push((*left, outer));
2371                    break;
2372                }
2373                _ => {
2374                    let tool = clip_tool(graph, id).ok_or_else(|| {
2375                        GeomError::InvalidInput(format!("operand {id:?} is not a half-space"))
2376                    })?;
2377                    if leaves.len() >= graph.len() {
2378                        return Err(unsupported_input(
2379                            "half-space intersection with more half-spaces than its graph has nodes",
2380                        ));
2381                    }
2382                    leaves.push((tool, outer));
2383                    break;
2384                }
2385            }
2386        }
2387    }
2388    Ok(leaves)
2389}
2390
2391/// A boolean tool that is a half-space, bounded or not.
2392#[derive(Debug, Clone, Copy)]
2393enum ClipTool {
2394    /// One side of a plane.
2395    Unbounded(axiolid_primitive::HalfSpace),
2396    /// One side of a plane within the prism of a boundary.
2397    Bounded {
2398        half_space: NodeId,
2399        boundary: NodeId,
2400        placement: Transform3,
2401        prism_axis: PrismAxis,
2402    },
2403}
2404
2405/// The half-space `id` is, bounded or not; `None` when it is none. The
2406/// instances above one are the region's to read ([`region`]).
2407fn clip_tool(graph: &GeometryGraph, id: NodeId) -> Option<ClipTool> {
2408    match graph.get(id)? {
2409        GeometryNode::HalfSpace(half_space) => Some(ClipTool::Unbounded(*half_space)),
2410        GeometryNode::SolidOperation(operation) => {
2411            let (half_space, boundary, frame, prism_axis) =
2412                crate::half_space_boundary::bounded(operation)?;
2413            Some(ClipTool::Bounded {
2414                half_space,
2415                boundary,
2416                placement: frame,
2417                prism_axis,
2418            })
2419        }
2420        _ => None,
2421    }
2422}
2423
2424/// Which capability a node family would need.
2425///
2426/// Reporting the real missing capability lets a caller register a provider
2427/// that supplies it, instead of guessing from a generic failure.
2428fn unsupported_operation(node: &GeometryNode) -> Operation {
2429    match node {
2430        GeometryNode::Curve2(_) | GeometryNode::Curve3(_) | GeometryNode::OpenProfile(_) => {
2431            Operation::CurveEvaluation
2432        }
2433        GeometryNode::Surface(_) => Operation::SurfaceEvaluation,
2434        GeometryNode::Profile(_) => Operation::ProfileTriangulation,
2435        GeometryNode::BRep(_) | GeometryNode::PolygonMesh(_) => Operation::Tessellation,
2436        _ => Operation::GraphCompilation,
2437    }
2438}
2439
2440impl<B: MeshBoolean> MeshCompiler for ReferenceMeshCompiler<B> {
2441    /// Bounded by the peak mesh size, which is data-dependent, so the honest
2442    /// answer is unbounded rather than an invented constant.
2443    fn scratch_requirement(&self) -> ScratchRequirement {
2444        ScratchRequirement::Unbounded
2445    }
2446
2447    fn compile_mesh(
2448        &self,
2449        graph: &GeometryGraph,
2450        root: NodeId,
2451        options: &ExecutionOptions,
2452    ) -> GeomResult<TriMesh> {
2453        self.admit_budget(options)?;
2454        let mut cache = Cache::new();
2455        self.evaluate(graph, root, options, &mut cache, Measure::Skip)
2456            .map(|built| built.mesh)
2457    }
2458
2459    /// Every channel on the result, and every channel an input had that the
2460    /// result lost, is reported with its fate (#115).
2461    fn compile_mesh_reported(
2462        &self,
2463        graph: &GeometryGraph,
2464        root: NodeId,
2465        options: &ExecutionOptions,
2466    ) -> GeomResult<CompileOutcome> {
2467        self.admit_budget(options)?;
2468        let mut cache = Cache::new();
2469        let built = self.evaluate(graph, root, options, &mut cache, Measure::Skip)?;
2470        Ok(CompileOutcome::tracked(built.mesh, built.fates.into_vec()).with_closure(built.closure))
2471    }
2472
2473    /// Overriding the `_into` seam gives both call shapes one shared cache,
2474    /// so a subtree referenced by several roots is compiled once per batch.
2475    fn compile_mesh_batch_into(
2476        &self,
2477        graph: &GeometryGraph,
2478        roots: &[NodeId],
2479        options: &ExecutionOptions,
2480        destination: &mut Vec<TriMesh>,
2481    ) -> GeomResult<()> {
2482        self.admit_budget(options)?;
2483        destination.reserve(roots.len());
2484        let mut cache = Cache::new();
2485        for &root in roots {
2486            destination.push(
2487                self.evaluate(graph, root, options, &mut cache, Measure::Skip)?
2488                    .mesh,
2489            );
2490        }
2491        Ok(())
2492    }
2493}
2494
2495/// Curve nodes to polylines (#311): `Curve2` (lifted to `z = 0`),
2496/// `Curve3` and `OpenProfile`, and their placements, each bisected until
2497/// every span's certified chord bound fits the budget. See
2498/// `crate::polyline` for what is bounded how and what is refused.
2499impl<B: MeshBoolean> PolylineCompiler for ReferenceMeshCompiler<B> {
2500    fn compile_polyline(
2501        &self,
2502        graph: &GeometryGraph,
2503        root: NodeId,
2504        options: &ExecutionOptions,
2505    ) -> GeomResult<PolylineOutcome> {
2506        crate::polyline::compile(graph, root, options)
2507    }
2508}
2509
2510impl<B: MeshBoolean> ReferenceMeshCompiler<B> {
2511    /// Refuse a compilation whose memory budget this compiler cannot honour.
2512    ///
2513    /// Graph compilation caches every intermediate mesh, and peak size is
2514    /// data-dependent, so `scratch_requirement` honestly reports
2515    /// [`ScratchRequirement::Unbounded`]. By contract an unbounded requirement
2516    /// never fits a DECLARED budget -- admitting it anyway would make the
2517    /// budget advisory, which is the failure the type exists to prevent.
2518    ///
2519    /// Before this check the field was simply ignored here: a caller could set
2520    /// a budget and watch the compiler allocate past it without a word. A
2521    /// caller that sets no budget is unaffected.
2522    fn admit_budget(&self, options: &ExecutionOptions) -> GeomResult<()> {
2523        if self.scratch_requirement().fits_budget(options, 0) {
2524            return Ok(());
2525        }
2526        Err(GeomError::BudgetExceeded {
2527            resource: "graph compilation intermediate meshes",
2528        })
2529    }
2530
2531    /// The boolean provider this compiler dispatches to.
2532    ///
2533    /// Exposed so an application can apply its own source-format set
2534    /// operations with the same provider the compiler uses, rather than
2535    /// constructing a second one that might differ.
2536    pub const fn boolean_provider(&self) -> &B {
2537        &self.boolean
2538    }
2539
2540    /// Compile one root and report how far its exact surface may lie from
2541    /// the mesh (#232).
2542    ///
2543    /// The outcome is [`MeshCompiler::compile_mesh_reported`]'s. The report
2544    /// carries a certified upper bound on the distance from every point of
2545    /// the exact surface to the mesh's triangles, in world units, the
2546    /// paths that contributed, and whether the bound is within the chord
2547    /// budget asked for; see [`crate::DeviationReport`] and the
2548    /// [`deviation`](crate::deviation) module for what each path's bound
2549    /// rests on. A path with no certified bound is named, and makes the
2550    /// overall bound `None`. A boolean is measured against the result
2551    /// [`crate::ReferenceExactCompiler`] builds for it, where it builds one
2552    /// (#235): that costs an exact compilation and a certified search for
2553    /// each boolean whose result the root emits (not for the inner
2554    /// booleans of a chain).
2555    ///
2556    /// An inherent method rather than a [`MeshCompiler`] one: the bound is
2557    /// this compiler's claim about its own meshes, and another compiler
2558    /// would have to prove its own before it could make it.
2559    ///
2560    /// # Errors
2561    ///
2562    /// As [`MeshCompiler::compile_mesh_reported`].
2563    pub fn compile_mesh_with_deviation(
2564        &self,
2565        graph: &GeometryGraph,
2566        root: NodeId,
2567        options: &ExecutionOptions,
2568    ) -> GeomResult<(CompileOutcome, crate::DeviationReport)> {
2569        self.admit_budget(options)?;
2570        let mut cache = Cache::new();
2571        let emitted = crate::deviation::emitted_booleans(graph, root);
2572        let built = self.evaluate(
2573            graph,
2574            root,
2575            options,
2576            &mut cache,
2577            Measure::Deviation(&emitted),
2578        )?;
2579        let report = built.deviation.report(chord_error(options));
2580        Ok((
2581            CompileOutcome::tracked(built.mesh, built.fates.into_vec()).with_closure(built.closure),
2582            report,
2583        ))
2584    }
2585}
2586
2587/// Convert an authored polygon mesh to triangles (#160).
2588///
2589/// A plain triangle keeps its exact corner order. Any other face -- an
2590/// n-gon, a concave face, a face with holes (IFC4
2591/// `IfcIndexedPolygonalFaceWithVoids`) -- is triangulated in its own
2592/// plane by [`crate::planar::triangulate_authored_polygon`]. Positions
2593/// are kept as authored and shared, so the output welds exactly where the
2594/// input did; no position is moved. The only positions added are the
2595/// Steiner points a keyhole face places inside itself so that its seam is
2596/// no triangle edge (#283), each used by that face alone. A corner that
2597/// one face's
2598/// triangulation inserts into a ring edge (a hole touching the outer
2599/// edge at an interior point) is inserted into every face sharing that
2600/// edge by index, a triangle face included, so the faces still meet
2601/// along the same pieces of it ([`crate::weld`], #265).
2602///
2603/// A face with no area, or whose rings cross in its plane, is refused
2604/// with an error naming its index. A face off its plane by more than
2605/// the linear tolerance has no unique surface; it is triangulated in
2606/// its fit plane all the same (#254), and the largest warp of any such
2607/// face (the width of the slab its corners span about that plane, #261)
2608/// is returned with the mesh, `None` when every face is planar.
2609/// [`crate::planar`] states what that warp bounds; the caller reports
2610/// it as a certified deviation, never as exact. So is the largest
2611/// [`crate::planar::steiner_offset`] of a keyhole face whose Steiner
2612/// points lie off an axis plane (#283), `None` when there is none.
2613pub(crate) fn compile_authored_polygons(
2614    mesh: &axiolid_mesh::PolygonMesh,
2615    options: &ExecutionOptions,
2616) -> GeomResult<(TriMesh, Option<Scalar>, Option<Scalar>)> {
2617    let position_count = mesh.positions.len();
2618    if let Some(index) = mesh
2619        .faces
2620        .iter()
2621        .flat_map(|face| face_rings(face).flatten().copied())
2622        .find(|&index| index as usize >= position_count)
2623    {
2624        return Err(GeomError::InvalidInput(format!(
2625            "authored polygon index {index} exceeds position count {position_count}"
2626        )));
2627    }
2628
2629    let mut positions = Vec::new();
2630    positions
2631        .try_reserve_exact(position_count)
2632        .map_err(|_| GeomError::BudgetExceeded {
2633            resource: "authored polygon positions",
2634        })?;
2635    positions.extend_from_slice(&mesh.positions);
2636
2637    // A polygon with k corners (holes included) gives at most
2638    // k + 2 * holes - 2 triangles; bound by corner count times three.
2639    let index_bound = mesh
2640        .faces
2641        .iter()
2642        .try_fold(0_usize, |total, face| {
2643            let corners = face_rings(face).map(Vec::len).sum::<usize>();
2644            corners
2645                .checked_add(2 * face.holes.len())
2646                .and_then(|triangles| triangles.checked_mul(3))
2647                .and_then(|indices| total.checked_add(indices))
2648        })
2649        .ok_or(GeomError::BudgetExceeded {
2650            resource: "authored polygon indices",
2651        })?;
2652    let mut indices = Vec::new();
2653    indices
2654        .try_reserve_exact(index_bound)
2655        .map_err(|_| GeomError::BudgetExceeded {
2656            resource: "authored polygon indices",
2657        })?;
2658
2659    let linear = options.tolerance().linear();
2660    let splits = authored_edge_splits(mesh);
2661    let mut warp: Option<Scalar> = None;
2662    let mut steiner_offset: Option<Scalar> = None;
2663    for (face_index, face) in mesh.faces.iter().enumerate() {
2664        let mut rings: Vec<Vec<u32>> = face_rings(face).cloned().collect();
2665        // Weld through every corner a face inserts into a shared edge
2666        // (#265): this face takes each such corner on its own copy of
2667        // the edge, a triangle face included, which then has four.
2668        splits.split(&mut rings, |i| (i, mesh.positions[i as usize]), |i, _| i);
2669        if rings.len() == 1 && rings[0].len() == 3 {
2670            indices.extend_from_slice(&rings[0]);
2671            continue;
2672        }
2673        // A repeated corner (an exporter's closing point, a doubled
2674        // corner) needs no handling here: `planar::clip_projected` drops
2675        // coincident consecutive points itself
2676        // (`a_repeated_closing_corner_is_not_a_triangle`).
2677        let points: Vec<Vec<axiolid_core::Point3>> = rings
2678            .iter()
2679            .map(|ring| ring.iter().map(|&i| mesh.positions[i as usize]).collect())
2680            .collect();
2681        let views: Vec<&[axiolid_core::Point3]> = points.iter().map(Vec::as_slice).collect();
2682        let local = crate::planar::triangulate_authored_polygon(&views, linear)
2683            .map_err(|refusal| crate::planar::face_error(face_index, refusal))?;
2684        if let Some(face_warp) = local.warp {
2685            warp = Some(warp.map_or(face_warp, |w| w.max(face_warp)));
2686        }
2687        if let Some(offset) = local.steiner_offset {
2688            steiner_offset = Some(steiner_offset.map_or(offset, |w| w.max(offset)));
2689        }
2690        let mut corners: Vec<u32> = rings.into_iter().flatten().collect();
2691        // Steiner points follow the corners, each a new position of this
2692        // face alone (#283).
2693        for point in local.steiner {
2694            let index = u32::try_from(positions.len()).map_err(|_| GeomError::BudgetExceeded {
2695                resource: "authored polygon positions",
2696            })?;
2697            positions.push(point);
2698            corners.push(index);
2699        }
2700        indices.extend(local.indices.into_iter().map(|corner| corners[corner]));
2701    }
2702
2703    let triangles = TriMesh::new(positions, indices);
2704    triangles.validate_structure().map_err(|error| {
2705        GeomError::InvalidInput(format!("invalid authored polygon mesh: {error}"))
2706    })?;
2707    Ok((triangles, warp, steiner_offset))
2708}
2709
2710/// The corners the authored polygon faces of `mesh` insert into their own
2711/// ring edges, recorded against those edges by position index (#265,
2712/// [`crate::weld`]). Every index is in range: the caller checked.
2713fn authored_edge_splits(mesh: &axiolid_mesh::PolygonMesh) -> crate::weld::EdgeSplits<u32> {
2714    let mut splits = crate::weld::EdgeSplits::new();
2715    for face in &mesh.faces {
2716        // One triangle has no corner inside its own edge.
2717        if face.outer.len() == 3 && face.holes.is_empty() {
2718            continue;
2719        }
2720        let points: Vec<Vec<axiolid_core::Point3>> = face_rings(face)
2721            .map(|ring| ring.iter().map(|&i| mesh.positions[i as usize]).collect())
2722            .collect();
2723        let views: Vec<&[axiolid_core::Point3]> = points.iter().map(Vec::as_slice).collect();
2724        let touches = crate::planar::authored_edge_touches(&views);
2725        if !touches.is_empty() {
2726            let corners: Vec<(u32, axiolid_core::Point3)> = face_rings(face)
2727                .flatten()
2728                .map(|&i| (i, mesh.positions[i as usize]))
2729                .collect();
2730            splits.record(&corners, &touches);
2731        }
2732    }
2733    splits
2734}
2735
2736/// A face's outer ring followed by its holes.
2737fn face_rings(face: &axiolid_mesh::PolygonFace) -> impl Iterator<Item = &Vec<u32>> {
2738    std::iter::once(&face.outer).chain(face.holes.iter())
2739}
2740
2741/// A boolean needs two volumes; a surface model has none (#161), and
2742/// neither has a declared solid whose mesh does not close (#265).
2743///
2744/// Refused rather than handed to the provider, which would see a closed
2745/// surface model as a valid solid and return a confident, meaningless
2746/// result.
2747fn refuse_surface_operand(built: &Built, role: &'static str) -> GeomResult<()> {
2748    match built.closure {
2749        axiolid_mesh_compile_contract::MeshClosure::Solid => Ok(()),
2750        axiolid_mesh_compile_contract::MeshClosure::OpenSolid => {
2751            Err(GeomError::InvalidInput(format!(
2752                "boolean {role} is a solid whose mesh is not closed: it encloses no volume to \
2753                 combine"
2754            )))
2755        }
2756        _ => Err(GeomError::InvalidInput(format!(
2757            "boolean {role} is a surface model: it encloses no volume to combine"
2758        ))),
2759    }
2760}
2761
2762/// An authored mesh, with its closure read from its structure (#161).
2763///
2764/// A mesh node carries no "this is a solid" declaration the way a B-rep
2765/// does, so the geometry is the only evidence: a closed, consistently
2766/// wound two-manifold bounds a solid, anything else (an open face set, a
2767/// single sheet) is a surface.
2768///
2769/// Closure is a property of the authored connectivity, so it is read from
2770/// the corner indices ([`axiolid_mesh::EdgeAdjacency`]), not from
2771/// `audit_mesh`. The audit drops every triangle below an area threshold
2772/// before it counts edges, and real exports carry legitimate small faces
2773/// (a 1 mm reveal, the end of a thin lining); dropping them opened
2774/// otherwise closed shells, so 522 authored-closed face sets on real
2775/// models were reported as surfaces. Only a triangle that repeats a
2776/// corner index is skipped: it covers no edge pair at all.
2777fn authored_mesh(mesh: TriMesh, _options: &ExecutionOptions) -> Built {
2778    let closure = if authored_mesh_is_closed(&mesh) {
2779        axiolid_mesh_compile_contract::MeshClosure::Solid
2780    } else {
2781        axiolid_mesh_compile_contract::MeshClosure::Surface
2782    };
2783    // An authored mesh is its own exact surface (#232).
2784    Built::with_closure(mesh, closure).with_deviation(crate::deviation::Deviation::one(
2785        crate::deviation::DeviationPath::AuthoredMesh,
2786        "",
2787        crate::deviation::DeviationBound::Proven(0.0),
2788    ))
2789}
2790
2791/// Every edge of the index connectivity is shared by exactly two
2792/// triangles traversing it in opposite directions, the mesh has at least
2793/// one such triangle, and every position is finite.
2794fn authored_mesh_is_closed(mesh: &TriMesh) -> bool {
2795    if mesh.indices.is_empty() || mesh.positions.iter().any(|p| !p.is_finite()) {
2796        return false;
2797    }
2798    let adjacency = axiolid_mesh::EdgeAdjacency::build(mesh);
2799    adjacency.edge_count() > 0 && adjacency.is_closed_two_manifold()
2800}
2801
2802/// The linear part an operand is placed by through its whole chain of
2803/// instances, `outer` being its own transform: the product down to the
2804/// first node that is not an instance. `None` when the operand is not an
2805/// instance (a boolean's frame was checked when it was cut).
2806fn chain_linear(graph: &GeometryGraph, id: NodeId, outer: Transform3) -> Option<Transform3> {
2807    let Some(GeometryNode::Instance(first)) = graph.get(id) else {
2808        return None;
2809    };
2810    let mut composed = outer;
2811    let mut source = first.source;
2812    while let Some(GeometryNode::Instance(next)) = graph.get(source) {
2813        composed *= next.transform;
2814        source = next.source;
2815    }
2816    Some(composed)
2817}