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