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