axiolid_nurbs/
certified_curve_intersection.rs

1//! Certified planar curve/curve relationship classification.
2
3use crate::certified_bezier::{distance_between_point_intervals_upper, Interval};
4use crate::certified_projection::{CurvePairParameterBox, ParameterInterval};
5use crate::certified_refinement::{piecewise_bezier_cells, RefinementBudget};
6use axiolid_contracts::{GeomError, GeomResult, Sign};
7use axiolid_core::{Point2, Scalar};
8use axiolid_curve::BSplineCurve2;
9use axiolid_evaluate::curve::bspline_jet2;
10use axiolid_predicates::orient2d;
11
12const MAX_CERTIFIED_CURVE_INTERSECTION_NODES: u32 = 100_000;
13const MAX_CERTIFIED_CURVE_INTERSECTION_DEPTH: u16 = 64;
14
15/// Accuracy and work policy for certified planar root isolation.
16#[derive(Debug, Clone, Copy, PartialEq)]
17pub struct CertifiedCurveIntersectionOptions {
18    parameter_tolerance: Scalar,
19    max_nodes: u32,
20    max_depth: u16,
21}
22
23impl CertifiedCurveIntersectionOptions {
24    /// Construct a finite, non-vacuous root-isolation policy.
25    ///
26    /// `max_nodes` must be at most 100,000 and `max_depth` at most 64 so caller
27    /// policy cannot authorize process-sized allocations.
28    pub fn new(parameter_tolerance: Scalar, max_nodes: u32, max_depth: u16) -> GeomResult<Self> {
29        if !parameter_tolerance.is_finite()
30            || parameter_tolerance <= 0.0
31            || max_nodes == 0
32            || max_nodes > MAX_CERTIFIED_CURVE_INTERSECTION_NODES
33            || max_depth == 0
34            || max_depth > MAX_CERTIFIED_CURVE_INTERSECTION_DEPTH
35        {
36            return Err(GeomError::InvalidInput(
37                "curve-intersection tolerance must be finite and positive; max_nodes must be in 1..=100000 and max_depth in 1..=64".to_owned(),
38            ));
39        }
40        Ok(Self {
41            parameter_tolerance,
42            max_nodes,
43            max_depth,
44        })
45    }
46
47    /// Required maximum native-parameter width of each certified root interval.
48    pub const fn parameter_tolerance(self) -> Scalar {
49        self.parameter_tolerance
50    }
51
52    /// Maximum refinement work and generated product cells.
53    pub const fn max_nodes(self) -> u32 {
54        self.max_nodes
55    }
56
57    /// Maximum subdivision or Krawczyk-contraction depth per product cell.
58    pub const fn max_depth(self) -> u16 {
59        self.max_depth
60    }
61}
62
63/// One isolated transverse planar intersection.
64#[non_exhaustive]
65#[derive(Debug, Clone, PartialEq)]
66pub struct TransverseCurveIntersection2 {
67    /// Certified parameter interval on the first curve.
68    pub first_parameter: ParameterInterval,
69    /// Certified parameter interval on the second curve.
70    pub second_parameter: ParameterInterval,
71    /// Scalar-oracle representative of the common point.
72    pub point: Point2,
73    /// Residual of the representative evaluations, rounded upward.
74    pub residual_upper_bound: Scalar,
75    /// Conservative absolute lower bound on the root Jacobian determinant.
76    pub jacobian_determinant_lower_bound: Scalar,
77}
78
79/// One classified non-transverse contact, owned by exactly one parameter box.
80#[non_exhaustive]
81#[derive(Debug, Clone, Copy, PartialEq)]
82pub struct ClassifiedCurveContact2 {
83    /// Product-domain box that owns this contact.
84    pub parameters: CurvePairParameterBox,
85    /// Proven structural class, or `Unresolved` when no proof completed.
86    pub classification: CurveIntersectionDegeneracy,
87}
88
89/// Singular or not-yet-isolated curve relationship.
90#[non_exhaustive]
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub enum CurveIntersectionDegeneracy {
93    /// A structurally proven contact involving a zero-length curve.
94    PointContact,
95    /// A structurally proven endpoint tangency.
96    Tangency,
97    /// A structurally proven positive-dimensional overlap.
98    Overlap,
99    /// Candidate boxes remain but no supported proof classified them.
100    Unresolved,
101    /// A transverse contact proven to sit on a boundary shared by adjacent cells.
102    ///
103    /// Strict-interior Krawczyk isolation cannot prove a root that lies exactly
104    /// on a cell edge, so a crossing at an interior knot would otherwise be
105    /// reported as an unresolved fragment per incident cell. This class states
106    /// the positive fact instead: the residual is not excluded and the tangents
107    /// are certainly NOT parallel, so a transverse crossing exists in the fused
108    /// boundary box and is owned by it exactly once.
109    BoundaryCrossing,
110}
111
112/// Planar curve/curve classification under the implemented proof paths.
113#[non_exhaustive]
114#[derive(Debug, Clone, PartialEq)]
115pub enum CertifiedCurveIntersection2 {
116    /// Every product-domain cell was excluded or represented by one transverse root.
117    /// Each returned parameter interval is no wider than the requested tolerance.
118    Complete {
119        /// Isolated transverse roots. Empty means certified disjointness.
120        intersections: Vec<TransverseCurveIntersection2>,
121        /// Generated root cells inspected by the classifier.
122        visited_nodes: u32,
123    },
124    /// A singular or currently unsupported candidate was not called transverse.
125    Degenerate {
126        /// Strongest class proven across `contacts`.
127        ///
128        /// Precedence is `Overlap` > `Tangency` > `PointContact` > `Unresolved`:
129        /// a caller that only wants one verdict gets the most structural one,
130        /// while `contacts` retains which box proved what.
131        classification: CurveIntersectionDegeneracy,
132        /// Per-box classification. Boxes are deduplicated and boundary-owned,
133        /// so a root on a shared cell endpoint appears exactly once.
134        contacts: Vec<ClassifiedCurveContact2>,
135        /// Generated root cells inspected by the classifier.
136        visited_nodes: u32,
137    },
138}
139
140/// Classify intersections of two clamped planar B-spline curves.
141///
142/// The complete proof path covers exact-sign single-span polynomial lines,
143/// zero-length line `PointContact`s, and strict-interior Krawczyk isolation of
144/// transverse polynomial or positive-weight rational Bézier roots. Unsupported
145/// singular, tangential, coincident, seam, or proof-insufficient candidates return
146/// [`CurveIntersectionDegeneracy::Unresolved`] rather than an uncertified root.
147pub fn intersect_curve2_certified(
148    first: &BSplineCurve2,
149    second: &BSplineCurve2,
150    options: CertifiedCurveIntersectionOptions,
151) -> GeomResult<CertifiedCurveIntersection2> {
152    let mut budget =
153        RefinementBudget::new(options.max_nodes(), "certified curve intersection budget");
154    let first_cells = piecewise_bezier_cells(first, |p| [p.x, p.y, 0.0], &mut budget)?;
155    let second_cells = piecewise_bezier_cells(second, |p| [p.x, p.y, 0.0], &mut budget)?;
156    let count = first_cells.len().checked_mul(second_cells.len());
157    let visited_nodes = count
158        .and_then(|value| u32::try_from(value).ok())
159        .filter(|&value| value <= options.max_nodes())
160        .ok_or(axiolid_contracts::GeomError::BudgetExceeded {
161            resource: "certified curve intersection budget",
162        })?;
163    if first == second {
164        let mut contacts = Vec::new();
165        contacts
166            .try_reserve_exact(first_cells.len())
167            .map_err(|_| GeomError::BudgetExceeded {
168                resource: "certified curve intersection result allocation",
169            })?;
170        let classification = if has_control_point_extent(first) {
171            CurveIntersectionDegeneracy::Overlap
172        } else {
173            CurveIntersectionDegeneracy::PointContact
174        };
175        for (first_cell, second_cell) in first_cells.iter().zip(&second_cells) {
176            push_contact(
177                &mut contacts,
178                pair_box(first_cell, second_cell),
179                classification,
180            )?;
181        }
182        return Ok(degenerate(contacts, visited_nodes));
183    }
184    if structural_start_tangency(first, second) || structural_start_tangency(second, first) {
185        let mut contacts = Vec::new();
186        push_contact(
187            &mut contacts,
188            pair_box(&first_cells[0], &second_cells[0]),
189            CurveIntersectionDegeneracy::Tangency,
190        )?;
191        return Ok(degenerate(contacts, visited_nodes));
192    }
193    if linear_polynomial(first) && linear_polynomial(second) {
194        return classify_lines(
195            first,
196            second,
197            visited_nodes,
198            vec![pair_box(&first_cells[0], &second_cells[0])],
199            options,
200        );
201    }
202
203    classify_nonlinear(
204        first,
205        second,
206        first_cells,
207        second_cells,
208        visited_nodes,
209        options,
210    )
211}
212
213#[derive(Debug, Clone, Copy)]
214struct PendingCurvePair {
215    first_index: usize,
216    second_index: usize,
217    parameters: CurvePairParameterBox,
218    depth: u16,
219}
220
221fn classify_nonlinear(
222    first: &BSplineCurve2,
223    second: &BSplineCurve2,
224    first_cells: Vec<crate::certified_bezier::Cell>,
225    second_cells: Vec<crate::certified_bezier::Cell>,
226    mut visited_nodes: u32,
227    options: CertifiedCurveIntersectionOptions,
228) -> GeomResult<CertifiedCurveIntersection2> {
229    let mut pending = Vec::new();
230    pending
231        .try_reserve_exact(usize::from(options.max_depth()) + 1)
232        .map_err(|_| GeomError::BudgetExceeded {
233            resource: "certified curve intersection pending allocation",
234        })?;
235    let mut intersections = Vec::new();
236    let mut contacts = Vec::new();
237
238    for (first_index, first_base) in first_cells.iter().enumerate() {
239        for (second_index, second_base) in second_cells.iter().enumerate() {
240            pending.push(PendingCurvePair {
241                first_index,
242                second_index,
243                parameters: pair_box(first_base, second_base),
244                depth: 0,
245            });
246            while let Some(current) = pending.pop() {
247                let first_cell = first_cells[current.first_index]
248                    .restrict(current.parameters.first.start, current.parameters.first.end)?;
249                let second_cell = second_cells[current.second_index].restrict(
250                    current.parameters.second.start,
251                    current.parameters.second.end,
252                )?;
253                let depth = current.depth;
254                if residual_excludes_zero(&first_cell, &second_cell)? {
255                    continue;
256                }
257                if let Some(root) = krawczyk_root(first, second, &first_cell, &second_cell)? {
258                    if root.first_parameter.end - root.first_parameter.start
259                        <= options.parameter_tolerance()
260                        && root.second_parameter.end - root.second_parameter.start
261                            <= options.parameter_tolerance()
262                    {
263                        push_root(&mut intersections, root)?;
264                        continue;
265                    }
266                    if depth >= options.max_depth() {
267                        push_contact(
268                            &mut contacts,
269                            CurvePairParameterBox {
270                                first: root.first_parameter,
271                                second: root.second_parameter,
272                            },
273                            CurveIntersectionDegeneracy::Unresolved,
274                        )?;
275                        continue;
276                    }
277                    visited_nodes = visited_nodes
278                        .checked_add(1)
279                        .filter(|&count| count <= options.max_nodes())
280                        .ok_or(GeomError::BudgetExceeded {
281                            resource: "certified curve intersection budget",
282                        })?;
283                    pending.push(PendingCurvePair {
284                        first_index: current.first_index,
285                        second_index: current.second_index,
286                        parameters: CurvePairParameterBox {
287                            first: contract_interval(
288                                &first_cell,
289                                root.first_parameter,
290                                options.parameter_tolerance(),
291                            ),
292                            second: contract_interval(
293                                &second_cell,
294                                root.second_parameter,
295                                options.parameter_tolerance(),
296                            ),
297                        },
298                        depth: depth.checked_add(1).ok_or_else(|| {
299                            GeomError::Degenerate("curve-intersection depth overflow".to_owned())
300                        })?,
301                    });
302                    continue;
303                }
304                if depth >= options.max_depth() {
305                    push_contact(
306                        &mut contacts,
307                        pair_box(&first_cell, &second_cell),
308                        classify_exhausted_box(&first_cell, &second_cell, first_base, second_base)?,
309                    )?;
310                    continue;
311                }
312                visited_nodes = visited_nodes
313                    .checked_add(2)
314                    .filter(|&count| count <= options.max_nodes())
315                    .ok_or(axiolid_contracts::GeomError::BudgetExceeded {
316                        resource: "certified curve intersection budget",
317                    })?;
318                if first_cell.end - first_cell.start >= second_cell.end - second_cell.start {
319                    let (left, right) = split_parameter_interval(current.parameters.first)?;
320                    pending.push(PendingCurvePair {
321                        parameters: CurvePairParameterBox {
322                            first: left,
323                            ..current.parameters
324                        },
325                        depth: depth + 1,
326                        ..current
327                    });
328                    pending.push(PendingCurvePair {
329                        parameters: CurvePairParameterBox {
330                            first: right,
331                            ..current.parameters
332                        },
333                        depth: depth + 1,
334                        ..current
335                    });
336                } else {
337                    let (left, right) = split_parameter_interval(current.parameters.second)?;
338                    pending.push(PendingCurvePair {
339                        parameters: CurvePairParameterBox {
340                            second: left,
341                            ..current.parameters
342                        },
343                        depth: depth + 1,
344                        ..current
345                    });
346                    pending.push(PendingCurvePair {
347                        parameters: CurvePairParameterBox {
348                            second: right,
349                            ..current.parameters
350                        },
351                        depth: depth + 1,
352                        ..current
353                    });
354                }
355            }
356        }
357    }
358
359    if contacts.is_empty() {
360        Ok(CertifiedCurveIntersection2::Complete {
361            intersections,
362            visited_nodes,
363        })
364    } else {
365        Ok(degenerate(contacts, visited_nodes))
366    }
367}
368
369/// Rank a class so a single summary verdict can be derived from many contacts.
370///
371/// A positive-dimensional overlap subsumes a tangency, which subsumes an
372/// isolated point contact; `Unresolved` is weakest because it asserts nothing.
373fn classification_rank(classification: CurveIntersectionDegeneracy) -> u8 {
374    match classification {
375        CurveIntersectionDegeneracy::Overlap => 4,
376        CurveIntersectionDegeneracy::Tangency => 3,
377        CurveIntersectionDegeneracy::BoundaryCrossing => 2,
378        CurveIntersectionDegeneracy::PointContact => 1,
379        CurveIntersectionDegeneracy::Unresolved => 0,
380    }
381}
382
383/// Two parameter boxes describe the same contact.
384///
385/// Adjacent Bézier cells share an endpoint, so the same root is reachable from
386/// both sides of a knot. Ownership is resolved by comparing the closed boxes
387/// exactly: subdivision copies endpoints bit-for-bit rather than recomputing
388/// them, so equal boxes really are the same contact and not two near-misses.
389fn same_contact(left: &CurvePairParameterBox, right: &CurvePairParameterBox) -> bool {
390    left.first.start == right.first.start
391        && left.first.end == right.first.end
392        && left.second.start == right.second.start
393        && left.second.end == right.second.end
394}
395
396fn push_contact(
397    contacts: &mut Vec<ClassifiedCurveContact2>,
398    parameters: CurvePairParameterBox,
399    classification: CurveIntersectionDegeneracy,
400) -> GeomResult<()> {
401    if let Some(existing) = contacts
402        .iter_mut()
403        .find(|contact| same_contact(&contact.parameters, &parameters))
404    {
405        // Keep the strongest proof for a box we have already claimed instead of
406        // reporting the same contact twice under two different classes.
407        if classification_rank(classification) > classification_rank(existing.classification) {
408            existing.classification = classification;
409        }
410        return Ok(());
411    }
412    push_result(
413        contacts,
414        ClassifiedCurveContact2 {
415            parameters,
416            classification,
417        },
418    )
419}
420
421/// Collapse contacts that meet across a shared cell boundary.
422///
423/// Krawczyk proves roots in the STRICT interior of a box. A root lying exactly
424/// on a boundary shared by adjacent product cells is therefore never proved
425/// transverse: it fragments into up to four touching unresolved boxes, one per
426/// incident cell. Reporting four contacts for one geometric root is exactly
427/// the double-counting #5 forbids, so touching boxes of equal class are fused
428/// into their hull and the fused box owns the contact.
429fn fuse_touching(mut contacts: Vec<ClassifiedCurveContact2>) -> Vec<ClassifiedCurveContact2> {
430    let mut fused: Vec<ClassifiedCurveContact2> = Vec::new();
431    for contact in contacts.drain(..) {
432        if let Some(existing) = fused.iter_mut().find(|owner| {
433            // Overlap spans stay separate.
434            owner.classification == contact.classification
435                && owner.classification != CurveIntersectionDegeneracy::Overlap
436                && intervals_touch(owner.parameters.first, contact.parameters.first)
437                && intervals_touch(owner.parameters.second, contact.parameters.second)
438        }) {
439            existing.parameters = hull_box(existing.parameters, contact.parameters);
440            continue;
441        }
442        fused.push(contact);
443    }
444    fused
445}
446
447fn hull_box(left: CurvePairParameterBox, right: CurvePairParameterBox) -> CurvePairParameterBox {
448    CurvePairParameterBox {
449        first: hull_interval(left.first, right.first),
450        second: hull_interval(left.second, right.second),
451    }
452}
453
454fn hull_interval(left: ParameterInterval, right: ParameterInterval) -> ParameterInterval {
455    ParameterInterval {
456        start: left.start.min(right.start),
457        end: left.end.max(right.end),
458    }
459}
460
461fn degenerate(
462    contacts: Vec<ClassifiedCurveContact2>,
463    visited_nodes: u32,
464) -> CertifiedCurveIntersection2 {
465    let contacts = fuse_touching(contacts);
466    let classification = contacts
467        .iter()
468        .map(|contact| contact.classification)
469        .max_by_key(|&class| classification_rank(class))
470        .unwrap_or(CurveIntersectionDegeneracy::Unresolved);
471    CertifiedCurveIntersection2::Degenerate {
472        classification,
473        contacts,
474        visited_nodes,
475    }
476}
477
478/// Record a transverse root, resolving boundary ownership.
479///
480/// Two adjacent product cells share a closed edge, so a root exactly on that
481/// edge is isolated twice — once per cell. Both proofs are real; the root is
482/// not. The first owner wins and the duplicate is dropped, so a shared-endpoint
483/// crossing is reported exactly once (#5).
484fn push_root(
485    intersections: &mut Vec<TransverseCurveIntersection2>,
486    root: TransverseCurveIntersection2,
487) -> GeomResult<()> {
488    if intersections
489        .iter()
490        .any(|existing| roots_overlap(existing, &root))
491    {
492        return Ok(());
493    }
494    push_result(intersections, root)
495}
496
497/// Two isolated roots enclose the same point.
498///
499/// Certified intervals are closed, so touching intervals (`a.end == b.start`)
500/// are the shared-endpoint case that ownership must collapse. Disjoint
501/// intervals are genuinely different roots and are both kept.
502fn roots_overlap(
503    left: &TransverseCurveIntersection2,
504    right: &TransverseCurveIntersection2,
505) -> bool {
506    intervals_touch(left.first_parameter, right.first_parameter)
507        && intervals_touch(left.second_parameter, right.second_parameter)
508}
509
510fn intervals_touch(left: ParameterInterval, right: ParameterInterval) -> bool {
511    left.start <= right.end && right.start <= left.end
512}
513
514/// Classify a box that survived subdivision to the depth limit.
515///
516/// Reaching the limit means Krawczyk never proved a transverse root. For a
517/// genuine transverse crossing that should not happen: transversality makes the
518/// Jacobian invertible and the operator contractive. The usual cause is
519/// TANGENCY — the tangent directions align, the determinant approaches zero,
520/// and no contraction exists.
521///
522/// This is a positive proof, not an inference from failure. Over the WHOLE
523/// box, conservative derivative interval hulls must bound the cross product
524/// `d1 x d2` to an interval containing zero while the residual is not excluded.
525/// Evaluating parallelism at the midpoints alone would be unsound: two float
526/// midpoints of a transverse pair can be accidentally parallel. If the cross
527/// product is certainly nonzero, or either derivative hull contains the zero
528/// vector (a cusp or stationary parameterisation), the box stays `Unresolved`
529/// rather than carrying a class the code cannot prove.
530fn classify_exhausted_box(
531    first: &crate::certified_bezier::Cell,
532    second: &crate::certified_bezier::Cell,
533    first_base: &crate::certified_bezier::Cell,
534    second_base: &crate::certified_bezier::Cell,
535) -> GeomResult<CurveIntersectionDegeneracy> {
536    let first_derivative = first.derivative_intervals()?;
537    let second_derivative = second.derivative_intervals()?;
538
539    // A hull containing the zero vector cannot certify a direction at all.
540    if derivative_hull_contains_zero(first_derivative)
541        || derivative_hull_contains_zero(second_derivative)
542    {
543        return Ok(CurveIntersectionDegeneracy::Unresolved);
544    }
545
546    // 2D cross product over interval hulls: d1.x*d2.y - d1.y*d2.x.
547    let cross = first_derivative[0]
548        .multiply(second_derivative[1])?
549        .subtract(first_derivative[1].multiply(second_derivative[0])?)?;
550    if cross.contains_zero() {
551        return Ok(CurveIntersectionDegeneracy::Tangency);
552    }
553    // The residual is not excluded and the tangents are certainly not parallel,
554    // so a transverse crossing exists here. Strict-interior isolation simply
555    // cannot name it, because it sits on the shared cell boundary.
556    // `BoundaryCrossing` may only be claimed when the box actually reaches a
557    // shared cell endpoint. Budget exhaustion in a cell interior proves
558    // nothing and stays `Unresolved`.
559    let on_boundary =
560        touches_base_endpoint(first, first_base) || touches_base_endpoint(second, second_base);
561    if on_boundary {
562        return Ok(CurveIntersectionDegeneracy::BoundaryCrossing);
563    }
564    Ok(CurveIntersectionDegeneracy::Unresolved)
565}
566
567/// The refined box reaches an endpoint of its originating Bezier cell.
568///
569/// Subdivision copies endpoints bit-for-bit, so an exact comparison identifies
570/// the shared-boundary case without a tolerance.
571fn touches_base_endpoint(
572    refined: &crate::certified_bezier::Cell,
573    base: &crate::certified_bezier::Cell,
574) -> bool {
575    refined.start == base.start || refined.end == base.end
576}
577
578fn derivative_hull_contains_zero(derivative: [Interval; 3]) -> bool {
579    derivative[0].contains_zero() && derivative[1].contains_zero()
580}
581
582fn push_result<T>(target: &mut Vec<T>, value: T) -> GeomResult<()> {
583    target
584        .try_reserve(1)
585        .map_err(|_| GeomError::BudgetExceeded {
586            resource: "certified curve intersection result allocation",
587        })?;
588    target.push(value);
589    Ok(())
590}
591
592fn residual_excludes_zero(
593    first: &crate::certified_bezier::Cell,
594    second: &crate::certified_bezier::Cell,
595) -> GeomResult<bool> {
596    let first = first.coordinate_intervals()?;
597    let second = second.coordinate_intervals()?;
598    Ok(!first[0].subtract(second[0])?.contains_zero()
599        || !first[1].subtract(second[1])?.contains_zero())
600}
601
602fn pair_box(
603    first: &crate::certified_bezier::Cell,
604    second: &crate::certified_bezier::Cell,
605) -> CurvePairParameterBox {
606    CurvePairParameterBox {
607        first: ParameterInterval {
608            start: first.start,
609            end: first.end,
610        },
611        second: ParameterInterval {
612            start: second.start,
613            end: second.end,
614        },
615    }
616}
617
618fn krawczyk_root(
619    first_curve: &BSplineCurve2,
620    second_curve: &BSplineCurve2,
621    first: &crate::certified_bezier::Cell,
622    second: &crate::certified_bezier::Cell,
623) -> GeomResult<Option<TransverseCurveIntersection2>> {
624    let first_mid = first.start * 0.5 + first.end * 0.5;
625    let second_mid = second.start * 0.5 + second.end * 0.5;
626    let first_jet = bspline_jet2(first_curve, first_mid)?;
627    let second_jet = bspline_jet2(second_curve, second_mid)?;
628    let j00 = first_jet.first.x;
629    let j01 = -second_jet.first.x;
630    let j10 = first_jet.first.y;
631    let j11 = -second_jet.first.y;
632    let determinant = j00 * j11 - j01 * j10;
633    if determinant == 0.0 || !determinant.is_finite() {
634        return Ok(None);
635    }
636    let inverse = [
637        [j11 / determinant, -j01 / determinant],
638        [-j10 / determinant, j00 / determinant],
639    ];
640    if inverse.iter().flatten().any(|value| !value.is_finite()) {
641        return Ok(None);
642    }
643
644    let first_point = first.midpoint_point()?.euclidean()?;
645    let second_point = second.midpoint_point()?.euclidean()?;
646    let residual = [
647        first_point[0].subtract(second_point[0])?,
648        first_point[1].subtract(second_point[1])?,
649    ];
650    let first_derivative = first.derivative_intervals()?;
651    let second_derivative = second.derivative_intervals()?;
652    let minus_one = Interval::exact(-1.0)?;
653    let jacobian = [
654        [
655            first_derivative[0],
656            second_derivative[0].multiply(minus_one)?,
657        ],
658        [
659            first_derivative[1],
660            second_derivative[1].multiply(minus_one)?,
661        ],
662    ];
663
664    let corrected = [
665        Interval::exact(first_mid)?.subtract(linear_combination(
666            inverse[0][0],
667            residual[0],
668            inverse[0][1],
669            residual[1],
670        )?)?,
671        Interval::exact(second_mid)?.subtract(linear_combination(
672            inverse[1][0],
673            residual[0],
674            inverse[1][1],
675            residual[1],
676        )?)?,
677    ];
678    let zero = Interval::exact(0.0)?;
679    let one = Interval::exact(1.0)?;
680    let matrix = [
681        [
682            one.subtract(linear_combination(
683                inverse[0][0],
684                jacobian[0][0],
685                inverse[0][1],
686                jacobian[1][0],
687            )?)?,
688            zero.subtract(linear_combination(
689                inverse[0][0],
690                jacobian[0][1],
691                inverse[0][1],
692                jacobian[1][1],
693            )?)?,
694        ],
695        [
696            zero.subtract(linear_combination(
697                inverse[1][0],
698                jacobian[0][0],
699                inverse[1][1],
700                jacobian[1][0],
701            )?)?,
702            one.subtract(linear_combination(
703                inverse[1][0],
704                jacobian[0][1],
705                inverse[1][1],
706                jacobian[1][1],
707            )?)?,
708        ],
709    ];
710    // The two endpoint differences have opposite signs. Their interval hull,
711    // not their interval sum, is the centered parameter box.
712    let delta = [
713        Interval::hull([
714            Interval::exact(first.start)?.subtract(Interval::exact(first_mid)?)?,
715            Interval::exact(first.end)?.subtract(Interval::exact(first_mid)?)?,
716        ])?,
717        Interval::hull([
718            Interval::exact(second.start)?.subtract(Interval::exact(second_mid)?)?,
719            Interval::exact(second.end)?.subtract(Interval::exact(second_mid)?)?,
720        ])?,
721    ];
722    let image = [
723        corrected[0]
724            .add(matrix[0][0].multiply(delta[0])?)?
725            .add(matrix[0][1].multiply(delta[1])?)?,
726        corrected[1]
727            .add(matrix[1][0].multiply(delta[0])?)?
728            .add(matrix[1][1].multiply(delta[1])?)?,
729    ];
730    if !(image[0].lower() > first.start
731        && image[0].upper() < first.end
732        && image[1].lower() > second.start
733        && image[1].upper() < second.end)
734    {
735        return Ok(None);
736    }
737
738    let determinant_interval = jacobian[0][0]
739        .multiply(jacobian[1][1])?
740        .subtract(jacobian[0][1].multiply(jacobian[1][0])?)?;
741    let determinant_lower = determinant_interval.absolute_lower_bound();
742    if determinant_lower == 0.0 {
743        return Ok(None);
744    }
745    let first_parameter = image[0].lower() * 0.5 + image[0].upper() * 0.5;
746    let second_parameter = image[1].lower() * 0.5 + image[1].upper() * 0.5;
747    let first_value = bspline_jet2(first_curve, first_parameter)?.point;
748    let second_value = bspline_jet2(second_curve, second_parameter)?.point;
749    let residual = distance_between_point_intervals_upper(
750        point_intervals(first_value)?,
751        point_intervals(second_value)?,
752        2,
753    )?;
754    Ok(Some(TransverseCurveIntersection2 {
755        first_parameter: ParameterInterval {
756            start: image[0].lower(),
757            end: image[0].upper(),
758        },
759        second_parameter: ParameterInterval {
760            start: image[1].lower(),
761            end: image[1].upper(),
762        },
763        point: Point2::new(
764            first_value.x * 0.5 + second_value.x * 0.5,
765            first_value.y * 0.5 + second_value.y * 0.5,
766        ),
767        residual_upper_bound: residual,
768        jacobian_determinant_lower_bound: determinant_lower,
769    }))
770}
771
772fn point_intervals(point: Point2) -> GeomResult<[Interval; 3]> {
773    Ok([
774        Interval::exact(point.x)?,
775        Interval::exact(point.y)?,
776        Interval::exact(0.0)?,
777    ])
778}
779
780fn stable_start(cell: Scalar, root: Scalar, desired: Scalar) -> Scalar {
781    if desired > cell {
782        desired.min(root)
783    } else {
784        root
785    }
786}
787
788fn stable_end(cell: Scalar, root: Scalar, desired: Scalar) -> Scalar {
789    if desired < cell {
790        desired.max(root)
791    } else {
792        root
793    }
794}
795
796fn stable_contraction(
797    cell: &crate::certified_bezier::Cell,
798    root: ParameterInterval,
799    tolerance: Scalar,
800) -> ParameterInterval {
801    let center = midpoint(root);
802    let half = tolerance * 0.5;
803    ParameterInterval {
804        start: stable_start(cell.start, root.start, center - half),
805        end: stable_end(cell.end, root.end, center + half),
806    }
807}
808
809fn contract_interval(
810    cell: &crate::certified_bezier::Cell,
811    interval: ParameterInterval,
812    tolerance: Scalar,
813) -> ParameterInterval {
814    if cell.end - cell.start <= tolerance {
815        ParameterInterval {
816            start: cell.start,
817            end: cell.end,
818        }
819    } else {
820        stable_contraction(cell, interval, tolerance)
821    }
822}
823
824fn split_parameter_interval(
825    interval: ParameterInterval,
826) -> GeomResult<(ParameterInterval, ParameterInterval)> {
827    let split = midpoint(interval);
828    if split <= interval.start || split >= interval.end {
829        return Err(GeomError::Degenerate(
830            "certified curve intersection parameter split did not advance".to_owned(),
831        ));
832    }
833    Ok((
834        ParameterInterval {
835            start: interval.start,
836            end: split,
837        },
838        ParameterInterval {
839            start: split,
840            end: interval.end,
841        },
842    ))
843}
844
845fn linear_combination(
846    left_scalar: Scalar,
847    left: Interval,
848    right_scalar: Scalar,
849    right: Interval,
850) -> GeomResult<Interval> {
851    Interval::exact(left_scalar)?
852        .multiply(left)?
853        .add(Interval::exact(right_scalar)?.multiply(right)?)
854}
855
856fn classify_lines(
857    first: &BSplineCurve2,
858    second: &BSplineCurve2,
859    visited_nodes: u32,
860    boxes: Vec<CurvePairParameterBox>,
861    options: CertifiedCurveIntersectionOptions,
862) -> GeomResult<CertifiedCurveIntersection2> {
863    let [a, b] = [first.control_points[0], first.control_points[1]];
864    let [c, d] = [second.control_points[0], second.control_points[1]];
865    if a == b || c == d {
866        let intersects = match (a == b, c == d) {
867            (true, true) => a == c,
868            (true, false) => point_on_segment(a, c, d),
869            (false, true) => point_on_segment(c, a, b),
870            (false, false) => unreachable!("a degenerate segment was already established"),
871        };
872        return Ok(if intersects {
873            let mut contacts = Vec::new();
874            for owned in boxes {
875                push_contact(
876                    &mut contacts,
877                    owned,
878                    CurveIntersectionDegeneracy::PointContact,
879                )?;
880            }
881            degenerate(contacts, visited_nodes)
882        } else {
883            CertifiedCurveIntersection2::Complete {
884                intersections: Vec::new(),
885                visited_nodes,
886            }
887        });
888    }
889    let signs = [sign(a, b, c), sign(a, b, d), sign(c, d, a), sign(c, d, b)];
890    let first_collinear = signs[0] == Sign::Zero && signs[1] == Sign::Zero;
891    if first_collinear {
892        let classification = if boxes_overlap_positive(a, b, c, d) {
893            CurveIntersectionDegeneracy::Overlap
894        } else if boxes_overlap(a, b, c, d) {
895            CurveIntersectionDegeneracy::Tangency
896        } else {
897            return Ok(CertifiedCurveIntersection2::Complete {
898                intersections: Vec::new(),
899                visited_nodes,
900            });
901        };
902        return Ok(degenerate(
903            {
904                let mut contacts = Vec::new();
905                for owned in boxes {
906                    push_contact(&mut contacts, owned, classification)?;
907                }
908                contacts
909            },
910            visited_nodes,
911        ));
912    }
913    let intersects = !same_strict_sign(signs[0], signs[1])
914        && !same_strict_sign(signs[2], signs[3])
915        && boxes_overlap(a, b, c, d);
916    if !intersects {
917        return Ok(CertifiedCurveIntersection2::Complete {
918            intersections: Vec::new(),
919            visited_nodes,
920        });
921    }
922
923    let rx = Interval::exact(b.x)?.subtract(Interval::exact(a.x)?)?;
924    let ry = Interval::exact(b.y)?.subtract(Interval::exact(a.y)?)?;
925    let sx = Interval::exact(d.x)?.subtract(Interval::exact(c.x)?)?;
926    let sy = Interval::exact(d.y)?.subtract(Interval::exact(c.y)?)?;
927    let determinant = rx.multiply(sy)?.subtract(ry.multiply(sx)?)?;
928    let ox = Interval::exact(c.x)?.subtract(Interval::exact(a.x)?)?;
929    let oy = Interval::exact(c.y)?.subtract(Interval::exact(a.y)?)?;
930    let t = ox
931        .multiply(sy)?
932        .subtract(oy.multiply(sx)?)?
933        .divide_nonzero(determinant)?;
934    let u = ox
935        .multiply(ry)?
936        .subtract(oy.multiply(rx)?)?
937        .divide_nonzero(determinant)?;
938    let first_interval = native_parameter_interval(first, t)?;
939    let second_interval = native_parameter_interval(second, u)?;
940    if !intervals_resolved(
941        first_interval,
942        second_interval,
943        options.parameter_tolerance(),
944    ) {
945        return Ok(unresolved_intervals(
946            first_interval,
947            second_interval,
948            visited_nodes,
949        ));
950    }
951    let first_parameter = midpoint(first_interval);
952    let second_parameter = midpoint(second_interval);
953    let first_point = bspline_jet2(first, first_parameter)?.point;
954    let second_point = bspline_jet2(second, second_parameter)?.point;
955    let residual = distance_between_point_intervals_upper(
956        point_intervals(first_point)?,
957        point_intervals(second_point)?,
958        2,
959    )?;
960    let point = Point2::new(
961        first_point.x * 0.5 + second_point.x * 0.5,
962        first_point.y * 0.5 + second_point.y * 0.5,
963    );
964
965    Ok(CertifiedCurveIntersection2::Complete {
966        intersections: vec![TransverseCurveIntersection2 {
967            first_parameter: first_interval,
968            second_parameter: second_interval,
969            point,
970            residual_upper_bound: residual,
971            jacobian_determinant_lower_bound: determinant_lower(a, b, c, d)?,
972        }],
973        visited_nodes,
974    })
975}
976
977fn determinant_lower(a: Point2, b: Point2, c: Point2, d: Point2) -> GeomResult<Scalar> {
978    let rx = Interval::exact(b.x)?.subtract(Interval::exact(a.x)?)?;
979    let ry = Interval::exact(b.y)?.subtract(Interval::exact(a.y)?)?;
980    let sx = Interval::exact(d.x)?.subtract(Interval::exact(c.x)?)?;
981    let sy = Interval::exact(d.y)?.subtract(Interval::exact(c.y)?)?;
982    Ok(rx
983        .multiply(sy)?
984        .subtract(ry.multiply(sx)?)?
985        .absolute_lower_bound())
986}
987
988fn structural_start_tangency(line: &BSplineCurve2, quadratic: &BSplineCurve2) -> bool {
989    if !linear_polynomial(line)
990        || quadratic.degree != 2
991        || quadratic.weights.is_some()
992        || quadratic.knots.len() != 2
993        || quadratic.control_points.len() != 3
994    {
995        return false;
996    }
997    let [a, b] = [line.control_points[0], line.control_points[1]];
998    let q = &quadratic.control_points;
999    a == q[0] && q[1] != a && sign(a, b, q[1]) == Sign::Zero && sign(a, b, q[2]) != Sign::Zero
1000}
1001
1002fn linear_polynomial(curve: &BSplineCurve2) -> bool {
1003    curve.degree == 1
1004        && curve.weights.is_none()
1005        && curve.knots.len() == 2
1006        && curve.control_points.len() == 2
1007}
1008
1009fn sign(a: Point2, b: Point2, c: Point2) -> Sign {
1010    orient2d(a, b, c).sign().expect("orient2d always escalates")
1011}
1012
1013fn same_strict_sign(a: Sign, b: Sign) -> bool {
1014    a != Sign::Zero && a == b
1015}
1016
1017fn has_control_point_extent(curve: &BSplineCurve2) -> bool {
1018    curve
1019        .control_points
1020        .first()
1021        .is_some_and(|first| curve.control_points.iter().any(|point| point != first))
1022}
1023
1024fn point_on_segment(point: Point2, start: Point2, end: Point2) -> bool {
1025    sign(start, end, point) == Sign::Zero && boxes_overlap(point, point, start, end)
1026}
1027
1028fn boxes_overlap(a: Point2, b: Point2, c: Point2, d: Point2) -> bool {
1029    let overlap = |a0: Scalar, a1: Scalar, b0: Scalar, b1: Scalar| {
1030        a0.min(a1) <= b0.max(b1) && b0.min(b1) <= a0.max(a1)
1031    };
1032    overlap(a.x, b.x, c.x, d.x) && overlap(a.y, b.y, c.y, d.y)
1033}
1034
1035fn boxes_overlap_positive(a: Point2, b: Point2, c: Point2, d: Point2) -> bool {
1036    let width = |a0: Scalar, a1: Scalar, b0: Scalar, b1: Scalar| {
1037        a0.max(a1).min(b0.max(b1)) - a0.min(a1).max(b0.min(b1))
1038    };
1039    boxes_overlap(a, b, c, d)
1040        && (width(a.x, b.x, c.x, d.x) > 0.0 || width(a.y, b.y, c.y, d.y) > 0.0)
1041}
1042
1043fn midpoint(interval: ParameterInterval) -> Scalar {
1044    interval.start * 0.5 + interval.end * 0.5
1045}
1046
1047fn intervals_resolved(
1048    first: ParameterInterval,
1049    second: ParameterInterval,
1050    tolerance: Scalar,
1051) -> bool {
1052    first.end - first.start <= tolerance && second.end - second.start <= tolerance
1053}
1054
1055fn unresolved_box(box_: CurvePairParameterBox, visited_nodes: u32) -> CertifiedCurveIntersection2 {
1056    CertifiedCurveIntersection2::Degenerate {
1057        classification: CurveIntersectionDegeneracy::Unresolved,
1058        contacts: vec![ClassifiedCurveContact2 {
1059            parameters: box_,
1060            classification: CurveIntersectionDegeneracy::Unresolved,
1061        }],
1062        visited_nodes,
1063    }
1064}
1065
1066fn unresolved_intervals(
1067    first: ParameterInterval,
1068    second: ParameterInterval,
1069    visited: u32,
1070) -> CertifiedCurveIntersection2 {
1071    unresolved_box(CurvePairParameterBox { first, second }, visited)
1072}
1073
1074fn checked_parameter_interval(start: Scalar, end: Scalar) -> GeomResult<ParameterInterval> {
1075    (start <= end)
1076        .then_some(ParameterInterval { start, end })
1077        .ok_or_else(|| {
1078            GeomError::Degenerate("certified line parameter interval is empty".to_owned())
1079        })
1080}
1081
1082fn native_interval(bounds: ParameterInterval, value: Interval) -> GeomResult<ParameterInterval> {
1083    checked_parameter_interval(
1084        value.lower().max(bounds.start),
1085        value.upper().min(bounds.end),
1086    )
1087}
1088
1089fn native_parameter_interval_inner(
1090    bounds: ParameterInterval,
1091    unit: Interval,
1092) -> GeomResult<ParameterInterval> {
1093    let span = Interval::exact(bounds.end)?.subtract(Interval::exact(bounds.start)?)?;
1094    native_interval(
1095        bounds,
1096        Interval::exact(bounds.start)?.add(unit.multiply(span)?)?,
1097    )
1098}
1099
1100fn native_parameter_interval(
1101    curve: &BSplineCurve2,
1102    unit: Interval,
1103) -> GeomResult<ParameterInterval> {
1104    native_parameter_interval_inner(domain(curve), unit)
1105}
1106
1107fn domain(curve: &BSplineCurve2) -> ParameterInterval {
1108    ParameterInterval {
1109        start: curve.knots[0],
1110        end: curve.knots[curve.knots.len() - 1],
1111    }
1112}
1113
1114#[cfg(test)]
1115mod pending_storage_tests {
1116    use super::{CurvePairParameterBox, PendingCurvePair};
1117
1118    #[test]
1119    fn pending_curve_pairs_store_only_indices_parameters_and_depth() {
1120        let raw = 2 * size_of::<usize>() + size_of::<CurvePairParameterBox>() + size_of::<u16>();
1121        let alignment = align_of::<PendingCurvePair>();
1122        let expected = raw.next_multiple_of(alignment);
1123        assert_eq!(size_of::<PendingCurvePair>(), expected);
1124    }
1125}