Skip to main content

brepkit_math/
analytic_intersection.rs

1//! Closed-form and semi-analytic intersections of analytic surfaces with planes.
2//!
3//! Provides specialized intersection algorithms for cylinder, cone, sphere,
4//! and torus surfaces with planes, as well as a general marching approach
5//! for analytic-analytic surface intersections.
6
7use std::f64::consts::{FRAC_PI_2, TAU};
8
9use crate::MathError;
10use crate::aabb::Aabb3;
11use crate::curves::{Circle3D, Ellipse3D};
12use crate::frame::Frame3;
13use crate::nurbs::curve::NurbsCurve;
14use crate::nurbs::fitting::interpolate;
15use crate::nurbs::intersection::{IntersectionCurve, IntersectionPoint};
16use crate::surfaces::{ConicalSurface, CylindricalSurface, SphericalSurface, ToroidalSurface};
17use crate::tolerance::Tolerance;
18use crate::vec::{Point3, Vec3};
19
20/// Exact curve type resulting from plane-analytic surface intersection.
21#[derive(Debug, Clone)]
22pub enum ExactIntersectionCurve {
23    /// A circle (plane perpendicular to axis of cylinder/cone/sphere).
24    Circle(Circle3D),
25    /// An ellipse (plane oblique to cylinder/cone axis).
26    Ellipse(Ellipse3D),
27    /// Fallback to sampled point chain (torus, degenerate cases).
28    Points(Vec<Point3>),
29}
30
31/// Compute exact intersection curves between a plane and an analytic surface.
32///
33/// Returns exact `Circle3D` or `Ellipse3D` where possible, falling back to
34/// sampled points for complex cases (torus).
35///
36/// The plane is defined by `dot(normal, p) = d`.
37///
38/// # Errors
39///
40/// Returns an error if the intersection computation fails.
41pub fn exact_plane_analytic(
42    surface: AnalyticSurface<'_>,
43    plane_normal: Vec3,
44    plane_d: f64,
45) -> Result<Vec<ExactIntersectionCurve>, MathError> {
46    exact_plane_analytic_reaching(surface, plane_normal, plane_d, 0.0)
47}
48
49/// [`exact_plane_analytic`] with a cone's sampled hyperbola or parabola
50/// carried at least `reach` from the apex, so it spans whatever faces the
51/// caller will trim it to.
52///
53/// # Errors
54///
55/// Returns an error if the intersection computation fails.
56pub fn exact_plane_analytic_reaching(
57    surface: AnalyticSurface<'_>,
58    plane_normal: Vec3,
59    plane_d: f64,
60    reach: f64,
61) -> Result<Vec<ExactIntersectionCurve>, MathError> {
62    match surface {
63        AnalyticSurface::Cylinder(cyl) => exact_plane_cylinder(cyl, plane_normal, plane_d),
64        AnalyticSurface::Sphere(sphere) => exact_plane_sphere(sphere, plane_normal, plane_d),
65        AnalyticSurface::Cone(cone) => exact_plane_cone(cone, plane_normal, plane_d, reach),
66        AnalyticSurface::Torus(torus) => {
67            if let Some(circles) = exact_plane_torus(torus, plane_normal, plane_d)? {
68                return Ok(circles);
69            }
70            if let Some(loops) = plane_torus_winding_loops(torus, plane_normal, plane_d, 128) {
71                return Ok(loops
72                    .into_iter()
73                    .map(ExactIntersectionCurve::Points)
74                    .collect());
75            }
76            // Other torus sections are degree-4 — fall back to sampling.
77            let chains = sample_plane_torus(torus, plane_normal, plane_d)?;
78            Ok(chains
79                .into_iter()
80                .map(ExactIntersectionCurve::Points)
81                .collect())
82        }
83    }
84}
85
86/// The plane-torus sections that are circles:
87///
88/// - a plane across the axis at height `h` from the centre, `|h| < r`: the
89///   two circles of radius `R ± sqrt(r² − h²)` about the axis;
90/// - a plane through the axis: the two tube cross-sections of radius `r`,
91///   `R` either side of the axis.
92///
93/// A plane across the axis tangent to the tube's top or bottom touches it
94/// along the one circle of radius `R`, ring or spindle.
95///
96/// `Some` of no curves for a plane across the axis that misses the tube;
97/// `None` for any other plane, and for a section across the axis whose inner
98/// circle would reach the axis.
99fn exact_plane_torus(
100    torus: &ToroidalSurface,
101    normal: Vec3,
102    d: f64,
103) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
104    let len = normal.length();
105    let n = normal.normalize()?;
106    let d = d / len;
107    let axis = torus.z_axis();
108    let center = torus.center();
109    let (big, small) = (torus.major_radius(), torus.minor_radius());
110    let height = d - dot_np(n, center);
111    let along = n.dot(axis);
112    if along.abs() > 1.0 - 1e-10 {
113        if height.abs() >= small - 1e-10 * small {
114            if height.abs() > small + 1e-10 * small {
115                return Ok(Some(Vec::new()));
116            }
117            // Tangent to the tube's top or bottom: the plane touches the
118            // torus along the one circle of its own radius. That circle lies
119            // on the torus within the plane's distance from the tube's top,
120            // so it stands for the section while that distance is within the
121            // linear tolerance, whatever the scale. A tilted plane's section
122            // is no circle.
123            if big <= 1e-10 * small
124                || axis.cross(n).length() > 1e-12
125                || (small - height.abs()).abs() > crate::tolerance::Tolerance::default().linear
126            {
127                return Ok(None);
128            }
129            let middle = center + n * height;
130            return Ok(Some(vec![ExactIntersectionCurve::Circle(Circle3D::new(
131                middle, n, big,
132            )?)]));
133        }
134        let reach = small.mul_add(small, -(height * height)).sqrt();
135        if big - reach <= 1e-10 * big {
136            return Ok(None);
137        }
138        let middle = center + n * height;
139        return Ok(Some(vec![
140            ExactIntersectionCurve::Circle(Circle3D::new(middle, n, big + reach)?),
141            ExactIntersectionCurve::Circle(Circle3D::new(middle, n, big - reach)?),
142        ]));
143    }
144    if along.abs() < 1e-10 && height.abs() < 1e-10 * (big + small) {
145        let out = axis.cross(n).normalize()?;
146        return Ok(Some(vec![
147            ExactIntersectionCurve::Circle(Circle3D::new(center + out * big, n, small)?),
148            ExactIntersectionCurve::Circle(Circle3D::new(center - out * big, n, small)?),
149        ]));
150    }
151    Ok(None)
152}
153
154/// Exact plane-cylinder intersection.
155///
156/// - Plane perpendicular to axis → `Circle3D`
157/// - Plane oblique to axis → `Ellipse3D`
158/// - Plane parallel to axis → `Points` fallback (0 or 2 lines)
159fn exact_plane_cylinder(
160    cyl: &CylindricalSurface,
161    normal: Vec3,
162    d: f64,
163) -> Result<Vec<ExactIntersectionCurve>, MathError> {
164    let axis = cyl.axis();
165    let cos_theta = normal.dot(axis).abs();
166    let r = cyl.radius();
167
168    if cos_theta < 1e-10 {
169        // Plane parallel to cylinder axis → 0 or 2 line segments.
170        // Fall back to sampled points.
171        let chains = sample_plane_cylinder(cyl, normal, d)?;
172        return Ok(chains
173            .into_iter()
174            .map(ExactIntersectionCurve::Points)
175            .collect());
176    }
177
178    // Find where axis intersects the plane: axis_point + t*axis, dot(normal, P) = d
179    // t = (d - dot(normal, origin)) / dot(normal, axis)
180    let n_dot_axis = normal.dot(axis);
181    let n_dot_origin = dot_np(normal, cyl.origin());
182    let t = (d - n_dot_origin) / n_dot_axis;
183    let center_on_axis = Point3::new(
184        cyl.origin().x() + t * axis.x(),
185        cyl.origin().y() + t * axis.y(),
186        cyl.origin().z() + t * axis.z(),
187    );
188
189    if cos_theta > 1.0 - 1e-10 {
190        // Plane perpendicular to axis → Circle
191        let circle = Circle3D::new(center_on_axis, normal, r)?;
192        Ok(vec![ExactIntersectionCurve::Circle(circle)])
193    } else {
194        // Oblique plane → Ellipse
195        // Semi-minor = r (the cylinder radius, unchanged)
196        // Semi-major = r / cos(θ) where θ = angle between plane normal and axis
197        let semi_minor = r;
198        let semi_major = r / cos_theta;
199
200        // The major axis direction lies in the intersection of the plane
201        // with the plane containing the axis and the plane normal.
202        // It's the projection of the axis onto the cutting plane, normalized.
203        let axis_proj = Vec3::new(
204            axis.x() - n_dot_axis * normal.x(),
205            axis.y() - n_dot_axis * normal.y(),
206            axis.z() - n_dot_axis * normal.z(),
207        );
208        let u_axis = axis_proj.normalize()?;
209        let v_axis = normal.cross(u_axis);
210
211        let ellipse = Ellipse3D::with_axes(
212            center_on_axis,
213            normal,
214            semi_major,
215            semi_minor,
216            u_axis,
217            v_axis,
218        )?;
219        Ok(vec![ExactIntersectionCurve::Ellipse(ellipse)])
220    }
221}
222
223/// Exact plane-sphere intersection.
224///
225/// Always produces a `Circle3D` (or empty if no intersection).
226fn exact_plane_sphere(
227    sphere: &SphericalSurface,
228    normal: Vec3,
229    d: f64,
230) -> Result<Vec<ExactIntersectionCurve>, MathError> {
231    let h = dot_np(normal, sphere.center()) - d;
232    let r = sphere.radius();
233
234    if h.abs() > r - 1e-10 {
235        return Ok(vec![]);
236    }
237
238    let circle_r = (r.mul_add(r, -(h * h))).sqrt();
239    let circle_center = Point3::new(
240        h.mul_add(-normal.x(), sphere.center().x()),
241        h.mul_add(-normal.y(), sphere.center().y()),
242        h.mul_add(-normal.z(), sphere.center().z()),
243    );
244
245    let circle = Circle3D::new(circle_center, normal, circle_r)?;
246    Ok(vec![ExactIntersectionCurve::Circle(circle)])
247}
248
249/// Exact plane-cone intersection.
250///
251/// The conic type is set by the cone's half-opening angle from the axis
252/// (`γ = π/2 − half_angle`) versus the plane-axis angle `ψ`:
253/// - Plane perpendicular to axis (`ψ = π/2`) → `Circle3D`
254/// - `ψ > γ` (ellipse) → closed-form `Ellipse3D`
255/// - `ψ ≤ γ` (parabola/hyperbola) → bounded single-branch `Points` (one chain
256///   per branch — a hyperbola's two nappes never share a chain)
257fn exact_plane_cone(
258    cone: &ConicalSurface,
259    normal: Vec3,
260    d: f64,
261    reach: f64,
262) -> Result<Vec<ExactIntersectionCurve>, MathError> {
263    let axis = cone.axis();
264    let cos_theta = normal.dot(axis).abs();
265    let half_angle = cone.half_angle();
266
267    if cos_theta > 1.0 - 1e-10 {
268        // Plane perpendicular to axis → Circle
269        // Find where axis meets the plane
270        let n_dot_axis = normal.dot(axis);
271        let n_dot_apex = dot_np(normal, cone.apex());
272        let t = (d - n_dot_apex) / n_dot_axis;
273
274        // t is the signed distance from apex to plane along the axis.
275        // The real cone is a single nappe; the perpendicular-plane section is a
276        // circle whose radius follows from the axial offset |t|.
277        // |t| ≈ 0 means the plane passes through the apex → degenerate point.
278        if t.abs() < 1e-10 {
279            return Ok(vec![]);
280        }
281
282        let center = Point3::new(
283            cone.apex().x() + t * axis.x(),
284            cone.apex().y() + t * axis.y(),
285            cone.apex().z() + t * axis.z(),
286        );
287        // half_angle is the angle from the radial plane to the surface.
288        // Axial distance t = v * sin(half_angle), so v = t / sin(half_angle).
289        // Radius at v = v * cos(half_angle) = t * cos(half_angle) / sin(half_angle).
290        let circle_r = t.abs() * half_angle.cos() / half_angle.sin();
291        if circle_r < 1e-15 {
292            return Ok(vec![]);
293        }
294
295        let circle = Circle3D::new(center, normal, circle_r)?;
296        return Ok(vec![ExactIntersectionCurve::Circle(circle)]);
297    }
298
299    // Oblique plane. Classify the conic in the plane-aligned frame.
300    //
301    // Decompose the (unit) axis as a = c·n + p·e1, where c = n·a, e1 is the unit
302    // in-plane projection of the axis, and p = |projection| = sqrt(1−c²). Write a
303    // point Q on the plane as Q = apex + e·n + s·e1 + t·e2 (e = d − n·apex,
304    // e2 = n×e1). The cone equation (w·a)² = cos²γ·(w·w) with k = cos²γ =
305    // sin²(half_angle) reduces to (no s·t cross term, since e1/e2 align with the
306    // conic axes):
307    //     (p²−k)·s² + 2ecp·s + e²(c²−k) = k·t²
308    // The s² coefficient A = p²−k = sin²θ − sin²(half_angle) sets the type:
309    // A < 0 → ellipse, A = 0 → parabola, A > 0 → hyperbola.
310    let c = normal.dot(axis);
311    let p2 = (1.0 - c * c).max(0.0);
312    let p = p2.sqrt();
313    let k = half_angle.sin().powi(2);
314    let a_coeff = p2 - k;
315
316    // Build the plane-aligned frame e1 (in-plane axis projection), e2 = n×e1.
317    let m = Vec3::new(
318        axis.x() - c * normal.x(),
319        axis.y() - c * normal.y(),
320        axis.z() - c * normal.z(),
321    );
322    let m_len = m.length();
323    if m_len < 1e-12 {
324        // Axis parallel to normal — handled by the perpendicular branch above;
325        // fall back to sampling for safety.
326        let chains = sample_plane_cone(cone, normal, d, reach)?;
327        return Ok(chains
328            .into_iter()
329            .map(ExactIntersectionCurve::Points)
330            .collect());
331    }
332    let e1 = m * (1.0 / m_len);
333    let e2 = normal.cross(e1);
334    let apex = cone.apex();
335    let e = d - dot_np(normal, apex);
336
337    // Ellipse → closed form. A = p²−k < 0 with a margin to keep the
338    // near-parabolic regime on the robust sampled path.
339    if a_coeff < -1e-9 {
340        let abs_a = -a_coeff; // = k − p² > 0
341        // Real-nappe guard: in the ellipse regime n·g(u) keeps constant sign(c),
342        // so v = e/(n·g) ≥ 0 only when e and c share a sign. When e·c < 0 the
343        // plane is offset to the far side of the apex from the cone's opening —
344        // the section lies entirely on the phantom nappe, so there is no real
345        // curve (RHS below is positive regardless of sign, so it can't catch this).
346        if e * c < 0.0 {
347            return Ok(vec![]);
348        }
349        // |A|(s − s_c)² + k·t² = RHS, with s_c = ecp/|A| and
350        // RHS = e²·k·(1−k)/|A| (always > 0 for a real ellipse).
351        let s_c = e * c * p / abs_a;
352        let rhs = e * e * k * (1.0 - k) / abs_a;
353        if rhs <= 0.0 {
354            return Ok(vec![]);
355        }
356        let semi_s = (rhs / abs_a).sqrt(); // extent along e1
357        let semi_t = (rhs / k).sqrt(); // extent along e2
358        if semi_s < 1e-12 || semi_t < 1e-12 {
359            return Ok(vec![]);
360        }
361        let center = apex + normal * e + e1 * s_c;
362        let (semi_major, semi_minor, u_axis, v_axis) = if semi_s >= semi_t {
363            (semi_s, semi_t, e1, e2)
364        } else {
365            (semi_t, semi_s, e2, e1)
366        };
367        let ellipse = Ellipse3D::with_axes(center, normal, semi_major, semi_minor, u_axis, v_axis)?;
368        return Ok(vec![ExactIntersectionCurve::Ellipse(ellipse)]);
369    }
370
371    // Parabola / hyperbola (and the near-parabolic ellipse margin): the section
372    // is unbounded, so emit bounded, branch-separated sample chains.
373    let chains = sample_plane_cone(cone, normal, d, reach)?;
374    Ok(chains
375        .into_iter()
376        .map(ExactIntersectionCurve::Points)
377        .collect())
378}
379
380/// The exact arc from `from` to `to` of a plane's parabola or hyperbola
381/// section of a cone, both ends on one branch of it, as a rational quadratic
382/// NURBS.
383///
384/// A hyperbola `x = a cosh φ, y = b sinh φ` (in the plane frame of
385/// `exact_plane_cone`) is cut into pieces of at most one unit of `φ`, each
386/// a conic Bézier: its middle point is where the end tangents meet and its
387/// middle weight is the cosh of half the piece's span. A parabola is one
388/// polynomial quadratic. `None` for an elliptic or circular section, a plane
389/// through the apex, or ends off one branch of the section.
390///
391/// # Errors
392///
393/// Returns an error if the plane normal is zero or the curve cannot be built.
394#[allow(clippy::many_single_char_names)]
395pub fn plane_cone_conic_arc(
396    cone: &ConicalSurface,
397    normal: Vec3,
398    d: f64,
399    from: Point3,
400    to: Point3,
401) -> Result<Option<NurbsCurve>, MathError> {
402    let len = normal.length();
403    if len < 1e-15 {
404        return Err(MathError::ZeroVector);
405    }
406    let (normal, d) = (normal * (1.0 / len), d / len);
407    let axis = cone.axis();
408    let c = normal.dot(axis);
409    let p2 = (1.0 - c * c).max(0.0);
410    let p = p2.sqrt();
411    let k = cone.half_angle().sin().powi(2);
412    let a_coeff = p2 - k;
413    let m = Vec3::new(
414        axis.x() - c * normal.x(),
415        axis.y() - c * normal.y(),
416        axis.z() - c * normal.z(),
417    );
418    let m_len = m.length();
419    if m_len < 1e-12 || a_coeff < -1e-9 {
420        return Ok(None);
421    }
422    let e1 = m * (1.0 / m_len);
423    let e2 = normal.cross(e1);
424    let apex = cone.apex();
425    let e = d - dot_np(normal, apex);
426    let origin = apex + normal * e;
427    let plane_st = |q: Point3| {
428        let w = q - origin;
429        (w.dot(e1), w.dot(e2))
430    };
431    let ((s0, t0), (s1, t1)) = (plane_st(from), plane_st(to));
432    let scale = s0.abs().max(t0.abs()).max(s1.abs()).max(t1.abs()).max(1.0);
433    if e.abs() < 1e-9 * scale || (from - to).length() <= 1e-9 * scale {
434        return Ok(None);
435    }
436    let point = |s: f64, t: f64| origin + e1 * s + e2 * t;
437    let on_curve = |q: Point3, r: Point3| (q - r).length() <= 1e-6 * scale;
438    let (control, weights) = if a_coeff.abs() <= 1e-9 {
439        // (p² − k) s² vanishes: 2ecp·s + e²(c² − k) = k·t², s = α t² + β.
440        let lin = 2.0 * e * c * p;
441        if lin.abs() < 1e-12 * scale {
442            return Ok(None);
443        }
444        let (alpha, beta) = (k / lin, -e * e * (c * c - k) / lin);
445        if !on_curve(point(alpha * t0 * t0 + beta, t0), from)
446            || !on_curve(point(alpha * t1 * t1 + beta, t1), to)
447        {
448            return Ok(None);
449        }
450        let mid = point(alpha * t0 * t1 + beta, 0.5 * (t0 + t1));
451        (vec![from, mid, to], vec![1.0; 3])
452    } else {
453        // A (s − s_c)² − k t² = R with R = e² k (1 − k) / A.
454        let s_c = -e * c * p / a_coeff;
455        let r = e * e * k * (1.0 - k) / a_coeff;
456        if r <= 0.0 {
457            return Ok(None);
458        }
459        let (a, b) = ((r / a_coeff).sqrt(), (r / k).sqrt());
460        let (x0, x1) = (s0 - s_c, s1 - s_c);
461        if x0 * x1 <= 0.0 {
462            return Ok(None);
463        }
464        let side = x0.signum();
465        let hyperbola = |phi: f64| point(s_c + side * a * phi.cosh(), b * phi.sinh());
466        let (phi0, phi1) = ((t0 / b).asinh(), (t1 / b).asinh());
467        if !on_curve(hyperbola(phi0), from) || !on_curve(hyperbola(phi1), to) {
468            return Ok(None);
469        }
470        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
471        let pieces = ((phi1 - phi0).abs().ceil() as usize).max(1);
472        let mut control = vec![from];
473        let mut weights = vec![1.0];
474        for i in 0..pieces {
475            #[allow(clippy::cast_precision_loss)]
476            let (fa, fb) = (i as f64 / pieces as f64, (i + 1) as f64 / pieces as f64);
477            let (pa, pb) = (phi0 + (phi1 - phi0) * fa, phi0 + (phi1 - phi0) * fb);
478            let (mid, half) = (0.5 * (pa + pb), 0.5 * (pb - pa));
479            let w = half.cosh();
480            control.push(point(s_c + side * a * mid.cosh() / w, b * mid.sinh() / w));
481            weights.push(w);
482            control.push(if i + 1 == pieces { to } else { hyperbola(pb) });
483            weights.push(1.0);
484        }
485        (control, weights)
486    };
487    let pieces = (control.len() - 1) / 2;
488    let mut knots = vec![0.0; 3];
489    for i in 1..pieces {
490        #[allow(clippy::cast_precision_loss)]
491        knots.extend([i as f64; 2]);
492    }
493    #[allow(clippy::cast_precision_loss)]
494    knots.extend([pieces as f64; 3]);
495    let curve = NurbsCurve::new(2, knots, control, weights)?;
496    // The closed forms drop terms that vanish only on the exact conic (a
497    // barely elliptic section read as a parabola), so the arc must meet the
498    // cone between its ends too: a point at radius ρ and height h off the
499    // apex lies |ρ sin α − |h| cos α| from it.
500    let (sin_a, cos_a) = cone.half_angle().sin_cos();
501    let off_cone = |q: Point3| {
502        let w = q - apex;
503        let h = w.dot(axis);
504        (w - axis * h)
505            .length()
506            .mul_add(sin_a, -(h.abs() * cos_a))
507            .abs()
508    };
509    for i in 0..pieces {
510        for f in [0.25, 0.5, 0.75] {
511            #[allow(clippy::cast_precision_loss)]
512            if off_cone(curve.evaluate(i as f64 + f)) > 1e-9 * scale {
513                return Ok(None);
514            }
515        }
516    }
517    Ok(Some(curve))
518}
519
520/// Reference to an analytic surface for intersection dispatch.
521#[derive(Clone, Copy)]
522pub enum AnalyticSurface<'a> {
523    /// Cylindrical surface reference.
524    Cylinder(&'a CylindricalSurface),
525    /// Conical surface reference.
526    Cone(&'a ConicalSurface),
527    /// Spherical surface reference.
528    Sphere(&'a SphericalSurface),
529    /// Toroidal surface reference.
530    Torus(&'a ToroidalSurface),
531}
532
533/// Compute `n . p` treating a `Point3` as a position vector.
534fn dot_np(n: Vec3, p: Point3) -> f64 {
535    n.dot(Vec3::new(p.x(), p.y(), p.z()))
536}
537
538/// Intersect a plane with an analytic surface.
539///
540/// The plane is defined by `dot(normal, p) = d`.
541///
542/// # Errors
543///
544/// Returns an error if the intersection computation fails.
545pub fn intersect_plane_analytic(
546    surface: AnalyticSurface<'_>,
547    normal: Vec3,
548    d: f64,
549) -> Result<Vec<IntersectionCurve>, MathError> {
550    match surface {
551        AnalyticSurface::Cylinder(cyl) => intersect_plane_cylinder(cyl, normal, d),
552        AnalyticSurface::Cone(cone) => intersect_plane_cone(cone, normal, d),
553        AnalyticSurface::Sphere(sphere) => intersect_plane_sphere(sphere, normal, d),
554        AnalyticSurface::Torus(torus) => intersect_plane_torus(torus, normal, d),
555    }
556}
557
558/// Sample points on the plane-analytic intersection without NURBS curve fitting.
559///
560/// Returns chains of ordered 3D sample points. Each chain is one connected
561/// component of the intersection curve. This is much faster than
562/// `intersect_plane_analytic` when only sample points are needed (e.g. for
563/// boolean intersection segment generation).
564///
565/// # Errors
566///
567/// Returns an error if the intersection computation fails.
568pub fn sample_plane_analytic(
569    surface: AnalyticSurface<'_>,
570    normal: Vec3,
571    d: f64,
572) -> Result<Vec<Vec<Point3>>, MathError> {
573    match surface {
574        AnalyticSurface::Cylinder(cyl) => sample_plane_cylinder(cyl, normal, d),
575        AnalyticSurface::Cone(cone) => sample_plane_cone(cone, normal, d, 0.0),
576        AnalyticSurface::Sphere(sphere) => sample_plane_sphere(sphere, normal, d),
577        AnalyticSurface::Torus(torus) => sample_plane_torus(torus, normal, d),
578    }
579}
580
581/// Sample the plane-cylinder intersection as ordered 3D points.
582#[allow(clippy::cast_precision_loss, clippy::unnecessary_wraps)]
583fn sample_plane_cylinder(
584    cyl: &CylindricalSurface,
585    normal: Vec3,
586    d: f64,
587) -> Result<Vec<Vec<Point3>>, MathError> {
588    let n_samples = 64_usize;
589    let mut points = Vec::with_capacity(n_samples + 1);
590
591    for i in 0..=n_samples {
592        let u = TAU * (i as f64) / (n_samples as f64);
593        let base = cyl.evaluate(u, 0.0);
594        let n_dot_axis = normal.dot(cyl.axis());
595        let n_dot_base = dot_np(normal, base);
596
597        if n_dot_axis.abs() < 1e-12 {
598            if (n_dot_base - d).abs() < 1e-6 {
599                points.push(base);
600            }
601        } else {
602            let v = (d - n_dot_base) / n_dot_axis;
603            if v.abs() <= 100.0 {
604                points.push(cyl.evaluate(u, v));
605            }
606        }
607    }
608
609    if points.len() < 2 {
610        Ok(vec![])
611    } else {
612        Ok(vec![points])
613    }
614}
615
616/// Sample the plane-sphere intersection as ordered 3D points.
617#[allow(clippy::cast_precision_loss)]
618fn sample_plane_sphere(
619    sphere: &SphericalSurface,
620    normal: Vec3,
621    d: f64,
622) -> Result<Vec<Vec<Point3>>, MathError> {
623    let h = dot_np(normal, sphere.center()) - d;
624    let r = sphere.radius();
625
626    if h.abs() > r - 1e-10 {
627        return Ok(vec![]);
628    }
629
630    let circle_r = (r.mul_add(r, -(h * h))).sqrt();
631    let circle_center = Point3::new(
632        h.mul_add(-normal.x(), sphere.center().x()),
633        h.mul_add(-normal.y(), sphere.center().y()),
634        h.mul_add(-normal.z(), sphere.center().z()),
635    );
636
637    let basis = Frame3::from_normal(circle_center, normal)?;
638    let u_dir = basis.x;
639    let v_dir = basis.y;
640
641    let n_samples = 64_usize;
642    let mut points = Vec::with_capacity(n_samples + 1);
643
644    for i in 0..=n_samples {
645        let theta = TAU * (i as f64) / (n_samples as f64);
646        let (sin_t, cos_t) = theta.sin_cos();
647        points.push(circle_center + u_dir * (circle_r * cos_t) + v_dir * (circle_r * sin_t));
648    }
649
650    Ok(vec![points])
651}
652
653/// Sample the plane-cone intersection as ordered 3D points.
654///
655/// The cone is the single real nappe `v >= 0` of `P(u,v) = apex + v·g(u)`.
656/// Along each generator `g(u)` the plane `n·P = d` is linear in `v`, so
657/// `v = (d − n·apex) / (n·g(u))`. We keep only `v >= 0` (the phantom `v < 0`
658/// nappe is geometrically absent) and `v` below a finite bound (near an
659/// asymptote `n·g(u) → 0` so `v → ∞` — those points run off the surface and
660/// must be excluded). The angular samples that survive form one contiguous arc
661/// (ellipse) or two (parabola/hyperbola, one per branch); each contiguous run
662/// is returned as a separate ordered chain so the consumer never stitches two
663/// disjoint branches into one curve.
664#[allow(clippy::cast_precision_loss, clippy::unnecessary_wraps)]
665fn sample_plane_cone(
666    cone: &ConicalSurface,
667    normal: Vec3,
668    d: f64,
669    reach: f64,
670) -> Result<Vec<Vec<Point3>>, MathError> {
671    let apex = cone.apex();
672    let n_dot_apex = dot_np(normal, apex);
673    let e = d - n_dot_apex;
674
675    // Per-generator solve: along g(u) the plane is linear in v, v = e / (n·g(u)).
676    // Sample u densely; keep only the real nappe (v >= 0) and skip near-asymptote
677    // generators (n·g(u) ≈ 0 → v → ∞).
678    let n_samples = 512_usize;
679    let mut vs: Vec<Option<f64>> = Vec::with_capacity(n_samples);
680    let mut v_min = f64::INFINITY;
681    for i in 0..n_samples {
682        let u = TAU * (i as f64) / (n_samples as f64);
683        let g = cone.evaluate(u, 1.0) - apex;
684        let n_dot_g = normal.dot(Vec3::new(g.x(), g.y(), g.z()));
685        if n_dot_g.abs() < 1e-12 {
686            vs.push(None);
687            continue;
688        }
689        let v = e / n_dot_g;
690        if v >= -1e-12 {
691            let v = v.max(0.0);
692            v_min = v_min.min(v);
693            vs.push(Some(v));
694        } else {
695            vs.push(None);
696        }
697    }
698
699    if !v_min.is_finite() {
700        return Ok(Vec::new());
701    }
702
703    // Bound the arc around the conic vertex (closest approach to the apex, at
704    // v_min). An ellipse is naturally bounded; a parabola/hyperbola is not, so
705    // cap the cone radius at a generous multiple of the vertex radius. This is
706    // scale-invariant and centred on where any finite cone face's overlap lies;
707    // the downstream consumer trims the fitted curve to the actual face AABB, so
708    // over-coverage is harmless. The floor handles a vertex at the apex (v_min≈0).
709    // A caller that knows its faces asks for their reach: an open hyperbola
710    // (a vertex close to the axis) crosses a rim far past eight vertex radii.
711    let v_max = (8.0 * v_min).max(v_min + 4.0).max(reach);
712
713    // Per-sample v within the cap; the raw values stay in `vs` for the
714    // boundary solve below.
715    let kept: Vec<Option<f64>> = vs.iter().map(|v| v.filter(|&v| v <= v_max)).collect();
716
717    let point_at = |u: f64, v: f64| -> Point3 {
718        let g = cone.evaluate(u, 1.0) - apex;
719        apex + g * v
720    };
721    #[allow(clippy::cast_precision_loss)]
722    let u_of = |i: usize| TAU * (i as f64) / (n_samples as f64);
723    let n_dot_g_at = |u: f64| -> f64 {
724        let g = cone.evaluate(u, 1.0) - apex;
725        normal.dot(Vec3::new(g.x(), g.y(), g.z()))
726    };
727
728    if kept.iter().all(Option::is_some) {
729        // Closed loop (ellipse regime): emit all points and repeat the first.
730        let mut pts: Vec<Point3> = kept
731            .iter()
732            .enumerate()
733            .filter_map(|(i, v)| v.map(|v| point_at(u_of(i), v)))
734            .collect();
735        if let Some(&first) = pts.first() {
736            pts.push(first);
737        }
738        return Ok(vec![pts]);
739    }
740
741    // A hyperbola/parabola tail diverges as 1/(n·g), so between the last kept
742    // sample and its dropped neighbour v can leap far past `v_max` in one
743    // uniform-u pitch — and any finite face window inside that leap is lost
744    // (a taper cone grazed 0.05 by a prism plane lost its entire 0.5-tall
745    // section to exactly this aliasing). Extend each run end to the exact
746    // `v_max` boundary: bisect u for `n·g(u) = e/v_max` inside the dropped
747    // pitch (n·g is monotone there — its extrema sit at the conic vertex,
748    // far from any asymptote), then fill the tail with uniform-u samples.
749    let tail = |i_end: usize, forward: bool, kept: &[Option<f64>]| -> Vec<Point3> {
750        let Some(v_end) = kept[i_end] else {
751            return Vec::new();
752        };
753        let u_end = u_of(i_end);
754        #[allow(clippy::cast_precision_loss)]
755        let pitch = TAU / (n_samples as f64);
756        let u_next = if forward {
757            u_end + pitch
758        } else {
759            u_end - pitch
760        };
761        let target = e / v_max;
762        let h_end = n_dot_g_at(u_end) - target;
763        let h_next = n_dot_g_at(u_next) - target;
764        if v_end >= v_max || h_end == 0.0 || h_end.signum() == h_next.signum() {
765            return Vec::new();
766        }
767        let (mut lo, mut hi) = (u_end, u_next);
768        for _ in 0..60 {
769            let mid = f64::midpoint(lo, hi);
770            if (n_dot_g_at(mid) - target).signum() == h_end.signum() {
771                lo = mid;
772            } else {
773                hi = mid;
774            }
775        }
776        let u_star = f64::midpoint(lo, hi);
777        let tail_n = 8_usize;
778        (1..=tail_n)
779            .filter_map(|k| {
780                #[allow(clippy::cast_precision_loss)]
781                let u = u_end + (u_star - u_end) * (k as f64) / (tail_n as f64);
782                let ng = n_dot_g_at(u);
783                if ng.abs() < 1e-12 {
784                    return None;
785                }
786                let v = e / ng;
787                (v >= -1e-12 && v <= v_max * (1.0 + 1e-9)).then(|| point_at(u, v.max(0.0)))
788            })
789            .collect()
790    };
791
792    // Split into contiguous runs of kept samples, treating the array as
793    // circular (rotate past a gap) so a branch straddling u=0 stays whole.
794    let gap = kept.iter().position(Option::is_none).unwrap_or(0);
795    let mut chains: Vec<Vec<Point3>> = Vec::new();
796    let mut run: Vec<usize> = Vec::new();
797    let flush = |run: &mut Vec<usize>, chains: &mut Vec<Vec<Point3>>| {
798        if run.len() >= 2 {
799            let first = run[0];
800            let last = run[run.len() - 1];
801            let mut pts: Vec<Point3> = tail(first, false, &kept);
802            pts.reverse();
803            pts.extend(
804                run.iter()
805                    .filter_map(|&i| kept[i].map(|v| point_at(u_of(i), v))),
806            );
807            pts.extend(tail(last, true, &kept));
808            chains.push(pts);
809        }
810        run.clear();
811    };
812    for k in 0..n_samples {
813        let idx = (gap + k) % n_samples;
814        if kept[idx].is_some() {
815            run.push(idx);
816        } else {
817            flush(&mut run, &mut chains);
818        }
819    }
820    flush(&mut run, &mut chains);
821    Ok(chains.into_iter().filter(|c| c.len() >= 2).collect())
822}
823
824/// Sample the plane-torus intersection as ordered 3D points.
825///
826/// Uses the same closed-form loops as `intersect_plane_torus`
827/// but skips NURBS curve fitting (the callers here only need the points).
828#[allow(clippy::unnecessary_wraps)] // sibling match-arms and `?` callers need `Result`
829fn sample_plane_torus(
830    torus: &ToroidalSurface,
831    normal: Vec3,
832    d: f64,
833) -> Result<Vec<Vec<Point3>>, MathError> {
834    Ok(plane_torus_loops(torus, normal, d, 128)
835        .into_iter()
836        .map(|run| run.into_iter().map(|p| p.point).collect())
837        .collect())
838}
839
840/// Intersect a plane with a cylindrical surface.
841///
842/// For each `u` in `[0, 2pi)`, the cylinder point is linear in `v`,
843/// so the plane equation `dot(normal, P(u,v)) = d` is linear in `v`
844/// and can be solved directly.
845///
846/// # Errors
847///
848/// Returns an error if curve fitting fails.
849#[allow(clippy::cast_precision_loss)]
850pub fn intersect_plane_cylinder(
851    cyl: &CylindricalSurface,
852    normal: Vec3,
853    d: f64,
854) -> Result<Vec<IntersectionCurve>, MathError> {
855    let n_samples = 64_usize;
856    let mut points_3d = Vec::new();
857    let mut ipoints = Vec::new();
858
859    for i in 0..=n_samples {
860        let u = TAU * (i as f64) / (n_samples as f64);
861        // P(u, v) = origin + r*(cos(u)*x + sin(u)*y) + v*axis
862        // dot(normal, P) = d  =>  dot(normal, base(u)) + v * dot(normal, axis) = d
863        let base = cyl.evaluate(u, 0.0);
864        let n_dot_axis = normal.dot(cyl.axis());
865        let n_dot_base = dot_np(normal, base);
866
867        if n_dot_axis.abs() < 1e-12 {
868            // Plane parallel to axis -- check if base is on plane.
869            if (n_dot_base - d).abs() < 1e-6 {
870                let pt = base;
871                points_3d.push(pt);
872                ipoints.push(IntersectionPoint {
873                    point: pt,
874                    param1: (u, 0.0),
875                    param2: (0.0, 0.0),
876                });
877            }
878        } else {
879            let v = (d - n_dot_base) / n_dot_axis;
880            // Only keep points within a reasonable v range.
881            if v.abs() <= 100.0 {
882                let pt = cyl.evaluate(u, v);
883                points_3d.push(pt);
884                ipoints.push(IntersectionPoint {
885                    point: pt,
886                    param1: (u, v),
887                    param2: (0.0, 0.0),
888                });
889            }
890        }
891    }
892
893    build_curves_from_points(&points_3d, ipoints)
894}
895
896/// Intersect a plane with a spherical surface.
897///
898/// The intersection of a plane with a sphere is a circle (or empty/point).
899/// Computes the circle center, radius, and samples points on it.
900///
901/// # Errors
902///
903/// Returns an error if curve fitting fails.
904#[allow(clippy::cast_precision_loss)]
905pub fn intersect_plane_sphere(
906    sphere: &SphericalSurface,
907    normal: Vec3,
908    d: f64,
909) -> Result<Vec<IntersectionCurve>, MathError> {
910    let h = dot_np(normal, sphere.center()) - d;
911    let r = sphere.radius();
912
913    // No intersection if plane is too far from center.
914    if h.abs() > r - 1e-10 {
915        return Ok(vec![]);
916    }
917
918    let circle_r = (r.mul_add(r, -(h * h))).sqrt();
919    let circle_center = Point3::new(
920        h.mul_add(-normal.x(), sphere.center().x()),
921        h.mul_add(-normal.y(), sphere.center().y()),
922        h.mul_add(-normal.z(), sphere.center().z()),
923    );
924
925    // Build a local frame on the plane.
926    let basis = Frame3::from_normal(circle_center, normal)?;
927    let u_dir = basis.x;
928    let v_dir = basis.y;
929
930    let n_samples = 64_usize;
931    let mut points_3d = Vec::new();
932    let mut ipoints = Vec::new();
933
934    for i in 0..=n_samples {
935        let theta = TAU * (i as f64) / (n_samples as f64);
936        let (sin_t, cos_t) = theta.sin_cos();
937        let pt = circle_center + u_dir * (circle_r * cos_t) + v_dir * (circle_r * sin_t);
938        points_3d.push(pt);
939        ipoints.push(IntersectionPoint {
940            point: pt,
941            param1: (theta, 0.0),
942            param2: (0.0, 0.0),
943        });
944    }
945
946    build_curves_from_points(&points_3d, ipoints)
947}
948
949/// Intersect a plane with a conical surface.
950///
951/// Like a cylinder, the cone is linear along each generatrix, so the plane
952/// equation is linear in `v` for each fixed `u`.
953///
954/// # Errors
955///
956/// Returns an error if curve fitting fails.
957#[allow(clippy::cast_precision_loss)]
958pub fn intersect_plane_cone(
959    cone: &ConicalSurface,
960    normal: Vec3,
961    d: f64,
962) -> Result<Vec<IntersectionCurve>, MathError> {
963    let n_samples = 64_usize;
964    let mut points_3d = Vec::new();
965    let mut ipoints = Vec::new();
966
967    for i in 0..n_samples {
968        let u = TAU * (i as f64) / (n_samples as f64);
969        // P(u, v) = apex + v * dir(u)
970        // dot(normal, apex) + v * dot(normal, dir(u)) = d
971        let apex = cone.apex();
972        let n_dot_apex = dot_np(normal, apex);
973        // dir(u) = P(u,1) - apex
974        let p1 = cone.evaluate(u, 1.0);
975        let dir = p1 - apex;
976        let n_dot_dir = normal.dot(dir);
977
978        if n_dot_dir.abs() < 1e-12 {
979            continue;
980        }
981
982        let v = (d - n_dot_apex) / n_dot_dir;
983        // Allow negative v — the cone surface extends in both directions from the apex.
984        if v.abs() > 1e-10 && v.abs() < 100.0 {
985            let pt = cone.evaluate(u, v);
986            points_3d.push(pt);
987            ipoints.push(IntersectionPoint {
988                point: pt,
989                param1: (u, v),
990                param2: (0.0, 0.0),
991            });
992        }
993    }
994
995    build_curves_from_points(&points_3d, ipoints)
996}
997
998/// Intersect a plane with a toroidal surface.
999///
1000/// The section is a degree-4 curve, but for each `v` the `u` values solve in
1001/// closed form (see `plane_torus_loops`), so it is sampled by a v-scan
1002/// and each loop is fitted to a NURBS curve.
1003///
1004/// # Errors
1005///
1006/// Never returns an error today (curve-fit failures drop the affected loop);
1007/// the `Result` is kept for signature parity with the other plane-analytic
1008/// intersectors.
1009#[allow(clippy::unnecessary_wraps)]
1010pub fn intersect_plane_torus(
1011    torus: &ToroidalSurface,
1012    normal: Vec3,
1013    d: f64,
1014) -> Result<Vec<IntersectionCurve>, MathError> {
1015    // The section satisfies a per-v closed form (see `plane_torus_loops`),
1016    // so scan v and solve u directly instead of a 2D sign-change grid with
1017    // Newton refinement: O(n) rather than O(n²), and every point is exact.
1018    let mut curves = Vec::new();
1019    for ipts in plane_torus_loops(torus, normal, d, 128) {
1020        let pts: Vec<Point3> = ipts.iter().map(|p| p.point).collect();
1021        if let Ok(curve) = interpolate(&pts, 3.min(pts.len() - 1)) {
1022            curves.push(IntersectionCurve {
1023                curve,
1024                points: ipts,
1025            });
1026        }
1027    }
1028
1029    Ok(curves)
1030}
1031
1032/// The fewest and most steps along each branch of a loop a plane cuts from
1033/// a torus between its two turns.
1034const PLANE_TORUS_LOOP_SAMPLES: (f64, f64) = (24.0, 512.0);
1035
1036/// The loops a plane cuts from a torus, each as points on the torus in
1037/// order, closed by repeating its first point.
1038///
1039/// In the torus's own frame let `a = n·X`, `b = n·Y`, `c = n·Z`,
1040/// `s = hypot(a, b)`, `phi = atan2(b, a)`. Substituting the torus
1041/// parameterization into `n·P = d` gives
1042///   `(R + r·cos v)·s·cos(u − phi) + r·c·sin v = d − n·center`,
1043/// so at each tube angle `v` where `|rhs(v)| <= 1`, with
1044/// `rhs(v) = (d − n·center − r·c·sin v) / (s·(R + r·cos v))`, the section's
1045/// two branches are `u = phi ± acos(rhs(v))`. Each run of `v` where the
1046/// section exists is one loop: out along one branch to where the run ends
1047/// and the branches meet, and back along the other. A section present at
1048/// every `v` is two loops winding the tube, one per branch. Every point
1049/// comes from the closed form, so the loops need no chaining.
1050///
1051/// A loop whose branches touch inside its run (a plane tangent to an
1052/// equator) is a self-touching section, not a simple loop, and is left open.
1053///
1054/// When `s ≈ 0` the plane is perpendicular to the axis and the section is up
1055/// to two full circles at the `v` values solving `r·c·sin v = d − n·center`,
1056/// sampled by scanning `u`.
1057#[allow(clippy::cast_precision_loss, clippy::too_many_lines)]
1058fn plane_torus_loops(
1059    torus: &ToroidalSurface,
1060    normal: Vec3,
1061    d: f64,
1062    n_v: usize,
1063) -> Vec<Vec<IntersectionPoint>> {
1064    let big_r = torus.major_radius();
1065    let small_r = torus.minor_radius();
1066    let a = normal.dot(torus.x_axis());
1067    let b = normal.dot(torus.y_axis());
1068    let c = normal.dot(torus.z_axis());
1069    let s = a.hypot(b);
1070    let phi = b.atan2(a);
1071    let d_local = d - dot_np(normal, torus.center());
1072    let point = |u: f64, v: f64| IntersectionPoint {
1073        point: torus.evaluate(u, v),
1074        param1: (u, v.rem_euclid(TAU)),
1075        param2: (0.0, 0.0),
1076    };
1077    let closed = |mut run: Vec<IntersectionPoint>| {
1078        run.push(run[0]);
1079        run
1080    };
1081
1082    // Plane perpendicular to the axis: the section is up to two full circles.
1083    if s < 1e-12 {
1084        if c.abs() < 1e-12 {
1085            return Vec::new();
1086        }
1087        let sin_v = d_local / (small_r * c);
1088        if sin_v.abs() > 1.0 + 1e-9 {
1089            return Vec::new();
1090        }
1091        let v0 = sin_v.clamp(-1.0, 1.0).asin();
1092        let v1 = std::f64::consts::PI - v0;
1093        let mut vs = vec![v0];
1094        // Skip the mirror circle when the plane is tangent: at the tube's top
1095        // v0 == v1, and at its bottom they differ by a whole turn.
1096        let apart = (v1 - v0).rem_euclid(TAU);
1097        if apart.min(TAU - apart) > 1e-9 {
1098            vs.push(v1);
1099        }
1100        return vs
1101            .into_iter()
1102            .map(|v| {
1103                closed(
1104                    (0..n_v)
1105                        .map(|i| point(TAU * (i as f64) / (n_v as f64), v))
1106                        .collect(),
1107                )
1108            })
1109            .collect();
1110    }
1111
1112    // The scan is offset by half a step so it never lands on a node where
1113    // the branches touch (the inner-tangent figure-eight at v = π).
1114    let step = TAU / (n_v as f64);
1115    let v_off = step * 0.5;
1116    // R + r·cos v > 0 on a ring torus.
1117    let rhs_at = |v: f64| (d_local - small_r * c * v.sin()) / (s * small_r.mul_add(v.cos(), big_r));
1118    let branch = |v: f64, sign: f64| point(sign.mul_add(rhs_at(v).clamp(-1.0, 1.0).acos(), phi), v);
1119    let inside = |v: f64| rhs_at(v).abs() <= 1.0;
1120    let scan: Vec<f64> = (0..n_v).map(|i| (i as f64).mul_add(step, v_off)).collect();
1121    // Whether |rhs| reaches 1 between two scan samples inside the section:
1122    // the branches touch there.
1123    let touches = |lo: f64, hi: f64| {
1124        let golden = 0.5 * (5.0_f64.sqrt() - 1.0);
1125        let (mut lo, mut hi) = (lo, hi);
1126        for _ in 0..80 {
1127            let (m1, m2) = (hi - golden * (hi - lo), lo + golden * (hi - lo));
1128            if rhs_at(m1).abs() > rhs_at(m2).abs() {
1129                hi = m2;
1130            } else {
1131                lo = m1;
1132            }
1133        }
1134        1.0 - rhs_at(f64::midpoint(lo, hi)).abs() < 1e-12
1135    };
1136    // Where the section's run ends between an inside and an outside sample.
1137    let turn = |v_in: f64, v_out: f64| {
1138        let (mut lo, mut hi) = (v_in, v_out);
1139        for _ in 0..60 {
1140            let mid = f64::midpoint(lo, hi);
1141            if inside(mid) {
1142                lo = mid;
1143            } else {
1144                hi = mid;
1145            }
1146        }
1147        lo
1148    };
1149    let in_scan: Vec<bool> = scan.iter().map(|&v| inside(v)).collect();
1150    if in_scan.iter().all(|&x| x) {
1151        let touching = scan.iter().any(|&v| touches(v, v + step));
1152        return [1.0, -1.0]
1153            .into_iter()
1154            .map(|sign| {
1155                let run: Vec<IntersectionPoint> = scan.iter().map(|&v| branch(v, sign)).collect();
1156                if touching { run } else { closed(run) }
1157            })
1158            .collect();
1159    }
1160    let Some(first) = (0..n_v).find(|&i| in_scan[i] && !in_scan[(i + n_v - 1) % n_v]) else {
1161        return Vec::new();
1162    };
1163    let mut loops = Vec::new();
1164    let mut k = 0;
1165    while k < n_v {
1166        let i = (first + k) % n_v;
1167        if !in_scan[i] {
1168            k += 1;
1169            continue;
1170        }
1171        // The run from scan sample `i`, its `v` unwrapped past a turn.
1172        let len = (0..n_v - k).take_while(|&j| in_scan[(i + j) % n_v]).count();
1173        let v_a = scan[i];
1174        let v_b = ((len - 1) as f64).mul_add(step, v_a);
1175        let run_v = |j: usize| (j as f64).mul_add(step, v_a);
1176        let (t_lo, t_hi) = (turn(v_a, v_a - step), turn(v_b, v_b + step));
1177        let touching = (0..len - 1).any(|j| touches(run_v(j), run_v(j + 1)));
1178        // `u` moves like the square root of the distance in `v` to a turn, so
1179        // the loop is sampled at `v = t_lo + (t_hi - t_lo) (1 - cos θ) / 2`
1180        // for even steps of `θ`: `v` then moves like `θ²` at each turn and the
1181        // points fall at near-even steps along the curve, turns included. A
1182        // loop that sweeps far round the axis on a short run of `v` (a plane
1183        // through the centre, tilted a little) takes a step per half scan
1184        // step of `u` its branches sweep.
1185        let (u_lo, u_hi) = (0..len)
1186            .map(run_v)
1187            .chain([t_lo, t_hi])
1188            .map(|v| rhs_at(v).clamp(-1.0, 1.0).acos())
1189            .fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), u| {
1190                (lo.min(u), hi.max(u))
1191            });
1192        let m = (len as f64)
1193            .max((n_v as f64) * (u_hi - u_lo) / std::f64::consts::PI)
1194            .max(PLANE_TORUS_LOOP_SAMPLES.0)
1195            .min(PLANE_TORUS_LOOP_SAMPLES.1)
1196            .ceil();
1197        let at = |k: f64| {
1198            let f = 0.5 * (1.0 - (std::f64::consts::PI * k / m).cos());
1199            (t_hi - t_lo).mul_add(f, t_lo)
1200        };
1201        let steps = m as usize;
1202        let mut pts: Vec<IntersectionPoint> =
1203            (0..=steps).map(|k| branch(at(k as f64), 1.0)).collect();
1204        pts.extend((1..steps).rev().map(|k| branch(at(k as f64), -1.0)));
1205        loops.push(if touching { pts } else { closed(pts) });
1206        k += len;
1207    }
1208    loops
1209}
1210
1211/// The two sections of a plane that crosses every tube cross-section of a
1212/// torus twice (one parallel to the axis within `R − r` of it, or tilted a
1213/// little from that): both branches `u = phi ± acos(rhs(v))` of
1214/// [`plane_torus_loops`] are then defined for every `v`, so each closes
1215/// into a loop that winds once around the tube. Sampled at `n_v` steps from
1216/// `v = 0`, the outer equator, so every such loop on a torus starts on one
1217/// latitude, as the tube cross-sections of a plane through the axis do.
1218/// `None` when a branch lapses somewhere or the two come close to meeting.
1219#[allow(clippy::cast_precision_loss)]
1220fn plane_torus_winding_loops(
1221    torus: &ToroidalSurface,
1222    normal: Vec3,
1223    d: f64,
1224    n_v: usize,
1225) -> Option<Vec<Vec<Point3>>> {
1226    let big_r = torus.major_radius();
1227    let small_r = torus.minor_radius();
1228    let a = normal.dot(torus.x_axis());
1229    let b = normal.dot(torus.y_axis());
1230    let c = normal.dot(torus.z_axis());
1231    let s = a.hypot(b);
1232    if s < 1e-12 * normal.length() || small_r >= big_r {
1233        return None;
1234    }
1235    let phi = b.atan2(a);
1236    let d_local = d - dot_np(normal, torus.center());
1237    let rhs = |v: f64| (d_local - small_r * c * v.sin()) / (s * small_r.mul_add(v.cos(), big_r));
1238    let dense = 8 * n_v;
1239    if (0..dense).any(|i| rhs(TAU * i as f64 / dense as f64).abs() > 1.0 - 1e-3) {
1240        return None;
1241    }
1242    let mut loops = [Vec::with_capacity(n_v + 1), Vec::with_capacity(n_v + 1)];
1243    for i in 0..n_v {
1244        let v = TAU * i as f64 / n_v as f64;
1245        let delta = rhs(v).acos();
1246        loops[0].push(torus.evaluate(phi + delta, v));
1247        loops[1].push(torus.evaluate(phi - delta, v));
1248    }
1249    Some(
1250        loops
1251            .into_iter()
1252            .map(|mut run| {
1253                run.push(run[0]);
1254                run
1255            })
1256            .collect(),
1257    )
1258}
1259
1260/// Real intersection parameters `t` of the line `origin + t·dir` with a torus.
1261///
1262/// A line meets a torus in up to four points (degree-4). Substituting the line
1263/// into the torus implicit `(a² + b² + c² + R² − r²)² = 4R²(a² + b²)` — where
1264/// `(a, b, c)` are the line point's coordinates in the torus frame — gives a
1265/// quartic in `t`, solved here for its real roots, each refined by Newton steps
1266/// on the tube it lies nearer (on a spindle torus, the near tube or the one
1267/// swung across the axis). `dir` need not be unit length; `t` is in units of
1268/// `dir`. Returns the roots sorted ascending (0–4 of them).
1269///
1270/// Used by the boolean section trimmer to find where a plane×torus oval exits a
1271/// box face's straight boundary edge — the exact crossing shared by the two
1272/// adjacent faces, which is what makes the notch watertight.
1273#[must_use]
1274pub fn intersect_line_torus(torus: &ToroidalSurface, origin: Point3, dir: Vec3) -> Vec<f64> {
1275    let c = torus.center();
1276    let (xa, ya, za) = (torus.x_axis(), torus.y_axis(), torus.z_axis());
1277    let big_r = torus.major_radius();
1278    let small_r = torus.minor_radius();
1279
1280    // Line point in torus frame: a(t)=a0+a1 t, b(t)=b0+b1 t, c(t)=c0+c1 t.
1281    let o = Vec3::new(origin.x() - c.x(), origin.y() - c.y(), origin.z() - c.z());
1282    let (a0, a1) = (xa.dot(o), xa.dot(dir));
1283    let (b0, b1) = (ya.dot(o), ya.dot(dir));
1284    let (c0, c1) = (za.dot(o), za.dot(dir));
1285
1286    // G(t) = a² + b² + c² + R² − r²  (quadratic: g2 t² + g1 t + g0)
1287    let g2 = a1.mul_add(a1, b1.mul_add(b1, c1 * c1));
1288    let g1 = 2.0 * a1.mul_add(a0, b1.mul_add(b0, c1 * c0));
1289    let g0 = a0.mul_add(
1290        a0,
1291        b0.mul_add(b0, c0.mul_add(c0, big_r.mul_add(big_r, -small_r * small_r))),
1292    );
1293
1294    // H(t) = 4R² (a² + b²)  (quadratic: h2 t² + h1 t + h0)
1295    let four_rr = 4.0 * big_r * big_r;
1296    let h2 = four_rr * a1.mul_add(a1, b1 * b1);
1297    let h1 = four_rr * (2.0 * a1.mul_add(a0, b1 * b0));
1298    let h0 = four_rr * a0.mul_add(a0, b0 * b0);
1299
1300    // Quartic G² − H = 0:  e4 t⁴ + e3 t³ + e2 t² + e1 t + e0.
1301    let e4 = g2 * g2;
1302    let e3 = 2.0 * g2 * g1;
1303    let e2 = g1.mul_add(g1, 2.0 * g2 * g0) - h2;
1304    let e1 = 2.0f64.mul_add(g1 * g0, -h1);
1305    let e0 = g0.mul_add(g0, -h0);
1306
1307    let mut roots = real_roots_quartic(e4, e3, e2, e1, e0);
1308    // The quartic also holds a spindle torus's tube swung across the axis
1309    // (`G = -sqrt(H)`), whose roots pair up with the tube's own a few hundredths
1310    // apart when `R` is small: the solver leaves them loose, and polishing one
1311    // against the near tube carries it beside the other root. Each root is
1312    // polished on the tube it lies nearer.
1313    let tube = |t: f64, across: bool| -> f64 {
1314        let p = origin + dir * t;
1315        let q = Vec3::new(p.x() - c.x(), p.y() - c.y(), p.z() - c.z());
1316        let (a, b, cc) = (xa.dot(q), ya.dot(q), za.dot(q));
1317        let centre = if across { -big_r } else { big_r };
1318        (a.hypot(b) - centre).hypot(cc) - small_r
1319    };
1320    for t in &mut roots {
1321        let across = big_r < small_r && tube(*t, true).abs() < tube(*t, false).abs();
1322        for _ in 0..8 {
1323            let eps = 1e-7;
1324            let f = tube(*t, across);
1325            let df = (tube(*t + eps, across) - tube(*t - eps, across)) / (2.0 * eps);
1326            if df.abs() <= 1e-12 {
1327                break;
1328            }
1329            let step = f / df;
1330            *t -= step;
1331            if step.abs() < 1e-14 * (1.0 + t.abs()) {
1332                break;
1333            }
1334        }
1335    }
1336    roots.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1337    roots.dedup_by(|a, b| (*a - *b).abs() < 1e-9 * (1.0 + b.abs()));
1338    roots
1339}
1340
1341/// Real roots of `c4 x⁴ + c3 x³ + c2 x² + c1 x + c0` via Durand–Kerner, falling
1342/// back to the lower-degree solvers when the leading coefficients vanish.
1343fn real_roots_quartic(c4: f64, c3: f64, c2: f64, c1: f64, c0: f64) -> Vec<f64> {
1344    // Degenerate leading coefficient → lower degree.
1345    if c4.abs() < 1e-14 {
1346        return real_roots_cubic(c3, c2, c1, c0);
1347    }
1348    // Monic: x⁴ + a x³ + b x² + c x + d.
1349    let (a, b, c, d) = (c3 / c4, c2 / c4, c1 / c4, c0 / c4);
1350    let eval = |z: Complex| -> Complex {
1351        // Horner.
1352        let mut acc = Complex::new(1.0, 0.0);
1353        acc = acc * z + Complex::new(a, 0.0);
1354        acc = acc * z + Complex::new(b, 0.0);
1355        acc = acc * z + Complex::new(c, 0.0);
1356        acc * z + Complex::new(d, 0.0)
1357    };
1358    // Durand–Kerner: four roots seeded on a circle, iterated to convergence.
1359    let seed = Complex::new(0.4, 0.9);
1360    let mut r = [
1361        Complex::new(1.0, 0.0),
1362        seed,
1363        seed * seed,
1364        seed * seed * seed,
1365    ];
1366    for _ in 0..100 {
1367        let mut max_step = 0.0_f64;
1368        for i in 0..4 {
1369            let mut denom = Complex::new(1.0, 0.0);
1370            for j in 0..4 {
1371                if i != j {
1372                    denom = denom * (r[i] - r[j]);
1373                }
1374            }
1375            if denom.norm() < 1e-300 {
1376                continue;
1377            }
1378            let step = eval(r[i]) / denom;
1379            r[i] = r[i] - step;
1380            max_step = max_step.max(step.norm());
1381        }
1382        if max_step < 1e-14 {
1383            break;
1384        }
1385    }
1386    // Keep roots with negligible imaginary part AND a small REAL-polynomial
1387    // residual — Durand–Kerner stops after a fixed iteration cap whether or not
1388    // it converged, so a non-converged iterate could otherwise be returned as a
1389    // spurious root. Evaluate the monic quartic at each candidate (real part) and
1390    // keep only |p(x)| below a magnitude-scaled tolerance; de-dup near-equal
1391    // roots (a double root converges to two near-identical iterates).
1392    let p_real = |x: f64| -> f64 { (((x + a) * x + b) * x + c) * x + d };
1393    let mut out: Vec<f64> = Vec::new();
1394    for z in r {
1395        if z.im.abs() >= 1e-7 {
1396            continue;
1397        }
1398        let x = z.re;
1399        // Residual tolerance scales with the polynomial's coefficient magnitude
1400        // and |x|^4 so large-coefficient quartics are not over-rejected.
1401        let scale = 1.0 + a.abs() + b.abs() + c.abs() + d.abs() + x.abs().powi(4);
1402        if p_real(x).abs() > 1e-6 * scale {
1403            continue;
1404        }
1405        if out.iter().any(|&y| (y - x).abs() < 1e-9 * (1.0 + x.abs())) {
1406            continue;
1407        }
1408        out.push(x);
1409    }
1410    out
1411}
1412
1413/// Real roots of `a x³ + b x² + c x + d` (Cardano), with quadratic fallback.
1414fn real_roots_cubic(a: f64, b: f64, c: f64, d: f64) -> Vec<f64> {
1415    if a.abs() < 1e-14 {
1416        return real_roots_quadratic(b, c, d);
1417    }
1418    // Depressed cubic t³ + p t + q via x = t − b/(3a).
1419    let (b, c, d) = (b / a, c / a, d / a);
1420    let p = c - b * b / 3.0;
1421    let q = 2.0 * b * b * b / 27.0 - b * c / 3.0 + d;
1422    let shift = -b / 3.0;
1423    let disc = q * q / 4.0 + p * p * p / 27.0;
1424    if disc > 1e-14 {
1425        let sq = disc.sqrt();
1426        let u = (-q / 2.0 + sq).cbrt();
1427        let v = (-q / 2.0 - sq).cbrt();
1428        vec![u + v + shift]
1429    } else if disc < -1e-14 {
1430        // Three real roots (trigonometric).
1431        let m = 2.0 * (-p / 3.0).sqrt();
1432        let theta = (3.0 * q / (p * m)).clamp(-1.0, 1.0).acos() / 3.0;
1433        (0..3)
1434            .map(|k| {
1435                m.mul_add(
1436                    (theta - 2.0 * std::f64::consts::PI * f64::from(k) / 3.0).cos(),
1437                    shift,
1438                )
1439            })
1440            .collect()
1441    } else {
1442        // Repeated roots.
1443        let u = (-q / 2.0).cbrt();
1444        vec![2.0 * u + shift, -u + shift]
1445    }
1446}
1447
1448/// Real roots of `a x² + b x + c`, with linear fallback.
1449fn real_roots_quadratic(a: f64, b: f64, c: f64) -> Vec<f64> {
1450    if a.abs() < 1e-14 {
1451        if b.abs() < 1e-14 {
1452            return Vec::new();
1453        }
1454        return vec![-c / b];
1455    }
1456    let disc = b * b - 4.0 * a * c;
1457    if disc < 0.0 {
1458        Vec::new()
1459    } else {
1460        let sq = disc.sqrt();
1461        vec![(-b - sq) / (2.0 * a), (-b + sq) / (2.0 * a)]
1462    }
1463}
1464
1465/// Minimal complex number for the quartic root finder.
1466#[derive(Clone, Copy)]
1467struct Complex {
1468    re: f64,
1469    im: f64,
1470}
1471
1472impl Complex {
1473    const fn new(re: f64, im: f64) -> Self {
1474        Self { re, im }
1475    }
1476    fn norm(self) -> f64 {
1477        self.re.hypot(self.im)
1478    }
1479}
1480
1481impl std::ops::Add for Complex {
1482    type Output = Self;
1483    fn add(self, o: Self) -> Self {
1484        Self::new(self.re + o.re, self.im + o.im)
1485    }
1486}
1487
1488impl std::ops::Sub for Complex {
1489    type Output = Self;
1490    fn sub(self, o: Self) -> Self {
1491        Self::new(self.re - o.re, self.im - o.im)
1492    }
1493}
1494
1495impl std::ops::Mul for Complex {
1496    type Output = Self;
1497    fn mul(self, o: Self) -> Self {
1498        Self::new(
1499            self.re.mul_add(o.re, -(self.im * o.im)),
1500            self.re.mul_add(o.im, self.im * o.re),
1501        )
1502    }
1503}
1504
1505impl std::ops::Div for Complex {
1506    type Output = Self;
1507    fn div(self, o: Self) -> Self {
1508        let den = o.re.mul_add(o.re, o.im * o.im);
1509        Self::new(
1510            self.re.mul_add(o.re, self.im * o.im) / den,
1511            self.im.mul_add(o.re, -(self.re * o.im)) / den,
1512        )
1513    }
1514}
1515
1516/// Build intersection curves from a collection of ordered 3D points.
1517///
1518/// If there are enough points, fits a NURBS curve through them.
1519fn build_curves_from_points(
1520    points_3d: &[Point3],
1521    ipoints: Vec<IntersectionPoint>,
1522) -> Result<Vec<IntersectionCurve>, MathError> {
1523    if points_3d.len() < 2 {
1524        return Ok(vec![]);
1525    }
1526
1527    let degree = 3.min(points_3d.len() - 1);
1528    let curve = interpolate(points_3d, degree)?;
1529    Ok(vec![IntersectionCurve {
1530        curve,
1531        points: ipoints,
1532    }])
1533}
1534
1535// -- Analytic-Analytic Intersection -------------------------------------------
1536
1537/// Intersect two analytic surfaces using a general marching approach.
1538///
1539/// Seeds intersection points by sampling both parameter spaces on a grid,
1540/// then marches along the intersection curve using the cross product of
1541/// the two surface normals as the tangent direction.
1542///
1543/// # Errors
1544///
1545/// Returns an error if curve fitting fails.
1546#[allow(
1547    clippy::cast_precision_loss,
1548    clippy::too_many_lines,
1549    clippy::similar_names,
1550    clippy::unnecessary_wraps,
1551    clippy::type_complexity
1552)]
1553pub fn intersect_analytic_analytic(
1554    a: AnalyticSurface<'_>,
1555    b: AnalyticSurface<'_>,
1556    grid_res: usize,
1557) -> Result<Vec<IntersectionCurve>, MathError> {
1558    intersect_analytic_analytic_bounded(a, b, grid_res, None, None)
1559}
1560
1561/// Intersect two analytic surfaces with optional v-range overrides.
1562///
1563/// When `v_range_hint_a` or `v_range_hint_b` is `Some((min, max))`, the
1564/// marching algorithm searches that v-range instead of the hardcoded default.
1565/// This is essential for cylinders and cones whose default v-range is small
1566/// (-1..1 or 0.01..2) but whose actual face may extend much further.
1567///
1568/// # Errors
1569///
1570/// Returns `MathError` if algebraic intersection fails or marching diverges.
1571pub fn intersect_analytic_analytic_bounded(
1572    a: AnalyticSurface<'_>,
1573    b: AnalyticSurface<'_>,
1574    grid_res: usize,
1575    v_range_hint_a: Option<(f64, f64)>,
1576    v_range_hint_b: Option<(f64, f64)>,
1577) -> Result<Vec<IntersectionCurve>, MathError> {
1578    intersect_analytic_analytic_impl(a, b, grid_res, v_range_hint_a, v_range_hint_b, None)
1579}
1580
1581/// [`intersect_analytic_analytic_bounded`] with its general marcher confined
1582/// to `region`.
1583///
1584/// The marcher starts only from seeds that converge into the region (with a
1585/// margin of about two grid cells) and stops where a curve leaves it. Pairs
1586/// solved in closed form are returned whole, as by the unconfined call. Two
1587/// bounded faces meet only inside both their boxes, so their overlap is a
1588/// sound region for a face pair.
1589///
1590/// # Errors
1591///
1592/// Returns `MathError` if algebraic intersection fails or marching diverges.
1593pub fn intersect_analytic_analytic_in_region(
1594    a: AnalyticSurface<'_>,
1595    b: AnalyticSurface<'_>,
1596    grid_res: usize,
1597    v_range_hint_a: Option<(f64, f64)>,
1598    v_range_hint_b: Option<(f64, f64)>,
1599    region: Aabb3,
1600) -> Result<Vec<IntersectionCurve>, MathError> {
1601    intersect_analytic_analytic_impl(a, b, grid_res, v_range_hint_a, v_range_hint_b, Some(region))
1602}
1603
1604fn intersect_analytic_analytic_impl(
1605    a: AnalyticSurface<'_>,
1606    b: AnalyticSurface<'_>,
1607    grid_res: usize,
1608    v_range_hint_a: Option<(f64, f64)>,
1609    v_range_hint_b: Option<(f64, f64)>,
1610    region: Option<Aabb3>,
1611) -> Result<Vec<IntersectionCurve>, MathError> {
1612    // Try algebraic specialization for known surface pairs before falling
1613    // back to the general marching approach.
1614    if let Some(result) = try_algebraic_intersection(&a, &b, v_range_hint_a, v_range_hint_b)? {
1615        return Ok(result);
1616    }
1617
1618    let (surf_a, norm_a, u_range_a, default_v_a) = surface_closures(&a);
1619    let (surf_b, norm_b, u_range_b, default_v_b) = surface_closures(&b);
1620    let v_range_a = v_range_hint_a.unwrap_or(default_v_a);
1621    let v_range_b = v_range_hint_b.unwrap_or(default_v_b);
1622
1623    // Compute characteristic surface dimensions for adaptive parameters.
1624    let diag_a = {
1625        let p00 = surf_a(u_range_a.0, v_range_a.0);
1626        let p11 = surf_a(u_range_a.1, v_range_a.1);
1627        (p00 - p11).length()
1628    };
1629    let diag_b = {
1630        let p00 = surf_b(u_range_b.0, v_range_b.0);
1631        let p11 = surf_b(u_range_b.1, v_range_b.1);
1632        (p00 - p11).length()
1633    };
1634    let char_size = diag_a.min(diag_b).max(0.1);
1635
1636    // Sample surface A on a grid. For each grid point, project it
1637    // analytically onto surface B to find the closest point, then check
1638    // if the distance is below threshold (indicating near-intersection).
1639    #[allow(clippy::type_complexity)]
1640    let mut seeds: Vec<(Point3, (f64, f64), (f64, f64))> = Vec::new();
1641    // Coarse threshold scales with the surface size — the distance from
1642    // a grid point on A to its projection on B can be large even near
1643    // the intersection (e.g., sphere R=2 and cylinder R=1 → gap ≈ 1).
1644    let seed_threshold = diag_a.max(diag_b).max(1.0) * 0.5;
1645    let mut min_dist = f64::INFINITY;
1646
1647    #[allow(clippy::cast_precision_loss)]
1648    for ia in 0..grid_res {
1649        for ja in 0..grid_res {
1650            let ua =
1651                u_range_a.0 + (u_range_a.1 - u_range_a.0) * (ia as f64 + 0.5) / (grid_res as f64);
1652            let va =
1653                v_range_a.0 + (v_range_a.1 - v_range_a.0) * (ja as f64 + 0.5) / (grid_res as f64);
1654
1655            let pa = surf_a(ua, va);
1656
1657            // Analytically project onto surface B.
1658            let (ub, vb) = project_analytic(&b, pa, u_range_b, v_range_b);
1659            let pb = surf_b(ub, vb);
1660            let dist = (pa - pb).length();
1661            min_dist = min_dist.min(dist);
1662
1663            if dist < seed_threshold {
1664                // Use the coarse seed directly. The marching algorithm
1665                // corrects positions at each step via projection, so seeds
1666                // don't need to be on the exact intersection — they just
1667                // need to be close enough for the marcher to converge.
1668                let mid = Point3::new(
1669                    (pa.x() + pb.x()) * 0.5,
1670                    (pa.y() + pb.y()) * 0.5,
1671                    (pa.z() + pb.z()) * 0.5,
1672                );
1673                seeds.push((mid, (ua, va), (ub, vb)));
1674            }
1675        }
1676    }
1677
1678    // Cheap rejection: the grid samples surface A; the closest sample's
1679    // distance to B lower-bounds how near the two bounded patches come. A
1680    // transversal crossing puts a sample within ~one grid cell of it
1681    // (distance on the order of a cell), so if even the nearest sample is
1682    // several cells away the patches cannot cross — skip the expensive
1683    // marching and return empty. Result-preserving: non-crossing pairs
1684    // already march to nothing, just slowly (this is the gridfinity lip's
1685    // ~80 inner-wall × outer-wall pairs that dominate pavefiller time).
1686    let reject_dist = (char_size / grid_res as f64) * 3.0;
1687    if min_dist > reject_dist {
1688        return Ok(vec![]);
1689    }
1690
1691    if seeds.is_empty() {
1692        return Ok(vec![]);
1693    }
1694
1695    // Aggressively deduplicate seeds — we only need 1-2 per intersection
1696    // branch. Scale dedup radius to ~2% of characteristic surface size
1697    // (at least 10× the march step size) to avoid redundant marches.
1698    let march_step = (char_size * 0.02).clamp(0.005, 0.5);
1699    let dedup_radius = march_step * 10.0;
1700    let mut unique_seeds = Vec::new();
1701    for seed in &seeds {
1702        let dominated = unique_seeds
1703            .iter()
1704            .any(|s: &(Point3, (f64, f64), (f64, f64))| (s.0 - seed.0).length() < dedup_radius);
1705        if !dominated {
1706            unique_seeds.push(*seed);
1707        }
1708    }
1709
1710    // A seed is only near the intersection; settle it onto the curve before
1711    // asking whether it lies in the region, so a coarse grid point beside a
1712    // short in-region span still starts its march, and march from there, so
1713    // the march does not begin outside the region and stop at once.
1714    let region = region.map(|r| r.expanded(2.0 * char_size / grid_res as f64));
1715    if let Some(r) = region {
1716        for seed in &mut unique_seeds {
1717            let mut p = seed.0;
1718            for _ in 0..8 {
1719                let (ua, va) = project_analytic(&a, p, u_range_a, v_range_a);
1720                let pa = surf_a(ua, va);
1721                let (ub, vb) = project_analytic(&b, pa, u_range_b, v_range_b);
1722                let pb = surf_b(ub, vb);
1723                p = Point3::new(
1724                    (pa.x() + pb.x()) * 0.5,
1725                    (pa.y() + pb.y()) * 0.5,
1726                    (pa.z() + pb.z()) * 0.5,
1727                );
1728                if (pa - pb).length() < 1e-9 {
1729                    break;
1730                }
1731            }
1732            seed.0 = p;
1733        }
1734        unique_seeds.retain(|seed| r.contains_point(seed.0));
1735    }
1736
1737    // March from each seed.
1738    let mut curves = Vec::new();
1739    let mut used_seeds = vec![false; unique_seeds.len()];
1740
1741    for si in 0..unique_seeds.len() {
1742        if used_seeds[si] {
1743            continue;
1744        }
1745        used_seeds[si] = true;
1746
1747        let march_result = march_analytic_intersection(
1748            &a,
1749            &b,
1750            surf_a.as_ref(),
1751            norm_a.as_ref(),
1752            surf_b.as_ref(),
1753            norm_b.as_ref(),
1754            unique_seeds[si].0,
1755            u_range_a,
1756            v_range_a,
1757            u_range_b,
1758            v_range_b,
1759            march_step,
1760            is_u_periodic(&a),
1761            is_u_periodic(&b),
1762            region,
1763        );
1764
1765        if march_result.len() >= 2 {
1766            for (sj, other) in unique_seeds.iter().enumerate() {
1767                if !used_seeds[sj]
1768                    && march_result
1769                        .iter()
1770                        .any(|p| (*p - other.0).length() < dedup_radius)
1771                {
1772                    used_seeds[sj] = true;
1773                }
1774            }
1775
1776            let ipts: Vec<IntersectionPoint> = march_result
1777                .iter()
1778                .map(|&pt| IntersectionPoint {
1779                    point: pt,
1780                    param1: (0.0, 0.0),
1781                    param2: (0.0, 0.0),
1782                })
1783                .collect();
1784
1785            let degree = 3.min(march_result.len() - 1);
1786            if let Ok(curve) = interpolate(&march_result, degree) {
1787                curves.push(IntersectionCurve {
1788                    curve,
1789                    points: ipts,
1790                });
1791            }
1792        }
1793    }
1794
1795    Ok(curves)
1796}
1797
1798/// Try algebraic (closed-form or semi-algebraic) intersection for known
1799/// surface pairs before falling back to general marching.
1800///
1801/// Returns `Some(curves)` if a specialized method exists, `None` otherwise.
1802///
1803/// Currently handles:
1804/// - **Sphere-sphere**: intersection is a circle (plane through the two centers)
1805/// - **Coaxial cylinders**: same axis → circle(s) or empty
1806/// - **Sphere-cylinder**: reduce to quadratic in one parameter
1807/// - **Cone-cylinder**: parallel axes in the cone's own `v`, other axes
1808///   along the cylinder's rulings
1809/// - **Cone-sphere**: off the cone's axis, along the cone's generators
1810/// - **Torus-cylinder**: along the cylinder's rulings
1811#[allow(clippy::too_many_lines)]
1812fn try_algebraic_intersection(
1813    a: &AnalyticSurface<'_>,
1814    b: &AnalyticSurface<'_>,
1815    v_range_a: Option<(f64, f64)>,
1816    v_range_b: Option<(f64, f64)>,
1817) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
1818    match (a, b) {
1819        (AnalyticSurface::Cone(cone), AnalyticSurface::Cylinder(cyl)) => Ok(
1820            algebraic_parallel_cone_cylinder(cone, cyl, v_range_a, v_range_b)?
1821                .or_else(|| ruling_cone_cylinder(cone, cyl, true)),
1822        ),
1823        (AnalyticSurface::Cylinder(cyl), AnalyticSurface::Cone(cone)) => Ok(
1824            algebraic_parallel_cone_cylinder(cone, cyl, v_range_b, v_range_a)?
1825                .or_else(|| ruling_cone_cylinder(cone, cyl, false)),
1826        ),
1827        (AnalyticSurface::Sphere(s1), AnalyticSurface::Sphere(s2)) => {
1828            algebraic_sphere_sphere(s1, s2).map(Some)
1829        }
1830        (AnalyticSurface::Cylinder(c1), AnalyticSurface::Cylinder(c2)) => {
1831            let axis_dot = c1.axis().dot(c2.axis()).abs();
1832            if axis_dot > 1.0 - 1e-10 {
1833                // Axes are parallel — check if they're the same line.
1834                let delta = c2.origin() - c1.origin();
1835                let delta_vec = Vec3::new(delta.x(), delta.y(), delta.z());
1836                let along = delta_vec.dot(c1.axis());
1837                let perp = (delta_vec - c1.axis() * along).length();
1838                if perp < 1e-8 {
1839                    // Coaxial: same axis, different radii → no intersection
1840                    // (unless equal radius → degenerate overlap, skip)
1841                    if (c1.radius() - c2.radius()).abs() < 1e-8 {
1842                        return Ok(None); // Overlapping — let marcher handle
1843                    }
1844                    return Ok(Some(vec![])); // Coaxial, different radii
1845                }
1846            }
1847            // Non-coaxial: algebraic quadratic in v.
1848            algebraic_cylinder_cylinder(c1, c2)
1849        }
1850        // Sphere-cylinder (both orderings).
1851        (AnalyticSurface::Sphere(s), AnalyticSurface::Cylinder(c)) => {
1852            algebraic_sphere_cylinder(s, c, true)
1853        }
1854        (AnalyticSurface::Cylinder(c), AnalyticSurface::Sphere(s)) => {
1855            algebraic_sphere_cylinder(s, c, false)
1856        }
1857        (AnalyticSurface::Cone(c1), AnalyticSurface::Cone(c2)) => algebraic_cone_cone(c1, c2),
1858        (AnalyticSurface::Cone(cone), AnalyticSurface::Sphere(sphere)) => {
1859            Ok(ruling_cone_sphere(cone, sphere, true))
1860        }
1861        (AnalyticSurface::Sphere(sphere), AnalyticSurface::Cone(cone)) => {
1862            Ok(ruling_cone_sphere(cone, sphere, false))
1863        }
1864        (AnalyticSurface::Torus(t), AnalyticSurface::Cylinder(c)) => {
1865            Ok(parallel_axis_torus_cylinder(t, c, true)
1866                .or_else(|| ruling_torus_cylinder(t, c, true)))
1867        }
1868        (AnalyticSurface::Cylinder(c), AnalyticSurface::Torus(t)) => {
1869            Ok(parallel_axis_torus_cylinder(t, c, false)
1870                .or_else(|| ruling_torus_cylinder(t, c, false)))
1871        }
1872        _ => Ok(None),
1873    }
1874}
1875
1876/// A torus and a cylinder whose axes are parallel but distinct (a drill
1877/// through a ring parallel to its axis), traced along the cylinder's
1878/// rulings. A ruling stays at one distance `ρ` from the torus axis, so it
1879/// meets the tube where `(ρ − R)² + z² = r²`: a quadratic in its axial
1880/// parameter. `None` for any other pair (tilted or coaxial axes) and when
1881/// the sampling misses a window narrower than itself.
1882fn parallel_axis_torus_cylinder(
1883    torus: &ToroidalSurface,
1884    cyl: &CylindricalSurface,
1885    torus_first: bool,
1886) -> Option<Vec<IntersectionCurve>> {
1887    let axis = torus.z_axis();
1888    let along = cyl.axis().dot(axis);
1889    if along.abs() < 1.0 - 1e-10 {
1890        return None;
1891    }
1892    let offset = cyl.origin() - torus.center();
1893    if (offset - axis * offset.dot(axis)).length() < Tolerance::new().linear {
1894        return None;
1895    }
1896    let (major, minor) = (torus.major_radius(), torus.minor_radius());
1897    let roots = |u: f64| {
1898        let q = cyl.evaluate(u, 0.0) - torus.center();
1899        let height = q.dot(axis);
1900        let rho = (q - axis * height).length();
1901        let reach = minor * minor - (rho - major) * (rho - major);
1902        ruling_quadratic(1.0, 2.0 * along.signum() * height, height * height - reach)
1903    };
1904    let samples = ruling_samples(cyl, &roots);
1905    let loops = if samples.iter().all(Option::is_some) {
1906        closed_ruling_loops(cyl, &roots, &samples)
1907    } else {
1908        partial_ruling_loops(cyl, &roots, &samples)
1909    };
1910    if loops.is_empty() {
1911        return None;
1912    }
1913    Some(fit_ruling_loops(&loops, |p| {
1914        in_order(torus.project_point(p), cyl.project_point(p), torus_first)
1915    }))
1916}
1917
1918/// Where two circles in a half-plane through an axis cross, as `(rho, z)`
1919/// pairs (distance from the axis, height along it): each sweeps a circle
1920/// about the axis. `None` (defer to the marcher) when the circles coincide
1921/// or touch, or a crossing lands on or past the axis; `Some` of none when
1922/// they miss.
1923fn meridian_crossings(
1924    first: (f64, f64, f64),
1925    second: (f64, f64, f64),
1926    scale: f64,
1927) -> Option<Vec<(f64, f64)>> {
1928    let ((x1, z1, r1), (x2, z2, r2)) = (first, second);
1929    let (dx, dz) = (x2 - x1, z2 - z1);
1930    let dist = dx.hypot(dz);
1931    let slack = 1e-9 * scale;
1932    if dist < slack || (dist - (r1 + r2)).abs() < slack || (dist - (r1 - r2).abs()).abs() < slack {
1933        return None;
1934    }
1935    if dist > r1 + r2 || dist < (r1 - r2).abs() {
1936        return Some(Vec::new());
1937    }
1938    let along = r2.mul_add(-r2, r1.mul_add(r1, dist * dist)) / (2.0 * dist);
1939    let across = r1.mul_add(r1, -(along * along)).max(0.0).sqrt();
1940    let (ux, uz) = (dx / dist, dz / dist);
1941    let mut crossings = Vec::with_capacity(2);
1942    for side in [1.0, -1.0] {
1943        let rho = x1 + along * ux - side * across * uz;
1944        if rho <= slack {
1945            return None;
1946        }
1947        crossings.push((rho, z1 + along * uz + side * across * ux));
1948    }
1949    Some(crossings)
1950}
1951
1952/// Circles about an axis through `base`, at the given `(rho, z)` crossings.
1953fn circles_about_axis(
1954    base: Point3,
1955    axis: Vec3,
1956    crossings: &[(f64, f64)],
1957) -> Result<Vec<ExactIntersectionCurve>, MathError> {
1958    crossings
1959        .iter()
1960        .map(|&(rho, z)| {
1961            Circle3D::new(base + axis * z, axis, rho).map(ExactIntersectionCurve::Circle)
1962        })
1963        .collect()
1964}
1965
1966/// Exact intersection of two tori sharing an axis: their tube cross-sections
1967/// in a half-plane through the axis cross in up to two points, and each sweeps
1968/// a circle about the axis.
1969///
1970/// `None` (defer to the marcher) unless the axes lie on one line, or when the
1971/// cross-sections coincide or touch.
1972///
1973/// # Errors
1974///
1975/// Returns an error if a section circle cannot be built.
1976pub fn exact_torus_torus(
1977    first: &ToroidalSurface,
1978    second: &ToroidalSurface,
1979) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
1980    let axis = first.z_axis();
1981    let scale = first.major_radius() + second.major_radius();
1982    let offset = second.center() - first.center();
1983    // A spindle torus's tube also crosses the far side of the axis.
1984    if first.minor_radius() >= first.major_radius()
1985        || second.minor_radius() >= second.major_radius()
1986        || axis.cross(second.z_axis()).length() > 1e-9
1987        || offset.cross(axis).length() > 1e-9 * scale
1988    {
1989        return Ok(None);
1990    }
1991    let Some(crossings) = meridian_crossings(
1992        (first.major_radius(), 0.0, first.minor_radius()),
1993        (
1994            second.major_radius(),
1995            offset.dot(axis),
1996            second.minor_radius(),
1997        ),
1998        scale,
1999    ) else {
2000        return Ok(None);
2001    };
2002    circles_about_axis(first.center(), axis, &crossings).map(Some)
2003}
2004
2005/// Exact intersection of a torus with a cylinder sharing its axis.
2006///
2007/// The wall line and the tube's cross-section in a half-plane through the
2008/// axis cross in up to two points, each sweeping a circle about the axis. A
2009/// wall touching the tube's outer equator, ring or spindle, or a ring torus's
2010/// inner equator meets it along the one circle at the torus's centre.
2011///
2012/// `None` (defer to the marcher) unless the axes lie on one line, or when
2013/// the wall reaches the part of a spindle torus's tube across the axis.
2014///
2015/// # Errors
2016///
2017/// Returns an error if a section circle cannot be built.
2018pub fn exact_cylinder_torus(
2019    cylinder: &CylindricalSurface,
2020    torus: &ToroidalSurface,
2021) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2022    let axis = torus.z_axis();
2023    let scale = torus.major_radius() + cylinder.radius();
2024    let offset = cylinder.origin() - torus.center();
2025    let linear = crate::tolerance::Tolerance::default().linear;
2026    if axis.cross(cylinder.axis()).length() > 1e-9
2027        || offset.cross(axis).length() > 1e-9 * scale
2028        || cylinder.radius() + torus.major_radius() <= torus.minor_radius() + linear
2029    {
2030        return Ok(None);
2031    }
2032    let gap = cylinder.radius() - torus.major_radius();
2033    let small = torus.minor_radius();
2034    // The equator circle lies on the torus within the wall's distance from
2035    // the tube, so it stands for the section while that distance is within
2036    // the linear tolerance, whatever the scale.
2037    if (gap.abs() - small).abs() <= linear {
2038        return circles_about_axis(torus.center(), axis, &[(cylinder.radius(), 0.0)]).map(Some);
2039    }
2040    if gap.abs() > small {
2041        return Ok(Some(Vec::new()));
2042    }
2043    let height = small.mul_add(small, -(gap * gap)).sqrt();
2044    circles_about_axis(
2045        torus.center(),
2046        axis,
2047        &[(cylinder.radius(), height), (cylinder.radius(), -height)],
2048    )
2049    .map(Some)
2050}
2051
2052/// Exact intersection of a torus with a sphere centred on its axis.
2053///
2054/// The sphere's great circle and the tube's cross-section in a half-plane
2055/// through the axis cross in up to two points, and each sweeps a circle
2056/// about the axis.
2057///
2058/// `None` (defer to the marcher) unless the sphere's centre lies on the axis,
2059/// or when the two circles touch.
2060///
2061/// # Errors
2062///
2063/// Returns an error if a section circle cannot be built.
2064pub fn exact_sphere_torus(
2065    sphere: &SphericalSurface,
2066    torus: &ToroidalSurface,
2067) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2068    let axis = torus.z_axis();
2069    let scale = torus.major_radius() + sphere.radius();
2070    let offset = sphere.center() - torus.center();
2071    // A spindle torus's tube also crosses the far side of the axis.
2072    if torus.minor_radius() >= torus.major_radius() || offset.cross(axis).length() > 1e-9 * scale {
2073        return Ok(None);
2074    }
2075    let Some(crossings) = meridian_crossings(
2076        (0.0, offset.dot(axis), sphere.radius()),
2077        (torus.major_radius(), 0.0, torus.minor_radius()),
2078        scale,
2079    ) else {
2080        return Ok(None);
2081    };
2082    circles_about_axis(torus.center(), axis, &crossings).map(Some)
2083}
2084
2085/// Exact coaxial cone-cone intersection: returns the shared circle.
2086///
2087/// Two cones that share an axis are concentric circles at every axial
2088/// station, so they meet only where their radii are equal. Each cone's
2089/// radius is linear in the axial coordinate `t` (measured along the shared
2090/// axis from cone 1's apex): `r1 = m1·t` and `r2 = m2·σ·(t − d2)`, where
2091/// `m_i = cot(half_angle_i)`, `σ = sign(axis2·axis1)`, and `d2` is cone 2's
2092/// apex position in that coordinate. Equating gives a single crossing `t*`
2093/// → one circle (the shared rim). The general marcher mishandles this case:
2094/// at the radii-crossing the surfaces are nearly tangent, so a grid-seeded
2095/// march fragments the clean circle into dozens of degenerate micro-curves.
2096///
2097/// Returns `Some(vec![circle])` for a genuine crossing, `Some(vec![])` when
2098/// the cones do not meet (parallel radius lines or a crossing on the wrong
2099/// nappe), and `None` for the identical-cone overlap or a degenerate
2100/// (near-flat) cone — both of which fall through to the general path.
2101/// Parallel-but-offset axes with equal half-angle tangents reduce to a
2102/// radical-plane conic (`offset_parallel_cone_cone`); other offset
2103/// configurations defer to the marcher with `None`.
2104///
2105/// # Errors
2106///
2107/// Returns [`MathError`] if the shared-rim `Circle3D` cannot be constructed
2108/// (e.g. a non-finite center or radius from a malformed cone).
2109pub fn exact_cone_cone(
2110    c1: &ConicalSurface,
2111    c2: &ConicalSurface,
2112) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2113    let axis = c1.axis();
2114    let axis2 = c2.axis();
2115
2116    // Coaxial check: parallel axes and the second apex lies on the first axis.
2117    if axis.dot(axis2).abs() < 1.0 - 1e-10 {
2118        return Ok(None); // Non-coaxial: quartic curve, let the marcher handle.
2119    }
2120    let apex1 = c1.apex();
2121    let apex2 = c2.apex();
2122    let delta = apex2 - apex1;
2123    let delta_v = Vec3::new(delta.x(), delta.y(), delta.z());
2124    let along = delta_v.dot(axis);
2125    if (delta_v - axis * along).length() > 1e-8 {
2126        return offset_parallel_cone_cone(c1, c2);
2127    }
2128
2129    let (s1, s2) = (c1.half_angle().sin(), c2.half_angle().sin());
2130    if s1.abs() < 1e-12 || s2.abs() < 1e-12 {
2131        return Ok(None); // Degenerate (near-flat) cone.
2132    }
2133    let m1 = c1.half_angle().cos() / s1;
2134    let m2 = c2.half_angle().cos() / s2;
2135    let sigma = if axis.dot(axis2) >= 0.0 { 1.0 } else { -1.0 };
2136    let d2 = along; // apex2 position along `axis`, measured from apex1.
2137
2138    let denom = m1 - m2 * sigma;
2139    if denom.abs() < 1e-12 {
2140        // Parallel radius lines: identical cones (coincident apex, same opening)
2141        // overlap — defer to the general/same-domain path; otherwise no meeting.
2142        if sigma > 0.0 && d2.abs() < 1e-9 {
2143            return Ok(None);
2144        }
2145        return Ok(Some(vec![]));
2146    }
2147
2148    let t_star = (-m2 * sigma * d2) / denom;
2149    let radius = m1 * t_star;
2150    if radius < 1e-12 {
2151        return Ok(Some(vec![])); // Crossing on the wrong nappe / no real circle.
2152    }
2153
2154    let center = Point3::new(
2155        apex1.x() + axis.x() * t_star,
2156        apex1.y() + axis.y() * t_star,
2157        apex1.z() + axis.z() * t_star,
2158    );
2159    let circle = Circle3D::new(center, axis, radius)?;
2160    Ok(Some(vec![ExactIntersectionCurve::Circle(circle)]))
2161}
2162
2163/// Parallel-axis (or anti-parallel), offset-apex cones with equal half-angle
2164/// tangents: subtracting the two quadric equations cancels both the radial
2165/// and the axial quadratic terms (their coefficients depend only on
2166/// `tan²(half_angle)`), so every intersection point lies on a plane — the
2167/// degenerate member of the quadric pencil — and plane ∩ cone is an exact
2168/// conic. The gridfinity spacer lip fuse hits this exactly: opposed 45°
2169/// corner cones offset 0.25mm, which the marcher shreds into ~64 closed
2170/// micro-loops per pair (#1570). Unequal angles keep a genuine quadratic
2171/// term, and an unbounded section (hyperbola/parabola) has no closed-form
2172/// win over the marcher — both defer with `None`.
2173fn offset_parallel_cone_cone(
2174    c1: &ConicalSurface,
2175    c2: &ConicalSurface,
2176) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2177    if c1.half_angle().sin().abs() < 1e-12 || c2.half_angle().sin().abs() < 1e-12 {
2178        return Ok(None); // Degenerate (near-flat) cone, as in the coaxial path.
2179    }
2180    let t1 = c1.half_angle().tan();
2181    let t2 = c2.half_angle().tan();
2182    if !t1.is_finite() || !t2.is_finite() {
2183        return Ok(None);
2184    }
2185    if (t1 - t2).abs() > 1e-9 * (1.0 + t1.abs().max(t2.abs())) {
2186        return Ok(None);
2187    }
2188
2189    let w = c1.axis();
2190    let apex1 = c1.apex();
2191    let apex2 = c2.apex();
2192    let delta = apex2 - apex1;
2193    let delta_v = Vec3::new(delta.x(), delta.y(), delta.z());
2194    let s = delta_v.dot(w);
2195    let tm = 0.5 * (t1 + t2);
2196    let k = 1.0 + tm * tm;
2197
2198    // In the apex1 frame each cone is |P|² − k(P·w)² = 0 (shifted by δ for
2199    // cone 2; the axis SIGN drops out since only (P·w)² appears). Their
2200    // difference: P·(2δ − 2ksw) = |δ|² − ks².
2201    let n = (delta_v - w * (k * s)) * 2.0;
2202    let n_len = n.length();
2203    if n_len < 1e-12 {
2204        return Ok(None);
2205    }
2206    let n_hat = n * (1.0 / n_len);
2207    let d = (dot_np(n, apex1) + delta_v.dot(delta_v) - k * s * s) / n_len;
2208
2209    // `exact_plane_cone` already rejects sections on cone 1's phantom nappe;
2210    // cone 2's nappe must be checked here. A conic on the shared quadric
2211    // pencil cannot cross between nappes except exactly through apex 2, so
2212    // sampled quarter-points either all pass or all fail; a mixed verdict
2213    // means an apex-touching degeneracy — defer to the marcher.
2214    let axis2 = c2.axis();
2215    let scale = 1.0 + delta_v.length();
2216    let mut out = Vec::new();
2217    for curve in exact_plane_cone(c1, n_hat, d, 0.0)? {
2218        let samples: Vec<Point3> = match &curve {
2219            ExactIntersectionCurve::Circle(c) => (0..4)
2220                .map(|i| crate::traits::ParametricCurve::evaluate(c, TAU * f64::from(i) / 4.0))
2221                .collect(),
2222            ExactIntersectionCurve::Ellipse(e) => (0..4)
2223                .map(|i| crate::traits::ParametricCurve::evaluate(e, TAU * f64::from(i) / 4.0))
2224                .collect(),
2225            ExactIntersectionCurve::Points(_) => return Ok(None),
2226        };
2227        let on_real_nappe = |p: &Point3| {
2228            let rel = *p - apex2;
2229            Vec3::new(rel.x(), rel.y(), rel.z()).dot(axis2) >= -1e-9 * scale
2230        };
2231        let hits = samples.iter().filter(|p| on_real_nappe(p)).count();
2232        match hits {
2233            0 => {}
2234            4 => out.push(curve),
2235            _ => return Ok(None),
2236        }
2237    }
2238    Ok(Some(out))
2239}
2240
2241/// Exact coaxial cone-cylinder intersection: returns the shared circle.
2242///
2243/// A cone and a cylinder sharing an axis are concentric circles at every
2244/// axial station, so they meet only where the cone's radius equals the
2245/// cylinder's. The cone radius is linear in the axial coordinate `t` from its
2246/// apex (`r = m·t`, `m = cot(half_angle)`), the cylinder radius is the
2247/// constant `R`, so `m·t = R` gives a single crossing `t*` → one circle. This
2248/// is the gridfinity lip's top knife edge (inner tapered corner = cone, outer
2249/// corner = cylinder, concentric, radii matching at `Z_PEAK`); the general
2250/// marcher fragments that near-tangent contact into dozens of degenerate
2251/// micro-curves.
2252///
2253/// Returns `Some(vec![circle])` for a genuine crossing, `Some(vec![])` when
2254/// the crossing degenerates to the apex, and `None` (defer to the marcher)
2255/// when the surfaces are not coaxial or the cone is near-flat / near-axial.
2256///
2257/// # Errors
2258///
2259/// Returns [`MathError`] if the shared `Circle3D` cannot be constructed.
2260pub fn exact_cone_cylinder(
2261    cone: &ConicalSurface,
2262    cyl: &CylindricalSurface,
2263) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2264    let axis = cone.axis();
2265    let cyl_axis = cyl.axis();
2266
2267    // Coaxial check: parallel axes and the cone apex on the cylinder's axis.
2268    if axis.dot(cyl_axis).abs() < 1.0 - 1e-10 {
2269        return Ok(None);
2270    }
2271    let apex = cone.apex();
2272    let delta = apex - cyl.origin();
2273    let delta_v = Vec3::new(delta.x(), delta.y(), delta.z());
2274    let along = delta_v.dot(cyl_axis);
2275    if (delta_v - cyl_axis * along).length() > 1e-8 {
2276        return Ok(None);
2277    }
2278
2279    let s = cone.half_angle().sin();
2280    if s.abs() < 1e-12 {
2281        return Ok(None); // near-flat cone.
2282    }
2283    let m = cone.half_angle().cos() / s; // dr/dt along the cone axis.
2284    if m.abs() < 1e-12 {
2285        return Ok(None); // near-axial cone: radius ~constant.
2286    }
2287
2288    let t_star = cyl.radius() / m; // where the cone radius m·t equals R.
2289    if t_star.abs() < 1e-12 {
2290        return Ok(Some(vec![])); // crossing at the apex — no real circle.
2291    }
2292    let center = Point3::new(
2293        apex.x() + axis.x() * t_star,
2294        apex.y() + axis.y() * t_star,
2295        apex.z() + axis.z() * t_star,
2296    );
2297    let circle = Circle3D::new(center, axis, cyl.radius())?;
2298    Ok(Some(vec![ExactIntersectionCurve::Circle(circle)]))
2299}
2300
2301/// Algebraic cone-cone intersection (NURBS form for the general bounded
2302/// path). Delegates to [`exact_cone_cone`] and samples each exact conic
2303/// (coaxial circle or offset-parallel radical-plane ellipse) into an
2304/// interpolated NURBS `IntersectionCurve`, mirroring the
2305/// sphere-cylinder algebraic path. phase FF prefers the exact circle form
2306/// directly (so the section edge links to the coincident boundary), but a
2307/// caller of `intersect_analytic_analytic_bounded` still gets one clean
2308/// curve instead of the marcher's fragments.
2309fn algebraic_cone_cone(
2310    c1: &ConicalSurface,
2311    c2: &ConicalSurface,
2312) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
2313    let Some(exacts) = exact_cone_cone(c1, c2)? else {
2314        return Ok(None);
2315    };
2316    let mut curves = Vec::new();
2317    for exact in exacts {
2318        let n_samples = 33;
2319        let mut positions = Vec::with_capacity(n_samples);
2320        let mut points = Vec::with_capacity(n_samples);
2321        #[allow(clippy::cast_precision_loss)]
2322        for i in 0..n_samples {
2323            let theta = TAU * i as f64 / (n_samples - 1) as f64;
2324            let pt = match &exact {
2325                ExactIntersectionCurve::Circle(circle) => {
2326                    crate::traits::ParametricCurve::evaluate(circle, theta)
2327                }
2328                ExactIntersectionCurve::Ellipse(ellipse) => {
2329                    crate::traits::ParametricCurve::evaluate(ellipse, theta)
2330                }
2331                ExactIntersectionCurve::Points(_) => break,
2332            };
2333            positions.push(pt);
2334            points.push(IntersectionPoint {
2335                point: pt,
2336                param1: (0.0, 0.0),
2337                param2: (0.0, 0.0),
2338            });
2339        }
2340        if positions.is_empty() {
2341            continue;
2342        }
2343        let degree = 3.min(positions.len() - 1);
2344        let curve = interpolate(&positions, degree)?;
2345        curves.push(IntersectionCurve { curve, points });
2346    }
2347    Ok(Some(curves))
2348}
2349
2350/// Exact coaxial sphere-cylinder intersection: returns the shared circle(s).
2351///
2352/// A sphere of radius `R` centered at `C` and a cylinder of radius `r` whose
2353/// axis passes through `C` meet in concentric circles of radius `r` at the
2354/// axial stations where `sqrt(R² − z²) = r`, i.e. `z = ±sqrt(R² − r²)`
2355/// measured from `C` along the axis. A proper crossing yields two circles; a
2356/// tangent contact (`r = R`) yields one; a cylinder wider than the sphere, or
2357/// a non-coaxial configuration (quartic curve), yields none/defers.
2358///
2359/// Mirrors [`exact_cone_cylinder`] so phase FF can emit the section as an
2360/// exact `Circle3D` (which the closed-circle split + seam adoption recognise)
2361/// rather than the marcher's NURBS fragments.
2362///
2363/// Returns `Some(vec![..])` (0, 1, or 2 circles) for the coaxial case, and
2364/// `None` (defer to the general marcher) when the axes are not coaxial.
2365///
2366/// # Errors
2367///
2368/// Returns [`MathError`] if a shared `Circle3D` cannot be constructed.
2369pub fn exact_sphere_cylinder(
2370    sphere: &SphericalSurface,
2371    cyl: &CylindricalSurface,
2372) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2373    let sc = sphere.center();
2374    let r_sphere = sphere.radius();
2375    let co = cyl.origin();
2376    let axis = cyl.axis();
2377    let r_cyl = cyl.radius();
2378
2379    // Project sphere center onto the cylinder axis.
2380    let delta = sc - co;
2381    let delta_vec = Vec3::new(delta.x(), delta.y(), delta.z());
2382    let along = delta_vec.dot(axis);
2383    let perp_vec = delta_vec - axis * along;
2384    let d_perp = perp_vec.length();
2385
2386    // Non-coaxial sphere-cylinder intersections produce quartic curves;
2387    // defer those to the general marcher.
2388    if d_perp > 1e-7 {
2389        return Ok(None);
2390    }
2391
2392    // Coaxial: the sphere center lies on the cylinder axis. No real circle
2393    // when the cylinder is wider than the sphere or they are tangent-internal.
2394    if r_cyl > r_sphere + 1e-10 {
2395        return Ok(Some(vec![]));
2396    }
2397    let z_sq = r_sphere * r_sphere - r_cyl * r_cyl;
2398    if z_sq < 0.0 {
2399        return Ok(Some(vec![]));
2400    }
2401    let z = z_sq.sqrt();
2402
2403    // The sphere center projected onto the axis is the midpoint of the two
2404    // section circles, each offset by ±z along the axis with radius `r_cyl`.
2405    let center_axis_pt = Point3::new(
2406        co.x() + axis.x() * along,
2407        co.y() + axis.y() * along,
2408        co.z() + axis.z() * along,
2409    );
2410
2411    let mut circles = Vec::new();
2412    let offsets: &[f64] = if z < 1e-10 { &[0.0] } else { &[z, -z] };
2413    for &z_offset in offsets {
2414        let center = Point3::new(
2415            center_axis_pt.x() + axis.x() * z_offset,
2416            center_axis_pt.y() + axis.y() * z_offset,
2417            center_axis_pt.z() + axis.z() * z_offset,
2418        );
2419        let circle = Circle3D::new(center, axis, r_cyl)?;
2420        circles.push(ExactIntersectionCurve::Circle(circle));
2421    }
2422    Ok(Some(circles))
2423}
2424
2425/// Exact coaxial cone-sphere intersection: the shared circles.
2426///
2427/// When the cone's axis runs through the sphere's centre `C`, every generator
2428/// `apex + v g` (`g` a unit direction, `v` the distance from the apex) has
2429/// the same `h = g·(apex − C)`, so all of them meet the sphere at the same
2430/// roots of `v² + 2hv + |apex − C|² − R² = 0`. Each root ahead of the apex is
2431/// a circle of radius `v cos a` at `v sin a` along the axis: two for a ball
2432/// the cone passes through, one for a ball that swallows the apex, passes
2433/// through it or touches the wall, none for a ball the cone misses or that
2434/// sits inside it clear of the wall.
2435///
2436/// Returns `Some(vec![..])` for a coaxial pair and `None` (the ruling trace
2437/// or the marcher) otherwise.
2438///
2439/// # Errors
2440///
2441/// Returns [`MathError`] if a shared `Circle3D` cannot be constructed.
2442pub fn exact_cone_sphere(
2443    cone: &ConicalSurface,
2444    sphere: &SphericalSurface,
2445) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2446    let offset = cone.apex() - sphere.center();
2447    let along = offset.dot(cone.axis());
2448    if (offset - cone.axis() * along).length() > 1e-7 {
2449        return Ok(None);
2450    }
2451    let lin_tol = Tolerance::new().linear;
2452    let (sin_a, cos_a) = cone.half_angle().sin_cos();
2453    let (far_sq, radius_sq) = (offset.dot(offset), sphere.radius() * sphere.radius());
2454    let b = 2.0 * sin_a * along;
2455    let (disc, far, near) = ruling_quadratic(1.0, b, far_sq - radius_sq);
2456    // At a tangent ball `b² − 4c` cancels to a few ulps of its terms, which
2457    // can land below zero: that is a touch, not a miss.
2458    let noise = 16.0 * f64::EPSILON * 4.0f64.mul_add(far_sq + radius_sq, b * b);
2459    if disc < -noise {
2460        return Ok(Some(vec![]));
2461    }
2462    let roots: &[f64] = if far - near < lin_tol {
2463        &[far]
2464    } else {
2465        &[near, far]
2466    };
2467    let mut circles = Vec::new();
2468    for &v in roots {
2469        if v * cos_a > lin_tol {
2470            let centre = cone.apex() + cone.axis() * (v * sin_a);
2471            let circle = Circle3D::new(centre, cone.axis(), v * cos_a)?;
2472            circles.push(ExactIntersectionCurve::Circle(circle));
2473        }
2474    }
2475    Ok(Some(circles))
2476}
2477
2478/// Algebraic sphere-cylinder intersection (NURBS form for the general bounded
2479/// path). A coaxial pair delegates to [`exact_sphere_cylinder`] and samples
2480/// each exact circle into an interpolated NURBS `IntersectionCurve`. phase FF
2481/// prefers the exact circle form directly (so the section edge links to the
2482/// coincident boundary and the closed-circle splitter can carve the spherical
2483/// band), but a caller of `intersect_analytic_analytic_bounded` still gets
2484/// clean curves instead of the marcher's fragments. Any other pair is traced
2485/// along the cylinder's rulings ([`off_axis_sphere_cylinder`]).
2486fn algebraic_sphere_cylinder(
2487    sphere: &SphericalSurface,
2488    cyl: &CylindricalSurface,
2489    sphere_first: bool,
2490) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
2491    let Some(exacts) = exact_sphere_cylinder(sphere, cyl)? else {
2492        return Ok(off_axis_sphere_cylinder(sphere, cyl, sphere_first));
2493    };
2494
2495    let mut curves = Vec::new();
2496    for exact in exacts {
2497        let ExactIntersectionCurve::Circle(circle) = exact else {
2498            continue;
2499        };
2500        let n_samples = 33;
2501        let mut points = Vec::with_capacity(n_samples);
2502        let mut positions = Vec::with_capacity(n_samples);
2503        #[allow(clippy::cast_precision_loss)]
2504        for i in 0..n_samples {
2505            let theta = TAU * i as f64 / (n_samples - 1) as f64;
2506            let pt = crate::traits::ParametricCurve::evaluate(&circle, theta);
2507            positions.push(pt);
2508            let (param1, param2) = in_order(
2509                sphere.project_point(pt),
2510                cyl.project_point(pt),
2511                sphere_first,
2512            );
2513            points.push(IntersectionPoint {
2514                point: pt,
2515                param1,
2516                param2,
2517            });
2518        }
2519        let degree = 3.min(positions.len() - 1);
2520        let curve = interpolate(&positions, degree)?;
2521        curves.push(IntersectionCurve { curve, points });
2522    }
2523
2524    Ok(Some(curves))
2525}
2526
2527/// A sphere and a cylinder whose axis misses the sphere's centre (a drill
2528/// entering a ball off its axis), traced along the cylinder's rulings: the
2529/// ruling `c(u) + v·a` meets the sphere where `v² + 2(q·a)·v + |q|² − R² = 0`,
2530/// with `q = c(u) − C`. When every ruling meets the sphere (the cylinder
2531/// passes wholly through it) the roots trace an entry and an exit loop;
2532/// otherwise each window of meeting rulings carries one loop. `Some(empty)`
2533/// when the two cannot meet, `None` when the sampling misses a window
2534/// narrower than itself.
2535fn off_axis_sphere_cylinder(
2536    sphere: &SphericalSurface,
2537    cyl: &CylindricalSurface,
2538    sphere_first: bool,
2539) -> Option<Vec<IntersectionCurve>> {
2540    let (centre, radius) = (sphere.center(), sphere.radius());
2541    let axis = cyl.axis();
2542    let offset = centre - cyl.origin();
2543    let axis_distance = (offset - axis * offset.dot(axis)).length();
2544    let lin_tol = Tolerance::new().linear;
2545    if axis_distance > radius + cyl.radius() + lin_tol
2546        || axis_distance + radius < cyl.radius() - lin_tol
2547    {
2548        return Some(Vec::new());
2549    }
2550    let roots = |u: f64| {
2551        let q = cyl.evaluate(u, 0.0) - centre;
2552        ruling_quadratic(1.0, 2.0 * q.dot(axis), q.dot(q) - radius * radius)
2553    };
2554    let samples = ruling_samples(cyl, &roots);
2555    let loops = if samples.iter().all(Option::is_some) {
2556        closed_ruling_loops(cyl, &roots, &samples)
2557    } else {
2558        partial_ruling_loops(cyl, &roots, &samples)
2559    };
2560    if loops.is_empty() {
2561        return None;
2562    }
2563    Some(fit_ruling_loops(&loops, |p| {
2564        in_order(sphere.project_point(p), cyl.project_point(p), sphere_first)
2565    }))
2566}
2567
2568/// Parameters on the pair's first and second surfaces, from those on `a`
2569/// and `b` and whether `a` came first.
2570const fn in_order(a: (f64, f64), b: (f64, f64), a_first: bool) -> ((f64, f64), (f64, f64)) {
2571    if a_first { (a, b) } else { (b, a) }
2572}
2573
2574/// Algebraic cylinder-cylinder intersection for non-coaxial cylinders.
2575///
2576/// For two cylinders with axes that are NOT parallel, the intersection
2577/// consists of up to two closed space curves. These are found by
2578/// parameterizing one cylinder's angular coordinate `u ∈ [0, 2π]` and
2579/// solving a quadratic in the axial parameter `v` to find where each
2580/// "ring" of cylinder A sits on cylinder B.
2581///
2582/// The quadratic is:
2583///   `v²·(1 - α²) + 2v·(q·a₁ - α·q·a₂) + (|q|² - (q·a₂)² - r₂²) = 0`
2584/// where `α = a₁·a₂`, `q(u)` is the radial point on cylinder 1 minus
2585/// cylinder 2's origin, `a₁`/`a₂` are the cylinder axes, and `r₂` is
2586/// cylinder 2's radius.
2587#[allow(clippy::too_many_lines, clippy::unnecessary_wraps)]
2588fn algebraic_cylinder_cylinder(
2589    c1: &CylindricalSurface,
2590    c2: &CylindricalSurface,
2591) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
2592    let alpha = c1.axis().dot(c2.axis());
2593    let a_coeff = 1.0 - alpha * alpha;
2594
2595    // Should only be called for non-parallel axes.
2596    if a_coeff.abs() < 1e-12 {
2597        return Ok(None);
2598    }
2599
2600    let r1 = c1.radius();
2601    let r2 = c2.radius();
2602    let o1 = c1.origin();
2603    let o2 = c2.origin();
2604    let a1 = c1.axis();
2605    let a2 = c2.axis();
2606
2607    // Separation check: distance between axes vs sum of radii.
2608    // Closest approach of two skew lines:
2609    let delta = Vec3::new(o1.x() - o2.x(), o1.y() - o2.y(), o1.z() - o2.z());
2610    let cross = a1.cross(a2);
2611    let cross_len = cross.length();
2612    if cross_len > 1e-12 {
2613        let axis_dist = delta.dot(cross).abs() / cross_len;
2614        if axis_dist > r1 + r2 + Tolerance::new().linear {
2615            return Ok(Some(vec![])); // No intersection
2616        }
2617    }
2618
2619    // Solve every ruling of one cylinder against the other. When EVERY
2620    // ruling of the swept cylinder meets the other (the thinner of two
2621    // crossing tubes), the two roots trace the curve's two closed loops.
2622    // Swept the other way only a window of rulings meets, and each root
2623    // traces an open arc of one loop.
2624    let roots = |sweep: &CylindricalSurface, other: &CylindricalSurface| {
2625        let (o, a, radius) = (other.origin(), other.axis(), other.radius());
2626        let alpha = sweep.axis().dot(a);
2627        let quad = 1.0 - alpha * alpha;
2628        let (axis, sweep) = (sweep.axis(), sweep.clone());
2629        move |u: f64| {
2630            let q = sweep.evaluate(u, 0.0) - o;
2631            let (q_a1, q_a2) = (q.dot(axis), q.dot(a));
2632            let b = 2.0 * (q_a1 - alpha * q_a2);
2633            let c = q.dot(q) - q_a2 * q_a2 - radius * radius;
2634            ruling_quadratic(quad, b, c)
2635        }
2636    };
2637    let (roots1, roots2) = (roots(c1, c2), roots(c2, c1));
2638    let samples1 = ruling_samples(c1, &roots1);
2639    let loops = if samples1.iter().all(Option::is_some) {
2640        closed_ruling_loops(c1, &roots1, &samples1)
2641    } else {
2642        let samples2 = ruling_samples(c2, &roots2);
2643        if samples2.iter().all(Option::is_some) {
2644            closed_ruling_loops(c2, &roots2, &samples2)
2645        } else if samples1.iter().any(Option::is_some) {
2646            partial_ruling_loops(c1, &roots1, &samples1)
2647        } else {
2648            partial_ruling_loops(c2, &roots2, &samples2)
2649        }
2650    };
2651    if loops.is_empty() {
2652        return Ok(None);
2653    }
2654    Ok(Some(fit_ruling_loops(&loops, |p| {
2655        (c1.project_point(p), c2.project_point(p))
2656    })))
2657}
2658
2659/// A cone and a cylinder whose axes are not parallel, traced along the
2660/// cylinder's rulings. A ruling `q + t w` meets the cone's double quadric
2661/// `|p - apex|^2 = h^2 / sin^2(half_angle)`, with `h` the offset along the
2662/// cone's axis, where a quadratic in `t` vanishes. `None` (the marcher's
2663/// case) when a ruling meets the far nappe, where no cone face lies, when
2664/// the rulings run along the cone's generators, or when no ruling meets it.
2665fn ruling_cone_cylinder(
2666    cone: &ConicalSurface,
2667    cyl: &CylindricalSurface,
2668    cone_first: bool,
2669) -> Option<Vec<IntersectionCurve>> {
2670    let (sin_t, cos_t) = cone.half_angle().sin_cos();
2671    if sin_t < 1e-12 || cos_t < 1e-12 {
2672        return None;
2673    }
2674    let (apex, d, w) = (cone.apex(), cone.axis(), cyl.axis());
2675    let s = 1.0 / (sin_t * sin_t);
2676    let alpha = w.dot(d);
2677    let quad = 1.0 - s * alpha * alpha;
2678    if quad.abs() < 1e-9 {
2679        return None;
2680    }
2681    let roots = |u: f64| {
2682        let delta = cyl.evaluate(u, 0.0) - apex;
2683        let (dd, dw) = (delta.dot(d), delta.dot(w));
2684        let b = 2.0 * (dw - s * dd * alpha);
2685        let c = delta.dot(delta) - s * dd * dd;
2686        ruling_quadratic(quad, b, c)
2687    };
2688    let lin_tol = Tolerance::new().linear;
2689    let far_nappe = (0..WINDOW_SCAN * RULING_SAMPLES).any(|k| {
2690        #[allow(clippy::cast_precision_loss)]
2691        let u = TAU * (k as f64 + 0.5) / (WINDOW_SCAN * RULING_SAMPLES) as f64;
2692        let (disc, vp, vm) = roots(u);
2693        disc >= -lin_tol
2694            && [vp, vm]
2695                .iter()
2696                .any(|&t| (cyl.evaluate(u, t) - apex).dot(d) < -lin_tol)
2697    });
2698    if far_nappe {
2699        return None;
2700    }
2701    let samples = ruling_samples(cyl, &roots);
2702    // A window of meeting rulings narrower than the sampling would vanish
2703    // (a cone's tip just through the wall) while the others still made
2704    // loops; a finer scan finds every window, and any that the sampling
2705    // covers thinly goes to the marcher.
2706    let scan = WINDOW_SCAN * RULING_SAMPLES;
2707    // Ruling sample `i` lies midway between scan points
2708    // `WINDOW_SCAN i + 7` and `WINDOW_SCAN i + 8`.
2709    #[allow(clippy::cast_precision_loss)]
2710    let meets = |k: usize| roots(TAU * ((k % scan) as f64 + 0.5) / scan as f64).0 >= -lin_tol;
2711    if let Some(start) = (0..scan).find(|&k| !meets(k)) {
2712        let mut k = start;
2713        while k < start + scan {
2714            if !meets(k) {
2715                k += 1;
2716                continue;
2717            }
2718            let first = k;
2719            while k < start + scan && meets(k) {
2720                k += 1;
2721            }
2722            let covered = (first..k)
2723                .filter(|&j| j % WINDOW_SCAN == WINDOW_SCAN / 2 - 1 && meets(j + 1))
2724                .count();
2725            if covered < WINDOW_MIN_SAMPLES {
2726                return None;
2727            }
2728        }
2729    }
2730    let loops = if samples.iter().all(Option::is_some) {
2731        closed_ruling_loops(cyl, &roots, &samples)
2732    } else {
2733        partial_ruling_loops(cyl, &roots, &samples)
2734    };
2735    if loops.is_empty() {
2736        return None;
2737    }
2738    Some(fit_ruling_loops(&loops, |p| {
2739        in_order(cone.project_point(p), cyl.project_point(p), cone_first)
2740    }))
2741}
2742
2743/// A torus and a cylinder whose axes are not parallel, where every ruling
2744/// of the cylinder meets the torus the same nonzero even number of times (a
2745/// rod through the ring's tube): a ruling meets the torus where a quartic in
2746/// its parameter vanishes, and each of its roots, taken in order, sweeps one
2747/// closed loop around the cylinder. `None` (the marcher's case) when the
2748/// count varies between rulings, where the curve turns back between them,
2749/// checked on a scan finer than the sampling so a narrow window of missing
2750/// rulings is not stepped over, and for a spindle torus, whose quartic also
2751/// holds its inner lemon.
2752fn ruling_torus_cylinder(
2753    torus: &ToroidalSurface,
2754    cyl: &CylindricalSurface,
2755    torus_first: bool,
2756) -> Option<Vec<IntersectionCurve>> {
2757    if cyl.axis().dot(torus.z_axis()).abs() > 1.0 - 1e-9
2758        || torus.minor_radius() >= torus.major_radius()
2759    {
2760        return None;
2761    }
2762    let roots = |u: f64| intersect_line_torus(torus, cyl.evaluate(u, 0.0), cyl.axis());
2763    let rows: Vec<Vec<f64>> = (0..RULING_SAMPLES).map(|i| roots(ruling_u(i))).collect();
2764    let count = rows[0].len();
2765    let scan = WINDOW_SCAN * RULING_SAMPLES;
2766    #[allow(clippy::cast_precision_loss)]
2767    if count == 0
2768        || count % 2 == 1
2769        || (0..scan).any(|k| roots(TAU * (k as f64 + 0.5) / scan as f64).len() != count)
2770    {
2771        return None;
2772    }
2773    let loops: Vec<Vec<Point3>> = (0..count)
2774        .map(|j| {
2775            let mut pts: Vec<Point3> = rows
2776                .iter()
2777                .enumerate()
2778                .map(|(i, r)| cyl.evaluate(ruling_u(i), r[j]))
2779                .collect();
2780            pts.push(pts[0]);
2781            pts
2782        })
2783        .collect();
2784    Some(fit_ruling_loops(&loops, |p| {
2785        in_order(torus.project_point(p), cyl.project_point(p), torus_first)
2786    }))
2787}
2788
2789/// A cone and a sphere whose centre is off the cone's axis, where every
2790/// generator of the cone crosses the sphere twice on the cone's nappe (a pin
2791/// through a ball): along a generator `apex + v g` the sphere is a quadratic
2792/// in `v`, and each of its two roots, taken in order, sweeps one closed loop
2793/// around the cone. A ball holding the apex is left once by every generator,
2794/// one loop. `None` when the centre lies on the axis (phase FF takes
2795/// [`exact_cone_sphere`]'s circles there), or the apex on the sphere. A
2796/// sphere only some generators cross goes to [`window_cone_sphere`].
2797fn ruling_cone_sphere(
2798    cone: &ConicalSurface,
2799    sphere: &SphericalSurface,
2800    cone_first: bool,
2801) -> Option<Vec<IntersectionCurve>> {
2802    let (apex, centre, radius) = (cone.apex(), sphere.center(), sphere.radius());
2803    let offset = apex - centre;
2804    let lin_tol = Tolerance::new().linear;
2805    let along = offset.dot(cone.axis());
2806    let across = (offset - cone.axis() * along).length();
2807    if across < lin_tol {
2808        return None;
2809    }
2810    // With the apex inside the ball, `v² + 2hv + K` has roots of opposite
2811    // signs (`K = |offset|² − R² < 0`): every generator leaves the ball once
2812    // ahead of the apex, at `v = √(h² − K) − h`, one loop round the cone.
2813    // Where `h > 0` the root is taken as `−K / (h + √(h² − K))`, free of the
2814    // cancellation.
2815    let k = offset.dot(offset) - radius * radius;
2816    if radius - offset.length() > lin_tol {
2817        let exit = |u: f64| {
2818            let h = (cone.evaluate(u, 1.0) - apex).dot(offset);
2819            let root = h.mul_add(h, -k).sqrt();
2820            cone.evaluate(u, if h > 0.0 { -k / (h + root) } else { root - h })
2821        };
2822        // A ball that barely holds the apex turns the loop sharply where it
2823        // passes the apex, at the scale of `√−K`: a step whose midpoint
2824        // strays from its chord by a hundredth of the chord (a turn of about
2825        // 0.08 radians) is halved until the turn is resolved.
2826        let mut samples: Vec<(f64, Point3)> = (0..=RULING_SAMPLES)
2827            .map(|i| (ruling_u(i), exit(ruling_u(i))))
2828            .collect();
2829        for _ in 0..10 {
2830            let mut refined = Vec::with_capacity(2 * samples.len());
2831            for pair in samples.windows(2) {
2832                let ((u0, p0), (u1, p1)) = (pair[0], pair[1]);
2833                refined.push(pair[0]);
2834                let um = 0.5 * (u0 + u1);
2835                let pm = exit(um);
2836                let chord = (p1 - p0).length();
2837                if chord > lin_tol && (pm - (p0 + (p1 - p0) * 0.5)).length() > 0.01 * chord {
2838                    refined.push((um, pm));
2839                }
2840            }
2841            refined.extend(samples.last().copied());
2842            if refined.len() == samples.len() {
2843                break;
2844            }
2845            samples = refined;
2846        }
2847        let mut pts: Vec<Point3> = samples.iter().map(|&(_, p)| p).collect();
2848        if let Some(last) = pts.last_mut() {
2849            *last = samples[0].1;
2850        }
2851        return Some(fit_ruling_loops(&[pts], |p| {
2852            in_order(cone.project_point(p), sphere.project_point(p), cone_first)
2853        }));
2854    }
2855    // Both roots along a generator of unit direction `g` (`v` the distance
2856    // from the apex), nearer first, from `h = g·offset`, when it crosses the
2857    // sphere twice ahead of the apex.
2858    let crossing = |h: f64| {
2859        let (disc, vp, vm) = ruling_quadratic(1.0, 2.0 * h, k);
2860        (disc > lin_tol && vm >= lin_tol).then_some((vm, vp))
2861    };
2862    // Around the cone `h` runs between these bounds. Where both roots lie
2863    // ahead of the apex, raising `h` shrinks the discriminant and moves the
2864    // nearer root out, so every generator crosses when the two extreme ones
2865    // do.
2866    let (sin_a, cos_a) = cone.half_angle().sin_cos();
2867    if crossing(sin_a.mul_add(along, cos_a * across)).is_none()
2868        || crossing(sin_a.mul_add(along, -cos_a * across)).is_none()
2869    {
2870        return window_cone_sphere(cone, sphere, cone_first);
2871    }
2872    let rows: Vec<(f64, f64)> = (0..RULING_SAMPLES)
2873        .map(|i| crossing((cone.evaluate(ruling_u(i), 1.0) - apex).dot(offset)))
2874        .collect::<Option<_>>()?;
2875    let loops: Vec<Vec<Point3>> = [0, 1]
2876        .iter()
2877        .map(|&j| {
2878            let mut pts: Vec<Point3> = rows
2879                .iter()
2880                .enumerate()
2881                .map(|(i, &(near, far))| {
2882                    cone.evaluate(ruling_u(i), if j == 0 { near } else { far })
2883                })
2884                .collect();
2885            pts.push(pts[0]);
2886            pts
2887        })
2888        .collect();
2889    Some(fit_ruling_loops(&loops, |p| {
2890        in_order(cone.project_point(p), sphere.project_point(p), cone_first)
2891    }))
2892}
2893
2894/// A cone and a sphere off its axis that only some generators cross (a ball
2895/// against the cone's side), with the apex outside the sphere: along the
2896/// generator at `u`, `h(u) = g·(apex − C)` is `c + A cos(u − φ)`, and the
2897/// generator crosses twice ahead of the apex where `h ≤ −√K`, with
2898/// `K = |apex − C|² − R²`. That is one arc of `u`, whose ends are where the
2899/// generator touches the sphere; the section is one loop, the nearer roots
2900/// out along the arc and the farther ones back. Sampled at
2901/// `u = mid − half·cos θ`, the roots' split `±√(h² − K)` changes sign with
2902/// `sin θ` and the loop stays smooth through both touching generators.
2903/// `Some(empty)` when no generator crosses ahead of the apex. `None`, the
2904/// marcher's case, when the apex lies within the sphere, when the centre is
2905/// too near the axis to place the arc, and when every generator reaches the
2906/// sphere (the extremes' test then failed on a touching generator or a root
2907/// at the apex).
2908fn window_cone_sphere(
2909    cone: &ConicalSurface,
2910    sphere: &SphericalSurface,
2911    cone_first: bool,
2912) -> Option<Vec<IntersectionCurve>> {
2913    let offset = cone.apex() - sphere.center();
2914    let lin_tol = Tolerance::new().linear;
2915    if offset.length() - sphere.radius() <= lin_tol {
2916        return None;
2917    }
2918    let k = offset.dot(offset) - sphere.radius() * sphere.radius();
2919    let (sin_a, cos_a) = cone.half_angle().sin_cos();
2920    let (ox, oy) = (offset.dot(cone.x_axis()), offset.dot(cone.y_axis()));
2921    let (c, a) = (sin_a * offset.dot(cone.axis()), cos_a * ox.hypot(oy));
2922    if a < lin_tol {
2923        return None;
2924    }
2925    let reach = (-k.sqrt() - c) / a;
2926    if reach <= -1.0 {
2927        return Some(Vec::new());
2928    }
2929    if reach >= 1.0 {
2930        return None;
2931    }
2932    let (mid, half) = (oy.atan2(ox) + std::f64::consts::PI, reach.acos());
2933    let half = std::f64::consts::PI - half;
2934    let n = RULING_SAMPLES;
2935    let mut pts: Vec<Point3> = (0..n)
2936        .map(|i| {
2937            #[allow(clippy::cast_precision_loss)]
2938            let theta = TAU * i as f64 / n as f64;
2939            let u = half.mul_add(-theta.cos(), mid);
2940            let h = a.mul_add((u - mid + std::f64::consts::PI).cos(), c);
2941            let split = h.mul_add(h, -k).max(0.0).sqrt();
2942            cone.evaluate(u, -h - split.copysign(theta.sin()))
2943        })
2944        .collect();
2945    pts.push(pts[0]);
2946    Some(fit_ruling_loops(&[pts], |p| {
2947        in_order(cone.project_point(p), sphere.project_point(p), cone_first)
2948    }))
2949}
2950
2951/// Scan points per ruling sample when looking for windows of meeting
2952/// rulings, and the fewest samples a window needs to be fit.
2953const WINDOW_SCAN: usize = 16;
2954const WINDOW_MIN_SAMPLES: usize = 8;
2955
2956/// Rulings sampled around a swept cylinder, half a step off u = 0 so the
2957/// branches of a self-touching curve (equal crossing cylinders) do not share
2958/// a sample.
2959const RULING_SAMPLES: usize = 128;
2960
2961#[allow(clippy::cast_precision_loss)]
2962fn ruling_u(i: usize) -> f64 {
2963    TAU * (i as f64 + 0.5) / RULING_SAMPLES as f64
2964}
2965
2966/// The discriminant and roots of `quad·v² + b·v + c = 0`.
2967fn ruling_quadratic(quad: f64, b: f64, c: f64) -> (f64, f64, f64) {
2968    let disc = b * b - 4.0 * quad * c;
2969    let root = disc.max(0.0).sqrt();
2970    (disc, (-b + root) / (2.0 * quad), (-b - root) / (2.0 * quad))
2971}
2972
2973/// The two points where each sampled ruling of `sweep` meets the other
2974/// surface, from `roots(u)` (the discriminant and the two axial parameters),
2975/// or `None` for a ruling that misses it.
2976fn ruling_samples(
2977    sweep: &CylindricalSurface,
2978    roots: &impl Fn(f64) -> (f64, f64, f64),
2979) -> Vec<Option<(Point3, Point3)>> {
2980    let lin_tol = Tolerance::new().linear;
2981    (0..RULING_SAMPLES)
2982        .map(|i| {
2983            let u = ruling_u(i);
2984            let (disc, vp, vm) = roots(u);
2985            (disc >= -lin_tol).then(|| (sweep.evaluate(u, vp), sweep.evaluate(u, vm)))
2986        })
2987        .collect()
2988}
2989
2990/// Every ruling meets the other surface: each root traces a closed loop.
2991/// Where the loops pass closest at one ruling they are sampled from it,
2992/// closer together near it, so the fit follows the narrow neck between
2993/// them; where they touch there, they are the two halves of one curve
2994/// crossing itself, each with a corner at that point, which then falls at
2995/// their shared start and end, where the fit keeps it.
2996fn closed_ruling_loops(
2997    sweep: &CylindricalSurface,
2998    roots: &impl Fn(f64) -> (f64, f64, f64),
2999    samples: &[Option<(Point3, Point3)>],
3000) -> Vec<Vec<Point3>> {
3001    let (mut plus, mut minus): (Vec<Point3>, Vec<Point3>) =
3002        if let Some((neck, touching)) = narrowest_ruling(roots) {
3003            let count = 2 * RULING_SAMPLES;
3004            #[allow(clippy::cast_precision_loss)]
3005            (0..count)
3006                .map(|i| {
3007                    let t = i as f64 / count as f64;
3008                    let u = TAU.mul_add(t - 0.9 * (TAU * t).sin() / TAU, neck);
3009                    let (_, vp, vm) = roots(u);
3010                    if i == 0 && touching {
3011                        let at = sweep.evaluate(u, 0.5 * (vp + vm));
3012                        (at, at)
3013                    } else {
3014                        (sweep.evaluate(u, vp), sweep.evaluate(u, vm))
3015                    }
3016                })
3017                .unzip()
3018        } else {
3019            samples.iter().flatten().copied().unzip()
3020        };
3021    plus.push(plus[0]);
3022    minus.push(minus[0]);
3023    vec![plus, minus]
3024}
3025
3026/// The ruling where the two roots pass closest, when exactly one ruling
3027/// stands out: a local minimum of their gap, scanned finer than the
3028/// sampling and refined, that either closes to within tolerance (the
3029/// roots touch there; the tolerance scales with the widest gap, the loops'
3030/// own size, so the decision does not move with the model) or narrows
3031/// below a quarter of the widest gap while no other minimum does. Equal
3032/// crossing cylinders touch at two rulings and get `None`.
3033fn narrowest_ruling(roots: &impl Fn(f64) -> (f64, f64, f64)) -> Option<(f64, bool)> {
3034    let scan = WINDOW_SCAN * RULING_SAMPLES;
3035    #[allow(clippy::cast_precision_loss)]
3036    let step = TAU / scan as f64;
3037    let gap = |u: f64| {
3038        let (_, vp, vm) = roots(u);
3039        (vp - vm).abs()
3040    };
3041    #[allow(clippy::cast_precision_loss)]
3042    let gaps: Vec<f64> = (0..scan).map(|k| gap(step * k as f64)).collect();
3043    let widest = gaps.iter().copied().fold(0.0, f64::max);
3044    let tol = Tolerance::new().linear * (1.0 + widest);
3045    let mut necks = Vec::new();
3046    for k in 0..scan {
3047        let (before, here, after) = (gaps[(k + scan - 1) % scan], gaps[k], gaps[(k + 1) % scan]);
3048        if here > before || here >= after || here >= 0.25 * widest {
3049            continue;
3050        }
3051        #[allow(clippy::cast_precision_loss)]
3052        let (mut lo, mut hi) = (step * (k as f64 - 1.0), step * (k as f64 + 1.0));
3053        for _ in 0..100 {
3054            let (a, b) = (lo + (hi - lo) / 3.0, hi - (hi - lo) / 3.0);
3055            if gap(a) < gap(b) {
3056                hi = b;
3057            } else {
3058                lo = a;
3059            }
3060        }
3061        let u = 0.5 * (lo + hi);
3062        necks.push((u, gap(u) <= tol));
3063    }
3064    match necks[..] {
3065        [neck] => Some(neck),
3066        _ => None,
3067    }
3068}
3069
3070/// Only windows of rulings meet the other surface: each cyclic window
3071/// carries one loop, out along one root and back along the other, the two
3072/// joined where the discriminant vanishes. Empty when no sample meets it (a
3073/// window narrower than the sampling).
3074fn partial_ruling_loops(
3075    sweep: &CylindricalSurface,
3076    roots: &impl Fn(f64) -> (f64, f64, f64),
3077    samples: &[Option<(Point3, Point3)>],
3078) -> Vec<Vec<Point3>> {
3079    let branch_point = |inside: usize, outside: usize| -> Point3 {
3080        let (mut lo, mut hi) = (ruling_u(inside), ruling_u(outside));
3081        if (hi - lo).abs() > std::f64::consts::PI {
3082            hi += if hi < lo { TAU } else { -TAU };
3083        }
3084        for _ in 0..60 {
3085            let mid = 0.5 * (lo + hi);
3086            if roots(mid).0 >= 0.0 {
3087                lo = mid;
3088            } else {
3089                hi = mid;
3090            }
3091        }
3092        let (_, vp, vm) = roots(lo);
3093        sweep.evaluate(lo, 0.5 * (vp + vm))
3094    };
3095    let Some(first_gap) = samples.iter().position(Option::is_none) else {
3096        return Vec::new();
3097    };
3098    let mut loops = Vec::new();
3099    let mut k = 0;
3100    while k < RULING_SAMPLES {
3101        let i = (first_gap + k) % RULING_SAMPLES;
3102        if samples[i].is_none() {
3103            k += 1;
3104            continue;
3105        }
3106        let start = i;
3107        let mut run = Vec::new();
3108        while k < RULING_SAMPLES {
3109            let j = (first_gap + k) % RULING_SAMPLES;
3110            let Some(pair) = samples[j] else { break };
3111            run.push(pair);
3112            k += 1;
3113        }
3114        let end = (start + run.len() - 1) % RULING_SAMPLES;
3115        let head = branch_point(start, (start + RULING_SAMPLES - 1) % RULING_SAMPLES);
3116        let tail = branch_point(end, (end + 1) % RULING_SAMPLES);
3117        let mut pts = vec![head];
3118        pts.extend(run.iter().map(|p| p.0));
3119        pts.push(tail);
3120        pts.extend(run.iter().rev().map(|p| p.1));
3121        pts.push(head);
3122        loops.push(pts);
3123    }
3124    loops
3125}
3126
3127/// Cubic interpolants through the swept loops, with `params(p)` giving each
3128/// point's parameters on the two surfaces.
3129fn fit_ruling_loops(
3130    loops: &[Vec<Point3>],
3131    params: impl Fn(Point3) -> ((f64, f64), (f64, f64)),
3132) -> Vec<IntersectionCurve> {
3133    let mut curves = Vec::new();
3134    for pts in loops {
3135        if pts.len() < 4 {
3136            continue;
3137        }
3138        let ipts: Vec<IntersectionPoint> = pts
3139            .iter()
3140            .map(|&p| {
3141                let (param1, param2) = params(p);
3142                IntersectionPoint {
3143                    point: p,
3144                    param1,
3145                    param2,
3146                }
3147            })
3148            .collect();
3149        let degree = 3.min(pts.len() - 1);
3150        if let Ok(curve) = interpolate(pts, degree) {
3151            curves.push(IntersectionCurve {
3152                curve,
3153                points: ipts,
3154            });
3155        }
3156    }
3157    curves
3158}
3159
3160/// Algebraic cone-cylinder intersection for PARALLEL (or antiparallel) axes.
3161///
3162/// When the axes are parallel, every plane perpendicular to them cuts the cone
3163/// in a circle of radius `rho = v * cos(half_angle)` about a FIXED centre and
3164/// the cylinder in a circle of radius `R` about a second FIXED centre, so the
3165/// axis separation `d` is constant in `v`. Two coplanar circles meet at
3166/// `u = phi0 +/- acos((d^2 + rho^2 - R^2) / (2*d*rho))`, giving two branches
3167/// parameterised exactly by the cone's own `v`. The branches exist only where
3168/// `rho` lies in `[|d - R|, d + R]`, which bounds the curve naturally.
3169///
3170/// This replaces the general grid-seeded marcher for the configuration, which
3171/// mis-handles it badly: seeds are accepted anywhere within half the surface
3172/// diagonal of the partner, the march-result dedup only consumes seeds the
3173/// traced polyline passes near, and the survivors are dozens of overlapping
3174/// partial traces of the same curve. Those fragments carry no usable in-face
3175/// span, so a cone corner-round crossed by a boss cylinder never splits (a
3176/// counterbore/countersink meeting a pad — the gridfinity lightweight base).
3177///
3178/// Returns `None` (defer to the caller's other paths) when the axes are not
3179/// parallel, or when they are coaxial — a coaxial pair degenerates to shared
3180/// circles, which [`exact_cone_cylinder`] emits exactly and phase FF calls
3181/// directly. Note that `intersect_analytic_analytic_bounded` does NOT consult
3182/// `exact_cone_cylinder`, so a coaxial pair reaching this path through that
3183/// caller falls through to the marcher; only the FF path gets the exact circles.
3184// Result-wrapped to match the other `try_algebraic_intersection` arms' shape.
3185#[allow(clippy::unnecessary_wraps)]
3186fn algebraic_parallel_cone_cylinder(
3187    cone: &ConicalSurface,
3188    cyl: &CylindricalSurface,
3189    v_range_cone: Option<(f64, f64)>,
3190    v_range_cyl: Option<(f64, f64)>,
3191) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
3192    let axis = cone.axis();
3193    if axis.dot(cyl.axis()).abs() < 1.0 - 1e-10 {
3194        return Ok(None); // Skew or oblique: `ruling_cone_cylinder` traces it.
3195    }
3196
3197    let apex = cone.apex();
3198    let delta = cyl.origin() - apex;
3199    let along = delta.dot(axis);
3200    let perp = delta - axis * along;
3201    let d = perp.length();
3202    if d < 1e-9 {
3203        return Ok(None); // Coaxial — `exact_cone_cylinder` owns this.
3204    }
3205
3206    let (e1, e2) = (cone.x_axis(), cone.y_axis());
3207    let phi0 = perp.dot(e2).atan2(perp.dot(e1));
3208
3209    let (sin_t, cos_t) = cone.half_angle().sin_cos();
3210    if cos_t < 1e-12 || sin_t < 1e-12 {
3211        return Ok(None);
3212    }
3213    let r = cyl.radius();
3214
3215    // Branch existence: |d - R| <= rho <= d + R, with rho = v * cos(half_angle).
3216    let mut v_min = (d - r).abs() / cos_t;
3217    let mut v_max = (d + r) / cos_t;
3218    if v_max <= v_min {
3219        return Ok(Some(vec![]));
3220    }
3221
3222    // Narrow the sampled span to the faces' own extents so the fixed sample
3223    // budget resolves the in-face part of the curve rather than spreading over
3224    // a loop that mostly lies off both patches. A face's crossing can be a
3225    // fraction of a degree of the cone's sweep (the corner-round case above),
3226    // and an unnarrowed sampling puts fewer than one sample across it.
3227    let mut lo = v_min;
3228    let mut hi = v_max;
3229    // Clip EXACTLY to the hints, not to a padded window: an endpoint that lands
3230    // exactly on the face's own v-limit lies ON that boundary rim, so the
3231    // downstream pave machinery anchors it to the rim edge instead of leaving
3232    // the section dangling just past the face.
3233    if let Some((a, b)) = v_range_cone {
3234        let (a, b) = if a <= b { (a, b) } else { (b, a) };
3235        lo = lo.max(a);
3236        hi = hi.min(b);
3237    }
3238    if let Some((a, b)) = v_range_cyl {
3239        // The cylinder's v is a signed distance along its axis from its origin;
3240        // convert both ends to the cone's v via the shared axial direction.
3241        let flip = cyl.axis().dot(axis);
3242        let to_cone_v = |cv: f64| (along + cv * flip) / sin_t;
3243        let (a, b) = (to_cone_v(a), to_cone_v(b));
3244        let (a, b) = if a <= b { (a, b) } else { (b, a) };
3245        lo = lo.max(a);
3246        hi = hi.min(b);
3247    }
3248    let (turn_lo, turn_hi) = (v_min, v_max);
3249    v_min = lo.max(v_min);
3250    v_max = hi.min(v_max);
3251    if v_max - v_min <= 1e-12 {
3252        return Ok(Some(vec![]));
3253    }
3254    // A cylinder beside the axis, with both turning points inside both faces,
3255    // meets the cone in a whole loop: traced along the cylinder's rulings,
3256    // each of which meets the nappe once, it comes back as one closed curve,
3257    // which the band split of the cylinder it winds needs. Around the axis
3258    // the loop winds the cone too and stays in two branches, and so does a
3259    // wall passing so near the axis that the loop's bend there, about
3260    // `(d - r) / sqrt(d r)` of a turn wide, spans fewer than a few rulings.
3261    let slack = Tolerance::new().linear;
3262    #[allow(clippy::cast_precision_loss)]
3263    let resolved = d - r > 3.0 * (d * r).sqrt() * TAU / RULING_SAMPLES as f64;
3264    if v_min <= turn_lo + slack && v_max >= turn_hi - slack && resolved {
3265        let mut pts: Vec<Point3> = (0..RULING_SAMPLES)
3266            .map(|i| {
3267                let (sin_u, cos_u) = ruling_u(i).sin_cos();
3268                let foot = cyl.origin() + (cyl.x_axis() * cos_u + cyl.y_axis() * sin_u) * r;
3269                let off = foot - apex;
3270                let across = off - axis * off.dot(axis);
3271                apex + across + axis * (across.length() * sin_t / cos_t)
3272            })
3273            .collect();
3274        pts.push(pts[0]);
3275        return Ok(Some(fit_ruling_loops(&[pts], |p| {
3276            (cone.project_point(p), cyl.project_point(p))
3277        })));
3278    }
3279
3280    let n_samples = 128;
3281    let mut plus: Vec<Point3> = Vec::with_capacity(n_samples + 1);
3282    let mut minus: Vec<Point3> = Vec::with_capacity(n_samples + 1);
3283    #[allow(clippy::cast_precision_loss)]
3284    for i in 0..=n_samples {
3285        let v = v_min + (v_max - v_min) * (i as f64) / (n_samples as f64);
3286        let rho = v * cos_t;
3287        if rho < 1e-12 {
3288            // The apex. `cos_alpha` has rho in its denominator, so it is only
3289            // meaningful in the limit: it tends to 0 (alpha -> pi/2) when the
3290            // cylinder passes exactly through the apex (d == R), and diverges
3291            // otherwise — where the clamp would manufacture a spurious alpha of
3292            // 0 or pi. So keep the apex only in the d == R case, where it is a
3293            // genuine point of the intersection and the shared endpoint at
3294            // which the two branches meet.
3295            if (d - r).abs() < 1e-12 {
3296                let apex = cone.evaluate(phi0, v);
3297                plus.push(apex);
3298                minus.push(apex);
3299            }
3300            continue;
3301        }
3302        let cos_alpha = ((d * d + rho * rho - r * r) / (2.0 * d * rho)).clamp(-1.0, 1.0);
3303        let alpha = cos_alpha.acos();
3304        plus.push(cone.evaluate(phi0 + alpha, v));
3305        minus.push(cone.evaluate(phi0 - alpha, v));
3306    }
3307
3308    let mut curves = Vec::new();
3309    for pts in [&plus, &minus] {
3310        // Fewer than four samples in range means this branch does not cross the
3311        // bounded region at all (the other branch may still).
3312        if pts.len() < 4 {
3313            continue;
3314        }
3315        let ipts: Vec<IntersectionPoint> = pts
3316            .iter()
3317            .map(|&p| IntersectionPoint {
3318                point: p,
3319                param1: cone.project_point(p),
3320                param2: cyl.project_point(p),
3321            })
3322            .collect();
3323        let degree = 3.min(pts.len() - 1);
3324        match interpolate(pts, degree) {
3325            Ok(curve) => curves.push(IntersectionCurve {
3326                curve,
3327                points: ipts,
3328            }),
3329            // Emitting only the branch that happened to fit would starve the
3330            // section chain of exactly the piece this path exists to supply —
3331            // the same silent half-answer the marcher's fragments produced.
3332            // Defer the whole pair to the caller's other paths instead.
3333            Err(_) => return Ok(None),
3334        }
3335    }
3336
3337    Ok(Some(curves))
3338}
3339
3340/// Algebraic sphere-sphere intersection.
3341///
3342/// Two spheres intersect in a circle lying in the radical plane.
3343/// The radical plane is perpendicular to the line connecting the centers,
3344/// at a distance d1 from center1 where:
3345///   d1 = (D² + R1² - R2²) / (2D)
3346/// and D is the distance between centers.
3347fn algebraic_sphere_sphere(
3348    s1: &SphericalSurface,
3349    s2: &SphericalSurface,
3350) -> Result<Vec<IntersectionCurve>, MathError> {
3351    let c1 = s1.center();
3352    let c2 = s2.center();
3353    let r1 = s1.radius();
3354    let r2 = s2.radius();
3355
3356    let delta = c2 - c1;
3357    let d_sq = delta.x() * delta.x() + delta.y() * delta.y() + delta.z() * delta.z();
3358    let d = d_sq.sqrt();
3359
3360    if d < 1e-12 {
3361        // Concentric spheres: no intersection (unless same radius → degenerate).
3362        return Ok(vec![]);
3363    }
3364
3365    // Check separation conditions.
3366    if d > r1 + r2 + 1e-10 {
3367        return Ok(vec![]); // Too far apart
3368    }
3369    if d + r2.min(r1) + 1e-10 < r1.max(r2) {
3370        return Ok(vec![]); // One inside the other
3371    }
3372
3373    // Distance from c1 to the radical plane along the center line.
3374    let d1 = (d_sq + r1 * r1 - r2 * r2) / (2.0 * d);
3375
3376    // Radius of the intersection circle.
3377    let r_circle_sq = r1 * r1 - d1 * d1;
3378    if r_circle_sq < 0.0 {
3379        // Tangent or no intersection (numerical noise).
3380        if r_circle_sq > -1e-10 {
3381            // Tangent: single point.
3382            let axis = Vec3::new(delta.x() / d, delta.y() / d, delta.z() / d);
3383            let tangent_pt = Point3::new(
3384                c1.x() + axis.x() * d1,
3385                c1.y() + axis.y() * d1,
3386                c1.z() + axis.z() * d1,
3387            );
3388            let ipt = IntersectionPoint {
3389                point: tangent_pt,
3390                param1: (0.0, 0.0),
3391                param2: (0.0, 0.0),
3392            };
3393            // Single-point "curve" — not very useful but correct.
3394            return Ok(vec![IntersectionCurve {
3395                curve: interpolate(&[tangent_pt, tangent_pt], 1)?,
3396                points: vec![ipt],
3397            }]);
3398        }
3399        return Ok(vec![]);
3400    }
3401
3402    let r_circle = r_circle_sq.sqrt();
3403    let axis = Vec3::new(delta.x() / d, delta.y() / d, delta.z() / d);
3404    let center = Point3::new(
3405        c1.x() + axis.x() * d1,
3406        c1.y() + axis.y() * d1,
3407        c1.z() + axis.z() * d1,
3408    );
3409
3410    // Build a reference frame for the circle.
3411    let basis = Frame3::from_normal(center, axis)?;
3412    let u_dir = basis.x;
3413    let v_dir = basis.y;
3414
3415    // Sample the circle for the IntersectionCurve representation.
3416    let n_samples = 33; // Odd for symmetry
3417    let mut points = Vec::with_capacity(n_samples);
3418    let mut positions = Vec::with_capacity(n_samples);
3419    #[allow(clippy::cast_precision_loss)]
3420    for i in 0..n_samples {
3421        let theta = TAU * i as f64 / (n_samples - 1) as f64;
3422        let (sin_t, cos_t) = theta.sin_cos();
3423        let pt = Point3::new(
3424            center.x() + (u_dir.x() * cos_t + v_dir.x() * sin_t) * r_circle,
3425            center.y() + (u_dir.y() * cos_t + v_dir.y() * sin_t) * r_circle,
3426            center.z() + (u_dir.z() * cos_t + v_dir.z() * sin_t) * r_circle,
3427        );
3428        positions.push(pt);
3429        points.push(IntersectionPoint {
3430            point: pt,
3431            param1: (0.0, 0.0),
3432            param2: (0.0, 0.0),
3433        });
3434    }
3435
3436    let degree = 3.min(positions.len() - 1);
3437    let curve = interpolate(&positions, degree)?;
3438
3439    Ok(vec![IntersectionCurve { curve, points }])
3440}
3441
3442/// Newton correction: project a point back onto the intersection curve
3443/// of two analytic surfaces. Solves the 3×3 system:
3444///   δ · na = -da  (eliminate distance to surface A)
3445///   δ · nb = -db  (eliminate distance to surface B)
3446///   δ · t  = 0    (minimal correction, perpendicular to tangent)
3447#[allow(clippy::too_many_arguments)]
3448fn correct_to_intersection(
3449    a: &AnalyticSurface<'_>,
3450    b: &AnalyticSurface<'_>,
3451    surf_a: &dyn Fn(f64, f64) -> Point3,
3452    norm_a: &dyn Fn(f64, f64) -> Vec3,
3453    surf_b: &dyn Fn(f64, f64) -> Point3,
3454    norm_b: &dyn Fn(f64, f64) -> Vec3,
3455    point: Point3,
3456    u_range_a: (f64, f64),
3457    v_range_a: (f64, f64),
3458    u_range_b: (f64, f64),
3459    v_range_b: (f64, f64),
3460    max_iters: usize,
3461) -> Point3 {
3462    let mut p = point;
3463    for _ in 0..max_iters {
3464        let (ua, va) = project_analytic(a, p, u_range_a, v_range_a);
3465        let (ub, vb) = project_analytic(b, p, u_range_b, v_range_b);
3466        let pa = surf_a(ua, va);
3467        let pb = surf_b(ub, vb);
3468        let na = norm_a(ua, va);
3469        let nb = norm_b(ub, vb);
3470        let pv = Vec3::new(p.x(), p.y(), p.z());
3471
3472        let da = (pv - Vec3::new(pa.x(), pa.y(), pa.z())).dot(na);
3473        let db = (pv - Vec3::new(pb.x(), pb.y(), pb.z())).dot(nb);
3474
3475        if da.abs() < 1e-7 && db.abs() < 1e-7 {
3476            break;
3477        }
3478
3479        let t = na.cross(nb);
3480        let t_len = t.length();
3481        if t_len < 1e-10 {
3482            // Surfaces are tangent — fall back to midpoint.
3483            return Point3::new(
3484                (pa.x() + pb.x()) * 0.5,
3485                (pa.y() + pb.y()) * 0.5,
3486                (pa.z() + pb.z()) * 0.5,
3487            );
3488        }
3489        let t_hat = t * (1.0 / t_len);
3490
3491        // Solve [na; nb; t_hat] · δ = [-da, -db, 0] via Cramer's rule.
3492        let det = na.x() * (nb.y() * t_hat.z() - nb.z() * t_hat.y())
3493            - na.y() * (nb.x() * t_hat.z() - nb.z() * t_hat.x())
3494            + na.z() * (nb.x() * t_hat.y() - nb.y() * t_hat.x());
3495        if det.abs() < 1e-15 {
3496            return Point3::new(
3497                (pa.x() + pb.x()) * 0.5,
3498                (pa.y() + pb.y()) * 0.5,
3499                (pa.z() + pb.z()) * 0.5,
3500            );
3501        }
3502        let inv = 1.0 / det;
3503        // Cramer's rule: replace each column of A with rhs = (-da, -db, 0).
3504        let dx = inv
3505            * (-da * (nb.y() * t_hat.z() - nb.z() * t_hat.y())
3506                + db * (na.y() * t_hat.z() - na.z() * t_hat.y()));
3507        let dy = inv
3508            * (da * (nb.x() * t_hat.z() - nb.z() * t_hat.x())
3509                - db * (na.x() * t_hat.z() - na.z() * t_hat.x()));
3510        let dz = inv
3511            * (-da * (nb.x() * t_hat.y() - nb.y() * t_hat.x())
3512                + db * (na.x() * t_hat.y() - na.y() * t_hat.x()));
3513        let candidate = Point3::new(p.x() + dx, p.y() + dy, p.z() + dz);
3514
3515        // Divergence guard: if the correction moves farther from both
3516        // surfaces, abandon Newton and return the best point so far.
3517        let (uc, vc) = project_analytic(a, candidate, u_range_a, v_range_a);
3518        let (ud, vd) = project_analytic(b, candidate, u_range_b, v_range_b);
3519        let pc_a = surf_a(uc, vc);
3520        let pc_b = surf_b(ud, vd);
3521        let cv = Vec3::new(candidate.x(), candidate.y(), candidate.z());
3522        let da_new = (cv - Vec3::new(pc_a.x(), pc_a.y(), pc_a.z()))
3523            .dot(norm_a(uc, vc))
3524            .abs();
3525        let db_new = (cv - Vec3::new(pc_b.x(), pc_b.y(), pc_b.z()))
3526            .dot(norm_b(ud, vd))
3527            .abs();
3528        if da_new > da.abs() && db_new > db.abs() {
3529            return p;
3530        }
3531
3532        p = candidate;
3533    }
3534    p
3535}
3536
3537/// March along the intersection of two surfaces from a seed point.
3538///
3539/// Uses the cross product of surface normals as the tangent direction
3540/// and projects back onto both surfaces using analytical projection
3541/// (for cylinders/spheres) or grid search (fallback).
3542#[allow(clippy::too_many_arguments)]
3543fn march_analytic_intersection(
3544    a: &AnalyticSurface<'_>,
3545    b: &AnalyticSurface<'_>,
3546    surf_a: &dyn Fn(f64, f64) -> Point3,
3547    norm_a: &dyn Fn(f64, f64) -> Vec3,
3548    surf_b: &dyn Fn(f64, f64) -> Point3,
3549    norm_b: &dyn Fn(f64, f64) -> Vec3,
3550    seed: Point3,
3551    u_range_a: (f64, f64),
3552    v_range_a: (f64, f64),
3553    u_range_b: (f64, f64),
3554    v_range_b: (f64, f64),
3555    initial_step: f64,
3556    u_periodic_a: bool,
3557    u_periodic_b: bool,
3558    region: Option<Aabb3>,
3559) -> Vec<Point3> {
3560    let max_steps = 500;
3561    let h_min = 1e-6;
3562    let h_max = initial_step * 4.0;
3563    // Fixed closure threshold: the adaptive step `h` varies with curvature
3564    // and can shrink below the actual miss distance at the seed re-approach.
3565    // Use `initial_step * 5` to robustly detect closure on the first pass.
3566    let closure_dist = initial_step * 5.0;
3567    // Angular thresholds for curvature-adaptive stepping.
3568    let max_angle = 10.0_f64.to_radians();
3569    let min_angle = 2.0_f64.to_radians();
3570
3571    // March forward from seed, collecting points.
3572    let mut forward = Vec::new();
3573    // March backward from seed, collecting points (reversed at end).
3574    let mut backward = Vec::new();
3575
3576    for (direction, points) in [(1.0_f64, &mut forward), (-1.0_f64, &mut backward)] {
3577        let mut current = seed;
3578        let mut h = initial_step;
3579        let mut prev_tangent: Option<Vec3> = None;
3580
3581        for _ in 0..max_steps {
3582            let (ua, va) = project_analytic(a, current, u_range_a, v_range_a);
3583            let (ub, vb) = project_analytic(b, current, u_range_b, v_range_b);
3584
3585            let na = norm_a(ua, va);
3586            let nb = norm_b(ub, vb);
3587
3588            let tangent = na.cross(nb);
3589            let t_len = tangent.length();
3590            if t_len < 1e-10 {
3591                break;
3592            }
3593            let t_dir = tangent * (direction / t_len);
3594
3595            // Curvature-adaptive step: check angular deviation from previous tangent.
3596            if let Some(prev_t) = prev_tangent {
3597                let cos_angle = prev_t.dot(t_dir).clamp(-1.0, 1.0);
3598                let angle = cos_angle.acos();
3599                if angle > max_angle && h > h_min {
3600                    h = (h * 0.5).max(h_min);
3601                } else if angle < min_angle {
3602                    h = (h * 2.0).min(h_max);
3603                }
3604            }
3605            prev_tangent = Some(t_dir);
3606
3607            let next = Point3::new(
3608                h.mul_add(t_dir.x(), current.x()),
3609                h.mul_add(t_dir.y(), current.y()),
3610                h.mul_add(t_dir.z(), current.z()),
3611            );
3612
3613            let (ua2, va2) = project_analytic(a, next, u_range_a, v_range_a);
3614            let (ub2, vb2) = project_analytic(b, next, u_range_b, v_range_b);
3615
3616            let pa = surf_a(ua2, va2);
3617            let pb = surf_b(ub2, vb2);
3618            let mid = Point3::new(
3619                (pa.x() + pb.x()) * 0.5,
3620                (pa.y() + pb.y()) * 0.5,
3621                (pa.z() + pb.z()) * 0.5,
3622            );
3623            let out_a = (!u_periodic_a && (ua2 <= u_range_a.0 || ua2 >= u_range_a.1))
3624                || va2 <= v_range_a.0
3625                || va2 >= v_range_a.1;
3626            let out_b = (!u_periodic_b && (ub2 <= u_range_b.0 || ub2 >= u_range_b.1))
3627                || vb2 <= v_range_b.0
3628                || vb2 >= v_range_b.1;
3629
3630            if out_a || out_b {
3631                break;
3632            }
3633            if region.is_some_and(|r| !r.contains_point(mid)) {
3634                points.push(mid);
3635                break;
3636            }
3637
3638            // Check for loop closure — if we've collected enough points and
3639            // the current point is close to the seed, the curve is closed.
3640            // Require ≥10 steps to avoid premature closure near the seed.
3641            let dist_to_seed = (mid - seed).length();
3642            if points.len() > 10 && dist_to_seed < closure_dist {
3643                points.push(seed);
3644                break;
3645            }
3646
3647            points.push(mid);
3648            current = mid;
3649        }
3650    }
3651
3652    // Assemble result: backward (reversed) + seed + forward
3653    backward.reverse();
3654    let mut result = backward;
3655    result.push(seed);
3656    result.append(&mut forward);
3657
3658    // Refine all points onto the intersection curve via Newton correction.
3659    for pt in &mut result {
3660        *pt = correct_to_intersection(
3661            a, b, surf_a, norm_a, surf_b, norm_b, *pt, u_range_a, v_range_a, u_range_b, v_range_b,
3662            5,
3663        );
3664    }
3665
3666    result
3667}
3668
3669/// Project a 3D point onto an analytic surface using the surface's
3670/// analytical projection method. Falls back to grid search for surface
3671/// types without analytical projection.
3672fn project_analytic(
3673    surface: &AnalyticSurface<'_>,
3674    point: Point3,
3675    u_range: (f64, f64),
3676    v_range: (f64, f64),
3677) -> (f64, f64) {
3678    match surface {
3679        AnalyticSurface::Cylinder(cyl) => {
3680            let (u, v) = cyl.project_point(point);
3681            (u.clamp(u_range.0, u_range.1), v.clamp(v_range.0, v_range.1))
3682        }
3683        AnalyticSurface::Sphere(sphere) => {
3684            let (u, v) = sphere.project_point(point);
3685            (u.clamp(u_range.0, u_range.1), v.clamp(v_range.0, v_range.1))
3686        }
3687        AnalyticSurface::Cone(cone) => {
3688            let (u, v) = cone.project_point(point);
3689            (u.clamp(u_range.0, u_range.1), v.clamp(v_range.0, v_range.1))
3690        }
3691        AnalyticSurface::Torus(torus) => {
3692            let (u, v) = torus.project_point(point);
3693            (u.clamp(u_range.0, u_range.1), v.clamp(v_range.0, v_range.1))
3694        }
3695    }
3696}
3697
3698/// Returns `true` if the surface's u-parameter is periodic (wraps around 2π).
3699/// All current `AnalyticSurface` variants have periodic u — this is trivially
3700/// true today but exists as a guard for future non-periodic analytic types.
3701fn is_u_periodic(surface: &AnalyticSurface<'_>) -> bool {
3702    matches!(
3703        surface,
3704        AnalyticSurface::Cylinder(_)
3705            | AnalyticSurface::Cone(_)
3706            | AnalyticSurface::Sphere(_)
3707            | AnalyticSurface::Torus(_)
3708    )
3709}
3710
3711/// Extract closures and parameter ranges for an analytic surface.
3712#[allow(clippy::type_complexity)]
3713fn surface_closures<'a>(
3714    surface: &'a AnalyticSurface<'a>,
3715) -> (
3716    Box<dyn Fn(f64, f64) -> Point3 + 'a>,
3717    Box<dyn Fn(f64, f64) -> Vec3 + 'a>,
3718    (f64, f64),
3719    (f64, f64),
3720) {
3721    match surface {
3722        AnalyticSurface::Cylinder(cyl) => (
3723            Box::new(|u, v| cyl.evaluate(u, v)),
3724            Box::new(|u, v| cyl.normal(u, v)),
3725            (0.0, TAU),
3726            (-1.0, 1.0),
3727        ),
3728        AnalyticSurface::Cone(cone) => (
3729            Box::new(|u, v| cone.evaluate(u, v)),
3730            Box::new(|u, v| cone.normal(u, v)),
3731            (0.0, TAU),
3732            (0.01, 2.0),
3733        ),
3734        AnalyticSurface::Sphere(sphere) => (
3735            Box::new(|u, v| sphere.evaluate(u, v)),
3736            Box::new(|u, v| sphere.normal(u, v)),
3737            (0.0, TAU),
3738            (-FRAC_PI_2, FRAC_PI_2),
3739        ),
3740        AnalyticSurface::Torus(torus) => (
3741            Box::new(|u, v| torus.evaluate(u, v)),
3742            Box::new(|u, v| torus.normal(u, v)),
3743            (0.0, TAU),
3744            (0.0, TAU),
3745        ),
3746    }
3747}
3748
3749#[cfg(test)]
3750#[allow(clippy::unwrap_used, clippy::expect_used)]
3751mod tests {
3752    use super::*;
3753    use crate::tolerance::Tolerance;
3754
3755    /// Arcs of a cone's hyperbola (a plane parallel to the axis) and parabola
3756    /// (a plane parallel to a ruling) between two of their sampled points
3757    /// stay on both the plane and the cone everywhere, not just at samples.
3758    #[test]
3759    fn plane_cone_conic_arcs_lie_on_both_surfaces() {
3760        let half_angle = 1.1_f64;
3761        let cone = ConicalSurface::new(
3762            Point3::new(0.0, 0.0, 0.0),
3763            Vec3::new(0.0, 0.0, 1.0),
3764            half_angle,
3765        )
3766        .unwrap();
3767        let ruling = Vec3::new(half_angle.sin(), 0.0, half_angle.cos());
3768        for (normal, d) in [(Vec3::new(1.0, 0.0, 0.0), 0.5), (ruling, 1.0)] {
3769            let chains =
3770                exact_plane_analytic_reaching(AnalyticSurface::Cone(&cone), normal, d, 10.0)
3771                    .unwrap();
3772            let chain = chains
3773                .iter()
3774                .find_map(|c| match c {
3775                    ExactIntersectionCurve::Points(chain) => Some(chain),
3776                    _ => None,
3777                })
3778                .expect("a parabola or hyperbola section is sampled");
3779            let (from, to) = (chain[2], chain[chain.len() - 3]);
3780            let arc = plane_cone_conic_arc(&cone, normal, d, from, to)
3781                .unwrap()
3782                .expect("an exact arc");
3783            let (t0, t1) = arc.domain();
3784            assert!((arc.evaluate(t0) - from).length() < 1e-12);
3785            assert!((arc.evaluate(t1) - to).length() < 1e-12);
3786            for i in 0..=200 {
3787                let q = arc.evaluate(t0 + (t1 - t0) * f64::from(i) / 200.0);
3788                let w = q - Point3::new(0.0, 0.0, 0.0);
3789                let off_plane = (normal.dot(w) - d).abs();
3790                let off_cone = (w.z() - w.length() * half_angle.sin()).abs();
3791                assert!(off_plane < 1e-9, "off the plane by {off_plane}");
3792                assert!(off_cone < 1e-9, "off the cone by {off_cone}");
3793            }
3794        }
3795    }
3796
3797    /// A plane a few 1e-10 short of parallel to a ruling cuts a vast ellipse
3798    /// that the parabola's closed form only approximates: the arc is either
3799    /// declined or on the cone, and an arc with coincident ends is declined.
3800    #[test]
3801    fn plane_cone_conic_arc_declines_a_near_parabolic_ellipse() {
3802        let half_angle = 1.1_f64;
3803        let cone = ConicalSurface::new(
3804            Point3::new(0.0, 0.0, 0.0),
3805            Vec3::new(0.0, 0.0, 1.0),
3806            half_angle,
3807        )
3808        .unwrap();
3809        for shortfall in [1e-10, 3e-10, 8e-10] {
3810            let tilt = half_angle - shortfall / (2.0 * half_angle).sin();
3811            let normal = Vec3::new(tilt.sin(), 0.0, tilt.cos());
3812            let chains =
3813                exact_plane_analytic_reaching(AnalyticSurface::Cone(&cone), normal, 1.0, 10.0)
3814                    .unwrap();
3815            let Some(chain) = chains.iter().find_map(|c| match c {
3816                ExactIntersectionCurve::Points(chain) => Some(chain),
3817                _ => None,
3818            }) else {
3819                continue;
3820            };
3821            let (from, to) = (chain[2], chain[chain.len() - 3]);
3822            assert!(
3823                plane_cone_conic_arc(&cone, normal, 1.0, from, from)
3824                    .unwrap()
3825                    .is_none(),
3826                "coincident ends"
3827            );
3828            let Some(arc) = plane_cone_conic_arc(&cone, normal, 1.0, from, to).unwrap() else {
3829                continue;
3830            };
3831            let (t0, t1) = arc.domain();
3832            for i in 0..=200 {
3833                let w = arc.evaluate(t0 + (t1 - t0) * f64::from(i) / 200.0)
3834                    - Point3::new(0.0, 0.0, 0.0);
3835                let off_cone = (w.z() - w.length() * half_angle.sin()).abs();
3836                assert!(off_cone < 1e-8, "{shortfall}: off the cone by {off_cone}");
3837            }
3838        }
3839    }
3840
3841    #[test]
3842    fn plane_cylinder_perpendicular() {
3843        let cyl =
3844            CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 2.0)
3845                .unwrap();
3846
3847        // Horizontal plane at z=3 -- produces a circle at height 3.
3848        let curves = intersect_plane_cylinder(&cyl, Vec3::new(0.0, 0.0, 1.0), 3.0).unwrap();
3849        assert!(!curves.is_empty(), "should find intersection curve");
3850        assert!(
3851            curves[0].points.len() > 10,
3852            "should have many sample points"
3853        );
3854
3855        let tol = Tolerance::loose();
3856        for pt in &curves[0].points {
3857            assert!(
3858                tol.approx_eq(pt.point.z(), 3.0),
3859                "z should be ~3.0, got {}",
3860                pt.point.z()
3861            );
3862            let r = pt.point.x().hypot(pt.point.y());
3863            assert!(tol.approx_eq(r, 2.0), "radius should be ~2.0, got {r}");
3864        }
3865    }
3866
3867    #[test]
3868    fn plane_sphere_equator() {
3869        let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 3.0).unwrap();
3870
3871        let curves = intersect_plane_sphere(&sphere, Vec3::new(0.0, 0.0, 1.0), 0.0).unwrap();
3872        assert!(!curves.is_empty());
3873
3874        let tol = Tolerance::loose();
3875        for pt in &curves[0].points {
3876            assert!(
3877                tol.approx_eq(pt.point.z(), 0.0),
3878                "z should be ~0, got {}",
3879                pt.point.z()
3880            );
3881            let r = pt.point.x().hypot(pt.point.y());
3882            assert!(tol.approx_eq(r, 3.0), "radius should be ~3.0, got {r}");
3883        }
3884    }
3885
3886    #[test]
3887    fn plane_sphere_no_intersection() {
3888        let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 1.0).unwrap();
3889
3890        let curves = intersect_plane_sphere(&sphere, Vec3::new(0.0, 0.0, 1.0), 5.0).unwrap();
3891        assert!(curves.is_empty());
3892    }
3893
3894    #[test]
3895    fn plane_cone_cross_section() {
3896        let cone = ConicalSurface::new(
3897            Point3::new(0.0, 0.0, 0.0),
3898            Vec3::new(0.0, 0.0, 1.0),
3899            std::f64::consts::FRAC_PI_4,
3900        )
3901        .unwrap();
3902
3903        let curves = intersect_plane_cone(&cone, Vec3::new(0.0, 0.0, 1.0), 1.0).unwrap();
3904        assert!(!curves.is_empty(), "should find intersection with cone");
3905    }
3906
3907    /// The 1u gridfinity spacer lip fuse corner (#1570): the body's lip
3908    /// recess cone (45 deg, opening downward) meets the tool's lip cone
3909    /// (45 deg, opening upward) with axes offset 0.25mm in x and y. Equal
3910    /// half-angle tangents put the whole intersection on the radical plane,
3911    /// so the section is one exact ellipse; the marcher shredded this into
3912    /// ~64 closed micro-loops per pair.
3913    #[test]
3914    fn offset_parallel_equal_angle_cones_give_one_exact_ellipse() {
3915        let c1 = ConicalSurface::new(
3916            Point3::new(
3917                -16.999_999_999_999_975,
3918                -16.999_999_999_999_975,
3919                5.849_999_999_999_951,
3920            ),
3921            Vec3::new(0.0, 0.0, -1.0),
3922            0.785_398_163_397_433_5,
3923        )
3924        .unwrap();
3925        let c2 = ConicalSurface::new(
3926            Point3::new(
3927                -16.750_000_000_000_036,
3928                -16.750_000_000_000_018,
3929                0.749_999_999_999_881,
3930            ),
3931            Vec3::new(0.0, 0.0, 1.0),
3932            0.785_398_163_397_467_6,
3933        )
3934        .unwrap();
3935
3936        let curves = exact_cone_cone(&c1, &c2)
3937            .unwrap()
3938            .expect("offset parallel equal-angle cones must take the radical-plane path");
3939        assert_eq!(curves.len(), 1, "expected exactly one section conic");
3940        assert!(
3941            matches!(curves[0], ExactIntersectionCurve::Ellipse(_)),
3942            "expected an ellipse section, got {:?}",
3943            curves[0]
3944        );
3945        let ExactIntersectionCurve::Ellipse(ellipse) = &curves[0] else {
3946            return;
3947        };
3948
3949        // Every sample must lie on BOTH cones: distance to the axis equals
3950        // tan(half_angle) times the axial distance from the apex, on the
3951        // real nappe of each.
3952        for i in 0..16 {
3953            let p = crate::traits::ParametricCurve::evaluate(ellipse, TAU * f64::from(i) / 16.0);
3954            for (cone, label) in [(&c1, "c1"), (&c2, "c2")] {
3955                let rel = p - cone.apex();
3956                let rel_v = Vec3::new(rel.x(), rel.y(), rel.z());
3957                let axial = rel_v.dot(cone.axis());
3958                let radial = (rel_v - cone.axis() * axial).length();
3959                assert!(
3960                    axial > 0.0,
3961                    "{label}: sample on phantom nappe (axial {axial})"
3962                );
3963                let expect = cone.half_angle().tan() * axial;
3964                assert!(
3965                    (radial - expect).abs() < 1e-9,
3966                    "{label}: sample off surface by {}",
3967                    (radial - expect).abs()
3968                );
3969            }
3970        }
3971    }
3972
3973    /// Opposed cones whose real nappes occupy disjoint half-spaces share a
3974    /// radical-plane conic only on the phantom nappe — the exact path must
3975    /// report a definitive empty intersection, not defer to the marcher.
3976    #[test]
3977    fn offset_parallel_cones_opening_apart_have_no_real_intersection() {
3978        let c1 = ConicalSurface::new(
3979            Point3::new(0.0, 0.0, 5.0),
3980            Vec3::new(0.0, 0.0, -1.0),
3981            std::f64::consts::FRAC_PI_4,
3982        )
3983        .unwrap();
3984        let c2 = ConicalSurface::new(
3985            Point3::new(0.25, 0.25, 20.0),
3986            Vec3::new(0.0, 0.0, 1.0),
3987            std::f64::consts::FRAC_PI_4,
3988        )
3989        .unwrap();
3990        let curves = exact_cone_cone(&c1, &c2)
3991            .unwrap()
3992            .expect("radical-plane path");
3993        assert!(curves.is_empty(), "disjoint nappes must yield no curves");
3994    }
3995
3996    /// Unequal half-angles keep a quadratic term in the pencil — no plane
3997    /// reduction exists, so the exact path must defer to the marcher.
3998    #[test]
3999    fn offset_parallel_cones_with_unequal_angles_defer() {
4000        let c1 = ConicalSurface::new(
4001            Point3::new(0.0, 0.0, 5.0),
4002            Vec3::new(0.0, 0.0, -1.0),
4003            std::f64::consts::FRAC_PI_4,
4004        )
4005        .unwrap();
4006        let c2 = ConicalSurface::new(Point3::new(0.25, 0.25, 0.5), Vec3::new(0.0, 0.0, 1.0), 0.6)
4007            .unwrap();
4008        assert!(exact_cone_cone(&c1, &c2).unwrap().is_none());
4009    }
4010
4011    /// A 45 degree cone and a radius 0.1 tube tilted 40 degrees that pierces
4012    /// both nappes, so the ruling path declines and the general marcher runs:
4013    /// the tube's axis meets the near nappe at (0.1, -1.368, 1.370).
4014    fn cone_and_tilted_tube() -> (ConicalSurface, CylindricalSurface) {
4015        let cone = ConicalSurface::new(
4016            Point3::new(0.0, 0.0, 0.0),
4017            Vec3::new(0.0, 0.0, 1.0),
4018            std::f64::consts::FRAC_PI_4,
4019        )
4020        .unwrap();
4021        let (s, c) = 40.0_f64.to_radians().sin_cos();
4022        let tube =
4023            CylindricalSurface::new(Point3::new(0.1, 0.0, 3.0), Vec3::new(0.0, s, c), 0.1).unwrap();
4024        (cone, tube)
4025    }
4026
4027    #[test]
4028    fn marcher_keeps_to_its_region() {
4029        let (cone, tube) = cone_and_tilted_tube();
4030        let run = |region: Option<Aabb3>| {
4031            let (a, b) = (
4032                AnalyticSurface::Cone(&cone),
4033                AnalyticSurface::Cylinder(&tube),
4034            );
4035            let (va, vb) = (Some((0.5, 4.0)), Some((-5.0, 5.0)));
4036            match region {
4037                Some(r) => intersect_analytic_analytic_in_region(a, b, 32, va, vb, r),
4038                None => intersect_analytic_analytic_bounded(a, b, 32, va, vb),
4039            }
4040            .unwrap()
4041        };
4042        assert!(!run(None).is_empty());
4043        let near = Aabb3 {
4044            min: Point3::new(-0.5, -2.0, 0.8),
4045            max: Point3::new(0.7, -0.7, 2.0),
4046        };
4047        let curves = run(Some(near));
4048        assert!(!curves.is_empty(), "the loop through the region is kept");
4049        // The region's margin (two grid cells) and one marching step past it.
4050        let reach = near.expanded(0.6);
4051        for curve in &curves {
4052            assert!(curve.points.iter().all(|p| reach.contains_point(p.point)));
4053        }
4054        let away = Aabb3 {
4055            min: Point3::new(5.0, 5.0, 5.0),
4056            max: Point3::new(6.0, 6.0, 6.0),
4057        };
4058        assert!(run(Some(away)).is_empty(), "nothing is marched outside it");
4059    }
4060
4061    #[test]
4062    fn coaxial_cones_cross_at_single_circle() {
4063        // Two coaxial truncated cones (outer base r10->top r8, inner r9->r8
4064        // over height 10) cross where their radii match: z=10, r=8. The
4065        // intersection must be ONE clean circle, not the dozens of degenerate
4066        // micro-curves the general marcher produces at near-tangency.
4067        let outer = ConicalSurface::new(
4068            Point3::new(0.0, 0.0, 50.0),
4069            Vec3::new(0.0, 0.0, -1.0),
4070            5.0_f64.atan(),
4071        )
4072        .unwrap();
4073        let inner = ConicalSurface::new(
4074            Point3::new(0.0, 0.0, 90.0),
4075            Vec3::new(0.0, 0.0, -1.0),
4076            10.0_f64.atan(),
4077        )
4078        .unwrap();
4079
4080        let curves = intersect_analytic_analytic_bounded(
4081            AnalyticSurface::Cone(&outer),
4082            AnalyticSurface::Cone(&inner),
4083            32,
4084            None,
4085            None,
4086        )
4087        .unwrap();
4088
4089        assert_eq!(
4090            curves.len(),
4091            1,
4092            "coaxial cones crossing at one circle must yield exactly one curve, got {}",
4093            curves.len()
4094        );
4095        for p in &curves[0].points {
4096            let r = p.point.x().hypot(p.point.y());
4097            assert!(
4098                (p.point.z() - 10.0).abs() < 1e-6 && (r - 8.0).abs() < 1e-6,
4099                "intersection point off the expected z=10,r=8 circle: {:?}",
4100                p.point
4101            );
4102        }
4103    }
4104
4105    #[test]
4106    fn plane_torus_cross_section() {
4107        let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 5.0, 1.0).unwrap();
4108
4109        let curves = intersect_plane_torus(&torus, Vec3::new(0.0, 0.0, 1.0), 0.0).unwrap();
4110        assert!(
4111            !curves.is_empty(),
4112            "should find intersection curves with torus"
4113        );
4114    }
4115
4116    /// A plane resting on a torus's tube touches it along one circle of the
4117    /// torus's own radius, ring or spindle, at the tube's top or bottom.
4118    #[test]
4119    fn plane_tangent_to_a_tube_touches_it_along_one_circle() {
4120        for (major, minor) in [(5.0, 1.0), (0.1, 2.45)] {
4121            let torus = ToroidalSurface::new(Point3::new(1.0, 2.0, 3.0), major, minor).unwrap();
4122            for z in [3.0 - minor, 3.0 + minor] {
4123                let exact = exact_plane_analytic(
4124                    AnalyticSurface::Torus(&torus),
4125                    Vec3::new(0.0, 0.0, 1.0),
4126                    z,
4127                )
4128                .unwrap();
4129                assert_eq!(exact.len(), 1, "R={major} r={minor} z={z}");
4130                let circle = match &exact[0] {
4131                    ExactIntersectionCurve::Circle(c) => Some(c),
4132                    _ => None,
4133                };
4134                let c = circle.expect("a circle");
4135                assert!((c.radius() - major).abs() < 1e-12);
4136                assert!((c.center() - Point3::new(1.0, 2.0, z)).length() < 1e-12);
4137
4138                let sampled = intersect_plane_torus(&torus, Vec3::new(0.0, 0.0, 1.0), z).unwrap();
4139                assert_eq!(sampled.len(), 1, "R={major} r={minor} z={z}");
4140            }
4141            // A tilted plane cuts no single circle of radius R.
4142            let single = |normal: Vec3, d: f64| {
4143                matches!(
4144                    exact_plane_analytic(AnalyticSurface::Torus(&torus), normal, d)
4145                        .unwrap()
4146                        .as_slice(),
4147                    [ExactIntersectionCurve::Circle(_)]
4148                )
4149            };
4150            assert!(!single(Vec3::new(1e-6, 0.0, 1.0), 3.0 + minor));
4151        }
4152    }
4153
4154    /// Tangency is read from the plane's distance to the tube's top, so it
4155    /// survives rounding on a large torus, and a plane further inside than
4156    /// the linear tolerance still cuts its two circles.
4157    #[test]
4158    fn plane_tangent_to_a_large_tube_is_read_through_rounding() {
4159        let torus = ToroidalSurface::new(Point3::new(0.3, -0.7, 3.0), 200.0, 100.1).unwrap();
4160        let level = Vec3::new(0.0, 0.0, 1.0);
4161        let curves =
4162            |d: f64| exact_plane_analytic(AnalyticSurface::Torus(&torus), level, d).unwrap();
4163        assert!(matches!(
4164            curves(3.0 + 100.1).as_slice(),
4165            [ExactIntersectionCurve::Circle(_)]
4166        ));
4167        let inside = curves(3.0 + 100.1 - 4e-7);
4168        assert!(!matches!(
4169            inside.as_slice(),
4170            [ExactIntersectionCurve::Circle(_)]
4171        ));
4172    }
4173
4174    /// Signed distance of a point to a z-axis torus centred at the origin:
4175    /// `sqrt((sqrt(x^2+y^2) - R)^2 + z^2) - r`.
4176    fn torus_implicit(p: Point3, major: f64, minor: f64) -> f64 {
4177        let rho = p.x().hypot(p.y());
4178        ((rho - major).hypot(p.z())) - minor
4179    }
4180
4181    /// The gridfinity lightweight base's failing corner, reduced: a cavity
4182    /// corner-round cone (apex below the floor, 45 deg, axis +z) crossed by a
4183    /// parallel-axis boss cylinder. The general marcher returned ~49 overlapping
4184    /// partial traces of one curve here; the algebraic path must return exactly
4185    /// the two branches, each ON both surfaces and inside the cone's v-hint.
4186    #[test]
4187    fn oblique_cone_cylinder_traces_curves_on_both() {
4188        use crate::traits::ParametricCurve;
4189        // A pointed cone opening down from (0, 0, 3), radius half the depth,
4190        // and a rod along y through (x0, ., 1): one loop through the wall
4191        // when the rod pokes out, two when every ruling meets the cone.
4192        let cone = ConicalSurface::new(
4193            Point3::new(0.0, 0.0, 3.0),
4194            Vec3::new(0.0, 0.0, -1.0),
4195            2.0_f64.atan(),
4196        )
4197        .unwrap();
4198        for (x0, loops) in [(0.5, 1), (0.0, 2)] {
4199            let cyl =
4200                CylindricalSurface::new(Point3::new(x0, 0.0, 1.0), Vec3::new(0.0, 1.0, 0.0), 0.6)
4201                    .unwrap();
4202            for cone_first in [true, false] {
4203                let (a, b) = if cone_first {
4204                    (
4205                        AnalyticSurface::Cone(&cone),
4206                        AnalyticSurface::Cylinder(&cyl),
4207                    )
4208                } else {
4209                    (
4210                        AnalyticSurface::Cylinder(&cyl),
4211                        AnalyticSurface::Cone(&cone),
4212                    )
4213                };
4214                let curves = intersect_analytic_analytic(a, b, 32).unwrap();
4215                assert_eq!(curves.len(), loops, "x0 {x0}: loops");
4216                for c in &curves {
4217                    let (t0, t1) = c.curve.domain();
4218                    for k in 0..=64 {
4219                        let t = (t1 - t0).mul_add(f64::from(k) / 64.0, t0);
4220                        let p = ParametricCurve::evaluate(&c.curve, t);
4221                        // A cubic through the ruling samples, bent most at the
4222                        // loop's branch points.
4223                        let rod = (p.x() - x0).hypot(p.z() - 1.0);
4224                        assert!(
4225                            (rod - 0.6).abs() < 1e-4,
4226                            "x0 {x0}: off the rod by {}",
4227                            rod - 0.6
4228                        );
4229                        let cone_r = p.x().hypot(p.y());
4230                        assert!(
4231                            (cone_r - 0.5 * (3.0 - p.z())).abs() < 1e-4,
4232                            "x0 {x0}: off the cone at {p:?}"
4233                        );
4234                    }
4235                }
4236            }
4237        }
4238    }
4239
4240    #[test]
4241    fn a_rod_through_a_rings_tube_traces_four_loops() {
4242        use crate::traits::ParametricCurve;
4243        let ring = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 4.0, 1.5).unwrap();
4244        // Along y through (0.5, ., 0.3): every ruling enters and leaves the
4245        // tube on either side of the hole, four roots, four loops.
4246        let rod =
4247            CylindricalSurface::new(Point3::new(0.5, 0.0, 0.3), Vec3::new(0.0, 1.0, 0.0), 0.6)
4248                .unwrap();
4249        let curves = ruling_torus_cylinder(&ring, &rod, true).unwrap();
4250        assert_eq!(curves.len(), 4);
4251        for c in &curves {
4252            let (t0, t1) = c.curve.domain();
4253            for k in 0..=64 {
4254                let p =
4255                    ParametricCurve::evaluate(&c.curve, (t1 - t0).mul_add(f64::from(k) / 64.0, t0));
4256                let on_rod = (p.x() - 0.5).hypot(p.z() - 0.3) - 0.6;
4257                let on_ring = (p.x().hypot(p.y()) - 4.0).hypot(p.z()) - 1.5;
4258                assert!(
4259                    on_rod.abs() < 1e-4 && on_ring.abs() < 1e-4,
4260                    "off by {on_rod}, {on_ring}"
4261                );
4262            }
4263        }
4264        // Higher, the rod's top rulings pass over the tube: the count varies.
4265        let high =
4266            CylindricalSurface::new(Point3::new(0.5, 0.0, 1.0), Vec3::new(0.0, 1.0, 0.0), 0.6)
4267                .unwrap();
4268        assert!(ruling_torus_cylinder(&ring, &high, true).is_none());
4269        // Its top clears the tube only between two sampled rulings.
4270        let grazing =
4271            CylindricalSurface::new(Point3::new(0.5, 0.0, 0.9001), Vec3::new(0.0, 1.0, 0.0), 0.6)
4272                .unwrap();
4273        assert!(ruling_torus_cylinder(&ring, &grazing, true).is_none());
4274        // A spindle torus's quartic also holds its inner lemon.
4275        let spindle = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 1.0, 2.0).unwrap();
4276        let thin =
4277            CylindricalSurface::new(Point3::new(0.3, 0.0, 0.0), Vec3::new(0.0, 1.0, 0.0), 0.2)
4278                .unwrap();
4279        assert!(ruling_torus_cylinder(&spindle, &thin, true).is_none());
4280    }
4281
4282    #[test]
4283    fn a_pin_through_a_ball_traces_two_loops() {
4284        use crate::traits::ParametricCurve;
4285        let ball = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 3.0).unwrap();
4286        // A pin tapering from radius 1.2 at (1, 0.5, -5) to 0.4 at 10 up:
4287        // radial 0.08 per unit of height.
4288        let half = 0.08_f64.atan();
4289        let apex = Point3::new(1.0, 0.5, -5.0 + 1.2 / 0.08);
4290        let pin = ConicalSurface::new(apex, Vec3::new(0.0, 0.0, -1.0), FRAC_PI_2 - half).unwrap();
4291        for cone_first in [true, false] {
4292            let (a, b) = if cone_first {
4293                (AnalyticSurface::Cone(&pin), AnalyticSurface::Sphere(&ball))
4294            } else {
4295                (AnalyticSurface::Sphere(&ball), AnalyticSurface::Cone(&pin))
4296            };
4297            let curves = intersect_analytic_analytic(a, b, 32).unwrap();
4298            assert_eq!(curves.len(), 2, "entry and exit loops");
4299            for c in &curves {
4300                let (t0, t1) = c.curve.domain();
4301                for k in 0..=64 {
4302                    let p = ParametricCurve::evaluate(
4303                        &c.curve,
4304                        (t1 - t0).mul_add(f64::from(k) / 64.0, t0),
4305                    );
4306                    let on_ball = (p - Point3::new(0.0, 0.0, 0.0)).length() - 3.0;
4307                    let axial = apex.z() - p.z();
4308                    let on_pin = (p.x() - 1.0).hypot(p.y() - 0.5) - axial * half.tan();
4309                    assert!(
4310                        on_ball.abs() < 1e-4 && on_pin.abs() < 1e-4,
4311                        "off by {on_ball}, {on_pin}"
4312                    );
4313                }
4314            }
4315        }
4316        // On the ball's axis the loops are circles, which defer. A pin whose
4317        // apex is inside the ball leaves it once along every generator, and
4318        // one only partly through the ball meets it over the generators that
4319        // reach it: one loop each. One that opens away meets it not at all.
4320        let coaxial =
4321            ConicalSurface::new(Point3::new(0.0, 0.0, 10.0), Vec3::new(0.0, 0.0, -1.0), 1.4)
4322                .unwrap();
4323        assert!(ruling_cone_sphere(&coaxial, &ball, true).is_none());
4324        let aside = ConicalSurface::new(
4325            Point3::new(2.8, 0.0, 10.0),
4326            Vec3::new(0.0, 0.0, -1.0),
4327            FRAC_PI_2 - half,
4328        )
4329        .unwrap();
4330        assert_eq!(ruling_cone_sphere(&aside, &ball, true).unwrap().len(), 1);
4331        let holding = ConicalSurface::new(
4332            Point3::new(1.0, 0.5, 1.0),
4333            Vec3::new(0.0, 0.0, -1.0),
4334            FRAC_PI_2 - half,
4335        )
4336        .unwrap();
4337        assert_eq!(ruling_cone_sphere(&holding, &ball, true).unwrap().len(), 1);
4338        let away = ConicalSurface::new(
4339            Point3::new(1.0, 0.5, 10.0),
4340            Vec3::new(0.0, 0.0, 1.0),
4341            FRAC_PI_2 - half,
4342        )
4343        .unwrap();
4344        assert!(ruling_cone_sphere(&away, &ball, true).unwrap().is_empty());
4345        // Grazing: the generators that miss span about 0.002 radians,
4346        // narrower than a 2048-angle scan's step, and the near and far
4347        // roots join into one loop across them.
4348        let step = TAU / 2048.0;
4349        let grazed =
4350            SphericalSurface::new(Point3::new(step.cos(), step.sin(), 10.0), 9.255_250_971_8)
4351                .unwrap();
4352        let wide =
4353            ConicalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 0.5).unwrap();
4354        assert_eq!(ruling_cone_sphere(&wide, &grazed, true).unwrap().len(), 1);
4355    }
4356
4357    #[test]
4358    fn a_ball_beside_a_cone_meets_it_in_one_loop() {
4359        use crate::traits::ParametricCurve;
4360        // Apex 3 up, opening downward, the radius half the depth below it.
4361        let cone = ConicalSurface::new(
4362            Point3::new(0.0, 0.0, 3.0),
4363            Vec3::new(0.0, 0.0, -1.0),
4364            2.0_f64.atan(),
4365        )
4366        .unwrap();
4367        for (centre, radius) in [
4368            (Point3::new(1.0, 0.8, 1.2), 1.1),
4369            (Point3::new(1.5, 0.0, 0.0), 0.8),
4370        ] {
4371            let ball = SphericalSurface::new(centre, radius).unwrap();
4372            let curves = ruling_cone_sphere(&cone, &ball, true).unwrap();
4373            assert_eq!(curves.len(), 1, "one loop for the ball at {centre:?}");
4374            let (t0, t1) = curves[0].curve.domain();
4375            for k in 0..=64 {
4376                let p = ParametricCurve::evaluate(
4377                    &curves[0].curve,
4378                    (t1 - t0).mul_add(f64::from(k) / 64.0, t0),
4379                );
4380                let on_ball = (p - centre).length() - radius;
4381                let on_cone = p.x().hypot(p.y()) - (3.0 - p.z()) / 2.0;
4382                assert!(
4383                    on_ball.abs() < 1e-5 && on_cone.abs() < 1e-5,
4384                    "ball at {centre:?}: off by {on_ball}, {on_cone}"
4385                );
4386            }
4387        }
4388        // A ball the nappe's generators miss, and one on the apex.
4389        let clear = SphericalSurface::new(Point3::new(4.0, 0.0, 0.0), 0.5).unwrap();
4390        assert!(ruling_cone_sphere(&cone, &clear, true).unwrap().is_empty());
4391        let on_apex = SphericalSurface::new(Point3::new(0.6, 0.0, 3.8), 1.0).unwrap();
4392        assert!(ruling_cone_sphere(&cone, &on_apex, true).is_none());
4393    }
4394
4395    #[test]
4396    fn a_ball_holding_a_cones_apex_meets_it_in_one_loop() {
4397        use crate::traits::ParametricCurve;
4398        let cone = ConicalSurface::new(
4399            Point3::new(0.0, 0.0, 3.0),
4400            Vec3::new(0.0, 0.0, -1.0),
4401            2.0_f64.atan(),
4402        )
4403        .unwrap();
4404        // Off the axis, and barely holding the apex: the generators pointing
4405        // away from the centre leave it just past the apex.
4406        for (centre, radius) in [
4407            (Point3::new(0.5, 0.0, 2.5), 2.0),
4408            (Point3::new(-0.4, 0.3, 2.0), 1.5),
4409            (Point3::new(0.0, 0.8, 3.0), 0.8001),
4410            (Point3::new(0.0, 0.8, 3.0), 0.800_001),
4411        ] {
4412            let ball = SphericalSurface::new(centre, radius).unwrap();
4413            let curves = ruling_cone_sphere(&cone, &ball, true).unwrap();
4414            assert_eq!(curves.len(), 1, "one loop for the ball at {centre:?}");
4415            let (t0, t1) = curves[0].curve.domain();
4416            for k in 0..=4096 {
4417                let p = ParametricCurve::evaluate(
4418                    &curves[0].curve,
4419                    (t1 - t0).mul_add(f64::from(k) / 4096.0, t0),
4420                );
4421                let on_ball = (p - centre).length() - radius;
4422                let on_cone = p.x().hypot(p.y()) - (3.0 - p.z()) / 2.0;
4423                assert!(
4424                    on_ball.abs() < 1e-5 && on_cone.abs() < 1e-5 && p.z() < 3.0,
4425                    "ball at {centre:?}: off by {on_ball}, {on_cone} at {p:?}"
4426                );
4427            }
4428        }
4429    }
4430
4431    #[test]
4432    fn oblique_cone_cylinder_defers_where_rulings_cannot_trace_it() {
4433        let t = 2.0_f64.atan();
4434        let cone =
4435            ConicalSurface::new(Point3::new(0.0, 0.0, 3.0), Vec3::new(0.0, 0.0, -1.0), t).unwrap();
4436        // A rod through the apex meets the far nappe.
4437        let through_apex =
4438            CylindricalSurface::new(Point3::new(0.0, 0.0, 3.0), Vec3::new(0.0, 1.0, 0.0), 0.6)
4439                .unwrap();
4440        assert!(ruling_cone_cylinder(&cone, &through_apex, true).is_none());
4441        // A rod along a generator meets each ruling once.
4442        let generator = Vec3::new(t.cos(), 0.0, -t.sin());
4443        let along = CylindricalSurface::new(Point3::new(0.0, 0.3, 0.0), generator, 0.2).unwrap();
4444        assert!(ruling_cone_cylinder(&cone, &along, true).is_none());
4445        // A pin's tip just through a tube's wall: the tube's rulings that
4446        // meet it span a window narrower than the sampling.
4447        let pin =
4448            ConicalSurface::new(Point3::new(20.5, 0.0, 0.0), Vec3::new(-1.0, 0.0, 0.0), t).unwrap();
4449        let tube =
4450            CylindricalSurface::new(Point3::new(0.0, 0.0, -10.0), Vec3::new(0.0, 0.0, 1.0), 20.0)
4451                .unwrap();
4452        assert!(ruling_cone_cylinder(&pin, &tube, true).is_none());
4453    }
4454
4455    #[test]
4456    fn parallel_cone_cylinder_gives_two_exact_branches() {
4457        use crate::traits::ParametricCurve;
4458        let cone = ConicalSurface::new(
4459            Point3::new(-5.45, -36.55, -4.85),
4460            Vec3::new(0.0, 0.0, 1.0),
4461            std::f64::consts::FRAC_PI_4,
4462        )
4463        .unwrap();
4464        let cyl = CylindricalSurface::new(
4465            Point3::new(-8.0, -34.0, -5.0),
4466            Vec3::new(0.0, 0.0, 1.0),
4467            4.45,
4468        )
4469        .unwrap();
4470        // The cone face spans z in [-3.8, -3.0]; v = (z - apex_z) / sin(45 deg).
4471        let v_hint = (1.484_924_240_492_058, 2.616_295_090_390_43);
4472        let curves = intersect_analytic_analytic_bounded(
4473            AnalyticSurface::Cone(&cone),
4474            AnalyticSurface::Cylinder(&cyl),
4475            32,
4476            Some(v_hint),
4477            Some((0.0, 2.5)),
4478        )
4479        .unwrap();
4480
4481        assert_eq!(curves.len(), 2, "expected exactly the two branches");
4482        for c in &curves {
4483            let (t0, t1) = c.curve.domain();
4484            for k in 0..=32 {
4485                let t = (t1 - t0).mul_add(f64::from(k) / 32.0, t0);
4486                let p = ParametricCurve::evaluate(&c.curve, t);
4487                // On the cylinder: radial distance from its axis is the radius.
4488                let radial = ((p.x() + 8.0).powi(2) + (p.y() + 34.0).powi(2)).sqrt();
4489                assert!((radial - 4.45).abs() < 1e-6, "off cylinder: {radial}");
4490                // On the cone: radial distance from its axis is z - apex_z.
4491                let cone_r = ((p.x() + 5.45).powi(2) + (p.y() + 36.55).powi(2)).sqrt();
4492                assert!((cone_r - (p.z() + 4.85)).abs() < 1e-6, "off cone at {p:?}");
4493                // Inside the cone face's own v-window (the hint is respected).
4494                assert!(p.z() >= -3.8 - 1e-9 && p.z() <= -3.0 + 1e-9, "z={}", p.z());
4495            }
4496        }
4497    }
4498
4499    #[test]
4500    fn parallel_rod_through_a_cones_wall_closes_one_loop() {
4501        use crate::traits::ParametricCurve;
4502        // Apex 3 up, opening downward, the radius half the depth below it.
4503        let cone = ConicalSurface::new(
4504            Point3::new(0.0, 0.0, 3.0),
4505            Vec3::new(0.0, 0.0, -1.0),
4506            2.0_f64.atan(),
4507        )
4508        .unwrap();
4509        // Beside the axis: each ruling meets the nappe once.
4510        for (x, y) in [(0.0, 1.3), (1.2, 0.5)] {
4511            let rod =
4512                CylindricalSurface::new(Point3::new(x, y, -10.0), Vec3::new(0.0, 0.0, 1.0), 0.6)
4513                    .unwrap();
4514            let curves = algebraic_parallel_cone_cylinder(&cone, &rod, None, None)
4515                .unwrap()
4516                .unwrap();
4517            assert_eq!(curves.len(), 1, "one closed loop at ({x}, {y})");
4518            let (t0, t1) = curves[0].curve.domain();
4519            let (first, last) = (
4520                ParametricCurve::evaluate(&curves[0].curve, t0),
4521                ParametricCurve::evaluate(&curves[0].curve, t1),
4522            );
4523            assert!((first - last).length() < 1e-9, "open at ({x}, {y})");
4524            for k in 0..=64 {
4525                let p = ParametricCurve::evaluate(
4526                    &curves[0].curve,
4527                    (t1 - t0).mul_add(f64::from(k) / 64.0, t0),
4528                );
4529                let on_rod = (p.x() - x).hypot(p.y() - y) - 0.6;
4530                let on_cone = p.x().hypot(p.y()) - (3.0 - p.z()) / 2.0;
4531                assert!(
4532                    on_rod.abs() < 1e-5 && on_cone.abs() < 1e-5,
4533                    "({x}, {y}): off by {on_rod}, {on_cone}"
4534                );
4535            }
4536        }
4537        // Around the axis, and beside it by less than the bend the rulings
4538        // resolve: two branches.
4539        for (x, y) in [(0.3, 0.2), (0.65, 0.0)] {
4540            let rod =
4541                CylindricalSurface::new(Point3::new(x, y, -10.0), Vec3::new(0.0, 0.0, 1.0), 0.6)
4542                    .unwrap();
4543            let curves = algebraic_parallel_cone_cylinder(&cone, &rod, None, None)
4544                .unwrap()
4545                .unwrap();
4546            assert_eq!(curves.len(), 2, "two branches at ({x}, {y})");
4547        }
4548    }
4549
4550    /// A coaxial pair has no radical line; the algebraic path must defer rather
4551    /// than divide by a zero axis separation.
4552    #[test]
4553    fn coaxial_cone_cylinder_defers_to_other_paths() {
4554        let cone = ConicalSurface::new(
4555            Point3::new(0.0, 0.0, 0.0),
4556            Vec3::new(0.0, 0.0, 1.0),
4557            std::f64::consts::FRAC_PI_4,
4558        )
4559        .unwrap();
4560        let cyl =
4561            CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 2.0)
4562                .unwrap();
4563        assert!(
4564            algebraic_parallel_cone_cylinder(&cone, &cyl, None, None)
4565                .unwrap()
4566                .is_none()
4567        );
4568    }
4569
4570    #[test]
4571    fn oblique_cone_cylinder_defers_to_other_paths() {
4572        let cone = ConicalSurface::new(
4573            Point3::new(0.0, 0.0, 0.0),
4574            Vec3::new(0.0, 0.0, 1.0),
4575            std::f64::consts::FRAC_PI_4,
4576        )
4577        .unwrap();
4578        let cyl =
4579            CylindricalSurface::new(Point3::new(3.0, 0.0, 1.0), Vec3::new(1.0, 0.0, 0.0), 1.0)
4580                .unwrap();
4581        assert!(
4582            algebraic_parallel_cone_cylinder(&cone, &cyl, None, None)
4583                .unwrap()
4584                .is_none()
4585        );
4586    }
4587
4588    #[test]
4589    fn plane_torus_lobe_closes_and_stays_on_surface() {
4590        use crate::traits::ParametricCurve;
4591        let (major, minor) = (10.0, 3.0);
4592        let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), major, minor).unwrap();
4593
4594        // The census cutting planes (y=-4, x=6) each cut the +x and -x tube lobes
4595        // in a CLOSED oval. The greedy marcher stops one grid step short of
4596        // closing; the wrap-close must make every fitted lobe close exactly.
4597        for (n, d) in [
4598            (Vec3::new(0.0, -1.0, 0.0), 4.0),  // y = -4
4599            (Vec3::new(-1.0, 0.0, 0.0), -6.0), // x = 6
4600            (Vec3::new(0.0, 0.0, 1.0), 0.0),   // z = 0 -> two concentric circles
4601        ] {
4602            let curves = intersect_plane_torus(&torus, n, d).unwrap();
4603            assert!(!curves.is_empty(), "plane n={n:?} d={d} found no curves");
4604            for c in &curves {
4605                let p0 = ParametricCurve::evaluate(&c.curve, 0.0);
4606                let p1 = ParametricCurve::evaluate(&c.curve, 1.0);
4607                assert!(
4608                    (p0 - p1).length() < 1e-7,
4609                    "lobe not closed: gap={} (n={n:?} d={d})",
4610                    (p0 - p1).length()
4611                );
4612                // Every fitted sample stays on the torus (shape-preserving).
4613                for k in 0..=64 {
4614                    let t = f64::from(k) / 64.0;
4615                    let p = ParametricCurve::evaluate(&c.curve, t);
4616                    assert!(
4617                        torus_implicit(p, major, minor).abs() < 1e-2,
4618                        "off-surface point {p:?} implicit={}",
4619                        torus_implicit(p, major, minor)
4620                    );
4621                }
4622            }
4623        }
4624    }
4625
4626    #[test]
4627    fn plane_torus_inner_tangent_figure_eight_stays_open() {
4628        use crate::traits::ParametricCurve;
4629        let (major, minor) = (10.0, 3.0);
4630        let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), major, minor).unwrap();
4631
4632        // A plane tangent to the inner equator (x = major - minor = 7) cuts a
4633        // self-touching figure-eight: its two branches touch at the node on
4634        // the inner equator. It must stay OPEN so a self-touching curve is
4635        // never sealed into a simple loop.
4636        let curves =
4637            intersect_plane_torus(&torus, Vec3::new(-1.0, 0.0, 0.0), -(major - minor)).unwrap();
4638        assert!(!curves.is_empty(), "inner-tangent plane found no curves");
4639        let max_gap = curves
4640            .iter()
4641            .map(|c| {
4642                let p0 = ParametricCurve::evaluate(&c.curve, 0.0);
4643                let p1 = ParametricCurve::evaluate(&c.curve, 1.0);
4644                (p0 - p1).length()
4645            })
4646            .fold(0.0_f64, f64::max);
4647        assert!(
4648            max_gap > 1e-2,
4649            "figure-eight chain was wrongly force-closed (max end-gap={max_gap})"
4650        );
4651    }
4652
4653    /// A wall parallel to the axis cuts one loop from the ring past the
4654    /// inner equator (its two turns where the branches meet), and two loops
4655    /// winding the tube inside it; each closes, at any scale, however near
4656    /// the wall runs to an equator.
4657    #[test]
4658    fn plane_torus_wall_sections_close_into_their_loops() {
4659        for (major, minor) in [(4.0, 1.5), (100.0, 30.0), (0.05, 0.01)] {
4660            let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), major, minor).unwrap();
4661            for k in 1..200 {
4662                let (d, want) = match k.cmp(&100) {
4663                    // Within the inner equator: two loops winding the tube.
4664                    std::cmp::Ordering::Less => ((major - minor) * f64::from(k) / 100.0, 2),
4665                    // Past it, short of the outer equator: one loop.
4666                    std::cmp::Ordering::Greater => (
4667                        2.0f64.mul_add(minor * f64::from(k - 100) / 100.0, major - minor),
4668                        1,
4669                    ),
4670                    std::cmp::Ordering::Equal => continue,
4671                };
4672                let loops = plane_torus_loops(&torus, Vec3::new(1.0, 0.0, 0.0), d, 128);
4673                let closed = loops
4674                    .iter()
4675                    .filter(|l| (l[0].point - l[l.len() - 1].point).length() < 1e-12)
4676                    .count();
4677                assert_eq!(
4678                    (loops.len(), closed),
4679                    (want, want),
4680                    "R {major} r {minor}, wall at {d}"
4681                );
4682            }
4683        }
4684    }
4685
4686    /// A plane through the centre tilted a little from the equator cuts two
4687    /// loops that each run all the way round the axis on a short run of `v`;
4688    /// their fitted curves stay on the torus.
4689    #[test]
4690    fn plane_torus_sections_round_the_axis_stay_on_the_torus() {
4691        let (major, minor) = (4.0, 1.5);
4692        let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), major, minor).unwrap();
4693        for tilt in [0.03_f64, 0.08, 0.2] {
4694            let normal = Vec3::new(tilt.sin(), 0.0, tilt.cos());
4695            let curves = intersect_plane_torus(&torus, normal, 0.0).unwrap();
4696            assert_eq!(curves.len(), 2, "tilt {tilt}");
4697            for c in &curves {
4698                let (t0, t1) = c.curve.domain();
4699                let off = (0..=400)
4700                    .map(|k| {
4701                        let p = c
4702                            .curve
4703                            .evaluate((t1 - t0).mul_add(f64::from(k) / 400.0, t0));
4704                        (p.x().hypot(p.y()) - major).hypot(p.z()) - minor
4705                    })
4706                    .fold(0.0_f64, |m, e| m.max(e.abs()));
4707                assert!(
4708                    off < 1e-6,
4709                    "tilt {tilt}: fitted section {off} off the torus"
4710                );
4711            }
4712        }
4713    }
4714
4715    #[test]
4716    fn line_torus_box_edge_crossing_is_exact() {
4717        // The census box edge x=6, y=-4 (z varying) crosses the torus (R=10,r=3)
4718        // at z = ±sqrt(r² − (rho−R)²), rho = hypot(6,4) ≈ 7.2111 → z ≈ ±1.1055.
4719        let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 10.0, 3.0).unwrap();
4720        let ts = intersect_line_torus(
4721            &torus,
4722            Point3::new(6.0, -4.0, -5.0),
4723            Vec3::new(0.0, 0.0, 1.0),
4724        );
4725        // Vertical line through (6,-4) meets the tube twice.
4726        assert_eq!(ts.len(), 2, "expected 2 crossings, got {ts:?}");
4727        let zs: Vec<f64> = ts.iter().map(|t| -5.0 + t).collect();
4728        let rho = 6.0_f64.hypot(4.0);
4729        let z_exp = (9.0 - (rho - 10.0).powi(2)).sqrt();
4730        assert!(
4731            (zs[0] - (-z_exp)).abs() < 1e-9,
4732            "z0={} exp={}",
4733            zs[0],
4734            -z_exp
4735        );
4736        assert!((zs[1] - z_exp).abs() < 1e-9, "z1={} exp={}", zs[1], z_exp);
4737        // Each crossing lies on the torus.
4738        for &t in &ts {
4739            let p = Point3::new(6.0, -4.0, -5.0 + t);
4740            let rho = p.x().hypot(p.y());
4741            let impl_v = (rho - 10.0).hypot(p.z()) - 3.0;
4742            assert!(impl_v.abs() < 1e-9, "off-torus impl={impl_v}");
4743        }
4744    }
4745
4746    /// A ray out of a pocket corner's spindle torus (major 0.1, minor 2.45)
4747    /// meets the tube swung across the axis at t 0.124 and its own tube at
4748    /// 0.398: each root lies on one of the two tubes, not polished off the
4749    /// first onto a point beside the second.
4750    #[test]
4751    fn line_spindle_torus_roots_lie_on_their_tubes() {
4752        let torus = ToroidalSurface::new(Point3::new(41.0, 17.0, 4.7), 0.1, 2.45).unwrap();
4753        let origin = Point3::new(42.36, 18.31, 3.4);
4754        let dir = Vec3::new(
4755            0.573_576_436_351_046,
4756            0.740_535_693_464_567_5,
4757            0.350_889_803_483_932_2,
4758        );
4759        let ts = intersect_line_torus(&torus, origin, dir);
4760        let tube = |t: f64, across: f64| {
4761            let q = origin + dir * t;
4762            ((q.x() - 41.0).hypot(q.y() - 17.0) - across * 0.1).hypot(q.z() - 4.7) - 2.45
4763        };
4764        for &t in &ts {
4765            assert!(
4766                tube(t, 1.0).abs().min(tube(t, -1.0).abs()) < 1e-9,
4767                "root {t} off both tubes in {ts:?}"
4768            );
4769        }
4770        for want in [0.124, 0.398] {
4771            assert!(
4772                ts.iter().any(|&t| (t - want).abs() < 1e-3),
4773                "no root near {want} in {ts:?}"
4774            );
4775        }
4776    }
4777
4778    #[test]
4779    fn line_torus_miss_and_tangent() {
4780        let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 10.0, 3.0).unwrap();
4781        // A vertical line at rho beyond the outer rim (x=20) misses entirely.
4782        let miss = intersect_line_torus(
4783            &torus,
4784            Point3::new(20.0, 0.0, 0.0),
4785            Vec3::new(0.0, 0.0, 1.0),
4786        );
4787        assert!(miss.is_empty(), "expected no crossings, got {miss:?}");
4788        // The z-axis (rho=0) passes through the hole — no intersection.
4789        let axis =
4790            intersect_line_torus(&torus, Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0));
4791        assert!(axis.is_empty(), "z-axis should miss the tube, got {axis:?}");
4792    }
4793
4794    #[test]
4795    fn dispatch_via_analytic_surface() {
4796        let cyl =
4797            CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
4798                .unwrap();
4799        let curves = intersect_plane_analytic(
4800            AnalyticSurface::Cylinder(&cyl),
4801            Vec3::new(0.0, 0.0, 1.0),
4802            0.0,
4803        )
4804        .unwrap();
4805        assert!(!curves.is_empty());
4806    }
4807
4808    #[test]
4809    fn perpendicular_cylinders_intersect() {
4810        let cyl_z =
4811            CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
4812                .unwrap();
4813        let cyl_x =
4814            CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0), 1.0)
4815                .unwrap();
4816
4817        let curves = intersect_analytic_analytic(
4818            AnalyticSurface::Cylinder(&cyl_z),
4819            AnalyticSurface::Cylinder(&cyl_x),
4820            16,
4821        )
4822        .unwrap();
4823
4824        assert!(
4825            !curves.is_empty(),
4826            "perpendicular cylinders should intersect"
4827        );
4828
4829        for c in &curves {
4830            assert!(
4831                c.points.len() >= 2,
4832                "intersection curve should have >= 2 points, got {}",
4833                c.points.len()
4834            );
4835        }
4836    }
4837
4838    /// Neither cylinder's rulings all meet the other: the curve is one loop
4839    /// joined at its two branch points.
4840    #[test]
4841    fn partially_overlapping_cylinders_meet_in_one_closed_loop() {
4842        let cyl_z =
4843            CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
4844                .unwrap();
4845        let cyl_x =
4846            CylindricalSurface::new(Point3::new(0.0, 1.2, 0.0), Vec3::new(1.0, 0.0, 0.0), 1.0)
4847                .unwrap();
4848        let curves = algebraic_cylinder_cylinder(&cyl_z, &cyl_x)
4849            .unwrap()
4850            .unwrap();
4851        assert_eq!(curves.len(), 1);
4852        let curve = &curves[0].curve;
4853        let (t0, t1) = curve.domain();
4854        assert!((curve.evaluate(t0) - curve.evaluate(t1)).length() < 1e-9);
4855        let off = |p: Point3| {
4856            let on_z = (p.x().hypot(p.y()) - 1.0).abs();
4857            let on_x = ((p.y() - 1.2).hypot(p.z()) - 1.0).abs();
4858            on_z.max(on_x)
4859        };
4860        let worst = (0..=400)
4861            .map(|k| off(curve.evaluate(t0 + (t1 - t0) * f64::from(k) / 400.0)))
4862            .fold(0.0, f64::max);
4863        assert!(worst < 2e-4, "curve leaves the cylinders by {worst}");
4864    }
4865
4866    /// Near tangency the thick cylinder's window of rulings (0.02 either side
4867    /// of a quarter turn) falls between its samples; the thin one's sweep
4868    /// finds the loop.
4869    #[test]
4870    fn near_tangent_cylinders_find_their_loop_on_the_thinner_sweep() {
4871        let cyl_z =
4872            CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
4873                .unwrap();
4874        let cyl_x =
4875            CylindricalSurface::new(Point3::new(0.0, 1.1998, 0.0), Vec3::new(1.0, 0.0, 0.0), 0.2)
4876                .unwrap();
4877        let curves = algebraic_cylinder_cylinder(&cyl_z, &cyl_x)
4878            .unwrap()
4879            .expect("the thin cylinder's sweep finds the loop");
4880        assert_eq!(curves.len(), 1);
4881    }
4882
4883    #[test]
4884    fn sphere_cylinder_intersect() {
4885        let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 2.0).unwrap();
4886        let cyl =
4887            CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
4888                .unwrap();
4889
4890        let curves = intersect_analytic_analytic(
4891            AnalyticSurface::Sphere(&sphere),
4892            AnalyticSurface::Cylinder(&cyl),
4893            16,
4894        )
4895        .unwrap();
4896
4897        // A sphere of radius 2 and a cylinder of radius 1, both centered
4898        // at the origin, should intersect (the cylinder passes through
4899        // the sphere).
4900        assert!(!curves.is_empty(), "sphere and cylinder should intersect");
4901    }
4902
4903    #[test]
4904    fn exact_sphere_cylinder_coaxial_two_circles() {
4905        // Sphere r=6 at origin, coaxial cylinder r=3 along z: two latitude
4906        // circles at z = ±sqrt(36-9) = ±sqrt(27), each of radius 3.
4907        let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 6.0).unwrap();
4908        let cyl =
4909            CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 3.0)
4910                .unwrap();
4911        let circles = exact_sphere_cylinder(&sphere, &cyl)
4912            .unwrap()
4913            .expect("coaxial case returns Some");
4914        assert_eq!(circles.len(), 2, "through-bore meets the sphere twice");
4915        let mut zs: Vec<f64> = circles
4916            .iter()
4917            .filter_map(|c| match c {
4918                ExactIntersectionCurve::Circle(circle) => {
4919                    assert!(
4920                        (circle.radius() - 3.0).abs() < 1e-9,
4921                        "rim radius == cyl radius"
4922                    );
4923                    Some(circle.center().z())
4924                }
4925                _ => None,
4926            })
4927            .collect();
4928        assert_eq!(zs.len(), 2, "both sections must be exact circles");
4929        zs.sort_by(f64::total_cmp);
4930        let z = 27.0_f64.sqrt();
4931        assert!((zs[0] + z).abs() < 1e-9 && (zs[1] - z).abs() < 1e-9);
4932    }
4933
4934    #[test]
4935    fn exact_sphere_cylinder_non_coaxial_defers() {
4936        // Cylinder axis offset from the sphere center → quartic curve, deferred.
4937        let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 6.0).unwrap();
4938        let cyl =
4939            CylindricalSurface::new(Point3::new(2.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 3.0)
4940                .unwrap();
4941        assert!(
4942            exact_sphere_cylinder(&sphere, &cyl).unwrap().is_none(),
4943            "non-coaxial sphere/cylinder defers to the marcher"
4944        );
4945    }
4946
4947    #[test]
4948    fn a_ball_on_a_cones_axis_meets_it_in_circles() {
4949        // Apex 3 up, opening downward, the radius half the depth below it.
4950        let cone = ConicalSurface::new(
4951            Point3::new(0.0, 0.0, 3.0),
4952            Vec3::new(0.0, 0.0, -1.0),
4953            2.0_f64.atan(),
4954        )
4955        .unwrap();
4956        for (height, radius, count) in [
4957            (0.0, 2.0, 2), // through the cone's middle
4958            (2.5, 1.3, 1), // swallowing the apex
4959            (2.5, 0.5, 1), // through the apex: the apex root is no circle
4960            (0.0, 1.0, 0), // inside, clear of the wall
4961            (5.0, 1.0, 0), // on the other nappe's side
4962        ] {
4963            let centre = Point3::new(0.0, 0.0, height);
4964            let ball = SphericalSurface::new(centre, radius).unwrap();
4965            let curves = exact_cone_sphere(&cone, &ball).unwrap().unwrap();
4966            let circles = circles_of(&curves);
4967            assert_eq!(circles.len(), count, "ball at {height}, radius {radius}");
4968            for circle in circles {
4969                for k in 0..16 {
4970                    let p = circle.evaluate(TAU * f64::from(k) / 16.0);
4971                    let on_ball = (p - centre).length() - radius;
4972                    let on_cone = p.x().hypot(p.y()) - (3.0 - p.z()) / 2.0;
4973                    assert!(
4974                        on_ball.abs() < 1e-9 && on_cone.abs() < 1e-9,
4975                        "ball at {height}: off by {on_ball}, {on_cone}"
4976                    );
4977                }
4978            }
4979        }
4980        let aside = SphericalSurface::new(Point3::new(0.5, 0.0, 0.0), 2.0).unwrap();
4981        assert!(exact_cone_sphere(&cone, &aside).unwrap().is_none());
4982        // A ball touching a wide cone a million units out, where the
4983        // discriminant cancels to a few ulps below zero.
4984        let wide =
4985            ConicalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 0.3).unwrap();
4986        let far = SphericalSurface::new(Point3::new(0.0, 0.0, 1e6), 1e6 * 0.3_f64.cos()).unwrap();
4987        let curves = exact_cone_sphere(&wide, &far).unwrap().unwrap();
4988        let circles = circles_of(&curves);
4989        assert_eq!(circles.len(), 1, "the touch");
4990        let touch = 1e6 * 0.3_f64.sin() * 0.3_f64.cos();
4991        assert!(
4992            (circles[0].radius() - touch).abs() < 1e-3,
4993            "{}",
4994            circles[0].radius()
4995        );
4996    }
4997
4998    /// The circles among exact section curves.
4999    fn circles_of(curves: &[ExactIntersectionCurve]) -> Vec<&Circle3D> {
5000        curves
5001            .iter()
5002            .filter_map(|c| match c {
5003                ExactIntersectionCurve::Circle(circle) => Some(circle),
5004                _ => None,
5005            })
5006            .collect()
5007    }
5008
5009    /// Worst distance of a circle's points from a torus and from a second
5010    /// surface given by its own distance function.
5011    fn worst_off(
5012        circles: &[&Circle3D],
5013        torus: &ToroidalSurface,
5014        other: impl Fn(Point3) -> f64,
5015    ) -> f64 {
5016        let mut worst = 0.0_f64;
5017        for circle in circles {
5018            for k in 0..16 {
5019                let p = circle.evaluate(TAU * f64::from(k) / 16.0);
5020                let q = p - torus.center();
5021                let along = q.dot(torus.z_axis());
5022                let rho = (q - torus.z_axis() * along).length();
5023                let off = ((rho - torus.major_radius()).hypot(along) - torus.minor_radius()).abs();
5024                worst = worst.max(off).max(other(p).abs());
5025            }
5026        }
5027        worst
5028    }
5029
5030    #[test]
5031    fn exact_sphere_torus_meets_a_ball_on_the_axis_in_circles() {
5032        let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 4.0, 1.5).unwrap();
5033        for height in [0.0, 1.0] {
5034            let centre = Point3::new(0.0, 0.0, height);
5035            let sphere = SphericalSurface::new(centre, 3.0).unwrap();
5036            let curves = exact_sphere_torus(&sphere, &torus).unwrap().unwrap();
5037            let circles = circles_of(&curves);
5038            assert_eq!((curves.len(), circles.len()), (2, 2), "height {height}");
5039            let worst = worst_off(&circles, &torus, |p| (p - centre).length() - 3.0);
5040            assert!(worst < 1e-9, "height {height}: {worst}");
5041        }
5042    }
5043
5044    #[test]
5045    fn exact_sphere_torus_misses_touches_and_defers() {
5046        let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 4.0, 1.5).unwrap();
5047        let ball = |x: f64, r: f64| SphericalSurface::new(Point3::new(x, 0.0, 0.0), r).unwrap();
5048        assert!(
5049            exact_sphere_torus(&ball(0.0, 1.0), &torus)
5050                .unwrap()
5051                .unwrap()
5052                .is_empty(),
5053            "a small ball in the hole misses"
5054        );
5055        assert!(
5056            exact_sphere_torus(&ball(0.0, 2.5), &torus)
5057                .unwrap()
5058                .is_none(),
5059            "a ball touching the inner equator defers"
5060        );
5061        assert!(
5062            exact_sphere_torus(&ball(1.0, 3.0), &torus)
5063                .unwrap()
5064                .is_none(),
5065            "a ball off the axis defers"
5066        );
5067        let spindle = ToroidalSurface::with_axis_and_ref_dir(
5068            Point3::new(0.0, 0.0, 0.0),
5069            1.0,
5070            2.0,
5071            Vec3::new(0.0, 0.0, 1.0),
5072            Vec3::new(1.0, 0.0, 0.0),
5073        )
5074        .unwrap();
5075        assert!(
5076            exact_sphere_torus(&ball(0.0, 2.5), &spindle)
5077                .unwrap()
5078                .is_none()
5079        );
5080    }
5081
5082    #[test]
5083    fn exact_cylinder_torus_meets_a_coaxial_rod_in_circles() {
5084        let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 4.0, 1.5).unwrap();
5085        let z = Vec3::new(0.0, 0.0, 1.0);
5086        let rod = |r: f64| CylindricalSurface::new(Point3::new(0.0, 0.0, -5.0), z, r).unwrap();
5087        let curves = exact_cylinder_torus(&rod(4.2), &torus).unwrap().unwrap();
5088        let circles = circles_of(&curves);
5089        assert_eq!((curves.len(), circles.len()), (2, 2));
5090        let worst = worst_off(&circles, &torus, |p| p.x().hypot(p.y()) - 4.2);
5091        assert!(worst < 1e-9, "{worst}");
5092        assert!(
5093            exact_cylinder_torus(&rod(2.0), &torus)
5094                .unwrap()
5095                .unwrap()
5096                .is_empty(),
5097            "a rod clear in the hole misses"
5098        );
5099        for (wall, label) in [(5.5, "outer"), (2.5, "inner")] {
5100            let curves = exact_cylinder_torus(&rod(wall), &torus).unwrap().unwrap();
5101            let circles = circles_of(&curves);
5102            assert_eq!(circles.len(), 1, "a wall touching the {label} equator");
5103            assert!(circles[0].center().z().abs() < 1e-12, "{label}");
5104            let worst = worst_off(&circles, &torus, |p| p.x().hypot(p.y()) - wall);
5105            assert!(worst < 1e-9, "{label}: {worst}");
5106        }
5107        assert!(
5108            exact_cylinder_torus(&rod(5.5 + 1e-6), &torus)
5109                .unwrap()
5110                .unwrap()
5111                .is_empty(),
5112            "a wall clear of the tube by more than the tolerance misses it"
5113        );
5114        assert_eq!(
5115            exact_cylinder_torus(&rod(5.5 - 1e-6), &torus)
5116                .unwrap()
5117                .unwrap()
5118                .len(),
5119            2,
5120            "a wall into the tube by more than the tolerance crosses it twice"
5121        );
5122        // Within the linear tolerance of the tube the wall reads as touching
5123        // it: the two crossings lie under 1e-3 apart on a band the wall
5124        // never leaves by more than that tolerance.
5125        for wall in [5.5 - 5e-8, 5.5 + 5e-8] {
5126            let curves = exact_cylinder_torus(&rod(wall), &torus).unwrap().unwrap();
5127            assert_eq!(circles_of(&curves).len(), 1, "a wall at {wall}");
5128        }
5129        let tilted =
5130            CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.1, 1.0), 4.2)
5131                .unwrap();
5132        let offset = CylindricalSurface::new(Point3::new(0.5, 0.0, 0.0), z, 4.2).unwrap();
5133        assert!(exact_cylinder_torus(&tilted, &torus).unwrap().is_none());
5134        assert!(exact_cylinder_torus(&offset, &torus).unwrap().is_none());
5135        let spindle = ToroidalSurface::with_axis_and_ref_dir(
5136            Point3::new(0.0, 0.0, 0.0),
5137            1.0,
5138            2.0,
5139            z,
5140            Vec3::new(1.0, 0.0, 0.0),
5141        )
5142        .unwrap();
5143        assert!(
5144            exact_cylinder_torus(&rod(0.5), &spindle).unwrap().is_none(),
5145            "a spindle torus's inner lemon also meets the rod"
5146        );
5147        // The rounded air wall of a fillet whose radius nearly fills its
5148        // corner: the spindle's outer equator rests on the wall.
5149        let spindle = ToroidalSurface::with_axis_and_ref_dir(
5150            Point3::new(0.0, 0.0, 4.7),
5151            0.1,
5152            2.45,
5153            z,
5154            Vec3::new(1.0, 0.0, 0.0),
5155        )
5156        .unwrap();
5157        let curves = exact_cylinder_torus(&rod(2.55), &spindle).unwrap().unwrap();
5158        let circles = circles_of(&curves);
5159        assert_eq!(circles.len(), 1);
5160        assert!((circles[0].center().z() - 4.7).abs() < 1e-12);
5161        let worst = worst_off(&circles, &spindle, |p| p.x().hypot(p.y()) - 2.55);
5162        assert!(worst < 1e-9, "{worst}");
5163        let curves = exact_cylinder_torus(&rod(2.4), &spindle).unwrap().unwrap();
5164        let circles = circles_of(&curves);
5165        assert_eq!(circles.len(), 2, "a wall into the spindle's outer tube");
5166        let worst = worst_off(&circles, &spindle, |p| p.x().hypot(p.y()) - 2.4);
5167        assert!(worst < 1e-9, "{worst}");
5168    }
5169
5170    /// Loops of an off-axis sphere-cylinder pair: `(count, worst distance
5171    /// from either surface)`.
5172    fn off_axis_loops(cylinder_origin: Point3, cylinder_radius: f64) -> (usize, f64) {
5173        let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 2.0).unwrap();
5174        let cyl =
5175            CylindricalSurface::new(cylinder_origin, Vec3::new(0.0, 0.0, 1.0), cylinder_radius)
5176                .unwrap();
5177        let curves = algebraic_sphere_cylinder(&sphere, &cyl, true)
5178            .unwrap()
5179            .unwrap();
5180        let mut worst: f64 = 0.0;
5181        for c in &curves {
5182            for ip in &c.points {
5183                let on_sphere = sphere.evaluate(ip.param1.0, ip.param1.1);
5184                let on_cylinder = cyl.evaluate(ip.param2.0, ip.param2.1);
5185                worst = worst
5186                    .max((on_sphere - ip.point).length())
5187                    .max((on_cylinder - ip.point).length());
5188            }
5189            let (t0, t1) = c.curve.domain();
5190            assert!((c.curve.evaluate(t0) - c.curve.evaluate(t1)).length() < 1e-9);
5191            for k in 0..=400 {
5192                let p = c.curve.evaluate(t0 + (t1 - t0) * f64::from(k) / 400.0);
5193                let on_sphere = ((p - Point3::new(0.0, 0.0, 0.0)).length() - 2.0).abs();
5194                let on_cylinder = ((p.x() - cylinder_origin.x())
5195                    .hypot(p.y() - cylinder_origin.y())
5196                    - cylinder_radius)
5197                    .abs();
5198                worst = worst.max(on_sphere).max(on_cylinder);
5199            }
5200        }
5201        (curves.len(), worst)
5202    }
5203
5204    /// A drill off the ball's axis passes through it: an entry and an exit
5205    /// loop.
5206    #[test]
5207    fn off_axis_drill_through_a_sphere_meets_it_in_two_loops() {
5208        let (count, worst) = off_axis_loops(Point3::new(0.5, 0.0, 0.0), 0.2);
5209        assert_eq!(count, 2);
5210        assert!(worst < 1e-5, "loops leave the surfaces by {worst}");
5211    }
5212
5213    /// A cylinder over the ball's side: one loop joined at its branch points.
5214    #[test]
5215    fn cylinder_over_a_spheres_side_meets_it_in_one_loop() {
5216        let (count, worst) = off_axis_loops(Point3::new(1.8, 0.0, 0.0), 0.5);
5217        assert_eq!(count, 1);
5218        assert!(worst < 5e-4, "loop leaves the surfaces by {worst}");
5219    }
5220
5221    #[test]
5222    fn disjoint_cylinders_no_intersection() {
5223        let cyl_a =
5224            CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 0.5)
5225                .unwrap();
5226        let cyl_b =
5227            CylindricalSurface::new(Point3::new(5.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 0.5)
5228                .unwrap();
5229
5230        let curves = intersect_analytic_analytic(
5231            AnalyticSurface::Cylinder(&cyl_a),
5232            AnalyticSurface::Cylinder(&cyl_b),
5233            16,
5234        )
5235        .unwrap();
5236
5237        assert!(curves.is_empty(), "disjoint cylinders should not intersect");
5238    }
5239
5240    // ── Oblique plane × cone conic (ellipse / parabola / hyperbola) ──────
5241
5242    /// Collect 3D points from a returned exact curve, sampling analytic forms.
5243    fn collect_points(curve: &ExactIntersectionCurve) -> Vec<Point3> {
5244        use crate::traits::ParametricCurve;
5245        match curve {
5246            ExactIntersectionCurve::Circle(c) => (0..=64)
5247                .map(|i| ParametricCurve::evaluate(c, TAU * f64::from(i) / 64.0))
5248                .collect(),
5249            ExactIntersectionCurve::Ellipse(e) => (0..=64)
5250                .map(|i| ParametricCurve::evaluate(e, TAU * f64::from(i) / 64.0))
5251                .collect(),
5252            ExactIntersectionCurve::Points(pts) => pts.clone(),
5253        }
5254    }
5255
5256    /// Assert every returned point lies on the plane and the cone surface, on
5257    /// the real (`v >= 0`) nappe, and within a sane axial bound.
5258    fn assert_on_plane_and_cone(
5259        curves: &[ExactIntersectionCurve],
5260        cone: &ConicalSurface,
5261        n: Vec3,
5262        d: f64,
5263        z_bound: (f64, f64),
5264    ) {
5265        assert!(!curves.is_empty(), "expected at least one section curve");
5266        let mut total = 0;
5267        for curve in curves {
5268            for p in collect_points(curve) {
5269                total += 1;
5270                let plane_err = (n.x() * p.x() + n.y() * p.y() + n.z() * p.z() - d).abs();
5271                assert!(
5272                    plane_err < 1e-9,
5273                    "point off plane by {plane_err:.2e}: {p:?}"
5274                );
5275                let (u, v) = cone.project_point(p);
5276                let q = cone.evaluate(u, v);
5277                let cone_err =
5278                    ((p.x() - q.x()).powi(2) + (p.y() - q.y()).powi(2) + (p.z() - q.z()).powi(2))
5279                        .sqrt();
5280                assert!(cone_err < 1e-7, "point off cone by {cone_err:.2e}: {p:?}");
5281                assert!(v >= -1e-9, "point on phantom nappe (v={v:.4}): {p:?}");
5282                assert!(
5283                    p.z() >= z_bound.0 - 1e-6 && p.z() <= z_bound.1 + 1e-6,
5284                    "point z={:.4} outside sane bound {z_bound:?}: {p:?}",
5285                    p.z()
5286                );
5287            }
5288        }
5289        assert!(total >= 8, "too few section points ({total})");
5290    }
5291
5292    #[test]
5293    fn oblique_plane_cone_ellipse_is_exact_and_on_both() {
5294        // 45°-half-angle cone (axis +z). A plane tilted only ~16.7° off horizontal
5295        // has plane-axis angle ≈ 73° > 45° (the cone's half-opening from axis) →
5296        // ellipse. Must come back as an exact Ellipse, fully on both surfaces.
5297        let cone = ConicalSurface::new(
5298            Point3::new(0.0, 0.0, 0.0),
5299            Vec3::new(0.0, 0.0, 1.0),
5300            std::f64::consts::FRAC_PI_4,
5301        )
5302        .unwrap();
5303        let n = Vec3::new(0.3, 0.0, 1.0).normalize().unwrap();
5304        // Plane through (0,0,5): d = n·(0,0,5).
5305        let d = n.z() * 5.0;
5306        let curves = exact_plane_cone(&cone, n, d, 0.0).unwrap();
5307        assert!(
5308            curves
5309                .iter()
5310                .any(|c| matches!(c, ExactIntersectionCurve::Ellipse(_))),
5311            "oblique steep plane × cone must yield an exact Ellipse"
5312        );
5313        // The ellipse straddles z=5; with the 0.3 tilt the z-extent stays modest.
5314        assert_on_plane_and_cone(&curves, &cone, n, d, (0.0, 12.0));
5315    }
5316
5317    #[test]
5318    fn oblique_plane_cone_wrong_nappe_is_empty() {
5319        // Same ellipse-regime plane as above, but offset to the FAR side of the
5320        // apex (z=-5). The +z cone's real (v≥0) nappe is not met — only the
5321        // phantom v<0 nappe — so the result must be EMPTY, not a phantom ellipse.
5322        let cone = ConicalSurface::new(
5323            Point3::new(0.0, 0.0, 0.0),
5324            Vec3::new(0.0, 0.0, 1.0),
5325            std::f64::consts::FRAC_PI_4,
5326        )
5327        .unwrap();
5328        let n = Vec3::new(0.3, 0.0, 1.0).normalize().unwrap();
5329        let d = n.z() * -5.0;
5330        let curves = exact_plane_cone(&cone, n, d, 0.0).unwrap();
5331        assert!(
5332            curves.is_empty(),
5333            "plane on the phantom-nappe side must yield no real curve, got {}",
5334            curves.len()
5335        );
5336    }
5337
5338    #[test]
5339    fn oblique_plane_cone_parabola_on_both_single_branch() {
5340        // Plane normal at exactly 45° to the axis (= the cone half-opening) → the
5341        // plane is parallel to a generator → parabola. One unbounded branch.
5342        let cone = ConicalSurface::new(
5343            Point3::new(0.0, 0.0, 0.0),
5344            Vec3::new(0.0, 0.0, 1.0),
5345            std::f64::consts::FRAC_PI_4,
5346        )
5347        .unwrap();
5348        let n = Vec3::new(1.0, 0.0, 1.0).normalize().unwrap();
5349        let d = n.x() * 3.0 + n.z() * 3.0; // through (3,0,3)
5350        let curves = exact_plane_cone(&cone, n, d, 0.0).unwrap();
5351        assert_eq!(
5352            curves.len(),
5353            1,
5354            "a parabola is a single branch, got {}",
5355            curves.len()
5356        );
5357        // Bounded by r_max = 32·|e|; |e| here is O(few), so allow a wide z window.
5358        assert_on_plane_and_cone(&curves, &cone, n, d, (0.0, 400.0));
5359    }
5360
5361    #[test]
5362    fn oblique_plane_cone_hyperbola_real_nappe_only() {
5363        // Faithful scooplabel lip-foot geometry: a 45° cone with axis −z and
5364        // apex at (−59,−59,15.85) (a bin corner), cut by the upper ramp tread
5365        // plane n=(0,0.99518,0.09802), d=−58.36056. The plane is nearly parallel
5366        // to the axis (cos≈0.098) → plane-axis angle ≈ 5.6° < 45° → hyperbola.
5367        // The downward real nappe is hit by exactly one branch; the phantom
5368        // upward nappe (and the asymptote runaway) must NOT appear, and the arc
5369        // must stay near the apex (the plane is ~1.2 mm from it).
5370        let cone = ConicalSurface::new(
5371            Point3::new(-59.0, -59.0, 15.85),
5372            Vec3::new(0.0, 0.0, -1.0),
5373            std::f64::consts::FRAC_PI_4,
5374        )
5375        .unwrap();
5376        let n = Vec3::new(0.0, 0.995_18, 0.098_02).normalize().unwrap();
5377        let d = -58.360_56;
5378        let cos_theta = n.dot(cone.axis()).abs();
5379        assert!(cos_theta < 0.2, "expected a shallow (hyperbola) plane");
5380        let curves = exact_plane_cone(&cone, n, d, 0.0).unwrap();
5381        // Real downward nappe only: never above the apex (z=15.85). The vertex is
5382        // ~1.2 mm from the apex, so the bounded arc stays within a few mm of it.
5383        assert_on_plane_and_cone(&curves, &cone, n, d, (5.0, 15.85));
5384        // Every returned curve is sampled Points (no false Circle/Ellipse).
5385        for c in &curves {
5386            assert!(
5387                matches!(c, ExactIntersectionCurve::Points(_)),
5388                "hyperbola must be sampled Points, not a closed conic"
5389            );
5390        }
5391    }
5392}