axiolid_nurbs/
certified_curve_surface_intersection.rs

1//! Certified transverse intersections between internally continuous clamped 3D NURBS
2//! curves and surfaces.
3
4use axiolid_contracts::{GeomError, GeomResult};
5use axiolid_core::{Point3, Scalar};
6use axiolid_curve::BSplineCurve3;
7use axiolid_evaluate::{curve::bspline_jet3, surface::bspline_jet};
8use axiolid_surface::BSplineSurface;
9
10use crate::{
11    certified_bezier::{Cell, Interval},
12    certified_projection::ParameterInterval,
13    certified_refinement::{piecewise_bezier_cells, RefinementBudget},
14    certified_surface_bezier::{piecewise_bezier_patches, Patch},
15};
16
17const MAX_NODES: u32 = 100_000;
18const MAX_DEPTH: u16 = 64;
19
20/// Bounded policy for certified curve/surface root isolation.
21#[derive(Debug, Clone, Copy, PartialEq)]
22pub struct CertifiedCurveSurfaceIntersectionOptions {
23    parameter_tolerance: Scalar,
24    max_nodes: u32,
25    max_depth: u16,
26}
27
28impl CertifiedCurveSurfaceIntersectionOptions {
29    /// Construct a policy with a positive finite native-parameter resolution.
30    pub fn new(parameter_tolerance: Scalar, max_nodes: u32, max_depth: u16) -> GeomResult<Self> {
31        if !parameter_tolerance.is_finite() || parameter_tolerance <= 0.0 {
32            return Err(GeomError::InvalidInput(
33                "curve/surface parameter tolerance must be finite and positive".to_owned(),
34            ));
35        }
36        if max_nodes == 0 || max_nodes > MAX_NODES {
37            return Err(GeomError::InvalidInput(format!(
38                "curve/surface max_nodes must be in 1..={MAX_NODES}"
39            )));
40        }
41        if max_depth == 0 || max_depth > MAX_DEPTH {
42            return Err(GeomError::InvalidInput(format!(
43                "curve/surface max_depth must be in 1..={MAX_DEPTH}"
44            )));
45        }
46        Ok(Self {
47            parameter_tolerance,
48            max_nodes,
49            max_depth,
50        })
51    }
52
53    fn parameter_tolerance(self) -> Scalar {
54        self.parameter_tolerance
55    }
56    fn max_nodes(self) -> u32 {
57        self.max_nodes
58    }
59    fn max_depth(self) -> u16 {
60        self.max_depth
61    }
62}
63
64impl Default for CertifiedCurveSurfaceIntersectionOptions {
65    fn default() -> Self {
66        Self {
67            parameter_tolerance: 1.0e-8,
68            max_nodes: MAX_NODES,
69            max_depth: MAX_DEPTH,
70        }
71    }
72}
73
74/// Native parameter box for a curve and a tensor-product surface patch.
75#[derive(Debug, Clone, Copy, PartialEq)]
76#[non_exhaustive]
77pub struct CurveSurfaceParameterBox {
78    /// Curve parameter enclosure.
79    pub curve: ParameterInterval,
80    /// Surface `u` parameter enclosure.
81    pub surface_u: ParameterInterval,
82    /// Surface `v` parameter enclosure.
83    pub surface_v: ParameterInterval,
84}
85
86/// Existence-and-uniqueness certificate for one transverse intersection.
87#[derive(Debug, Clone, PartialEq)]
88#[non_exhaustive]
89pub struct TransverseCurveSurfaceIntersection3 {
90    /// Certified curve parameter enclosure.
91    pub curve_parameter: ParameterInterval,
92    /// Certified surface `u` parameter enclosure.
93    pub surface_u_parameter: ParameterInterval,
94    /// Certified surface `v` parameter enclosure.
95    pub surface_v_parameter: ParameterInterval,
96    /// Representative midpoint of the two evaluated images.
97    pub point: Point3,
98    /// Conservative residual norm upper bound over the certified parameter box.
99    pub residual_upper_bound: Scalar,
100    /// Positive lower bound for the absolute 3x3 Jacobian determinant.
101    pub jacobian_determinant_lower_bound: Scalar,
102}
103
104/// Certified curve/surface query outcome.
105#[derive(Debug, Clone, PartialEq)]
106#[non_exhaustive]
107pub enum CertifiedCurveSurfaceIntersection3 {
108    /// Every candidate box was excluded or proved to contain one transverse root.
109    Complete {
110        /// Pairwise-disjoint transverse-root certificates.
111        intersections: Vec<TransverseCurveSurfaceIntersection3>,
112        /// Number of parameter boxes processed or generated.
113        visited_nodes: u32,
114    },
115    /// One or more boxes could not be proved or excluded within policy.
116    Unresolved {
117        /// Transverse roots that were proved before another box remained unresolved.
118        intersections: Vec<TransverseCurveSurfaceIntersection3>,
119        /// Conservative boxes that may contain singular, tangential, boundary, or unresolved roots.
120        candidate_boxes: Vec<CurveSurfaceParameterBox>,
121        /// Number of parameter boxes processed or generated.
122        visited_nodes: u32,
123    },
124}
125
126#[derive(Debug, Clone, Copy)]
127struct Pending {
128    curve_index: usize,
129    patch_index: usize,
130    parameters: CurveSurfaceParameterBox,
131    depth: u16,
132}
133
134/// Certify all isolated transverse roots of `curve(t) = surface(u, v)`.
135///
136/// The implementation accepts finite polynomial inputs and rational inputs with
137/// finite, strictly positive weights. All axes must be clamped and have continuous
138/// internal span joins (internal multiplicity `1..=degree`). Full-multiplicity internal knots are valid NURBS representation but are not
139/// supported by this certified query. A successful certificate uses outward rational Bézier bounds and a strict
140/// interior 3D Krawczyk image. Tangential, singular, patch-boundary, and
141/// proof-insufficient cases are returned conservatively as [`Unresolved`](CertifiedCurveSurfaceIntersection3::Unresolved).
142pub fn intersect_curve_surface_certified(
143    curve: &BSplineCurve3,
144    surface: &BSplineSurface,
145    options: CertifiedCurveSurfaceIntersectionOptions,
146) -> GeomResult<CertifiedCurveSurfaceIntersection3> {
147    let options = CertifiedCurveSurfaceIntersectionOptions::new(
148        options.parameter_tolerance,
149        options.max_nodes,
150        options.max_depth,
151    )?;
152    let mut budget = RefinementBudget::new(
153        options.max_nodes(),
154        "certified curve/surface intersection budget",
155    );
156    let curve_cells =
157        piecewise_bezier_cells(curve, |point| [point.x, point.y, point.z], &mut budget)?;
158    let patches = piecewise_bezier_patches(surface, &mut budget)?;
159    let initial =
160        curve_cells
161            .len()
162            .checked_mul(patches.len())
163            .ok_or(GeomError::BudgetExceeded {
164                resource: "certified curve/surface intersection budget",
165            })?;
166    budget.charge(u128::try_from(initial).ok())?;
167    let mut visited_nodes = u32::try_from(initial).map_err(|_| GeomError::BudgetExceeded {
168        resource: "certified curve/surface intersection budget",
169    })?;
170
171    let mut intersections = Vec::new();
172    let mut unresolved = Vec::new();
173    // The surface's own domain, captured before subdivision so a true
174    // edge stays distinguishable from a face introduced by splitting.
175    let domain = SurfaceDomain {
176        u_start: *surface
177            .u_knots
178            .first()
179            .ok_or_else(|| GeomError::InvalidInput("surface has no u knots".to_owned()))?,
180        u_end: *surface
181            .u_knots
182            .last()
183            .ok_or_else(|| GeomError::InvalidInput("surface has no u knots".to_owned()))?,
184        v_start: *surface
185            .v_knots
186            .first()
187            .ok_or_else(|| GeomError::InvalidInput("surface has no v knots".to_owned()))?,
188        v_end: *surface
189            .v_knots
190            .last()
191            .ok_or_else(|| GeomError::InvalidInput("surface has no v knots".to_owned()))?,
192    };
193    let mut pending = Vec::new();
194    pending
195        .try_reserve(usize::from(options.max_depth()) + 1)
196        .map_err(|_| GeomError::BudgetExceeded {
197            resource: "certified curve/surface pending allocation",
198        })?;
199
200    for curve_index in 0..curve_cells.len() {
201        for patch_index in 0..patches.len() {
202            pending.push(Pending {
203                curve_index,
204                patch_index,
205                parameters: base_box(&curve_cells[curve_index], &patches[patch_index]),
206                depth: 0,
207            });
208            while let Some(current) = pending.pop() {
209                let curve_cell = curve_cells[current.curve_index]
210                    .restrict(current.parameters.curve.start, current.parameters.curve.end)?;
211                let patch = patches[current.patch_index].restrict(
212                    current.parameters.surface_u.start,
213                    current.parameters.surface_u.end,
214                    current.parameters.surface_v.start,
215                    current.parameters.surface_v.end,
216                )?;
217                if residual_excludes_zero(&curve_cell, &patch)? {
218                    continue;
219                }
220                if let Some(root) = krawczyk_root(curve, surface, &curve_cell, &patch)? {
221                    if certificate_meets_resolution(&root, options.parameter_tolerance()) {
222                        push_result(&mut intersections, root)?;
223                        continue;
224                    }
225                    if current.depth >= options.max_depth() {
226                        push_result(&mut unresolved, certificate_box(&root))?;
227                        continue;
228                    }
229                    let contracted = CurveSurfaceParameterBox {
230                        curve: contract_interval(
231                            current.parameters.curve,
232                            root.curve_parameter,
233                            options.parameter_tolerance(),
234                        ),
235                        surface_u: contract_interval(
236                            current.parameters.surface_u,
237                            root.surface_u_parameter,
238                            options.parameter_tolerance(),
239                        ),
240                        surface_v: contract_interval(
241                            current.parameters.surface_v,
242                            root.surface_v_parameter,
243                            options.parameter_tolerance(),
244                        ),
245                    };
246                    if contracted == current.parameters {
247                        push_result(&mut unresolved, contracted)?;
248                        continue;
249                    }
250                    budget.charge(Some(1))?;
251                    visited_nodes = checked_nodes(visited_nodes, 1, options.max_nodes())?;
252                    pending.push(Pending {
253                        parameters: contracted,
254                        depth: current.depth.checked_add(1).ok_or_else(|| {
255                            GeomError::Degenerate(
256                                "curve/surface intersection depth overflow".to_owned(),
257                            )
258                        })?,
259                        ..current
260                    });
261                    continue;
262                }
263                // The 3x3 operator could not certify this box. Before
264                // subdividing, try the reduced solve on any DOMAIN edge the
265                // box touches: a root sitting exactly on a patch edge is
266                // unreachable for strict containment and would otherwise
267                // halve forever until depth runs out.
268                //
269                // Only true domain edges qualify. A face created by
270                // subdivision has interior geometry on both sides, so a root
271                // there is genuinely interior and belongs to the 3x3 path;
272                // pinning it would report a root the caller could also find
273                // by subdividing, i.e. a duplicate.
274                if let Some(root) = edge_root(
275                    curve,
276                    surface,
277                    &curve_cell,
278                    &patch,
279                    &domain,
280                    options.parameter_tolerance(),
281                )? {
282                    push_result(&mut intersections, root)?;
283                    continue;
284                }
285                if current.depth >= options.max_depth() {
286                    push_result(&mut unresolved, current.parameters)?;
287                    continue;
288                }
289                budget.charge(Some(2))?;
290                visited_nodes = checked_nodes(visited_nodes, 2, options.max_nodes())?;
291                split_pending(current, &mut pending)?;
292            }
293        }
294    }
295
296    if unresolved.is_empty() {
297        Ok(CertifiedCurveSurfaceIntersection3::Complete {
298            intersections,
299            visited_nodes,
300        })
301    } else {
302        Ok(CertifiedCurveSurfaceIntersection3::Unresolved {
303            intersections,
304            candidate_boxes: unresolved,
305            visited_nodes,
306        })
307    }
308}
309
310fn checked_nodes(current: u32, additional: u32, maximum: u32) -> GeomResult<u32> {
311    current
312        .checked_add(additional)
313        .filter(|&value| value <= maximum)
314        .ok_or(GeomError::BudgetExceeded {
315            resource: "certified curve/surface intersection budget",
316        })
317}
318
319fn push_result<T>(target: &mut Vec<T>, value: T) -> GeomResult<()> {
320    target
321        .try_reserve(1)
322        .map_err(|_| GeomError::BudgetExceeded {
323            resource: "certified curve/surface result allocation",
324        })?;
325    target.push(value);
326    Ok(())
327}
328
329fn base_box(curve: &Cell, patch: &Patch) -> CurveSurfaceParameterBox {
330    CurveSurfaceParameterBox {
331        curve: ParameterInterval {
332            start: curve.start,
333            end: curve.end,
334        },
335        surface_u: ParameterInterval {
336            start: patch.u_start,
337            end: patch.u_end,
338        },
339        surface_v: ParameterInterval {
340            start: patch.v_start,
341            end: patch.v_end,
342        },
343    }
344}
345
346fn certificate_box(root: &TransverseCurveSurfaceIntersection3) -> CurveSurfaceParameterBox {
347    CurveSurfaceParameterBox {
348        curve: root.curve_parameter,
349        surface_u: root.surface_u_parameter,
350        surface_v: root.surface_v_parameter,
351    }
352}
353
354fn certificate_meets_resolution(
355    root: &TransverseCurveSurfaceIntersection3,
356    tolerance: Scalar,
357) -> bool {
358    root.curve_parameter.end - root.curve_parameter.start <= tolerance
359        && root.surface_u_parameter.end - root.surface_u_parameter.start <= tolerance
360        && root.surface_v_parameter.end - root.surface_v_parameter.start <= tolerance
361}
362
363fn residual_excludes_zero(curve: &Cell, patch: &Patch) -> GeomResult<bool> {
364    let curve = curve.coordinate_intervals()?;
365    let surface = patch.coordinate_intervals()?;
366    Ok((0..3).any(|axis| {
367        curve[axis].upper() < surface[axis].lower() || surface[axis].upper() < curve[axis].lower()
368    }))
369}
370
371fn split_interval(
372    interval: ParameterInterval,
373) -> GeomResult<(ParameterInterval, ParameterInterval)> {
374    let midpoint = interval.start * 0.5 + interval.end * 0.5;
375    if midpoint <= interval.start || midpoint >= interval.end {
376        return Err(GeomError::Degenerate(
377            "certified curve/surface parameter split did not advance".to_owned(),
378        ));
379    }
380    Ok((
381        ParameterInterval {
382            start: interval.start,
383            end: midpoint,
384        },
385        ParameterInterval {
386            start: midpoint,
387            end: interval.end,
388        },
389    ))
390}
391
392fn split_pending(current: Pending, pending: &mut Vec<Pending>) -> GeomResult<()> {
393    let widths = [
394        current.parameters.curve.end - current.parameters.curve.start,
395        current.parameters.surface_u.end - current.parameters.surface_u.start,
396        current.parameters.surface_v.end - current.parameters.surface_v.start,
397    ];
398    let axis = if widths[0] >= widths[1] && widths[0] >= widths[2] {
399        0
400    } else if widths[1] >= widths[2] {
401        1
402    } else {
403        2
404    };
405    let source = match axis {
406        0 => current.parameters.curve,
407        1 => current.parameters.surface_u,
408        _ => current.parameters.surface_v,
409    };
410    let (left, right) = split_interval(source)?;
411    let depth = current.depth.checked_add(1).ok_or_else(|| {
412        GeomError::Degenerate("curve/surface intersection depth overflow".to_owned())
413    })?;
414    let with_interval = |interval| {
415        let mut parameters = current.parameters;
416        match axis {
417            0 => parameters.curve = interval,
418            1 => parameters.surface_u = interval,
419            _ => parameters.surface_v = interval,
420        }
421        Pending {
422            parameters,
423            depth,
424            ..current
425        }
426    };
427    pending.push(with_interval(left));
428    pending.push(with_interval(right));
429    Ok(())
430}
431
432fn stable_start(cell: Scalar, root: Scalar, desired: Scalar) -> Scalar {
433    if desired > cell {
434        desired.min(root)
435    } else {
436        root
437    }
438}
439
440fn stable_end(cell: Scalar, root: Scalar, desired: Scalar) -> Scalar {
441    if desired < cell {
442        desired.max(root)
443    } else {
444        root
445    }
446}
447
448fn contract_interval(
449    cell: ParameterInterval,
450    root: ParameterInterval,
451    tolerance: Scalar,
452) -> ParameterInterval {
453    if cell.end - cell.start <= tolerance {
454        return cell;
455    }
456    let center = root.start * 0.5 + root.end * 0.5;
457    let half = tolerance * 0.5;
458    ParameterInterval {
459        start: stable_start(cell.start, root.start, center - half),
460        end: stable_end(cell.end, root.end, center + half),
461    }
462}
463
464fn krawczyk_root(
465    curve: &BSplineCurve3,
466    surface: &BSplineSurface,
467    curve_cell: &Cell,
468    patch: &Patch,
469) -> GeomResult<Option<TransverseCurveSurfaceIntersection3>> {
470    let center = [
471        curve_cell.start * 0.5 + curve_cell.end * 0.5,
472        patch.u_start * 0.5 + patch.u_end * 0.5,
473        patch.v_start * 0.5 + patch.v_end * 0.5,
474    ];
475    let curve_jet = bspline_jet3(curve, center[0])?;
476    let surface_jet = bspline_jet(surface, center[1], center[2])?;
477    let point_jacobian = [
478        [curve_jet.first.x, -surface_jet.du.x, -surface_jet.dv.x],
479        [curve_jet.first.y, -surface_jet.du.y, -surface_jet.dv.y],
480        [curve_jet.first.z, -surface_jet.du.z, -surface_jet.dv.z],
481    ];
482    let Some(inverse) = inverse3(point_jacobian) else {
483        return Ok(None);
484    };
485
486    let curve_midpoint = curve_cell.midpoint_point()?.euclidean()?;
487    let surface_midpoint = patch.midpoint_point()?.euclidean()?;
488    let residual = [
489        curve_midpoint[0].subtract(surface_midpoint[0])?,
490        curve_midpoint[1].subtract(surface_midpoint[1])?,
491        curve_midpoint[2].subtract(surface_midpoint[2])?,
492    ];
493    let curve_derivative = curve_cell.derivative_intervals()?;
494    let surface_u = patch.partial_u_intervals()?;
495    let surface_v = patch.partial_v_intervals()?;
496    let minus_one = Interval::exact(-1.0)?;
497    let jacobian = [
498        [
499            curve_derivative[0],
500            surface_u[0].multiply(minus_one)?,
501            surface_v[0].multiply(minus_one)?,
502        ],
503        [
504            curve_derivative[1],
505            surface_u[1].multiply(minus_one)?,
506            surface_v[1].multiply(minus_one)?,
507        ],
508        [
509            curve_derivative[2],
510            surface_u[2].multiply(minus_one)?,
511            surface_v[2].multiply(minus_one)?,
512        ],
513    ];
514
515    let zero = Interval::exact(0.0)?;
516    let one = Interval::exact(1.0)?;
517    let mut corrected = [zero; 3];
518    for row in 0..3 {
519        corrected[row] = Interval::exact(center[row])?.subtract(dot3(inverse[row], residual)?)?;
520    }
521    let mut matrix = [[zero; 3]; 3];
522    for row in 0..3 {
523        for column in 0..3 {
524            let jacobian_column = [
525                jacobian[0][column],
526                jacobian[1][column],
527                jacobian[2][column],
528            ];
529            let identity = if row == column { one } else { zero };
530            matrix[row][column] = identity.subtract(dot3(inverse[row], jacobian_column)?)?;
531        }
532    }
533    let bounds = [
534        ParameterInterval {
535            start: curve_cell.start,
536            end: curve_cell.end,
537        },
538        ParameterInterval {
539            start: patch.u_start,
540            end: patch.u_end,
541        },
542        ParameterInterval {
543            start: patch.v_start,
544            end: patch.v_end,
545        },
546    ];
547    let mut delta = [zero; 3];
548    for axis in 0..3 {
549        delta[axis] = Interval::hull([
550            Interval::exact(bounds[axis].start)?.subtract(Interval::exact(center[axis])?)?,
551            Interval::exact(bounds[axis].end)?.subtract(Interval::exact(center[axis])?)?,
552        ])?;
553    }
554    let mut image = [zero; 3];
555    for row in 0..3 {
556        image[row] = corrected[row].add(
557            matrix[row][0]
558                .multiply(delta[0])?
559                .add(matrix[row][1].multiply(delta[1])?)?
560                .add(matrix[row][2].multiply(delta[2])?)?,
561        )?;
562    }
563    if !(image[0].lower() > bounds[0].start
564        && image[0].upper() < bounds[0].end
565        && image[1].lower() > bounds[1].start
566        && image[1].upper() < bounds[1].end
567        && image[2].lower() > bounds[2].start
568        && image[2].upper() < bounds[2].end)
569    {
570        return Ok(None);
571    }
572    let determinant_lower = determinant3_interval(jacobian)?.absolute_lower_bound();
573    if determinant_lower == 0.0 {
574        return Ok(None);
575    }
576    let curve_parameter = ParameterInterval {
577        start: image[0].lower(),
578        end: image[0].upper(),
579    };
580    let surface_u_parameter = ParameterInterval {
581        start: image[1].lower(),
582        end: image[1].upper(),
583    };
584    let surface_v_parameter = ParameterInterval {
585        start: image[2].lower(),
586        end: image[2].upper(),
587    };
588    let root_curve = curve_cell.restrict(curve_parameter.start, curve_parameter.end)?;
589    let root_patch = patch.restrict(
590        surface_u_parameter.start,
591        surface_u_parameter.end,
592        surface_v_parameter.start,
593        surface_v_parameter.end,
594    )?;
595    let residual_upper_bound = residual_norm_upper(&root_curve, &root_patch)?;
596    let curve_value = bspline_jet3(curve, interval_midpoint(curve_parameter))?.point;
597    let surface_value = bspline_jet(
598        surface,
599        interval_midpoint(surface_u_parameter),
600        interval_midpoint(surface_v_parameter),
601    )?
602    .point;
603    let point = Point3::new(
604        curve_value.x * 0.5 + surface_value.x * 0.5,
605        curve_value.y * 0.5 + surface_value.y * 0.5,
606        curve_value.z * 0.5 + surface_value.z * 0.5,
607    );
608    if !point.is_finite() || !residual_upper_bound.is_finite() {
609        return Err(GeomError::Degenerate(
610            "certified curve/surface representative overflowed".to_owned(),
611        ));
612    }
613    Ok(Some(TransverseCurveSurfaceIntersection3 {
614        curve_parameter,
615        surface_u_parameter,
616        surface_v_parameter,
617        point,
618        residual_upper_bound,
619        jacobian_determinant_lower_bound: determinant_lower,
620    }))
621}
622
623fn dot3(coefficients: [Scalar; 3], values: [Interval; 3]) -> GeomResult<Interval> {
624    Interval::exact(coefficients[0])?
625        .multiply(values[0])?
626        .add(Interval::exact(coefficients[1])?.multiply(values[1])?)?
627        .add(Interval::exact(coefficients[2])?.multiply(values[2])?)
628}
629
630fn inverse3(matrix: [[Scalar; 3]; 3]) -> Option<[[Scalar; 3]; 3]> {
631    if matrix.iter().flatten().any(|value| !value.is_finite()) {
632        return None;
633    }
634    let [[a, b, c], [d, e, f], [g, h, i]] = matrix;
635    let determinant = a * (e * i - f * h) - b * (d * i - f * g) + c * (d * h - e * g);
636    if determinant == 0.0 || !determinant.is_finite() {
637        return None;
638    }
639    let inverse = [
640        [
641            (e * i - f * h) / determinant,
642            (c * h - b * i) / determinant,
643            (b * f - c * e) / determinant,
644        ],
645        [
646            (f * g - d * i) / determinant,
647            (a * i - c * g) / determinant,
648            (c * d - a * f) / determinant,
649        ],
650        [
651            (d * h - e * g) / determinant,
652            (b * g - a * h) / determinant,
653            (a * e - b * d) / determinant,
654        ],
655    ];
656    inverse
657        .iter()
658        .flatten()
659        .all(|value| value.is_finite())
660        .then_some(inverse)
661}
662
663fn determinant3_interval(matrix: [[Interval; 3]; 3]) -> GeomResult<Interval> {
664    let first = matrix[0][0].multiply(
665        matrix[1][1]
666            .multiply(matrix[2][2])?
667            .subtract(matrix[1][2].multiply(matrix[2][1])?)?,
668    )?;
669    let second = matrix[0][1].multiply(
670        matrix[1][0]
671            .multiply(matrix[2][2])?
672            .subtract(matrix[1][2].multiply(matrix[2][0])?)?,
673    )?;
674    let third = matrix[0][2].multiply(
675        matrix[1][0]
676            .multiply(matrix[2][1])?
677            .subtract(matrix[1][1].multiply(matrix[2][0])?)?,
678    )?;
679    first.subtract(second)?.add(third)
680}
681
682fn residual_norm_upper(curve: &Cell, patch: &Patch) -> GeomResult<Scalar> {
683    let curve = curve.coordinate_intervals()?;
684    let surface = patch.coordinate_intervals()?;
685    let mut maximum = [0.0; 3];
686    for axis in 0..3 {
687        let difference = curve[axis].subtract(surface[axis])?;
688        maximum[axis] = difference.lower().abs().max(difference.upper().abs());
689    }
690    let norm = maximum[0].hypot(maximum[1]).hypot(maximum[2]);
691    if !norm.is_finite() {
692        return Err(GeomError::Degenerate(
693            "certified curve/surface residual bound overflowed".to_owned(),
694        ));
695    }
696    Ok(next_up(norm))
697}
698
699fn interval_midpoint(interval: ParameterInterval) -> Scalar {
700    interval.start * 0.5 + interval.end * 0.5
701}
702
703fn next_up(value: Scalar) -> Scalar {
704    if value == Scalar::INFINITY {
705        return value;
706    }
707    if value == 0.0 {
708        return Scalar::from_bits(1);
709    }
710    let bits = value.to_bits();
711    if value > 0.0 {
712        Scalar::from_bits(bits + 1)
713    } else {
714        Scalar::from_bits(bits - 1)
715    }
716}
717
718#[cfg(test)]
719mod tests {
720    use super::*;
721
722    #[test]
723    fn rejects_unbounded_or_zero_policy() {
724        assert!(CertifiedCurveSurfaceIntersectionOptions::new(0.0, 1, 1).is_err());
725        assert!(CertifiedCurveSurfaceIntersectionOptions::new(1.0e-6, 0, 1).is_err());
726        assert!(CertifiedCurveSurfaceIntersectionOptions::new(1.0e-6, MAX_NODES + 1, 1).is_err());
727        assert!(CertifiedCurveSurfaceIntersectionOptions::new(1.0e-6, 1, MAX_DEPTH + 1).is_err());
728    }
729
730    #[test]
731    fn interval_determinant_encloses_identity() {
732        let zero = Interval::exact(0.0).expect("zero");
733        let one = Interval::exact(1.0).expect("one");
734        let determinant =
735            determinant3_interval([[one, zero, zero], [zero, one, zero], [zero, zero, one]])
736                .expect("determinant");
737        assert!(determinant.lower() <= 1.0 && determinant.upper() >= 1.0);
738        assert!(determinant.absolute_lower_bound() > 0.0);
739    }
740}
741
742/// Which surface parameter is pinned to a domain edge, and where.
743///
744/// A root lying exactly ON a patch edge cannot be certified by the 3x3
745/// operator: strict containment `image.lower() > bounds.start` is
746/// unsatisfiable when the root equals `bounds.start`. Pinning that one
747/// parameter turns the 3-unknown system into a 2-unknown one whose root
748/// IS interior, so the same Krawczyk argument applies unchanged.
749#[derive(Debug, Clone, Copy, PartialEq, Eq)]
750enum PinnedEdge {
751    /// Surface `u` is fixed at the domain edge.
752    U,
753    /// Surface `v` is fixed at the domain edge.
754    V,
755}
756/// Certify a root pinned to `edge` at parameter `pinned_value`.
757///
758/// Solves the reduced 2x2 system in the two free unknowns with the same
759/// interval-Krawczyk argument the 3x3 path uses: build the operator at the
760/// box centre, require its image to land strictly inside the free-parameter
761/// box, and require a nonzero Jacobian determinant lower bound. The pinned
762/// parameter is reported as a degenerate interval, which is the honest
763/// enclosure: it is known exactly, not bracketed.
764fn krawczyk_root_on_edge(
765    curve: &BSplineCurve3,
766    surface: &BSplineSurface,
767    curve_cell: &Cell,
768    patch: &Patch,
769    edge: PinnedEdge,
770    pinned_value: Scalar,
771) -> GeomResult<Option<TransverseCurveSurfaceIntersection3>> {
772    // Free axes: curve t is always free; the other surface parameter is
773    // whichever one is not pinned.
774    let (free_start, free_end) = match edge {
775        PinnedEdge::U => (patch.v_start, patch.v_end),
776        PinnedEdge::V => (patch.u_start, patch.u_end),
777    };
778    let center_t = curve_cell.start * 0.5 + curve_cell.end * 0.5;
779    let center_free = free_start * 0.5 + free_end * 0.5;
780    let (center_u, center_v) = match edge {
781        PinnedEdge::U => (pinned_value, center_free),
782        PinnedEdge::V => (center_free, pinned_value),
783    };
784
785    let curve_jet = bspline_jet3(curve, center_t)?;
786    let surface_jet = bspline_jet(surface, center_u, center_v)?;
787    // Column 2 is the derivative along the FREE surface parameter only.
788    let free_partial = match edge {
789        PinnedEdge::U => surface_jet.dv,
790        PinnedEdge::V => surface_jet.du,
791    };
792    let point_jacobian = [
793        [curve_jet.first.x, -free_partial.x],
794        [curve_jet.first.y, -free_partial.y],
795        [curve_jet.first.z, -free_partial.z],
796    ];
797
798    // Three equations, two unknowns. Pick the two rows whose 2x2 minor has
799    // the largest magnitude: that is the best-conditioned choice available,
800    // and any nonzero minor is a valid certificate basis. The discarded row
801    // is not ignored -- the residual bound below is taken over all three.
802    let rows = [(0usize, 1usize), (0, 2), (1, 2)];
803    let mut best: Option<RowSelection> = None;
804    let mut best_magnitude = 0.0;
805    for (first, second) in rows {
806        let candidate = [
807            [point_jacobian[first][0], point_jacobian[first][1]],
808            [point_jacobian[second][0], point_jacobian[second][1]],
809        ];
810        let Some(inverse) = inverse2(candidate) else {
811            continue;
812        };
813        let magnitude =
814            (candidate[0][0] * candidate[1][1] - candidate[0][1] * candidate[1][0]).abs();
815        if magnitude > best_magnitude {
816            best_magnitude = magnitude;
817            best = Some((candidate, inverse, (first, second)));
818        }
819    }
820    let Some((_, inverse, (row_a, row_b))) = best else {
821        return Ok(None);
822    };
823
824    let curve_midpoint = curve_cell.midpoint_point()?.euclidean()?;
825    let surface_midpoint = patch.midpoint_point()?.euclidean()?;
826    let residual_all = [
827        curve_midpoint[0].subtract(surface_midpoint[0])?,
828        curve_midpoint[1].subtract(surface_midpoint[1])?,
829        curve_midpoint[2].subtract(surface_midpoint[2])?,
830    ];
831    let residual = [residual_all[row_a], residual_all[row_b]];
832
833    let curve_derivative = curve_cell.derivative_intervals()?;
834    let free_intervals = match edge {
835        PinnedEdge::U => patch.partial_v_intervals()?,
836        PinnedEdge::V => patch.partial_u_intervals()?,
837    };
838    let minus_one = Interval::exact(-1.0)?;
839    let jacobian = [
840        [
841            curve_derivative[row_a],
842            free_intervals[row_a].multiply(minus_one)?,
843        ],
844        [
845            curve_derivative[row_b],
846            free_intervals[row_b].multiply(minus_one)?,
847        ],
848    ];
849
850    let zero = Interval::exact(0.0)?;
851    let one = Interval::exact(1.0)?;
852    let center = [center_t, center_free];
853    let mut corrected = [zero; 2];
854    for row in 0..2 {
855        corrected[row] = Interval::exact(center[row])?.subtract(dot2(inverse[row], residual)?)?;
856    }
857    let mut matrix = [[zero; 2]; 2];
858    for row in 0..2 {
859        for column in 0..2 {
860            let jacobian_column = [jacobian[0][column], jacobian[1][column]];
861            let identity = if row == column { one } else { zero };
862            matrix[row][column] = identity.subtract(dot2(inverse[row], jacobian_column)?)?;
863        }
864    }
865    let bounds = [
866        ParameterInterval {
867            start: curve_cell.start,
868            end: curve_cell.end,
869        },
870        ParameterInterval {
871            start: free_start,
872            end: free_end,
873        },
874    ];
875    let mut delta = [zero; 2];
876    for axis in 0..2 {
877        delta[axis] = Interval::hull([
878            Interval::exact(bounds[axis].start)?.subtract(Interval::exact(center[axis])?)?,
879            Interval::exact(bounds[axis].end)?.subtract(Interval::exact(center[axis])?)?,
880        ])?;
881    }
882    let mut image = [zero; 2];
883    for row in 0..2 {
884        image[row] = corrected[row].add(
885            matrix[row][0]
886                .multiply(delta[0])?
887                .add(matrix[row][1].multiply(delta[1])?)?,
888        )?;
889    }
890    // Same strict-containment demand as the 3x3 path, now over the two free
891    // axes only. The pinned axis needs no containment: it is fixed exactly.
892    if !(image[0].lower() > bounds[0].start
893        && image[0].upper() < bounds[0].end
894        && image[1].lower() > bounds[1].start
895        && image[1].upper() < bounds[1].end)
896    {
897        return Ok(None);
898    }
899    let determinant_lower = determinant2_interval(jacobian)?.absolute_lower_bound();
900    if determinant_lower == 0.0 {
901        return Ok(None);
902    }
903
904    let curve_parameter = ParameterInterval {
905        start: image[0].lower(),
906        end: image[0].upper(),
907    };
908    let free_parameter = ParameterInterval {
909        start: image[1].lower(),
910        end: image[1].upper(),
911    };
912    let pinned = ParameterInterval {
913        start: pinned_value,
914        end: pinned_value,
915    };
916    let (surface_u_parameter, surface_v_parameter) = match edge {
917        PinnedEdge::U => (pinned, free_parameter),
918        PinnedEdge::V => (free_parameter, pinned),
919    };
920
921    // `restrict` rejects a zero-width box, and the pinned axis is exactly
922    // zero-width by construction. Widen it to the smallest sliver that still
923    // lies inside the patch. This is sound in both directions a caller
924    // depends on: a SUPERSET of the true degenerate box can only inflate
925    // `residual_norm_upper` (conservative) and can only make the
926    // disjointness test below harder to pass (conservative). It never
927    // certifies a root that the exact degenerate box would reject.
928    let root_curve = {
929        let (start, end) = sliver(
930            curve_parameter.start,
931            curve_parameter.end,
932            curve_cell.start,
933            curve_cell.end,
934        );
935        curve_cell.restrict(start, end)?
936    };
937    let root_patch = {
938        let (u_start, u_end) = sliver(
939            surface_u_parameter.start,
940            surface_u_parameter.end,
941            patch.u_start,
942            patch.u_end,
943        );
944        let (v_start, v_end) = sliver(
945            surface_v_parameter.start,
946            surface_v_parameter.end,
947            patch.v_start,
948            patch.v_end,
949        );
950        patch.restrict(u_start, u_end, v_start, v_end)?
951    };
952    // The 2x2 solve only enforced two of the three coordinate equations.
953    // The third must be CHECKED, not assumed: a curve can meet the edge line
954    // in the chosen two coordinates while missing it in the third. The
955    // residual bound is conservative over the whole certified box, so
956    // requiring it to enclose zero is a sound test.
957    //
958    // NOT COVERED BY TESTS: every off-patch case reachable today is rejected
959    // earlier by `residual_excludes_zero` during subdivision, so this branch
960    // never fires in the current corpus -- deleting it does not fail any
961    // test. It is retained as defence for inputs that reach here with a
962    // satisfied 2x2 system and a violated third row, which the planar-only
963    // certified path cannot yet construct. Do not treat it as verified.
964    let residual_upper_bound = residual_norm_upper(&root_curve, &root_patch)?;
965    let discarded = 3 - row_a - row_b;
966    let curve_span = root_curve.coordinate_intervals()?[discarded];
967    let patch_span = root_patch.coordinate_intervals()?[discarded];
968    if curve_span.upper() < patch_span.lower() || patch_span.upper() < curve_span.lower() {
969        return Ok(None);
970    }
971
972    let curve_value = bspline_jet3(curve, interval_midpoint(curve_parameter))?.point;
973    let surface_value = bspline_jet(
974        surface,
975        interval_midpoint(surface_u_parameter),
976        interval_midpoint(surface_v_parameter),
977    )?
978    .point;
979    let point = Point3::new(
980        curve_value.x * 0.5 + surface_value.x * 0.5,
981        curve_value.y * 0.5 + surface_value.y * 0.5,
982        curve_value.z * 0.5 + surface_value.z * 0.5,
983    );
984    if !point.is_finite() || !residual_upper_bound.is_finite() {
985        return Err(GeomError::Degenerate(
986            "certified curve/surface edge representative overflowed".to_owned(),
987        ));
988    }
989    Ok(Some(TransverseCurveSurfaceIntersection3 {
990        curve_parameter,
991        surface_u_parameter,
992        surface_v_parameter,
993        point,
994        residual_upper_bound,
995        jacobian_determinant_lower_bound: determinant_lower,
996    }))
997}
998
999/// Widen a possibly-degenerate parameter range into a nonempty one.
1000///
1001/// The pinned axis of a boundary-restricted certificate has `start == end`,
1002/// which `restrict` rejects. Returns the smallest nonempty range containing
1003/// `[start, end]` and clamped inside `[low, high]`, so the result is always
1004/// a superset of the requested range that the Bezier restriction accepts.
1005fn sliver(start: Scalar, end: Scalar, low: Scalar, high: Scalar) -> (Scalar, Scalar) {
1006    if end > start {
1007        return (start, end);
1008    }
1009    if high <= low {
1010        return (low, high);
1011    }
1012    let widened_end = next_up(end);
1013    if widened_end < high {
1014        return (start, widened_end);
1015    }
1016    // At the upper limit there is no room above, so grow downwards instead.
1017    let widened_start = -next_up(-start);
1018    if widened_start > low {
1019        (widened_start, end)
1020    } else {
1021        (low, high)
1022    }
1023}
1024
1025/// A certified 2x2 row selection for a boundary-restricted solve: the chosen
1026/// submatrix, its numeric inverse, and which two coordinate rows were kept.
1027type RowSelection = ([[Scalar; 2]; 2], [[Scalar; 2]; 2], (usize, usize));
1028
1029fn dot2(coefficients: [Scalar; 2], values: [Interval; 2]) -> GeomResult<Interval> {
1030    Interval::exact(coefficients[0])?
1031        .multiply(values[0])?
1032        .add(Interval::exact(coefficients[1])?.multiply(values[1])?)
1033}
1034
1035fn inverse2(matrix: [[Scalar; 2]; 2]) -> Option<[[Scalar; 2]; 2]> {
1036    if matrix.iter().flatten().any(|value| !value.is_finite()) {
1037        return None;
1038    }
1039    let [[a, b], [c, d]] = matrix;
1040    let determinant = a * d - b * c;
1041    if determinant == 0.0 || !determinant.is_finite() {
1042        return None;
1043    }
1044    let inverse = [
1045        [d / determinant, -b / determinant],
1046        [-c / determinant, a / determinant],
1047    ];
1048    inverse
1049        .iter()
1050        .flatten()
1051        .all(|value| value.is_finite())
1052        .then_some(inverse)
1053}
1054
1055fn determinant2_interval(matrix: [[Interval; 2]; 2]) -> GeomResult<Interval> {
1056    matrix[0][0]
1057        .multiply(matrix[1][1])?
1058        .subtract(matrix[0][1].multiply(matrix[1][0])?)
1059}
1060
1061/// The surface's own parameter domain, used to tell a true edge from a
1062/// face created by subdivision.
1063#[derive(Debug, Clone, Copy)]
1064struct SurfaceDomain {
1065    u_start: Scalar,
1066    u_end: Scalar,
1067    v_start: Scalar,
1068    v_end: Scalar,
1069}
1070
1071/// Try a boundary-restricted certificate on every domain edge this box
1072/// touches.
1073///
1074/// Returns the first certified root. At most four edges are examined and
1075/// each is a bounded 2x2 solve, so this adds constant work per node.
1076fn edge_root(
1077    curve: &BSplineCurve3,
1078    surface: &BSplineSurface,
1079    curve_cell: &Cell,
1080    patch: &Patch,
1081    domain: &SurfaceDomain,
1082    tolerance: Scalar,
1083) -> GeomResult<Option<TransverseCurveSurfaceIntersection3>> {
1084    let candidates = [
1085        (
1086            patch.u_start == domain.u_start,
1087            PinnedEdge::U,
1088            patch.u_start,
1089        ),
1090        (patch.u_end == domain.u_end, PinnedEdge::U, patch.u_end),
1091        (
1092            patch.v_start == domain.v_start,
1093            PinnedEdge::V,
1094            patch.v_start,
1095        ),
1096        (patch.v_end == domain.v_end, PinnedEdge::V, patch.v_end),
1097    ];
1098    for (on_domain_edge, edge, value) in candidates {
1099        if !on_domain_edge {
1100            continue;
1101        }
1102        if let Some(root) = krawczyk_root_on_edge(curve, surface, curve_cell, patch, edge, value)? {
1103            if certificate_meets_resolution(&root, tolerance) {
1104                return Ok(Some(root));
1105            }
1106        }
1107    }
1108    Ok(None)
1109}