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