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 one Newton
1266/// step against the implicit). `dir` need not be unit length; `t` is in units of
1267/// `dir`. Returns the roots sorted ascending (0–4 of them).
1268///
1269/// Used by the boolean section trimmer to find where a plane×torus oval exits a
1270/// box face's straight boundary edge — the exact crossing shared by the two
1271/// adjacent faces, which is what makes the notch watertight.
1272#[must_use]
1273pub fn intersect_line_torus(torus: &ToroidalSurface, origin: Point3, dir: Vec3) -> Vec<f64> {
1274    let c = torus.center();
1275    let (xa, ya, za) = (torus.x_axis(), torus.y_axis(), torus.z_axis());
1276    let big_r = torus.major_radius();
1277    let small_r = torus.minor_radius();
1278
1279    // Line point in torus frame: a(t)=a0+a1 t, b(t)=b0+b1 t, c(t)=c0+c1 t.
1280    let o = Vec3::new(origin.x() - c.x(), origin.y() - c.y(), origin.z() - c.z());
1281    let (a0, a1) = (xa.dot(o), xa.dot(dir));
1282    let (b0, b1) = (ya.dot(o), ya.dot(dir));
1283    let (c0, c1) = (za.dot(o), za.dot(dir));
1284
1285    // G(t) = a² + b² + c² + R² − r²  (quadratic: g2 t² + g1 t + g0)
1286    let g2 = a1.mul_add(a1, b1.mul_add(b1, c1 * c1));
1287    let g1 = 2.0 * a1.mul_add(a0, b1.mul_add(b0, c1 * c0));
1288    let g0 = a0.mul_add(
1289        a0,
1290        b0.mul_add(b0, c0.mul_add(c0, big_r.mul_add(big_r, -small_r * small_r))),
1291    );
1292
1293    // H(t) = 4R² (a² + b²)  (quadratic: h2 t² + h1 t + h0)
1294    let four_rr = 4.0 * big_r * big_r;
1295    let h2 = four_rr * a1.mul_add(a1, b1 * b1);
1296    let h1 = four_rr * (2.0 * a1.mul_add(a0, b1 * b0));
1297    let h0 = four_rr * a0.mul_add(a0, b0 * b0);
1298
1299    // Quartic G² − H = 0:  e4 t⁴ + e3 t³ + e2 t² + e1 t + e0.
1300    let e4 = g2 * g2;
1301    let e3 = 2.0 * g2 * g1;
1302    let e2 = g1.mul_add(g1, 2.0 * g2 * g0) - h2;
1303    let e1 = 2.0f64.mul_add(g1 * g0, -h1);
1304    let e0 = g0.mul_add(g0, -h0);
1305
1306    let mut roots = real_roots_quartic(e4, e3, e2, e1, e0);
1307    // One Newton polish against the torus implicit for full precision.
1308    let impl_f = |t: f64| -> f64 {
1309        let p = origin + dir * t;
1310        let q = Vec3::new(p.x() - c.x(), p.y() - c.y(), p.z() - c.z());
1311        let (a, b, cc) = (xa.dot(q), ya.dot(q), za.dot(q));
1312        (a.hypot(b) - big_r).hypot(cc) - small_r
1313    };
1314    for t in &mut roots {
1315        let eps = 1e-7;
1316        let f = impl_f(*t);
1317        let df = (impl_f(*t + eps) - impl_f(*t - eps)) / (2.0 * eps);
1318        if df.abs() > 1e-12 {
1319            *t -= f / df;
1320        }
1321    }
1322    roots.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1323    roots
1324}
1325
1326/// Real roots of `c4 x⁴ + c3 x³ + c2 x² + c1 x + c0` via Durand–Kerner, falling
1327/// back to the lower-degree solvers when the leading coefficients vanish.
1328fn real_roots_quartic(c4: f64, c3: f64, c2: f64, c1: f64, c0: f64) -> Vec<f64> {
1329    // Degenerate leading coefficient → lower degree.
1330    if c4.abs() < 1e-14 {
1331        return real_roots_cubic(c3, c2, c1, c0);
1332    }
1333    // Monic: x⁴ + a x³ + b x² + c x + d.
1334    let (a, b, c, d) = (c3 / c4, c2 / c4, c1 / c4, c0 / c4);
1335    let eval = |z: Complex| -> Complex {
1336        // Horner.
1337        let mut acc = Complex::new(1.0, 0.0);
1338        acc = acc * z + Complex::new(a, 0.0);
1339        acc = acc * z + Complex::new(b, 0.0);
1340        acc = acc * z + Complex::new(c, 0.0);
1341        acc * z + Complex::new(d, 0.0)
1342    };
1343    // Durand–Kerner: four roots seeded on a circle, iterated to convergence.
1344    let seed = Complex::new(0.4, 0.9);
1345    let mut r = [
1346        Complex::new(1.0, 0.0),
1347        seed,
1348        seed * seed,
1349        seed * seed * seed,
1350    ];
1351    for _ in 0..100 {
1352        let mut max_step = 0.0_f64;
1353        for i in 0..4 {
1354            let mut denom = Complex::new(1.0, 0.0);
1355            for j in 0..4 {
1356                if i != j {
1357                    denom = denom * (r[i] - r[j]);
1358                }
1359            }
1360            if denom.norm() < 1e-300 {
1361                continue;
1362            }
1363            let step = eval(r[i]) / denom;
1364            r[i] = r[i] - step;
1365            max_step = max_step.max(step.norm());
1366        }
1367        if max_step < 1e-14 {
1368            break;
1369        }
1370    }
1371    // Keep roots with negligible imaginary part AND a small REAL-polynomial
1372    // residual — Durand–Kerner stops after a fixed iteration cap whether or not
1373    // it converged, so a non-converged iterate could otherwise be returned as a
1374    // spurious root. Evaluate the monic quartic at each candidate (real part) and
1375    // keep only |p(x)| below a magnitude-scaled tolerance; de-dup near-equal
1376    // roots (a double root converges to two near-identical iterates).
1377    let p_real = |x: f64| -> f64 { (((x + a) * x + b) * x + c) * x + d };
1378    let mut out: Vec<f64> = Vec::new();
1379    for z in r {
1380        if z.im.abs() >= 1e-7 {
1381            continue;
1382        }
1383        let x = z.re;
1384        // Residual tolerance scales with the polynomial's coefficient magnitude
1385        // and |x|^4 so large-coefficient quartics are not over-rejected.
1386        let scale = 1.0 + a.abs() + b.abs() + c.abs() + d.abs() + x.abs().powi(4);
1387        if p_real(x).abs() > 1e-6 * scale {
1388            continue;
1389        }
1390        if out.iter().any(|&y| (y - x).abs() < 1e-9 * (1.0 + x.abs())) {
1391            continue;
1392        }
1393        out.push(x);
1394    }
1395    out
1396}
1397
1398/// Real roots of `a x³ + b x² + c x + d` (Cardano), with quadratic fallback.
1399fn real_roots_cubic(a: f64, b: f64, c: f64, d: f64) -> Vec<f64> {
1400    if a.abs() < 1e-14 {
1401        return real_roots_quadratic(b, c, d);
1402    }
1403    // Depressed cubic t³ + p t + q via x = t − b/(3a).
1404    let (b, c, d) = (b / a, c / a, d / a);
1405    let p = c - b * b / 3.0;
1406    let q = 2.0 * b * b * b / 27.0 - b * c / 3.0 + d;
1407    let shift = -b / 3.0;
1408    let disc = q * q / 4.0 + p * p * p / 27.0;
1409    if disc > 1e-14 {
1410        let sq = disc.sqrt();
1411        let u = (-q / 2.0 + sq).cbrt();
1412        let v = (-q / 2.0 - sq).cbrt();
1413        vec![u + v + shift]
1414    } else if disc < -1e-14 {
1415        // Three real roots (trigonometric).
1416        let m = 2.0 * (-p / 3.0).sqrt();
1417        let theta = (3.0 * q / (p * m)).clamp(-1.0, 1.0).acos() / 3.0;
1418        (0..3)
1419            .map(|k| {
1420                m.mul_add(
1421                    (theta - 2.0 * std::f64::consts::PI * f64::from(k) / 3.0).cos(),
1422                    shift,
1423                )
1424            })
1425            .collect()
1426    } else {
1427        // Repeated roots.
1428        let u = (-q / 2.0).cbrt();
1429        vec![2.0 * u + shift, -u + shift]
1430    }
1431}
1432
1433/// Real roots of `a x² + b x + c`, with linear fallback.
1434fn real_roots_quadratic(a: f64, b: f64, c: f64) -> Vec<f64> {
1435    if a.abs() < 1e-14 {
1436        if b.abs() < 1e-14 {
1437            return Vec::new();
1438        }
1439        return vec![-c / b];
1440    }
1441    let disc = b * b - 4.0 * a * c;
1442    if disc < 0.0 {
1443        Vec::new()
1444    } else {
1445        let sq = disc.sqrt();
1446        vec![(-b - sq) / (2.0 * a), (-b + sq) / (2.0 * a)]
1447    }
1448}
1449
1450/// Minimal complex number for the quartic root finder.
1451#[derive(Clone, Copy)]
1452struct Complex {
1453    re: f64,
1454    im: f64,
1455}
1456
1457impl Complex {
1458    const fn new(re: f64, im: f64) -> Self {
1459        Self { re, im }
1460    }
1461    fn norm(self) -> f64 {
1462        self.re.hypot(self.im)
1463    }
1464}
1465
1466impl std::ops::Add for Complex {
1467    type Output = Self;
1468    fn add(self, o: Self) -> Self {
1469        Self::new(self.re + o.re, self.im + o.im)
1470    }
1471}
1472
1473impl std::ops::Sub for Complex {
1474    type Output = Self;
1475    fn sub(self, o: Self) -> Self {
1476        Self::new(self.re - o.re, self.im - o.im)
1477    }
1478}
1479
1480impl std::ops::Mul for Complex {
1481    type Output = Self;
1482    fn mul(self, o: Self) -> Self {
1483        Self::new(
1484            self.re.mul_add(o.re, -(self.im * o.im)),
1485            self.re.mul_add(o.im, self.im * o.re),
1486        )
1487    }
1488}
1489
1490impl std::ops::Div for Complex {
1491    type Output = Self;
1492    fn div(self, o: Self) -> Self {
1493        let den = o.re.mul_add(o.re, o.im * o.im);
1494        Self::new(
1495            self.re.mul_add(o.re, self.im * o.im) / den,
1496            self.im.mul_add(o.re, -(self.re * o.im)) / den,
1497        )
1498    }
1499}
1500
1501/// Build intersection curves from a collection of ordered 3D points.
1502///
1503/// If there are enough points, fits a NURBS curve through them.
1504fn build_curves_from_points(
1505    points_3d: &[Point3],
1506    ipoints: Vec<IntersectionPoint>,
1507) -> Result<Vec<IntersectionCurve>, MathError> {
1508    if points_3d.len() < 2 {
1509        return Ok(vec![]);
1510    }
1511
1512    let degree = 3.min(points_3d.len() - 1);
1513    let curve = interpolate(points_3d, degree)?;
1514    Ok(vec![IntersectionCurve {
1515        curve,
1516        points: ipoints,
1517    }])
1518}
1519
1520// -- Analytic-Analytic Intersection -------------------------------------------
1521
1522/// Intersect two analytic surfaces using a general marching approach.
1523///
1524/// Seeds intersection points by sampling both parameter spaces on a grid,
1525/// then marches along the intersection curve using the cross product of
1526/// the two surface normals as the tangent direction.
1527///
1528/// # Errors
1529///
1530/// Returns an error if curve fitting fails.
1531#[allow(
1532    clippy::cast_precision_loss,
1533    clippy::too_many_lines,
1534    clippy::similar_names,
1535    clippy::unnecessary_wraps,
1536    clippy::type_complexity
1537)]
1538pub fn intersect_analytic_analytic(
1539    a: AnalyticSurface<'_>,
1540    b: AnalyticSurface<'_>,
1541    grid_res: usize,
1542) -> Result<Vec<IntersectionCurve>, MathError> {
1543    intersect_analytic_analytic_bounded(a, b, grid_res, None, None)
1544}
1545
1546/// Intersect two analytic surfaces with optional v-range overrides.
1547///
1548/// When `v_range_hint_a` or `v_range_hint_b` is `Some((min, max))`, the
1549/// marching algorithm searches that v-range instead of the hardcoded default.
1550/// This is essential for cylinders and cones whose default v-range is small
1551/// (-1..1 or 0.01..2) but whose actual face may extend much further.
1552///
1553/// # Errors
1554///
1555/// Returns `MathError` if algebraic intersection fails or marching diverges.
1556pub fn intersect_analytic_analytic_bounded(
1557    a: AnalyticSurface<'_>,
1558    b: AnalyticSurface<'_>,
1559    grid_res: usize,
1560    v_range_hint_a: Option<(f64, f64)>,
1561    v_range_hint_b: Option<(f64, f64)>,
1562) -> Result<Vec<IntersectionCurve>, MathError> {
1563    intersect_analytic_analytic_impl(a, b, grid_res, v_range_hint_a, v_range_hint_b, None)
1564}
1565
1566/// [`intersect_analytic_analytic_bounded`] with its general marcher confined
1567/// to `region`.
1568///
1569/// The marcher starts only from seeds that converge into the region (with a
1570/// margin of about two grid cells) and stops where a curve leaves it. Pairs
1571/// solved in closed form are returned whole, as by the unconfined call. Two
1572/// bounded faces meet only inside both their boxes, so their overlap is a
1573/// sound region for a face pair.
1574///
1575/// # Errors
1576///
1577/// Returns `MathError` if algebraic intersection fails or marching diverges.
1578pub fn intersect_analytic_analytic_in_region(
1579    a: AnalyticSurface<'_>,
1580    b: AnalyticSurface<'_>,
1581    grid_res: usize,
1582    v_range_hint_a: Option<(f64, f64)>,
1583    v_range_hint_b: Option<(f64, f64)>,
1584    region: Aabb3,
1585) -> Result<Vec<IntersectionCurve>, MathError> {
1586    intersect_analytic_analytic_impl(a, b, grid_res, v_range_hint_a, v_range_hint_b, Some(region))
1587}
1588
1589fn intersect_analytic_analytic_impl(
1590    a: AnalyticSurface<'_>,
1591    b: AnalyticSurface<'_>,
1592    grid_res: usize,
1593    v_range_hint_a: Option<(f64, f64)>,
1594    v_range_hint_b: Option<(f64, f64)>,
1595    region: Option<Aabb3>,
1596) -> Result<Vec<IntersectionCurve>, MathError> {
1597    // Try algebraic specialization for known surface pairs before falling
1598    // back to the general marching approach.
1599    if let Some(result) = try_algebraic_intersection(&a, &b, v_range_hint_a, v_range_hint_b)? {
1600        return Ok(result);
1601    }
1602
1603    let (surf_a, norm_a, u_range_a, default_v_a) = surface_closures(&a);
1604    let (surf_b, norm_b, u_range_b, default_v_b) = surface_closures(&b);
1605    let v_range_a = v_range_hint_a.unwrap_or(default_v_a);
1606    let v_range_b = v_range_hint_b.unwrap_or(default_v_b);
1607
1608    // Compute characteristic surface dimensions for adaptive parameters.
1609    let diag_a = {
1610        let p00 = surf_a(u_range_a.0, v_range_a.0);
1611        let p11 = surf_a(u_range_a.1, v_range_a.1);
1612        (p00 - p11).length()
1613    };
1614    let diag_b = {
1615        let p00 = surf_b(u_range_b.0, v_range_b.0);
1616        let p11 = surf_b(u_range_b.1, v_range_b.1);
1617        (p00 - p11).length()
1618    };
1619    let char_size = diag_a.min(diag_b).max(0.1);
1620
1621    // Sample surface A on a grid. For each grid point, project it
1622    // analytically onto surface B to find the closest point, then check
1623    // if the distance is below threshold (indicating near-intersection).
1624    #[allow(clippy::type_complexity)]
1625    let mut seeds: Vec<(Point3, (f64, f64), (f64, f64))> = Vec::new();
1626    // Coarse threshold scales with the surface size — the distance from
1627    // a grid point on A to its projection on B can be large even near
1628    // the intersection (e.g., sphere R=2 and cylinder R=1 → gap ≈ 1).
1629    let seed_threshold = diag_a.max(diag_b).max(1.0) * 0.5;
1630    let mut min_dist = f64::INFINITY;
1631
1632    #[allow(clippy::cast_precision_loss)]
1633    for ia in 0..grid_res {
1634        for ja in 0..grid_res {
1635            let ua =
1636                u_range_a.0 + (u_range_a.1 - u_range_a.0) * (ia as f64 + 0.5) / (grid_res as f64);
1637            let va =
1638                v_range_a.0 + (v_range_a.1 - v_range_a.0) * (ja as f64 + 0.5) / (grid_res as f64);
1639
1640            let pa = surf_a(ua, va);
1641
1642            // Analytically project onto surface B.
1643            let (ub, vb) = project_analytic(&b, pa, u_range_b, v_range_b);
1644            let pb = surf_b(ub, vb);
1645            let dist = (pa - pb).length();
1646            min_dist = min_dist.min(dist);
1647
1648            if dist < seed_threshold {
1649                // Use the coarse seed directly. The marching algorithm
1650                // corrects positions at each step via projection, so seeds
1651                // don't need to be on the exact intersection — they just
1652                // need to be close enough for the marcher to converge.
1653                let mid = Point3::new(
1654                    (pa.x() + pb.x()) * 0.5,
1655                    (pa.y() + pb.y()) * 0.5,
1656                    (pa.z() + pb.z()) * 0.5,
1657                );
1658                seeds.push((mid, (ua, va), (ub, vb)));
1659            }
1660        }
1661    }
1662
1663    // Cheap rejection: the grid samples surface A; the closest sample's
1664    // distance to B lower-bounds how near the two bounded patches come. A
1665    // transversal crossing puts a sample within ~one grid cell of it
1666    // (distance on the order of a cell), so if even the nearest sample is
1667    // several cells away the patches cannot cross — skip the expensive
1668    // marching and return empty. Result-preserving: non-crossing pairs
1669    // already march to nothing, just slowly (this is the gridfinity lip's
1670    // ~80 inner-wall × outer-wall pairs that dominate pavefiller time).
1671    let reject_dist = (char_size / grid_res as f64) * 3.0;
1672    if min_dist > reject_dist {
1673        return Ok(vec![]);
1674    }
1675
1676    if seeds.is_empty() {
1677        return Ok(vec![]);
1678    }
1679
1680    // Aggressively deduplicate seeds — we only need 1-2 per intersection
1681    // branch. Scale dedup radius to ~2% of characteristic surface size
1682    // (at least 10× the march step size) to avoid redundant marches.
1683    let march_step = (char_size * 0.02).clamp(0.005, 0.5);
1684    let dedup_radius = march_step * 10.0;
1685    let mut unique_seeds = Vec::new();
1686    for seed in &seeds {
1687        let dominated = unique_seeds
1688            .iter()
1689            .any(|s: &(Point3, (f64, f64), (f64, f64))| (s.0 - seed.0).length() < dedup_radius);
1690        if !dominated {
1691            unique_seeds.push(*seed);
1692        }
1693    }
1694
1695    // A seed is only near the intersection; settle it onto the curve before
1696    // asking whether it lies in the region, so a coarse grid point beside a
1697    // short in-region span still starts its march, and march from there, so
1698    // the march does not begin outside the region and stop at once.
1699    let region = region.map(|r| r.expanded(2.0 * char_size / grid_res as f64));
1700    if let Some(r) = region {
1701        for seed in &mut unique_seeds {
1702            let mut p = seed.0;
1703            for _ in 0..8 {
1704                let (ua, va) = project_analytic(&a, p, u_range_a, v_range_a);
1705                let pa = surf_a(ua, va);
1706                let (ub, vb) = project_analytic(&b, pa, u_range_b, v_range_b);
1707                let pb = surf_b(ub, vb);
1708                p = Point3::new(
1709                    (pa.x() + pb.x()) * 0.5,
1710                    (pa.y() + pb.y()) * 0.5,
1711                    (pa.z() + pb.z()) * 0.5,
1712                );
1713                if (pa - pb).length() < 1e-9 {
1714                    break;
1715                }
1716            }
1717            seed.0 = p;
1718        }
1719        unique_seeds.retain(|seed| r.contains_point(seed.0));
1720    }
1721
1722    // March from each seed.
1723    let mut curves = Vec::new();
1724    let mut used_seeds = vec![false; unique_seeds.len()];
1725
1726    for si in 0..unique_seeds.len() {
1727        if used_seeds[si] {
1728            continue;
1729        }
1730        used_seeds[si] = true;
1731
1732        let march_result = march_analytic_intersection(
1733            &a,
1734            &b,
1735            surf_a.as_ref(),
1736            norm_a.as_ref(),
1737            surf_b.as_ref(),
1738            norm_b.as_ref(),
1739            unique_seeds[si].0,
1740            u_range_a,
1741            v_range_a,
1742            u_range_b,
1743            v_range_b,
1744            march_step,
1745            is_u_periodic(&a),
1746            is_u_periodic(&b),
1747            region,
1748        );
1749
1750        if march_result.len() >= 2 {
1751            for (sj, other) in unique_seeds.iter().enumerate() {
1752                if !used_seeds[sj]
1753                    && march_result
1754                        .iter()
1755                        .any(|p| (*p - other.0).length() < dedup_radius)
1756                {
1757                    used_seeds[sj] = true;
1758                }
1759            }
1760
1761            let ipts: Vec<IntersectionPoint> = march_result
1762                .iter()
1763                .map(|&pt| IntersectionPoint {
1764                    point: pt,
1765                    param1: (0.0, 0.0),
1766                    param2: (0.0, 0.0),
1767                })
1768                .collect();
1769
1770            let degree = 3.min(march_result.len() - 1);
1771            if let Ok(curve) = interpolate(&march_result, degree) {
1772                curves.push(IntersectionCurve {
1773                    curve,
1774                    points: ipts,
1775                });
1776            }
1777        }
1778    }
1779
1780    Ok(curves)
1781}
1782
1783/// Try algebraic (closed-form or semi-algebraic) intersection for known
1784/// surface pairs before falling back to general marching.
1785///
1786/// Returns `Some(curves)` if a specialized method exists, `None` otherwise.
1787///
1788/// Currently handles:
1789/// - **Sphere-sphere**: intersection is a circle (plane through the two centers)
1790/// - **Coaxial cylinders**: same axis → circle(s) or empty
1791/// - **Sphere-cylinder**: reduce to quadratic in one parameter
1792/// - **Cone-cylinder**: parallel axes in the cone's own `v`, other axes
1793///   along the cylinder's rulings
1794/// - **Cone-sphere**: off the cone's axis, along the cone's generators
1795/// - **Torus-cylinder**: along the cylinder's rulings
1796#[allow(clippy::too_many_lines)]
1797fn try_algebraic_intersection(
1798    a: &AnalyticSurface<'_>,
1799    b: &AnalyticSurface<'_>,
1800    v_range_a: Option<(f64, f64)>,
1801    v_range_b: Option<(f64, f64)>,
1802) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
1803    match (a, b) {
1804        (AnalyticSurface::Cone(cone), AnalyticSurface::Cylinder(cyl)) => Ok(
1805            algebraic_parallel_cone_cylinder(cone, cyl, v_range_a, v_range_b)?
1806                .or_else(|| ruling_cone_cylinder(cone, cyl, true)),
1807        ),
1808        (AnalyticSurface::Cylinder(cyl), AnalyticSurface::Cone(cone)) => Ok(
1809            algebraic_parallel_cone_cylinder(cone, cyl, v_range_b, v_range_a)?
1810                .or_else(|| ruling_cone_cylinder(cone, cyl, false)),
1811        ),
1812        (AnalyticSurface::Sphere(s1), AnalyticSurface::Sphere(s2)) => {
1813            algebraic_sphere_sphere(s1, s2).map(Some)
1814        }
1815        (AnalyticSurface::Cylinder(c1), AnalyticSurface::Cylinder(c2)) => {
1816            let axis_dot = c1.axis().dot(c2.axis()).abs();
1817            if axis_dot > 1.0 - 1e-10 {
1818                // Axes are parallel — check if they're the same line.
1819                let delta = c2.origin() - c1.origin();
1820                let delta_vec = Vec3::new(delta.x(), delta.y(), delta.z());
1821                let along = delta_vec.dot(c1.axis());
1822                let perp = (delta_vec - c1.axis() * along).length();
1823                if perp < 1e-8 {
1824                    // Coaxial: same axis, different radii → no intersection
1825                    // (unless equal radius → degenerate overlap, skip)
1826                    if (c1.radius() - c2.radius()).abs() < 1e-8 {
1827                        return Ok(None); // Overlapping — let marcher handle
1828                    }
1829                    return Ok(Some(vec![])); // Coaxial, different radii
1830                }
1831            }
1832            // Non-coaxial: algebraic quadratic in v.
1833            algebraic_cylinder_cylinder(c1, c2)
1834        }
1835        // Sphere-cylinder (both orderings).
1836        (AnalyticSurface::Sphere(s), AnalyticSurface::Cylinder(c)) => {
1837            algebraic_sphere_cylinder(s, c, true)
1838        }
1839        (AnalyticSurface::Cylinder(c), AnalyticSurface::Sphere(s)) => {
1840            algebraic_sphere_cylinder(s, c, false)
1841        }
1842        (AnalyticSurface::Cone(c1), AnalyticSurface::Cone(c2)) => algebraic_cone_cone(c1, c2),
1843        (AnalyticSurface::Cone(cone), AnalyticSurface::Sphere(sphere)) => {
1844            Ok(ruling_cone_sphere(cone, sphere, true))
1845        }
1846        (AnalyticSurface::Sphere(sphere), AnalyticSurface::Cone(cone)) => {
1847            Ok(ruling_cone_sphere(cone, sphere, false))
1848        }
1849        (AnalyticSurface::Torus(t), AnalyticSurface::Cylinder(c)) => {
1850            Ok(parallel_axis_torus_cylinder(t, c, true)
1851                .or_else(|| ruling_torus_cylinder(t, c, true)))
1852        }
1853        (AnalyticSurface::Cylinder(c), AnalyticSurface::Torus(t)) => {
1854            Ok(parallel_axis_torus_cylinder(t, c, false)
1855                .or_else(|| ruling_torus_cylinder(t, c, false)))
1856        }
1857        _ => Ok(None),
1858    }
1859}
1860
1861/// A torus and a cylinder whose axes are parallel but distinct (a drill
1862/// through a ring parallel to its axis), traced along the cylinder's
1863/// rulings. A ruling stays at one distance `ρ` from the torus axis, so it
1864/// meets the tube where `(ρ − R)² + z² = r²`: a quadratic in its axial
1865/// parameter. `None` for any other pair (tilted or coaxial axes) and when
1866/// the sampling misses a window narrower than itself.
1867fn parallel_axis_torus_cylinder(
1868    torus: &ToroidalSurface,
1869    cyl: &CylindricalSurface,
1870    torus_first: bool,
1871) -> Option<Vec<IntersectionCurve>> {
1872    let axis = torus.z_axis();
1873    let along = cyl.axis().dot(axis);
1874    if along.abs() < 1.0 - 1e-10 {
1875        return None;
1876    }
1877    let offset = cyl.origin() - torus.center();
1878    if (offset - axis * offset.dot(axis)).length() < Tolerance::new().linear {
1879        return None;
1880    }
1881    let (major, minor) = (torus.major_radius(), torus.minor_radius());
1882    let roots = |u: f64| {
1883        let q = cyl.evaluate(u, 0.0) - torus.center();
1884        let height = q.dot(axis);
1885        let rho = (q - axis * height).length();
1886        let reach = minor * minor - (rho - major) * (rho - major);
1887        ruling_quadratic(1.0, 2.0 * along.signum() * height, height * height - reach)
1888    };
1889    let samples = ruling_samples(cyl, &roots);
1890    let loops = if samples.iter().all(Option::is_some) {
1891        closed_ruling_loops(cyl, &roots, &samples)
1892    } else {
1893        partial_ruling_loops(cyl, &roots, &samples)
1894    };
1895    if loops.is_empty() {
1896        return None;
1897    }
1898    Some(fit_ruling_loops(&loops, |p| {
1899        in_order(torus.project_point(p), cyl.project_point(p), torus_first)
1900    }))
1901}
1902
1903/// Where two circles in a half-plane through an axis cross, as `(rho, z)`
1904/// pairs (distance from the axis, height along it): each sweeps a circle
1905/// about the axis. `None` (defer to the marcher) when the circles coincide
1906/// or touch, or a crossing lands on or past the axis; `Some` of none when
1907/// they miss.
1908fn meridian_crossings(
1909    first: (f64, f64, f64),
1910    second: (f64, f64, f64),
1911    scale: f64,
1912) -> Option<Vec<(f64, f64)>> {
1913    let ((x1, z1, r1), (x2, z2, r2)) = (first, second);
1914    let (dx, dz) = (x2 - x1, z2 - z1);
1915    let dist = dx.hypot(dz);
1916    let slack = 1e-9 * scale;
1917    if dist < slack || (dist - (r1 + r2)).abs() < slack || (dist - (r1 - r2).abs()).abs() < slack {
1918        return None;
1919    }
1920    if dist > r1 + r2 || dist < (r1 - r2).abs() {
1921        return Some(Vec::new());
1922    }
1923    let along = r2.mul_add(-r2, r1.mul_add(r1, dist * dist)) / (2.0 * dist);
1924    let across = r1.mul_add(r1, -(along * along)).max(0.0).sqrt();
1925    let (ux, uz) = (dx / dist, dz / dist);
1926    let mut crossings = Vec::with_capacity(2);
1927    for side in [1.0, -1.0] {
1928        let rho = x1 + along * ux - side * across * uz;
1929        if rho <= slack {
1930            return None;
1931        }
1932        crossings.push((rho, z1 + along * uz + side * across * ux));
1933    }
1934    Some(crossings)
1935}
1936
1937/// Circles about an axis through `base`, at the given `(rho, z)` crossings.
1938fn circles_about_axis(
1939    base: Point3,
1940    axis: Vec3,
1941    crossings: &[(f64, f64)],
1942) -> Result<Vec<ExactIntersectionCurve>, MathError> {
1943    crossings
1944        .iter()
1945        .map(|&(rho, z)| {
1946            Circle3D::new(base + axis * z, axis, rho).map(ExactIntersectionCurve::Circle)
1947        })
1948        .collect()
1949}
1950
1951/// Exact intersection of two tori sharing an axis: their tube cross-sections
1952/// in a half-plane through the axis cross in up to two points, and each sweeps
1953/// a circle about the axis.
1954///
1955/// `None` (defer to the marcher) unless the axes lie on one line, or when the
1956/// cross-sections coincide or touch.
1957///
1958/// # Errors
1959///
1960/// Returns an error if a section circle cannot be built.
1961pub fn exact_torus_torus(
1962    first: &ToroidalSurface,
1963    second: &ToroidalSurface,
1964) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
1965    let axis = first.z_axis();
1966    let scale = first.major_radius() + second.major_radius();
1967    let offset = second.center() - first.center();
1968    // A spindle torus's tube also crosses the far side of the axis.
1969    if first.minor_radius() >= first.major_radius()
1970        || second.minor_radius() >= second.major_radius()
1971        || axis.cross(second.z_axis()).length() > 1e-9
1972        || offset.cross(axis).length() > 1e-9 * scale
1973    {
1974        return Ok(None);
1975    }
1976    let Some(crossings) = meridian_crossings(
1977        (first.major_radius(), 0.0, first.minor_radius()),
1978        (
1979            second.major_radius(),
1980            offset.dot(axis),
1981            second.minor_radius(),
1982        ),
1983        scale,
1984    ) else {
1985        return Ok(None);
1986    };
1987    circles_about_axis(first.center(), axis, &crossings).map(Some)
1988}
1989
1990/// Exact intersection of a torus with a cylinder sharing its axis.
1991///
1992/// The wall line and the tube's cross-section in a half-plane through the
1993/// axis cross in up to two points, each sweeping a circle about the axis.
1994///
1995/// `None` (defer to the marcher) unless the axes lie on one line, or when
1996/// the wall touches the tube.
1997///
1998/// # Errors
1999///
2000/// Returns an error if a section circle cannot be built.
2001pub fn exact_cylinder_torus(
2002    cylinder: &CylindricalSurface,
2003    torus: &ToroidalSurface,
2004) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2005    let axis = torus.z_axis();
2006    let scale = torus.major_radius() + cylinder.radius();
2007    let offset = cylinder.origin() - torus.center();
2008    // A spindle torus's tube also crosses the far side of the axis.
2009    if torus.minor_radius() >= torus.major_radius()
2010        || axis.cross(cylinder.axis()).length() > 1e-9
2011        || offset.cross(axis).length() > 1e-9 * scale
2012    {
2013        return Ok(None);
2014    }
2015    let gap = cylinder.radius() - torus.major_radius();
2016    let small = torus.minor_radius();
2017    if (gap.abs() - small).abs() < 1e-9 * scale {
2018        return Ok(None);
2019    }
2020    if gap.abs() > small {
2021        return Ok(Some(Vec::new()));
2022    }
2023    let height = small.mul_add(small, -(gap * gap)).sqrt();
2024    circles_about_axis(
2025        torus.center(),
2026        axis,
2027        &[(cylinder.radius(), height), (cylinder.radius(), -height)],
2028    )
2029    .map(Some)
2030}
2031
2032/// Exact intersection of a torus with a sphere centred on its axis.
2033///
2034/// The sphere's great circle and the tube's cross-section in a half-plane
2035/// through the axis cross in up to two points, and each sweeps a circle
2036/// about the axis.
2037///
2038/// `None` (defer to the marcher) unless the sphere's centre lies on the axis,
2039/// or when the two circles touch.
2040///
2041/// # Errors
2042///
2043/// Returns an error if a section circle cannot be built.
2044pub fn exact_sphere_torus(
2045    sphere: &SphericalSurface,
2046    torus: &ToroidalSurface,
2047) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2048    let axis = torus.z_axis();
2049    let scale = torus.major_radius() + sphere.radius();
2050    let offset = sphere.center() - torus.center();
2051    // A spindle torus's tube also crosses the far side of the axis.
2052    if torus.minor_radius() >= torus.major_radius() || offset.cross(axis).length() > 1e-9 * scale {
2053        return Ok(None);
2054    }
2055    let Some(crossings) = meridian_crossings(
2056        (0.0, offset.dot(axis), sphere.radius()),
2057        (torus.major_radius(), 0.0, torus.minor_radius()),
2058        scale,
2059    ) else {
2060        return Ok(None);
2061    };
2062    circles_about_axis(torus.center(), axis, &crossings).map(Some)
2063}
2064
2065/// Exact coaxial cone-cone intersection: returns the shared circle.
2066///
2067/// Two cones that share an axis are concentric circles at every axial
2068/// station, so they meet only where their radii are equal. Each cone's
2069/// radius is linear in the axial coordinate `t` (measured along the shared
2070/// axis from cone 1's apex): `r1 = m1·t` and `r2 = m2·σ·(t − d2)`, where
2071/// `m_i = cot(half_angle_i)`, `σ = sign(axis2·axis1)`, and `d2` is cone 2's
2072/// apex position in that coordinate. Equating gives a single crossing `t*`
2073/// → one circle (the shared rim). The general marcher mishandles this case:
2074/// at the radii-crossing the surfaces are nearly tangent, so a grid-seeded
2075/// march fragments the clean circle into dozens of degenerate micro-curves.
2076///
2077/// Returns `Some(vec![circle])` for a genuine crossing, `Some(vec![])` when
2078/// the cones do not meet (parallel radius lines or a crossing on the wrong
2079/// nappe), and `None` for the identical-cone overlap or a degenerate
2080/// (near-flat) cone — both of which fall through to the general path.
2081/// Parallel-but-offset axes with equal half-angle tangents reduce to a
2082/// radical-plane conic (`offset_parallel_cone_cone`); other offset
2083/// configurations defer to the marcher with `None`.
2084///
2085/// # Errors
2086///
2087/// Returns [`MathError`] if the shared-rim `Circle3D` cannot be constructed
2088/// (e.g. a non-finite center or radius from a malformed cone).
2089pub fn exact_cone_cone(
2090    c1: &ConicalSurface,
2091    c2: &ConicalSurface,
2092) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2093    let axis = c1.axis();
2094    let axis2 = c2.axis();
2095
2096    // Coaxial check: parallel axes and the second apex lies on the first axis.
2097    if axis.dot(axis2).abs() < 1.0 - 1e-10 {
2098        return Ok(None); // Non-coaxial: quartic curve, let the marcher handle.
2099    }
2100    let apex1 = c1.apex();
2101    let apex2 = c2.apex();
2102    let delta = apex2 - apex1;
2103    let delta_v = Vec3::new(delta.x(), delta.y(), delta.z());
2104    let along = delta_v.dot(axis);
2105    if (delta_v - axis * along).length() > 1e-8 {
2106        return offset_parallel_cone_cone(c1, c2);
2107    }
2108
2109    let (s1, s2) = (c1.half_angle().sin(), c2.half_angle().sin());
2110    if s1.abs() < 1e-12 || s2.abs() < 1e-12 {
2111        return Ok(None); // Degenerate (near-flat) cone.
2112    }
2113    let m1 = c1.half_angle().cos() / s1;
2114    let m2 = c2.half_angle().cos() / s2;
2115    let sigma = if axis.dot(axis2) >= 0.0 { 1.0 } else { -1.0 };
2116    let d2 = along; // apex2 position along `axis`, measured from apex1.
2117
2118    let denom = m1 - m2 * sigma;
2119    if denom.abs() < 1e-12 {
2120        // Parallel radius lines: identical cones (coincident apex, same opening)
2121        // overlap — defer to the general/same-domain path; otherwise no meeting.
2122        if sigma > 0.0 && d2.abs() < 1e-9 {
2123            return Ok(None);
2124        }
2125        return Ok(Some(vec![]));
2126    }
2127
2128    let t_star = (-m2 * sigma * d2) / denom;
2129    let radius = m1 * t_star;
2130    if radius < 1e-12 {
2131        return Ok(Some(vec![])); // Crossing on the wrong nappe / no real circle.
2132    }
2133
2134    let center = Point3::new(
2135        apex1.x() + axis.x() * t_star,
2136        apex1.y() + axis.y() * t_star,
2137        apex1.z() + axis.z() * t_star,
2138    );
2139    let circle = Circle3D::new(center, axis, radius)?;
2140    Ok(Some(vec![ExactIntersectionCurve::Circle(circle)]))
2141}
2142
2143/// Parallel-axis (or anti-parallel), offset-apex cones with equal half-angle
2144/// tangents: subtracting the two quadric equations cancels both the radial
2145/// and the axial quadratic terms (their coefficients depend only on
2146/// `tan²(half_angle)`), so every intersection point lies on a plane — the
2147/// degenerate member of the quadric pencil — and plane ∩ cone is an exact
2148/// conic. The gridfinity spacer lip fuse hits this exactly: opposed 45°
2149/// corner cones offset 0.25mm, which the marcher shreds into ~64 closed
2150/// micro-loops per pair (#1570). Unequal angles keep a genuine quadratic
2151/// term, and an unbounded section (hyperbola/parabola) has no closed-form
2152/// win over the marcher — both defer with `None`.
2153fn offset_parallel_cone_cone(
2154    c1: &ConicalSurface,
2155    c2: &ConicalSurface,
2156) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2157    if c1.half_angle().sin().abs() < 1e-12 || c2.half_angle().sin().abs() < 1e-12 {
2158        return Ok(None); // Degenerate (near-flat) cone, as in the coaxial path.
2159    }
2160    let t1 = c1.half_angle().tan();
2161    let t2 = c2.half_angle().tan();
2162    if !t1.is_finite() || !t2.is_finite() {
2163        return Ok(None);
2164    }
2165    if (t1 - t2).abs() > 1e-9 * (1.0 + t1.abs().max(t2.abs())) {
2166        return Ok(None);
2167    }
2168
2169    let w = c1.axis();
2170    let apex1 = c1.apex();
2171    let apex2 = c2.apex();
2172    let delta = apex2 - apex1;
2173    let delta_v = Vec3::new(delta.x(), delta.y(), delta.z());
2174    let s = delta_v.dot(w);
2175    let tm = 0.5 * (t1 + t2);
2176    let k = 1.0 + tm * tm;
2177
2178    // In the apex1 frame each cone is |P|² − k(P·w)² = 0 (shifted by δ for
2179    // cone 2; the axis SIGN drops out since only (P·w)² appears). Their
2180    // difference: P·(2δ − 2ksw) = |δ|² − ks².
2181    let n = (delta_v - w * (k * s)) * 2.0;
2182    let n_len = n.length();
2183    if n_len < 1e-12 {
2184        return Ok(None);
2185    }
2186    let n_hat = n * (1.0 / n_len);
2187    let d = (dot_np(n, apex1) + delta_v.dot(delta_v) - k * s * s) / n_len;
2188
2189    // `exact_plane_cone` already rejects sections on cone 1's phantom nappe;
2190    // cone 2's nappe must be checked here. A conic on the shared quadric
2191    // pencil cannot cross between nappes except exactly through apex 2, so
2192    // sampled quarter-points either all pass or all fail; a mixed verdict
2193    // means an apex-touching degeneracy — defer to the marcher.
2194    let axis2 = c2.axis();
2195    let scale = 1.0 + delta_v.length();
2196    let mut out = Vec::new();
2197    for curve in exact_plane_cone(c1, n_hat, d, 0.0)? {
2198        let samples: Vec<Point3> = match &curve {
2199            ExactIntersectionCurve::Circle(c) => (0..4)
2200                .map(|i| crate::traits::ParametricCurve::evaluate(c, TAU * f64::from(i) / 4.0))
2201                .collect(),
2202            ExactIntersectionCurve::Ellipse(e) => (0..4)
2203                .map(|i| crate::traits::ParametricCurve::evaluate(e, TAU * f64::from(i) / 4.0))
2204                .collect(),
2205            ExactIntersectionCurve::Points(_) => return Ok(None),
2206        };
2207        let on_real_nappe = |p: &Point3| {
2208            let rel = *p - apex2;
2209            Vec3::new(rel.x(), rel.y(), rel.z()).dot(axis2) >= -1e-9 * scale
2210        };
2211        let hits = samples.iter().filter(|p| on_real_nappe(p)).count();
2212        match hits {
2213            0 => {}
2214            4 => out.push(curve),
2215            _ => return Ok(None),
2216        }
2217    }
2218    Ok(Some(out))
2219}
2220
2221/// Exact coaxial cone-cylinder intersection: returns the shared circle.
2222///
2223/// A cone and a cylinder sharing an axis are concentric circles at every
2224/// axial station, so they meet only where the cone's radius equals the
2225/// cylinder's. The cone radius is linear in the axial coordinate `t` from its
2226/// apex (`r = m·t`, `m = cot(half_angle)`), the cylinder radius is the
2227/// constant `R`, so `m·t = R` gives a single crossing `t*` → one circle. This
2228/// is the gridfinity lip's top knife edge (inner tapered corner = cone, outer
2229/// corner = cylinder, concentric, radii matching at `Z_PEAK`); the general
2230/// marcher fragments that near-tangent contact into dozens of degenerate
2231/// micro-curves.
2232///
2233/// Returns `Some(vec![circle])` for a genuine crossing, `Some(vec![])` when
2234/// the crossing degenerates to the apex, and `None` (defer to the marcher)
2235/// when the surfaces are not coaxial or the cone is near-flat / near-axial.
2236///
2237/// # Errors
2238///
2239/// Returns [`MathError`] if the shared `Circle3D` cannot be constructed.
2240pub fn exact_cone_cylinder(
2241    cone: &ConicalSurface,
2242    cyl: &CylindricalSurface,
2243) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2244    let axis = cone.axis();
2245    let cyl_axis = cyl.axis();
2246
2247    // Coaxial check: parallel axes and the cone apex on the cylinder's axis.
2248    if axis.dot(cyl_axis).abs() < 1.0 - 1e-10 {
2249        return Ok(None);
2250    }
2251    let apex = cone.apex();
2252    let delta = apex - cyl.origin();
2253    let delta_v = Vec3::new(delta.x(), delta.y(), delta.z());
2254    let along = delta_v.dot(cyl_axis);
2255    if (delta_v - cyl_axis * along).length() > 1e-8 {
2256        return Ok(None);
2257    }
2258
2259    let s = cone.half_angle().sin();
2260    if s.abs() < 1e-12 {
2261        return Ok(None); // near-flat cone.
2262    }
2263    let m = cone.half_angle().cos() / s; // dr/dt along the cone axis.
2264    if m.abs() < 1e-12 {
2265        return Ok(None); // near-axial cone: radius ~constant.
2266    }
2267
2268    let t_star = cyl.radius() / m; // where the cone radius m·t equals R.
2269    if t_star.abs() < 1e-12 {
2270        return Ok(Some(vec![])); // crossing at the apex — no real circle.
2271    }
2272    let center = Point3::new(
2273        apex.x() + axis.x() * t_star,
2274        apex.y() + axis.y() * t_star,
2275        apex.z() + axis.z() * t_star,
2276    );
2277    let circle = Circle3D::new(center, axis, cyl.radius())?;
2278    Ok(Some(vec![ExactIntersectionCurve::Circle(circle)]))
2279}
2280
2281/// Algebraic cone-cone intersection (NURBS form for the general bounded
2282/// path). Delegates to [`exact_cone_cone`] and samples each exact conic
2283/// (coaxial circle or offset-parallel radical-plane ellipse) into an
2284/// interpolated NURBS `IntersectionCurve`, mirroring the
2285/// sphere-cylinder algebraic path. phase FF prefers the exact circle form
2286/// directly (so the section edge links to the coincident boundary), but a
2287/// caller of `intersect_analytic_analytic_bounded` still gets one clean
2288/// curve instead of the marcher's fragments.
2289fn algebraic_cone_cone(
2290    c1: &ConicalSurface,
2291    c2: &ConicalSurface,
2292) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
2293    let Some(exacts) = exact_cone_cone(c1, c2)? else {
2294        return Ok(None);
2295    };
2296    let mut curves = Vec::new();
2297    for exact in exacts {
2298        let n_samples = 33;
2299        let mut positions = Vec::with_capacity(n_samples);
2300        let mut points = Vec::with_capacity(n_samples);
2301        #[allow(clippy::cast_precision_loss)]
2302        for i in 0..n_samples {
2303            let theta = TAU * i as f64 / (n_samples - 1) as f64;
2304            let pt = match &exact {
2305                ExactIntersectionCurve::Circle(circle) => {
2306                    crate::traits::ParametricCurve::evaluate(circle, theta)
2307                }
2308                ExactIntersectionCurve::Ellipse(ellipse) => {
2309                    crate::traits::ParametricCurve::evaluate(ellipse, theta)
2310                }
2311                ExactIntersectionCurve::Points(_) => break,
2312            };
2313            positions.push(pt);
2314            points.push(IntersectionPoint {
2315                point: pt,
2316                param1: (0.0, 0.0),
2317                param2: (0.0, 0.0),
2318            });
2319        }
2320        if positions.is_empty() {
2321            continue;
2322        }
2323        let degree = 3.min(positions.len() - 1);
2324        let curve = interpolate(&positions, degree)?;
2325        curves.push(IntersectionCurve { curve, points });
2326    }
2327    Ok(Some(curves))
2328}
2329
2330/// Exact coaxial sphere-cylinder intersection: returns the shared circle(s).
2331///
2332/// A sphere of radius `R` centered at `C` and a cylinder of radius `r` whose
2333/// axis passes through `C` meet in concentric circles of radius `r` at the
2334/// axial stations where `sqrt(R² − z²) = r`, i.e. `z = ±sqrt(R² − r²)`
2335/// measured from `C` along the axis. A proper crossing yields two circles; a
2336/// tangent contact (`r = R`) yields one; a cylinder wider than the sphere, or
2337/// a non-coaxial configuration (quartic curve), yields none/defers.
2338///
2339/// Mirrors [`exact_cone_cylinder`] so phase FF can emit the section as an
2340/// exact `Circle3D` (which the closed-circle split + seam adoption recognise)
2341/// rather than the marcher's NURBS fragments.
2342///
2343/// Returns `Some(vec![..])` (0, 1, or 2 circles) for the coaxial case, and
2344/// `None` (defer to the general marcher) when the axes are not coaxial.
2345///
2346/// # Errors
2347///
2348/// Returns [`MathError`] if a shared `Circle3D` cannot be constructed.
2349pub fn exact_sphere_cylinder(
2350    sphere: &SphericalSurface,
2351    cyl: &CylindricalSurface,
2352) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2353    let sc = sphere.center();
2354    let r_sphere = sphere.radius();
2355    let co = cyl.origin();
2356    let axis = cyl.axis();
2357    let r_cyl = cyl.radius();
2358
2359    // Project sphere center onto the cylinder axis.
2360    let delta = sc - co;
2361    let delta_vec = Vec3::new(delta.x(), delta.y(), delta.z());
2362    let along = delta_vec.dot(axis);
2363    let perp_vec = delta_vec - axis * along;
2364    let d_perp = perp_vec.length();
2365
2366    // Non-coaxial sphere-cylinder intersections produce quartic curves;
2367    // defer those to the general marcher.
2368    if d_perp > 1e-7 {
2369        return Ok(None);
2370    }
2371
2372    // Coaxial: the sphere center lies on the cylinder axis. No real circle
2373    // when the cylinder is wider than the sphere or they are tangent-internal.
2374    if r_cyl > r_sphere + 1e-10 {
2375        return Ok(Some(vec![]));
2376    }
2377    let z_sq = r_sphere * r_sphere - r_cyl * r_cyl;
2378    if z_sq < 0.0 {
2379        return Ok(Some(vec![]));
2380    }
2381    let z = z_sq.sqrt();
2382
2383    // The sphere center projected onto the axis is the midpoint of the two
2384    // section circles, each offset by ±z along the axis with radius `r_cyl`.
2385    let center_axis_pt = Point3::new(
2386        co.x() + axis.x() * along,
2387        co.y() + axis.y() * along,
2388        co.z() + axis.z() * along,
2389    );
2390
2391    let mut circles = Vec::new();
2392    let offsets: &[f64] = if z < 1e-10 { &[0.0] } else { &[z, -z] };
2393    for &z_offset in offsets {
2394        let center = Point3::new(
2395            center_axis_pt.x() + axis.x() * z_offset,
2396            center_axis_pt.y() + axis.y() * z_offset,
2397            center_axis_pt.z() + axis.z() * z_offset,
2398        );
2399        let circle = Circle3D::new(center, axis, r_cyl)?;
2400        circles.push(ExactIntersectionCurve::Circle(circle));
2401    }
2402    Ok(Some(circles))
2403}
2404
2405/// Exact coaxial cone-sphere intersection: the shared circles.
2406///
2407/// When the cone's axis runs through the sphere's centre `C`, every generator
2408/// `apex + v g` (`g` a unit direction, `v` the distance from the apex) has
2409/// the same `h = g·(apex − C)`, so all of them meet the sphere at the same
2410/// roots of `v² + 2hv + |apex − C|² − R² = 0`. Each root ahead of the apex is
2411/// a circle of radius `v cos a` at `v sin a` along the axis: two for a ball
2412/// the cone passes through, one for a ball that swallows the apex, passes
2413/// through it or touches the wall, none for a ball the cone misses or that
2414/// sits inside it clear of the wall.
2415///
2416/// Returns `Some(vec![..])` for a coaxial pair and `None` (the ruling trace
2417/// or the marcher) otherwise.
2418///
2419/// # Errors
2420///
2421/// Returns [`MathError`] if a shared `Circle3D` cannot be constructed.
2422pub fn exact_cone_sphere(
2423    cone: &ConicalSurface,
2424    sphere: &SphericalSurface,
2425) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2426    let offset = cone.apex() - sphere.center();
2427    let along = offset.dot(cone.axis());
2428    if (offset - cone.axis() * along).length() > 1e-7 {
2429        return Ok(None);
2430    }
2431    let lin_tol = Tolerance::new().linear;
2432    let (sin_a, cos_a) = cone.half_angle().sin_cos();
2433    let (far_sq, radius_sq) = (offset.dot(offset), sphere.radius() * sphere.radius());
2434    let b = 2.0 * sin_a * along;
2435    let (disc, far, near) = ruling_quadratic(1.0, b, far_sq - radius_sq);
2436    // At a tangent ball `b² − 4c` cancels to a few ulps of its terms, which
2437    // can land below zero: that is a touch, not a miss.
2438    let noise = 16.0 * f64::EPSILON * 4.0f64.mul_add(far_sq + radius_sq, b * b);
2439    if disc < -noise {
2440        return Ok(Some(vec![]));
2441    }
2442    let roots: &[f64] = if far - near < lin_tol {
2443        &[far]
2444    } else {
2445        &[near, far]
2446    };
2447    let mut circles = Vec::new();
2448    for &v in roots {
2449        if v * cos_a > lin_tol {
2450            let centre = cone.apex() + cone.axis() * (v * sin_a);
2451            let circle = Circle3D::new(centre, cone.axis(), v * cos_a)?;
2452            circles.push(ExactIntersectionCurve::Circle(circle));
2453        }
2454    }
2455    Ok(Some(circles))
2456}
2457
2458/// Algebraic sphere-cylinder intersection (NURBS form for the general bounded
2459/// path). A coaxial pair delegates to [`exact_sphere_cylinder`] and samples
2460/// each exact circle into an interpolated NURBS `IntersectionCurve`. phase FF
2461/// prefers the exact circle form directly (so the section edge links to the
2462/// coincident boundary and the closed-circle splitter can carve the spherical
2463/// band), but a caller of `intersect_analytic_analytic_bounded` still gets
2464/// clean curves instead of the marcher's fragments. Any other pair is traced
2465/// along the cylinder's rulings ([`off_axis_sphere_cylinder`]).
2466fn algebraic_sphere_cylinder(
2467    sphere: &SphericalSurface,
2468    cyl: &CylindricalSurface,
2469    sphere_first: bool,
2470) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
2471    let Some(exacts) = exact_sphere_cylinder(sphere, cyl)? else {
2472        return Ok(off_axis_sphere_cylinder(sphere, cyl, sphere_first));
2473    };
2474
2475    let mut curves = Vec::new();
2476    for exact in exacts {
2477        let ExactIntersectionCurve::Circle(circle) = exact else {
2478            continue;
2479        };
2480        let n_samples = 33;
2481        let mut points = Vec::with_capacity(n_samples);
2482        let mut positions = Vec::with_capacity(n_samples);
2483        #[allow(clippy::cast_precision_loss)]
2484        for i in 0..n_samples {
2485            let theta = TAU * i as f64 / (n_samples - 1) as f64;
2486            let pt = crate::traits::ParametricCurve::evaluate(&circle, theta);
2487            positions.push(pt);
2488            let (param1, param2) = in_order(
2489                sphere.project_point(pt),
2490                cyl.project_point(pt),
2491                sphere_first,
2492            );
2493            points.push(IntersectionPoint {
2494                point: pt,
2495                param1,
2496                param2,
2497            });
2498        }
2499        let degree = 3.min(positions.len() - 1);
2500        let curve = interpolate(&positions, degree)?;
2501        curves.push(IntersectionCurve { curve, points });
2502    }
2503
2504    Ok(Some(curves))
2505}
2506
2507/// A sphere and a cylinder whose axis misses the sphere's centre (a drill
2508/// entering a ball off its axis), traced along the cylinder's rulings: the
2509/// ruling `c(u) + v·a` meets the sphere where `v² + 2(q·a)·v + |q|² − R² = 0`,
2510/// with `q = c(u) − C`. When every ruling meets the sphere (the cylinder
2511/// passes wholly through it) the roots trace an entry and an exit loop;
2512/// otherwise each window of meeting rulings carries one loop. `Some(empty)`
2513/// when the two cannot meet, `None` when the sampling misses a window
2514/// narrower than itself.
2515fn off_axis_sphere_cylinder(
2516    sphere: &SphericalSurface,
2517    cyl: &CylindricalSurface,
2518    sphere_first: bool,
2519) -> Option<Vec<IntersectionCurve>> {
2520    let (centre, radius) = (sphere.center(), sphere.radius());
2521    let axis = cyl.axis();
2522    let offset = centre - cyl.origin();
2523    let axis_distance = (offset - axis * offset.dot(axis)).length();
2524    let lin_tol = Tolerance::new().linear;
2525    if axis_distance > radius + cyl.radius() + lin_tol
2526        || axis_distance + radius < cyl.radius() - lin_tol
2527    {
2528        return Some(Vec::new());
2529    }
2530    let roots = |u: f64| {
2531        let q = cyl.evaluate(u, 0.0) - centre;
2532        ruling_quadratic(1.0, 2.0 * q.dot(axis), q.dot(q) - radius * radius)
2533    };
2534    let samples = ruling_samples(cyl, &roots);
2535    let loops = if samples.iter().all(Option::is_some) {
2536        closed_ruling_loops(cyl, &roots, &samples)
2537    } else {
2538        partial_ruling_loops(cyl, &roots, &samples)
2539    };
2540    if loops.is_empty() {
2541        return None;
2542    }
2543    Some(fit_ruling_loops(&loops, |p| {
2544        in_order(sphere.project_point(p), cyl.project_point(p), sphere_first)
2545    }))
2546}
2547
2548/// Parameters on the pair's first and second surfaces, from those on `a`
2549/// and `b` and whether `a` came first.
2550const fn in_order(a: (f64, f64), b: (f64, f64), a_first: bool) -> ((f64, f64), (f64, f64)) {
2551    if a_first { (a, b) } else { (b, a) }
2552}
2553
2554/// Algebraic cylinder-cylinder intersection for non-coaxial cylinders.
2555///
2556/// For two cylinders with axes that are NOT parallel, the intersection
2557/// consists of up to two closed space curves. These are found by
2558/// parameterizing one cylinder's angular coordinate `u ∈ [0, 2π]` and
2559/// solving a quadratic in the axial parameter `v` to find where each
2560/// "ring" of cylinder A sits on cylinder B.
2561///
2562/// The quadratic is:
2563///   `v²·(1 - α²) + 2v·(q·a₁ - α·q·a₂) + (|q|² - (q·a₂)² - r₂²) = 0`
2564/// where `α = a₁·a₂`, `q(u)` is the radial point on cylinder 1 minus
2565/// cylinder 2's origin, `a₁`/`a₂` are the cylinder axes, and `r₂` is
2566/// cylinder 2's radius.
2567#[allow(clippy::too_many_lines, clippy::unnecessary_wraps)]
2568fn algebraic_cylinder_cylinder(
2569    c1: &CylindricalSurface,
2570    c2: &CylindricalSurface,
2571) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
2572    let alpha = c1.axis().dot(c2.axis());
2573    let a_coeff = 1.0 - alpha * alpha;
2574
2575    // Should only be called for non-parallel axes.
2576    if a_coeff.abs() < 1e-12 {
2577        return Ok(None);
2578    }
2579
2580    let r1 = c1.radius();
2581    let r2 = c2.radius();
2582    let o1 = c1.origin();
2583    let o2 = c2.origin();
2584    let a1 = c1.axis();
2585    let a2 = c2.axis();
2586
2587    // Separation check: distance between axes vs sum of radii.
2588    // Closest approach of two skew lines:
2589    let delta = Vec3::new(o1.x() - o2.x(), o1.y() - o2.y(), o1.z() - o2.z());
2590    let cross = a1.cross(a2);
2591    let cross_len = cross.length();
2592    if cross_len > 1e-12 {
2593        let axis_dist = delta.dot(cross).abs() / cross_len;
2594        if axis_dist > r1 + r2 + Tolerance::new().linear {
2595            return Ok(Some(vec![])); // No intersection
2596        }
2597    }
2598
2599    // Solve every ruling of one cylinder against the other. When EVERY
2600    // ruling of the swept cylinder meets the other (the thinner of two
2601    // crossing tubes), the two roots trace the curve's two closed loops.
2602    // Swept the other way only a window of rulings meets, and each root
2603    // traces an open arc of one loop.
2604    let roots = |sweep: &CylindricalSurface, other: &CylindricalSurface| {
2605        let (o, a, radius) = (other.origin(), other.axis(), other.radius());
2606        let alpha = sweep.axis().dot(a);
2607        let quad = 1.0 - alpha * alpha;
2608        let (axis, sweep) = (sweep.axis(), sweep.clone());
2609        move |u: f64| {
2610            let q = sweep.evaluate(u, 0.0) - o;
2611            let (q_a1, q_a2) = (q.dot(axis), q.dot(a));
2612            let b = 2.0 * (q_a1 - alpha * q_a2);
2613            let c = q.dot(q) - q_a2 * q_a2 - radius * radius;
2614            ruling_quadratic(quad, b, c)
2615        }
2616    };
2617    let (roots1, roots2) = (roots(c1, c2), roots(c2, c1));
2618    let samples1 = ruling_samples(c1, &roots1);
2619    let loops = if samples1.iter().all(Option::is_some) {
2620        closed_ruling_loops(c1, &roots1, &samples1)
2621    } else {
2622        let samples2 = ruling_samples(c2, &roots2);
2623        if samples2.iter().all(Option::is_some) {
2624            closed_ruling_loops(c2, &roots2, &samples2)
2625        } else if samples1.iter().any(Option::is_some) {
2626            partial_ruling_loops(c1, &roots1, &samples1)
2627        } else {
2628            partial_ruling_loops(c2, &roots2, &samples2)
2629        }
2630    };
2631    if loops.is_empty() {
2632        return Ok(None);
2633    }
2634    Ok(Some(fit_ruling_loops(&loops, |p| {
2635        (c1.project_point(p), c2.project_point(p))
2636    })))
2637}
2638
2639/// A cone and a cylinder whose axes are not parallel, traced along the
2640/// cylinder's rulings. A ruling `q + t w` meets the cone's double quadric
2641/// `|p - apex|^2 = h^2 / sin^2(half_angle)`, with `h` the offset along the
2642/// cone's axis, where a quadratic in `t` vanishes. `None` (the marcher's
2643/// case) when a ruling meets the far nappe, where no cone face lies, when
2644/// the rulings run along the cone's generators, or when no ruling meets it.
2645fn ruling_cone_cylinder(
2646    cone: &ConicalSurface,
2647    cyl: &CylindricalSurface,
2648    cone_first: bool,
2649) -> Option<Vec<IntersectionCurve>> {
2650    let (sin_t, cos_t) = cone.half_angle().sin_cos();
2651    if sin_t < 1e-12 || cos_t < 1e-12 {
2652        return None;
2653    }
2654    let (apex, d, w) = (cone.apex(), cone.axis(), cyl.axis());
2655    let s = 1.0 / (sin_t * sin_t);
2656    let alpha = w.dot(d);
2657    let quad = 1.0 - s * alpha * alpha;
2658    if quad.abs() < 1e-9 {
2659        return None;
2660    }
2661    let roots = |u: f64| {
2662        let delta = cyl.evaluate(u, 0.0) - apex;
2663        let (dd, dw) = (delta.dot(d), delta.dot(w));
2664        let b = 2.0 * (dw - s * dd * alpha);
2665        let c = delta.dot(delta) - s * dd * dd;
2666        ruling_quadratic(quad, b, c)
2667    };
2668    let lin_tol = Tolerance::new().linear;
2669    let far_nappe = (0..WINDOW_SCAN * RULING_SAMPLES).any(|k| {
2670        #[allow(clippy::cast_precision_loss)]
2671        let u = TAU * (k as f64 + 0.5) / (WINDOW_SCAN * RULING_SAMPLES) as f64;
2672        let (disc, vp, vm) = roots(u);
2673        disc >= -lin_tol
2674            && [vp, vm]
2675                .iter()
2676                .any(|&t| (cyl.evaluate(u, t) - apex).dot(d) < -lin_tol)
2677    });
2678    if far_nappe {
2679        return None;
2680    }
2681    let samples = ruling_samples(cyl, &roots);
2682    // A window of meeting rulings narrower than the sampling would vanish
2683    // (a cone's tip just through the wall) while the others still made
2684    // loops; a finer scan finds every window, and any that the sampling
2685    // covers thinly goes to the marcher.
2686    let scan = WINDOW_SCAN * RULING_SAMPLES;
2687    // Ruling sample `i` lies midway between scan points
2688    // `WINDOW_SCAN i + 7` and `WINDOW_SCAN i + 8`.
2689    #[allow(clippy::cast_precision_loss)]
2690    let meets = |k: usize| roots(TAU * ((k % scan) as f64 + 0.5) / scan as f64).0 >= -lin_tol;
2691    if let Some(start) = (0..scan).find(|&k| !meets(k)) {
2692        let mut k = start;
2693        while k < start + scan {
2694            if !meets(k) {
2695                k += 1;
2696                continue;
2697            }
2698            let first = k;
2699            while k < start + scan && meets(k) {
2700                k += 1;
2701            }
2702            let covered = (first..k)
2703                .filter(|&j| j % WINDOW_SCAN == WINDOW_SCAN / 2 - 1 && meets(j + 1))
2704                .count();
2705            if covered < WINDOW_MIN_SAMPLES {
2706                return None;
2707            }
2708        }
2709    }
2710    let loops = if samples.iter().all(Option::is_some) {
2711        closed_ruling_loops(cyl, &roots, &samples)
2712    } else {
2713        partial_ruling_loops(cyl, &roots, &samples)
2714    };
2715    if loops.is_empty() {
2716        return None;
2717    }
2718    Some(fit_ruling_loops(&loops, |p| {
2719        in_order(cone.project_point(p), cyl.project_point(p), cone_first)
2720    }))
2721}
2722
2723/// A torus and a cylinder whose axes are not parallel, where every ruling
2724/// of the cylinder meets the torus the same nonzero even number of times (a
2725/// rod through the ring's tube): a ruling meets the torus where a quartic in
2726/// its parameter vanishes, and each of its roots, taken in order, sweeps one
2727/// closed loop around the cylinder. `None` (the marcher's case) when the
2728/// count varies between rulings, where the curve turns back between them,
2729/// checked on a scan finer than the sampling so a narrow window of missing
2730/// rulings is not stepped over, and for a spindle torus, whose quartic also
2731/// holds its inner lemon.
2732fn ruling_torus_cylinder(
2733    torus: &ToroidalSurface,
2734    cyl: &CylindricalSurface,
2735    torus_first: bool,
2736) -> Option<Vec<IntersectionCurve>> {
2737    if cyl.axis().dot(torus.z_axis()).abs() > 1.0 - 1e-9
2738        || torus.minor_radius() >= torus.major_radius()
2739    {
2740        return None;
2741    }
2742    let roots = |u: f64| intersect_line_torus(torus, cyl.evaluate(u, 0.0), cyl.axis());
2743    let rows: Vec<Vec<f64>> = (0..RULING_SAMPLES).map(|i| roots(ruling_u(i))).collect();
2744    let count = rows[0].len();
2745    let scan = WINDOW_SCAN * RULING_SAMPLES;
2746    #[allow(clippy::cast_precision_loss)]
2747    if count == 0
2748        || count % 2 == 1
2749        || (0..scan).any(|k| roots(TAU * (k as f64 + 0.5) / scan as f64).len() != count)
2750    {
2751        return None;
2752    }
2753    let loops: Vec<Vec<Point3>> = (0..count)
2754        .map(|j| {
2755            let mut pts: Vec<Point3> = rows
2756                .iter()
2757                .enumerate()
2758                .map(|(i, r)| cyl.evaluate(ruling_u(i), r[j]))
2759                .collect();
2760            pts.push(pts[0]);
2761            pts
2762        })
2763        .collect();
2764    Some(fit_ruling_loops(&loops, |p| {
2765        in_order(torus.project_point(p), cyl.project_point(p), torus_first)
2766    }))
2767}
2768
2769/// A cone and a sphere whose centre is off the cone's axis, where every
2770/// generator of the cone crosses the sphere twice on the cone's nappe (a pin
2771/// through a ball): along a generator `apex + v g` the sphere is a quadratic
2772/// in `v`, and each of its two roots, taken in order, sweeps one closed loop
2773/// around the cone. A ball holding the apex is left once by every generator,
2774/// one loop. `None` when the centre lies on the axis (phase FF takes
2775/// [`exact_cone_sphere`]'s circles there), or the apex on the sphere. A
2776/// sphere only some generators cross goes to [`window_cone_sphere`].
2777fn ruling_cone_sphere(
2778    cone: &ConicalSurface,
2779    sphere: &SphericalSurface,
2780    cone_first: bool,
2781) -> Option<Vec<IntersectionCurve>> {
2782    let (apex, centre, radius) = (cone.apex(), sphere.center(), sphere.radius());
2783    let offset = apex - centre;
2784    let lin_tol = Tolerance::new().linear;
2785    let along = offset.dot(cone.axis());
2786    let across = (offset - cone.axis() * along).length();
2787    if across < lin_tol {
2788        return None;
2789    }
2790    // With the apex inside the ball, `v² + 2hv + K` has roots of opposite
2791    // signs (`K = |offset|² − R² < 0`): every generator leaves the ball once
2792    // ahead of the apex, at `v = √(h² − K) − h`, one loop round the cone.
2793    // Where `h > 0` the root is taken as `−K / (h + √(h² − K))`, free of the
2794    // cancellation.
2795    let k = offset.dot(offset) - radius * radius;
2796    if radius - offset.length() > lin_tol {
2797        let exit = |u: f64| {
2798            let h = (cone.evaluate(u, 1.0) - apex).dot(offset);
2799            let root = h.mul_add(h, -k).sqrt();
2800            cone.evaluate(u, if h > 0.0 { -k / (h + root) } else { root - h })
2801        };
2802        // A ball that barely holds the apex turns the loop sharply where it
2803        // passes the apex, at the scale of `√−K`: a step whose midpoint
2804        // strays from its chord by a hundredth of the chord (a turn of about
2805        // 0.08 radians) is halved until the turn is resolved.
2806        let mut samples: Vec<(f64, Point3)> = (0..=RULING_SAMPLES)
2807            .map(|i| (ruling_u(i), exit(ruling_u(i))))
2808            .collect();
2809        for _ in 0..10 {
2810            let mut refined = Vec::with_capacity(2 * samples.len());
2811            for pair in samples.windows(2) {
2812                let ((u0, p0), (u1, p1)) = (pair[0], pair[1]);
2813                refined.push(pair[0]);
2814                let um = 0.5 * (u0 + u1);
2815                let pm = exit(um);
2816                let chord = (p1 - p0).length();
2817                if chord > lin_tol && (pm - (p0 + (p1 - p0) * 0.5)).length() > 0.01 * chord {
2818                    refined.push((um, pm));
2819                }
2820            }
2821            refined.extend(samples.last().copied());
2822            if refined.len() == samples.len() {
2823                break;
2824            }
2825            samples = refined;
2826        }
2827        let mut pts: Vec<Point3> = samples.iter().map(|&(_, p)| p).collect();
2828        if let Some(last) = pts.last_mut() {
2829            *last = samples[0].1;
2830        }
2831        return Some(fit_ruling_loops(&[pts], |p| {
2832            in_order(cone.project_point(p), sphere.project_point(p), cone_first)
2833        }));
2834    }
2835    // Both roots along a generator of unit direction `g` (`v` the distance
2836    // from the apex), nearer first, from `h = g·offset`, when it crosses the
2837    // sphere twice ahead of the apex.
2838    let crossing = |h: f64| {
2839        let (disc, vp, vm) = ruling_quadratic(1.0, 2.0 * h, k);
2840        (disc > lin_tol && vm >= lin_tol).then_some((vm, vp))
2841    };
2842    // Around the cone `h` runs between these bounds. Where both roots lie
2843    // ahead of the apex, raising `h` shrinks the discriminant and moves the
2844    // nearer root out, so every generator crosses when the two extreme ones
2845    // do.
2846    let (sin_a, cos_a) = cone.half_angle().sin_cos();
2847    if crossing(sin_a.mul_add(along, cos_a * across)).is_none()
2848        || crossing(sin_a.mul_add(along, -cos_a * across)).is_none()
2849    {
2850        return window_cone_sphere(cone, sphere, cone_first);
2851    }
2852    let rows: Vec<(f64, f64)> = (0..RULING_SAMPLES)
2853        .map(|i| crossing((cone.evaluate(ruling_u(i), 1.0) - apex).dot(offset)))
2854        .collect::<Option<_>>()?;
2855    let loops: Vec<Vec<Point3>> = [0, 1]
2856        .iter()
2857        .map(|&j| {
2858            let mut pts: Vec<Point3> = rows
2859                .iter()
2860                .enumerate()
2861                .map(|(i, &(near, far))| {
2862                    cone.evaluate(ruling_u(i), if j == 0 { near } else { far })
2863                })
2864                .collect();
2865            pts.push(pts[0]);
2866            pts
2867        })
2868        .collect();
2869    Some(fit_ruling_loops(&loops, |p| {
2870        in_order(cone.project_point(p), sphere.project_point(p), cone_first)
2871    }))
2872}
2873
2874/// A cone and a sphere off its axis that only some generators cross (a ball
2875/// against the cone's side), with the apex outside the sphere: along the
2876/// generator at `u`, `h(u) = g·(apex − C)` is `c + A cos(u − φ)`, and the
2877/// generator crosses twice ahead of the apex where `h ≤ −√K`, with
2878/// `K = |apex − C|² − R²`. That is one arc of `u`, whose ends are where the
2879/// generator touches the sphere; the section is one loop, the nearer roots
2880/// out along the arc and the farther ones back. Sampled at
2881/// `u = mid − half·cos θ`, the roots' split `±√(h² − K)` changes sign with
2882/// `sin θ` and the loop stays smooth through both touching generators.
2883/// `Some(empty)` when no generator crosses ahead of the apex. `None`, the
2884/// marcher's case, when the apex lies within the sphere, when the centre is
2885/// too near the axis to place the arc, and when every generator reaches the
2886/// sphere (the extremes' test then failed on a touching generator or a root
2887/// at the apex).
2888fn window_cone_sphere(
2889    cone: &ConicalSurface,
2890    sphere: &SphericalSurface,
2891    cone_first: bool,
2892) -> Option<Vec<IntersectionCurve>> {
2893    let offset = cone.apex() - sphere.center();
2894    let lin_tol = Tolerance::new().linear;
2895    if offset.length() - sphere.radius() <= lin_tol {
2896        return None;
2897    }
2898    let k = offset.dot(offset) - sphere.radius() * sphere.radius();
2899    let (sin_a, cos_a) = cone.half_angle().sin_cos();
2900    let (ox, oy) = (offset.dot(cone.x_axis()), offset.dot(cone.y_axis()));
2901    let (c, a) = (sin_a * offset.dot(cone.axis()), cos_a * ox.hypot(oy));
2902    if a < lin_tol {
2903        return None;
2904    }
2905    let reach = (-k.sqrt() - c) / a;
2906    if reach <= -1.0 {
2907        return Some(Vec::new());
2908    }
2909    if reach >= 1.0 {
2910        return None;
2911    }
2912    let (mid, half) = (oy.atan2(ox) + std::f64::consts::PI, reach.acos());
2913    let half = std::f64::consts::PI - half;
2914    let n = RULING_SAMPLES;
2915    let mut pts: Vec<Point3> = (0..n)
2916        .map(|i| {
2917            #[allow(clippy::cast_precision_loss)]
2918            let theta = TAU * i as f64 / n as f64;
2919            let u = half.mul_add(-theta.cos(), mid);
2920            let h = a.mul_add((u - mid + std::f64::consts::PI).cos(), c);
2921            let split = h.mul_add(h, -k).max(0.0).sqrt();
2922            cone.evaluate(u, -h - split.copysign(theta.sin()))
2923        })
2924        .collect();
2925    pts.push(pts[0]);
2926    Some(fit_ruling_loops(&[pts], |p| {
2927        in_order(cone.project_point(p), sphere.project_point(p), cone_first)
2928    }))
2929}
2930
2931/// Scan points per ruling sample when looking for windows of meeting
2932/// rulings, and the fewest samples a window needs to be fit.
2933const WINDOW_SCAN: usize = 16;
2934const WINDOW_MIN_SAMPLES: usize = 8;
2935
2936/// Rulings sampled around a swept cylinder, half a step off u = 0 so the
2937/// branches of a self-touching curve (equal crossing cylinders) do not share
2938/// a sample.
2939const RULING_SAMPLES: usize = 128;
2940
2941#[allow(clippy::cast_precision_loss)]
2942fn ruling_u(i: usize) -> f64 {
2943    TAU * (i as f64 + 0.5) / RULING_SAMPLES as f64
2944}
2945
2946/// The discriminant and roots of `quad·v² + b·v + c = 0`.
2947fn ruling_quadratic(quad: f64, b: f64, c: f64) -> (f64, f64, f64) {
2948    let disc = b * b - 4.0 * quad * c;
2949    let root = disc.max(0.0).sqrt();
2950    (disc, (-b + root) / (2.0 * quad), (-b - root) / (2.0 * quad))
2951}
2952
2953/// The two points where each sampled ruling of `sweep` meets the other
2954/// surface, from `roots(u)` (the discriminant and the two axial parameters),
2955/// or `None` for a ruling that misses it.
2956fn ruling_samples(
2957    sweep: &CylindricalSurface,
2958    roots: &impl Fn(f64) -> (f64, f64, f64),
2959) -> Vec<Option<(Point3, Point3)>> {
2960    let lin_tol = Tolerance::new().linear;
2961    (0..RULING_SAMPLES)
2962        .map(|i| {
2963            let u = ruling_u(i);
2964            let (disc, vp, vm) = roots(u);
2965            (disc >= -lin_tol).then(|| (sweep.evaluate(u, vp), sweep.evaluate(u, vm)))
2966        })
2967        .collect()
2968}
2969
2970/// Every ruling meets the other surface: each root traces a closed loop.
2971/// Where the loops pass closest at one ruling they are sampled from it,
2972/// closer together near it, so the fit follows the narrow neck between
2973/// them; where they touch there, they are the two halves of one curve
2974/// crossing itself, each with a corner at that point, which then falls at
2975/// their shared start and end, where the fit keeps it.
2976fn closed_ruling_loops(
2977    sweep: &CylindricalSurface,
2978    roots: &impl Fn(f64) -> (f64, f64, f64),
2979    samples: &[Option<(Point3, Point3)>],
2980) -> Vec<Vec<Point3>> {
2981    let (mut plus, mut minus): (Vec<Point3>, Vec<Point3>) =
2982        if let Some((neck, touching)) = narrowest_ruling(roots) {
2983            let count = 2 * RULING_SAMPLES;
2984            #[allow(clippy::cast_precision_loss)]
2985            (0..count)
2986                .map(|i| {
2987                    let t = i as f64 / count as f64;
2988                    let u = TAU.mul_add(t - 0.9 * (TAU * t).sin() / TAU, neck);
2989                    let (_, vp, vm) = roots(u);
2990                    if i == 0 && touching {
2991                        let at = sweep.evaluate(u, 0.5 * (vp + vm));
2992                        (at, at)
2993                    } else {
2994                        (sweep.evaluate(u, vp), sweep.evaluate(u, vm))
2995                    }
2996                })
2997                .unzip()
2998        } else {
2999            samples.iter().flatten().copied().unzip()
3000        };
3001    plus.push(plus[0]);
3002    minus.push(minus[0]);
3003    vec![plus, minus]
3004}
3005
3006/// The ruling where the two roots pass closest, when exactly one ruling
3007/// stands out: a local minimum of their gap, scanned finer than the
3008/// sampling and refined, that either closes to within tolerance (the
3009/// roots touch there; the tolerance scales with the widest gap, the loops'
3010/// own size, so the decision does not move with the model) or narrows
3011/// below a quarter of the widest gap while no other minimum does. Equal
3012/// crossing cylinders touch at two rulings and get `None`.
3013fn narrowest_ruling(roots: &impl Fn(f64) -> (f64, f64, f64)) -> Option<(f64, bool)> {
3014    let scan = WINDOW_SCAN * RULING_SAMPLES;
3015    #[allow(clippy::cast_precision_loss)]
3016    let step = TAU / scan as f64;
3017    let gap = |u: f64| {
3018        let (_, vp, vm) = roots(u);
3019        (vp - vm).abs()
3020    };
3021    #[allow(clippy::cast_precision_loss)]
3022    let gaps: Vec<f64> = (0..scan).map(|k| gap(step * k as f64)).collect();
3023    let widest = gaps.iter().copied().fold(0.0, f64::max);
3024    let tol = Tolerance::new().linear * (1.0 + widest);
3025    let mut necks = Vec::new();
3026    for k in 0..scan {
3027        let (before, here, after) = (gaps[(k + scan - 1) % scan], gaps[k], gaps[(k + 1) % scan]);
3028        if here > before || here >= after || here >= 0.25 * widest {
3029            continue;
3030        }
3031        #[allow(clippy::cast_precision_loss)]
3032        let (mut lo, mut hi) = (step * (k as f64 - 1.0), step * (k as f64 + 1.0));
3033        for _ in 0..100 {
3034            let (a, b) = (lo + (hi - lo) / 3.0, hi - (hi - lo) / 3.0);
3035            if gap(a) < gap(b) {
3036                hi = b;
3037            } else {
3038                lo = a;
3039            }
3040        }
3041        let u = 0.5 * (lo + hi);
3042        necks.push((u, gap(u) <= tol));
3043    }
3044    match necks[..] {
3045        [neck] => Some(neck),
3046        _ => None,
3047    }
3048}
3049
3050/// Only windows of rulings meet the other surface: each cyclic window
3051/// carries one loop, out along one root and back along the other, the two
3052/// joined where the discriminant vanishes. Empty when no sample meets it (a
3053/// window narrower than the sampling).
3054fn partial_ruling_loops(
3055    sweep: &CylindricalSurface,
3056    roots: &impl Fn(f64) -> (f64, f64, f64),
3057    samples: &[Option<(Point3, Point3)>],
3058) -> Vec<Vec<Point3>> {
3059    let branch_point = |inside: usize, outside: usize| -> Point3 {
3060        let (mut lo, mut hi) = (ruling_u(inside), ruling_u(outside));
3061        if (hi - lo).abs() > std::f64::consts::PI {
3062            hi += if hi < lo { TAU } else { -TAU };
3063        }
3064        for _ in 0..60 {
3065            let mid = 0.5 * (lo + hi);
3066            if roots(mid).0 >= 0.0 {
3067                lo = mid;
3068            } else {
3069                hi = mid;
3070            }
3071        }
3072        let (_, vp, vm) = roots(lo);
3073        sweep.evaluate(lo, 0.5 * (vp + vm))
3074    };
3075    let Some(first_gap) = samples.iter().position(Option::is_none) else {
3076        return Vec::new();
3077    };
3078    let mut loops = Vec::new();
3079    let mut k = 0;
3080    while k < RULING_SAMPLES {
3081        let i = (first_gap + k) % RULING_SAMPLES;
3082        if samples[i].is_none() {
3083            k += 1;
3084            continue;
3085        }
3086        let start = i;
3087        let mut run = Vec::new();
3088        while k < RULING_SAMPLES {
3089            let j = (first_gap + k) % RULING_SAMPLES;
3090            let Some(pair) = samples[j] else { break };
3091            run.push(pair);
3092            k += 1;
3093        }
3094        let end = (start + run.len() - 1) % RULING_SAMPLES;
3095        let head = branch_point(start, (start + RULING_SAMPLES - 1) % RULING_SAMPLES);
3096        let tail = branch_point(end, (end + 1) % RULING_SAMPLES);
3097        let mut pts = vec![head];
3098        pts.extend(run.iter().map(|p| p.0));
3099        pts.push(tail);
3100        pts.extend(run.iter().rev().map(|p| p.1));
3101        pts.push(head);
3102        loops.push(pts);
3103    }
3104    loops
3105}
3106
3107/// Cubic interpolants through the swept loops, with `params(p)` giving each
3108/// point's parameters on the two surfaces.
3109fn fit_ruling_loops(
3110    loops: &[Vec<Point3>],
3111    params: impl Fn(Point3) -> ((f64, f64), (f64, f64)),
3112) -> Vec<IntersectionCurve> {
3113    let mut curves = Vec::new();
3114    for pts in loops {
3115        if pts.len() < 4 {
3116            continue;
3117        }
3118        let ipts: Vec<IntersectionPoint> = pts
3119            .iter()
3120            .map(|&p| {
3121                let (param1, param2) = params(p);
3122                IntersectionPoint {
3123                    point: p,
3124                    param1,
3125                    param2,
3126                }
3127            })
3128            .collect();
3129        let degree = 3.min(pts.len() - 1);
3130        if let Ok(curve) = interpolate(pts, degree) {
3131            curves.push(IntersectionCurve {
3132                curve,
3133                points: ipts,
3134            });
3135        }
3136    }
3137    curves
3138}
3139
3140/// Algebraic cone-cylinder intersection for PARALLEL (or antiparallel) axes.
3141///
3142/// When the axes are parallel, every plane perpendicular to them cuts the cone
3143/// in a circle of radius `rho = v * cos(half_angle)` about a FIXED centre and
3144/// the cylinder in a circle of radius `R` about a second FIXED centre, so the
3145/// axis separation `d` is constant in `v`. Two coplanar circles meet at
3146/// `u = phi0 +/- acos((d^2 + rho^2 - R^2) / (2*d*rho))`, giving two branches
3147/// parameterised exactly by the cone's own `v`. The branches exist only where
3148/// `rho` lies in `[|d - R|, d + R]`, which bounds the curve naturally.
3149///
3150/// This replaces the general grid-seeded marcher for the configuration, which
3151/// mis-handles it badly: seeds are accepted anywhere within half the surface
3152/// diagonal of the partner, the march-result dedup only consumes seeds the
3153/// traced polyline passes near, and the survivors are dozens of overlapping
3154/// partial traces of the same curve. Those fragments carry no usable in-face
3155/// span, so a cone corner-round crossed by a boss cylinder never splits (a
3156/// counterbore/countersink meeting a pad — the gridfinity lightweight base).
3157///
3158/// Returns `None` (defer to the caller's other paths) when the axes are not
3159/// parallel, or when they are coaxial — a coaxial pair degenerates to shared
3160/// circles, which [`exact_cone_cylinder`] emits exactly and phase FF calls
3161/// directly. Note that `intersect_analytic_analytic_bounded` does NOT consult
3162/// `exact_cone_cylinder`, so a coaxial pair reaching this path through that
3163/// caller falls through to the marcher; only the FF path gets the exact circles.
3164// Result-wrapped to match the other `try_algebraic_intersection` arms' shape.
3165#[allow(clippy::unnecessary_wraps)]
3166fn algebraic_parallel_cone_cylinder(
3167    cone: &ConicalSurface,
3168    cyl: &CylindricalSurface,
3169    v_range_cone: Option<(f64, f64)>,
3170    v_range_cyl: Option<(f64, f64)>,
3171) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
3172    let axis = cone.axis();
3173    if axis.dot(cyl.axis()).abs() < 1.0 - 1e-10 {
3174        return Ok(None); // Skew or oblique: `ruling_cone_cylinder` traces it.
3175    }
3176
3177    let apex = cone.apex();
3178    let delta = cyl.origin() - apex;
3179    let along = delta.dot(axis);
3180    let perp = delta - axis * along;
3181    let d = perp.length();
3182    if d < 1e-9 {
3183        return Ok(None); // Coaxial — `exact_cone_cylinder` owns this.
3184    }
3185
3186    let (e1, e2) = (cone.x_axis(), cone.y_axis());
3187    let phi0 = perp.dot(e2).atan2(perp.dot(e1));
3188
3189    let (sin_t, cos_t) = cone.half_angle().sin_cos();
3190    if cos_t < 1e-12 || sin_t < 1e-12 {
3191        return Ok(None);
3192    }
3193    let r = cyl.radius();
3194
3195    // Branch existence: |d - R| <= rho <= d + R, with rho = v * cos(half_angle).
3196    let mut v_min = (d - r).abs() / cos_t;
3197    let mut v_max = (d + r) / cos_t;
3198    if v_max <= v_min {
3199        return Ok(Some(vec![]));
3200    }
3201
3202    // Narrow the sampled span to the faces' own extents so the fixed sample
3203    // budget resolves the in-face part of the curve rather than spreading over
3204    // a loop that mostly lies off both patches. A face's crossing can be a
3205    // fraction of a degree of the cone's sweep (the corner-round case above),
3206    // and an unnarrowed sampling puts fewer than one sample across it.
3207    let mut lo = v_min;
3208    let mut hi = v_max;
3209    // Clip EXACTLY to the hints, not to a padded window: an endpoint that lands
3210    // exactly on the face's own v-limit lies ON that boundary rim, so the
3211    // downstream pave machinery anchors it to the rim edge instead of leaving
3212    // the section dangling just past the face.
3213    if let Some((a, b)) = v_range_cone {
3214        let (a, b) = if a <= b { (a, b) } else { (b, a) };
3215        lo = lo.max(a);
3216        hi = hi.min(b);
3217    }
3218    if let Some((a, b)) = v_range_cyl {
3219        // The cylinder's v is a signed distance along its axis from its origin;
3220        // convert both ends to the cone's v via the shared axial direction.
3221        let flip = cyl.axis().dot(axis);
3222        let to_cone_v = |cv: f64| (along + cv * flip) / sin_t;
3223        let (a, b) = (to_cone_v(a), to_cone_v(b));
3224        let (a, b) = if a <= b { (a, b) } else { (b, a) };
3225        lo = lo.max(a);
3226        hi = hi.min(b);
3227    }
3228    let (turn_lo, turn_hi) = (v_min, v_max);
3229    v_min = lo.max(v_min);
3230    v_max = hi.min(v_max);
3231    if v_max - v_min <= 1e-12 {
3232        return Ok(Some(vec![]));
3233    }
3234    // A cylinder beside the axis, with both turning points inside both faces,
3235    // meets the cone in a whole loop: traced along the cylinder's rulings,
3236    // each of which meets the nappe once, it comes back as one closed curve,
3237    // which the band split of the cylinder it winds needs. Around the axis
3238    // the loop winds the cone too and stays in two branches, and so does a
3239    // wall passing so near the axis that the loop's bend there, about
3240    // `(d - r) / sqrt(d r)` of a turn wide, spans fewer than a few rulings.
3241    let slack = Tolerance::new().linear;
3242    #[allow(clippy::cast_precision_loss)]
3243    let resolved = d - r > 3.0 * (d * r).sqrt() * TAU / RULING_SAMPLES as f64;
3244    if v_min <= turn_lo + slack && v_max >= turn_hi - slack && resolved {
3245        let mut pts: Vec<Point3> = (0..RULING_SAMPLES)
3246            .map(|i| {
3247                let (sin_u, cos_u) = ruling_u(i).sin_cos();
3248                let foot = cyl.origin() + (cyl.x_axis() * cos_u + cyl.y_axis() * sin_u) * r;
3249                let off = foot - apex;
3250                let across = off - axis * off.dot(axis);
3251                apex + across + axis * (across.length() * sin_t / cos_t)
3252            })
3253            .collect();
3254        pts.push(pts[0]);
3255        return Ok(Some(fit_ruling_loops(&[pts], |p| {
3256            (cone.project_point(p), cyl.project_point(p))
3257        })));
3258    }
3259
3260    let n_samples = 128;
3261    let mut plus: Vec<Point3> = Vec::with_capacity(n_samples + 1);
3262    let mut minus: Vec<Point3> = Vec::with_capacity(n_samples + 1);
3263    #[allow(clippy::cast_precision_loss)]
3264    for i in 0..=n_samples {
3265        let v = v_min + (v_max - v_min) * (i as f64) / (n_samples as f64);
3266        let rho = v * cos_t;
3267        if rho < 1e-12 {
3268            // The apex. `cos_alpha` has rho in its denominator, so it is only
3269            // meaningful in the limit: it tends to 0 (alpha -> pi/2) when the
3270            // cylinder passes exactly through the apex (d == R), and diverges
3271            // otherwise — where the clamp would manufacture a spurious alpha of
3272            // 0 or pi. So keep the apex only in the d == R case, where it is a
3273            // genuine point of the intersection and the shared endpoint at
3274            // which the two branches meet.
3275            if (d - r).abs() < 1e-12 {
3276                let apex = cone.evaluate(phi0, v);
3277                plus.push(apex);
3278                minus.push(apex);
3279            }
3280            continue;
3281        }
3282        let cos_alpha = ((d * d + rho * rho - r * r) / (2.0 * d * rho)).clamp(-1.0, 1.0);
3283        let alpha = cos_alpha.acos();
3284        plus.push(cone.evaluate(phi0 + alpha, v));
3285        minus.push(cone.evaluate(phi0 - alpha, v));
3286    }
3287
3288    let mut curves = Vec::new();
3289    for pts in [&plus, &minus] {
3290        // Fewer than four samples in range means this branch does not cross the
3291        // bounded region at all (the other branch may still).
3292        if pts.len() < 4 {
3293            continue;
3294        }
3295        let ipts: Vec<IntersectionPoint> = pts
3296            .iter()
3297            .map(|&p| IntersectionPoint {
3298                point: p,
3299                param1: cone.project_point(p),
3300                param2: cyl.project_point(p),
3301            })
3302            .collect();
3303        let degree = 3.min(pts.len() - 1);
3304        match interpolate(pts, degree) {
3305            Ok(curve) => curves.push(IntersectionCurve {
3306                curve,
3307                points: ipts,
3308            }),
3309            // Emitting only the branch that happened to fit would starve the
3310            // section chain of exactly the piece this path exists to supply —
3311            // the same silent half-answer the marcher's fragments produced.
3312            // Defer the whole pair to the caller's other paths instead.
3313            Err(_) => return Ok(None),
3314        }
3315    }
3316
3317    Ok(Some(curves))
3318}
3319
3320/// Algebraic sphere-sphere intersection.
3321///
3322/// Two spheres intersect in a circle lying in the radical plane.
3323/// The radical plane is perpendicular to the line connecting the centers,
3324/// at a distance d1 from center1 where:
3325///   d1 = (D² + R1² - R2²) / (2D)
3326/// and D is the distance between centers.
3327fn algebraic_sphere_sphere(
3328    s1: &SphericalSurface,
3329    s2: &SphericalSurface,
3330) -> Result<Vec<IntersectionCurve>, MathError> {
3331    let c1 = s1.center();
3332    let c2 = s2.center();
3333    let r1 = s1.radius();
3334    let r2 = s2.radius();
3335
3336    let delta = c2 - c1;
3337    let d_sq = delta.x() * delta.x() + delta.y() * delta.y() + delta.z() * delta.z();
3338    let d = d_sq.sqrt();
3339
3340    if d < 1e-12 {
3341        // Concentric spheres: no intersection (unless same radius → degenerate).
3342        return Ok(vec![]);
3343    }
3344
3345    // Check separation conditions.
3346    if d > r1 + r2 + 1e-10 {
3347        return Ok(vec![]); // Too far apart
3348    }
3349    if d + r2.min(r1) + 1e-10 < r1.max(r2) {
3350        return Ok(vec![]); // One inside the other
3351    }
3352
3353    // Distance from c1 to the radical plane along the center line.
3354    let d1 = (d_sq + r1 * r1 - r2 * r2) / (2.0 * d);
3355
3356    // Radius of the intersection circle.
3357    let r_circle_sq = r1 * r1 - d1 * d1;
3358    if r_circle_sq < 0.0 {
3359        // Tangent or no intersection (numerical noise).
3360        if r_circle_sq > -1e-10 {
3361            // Tangent: single point.
3362            let axis = Vec3::new(delta.x() / d, delta.y() / d, delta.z() / d);
3363            let tangent_pt = Point3::new(
3364                c1.x() + axis.x() * d1,
3365                c1.y() + axis.y() * d1,
3366                c1.z() + axis.z() * d1,
3367            );
3368            let ipt = IntersectionPoint {
3369                point: tangent_pt,
3370                param1: (0.0, 0.0),
3371                param2: (0.0, 0.0),
3372            };
3373            // Single-point "curve" — not very useful but correct.
3374            return Ok(vec![IntersectionCurve {
3375                curve: interpolate(&[tangent_pt, tangent_pt], 1)?,
3376                points: vec![ipt],
3377            }]);
3378        }
3379        return Ok(vec![]);
3380    }
3381
3382    let r_circle = r_circle_sq.sqrt();
3383    let axis = Vec3::new(delta.x() / d, delta.y() / d, delta.z() / d);
3384    let center = Point3::new(
3385        c1.x() + axis.x() * d1,
3386        c1.y() + axis.y() * d1,
3387        c1.z() + axis.z() * d1,
3388    );
3389
3390    // Build a reference frame for the circle.
3391    let basis = Frame3::from_normal(center, axis)?;
3392    let u_dir = basis.x;
3393    let v_dir = basis.y;
3394
3395    // Sample the circle for the IntersectionCurve representation.
3396    let n_samples = 33; // Odd for symmetry
3397    let mut points = Vec::with_capacity(n_samples);
3398    let mut positions = Vec::with_capacity(n_samples);
3399    #[allow(clippy::cast_precision_loss)]
3400    for i in 0..n_samples {
3401        let theta = TAU * i as f64 / (n_samples - 1) as f64;
3402        let (sin_t, cos_t) = theta.sin_cos();
3403        let pt = Point3::new(
3404            center.x() + (u_dir.x() * cos_t + v_dir.x() * sin_t) * r_circle,
3405            center.y() + (u_dir.y() * cos_t + v_dir.y() * sin_t) * r_circle,
3406            center.z() + (u_dir.z() * cos_t + v_dir.z() * sin_t) * r_circle,
3407        );
3408        positions.push(pt);
3409        points.push(IntersectionPoint {
3410            point: pt,
3411            param1: (0.0, 0.0),
3412            param2: (0.0, 0.0),
3413        });
3414    }
3415
3416    let degree = 3.min(positions.len() - 1);
3417    let curve = interpolate(&positions, degree)?;
3418
3419    Ok(vec![IntersectionCurve { curve, points }])
3420}
3421
3422/// Newton correction: project a point back onto the intersection curve
3423/// of two analytic surfaces. Solves the 3×3 system:
3424///   δ · na = -da  (eliminate distance to surface A)
3425///   δ · nb = -db  (eliminate distance to surface B)
3426///   δ · t  = 0    (minimal correction, perpendicular to tangent)
3427#[allow(clippy::too_many_arguments)]
3428fn correct_to_intersection(
3429    a: &AnalyticSurface<'_>,
3430    b: &AnalyticSurface<'_>,
3431    surf_a: &dyn Fn(f64, f64) -> Point3,
3432    norm_a: &dyn Fn(f64, f64) -> Vec3,
3433    surf_b: &dyn Fn(f64, f64) -> Point3,
3434    norm_b: &dyn Fn(f64, f64) -> Vec3,
3435    point: Point3,
3436    u_range_a: (f64, f64),
3437    v_range_a: (f64, f64),
3438    u_range_b: (f64, f64),
3439    v_range_b: (f64, f64),
3440    max_iters: usize,
3441) -> Point3 {
3442    let mut p = point;
3443    for _ in 0..max_iters {
3444        let (ua, va) = project_analytic(a, p, u_range_a, v_range_a);
3445        let (ub, vb) = project_analytic(b, p, u_range_b, v_range_b);
3446        let pa = surf_a(ua, va);
3447        let pb = surf_b(ub, vb);
3448        let na = norm_a(ua, va);
3449        let nb = norm_b(ub, vb);
3450        let pv = Vec3::new(p.x(), p.y(), p.z());
3451
3452        let da = (pv - Vec3::new(pa.x(), pa.y(), pa.z())).dot(na);
3453        let db = (pv - Vec3::new(pb.x(), pb.y(), pb.z())).dot(nb);
3454
3455        if da.abs() < 1e-7 && db.abs() < 1e-7 {
3456            break;
3457        }
3458
3459        let t = na.cross(nb);
3460        let t_len = t.length();
3461        if t_len < 1e-10 {
3462            // Surfaces are tangent — fall back to midpoint.
3463            return Point3::new(
3464                (pa.x() + pb.x()) * 0.5,
3465                (pa.y() + pb.y()) * 0.5,
3466                (pa.z() + pb.z()) * 0.5,
3467            );
3468        }
3469        let t_hat = t * (1.0 / t_len);
3470
3471        // Solve [na; nb; t_hat] · δ = [-da, -db, 0] via Cramer's rule.
3472        let det = na.x() * (nb.y() * t_hat.z() - nb.z() * t_hat.y())
3473            - na.y() * (nb.x() * t_hat.z() - nb.z() * t_hat.x())
3474            + na.z() * (nb.x() * t_hat.y() - nb.y() * t_hat.x());
3475        if det.abs() < 1e-15 {
3476            return Point3::new(
3477                (pa.x() + pb.x()) * 0.5,
3478                (pa.y() + pb.y()) * 0.5,
3479                (pa.z() + pb.z()) * 0.5,
3480            );
3481        }
3482        let inv = 1.0 / det;
3483        // Cramer's rule: replace each column of A with rhs = (-da, -db, 0).
3484        let dx = inv
3485            * (-da * (nb.y() * t_hat.z() - nb.z() * t_hat.y())
3486                + db * (na.y() * t_hat.z() - na.z() * t_hat.y()));
3487        let dy = inv
3488            * (da * (nb.x() * t_hat.z() - nb.z() * t_hat.x())
3489                - db * (na.x() * t_hat.z() - na.z() * t_hat.x()));
3490        let dz = inv
3491            * (-da * (nb.x() * t_hat.y() - nb.y() * t_hat.x())
3492                + db * (na.x() * t_hat.y() - na.y() * t_hat.x()));
3493        let candidate = Point3::new(p.x() + dx, p.y() + dy, p.z() + dz);
3494
3495        // Divergence guard: if the correction moves farther from both
3496        // surfaces, abandon Newton and return the best point so far.
3497        let (uc, vc) = project_analytic(a, candidate, u_range_a, v_range_a);
3498        let (ud, vd) = project_analytic(b, candidate, u_range_b, v_range_b);
3499        let pc_a = surf_a(uc, vc);
3500        let pc_b = surf_b(ud, vd);
3501        let cv = Vec3::new(candidate.x(), candidate.y(), candidate.z());
3502        let da_new = (cv - Vec3::new(pc_a.x(), pc_a.y(), pc_a.z()))
3503            .dot(norm_a(uc, vc))
3504            .abs();
3505        let db_new = (cv - Vec3::new(pc_b.x(), pc_b.y(), pc_b.z()))
3506            .dot(norm_b(ud, vd))
3507            .abs();
3508        if da_new > da.abs() && db_new > db.abs() {
3509            return p;
3510        }
3511
3512        p = candidate;
3513    }
3514    p
3515}
3516
3517/// March along the intersection of two surfaces from a seed point.
3518///
3519/// Uses the cross product of surface normals as the tangent direction
3520/// and projects back onto both surfaces using analytical projection
3521/// (for cylinders/spheres) or grid search (fallback).
3522#[allow(clippy::too_many_arguments)]
3523fn march_analytic_intersection(
3524    a: &AnalyticSurface<'_>,
3525    b: &AnalyticSurface<'_>,
3526    surf_a: &dyn Fn(f64, f64) -> Point3,
3527    norm_a: &dyn Fn(f64, f64) -> Vec3,
3528    surf_b: &dyn Fn(f64, f64) -> Point3,
3529    norm_b: &dyn Fn(f64, f64) -> Vec3,
3530    seed: Point3,
3531    u_range_a: (f64, f64),
3532    v_range_a: (f64, f64),
3533    u_range_b: (f64, f64),
3534    v_range_b: (f64, f64),
3535    initial_step: f64,
3536    u_periodic_a: bool,
3537    u_periodic_b: bool,
3538    region: Option<Aabb3>,
3539) -> Vec<Point3> {
3540    let max_steps = 500;
3541    let h_min = 1e-6;
3542    let h_max = initial_step * 4.0;
3543    // Fixed closure threshold: the adaptive step `h` varies with curvature
3544    // and can shrink below the actual miss distance at the seed re-approach.
3545    // Use `initial_step * 5` to robustly detect closure on the first pass.
3546    let closure_dist = initial_step * 5.0;
3547    // Angular thresholds for curvature-adaptive stepping.
3548    let max_angle = 10.0_f64.to_radians();
3549    let min_angle = 2.0_f64.to_radians();
3550
3551    // March forward from seed, collecting points.
3552    let mut forward = Vec::new();
3553    // March backward from seed, collecting points (reversed at end).
3554    let mut backward = Vec::new();
3555
3556    for (direction, points) in [(1.0_f64, &mut forward), (-1.0_f64, &mut backward)] {
3557        let mut current = seed;
3558        let mut h = initial_step;
3559        let mut prev_tangent: Option<Vec3> = None;
3560
3561        for _ in 0..max_steps {
3562            let (ua, va) = project_analytic(a, current, u_range_a, v_range_a);
3563            let (ub, vb) = project_analytic(b, current, u_range_b, v_range_b);
3564
3565            let na = norm_a(ua, va);
3566            let nb = norm_b(ub, vb);
3567
3568            let tangent = na.cross(nb);
3569            let t_len = tangent.length();
3570            if t_len < 1e-10 {
3571                break;
3572            }
3573            let t_dir = tangent * (direction / t_len);
3574
3575            // Curvature-adaptive step: check angular deviation from previous tangent.
3576            if let Some(prev_t) = prev_tangent {
3577                let cos_angle = prev_t.dot(t_dir).clamp(-1.0, 1.0);
3578                let angle = cos_angle.acos();
3579                if angle > max_angle && h > h_min {
3580                    h = (h * 0.5).max(h_min);
3581                } else if angle < min_angle {
3582                    h = (h * 2.0).min(h_max);
3583                }
3584            }
3585            prev_tangent = Some(t_dir);
3586
3587            let next = Point3::new(
3588                h.mul_add(t_dir.x(), current.x()),
3589                h.mul_add(t_dir.y(), current.y()),
3590                h.mul_add(t_dir.z(), current.z()),
3591            );
3592
3593            let (ua2, va2) = project_analytic(a, next, u_range_a, v_range_a);
3594            let (ub2, vb2) = project_analytic(b, next, u_range_b, v_range_b);
3595
3596            let pa = surf_a(ua2, va2);
3597            let pb = surf_b(ub2, vb2);
3598            let mid = Point3::new(
3599                (pa.x() + pb.x()) * 0.5,
3600                (pa.y() + pb.y()) * 0.5,
3601                (pa.z() + pb.z()) * 0.5,
3602            );
3603            let out_a = (!u_periodic_a && (ua2 <= u_range_a.0 || ua2 >= u_range_a.1))
3604                || va2 <= v_range_a.0
3605                || va2 >= v_range_a.1;
3606            let out_b = (!u_periodic_b && (ub2 <= u_range_b.0 || ub2 >= u_range_b.1))
3607                || vb2 <= v_range_b.0
3608                || vb2 >= v_range_b.1;
3609
3610            if out_a || out_b {
3611                break;
3612            }
3613            if region.is_some_and(|r| !r.contains_point(mid)) {
3614                points.push(mid);
3615                break;
3616            }
3617
3618            // Check for loop closure — if we've collected enough points and
3619            // the current point is close to the seed, the curve is closed.
3620            // Require ≥10 steps to avoid premature closure near the seed.
3621            let dist_to_seed = (mid - seed).length();
3622            if points.len() > 10 && dist_to_seed < closure_dist {
3623                points.push(seed);
3624                break;
3625            }
3626
3627            points.push(mid);
3628            current = mid;
3629        }
3630    }
3631
3632    // Assemble result: backward (reversed) + seed + forward
3633    backward.reverse();
3634    let mut result = backward;
3635    result.push(seed);
3636    result.append(&mut forward);
3637
3638    // Refine all points onto the intersection curve via Newton correction.
3639    for pt in &mut result {
3640        *pt = correct_to_intersection(
3641            a, b, surf_a, norm_a, surf_b, norm_b, *pt, u_range_a, v_range_a, u_range_b, v_range_b,
3642            5,
3643        );
3644    }
3645
3646    result
3647}
3648
3649/// Project a 3D point onto an analytic surface using the surface's
3650/// analytical projection method. Falls back to grid search for surface
3651/// types without analytical projection.
3652fn project_analytic(
3653    surface: &AnalyticSurface<'_>,
3654    point: Point3,
3655    u_range: (f64, f64),
3656    v_range: (f64, f64),
3657) -> (f64, f64) {
3658    match surface {
3659        AnalyticSurface::Cylinder(cyl) => {
3660            let (u, v) = cyl.project_point(point);
3661            (u.clamp(u_range.0, u_range.1), v.clamp(v_range.0, v_range.1))
3662        }
3663        AnalyticSurface::Sphere(sphere) => {
3664            let (u, v) = sphere.project_point(point);
3665            (u.clamp(u_range.0, u_range.1), v.clamp(v_range.0, v_range.1))
3666        }
3667        AnalyticSurface::Cone(cone) => {
3668            let (u, v) = cone.project_point(point);
3669            (u.clamp(u_range.0, u_range.1), v.clamp(v_range.0, v_range.1))
3670        }
3671        AnalyticSurface::Torus(torus) => {
3672            let (u, v) = torus.project_point(point);
3673            (u.clamp(u_range.0, u_range.1), v.clamp(v_range.0, v_range.1))
3674        }
3675    }
3676}
3677
3678/// Returns `true` if the surface's u-parameter is periodic (wraps around 2π).
3679/// All current `AnalyticSurface` variants have periodic u — this is trivially
3680/// true today but exists as a guard for future non-periodic analytic types.
3681fn is_u_periodic(surface: &AnalyticSurface<'_>) -> bool {
3682    matches!(
3683        surface,
3684        AnalyticSurface::Cylinder(_)
3685            | AnalyticSurface::Cone(_)
3686            | AnalyticSurface::Sphere(_)
3687            | AnalyticSurface::Torus(_)
3688    )
3689}
3690
3691/// Extract closures and parameter ranges for an analytic surface.
3692#[allow(clippy::type_complexity)]
3693fn surface_closures<'a>(
3694    surface: &'a AnalyticSurface<'a>,
3695) -> (
3696    Box<dyn Fn(f64, f64) -> Point3 + 'a>,
3697    Box<dyn Fn(f64, f64) -> Vec3 + 'a>,
3698    (f64, f64),
3699    (f64, f64),
3700) {
3701    match surface {
3702        AnalyticSurface::Cylinder(cyl) => (
3703            Box::new(|u, v| cyl.evaluate(u, v)),
3704            Box::new(|u, v| cyl.normal(u, v)),
3705            (0.0, TAU),
3706            (-1.0, 1.0),
3707        ),
3708        AnalyticSurface::Cone(cone) => (
3709            Box::new(|u, v| cone.evaluate(u, v)),
3710            Box::new(|u, v| cone.normal(u, v)),
3711            (0.0, TAU),
3712            (0.01, 2.0),
3713        ),
3714        AnalyticSurface::Sphere(sphere) => (
3715            Box::new(|u, v| sphere.evaluate(u, v)),
3716            Box::new(|u, v| sphere.normal(u, v)),
3717            (0.0, TAU),
3718            (-FRAC_PI_2, FRAC_PI_2),
3719        ),
3720        AnalyticSurface::Torus(torus) => (
3721            Box::new(|u, v| torus.evaluate(u, v)),
3722            Box::new(|u, v| torus.normal(u, v)),
3723            (0.0, TAU),
3724            (0.0, TAU),
3725        ),
3726    }
3727}
3728
3729#[cfg(test)]
3730#[allow(clippy::unwrap_used, clippy::expect_used)]
3731mod tests {
3732    use super::*;
3733    use crate::tolerance::Tolerance;
3734
3735    /// Arcs of a cone's hyperbola (a plane parallel to the axis) and parabola
3736    /// (a plane parallel to a ruling) between two of their sampled points
3737    /// stay on both the plane and the cone everywhere, not just at samples.
3738    #[test]
3739    fn plane_cone_conic_arcs_lie_on_both_surfaces() {
3740        let half_angle = 1.1_f64;
3741        let cone = ConicalSurface::new(
3742            Point3::new(0.0, 0.0, 0.0),
3743            Vec3::new(0.0, 0.0, 1.0),
3744            half_angle,
3745        )
3746        .unwrap();
3747        let ruling = Vec3::new(half_angle.sin(), 0.0, half_angle.cos());
3748        for (normal, d) in [(Vec3::new(1.0, 0.0, 0.0), 0.5), (ruling, 1.0)] {
3749            let chains =
3750                exact_plane_analytic_reaching(AnalyticSurface::Cone(&cone), normal, d, 10.0)
3751                    .unwrap();
3752            let chain = chains
3753                .iter()
3754                .find_map(|c| match c {
3755                    ExactIntersectionCurve::Points(chain) => Some(chain),
3756                    _ => None,
3757                })
3758                .expect("a parabola or hyperbola section is sampled");
3759            let (from, to) = (chain[2], chain[chain.len() - 3]);
3760            let arc = plane_cone_conic_arc(&cone, normal, d, from, to)
3761                .unwrap()
3762                .expect("an exact arc");
3763            let (t0, t1) = arc.domain();
3764            assert!((arc.evaluate(t0) - from).length() < 1e-12);
3765            assert!((arc.evaluate(t1) - to).length() < 1e-12);
3766            for i in 0..=200 {
3767                let q = arc.evaluate(t0 + (t1 - t0) * f64::from(i) / 200.0);
3768                let w = q - Point3::new(0.0, 0.0, 0.0);
3769                let off_plane = (normal.dot(w) - d).abs();
3770                let off_cone = (w.z() - w.length() * half_angle.sin()).abs();
3771                assert!(off_plane < 1e-9, "off the plane by {off_plane}");
3772                assert!(off_cone < 1e-9, "off the cone by {off_cone}");
3773            }
3774        }
3775    }
3776
3777    /// A plane a few 1e-10 short of parallel to a ruling cuts a vast ellipse
3778    /// that the parabola's closed form only approximates: the arc is either
3779    /// declined or on the cone, and an arc with coincident ends is declined.
3780    #[test]
3781    fn plane_cone_conic_arc_declines_a_near_parabolic_ellipse() {
3782        let half_angle = 1.1_f64;
3783        let cone = ConicalSurface::new(
3784            Point3::new(0.0, 0.0, 0.0),
3785            Vec3::new(0.0, 0.0, 1.0),
3786            half_angle,
3787        )
3788        .unwrap();
3789        for shortfall in [1e-10, 3e-10, 8e-10] {
3790            let tilt = half_angle - shortfall / (2.0 * half_angle).sin();
3791            let normal = Vec3::new(tilt.sin(), 0.0, tilt.cos());
3792            let chains =
3793                exact_plane_analytic_reaching(AnalyticSurface::Cone(&cone), normal, 1.0, 10.0)
3794                    .unwrap();
3795            let Some(chain) = chains.iter().find_map(|c| match c {
3796                ExactIntersectionCurve::Points(chain) => Some(chain),
3797                _ => None,
3798            }) else {
3799                continue;
3800            };
3801            let (from, to) = (chain[2], chain[chain.len() - 3]);
3802            assert!(
3803                plane_cone_conic_arc(&cone, normal, 1.0, from, from)
3804                    .unwrap()
3805                    .is_none(),
3806                "coincident ends"
3807            );
3808            let Some(arc) = plane_cone_conic_arc(&cone, normal, 1.0, from, to).unwrap() else {
3809                continue;
3810            };
3811            let (t0, t1) = arc.domain();
3812            for i in 0..=200 {
3813                let w = arc.evaluate(t0 + (t1 - t0) * f64::from(i) / 200.0)
3814                    - Point3::new(0.0, 0.0, 0.0);
3815                let off_cone = (w.z() - w.length() * half_angle.sin()).abs();
3816                assert!(off_cone < 1e-8, "{shortfall}: off the cone by {off_cone}");
3817            }
3818        }
3819    }
3820
3821    #[test]
3822    fn plane_cylinder_perpendicular() {
3823        let cyl =
3824            CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 2.0)
3825                .unwrap();
3826
3827        // Horizontal plane at z=3 -- produces a circle at height 3.
3828        let curves = intersect_plane_cylinder(&cyl, Vec3::new(0.0, 0.0, 1.0), 3.0).unwrap();
3829        assert!(!curves.is_empty(), "should find intersection curve");
3830        assert!(
3831            curves[0].points.len() > 10,
3832            "should have many sample points"
3833        );
3834
3835        let tol = Tolerance::loose();
3836        for pt in &curves[0].points {
3837            assert!(
3838                tol.approx_eq(pt.point.z(), 3.0),
3839                "z should be ~3.0, got {}",
3840                pt.point.z()
3841            );
3842            let r = pt.point.x().hypot(pt.point.y());
3843            assert!(tol.approx_eq(r, 2.0), "radius should be ~2.0, got {r}");
3844        }
3845    }
3846
3847    #[test]
3848    fn plane_sphere_equator() {
3849        let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 3.0).unwrap();
3850
3851        let curves = intersect_plane_sphere(&sphere, Vec3::new(0.0, 0.0, 1.0), 0.0).unwrap();
3852        assert!(!curves.is_empty());
3853
3854        let tol = Tolerance::loose();
3855        for pt in &curves[0].points {
3856            assert!(
3857                tol.approx_eq(pt.point.z(), 0.0),
3858                "z should be ~0, got {}",
3859                pt.point.z()
3860            );
3861            let r = pt.point.x().hypot(pt.point.y());
3862            assert!(tol.approx_eq(r, 3.0), "radius should be ~3.0, got {r}");
3863        }
3864    }
3865
3866    #[test]
3867    fn plane_sphere_no_intersection() {
3868        let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 1.0).unwrap();
3869
3870        let curves = intersect_plane_sphere(&sphere, Vec3::new(0.0, 0.0, 1.0), 5.0).unwrap();
3871        assert!(curves.is_empty());
3872    }
3873
3874    #[test]
3875    fn plane_cone_cross_section() {
3876        let cone = ConicalSurface::new(
3877            Point3::new(0.0, 0.0, 0.0),
3878            Vec3::new(0.0, 0.0, 1.0),
3879            std::f64::consts::FRAC_PI_4,
3880        )
3881        .unwrap();
3882
3883        let curves = intersect_plane_cone(&cone, Vec3::new(0.0, 0.0, 1.0), 1.0).unwrap();
3884        assert!(!curves.is_empty(), "should find intersection with cone");
3885    }
3886
3887    /// The 1u gridfinity spacer lip fuse corner (#1570): the body's lip
3888    /// recess cone (45 deg, opening downward) meets the tool's lip cone
3889    /// (45 deg, opening upward) with axes offset 0.25mm in x and y. Equal
3890    /// half-angle tangents put the whole intersection on the radical plane,
3891    /// so the section is one exact ellipse; the marcher shredded this into
3892    /// ~64 closed micro-loops per pair.
3893    #[test]
3894    fn offset_parallel_equal_angle_cones_give_one_exact_ellipse() {
3895        let c1 = ConicalSurface::new(
3896            Point3::new(
3897                -16.999_999_999_999_975,
3898                -16.999_999_999_999_975,
3899                5.849_999_999_999_951,
3900            ),
3901            Vec3::new(0.0, 0.0, -1.0),
3902            0.785_398_163_397_433_5,
3903        )
3904        .unwrap();
3905        let c2 = ConicalSurface::new(
3906            Point3::new(
3907                -16.750_000_000_000_036,
3908                -16.750_000_000_000_018,
3909                0.749_999_999_999_881,
3910            ),
3911            Vec3::new(0.0, 0.0, 1.0),
3912            0.785_398_163_397_467_6,
3913        )
3914        .unwrap();
3915
3916        let curves = exact_cone_cone(&c1, &c2)
3917            .unwrap()
3918            .expect("offset parallel equal-angle cones must take the radical-plane path");
3919        assert_eq!(curves.len(), 1, "expected exactly one section conic");
3920        assert!(
3921            matches!(curves[0], ExactIntersectionCurve::Ellipse(_)),
3922            "expected an ellipse section, got {:?}",
3923            curves[0]
3924        );
3925        let ExactIntersectionCurve::Ellipse(ellipse) = &curves[0] else {
3926            return;
3927        };
3928
3929        // Every sample must lie on BOTH cones: distance to the axis equals
3930        // tan(half_angle) times the axial distance from the apex, on the
3931        // real nappe of each.
3932        for i in 0..16 {
3933            let p = crate::traits::ParametricCurve::evaluate(ellipse, TAU * f64::from(i) / 16.0);
3934            for (cone, label) in [(&c1, "c1"), (&c2, "c2")] {
3935                let rel = p - cone.apex();
3936                let rel_v = Vec3::new(rel.x(), rel.y(), rel.z());
3937                let axial = rel_v.dot(cone.axis());
3938                let radial = (rel_v - cone.axis() * axial).length();
3939                assert!(
3940                    axial > 0.0,
3941                    "{label}: sample on phantom nappe (axial {axial})"
3942                );
3943                let expect = cone.half_angle().tan() * axial;
3944                assert!(
3945                    (radial - expect).abs() < 1e-9,
3946                    "{label}: sample off surface by {}",
3947                    (radial - expect).abs()
3948                );
3949            }
3950        }
3951    }
3952
3953    /// Opposed cones whose real nappes occupy disjoint half-spaces share a
3954    /// radical-plane conic only on the phantom nappe — the exact path must
3955    /// report a definitive empty intersection, not defer to the marcher.
3956    #[test]
3957    fn offset_parallel_cones_opening_apart_have_no_real_intersection() {
3958        let c1 = ConicalSurface::new(
3959            Point3::new(0.0, 0.0, 5.0),
3960            Vec3::new(0.0, 0.0, -1.0),
3961            std::f64::consts::FRAC_PI_4,
3962        )
3963        .unwrap();
3964        let c2 = ConicalSurface::new(
3965            Point3::new(0.25, 0.25, 20.0),
3966            Vec3::new(0.0, 0.0, 1.0),
3967            std::f64::consts::FRAC_PI_4,
3968        )
3969        .unwrap();
3970        let curves = exact_cone_cone(&c1, &c2)
3971            .unwrap()
3972            .expect("radical-plane path");
3973        assert!(curves.is_empty(), "disjoint nappes must yield no curves");
3974    }
3975
3976    /// Unequal half-angles keep a quadratic term in the pencil — no plane
3977    /// reduction exists, so the exact path must defer to the marcher.
3978    #[test]
3979    fn offset_parallel_cones_with_unequal_angles_defer() {
3980        let c1 = ConicalSurface::new(
3981            Point3::new(0.0, 0.0, 5.0),
3982            Vec3::new(0.0, 0.0, -1.0),
3983            std::f64::consts::FRAC_PI_4,
3984        )
3985        .unwrap();
3986        let c2 = ConicalSurface::new(Point3::new(0.25, 0.25, 0.5), Vec3::new(0.0, 0.0, 1.0), 0.6)
3987            .unwrap();
3988        assert!(exact_cone_cone(&c1, &c2).unwrap().is_none());
3989    }
3990
3991    /// A 45 degree cone and a radius 0.1 tube tilted 40 degrees that pierces
3992    /// both nappes, so the ruling path declines and the general marcher runs:
3993    /// the tube's axis meets the near nappe at (0.1, -1.368, 1.370).
3994    fn cone_and_tilted_tube() -> (ConicalSurface, CylindricalSurface) {
3995        let cone = ConicalSurface::new(
3996            Point3::new(0.0, 0.0, 0.0),
3997            Vec3::new(0.0, 0.0, 1.0),
3998            std::f64::consts::FRAC_PI_4,
3999        )
4000        .unwrap();
4001        let (s, c) = 40.0_f64.to_radians().sin_cos();
4002        let tube =
4003            CylindricalSurface::new(Point3::new(0.1, 0.0, 3.0), Vec3::new(0.0, s, c), 0.1).unwrap();
4004        (cone, tube)
4005    }
4006
4007    #[test]
4008    fn marcher_keeps_to_its_region() {
4009        let (cone, tube) = cone_and_tilted_tube();
4010        let run = |region: Option<Aabb3>| {
4011            let (a, b) = (
4012                AnalyticSurface::Cone(&cone),
4013                AnalyticSurface::Cylinder(&tube),
4014            );
4015            let (va, vb) = (Some((0.5, 4.0)), Some((-5.0, 5.0)));
4016            match region {
4017                Some(r) => intersect_analytic_analytic_in_region(a, b, 32, va, vb, r),
4018                None => intersect_analytic_analytic_bounded(a, b, 32, va, vb),
4019            }
4020            .unwrap()
4021        };
4022        assert!(!run(None).is_empty());
4023        let near = Aabb3 {
4024            min: Point3::new(-0.5, -2.0, 0.8),
4025            max: Point3::new(0.7, -0.7, 2.0),
4026        };
4027        let curves = run(Some(near));
4028        assert!(!curves.is_empty(), "the loop through the region is kept");
4029        // The region's margin (two grid cells) and one marching step past it.
4030        let reach = near.expanded(0.6);
4031        for curve in &curves {
4032            assert!(curve.points.iter().all(|p| reach.contains_point(p.point)));
4033        }
4034        let away = Aabb3 {
4035            min: Point3::new(5.0, 5.0, 5.0),
4036            max: Point3::new(6.0, 6.0, 6.0),
4037        };
4038        assert!(run(Some(away)).is_empty(), "nothing is marched outside it");
4039    }
4040
4041    #[test]
4042    fn coaxial_cones_cross_at_single_circle() {
4043        // Two coaxial truncated cones (outer base r10->top r8, inner r9->r8
4044        // over height 10) cross where their radii match: z=10, r=8. The
4045        // intersection must be ONE clean circle, not the dozens of degenerate
4046        // micro-curves the general marcher produces at near-tangency.
4047        let outer = ConicalSurface::new(
4048            Point3::new(0.0, 0.0, 50.0),
4049            Vec3::new(0.0, 0.0, -1.0),
4050            5.0_f64.atan(),
4051        )
4052        .unwrap();
4053        let inner = ConicalSurface::new(
4054            Point3::new(0.0, 0.0, 90.0),
4055            Vec3::new(0.0, 0.0, -1.0),
4056            10.0_f64.atan(),
4057        )
4058        .unwrap();
4059
4060        let curves = intersect_analytic_analytic_bounded(
4061            AnalyticSurface::Cone(&outer),
4062            AnalyticSurface::Cone(&inner),
4063            32,
4064            None,
4065            None,
4066        )
4067        .unwrap();
4068
4069        assert_eq!(
4070            curves.len(),
4071            1,
4072            "coaxial cones crossing at one circle must yield exactly one curve, got {}",
4073            curves.len()
4074        );
4075        for p in &curves[0].points {
4076            let r = p.point.x().hypot(p.point.y());
4077            assert!(
4078                (p.point.z() - 10.0).abs() < 1e-6 && (r - 8.0).abs() < 1e-6,
4079                "intersection point off the expected z=10,r=8 circle: {:?}",
4080                p.point
4081            );
4082        }
4083    }
4084
4085    #[test]
4086    fn plane_torus_cross_section() {
4087        let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 5.0, 1.0).unwrap();
4088
4089        let curves = intersect_plane_torus(&torus, Vec3::new(0.0, 0.0, 1.0), 0.0).unwrap();
4090        assert!(
4091            !curves.is_empty(),
4092            "should find intersection curves with torus"
4093        );
4094    }
4095
4096    /// A plane resting on a torus's tube touches it along one circle of the
4097    /// torus's own radius, ring or spindle, at the tube's top or bottom.
4098    #[test]
4099    fn plane_tangent_to_a_tube_touches_it_along_one_circle() {
4100        for (major, minor) in [(5.0, 1.0), (0.1, 2.45)] {
4101            let torus = ToroidalSurface::new(Point3::new(1.0, 2.0, 3.0), major, minor).unwrap();
4102            for z in [3.0 - minor, 3.0 + minor] {
4103                let exact = exact_plane_analytic(
4104                    AnalyticSurface::Torus(&torus),
4105                    Vec3::new(0.0, 0.0, 1.0),
4106                    z,
4107                )
4108                .unwrap();
4109                assert_eq!(exact.len(), 1, "R={major} r={minor} z={z}");
4110                let circle = match &exact[0] {
4111                    ExactIntersectionCurve::Circle(c) => Some(c),
4112                    _ => None,
4113                };
4114                let c = circle.expect("a circle");
4115                assert!((c.radius() - major).abs() < 1e-12);
4116                assert!((c.center() - Point3::new(1.0, 2.0, z)).length() < 1e-12);
4117
4118                let sampled = intersect_plane_torus(&torus, Vec3::new(0.0, 0.0, 1.0), z).unwrap();
4119                assert_eq!(sampled.len(), 1, "R={major} r={minor} z={z}");
4120            }
4121            // A tilted plane cuts no single circle of radius R.
4122            let single = |normal: Vec3, d: f64| {
4123                matches!(
4124                    exact_plane_analytic(AnalyticSurface::Torus(&torus), normal, d)
4125                        .unwrap()
4126                        .as_slice(),
4127                    [ExactIntersectionCurve::Circle(_)]
4128                )
4129            };
4130            assert!(!single(Vec3::new(1e-6, 0.0, 1.0), 3.0 + minor));
4131        }
4132    }
4133
4134    /// Tangency is read from the plane's distance to the tube's top, so it
4135    /// survives rounding on a large torus, and a plane further inside than
4136    /// the linear tolerance still cuts its two circles.
4137    #[test]
4138    fn plane_tangent_to_a_large_tube_is_read_through_rounding() {
4139        let torus = ToroidalSurface::new(Point3::new(0.3, -0.7, 3.0), 200.0, 100.1).unwrap();
4140        let level = Vec3::new(0.0, 0.0, 1.0);
4141        let curves =
4142            |d: f64| exact_plane_analytic(AnalyticSurface::Torus(&torus), level, d).unwrap();
4143        assert!(matches!(
4144            curves(3.0 + 100.1).as_slice(),
4145            [ExactIntersectionCurve::Circle(_)]
4146        ));
4147        let inside = curves(3.0 + 100.1 - 4e-7);
4148        assert!(!matches!(
4149            inside.as_slice(),
4150            [ExactIntersectionCurve::Circle(_)]
4151        ));
4152    }
4153
4154    /// Signed distance of a point to a z-axis torus centred at the origin:
4155    /// `sqrt((sqrt(x^2+y^2) - R)^2 + z^2) - r`.
4156    fn torus_implicit(p: Point3, major: f64, minor: f64) -> f64 {
4157        let rho = p.x().hypot(p.y());
4158        ((rho - major).hypot(p.z())) - minor
4159    }
4160
4161    /// The gridfinity lightweight base's failing corner, reduced: a cavity
4162    /// corner-round cone (apex below the floor, 45 deg, axis +z) crossed by a
4163    /// parallel-axis boss cylinder. The general marcher returned ~49 overlapping
4164    /// partial traces of one curve here; the algebraic path must return exactly
4165    /// the two branches, each ON both surfaces and inside the cone's v-hint.
4166    #[test]
4167    fn oblique_cone_cylinder_traces_curves_on_both() {
4168        use crate::traits::ParametricCurve;
4169        // A pointed cone opening down from (0, 0, 3), radius half the depth,
4170        // and a rod along y through (x0, ., 1): one loop through the wall
4171        // when the rod pokes out, two when every ruling meets the cone.
4172        let cone = ConicalSurface::new(
4173            Point3::new(0.0, 0.0, 3.0),
4174            Vec3::new(0.0, 0.0, -1.0),
4175            2.0_f64.atan(),
4176        )
4177        .unwrap();
4178        for (x0, loops) in [(0.5, 1), (0.0, 2)] {
4179            let cyl =
4180                CylindricalSurface::new(Point3::new(x0, 0.0, 1.0), Vec3::new(0.0, 1.0, 0.0), 0.6)
4181                    .unwrap();
4182            for cone_first in [true, false] {
4183                let (a, b) = if cone_first {
4184                    (
4185                        AnalyticSurface::Cone(&cone),
4186                        AnalyticSurface::Cylinder(&cyl),
4187                    )
4188                } else {
4189                    (
4190                        AnalyticSurface::Cylinder(&cyl),
4191                        AnalyticSurface::Cone(&cone),
4192                    )
4193                };
4194                let curves = intersect_analytic_analytic(a, b, 32).unwrap();
4195                assert_eq!(curves.len(), loops, "x0 {x0}: loops");
4196                for c in &curves {
4197                    let (t0, t1) = c.curve.domain();
4198                    for k in 0..=64 {
4199                        let t = (t1 - t0).mul_add(f64::from(k) / 64.0, t0);
4200                        let p = ParametricCurve::evaluate(&c.curve, t);
4201                        // A cubic through the ruling samples, bent most at the
4202                        // loop's branch points.
4203                        let rod = (p.x() - x0).hypot(p.z() - 1.0);
4204                        assert!(
4205                            (rod - 0.6).abs() < 1e-4,
4206                            "x0 {x0}: off the rod by {}",
4207                            rod - 0.6
4208                        );
4209                        let cone_r = p.x().hypot(p.y());
4210                        assert!(
4211                            (cone_r - 0.5 * (3.0 - p.z())).abs() < 1e-4,
4212                            "x0 {x0}: off the cone at {p:?}"
4213                        );
4214                    }
4215                }
4216            }
4217        }
4218    }
4219
4220    #[test]
4221    fn a_rod_through_a_rings_tube_traces_four_loops() {
4222        use crate::traits::ParametricCurve;
4223        let ring = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 4.0, 1.5).unwrap();
4224        // Along y through (0.5, ., 0.3): every ruling enters and leaves the
4225        // tube on either side of the hole, four roots, four loops.
4226        let rod =
4227            CylindricalSurface::new(Point3::new(0.5, 0.0, 0.3), Vec3::new(0.0, 1.0, 0.0), 0.6)
4228                .unwrap();
4229        let curves = ruling_torus_cylinder(&ring, &rod, true).unwrap();
4230        assert_eq!(curves.len(), 4);
4231        for c in &curves {
4232            let (t0, t1) = c.curve.domain();
4233            for k in 0..=64 {
4234                let p =
4235                    ParametricCurve::evaluate(&c.curve, (t1 - t0).mul_add(f64::from(k) / 64.0, t0));
4236                let on_rod = (p.x() - 0.5).hypot(p.z() - 0.3) - 0.6;
4237                let on_ring = (p.x().hypot(p.y()) - 4.0).hypot(p.z()) - 1.5;
4238                assert!(
4239                    on_rod.abs() < 1e-4 && on_ring.abs() < 1e-4,
4240                    "off by {on_rod}, {on_ring}"
4241                );
4242            }
4243        }
4244        // Higher, the rod's top rulings pass over the tube: the count varies.
4245        let high =
4246            CylindricalSurface::new(Point3::new(0.5, 0.0, 1.0), Vec3::new(0.0, 1.0, 0.0), 0.6)
4247                .unwrap();
4248        assert!(ruling_torus_cylinder(&ring, &high, true).is_none());
4249        // Its top clears the tube only between two sampled rulings.
4250        let grazing =
4251            CylindricalSurface::new(Point3::new(0.5, 0.0, 0.9001), Vec3::new(0.0, 1.0, 0.0), 0.6)
4252                .unwrap();
4253        assert!(ruling_torus_cylinder(&ring, &grazing, true).is_none());
4254        // A spindle torus's quartic also holds its inner lemon.
4255        let spindle = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 1.0, 2.0).unwrap();
4256        let thin =
4257            CylindricalSurface::new(Point3::new(0.3, 0.0, 0.0), Vec3::new(0.0, 1.0, 0.0), 0.2)
4258                .unwrap();
4259        assert!(ruling_torus_cylinder(&spindle, &thin, true).is_none());
4260    }
4261
4262    #[test]
4263    fn a_pin_through_a_ball_traces_two_loops() {
4264        use crate::traits::ParametricCurve;
4265        let ball = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 3.0).unwrap();
4266        // A pin tapering from radius 1.2 at (1, 0.5, -5) to 0.4 at 10 up:
4267        // radial 0.08 per unit of height.
4268        let half = 0.08_f64.atan();
4269        let apex = Point3::new(1.0, 0.5, -5.0 + 1.2 / 0.08);
4270        let pin = ConicalSurface::new(apex, Vec3::new(0.0, 0.0, -1.0), FRAC_PI_2 - half).unwrap();
4271        for cone_first in [true, false] {
4272            let (a, b) = if cone_first {
4273                (AnalyticSurface::Cone(&pin), AnalyticSurface::Sphere(&ball))
4274            } else {
4275                (AnalyticSurface::Sphere(&ball), AnalyticSurface::Cone(&pin))
4276            };
4277            let curves = intersect_analytic_analytic(a, b, 32).unwrap();
4278            assert_eq!(curves.len(), 2, "entry and exit loops");
4279            for c in &curves {
4280                let (t0, t1) = c.curve.domain();
4281                for k in 0..=64 {
4282                    let p = ParametricCurve::evaluate(
4283                        &c.curve,
4284                        (t1 - t0).mul_add(f64::from(k) / 64.0, t0),
4285                    );
4286                    let on_ball = (p - Point3::new(0.0, 0.0, 0.0)).length() - 3.0;
4287                    let axial = apex.z() - p.z();
4288                    let on_pin = (p.x() - 1.0).hypot(p.y() - 0.5) - axial * half.tan();
4289                    assert!(
4290                        on_ball.abs() < 1e-4 && on_pin.abs() < 1e-4,
4291                        "off by {on_ball}, {on_pin}"
4292                    );
4293                }
4294            }
4295        }
4296        // On the ball's axis the loops are circles, which defer. A pin whose
4297        // apex is inside the ball leaves it once along every generator, and
4298        // one only partly through the ball meets it over the generators that
4299        // reach it: one loop each. One that opens away meets it not at all.
4300        let coaxial =
4301            ConicalSurface::new(Point3::new(0.0, 0.0, 10.0), Vec3::new(0.0, 0.0, -1.0), 1.4)
4302                .unwrap();
4303        assert!(ruling_cone_sphere(&coaxial, &ball, true).is_none());
4304        let aside = ConicalSurface::new(
4305            Point3::new(2.8, 0.0, 10.0),
4306            Vec3::new(0.0, 0.0, -1.0),
4307            FRAC_PI_2 - half,
4308        )
4309        .unwrap();
4310        assert_eq!(ruling_cone_sphere(&aside, &ball, true).unwrap().len(), 1);
4311        let holding = ConicalSurface::new(
4312            Point3::new(1.0, 0.5, 1.0),
4313            Vec3::new(0.0, 0.0, -1.0),
4314            FRAC_PI_2 - half,
4315        )
4316        .unwrap();
4317        assert_eq!(ruling_cone_sphere(&holding, &ball, true).unwrap().len(), 1);
4318        let away = ConicalSurface::new(
4319            Point3::new(1.0, 0.5, 10.0),
4320            Vec3::new(0.0, 0.0, 1.0),
4321            FRAC_PI_2 - half,
4322        )
4323        .unwrap();
4324        assert!(ruling_cone_sphere(&away, &ball, true).unwrap().is_empty());
4325        // Grazing: the generators that miss span about 0.002 radians,
4326        // narrower than a 2048-angle scan's step, and the near and far
4327        // roots join into one loop across them.
4328        let step = TAU / 2048.0;
4329        let grazed =
4330            SphericalSurface::new(Point3::new(step.cos(), step.sin(), 10.0), 9.255_250_971_8)
4331                .unwrap();
4332        let wide =
4333            ConicalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 0.5).unwrap();
4334        assert_eq!(ruling_cone_sphere(&wide, &grazed, true).unwrap().len(), 1);
4335    }
4336
4337    #[test]
4338    fn a_ball_beside_a_cone_meets_it_in_one_loop() {
4339        use crate::traits::ParametricCurve;
4340        // Apex 3 up, opening downward, the radius half the depth below it.
4341        let cone = ConicalSurface::new(
4342            Point3::new(0.0, 0.0, 3.0),
4343            Vec3::new(0.0, 0.0, -1.0),
4344            2.0_f64.atan(),
4345        )
4346        .unwrap();
4347        for (centre, radius) in [
4348            (Point3::new(1.0, 0.8, 1.2), 1.1),
4349            (Point3::new(1.5, 0.0, 0.0), 0.8),
4350        ] {
4351            let ball = SphericalSurface::new(centre, radius).unwrap();
4352            let curves = ruling_cone_sphere(&cone, &ball, true).unwrap();
4353            assert_eq!(curves.len(), 1, "one loop for the ball at {centre:?}");
4354            let (t0, t1) = curves[0].curve.domain();
4355            for k in 0..=64 {
4356                let p = ParametricCurve::evaluate(
4357                    &curves[0].curve,
4358                    (t1 - t0).mul_add(f64::from(k) / 64.0, t0),
4359                );
4360                let on_ball = (p - centre).length() - radius;
4361                let on_cone = p.x().hypot(p.y()) - (3.0 - p.z()) / 2.0;
4362                assert!(
4363                    on_ball.abs() < 1e-5 && on_cone.abs() < 1e-5,
4364                    "ball at {centre:?}: off by {on_ball}, {on_cone}"
4365                );
4366            }
4367        }
4368        // A ball the nappe's generators miss, and one on the apex.
4369        let clear = SphericalSurface::new(Point3::new(4.0, 0.0, 0.0), 0.5).unwrap();
4370        assert!(ruling_cone_sphere(&cone, &clear, true).unwrap().is_empty());
4371        let on_apex = SphericalSurface::new(Point3::new(0.6, 0.0, 3.8), 1.0).unwrap();
4372        assert!(ruling_cone_sphere(&cone, &on_apex, true).is_none());
4373    }
4374
4375    #[test]
4376    fn a_ball_holding_a_cones_apex_meets_it_in_one_loop() {
4377        use crate::traits::ParametricCurve;
4378        let cone = ConicalSurface::new(
4379            Point3::new(0.0, 0.0, 3.0),
4380            Vec3::new(0.0, 0.0, -1.0),
4381            2.0_f64.atan(),
4382        )
4383        .unwrap();
4384        // Off the axis, and barely holding the apex: the generators pointing
4385        // away from the centre leave it just past the apex.
4386        for (centre, radius) in [
4387            (Point3::new(0.5, 0.0, 2.5), 2.0),
4388            (Point3::new(-0.4, 0.3, 2.0), 1.5),
4389            (Point3::new(0.0, 0.8, 3.0), 0.8001),
4390            (Point3::new(0.0, 0.8, 3.0), 0.800_001),
4391        ] {
4392            let ball = SphericalSurface::new(centre, radius).unwrap();
4393            let curves = ruling_cone_sphere(&cone, &ball, true).unwrap();
4394            assert_eq!(curves.len(), 1, "one loop for the ball at {centre:?}");
4395            let (t0, t1) = curves[0].curve.domain();
4396            for k in 0..=4096 {
4397                let p = ParametricCurve::evaluate(
4398                    &curves[0].curve,
4399                    (t1 - t0).mul_add(f64::from(k) / 4096.0, t0),
4400                );
4401                let on_ball = (p - centre).length() - radius;
4402                let on_cone = p.x().hypot(p.y()) - (3.0 - p.z()) / 2.0;
4403                assert!(
4404                    on_ball.abs() < 1e-5 && on_cone.abs() < 1e-5 && p.z() < 3.0,
4405                    "ball at {centre:?}: off by {on_ball}, {on_cone} at {p:?}"
4406                );
4407            }
4408        }
4409    }
4410
4411    #[test]
4412    fn oblique_cone_cylinder_defers_where_rulings_cannot_trace_it() {
4413        let t = 2.0_f64.atan();
4414        let cone =
4415            ConicalSurface::new(Point3::new(0.0, 0.0, 3.0), Vec3::new(0.0, 0.0, -1.0), t).unwrap();
4416        // A rod through the apex meets the far nappe.
4417        let through_apex =
4418            CylindricalSurface::new(Point3::new(0.0, 0.0, 3.0), Vec3::new(0.0, 1.0, 0.0), 0.6)
4419                .unwrap();
4420        assert!(ruling_cone_cylinder(&cone, &through_apex, true).is_none());
4421        // A rod along a generator meets each ruling once.
4422        let generator = Vec3::new(t.cos(), 0.0, -t.sin());
4423        let along = CylindricalSurface::new(Point3::new(0.0, 0.3, 0.0), generator, 0.2).unwrap();
4424        assert!(ruling_cone_cylinder(&cone, &along, true).is_none());
4425        // A pin's tip just through a tube's wall: the tube's rulings that
4426        // meet it span a window narrower than the sampling.
4427        let pin =
4428            ConicalSurface::new(Point3::new(20.5, 0.0, 0.0), Vec3::new(-1.0, 0.0, 0.0), t).unwrap();
4429        let tube =
4430            CylindricalSurface::new(Point3::new(0.0, 0.0, -10.0), Vec3::new(0.0, 0.0, 1.0), 20.0)
4431                .unwrap();
4432        assert!(ruling_cone_cylinder(&pin, &tube, true).is_none());
4433    }
4434
4435    #[test]
4436    fn parallel_cone_cylinder_gives_two_exact_branches() {
4437        use crate::traits::ParametricCurve;
4438        let cone = ConicalSurface::new(
4439            Point3::new(-5.45, -36.55, -4.85),
4440            Vec3::new(0.0, 0.0, 1.0),
4441            std::f64::consts::FRAC_PI_4,
4442        )
4443        .unwrap();
4444        let cyl = CylindricalSurface::new(
4445            Point3::new(-8.0, -34.0, -5.0),
4446            Vec3::new(0.0, 0.0, 1.0),
4447            4.45,
4448        )
4449        .unwrap();
4450        // The cone face spans z in [-3.8, -3.0]; v = (z - apex_z) / sin(45 deg).
4451        let v_hint = (1.484_924_240_492_058, 2.616_295_090_390_43);
4452        let curves = intersect_analytic_analytic_bounded(
4453            AnalyticSurface::Cone(&cone),
4454            AnalyticSurface::Cylinder(&cyl),
4455            32,
4456            Some(v_hint),
4457            Some((0.0, 2.5)),
4458        )
4459        .unwrap();
4460
4461        assert_eq!(curves.len(), 2, "expected exactly the two branches");
4462        for c in &curves {
4463            let (t0, t1) = c.curve.domain();
4464            for k in 0..=32 {
4465                let t = (t1 - t0).mul_add(f64::from(k) / 32.0, t0);
4466                let p = ParametricCurve::evaluate(&c.curve, t);
4467                // On the cylinder: radial distance from its axis is the radius.
4468                let radial = ((p.x() + 8.0).powi(2) + (p.y() + 34.0).powi(2)).sqrt();
4469                assert!((radial - 4.45).abs() < 1e-6, "off cylinder: {radial}");
4470                // On the cone: radial distance from its axis is z - apex_z.
4471                let cone_r = ((p.x() + 5.45).powi(2) + (p.y() + 36.55).powi(2)).sqrt();
4472                assert!((cone_r - (p.z() + 4.85)).abs() < 1e-6, "off cone at {p:?}");
4473                // Inside the cone face's own v-window (the hint is respected).
4474                assert!(p.z() >= -3.8 - 1e-9 && p.z() <= -3.0 + 1e-9, "z={}", p.z());
4475            }
4476        }
4477    }
4478
4479    #[test]
4480    fn parallel_rod_through_a_cones_wall_closes_one_loop() {
4481        use crate::traits::ParametricCurve;
4482        // Apex 3 up, opening downward, the radius half the depth below it.
4483        let cone = ConicalSurface::new(
4484            Point3::new(0.0, 0.0, 3.0),
4485            Vec3::new(0.0, 0.0, -1.0),
4486            2.0_f64.atan(),
4487        )
4488        .unwrap();
4489        // Beside the axis: each ruling meets the nappe once.
4490        for (x, y) in [(0.0, 1.3), (1.2, 0.5)] {
4491            let rod =
4492                CylindricalSurface::new(Point3::new(x, y, -10.0), Vec3::new(0.0, 0.0, 1.0), 0.6)
4493                    .unwrap();
4494            let curves = algebraic_parallel_cone_cylinder(&cone, &rod, None, None)
4495                .unwrap()
4496                .unwrap();
4497            assert_eq!(curves.len(), 1, "one closed loop at ({x}, {y})");
4498            let (t0, t1) = curves[0].curve.domain();
4499            let (first, last) = (
4500                ParametricCurve::evaluate(&curves[0].curve, t0),
4501                ParametricCurve::evaluate(&curves[0].curve, t1),
4502            );
4503            assert!((first - last).length() < 1e-9, "open at ({x}, {y})");
4504            for k in 0..=64 {
4505                let p = ParametricCurve::evaluate(
4506                    &curves[0].curve,
4507                    (t1 - t0).mul_add(f64::from(k) / 64.0, t0),
4508                );
4509                let on_rod = (p.x() - x).hypot(p.y() - y) - 0.6;
4510                let on_cone = p.x().hypot(p.y()) - (3.0 - p.z()) / 2.0;
4511                assert!(
4512                    on_rod.abs() < 1e-5 && on_cone.abs() < 1e-5,
4513                    "({x}, {y}): off by {on_rod}, {on_cone}"
4514                );
4515            }
4516        }
4517        // Around the axis, and beside it by less than the bend the rulings
4518        // resolve: two branches.
4519        for (x, y) in [(0.3, 0.2), (0.65, 0.0)] {
4520            let rod =
4521                CylindricalSurface::new(Point3::new(x, y, -10.0), Vec3::new(0.0, 0.0, 1.0), 0.6)
4522                    .unwrap();
4523            let curves = algebraic_parallel_cone_cylinder(&cone, &rod, None, None)
4524                .unwrap()
4525                .unwrap();
4526            assert_eq!(curves.len(), 2, "two branches at ({x}, {y})");
4527        }
4528    }
4529
4530    /// A coaxial pair has no radical line; the algebraic path must defer rather
4531    /// than divide by a zero axis separation.
4532    #[test]
4533    fn coaxial_cone_cylinder_defers_to_other_paths() {
4534        let cone = ConicalSurface::new(
4535            Point3::new(0.0, 0.0, 0.0),
4536            Vec3::new(0.0, 0.0, 1.0),
4537            std::f64::consts::FRAC_PI_4,
4538        )
4539        .unwrap();
4540        let cyl =
4541            CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 2.0)
4542                .unwrap();
4543        assert!(
4544            algebraic_parallel_cone_cylinder(&cone, &cyl, None, None)
4545                .unwrap()
4546                .is_none()
4547        );
4548    }
4549
4550    #[test]
4551    fn oblique_cone_cylinder_defers_to_other_paths() {
4552        let cone = ConicalSurface::new(
4553            Point3::new(0.0, 0.0, 0.0),
4554            Vec3::new(0.0, 0.0, 1.0),
4555            std::f64::consts::FRAC_PI_4,
4556        )
4557        .unwrap();
4558        let cyl =
4559            CylindricalSurface::new(Point3::new(3.0, 0.0, 1.0), Vec3::new(1.0, 0.0, 0.0), 1.0)
4560                .unwrap();
4561        assert!(
4562            algebraic_parallel_cone_cylinder(&cone, &cyl, None, None)
4563                .unwrap()
4564                .is_none()
4565        );
4566    }
4567
4568    #[test]
4569    fn plane_torus_lobe_closes_and_stays_on_surface() {
4570        use crate::traits::ParametricCurve;
4571        let (major, minor) = (10.0, 3.0);
4572        let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), major, minor).unwrap();
4573
4574        // The census cutting planes (y=-4, x=6) each cut the +x and -x tube lobes
4575        // in a CLOSED oval. The greedy marcher stops one grid step short of
4576        // closing; the wrap-close must make every fitted lobe close exactly.
4577        for (n, d) in [
4578            (Vec3::new(0.0, -1.0, 0.0), 4.0),  // y = -4
4579            (Vec3::new(-1.0, 0.0, 0.0), -6.0), // x = 6
4580            (Vec3::new(0.0, 0.0, 1.0), 0.0),   // z = 0 -> two concentric circles
4581        ] {
4582            let curves = intersect_plane_torus(&torus, n, d).unwrap();
4583            assert!(!curves.is_empty(), "plane n={n:?} d={d} found no curves");
4584            for c in &curves {
4585                let p0 = ParametricCurve::evaluate(&c.curve, 0.0);
4586                let p1 = ParametricCurve::evaluate(&c.curve, 1.0);
4587                assert!(
4588                    (p0 - p1).length() < 1e-7,
4589                    "lobe not closed: gap={} (n={n:?} d={d})",
4590                    (p0 - p1).length()
4591                );
4592                // Every fitted sample stays on the torus (shape-preserving).
4593                for k in 0..=64 {
4594                    let t = f64::from(k) / 64.0;
4595                    let p = ParametricCurve::evaluate(&c.curve, t);
4596                    assert!(
4597                        torus_implicit(p, major, minor).abs() < 1e-2,
4598                        "off-surface point {p:?} implicit={}",
4599                        torus_implicit(p, major, minor)
4600                    );
4601                }
4602            }
4603        }
4604    }
4605
4606    #[test]
4607    fn plane_torus_inner_tangent_figure_eight_stays_open() {
4608        use crate::traits::ParametricCurve;
4609        let (major, minor) = (10.0, 3.0);
4610        let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), major, minor).unwrap();
4611
4612        // A plane tangent to the inner equator (x = major - minor = 7) cuts a
4613        // self-touching figure-eight: its two branches touch at the node on
4614        // the inner equator. It must stay OPEN so a self-touching curve is
4615        // never sealed into a simple loop.
4616        let curves =
4617            intersect_plane_torus(&torus, Vec3::new(-1.0, 0.0, 0.0), -(major - minor)).unwrap();
4618        assert!(!curves.is_empty(), "inner-tangent plane found no curves");
4619        let max_gap = curves
4620            .iter()
4621            .map(|c| {
4622                let p0 = ParametricCurve::evaluate(&c.curve, 0.0);
4623                let p1 = ParametricCurve::evaluate(&c.curve, 1.0);
4624                (p0 - p1).length()
4625            })
4626            .fold(0.0_f64, f64::max);
4627        assert!(
4628            max_gap > 1e-2,
4629            "figure-eight chain was wrongly force-closed (max end-gap={max_gap})"
4630        );
4631    }
4632
4633    /// A wall parallel to the axis cuts one loop from the ring past the
4634    /// inner equator (its two turns where the branches meet), and two loops
4635    /// winding the tube inside it; each closes, at any scale, however near
4636    /// the wall runs to an equator.
4637    #[test]
4638    fn plane_torus_wall_sections_close_into_their_loops() {
4639        for (major, minor) in [(4.0, 1.5), (100.0, 30.0), (0.05, 0.01)] {
4640            let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), major, minor).unwrap();
4641            for k in 1..200 {
4642                let (d, want) = match k.cmp(&100) {
4643                    // Within the inner equator: two loops winding the tube.
4644                    std::cmp::Ordering::Less => ((major - minor) * f64::from(k) / 100.0, 2),
4645                    // Past it, short of the outer equator: one loop.
4646                    std::cmp::Ordering::Greater => (
4647                        2.0f64.mul_add(minor * f64::from(k - 100) / 100.0, major - minor),
4648                        1,
4649                    ),
4650                    std::cmp::Ordering::Equal => continue,
4651                };
4652                let loops = plane_torus_loops(&torus, Vec3::new(1.0, 0.0, 0.0), d, 128);
4653                let closed = loops
4654                    .iter()
4655                    .filter(|l| (l[0].point - l[l.len() - 1].point).length() < 1e-12)
4656                    .count();
4657                assert_eq!(
4658                    (loops.len(), closed),
4659                    (want, want),
4660                    "R {major} r {minor}, wall at {d}"
4661                );
4662            }
4663        }
4664    }
4665
4666    /// A plane through the centre tilted a little from the equator cuts two
4667    /// loops that each run all the way round the axis on a short run of `v`;
4668    /// their fitted curves stay on the torus.
4669    #[test]
4670    fn plane_torus_sections_round_the_axis_stay_on_the_torus() {
4671        let (major, minor) = (4.0, 1.5);
4672        let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), major, minor).unwrap();
4673        for tilt in [0.03_f64, 0.08, 0.2] {
4674            let normal = Vec3::new(tilt.sin(), 0.0, tilt.cos());
4675            let curves = intersect_plane_torus(&torus, normal, 0.0).unwrap();
4676            assert_eq!(curves.len(), 2, "tilt {tilt}");
4677            for c in &curves {
4678                let (t0, t1) = c.curve.domain();
4679                let off = (0..=400)
4680                    .map(|k| {
4681                        let p = c
4682                            .curve
4683                            .evaluate((t1 - t0).mul_add(f64::from(k) / 400.0, t0));
4684                        (p.x().hypot(p.y()) - major).hypot(p.z()) - minor
4685                    })
4686                    .fold(0.0_f64, |m, e| m.max(e.abs()));
4687                assert!(
4688                    off < 1e-6,
4689                    "tilt {tilt}: fitted section {off} off the torus"
4690                );
4691            }
4692        }
4693    }
4694
4695    #[test]
4696    fn line_torus_box_edge_crossing_is_exact() {
4697        // The census box edge x=6, y=-4 (z varying) crosses the torus (R=10,r=3)
4698        // at z = ±sqrt(r² − (rho−R)²), rho = hypot(6,4) ≈ 7.2111 → z ≈ ±1.1055.
4699        let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 10.0, 3.0).unwrap();
4700        let ts = intersect_line_torus(
4701            &torus,
4702            Point3::new(6.0, -4.0, -5.0),
4703            Vec3::new(0.0, 0.0, 1.0),
4704        );
4705        // Vertical line through (6,-4) meets the tube twice.
4706        assert_eq!(ts.len(), 2, "expected 2 crossings, got {ts:?}");
4707        let zs: Vec<f64> = ts.iter().map(|t| -5.0 + t).collect();
4708        let rho = 6.0_f64.hypot(4.0);
4709        let z_exp = (9.0 - (rho - 10.0).powi(2)).sqrt();
4710        assert!(
4711            (zs[0] - (-z_exp)).abs() < 1e-9,
4712            "z0={} exp={}",
4713            zs[0],
4714            -z_exp
4715        );
4716        assert!((zs[1] - z_exp).abs() < 1e-9, "z1={} exp={}", zs[1], z_exp);
4717        // Each crossing lies on the torus.
4718        for &t in &ts {
4719            let p = Point3::new(6.0, -4.0, -5.0 + t);
4720            let rho = p.x().hypot(p.y());
4721            let impl_v = (rho - 10.0).hypot(p.z()) - 3.0;
4722            assert!(impl_v.abs() < 1e-9, "off-torus impl={impl_v}");
4723        }
4724    }
4725
4726    #[test]
4727    fn line_torus_miss_and_tangent() {
4728        let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 10.0, 3.0).unwrap();
4729        // A vertical line at rho beyond the outer rim (x=20) misses entirely.
4730        let miss = intersect_line_torus(
4731            &torus,
4732            Point3::new(20.0, 0.0, 0.0),
4733            Vec3::new(0.0, 0.0, 1.0),
4734        );
4735        assert!(miss.is_empty(), "expected no crossings, got {miss:?}");
4736        // The z-axis (rho=0) passes through the hole — no intersection.
4737        let axis =
4738            intersect_line_torus(&torus, Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0));
4739        assert!(axis.is_empty(), "z-axis should miss the tube, got {axis:?}");
4740    }
4741
4742    #[test]
4743    fn dispatch_via_analytic_surface() {
4744        let cyl =
4745            CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
4746                .unwrap();
4747        let curves = intersect_plane_analytic(
4748            AnalyticSurface::Cylinder(&cyl),
4749            Vec3::new(0.0, 0.0, 1.0),
4750            0.0,
4751        )
4752        .unwrap();
4753        assert!(!curves.is_empty());
4754    }
4755
4756    #[test]
4757    fn perpendicular_cylinders_intersect() {
4758        let cyl_z =
4759            CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
4760                .unwrap();
4761        let cyl_x =
4762            CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0), 1.0)
4763                .unwrap();
4764
4765        let curves = intersect_analytic_analytic(
4766            AnalyticSurface::Cylinder(&cyl_z),
4767            AnalyticSurface::Cylinder(&cyl_x),
4768            16,
4769        )
4770        .unwrap();
4771
4772        assert!(
4773            !curves.is_empty(),
4774            "perpendicular cylinders should intersect"
4775        );
4776
4777        for c in &curves {
4778            assert!(
4779                c.points.len() >= 2,
4780                "intersection curve should have >= 2 points, got {}",
4781                c.points.len()
4782            );
4783        }
4784    }
4785
4786    /// Neither cylinder's rulings all meet the other: the curve is one loop
4787    /// joined at its two branch points.
4788    #[test]
4789    fn partially_overlapping_cylinders_meet_in_one_closed_loop() {
4790        let cyl_z =
4791            CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
4792                .unwrap();
4793        let cyl_x =
4794            CylindricalSurface::new(Point3::new(0.0, 1.2, 0.0), Vec3::new(1.0, 0.0, 0.0), 1.0)
4795                .unwrap();
4796        let curves = algebraic_cylinder_cylinder(&cyl_z, &cyl_x)
4797            .unwrap()
4798            .unwrap();
4799        assert_eq!(curves.len(), 1);
4800        let curve = &curves[0].curve;
4801        let (t0, t1) = curve.domain();
4802        assert!((curve.evaluate(t0) - curve.evaluate(t1)).length() < 1e-9);
4803        let off = |p: Point3| {
4804            let on_z = (p.x().hypot(p.y()) - 1.0).abs();
4805            let on_x = ((p.y() - 1.2).hypot(p.z()) - 1.0).abs();
4806            on_z.max(on_x)
4807        };
4808        let worst = (0..=400)
4809            .map(|k| off(curve.evaluate(t0 + (t1 - t0) * f64::from(k) / 400.0)))
4810            .fold(0.0, f64::max);
4811        assert!(worst < 2e-4, "curve leaves the cylinders by {worst}");
4812    }
4813
4814    /// Near tangency the thick cylinder's window of rulings (0.02 either side
4815    /// of a quarter turn) falls between its samples; the thin one's sweep
4816    /// finds the loop.
4817    #[test]
4818    fn near_tangent_cylinders_find_their_loop_on_the_thinner_sweep() {
4819        let cyl_z =
4820            CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
4821                .unwrap();
4822        let cyl_x =
4823            CylindricalSurface::new(Point3::new(0.0, 1.1998, 0.0), Vec3::new(1.0, 0.0, 0.0), 0.2)
4824                .unwrap();
4825        let curves = algebraic_cylinder_cylinder(&cyl_z, &cyl_x)
4826            .unwrap()
4827            .expect("the thin cylinder's sweep finds the loop");
4828        assert_eq!(curves.len(), 1);
4829    }
4830
4831    #[test]
4832    fn sphere_cylinder_intersect() {
4833        let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 2.0).unwrap();
4834        let cyl =
4835            CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
4836                .unwrap();
4837
4838        let curves = intersect_analytic_analytic(
4839            AnalyticSurface::Sphere(&sphere),
4840            AnalyticSurface::Cylinder(&cyl),
4841            16,
4842        )
4843        .unwrap();
4844
4845        // A sphere of radius 2 and a cylinder of radius 1, both centered
4846        // at the origin, should intersect (the cylinder passes through
4847        // the sphere).
4848        assert!(!curves.is_empty(), "sphere and cylinder should intersect");
4849    }
4850
4851    #[test]
4852    fn exact_sphere_cylinder_coaxial_two_circles() {
4853        // Sphere r=6 at origin, coaxial cylinder r=3 along z: two latitude
4854        // circles at z = ±sqrt(36-9) = ±sqrt(27), each of radius 3.
4855        let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 6.0).unwrap();
4856        let cyl =
4857            CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 3.0)
4858                .unwrap();
4859        let circles = exact_sphere_cylinder(&sphere, &cyl)
4860            .unwrap()
4861            .expect("coaxial case returns Some");
4862        assert_eq!(circles.len(), 2, "through-bore meets the sphere twice");
4863        let mut zs: Vec<f64> = circles
4864            .iter()
4865            .filter_map(|c| match c {
4866                ExactIntersectionCurve::Circle(circle) => {
4867                    assert!(
4868                        (circle.radius() - 3.0).abs() < 1e-9,
4869                        "rim radius == cyl radius"
4870                    );
4871                    Some(circle.center().z())
4872                }
4873                _ => None,
4874            })
4875            .collect();
4876        assert_eq!(zs.len(), 2, "both sections must be exact circles");
4877        zs.sort_by(f64::total_cmp);
4878        let z = 27.0_f64.sqrt();
4879        assert!((zs[0] + z).abs() < 1e-9 && (zs[1] - z).abs() < 1e-9);
4880    }
4881
4882    #[test]
4883    fn exact_sphere_cylinder_non_coaxial_defers() {
4884        // Cylinder axis offset from the sphere center → quartic curve, deferred.
4885        let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 6.0).unwrap();
4886        let cyl =
4887            CylindricalSurface::new(Point3::new(2.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 3.0)
4888                .unwrap();
4889        assert!(
4890            exact_sphere_cylinder(&sphere, &cyl).unwrap().is_none(),
4891            "non-coaxial sphere/cylinder defers to the marcher"
4892        );
4893    }
4894
4895    #[test]
4896    fn a_ball_on_a_cones_axis_meets_it_in_circles() {
4897        // Apex 3 up, opening downward, the radius half the depth below it.
4898        let cone = ConicalSurface::new(
4899            Point3::new(0.0, 0.0, 3.0),
4900            Vec3::new(0.0, 0.0, -1.0),
4901            2.0_f64.atan(),
4902        )
4903        .unwrap();
4904        for (height, radius, count) in [
4905            (0.0, 2.0, 2), // through the cone's middle
4906            (2.5, 1.3, 1), // swallowing the apex
4907            (2.5, 0.5, 1), // through the apex: the apex root is no circle
4908            (0.0, 1.0, 0), // inside, clear of the wall
4909            (5.0, 1.0, 0), // on the other nappe's side
4910        ] {
4911            let centre = Point3::new(0.0, 0.0, height);
4912            let ball = SphericalSurface::new(centre, radius).unwrap();
4913            let curves = exact_cone_sphere(&cone, &ball).unwrap().unwrap();
4914            let circles = circles_of(&curves);
4915            assert_eq!(circles.len(), count, "ball at {height}, radius {radius}");
4916            for circle in circles {
4917                for k in 0..16 {
4918                    let p = circle.evaluate(TAU * f64::from(k) / 16.0);
4919                    let on_ball = (p - centre).length() - radius;
4920                    let on_cone = p.x().hypot(p.y()) - (3.0 - p.z()) / 2.0;
4921                    assert!(
4922                        on_ball.abs() < 1e-9 && on_cone.abs() < 1e-9,
4923                        "ball at {height}: off by {on_ball}, {on_cone}"
4924                    );
4925                }
4926            }
4927        }
4928        let aside = SphericalSurface::new(Point3::new(0.5, 0.0, 0.0), 2.0).unwrap();
4929        assert!(exact_cone_sphere(&cone, &aside).unwrap().is_none());
4930        // A ball touching a wide cone a million units out, where the
4931        // discriminant cancels to a few ulps below zero.
4932        let wide =
4933            ConicalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 0.3).unwrap();
4934        let far = SphericalSurface::new(Point3::new(0.0, 0.0, 1e6), 1e6 * 0.3_f64.cos()).unwrap();
4935        let curves = exact_cone_sphere(&wide, &far).unwrap().unwrap();
4936        let circles = circles_of(&curves);
4937        assert_eq!(circles.len(), 1, "the touch");
4938        let touch = 1e6 * 0.3_f64.sin() * 0.3_f64.cos();
4939        assert!(
4940            (circles[0].radius() - touch).abs() < 1e-3,
4941            "{}",
4942            circles[0].radius()
4943        );
4944    }
4945
4946    /// The circles among exact section curves.
4947    fn circles_of(curves: &[ExactIntersectionCurve]) -> Vec<&Circle3D> {
4948        curves
4949            .iter()
4950            .filter_map(|c| match c {
4951                ExactIntersectionCurve::Circle(circle) => Some(circle),
4952                _ => None,
4953            })
4954            .collect()
4955    }
4956
4957    /// Worst distance of a circle's points from a torus and from a second
4958    /// surface given by its own distance function.
4959    fn worst_off(
4960        circles: &[&Circle3D],
4961        torus: &ToroidalSurface,
4962        other: impl Fn(Point3) -> f64,
4963    ) -> f64 {
4964        let mut worst = 0.0_f64;
4965        for circle in circles {
4966            for k in 0..16 {
4967                let p = circle.evaluate(TAU * f64::from(k) / 16.0);
4968                let q = p - torus.center();
4969                let along = q.dot(torus.z_axis());
4970                let rho = (q - torus.z_axis() * along).length();
4971                let off = ((rho - torus.major_radius()).hypot(along) - torus.minor_radius()).abs();
4972                worst = worst.max(off).max(other(p).abs());
4973            }
4974        }
4975        worst
4976    }
4977
4978    #[test]
4979    fn exact_sphere_torus_meets_a_ball_on_the_axis_in_circles() {
4980        let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 4.0, 1.5).unwrap();
4981        for height in [0.0, 1.0] {
4982            let centre = Point3::new(0.0, 0.0, height);
4983            let sphere = SphericalSurface::new(centre, 3.0).unwrap();
4984            let curves = exact_sphere_torus(&sphere, &torus).unwrap().unwrap();
4985            let circles = circles_of(&curves);
4986            assert_eq!((curves.len(), circles.len()), (2, 2), "height {height}");
4987            let worst = worst_off(&circles, &torus, |p| (p - centre).length() - 3.0);
4988            assert!(worst < 1e-9, "height {height}: {worst}");
4989        }
4990    }
4991
4992    #[test]
4993    fn exact_sphere_torus_misses_touches_and_defers() {
4994        let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 4.0, 1.5).unwrap();
4995        let ball = |x: f64, r: f64| SphericalSurface::new(Point3::new(x, 0.0, 0.0), r).unwrap();
4996        assert!(
4997            exact_sphere_torus(&ball(0.0, 1.0), &torus)
4998                .unwrap()
4999                .unwrap()
5000                .is_empty(),
5001            "a small ball in the hole misses"
5002        );
5003        assert!(
5004            exact_sphere_torus(&ball(0.0, 2.5), &torus)
5005                .unwrap()
5006                .is_none(),
5007            "a ball touching the inner equator defers"
5008        );
5009        assert!(
5010            exact_sphere_torus(&ball(1.0, 3.0), &torus)
5011                .unwrap()
5012                .is_none(),
5013            "a ball off the axis defers"
5014        );
5015        let spindle = ToroidalSurface::with_axis_and_ref_dir(
5016            Point3::new(0.0, 0.0, 0.0),
5017            1.0,
5018            2.0,
5019            Vec3::new(0.0, 0.0, 1.0),
5020            Vec3::new(1.0, 0.0, 0.0),
5021        )
5022        .unwrap();
5023        assert!(
5024            exact_sphere_torus(&ball(0.0, 2.5), &spindle)
5025                .unwrap()
5026                .is_none()
5027        );
5028    }
5029
5030    #[test]
5031    fn exact_cylinder_torus_meets_a_coaxial_rod_in_circles() {
5032        let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 4.0, 1.5).unwrap();
5033        let z = Vec3::new(0.0, 0.0, 1.0);
5034        let rod = |r: f64| CylindricalSurface::new(Point3::new(0.0, 0.0, -5.0), z, r).unwrap();
5035        let curves = exact_cylinder_torus(&rod(4.2), &torus).unwrap().unwrap();
5036        let circles = circles_of(&curves);
5037        assert_eq!((curves.len(), circles.len()), (2, 2));
5038        let worst = worst_off(&circles, &torus, |p| p.x().hypot(p.y()) - 4.2);
5039        assert!(worst < 1e-9, "{worst}");
5040        assert!(
5041            exact_cylinder_torus(&rod(2.0), &torus)
5042                .unwrap()
5043                .unwrap()
5044                .is_empty(),
5045            "a rod clear in the hole misses"
5046        );
5047        assert!(
5048            exact_cylinder_torus(&rod(5.5), &torus).unwrap().is_none(),
5049            "a wall touching the outer equator defers"
5050        );
5051        let tilted =
5052            CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.1, 1.0), 4.2)
5053                .unwrap();
5054        let offset = CylindricalSurface::new(Point3::new(0.5, 0.0, 0.0), z, 4.2).unwrap();
5055        assert!(exact_cylinder_torus(&tilted, &torus).unwrap().is_none());
5056        assert!(exact_cylinder_torus(&offset, &torus).unwrap().is_none());
5057        let spindle = ToroidalSurface::with_axis_and_ref_dir(
5058            Point3::new(0.0, 0.0, 0.0),
5059            1.0,
5060            2.0,
5061            z,
5062            Vec3::new(1.0, 0.0, 0.0),
5063        )
5064        .unwrap();
5065        assert!(
5066            exact_cylinder_torus(&rod(0.5), &spindle).unwrap().is_none(),
5067            "a spindle torus's inner lemon also meets the rod"
5068        );
5069    }
5070
5071    /// Loops of an off-axis sphere-cylinder pair: `(count, worst distance
5072    /// from either surface)`.
5073    fn off_axis_loops(cylinder_origin: Point3, cylinder_radius: f64) -> (usize, f64) {
5074        let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 2.0).unwrap();
5075        let cyl =
5076            CylindricalSurface::new(cylinder_origin, Vec3::new(0.0, 0.0, 1.0), cylinder_radius)
5077                .unwrap();
5078        let curves = algebraic_sphere_cylinder(&sphere, &cyl, true)
5079            .unwrap()
5080            .unwrap();
5081        let mut worst: f64 = 0.0;
5082        for c in &curves {
5083            for ip in &c.points {
5084                let on_sphere = sphere.evaluate(ip.param1.0, ip.param1.1);
5085                let on_cylinder = cyl.evaluate(ip.param2.0, ip.param2.1);
5086                worst = worst
5087                    .max((on_sphere - ip.point).length())
5088                    .max((on_cylinder - ip.point).length());
5089            }
5090            let (t0, t1) = c.curve.domain();
5091            assert!((c.curve.evaluate(t0) - c.curve.evaluate(t1)).length() < 1e-9);
5092            for k in 0..=400 {
5093                let p = c.curve.evaluate(t0 + (t1 - t0) * f64::from(k) / 400.0);
5094                let on_sphere = ((p - Point3::new(0.0, 0.0, 0.0)).length() - 2.0).abs();
5095                let on_cylinder = ((p.x() - cylinder_origin.x())
5096                    .hypot(p.y() - cylinder_origin.y())
5097                    - cylinder_radius)
5098                    .abs();
5099                worst = worst.max(on_sphere).max(on_cylinder);
5100            }
5101        }
5102        (curves.len(), worst)
5103    }
5104
5105    /// A drill off the ball's axis passes through it: an entry and an exit
5106    /// loop.
5107    #[test]
5108    fn off_axis_drill_through_a_sphere_meets_it_in_two_loops() {
5109        let (count, worst) = off_axis_loops(Point3::new(0.5, 0.0, 0.0), 0.2);
5110        assert_eq!(count, 2);
5111        assert!(worst < 1e-5, "loops leave the surfaces by {worst}");
5112    }
5113
5114    /// A cylinder over the ball's side: one loop joined at its branch points.
5115    #[test]
5116    fn cylinder_over_a_spheres_side_meets_it_in_one_loop() {
5117        let (count, worst) = off_axis_loops(Point3::new(1.8, 0.0, 0.0), 0.5);
5118        assert_eq!(count, 1);
5119        assert!(worst < 5e-4, "loop leaves the surfaces by {worst}");
5120    }
5121
5122    #[test]
5123    fn disjoint_cylinders_no_intersection() {
5124        let cyl_a =
5125            CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 0.5)
5126                .unwrap();
5127        let cyl_b =
5128            CylindricalSurface::new(Point3::new(5.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 0.5)
5129                .unwrap();
5130
5131        let curves = intersect_analytic_analytic(
5132            AnalyticSurface::Cylinder(&cyl_a),
5133            AnalyticSurface::Cylinder(&cyl_b),
5134            16,
5135        )
5136        .unwrap();
5137
5138        assert!(curves.is_empty(), "disjoint cylinders should not intersect");
5139    }
5140
5141    // ── Oblique plane × cone conic (ellipse / parabola / hyperbola) ──────
5142
5143    /// Collect 3D points from a returned exact curve, sampling analytic forms.
5144    fn collect_points(curve: &ExactIntersectionCurve) -> Vec<Point3> {
5145        use crate::traits::ParametricCurve;
5146        match curve {
5147            ExactIntersectionCurve::Circle(c) => (0..=64)
5148                .map(|i| ParametricCurve::evaluate(c, TAU * f64::from(i) / 64.0))
5149                .collect(),
5150            ExactIntersectionCurve::Ellipse(e) => (0..=64)
5151                .map(|i| ParametricCurve::evaluate(e, TAU * f64::from(i) / 64.0))
5152                .collect(),
5153            ExactIntersectionCurve::Points(pts) => pts.clone(),
5154        }
5155    }
5156
5157    /// Assert every returned point lies on the plane and the cone surface, on
5158    /// the real (`v >= 0`) nappe, and within a sane axial bound.
5159    fn assert_on_plane_and_cone(
5160        curves: &[ExactIntersectionCurve],
5161        cone: &ConicalSurface,
5162        n: Vec3,
5163        d: f64,
5164        z_bound: (f64, f64),
5165    ) {
5166        assert!(!curves.is_empty(), "expected at least one section curve");
5167        let mut total = 0;
5168        for curve in curves {
5169            for p in collect_points(curve) {
5170                total += 1;
5171                let plane_err = (n.x() * p.x() + n.y() * p.y() + n.z() * p.z() - d).abs();
5172                assert!(
5173                    plane_err < 1e-9,
5174                    "point off plane by {plane_err:.2e}: {p:?}"
5175                );
5176                let (u, v) = cone.project_point(p);
5177                let q = cone.evaluate(u, v);
5178                let cone_err =
5179                    ((p.x() - q.x()).powi(2) + (p.y() - q.y()).powi(2) + (p.z() - q.z()).powi(2))
5180                        .sqrt();
5181                assert!(cone_err < 1e-7, "point off cone by {cone_err:.2e}: {p:?}");
5182                assert!(v >= -1e-9, "point on phantom nappe (v={v:.4}): {p:?}");
5183                assert!(
5184                    p.z() >= z_bound.0 - 1e-6 && p.z() <= z_bound.1 + 1e-6,
5185                    "point z={:.4} outside sane bound {z_bound:?}: {p:?}",
5186                    p.z()
5187                );
5188            }
5189        }
5190        assert!(total >= 8, "too few section points ({total})");
5191    }
5192
5193    #[test]
5194    fn oblique_plane_cone_ellipse_is_exact_and_on_both() {
5195        // 45°-half-angle cone (axis +z). A plane tilted only ~16.7° off horizontal
5196        // has plane-axis angle ≈ 73° > 45° (the cone's half-opening from axis) →
5197        // ellipse. Must come back as an exact Ellipse, fully on both surfaces.
5198        let cone = ConicalSurface::new(
5199            Point3::new(0.0, 0.0, 0.0),
5200            Vec3::new(0.0, 0.0, 1.0),
5201            std::f64::consts::FRAC_PI_4,
5202        )
5203        .unwrap();
5204        let n = Vec3::new(0.3, 0.0, 1.0).normalize().unwrap();
5205        // Plane through (0,0,5): d = n·(0,0,5).
5206        let d = n.z() * 5.0;
5207        let curves = exact_plane_cone(&cone, n, d, 0.0).unwrap();
5208        assert!(
5209            curves
5210                .iter()
5211                .any(|c| matches!(c, ExactIntersectionCurve::Ellipse(_))),
5212            "oblique steep plane × cone must yield an exact Ellipse"
5213        );
5214        // The ellipse straddles z=5; with the 0.3 tilt the z-extent stays modest.
5215        assert_on_plane_and_cone(&curves, &cone, n, d, (0.0, 12.0));
5216    }
5217
5218    #[test]
5219    fn oblique_plane_cone_wrong_nappe_is_empty() {
5220        // Same ellipse-regime plane as above, but offset to the FAR side of the
5221        // apex (z=-5). The +z cone's real (v≥0) nappe is not met — only the
5222        // phantom v<0 nappe — so the result must be EMPTY, not a phantom ellipse.
5223        let cone = ConicalSurface::new(
5224            Point3::new(0.0, 0.0, 0.0),
5225            Vec3::new(0.0, 0.0, 1.0),
5226            std::f64::consts::FRAC_PI_4,
5227        )
5228        .unwrap();
5229        let n = Vec3::new(0.3, 0.0, 1.0).normalize().unwrap();
5230        let d = n.z() * -5.0;
5231        let curves = exact_plane_cone(&cone, n, d, 0.0).unwrap();
5232        assert!(
5233            curves.is_empty(),
5234            "plane on the phantom-nappe side must yield no real curve, got {}",
5235            curves.len()
5236        );
5237    }
5238
5239    #[test]
5240    fn oblique_plane_cone_parabola_on_both_single_branch() {
5241        // Plane normal at exactly 45° to the axis (= the cone half-opening) → the
5242        // plane is parallel to a generator → parabola. One unbounded branch.
5243        let cone = ConicalSurface::new(
5244            Point3::new(0.0, 0.0, 0.0),
5245            Vec3::new(0.0, 0.0, 1.0),
5246            std::f64::consts::FRAC_PI_4,
5247        )
5248        .unwrap();
5249        let n = Vec3::new(1.0, 0.0, 1.0).normalize().unwrap();
5250        let d = n.x() * 3.0 + n.z() * 3.0; // through (3,0,3)
5251        let curves = exact_plane_cone(&cone, n, d, 0.0).unwrap();
5252        assert_eq!(
5253            curves.len(),
5254            1,
5255            "a parabola is a single branch, got {}",
5256            curves.len()
5257        );
5258        // Bounded by r_max = 32·|e|; |e| here is O(few), so allow a wide z window.
5259        assert_on_plane_and_cone(&curves, &cone, n, d, (0.0, 400.0));
5260    }
5261
5262    #[test]
5263    fn oblique_plane_cone_hyperbola_real_nappe_only() {
5264        // Faithful scooplabel lip-foot geometry: a 45° cone with axis −z and
5265        // apex at (−59,−59,15.85) (a bin corner), cut by the upper ramp tread
5266        // plane n=(0,0.99518,0.09802), d=−58.36056. The plane is nearly parallel
5267        // to the axis (cos≈0.098) → plane-axis angle ≈ 5.6° < 45° → hyperbola.
5268        // The downward real nappe is hit by exactly one branch; the phantom
5269        // upward nappe (and the asymptote runaway) must NOT appear, and the arc
5270        // must stay near the apex (the plane is ~1.2 mm from it).
5271        let cone = ConicalSurface::new(
5272            Point3::new(-59.0, -59.0, 15.85),
5273            Vec3::new(0.0, 0.0, -1.0),
5274            std::f64::consts::FRAC_PI_4,
5275        )
5276        .unwrap();
5277        let n = Vec3::new(0.0, 0.995_18, 0.098_02).normalize().unwrap();
5278        let d = -58.360_56;
5279        let cos_theta = n.dot(cone.axis()).abs();
5280        assert!(cos_theta < 0.2, "expected a shallow (hyperbola) plane");
5281        let curves = exact_plane_cone(&cone, n, d, 0.0).unwrap();
5282        // Real downward nappe only: never above the apex (z=15.85). The vertex is
5283        // ~1.2 mm from the apex, so the bounded arc stays within a few mm of it.
5284        assert_on_plane_and_cone(&curves, &cone, n, d, (5.0, 15.85));
5285        // Every returned curve is sampled Points (no false Circle/Ellipse).
5286        for c in &curves {
5287            assert!(
5288                matches!(c, ExactIntersectionCurve::Points(_)),
5289                "hyperbola must be sampled Points, not a closed conic"
5290            );
5291        }
5292    }
5293}