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