axiolid_construct/
trimmed_intersection.rs

1//! Topology-aware integration of certified affine surface intersections.
2//!
3//! The supported arrangement splits one rectangular patch whose certified trace
4//! is a boundary-to-boundary chord. The same edge is retained as an explicit
5//! embedded pcurve on the other patch when both endpoints are strictly interior
6//! there. This avoids pretending that an interior segment partitions a disk.
7
8use axiolid_contracts::GeomResult;
9use axiolid_nurbs::{intersect_surface_surface_certified, CertifiedSurfaceSurfaceIntersection3};
10use axiolid_surface::BSplineSurface;
11
12pub use crate::trimmed_intersection_types::{
13    CertifiedDualTrimmedSurfacePair3, CertifiedSurfacePairSplit3, CertifiedSurfacePairSplitOptions,
14    CertifiedTrimmedSurfacePair3, EmbeddedFaceCurve, SurfacePairMember,
15    SurfacePairSplitUnresolvedReason,
16};
17
18use crate::trimmed_intersection_assembly::{assemble, assemble_dual};
19use crate::trimmed_intersection_classify::{classify, Classification};
20
21/// Intersect two supported affine patches and construct a topology-aware trimmed arrangement.
22///
23/// `Split` is returned only for a single certified trace whose endpoints form a
24/// boundary-to-boundary chord on exactly one patch and are both interior to the
25/// other patch. The returned analytic B-rep has two closed trimmed faces for the
26/// split patch and one closed rectangular face for the other patch. The wrapper's
27/// embedded-curve relation attaches the same intersection edge to the unsplit
28/// face without inventing a dangling trim loop.
29///
30/// Empty intersections remain `Empty`. Incomplete tracing, dual ownership,
31/// corners, mixed ownership, coincidences, and residual-policy misses remain
32/// `Unresolved` with the original intersection evidence.
33pub fn split_surface_pair_certified(
34    first: &BSplineSurface,
35    second: &BSplineSurface,
36    options: CertifiedSurfacePairSplitOptions,
37) -> GeomResult<CertifiedSurfacePairSplit3> {
38    let intersection =
39        intersect_surface_surface_certified(first, second, options.intersection_options())?;
40    let (mut traces, visited_patch_pairs, boundary_queries) = match intersection {
41        CertifiedSurfaceSurfaceIntersection3::Unresolved { .. } => {
42            return Ok(CertifiedSurfacePairSplit3::Unresolved {
43                intersection,
44                reason: SurfacePairSplitUnresolvedReason::IntersectionUnresolved,
45            });
46        }
47        CertifiedSurfaceSurfaceIntersection3::Complete {
48            traces,
49            visited_patch_pairs,
50            boundary_queries,
51        } => (traces, visited_patch_pairs, boundary_queries),
52        other => {
53            return Ok(CertifiedSurfacePairSplit3::Unresolved {
54                intersection: other,
55                reason: SurfacePairSplitUnresolvedReason::IntersectionUnresolved,
56            });
57        }
58    };
59    if traces.is_empty() {
60        return Ok(CertifiedSurfacePairSplit3::Empty {
61            visited_patch_pairs,
62            boundary_queries: u32::from(boundary_queries),
63        });
64    }
65    if traces.len() != 1 {
66        return Ok(unresolved_complete(
67            traces,
68            visited_patch_pairs,
69            boundary_queries,
70            SurfacePairSplitUnresolvedReason::UnsupportedTraceCount,
71        ));
72    }
73    let Some(trace_ref) = traces.first() else {
74        return Ok(CertifiedSurfacePairSplit3::Empty {
75            visited_patch_pairs,
76            boundary_queries: u32::from(boundary_queries),
77        });
78    };
79    let residual_upper_bound = trace_ref
80        .start
81        .residual_upper_bound
82        .max(trace_ref.end.residual_upper_bound);
83    if !residual_upper_bound.is_finite() || residual_upper_bound > options.max_surface_residual() {
84        return Ok(unresolved_complete(
85            traces,
86            visited_patch_pairs,
87            boundary_queries,
88            SurfacePairSplitUnresolvedReason::ResidualExceedsPolicy,
89        ));
90    }
91    // The classifier distinguishes a proven terminal refusal from an
92    // unimplemented combination; pass its verdict through unchanged rather
93    // than collapsing both to one reason.
94    let classification = match classify(first, second, trace_ref) {
95        Ok(classification) => classification,
96        Err(reason) => {
97            return Ok(unresolved_complete(
98                traces,
99                visited_patch_pairs,
100                boundary_queries,
101                reason,
102            ));
103        }
104    };
105    let Some(trace) = traces.pop() else {
106        return Ok(CertifiedSurfacePairSplit3::Empty {
107            visited_patch_pairs,
108            boundary_queries: u32::from(boundary_queries),
109        });
110    };
111    // Both shapes assemble from the same certified trace; they differ only
112    // in how many faces the chord bounds.
113    match classification {
114        Classification::Single(single) => {
115            let split = assemble(
116                first,
117                second,
118                trace,
119                single,
120                residual_upper_bound,
121                visited_patch_pairs,
122                boundary_queries,
123            )?;
124            Ok(CertifiedSurfacePairSplit3::Split(split))
125        }
126        Classification::Dual(dual) => {
127            let split = assemble_dual(
128                first,
129                second,
130                trace,
131                dual,
132                residual_upper_bound,
133                visited_patch_pairs,
134                boundary_queries,
135            )?;
136            Ok(CertifiedSurfacePairSplit3::DualSplit(split))
137        }
138    }
139}
140
141fn unresolved_complete(
142    traces: Vec<axiolid_nurbs::TransverseSurfaceSurfaceTrace3>,
143    visited_patch_pairs: u32,
144    boundary_queries: u8,
145    reason: SurfacePairSplitUnresolvedReason,
146) -> CertifiedSurfacePairSplit3 {
147    CertifiedSurfacePairSplit3::Unresolved {
148        intersection: CertifiedSurfaceSurfaceIntersection3::Complete {
149            traces,
150            visited_patch_pairs,
151            boundary_queries,
152        },
153        reason,
154    }
155}