Skip to main content

brepkit_math/
analytic_intersection.rs

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