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