Skip to main content

ogeom_intersect/
section.rs

1//! Where two surfaces meet: the one call.
2//!
3//! Everything else in this crate is a stage: closed forms, seeding, tracing,
4//! fitting. This is the function an application calls, and the one `ogeom-bool`
5//! builds on: give it two surfaces, get back what they do to each other,
6//! with the analytic path taken where it exists and the marched-and-fitted
7//! path where it does not. The caller does not choose; the pair does.
8//!
9//! # What a section curve carries
10//!
11//! Three descriptions, because three consumers: the curve in space for the
12//! edge, and a pcurve per surface for the faces; face splitting happens in
13//! parameter space, and a curve a face cannot express is one it cannot be
14//! split along. Analytic results carry exact pcurves where the projection has
15//! a closed form and `None` where it does not; fitted results always carry
16//! fitted pcurves, because the tracer recorded the parameters as it walked.
17//!
18//! A pcurve here is **same-parameter** with its 3D curve: evaluating either at
19//! the same `t` lands on the same point of the intersection. That is the claim
20//! `docs/DATA_MODEL.md` §6 makes edges carry, and it is arranged here by
21//! construction (the 2D curves inherit the 3D curve's own parameterization)
22//! rather than asserted and repaired later.
23
24use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
25use ogeom_geom::{
26    Circle2d, Curve, Curve2d as _, Curve3d, Ellipse2d, Line2d, Offset2d, PlanarCurve, Surface,
27    SurfaceGeometry,
28};
29use ogeom_math::{Circle2, Ellipse2, Frame2, Point, Point2};
30
31use crate::approx::approximate_branch;
32use crate::contact::trace_tangential;
33use crate::march::{Marching, TANGENTIAL_SINE, branches_from_touches, leaves_at_an_angle};
34use crate::surface::{Meeting, surface_surface};
35
36/// How to intersect, when the general path runs.
37#[derive(Debug, Clone, Copy, PartialEq)]
38pub struct IntersectOptions {
39    /// The tolerance the fitted curves are held to.
40    pub tolerance: f64,
41    /// The marching settings, for pairs with no closed form.
42    pub marching: Marching,
43}
44
45impl Default for IntersectOptions {
46    fn default() -> Self {
47        Self {
48            tolerance: 1e-6,
49            marching: Marching::default(),
50        }
51    }
52}
53
54/// One curve of a section, with its parameter-space descriptions.
55#[derive(Debug, Clone, PartialEq)]
56pub struct SectionCurve {
57    /// The curve in space.
58    pub curve: Curve,
59    /// The curve in the first surface's parameter space, where it has one.
60    ///
61    /// Always present for a fitted curve. For an exact curve, present when the
62    /// projection has a closed form (a line on a plane, a circle on the
63    /// cylinder it wraps) and `None` where it does not, which is a statement
64    /// about the projection rather than about the curve.
65    pub on_a: Option<PlanarCurve>,
66    /// The same, on the second surface.
67    pub on_b: Option<PlanarCurve>,
68    /// How far this curve may sit from the true intersection.
69    ///
70    /// Zero for an exact curve. For a fitted one, the trace's chord tolerance
71    /// plus the fit's reported error: the sum of the stated parts.
72    pub tolerance: f64,
73    /// Whether the curve came from a closed form.
74    pub exact: bool,
75    /// Whether it is a closed loop.
76    pub closed: bool,
77    /// Whether the surfaces *touch* along this curve rather than crossing
78    /// it.
79    ///
80    /// A tangential contact is a real curve (the two surfaces meet there,
81    /// and a drawing has to show it), but it carries no boundary parity:
82    /// neither surface passes through the other, so nothing is inside on
83    /// one side and outside on the other. Consumers that classify by
84    /// crossing must leave these out of that arithmetic; consumers that
85    /// draw or measure contact want them.
86    pub tangential: bool,
87}
88
89/// What two surfaces do to each other.
90#[derive(Debug, Clone, PartialEq)]
91pub enum SurfaceIntersection {
92    /// They do not meet.
93    ///
94    /// From the general path this means *no crossing was found at the seeding
95    /// resolution*: a branch thinner than the sampling grid is found only from
96    /// a border it pierces or from the approach of the surfaces near it, and
97    /// the completeness instrument in `tests/support/coverage.rs` is what
98    /// checks.
99    Apart,
100    /// They touch at isolated points without crossing.
101    Touching(Vec<Point>),
102    /// They meet along these curves.
103    Along(Vec<SectionCurve>),
104    /// They are the same surface wherever they overlap.
105    Same,
106}
107
108/// Where two surfaces meet.
109///
110/// The analytic path answers the pairs with closed forms, exactly, with
111/// tolerance zero. Every other pair is seeded, traced and fitted to
112/// `options.tolerance`, after a pair without a closed form is first measured
113/// for coincidence: two patches lying on one another to within
114/// `options.tolerance` wherever they overlap are [`Same`]. One call, and the
115/// pair decides the path.
116///
117/// # Errors
118///
119/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if the options
120/// are unusable. A pair the marcher finds nothing for is [`Apart`], not an
121/// error; see that variant for what it can and cannot claim.
122///
123/// [`Apart`]: SurfaceIntersection::Apart
124/// [`Same`]: SurfaceIntersection::Same
125pub fn intersect_surfaces(
126    a: &SurfaceGeometry,
127    b: &SurfaceGeometry,
128    options: IntersectOptions,
129    tol: Tolerances,
130) -> OgeomResult<SurfaceIntersection> {
131    intersect_surfaces_within(a, b, (None, None), options, tol)
132}
133
134/// A rectangle of a surface's parameters, `((u0, u1), (v0, v1))`.
135pub type ParameterWindow = ((f64, f64), (f64, f64));
136
137/// [`intersect_surfaces`] with the marched path held to a window of each
138/// surface's parameters.
139///
140/// Where the pair has a closed form the windows change nothing. Where it
141/// is marched, seeding and tracing walk only each surface's window, so a
142/// face trimmed from a larger patch is not seeded and traced over the
143/// whole patch; a branch is followed to the window's edge and stops there.
144/// The chart is untouched, so every pcurve returned means on the surface
145/// what it would have meant without the window.
146///
147/// A window is clamped to its surface's domain in a direction that does
148/// not repeat. In a periodic direction it is kept as given, so it may
149/// straddle the seam and the walk reads it in the window's own period;
150/// one that covers a period leaves that direction whole. A drum keeps its
151/// turn whole that way and is narrowed along its axis alone; any other
152/// surface with a periodic direction left whole is walked whole, since a
153/// window does not wrap. `None` leaves that surface whole.
154///
155/// # Errors
156///
157/// As [`intersect_surfaces`].
158pub fn intersect_surfaces_within(
159    a: &SurfaceGeometry,
160    b: &SurfaceGeometry,
161    windows: (Option<ParameterWindow>, Option<ParameterWindow>),
162    options: IntersectOptions,
163    tol: Tolerances,
164) -> OgeomResult<SurfaceIntersection> {
165    if !options.tolerance.is_finite() || options.tolerance <= 0.0 {
166        ogeom_bail!(
167            Construction,
168            "a tolerance of {} is not a distance",
169            options.tolerance
170        );
171    }
172
173    // A plane all but along a drum's axis meets it in an ellipse
174    // kilometres long, whose parameter is too coarse a ruler for the few
175    // millimetres of it the drum's height holds: a crossing solved on it
176    // lands tens of microns off. Over that height it is two lines.
177    if let Some(sections) = near_parallel_plane_drum(a, b, tol) {
178        return Ok(if sections.is_empty() {
179            SurfaceIntersection::Apart
180        } else {
181            SurfaceIntersection::Along(sections)
182        });
183    }
184    match surface_surface(a, b, tol) {
185        Ok(Meeting::Apart) => Ok(SurfaceIntersection::Apart),
186        Ok(Meeting::Same) => Ok(SurfaceIntersection::Same),
187        Ok(Meeting::Touching(points)) => Ok(SurfaceIntersection::Touching(points)),
188        Ok(Meeting::Along(curves)) => {
189            let sections: Vec<SectionCurve> = curves
190                .into_iter()
191                .filter_map(|curve| exact_section(curve, a, b, tol))
192                .collect();
193            Ok(if sections.is_empty() {
194                // Every curve fell outside the surfaces' stated extents: the
195                // unbounded geometries meet, the surfaces as given do not.
196                SurfaceIntersection::Apart
197            } else {
198                SurfaceIntersection::Along(sections)
199            })
200        }
201        // No closed form for this pair. Coincidence is measured first: the
202        // marcher seeds on sign changes, and a pair that never separates
203        // has none, so what it would trace over a coincident pair is noise
204        // under a section's name, and costs seconds to produce.
205        Err(_) if surfaces_coincide(a, b, options.tolerance, tol) => Ok(SurfaceIntersection::Same),
206        // Otherwise the marcher, unless the pair is two drums all but
207        // parallel.
208        Err(_) => match near_parallel_drums(a, b, tol)
209            .or_else(|| ball_through_drum(a, b, tol))
210            .or_else(|| axial_plane_revolution(a, b, tol))
211            .or_else(|| crate::extrusion::plane_extrusion(a, b, tol))
212            .or_else(|| crate::extrusion::drum_extrusion(a, b, tol))
213            .or_else(|| plane_along_spline_lines(a, b, tol))
214        {
215            Some(sections) if sections.is_empty() => Ok(SurfaceIntersection::Apart),
216            Some(sections) => Ok(SurfaceIntersection::Along(sections)),
217            None => marched(a, b, windows, options, tol),
218        },
219    }
220}
221
222/// Whether two surfaces are one surface wherever they overlap, measured to
223/// `reach`.
224///
225/// The closed forms answer this for the pairs they know. Where there is no
226/// closed form it does not stop being a fair question (two patches restated
227/// from one plane are the same surface, and nothing in their control points
228/// says so), but it stops being answerable exactly, so it is measured, and
229/// only for the pairs the analytic layer has declined.
230///
231/// Sampled on the *smaller* window, because the answer is about the region
232/// the two share and a stated window is not that region: a plane's own
233/// extends for a billion units either way, and a grid over it samples
234/// nothing. A sample whose foot lands on the rim of the other is skipped
235/// rather than counted against: the other patch simply does not reach that
236/// far, and a distance measured to its rim is about the window, not the
237/// surface.
238///
239/// The samples that agree must spread over an area, not lie along one
240/// line of either chart: samples along one curve prove only that the curve
241/// lies in both surfaces, as a wall's border does in the plane it leaves.
242/// Where they do lie along one line, each is stepped off it in the sampled
243/// chart, by half a grid step and then by halves of that until its foot
244/// lands inside the other's window; one such sample must agree, and none
245/// may stand off.
246///
247/// One-sided by construction: a pair that crosses puts interior samples well
248/// off the other, so it cannot pass, and a pair this cannot resolve is
249/// marched.
250fn surfaces_coincide(
251    a: &SurfaceGeometry,
252    b: &SurfaceGeometry,
253    reach: f64,
254    tol: Tolerances,
255) -> bool {
256    /// Samples per direction over the window, and how many must land inside
257    /// the other before agreement means anything.
258    const GRID: i32 = 6;
259    const EVIDENCE: usize = 4;
260    /// How many times a step off a line of samples is halved looking for
261    /// the other's window: down to a millionth of a grid step.
262    const HALVINGS: u32 = 20;
263
264    let span = |s: &SurfaceGeometry| -> f64 {
265        let ((ua, ub), (va, vb)) = s.domain();
266        (ub - ua).abs().max((vb - va).abs())
267    };
268    let (sampled, against) = if span(a) <= span(b) { (a, b) } else { (b, a) };
269    let ((ua, ub), (va, vb)) = sampled.domain();
270    if !(ua.is_finite() && ub.is_finite() && va.is_finite() && vb.is_finite()) {
271        return false;
272    }
273    let ((wu0, wu1), (wv0, wv1)) = against.domain();
274    // A twentieth of the window in from each rim: enough that a foot the
275    // search pinned to the rim is not read as one the surface truly reaches.
276    let (mu, mv) = ((wu1 - wu0) * 0.05, (wv1 - wv0) * 0.05);
277    let (du, dv) = ((ub - ua) / f64::from(GRID), (vb - va) / f64::from(GRID));
278
279    // The foot on the other of the sample at `(u, v)`: `Some(None)` where it
280    // lands on the window's rim, `None` where the sample stands off the
281    // other or cannot be measured.
282    let measure = |u: f64, v: f64| -> Option<Option<(f64, f64)>> {
283        let p = sampled.point_at(u, v, tol).ok()?;
284        let foot = ogeom_geom::project_on_surface(against, p, 16, tol).ok()?;
285        let (fu, fv) = foot.parameters;
286        if fu <= wu0 + mu || fu >= wu1 - mu || fv <= wv0 + mv || fv >= wv1 - mv {
287            return Some(None);
288        }
289        (foot.distance <= reach).then_some(Some(foot.parameters))
290    };
291    let mut agreeing: Vec<((i32, i32), (f64, f64))> = Vec::new();
292    for i in 0..=GRID {
293        for j in 0..=GRID {
294            let (u, v) = (du.mul_add(f64::from(i), ua), dv.mul_add(f64::from(j), va));
295            match measure(u, v) {
296                None => return false,
297                Some(None) => {}
298                Some(Some(foot)) => agreeing.push(((i, j), foot)),
299            }
300        }
301    }
302    if agreeing.len() < EVIDENCE {
303        return false;
304    }
305
306    // Along one line of the sampled grid, exactly; and of the other's
307    // chart, to the parametric confusion of the feet's spread.
308    let (first, _) = agreeing[0];
309    let lined_up = agreeing
310        .iter()
311        .find(|(at, _)| *at != first)
312        .map(|&(at, _)| (at.0 - first.0, at.1 - first.1))
313        .filter(|&(di, dj)| {
314            agreeing
315                .iter()
316                .all(|&((i, j), _)| di * (j - first.1) == dj * (i - first.0))
317        });
318    let feet_lined_up = {
319        let mut far = (agreeing[0].1, agreeing[0].1, 0.0_f64);
320        for (x, (_, p)) in agreeing.iter().enumerate() {
321            for (_, q) in &agreeing[x + 1..] {
322                let d = (q.0 - p.0).hypot(q.1 - p.1);
323                if d > far.2 {
324                    far = (*p, *q, d);
325                }
326            }
327        }
328        let (p, q, d) = far;
329        agreeing.iter().all(|(_, r)| {
330            ((q.0 - p.0) * (r.1 - p.1) - (q.1 - p.1) * (r.0 - p.0)).abs()
331                <= tol.parametric() * d * d
332        })
333    };
334    if lined_up.is_none() && !feet_lined_up {
335        return true;
336    }
337
338    // Off the line: across it in the sampled chart, or either way where only
339    // the feet line up.
340    let across: &[(f64, f64)] = match lined_up {
341        Some((0, _)) => &[(1.0, 0.0)],
342        Some(_) => &[(0.0, 1.0)],
343        None => &[(1.0, 0.0), (0.0, 1.0)],
344    };
345    let mut spread = false;
346    for &((i, j), _) in &agreeing {
347        for &(ku, kv) in across {
348            for sign in [-1.0, 1.0] {
349                let mut h = 0.5 * sign;
350                for _ in 0..HALVINGS {
351                    let (u, v) = (
352                        du.mul_add(ku.mul_add(h, f64::from(i)), ua),
353                        dv.mul_add(kv.mul_add(h, f64::from(j)), va),
354                    );
355                    if !(ua..=ub).contains(&u) || !(va..=vb).contains(&v) {
356                        break;
357                    }
358                    match measure(u, v) {
359                        None => return false,
360                        Some(None) => h *= 0.5,
361                        Some(Some(_)) => {
362                            spread = true;
363                            break;
364                        }
365                    }
366                }
367            }
368        }
369    }
370    spread
371}
372
373/// A plane through the axis of a surface of revolution whose profile lies
374/// in a plane through that axis: the profile turned to each angle that sets
375/// its plane on the cut, a column of the revolution's chart.
376///
377/// Marched, such a section runs through the pole wherever the profile meets
378/// the axis, where the chart pinches to a point and the march stalls. Here
379/// the profile is cut where it crosses the axis, and each piece is turned to
380/// both angles, the one setting its side of the axis on each half of the
381/// cut: a revolution whose profile crosses the axis covers the section
382/// twice, once from each column, and a face on either column finds its own
383/// piece. `None` for any other plane or profile, and where the turns fall
384/// outside the sweep the answer is empty.
385fn axial_plane_revolution(
386    a: &SurfaceGeometry,
387    b: &SurfaceGeometry,
388    tol: Tolerances,
389) -> Option<Vec<SectionCurve>> {
390    const SAMPLES: u32 = 64;
391    let (plane, revolution, plane_first) = match (a, b) {
392        (SurfaceGeometry::Plane(p), SurfaceGeometry::Revolution(r)) => (p, r, true),
393        (SurfaceGeometry::Revolution(r), SurfaceGeometry::Plane(p)) => (p, r, false),
394        _ => return None,
395    };
396    let cut = plane.plane();
397    let axis = revolution.axis();
398    let normal = cut.normal();
399    if normal.dot(axis.direction).abs() > tol.angular()
400        || cut.signed_distance_to(axis.location).abs() > tol.confusion()
401    {
402        return None;
403    }
404    let profile = revolution.curve();
405    let (v0, v1) = profile.domain();
406    let at = |k: u32| v0 + (v1 - v0) * f64::from(k) / f64::from(SAMPLES);
407    let radial = |v: f64| {
408        let p = profile.point_at(v, tol).ok()?;
409        Some(p - axis.project(p))
410    };
411    // The profile's own side of the axis, from its point furthest off it.
412    let mut widest = ogeom_math::Vector::ZERO;
413    for k in 0..=SAMPLES {
414        let r = radial(at(k))?;
415        if r.magnitude() > widest.magnitude() {
416            widest = r;
417        }
418    }
419    let side = ogeom_math::Direction::new(widest, tol).ok()?;
420    let across = axis.direction.cross_with(side.vector());
421    // Every point of the profile in the plane of the axis and that side.
422    let offset = |v: f64| radial(v).map(|r| (r.dot(side.vector()), r.dot(across)));
423    for k in 0..=SAMPLES {
424        let (_, off) = offset(at(k))?;
425        if off.abs() > tol.confusion() {
426            return None;
427        }
428    }
429    // The pieces between the profile's crossings of the axis, each crossing
430    // narrowed by bisection.
431    let mut cuts = vec![v0];
432    for k in 0..SAMPLES {
433        let (mut lo, mut hi) = (at(k), at(k + 1));
434        let (s_lo, s_hi) = (offset(lo)?.0, offset(hi)?.0);
435        if s_lo.abs() <= tol.confusion() || s_lo * s_hi >= 0.0 {
436            continue;
437        }
438        for _ in 0..80 {
439            let mid = f64::midpoint(lo, hi);
440            if offset(mid)?.0 * s_lo > 0.0 {
441                lo = mid;
442            } else {
443                hi = mid;
444            }
445        }
446        cuts.push(f64::midpoint(lo, hi));
447    }
448    cuts.push(v1);
449    // The turns setting the profile's side on the cut's two halves.
450    let out = axis.direction.cross_with(normal.vector());
451    let first = across.dot(out).atan2(side.vector().dot(out));
452    let (u0, u1) = revolution.domain().0;
453    let turns: Vec<f64> = [first, first + core::f64::consts::PI]
454        .into_iter()
455        .filter_map(|u| {
456            let u = u0 + (u - u0).rem_euclid(core::f64::consts::TAU);
457            let u = if (u - u0 - core::f64::consts::TAU).abs() <= tol.angular() {
458                u0
459            } else {
460                u
461            };
462            (u <= u1 + tol.angular()).then_some(u.min(u1))
463        })
464        .collect();
465    let mut sections = Vec::new();
466    for &u in &turns {
467        let turned = ogeom_geom::Transformable::transformed(
468            profile,
469            &ogeom_math::Transform::rotation(axis, u),
470            tol,
471        )
472        .ok()?;
473        for piece in cuts.windows(2) {
474            let (va, vb) = (piece[0], piece[1]);
475            if vb - va <= tol.parametric() {
476                continue;
477            }
478            let curve: Curve = ogeom_geom::TrimmedCurve::new(turned.clone(), va, vb, tol)
479                .ok()?
480                .into();
481            let column: PlanarCurve = Line2d::over(
482                ogeom_math::Axis2::new(Point2::new(u, 0.0), ogeom_math::Direction2::Y),
483                va,
484                vb,
485            )
486            .ok()?
487            .into();
488            let flat = exact_pcurve(&curve, (va, vb), a_or_b(plane_first, a, b), tol);
489            let (on_a, on_b) = if plane_first {
490                (flat, Some(column))
491            } else {
492                (Some(column), flat)
493            };
494            sections.push(SectionCurve {
495                on_a,
496                on_b,
497                tolerance: 0.0,
498                exact: true,
499                closed: false,
500                tangential: false,
501                curve,
502            });
503        }
504    }
505    Some(sections)
506}
507
508/// A plane holding whole columns or rows of a spline surface, and meeting
509/// it nowhere else: those iso lines, exactly.
510///
511/// A surface of revolution converted to a spline and scaled keeps its
512/// meridians as columns, and a plane through its axis holds two of them,
513/// one often the seam along the chart's border. Marched, such a section
514/// runs along the chart's edge or through its poles and is not found. An
515/// iso line lies in the plane where every control point of it does, which
516/// is a root of each control point's weighted distance to the plane, found
517/// along the chart. The answer stands only where a grid over the chart
518/// finds the surface on one side of the plane between the lines found;
519/// anything else meets the plane elsewhere too and is marched.
520///
521/// A line on an open border of the chart has the surface on one side only.
522/// It stands where the surface leaves the plane there at an angle all
523/// along it, as [`leaves_at_an_angle`] has it, against the grid's nearest
524/// row in and the band its samples ignore: the plane then meets the face
525/// along its boundary edge, as a plane through a prism's side edge meets
526/// the side faces. A border the surface leaves tangentially is a contact,
527/// and the pair is marched.
528///
529/// An extrusion of a spline profile is read as the spline patch it is, on
530/// the same chart: its columns are its straight rulings, and are stated as
531/// lines.
532fn plane_along_spline_lines(
533    a: &SurfaceGeometry,
534    b: &SurfaceGeometry,
535    tol: Tolerances,
536) -> Option<Vec<SectionCurve>> {
537    const SAMPLES: u32 = 96;
538    let (plane, other, plane_first) = match (a, b) {
539        (SurfaceGeometry::Plane(p), other) => (p.plane(), other, true),
540        (other, SurfaceGeometry::Plane(p)) => (p.plane(), other, false),
541        _ => return None,
542    };
543    let extruded;
544    let (spline, ruled) = match other {
545        SurfaceGeometry::BSpline(s) => (s, None),
546        SurfaceGeometry::Extrusion(e) => {
547            extruded = extrusion_patch(e, tol)?;
548            (&extruded, Some(e))
549        }
550        _ => return None,
551    };
552    let distance = |p: Point| plane.signed_distance_to(p);
553    let ((u0, u1), (v0, v1)) = spline.domain();
554    // An iso line's control points' weighted distances to the plane, and
555    // whether the line has any length.
556    let line_at = |along_u: bool, t: f64| -> Option<ogeom_geom::BSplineCurve> {
557        if along_u {
558            spline.iso_u_curve(t, tol).ok()
559        } else {
560            spline.iso_v_curve(t, tol).ok()
561        }
562    };
563    let weighted = |curve: &ogeom_geom::BSplineCurve| -> Vec<f64> {
564        curve
565            .control_points()
566            .iter()
567            .map(|w| w.weight * distance(w.point()))
568            .collect()
569    };
570    let lies_in = |curve: &ogeom_geom::BSplineCurve| {
571        curve
572            .control_points()
573            .iter()
574            .all(|w| distance(w.point()).abs() <= tol.confusion())
575    };
576    let has_length = |curve: &ogeom_geom::BSplineCurve| {
577        let first = curve.control_points()[0].point();
578        curve
579            .control_points()
580            .iter()
581            .any(|w| w.point().distance(first) > tol.confusion())
582    };
583    // The iso lines of one family lying in the plane: the chart's borders,
584    // and every root of the control point that strays furthest.
585    let found = |along_u: bool| -> Option<Vec<f64>> {
586        let (lo, hi) = if along_u { (u0, u1) } else { (v0, v1) };
587        let at = |k: u32| lo + (hi - lo) * f64::from(k) / f64::from(SAMPLES);
588        let rows: Vec<Vec<f64>> = (0..=SAMPLES)
589            .map(|k| line_at(along_u, at(k)).map(|c| weighted(&c)))
590            .collect::<Option<_>>()?;
591        let count = rows[0].len();
592        if rows.iter().any(|r| r.len() != count) {
593            return None;
594        }
595        let widest = (0..count).max_by(|&i, &j| {
596            let spread = |i: usize| rows.iter().fold(0.0_f64, |m, r| m.max(r[i].abs()));
597            spread(i).total_cmp(&spread(j))
598        })?;
599        let mut roots = vec![lo, hi];
600        for k in 0..SAMPLES {
601            let (mut a, mut b) = (at(k), at(k + 1));
602            let (da, db) = (rows[k as usize][widest], rows[k as usize + 1][widest]);
603            if da == 0.0 {
604                roots.push(a);
605                continue;
606            }
607            if da * db > 0.0 {
608                continue;
609            }
610            let sign = da.signum();
611            for _ in 0..80 {
612                let mid = f64::midpoint(a, b);
613                let d = weighted(&line_at(along_u, mid)?)[widest];
614                if d * sign > 0.0 {
615                    a = mid;
616                } else {
617                    b = mid;
618                }
619            }
620            roots.push(f64::midpoint(a, b));
621        }
622        roots.sort_by(f64::total_cmp);
623        roots.dedup_by(|x, y| (*x - *y).abs() <= tol.parametric());
624        Some(
625            roots
626                .into_iter()
627                .filter(|&t| line_at(along_u, t).is_some_and(|c| lies_in(&c) && has_length(&c)))
628                .collect(),
629        )
630    };
631    let columns = found(true)?;
632    let rows = found(false)?;
633    if columns.is_empty() && rows.is_empty() {
634        return None;
635    }
636    // The chart cut by the lines found into cells: the surface keeps to one
637    // side of the plane within each, or it meets the plane elsewhere too.
638    let strip = |lines: &[f64], t: f64| lines.iter().filter(|&&x| x < t).count();
639    let near_line = |lines: &[f64], t: f64, span: f64| {
640        lines
641            .iter()
642            .any(|&x| (x - t).abs() <= span / f64::from(SAMPLES) * 0.25)
643    };
644    let mut sides: ogeom_core::FastMap<(usize, usize), f64> = ogeom_core::FastMap::default();
645    let band = tol.confusion() * 10.0;
646    for i in 0..=SAMPLES {
647        let u = u0 + (u1 - u0) * (f64::from(i) + 0.5) / f64::from(SAMPLES + 1);
648        if near_line(&columns, u, u1 - u0) {
649            continue;
650        }
651        for j in 0..=SAMPLES {
652            let v = v0 + (v1 - v0) * (f64::from(j) + 0.5) / f64::from(SAMPLES + 1);
653            if near_line(&rows, v, v1 - v0) {
654                continue;
655            }
656            let d = distance(spline.point_at(u, v, tol).ok()?);
657            if d.abs() <= band {
658                continue;
659            }
660            let cell = (strip(&columns, u), strip(&rows, v));
661            match sides.get(&cell) {
662                Some(side) if side * d < 0.0 => return None,
663                Some(_) => {}
664                None => {
665                    sides.insert(cell, d.signum());
666                }
667            }
668        }
669    }
670    // Each line must be a crossing, the cells either side of it on
671    // opposite sides of the plane (across the border of a closed chart,
672    // the cells at its two ends), or lie on an open border the surface
673    // leaves at an angle, the one cell beside it on a side. A line the
674    // surface only touches, or with no cell beside it to say, is a tangency
675    // the marcher and the contact handling answer, and so is the whole pair.
676    let closed_u = spline.is_closed_u(tol);
677    let closed_v = spline.is_closed_v(tol);
678    let plane_normal = plane.normal().vector();
679    let leaves_plane = |along_u: bool, t: f64| -> bool {
680        let (lo, hi) = if along_u { (v0, v1) } else { (u0, u1) };
681        // The grid's nearest row in from the border, half a step in.
682        let (from, to) = if along_u { (u0, u1) } else { (v0, v1) };
683        let half_step = (to - from) / (2.0 * f64::from(SAMPLES + 1));
684        let inside = if (t - from).abs() <= (to - t).abs() {
685            t + half_step
686        } else {
687            t - half_step
688        };
689        [0.0, 0.07, 0.19, 0.37, 0.53, 0.71, 0.89, 1.0]
690            .iter()
691            .all(|&f| {
692                let s = (hi - lo).mul_add(f, lo);
693                let (on, near) = if along_u {
694                    ((t, s), (inside, s))
695                } else {
696                    ((s, t), (s, inside))
697                };
698                let (Ok(n), Ok(p), Ok(q)) = (
699                    spline.normal_at(on.0, on.1, tol),
700                    spline.point_at(on.0, on.1, tol),
701                    spline.point_at(near.0, near.1, tol),
702                ) else {
703                    return false;
704                };
705                let sine = n.vector().cross(plane_normal).magnitude();
706                leaves_at_an_angle(sine, p.distance(q), distance(q).abs(), band)
707            })
708    };
709    let on_open_border = |t: f64, (lo, hi): (f64, f64), closed: bool| {
710        !closed && ((t - lo).abs() <= tol.parametric() || (t - hi).abs() <= tol.parametric())
711    };
712    let side_of = |cu: Option<usize>, cv: Option<usize>| -> Option<f64> {
713        let mut found = sides
714            .iter()
715            .filter(|((u, v), _)| cu.is_none_or(|c| c == *u) && cv.is_none_or(|c| c == *v))
716            .map(|(_, s)| *s);
717        let first = found.next()?;
718        found.all(|s| s == first).then_some(first)
719    };
720    let crosses = |k: usize,
721                   count: usize,
722                   closed: bool,
723                   border: bool,
724                   cell: &dyn Fn(usize) -> Option<f64>| {
725        let below = cell(k).or_else(|| closed.then(|| (0..=count).rev().find_map(cell)).flatten());
726        let above = cell(k + 1).or_else(|| closed.then(|| (0..=count).find_map(cell)).flatten());
727        match (below, above) {
728            (Some(x), Some(y)) => x * y < 0.0,
729            (Some(_), None) | (None, Some(_)) => border,
730            (None, None) => false,
731        }
732    };
733    for (k, &u) in columns.iter().enumerate() {
734        let border = on_open_border(u, (u0, u1), closed_u) && leaves_plane(true, u);
735        if !crosses(k, columns.len(), closed_u, border, &|c| {
736            side_of(Some(c), None)
737        }) {
738            return None;
739        }
740    }
741    for (k, &v) in rows.iter().enumerate() {
742        let border = on_open_border(v, (v0, v1), closed_v) && leaves_plane(false, v);
743        if !crosses(k, rows.len(), closed_v, border, &|c| side_of(None, Some(c))) {
744            return None;
745        }
746    }
747    let mut sections = Vec::new();
748    let mut emit = |along_u: bool, t: f64| -> Option<()> {
749        let iso = line_at(along_u, t)?;
750        let curve: Curve = match ruled {
751            Some(extrusion) if along_u => {
752                let range = Curve3d::domain(&iso);
753                let axis = ogeom_math::Axis {
754                    location: extrusion.curve().point_at(t, tol).ok()?,
755                    direction: extrusion.direction(),
756                };
757                ogeom_geom::LineCurve::over(axis, range.0, range.1)
758                    .ok()?
759                    .into()
760            }
761            _ => iso.into(),
762        };
763        let range = curve.domain();
764        let chart: PlanarCurve = if along_u {
765            Line2d::over(
766                ogeom_math::Axis2::new(Point2::new(t, 0.0), ogeom_math::Direction2::Y),
767                range.0,
768                range.1,
769            )
770        } else {
771            Line2d::over(
772                ogeom_math::Axis2::new(Point2::new(0.0, t), ogeom_math::Direction2::X),
773                range.0,
774                range.1,
775            )
776        }
777        .ok()?
778        .into();
779        let flat = exact_pcurve(&curve, range, a_or_b(plane_first, a, b), tol)?;
780        let (on_a, on_b) = if plane_first {
781            (Some(flat), Some(chart))
782        } else {
783            (Some(chart), Some(flat))
784        };
785        sections.push(SectionCurve {
786            on_a,
787            on_b,
788            tolerance: 0.0,
789            exact: true,
790            closed: curve.is_closed(tol),
791            tangential: false,
792            curve,
793        });
794        Some(())
795    };
796    // Every line found: the chart's two borders across a closed surface
797    // are one line in space, stated once.
798    for (k, &u) in columns.iter().enumerate() {
799        if closed_u
800            && k + 1 == columns.len()
801            && k > 0
802            && columns[0] == u0
803            && (u - u1).abs() <= tol.parametric()
804        {
805            continue;
806        }
807        emit(true, u)?;
808    }
809    for (k, &v) in rows.iter().enumerate() {
810        if closed_v
811            && k + 1 == rows.len()
812            && k > 0
813            && rows[0] == v0
814            && (v - v1).abs() <= tol.parametric()
815        {
816            continue;
817        }
818        emit(false, v)?;
819    }
820    Some(sections)
821}
822
823/// An extrusion of a spline profile as a spline patch on the extrusion's
824/// own chart: the profile's knots along `u`, and a straight run along `v`
825/// over the extrusion's window, so each point and each parameter is the
826/// extrusion's. `None` for any other profile.
827fn extrusion_patch(
828    extrusion: &ogeom_geom::ExtrusionSurface,
829    tol: Tolerances,
830) -> Option<ogeom_geom::BSplineSurface> {
831    let Curve::BSpline(profile) = extrusion.curve() else {
832        return None;
833    };
834    let (_, (v0, v1)) = extrusion.domain();
835    let step = extrusion.direction().vector();
836    let mut grid = Vec::with_capacity(profile.control_points().len() * 2);
837    for c in profile.control_points() {
838        for v in [v0, v1] {
839            grid.push(ogeom_math::Weighted {
840                scaled: Point::from_vector(c.scaled.to_vector() + step * (v * c.weight)),
841                weight: c.weight,
842            });
843        }
844    }
845    let count = profile.control_points().len();
846    ogeom_geom::BSplineSurface::rational(
847        profile.knots().clone(),
848        ogeom_math::KnotVector::new(vec![v0, v0, v1, v1], 1).ok()?,
849        ogeom_math::ControlGrid::new(grid, count, 2).ok()?,
850    )
851    .ok()
852    .filter(|patch| {
853        let same = |p: (f64, f64), q: (f64, f64)| {
854            (p.0 - q.0).abs() <= tol.parametric() && (p.1 - q.1).abs() <= tol.parametric()
855        };
856        let (pu, pv) = patch.domain();
857        let (eu, ev) = extrusion.domain();
858        same(pu, eu) && same(pv, ev)
859    })
860}
861
862/// The first surface where `first` holds, else the second.
863fn a_or_b<'s>(first: bool, a: &'s SurfaceGeometry, b: &'s SurfaceGeometry) -> &'s SurfaceGeometry {
864    if first { a } else { b }
865}
866
867/// Two drums whose axes are all but parallel, over the height they share.
868///
869/// Parallel drums meet in straight lines along their axes, and drums whose
870/// axes lean a ten-thousandth apart (a drilled hole beside a fillet of a
871/// converted mesh, each axis fitted to its own facets) meet in a quartic
872/// that departs from those lines by less than a micron over any height a
873/// part has. Marched, it comes back as fitted curves that cost seconds to
874/// cross and wander where the drums nearly touch. Here each is solved in
875/// the cross-sections along the shared height and kept as the line through
876/// its ends where every station lies near it, that departure stated as the
877/// section's tolerance.
878///
879/// `None` where the axes lean further, where the drums do not cross
880/// cleanly at every station (a crossing starting part way up, or a near
881/// touch), or where a station strays: the marcher answers those. An empty
882/// answer is drums that share no height.
883fn near_parallel_drums(
884    a: &SurfaceGeometry,
885    b: &SurfaceGeometry,
886    tol: Tolerances,
887) -> Option<Vec<SectionCurve>> {
888    const LEAN: f64 = 1e-3;
889    let (SurfaceGeometry::Cylinder(sa), SurfaceGeometry::Cylinder(sb)) = (a, b) else {
890        return None;
891    };
892    let (ca, cb) = (sa.cylinder(), sb.cylinder());
893    let (axis_a, axis_b) = (ca.axis(), cb.axis());
894    let (da, db) = (axis_a.direction.vector(), axis_b.direction.vector());
895    let (ra, rb) = (ca.radius(), cb.radius());
896    let cos = da.dot(db);
897    if da.cross(db).magnitude() > LEAN || cos.abs() < 0.5 {
898        return None;
899    }
900    let (pa, pb) = (axis_a.location, axis_b.location);
901    // The shared height, measured along the first axis.
902    let (_, (a0, a1)) = a.domain();
903    let (_, (b0, b1)) = b.domain();
904    let along = |v: f64| (pb - pa).dot(da) + v * cos;
905    let (lo, hi) = (
906        a0.min(a1).max(along(b0).min(along(b1))),
907        a0.max(a1).min(along(b0).max(along(b1))),
908    );
909    if !(lo.is_finite() && hi.is_finite()) {
910        return None;
911    }
912    if hi - lo <= tol.confusion() {
913        return Some(Vec::new());
914    }
915    // Where the two cross-sections at a station meet, left and right of
916    // the line of centres: the second drum's section is an ellipse only a
917    // square of its lean away from a circle, a stated part of the stray.
918    let meet = |z: f64| -> Option<[Point; 2]> {
919        let centre_a = pa + da * z;
920        let s = (centre_a - pb).dot(da) / cos;
921        let centre_b = pb + db * s;
922        let mut between = centre_b - centre_a;
923        between = between - da * between.dot(da);
924        let d = between.magnitude();
925        // Axes, or a touch, well inside the weld distance are one: the
926        // sliver between the drums is welded rather than sectioned. Half
927        // of it, so a sliver at the weld distance itself is sectioned
928        // whole rather than lost between the two readings.
929        let margin = tol.confusion() * 50.0;
930        if d <= margin || d >= ra + rb - margin || d <= (ra - rb).abs() + margin {
931            return None;
932        }
933        let x = (d * d + ra * ra - rb * rb) / (2.0 * d);
934        let h = (ra * ra - x * x).max(0.0).sqrt();
935        let ex = between / d;
936        let ey = da.cross(ex);
937        Some([centre_a + ex * x + ey * h, centre_a + ex * x - ey * h])
938    };
939    lines_through_stations(lo, hi, meet, rb * (1.0 / cos.abs() - 1.0), tol)
940}
941
942/// How far a near-parallel pair's sections may stray from the true
943/// crossing: what a fitted section typically carries.
944const NEAR_PARALLEL_STRAY: f64 = 1e-5;
945
946/// The two curves a near-parallel pair meets in over the height `lo..hi`,
947/// from where `meet` puts the crossing at each height: the line through the
948/// ends where every station lies within a micron of it, else a cubic
949/// through the stations at their heights, checked midway between them.
950/// Either is kept within [`NEAR_PARALLEL_STRAY`], the departure stated as
951/// its tolerance. `None` where a station has no clean crossing or the
952/// curve strays.
953fn lines_through_stations(
954    lo: f64,
955    hi: f64,
956    meet: impl Fn(f64) -> Option<[Point; 2]>,
957    stated: f64,
958    tol: Tolerances,
959) -> Option<Vec<SectionCurve>> {
960    const STATIONS: u32 = 32;
961    const STRAIGHT: f64 = 1e-6;
962    let at = |k: f64| (hi - lo).mul_add(k / f64::from(STATIONS), lo);
963    let heights: Vec<f64> = (0..=STATIONS).map(|k| at(f64::from(k))).collect();
964    let met: Vec<[Point; 2]> = heights.iter().map(|&z| meet(z)).collect::<Option<_>>()?;
965    let between: Vec<[Point; 2]> = (0..STATIONS)
966        .map(|k| meet(at(f64::from(k) + 0.5)))
967        .collect::<Option<_>>()?;
968    let mut out = Vec::with_capacity(2);
969    for side in 0..2 {
970        let (from, to) = (met[0][side], met[met.len() - 1][side]);
971        let span = to - from;
972        let length = span.magnitude();
973        if length <= tol.confusion() {
974            return None;
975        }
976        let off_line = |p: Point| {
977            let t = (p - from).dot(span) / (length * length);
978            p.distance(from + span * t)
979        };
980        let stray = met
981            .iter()
982            .chain(&between)
983            .map(|pair| off_line(pair[side]))
984            .fold(0.0_f64, f64::max);
985        let (curve, stray): (Curve, f64) = if stray <= STRAIGHT {
986            (
987                ogeom_geom::LineCurve::segment(from, to, tol).ok()?.into(),
988                stray,
989            )
990        } else {
991            let points: Vec<Point> = met.iter().map(|pair| pair[side]).collect();
992            let fitted =
993                ogeom_geom::fit::fit_points_at(&heights, &points, 3, tol.confusion(), tol).ok()?;
994            let curve: Curve = fitted.curve.into();
995            let mut worst = fitted.error;
996            for (k, pair) in (0..STATIONS).zip(&between) {
997                let p = curve.point_at(at(f64::from(k) + 0.5), tol).ok()?;
998                worst = worst.max(p.distance(pair[side]));
999            }
1000            (curve, worst)
1001        };
1002        let tolerance = stray + stated + tol.confusion();
1003        if tolerance > NEAR_PARALLEL_STRAY {
1004            return None;
1005        }
1006        out.push(SectionCurve {
1007            curve,
1008            on_a: None,
1009            on_b: None,
1010            tolerance,
1011            exact: false,
1012            closed: false,
1013            tangential: false,
1014        });
1015    }
1016    Some(out)
1017}
1018
1019/// A drum passing clean through a ball: every line along the drum meets
1020/// the ball twice, within the drum's height.
1021///
1022/// Then each of the two loops the drum and ball meet in is a function of
1023/// the angle round the drum: at each angle, where the line along the drum
1024/// enters and leaves the ball is a quadratic's two roots. The loops are
1025/// sampled so, exactly, and fitted closed, the fit's error stated as the
1026/// section's tolerance. Marched instead, a drum that all but grazes the
1027/// ball's far side leaves loops long and thin, and the trace wanders along
1028/// them past any bound. `None` where some line misses or grazes the ball,
1029/// or leaves the drum's height: the marcher answers those.
1030fn ball_through_drum(
1031    a: &SurfaceGeometry,
1032    b: &SurfaceGeometry,
1033    tol: Tolerances,
1034) -> Option<Vec<SectionCurve>> {
1035    const SAMPLES: u32 = 256;
1036    const STRAY: f64 = 1e-5;
1037    let (ball, drum, ball_first) = match (a, b) {
1038        (SurfaceGeometry::Sphere(s), SurfaceGeometry::Cylinder(c)) => (s, c, true),
1039        (SurfaceGeometry::Cylinder(c), SurfaceGeometry::Sphere(s)) => (s, c, false),
1040        _ => return None,
1041    };
1042    let (sphere, cylinder) = (ball.sphere(), drum.cylinder());
1043    let frame = cylinder.frame();
1044    let (x, y, d) = (frame.x().vector(), frame.y().vector(), frame.z().vector());
1045    let (origin, r) = (frame.origin(), cylinder.radius());
1046    let (centre, big) = (sphere.centre(), sphere.radius());
1047    let ball_frame = sphere.frame();
1048    let (_, (h0, h1)) = drum.domain();
1049    // A line that only just meets the ball leaves the loop turning sharply
1050    // there; a tenth of the drum's radius of chord inside the ball keeps
1051    // the loops smooth enough to fit.
1052    let margin = r * 0.1;
1053    // Where the line along the drum at `angle` enters and leaves the ball.
1054    let heights = |angle: f64| -> Option<[f64; 2]> {
1055        let foot = origin + (x * angle.cos() + y * angle.sin()) * r;
1056        let w = foot - centre;
1057        let half = d.dot(w);
1058        let disc = half.mul_add(half, -(w.dot(w) - big * big));
1059        if disc <= margin * margin {
1060            return None;
1061        }
1062        let root = disc.sqrt();
1063        let pair = [-half - root, -half + root];
1064        pair.iter().all(|v| *v >= h0 && *v <= h1).then_some(pair)
1065    };
1066    let at = |angle: f64, v: f64| origin + (x * angle.cos() + y * angle.sin()) * r + d * v;
1067    // The ball's longitude and latitude of a point, as its chart reads them.
1068    let on_ball = |p: Point, before: Option<Point2>| -> Point2 {
1069        let local = ball_frame.to_local(p);
1070        let lat = local.z.atan2(local.x.hypot(local.y));
1071        let mut lon = local.y.atan2(local.x).rem_euclid(core::f64::consts::TAU);
1072        if let Some(prev) = before {
1073            while lon - prev.x > core::f64::consts::PI {
1074                lon -= core::f64::consts::TAU;
1075            }
1076            while prev.x - lon > core::f64::consts::PI {
1077                lon += core::f64::consts::TAU;
1078            }
1079        }
1080        Point2::new(lon, lat)
1081    };
1082    let angle_of = |k: f64| core::f64::consts::TAU * k / f64::from(SAMPLES);
1083    let params: Vec<f64> = (0..=SAMPLES).map(|k| angle_of(f64::from(k))).collect();
1084    let mut sampled: Vec<[f64; 2]> = Vec::with_capacity(params.len());
1085    for &angle in &params {
1086        sampled.push(heights(angle)?);
1087    }
1088    let mut out = Vec::with_capacity(2);
1089    for side in 0..2 {
1090        let points: Vec<Point> = params
1091            .iter()
1092            .zip(&sampled)
1093            .map(|(&angle, pair)| at(angle, pair[side]))
1094            .collect();
1095        let on_drum: Vec<Point2> = params
1096            .iter()
1097            .zip(&sampled)
1098            .map(|(&angle, pair)| Point2::new(angle, pair[side]))
1099            .collect();
1100        let mut on_sphere: Vec<Point2> = Vec::with_capacity(points.len());
1101        for p in &points {
1102            let q = on_ball(*p, on_sphere.last().copied());
1103            on_sphere.push(q);
1104        }
1105        let target = tol.confusion() * 10.0;
1106        let curve: Curve = ogeom_geom::fit::fit_points_at(&params, &points, 3, target, tol)
1107            .ok()?
1108            .curve
1109            .into();
1110        let drum_image: PlanarCurve =
1111            ogeom_geom::fit::fit_points_2d_at(&params, &on_drum, 3, target, tol)
1112                .ok()?
1113                .curve
1114                .into();
1115        let ball_image: PlanarCurve =
1116            ogeom_geom::fit::fit_points_2d_at(&params, &on_sphere, 3, target, tol)
1117                .ok()?
1118                .curve
1119                .into();
1120        // Checked at the samples and midway between them: the curve, and
1121        // each surface read through its image, against the true meeting.
1122        let mut stray = 0.0_f64;
1123        for k in 0..(2 * SAMPLES) {
1124            let angle = angle_of(f64::from(k) / 2.0);
1125            let truth = at(angle, heights(angle)?[side]);
1126            let on_curve = curve.point_at(angle, tol).ok()?;
1127            let uv = drum_image.point_at(angle, tol).ok()?;
1128            let through_drum = drum.point_at(uv.x, uv.y, tol).ok()?;
1129            let uv = ball_image.point_at(angle, tol).ok()?;
1130            let through_ball = ball.point_at(uv.x, uv.y, tol).ok()?;
1131            stray = stray
1132                .max(truth.distance(on_curve))
1133                .max(truth.distance(through_drum))
1134                .max(truth.distance(through_ball));
1135        }
1136        let tolerance = stray.max(tol.confusion());
1137        if tolerance > STRAY {
1138            return None;
1139        }
1140        let (on_a, on_b) = if ball_first {
1141            (ball_image, drum_image)
1142        } else {
1143            (drum_image, ball_image)
1144        };
1145        out.push(SectionCurve {
1146            curve,
1147            on_a: Some(on_a),
1148            on_b: Some(on_b),
1149            tolerance,
1150            exact: false,
1151            closed: true,
1152            tangential: false,
1153        });
1154    }
1155    Some(out)
1156}
1157
1158/// A plane leaning all but along a drum's axis, over the drum's height.
1159///
1160/// The closed form is an ellipse whose long axis is the drum's radius over
1161/// the lean, kilometres for a facet group fitted a hundred-thousandth off
1162/// a hole's axis. Its parameter spans the few millimetres the drum holds in
1163/// a millionth of a turn, and crossings solved on it are only as good as
1164/// that ruler. The crossing is solved instead in the drum's cross-sections
1165/// along its height and kept as two lines where they hold, as
1166/// [`near_parallel_drums`] does. `None` where the lean is exactly nothing
1167/// (the closed form's lines are exact) or more than a thousandth, or where
1168/// the plane does not cross the drum cleanly all the way up.
1169fn near_parallel_plane_drum(
1170    a: &SurfaceGeometry,
1171    b: &SurfaceGeometry,
1172    tol: Tolerances,
1173) -> Option<Vec<SectionCurve>> {
1174    const LEAN: f64 = 1e-3;
1175    const SPAN: f64 = 3e4;
1176    let (plane, drum, surface) = match (a, b) {
1177        (SurfaceGeometry::Plane(p), SurfaceGeometry::Cylinder(c)) => (p.plane(), c.cylinder(), b),
1178        (SurfaceGeometry::Cylinder(c), SurfaceGeometry::Plane(p)) => (p.plane(), c.cylinder(), a),
1179        _ => return None,
1180    };
1181    let axis = drum.axis();
1182    let (d, r) = (axis.direction.vector(), drum.radius());
1183    let n = plane.normal().vector();
1184    let lean = n.dot(d).abs();
1185    // Only where the ellipse is thirty metres or more across: there a
1186    // parameter solved to its last billionth lands tens of nanometres off in
1187    // space, past the weld of a face with tight edges. A shorter one is
1188    // ruler enough, and its closed form crosses faster than a fitted curve.
1189    if lean <= tol.angular() || lean > LEAN || r / lean < SPAN {
1190        return None;
1191    }
1192    let across = n - d * n.dot(d);
1193    let k = across.magnitude();
1194    let e1 = across / k;
1195    let e2 = d.cross(e1);
1196    let (_, (lo, hi)) = surface.domain();
1197    if !(lo.is_finite() && hi.is_finite()) || hi - lo <= tol.confusion() {
1198        return None;
1199    }
1200    let meet = |z: f64| -> Option<[Point; 2]> {
1201        let centre = axis.location + d * z;
1202        let u = -plane.signed_distance_to(centre) / k;
1203        let margin = tol.confusion() * 1e3;
1204        if u.abs() >= r - margin {
1205            return None;
1206        }
1207        let w = r.mul_add(r, -(u * u)).sqrt();
1208        Some([centre + e1 * u + e2 * w, centre + e1 * u - e2 * w])
1209    };
1210    lines_through_stations(lo, hi, meet, 0.0, tol)
1211}
1212
1213/// An exact curve dressed as a section, clipped to the surfaces it lies on.
1214///
1215/// The analytic layer works on the unbounded geometry (a plane and a cylinder
1216/// meet in unbounded lines), but the *surfaces* carry finite extents, and a
1217/// section running a billion units past both is not something an edge can be
1218/// built on. A line is clipped to the parameter interval where it is inside
1219/// both extents, through its exact pcurves; a curve wholly outside either
1220/// extent is dropped, or the boolean above would see a phantom edge on a
1221/// region the face does not have.
1222///
1223/// A *closed* curve partially outside an extent is kept whole: cutting it into
1224/// arcs is the restriction problem, and the restriction that matters is the
1225/// face's trim, which is the boolean's job. The extent here is only the
1226/// surface's parameterization window.
1227fn exact_section(
1228    curve: Curve,
1229    a: &SurfaceGeometry,
1230    b: &SurfaceGeometry,
1231    tol: Tolerances,
1232) -> Option<SectionCurve> {
1233    let closed = match &curve {
1234        Curve::Circle(_) | Curve::Ellipse(_) => true,
1235        _ => curve.is_closed(tol),
1236    };
1237    let range = curve.domain();
1238    let on_a = exact_pcurve(&curve, range, a, tol);
1239    let on_b = exact_pcurve(&curve, range, b, tol);
1240
1241    if let Curve::Line(_) = &curve {
1242        // Clip through whichever pcurves exist; a missing pcurve leaves that
1243        // surface's extent unenforced, which errs long rather than wrong.
1244        let mut interval = curve.domain();
1245        if let Some(p) = &on_a {
1246            interval = intersect_intervals(interval, inside_box(p, a))?;
1247        }
1248        if let Some(p) = &on_b {
1249            interval = intersect_intervals(interval, inside_box(p, b))?;
1250        }
1251        let (lo, hi) = interval;
1252        let Curve::Line(line) = &curve else {
1253            unreachable!()
1254        };
1255        let clipped: Curve = ogeom_geom::LineCurve::over(line.axis(), lo, hi)
1256            .ok()?
1257            .into();
1258        let clip2 = |p: &PlanarCurve| -> Option<PlanarCurve> {
1259            let PlanarCurve::Line(l) = p else {
1260                return Some(p.clone());
1261            };
1262            Some(Line2d::over(l.axis(), lo, hi).ok()?.into())
1263        };
1264        let (ca, cb) = (on_a.as_ref().and_then(clip2), on_b.as_ref().and_then(clip2));
1265        let tangential = touching_along(&clipped, ca.as_ref(), cb.as_ref(), a, b, tol);
1266        return Some(SectionCurve {
1267            on_a: ca,
1268            on_b: cb,
1269            tolerance: 0.0,
1270            exact: true,
1271            closed: false,
1272            tangential,
1273            curve: clipped,
1274        });
1275    }
1276
1277    // A closed curve: dropped only when wholly outside an extent it has a
1278    // pcurve to check against.
1279    for (pcurve, surface) in [(&on_a, a), (&on_b, b)] {
1280        if let Some(p) = pcurve
1281            && !touches_box(p, surface, tol)
1282        {
1283            return None;
1284        }
1285    }
1286    let tangential = touching_along(&curve, on_a.as_ref(), on_b.as_ref(), a, b, tol);
1287    Some(SectionCurve {
1288        on_a,
1289        on_b,
1290        tolerance: 0.0,
1291        exact: true,
1292        closed,
1293        tangential,
1294        curve,
1295    })
1296}
1297
1298/// Whether the surfaces touch along an exact curve rather than crossing it:
1299/// their normals parallel at stations along its length.
1300///
1301/// Decided through the curve's own pcurves, which is where the normals can
1302/// be read without inverting anything. A curve missing a pcurve on either
1303/// surface is reported as a crossing, the honest default, since a section
1304/// nobody can place in a chart is one nothing can classify as contact
1305/// either.
1306fn touching_along(
1307    curve: &Curve,
1308    on_a: Option<&PlanarCurve>,
1309    on_b: Option<&PlanarCurve>,
1310    a: &SurfaceGeometry,
1311    b: &SurfaceGeometry,
1312    tol: Tolerances,
1313) -> bool {
1314    // The chart position of a sample: through the pcurve where one exists,
1315    // through the surface's own closed-form inversion where not. A meridian
1316    // through a sphere's poles has no pcurve (its longitude jumps half a
1317    // turn at each pole), but every *point* of it inverts fine, and a
1318    // tangency that would be missed for want of a pcurve becomes a crossing
1319    // section lying along a face's own boundary, which is the worst thing a
1320    // section can be.
1321    let sample_uv = |pc: Option<&PlanarCurve>,
1322                     surface: &SurfaceGeometry,
1323                     t: f64|
1324     -> Option<ogeom_math::Point2> {
1325        if let Some(pc) = pc {
1326            return pc.point_at(t, tol).ok();
1327        }
1328        let p = curve.point_at(t, tol).ok()?;
1329        chart_inversion(surface, p, tol)
1330    };
1331    let (lo, hi) = curve.domain();
1332    // Offsets chosen off the round fractions, so a curve through a chart
1333    // degeneracy (a meridian's poles sit at quarters of its turn) is
1334    // sampled beside the degenerate points rather than on them. A sample
1335    // whose inversion still fails is skipped: the point says nothing,
1336    // not that the surfaces cross.
1337    let mut judged = 0_usize;
1338    for f in [0.07, 0.19, 0.37, 0.53, 0.71, 0.89] {
1339        let t = (hi - lo).mul_add(f, lo);
1340        let (Some(ua), Some(ub)) = (sample_uv(on_a, a, t), sample_uv(on_b, b, t)) else {
1341            continue;
1342        };
1343        let (Ok(na), Ok(nb)) = (a.normal_at(ua.x, ua.y, tol), b.normal_at(ub.x, ub.y, tol)) else {
1344            continue;
1345        };
1346        if na.vector().cross(nb.vector()).magnitude() > 1e-6 {
1347            return false;
1348        }
1349        judged += 1;
1350    }
1351    judged >= 3
1352}
1353
1354/// A point's chart position on an analytic surface, by closed form.
1355fn chart_inversion(
1356    surface: &SurfaceGeometry,
1357    p: ogeom_math::Point,
1358    tol: Tolerances,
1359) -> Option<ogeom_math::Point2> {
1360    use ogeom_math::elementary;
1361    let (u, v) = match surface {
1362        SurfaceGeometry::Plane(s) => elementary::plane_parameters(&s.plane(), p),
1363        SurfaceGeometry::Cylinder(s) => {
1364            elementary::cylinder_parameters(&s.cylinder(), p, tol).ok()?
1365        }
1366        SurfaceGeometry::Cone(s) => elementary::cone_parameters(&s.cone(), p, tol).ok()?,
1367        SurfaceGeometry::Sphere(s) => elementary::sphere_parameters(&s.sphere(), p, tol).ok()?,
1368        SurfaceGeometry::Torus(s) => elementary::torus_parameters(&s.torus(), p, tol).ok()?,
1369        _ => return None,
1370    };
1371    Some(ogeom_math::Point2::new(u, v))
1372}
1373
1374/// The parameter interval over which a 2D line stays inside a surface's
1375/// parameter box. `None` when it never enters.
1376pub(crate) fn inside_box(pcurve: &PlanarCurve, surface: &SurfaceGeometry) -> Option<(f64, f64)> {
1377    // The pcurve as a point and a rate along its own parameter: a line, or
1378    // a degree-one spline of two points (a cone's ruling), linear in it.
1379    let (o, d) = match pcurve {
1380        PlanarCurve::Line(line) => {
1381            let axis = line.axis();
1382            (axis.location, axis.direction.vector())
1383        }
1384        PlanarCurve::BSpline(spline)
1385            if spline.knots().degree() == 1 && spline.control_points().len() == 2 =>
1386        {
1387            let (t0, t1) = spline.knots().domain();
1388            let (p0, p1) = (
1389                spline.control_points()[0].point(),
1390                spline.control_points()[1].point(),
1391            );
1392            if t1 <= t0 {
1393                return None;
1394            }
1395            let rate = (p1 - p0) / (t1 - t0);
1396            (p0 - rate * t0, rate)
1397        }
1398        _ => return None,
1399    };
1400    let ((ua, ub), (va, vb)) = surface.domain();
1401
1402    // The slab test, one axis at a time.
1403    let mut lo = f64::NEG_INFINITY;
1404    let mut hi = f64::INFINITY;
1405    for (origin, direction, low, high) in [(o.x, d.x, ua, ub), (o.y, d.y, va, vb)] {
1406        if direction.abs() <= f64::MIN_POSITIVE {
1407            if origin < low || origin > high {
1408                return None;
1409            }
1410            continue;
1411        }
1412        let (a, b) = ((low - origin) / direction, (high - origin) / direction);
1413        let (near, far) = if a < b { (a, b) } else { (b, a) };
1414        lo = lo.max(near);
1415        hi = hi.min(far);
1416    }
1417    if lo >= hi {
1418        return None;
1419    }
1420    Some((lo, hi))
1421}
1422
1423/// Whether a closed pcurve may pass through the surface's box.
1424pub(crate) fn touches_box(
1425    pcurve: &PlanarCurve,
1426    surface: &SurfaceGeometry,
1427    tol: Tolerances,
1428) -> bool {
1429    use ogeom_geom::Curve2d;
1430    let ((ua, ub), (va, vb)) = surface.domain();
1431    let (lo, hi) = pcurve.domain();
1432    // Asked of the spans between samples, not the samples alone: a plane all
1433    // but parallel to a cylinder's axis meets it in an ellipse kilometres
1434    // long, whose image on the cylinder's chart sweeps through a window a few
1435    // millimetres tall in a sliver of its turn, between any two samples.
1436    // Each span is taken as its chord's box widened by the chord's length,
1437    // which holds the curve between them wherever it bends no tighter than
1438    // the samples are apart. Kept wrongly, a curve costs a section the trim
1439    // then cuts to nothing; dropped wrongly, the faces never split.
1440    const SPANS: u32 = 64;
1441    let points: Vec<Option<ogeom_math::Point2>> = (0..=SPANS)
1442        .map(|i| {
1443            pcurve
1444                .point_at(lo + (hi - lo) * f64::from(i) / f64::from(SPANS), tol)
1445                .ok()
1446        })
1447        .collect();
1448    points.windows(2).any(|pair| {
1449        let (Some(p), Some(q)) = (pair[0], pair[1]) else {
1450            return false;
1451        };
1452        let pad = p.distance(q);
1453        // Periodic directions always contain; only a bounded one excludes.
1454        let u_ok =
1455            surface.is_periodic_u() || (p.x.max(q.x) + pad >= ua && p.x.min(q.x) - pad <= ub);
1456        let v_ok =
1457            surface.is_periodic_v() || (p.y.max(q.y) + pad >= va && p.y.min(q.y) - pad <= vb);
1458        u_ok && v_ok
1459    })
1460}
1461
1462/// The overlap of two intervals. `None` when they miss.
1463fn intersect_intervals(a: (f64, f64), b: Option<(f64, f64)>) -> Option<(f64, f64)> {
1464    let b = b?;
1465    let (lo, hi) = (a.0.max(b.0), a.1.min(b.1));
1466    if lo >= hi {
1467        return None;
1468    }
1469    Some((lo, hi))
1470}
1471
1472/// The general path: seed, trace, fit.
1473fn marched(
1474    a: &SurfaceGeometry,
1475    b: &SurfaceGeometry,
1476    windows: (Option<ParameterWindow>, Option<ParameterWindow>),
1477    options: IntersectOptions,
1478    tol: Tolerances,
1479) -> OgeomResult<SurfaceIntersection> {
1480    // Seeding and tracing walk the windowed views; everything after reads
1481    // the surfaces as given, whose chart the views share.
1482    let view_a = windows.0.and_then(|w| windowed(a, w, tol));
1483    let view_b = windows.1.and_then(|w| windowed(b, w, tol));
1484    let mut pole_reach = 0.0;
1485    let (mut out, mut contacts, touched) = loop {
1486        let (traced, touched) = branches_from_touches(
1487            view_a.as_ref().unwrap_or(a),
1488            view_b.as_ref().unwrap_or(b),
1489            options.marching,
1490            pole_reach,
1491            tol,
1492        )?;
1493        if traced.is_empty() && touched.is_empty() {
1494            return Ok(SurfaceIntersection::Apart);
1495        }
1496        let mut out = Vec::with_capacity(traced.len());
1497        let mut contacts: Vec<crate::march::Traced> = Vec::new();
1498        let mut again: Option<f64> = None;
1499        // A stretch of a border of either chart lying in the other surface
1500        // runs on that border, each point at the border's own parameter. It
1501        // was kept only where the surface leaves the other at an angle there,
1502        // which it is not asked again.
1503        let domains = (
1504            view_a.as_ref().unwrap_or(a).domain(),
1505            view_b.as_ref().unwrap_or(b).domain(),
1506        );
1507        let on_border = |params: &[(f64, f64)], ((u0, u1), (v0, v1)): ((f64, f64), (f64, f64))| {
1508            let Some(&(u, v)) = params.first() else {
1509                return false;
1510            };
1511            ((u == u0 || u == u1) && params.iter().all(|p| p.0 == u))
1512                || ((v == v0 || v == v1) && params.iter().all(|p| p.1 == v))
1513        };
1514        for (branch, crossing) in &traced {
1515            // A branch along which the two surfaces share their normal is a
1516            // tangency, not a crossing: the marcher's seeding cannot tell the
1517            // noise floor of a tangential valley from a genuine sign change, and
1518            // what it traces there is a stalled fragment of the valley, not a
1519            // section. The valley is still a curve, though, and the tangential
1520            // walker is the one that can follow it, so the fragment becomes a
1521            // seed rather than a discard, and what comes back is marked as
1522            // contact so nobody classifies by it. A branch leaving a touch
1523            // along an arm, where the section crosses itself, is a crossing
1524            // however shallow: near the touch no sample of it crosses
1525            // measurably, and the valley walker would follow some other curve
1526            // within its reach of both.
1527            let lying = on_border(&branch.on_a, domains.0) || on_border(&branch.on_b, domains.1);
1528            if !lying && !crossing && branch_is_tangential(a, b, branch, tol)? {
1529                if let Some(contact) = walk_contact(a, b, branch, &contacts, options.marching, tol)?
1530                {
1531                    contacts.push(contact);
1532                }
1533                continue;
1534            }
1535            if branch.stopped == crate::march::Stopped::RanOut {
1536                ogeom_bail!(
1537                    NotDone,
1538                    "a marched section ran out of its point budget before \
1539                 finishing; the seam is longer than the chord affords and \
1540                 fitting the truncation would state a curve that is not there"
1541                );
1542            }
1543            // A fit past its budget is still honest data: the error it reached
1544            // is carried on the record and every consumer widens by it: an
1545            // imported part's ragged pair can trace branches nothing fits, and
1546            // those sections fall outside every trim downstream. Only a trace
1547            // cut off by the point budget, refused above, states a curve that
1548            // is not there. (A boolean marching an *exact* pair whose image has
1549            // no closed form holds its own marched sections to a budget, in
1550            // its own fallback, where a miss is a miss.)
1551            let fitted = fitted_in_pieces(a, b, branch, options, tol)?;
1552            // Fitted whole past a pole of either chart within its stated
1553            // tolerance, the section passes through the pole as far as the fit
1554            // can say: the pair is traced again, cut there as within the chord.
1555            if pole_reach == 0.0 {
1556                for f in &fitted {
1557                    let stated = options.marching.chord + f.fit_error;
1558                    if crate::march::passes_a_pole(&f.curve.clone().into(), [a, b], stated, tol) {
1559                        again = Some(again.map_or(stated, |r: f64| r.max(stated)));
1560                    }
1561                }
1562            }
1563            for fitted in fitted {
1564                out.push(SectionCurve {
1565                    curve: fitted.curve.into(),
1566                    on_a: Some(fitted.on_a.into()),
1567                    on_b: Some(fitted.on_b.into()),
1568                    // The sum of the stated parts: the trace is within its chord of
1569                    // the truth, the fit within its error of the trace. Cut at a
1570                    // pole within a reach, the trace passes the pole up to that
1571                    // reach off the truth.
1572                    tolerance: (options.marching.chord + fitted.fit_error).max(pole_reach),
1573                    exact: false,
1574                    closed: fitted.closed,
1575                    tangential: false,
1576                });
1577            }
1578        }
1579        if let Some(reach) = again {
1580            pole_reach = reach;
1581            continue;
1582        }
1583        break (out, contacts, touched);
1584    };
1585    // The contacts walked through isolated touches, after those walked from
1586    // fragments, each kept unless it lies on one already.
1587    for walked in touched {
1588        let on_one = walked
1589            .points
1590            .get(walked.points.len() / 2)
1591            .is_some_and(|&p| on_one_already(p, &contacts, options.marching, tol));
1592        if !on_one {
1593            contacts.push(walked);
1594        }
1595    }
1596    for contact in &contacts {
1597        let fitted = approximate_branch(a, b, contact, options.tolerance, tol)?;
1598        out.push(SectionCurve {
1599            curve: fitted.curve.into(),
1600            on_a: Some(fitted.on_a.into()),
1601            on_b: Some(fitted.on_b.into()),
1602            tolerance: options.marching.chord + fitted.fit_error,
1603            exact: false,
1604            closed: fitted.closed,
1605            tangential: true,
1606        });
1607    }
1608    if out.is_empty() {
1609        return Ok(SurfaceIntersection::Apart);
1610    }
1611    Ok(SurfaceIntersection::Along(out))
1612}
1613
1614/// A surface narrowed to a window of its parameters, or `None` where the
1615/// window leaves it whole: see [`intersect_surfaces_within`].
1616fn windowed(
1617    surface: &SurfaceGeometry,
1618    window: ParameterWindow,
1619    tol: Tolerances,
1620) -> Option<SurfaceGeometry> {
1621    let (du, dv) = surface.domain();
1622    let eps = tol.parametric();
1623    // The part of `want` inside `have`, or `None` where a periodic
1624    // direction's window spans a period. A periodic window may straddle the
1625    // seam: the trim holds the parameter inside the window and the basis
1626    // wraps it, so the walk reads the window in the face's own terms.
1627    let narrow = |want: (f64, f64), have: (f64, f64), periodic: bool| -> Option<(f64, f64)> {
1628        if periodic {
1629            return (want.1 - want.0 < have.1 - have.0 - eps).then_some(want);
1630        }
1631        Some((want.0.max(have.0), want.1.min(have.1)))
1632    };
1633    let finite = |w: (f64, f64)| w.0.is_finite() && w.1.is_finite() && w.1 > w.0;
1634    if !finite(window.0) || !finite(window.1) {
1635        return None;
1636    }
1637    let narrower = |got: (f64, f64), have: (f64, f64)| {
1638        got.1 - got.0 < have.1 - have.0 - eps || got.0 > have.0 + eps || got.1 < have.1 - eps
1639    };
1640    let u = narrow(window.0, du, surface.is_periodic_u()).filter(|&u| narrower(u, du));
1641    let v = narrow(window.1, dv, surface.is_periodic_v()).filter(|&v| narrower(v, dv));
1642    if u.is_some_and(|u| u.1 - u.0 <= eps) || v.is_some_and(|v| v.1 - v.0 <= eps) {
1643        return None;
1644    }
1645    // A drum's height narrows in its own terms, keeping the turn periodic
1646    // where the window cannot cut it: a trim does not wrap.
1647    let base: Option<SurfaceGeometry> = match (surface, v) {
1648        (SurfaceGeometry::Cylinder(c), Some(v)) => {
1649            ogeom_geom::CylinderSurface::new(c.cylinder(), v)
1650                .ok()
1651                .map(Into::into)
1652        }
1653        (SurfaceGeometry::Cone(c), Some(v)) => ogeom_geom::ConeSurface::new(c.cone(), v)
1654            .ok()
1655            .map(Into::into),
1656        _ => None,
1657    };
1658    let trim = |basis: SurfaceGeometry, u: (f64, f64), v: (f64, f64)| {
1659        ogeom_geom::TrimmedSurface::new(basis, u, v, tol)
1660            .ok()
1661            .map(Into::into)
1662    };
1663    match (base, u) {
1664        (Some(base), Some(u)) => {
1665            let v = base.domain().1;
1666            trim(base, u, v)
1667        }
1668        (Some(base), None) => Some(base),
1669        (None, _) => {
1670            // A periodic direction the window leaves whole would stop
1671            // wrapping under a trim.
1672            let wraps_u = surface.is_periodic_u() && u.is_none();
1673            let wraps_v = surface.is_periodic_v() && v.is_none();
1674            if (u.is_none() && v.is_none()) || wraps_u || wraps_v {
1675                return None;
1676            }
1677            trim(surface.clone(), u.unwrap_or(du), v.unwrap_or(dv))
1678        }
1679    }
1680}
1681
1682/// A traced branch fitted, in pieces where whole it will not fit.
1683///
1684/// A trace winding several turns round a drum (a thread's flank meeting a
1685/// bore) is long and turns the same way throughout, and one fit of it can
1686/// run out of room and come back with an error of the drum's size. An open
1687/// branch whose fit misses by more than a hundred times its tolerance, or
1688/// strays farther from the trace than the finest step the marcher took
1689/// along it, is split at its middle sample and each half fitted the same
1690/// way, down to a floor of samples and depth; the pieces meet at the shared
1691/// sample. The marcher shortens its step where the section turns sharply
1692/// (round a neck a thousandth wide where a drill all but touches a torus),
1693/// and a fit missing by more than that step there wobbles across the neck.
1694/// A closed branch is split the same way, its two halves open and meeting
1695/// at both ends: a loop round a thin drum lying all but tangent inside a
1696/// wider one turns sharply at its tip, and fitted whole it can come back
1697/// off the trace by the drum's size. A fit that misses its tolerance by
1698/// less stands whole, its error stated: a caller takes one curve per branch
1699/// where it can, and a few microns do not warrant more. So does a branch no
1700/// split helps. Either is fitted again from the section filled in between
1701/// its samples where that brings it closer ([`filled_in`]).
1702fn fitted_in_pieces(
1703    a: &SurfaceGeometry,
1704    b: &SurfaceGeometry,
1705    branch: &crate::march::Traced,
1706    options: IntersectOptions,
1707    tol: Tolerances,
1708) -> OgeomResult<Vec<crate::approx::IntersectionCurve>> {
1709    const DEPTH: u32 = 6;
1710    const FLOOR: usize = 16;
1711    fn go(
1712        a: &SurfaceGeometry,
1713        b: &SurfaceGeometry,
1714        branch: &crate::march::Traced,
1715        options: IntersectOptions,
1716        depth: u32,
1717        tol: Tolerances,
1718    ) -> OgeomResult<Vec<crate::approx::IntersectionCurve>> {
1719        let tolerance = options.tolerance;
1720        let whole = approximate_branch(a, b, branch, tolerance, tol)?;
1721        // The finest step the marcher took between the branch's ends: the
1722        // first and last steps land on a patch edge or close the loop, and
1723        // their length says nothing of how sharply the section turns.
1724        let points = &branch.points;
1725        let inner = points.get(1..points.len().saturating_sub(1)).unwrap_or(&[]);
1726        let step = inner
1727            .windows(2)
1728            .map(|w| w[0].distance(w[1]))
1729            .fold(f64::INFINITY, f64::min);
1730        if whole.met
1731            || whole.fit_error <= step.min(tolerance * 1e2)
1732            || depth == 0
1733            || branch.points.len() < 2 * FLOOR
1734        {
1735            return Ok(vec![filled_in(a, b, branch, whole, options, tol)?]);
1736        }
1737        let middle = branch.points.len() / 2;
1738        // A half of a loop is open: it stops where the other half starts.
1739        let stopped = if branch.closed() {
1740            crate::march::Stopped::Stalled
1741        } else {
1742            branch.stopped
1743        };
1744        let half = |range: core::ops::RangeInclusive<usize>| crate::march::Traced {
1745            points: branch.points[range.clone()].to_vec(),
1746            on_a: branch.on_a[range.clone()].to_vec(),
1747            on_b: branch.on_b[range].to_vec(),
1748            stopped,
1749        };
1750        let mut pieces = go(a, b, &half(0..=middle), options, depth - 1, tol)?;
1751        pieces.extend(go(
1752            a,
1753            b,
1754            &half(middle..=branch.points.len() - 1),
1755            options,
1756            depth - 1,
1757            tol,
1758        )?);
1759        // Worse in pieces than whole (a trace that is noise, not length):
1760        // the whole stands.
1761        let worst = pieces.iter().map(|p| p.fit_error).fold(0.0_f64, f64::max);
1762        Ok(if worst < whole.fit_error {
1763            pieces
1764        } else {
1765            vec![whole]
1766        })
1767    }
1768    go(a, b, branch, options, DEPTH, tol)
1769}
1770
1771/// A branch's fit, fitted again from the section filled in between its
1772/// samples where it misses its tolerance, the closer standing.
1773///
1774/// The marcher spaces its samples by how far a chord sags, which bounds the
1775/// polyline, not a cubic laid through it: on a short branch with a few
1776/// samples spread unevenly, the fit can pass every sample within its
1777/// tolerance and stray past it between two far apart, where nothing holds
1778/// it. Each round solves the section at the middle of every chord
1779/// ([`crate::march::with_middles`]) and fits again, while the fit misses
1780/// and each round brings it closer. Every fit's error is measured against
1781/// the surfaces' crossing as before: filling in states nothing it does not
1782/// measure.
1783fn filled_in(
1784    a: &SurfaceGeometry,
1785    b: &SurfaceGeometry,
1786    branch: &crate::march::Traced,
1787    whole: crate::approx::IntersectionCurve,
1788    options: IntersectOptions,
1789    tol: Tolerances,
1790) -> OgeomResult<crate::approx::IntersectionCurve> {
1791    let mut best = whole;
1792    let mut dense = branch.clone();
1793    while best.fit_error > options.tolerance {
1794        let marching = options.marching;
1795        let Some(more) = crate::march::with_middles(
1796            (a, b),
1797            &dense,
1798            (options.tolerance, marching.chord),
1799            marching.max_points,
1800            tol,
1801        ) else {
1802            break;
1803        };
1804        if more.points.len() == dense.points.len() {
1805            break;
1806        }
1807        match approximate_branch(a, b, &more, options.tolerance, tol) {
1808            Ok(again) if again.fit_error < best.fit_error => best = again,
1809            _ => break,
1810        }
1811        dense = more;
1812    }
1813    Ok(best)
1814}
1815
1816/// Follow the contact a tangential fragment sits on, unless one already
1817/// traced covers it.
1818///
1819/// A tangential valley hands the crossing marcher several stalled fragments
1820/// (the seeds converge onto the contact from wherever they started and
1821/// wander there), so the fragments are candidates for *one* curve, not
1822/// several. A fragment whose middle already lies on a traced contact is one
1823/// of those repeats.
1824fn walk_contact(
1825    a: &SurfaceGeometry,
1826    b: &SurfaceGeometry,
1827    fragment: &crate::march::Traced,
1828    already: &[crate::march::Traced],
1829    marching: Marching,
1830    tol: Tolerances,
1831) -> OgeomResult<Option<crate::march::Traced>> {
1832    let middle = fragment.points.len() / 2;
1833    let Some(point) = fragment.points.get(middle).copied() else {
1834        return Ok(None);
1835    };
1836    if on_one_already(point, already, marching, tol) {
1837        return Ok(None);
1838    }
1839    let seed = crate::march::Contact {
1840        point,
1841        on_a: fragment.on_a[middle],
1842        on_b: fragment.on_b[middle],
1843    };
1844    // The walker refuses a seed that is not a contact; that refusal is an
1845    // answer, not a failure: the fragment simply had nothing to follow.
1846    // A walk that stalls where it started says the same thing in points:
1847    // too few to fit, so there is no contact curve to report here.
1848    Ok(trace_tangential(a, b, seed, marching, tol)
1849        .ok()
1850        .filter(|traced| traced.points.len() >= 4))
1851}
1852
1853/// Whether `point` lies on a contact of `already`: traced points sit a
1854/// step apart, so "on this curve" allows half a step of gap to the
1855/// nearest sample plus the chord budget.
1856fn on_one_already(
1857    point: Point,
1858    already: &[crate::march::Traced],
1859    marching: Marching,
1860    tol: Tolerances,
1861) -> bool {
1862    already.iter().any(|traced| {
1863        let spacing = traced
1864            .points
1865            .windows(2)
1866            .map(|w| w[0].distance(w[1]))
1867            .fold(0.0f64, f64::max);
1868        let near = traced
1869            .points
1870            .iter()
1871            .map(|p| p.distance(point))
1872            .fold(f64::INFINITY, f64::min);
1873        near <= spacing.mul_add(0.5, marching.chord.max(tol.confusion()))
1874    })
1875}
1876
1877/// Whether a traced branch runs along a tangency of the two surfaces:
1878/// their normals parallel, sampled along its length.
1879fn branch_is_tangential(
1880    a: &SurfaceGeometry,
1881    b: &SurfaceGeometry,
1882    branch: &crate::march::Traced,
1883    tol: Tolerances,
1884) -> OgeomResult<bool> {
1885    use ogeom_geom::Surface as _;
1886    let count = branch.points.len();
1887    if count == 0 {
1888        return Ok(true);
1889    }
1890    let exact = exact_surface(a) && exact_surface(b);
1891    for k in 0..5 {
1892        let i = ((k * (count - 1)) / 4).min(count - 1);
1893        let (ua, va) = branch.on_a[i];
1894        let (ub, vb) = branch.on_b[i];
1895        let (dau, dav) = a.d1_at(ua, va, tol)?;
1896        let (dbu, dbv) = b.d1_at(ub, vb, tol)?;
1897        let na = dau.cross(dav);
1898        let nb = dbu.cross(dbv);
1899        let (ma, mb) = (na.magnitude(), nb.magnitude());
1900        if ma <= tol.confusion() || mb <= tol.confusion() {
1901            continue;
1902        }
1903        // The threshold carries the fitted world: a blend surface within a
1904        // fit tolerance of true tangency crosses its host at an angle that
1905        // grows as the square root of that tolerance, and calling such a
1906        // graze transversal splits faces along slivers no classifier can
1907        // hold. Genuinely transversal analytic pairs meeting under two
1908        // degrees are the pathology, not the rule.
1909        if na.cross(nb).magnitude() / (ma * mb) > TANGENTIAL_SINE {
1910            return Ok(false);
1911        }
1912        // Two surfaces that are their own geometry hold no fit to graze
1913        // within: under the threshold they are tangent only where they do
1914        // not measurably cross.
1915        let at = crate::march::Contact {
1916            point: branch.points[i],
1917            on_a: (ua, va),
1918            on_b: (ub, vb),
1919        };
1920        if exact && crate::march::crosses_measurably(a, b, at, tol) {
1921            return Ok(false);
1922        }
1923    }
1924    Ok(true)
1925}
1926
1927/// Whether a surface is its own geometry rather than what may be a fit of
1928/// another: an analytic surface, or one swept from an analytic curve. A
1929/// patch, an offset, or a sweep of a spline may be a fit, tangent to its
1930/// neighbours only to within it.
1931fn exact_surface(surface: &SurfaceGeometry) -> bool {
1932    fn exact_curve(curve: &Curve) -> bool {
1933        match curve {
1934            Curve::Line(_)
1935            | Curve::Circle(_)
1936            | Curve::Ellipse(_)
1937            | Curve::Hyperbola(_)
1938            | Curve::Parabola(_)
1939            | Curve::Helix(_) => true,
1940            Curve::Trimmed(t) => exact_curve(t.basis()),
1941            _ => false,
1942        }
1943    }
1944    match surface {
1945        SurfaceGeometry::Plane(_)
1946        | SurfaceGeometry::Cylinder(_)
1947        | SurfaceGeometry::Cone(_)
1948        | SurfaceGeometry::Sphere(_)
1949        | SurfaceGeometry::Torus(_) => true,
1950        SurfaceGeometry::Extrusion(e) => exact_curve(e.curve()),
1951        SurfaceGeometry::Revolution(r) => exact_curve(r.curve()),
1952        SurfaceGeometry::Trimmed(t) => exact_surface(t.basis()),
1953        _ => false,
1954    }
1955}
1956
1957/// The exact pcurve of a curve lying on a surface, where the projection has
1958/// a closed form; `None` where it does not.
1959///
1960/// Public because the boolean's same-domain handling needs it: two faces on
1961/// one geometric surface may still carry different charts, and the other
1962/// face's boundary edges have to be spoken in this face's parameters before
1963/// they can split it.
1964#[must_use]
1965pub fn exact_pcurve_of(
1966    curve: &Curve,
1967    surface: &SurfaceGeometry,
1968    tol: Tolerances,
1969) -> Option<PlanarCurve> {
1970    exact_pcurve(curve, curve.domain(), surface, tol)
1971}
1972
1973/// As [`exact_pcurve_of`], with the parameter range the caller actually
1974/// uses.
1975///
1976/// A curve's chart image can depend on *which part* of the curve is meant: a
1977/// ruling on a cone crosses the apex, and its angle on the far nappe is half
1978/// a turn from its angle on the near one. The curve's own domain may span
1979/// both (an imported line's usually does), so a caller that knows its edge's
1980/// range must say so, or the exact projection may answer for the wrong side.
1981#[must_use]
1982pub fn exact_pcurve_over(
1983    curve: &Curve,
1984    range: (f64, f64),
1985    surface: &SurfaceGeometry,
1986    tol: Tolerances,
1987) -> Option<PlanarCurve> {
1988    exact_pcurve(curve, range, surface, tol)
1989}
1990
1991/// The exact pcurve of an analytic curve on an analytic surface, where the
1992/// projection has a closed form.
1993///
1994/// Same-parameter by construction: each 2D curve inherits the 3D curve's own
1995/// parameterization, so the two evaluate to the same point of the intersection
1996/// at the same `t`. The cases are the ones where that inheritance is exact;
1997/// anything else returns `None` rather than a fit, because an *exact* result
1998/// with a fitted pcurve would be a curve whose descriptions disagree by an
1999/// amount nothing on it records.
2000fn exact_pcurve(
2001    curve: &Curve,
2002    range: (f64, f64),
2003    surface: &SurfaceGeometry,
2004    tol: Tolerances,
2005) -> Option<PlanarCurve> {
2006    // A trim is a statement about *where* on a curve, not about what it is:
2007    // the basis carries the shape and the trim shares its parameter, so the
2008    // pcurve is the basis's own pcurve trimmed the same way. Answered here
2009    // rather than in every surface's own case, because the answer does not
2010    // depend on the surface at all. A *reversed* trim reads its basis at
2011    // `s + e - u` over its window `[s, e]`, and so does the planar trim
2012    // reversed, so the range the caller uses is mirrored the same way
2013    // before it reaches the basis.
2014    if let Curve::Trimmed(trimmed) = curve {
2015        let window = ogeom_geom::Curve3d::domain(&**trimmed);
2016        let range = if trimmed.is_reversed() {
2017            let sum = window.0 + window.1;
2018            (sum - range.1, sum - range.0)
2019        } else {
2020            range
2021        };
2022        let basis = exact_pcurve(trimmed.basis(), range, surface, tol)?;
2023        let pcurve: PlanarCurve = ogeom_geom::Trimmed2d::new(basis, window.0, window.1, tol)
2024            .ok()?
2025            .into();
2026        return Some(if trimmed.is_reversed() {
2027            ogeom_geom::Reversible::reversed(&pcurve)
2028        } else {
2029            pcurve
2030        });
2031    }
2032    // The cases below read a circle's or an ellipse's frame as the way its
2033    // parameter runs, so one running backwards is read as its forward
2034    // spelling, the same point at every parameter.
2035    let forward = match curve {
2036        Curve::Circle(c) if c.is_reversed() => Some(Curve::Circle(c.forward(tol).ok()?)),
2037        Curve::Ellipse(e) if e.is_reversed() => Some(Curve::Ellipse(e.forward(tol).ok()?)),
2038        _ => None,
2039    };
2040    if let Some(forward) = forward {
2041        return exact_pcurve(&forward, range, surface, tol);
2042    }
2043    // The circle cases read the frame's `z` as the axis the parameter turns
2044    // about, which holds on a right-handed frame only. A mirrored circle
2045    // stands on a left-handed one, its parameter turning from `x` towards a
2046    // `y` that runs clockwise about `z`, and is read as its right-handed
2047    // spelling: the same `x` and `y`, `z` reversed, the same point at every
2048    // parameter.
2049    if let Curve::Circle(c) = curve
2050        && c.circle().frame().handedness() == ogeom_math::Handedness::Left
2051    {
2052        let frame = c.circle().frame();
2053        let right =
2054            ogeom_math::Frame::from_axes(frame.origin(), frame.x(), frame.y(), -frame.z(), tol)
2055                .ok()?;
2056        let circle = ogeom_math::Circle::new(right, c.circle().radius(), tol).ok()?;
2057        return exact_pcurve(
2058            &Curve::Circle(ogeom_geom::CircleCurve::new(circle)),
2059            range,
2060            surface,
2061            tol,
2062        );
2063    }
2064    if let (Curve::Offset(offset), SurfaceGeometry::Plane(p)) = (curve, surface) {
2065        return offset_on_plane(offset, range, surface, p.plane(), tol);
2066    }
2067    match surface {
2068        SurfaceGeometry::Plane(p) => on_plane(curve, p.plane(), tol),
2069        SurfaceGeometry::Cylinder(c) => on_cylinder(curve, range, c.cylinder(), tol),
2070        SurfaceGeometry::Sphere(s) => on_sphere(curve, range, s.sphere(), tol),
2071        SurfaceGeometry::Torus(t) => on_torus(curve, range, t.torus(), tol),
2072        SurfaceGeometry::Cone(c) => on_cone(curve, range, c.cone(), tol),
2073        _ => None,
2074    }
2075}
2076
2077/// The parameter window of a circle's pcurve that inherits the circle's own
2078/// parameter: the whole turn, widened to the range the edge uses. An arc
2079/// whose range runs past the turn's end (its start late in the turn, its
2080/// end past the circle's origin) is read on the same straight chart line
2081/// continued, not wrapped.
2082fn circle_window(range: (f64, f64)) -> (f64, f64) {
2083    let tau = core::f64::consts::TAU;
2084    if range.0.is_finite() && range.1.is_finite() {
2085        (range.0.min(range.1).min(0.0), range.0.max(range.1).max(tau))
2086    } else {
2087        (0.0, tau)
2088    }
2089}
2090
2091/// The axis an analytic surface's angle `u` turns about: `x × y`, which is
2092/// the frame's `z` on a right-handed frame and `-z` on a left-handed
2093/// (mirrored) one. A circle round the axis runs up `u` where it turns about
2094/// this axis and down `u` where it turns against it.
2095fn spin(frame: &ogeom_math::Frame) -> ogeom_math::Vector {
2096    frame.z().vector() * frame.handedness().sign()
2097}
2098
2099/// The pcurve of a curve on a cone, for the two straight-line families.
2100///
2101/// A ruling (through the apex, on the surface) runs at constant `u`; a
2102/// circle perpendicular to the axis, centred on it, with the radius the cone
2103/// has at that height, runs at constant `v`. Both inherit the 3D curve's own
2104/// parameter, the circle with phase and winding exactly as the cylinder case.
2105/// The ruling's angle is measured over `range`, because the same line has
2106/// the opposite angle on the other side of the apex.
2107fn on_cone(
2108    curve: &Curve,
2109    range: (f64, f64),
2110    cone: ogeom_math::Cone,
2111    tol: Tolerances,
2112) -> Option<PlanarCurve> {
2113    let frame = cone.frame();
2114    let axis_z = frame.z().vector();
2115    let tau = core::f64::consts::TAU;
2116    match curve {
2117        Curve::Circle(c) => {
2118            let circle = c.circle();
2119            if circle.frame().z().vector().cross(axis_z).magnitude() > tol.angular() {
2120                return None;
2121            }
2122            let local = frame.to_local(circle.centre());
2123            if local.x.hypot(local.y) > tol.confusion() {
2124                return None;
2125            }
2126            // The cone's radius at the circle's height must be the circle's,
2127            // or (past the apex, on the far nappe, where the radius runs
2128            // negative) its negative: the same parallel half a turn round.
2129            let expected = cone
2130                .half_angle()
2131                .tan()
2132                .mul_add(local.z, cone.reference_radius());
2133            let turned = if (expected - circle.radius()).abs() <= tol.confusion() * 10.0 {
2134                0.0
2135            } else if (expected + circle.radius()).abs() <= tol.confusion() * 10.0 {
2136                core::f64::consts::PI
2137            } else {
2138                return None;
2139            };
2140            let start = circle.centre() + circle.frame().x().vector() * circle.radius();
2141            let at = frame.to_local(start);
2142            let phase = at.y.atan2(at.x) + turned;
2143            let winding = circle.frame().z().vector().dot(spin(&frame)).signum();
2144            let towards =
2145                ogeom_math::Direction2::new(ogeom_math::Vector2::new(winding, 0.0), tol).ok()?;
2146            Some(
2147                Line2d::over(
2148                    ogeom_math::Axis2::new(Point2::new(phase, local.z), towards),
2149                    circle_window(range).0,
2150                    circle_window(range).1,
2151                )
2152                .ok()?
2153                .into(),
2154            )
2155        }
2156        Curve::Line(line) => {
2157            // A ruling: verified by sample, not assumed: three points on
2158            // the surface pin a line to it.
2159            let axis = line.axis();
2160            let on = |t: f64| {
2161                let p = axis.location + axis.direction.vector() * t;
2162                cone.distance_to(p) <= tol.confusion() * 10.0
2163            };
2164            if !on(0.0) || !on(1.0) || !on(-1.0) {
2165                return None;
2166            }
2167            // A ruling reaching the tip may be *stated* from the apex
2168            // itself (where the angle is atan2(0, 0), garbage) and its
2169            // own domain usually spans both nappes, where the angles differ
2170            // by half a turn. Measure the angle at whichever end of the
2171            // *used* range stands farthest from the axis: that is the side
2172            // the caller means.
2173            let (lo, hi) = if range.0.is_finite() && range.1.is_finite() && range.0 != range.1 {
2174                range
2175            } else {
2176                line.domain()
2177            };
2178            // Only the used range votes. The line's own origin is stated
2179            // wherever the file likes (some writers park it hundreds of
2180            // kilometres down the infinite line, past the apex on the other
2181            // nappe), and letting it compete reads the angle half a turn
2182            // from the side the edge actually uses.
2183            let mut local: Option<ogeom_math::Point> = None;
2184            for t in [lo, hi] {
2185                if !t.is_finite() {
2186                    continue;
2187                }
2188                let candidate = frame.to_local(axis.location + axis.direction.vector() * t);
2189                if local.is_none_or(|held| candidate.x.hypot(candidate.y) > held.x.hypot(held.y)) {
2190                    local = Some(candidate);
2191                }
2192            }
2193            let local = local?;
2194            if local.x.hypot(local.y) <= tol.confusion() {
2195                return None;
2196            }
2197            // The chart's point at angle `u` and height `z` stands at the
2198            // radius `radius_at(z)` along `u`'s direction, and past the apex
2199            // that radius runs negative: a point on the far nappe is on the
2200            // column half a turn from its own bearing. The whole ruling,
2201            // through the apex or not, is that one column.
2202            let side = cone.radius_at(local.z).signum();
2203            let u = (local.y * side).atan2(local.x * side).rem_euclid(tau);
2204            // Same-parameter exactly: a degree-one spline over the used
2205            // range maps t linearly onto the chart column, whatever rate
2206            // the slant climbs at.
2207            let v_at = |t: f64| {
2208                frame
2209                    .to_local(axis.location + axis.direction.vector() * t)
2210                    .z
2211            };
2212            let knots = ogeom_math::KnotVector::new(vec![lo, lo, hi, hi], 1).ok()?;
2213            Some(
2214                ogeom_geom::BSpline2d::new(
2215                    knots,
2216                    vec![Point2::new(u, v_at(lo)), Point2::new(u, v_at(hi))],
2217                    tol,
2218                )
2219                .ok()?
2220                .into(),
2221            )
2222        }
2223        _ => None,
2224    }
2225}
2226
2227/// The pcurve of a circle on a torus, for the two families that are straight
2228/// lines in `(u, v)`.
2229///
2230/// A *parallel* (centred on the axis, in a plane perpendicular to it) runs
2231/// at constant `v`; a *tube circle* (minor radius, centred on the tube's
2232/// spine, in a plane through the axis) runs at constant `u`. Both inherit
2233/// the circle's own angle, phase and winding included, exactly as the
2234/// cylinder case does. Fillet faces are tori more often than not, so the
2235/// STEP reader is the chief consumer.
2236fn on_torus(
2237    curve: &Curve,
2238    range: (f64, f64),
2239    torus: ogeom_math::Torus,
2240    tol: Tolerances,
2241) -> Option<PlanarCurve> {
2242    let Curve::Circle(c) = curve else {
2243        return None;
2244    };
2245    let circle = c.circle();
2246    let frame = torus.frame();
2247    let axis_z = frame.z().vector();
2248    let normal = circle.frame().z().vector();
2249    let local = frame.to_local(circle.centre());
2250
2251    // A parallel of the sweep.
2252    if normal.cross(axis_z).magnitude() <= tol.angular()
2253        && local.x.hypot(local.y) <= tol.confusion()
2254    {
2255        let sin_v = local.z / torus.minor_radius();
2256        // On its own side of the axis, or (on a spindle, whose tube swallows
2257        // the axis) on the tube's folded half past it, where the sweep's
2258        // radius runs negative: the same parallel half a turn round.
2259        let (cos_v, turned) = [
2260            (circle.radius() - torus.major_radius(), 0.0),
2261            (
2262                -circle.radius() - torus.major_radius(),
2263                core::f64::consts::PI,
2264            ),
2265        ]
2266        .into_iter()
2267        .map(|(reach, turned)| (reach / torus.minor_radius(), turned))
2268        .find(|(cos_v, _)| (sin_v.hypot(*cos_v) - 1.0).abs() <= tol.confusion())?;
2269        let v = sin_v.atan2(cos_v);
2270        let start = circle.centre() + circle.frame().x().vector() * circle.radius();
2271        let at = frame.to_local(start);
2272        let phase = at.y.atan2(at.x) + turned;
2273        let winding = normal.dot(spin(&frame)).signum();
2274        let towards =
2275            ogeom_math::Direction2::new(ogeom_math::Vector2::new(winding, 0.0), tol).ok()?;
2276        return Some(
2277            Line2d::over(
2278                ogeom_math::Axis2::new(Point2::new(phase, v), towards),
2279                circle_window(range).0,
2280                circle_window(range).1,
2281            )
2282            .ok()?
2283            .into(),
2284        );
2285    }
2286
2287    // A circle of the tube.
2288    if (circle.radius() - torus.minor_radius()).abs() <= tol.confusion()
2289        && normal.dot(axis_z).abs() <= tol.angular()
2290        && (local.x.hypot(local.y) - torus.major_radius()).abs() <= tol.confusion()
2291        && local.z.abs() <= tol.confusion()
2292    {
2293        let u = local.y.atan2(local.x);
2294        let radial = frame.x().vector() * u.cos() + frame.y().vector() * u.sin();
2295        let xc = circle.frame().x().vector();
2296        let phase = xc.dot(axis_z).atan2(xc.dot(radial));
2297        let winding = normal.dot(radial.cross(axis_z)).signum();
2298        let towards =
2299            ogeom_math::Direction2::new(ogeom_math::Vector2::new(0.0, winding), tol).ok()?;
2300        return Some(
2301            Line2d::over(
2302                ogeom_math::Axis2::new(Point2::new(u, phase), towards),
2303                circle_window(range).0,
2304                circle_window(range).1,
2305            )
2306            .ok()?
2307            .into(),
2308        );
2309    }
2310    None
2311}
2312
2313/// The pcurve of an offset curve on a plane its reference stands square
2314/// to: the basis's own pcurve, offset in the chart.
2315///
2316/// The offset moves each point along the tangent crossed with the
2317/// reference. Within the plane, the tangent crossed with the chart's
2318/// `x × y` is the tangent turned a quarter right, so the chart offset is
2319/// the distance along the left normal with the sign flipped, and flipped
2320/// again where the reference runs against `x × y`. A reference leaning `ε`
2321/// off the normal turns the offset direction by at most `ε`, moving the
2322/// point by at most `2 |d| sin(ε / 2)`, under `|d| tan ε`; the offset is
2323/// read in the plane only where that stays within the confusion distance.
2324/// Same-parameter as the basis's pcurve is.
2325fn offset_on_plane(
2326    offset: &ogeom_geom::OffsetCurve,
2327    range: (f64, f64),
2328    surface: &SurfaceGeometry,
2329    plane: ogeom_math::Plane,
2330    tol: Tolerances,
2331) -> Option<PlanarCurve> {
2332    let frame = plane.frame();
2333    let up = frame.x().vector().cross(frame.y().vector());
2334    let reference = offset.reference().vector();
2335    let (cos, sin) = (reference.dot(up), reference.cross(up).magnitude());
2336    if cos == 0.0 || offset.distance().abs() * sin > tol.confusion() * cos.abs() {
2337        return None;
2338    }
2339    let basis = exact_pcurve(offset.basis(), range, surface, tol)?;
2340    Offset2d::new(basis, -cos.signum() * offset.distance())
2341        .ok()
2342        .map(|o| PlanarCurve::Offset(Box::new(o)))
2343}
2344
2345/// Project a curve lying in a plane into the plane's own coordinates.
2346///
2347/// Exact for a line, a circle, an ellipse, a hyperbola, a B-spline, a
2348/// parabola and a curve drawn on this plane under another chart: the
2349/// plane's frame is orthonormal, so lengths and the curves' own
2350/// parameterizations survive the projection unchanged.
2351fn on_plane(curve: &Curve, plane: ogeom_math::Plane, tol: Tolerances) -> Option<PlanarCurve> {
2352    let frame = plane.frame();
2353    let flat = |p: Point| {
2354        let local = frame.to_local(p);
2355        Point2::new(local.x, local.y)
2356    };
2357    // A direction is turned into the plane's axes alone: carried to a
2358    // point off the frame's origin and back, it would pick up the
2359    // rounding of the origin's coordinates, which far from the world's
2360    // origin tips it past the angular tolerance.
2361    let flat_direction = |d: ogeom_math::Direction| {
2362        let local = frame.vector_to_local(d.vector());
2363        ogeom_math::Direction2::new(ogeom_math::Vector2::new(local.x, local.y), tol).ok()
2364    };
2365    match curve {
2366        Curve::Line(line) => {
2367            let axis = line.axis();
2368            let through = flat(axis.location);
2369            let direction = flat_direction(axis.direction)?;
2370            let (lo, hi) = line.domain();
2371            Some(
2372                Line2d::over(ogeom_math::Axis2::new(through, direction), lo, hi)
2373                    .ok()?
2374                    .into(),
2375            )
2376        }
2377        Curve::Circle(c) => {
2378            let circle = c.circle();
2379            let frame2 = Frame2::from_axes(
2380                flat(circle.centre()),
2381                flat_direction(circle.frame().x())?,
2382                flat_direction(circle.frame().y())?,
2383                tol,
2384            )
2385            .ok()?;
2386            Some(Circle2d::new(Circle2::new(frame2, circle.radius(), tol).ok()?).into())
2387        }
2388        Curve::Ellipse(e) => {
2389            let ellipse = e.ellipse();
2390            let frame2 = Frame2::from_axes(
2391                flat(ellipse.centre()),
2392                flat_direction(ellipse.frame().x())?,
2393                flat_direction(ellipse.frame().y())?,
2394                tol,
2395            )
2396            .ok()?;
2397            Some(
2398                Ellipse2d::new(
2399                    Ellipse2::new(frame2, ellipse.major_radius(), ellipse.minor_radius(), tol)
2400                        .ok()?,
2401                )
2402                .into(),
2403            )
2404        }
2405        Curve::Hyperbola(h) => {
2406            // Run backwards over a domain symmetric about zero, the branch
2407            // is the forward one with `y` turned round: `cosh` is even and
2408            // `sinh` odd, and the chart image runs forward, as the `.brep`
2409            // format can state it. Over any other
2410            // domain the reversal reads the branch at `lo + hi - u`, a
2411            // shift no frame states, and the chart image is reversed over
2412            // the same domain, which reads it there too.
2413            let hyperbola = h.hyperbola();
2414            let (lo, hi) = h.domain();
2415            let turned = h.is_reversed() && lo + hi == 0.0;
2416            let y = if turned {
2417                -hyperbola.frame().y()
2418            } else {
2419                hyperbola.frame().y()
2420            };
2421            let frame2 = Frame2::from_axes(
2422                flat(hyperbola.centre()),
2423                flat_direction(hyperbola.frame().x())?,
2424                flat_direction(y)?,
2425                tol,
2426            )
2427            .ok()?;
2428            let flat_hyperbola = ogeom_math::Hyperbola2::new(
2429                frame2,
2430                hyperbola.major_radius(),
2431                hyperbola.minor_radius(),
2432                tol,
2433            )
2434            .ok()?;
2435            let image: PlanarCurve = ogeom_geom::Hyperbola2d::over(flat_hyperbola, lo, hi)
2436                .ok()?
2437                .into();
2438            Some(if h.is_reversed() && !turned {
2439                ogeom_geom::Reversible::reversed(&image)
2440            } else {
2441                image
2442            })
2443        }
2444        Curve::BSpline(b) => {
2445            // Affine invariance: a (rational) B-spline in the plane projects
2446            // into the plane's own coordinates control point by control
2447            // point, knots and weights untouched: exact, and same-parameter
2448            // by construction.
2449            let control = b
2450                .control_points()
2451                .iter()
2452                .map(|w| ogeom_math::Weighted::new(flat((*w).point()), w.weight, tol))
2453                .collect::<Result<Vec<_>, _>>()
2454                .ok()?;
2455            Some(
2456                ogeom_geom::BSpline2d::rational(b.knots().clone(), control)
2457                    .ok()?
2458                    .into(),
2459            )
2460        }
2461        Curve::Parabola(parabola) => {
2462            // Quadratic in its own parameter, run either way: the one
2463            // quadratic Bézier over its domain from end to end, its middle
2464            // control point where the end tangents meet, is the same curve
2465            // at every parameter.
2466            let (lo, hi) = parabola.domain();
2467            let start = parabola.point_at(lo, tol).ok()?;
2468            let middle = start + parabola.d1_at(lo, tol).ok()? * ((hi - lo) / 2.0);
2469            let end = parabola.point_at(hi, tol).ok()?;
2470            let knots = ogeom_math::KnotVector::new(vec![lo, lo, lo, hi, hi, hi], 2).ok()?;
2471            Some(
2472                ogeom_geom::BSpline2d::new(knots, vec![flat(start), flat(middle), flat(end)], tol)
2473                    .ok()?
2474                    .into(),
2475            )
2476        }
2477        Curve::OnSurface(on) => {
2478            // Drawn on this plane under another chart: the other chart's
2479            // origin and axes, read in this one, are a frame, and the
2480            // pcurve moved by it is the same point at every parameter. A
2481            // chart on a plane tipped or standing off this one is not this
2482            // plane's, and is left alone.
2483            let chart = chart_plane(on.surface())?.frame();
2484            if !chart.z().is_parallel(frame.z(), tol)
2485                || plane.distance_to(chart.origin()) > tol.confusion()
2486            {
2487                return None;
2488            }
2489            let frame2 = Frame2::from_axes(
2490                flat(chart.origin()),
2491                flat_direction(chart.x())?,
2492                flat_direction(chart.y())?,
2493                tol,
2494            )
2495            .ok()?;
2496            on.pcurve()
2497                .transformed(&ogeom_math::Transform2::from_frame(&frame2), tol)
2498                .ok()
2499        }
2500        _ => None,
2501    }
2502}
2503
2504/// The plane whose frame's `x` and `y` are a surface's chart, where it has
2505/// one: a plane, or a plane trimmed, which keeps its basis's chart.
2506fn chart_plane(surface: &SurfaceGeometry) -> Option<ogeom_math::Plane> {
2507    match surface {
2508        SurfaceGeometry::Plane(p) => Some(p.plane()),
2509        SurfaceGeometry::Trimmed(t) => chart_plane(t.basis()),
2510        _ => None,
2511    }
2512}
2513
2514/// The pcurve of a curve on a cylinder, where it is a straight line in
2515/// parameter space.
2516///
2517/// A line along the axis runs at constant `u`; a full circle around it runs at
2518/// constant `v`. Both are lines in `(u, v)`, exactly, and both inherit the 3D
2519/// curve's own parameter: height for the line, angle for the circle.
2520fn on_cylinder(
2521    curve: &Curve,
2522    range: (f64, f64),
2523    cylinder: ogeom_math::Cylinder,
2524    tol: Tolerances,
2525) -> Option<PlanarCurve> {
2526    let axis = cylinder.axis();
2527    let frame = cylinder.frame();
2528    match curve {
2529        Curve::Line(line) => {
2530            // Parallel to the axis, on the surface.
2531            let direction = line.axis().direction;
2532            let along = direction.dot(axis.direction);
2533            if !direction.is_parallel(axis.direction, tol) {
2534                return None;
2535            }
2536            let through = line.axis().location;
2537            if (axis.distance_to(through) - cylinder.radius()).abs() > tol.confusion() {
2538                return None;
2539            }
2540            let local = frame.to_local(through);
2541            let u = local.y.atan2(local.x).rem_euclid(core::f64::consts::TAU);
2542            // The 3D line's parameter is length from its origin; at constant u
2543            // the pcurve's `v` runs at the same rate, signed by whether the
2544            // line runs with the axis or against it.
2545            let (lo, hi) = line.domain();
2546            let start = Point2::new(u, local.z);
2547            let towards =
2548                ogeom_math::Direction2::new(ogeom_math::Vector2::new(0.0, along.signum()), tol)
2549                    .ok()?;
2550            Some(
2551                Line2d::over(ogeom_math::Axis2::new(start, towards), lo, hi)
2552                    .ok()?
2553                    .into(),
2554            )
2555        }
2556        Curve::Circle(c) => {
2557            let circle = c.circle();
2558            // Perpendicular to the axis, centred on it, of the same radius.
2559            if circle
2560                .frame()
2561                .z()
2562                .cross_with(axis.direction.vector())
2563                .magnitude()
2564                > tol.angular()
2565            {
2566                return None;
2567            }
2568            if axis.distance_to(circle.centre()) > tol.confusion() {
2569                return None;
2570            }
2571            if (circle.radius() - cylinder.radius()).abs() > tol.confusion() {
2572                return None;
2573            }
2574            let local = frame.to_local(circle.centre());
2575            // Where the circle's own angle zero sits in the cylinder's angle,
2576            // and which way its parameter runs around the axis. A section
2577            // circle inherits its winding from the pair that made it, and one
2578            // wound against the cylinder's `u` (a circle cut by a plane whose
2579            // normal opposes the axis) runs its pcurve in `-u`. Written `+u`
2580            // unconditionally, the pcurve evaluates half a turn away from the
2581            // curve, and the face's arrangement tears along a seam that is
2582            // not there.
2583            let start = circle.centre() + circle.frame().x().vector() * circle.radius();
2584            let at = frame.to_local(start);
2585            let phase = at.y.atan2(at.x);
2586            let winding = circle.frame().z().vector().dot(spin(&frame)).signum();
2587            let towards =
2588                ogeom_math::Direction2::new(ogeom_math::Vector2::new(winding, 0.0), tol).ok()?;
2589            Some(
2590                Line2d::over(
2591                    ogeom_math::Axis2::new(Point2::new(phase, local.z), towards),
2592                    circle_window(range).0,
2593                    circle_window(range).1,
2594                )
2595                .ok()?
2596                .into(),
2597            )
2598        }
2599        Curve::Ellipse(_) => {
2600            // An oblique plane's section: its plan projection is the
2601            // cylinder's own cross-section circle traced *uniformly*, so
2602            // the chart trace is u = s·t + φ, v = c₀ + a·cos t + b·sin t:
2603            // the trig-affine family. Derived from the curve's own
2604            // evaluations and verified by sample, never assumed.
2605            use ogeom_geom::Curve3d as _;
2606            let tau = core::f64::consts::TAU;
2607            let local = |t: f64| -> Option<ogeom_math::Point> {
2608                Some(frame.to_local(curve.point_at(t, tol).ok()?))
2609            };
2610            let l0 = local(0.0)?;
2611            let lq = local(tau / 4.0)?;
2612            let lh = local(tau / 2.0)?;
2613            // On the surface at all: plan radius must be the cylinder's.
2614            let r = cylinder.radius();
2615            for l in [&l0, &lq, &lh] {
2616                if (l.x.hypot(l.y) - r).abs() > tol.confusion() * 10.0 {
2617                    return None;
2618                }
2619            }
2620            let phase = l0.y.atan2(l0.x);
2621            // Winding from the quarter-turn sample: uniform tracing puts it
2622            // a quarter turn away, one side or the other.
2623            let uq = lq.y.atan2(lq.x);
2624            let step = (uq - phase).rem_euclid(tau);
2625            let winding = if (step - tau / 4.0).abs() < 1e-6 {
2626                1.0
2627            } else if (step - 3.0 * tau / 4.0).abs() < 1e-6 {
2628                -1.0
2629            } else {
2630                return None;
2631            };
2632            // Height coefficients from three samples.
2633            let c0 = f64::midpoint(l0.z, lh.z);
2634            let a = (l0.z - lh.z) / 2.0;
2635            let b = lq.z - c0;
2636            // The trig formula is global (cosine wraps, the linear angle
2637            // unwraps the chart), so the pcurve lives on whatever range the
2638            // edge actually spans, a loop crossing the period included.
2639            let candidate = ogeom_geom::Trig2d::new(
2640                Point2::new(phase, c0),
2641                ogeom_math::Vector2::new(winding, 0.0),
2642                ogeom_math::Vector2::new(0.0, a),
2643                ogeom_math::Vector2::new(0.0, b),
2644                range,
2645            )
2646            .ok()?;
2647            // The same-parameter law, verified at points the derivation
2648            // never touched, inside the range the edge will use.
2649            use ogeom_geom::Curve2d as _;
2650            for i in 0..7 {
2651                let t = range.0 + (range.1 - range.0) * (0.09 + 0.13 * f64::from(i)) / 0.91;
2652                let l = local(t)?;
2653                let chart = candidate.point_at(t, tol).ok()?;
2654                let du = (chart.x - l.y.atan2(l.x)).rem_euclid(tau);
2655                if du.min(tau - du) > 1e-9 {
2656                    return None;
2657                }
2658                if (chart.y - l.z).abs() > tol.confusion() * 10.0 {
2659                    return None;
2660                }
2661            }
2662            Some(PlanarCurve::Trig(candidate))
2663        }
2664        _ => None,
2665    }
2666}
2667
2668/// The pcurve of half a meridian: a great circle through both poles,
2669/// restricted to one side of them.
2670///
2671/// The whole circle has no chart image a single curve can carry (its
2672/// longitude jumps by half a turn at each pole), but each *half* does, and it
2673/// is a straight line. Writing the circle's own parameter as `t` and the
2674/// sphere's axis as `Z = cos α·X + sin α·Y` in the circle's own frame, the
2675/// point's height above the equator is `r·cos(t − α)`, so the latitude is
2676/// `asin(cos(t − α))`, which on `t − α ∈ [0, π]` is exactly `π/2 − (t − α)`,
2677/// affine in `t`, with slope one. The longitude is constant on that half and
2678/// half a turn away on the other. So the pcurve is a vertical line in the
2679/// chart, sharing the circle's parameter exactly, and the caller's `range` is
2680/// what says which half is meant.
2681///
2682/// The half is not assumed: the returned line is lifted back through the
2683/// sphere at stations along the range and compared against the circle, so a
2684/// misread orientation is caught here rather than downstream.
2685fn on_meridian(
2686    curve: &ogeom_geom::CircleCurve,
2687    range: (f64, f64),
2688    sphere: ogeom_math::Sphere,
2689    tol: Tolerances,
2690) -> Option<PlanarCurve> {
2691    let circle = curve.circle();
2692    // A reversed circle runs its own angle backwards, and the shifted angle
2693    // below is measured in the *curve's* parameter, so the sign travels with
2694    // it: the sweep flips and so do both the latitude's slope and which half
2695    // of the circle a range names.
2696    let sweep = if curve.is_reversed() { -1.0 } else { 1.0 };
2697    let frame = sphere.frame();
2698    let z = frame.z().vector();
2699    // A great circle: the sphere's own centre and radius, in a plane holding
2700    // the axis. Anything else is not a meridian.
2701    if circle.centre().distance(sphere.centre()) > tol.confusion() {
2702        return None;
2703    }
2704    if (circle.radius() - sphere.radius()).abs() > tol.confusion() {
2705        return None;
2706    }
2707    let (cx, cy) = (circle.frame().x().vector(), circle.frame().y().vector());
2708    let (xz, yz) = (cx.dot(z), cy.dot(z));
2709    // The axis must lie *in* the circle's plane, or the circle is neither a
2710    // parallel nor a meridian and has no closed-form chart image at all.
2711    if xz.hypot(yz) < 1.0 - tol.angular() {
2712        return None;
2713    }
2714    let raw_alpha = yz.atan2(xz);
2715    // `w` is the circle's own horizontal direction: the axis turned a quarter
2716    // turn within the circle's plane.
2717    let w = cx * -raw_alpha.sin() + cy * raw_alpha.cos();
2718    let local = frame.to_local(sphere.centre() + w);
2719    let longitude = local.y.atan2(local.x);
2720
2721    let half = core::f64::consts::PI;
2722    let mid = f64::midpoint(range.0, range.1);
2723    // Where the range sits relative to the poles, in the shifted angle
2724    // `x = sweep·t − α` that measures the descent from the north pole.
2725    let x_mid = (sweep * mid - raw_alpha).rem_euclid(core::f64::consts::TAU);
2726    let x_mid = if x_mid > half {
2727        x_mid - core::f64::consts::TAU
2728    } else {
2729        x_mid
2730    };
2731    let span = sweep * (range.1 - range.0);
2732    let (mut x0, mut x1) = (x_mid - span / 2.0, x_mid + span / 2.0);
2733    if x0 > x1 {
2734        core::mem::swap(&mut x0, &mut x1);
2735    }
2736    // The turn count `α` was written with is what decides whether the
2737    // latitude comes out inside the chart or a whole turn away from it, so
2738    // the branch the range actually sits on is the one the line is built
2739    // from.
2740    let alpha = sweep.mul_add(mid, -x_mid);
2741    // A range read off points found by projection overshoots a pole by
2742    // what that projection leaves: an angle the radius turns into the
2743    // confusion distance is no overshoot, and the lift below measures it.
2744    let slack = tol
2745        .parametric()
2746        .max(1e-9)
2747        .max(tol.confusion() / sphere.radius());
2748    let (axis_point, towards) = if x0 >= -slack && x1 <= half + slack {
2749        // The descending half: latitude π/2 − (sweep·t − α), longitude
2750        // constant.
2751        (
2752            Point2::new(longitude, half.mul_add(0.5, alpha)),
2753            ogeom_math::Vector2::new(0.0, -sweep),
2754        )
2755    } else if x0 >= -half - slack && x1 <= slack {
2756        // The ascending half, half a turn round the chart.
2757        (
2758            Point2::new(longitude + half, half.mul_add(0.5, -alpha)),
2759            ogeom_math::Vector2::new(0.0, sweep),
2760        )
2761    } else {
2762        // The range straddles a pole: no one line covers it.
2763        return None;
2764    };
2765    let towards = ogeom_math::Direction2::new(towards, tol).ok()?;
2766    let margin = (range.1 - range.0) * 0.25;
2767    let line: PlanarCurve = Line2d::over(
2768        ogeom_math::Axis2::new(axis_point, towards),
2769        range.0 - margin,
2770        range.1 + margin,
2771    )
2772    .ok()?
2773    .into();
2774
2775    // Measured, not assumed: the chart line lifted back through the sphere is
2776    // the circle it claims to be.
2777    for k in 0..=4 {
2778        let t = (range.1 - range.0).mul_add(f64::from(k) / 4.0, range.0);
2779        let uv = line.point_at(t, tol).ok()?;
2780        let lifted = ogeom_math::elementary::sphere_at(&sphere, uv.x, uv.y).point;
2781        let want = curve.point_at(t, tol).ok()?;
2782        if lifted.distance(want) > tol.confusion() {
2783            return None;
2784        }
2785    }
2786    Some(line)
2787}
2788
2789/// The pcurve of a circle on a sphere: a parallel of latitude, or one half of
2790/// a meridian.
2791fn on_sphere(
2792    curve: &Curve,
2793    range: (f64, f64),
2794    sphere: ogeom_math::Sphere,
2795    tol: Tolerances,
2796) -> Option<PlanarCurve> {
2797    let Curve::Circle(c) = curve else {
2798        return None;
2799    };
2800    let circle = c.circle();
2801    let frame = sphere.frame();
2802    // Perpendicular to the sphere's axis and centred on it: a parallel of
2803    // latitude, which is a horizontal line in (longitude, latitude).
2804    if circle
2805        .frame()
2806        .z()
2807        .cross_with(frame.z().vector())
2808        .magnitude()
2809        > tol.angular()
2810    {
2811        return on_meridian(c, range, sphere, tol);
2812    }
2813    let local = frame.to_local(circle.centre());
2814    if local.x.abs() > tol.confusion() || local.y.abs() > tol.confusion() {
2815        return None;
2816    }
2817    let latitude = (local.z / sphere.radius()).clamp(-1.0, 1.0).asin();
2818    // Sanity: the circle's radius must be the parallel's.
2819    if (circle.radius() - sphere.radius() * latitude.cos()).abs() > tol.confusion() {
2820        return None;
2821    }
2822    let start = circle.centre() + circle.frame().x().vector() * circle.radius();
2823    let at = frame.to_local(start);
2824    let phase = at.y.atan2(at.x);
2825    // Phase and winding exactly as the cylinder case: a parallel whose own
2826    // axis opposes the sphere's marches its angle *down* the longitude.
2827    let winding = circle.frame().z().vector().dot(spin(&frame)).signum();
2828    let towards = ogeom_math::Direction2::new(ogeom_math::Vector2::new(winding, 0.0), tol).ok()?;
2829    Some(
2830        Line2d::over(
2831            ogeom_math::Axis2::new(Point2::new(phase, latitude), towards),
2832            circle_window(range).0,
2833            circle_window(range).1,
2834        )
2835        .ok()?
2836        .into(),
2837    )
2838}
2839
2840#[cfg(test)]
2841#[allow(clippy::unwrap_used, clippy::expect_used)]
2842mod tests {
2843    use super::*;
2844    use ogeom_geom::{Curve2d, Curve3d, CylinderSurface, PlaneSurface, SphereSurface};
2845    use ogeom_math::{Cylinder, Direction, Frame, Plane, Sphere, Vector};
2846
2847    const T: Tolerances = Tolerances::millimetres();
2848
2849    fn sphere(centre: Point, radius: f64) -> SurfaceGeometry {
2850        SphereSurface::new(Sphere::centred(centre, radius, T).unwrap()).into()
2851    }
2852
2853    fn cylinder(axis: Vector, radius: f64) -> SurfaceGeometry {
2854        let frame = Frame::new(
2855            Point::ORIGIN,
2856            Direction::new(axis, T).unwrap(),
2857            Direction::from_cross(axis, Vector::new(0.3, 0.5, 0.9), T).unwrap(),
2858            T,
2859        )
2860        .unwrap();
2861        CylinderSurface::new(Cylinder::new(frame, radius, T).unwrap(), (-4.0, 4.0))
2862            .unwrap()
2863            .into()
2864    }
2865
2866    fn plane(origin: Point, normal: Vector) -> SurfaceGeometry {
2867        PlaneSurface::over(
2868            Plane::through(origin, Direction::new(normal, T).unwrap()),
2869            (-6.0, 6.0),
2870            (-6.0, 6.0),
2871        )
2872        .unwrap()
2873        .into()
2874    }
2875
2876    /// Same-parameter: pcurve lifted through its surface equals the 3D curve,
2877    /// at the same parameter, everywhere sampled.
2878    fn assert_same_parameter(
2879        section: &SectionCurve,
2880        surface: &SurfaceGeometry,
2881        pcurve: &PlanarCurve,
2882        samples: usize,
2883    ) {
2884        let (lo, hi) = section.curve.domain();
2885        let (plo, phi) = pcurve.domain();
2886        assert!(
2887            (lo - plo).abs() < 1e-9 && (hi - phi).abs() < 1e-9,
2888            "domains disagree: [{lo}, {hi}] against [{plo}, {phi}]"
2889        );
2890        for i in 0..=samples {
2891            #[allow(clippy::cast_precision_loss)]
2892            let t = lo + (hi - lo) * i as f64 / samples as f64;
2893            let on_curve = section.curve.point_at(t, T).unwrap();
2894            let at = pcurve.point_at(t, T).unwrap();
2895            let lifted = surface.point_at(at.x, at.y, T).unwrap();
2896            assert!(
2897                on_curve.is_equal(lifted, T),
2898                "at t = {t}: curve {on_curve:?}, lifted {lifted:?}"
2899            );
2900        }
2901    }
2902
2903    /// A straight side from `(√2, 0)` to `(0, √2)` as a clamped cubic
2904    /// B-spline, its control points even along it, extruded 3 along `z`
2905    /// from `z = 0` and over `z` in `[1, 4]`; and the same side as a
2906    /// cubic-by-linear spline patch from `z = 0`.
2907    fn straight_spline_sides() -> [SurfaceGeometry; 3] {
2908        let (from, to) = (
2909            Point::new(2.0_f64.sqrt(), 0.0, 0.0),
2910            Point::new(0.0, 2.0_f64.sqrt(), 0.0),
2911        );
2912        let along = |k: u32| from + (to - from) * (f64::from(k) / 3.0);
2913        let knots = || ogeom_math::KnotVector::clamped_uniform(3, 4).unwrap();
2914        let side = ogeom_geom::BSplineCurve::new(knots(), (0..4).map(along).collect(), T).unwrap();
2915        let lowered =
2916            ogeom_geom::ExtrusionSurface::over(side.clone().into(), Direction::Z, (1.0, 4.0))
2917                .unwrap();
2918        let extruded = ogeom_geom::ExtrusionSurface::new(side.into(), Direction::Z, 3.0).unwrap();
2919        let rows: Vec<Point> = (0..4)
2920            .flat_map(|k| [along(k), along(k) + Vector::new(0.0, 0.0, 3.0)])
2921            .collect();
2922        let patch = ogeom_geom::BSplineSurface::new(
2923            knots(),
2924            ogeom_math::KnotVector::clamped_uniform(1, 2).unwrap(),
2925            &ogeom_math::ControlGrid::new(rows, 4, 2).unwrap(),
2926            T,
2927        )
2928        .unwrap();
2929        [extruded.into(), lowered.into(), patch.into()]
2930    }
2931
2932    /// A plane holding the extrusion direction through either end of a
2933    /// spline side meets the side in the straight ruling at that border,
2934    /// exactly, as a plane through a planar side's edge does; through the
2935    /// side's inside, in the ruling there. A border the surface leaves
2936    /// tangentially is not stated as a crossing.
2937    #[test]
2938    fn a_plane_through_a_spline_patch_s_border_ruling_meets_it_there() {
2939        let r = 2.0_f64.sqrt();
2940        for side in straight_spline_sides() {
2941            for (through, normal, u) in [
2942                (Point::ORIGIN, Vector::Y, 0.0),
2943                (Point::ORIGIN, Vector::X, 1.0),
2944                (Point::new(0.5, 0.0, 0.0), Vector::X, 1.0 - 0.5 / r),
2945            ] {
2946                let cut = plane(through, normal);
2947                for (a, b, cut_first) in [(&cut, &side, true), (&side, &cut, false)] {
2948                    let met = intersect_surfaces(a, b, IntersectOptions::default(), T).unwrap();
2949                    let SurfaceIntersection::Along(sections) = met else {
2950                        panic!("the plane through {through:?} holds a ruling: {met:?}");
2951                    };
2952                    assert_eq!(sections.len(), 1, "{through:?}, {normal:?}");
2953                    let section = &sections[0];
2954                    assert!(section.exact && !section.tangential, "{section:?}");
2955                    let (_, (v0, v1)) = side.domain();
2956                    let want = [
2957                        side.point_at(u, v0, T).unwrap(),
2958                        side.point_at(u, v1, T).unwrap(),
2959                    ];
2960                    let (lo, hi) = section.curve.domain();
2961                    let ends = [
2962                        section.curve.point_at(lo, T).unwrap(),
2963                        section.curve.point_at(hi, T).unwrap(),
2964                    ];
2965                    assert!(
2966                        ends[0].is_equal(want[0], T) && ends[1].is_equal(want[1], T),
2967                        "{through:?}: the ruling at u = {u}, {want:?}, got {ends:?}"
2968                    );
2969                    let (on_side, on_cut) = if cut_first {
2970                        (&section.on_b, &section.on_a)
2971                    } else {
2972                        (&section.on_a, &section.on_b)
2973                    };
2974                    assert_same_parameter(section, &side, on_side.as_ref().unwrap(), 16);
2975                    assert_same_parameter(section, &cut, on_cut.as_ref().unwrap(), 16);
2976                }
2977            }
2978        }
2979
2980        // A side leaving `(0, 0)` along `x` and bending away: the plane
2981        // `y = 0` holds its first ruling, tangent to it there.
2982        let bend = [(0.0, 0.0), (1.0, 0.0), (2.0, 0.5), (2.0, 2.0)]
2983            .map(|(x, y)| Point::new(x, y, 0.0))
2984            .to_vec();
2985        let knots = ogeom_math::KnotVector::clamped_uniform(3, 4).unwrap();
2986        let side = ogeom_geom::BSplineCurve::new(knots, bend, T).unwrap();
2987        let bent: SurfaceGeometry =
2988            ogeom_geom::ExtrusionSurface::new(side.into(), Direction::Z, 3.0)
2989                .unwrap()
2990                .into();
2991        let cut = plane(Point::ORIGIN, Vector::Y);
2992        if let SurfaceIntersection::Along(sections) =
2993            intersect_surfaces(&cut, &bent, IntersectOptions::default(), T).unwrap()
2994        {
2995            assert!(
2996                sections.iter().all(|s| !s.exact || s.tangential),
2997                "a tangent border is no crossing: {sections:?}"
2998            );
2999        }
3000    }
3001
3002    /// A side running from the corner `(√2, 0)` toward `(0, √2)`, stood
3003    /// up three tall, in each spelling no closed form or iso-line path
3004    /// reads: an extrusion of half an ellipse bulging outward, trimmed; of
3005    /// a B-spline line, trimmed; of a B-spline line offset onto the side;
3006    /// a spline patch; an extrusion of a B-spline line. Each starts on the
3007    /// ruling through the corner, a border of its chart, and leaves it
3008    /// along `(1, 1)` or `(-1, 1)`, into `y > 0`.
3009    fn sides_from_the_corner() -> Vec<SurfaceGeometry> {
3010        use ogeom_geom::{BSplineCurve, EllipseCurve, ExtrusionSurface, OffsetCurve, TrimmedCurve};
3011        let r = 2.0_f64.sqrt();
3012        let (from, to) = (Point::new(r, 0.0, 0.0), Point::new(0.0, r, 0.0));
3013        let line = |from: Point, to: Point| -> Curve {
3014            let along = |k: u32| from + (to - from) * (f64::from(k) / 3.0);
3015            let knots = ogeom_math::KnotVector::clamped_uniform(3, 4).unwrap();
3016            BSplineCurve::new(knots, (0..4).map(along).collect(), T)
3017                .unwrap()
3018                .into()
3019        };
3020        let middle = from.midpoint(to);
3021        let frame = Frame::new(
3022            middle,
3023            Direction::Z,
3024            Direction::new(from - to, T).unwrap(),
3025            T,
3026        )
3027        .unwrap();
3028        let half_ellipse = TrimmedCurve::new(
3029            EllipseCurve::new(ogeom_math::Ellipse::new(frame, 1.0, 0.4, T).unwrap()).into(),
3030            0.0,
3031            core::f64::consts::PI,
3032            T,
3033        )
3034        .unwrap();
3035        let run = to - from;
3036        let trimmed_line =
3037            TrimmedCurve::new(line(from - run * 0.5, to + run * 0.5), 0.25, 0.75, T).unwrap();
3038        let inset = Vector::new(1.0, 1.0, 0.0) * (0.25 / r);
3039        let offset = OffsetCurve::new(line(from - inset, to - inset), 0.25, Direction::Z).unwrap();
3040        let extruded = |curve: Curve| -> SurfaceGeometry {
3041            ExtrusionSurface::new(curve, Direction::Z, 3.0)
3042                .unwrap()
3043                .into()
3044        };
3045        let [_, _, patch] = straight_spline_sides();
3046        vec![
3047            extruded(half_ellipse.into()),
3048            extruded(trimmed_line.into()),
3049            extruded(Curve::Offset(Box::new(offset))),
3050            patch,
3051            extruded(line(from, to)),
3052        ]
3053    }
3054
3055    /// Where a border of one surface's chart lies in the other surface
3056    /// and the two leave each other there at an angle, they meet along
3057    /// it, whichever surface comes first: here the ruling `x = √2, y = 0`
3058    /// of each side, met by the plane `y = 0` and by the drum of radius
3059    /// one about `(√2, -1)`, which rests on the ruling from below. Every
3060    /// side lies in `y >= 0`, so the ruling is all they share.
3061    #[test]
3062    fn a_border_lying_in_the_other_surface_is_a_section_along_it() {
3063        let r = 2.0_f64.sqrt();
3064        let drum: SurfaceGeometry = CylinderSurface::new(
3065            Cylinder::new(
3066                Frame::new(Point::new(r, -1.0, -1.0), Direction::Z, Direction::X, T).unwrap(),
3067                1.0,
3068                T,
3069            )
3070            .unwrap(),
3071            (0.0, 5.0),
3072        )
3073        .unwrap()
3074        .into();
3075        let cutters = [
3076            ("the plane y = 0", plane(Point::ORIGIN, Vector::Y)),
3077            ("the drum", drum),
3078        ];
3079        for (k, side) in sides_from_the_corner().iter().enumerate() {
3080            for (name, cutter) in &cutters {
3081                for (a, b) in [(cutter, side), (side, cutter)] {
3082                    let met = intersect_surfaces(a, b, IntersectOptions::default(), T).unwrap();
3083                    let SurfaceIntersection::Along(sections) = met else {
3084                        panic!("side {k}, {name}: the ruling is shared, got {met:?}");
3085                    };
3086                    assert_eq!(sections.len(), 1, "side {k}, {name}: {sections:?}");
3087                    let section = &sections[0];
3088                    assert!(!section.tangential, "side {k}, {name}: {section:?}");
3089                    // Each point of the section within its stated tolerance
3090                    // of the ruling, the ends on its ends.
3091                    let (lo, hi) = section.curve.domain();
3092                    let near = section.tolerance + T.confusion();
3093                    for i in 0..=16 {
3094                        let p = section
3095                            .curve
3096                            .point_at(lo + (hi - lo) * f64::from(i) / 16.0, T)
3097                            .unwrap();
3098                        let off = (p.x - r).hypot(p.y);
3099                        assert!(
3100                            off <= near,
3101                            "side {k}, {name}: {p:?} is {off} off the ruling"
3102                        );
3103                    }
3104                    let ends = [
3105                        section.curve.point_at(lo, T).unwrap(),
3106                        section.curve.point_at(hi, T).unwrap(),
3107                    ];
3108                    let mut heights = [ends[0].z, ends[1].z];
3109                    heights.sort_by(f64::total_cmp);
3110                    assert!(
3111                        heights[0].abs() <= near && (heights[1] - 3.0).abs() <= near,
3112                        "side {k}, {name}: the section runs {heights:?}"
3113                    );
3114                }
3115            }
3116        }
3117
3118        // The drum of radius one about `(√2 - 1/√2, -1/√2)` touches the
3119        // spline patch's plane along the ruling, from outside: the ruling
3120        // lies in it, and the two leave each other tangentially there.
3121        // That is a contact, not a crossing.
3122        let [_, _, patch] = straight_spline_sides();
3123        let resting: SurfaceGeometry = CylinderSurface::new(
3124            Cylinder::new(
3125                Frame::new(
3126                    Point::new(r - 1.0 / r, -1.0 / r, -1.0),
3127                    Direction::Z,
3128                    Direction::X,
3129                    T,
3130                )
3131                .unwrap(),
3132                1.0,
3133                T,
3134            )
3135            .unwrap(),
3136            (0.0, 5.0),
3137        )
3138        .unwrap()
3139        .into();
3140        for (a, b) in [(&resting, &patch), (&patch, &resting)] {
3141            if let SurfaceIntersection::Along(sections) =
3142                intersect_surfaces(a, b, IntersectOptions::default(), T).unwrap()
3143            {
3144                assert!(
3145                    sections.iter().all(|s| s.tangential),
3146                    "a border left tangentially is no crossing: {sections:?}"
3147                );
3148            }
3149        }
3150    }
3151
3152    /// The patch over `0 ≤ x ≤ 1`, `0 ≤ y ≤ 12` standing `z = s y + q y²`
3153    /// off the plane `z = 0`, linear along `x`, quadratic along `y` with
3154    /// the middle control point on the start's heading half way: its border
3155    /// `y = 0` lies in the plane, which it leaves there at the sine
3156    /// `s / √(1 + s²)`.
3157    fn wall_leaving(s: f64, q: f64) -> SurfaceGeometry {
3158        let points: Vec<Point> = [0.0, 1.0]
3159            .into_iter()
3160            .flat_map(|x| {
3161                [(0.0, 0.0), (6.0, 6.0 * s), (12.0, 12.0 * s + 144.0 * q)]
3162                    .map(|(y, z)| Point::new(x, y, z))
3163            })
3164            .collect();
3165        ogeom_geom::BSplineSurface::new(
3166            ogeom_math::KnotVector::clamped_uniform(1, 2).unwrap(),
3167            ogeom_math::KnotVector::clamped_uniform(2, 3).unwrap(),
3168            &ogeom_math::ControlGrid::new(points, 2, 3).unwrap(),
3169            T,
3170        )
3171        .unwrap()
3172        .into()
3173    }
3174
3175    /// A window of the plane `z = 0` reaching a unit past the wall's border
3176    /// `y = 0` and no further than `y = 1`, short of the wall's next row
3177    /// of samples at `y = 2`, meets the wall along that border however
3178    /// shallowly the wall leaves it: the samples inside the window all lie
3179    /// on the border, which says only that the border lies in the plane,
3180    /// and the wall's rise `s d` a distance `d` in is resolved within a
3181    /// fraction of a millimetre. A wall leaving the plane tangentially there
3182    /// is a contact, never a crossing.
3183    #[test]
3184    fn a_wall_leaving_a_plane_window_shallowly_meets_it_along_its_border() {
3185        let window: SurfaceGeometry =
3186            PlaneSurface::over(Plane::new(Frame::WORLD), (-1.0, 2.0), (-1.0, 1.0))
3187                .unwrap()
3188                .into();
3189        for s in [0.005, 0.012, 0.023] {
3190            let wall = wall_leaving(s, 0.002);
3191            for (a, b) in [(&window, &wall), (&wall, &window)] {
3192                let met = intersect_surfaces(a, b, IntersectOptions::default(), T).unwrap();
3193                let SurfaceIntersection::Along(sections) = met else {
3194                    panic!("sine {s}: the border is shared, got {met:?}");
3195                };
3196                assert_eq!(sections.len(), 1, "sine {s}: {sections:?}");
3197                let section = &sections[0];
3198                assert!(!section.tangential, "sine {s}: {section:?}");
3199                let (lo, hi) = section.curve.domain();
3200                let near = section.tolerance + T.confusion();
3201                for i in 0..=16 {
3202                    let p = section
3203                        .curve
3204                        .point_at(lo + (hi - lo) * f64::from(i) / 16.0, T)
3205                        .unwrap();
3206                    assert!(
3207                        p.y.hypot(p.z) <= near && (-near..=1.0 + near).contains(&p.x),
3208                        "sine {s}: {p:?} is off the border"
3209                    );
3210                }
3211                let mut xs = [lo, hi].map(|t| section.curve.point_at(t, T).unwrap().x);
3212                xs.sort_by(f64::total_cmp);
3213                assert!(
3214                    xs[0].abs() <= near && (xs[1] - 1.0).abs() <= near,
3215                    "sine {s}: the section runs {xs:?}"
3216                );
3217            }
3218        }
3219        let resting = wall_leaving(0.0, 0.002);
3220        for (a, b) in [(&window, &resting), (&resting, &window)] {
3221            if let SurfaceIntersection::Along(sections) =
3222                intersect_surfaces(a, b, IntersectOptions::default(), T).unwrap()
3223            {
3224                assert!(
3225                    sections.iter().all(|s| s.tangential),
3226                    "a border left tangentially is no crossing: {sections:?}"
3227                );
3228            }
3229        }
3230    }
3231
3232    #[test]
3233    fn an_analytic_pair_comes_back_exact_with_matching_pcurves() {
3234        // A plane through a cylinder's axis: two lines, and every description
3235        // agrees at the same parameter, which is the claim edges carry and
3236        // booleans rely on.
3237        let drum = cylinder(Vector::Z, 2.0);
3238        let cut = plane(Point::ORIGIN, Vector::X);
3239        let SurfaceIntersection::Along(curves) =
3240            intersect_surfaces(&drum, &cut, IntersectOptions::default(), T).unwrap()
3241        else {
3242            panic!("a plane through a cylinder meets it along curves");
3243        };
3244        assert_eq!(curves.len(), 2);
3245        for section in &curves {
3246            assert!(section.exact);
3247            assert!((section.tolerance - 0.0).abs() < f64::EPSILON);
3248            let on_a = section.on_a.as_ref().expect("a line has a cylinder pcurve");
3249            let on_b = section.on_b.as_ref().expect("and a plane pcurve");
3250            assert_same_parameter(section, &drum, on_a, 50);
3251            assert_same_parameter(section, &cut, on_b, 50);
3252        }
3253    }
3254
3255    #[test]
3256    fn an_oblique_cut_gives_the_ellipse_a_trig_pcurve_on_the_drum() {
3257        // The oblique ellipse's pcurve runs linearly in the chart angle and
3258        // sinusoidally in height (the trig-affine family), exactly,
3259        // same-parameter, both sides.
3260        let drum = cylinder(Vector::Z, 2.0);
3261        let angle: f64 = 0.5;
3262        let cut = plane(Point::ORIGIN, Vector::new(0.0, angle.sin(), angle.cos()));
3263        let SurfaceIntersection::Along(curves) =
3264            intersect_surfaces(&drum, &cut, IntersectOptions::default(), T).unwrap()
3265        else {
3266            panic!("an oblique plane meets the cylinder along its ellipse");
3267        };
3268        assert_eq!(curves.len(), 1);
3269        let section = &curves[0];
3270        assert!(section.exact);
3271        assert!(matches!(section.curve, Curve::Ellipse(_)));
3272        let on_drum = section
3273            .on_a
3274            .as_ref()
3275            .expect("the oblique ellipse now carries its cylinder pcurve");
3276        assert!(
3277            matches!(on_drum, PlanarCurve::Trig(_)),
3278            "the chart trace is trig-affine: {on_drum:?}"
3279        );
3280        assert_same_parameter(section, &drum, on_drum, 60);
3281        let on_plane = section.on_b.as_ref().expect("and its plane pcurve");
3282        assert_same_parameter(section, &cut, on_plane, 60);
3283    }
3284
3285    #[test]
3286    fn a_perpendicular_cut_gives_a_circle_with_a_straight_pcurve() {
3287        let drum = cylinder(Vector::Z, 2.0);
3288        let cut = plane(Point::new(0.0, 0.0, 1.0), Vector::Z);
3289        let SurfaceIntersection::Along(curves) =
3290            intersect_surfaces(&drum, &cut, IntersectOptions::default(), T).unwrap()
3291        else {
3292            panic!("expected curves");
3293        };
3294        assert_eq!(curves.len(), 1);
3295        let section = &curves[0];
3296        assert!(section.closed);
3297        assert!(matches!(section.curve, Curve::Circle(_)));
3298        // On the cylinder the circle is a horizontal line in (u, v).
3299        assert!(matches!(
3300            section.on_a.as_ref().unwrap(),
3301            PlanarCurve::Line(_)
3302        ));
3303        assert_same_parameter(section, &drum, section.on_a.as_ref().unwrap(), 60);
3304        assert_same_parameter(section, &cut, section.on_b.as_ref().unwrap(), 60);
3305    }
3306
3307    #[test]
3308    fn coaxial_cylinder_and_sphere_give_circles_with_pcurves_on_both() {
3309        let drum = cylinder(Vector::Z, 1.5);
3310        let ball = sphere(Point::ORIGIN, 3.0);
3311        let SurfaceIntersection::Along(curves) =
3312            intersect_surfaces(&drum, &ball, IntersectOptions::default(), T).unwrap()
3313        else {
3314            panic!("expected curves");
3315        };
3316        assert_eq!(curves.len(), 2);
3317        for section in &curves {
3318            assert!(section.exact);
3319            assert_same_parameter(section, &drum, section.on_a.as_ref().unwrap(), 40);
3320            assert_same_parameter(section, &ball, section.on_b.as_ref().unwrap(), 40);
3321        }
3322    }
3323
3324    fn torus(origin: Point, axis: Vector, major: f64, minor: f64) -> SurfaceGeometry {
3325        let frame = Frame::new(
3326            origin,
3327            Direction::new(axis, T).unwrap(),
3328            Direction::from_cross(axis, Vector::new(0.3, 0.5, 0.9), T).unwrap(),
3329            T,
3330        )
3331        .unwrap();
3332        ogeom_geom::TorusSurface::new(ogeom_math::Torus::new(frame, major, minor, T).unwrap())
3333            .into()
3334    }
3335
3336    #[test]
3337    fn an_axis_normal_plane_meets_a_torus_in_two_parallels_with_pcurves() {
3338        let ring = torus(Point::ORIGIN, Vector::Z, 2.0, 0.5);
3339        let cut = plane(Point::new(0.0, 0.0, 0.3), Vector::Z);
3340        let SurfaceIntersection::Along(curves) =
3341            intersect_surfaces(&ring, &cut, IntersectOptions::default(), T).unwrap()
3342        else {
3343            panic!("an axis-normal plane through the tube meets it along curves");
3344        };
3345        assert_eq!(curves.len(), 2);
3346        let spread = 0.5_f64.mul_add(0.5, -(0.3 * 0.3)).sqrt();
3347        let mut radii: Vec<f64> = curves
3348            .iter()
3349            .map(|s| {
3350                let Curve::Circle(c) = &s.curve else {
3351                    panic!("a parallel is a circle");
3352                };
3353                c.circle().radius()
3354            })
3355            .collect();
3356        radii.sort_by(|a, b| a.partial_cmp(b).unwrap());
3357        assert!((radii[0] - (2.0 - spread)).abs() < 1e-12);
3358        assert!((radii[1] - (2.0 + spread)).abs() < 1e-12);
3359        for section in &curves {
3360            assert!(section.exact);
3361            assert_same_parameter(section, &ring, section.on_a.as_ref().unwrap(), 48);
3362            assert_same_parameter(section, &cut, section.on_b.as_ref().unwrap(), 48);
3363        }
3364    }
3365
3366    #[test]
3367    fn the_plane_a_ball_rolls_on_touches_its_torus_along_the_circle_it_rolled() {
3368        // Tangency with length is reported as the curve it is (the way a
3369        // tangent plane reports its line on a cylinder), because the blend
3370        // machinery builds faces whose boundaries are exactly these circles,
3371        // and a Touching with no curve in it would read as a refusal upstream.
3372        let ring = torus(Point::ORIGIN, Vector::Z, 2.0, 0.5);
3373        let cut = plane(Point::new(0.0, 0.0, 0.5), Vector::Z);
3374        let SurfaceIntersection::Along(curves) =
3375            intersect_surfaces(&ring, &cut, IntersectOptions::default(), T).unwrap()
3376        else {
3377            panic!("the rolling plane touches along a circle, not at points");
3378        };
3379        assert_eq!(curves.len(), 1);
3380        let Curve::Circle(c) = &curves[0].curve else {
3381            panic!("the tangency is a circle");
3382        };
3383        assert!((c.circle().radius() - 2.0).abs() < 1e-12);
3384        assert_same_parameter(&curves[0], &ring, curves[0].on_a.as_ref().unwrap(), 48);
3385        assert_same_parameter(&curves[0], &cut, curves[0].on_b.as_ref().unwrap(), 48);
3386    }
3387
3388    #[test]
3389    fn a_coaxial_cylinder_meets_a_torus_in_two_parallels_and_touches_in_one() {
3390        let ring = torus(Point::ORIGIN, Vector::Z, 2.0, 0.5);
3391        let drum = cylinder(Vector::Z, 2.2);
3392        let SurfaceIntersection::Along(curves) =
3393            intersect_surfaces(&drum, &ring, IntersectOptions::default(), T).unwrap()
3394        else {
3395            panic!("a coaxial cylinder through the tube meets it along curves");
3396        };
3397        assert_eq!(curves.len(), 2);
3398        for section in &curves {
3399            assert!(section.exact);
3400            let Curve::Circle(c) = &section.curve else {
3401                panic!("a parallel is a circle");
3402            };
3403            assert!((c.circle().radius() - 2.2).abs() < 1e-12);
3404            assert_same_parameter(section, &drum, section.on_a.as_ref().unwrap(), 48);
3405            assert_same_parameter(section, &ring, section.on_b.as_ref().unwrap(), 48);
3406        }
3407
3408        // Tangent at the tube's outer equator: one circle, with both pcurves.
3409        let grazing = cylinder(Vector::Z, 2.5);
3410        let SurfaceIntersection::Along(touch) =
3411            intersect_surfaces(&grazing, &ring, IntersectOptions::default(), T).unwrap()
3412        else {
3413            panic!("the grazing cylinder touches along the equator");
3414        };
3415        assert_eq!(touch.len(), 1);
3416        assert_same_parameter(&touch[0], &grazing, touch[0].on_a.as_ref().unwrap(), 48);
3417        assert_same_parameter(&touch[0], &ring, touch[0].on_b.as_ref().unwrap(), 48);
3418    }
3419
3420    #[test]
3421    fn coaxial_tori_are_the_same_or_meet_in_parallels() {
3422        let ring = torus(Point::ORIGIN, Vector::Z, 2.0, 0.5);
3423        assert!(matches!(
3424            intersect_surfaces(&ring, &ring.clone(), IntersectOptions::default(), T).unwrap(),
3425            SurfaceIntersection::Same
3426        ));
3427
3428        // The same tube lifted half a radius: the profile circles cross
3429        // twice, and each crossing revolves into a parallel shared exactly.
3430        let lifted = torus(Point::new(0.0, 0.0, 0.5), Vector::Z, 2.0, 0.5);
3431        let SurfaceIntersection::Along(curves) =
3432            intersect_surfaces(&ring, &lifted, IntersectOptions::default(), T).unwrap()
3433        else {
3434            panic!("lifted coaxial tori meet along curves");
3435        };
3436        assert_eq!(curves.len(), 2);
3437        for section in &curves {
3438            assert!(section.exact);
3439            assert_same_parameter(section, &ring, section.on_a.as_ref().unwrap(), 48);
3440            assert_same_parameter(section, &lifted, section.on_b.as_ref().unwrap(), 48);
3441        }
3442    }
3443
3444    #[test]
3445    fn a_pair_with_no_closed_form_comes_back_fitted_with_pcurves() {
3446        // Crossed cylinders: the marched path, end to end through one call.
3447        let a = cylinder(Vector::Z, 1.0);
3448        let b = cylinder(Vector::X, 1.6);
3449        let options = IntersectOptions {
3450            tolerance: 1e-5,
3451            marching: Marching {
3452                chord: 1e-5,
3453                ..Marching::default()
3454            },
3455        };
3456        let SurfaceIntersection::Along(curves) = intersect_surfaces(&a, &b, options, T).unwrap()
3457        else {
3458            panic!("crossed cylinders meet along curves");
3459        };
3460        assert_eq!(curves.len(), 2);
3461        for section in &curves {
3462            assert!(!section.exact);
3463            assert!(section.closed);
3464            assert!(
3465                section.tolerance <= 1e-5 + 1e-4,
3466                "got {}",
3467                section.tolerance
3468            );
3469            assert!(section.on_a.is_some() && section.on_b.is_some());
3470
3471            // The fitted curve lies on both cylinders to its stated tolerance.
3472            let (lo, hi) = section.curve.domain();
3473            for i in 0..=200 {
3474                #[allow(clippy::cast_precision_loss)]
3475                let t = lo + (hi - lo) * f64::from(i) / 200.0;
3476                let p = section.curve.point_at(t, T).unwrap();
3477                let (SurfaceGeometry::Cylinder(x), SurfaceGeometry::Cylinder(y)) = (&a, &b) else {
3478                    unreachable!()
3479                };
3480                let off = x
3481                    .cylinder()
3482                    .distance_to(p)
3483                    .abs()
3484                    .max(y.cylinder().distance_to(p).abs());
3485                assert!(
3486                    off <= section.tolerance * 2.0,
3487                    "at t = {t} the fitted curve is {off:e} off, tolerance {}",
3488                    section.tolerance
3489                );
3490            }
3491        }
3492    }
3493
3494    /// A plane all but parallel to a drum's axis meets it in an ellipse ten
3495    /// metres long, which crosses the drum's few units of height only in a
3496    /// sliver of its turn. It is still a section of the two.
3497    #[test]
3498    fn a_plane_all_but_along_the_axis_still_meets_a_short_drum() {
3499        let drum = cylinder(Vector::Z, 1.0);
3500        let wall: SurfaceGeometry = PlaneSurface::over(
3501            Plane::through(
3502                Point::new(0.0, 0.6, 0.0),
3503                Direction::new(Vector::new(0.0, 1.0, 1e-4), T).unwrap(),
3504            ),
3505            (-1e9, 1e9),
3506            (-1e9, 1e9),
3507        )
3508        .unwrap()
3509        .into();
3510        let met = intersect_surfaces(&wall, &drum, IntersectOptions::default(), T).unwrap();
3511        let SurfaceIntersection::Along(sections) = met else {
3512            panic!("the wall crosses the drum: {met:?}");
3513        };
3514        assert_eq!(sections.len(), 1);
3515        let curve = &sections[0].curve;
3516        let (lo, hi) = curve.domain();
3517        let inside = (0..=100_000).any(|k| {
3518            let p = curve
3519                .point_at(lo + (hi - lo) * f64::from(k) / 100_000.0, T)
3520                .unwrap();
3521            p.z.abs() <= 4.0
3522        });
3523        assert!(inside, "and the section runs through the drum's height");
3524    }
3525
3526    /// Every point of a section within its stated tolerance of both
3527    /// surfaces, sampled along it.
3528    fn on_both(section: &SectionCurve, a: &SurfaceGeometry, b: &SurfaceGeometry) {
3529        let (lo, hi) = section.curve.domain();
3530        for k in 0..=64 {
3531            let p = section
3532                .curve
3533                .point_at(lo + (hi - lo) * f64::from(k) / 64.0, T)
3534                .unwrap();
3535            for surface in [a, b] {
3536                let off = match surface {
3537                    SurfaceGeometry::Plane(plane) => plane.plane().signed_distance_to(p).abs(),
3538                    SurfaceGeometry::Cylinder(drum) => {
3539                        let axis = drum.cylinder().axis();
3540                        let rel = p - axis.location;
3541                        let d = axis.direction.vector();
3542                        ((rel - d * rel.dot(d)).magnitude() - drum.cylinder().radius()).abs()
3543                    }
3544                    _ => unreachable!("planes and drums only"),
3545                };
3546                assert!(
3547                    off <= section.tolerance + 1e-9,
3548                    "{p:?} is {off:e} off, stated {:e}",
3549                    section.tolerance
3550                );
3551            }
3552        }
3553    }
3554
3555    /// A plane leaning two hundred-thousandths off a drum's axis, grazing
3556    /// it: the closed form's ellipse is fifty metres long, its parameter
3557    /// too coarse for the drum's eight units of height. The two sections
3558    /// come back as curves along that height, within their stated
3559    /// tolerance of both surfaces.
3560    #[test]
3561    fn a_plane_all_but_along_a_drums_axis_meets_it_in_two_near_lines() {
3562        let drum = cylinder(Vector::Z, 1.0);
3563        let wall: SurfaceGeometry = PlaneSurface::over(
3564            Plane::through(
3565                Point::new(0.0, 0.99, 0.0),
3566                Direction::new(Vector::new(0.0, 1.0, 2e-5), T).unwrap(),
3567            ),
3568            (-1e9, 1e9),
3569            (-1e9, 1e9),
3570        )
3571        .unwrap()
3572        .into();
3573        let met = intersect_surfaces(&wall, &drum, IntersectOptions::default(), T).unwrap();
3574        let SurfaceIntersection::Along(sections) = met else {
3575            panic!("the wall crosses the drum: {met:?}");
3576        };
3577        assert_eq!(sections.len(), 2);
3578        for section in &sections {
3579            assert!(section.tolerance > 0.0 && section.tolerance <= 1e-5);
3580            on_both(section, &wall, &drum);
3581        }
3582    }
3583
3584    /// Two drums whose axes lean five hundred-thousandths apart meet in two
3585    /// curves all but straight, returned as such over the height they share
3586    /// rather than marched.
3587    #[test]
3588    fn drums_all_but_parallel_meet_in_two_near_lines() {
3589        let drill = cylinder(Vector::Z, 1.0);
3590        let frame = Frame::new(
3591            Point::new(1.5, 0.0, 0.0),
3592            Direction::new(Vector::new(5e-5, 0.0, 1.0), T).unwrap(),
3593            Direction::X,
3594            T,
3595        )
3596        .unwrap();
3597        let bore: SurfaceGeometry =
3598            CylinderSurface::new(Cylinder::new(frame, 1.0, T).unwrap(), (-3.0, 3.0))
3599                .unwrap()
3600                .into();
3601        let met = intersect_surfaces(&drill, &bore, IntersectOptions::default(), T).unwrap();
3602        let SurfaceIntersection::Along(sections) = met else {
3603            panic!("the drums cross: {met:?}");
3604        };
3605        assert_eq!(sections.len(), 2);
3606        for section in &sections {
3607            assert!(!section.exact && section.tolerance <= 1e-5);
3608            let (lo, hi) = section.curve.domain();
3609            let (p, q) = (
3610                section.curve.point_at(lo, T).unwrap(),
3611                section.curve.point_at(hi, T).unwrap(),
3612            );
3613            assert!(
3614                (p.z - q.z).abs() > 5.9,
3615                "over the shared height: {p:?} {q:?}"
3616            );
3617            on_both(section, &drill, &bore);
3618        }
3619    }
3620
3621    #[test]
3622    fn exact_lines_are_clipped_to_the_surfaces_extents() {
3623        // The analytic layer answers for the unbounded geometry; the surfaces
3624        // are finite. A section line a billion units long is not something an
3625        // edge can be built on, and one wholly outside the extents is a
3626        // phantom.
3627        let drum = cylinder(Vector::Z, 2.0);
3628        let cut = plane(Point::ORIGIN, Vector::X);
3629        let SurfaceIntersection::Along(curves) =
3630            intersect_surfaces(&drum, &cut, IntersectOptions::default(), T).unwrap()
3631        else {
3632            panic!("expected curves");
3633        };
3634        for section in &curves {
3635            let (lo, hi) = section.curve.domain();
3636            // Bounded by the cylinder's height, not by LINE_EXTENT.
3637            assert!(
3638                hi - lo <= 8.0 + 1e-9,
3639                "the line was not clipped: [{lo}, {hi}]"
3640            );
3641            let start = section.curve.point_at(lo, T).unwrap();
3642            let end = section.curve.point_at(hi, T).unwrap();
3643            assert!(start.z >= -4.0 - 1e-9 && end.z <= 4.0 + 1e-9);
3644        }
3645
3646        // A circle at a height the bounded cylinder does not reach is not an
3647        // intersection of these surfaces, however truly the unbounded ones
3648        // meet there.
3649        let high = plane(Point::new(0.0, 0.0, 10.0), Vector::Z);
3650        assert_eq!(
3651            intersect_surfaces(&drum, &high, IntersectOptions::default(), T).unwrap(),
3652            SurfaceIntersection::Apart
3653        );
3654    }
3655
3656    #[test]
3657    fn the_degenerate_answers_pass_through() {
3658        assert_eq!(
3659            intersect_surfaces(
3660                &sphere(Point::ORIGIN, 1.0),
3661                &sphere(Point::new(5.0, 0.0, 0.0), 1.0),
3662                IntersectOptions::default(),
3663                T
3664            )
3665            .unwrap(),
3666            SurfaceIntersection::Apart
3667        );
3668        assert_eq!(
3669            intersect_surfaces(
3670                &sphere(Point::ORIGIN, 1.0),
3671                &sphere(Point::ORIGIN, 1.0),
3672                IntersectOptions::default(),
3673                T
3674            )
3675            .unwrap(),
3676            SurfaceIntersection::Same
3677        );
3678        assert!(matches!(
3679            intersect_surfaces(
3680                &plane(Point::ORIGIN, Vector::Z),
3681                &sphere(Point::new(0.0, 0.0, 2.0), 2.0),
3682                IntersectOptions::default(),
3683                T
3684            )
3685            .unwrap(),
3686            SurfaceIntersection::Touching(ref p) if p.len() == 1
3687        ));
3688    }
3689
3690    #[test]
3691    fn unusable_options_are_refused() {
3692        let a = sphere(Point::ORIGIN, 1.0);
3693        let b = plane(Point::ORIGIN, Vector::Z);
3694        for tolerance in [0.0, -1.0, f64::NAN] {
3695            let options = IntersectOptions {
3696                tolerance,
3697                ..IntersectOptions::default()
3698            };
3699            assert!(intersect_surfaces(&a, &b, options, T).is_err());
3700        }
3701    }
3702
3703    #[test]
3704    fn a_circle_wound_against_the_axis_keeps_its_pcurve_same_parameter() {
3705        // A plane whose normal opposes the cylinder's axis cuts a circle
3706        // wound against the cylinder's `u`, and the pcurve must run in `-u`
3707        // with it. Written `+u` unconditionally, the pcurve evaluates half a
3708        // turn away from the curve and every face built on the section tears
3709        // in parameter space. Both windings are pinned by lifting the pcurve
3710        // through the surface and demanding the curve's own point back.
3711        let drum: SurfaceGeometry = CylinderSurface::new(
3712            Cylinder::new(
3713                Frame::new(Point::new(2.0, 2.0, -1.0), Direction::Z, Direction::X, T).unwrap(),
3714                0.5,
3715                T,
3716            )
3717            .unwrap(),
3718            (0.0, 3.0),
3719        )
3720        .unwrap()
3721        .into();
3722        for normal in [Direction::Z, -Direction::Z] {
3723            let frame = Frame::new(Point::ORIGIN, normal, Direction::X, T).unwrap();
3724            let ground: SurfaceGeometry =
3725                PlaneSurface::over(Plane::new(frame), (-4.0, 4.0), (-4.0, 4.0))
3726                    .unwrap()
3727                    .into();
3728            let met = intersect_surfaces(&ground, &drum, IntersectOptions::default(), T).unwrap();
3729            let SurfaceIntersection::Along(curves) = met else {
3730                panic!("a plane through a cylinder sections it");
3731            };
3732            for sc in &curves {
3733                let pcurve = sc
3734                    .on_b
3735                    .as_ref()
3736                    .expect("a circle on its cylinder has a pcurve");
3737                let (lo, hi) = sc.curve.domain();
3738                for i in 0..8 {
3739                    let t = lo + (hi - lo) * f64::from(i) / 8.0;
3740                    let p3 = sc.curve.point_at(t, T).unwrap();
3741                    let uv = pcurve.point_at(t, T).unwrap();
3742                    let lifted = drum
3743                        .point_at(uv.x.rem_euclid(core::f64::consts::TAU), uv.y, T)
3744                        .unwrap();
3745                    assert!(
3746                        p3.distance(lifted) < 1e-9,
3747                        "normal {normal:?}, t {t}: pcurve lifts {lifted:?} against {p3:?}"
3748                    );
3749                }
3750            }
3751        }
3752    }
3753
3754    #[test]
3755    fn a_circle_on_a_left_handed_frame_keeps_its_pcurve_same_parameter() {
3756        // A mirrored circle stands on a left-handed frame: its parameter
3757        // runs from `x` towards a `y` that turns clockwise about `z`. Read
3758        // by `z` alone, its pcurve runs the other way round the axis from
3759        // the curve, and lifts to the circle's mirror image across its `x`.
3760        // Each circle is taken on both frames through its `x`, and its
3761        // pcurve lifted through the surface must give the curve's own point
3762        // back at every parameter.
3763        use ogeom_geom::ConeSurface;
3764        let cone: SurfaceGeometry = ConeSurface::new(
3765            ogeom_math::Cone::new(Frame::WORLD, 1.0, core::f64::consts::FRAC_PI_6, T).unwrap(),
3766            (-1.0, 3.0),
3767        )
3768        .unwrap()
3769        .into();
3770        let on = |centre: Point, normal: Direction, radius: f64| {
3771            (Frame::new(centre, normal, Direction::X, T).unwrap(), radius)
3772        };
3773        let cases = [
3774            (
3775                cylinder(Vector::Z, 2.0),
3776                on(Point::new(0.0, 0.0, 1.0), Direction::Z, 2.0),
3777            ),
3778            (
3779                sphere(Point::ORIGIN, 2.0),
3780                on(Point::new(0.0, 0.0, 1.0), Direction::Z, 3.0_f64.sqrt()),
3781            ),
3782            (
3783                cone.clone(),
3784                on(
3785                    Point::new(0.0, 0.0, 1.0),
3786                    Direction::Z,
3787                    1.0 + core::f64::consts::FRAC_PI_6.tan(),
3788                ),
3789            ),
3790            (
3791                torus(Point::ORIGIN, Vector::Z, 2.0, 0.5),
3792                on(Point::ORIGIN, Direction::Z, 2.5),
3793            ),
3794            (
3795                torus(Point::ORIGIN, Vector::Z, 2.0, 0.5),
3796                on(Point::new(2.0, 0.0, 0.0), Direction::Y, 0.5),
3797            ),
3798        ];
3799        for (surface, (frame, radius)) in &cases {
3800            for frame in [*frame, frame.mirrored()] {
3801                let circle: Curve = ogeom_geom::CircleCurve::new(
3802                    ogeom_math::Circle::new(frame, *radius, T).unwrap(),
3803                )
3804                .into();
3805                let pcurve = exact_pcurve_of(&circle, surface, T)
3806                    .expect("a circle of the surface has a closed-form pcurve");
3807                for i in 0..=8 {
3808                    let t = core::f64::consts::TAU * f64::from(i) / 8.0;
3809                    let uv = pcurve.point_at(t, T).unwrap();
3810                    let lifted = surface
3811                        .point_at(uv.x.rem_euclid(core::f64::consts::TAU), uv.y, T)
3812                        .or_else(|_| surface.point_at(uv.x, uv.y, T))
3813                        .unwrap();
3814                    let p3 = circle.point_at(t, T).unwrap();
3815                    // Both sides are closed forms of one point: rounding
3816                    // only.
3817                    assert!(
3818                        p3.distance(lifted) < 1e-9,
3819                        "{:?} frame, t {t}: pcurve lifts {lifted:?} against {p3:?}",
3820                        frame.handedness()
3821                    );
3822                }
3823            }
3824        }
3825    }
3826
3827    /// A surface on a left-handed (mirrored) frame turns its angle `u`
3828    /// about `-z`: a circle round its axis that turns about `+z` runs down
3829    /// `u`, not up it. Each surface is taken on both frames through its own
3830    /// `x`, each circle on it on both of its own, and the circle's pcurve
3831    /// lifted through the surface must give the curve's own point back at
3832    /// every parameter.
3833    #[test]
3834    fn a_circle_on_a_left_handed_surface_keeps_its_pcurve_same_parameter() {
3835        use ogeom_geom::{ConeSurface, TorusSurface};
3836        let on = |centre: Point, normal: Direction, radius: f64| {
3837            (Frame::new(centre, normal, Direction::X, T).unwrap(), radius)
3838        };
3839        let cylinder = |frame: Frame| -> SurfaceGeometry {
3840            CylinderSurface::new(Cylinder::new(frame, 2.0, T).unwrap(), (-4.0, 4.0))
3841                .unwrap()
3842                .into()
3843        };
3844        let cone = |frame: Frame| -> SurfaceGeometry {
3845            ConeSurface::new(
3846                ogeom_math::Cone::new(frame, 1.0, core::f64::consts::FRAC_PI_6, T).unwrap(),
3847                (-1.0, 3.0),
3848            )
3849            .unwrap()
3850            .into()
3851        };
3852        let sphere = |frame: Frame| -> SurfaceGeometry {
3853            SphereSurface::new(Sphere::new(frame, 2.0, T).unwrap()).into()
3854        };
3855        let torus = |frame: Frame| -> SurfaceGeometry {
3856            TorusSurface::new(ogeom_math::Torus::new(frame, 2.0, 0.5, T).unwrap()).into()
3857        };
3858        type Make<'a> = &'a dyn Fn(Frame) -> SurfaceGeometry;
3859        let surfaces: [(Make, Vec<(Frame, f64)>); 4] = [
3860            (
3861                &cylinder,
3862                vec![on(Point::new(0.0, 0.0, 1.0), Direction::Z, 2.0)],
3863            ),
3864            (
3865                &cone,
3866                vec![on(
3867                    Point::new(0.0, 0.0, 1.0),
3868                    Direction::Z,
3869                    1.0 + core::f64::consts::FRAC_PI_6.tan(),
3870                )],
3871            ),
3872            (
3873                &sphere,
3874                vec![
3875                    on(Point::new(0.0, 0.0, 1.0), Direction::Z, 3.0_f64.sqrt()),
3876                    on(Point::ORIGIN, Direction::Y, 2.0),
3877                ],
3878            ),
3879            (
3880                &torus,
3881                vec![
3882                    on(Point::ORIGIN, Direction::Z, 2.5),
3883                    on(Point::new(0.0, 0.0, 0.5), Direction::Z, 2.0),
3884                    on(Point::new(2.0, 0.0, 0.0), Direction::Y, 0.5),
3885                ],
3886            ),
3887        ];
3888        let tau = core::f64::consts::TAU;
3889        for (make, circles) in &surfaces {
3890            for surface_frame in [Frame::WORLD, Frame::WORLD.mirrored()] {
3891                let surface = make(surface_frame);
3892                for (frame, radius) in circles {
3893                    for frame in [*frame, frame.mirrored()] {
3894                        let circle: Curve = ogeom_geom::CircleCurve::new(
3895                            ogeom_math::Circle::new(frame, *radius, T).unwrap(),
3896                        )
3897                        .into();
3898                        // A meridian's half is imaged over its own range.
3899                        let range = if frame.z().vector().dot(Vector::Z).abs() < 0.5 {
3900                            let half = core::f64::consts::FRAC_PI_2 - 0.1;
3901                            (-half, half)
3902                        } else {
3903                            (0.0, tau)
3904                        };
3905                        let pcurve = exact_pcurve_over(&circle, range, &surface, T)
3906                            .expect("a circle of the surface has a closed-form pcurve");
3907                        for i in 0..=8 {
3908                            let t = (range.1 - range.0).mul_add(f64::from(i) / 8.0, range.0);
3909                            let uv = pcurve.point_at(t, T).unwrap();
3910                            let lifted = surface
3911                                .point_at(uv.x.rem_euclid(tau), uv.y, T)
3912                                .or_else(|_| {
3913                                    surface.point_at(uv.x.rem_euclid(tau), uv.y.rem_euclid(tau), T)
3914                                })
3915                                .unwrap();
3916                            let p3 = circle.point_at(t, T).unwrap();
3917                            // Both sides are closed forms of one point:
3918                            // rounding only.
3919                            assert!(
3920                                p3.distance(lifted) < 1e-9,
3921                                "{:?} surface, {:?} circle {frame:?}, t {t}: pcurve lifts \
3922                                 {lifted:?} against {p3:?}",
3923                                surface_frame.handedness(),
3924                                frame.handedness()
3925                            );
3926                        }
3927                    }
3928                }
3929            }
3930        }
3931    }
3932
3933    /// A plane through a ball's own axis cuts a meridian. The whole circle has
3934    /// no chart image (its longitude jumps half a turn at each pole), but
3935    /// each half is a straight line in the chart, exactly, at the circle's own
3936    /// parameter. Pinned by lifting the line back through the sphere and
3937    /// demanding the circle's point, on every half of every orientation.
3938    #[test]
3939    fn a_meridian_half_has_an_exact_line_for_a_pcurve() {
3940        use ogeom_geom::Surface as _;
3941        let half = core::f64::consts::PI;
3942        for (centre, radius) in [(Point::ORIGIN, 4.0), (Point::new(1.0, -2.0, 0.5), 1.25)] {
3943            let ball = sphere(centre, radius);
3944            let SurfaceGeometry::Sphere(s) = &ball else {
3945                panic!("a sphere surface");
3946            };
3947            // Three planes through the axis, at different azimuths, so the
3948            // constant longitude is not accidentally zero.
3949            for azimuth in [0.0_f64, 0.7, 2.4] {
3950                let normal = Vector::new(-azimuth.sin(), azimuth.cos(), 0.0);
3951                let cut = plane(centre, normal);
3952                let SurfaceIntersection::Along(curves) =
3953                    intersect_surfaces(&ball, &cut, IntersectOptions::default(), T).unwrap()
3954                else {
3955                    panic!("a plane through the centre meets the ball along a circle");
3956                };
3957                assert_eq!(curves.len(), 1, "one great circle");
3958                let circle = &curves[0].curve;
3959                assert!(curves[0].exact);
3960                // The whole circle has no chart image; each half does.
3961                assert!(
3962                    exact_pcurve_over(circle, circle.domain(), &ball, T).is_none(),
3963                    "the whole meridian has no single chart image"
3964                );
3965                for (lo, hi) in [(0.0, half), (half, 2.0 * half), (0.3, half - 0.1)] {
3966                    let pcurve = exact_pcurve_over(circle, (lo, hi), &ball, T)
3967                        .expect("half a meridian has an exact pcurve");
3968                    assert!(
3969                        matches!(pcurve, PlanarCurve::Line(_)),
3970                        "and it is a straight line in the chart"
3971                    );
3972                    for i in 0..=16 {
3973                        let t = (hi - lo).mul_add(f64::from(i) / 16.0, lo);
3974                        let want = circle.point_at(t, T).unwrap();
3975                        let uv = pcurve.point_at(t, T).unwrap();
3976                        assert!(
3977                            uv.y >= -half.mul_add(0.5, 1e-12) && uv.y <= half.mul_add(0.5, 1e-12),
3978                            "the latitude stays inside the chart: {}",
3979                            uv.y
3980                        );
3981                        let lifted = ball
3982                            .point_at(uv.x.rem_euclid(core::f64::consts::TAU), uv.y, T)
3983                            .unwrap();
3984                        assert!(
3985                            want.distance(lifted) < 1e-9,
3986                            "azimuth {azimuth}, t {t}: {lifted:?} against {want:?}"
3987                        );
3988                    }
3989                }
3990                // A range straddling a pole has none, and says so rather than
3991                // answering for one side.
3992                assert!(
3993                    exact_pcurve_over(circle, (half - 0.2, half + 0.2), &ball, T).is_none(),
3994                    "a range across a pole has no one line"
3995                );
3996                let _ = s;
3997            }
3998        }
3999    }
4000
4001    /// A half meridian whose ends were read off the poles by projection
4002    /// runs a few nanoradians past them. That is the projection's own
4003    /// error, far inside the confusion distance, and the half is still the
4004    /// straight line it is, lifting onto the circle across the whole range.
4005    #[test]
4006    fn a_meridian_half_read_slightly_past_its_poles_is_still_a_line() {
4007        let half = core::f64::consts::PI;
4008        let ball = sphere(Point::new(1.0, -2.0, 0.5), 0.2);
4009        let cut = plane(
4010            Point::new(1.0, -2.0, 0.5),
4011            Vector::new(-0.7_f64.sin(), 0.7_f64.cos(), 0.0),
4012        );
4013        let SurfaceIntersection::Along(curves) =
4014            intersect_surfaces(&ball, &cut, IntersectOptions::default(), T).unwrap()
4015        else {
4016            panic!("a plane through the centre meets the ball along a circle");
4017        };
4018        let circle = &curves[0].curve;
4019        let SurfaceGeometry::Sphere(s) = &ball else {
4020            panic!("a sphere surface");
4021        };
4022        for over in [2e-9, 1e-8, 1e-7] {
4023            for (lo, hi) in [(-over, half + over), (half - over, 2.0 * half + over)] {
4024                let pcurve = exact_pcurve_over(circle, (lo, hi), &ball, T)
4025                    .unwrap_or_else(|| panic!("{over} past the poles: no line"));
4026                for i in 0..=16 {
4027                    let t = (hi - lo).mul_add(f64::from(i) / 16.0, lo);
4028                    let uv = pcurve.point_at(t, T).unwrap();
4029                    let lifted = ogeom_math::elementary::sphere_at(&s.sphere(), uv.x, uv.y).point;
4030                    let want = circle.point_at(t, T).unwrap();
4031                    assert!(
4032                        want.distance(lifted) < 1e-9,
4033                        "{over}, t {t}: {lifted:?} against {want:?}"
4034                    );
4035                }
4036            }
4037        }
4038    }
4039
4040    /// A trim says *where* on a curve, not what it is. The basis carries the
4041    /// shape and the trim shares its parameter, so a trimmed curve's pcurve is
4042    /// the basis's own pcurve trimmed the same way, on every surface, since
4043    /// the answer does not depend on the surface at all.
4044    ///
4045    /// A fillet's own end cap is a plane and the edges bounding it are trimmed
4046    /// curves. Without this, the boolean refuses the coincidence because it
4047    /// cannot put a trimmed curve into a chart it plainly lies in.
4048    #[test]
4049    fn a_trimmed_curve_carries_its_basis_pcurve_trimmed_the_same_way() {
4050        use ogeom_geom::TrimmedCurve;
4051        let drum = cylinder(Vector::Z, 2.0);
4052        let ground = plane(Point::new(0.0, 0.0, 1.0), Vector::Z);
4053        // The circle where they meet, and a quarter of it.
4054        let SurfaceIntersection::Along(curves) =
4055            intersect_surfaces(&drum, &ground, IntersectOptions::default(), T).unwrap()
4056        else {
4057            panic!("a plane across a cylinder meets it in a circle");
4058        };
4059        let whole = curves[0].curve.clone();
4060        let (lo, hi) = whole.domain();
4061        let quarter: Curve = TrimmedCurve::new(whole.clone(), lo + 0.3, lo + (hi - lo) / 4.0, T)
4062            .unwrap()
4063            .into();
4064
4065        for surface in [&drum, &ground] {
4066            let full = exact_pcurve_of(&whole, surface, T).expect("the whole circle has one");
4067            let part = exact_pcurve_of(&quarter, surface, T).expect("and so does a quarter of it");
4068            // Same parameter, same point: the trim changed the range and
4069            // nothing else.
4070            let (a, b) = quarter.domain();
4071            for i in 0..=8 {
4072                let t = (b - a).mul_add(f64::from(i) / 8.0, a);
4073                let (whole_at, part_at) =
4074                    (full.point_at(t, T).unwrap(), part.point_at(t, T).unwrap());
4075                assert!(
4076                    whole_at.distance(part_at) < 1e-12,
4077                    "the trim carries the basis: {whole_at:?} against {part_at:?}"
4078                );
4079                // And it lifts back onto the curve it came from.
4080                let lifted = surface
4081                    .point_at(part_at.x.rem_euclid(core::f64::consts::TAU), part_at.y, T)
4082                    .or_else(|_| surface.point_at(part_at.x, part_at.y, T))
4083                    .unwrap();
4084                assert!(
4085                    lifted.distance(quarter.point_at(t, T).unwrap()) < 1e-9,
4086                    "same-parameter, still"
4087                );
4088            }
4089        }
4090    }
4091
4092    /// An arc whose range runs past the end of its circle's turn (from late
4093    /// in the turn across the circle's origin) has a pcurve on a surface of
4094    /// revolution over that whole range, the same point at every parameter
4095    /// of it.
4096    #[test]
4097    fn an_arc_across_its_circles_origin_has_a_pcurve_over_its_range() {
4098        let drum = cylinder(Vector::Z, 2.0);
4099        let ball = sphere(Point::ORIGIN, 2.0);
4100        let ground = plane(Point::ORIGIN, Vector::Z);
4101        for surface in [&drum, &ball] {
4102            let SurfaceIntersection::Along(curves) =
4103                intersect_surfaces(surface, &ground, IntersectOptions::default(), T).unwrap()
4104            else {
4105                panic!("a plane through the axis's normal meets it in a circle");
4106            };
4107            let circle = curves[0].curve.clone();
4108            let tau = core::f64::consts::TAU;
4109            for range in [(4.7, tau + 1.0), (-1.0, 1.5)] {
4110                let pcurve = exact_pcurve_over(&circle, range, surface, T).expect("a parallel");
4111                for i in 0..=8 {
4112                    let t = (range.1 - range.0).mul_add(f64::from(i) / 8.0, range.0);
4113                    let at = pcurve.point_at(t, T).unwrap();
4114                    let lifted = surface.point_at(at.x.rem_euclid(tau), at.y, T).unwrap();
4115                    assert!(
4116                        lifted.distance(circle.point_at(t, T).unwrap()) < 1e-9,
4117                        "{range:?} at {t}"
4118                    );
4119                }
4120            }
4121        }
4122    }
4123
4124    /// A plane through a cone's apex: tangent, it touches along one ruling;
4125    /// steeper, it holds two; shallower, it meets the apex alone. Every
4126    /// ruling lies on both surfaces and stays within the cone's window.
4127    #[test]
4128    fn a_plane_through_a_cones_apex_holds_its_rulings() {
4129        use ogeom_geom::ConeSurface;
4130        let frame = Frame::new(
4131            Point::new(100.0, 200.0, 300.0),
4132            Direction::Z,
4133            Direction::X,
4134            T,
4135        )
4136        .unwrap();
4137        let cone = ogeom_math::Cone::new(frame, 10.0, core::f64::consts::FRAC_PI_4, T).unwrap();
4138        let surface: SurfaceGeometry = ConeSurface::new(cone, (-5.0, 50.0)).unwrap().into();
4139        let apex = Point::new(100.0, 200.0, 290.0);
4140        let plane = |normal: Vector| -> SurfaceGeometry {
4141            PlaneSurface::new(Plane::through(apex, Direction::new(normal, T).unwrap())).into()
4142        };
4143        let cases = [
4144            (Vector::new(1.0, 0.0, -1.0), 1, true),
4145            (Vector::new(1.0, 0.0, 0.0), 2, false),
4146        ];
4147        for (normal, count, tangent) in cases {
4148            let cut = plane(normal);
4149            let SurfaceIntersection::Along(sections) =
4150                intersect_surfaces(&surface, &cut, IntersectOptions::default(), T).unwrap()
4151            else {
4152                panic!("{normal:?}: rulings");
4153            };
4154            assert_eq!(sections.len(), count, "{normal:?}");
4155            for section in &sections {
4156                assert_eq!(section.tangential, tangent, "{normal:?}");
4157                let (lo, hi) = section.curve.domain();
4158                for k in 0..=4 {
4159                    let p = section
4160                        .curve
4161                        .point_at(lo + (hi - lo) * f64::from(k) / 4.0, T)
4162                        .unwrap();
4163                    assert!(cone.distance_to(p) < 1e-9, "{p:?} on the cone");
4164                    let height = p.z - 300.0;
4165                    assert!(
4166                        (-5.0 - 1e-9..=50.0 + 1e-9).contains(&height),
4167                        "{p:?} in the window"
4168                    );
4169                }
4170            }
4171        }
4172        let shallow = plane(Vector::new(0.2, 0.0, 1.0));
4173        assert!(matches!(
4174            intersect_surfaces(&surface, &shallow, IntersectOptions::default(), T).unwrap(),
4175            SurfaceIntersection::Touching(_) | SurfaceIntersection::Apart
4176        ));
4177    }
4178
4179    #[test]
4180    fn a_far_stated_ruling_reads_its_angle_on_the_used_nappe() {
4181        use ogeom_geom::ConeSurface;
4182        // A 45-degree cone opening along +z, reference radius 24 at the
4183        // frame's origin; a ruling at chart angle 0.01, exactly as a real
4184        // file states it: the line's own origin parked seven hundred
4185        // kilometres down the infinite line, past the apex on the other
4186        // nappe. Only the used range may vote on the angle, or the pcurve
4187        // lands half a turn away and the face triangulates as a fan across
4188        // the whole chart.
4189        let cone =
4190            ogeom_math::Cone::new(Frame::WORLD, 24.0, core::f64::consts::FRAC_PI_4, T).unwrap();
4191        let surface: SurfaceGeometry = ConeSurface::new(cone, (-1e5, 1e5)).unwrap().into();
4192        let u_true = 0.01_f64;
4193        let radial = Vector::new(u_true.cos(), u_true.sin(), 0.0);
4194        // The ruling climbs outward at 45 degrees; its stated origin sits
4195        // far beyond the apex (z = -24 on this cone), on the other nappe.
4196        let direction =
4197            Direction::new((radial + Vector::new(0.0, 0.0, 1.0)) / 2f64.sqrt(), T).unwrap();
4198        let far = -7.0e5;
4199        let origin = Point::ORIGIN + radial * 24.0 + direction.vector() * far;
4200        let line = ogeom_geom::LineCurve::over(
4201            ogeom_math::Axis::new(origin, direction),
4202            far.abs() - 1.0,
4203            far.abs() + 1.0,
4204        )
4205        .unwrap();
4206        let curve: Curve = line.into();
4207        let range = ogeom_geom::Curve3d::domain(&curve);
4208        let pcurve = exact_pcurve_over(&curve, range, &surface, T).expect("a ruling inverts");
4209        let at = pcurve.point_at(range.0, T).unwrap();
4210        let tau = core::f64::consts::TAU;
4211        let gap = (at.x - u_true)
4212            .rem_euclid(tau)
4213            .min(tau - (at.x - u_true).rem_euclid(tau));
4214        assert!(
4215            gap < 1e-6,
4216            "the ruling's chart angle must be the used side's: got u {} against {u_true}",
4217            at.x
4218        );
4219    }
4220
4221    /// A thin drum crossing a wide one obliquely, its far side passing two
4222    /// hundredths of a millimetre inside the wide wall: two loops (the
4223    /// second cut short by the wide drum's end), each turning sharply at
4224    /// the tip where the drums all but touch. Each comes back as curves
4225    /// within a few microns of both drums; the whole first loop fitted as
4226    /// one misses its trace by millimetres.
4227    #[test]
4228    fn loops_turning_sharply_where_drums_all_but_touch_fit_in_pieces() {
4229        let drum = |origin: Point, axis: Vector, x: Vector, radius: f64, height: (f64, f64)| {
4230            let frame = Frame::new(
4231                origin,
4232                Direction::new(axis, T).unwrap(),
4233                Direction::new(x, T).unwrap(),
4234                T,
4235            )
4236            .unwrap();
4237            let cylinder = Cylinder::new(frame, radius, T).unwrap();
4238            (
4239                cylinder,
4240                SurfaceGeometry::from(CylinderSurface::new(cylinder, height).unwrap()),
4241            )
4242        };
4243        let radius = 3.175;
4244        let (thin_drum, thin) = drum(
4245            Point::new(10.994_218_762_109_735, 53.975, -209.55),
4246            Vector::new(0.0, 0.0, -1.0),
4247            Vector::new(-1.0, 0.0, 0.0),
4248            radius,
4249            (-1000.0, 1000.0),
4250        );
4251        let (wide_drum, wide) = drum(
4252            Point::new(
4253                -14.478_610_818_124_423,
4254                3.999_371_635_181_902,
4255                -277.138_401_510_994_7,
4256            ),
4257            Vector::new(
4258                -0.565_016_635_381_368_8,
4259                0.528_780_602_945_684_7,
4260                0.633_361_883_673_714_3,
4261            ),
4262            Vector::new(0.0, 0.767_637_390_304_296_3, -0.640_884_417_821_817),
4263            57.088_766_757_544_09,
4264            (0.0, 171.306_147_621_762_6),
4265        );
4266        let options = IntersectOptions {
4267            tolerance: 1e-5,
4268            marching: crate::Marching {
4269                chord: 1e-5,
4270                ..crate::Marching::default()
4271            },
4272        };
4273        let SurfaceIntersection::Along(curves) =
4274            intersect_surfaces(&thin, &wide, options, T).unwrap()
4275        else {
4276            panic!("the drums cross");
4277        };
4278        let mut length = 0.0;
4279        for section in &curves {
4280            assert!(!section.tangential);
4281            assert!(
4282                section.tolerance < 1e-3,
4283                "a section states {} of doubt",
4284                section.tolerance
4285            );
4286            let (lo, hi) = section.curve.domain();
4287            let mut previous = section.curve.point_at(lo, T).unwrap();
4288            for i in 1..=400 {
4289                let t = lo + (hi - lo) * f64::from(i) / 400.0;
4290                let p = section.curve.point_at(t, T).unwrap();
4291                let off = thin_drum
4292                    .distance_to(p)
4293                    .abs()
4294                    .max(wide_drum.distance_to(p).abs());
4295                assert!(off < 1e-3, "a section stands {off} off the drums");
4296                length += previous.distance(p);
4297                previous = p;
4298            }
4299        }
4300        // Each loop runs the thin drum's girth stretched along the wide
4301        // wall: the two together far longer than two girths.
4302        assert!(
4303            length > 4.0 * core::f64::consts::PI * radius,
4304            "the sections cover {length}"
4305        );
4306    }
4307
4308    #[test]
4309    fn coincidence_is_measured_over_the_overlap_and_nowhere_else() {
4310        // Patches restated from planes: the geometry no longer says "plane",
4311        // which is the whole reason this measurement exists.
4312        let patch = |plane: ogeom_math::Plane, u: (f64, f64), v: (f64, f64)| {
4313            let surface: SurfaceGeometry = PlaneSurface::over(plane, u, v).unwrap().into();
4314            SurfaceGeometry::from(surface.to_bspline(T).unwrap())
4315        };
4316        let reach = T.confusion() * 1e2;
4317
4318        // Two windows on one plane, overlapping over a quarter of each. They
4319        // are the same surface exactly where they meet, which is the claim.
4320        let here = patch(ogeom_math::Plane::XY, (0.0, 10.0), (0.0, 10.0));
4321        let over = patch(ogeom_math::Plane::XY, (5.0, 15.0), (5.0, 15.0));
4322        assert!(surfaces_coincide(&here, &over, reach, T));
4323
4324        // The same plane lifted clear of itself is not the same surface, and
4325        // a plane square to it crosses rather than coincides: the case that
4326        // must keep marching, since a crossing has a section to find.
4327        let above = patch(
4328            ogeom_math::Plane::new(
4329                Frame::new(Point::new(0.0, 0.0, 1.0), Direction::Z, Direction::X, T).unwrap(),
4330            ),
4331            (0.0, 10.0),
4332            (0.0, 10.0),
4333        );
4334        assert!(!surfaces_coincide(&here, &above, reach, T));
4335        let across = patch(
4336            ogeom_math::Plane::new(
4337                Frame::new(Point::new(5.0, 0.0, 0.0), Direction::X, Direction::Y, T).unwrap(),
4338            ),
4339            (0.0, 10.0),
4340            (0.0, 10.0),
4341        );
4342        assert!(!surfaces_coincide(&here, &across, reach, T));
4343    }
4344
4345    /// A curve offset square to a plane's normal, and a parabola, lying in
4346    /// the plane, have exact pcurves on it, the same point at every
4347    /// parameter: an offset of a B-spline and of a circle, the reference
4348    /// along the normal or against it, the distance either sign. An
4349    /// offset whose reference leans off the normal leaves the plane and
4350    /// has none.
4351    #[test]
4352    fn an_offset_square_to_a_plane_and_a_parabola_have_pcurves_on_it() {
4353        use ogeom_geom::{BSplineCurve, CircleCurve, OffsetCurve, ParabolaCurve};
4354        let normal = Vector::new(0.3, -0.5, 0.8);
4355        let up = Direction::new(normal, T).unwrap();
4356        let origin = Point::new(1.0, 2.0, -0.5);
4357        let ground = plane(origin, normal);
4358        let frame = Frame::new(
4359            origin,
4360            up,
4361            Direction::new(Vector::new(1.0, 0.0, -0.375), T).unwrap(),
4362            T,
4363        )
4364        .unwrap();
4365        let on = |x: f64, y: f64| frame.origin() + frame.x().vector() * x + frame.y().vector() * y;
4366        let knots = ogeom_math::KnotVector::clamped_uniform(3, 5).unwrap();
4367        let wiggle: Curve = BSplineCurve::new(
4368            knots,
4369            vec![
4370                on(0.0, 0.0),
4371                on(1.0, 1.0),
4372                on(2.0, -0.5),
4373                on(3.0, 0.5),
4374                on(4.0, 0.0),
4375            ],
4376            T,
4377        )
4378        .unwrap()
4379        .into();
4380        let round: Curve = CircleCurve::new(ogeom_math::Circle::new(frame, 1.5, T).unwrap()).into();
4381        let parabola: Curve =
4382            ParabolaCurve::over(ogeom_math::Parabola::new(frame, 0.7, T).unwrap(), -1.5, 2.0)
4383                .unwrap()
4384                .into();
4385        let mut curves = vec![parabola];
4386        for basis in [wiggle, round] {
4387            for reference in [up, -up] {
4388                for distance in [0.4, -0.3] {
4389                    let offset = OffsetCurve::new(basis.clone(), distance, reference).unwrap();
4390                    curves.push(Curve::Offset(Box::new(offset)));
4391                }
4392            }
4393        }
4394        for curve in &curves {
4395            let pcurve = exact_pcurve_of(curve, &ground, T).expect("it lies in the plane");
4396            let (lo, hi) = curve.domain();
4397            for i in 0..=16 {
4398                let t = (hi - lo).mul_add(f64::from(i) / 16.0, lo);
4399                let at = pcurve.point_at(t, T).unwrap();
4400                let lifted = ground.point_at(at.x, at.y, T).unwrap();
4401                let point = curve.point_at(t, T).unwrap();
4402                assert!(
4403                    lifted.distance(point) < 1e-9,
4404                    "{curve:?} at {t}: {lifted:?} against {point:?}"
4405                );
4406            }
4407        }
4408        // Leaned by `ε`, the reference lifts a point offset `d` off the
4409        // plane by up to `d tan ε`, here past the confusion distance.
4410        let leaned = Direction::new(normal + frame.x().vector() * 0.05, T).unwrap();
4411        let knots = ogeom_math::KnotVector::clamped_uniform(3, 4).unwrap();
4412        let straight: Curve = BSplineCurve::new(
4413            knots,
4414            vec![on(0.0, 0.0), on(1.0, 0.0), on(2.0, 0.0), on(3.0, 0.0)],
4415            T,
4416        )
4417        .unwrap()
4418        .into();
4419        let off = Curve::Offset(Box::new(OffsetCurve::new(straight, 0.4, leaned).unwrap()));
4420        assert!(exact_pcurve_of(&off, &ground, T).is_none());
4421    }
4422
4423    /// A hyperbola branch in a plane has an exact pcurve there at the same
4424    /// parameter, with its derivatives: forward, on a frame turned over, run
4425    /// backwards over a domain symmetric about zero or over any other, and
4426    /// offset.
4427    #[test]
4428    fn a_hyperbola_in_a_plane_has_its_pcurve_there() {
4429        use ogeom_geom::{HyperbolaCurve, OffsetCurve, Reversible as _};
4430        let normal = Vector::new(0.3, -0.5, 0.8);
4431        let up = Direction::new(normal, T).unwrap();
4432        let origin = Point::new(1.0, 2.0, -0.5);
4433        let ground = plane(origin, normal);
4434        let x = Direction::new(Vector::new(1.0, 0.0, -0.375), T).unwrap();
4435        let base = Frame::new(origin, up, x, T).unwrap();
4436        let centre = origin + base.x().vector() * 0.2 + base.y().vector() * 0.1;
4437        let frame = Frame::new(centre, up, x, T).unwrap();
4438        let turned_over =
4439            Frame::from_axes(frame.origin(), frame.x(), -frame.y(), -frame.z(), T).unwrap();
4440        let branch = |frame: Frame, lo: f64, hi: f64| -> Curve {
4441            HyperbolaCurve::over(
4442                ogeom_math::Hyperbola::new(frame, 1.5, 0.8, T).unwrap(),
4443                lo,
4444                hi,
4445            )
4446            .unwrap()
4447            .into()
4448        };
4449        let forward = branch(frame, -1.2, 0.7);
4450        let mut curves = vec![
4451            forward.clone(),
4452            branch(turned_over, -1.2, 0.7),
4453            branch(frame, -1.2, 1.2).reversed(),
4454            branch(frame, -1.2, 0.7).reversed(),
4455        ];
4456        for reference in [up, -up] {
4457            let offset = OffsetCurve::new(forward.clone(), 0.3, reference).unwrap();
4458            curves.push(Curve::Offset(Box::new(offset)));
4459        }
4460        for curve in &curves {
4461            let pcurve = exact_pcurve_of(curve, &ground, T).expect("it lies in the plane");
4462            let (lo, hi) = curve.domain();
4463            for i in 0..=16 {
4464                let t = (hi - lo).mul_add(f64::from(i) / 16.0, lo);
4465                let at = pcurve.point_at(t, T).unwrap();
4466                let lifted = ground.point_at(at.x, at.y, T).unwrap();
4467                let point = curve.point_at(t, T).unwrap();
4468                assert!(
4469                    lifted.distance(point) < 1e-9,
4470                    "{curve:?} at {t}: {lifted:?} against {point:?}"
4471                );
4472                if matches!(curve, Curve::Offset(_)) {
4473                    continue;
4474                }
4475                // The plane's chart is an isometry, so each derivative in
4476                // the chart, lifted along its axes, is the curve's own.
4477                let flat = pcurve.derivatives_at(t, 2, T).unwrap();
4478                let space = curve.derivatives_at(t, 2, T).unwrap();
4479                let axes = ground_axes(&ground);
4480                for (order, (f, s)) in flat.iter().zip(&space).enumerate().skip(1) {
4481                    let lifted = axes.0 * f.x + axes.1 * f.y;
4482                    assert!(
4483                        (lifted - *s).magnitude() < 1e-9,
4484                        "{curve:?} at {t}, order {order}: {lifted:?} against {s:?}"
4485                    );
4486                }
4487            }
4488        }
4489    }
4490
4491    /// Each lifted point of `pcurve` on `surface`, and its first derivative
4492    /// lifted along the chart's tangents, against the curve's own at 65
4493    /// parameters over `range`, to 1e-12.
4494    fn lifts_onto(
4495        curve: &Curve,
4496        pcurve: &PlanarCurve,
4497        range: (f64, f64),
4498        surface: &SurfaceGeometry,
4499    ) {
4500        use ogeom_geom::Surface as _;
4501        for i in 0..=64 {
4502            let t = (range.1 - range.0).mul_add(f64::from(i) / 64.0, range.0);
4503            let at = pcurve.derivatives_at(t, 1, T).unwrap();
4504            let lifted = surface.point_at(at[0].x, at[0].y, T).unwrap();
4505            let point = curve.point_at(t, T).unwrap();
4506            assert!(
4507                lifted.distance(point) < 1e-12,
4508                "{curve:?} at {t}: {lifted:?} against {point:?}"
4509            );
4510            let (su, sv) = surface.d1_at(at[0].x, at[0].y, T).unwrap();
4511            let tangent = su * at[1].x + sv * at[1].y;
4512            let own = curve.d1_at(t, T).unwrap();
4513            assert!(
4514                (tangent - own).magnitude() < 1e-12,
4515                "{curve:?} at {t}: tangent {tangent:?} against {own:?}"
4516            );
4517        }
4518    }
4519
4520    /// A trim run backwards reads its basis at `s + e - u` over its window
4521    /// `[s, e]`, and its pcurve is the basis's trimmed the same way and
4522    /// reversed: a circle arc inside its turn and across its seam, a
4523    /// hyperbola arc, and a trim of a trim, each at the same parameter.
4524    #[test]
4525    fn a_reversed_trim_in_a_plane_has_its_pcurve_there() {
4526        use ogeom_geom::{CircleCurve, HyperbolaCurve, Reversible as _, TrimmedCurve};
4527        let normal = Vector::new(0.3, -0.5, 0.8);
4528        let up = Direction::new(normal, T).unwrap();
4529        let origin = Point::new(1.0, 2.0, -0.5);
4530        let ground = plane(origin, normal);
4531        let x = Direction::new(Vector::new(1.0, 0.0, -0.375), T).unwrap();
4532        let base = Frame::new(origin, up, x, T).unwrap();
4533        let centre = origin + base.x().vector() * 0.2 + base.y().vector() * 0.1;
4534        let frame = Frame::new(centre, up, x, T).unwrap();
4535        let circle: Curve =
4536            CircleCurve::new(ogeom_math::Circle::new(frame, 1.3, T).unwrap()).into();
4537        let hyperbola: Curve = HyperbolaCurve::over(
4538            ogeom_math::Hyperbola::new(frame, 1.5, 0.8, T).unwrap(),
4539            -1.2,
4540            0.7,
4541        )
4542        .unwrap()
4543        .into();
4544        let trim = |basis: &Curve, lo: f64, hi: f64| -> Curve {
4545            TrimmedCurve::new(basis.clone(), lo, hi, T).unwrap().into()
4546        };
4547        let curves = [
4548            trim(&circle, 0.4, 2.9).reversed(),
4549            trim(&circle, 5.5, 7.1).reversed(),
4550            trim(&circle.reversed(), 0.4, 2.9).reversed(),
4551            trim(&hyperbola, -0.9, 0.5).reversed(),
4552            trim(&trim(&hyperbola, -1.0, 0.6).reversed(), -0.9, 0.5).reversed(),
4553        ];
4554        for curve in &curves {
4555            let pcurve = exact_pcurve_of(curve, &ground, T).expect("it lies in the plane");
4556            lifts_onto(curve, &pcurve, curve.domain(), &ground);
4557        }
4558    }
4559
4560    /// A ruling on a cone, trimmed and run backwards: its pcurve is the
4561    /// ruling's column trimmed the same way and reversed, at the same
4562    /// parameter.
4563    #[test]
4564    fn a_reversed_trim_of_a_ruling_on_a_cone_has_its_pcurve_there() {
4565        use ogeom_geom::{ConeSurface, Reversible as _, TrimmedCurve};
4566        let cone =
4567            ogeom_math::Cone::new(Frame::WORLD, 24.0, core::f64::consts::FRAC_PI_4, T).unwrap();
4568        let surface: SurfaceGeometry = ConeSurface::new(cone, (-1e3, 1e3)).unwrap().into();
4569        let radial = Vector::new(0.3_f64.cos(), 0.3_f64.sin(), 0.0);
4570        let direction =
4571            Direction::new((radial + Vector::new(0.0, 0.0, 1.0)) / 2f64.sqrt(), T).unwrap();
4572        let apex = Point::new(0.0, 0.0, -24.0);
4573        let line: Curve =
4574            ogeom_geom::LineCurve::over(ogeom_math::Axis::new(apex, direction), 5.0, 50.0)
4575                .unwrap()
4576                .into();
4577        let curve = Curve::from(TrimmedCurve::new(line, 10.0, 30.0, T).unwrap()).reversed();
4578        let pcurve = exact_pcurve_of(&curve, &surface, T).expect("a ruling inverts");
4579        lifts_onto(&curve, &pcurve, curve.domain(), &surface);
4580    }
4581
4582    /// A ruling past a cone's apex lies on the far nappe, where the chart's
4583    /// radius runs negative, so its chart column is half a turn from its own
4584    /// bearing there. The ruling is used wholly on either side of the apex,
4585    /// through the apex with its farther end on either side, from an origin
4586    /// stated past the apex on the side it does not use, and run backwards.
4587    /// Each lifts onto its line at the same parameter, on a cone widening up
4588    /// its axis and on one narrowing.
4589    #[test]
4590    fn a_ruling_past_the_apex_has_its_pcurve_on_its_own_nappe() {
4591        use ogeom_geom::{ConeSurface, Reversible as _};
4592        let frame = Frame::new(
4593            Point::new(1.0, -2.0, 0.5),
4594            Direction::new(Vector::new(0.2, -0.3, 1.0), T).unwrap(),
4595            Direction::from_cross(Vector::new(0.2, -0.3, 1.0), Vector::new(0.0, 1.0, 0.0), T)
4596                .unwrap(),
4597            T,
4598        )
4599        .unwrap();
4600        for half_angle in [core::f64::consts::FRAC_PI_4, -core::f64::consts::FRAC_PI_6] {
4601            let cone = ogeom_math::Cone::new(frame, 3.0, half_angle, T).unwrap();
4602            let surface: SurfaceGeometry = ConeSurface::new(cone, (-200.0, 200.0)).unwrap().into();
4603            // The column at angle 0.3, climbing the chart's height: through
4604            // the apex at the height where the radius is zero.
4605            let u = 0.3_f64;
4606            let (x, y, z) = (frame.x().vector(), frame.y().vector(), frame.z().vector());
4607            let bearing = x * u.cos() + y * u.sin();
4608            let apex = frame.origin() + z * (-3.0 / half_angle.tan());
4609            let up = Direction::new(bearing * half_angle.tan() + z, T).unwrap();
4610            let stated = |origin: f64, range: (f64, f64)| -> Curve {
4611                let at = apex + up.vector() * origin;
4612                ogeom_geom::LineCurve::over(
4613                    ogeom_math::Axis::new(at, up),
4614                    range.0 - origin,
4615                    range.1 - origin,
4616                )
4617                .unwrap()
4618                .into()
4619            };
4620            let lines = [
4621                stated(0.0, (5.0, 40.0)),
4622                stated(0.0, (-40.0, -5.0)),
4623                stated(0.0, (-10.0, 30.0)),
4624                stated(0.0, (-30.0, 10.0)),
4625                stated(60.0, (-40.0, -5.0)),
4626                stated(-60.0, (5.0, 40.0)),
4627                stated(0.0, (-30.0, 10.0)).reversed(),
4628                stated(0.0, (-40.0, -5.0)).reversed(),
4629            ];
4630            for line in &lines {
4631                let range = line.domain();
4632                let pcurve = exact_pcurve_over(line, range, &surface, T).expect("a ruling inverts");
4633                lifts_onto(line, &pcurve, range, &surface);
4634            }
4635        }
4636    }
4637
4638    /// A curve drawn on a plane under another chart has its pcurve in the
4639    /// plane's own, at the same parameter, wherever that chart's plane is
4640    /// this one: the same chart, one turned and moved within the plane,
4641    /// one whose normal is turned round (a mirror image in the chart), and
4642    /// one trimmed. A chart on a plane tipped off this one, or standing off
4643    /// it, is another plane, and has none.
4644    #[test]
4645    fn a_curve_drawn_on_the_plane_under_any_chart_has_its_pcurve_there() {
4646        use ogeom_geom::{
4647            BSpline2d, Circle2d, CurveOnSurface, Line2d, PlaneSurface, Reversible as _,
4648            TrimmedSurface,
4649        };
4650        let normal = Vector::new(0.3, -0.5, 0.8);
4651        let up = Direction::new(normal, T).unwrap();
4652        let origin = Point::new(1.0, 2.0, -0.5);
4653        let ground = plane(origin, normal);
4654        let x = Direction::new(Vector::new(1.0, 0.0, -0.375), T).unwrap();
4655        let own = Frame::new(origin, up, x, T).unwrap();
4656        let elsewhere = origin + own.x().vector() * 0.4 + own.y().vector() * 0.5;
4657        let chart = |frame: Frame| -> SurfaceGeometry {
4658            PlaneSurface::over(Plane::new(frame), (-6.0, 6.0), (-6.0, 6.0))
4659                .unwrap()
4660                .into()
4661        };
4662        let turned = Frame::new(
4663            elsewhere,
4664            up,
4665            Direction::from_cross(normal, Vector::new(0.2, 0.9, 0.1), T).unwrap(),
4666            T,
4667        )
4668        .unwrap();
4669        let mirrored = Frame::new(elsewhere, -up, x, T).unwrap();
4670        let trimmed: SurfaceGeometry =
4671            TrimmedSurface::new(chart(turned), (-3.0, 3.0), (-3.0, 3.0), T)
4672                .unwrap()
4673                .into();
4674        let ring = ogeom_math::Circle2::new(
4675            ogeom_math::Frame2::new(Point2::new(0.3, -0.2), ogeom_math::Direction2::X),
4676            0.9,
4677            T,
4678        )
4679        .unwrap();
4680        let knots = ogeom_math::KnotVector::clamped_uniform(3, 5).unwrap();
4681        let wave = BSpline2d::new(
4682            knots,
4683            vec![
4684                Point2::new(-1.0, 0.0),
4685                Point2::new(-0.5, 0.7),
4686                Point2::new(0.0, -0.4),
4687                Point2::new(0.6, 0.5),
4688                Point2::new(1.1, 0.1),
4689            ],
4690            T,
4691        )
4692        .unwrap();
4693        let pcurves: [PlanarCurve; 4] = [
4694            Line2d::segment(Point2::new(-0.5, 0.25), Point2::new(1.0, 0.75), T)
4695                .unwrap()
4696                .into(),
4697            Circle2d::new(ring).into(),
4698            PlanarCurve::from(Circle2d::new(ring)).reversed(),
4699            wave.into(),
4700        ];
4701        for surface in [chart(own), chart(turned), chart(mirrored), trimmed] {
4702            for drawn in &pcurves {
4703                let curve = Curve::OnSurface(Box::new(CurveOnSurface::new(
4704                    drawn.clone(),
4705                    surface.clone(),
4706                )));
4707                let pcurve = exact_pcurve_of(&curve, &ground, T).expect("it lies in the plane");
4708                lifts_onto(&curve, &pcurve, curve.domain(), &ground);
4709            }
4710        }
4711        let tipped = Frame::new(
4712            elsewhere,
4713            Direction::new(normal + Vector::new(1e-3, 0.0, 0.0), T).unwrap(),
4714            x,
4715            T,
4716        );
4717        let standing = Frame::new(elsewhere + up.vector() * 1e-3, up, x, T).unwrap();
4718        for frame in [tipped.unwrap(), standing] {
4719            let curve = Curve::OnSurface(Box::new(CurveOnSurface::new(
4720                pcurves[0].clone(),
4721                chart(frame),
4722            )));
4723            assert!(exact_pcurve_of(&curve, &ground, T).is_none());
4724        }
4725    }
4726
4727    /// The plane's chart axes in space.
4728    fn ground_axes(ground: &SurfaceGeometry) -> (Vector, Vector) {
4729        let SurfaceGeometry::Plane(p) = ground else {
4730            unreachable!("a plane")
4731        };
4732        let frame = p.plane().frame();
4733        (frame.x().vector(), frame.y().vector())
4734    }
4735
4736    /// The cone `ρ = 3.2 − z / 10` about the `z` axis, down from `z = 2`.
4737    fn drafted_drum() -> SurfaceGeometry {
4738        let frame = Frame::new(Point::new(0.0, 0.0, 2.0), -Direction::Z, -Direction::X, T).unwrap();
4739        ogeom_geom::ConeSurface::new(
4740            ogeom_math::Cone::new(frame, 3.0, 0.1_f64.atan(), T).unwrap(),
4741            (0.0, 2.0),
4742        )
4743        .unwrap()
4744        .into()
4745    }
4746
4747    /// The rim of [`drafted_drum`] rounded at radius ½ under `z = 2`, its
4748    /// section's arc in the plane `y = 0` from its contact with the cone
4749    /// over `turn`, swept along `y`, the rim's tangent there, over `sweep`:
4750    /// a band of the drum of radius ½ about `y` through `(c, 0, 3 / 2)`,
4751    /// `c = 3.05 − √1.01 / 2`. Returns the band and `c`.
4752    fn carried_round(turn: f64, sweep: (f64, f64)) -> (SurfaceGeometry, f64) {
4753        let lean = 1.01_f64.sqrt();
4754        let c = lean.mul_add(-0.5, 3.05);
4755        let frame = Frame::from_axes(
4756            Point::new(c, 0.0, 1.5),
4757            Direction::new(Vector::new(1.0, 0.0, 0.1), T).unwrap(),
4758            Direction::new(Vector::new(-0.1, 0.0, 1.0), T).unwrap(),
4759            -Direction::Y,
4760            T,
4761        )
4762        .unwrap();
4763        let arc: Curve = ogeom_geom::TrimmedCurve::new(
4764            ogeom_geom::CircleCurve::new(ogeom_math::Circle::new(frame, 0.5, T).unwrap()).into(),
4765            0.0,
4766            turn,
4767            T,
4768        )
4769        .unwrap()
4770        .into();
4771        let band = ogeom_geom::ExtrusionSurface::over(arc, Direction::Y, sweep)
4772            .unwrap()
4773            .into();
4774        (band, c)
4775    }
4776
4777    /// [`carried_round`] over `turn` and `y` over `sweep`, turned `angle`
4778    /// about the cone's axis, against the cone in both orders, the band
4779    /// walked within `window` where one is given. The arc touches the cone
4780    /// at its start, where the sweep's ruling is the rim's tangent: the band
4781    /// stands off the cone by about `0.164 y² − u² / 4` near there, so two
4782    /// branches leave that node on the band's border, one to each end of
4783    /// the sweep or the window. Each is the point of the band at the turn
4784    /// `u` solving `√(x(u)² + y²) = 3.2 − z(u) / 10` at each `y`, turned
4785    /// with the band. Every section must be a crossing on both surfaces,
4786    /// stating no wider a tolerance than the chord and the fit asked for,
4787    /// the two branches must end at the node and at both ends, and each end
4788    /// must end one section only: a branch returned twice ends there twice.
4789    /// No section may end anywhere else.
4790    fn crosses_from_the_node_to_both_ends(
4791        (turn, sweep): (f64, (f64, f64)),
4792        angle: f64,
4793        window: Option<ParameterWindow>,
4794    ) {
4795        let (band, c) = carried_round(turn, sweep);
4796        let about = ogeom_math::Transform::rotation(
4797            ogeom_math::Axis {
4798                location: Point::ORIGIN,
4799                direction: Direction::Z,
4800            },
4801            angle,
4802        );
4803        let band = ogeom_geom::Transformable::transformed(&band, &about, T).unwrap();
4804        let (sin, cos) = angle.sin_cos();
4805        let turned = |p: Point| {
4806            Point::new(
4807                p.x.mul_add(cos, -p.y * sin),
4808                p.x.mul_add(sin, p.y * cos),
4809                p.z,
4810            )
4811        };
4812        let back = |p: Point| {
4813            Point::new(
4814                p.x.mul_add(cos, p.y * sin),
4815                p.y.mul_add(cos, -p.x * sin),
4816                p.z,
4817            )
4818        };
4819        let drum = drafted_drum();
4820        let lean = 1.01_f64.sqrt();
4821        let at = |u: f64, y: f64| {
4822            let (cos, sin) = (u.cos(), u.sin());
4823            Point::new(
4824                0.5f64.mul_add((cos - 0.1 * sin) / lean, c),
4825                y,
4826                0.5f64.mul_add((0.1f64.mul_add(cos, sin)) / lean, 1.5),
4827            )
4828        };
4829        let off = |p: Point| p.x.hypot(p.y) - 0.1f64.mul_add(-p.z, 3.2);
4830        let crossing = |y: f64| {
4831            let (mut lo, mut hi) = (1e-9, 0.425);
4832            for _ in 0..200 {
4833                let mid = f64::midpoint(lo, hi);
4834                if off(at(mid, y)).signum() == off(at(lo, y)).signum() {
4835                    lo = mid;
4836                } else {
4837                    hi = mid;
4838                }
4839            }
4840            turned(at(f64::midpoint(lo, hi), y))
4841        };
4842        let node = turned(at(0.0, 0.0));
4843        let ends = window.map_or(sweep, |w| w.1);
4844        let far = [crossing(ends.0), crossing(ends.1)];
4845        // A boolean marches at a chord of a ten-millionth of the parts.
4846        let options = window.map_or_else(IntersectOptions::default, |_| IntersectOptions {
4847            tolerance: 1e-7,
4848            marching: Marching {
4849                chord: 1e-7,
4850                ..Marching::default()
4851            },
4852        });
4853        for band_first in [true, false] {
4854            let met = if band_first {
4855                intersect_surfaces_within(&band, &drum, (window, None), options, T)
4856            } else {
4857                intersect_surfaces_within(&drum, &band, (None, window), options, T)
4858            }
4859            .unwrap();
4860            let SurfaceIntersection::Along(sections) = met else {
4861                panic!("turned {angle}: the band crosses the cone, got {met:?}");
4862            };
4863            let mut found: Vec<Point> = Vec::new();
4864            for section in &sections {
4865                assert!(
4866                    !section.tangential,
4867                    "turned {angle}, band first {band_first}: {section:?}"
4868                );
4869                assert!(
4870                    section.tolerance <= options.marching.chord + options.tolerance,
4871                    "turned {angle}, band first {band_first}: a tolerance of {:e}",
4872                    section.tolerance
4873                );
4874                let (lo, hi) = section.curve.domain();
4875                let near = section.tolerance + T.confusion();
4876                for i in 0..=16 {
4877                    let p = section
4878                        .curve
4879                        .point_at(lo + (hi - lo) * f64::from(i) / 16.0, T)
4880                        .unwrap();
4881                    let q = back(p);
4882                    let from_band = ((q.x - c).hypot(q.z - 1.5) - 0.5).abs();
4883                    assert!(
4884                        from_band <= near && off(p).abs() <= near,
4885                        "{p:?} is off the band by {from_band:e} or the cone by {:e}",
4886                        off(p)
4887                    );
4888                }
4889                found.extend([lo, hi].map(|t| section.curve.point_at(t, T).unwrap()));
4890            }
4891            let near = sections.iter().map(|s| s.tolerance).fold(0.0, f64::max) + T.confusion();
4892            assert!(
4893                found.iter().any(|p| p.distance(node) <= near),
4894                "turned {angle}: no section ends at the node {node:?}: {found:?}"
4895            );
4896            for want in far {
4897                let count = found.iter().filter(|p| p.distance(want) <= near).count();
4898                assert_eq!(
4899                    count, 1,
4900                    "turned {angle}: {count} sections end at {want:?}: {found:?}"
4901                );
4902            }
4903            for p in &found {
4904                assert!(
4905                    p.distance(node) <= near || far.iter().any(|q| p.distance(*q) <= near),
4906                    "turned {angle}, band first {band_first}: a section ends at {p:?}, \
4907                     neither the node nor an end: {found:?}"
4908                );
4909            }
4910        }
4911    }
4912
4913    /// [`crosses_from_the_node_to_both_ends`] with `y` from −0.012 to 0.02:
4914    /// the two surfaces' normals part by a sine of at most 0.018 along the
4915    /// branches, under the marcher's tangency threshold. Both are sections
4916    /// the two cross along, not a contact: they were traced, read as
4917    /// tangential by their sine alone, and the pair came back apart.
4918    #[test]
4919    fn a_band_crossing_a_cone_shallowly_from_a_node_on_its_border_crosses_it() {
4920        crosses_from_the_node_to_both_ends((0.425, (-0.012, 0.02)), 0.0, None);
4921    }
4922
4923    /// [`crosses_from_the_node_to_both_ends`] with the branch to the lower
4924    /// end of the sweep 0.0021 and 0.0043 long, within the sixty chords
4925    /// round the node where the march's samples count as on it, over a
4926    /// short turn and a long one. That branch was all samples on the node
4927    /// and went; where the walk from it stalled onto the other branch, the
4928    /// arm it leaves the node along was walked out only from past the
4929    /// node's radius, outside the band. The section stopped at the node.
4930    #[test]
4931    fn a_band_crossing_a_cone_from_a_node_keeps_its_arm_short_of_the_node_s_radius() {
4932        for turn in [0.425, 1.2] {
4933            for low in [-0.004, -0.002] {
4934                crosses_from_the_node_to_both_ends((turn, (low, 0.02)), 0.0, None);
4935            }
4936        }
4937    }
4938
4939    /// [`crosses_from_the_node_to_both_ends`] with the branch to the lower
4940    /// end of the sweep 0.0008 and 0.0005 long, under the ten chords an arc
4941    /// of the march is debris within, over a short turn and a long one.
4942    /// That branch, the section out to the band's border, was dropped as
4943    /// debris; kept, every sample of it lies where the surfaces part by
4944    /// less than they measurably cross at, and it was read as a contact.
4945    #[test]
4946    fn a_band_crossing_a_cone_from_a_node_keeps_its_arm_shorter_than_ten_chords() {
4947        for turn in [0.425, 1.2] {
4948            for low in [-0.000_74, -0.000_5] {
4949                crosses_from_the_node_to_both_ends((turn, (low, 0.02)), 0.0, None);
4950            }
4951        }
4952    }
4953
4954    /// [`crosses_from_the_node_to_both_ends`] over the band a fillet
4955    /// junction carries on past its cap at a drum drafted foot out:
4956    /// the round's whole quarter turn swept along `y` from −2.42 to 4.83,
4957    /// walked within the window of the face a boolean cuts from it, `u` to
4958    /// 0.42 and `y` from −0.004 to 0.02, turned about the cone's axis every
4959    /// half degree. Both branches leave that window through its ends in
4960    /// `y`, crossing the cone there. At the odd multiples of 7.5 degrees the
4961    /// cells the march seeds from found neither, and the pair came back
4962    /// apart. Turned 60 degrees, the cone named first, the lower branch was
4963    /// traced twice, the two tracings standing microns apart across it
4964    /// where it nears the node: the stretch of one there was kept beside
4965    /// the other, read as a contact, and the valley walker followed the
4966    /// band's border, tangent to the cone at the node, 0.0078 either way.
4967    #[test]
4968    fn a_band_crossing_a_cone_is_two_crossings_at_every_turn() {
4969        let window = (
4970            (0.0, 0.424_721_486_693_660_6),
4971            (-0.003_979_668_916_835_106, 0.019_898_344_584_175_24),
4972        );
4973        for k in 0..720 {
4974            crosses_from_the_node_to_both_ends(
4975                (
4976                    1.471_127_674_303_734_5,
4977                    (-2.415_259_779_545_657_6, 4.830_519_559_091_315),
4978                ),
4979                (f64::from(k) * 0.5).to_radians(),
4980                Some(window),
4981            );
4982        }
4983    }
4984
4985    /// [`crosses_from_the_node_to_both_ends`] over the whole band, `y` from
4986    /// −0.004 to 0.02, a turn of 1.2 and the round's quarter turn, turned
4987    /// every half degree. At most turns no pair of the grid's cells
4988    /// crosses, and the band's borders seed alone: the ruling at the node
4989    /// first, where the surfaces are tangent, then where each branch leaves
4990    /// through an end of the sweep. Both of those lie within a seed's reach
4991    /// of the node and were merged into it, and no walk sets out from a
4992    /// tangency: the pair came back apart at 612 and 613 turns of 720.
4993    #[test]
4994    fn a_band_crossing_a_cone_unwindowed_is_two_crossings_at_every_turn() {
4995        for turn in [1.2, 1.471_127_674_303_734_5] {
4996            for k in 0..720 {
4997                crosses_from_the_node_to_both_ends(
4998                    (turn, (-0.004, 0.02)),
4999                    (f64::from(k) * 0.5).to_radians(),
5000                    None,
5001                );
5002            }
5003        }
5004    }
5005
5006    /// [`crosses_from_the_node_to_both_ends`] at the quarter turn, `y` from
5007    /// −0.002 to 0.02, unwindowed, turned every half degree. Turned 150
5008    /// degrees, the cone named first, the lower branch is seven samples,
5009    /// five once thinned, two of them `1.8e-3` apart with nothing between.
5010    /// The cubic laid through them passed each within `1.2e-7` and strayed
5011    /// `1.0e-6` from the section between those two, stating a tolerance of
5012    /// `1.0100720e-4`, past the chord and fit asked for.
5013    #[test]
5014    fn a_band_crossing_a_cone_unwindowed_states_its_fit_on_a_short_branch() {
5015        for k in 0..720 {
5016            crosses_from_the_node_to_both_ends(
5017                (1.471_127_674_303_734_5, (-0.002, 0.02)),
5018                (f64::from(k) * 0.5).to_radians(),
5019                None,
5020            );
5021        }
5022    }
5023
5024    /// [`crosses_from_the_node_to_both_ends`] over the band of
5025    /// [`a_band_crossing_a_cone_is_two_crossings_at_every_turn`], walked
5026    /// within windows cut `8e-4` and `5e-5` below the node, turned every
5027    /// half degree: the lower branch leaves through the window's lower
5028    /// border. Cut at `8e-4`, the seed found where that border pierces the
5029    /// cone stopped at its solve's residual and, held on the border, slid
5030    /// along it by that residual over the sine the section crosses it at,
5031    /// `8.9e-7`, past a seed's reach of the true crossing: the branch walked
5032    /// from it took the place of the one walked from the crossing, and at
5033    /// 180 and 264 degrees no section ended there. Cut at `5e-5`, a branch
5034    /// walked both ways from a seed by the node stalled there at one end and
5035    /// left the window at the other, and the end on the border, clear of
5036    /// the node, was carried onto it as if stalled too: at 120, 264 and 315
5037    /// degrees the crossing of the border lay inside a section.
5038    #[test]
5039    fn a_band_crossing_a_cone_in_a_small_window_ends_on_its_border() {
5040        for low in [-8e-4, -5e-5] {
5041            let window = (
5042                (0.0, 0.424_721_486_693_660_6),
5043                (low, 0.019_898_344_584_175_24),
5044            );
5045            for k in 0..720 {
5046                crosses_from_the_node_to_both_ends(
5047                    (
5048                        1.471_127_674_303_734_5,
5049                        (-2.415_259_779_545_657_6, 4.830_519_559_091_315),
5050                    ),
5051                    (f64::from(k) * 0.5).to_radians(),
5052                    Some(window),
5053                );
5054            }
5055        }
5056    }
5057
5058    /// [`crosses_from_the_node_to_both_ends`] over the band of
5059    /// [`a_band_crossing_a_cone_is_two_crossings_at_every_turn`], walked
5060    /// within a window cut `1e-5` below the node, turned every half degree:
5061    /// the lower branch is an arm `1.1e-5` long, out to the window's lower
5062    /// border. Inside the node's snap, `√(2ε / bend)`, about `3.2e-4` here,
5063    /// every point stands within `ε` of both surfaces, and the walk running
5064    /// through the node landed on that border `1.7e-4` along it from where
5065    /// the arm crosses it: at a dozen turns that came back as a third
5066    /// section beside the arm, ending where no section does.
5067    #[test]
5068    fn a_band_crossing_a_cone_in_a_window_cut_by_its_node_has_no_section_along_the_cut() {
5069        let window = (
5070            (0.0, 0.424_721_486_693_660_6),
5071            (-1e-5, 0.019_898_344_584_175_24),
5072        );
5073        for k in 0..720 {
5074            crosses_from_the_node_to_both_ends(
5075                (
5076                    1.471_127_674_303_734_5,
5077                    (-2.415_259_779_545_657_6, 4.830_519_559_091_315),
5078                ),
5079                (f64::from(k) * 0.5).to_radians(),
5080                Some(window),
5081            );
5082        }
5083    }
5084
5085    /// [`crosses_from_the_node_to_both_ends`] with the branch to the lower
5086    /// end of the sweep about `3.4e-4`, `2.2e-4` and `1.2e-4` long, at the
5087    /// default chord of `1e-4`, turned every degree: each arm leaves the
5088    /// band at or inside the node's snap, `3.2e-4`, where no walk is set
5089    /// out from, and was missing.
5090    #[test]
5091    fn a_band_crossing_a_cone_from_a_node_keeps_its_arm_shorter_than_the_snap() {
5092        for low in [-0.000_32, -0.000_2, -0.000_11] {
5093            for k in 0..360 {
5094                crosses_from_the_node_to_both_ends(
5095                    (0.425, (low, 0.02)),
5096                    f64::from(k).to_radians(),
5097                    None,
5098                );
5099            }
5100        }
5101    }
5102
5103    /// [`crosses_from_the_node_to_both_ends`] turned 60 degrees, the cone
5104    /// named first: the lower crossing's samples by the node stood across
5105    /// the arm by up to `9e-6`, each within `3e-10` of both surfaces, and
5106    /// its fit stated a tolerance of `6.5e-4` at a chord of `1e-7`. Every
5107    /// half degree is weighed by
5108    /// [`a_band_crossing_a_cone_is_two_crossings_at_every_turn`].
5109    #[test]
5110    fn a_band_crossing_a_cone_states_a_tight_tolerance_by_its_node() {
5111        let window = (
5112            (0.0, 0.424_721_486_693_660_6),
5113            (-0.003_979_668_916_835_106, 0.019_898_344_584_175_24),
5114        );
5115        crosses_from_the_node_to_both_ends(
5116            (
5117                1.471_127_674_303_734_5,
5118                (-2.415_259_779_545_657_6, 4.830_519_559_091_315),
5119            ),
5120            60.0_f64.to_radians(),
5121            Some(window),
5122        );
5123    }
5124
5125    /// A drum of radius ½ laid along a ruling of [`drafted_drum`], outside
5126    /// it, its axis that far off the ruling along the cone's normal, less
5127    /// `into`: the cone's tangent plane holds along the ruling, so resting
5128    /// there the two touch along it, and pressed in by `ε` they cross along
5129    /// two curves either side of it with `ε` between them. Resting, or
5130    /// pressed in by less than the confusion distance a marched point
5131    /// stands within, they are a contact; pressed in by a micron or ten,
5132    /// where the two cross at a sine of 0.002 or 0.007, they cross along
5133    /// two sections, one each side of the ruling, every point on both. The
5134    /// sine alone read the crossings as a contact.
5135    #[test]
5136    fn a_drum_pressed_into_a_cone_along_a_ruling_crosses_it_only_measurably() {
5137        let drum = drafted_drum();
5138        let lean = 1.01_f64.sqrt();
5139        let up = Vector::new(-0.1, 0.0, 1.0) / lean;
5140        let out = Vector::new(1.0, 0.0, 0.1) / lean;
5141        let off = |p: Point| p.x.hypot(p.y) - 0.1f64.mul_add(-p.z, 3.2);
5142        for into in [0.0, 0.4 * T.confusion(), 1e-6, 1e-5] {
5143            let foot = Point::new(3.0, 0.0, 2.0) + out * (0.5 - into);
5144            let frame = Frame::new(
5145                foot,
5146                Direction::new(up, T).unwrap(),
5147                Direction::new(out, T).unwrap(),
5148                T,
5149            )
5150            .unwrap();
5151            let roller: SurfaceGeometry =
5152                CylinderSurface::new(Cylinder::new(frame, 0.5, T).unwrap(), (-1.8, -0.2))
5153                    .unwrap()
5154                    .into();
5155            let from_axis = |p: Point| {
5156                let d = p - foot;
5157                (d - up * d.dot(up)).magnitude()
5158            };
5159            for (a, b) in [(&roller, &drum), (&drum, &roller)] {
5160                let met = intersect_surfaces(a, b, IntersectOptions::default(), T).unwrap();
5161                let SurfaceIntersection::Along(sections) = met else {
5162                    panic!("pressed in {into:e}: {met:?}");
5163                };
5164                if into <= T.confusion() {
5165                    assert!(
5166                        sections.iter().all(|s| s.tangential),
5167                        "pressed in {into:e}: {sections:?}"
5168                    );
5169                    continue;
5170                }
5171                assert_eq!(sections.len(), 2, "pressed in {into:e}: {sections:?}");
5172                let mut sides = Vec::new();
5173                for section in &sections {
5174                    assert!(!section.tangential, "pressed in {into:e}: {section:?}");
5175                    let (lo, hi) = section.curve.domain();
5176                    let near = section.tolerance + T.confusion();
5177                    let mut side = 0.0;
5178                    for i in 0..=16 {
5179                        let p = section
5180                            .curve
5181                            .point_at(lo + (hi - lo) * f64::from(i) / 16.0, T)
5182                            .unwrap();
5183                        assert!(
5184                            (from_axis(p) - 0.5).abs() <= near && off(p).abs() <= near,
5185                            "pressed in {into:e}: {p:?} is off the drum or the cone"
5186                        );
5187                        side += p.y;
5188                    }
5189                    sides.push(side);
5190                }
5191                assert!(
5192                    sides[0] * sides[1] < 0.0,
5193                    "pressed in {into:e}: both on one side {sides:?}"
5194                );
5195            }
5196        }
5197    }
5198}