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