Skip to main content

brepkit_math/
analytic_intersection.rs

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