Skip to main content

ogeom_intersect/
curve_surface.rs

1//! Where a curve pierces a surface.
2//!
3//! Two consumers drive it: where an edge crosses a face in the boolean, and
4//! the exact point-in-solid classifier, which is a ray/surface query per
5//! face.
6//!
7//! # Well-posed
8//!
9//! `C(t) = S(u, v)` is three equations in three unknowns; unlike the
10//! surface/surface system, nothing has to be pinned for Newton to converge to
11//! a point. The analytic cases are still answered in closed form first: a line
12//! against a plane or a quadric is a linear or quadratic equation, and solving
13//! a quadratic by iteration would be slower and less exact than writing down
14//! its roots.
15//!
16//! # A curve lying in the surface
17//!
18//! A line in a plane crosses it nowhere and everywhere. That is an overlap
19//! (the parameter range of the curve that lies in the surface), and it is a
20//! different answer from any list of points. Detected where the analytic
21//! forms can see it, and on the general path as a run of piercings between
22//! which the curve never leaves the surface.
23
24use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
25use ogeom_geom::{Curve, Curve3d, Surface, SurfaceGeometry};
26use ogeom_math::{Point, Vector, solve};
27
28use crate::march::{Cell, sample_by, segment_meets_triangle};
29
30/// One piercing of a surface by a curve.
31#[derive(Debug, Clone, Copy, PartialEq)]
32pub struct Piercing {
33    /// The parameter on the curve.
34    pub on_curve: f64,
35    /// The parameters on the surface.
36    pub on_surface: (f64, f64),
37    /// Where, taken from the curve.
38    pub point: Point,
39    /// The distance between the two evaluations there.
40    pub gap: f64,
41}
42
43/// What a curve does to a surface.
44#[derive(Debug, Clone, PartialEq)]
45pub struct CurveSurfaceIntersection {
46    /// Isolated piercings, in order along the curve.
47    pub crossings: Vec<Piercing>,
48    /// Parameter ranges of the curve that lie *in* the surface.
49    ///
50    /// Detected for the analytic cases (a line in a plane) and, on the
51    /// general path, as a run of piercings longer than the curve's sampling
52    /// step between which the curve never leaves the surface.
53    pub lying: Vec<(f64, f64)>,
54}
55
56impl CurveSurfaceIntersection {
57    /// No contact found.
58    #[must_use]
59    pub fn is_empty(&self) -> bool {
60        self.crossings.is_empty() && self.lying.is_empty()
61    }
62
63    const fn empty() -> Self {
64        Self {
65            crossings: Vec::new(),
66            lying: Vec::new(),
67        }
68    }
69}
70
71/// How hard the general path looks.
72#[derive(Debug, Clone, Copy, PartialEq)]
73pub struct CurveSurfaceOptions {
74    /// How many segments the curve is sampled into for seeding.
75    pub samples: usize,
76    /// How finely the surface is sampled, per direction.
77    pub grid: usize,
78    /// The widest gap that still counts as a piercing.
79    pub gap: f64,
80}
81
82impl Default for CurveSurfaceOptions {
83    fn default() -> Self {
84        Self {
85            samples: 128,
86            grid: 24,
87            gap: 1e-7,
88        }
89    }
90}
91
92/// Where a curve pierces a surface.
93///
94/// Analytic line/plane, line/sphere and line/cylinder are answered in closed
95/// form; everything else is seeded polyhedrally and polished by Newton on the
96/// well-posed three-by-three system.
97///
98/// # Errors
99///
100/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if the options
101/// are unusable.
102pub fn intersect_curve_surface(
103    curve: &Curve,
104    surface: &SurfaceGeometry,
105    options: CurveSurfaceOptions,
106    tol: Tolerances,
107) -> OgeomResult<CurveSurfaceIntersection> {
108    if let (Curve::Line(_), SurfaceGeometry::BSpline(spline)) = (curve, surface)
109        && options.samples >= 2
110        && options.gap.is_finite()
111        && options.gap > 0.0
112        && let Some(patches) = crate::bezier_clip::Patches::of(spline, tol)?
113    {
114        return clipped(curve, surface, &patches, options, tol);
115    }
116    intersect_with(curve, surface, options, tol, || {
117        let cells = sample_by(surface, seeding(surface, options.grid), tol);
118        let tree = CellTree::over(&cells, options.gap);
119        (
120            std::borrow::Cow::Owned(cells),
121            std::borrow::Cow::Owned(tree),
122        )
123    })
124}
125
126/// Where a curve touches a surface without crossing it, solved from
127/// `seed` on the curve: the touch, or `None` if there is none beside the
128/// seed.
129///
130/// At a touch the gap between the curve and the surface falls to zero
131/// without changing sign, so `C(t) = S(u, v)` has a double root there:
132/// Newton on it slows to a halving of the distance per step and stops where
133/// the gap, the square of the distance left, meets its tolerance, short of
134/// the touch by the square root of that tolerance times the radius the two
135/// part at. [`intersect_curve_surface`] lands anywhere in that valley. The
136/// touch is solved here as one instead, from the seed's foot on the
137/// surface: where that foot is the curve point's nearest (two rows) and the
138/// curve runs in the surface's tangent plane (one row), rows that stay
139/// regular where the two part at second order. It is kept where the curve
140/// and the surface are within the confusion distance there.
141#[must_use]
142pub fn touch_curve_surface(
143    curve: &Curve,
144    surface: &SurfaceGeometry,
145    seed: f64,
146    tol: Tolerances,
147) -> Option<Piercing> {
148    let foot =
149        ogeom_geom::project_on_surface(surface, curve.point_at(seed, tol).ok()?, 16, tol).ok()?;
150    let along = |t: f64| {
151        let d = curve.derivatives_at(t, 2, tol).ok()?;
152        Some((Point::ORIGIN + *d.first()?, *d.get(1)?, *d.get(2)?))
153    };
154    let start = [seed, foot.parameters.0, foot.parameters.1];
155    let [t, u, v] = touch_solved(along, surface, start, tol)?;
156    let (lo, hi) = curve.domain();
157    let ((ua, ub), (va, vb)) = surface.domain();
158    let within = |x: f64, lo: f64, hi: f64, periodic: bool| {
159        periodic || (x >= lo - tol.parametric() && x <= hi + tol.parametric())
160    };
161    if !within(t, lo, hi, curve.is_periodic())
162        || !within(u, ua, ub, surface.is_periodic_u())
163        || !within(v, va, vb, surface.is_periodic_v())
164    {
165        return None;
166    }
167    let point = curve.point_at(t, tol).ok()?;
168    let gap = point.distance(surface.point_at(u, v, tol).ok()?);
169    (gap <= tol.confusion()).then_some(Piercing {
170        on_curve: t,
171        on_surface: (u, v),
172        point,
173        gap,
174    })
175}
176
177/// Where a curve touches a surface, solved from `start`, a parameter on the
178/// curve and one on the surface beside the touch: the three there, or
179/// `None` where the solve does not find a touch beside the start.
180///
181/// `along` gives the curve's point and first two derivatives at a
182/// parameter. The curve touches the surface where the surface's foot of it
183/// is its closest point (two rows), and the curve runs in the surface's
184/// tangent plane (one row). The three rows are regular where the curve and
185/// the surface part at second order. The gap along the normal is not a row,
186/// as it touches zero without crossing it: the caller measures it.
187///
188/// The solve is kept only where its linear model holds over the move it
189/// made, by Kantorovich's condition as [`crate::walk::settle`] asks it:
190/// `‖J₀⁻¹·(J₁ − J₀)‖ ≤ ½` in the Frobenius norm, which is never less than
191/// the operator norm. A start further from the touch than that, or one
192/// beside a crossing, where the curve turns into the surface's tangent
193/// plane only away from the crossing if at all, is not trusted to have
194/// found a touch beside it.
195pub(crate) fn touch_solved(
196    along: impl Fn(f64) -> Option<(Point, Vector, Vector)>,
197    surface: &SurfaceGeometry,
198    start: [f64; 3],
199    tol: Tolerances,
200) -> Option<[f64; 3]> {
201    let system = |x: &[f64; 3]| -> Option<([f64; 3], [[f64; 3]; 3])> {
202        let (p, c, c2) = along(x[0])?;
203        let (q, ou, ov) = surface.point_d1_at(x[1], x[2], tol).ok()?;
204        let (ouu, ouv, ovv) = surface.d2_at(x[1], x[2], tol).ok()?;
205        let gap = p - q;
206        let normal = ou.cross(ov);
207        let residual = [gap.dot(ou), gap.dot(ov), c.dot(normal)];
208        let jacobian = [
209            [
210                c.dot(ou),
211                gap.dot(ouu) - ou.dot(ou),
212                gap.dot(ouv) - ov.dot(ou),
213            ],
214            [
215                c.dot(ov),
216                gap.dot(ouv) - ou.dot(ov),
217                gap.dot(ovv) - ov.dot(ov),
218            ],
219            [
220                c2.dot(normal),
221                c.dot(ouu.cross(ov) + ou.cross(ouv)),
222                c.dot(ouv.cross(ov) + ou.cross(ovv)),
223            ],
224        ];
225        Some((residual, jacobian))
226    };
227    let (_, jacobian) = system(&start)?;
228    // The rows are products of derivatives with no common scale, so the
229    // solve is run until its steps stop moving the parameters, and what it
230    // found is a root where the step from there would not move them either:
231    // at the root the damped search cannot lower the residual further, and
232    // reports running out.
233    let criteria = solve::Criteria {
234        residual: 0.0,
235        step: tol.parametric(),
236        max_iterations: 40,
237    };
238    let (found, _, _, _) = solve::newton_system_fixed(
239        |x| system(x).unwrap_or(([f64::INFINITY; 3], [[0.0; 3]; 3])),
240        start,
241        criteria,
242    )
243    .ok()?;
244    let (left, after) = system(&found)?;
245    let step = solve::linear_fixed(after, left)?;
246    if step.iter().any(|d| d.abs() > tol.parametric()) {
247        return None;
248    }
249    let mut drift = 0.0;
250    for column in 0..3 {
251        let change: [f64; 3] =
252            core::array::from_fn(|row| after[row][column] - jacobian[row][column]);
253        let moved = solve::linear_fixed(jacobian, change)?;
254        drift += moved.iter().map(|v| v * v).sum::<f64>();
255    }
256    (drift.sqrt() <= 0.5).then_some(found)
257}
258
259/// A surface made ready for many curve queries.
260///
261/// The general path seeds from a sampling of the surface into flat cells,
262/// and that sampling depends only on the surface and the options. Taken
263/// once, on the first query that needs it, and shared by every later one,
264/// so a caller asking the same face about many curves (a ray per drawn
265/// point, a ray per classified point) pays for it once. Every answer is the
266/// one [`intersect_curve_surface`] gives for the same curve.
267#[derive(Debug)]
268pub struct PreparedSurface {
269    surface: SurfaceGeometry,
270    options: CurveSurfaceOptions,
271    tol: Tolerances,
272    cells: std::sync::OnceLock<(Vec<Cell>, CellTree)>,
273    /// A spline's Bézier patches, for straight curves.
274    patches: std::sync::OnceLock<Option<crate::bezier_clip::Patches>>,
275}
276
277impl PreparedSurface {
278    /// Ready `surface` for queries at `options` and `tol`.
279    #[must_use]
280    pub const fn new(
281        surface: SurfaceGeometry,
282        options: CurveSurfaceOptions,
283        tol: Tolerances,
284    ) -> Self {
285        Self {
286            surface,
287            options,
288            tol,
289            cells: std::sync::OnceLock::new(),
290            patches: std::sync::OnceLock::new(),
291        }
292    }
293
294    /// The surface queried.
295    #[must_use]
296    pub const fn surface(&self) -> &SurfaceGeometry {
297        &self.surface
298    }
299
300    /// Where `curve` pierces the surface, as [`intersect_curve_surface`].
301    ///
302    /// # Errors
303    ///
304    /// As [`intersect_curve_surface`].
305    pub fn intersect(&self, curve: &Curve) -> OgeomResult<CurveSurfaceIntersection> {
306        let (surface, options, tol) = (&self.surface, self.options, self.tol);
307        // A straight curve meets a spline where its patches' control hulls
308        // say it may, and nowhere else.
309        if let (Curve::Line(_), SurfaceGeometry::BSpline(spline)) = (curve, surface)
310            && options.samples >= 2
311            && options.gap.is_finite()
312            && options.gap > 0.0
313            && let Some(patches) = self
314                .patches
315                .get_or_init(|| crate::bezier_clip::Patches::of(spline, tol).ok().flatten())
316        {
317            return clipped(curve, surface, patches, options, tol);
318        }
319        intersect_with(curve, surface, options, tol, || {
320            let (cells, tree) = self.cells.get_or_init(|| {
321                let cells = sample_by(surface, seeding(surface, options.grid), tol);
322                let tree = CellTree::over(&cells, options.gap);
323                (cells, tree)
324            });
325            (
326                std::borrow::Cow::Borrowed(cells.as_slice()),
327                std::borrow::Cow::Borrowed(tree),
328            )
329        })
330    }
331}
332
333fn intersect_with<'c>(
334    curve: &Curve,
335    surface: &SurfaceGeometry,
336    options: CurveSurfaceOptions,
337    tol: Tolerances,
338    cells: impl FnOnce() -> (std::borrow::Cow<'c, [Cell]>, std::borrow::Cow<'c, CellTree>),
339) -> OgeomResult<CurveSurfaceIntersection> {
340    if options.samples < 2 || options.grid < 2 {
341        ogeom_bail!(Construction, "seeding needs at least two steps each way");
342    }
343    if !options.gap.is_finite() || options.gap <= 0.0 {
344        ogeom_bail!(Construction, "a gap of {} is not a distance", options.gap);
345    }
346
347    match (curve, surface) {
348        (Curve::Line(line), SurfaceGeometry::Plane(p)) => {
349            Ok(line_plane(line, p.plane(), curve, surface, tol))
350        }
351        (Curve::Line(line), SurfaceGeometry::Sphere(s)) => Ok(line_quadric(
352            line,
353            curve,
354            surface,
355            sphere_roots(line, s.sphere()),
356            options,
357            tol,
358        )),
359        (Curve::Line(line), SurfaceGeometry::Cylinder(c)) => Ok(line_quadric(
360            line,
361            curve,
362            surface,
363            cylinder_roots(line, c.cylinder()),
364            options,
365            tol,
366        )),
367        (Curve::Line(line), SurfaceGeometry::Cone(c)) => Ok(line_quadric(
368            line,
369            curve,
370            surface,
371            cone_roots(line, c.cone(), tol),
372            options,
373            tol,
374        )),
375        (Curve::Line(line), SurfaceGeometry::Torus(t)) => Ok(line_quadric(
376            line,
377            curve,
378            surface,
379            torus_roots(line, t.torus()),
380            options,
381            tol,
382        )),
383        _ => {
384            let (cells, tree) = cells();
385            general(curve, surface, &cells, &tree, options, tol)
386        }
387    }
388}
389
390// --- analytic ----------------------------------------------------------------
391
392fn line_plane(
393    line: &ogeom_geom::LineCurve,
394    plane: ogeom_math::Plane,
395    curve: &Curve,
396    surface: &SurfaceGeometry,
397    tol: Tolerances,
398) -> CurveSurfaceIntersection {
399    let axis = line.axis();
400    let along = plane.normal().dot(axis.direction);
401    let height = plane.signed_distance_to(axis.location);
402
403    if along.abs() <= tol.angular() {
404        // Parallel: in the plane, or never touching it.
405        if height.abs() <= tol.confusion() {
406            return CurveSurfaceIntersection {
407                crossings: Vec::new(),
408                lying: vec![line.domain()],
409            };
410        }
411        return CurveSurfaceIntersection::empty();
412    }
413
414    let t = -height / along;
415    let (lo, hi) = line.domain();
416    if t < lo - tol.parametric() || t > hi + tol.parametric() {
417        return CurveSurfaceIntersection::empty();
418    }
419    let point = axis.location + axis.direction.vector() * t;
420    let Some(found) = invert(surface, point, curve, t, tol) else {
421        return CurveSurfaceIntersection::empty();
422    };
423    if found.gap > tol.confusion() {
424        // The crossing is real on the unbounded plane but outside this
425        // surface's stated extents; the clamped polish says so as a gap.
426        return CurveSurfaceIntersection::empty();
427    }
428    CurveSurfaceIntersection {
429        crossings: vec![found],
430        lying: Vec::new(),
431    }
432}
433
434/// How far rounding can move a discriminant `p - q` from its true value.
435fn rounding(p: f64, q: f64) -> f64 {
436    8.0 * f64::EPSILON * p.abs().max(q.abs())
437}
438
439/// The line parameters at which a line meets a sphere.
440fn sphere_roots(line: &ogeom_geom::LineCurve, sphere: ogeom_math::Sphere) -> Vec<f64> {
441    let axis = line.axis();
442    let d = axis.direction.vector();
443    let m = axis.location - sphere.centre();
444    // |m + t d|^2 = r^2, with |d| = 1.
445    let b = m.dot(d);
446    let c = sphere.radius().mul_add(-sphere.radius(), m.dot(m));
447    let discriminant = b.mul_add(b, -c);
448    // A tangent line has a discriminant of exactly zero, which rounding
449    // leaves a few ulps either side; below zero by no more than that is the
450    // tangency, and the polish that follows rejects a false one by its gap.
451    if discriminant < -rounding(b * b, c) {
452        return Vec::new();
453    }
454    let root = discriminant.max(0.0).sqrt();
455    if root == 0.0 {
456        vec![-b]
457    } else {
458        vec![-b - root, -b + root]
459    }
460}
461
462/// The line parameters at which a line meets a cylinder.
463/// A line's offset from a frame's origin and its direction, in the frame's
464/// own axes.
465fn in_frame(
466    line: &ogeom_geom::LineCurve,
467    frame: ogeom_math::Frame,
468) -> (ogeom_math::Vector, ogeom_math::Vector) {
469    let axis = line.axis();
470    let local = |v: ogeom_math::Vector| {
471        ogeom_math::Vector::new(
472            v.dot(frame.x().vector()),
473            v.dot(frame.y().vector()),
474            v.dot(frame.z().vector()),
475        )
476    };
477    (
478        local(axis.location - frame.origin()),
479        local(axis.direction.vector()),
480    )
481}
482
483/// The line parameters at which a line meets a cone, either nappe: the
484/// polish that follows keeps those on the cone's stated extent.
485fn cone_roots(
486    line: &ogeom_geom::LineCurve,
487    cone: ogeom_math::Cone,
488    tol: ogeom_core::Tolerances,
489) -> Vec<f64> {
490    let (m, d) = in_frame(line, cone.frame());
491    // x^2 + y^2 = (r0 + k z)^2 along m + t d.
492    let (r0, k) = (cone.reference_radius(), cone.half_angle().tan());
493    let rim = k.mul_add(m.z, r0);
494    let a = d.x.mul_add(d.x, d.y * d.y) - k * k * d.z * d.z;
495    let b = 2.0 * (m.x.mul_add(d.x, m.y * d.y) - k * d.z * rim);
496    let c = m.x.mul_add(m.x, m.y * m.y) - rim * rim;
497    ogeom_math::solve::roots(&[c, b, a], tol.parametric()).unwrap_or_default()
498}
499
500/// The line parameters at which a line meets a torus: the real roots of
501/// `(|w|^2 + R^2 - r^2)^2 = 4 R^2 (w_x^2 + w_y^2)` along `w = m + t d` in
502/// the torus's frame.
503fn torus_roots(line: &ogeom_geom::LineCurve, torus: ogeom_math::Torus) -> Vec<f64> {
504    let (m, d) = in_frame(line, torus.frame());
505    let (big, small) = (torus.major_radius(), torus.minor_radius());
506    // Solved about the line's nearest approach to the centre, in units of
507    // the torus's own size, so the roots stand near one: a torus hundreds
508    // of millimetres down the line puts them there otherwise, and the
509    // coefficients' spread costs them digits.
510    let near = -m.dot(d) / d.dot(d);
511    let size = big + small;
512    let m = (m + d * near) / size;
513    let (big, small) = (big / size, small / size);
514    let a = d.dot(d);
515    let b = 2.0 * m.dot(d);
516    let c = m.dot(m) + big * big - small * small;
517    let p = d.x.mul_add(d.x, d.y * d.y);
518    let q = 2.0 * m.x.mul_add(d.x, m.y * d.y);
519    let s = m.x.mul_add(m.x, m.y * m.y);
520    let four = 4.0 * big * big;
521    let coefficients = [
522        c.mul_add(c, -four * s),
523        2.0f64.mul_add(b * c, -four * q),
524        b.mul_add(b, 2.0 * a * c) - four * p,
525        2.0 * a * b,
526        a * a,
527    ];
528    let (found, count) = quartic_roots(&coefficients);
529    found[..count]
530        .iter()
531        .map(|sigma| near + sigma * size)
532        .collect()
533}
534
535/// The real roots of a quartic whose roots stand near one, tangencies
536/// included: a critical point where the quartic is within 1e-9 of its coefficients'
537/// size of zero is a tangency, the coefficients being differences of terms
538/// that size. Two roots a few roots of epsilon apart are merged into the
539/// one tangency rounding split them from.
540fn quartic_roots(c: &[f64; 5]) -> ([f64; 4], usize) {
541    let mut found = [0.0; 4];
542    let touch = 1e-9 * c.iter().map(|x| x.abs()).sum::<f64>();
543    let count = ogeom_math::solve::real_roots(c, touch, &mut found);
544    let mut merged = 0;
545    for i in 0..count {
546        let t = found[i];
547        if merged > 0 && (t - found[merged - 1]).abs() <= 1e-6 * (1.0 + t.abs()) {
548            found[merged - 1] = f64::midpoint(found[merged - 1], t);
549        } else {
550            found[merged] = t;
551            merged += 1;
552        }
553    }
554    (found, merged)
555}
556
557fn cylinder_roots(line: &ogeom_geom::LineCurve, cylinder: ogeom_math::Cylinder) -> Vec<f64> {
558    let axis = line.axis();
559    let w = cylinder.axis().direction.vector();
560    // Strip the components along the cylinder's axis; what is left is a 2D
561    // circle problem in the perpendicular plane.
562    let d = axis.direction.vector();
563    let m = axis.location - cylinder.axis().location;
564    let d_perp = d - w * d.dot(w);
565    let m_perp = m - w * m.dot(w);
566    let a = d_perp.dot(d_perp);
567    if a <= f64::MIN_POSITIVE {
568        // The line runs along the axis direction: on the wall it would lie,
569        // not pierce, and lying is not detected here.
570        return Vec::new();
571    }
572    let b = d_perp.dot(m_perp);
573    let c = cylinder
574        .radius()
575        .mul_add(-cylinder.radius(), m_perp.dot(m_perp));
576    let discriminant = b.mul_add(b, -(a * c));
577    if discriminant < -rounding(b * b, a * c) {
578        return Vec::new();
579    }
580    let root = discriminant.max(0.0).sqrt();
581    if root == 0.0 {
582        vec![-b / a]
583    } else {
584        vec![(-b - root) / a, (-b + root) / a]
585    }
586}
587
588/// Roots dressed as piercings, filtered by the line's own range and the
589/// surface's extents.
590fn line_quadric(
591    line: &ogeom_geom::LineCurve,
592    curve: &Curve,
593    surface: &SurfaceGeometry,
594    roots: Vec<f64>,
595    options: CurveSurfaceOptions,
596    tol: Tolerances,
597) -> CurveSurfaceIntersection {
598    let axis = line.axis();
599    let (lo, hi) = line.domain();
600    let mut crossings = Vec::new();
601    for t in roots {
602        if t < lo - tol.parametric() || t > hi + tol.parametric() {
603            continue;
604        }
605        let point = axis.location + axis.direction.vector() * t;
606        let Some(found) = invert(surface, point, curve, t, tol) else {
607            continue;
608        };
609        // The extent check is the gap. The polish clamps the surface
610        // parameters into the stated domain, so a root beyond the cylinder's
611        // height converges to the rim with a gap of exactly how far past it
612        // was: a piercing of the unbounded geometry, not of this surface.
613        if found.gap > tol.confusion() {
614            continue;
615        }
616        let _ = options;
617        crossings.push(Piercing {
618            on_curve: t,
619            on_surface: found.on_surface,
620            point,
621            gap: found.gap,
622        });
623    }
624    crossings.sort_by(|a, b| {
625        a.on_curve
626            .partial_cmp(&b.on_curve)
627            .unwrap_or(core::cmp::Ordering::Equal)
628    });
629    CurveSurfaceIntersection {
630        crossings,
631        lying: Vec::new(),
632    }
633}
634
635/// Surface parameters of a point known to lie on an analytic surface.
636///
637/// Closed-form inversion for the quadrics; refined by one Newton pass so the
638/// reported parameters evaluate back onto the point to rounding.
639fn invert(
640    surface: &SurfaceGeometry,
641    point: Point,
642    curve: &Curve,
643    on_curve: f64,
644    tol: Tolerances,
645) -> Option<Piercing> {
646    let guess = match surface {
647        SurfaceGeometry::Plane(p) => {
648            let local = p.plane().frame().to_local(point);
649            (local.x, local.y)
650        }
651        SurfaceGeometry::Sphere(s) => {
652            let local = s.sphere().frame().to_local(point);
653            let latitude = (local.z / s.sphere().radius()).clamp(-1.0, 1.0).asin();
654            (
655                local.y.atan2(local.x).rem_euclid(core::f64::consts::TAU),
656                latitude,
657            )
658        }
659        SurfaceGeometry::Cylinder(c) => {
660            let local = c.cylinder().frame().to_local(point);
661            (
662                local.y.atan2(local.x).rem_euclid(core::f64::consts::TAU),
663                local.z,
664            )
665        }
666        SurfaceGeometry::Cone(c) => {
667            ogeom_math::elementary::cone_parameters(&c.cone(), point, tol).ok()?
668        }
669        SurfaceGeometry::Torus(t) => {
670            ogeom_math::elementary::torus_parameters(&t.torus(), point, tol).ok()?
671        }
672        _ => return None,
673    };
674    // One polish step against the curve point, so parameter rounding in the
675    // inversion does not survive into the result, and the gap comes with it,
676    // because the polish clamps into the surface's extents and the gap is
677    // what says whether the clamped answer still touches the curve.
678    polish(curve, surface, on_curve, guess, tol)
679}
680
681// --- general -----------------------------------------------------------------
682
683fn general(
684    curve: &Curve,
685    surface: &SurfaceGeometry,
686    cells: &[Cell],
687    tree: &CellTree,
688    options: CurveSurfaceOptions,
689    tol: Tolerances,
690) -> OgeomResult<CurveSurfaceIntersection> {
691    let (lo, hi) = curve.domain();
692
693    let mut points = Vec::with_capacity(options.samples + 1);
694    for i in 0..=options.samples {
695        #[allow(clippy::cast_precision_loss)]
696        let t = lo + (hi - lo) * i as f64 / options.samples as f64;
697        if let Ok(p) = curve.point_at(t, tol) {
698            points.push((t, p));
699        }
700    }
701
702    let segments: Vec<((f64, Point), (f64, Point))> =
703        points.windows(2).map(|pair| (pair[0], pair[1])).collect();
704    // Which segments come near which cells, by cell index.
705    let mut pairs: Vec<(usize, usize)> = Vec::new();
706    let meet = |i: usize, c: usize, pairs: &mut Vec<(usize, usize)>| {
707        let cell = &cells[c];
708        let ((_, p0), (_, p1)) = segments[i];
709        if segment_near_cell(p0, p1, cell, options.gap.max(cell.sag)) {
710            pairs.push((i, c));
711        }
712    };
713    // Each segment asks the tree for the cells whose boxes, grown by their
714    // own margins, its box meets.
715    let mut found: Vec<usize> = Vec::new();
716    for (i, &((_, p0), (_, p1))) in segments.iter().enumerate() {
717        let low = Point::new(p0.x.min(p1.x), p0.y.min(p1.y), p0.z.min(p1.z));
718        let high = Point::new(p0.x.max(p1.x), p0.y.max(p1.y), p0.z.max(p1.z));
719        found.clear();
720        tree.visit(low, high, &mut found);
721        for &c in &found {
722            meet(i, c, &mut pairs);
723        }
724    }
725    pairs.sort_unstable();
726    let mut crossings: Vec<Piercing> = Vec::new();
727    for (segment, k) in pairs {
728        let ((t0, p0), (t1, p1)) = segments[segment];
729        let cell = &cells[k];
730        {
731            // Near the cell within the surface's own bow from it: a curve
732            // crossing the surface in the gap between the flat cell and
733            // the curved patch it stands for (a ray starting a few microns
734            // from the wall it leaves by) meets no cell, and is seeded by
735            // every segment passing within the cell's bow of its triangle.
736            if segment_meets_triangle(p0, p1, cell.corners).is_none()
737                && !(cell.sag > options.gap
738                    && segment_near_cell(p0, p1, cell, cell.sag)
739                    && segment_triangle_distance(p0, p1, cell.corners) <= cell.sag)
740            {
741                continue;
742            }
743            // Newton from where the segment meets the cell, not from the
744            // cell's corner: on a wall bowing a few hundredths of a
745            // millimetre over a cell, the corner can stand far enough off
746            // that the first step leaves the chart, and clamped at its edge
747            // the solve stalls there. The corner stays a second try.
748            let (near_t, near_uv) = seed_in(cell, p0, p1, t0, t1);
749            let Some(found) = [(near_t, near_uv), (f64::midpoint(t0, t1), cell.at)]
750                .into_iter()
751                .filter_map(|(t, uv)| polish(curve, surface, t, uv, tol))
752                .find(|found| found.gap <= options.gap)
753            else {
754                continue;
755            };
756            let reach = tol.confusion() * 100.0;
757            if !crossings
758                .iter()
759                .any(|c| c.point.distance(found.point) <= reach)
760            {
761                crossings.push(found);
762            }
763        }
764    }
765    crossings.sort_by(|a, b| {
766        a.on_curve
767            .partial_cmp(&b.on_curve)
768            .unwrap_or(core::cmp::Ordering::Equal)
769    });
770    Ok(gathered(curve, surface, crossings, options, tol))
771}
772
773/// Where a straight curve pierces a spline surface, seeded where the
774/// surface's patches' control hulls come within the gap of it.
775fn clipped(
776    curve: &Curve,
777    surface: &SurfaceGeometry,
778    patches: &crate::bezier_clip::Patches,
779    options: CurveSurfaceOptions,
780    tol: Tolerances,
781) -> OgeomResult<CurveSurfaceIntersection> {
782    let (lo, hi) = curve.domain();
783    let (a, b) = (curve.point_at(lo, tol)?, curve.point_at(hi, tol)?);
784    let reach = tol.confusion() * 100.0;
785    let mut crossings: Vec<Piercing> = Vec::new();
786    for (f, uv) in patches.seeds(a, b, options.gap) {
787        let Some(found) = polish(curve, surface, (hi - lo).mul_add(f, lo), uv, tol) else {
788            continue;
789        };
790        if found.gap > options.gap
791            || crossings
792                .iter()
793                .any(|c| c.point.distance(found.point) <= reach)
794        {
795            continue;
796        }
797        crossings.push(found);
798    }
799    crossings.sort_by(|a, b| {
800        a.on_curve
801            .partial_cmp(&b.on_curve)
802            .unwrap_or(core::cmp::Ordering::Equal)
803    });
804    Ok(gathered(curve, surface, crossings, options, tol))
805}
806
807/// Piercings gathered into the contacts they describe.
808///
809/// Newton lands a little apart each time it is started near a tangency,
810/// where the gap touches zero without crossing it, and all along a stretch
811/// of curve lying in the surface; a radius of rounding merges neither. So
812/// neighbouring piercings between which the curve never leaves the surface
813/// are one contact: a run shorter than the curve's sampling step is one
814/// tangency, kept once (where the gap is least), and a longer one is a
815/// stretch lying in the surface.
816fn gathered(
817    curve: &Curve,
818    surface: &SurfaceGeometry,
819    crossings: Vec<Piercing>,
820    options: CurveSurfaceOptions,
821    tol: Tolerances,
822) -> CurveSurfaceIntersection {
823    let stays_on = |x: &Piercing, y: &Piercing| -> bool {
824        (1..=3).all(|k| {
825            let t = x.on_curve + (y.on_curve - x.on_curve) * f64::from(k) / 4.0;
826            let Ok(p) = curve.point_at(t, tol) else {
827                return false;
828            };
829            // From either end: by a pole or an apex one side's chart
830            // turns too fast for the other's seed.
831            [x.on_surface, y.on_surface].into_iter().any(|seed| {
832                crate::march::nearest_on(surface, seed, p, tol)
833                    .is_some_and(|(_, q)| q.distance(p) <= options.gap)
834            })
835        })
836    };
837    let (lo, hi) = curve.domain();
838    #[allow(clippy::cast_precision_loss)]
839    let step = (hi - lo).abs() / options.samples.max(1) as f64;
840    let mut runs: Vec<Vec<Piercing>> = Vec::new();
841    for crossing in crossings {
842        match runs.last_mut() {
843            Some(run) if run.last().is_some_and(|last| stays_on(last, &crossing)) => {
844                run.push(crossing);
845            }
846            _ => runs.push(vec![crossing]),
847        }
848    }
849    let mut out = CurveSurfaceIntersection {
850        crossings: Vec::new(),
851        lying: Vec::new(),
852    };
853    for run in runs {
854        let (Some(first), Some(last)) = (run.first(), run.last()) else {
855            continue;
856        };
857        if run.len() > 1 && (last.on_curve - first.on_curve).abs() >= step {
858            out.lying.push((first.on_curve, last.on_curve));
859        } else if let Some(best) = run.iter().min_by(|a, b| {
860            a.gap
861                .partial_cmp(&b.gap)
862                .unwrap_or(core::cmp::Ordering::Equal)
863        }) {
864            out.crossings.push(*best);
865        }
866    }
867    out
868}
869
870/// How many seed cells to lay along each direction of a surface: the
871/// asked grid, and for a spline at least two per knot span. A patch swept
872/// several turns round an axis (a thread's flank) spans dozens of knots
873/// along its length, and a grid of the asked size lays flat cells a turn's
874/// fraction wide whose chords stand a tenth of a millimetre off the wall;
875/// a curve crossing the wall inside that gap meets no cell and is missed.
876fn seeding(surface: &SurfaceGeometry, grid: usize) -> (usize, usize) {
877    const CAP: usize = 1024;
878    let SurfaceGeometry::BSpline(spline) = surface else {
879        return (grid, grid);
880    };
881    let spans = |knots: &ogeom_math::KnotVector| knots.distinct().len().saturating_sub(1);
882    (
883        grid.max(2 * spans(spline.u_knots())).min(CAP.max(grid)),
884        grid.max(2 * spans(spline.v_knots())).min(CAP.max(grid)),
885    )
886}
887
888/// Where to start Newton for a segment near a cell: the point of the cell
889/// the segment passes through, or failing that the one nearest its middle,
890/// with its parameters on the curve and on the surface read off the cell's
891/// corners.
892fn seed_in(cell: &Cell, p0: Point, p1: Point, t0: f64, t1: f64) -> (f64, (f64, f64)) {
893    let [a, b, c] = cell.corners;
894    let (t, at) = segment_meets_triangle(p0, p1, cell.corners).map_or_else(
895        || (f64::midpoint(t0, t1), p0.midpoint(p1)),
896        |x| {
897            let length = p0.distance(p1);
898            let f = if length > 0.0 {
899                p0.distance(x) / length
900            } else {
901                0.5
902            };
903            (t0 + (t1 - t0) * f, x)
904        },
905    );
906    // Barycentric weights of the point's foot in the cell's plane, pulled
907    // back inside the cell.
908    let (e1, e2, d) = (b - a, c - a, at - a);
909    let (d11, d12, d22) = (e1.dot(e1), e1.dot(e2), e2.dot(e2));
910    let (d1, d2) = (d.dot(e1), d.dot(e2));
911    let det = d11 * d22 - d12 * d12;
912    let (mut wb, mut wc) = if det > 0.0 {
913        ((d22 * d1 - d12 * d2) / det, (d11 * d2 - d12 * d1) / det)
914    } else {
915        (1.0 / 3.0, 1.0 / 3.0)
916    };
917    wb = wb.clamp(0.0, 1.0);
918    wc = wc.clamp(0.0, 1.0);
919    if wb + wc > 1.0 {
920        let sum = wb + wc;
921        wb /= sum;
922        wc /= sum;
923    }
924    let wa = 1.0 - wb - wc;
925    let [pa, pb, pc] = cell.params;
926    (
927        t,
928        (
929            wa * pa.0 + wb * pb.0 + wc * pc.0,
930            wa * pa.1 + wb * pb.1 + wc * pc.1,
931        ),
932    )
933}
934
935/// The distance between the segment `p..q` and the triangle `corners`:
936/// nothing where it pierces the triangle, else the least of its ends'
937/// distances to the triangle and its distances to the triangle's sides.
938fn segment_triangle_distance(p: Point, q: Point, corners: [Point; 3]) -> f64 {
939    if segment_meets_triangle(p, q, corners).is_some() {
940        return 0.0;
941    }
942    let [a, b, c] = corners;
943    let mut least = point_triangle_distance(p, corners).min(point_triangle_distance(q, corners));
944    for (x, y) in [(a, b), (b, c), (c, a)] {
945        least = least.min(segment_segment_distance(p, q, x, y));
946    }
947    least
948}
949
950/// The distance from `p` to the triangle `corners`.
951fn point_triangle_distance(p: Point, [a, b, c]: [Point; 3]) -> f64 {
952    let normal = (b - a).cross(c - a);
953    let area = normal.magnitude();
954    if area > 0.0 {
955        let n = normal / area;
956        let foot = p - n * (p - a).dot(n);
957        let inside = [(a, b), (b, c), (c, a)]
958            .iter()
959            .all(|&(x, y)| (y - x).cross(foot - x).dot(n) >= 0.0);
960        if inside {
961            return (p - a).dot(n).abs();
962        }
963    }
964    distance_to_segment(p, a, b)
965        .min(distance_to_segment(p, b, c))
966        .min(distance_to_segment(p, c, a))
967}
968
969/// The distance between the segments `p..q` and `r..s`.
970fn segment_segment_distance(p: Point, q: Point, r: Point, s: Point) -> f64 {
971    let (d1, d2, w) = (q - p, s - r, p - r);
972    let (a, e, f) = (d1.dot(d1), d2.dot(d2), d2.dot(w));
973    let (b, c) = (d1.dot(d2), d1.dot(w));
974    let denominator = a * e - b * b;
975    let mut t = if denominator > 1e-300 {
976        ((b * f - c * e) / denominator).clamp(0.0, 1.0)
977    } else {
978        0.0
979    };
980    let mut u = if e > 0.0 { (b * t + f) / e } else { 0.0 };
981    if u < 0.0 {
982        u = 0.0;
983        t = if a > 0.0 {
984            (-c / a).clamp(0.0, 1.0)
985        } else {
986            0.0
987        };
988    } else if u > 1.0 {
989        u = 1.0;
990        t = if a > 0.0 {
991            ((b - c) / a).clamp(0.0, 1.0)
992        } else {
993            0.0
994        };
995    }
996    (p + d1 * t).distance(r + d2 * u)
997}
998
999/// The distance from `p` to the segment `a..b`.
1000fn distance_to_segment(p: Point, a: Point, b: Point) -> f64 {
1001    let ab = b - a;
1002    let length = ab.dot(ab);
1003    let f = if length > 0.0 {
1004        ((p - a).dot(ab) / length).clamp(0.0, 1.0)
1005    } else {
1006        0.0
1007    };
1008    (a + ab * f).distance(p)
1009}
1010
1011/// The cells binned in a tree of boxes, each cell's box grown by the
1012/// farthest a curve may stand from it and still be seeded there: the gap,
1013/// or the cell's bow where that is wider. Built once for a surface and
1014/// asked once per segment of every curve.
1015#[derive(Debug, Clone)]
1016pub(crate) struct CellTree {
1017    nodes: Vec<TreeNode>,
1018    order: Vec<usize>,
1019}
1020
1021#[derive(Debug, Clone)]
1022struct TreeNode {
1023    low: Point,
1024    high: Point,
1025    /// A leaf's range of `order`, or an inner node's two children.
1026    span: (usize, usize),
1027    leaf: bool,
1028}
1029
1030impl CellTree {
1031    /// The most cells a leaf holds.
1032    const LEAF: usize = 8;
1033
1034    pub(crate) fn over(cells: &[Cell], gap: f64) -> Self {
1035        let boxes: Vec<(Point, Point)> = cells
1036            .iter()
1037            .map(|c| {
1038                let m = gap.max(c.sag);
1039                (
1040                    Point::new(c.low.x - m, c.low.y - m, c.low.z - m),
1041                    Point::new(c.high.x + m, c.high.y + m, c.high.z + m),
1042                )
1043            })
1044            .collect();
1045        let mut tree = Self {
1046            nodes: Vec::new(),
1047            order: (0..cells.len()).collect(),
1048        };
1049        if !cells.is_empty() {
1050            tree.build(&boxes, 0, cells.len());
1051        }
1052        tree
1053    }
1054
1055    fn build(&mut self, boxes: &[(Point, Point)], from: usize, to: usize) -> usize {
1056        let (mut low, mut high) = boxes[self.order[from]];
1057        for &i in &self.order[from..to] {
1058            let (l, h) = boxes[i];
1059            low = Point::new(low.x.min(l.x), low.y.min(l.y), low.z.min(l.z));
1060            high = Point::new(high.x.max(h.x), high.y.max(h.y), high.z.max(h.z));
1061        }
1062        let at = self.nodes.len();
1063        self.nodes.push(TreeNode {
1064            low,
1065            high,
1066            span: (from, to),
1067            leaf: true,
1068        });
1069        if to - from <= Self::LEAF {
1070            return at;
1071        }
1072        // Split at the median along the box's longest side.
1073        let extent = high - low;
1074        let axis = if extent.x >= extent.y && extent.x >= extent.z {
1075            0
1076        } else if extent.y >= extent.z {
1077            1
1078        } else {
1079            2
1080        };
1081        let key = |i: usize| {
1082            let (l, h) = boxes[i];
1083            match axis {
1084                0 => l.x + h.x,
1085                1 => l.y + h.y,
1086                _ => l.z + h.z,
1087            }
1088        };
1089        let middle = from + (to - from) / 2;
1090        self.order[from..to]
1091            .select_nth_unstable_by(middle - from, |&a, &b| key(a).total_cmp(&key(b)));
1092        let left = self.build(boxes, from, middle);
1093        let right = self.build(boxes, middle, to);
1094        self.nodes[at].span = (left, right);
1095        self.nodes[at].leaf = false;
1096        at
1097    }
1098
1099    /// The cells whose grown boxes meet the box `low..high`, added to `out`.
1100    pub(crate) fn visit(&self, low: Point, high: Point, out: &mut Vec<usize>) {
1101        if self.nodes.is_empty() {
1102            return;
1103        }
1104        let mut stack = vec![0_usize];
1105        while let Some(n) = stack.pop() {
1106            let node = &self.nodes[n];
1107            if node.low.x > high.x
1108                || low.x > node.high.x
1109                || node.low.y > high.y
1110                || low.y > node.high.y
1111                || node.low.z > high.z
1112                || low.z > node.high.z
1113            {
1114                continue;
1115            }
1116            if node.leaf {
1117                out.extend_from_slice(&self.order[node.span.0..node.span.1]);
1118            } else {
1119                stack.push(node.span.0);
1120                stack.push(node.span.1);
1121            }
1122        }
1123    }
1124}
1125
1126/// Whether a segment's box comes near a cell's.
1127fn segment_near_cell(a: Point, b: Point, cell: &Cell, margin: f64) -> bool {
1128    let low = Point::new(a.x.min(b.x), a.y.min(b.y), a.z.min(b.z));
1129    let high = Point::new(a.x.max(b.x), a.y.max(b.y), a.z.max(b.z));
1130    low.x <= cell.high.x + margin
1131        && cell.low.x <= high.x + margin
1132        && low.y <= cell.high.y + margin
1133        && cell.low.y <= high.y + margin
1134        && low.z <= cell.high.z + margin
1135        && cell.low.z <= high.z + margin
1136}
1137
1138/// Newton on the well-posed system `C(t) = S(u, v)`.
1139fn polish(
1140    curve: &Curve,
1141    surface: &SurfaceGeometry,
1142    seed_t: f64,
1143    seed_uv: (f64, f64),
1144    tol: Tolerances,
1145) -> Option<Piercing> {
1146    let clamp_t = |t: f64| {
1147        let (lo, hi) = curve.domain();
1148        if curve.is_periodic() {
1149            let span = hi - lo;
1150            if span > 0.0 {
1151                return lo + (t - lo).rem_euclid(span);
1152            }
1153        }
1154        t.clamp(lo, hi)
1155    };
1156    let clamp_uv = |u: f64, v: f64| {
1157        let ((ua, ub), (va, vb)) = surface.domain();
1158        let fold = |x: f64, lo: f64, hi: f64, periodic: bool| {
1159            if periodic {
1160                let span = hi - lo;
1161                if span > 0.0 {
1162                    return lo + (x - lo).rem_euclid(span);
1163                }
1164            }
1165            x.clamp(lo, hi)
1166        };
1167        (
1168            fold(u, ua, ub, surface.is_periodic_u()),
1169            fold(v, va, vb, surface.is_periodic_v()),
1170        )
1171    };
1172
1173    // The gap and its Jacobian apart: the damped step measures many trial
1174    // points and steps onto few, and a spline's derivatives cost more than
1175    // its point.
1176    let gap_at = |x: &[f64; 3]| {
1177        let t = clamp_t(x[0]);
1178        let (u, v) = clamp_uv(x[1], x[2]);
1179        let (Ok(pc), Ok(ps)) = (curve.point_at(t, tol), surface.point_at(u, v, tol)) else {
1180            // Nowhere to measure from: infinite, so the damped step backs off.
1181            return [f64::INFINITY; 3];
1182        };
1183        let gap = pc - ps;
1184        [gap.x, gap.y, gap.z]
1185    };
1186    let jacobian_at = |x: &[f64; 3]| {
1187        let t = clamp_t(x[0]);
1188        let (u, v) = clamp_uv(x[1], x[2]);
1189        let (Ok(dc), Ok((du, dv))) = (curve.d1_at(t, tol), surface.d1_at(u, v, tol)) else {
1190            return None;
1191        };
1192        Some([
1193            [dc.x, -du.x, -dv.x],
1194            [dc.y, -du.y, -dv.y],
1195            [dc.z, -du.z, -dv.z],
1196        ])
1197    };
1198    let criteria = solve::Criteria {
1199        residual: tol.confusion() * 0.01,
1200        step: tol.parametric(),
1201        max_iterations: 40,
1202    };
1203    let found = solve::newton_system_fixed_lazy(
1204        gap_at,
1205        jacobian_at,
1206        [seed_t, seed_uv.0, seed_uv.1],
1207        criteria,
1208    )
1209    .ok()?;
1210    let t = clamp_t(found.0[0]);
1211    let (u, v) = clamp_uv(found.0[1], found.0[2]);
1212    let pc = curve.point_at(t, tol).ok()?;
1213    let ps = surface.point_at(u, v, tol).ok()?;
1214    Some(Piercing {
1215        on_curve: t,
1216        on_surface: (u, v),
1217        point: pc,
1218        gap: pc.distance(ps),
1219    })
1220}
1221
1222#[cfg(test)]
1223#[allow(clippy::unwrap_used)]
1224mod tests {
1225    use super::*;
1226    use ogeom_geom::{
1227        BSplineCurve, CircleCurve, CylinderSurface, LineCurve, PlaneSurface, SphereSurface,
1228    };
1229    use ogeom_math::{Circle, Cylinder, Direction, Frame, KnotVector, Plane, Sphere, Vector};
1230
1231    const T: Tolerances = Tolerances::millimetres();
1232
1233    fn sphere(radius: f64) -> SurfaceGeometry {
1234        SphereSurface::new(Sphere::centred(Point::ORIGIN, radius, T).unwrap()).into()
1235    }
1236
1237    fn cylinder(radius: f64, height: (f64, f64)) -> SurfaceGeometry {
1238        CylinderSurface::new(Cylinder::new(Frame::WORLD, radius, T).unwrap(), height)
1239            .unwrap()
1240            .into()
1241    }
1242
1243    fn plane(origin: Point, normal: Vector) -> SurfaceGeometry {
1244        PlaneSurface::over(
1245            Plane::through(origin, Direction::new(normal, T).unwrap()),
1246            (-6.0, 6.0),
1247            (-6.0, 6.0),
1248        )
1249        .unwrap()
1250        .into()
1251    }
1252
1253    fn segment(from: Point, to: Point) -> Curve {
1254        LineCurve::segment(from, to, T).unwrap().into()
1255    }
1256
1257    #[test]
1258    fn a_line_through_a_sphere_pierces_it_where_the_quadratic_says() {
1259        let ball = sphere(2.0);
1260        let ray = segment(Point::new(-5.0, 0.0, 0.0), Point::new(5.0, 0.0, 0.0));
1261        let found =
1262            intersect_curve_surface(&ray, &ball, CurveSurfaceOptions::default(), T).unwrap();
1263        assert_eq!(found.crossings.len(), 2);
1264        assert!(
1265            found.crossings[0]
1266                .point
1267                .is_equal(Point::new(-2.0, 0.0, 0.0), T)
1268        );
1269        assert!(
1270            found.crossings[1]
1271                .point
1272                .is_equal(Point::new(2.0, 0.0, 0.0), T)
1273        );
1274        for hit in &found.crossings {
1275            assert!(hit.gap < 1e-12);
1276            // The surface parameters evaluate back onto the point.
1277            let lifted = ball
1278                .point_at(hit.on_surface.0, hit.on_surface.1, T)
1279                .unwrap();
1280            assert!(lifted.is_equal(hit.point, T));
1281        }
1282
1283        // Tangent: one root. Missing: none.
1284        let grazing = segment(Point::new(-5.0, 0.0, 2.0), Point::new(5.0, 0.0, 2.0));
1285        assert_eq!(
1286            intersect_curve_surface(&grazing, &ball, CurveSurfaceOptions::default(), T)
1287                .unwrap()
1288                .crossings
1289                .len(),
1290            1
1291        );
1292        let missing = segment(Point::new(-5.0, 0.0, 3.0), Point::new(5.0, 0.0, 3.0));
1293        assert!(
1294            intersect_curve_surface(&missing, &ball, CurveSurfaceOptions::default(), T)
1295                .unwrap()
1296                .is_empty()
1297        );
1298    }
1299
1300    #[test]
1301    fn a_line_through_a_cylinder_respects_its_height() {
1302        let drum = cylinder(2.0, (-1.0, 1.0));
1303        // Crosses the infinite cylinder at z = 0: inside the height, two hits.
1304        let level = segment(Point::new(-5.0, 0.0, 0.0), Point::new(5.0, 0.0, 0.0));
1305        assert_eq!(
1306            intersect_curve_surface(&level, &drum, CurveSurfaceOptions::default(), T)
1307                .unwrap()
1308                .crossings
1309                .len(),
1310            2
1311        );
1312        // Crosses at z = 3: the unbounded geometry meets it, this surface
1313        // does not reach there.
1314        let high = segment(Point::new(-5.0, 0.0, 3.0), Point::new(5.0, 0.0, 3.0));
1315        assert!(
1316            intersect_curve_surface(&high, &drum, CurveSurfaceOptions::default(), T)
1317                .unwrap()
1318                .is_empty()
1319        );
1320    }
1321
1322    #[test]
1323    fn a_line_lying_in_a_plane_is_an_overlap_not_a_crossing_list() {
1324        let ground = plane(Point::ORIGIN, Vector::Z);
1325        let lying = segment(Point::new(-3.0, 1.0, 0.0), Point::new(3.0, 1.0, 0.0));
1326        let found =
1327            intersect_curve_surface(&lying, &ground, CurveSurfaceOptions::default(), T).unwrap();
1328        assert!(found.crossings.is_empty());
1329        assert_eq!(found.lying.len(), 1);
1330
1331        let crossing = segment(Point::new(0.0, 0.0, -1.0), Point::new(0.0, 0.0, 1.0));
1332        let found =
1333            intersect_curve_surface(&crossing, &ground, CurveSurfaceOptions::default(), T).unwrap();
1334        assert_eq!(found.crossings.len(), 1);
1335        assert!(found.crossings[0].point.is_equal(Point::ORIGIN, T));
1336
1337        let parallel = segment(Point::new(-3.0, 0.0, 1.0), Point::new(3.0, 0.0, 1.0));
1338        assert!(
1339            intersect_curve_surface(&parallel, &ground, CurveSurfaceOptions::default(), T)
1340                .unwrap()
1341                .is_empty()
1342        );
1343    }
1344
1345    #[test]
1346    fn a_circle_pierces_a_plane_twice_through_the_general_path() {
1347        // A circle in the xz plane against the ground: no analytic case
1348        // handles circle/plane here, so this is the seeded Newton path, and
1349        // the answer is known exactly anyway.
1350        let ring: Curve = CircleCurve::new(
1351            Circle::new(
1352                Frame::new(Point::new(0.0, 0.0, 0.0), -Direction::Y, Direction::X, T).unwrap(),
1353                2.0,
1354                T,
1355            )
1356            .unwrap(),
1357        )
1358        .into();
1359        let ground = plane(Point::ORIGIN, Vector::Z);
1360        let found =
1361            intersect_curve_surface(&ring, &ground, CurveSurfaceOptions::default(), T).unwrap();
1362        assert_eq!(found.crossings.len(), 2);
1363        for hit in &found.crossings {
1364            assert!(hit.gap < 1e-9);
1365            assert!(hit.point.z.abs() < 1e-9);
1366            assert!((hit.point.to_vector().magnitude() - 2.0).abs() < 1e-9);
1367        }
1368    }
1369
1370    #[test]
1371    fn a_prepared_surface_answers_as_the_one_shot_query() {
1372        // A half-pipe spline strip, which only the seeded path can answer,
1373        // asked about many lines in turn: the stored sampling must give
1374        // every one the answer a fresh sampling gives, piercings and all.
1375        use ogeom_geom::BSplineSurface;
1376        use ogeom_math::ControlGrid;
1377        let mut points = Vec::new();
1378        for i in 0..7 {
1379            let a = core::f64::consts::PI * f64::from(i) / 6.0;
1380            for j in 0..2 {
1381                points.push(Point::new(2.0 * a.cos(), 3.0 * f64::from(j), 2.0 * a.sin()));
1382            }
1383        }
1384        let strip: SurfaceGeometry = BSplineSurface::new(
1385            KnotVector::clamped_uniform(3, 7).unwrap(),
1386            KnotVector::clamped_uniform(1, 2).unwrap(),
1387            &ControlGrid::new(points, 7, 2).unwrap(),
1388            T,
1389        )
1390        .unwrap()
1391        .into();
1392        let options = CurveSurfaceOptions::default();
1393        let prepared = PreparedSurface::new(strip.clone(), options, T);
1394        let mut pierced = 0;
1395        for k in 0..12 {
1396            let x = -2.5 + 5.0 * f64::from(k) / 11.0;
1397            let lines: [Curve; 2] = [
1398                LineCurve::new(ogeom_math::Axis::new(
1399                    Point::new(x, 1.5, 0.0),
1400                    Direction::new(Vector::new(0.1, 0.2, 1.0), T).unwrap(),
1401                ))
1402                .into(),
1403                segment(Point::new(x, -1.0, -1.0), Point::new(-x, 4.0, 3.0)),
1404            ];
1405            for line in &lines {
1406                let once = intersect_curve_surface(line, &strip, options, T).unwrap();
1407                pierced += once.crossings.len();
1408                assert_eq!(prepared.intersect(line).unwrap(), once, "line {k}");
1409            }
1410        }
1411        assert!(pierced > 0, "some lines pass through the strip");
1412    }
1413
1414    #[test]
1415    fn a_spline_through_a_sphere_is_found_and_polished() {
1416        // A spline wandering through the ball: piercings with no closed form
1417        // anywhere, verified implicitly: each reported point is on the
1418        // sphere to the gap it claims.
1419        let wander: Curve = BSplineCurve::new(
1420            KnotVector::new(vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0], 3).unwrap(),
1421            vec![
1422                Point::new(-4.0, -1.0, -1.0),
1423                Point::new(-1.0, 2.0, 1.0),
1424                Point::new(1.0, -2.0, -1.0),
1425                Point::new(4.0, 1.0, 1.0),
1426            ],
1427            T,
1428        )
1429        .unwrap()
1430        .into();
1431        let ball = sphere(2.0);
1432        let found =
1433            intersect_curve_surface(&wander, &ball, CurveSurfaceOptions::default(), T).unwrap();
1434        assert!(!found.crossings.is_empty(), "the spline passes through");
1435        for hit in &found.crossings {
1436            assert!(hit.gap < 1e-9);
1437            let SurfaceGeometry::Sphere(s) = &ball else {
1438                unreachable!()
1439            };
1440            assert!(s.sphere().distance_to(hit.point).abs() < 1e-9);
1441        }
1442    }
1443
1444    #[test]
1445    fn unusable_options_are_refused() {
1446        let ball = sphere(1.0);
1447        let ray = segment(Point::new(-5.0, 0.0, 0.0), Point::new(5.0, 0.0, 0.0));
1448        for options in [
1449            CurveSurfaceOptions {
1450                samples: 1,
1451                ..CurveSurfaceOptions::default()
1452            },
1453            CurveSurfaceOptions {
1454                grid: 1,
1455                ..CurveSurfaceOptions::default()
1456            },
1457            CurveSurfaceOptions {
1458                gap: 0.0,
1459                ..CurveSurfaceOptions::default()
1460            },
1461        ] {
1462            assert!(intersect_curve_surface(&ray, &ball, options, T).is_err());
1463        }
1464    }
1465
1466    /// The general path (a torus has no closed form against a line) answers
1467    /// each tangency once, and a curve lying in the surface as the stretch
1468    /// it lies along rather than as hundreds of piercings.
1469    /// A torus far down a line is met where it is: its quartic, solved where
1470    /// the line starts, has roots hundreds of units out, and the eigenvalues
1471    /// lost them.
1472    #[test]
1473    fn a_torus_far_down_a_line_is_still_met() {
1474        use ogeom_geom::TorusSurface;
1475        use ogeom_math::Torus;
1476        let options = CurveSurfaceOptions::default();
1477        let tilted = Frame::new(
1478            Point::new(0.0, 785.0, -140.0),
1479            Direction::new(Vector::new(0.0, -0.999_390_827, 0.034_899_497), T).unwrap(),
1480            Direction::X,
1481            T,
1482        )
1483        .unwrap();
1484        let torus: SurfaceGeometry =
1485            TorusSurface::new(Torus::new(tilted, 120.0, 12.0, T).unwrap()).into();
1486        let w = Vector::new(1.0, 1.0, 1.0) / 3.0_f64.sqrt();
1487        let from = Point::new(-160.0, 531.0, -320.0);
1488        let line = segment(from, from + w * 800.0);
1489        let found = intersect_curve_surface(&line, &torus, options, T).unwrap();
1490        let cells = sample_by(&torus, seeding(&torus, options.grid), T);
1491        let tree = CellTree::over(&cells, options.gap);
1492        let general = general(&line, &torus, &cells, &tree, options, T).unwrap();
1493        assert!(!general.crossings.is_empty());
1494        assert_eq!(found.crossings.len(), general.crossings.len());
1495        for (a, b) in found.crossings.iter().zip(&general.crossings) {
1496            assert!(
1497                a.point.distance(b.point) < 1e-6,
1498                "{:?} against {:?}",
1499                a.point,
1500                b.point
1501            );
1502        }
1503    }
1504
1505    /// A line through a torus's middle crosses the tube four times, and one
1506    /// through a cone's axis crosses its wall twice, at the closed forms'
1507    /// points.
1508    #[test]
1509    fn lines_cross_a_torus_and_a_cone_where_the_closed_forms_say() {
1510        use ogeom_geom::{ConeSurface, TorusSurface};
1511        use ogeom_math::{Cone, Torus};
1512        let options = CurveSurfaceOptions::default();
1513        let torus: SurfaceGeometry =
1514            TorusSurface::new(Torus::new(Frame::WORLD, 60.0, 20.0, T).unwrap()).into();
1515        let across = segment(Point::new(-100.0, 0.0, 0.0), Point::new(100.0, 0.0, 0.0));
1516        let found = intersect_curve_surface(&across, &torus, options, T).unwrap();
1517        let xs: Vec<f64> = found.crossings.iter().map(|c| c.point.x).collect();
1518        assert_eq!(xs.len(), 4, "{xs:?}");
1519        for (got, want) in xs.iter().zip([-80.0, -40.0, 40.0, 80.0]) {
1520            assert!((got - want).abs() < 1e-9, "{xs:?}");
1521        }
1522        let cone: SurfaceGeometry = ConeSurface::new(
1523            Cone::new(Frame::WORLD, 10.0, core::f64::consts::FRAC_PI_4, T).unwrap(),
1524            (0.0, 20.0),
1525        )
1526        .unwrap()
1527        .into();
1528        let level = segment(Point::new(-50.0, 0.0, 5.0), Point::new(50.0, 0.0, 5.0));
1529        let found = intersect_curve_surface(&level, &cone, options, T).unwrap();
1530        let xs: Vec<f64> = found.crossings.iter().map(|c| c.point.x).collect();
1531        assert_eq!(xs.len(), 2, "{xs:?}");
1532        for (got, want) in xs.iter().zip([-15.0, 15.0]) {
1533            assert!((got - want).abs() < 1e-9, "{xs:?}");
1534        }
1535    }
1536
1537    /// A line anchored far from a cone meets it where one anchored beside
1538    /// it does: the quadratic's constant term grows with the anchor's
1539    /// distance, and its leading term must not be dropped for that.
1540    #[test]
1541    fn a_line_anchored_far_away_still_crosses_a_cone() {
1542        use ogeom_geom::ConeSurface;
1543        use ogeom_math::Cone;
1544        let options = CurveSurfaceOptions::default();
1545        let cone: SurfaceGeometry = ConeSurface::new(
1546            Cone::new(Frame::WORLD, 10.0, core::f64::consts::FRAC_PI_4, T).unwrap(),
1547            (0.0, 20.0),
1548        )
1549        .unwrap()
1550        .into();
1551        let far = segment(Point::new(-1e5, 0.0, 5.0), Point::new(50.0, 0.0, 5.0));
1552        let found = intersect_curve_surface(&far, &cone, options, T).unwrap();
1553        let xs: Vec<f64> = found.crossings.iter().map(|c| c.point.x).collect();
1554        assert_eq!(xs.len(), 2, "{xs:?}");
1555        for (got, want) in xs.iter().zip([-15.0, 15.0]) {
1556            assert!((got - want).abs() < 1e-6, "{xs:?}");
1557        }
1558    }
1559
1560    /// Lines that miss a torus through its hole or along its axis meet
1561    /// nothing; one grazing the outer equator touches it once, at the
1562    /// tangent point; a segment ending on the tube keeps its end crossings.
1563    #[test]
1564    fn lines_through_the_hole_along_the_axis_and_grazing_a_torus() {
1565        use ogeom_geom::TorusSurface;
1566        use ogeom_math::Torus;
1567        let options = CurveSurfaceOptions::default();
1568        let tilted = Frame::new(
1569            Point::new(3.0, -2.0, 1.0),
1570            Direction::new(Vector::new(0.1, 0.2, 1.0), T).unwrap(),
1571            Direction::new(Vector::new(1.0, 0.0, -0.1), T).unwrap(),
1572            T,
1573        )
1574        .unwrap();
1575        let torus: SurfaceGeometry =
1576            TorusSurface::new(Torus::new(tilted, 60.0, 20.0, T).unwrap()).into();
1577        let local = |x: f64, y: f64, z: f64| tilted.to_world(Point::new(x, y, z));
1578        let meets = |from: Point, to: Point| {
1579            intersect_curve_surface(&segment(from, to), &torus, options, T)
1580                .unwrap()
1581                .crossings
1582        };
1583        assert!(meets(local(0.0, 0.0, -100.0), local(0.0, 0.0, 100.0)).is_empty());
1584        assert!(meets(local(5.0, 3.0, -100.0), local(-15.0, 3.0, 100.0)).is_empty());
1585        let grazing = meets(local(-100.0, 80.0, 0.0), local(100.0, 80.0, 0.0));
1586        assert_eq!(grazing.len(), 1, "{grazing:?}");
1587        assert!(grazing[0].point.distance(local(0.0, 80.0, 0.0)) < 1e-6);
1588        let ends = meets(local(-80.0, 0.0, 0.0), local(80.0, 0.0, 0.0));
1589        let xs: Vec<f64> = ends.iter().map(|c| tilted.to_local(c.point).x).collect();
1590        assert_eq!(xs.len(), 4, "{xs:?}");
1591        for (got, want) in xs.iter().zip([-80.0, -40.0, 40.0, 80.0]) {
1592            assert!((got - want).abs() < 1e-9, "{xs:?}");
1593        }
1594    }
1595
1596    #[test]
1597    fn tangencies_come_back_once_and_lying_curves_as_stretches() {
1598        use ogeom_geom::TorusSurface;
1599        use ogeom_math::Torus;
1600        let torus: SurfaceGeometry =
1601            TorusSurface::new(Torus::new(Frame::WORLD, 60.0, 20.0, T).unwrap()).into();
1602        let options = CurveSurfaceOptions::default();
1603        // Along the tube's top: tangent at two points.
1604        let top = segment(Point::new(-100.0, 0.0, 20.0), Point::new(100.0, 0.0, 20.0));
1605        let found = intersect_curve_surface(&top, &torus, options, T).unwrap();
1606        assert_eq!(found.crossings.len(), 2, "{:?}", found.crossings);
1607        assert!(found.lying.is_empty());
1608        // Touching the inner equator and crossing the outer twice.
1609        let inner = segment(Point::new(-100.0, 40.0, 0.0), Point::new(100.0, 40.0, 0.0));
1610        let found = intersect_curve_surface(&inner, &torus, options, T).unwrap();
1611        assert_eq!(found.crossings.len(), 3, "{:?}", found.crossings);
1612        // A parallel of the torus lies in it the whole way round.
1613        let parallel: Curve = CircleCurve::new(
1614            Circle::new(
1615                Frame::new(Point::new(0.0, 0.0, 20.0), Direction::Z, Direction::X, T).unwrap(),
1616                60.0,
1617                T,
1618            )
1619            .unwrap(),
1620        )
1621        .into();
1622        let found = intersect_curve_surface(&parallel, &torus, options, T).unwrap();
1623        assert!(found.crossings.is_empty(), "{:?}", found.crossings);
1624        assert_eq!(found.lying.len(), 1);
1625        let (from, to) = found.lying[0];
1626        assert!((to - from).abs() > 6.0, "{from} to {to}");
1627    }
1628
1629    /// A wall leaning off a round's torus band, its foot back, cuts the
1630    /// band along a section that leaves the wall's face tangent to two of
1631    /// its edges: the top edge, where the band is tangent to the top, and
1632    /// the wall's seam with the drum, where the band is tangent to the
1633    /// drum. Each edge touches the torus there, the gap falling to zero
1634    /// without changing sign, and the touch comes back on its closed-form
1635    /// point from a seed a millimetre's thousandth along the edge, where a
1636    /// solve for the gap stops in the valley. A line crossing the band is
1637    /// not a touch.
1638    #[test]
1639    fn an_edge_tangent_to_a_band_touches_it_where_the_band_meets_its_face() {
1640        use ogeom_geom::TorusSurface;
1641        use ogeom_math::Torus;
1642        let band: SurfaceGeometry = TorusSurface::new(
1643            Torus::new(
1644                Frame::new(Point::new(4.0, 4.0, 1.5), Direction::Z, -Direction::X, T).unwrap(),
1645                2.0,
1646                0.5,
1647                T,
1648            )
1649            .unwrap(),
1650        )
1651        .into();
1652        let lean = 0.1_f64;
1653        let wall = plane(Point::new(4.0, 4.0, 2.0), Vector::new(0.0, 1.0, -lean));
1654        let drum = CylinderSurface::new(
1655            Cylinder::new(
1656                Frame::new(Point::new(4.0, 4.0, 0.0), Direction::Z, Direction::X, T).unwrap(),
1657                2.5,
1658                T,
1659            )
1660            .unwrap(),
1661            (0.0, 2.0),
1662        )
1663        .unwrap()
1664        .into();
1665        let crate::section::SurfaceIntersection::Along(seams) = crate::section::intersect_surfaces(
1666            &wall,
1667            &drum,
1668            crate::section::IntersectOptions::default(),
1669            T,
1670        )
1671        .unwrap() else {
1672            panic!("the wall meets the drum");
1673        };
1674        let foot = Point::new(4.0 + 2.5_f64.mul_add(2.5, -0.05 * 0.05).sqrt(), 3.95, 1.5);
1675        // The nearest of a fine sampling: a seed, not an answer.
1676        let nearest = |c: &Curve, p: Point| {
1677            let (lo, hi) = c.domain();
1678            (0..=20_000)
1679                .map(|k| lo + (hi - lo) * f64::from(k) / 20_000.0)
1680                .map(|t| (t, c.point_at(t, T).unwrap().distance(p)))
1681                .min_by(|a, b| a.1.total_cmp(&b.1))
1682                .unwrap()
1683        };
1684        let seam = seams
1685            .iter()
1686            .map(|s| &s.curve)
1687            .find(|c| nearest(c, foot).1 < 1e-2)
1688            .unwrap();
1689        let top = segment(Point::new(1.5, 4.0, 2.0), Point::new(6.5, 4.0, 2.0));
1690        for (edge, want) in [(&top, Point::new(6.0, 4.0, 2.0)), (seam, foot)] {
1691            let at = nearest(edge, want).0;
1692            let step = 1e-3 / edge.d1_at(at, T).unwrap().magnitude();
1693            for seed in [at - step, at + step] {
1694                let touch = touch_curve_surface(edge, &band, seed, T).unwrap();
1695                assert!(
1696                    touch.point.distance(want) < 1e-9 && touch.gap < 1e-9,
1697                    "{touch:?} against {want:?}"
1698                );
1699            }
1700        }
1701        let across = segment(Point::new(6.2, 4.0, 3.0), Point::new(6.2, 4.0, 0.0));
1702        let found =
1703            intersect_curve_surface(&across, &band, CurveSurfaceOptions::default(), T).unwrap();
1704        assert_eq!(found.crossings.len(), 2, "{:?}", found.crossings);
1705        for crossing in found.crossings {
1706            assert!(touch_curve_surface(&across, &band, crossing.on_curve, T).is_none());
1707        }
1708    }
1709}