axiolid_nurbs/intersection_curve.rs
1//! Constructed intersection curves from certified traces (#6).
2//!
3//! # What is constructed, and why it is sound
4//!
5//! `intersect_surface_surface_certified` proves WHERE an intersection is:
6//! it returns endpoint enclosures and a transversality bound. It does not
7//! return a curve. Exact booleans, section curves, offsets, and fillets all
8//! need the curve itself, which is why this is the gate in front of them.
9//!
10//! The construction here is deliberately narrow, and its narrowness is the
11//! proof. A trace is only turned into a curve when BOTH surfaces are exact
12//! single-span AFFINE patches, which the trace certificate already
13//! establishes. Two planes meet in a straight line, so a degree-1 segment
14//! between the two certified endpoints is the EXACT intersection, not a
15//! sampled approximation of it:
16//!
17//! - each affine patch is a subset of a plane, and the signed distance to a
18//! plane is an AFFINE function of position;
19//! - an affine function on a segment attains its extremes at the endpoints;
20//! - so a deviation bound proven at both endpoints bounds the WHOLE segment.
21//!
22//! That is why the reported `deviation_upper_bound` is a real bound over the
23//! curve rather than a bound at the two points that happened to be checked.
24//!
25//! Everything else REFUSES by name. A curved or unresolved trace has no
26//! straight-line proof, and emitting a polyline there would be a tessellation
27//! wearing the word "certified".
28
29use axiolid_contracts::{GeomError, GeomResult};
30use axiolid_core::{Point3, Scalar};
31use axiolid_curve::{BSplineCurve3, KnotSpec};
32use axiolid_surface::BSplineSurface;
33
34use crate::certified_curve_surface_intersection::{
35 intersect_curve_surface_certified, CertifiedCurveSurfaceIntersection3,
36 CertifiedCurveSurfaceIntersectionOptions,
37};
38use crate::certified_surface_surface_intersection::{
39 intersect_surface_surface_certified, CertifiedSurfaceSurfaceIntersection3,
40 CertifiedSurfaceSurfaceIntersectionOptions, TransverseSurfaceSurfaceTrace3,
41};
42
43/// Why a certified query produced no constructed intersection curve.
44#[non_exhaustive]
45#[derive(Debug, Clone, PartialEq)]
46pub enum IntersectionCurveRefusal {
47 /// The underlying certified query did not resolve every candidate.
48 ///
49 /// Constructing a curve from an incomplete cover would assert an extent
50 /// the search never proved.
51 Unresolved {
52 /// Number of conservative candidate boxes still outstanding.
53 candidates: usize,
54 },
55 /// The inputs are not the exact affine family this construction proves.
56 ///
57 /// A curved intersection has no straight-line proof; emitting a polyline
58 /// would be tessellation, not certification.
59 UnsupportedGeometry,
60 /// The surfaces provably do not meet, so there is no curve to construct.
61 Disjoint,
62}
63
64/// An intersection curve constructed from a certified trace.
65#[derive(Debug, Clone, PartialEq)]
66pub struct ConstructedIntersectionCurve3 {
67 /// The constructed curve in model space.
68 ///
69 /// Exact for the affine family: two planes meet in a straight line, so
70 /// this degree-1 curve IS the intersection, not a sampling of it.
71 pub curve: BSplineCurve3,
72 /// Upper bound on how far the curve departs from either surface.
73 ///
74 /// Valid over the WHOLE curve, not just the endpoints: distance to a plane
75 /// is affine, so its extremes on a segment are attained at the endpoints.
76 pub deviation_upper_bound: Scalar,
77 /// Transversality bound carried over from the certified trace.
78 pub normal_cross_squared_lower_bound: Scalar,
79 /// The trace this curve was constructed from.
80 pub trace: TransverseSurfaceSurfaceTrace3,
81}
82
83/// Construct intersection curves for two clamped NURBS surfaces.
84///
85/// Returns `Ok(Ok(curves))` only when every candidate was resolved AND every
86/// retained trace admits an exact straight-line construction.
87///
88/// A structurally sound refusal is `Ok(Err(..))`: "these surfaces do not meet"
89/// and "this shape is not provable here" are answers about geometry. `Err` is
90/// reserved for invalid input and exhausted budgets.
91pub fn construct_surface_surface_curves(
92 first: &BSplineSurface,
93 second: &BSplineSurface,
94 options: CertifiedSurfaceSurfaceIntersectionOptions,
95) -> GeomResult<Result<Vec<ConstructedIntersectionCurve3>, IntersectionCurveRefusal>> {
96 let traces = match intersect_surface_surface_certified(first, second, options)? {
97 CertifiedSurfaceSurfaceIntersection3::Complete { traces, .. } => traces,
98 // Partial knowledge must not become a confident curve.
99 CertifiedSurfaceSurfaceIntersection3::Unresolved {
100 candidate_boxes, ..
101 } => {
102 return Ok(Err(IntersectionCurveRefusal::Unresolved {
103 candidates: candidate_boxes.len(),
104 }));
105 }
106 };
107
108 // A resolved query with no trace is a PROOF of disjointness, which is a
109 // different fact from "we could not tell" and is named separately.
110 if traces.is_empty() {
111 return Ok(Err(IntersectionCurveRefusal::Disjoint));
112 }
113
114 let mut curves = Vec::new();
115 curves
116 .try_reserve_exact(traces.len())
117 .map_err(|_| GeomError::BudgetExceeded {
118 resource: "constructed intersection curve allocation",
119 })?;
120 for trace in traces {
121 // Every trace must be constructible. Returning the provable subset and
122 // dropping the rest would silently under-report the intersection.
123 let Some(curve) = construct_from_trace(&trace) else {
124 return Ok(Err(IntersectionCurveRefusal::UnsupportedGeometry));
125 };
126 curves.push(curve);
127 }
128 Ok(Ok(curves))
129}
130
131/// Build the exact segment between a trace endpoints.
132///
133/// Returns `None` when the endpoints coincide: a zero-length "curve" would be
134/// a point contact reported as a segment, which downstream booleans would
135/// mistake for an edge.
136fn construct_from_trace(
137 trace: &TransverseSurfaceSurfaceTrace3,
138) -> Option<ConstructedIntersectionCurve3> {
139 let start = trace.start.point;
140 let end = trace.end.point;
141 if !start.is_finite() || !end.is_finite() || start == end {
142 return None;
143 }
144
145 // Affine distance-to-plane means the endpoint residuals bound the segment.
146 let deviation_upper_bound = trace
147 .start
148 .residual_upper_bound
149 .max(trace.end.residual_upper_bound);
150 if !deviation_upper_bound.is_finite() {
151 return None;
152 }
153
154 Some(ConstructedIntersectionCurve3 {
155 curve: segment(start, end),
156 deviation_upper_bound,
157 normal_cross_squared_lower_bound: trace.normal_cross_squared_lower_bound,
158 trace: trace.clone(),
159 })
160}
161
162fn segment(start: Point3, end: Point3) -> BSplineCurve3 {
163 BSplineCurve3 {
164 degree: 1,
165 control_points: vec![start, end],
166 knots: vec![0.0, 1.0],
167 multiplicities: vec![2, 2],
168 weights: None,
169 knot_spec: KnotSpec::PiecewiseBezier,
170 closed: false,
171 self_intersect: Some(false),
172 }
173}
174
175/// A certified curve/surface crossing, reported as a point rather than a curve.
176#[derive(Debug, Clone, Copy, PartialEq)]
177pub struct ConstructedCurveSurfacePoint3 {
178 /// Native curve parameter of the crossing.
179 pub curve_parameter: Scalar,
180 /// Native surface parameters of the crossing.
181 pub surface_parameters: [Scalar; 2],
182 /// Model-space position of the crossing.
183 pub point: Point3,
184}
185
186/// Construct curve/surface intersection points for a clamped NURBS pair.
187///
188/// A transverse curve/surface intersection is ISOLATED: the correct output is
189/// a set of points, and manufacturing a curve through them would invent an
190/// extent nothing proved. A curve arises only when the curve LIES IN the
191/// surface, which is a coincident case this certification does not cover and
192/// which is refused rather than approximated.
193pub fn construct_curve_surface_points(
194 curve: &BSplineCurve3,
195 surface: &BSplineSurface,
196 options: CertifiedCurveSurfaceIntersectionOptions,
197) -> GeomResult<Result<Vec<ConstructedCurveSurfacePoint3>, IntersectionCurveRefusal>> {
198 let intersections = match intersect_curve_surface_certified(curve, surface, options)? {
199 CertifiedCurveSurfaceIntersection3::Complete { intersections, .. } => intersections,
200 CertifiedCurveSurfaceIntersection3::Unresolved {
201 candidate_boxes, ..
202 } => {
203 return Ok(Err(IntersectionCurveRefusal::Unresolved {
204 candidates: candidate_boxes.len(),
205 }));
206 }
207 };
208 if intersections.is_empty() {
209 return Ok(Err(IntersectionCurveRefusal::Disjoint));
210 }
211
212 let mut points = Vec::new();
213 points
214 .try_reserve_exact(intersections.len())
215 .map_err(|_| GeomError::BudgetExceeded {
216 resource: "constructed intersection point allocation",
217 })?;
218 for intersection in intersections {
219 points.push(ConstructedCurveSurfacePoint3 {
220 curve_parameter: midpoint(intersection.curve_parameter),
221 surface_parameters: [
222 midpoint(intersection.surface_u_parameter),
223 midpoint(intersection.surface_v_parameter),
224 ],
225 point: intersection.point,
226 });
227 }
228 Ok(Ok(points))
229}
230
231fn midpoint(interval: crate::certified_projection::ParameterInterval) -> Scalar {
232 interval.start * 0.5 + interval.end * 0.5
233}