Skip to main content

ogeom_intersect/
march.rs

1//! The general surface/surface intersector: seed, then walk.
2//!
3//! Where two surfaces meet has no closed form in general: the curve is
4//! transcendental, and `docs/DATA_MODEL.md` ยง9 is blunt about the consequence:
5//! there is no exact answer to be exact about, which is why the topology carries
6//! tolerances. What there is instead is a curve that can be *followed*, one
7//! corrected step at a time, to a stated accuracy.
8//!
9//! # Two problems, kept apart on purpose
10//!
11//! **Finding a branch** and **following one** fail in completely different ways,
12//! and lumping them together is how an intersector comes to look better than it
13//! is. A tracer that follows one branch beautifully while never noticing the
14//! second reports a smooth, accurate, *wrong* answer, and the obvious accuracy
15//! measure, "is every point on both surfaces", scores it perfectly.
16//!
17//! So [`seeds`] and [`trace`] are separate, separately testable, and separately
18//! measured. Seeding is polyhedral: both surfaces are sampled into triangles and
19//! the triangle pairs that cross give starting points. It finds a branch if the
20//! sampling resolves it, and *misses one thinner than the grid*, which is a
21//! real limitation with a knob attached rather than a mystery.
22//!
23//! # Following the curve
24//!
25//! At a point on both surfaces the intersection runs along the cross product of
26//! the two normals: the one direction that stays in both tangent planes. Step
27//! along it and you leave both surfaces slightly; a Newton correction brings you
28//! back.
29//!
30//! The correction has four unknowns (two parameters on each surface) and three
31//! equations, `A(u1,v1) = B(u2,v2)`. That is deliberately one short, because the
32//! solution set *is* the curve and pinning it to a point needs one more
33//! condition. The fourth is a plane across the direction of travel: it says how
34//! far along to land, and it is what turns "somewhere on the curve" into "the
35//! next point".
36//!
37//! # What it reports about itself
38//!
39//! Whether the curve closed, and whether it ran out of steps. A polyline that
40//! stopped because it hit a limit is not the same answer as one that stopped
41//! because the curve ended, and a caller that cannot tell them apart will treat
42//! a truncated branch as a complete one.
43
44use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
45use ogeom_geom::{Surface, SurfaceGeometry};
46use ogeom_math::{Point, Vector, solve};
47
48/// How hard to look, and how closely to follow.
49#[derive(Debug, Clone, Copy, PartialEq)]
50pub struct Marching {
51    /// How far the polyline may sit from the true curve, in space.
52    pub chord: f64,
53    /// How finely each surface is sampled when looking for branches.
54    ///
55    /// The limitation with a knob on it: a branch narrower than one cell can be
56    /// stepped over entirely. Raising this costs time quadratically and is the
57    /// only thing that makes a thin branch findable.
58    pub grid: usize,
59    /// A ceiling on the points in one branch, so a curve that will not close
60    /// cannot run forever.
61    pub max_points: usize,
62}
63
64impl Default for Marching {
65    fn default() -> Self {
66        Self {
67            chord: 1e-4,
68            grid: 24,
69            max_points: 20_000,
70        }
71    }
72}
73
74impl Marching {
75    /// Check the settings are usable.
76    ///
77    /// # Errors
78    ///
79    /// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if the chord is
80    /// not positive, the grid is too coarse to hold a triangle, or no points are
81    /// allowed.
82    pub fn validate(&self) -> OgeomResult<()> {
83        if !self.chord.is_finite() || self.chord <= 0.0 {
84            ogeom_bail!(Construction, "a chord of {} is not a distance", self.chord);
85        }
86        if self.grid < 2 {
87            ogeom_bail!(Construction, "a sampling grid needs at least two steps");
88        }
89        if self.max_points < 2 {
90            ogeom_bail!(Construction, "a branch needs at least two points");
91        }
92        Ok(())
93    }
94}
95
96/// A point that lies on both surfaces, with where it is on each.
97#[derive(Debug, Clone, Copy, PartialEq)]
98pub struct Contact {
99    /// Parameters on the first surface.
100    pub on_a: (f64, f64),
101    /// Parameters on the second.
102    pub on_b: (f64, f64),
103    /// Where that is in space.
104    pub point: Point,
105}
106
107/// Why a traced branch stopped.
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109pub enum Stopped {
110    /// It came back to where it started.
111    Closed,
112    /// It reached the edge of one surface's domain.
113    LeftTheDomain,
114    /// The correction stopped converging: a tangency or a singular point.
115    ///
116    /// Reported rather than pushed through. Marching past a point where the two
117    /// normals are parallel is how a tracer jumps onto the wrong branch, and a
118    /// wrong branch is a plausible answer to a different question.
119    Stalled,
120    /// It hit [`Marching::max_points`].
121    ///
122    /// Distinct from every other reason, because this one means the answer is
123    /// *incomplete* rather than finished.
124    RanOut,
125}
126
127/// One traced branch.
128#[derive(Debug, Clone, PartialEq)]
129pub struct Traced {
130    /// The points along it, in order.
131    pub points: Vec<Point>,
132    /// Where each point is on the first surface.
133    pub on_a: Vec<(f64, f64)>,
134    /// Where each point is on the second.
135    pub on_b: Vec<(f64, f64)>,
136    /// Why it stopped.
137    pub stopped: Stopped,
138}
139
140impl Traced {
141    /// Whether the branch is finished rather than truncated.
142    #[must_use]
143    pub const fn complete(&self) -> bool {
144        !matches!(self.stopped, Stopped::RanOut)
145    }
146
147    /// Whether it closed on itself.
148    #[must_use]
149    pub const fn closed(&self) -> bool {
150        matches!(self.stopped, Stopped::Closed)
151    }
152}
153
154/// Starting points on the intersection, one per branch found.
155///
156/// Polyhedral: both surfaces are sampled into triangles, the pairs that cross
157/// give approximate points, and each is corrected onto both surfaces exactly.
158/// Points that land on the same spot are merged, so a branch crossing many
159/// cells yields one seed rather than dozens.
160///
161/// # Errors
162///
163/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if the settings are
164/// unusable.
165pub fn seeds(
166    a: &SurfaceGeometry,
167    b: &SurfaceGeometry,
168    options: Marching,
169    tol: Tolerances,
170) -> OgeomResult<Vec<Contact>> {
171    options.validate()?;
172    let (mesh_a, mesh_b) = (sample(a, options.grid, tol), sample(b, options.grid, tol));
173    let apart = span(a).min(span(b)) / f64::from(u32::try_from(options.grid).unwrap_or(1));
174    let near = CellBins::over(&mesh_b, options.chord);
175
176    let mut found: Vec<Contact> = Vec::new();
177    let mut candidates: Vec<usize> = Vec::new();
178    for cell_a in &mesh_a {
179        // Cheap rejection first: most pairs are nowhere near each other,
180        // and the segment test below is far from free. The bins hand over
181        // only the cells whose boxes could meet this one's, in their own
182        // order, so what follows sees what the full scan would have.
183        near.candidates(cell_a, &mut candidates);
184        for &j in &candidates {
185            let cell_b = &mesh_b[j];
186            if !overlap(cell_a, cell_b, options.chord) {
187                continue;
188            }
189            let Some(guess) = triangles_cross(cell_a, cell_b) else {
190                continue;
191            };
192            let start = [cell_a.at.0, cell_a.at.1, cell_b.at.0, cell_b.at.1];
193            let Some(contact) = correct(a, b, start, guess, None, tol) else {
194                continue;
195            };
196            // One seed per branch, not one per cell it passes through. The
197            // spacing is the *finer* surface's grid: two distinct branches
198            // closer than that were never going to be told apart by this
199            // sampling anyway, while the coarser surface's cells say nothing
200            // about how far apart branches can be: a plane's clamped domain
201            // spans a million units, and its cell would merge every branch
202            // through a blend into one.
203            if found
204                .iter()
205                .any(|c| c.point.distance(contact.point) <= apart)
206            {
207                continue;
208            }
209            found.push(contact);
210        }
211    }
212    // A branch that runs in from a spline's border at a grazing angle can
213    // be thinner than the sampling's sag, and no pair of cells crosses on
214    // it. Where it meets the border it is a curve piercing a surface,
215    // which is found exactly: each border of each spline is intersected
216    // with the other surface, and every piercing seeds.
217    for (from_a, border_of, other) in [(true, a, b), (false, b, a)] {
218        for (border, at) in spline_borders(border_of, tol) {
219            let Ok(met) = crate::intersect_curve_surface(
220                &border,
221                other,
222                crate::CurveSurfaceOptions::default(),
223                tol,
224            ) else {
225                continue;
226            };
227            for piercing in met.crossings {
228                let on_border = at(piercing.on_curve);
229                let start = if from_a {
230                    [
231                        on_border.0,
232                        on_border.1,
233                        piercing.on_surface.0,
234                        piercing.on_surface.1,
235                    ]
236                } else {
237                    [
238                        piercing.on_surface.0,
239                        piercing.on_surface.1,
240                        on_border.0,
241                        on_border.1,
242                    ]
243                };
244                let Some(contact) = correct(a, b, start, piercing.point, None, tol) else {
245                    continue;
246                };
247                if found
248                    .iter()
249                    .any(|c| c.point.distance(contact.point) <= apart)
250                {
251                    continue;
252                }
253                found.push(contact);
254            }
255        }
256    }
257    Ok(found)
258}
259
260/// A border of a surface as a curve, and the map from the curve's
261/// parameter to the surface's.
262type Border = (ogeom_geom::Curve, Box<dyn Fn(f64) -> (f64, f64)>);
263
264/// The open borders of a spline surface.
265fn spline_borders(surface: &SurfaceGeometry, tol: Tolerances) -> Vec<Border> {
266    let SurfaceGeometry::BSpline(spline) = surface else {
267        return Vec::new();
268    };
269    let ((u0, u1), (v0, v1)) = surface.domain();
270    let mut out: Vec<Border> = Vec::new();
271    if !surface.is_closed_u(tol) {
272        for u in [u0, u1] {
273            if let Ok(c) = spline.iso_u_curve(u, tol) {
274                out.push((ogeom_geom::Curve::BSpline(c), Box::new(move |t| (u, t))));
275            }
276        }
277    }
278    if !surface.is_closed_v(tol) {
279        for v in [v0, v1] {
280            if let Ok(c) = spline.iso_v_curve(v, tol) {
281                out.push((ogeom_geom::Curve::BSpline(c), Box::new(move |t| (t, v))));
282            }
283        }
284    }
285    out
286}
287
288/// Every branch of the intersection: seed, trace each, and keep the distinct
289/// ones.
290///
291/// A branch crossing many sampling cells produces many seeds, and tracing from
292/// any of them gives the same curve. So a seed already lying on something
293/// traced is dropped rather than followed again, which is what makes the
294/// *number* of branches returned meaningful, and it is the number a boolean
295/// will act on.
296///
297/// # Errors
298///
299/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if the settings are
300/// unusable.
301pub fn branches(
302    a: &SurfaceGeometry,
303    b: &SurfaceGeometry,
304    options: Marching,
305    tol: Tolerances,
306) -> OgeomResult<Vec<Traced>> {
307    let found = seeds(a, b, options, tol)?;
308    let mut out: Vec<Traced> = Vec::new();
309    for seed in found {
310        // Already on something we have followed.
311        let reach = options.chord.max(tol.confusion()) * 8.0;
312        if out
313            .iter()
314            .any(|branch| passes_near(branch, seed.point, reach))
315        {
316            continue;
317        }
318        // A seed that will not trace (a tangency) is reported by being
319        // absent rather than by an error, since the other branches are still
320        // real answers.
321        if let Ok(branch) = trace(a, b, seed, options, tol)
322            && branch.points.len() >= 2
323            && !is_fragment(&branch, options)
324        {
325            // A branch whose middle lies on one already traced is that
326            // branch again, reached from a seed its trace stopped short of.
327            let middle = branch.points[branch.points.len() / 2];
328            if out.iter().any(|other| passes_near(other, middle, reach)) {
329                continue;
330            }
331            out.push(branch);
332        }
333    }
334    Ok(stitch_stalled(out, a, b, options, tol))
335}
336
337/// Below this sine the surfaces count as tangent at a point: the
338/// branch-point certificate a stall end must carry to participate in
339/// stitching.
340const BRANCH_POINT_SINE: f64 = 0.05;
341
342/// The sine of the normal angle at a contact: the transversality measure.
343fn crossing_sine(
344    a: &SurfaceGeometry,
345    b: &SurfaceGeometry,
346    on_a: (f64, f64),
347    on_b: (f64, f64),
348    tol: Tolerances,
349) -> f64 {
350    let Ok(na) = a.normal_at(on_a.0, on_a.1, tol) else {
351        return 0.0;
352    };
353    let Ok(nb) = b.normal_at(on_b.0, on_b.1, tol) else {
354        return 0.0;
355    };
356    na.vector().cross(nb.vector()).magnitude()
357}
358
359/// Whether a stalled trace is a fragment rather than a curve.
360///
361/// Coincident or near-coincident surfaces defeat the tangency check at a seed:
362/// rounding in the corrected parameters leaves the two normals a whisker apart,
363/// the walk takes a couple of steps, and then stalls where the arithmetic gives
364/// out. What comes back lies on both surfaces perfectly and describes nothing:
365/// identical spheres yielded six such fragments, each a few points long.
366///
367/// A stalled branch shorter than a handful of chords carries no information the
368/// seed did not, so it is noise from a degenerate configuration and dropped. A
369/// *real* stalled branch (one that ran into a genuine tangency) has length
370/// behind it and is kept, because a truncated real answer is still an answer.
371///
372/// The marcher is deliberately not a coincidence detector: for the pairs with
373/// closed forms, [`surface_surface`](crate::surface_surface) answers
374/// [`Same`](crate::Meeting::Same), and that check belongs before this one.
375fn is_fragment(branch: &Traced, options: Marching) -> bool {
376    if branch.stopped != Stopped::Stalled {
377        return false;
378    }
379    let length: f64 = branch
380        .points
381        .windows(2)
382        .map(|pair| pair[0].distance(pair[1]))
383        .sum();
384    length < options.chord * 10.0
385}
386
387/// Whether a traced branch passes within a distance of a point.
388///
389/// Measured against the polyline's *segments*, not its vertices. The vertices
390/// are a marching step apart (far more than the chord tolerance), so a seed
391/// sitting neatly between two of them looks distant from both, and comparing to
392/// vertices alone reported one circle nine times.
393fn passes_near(branch: &Traced, p: Point, reach: f64) -> bool {
394    branch
395        .points
396        .windows(2)
397        .any(|pair| distance_to_segment(p, pair[0], pair[1]) <= reach)
398}
399
400/// Distance from a point to a segment.
401fn distance_to_segment(p: Point, a: Point, b: Point) -> f64 {
402    let along = b - a;
403    let length = along.square_magnitude();
404    if length <= f64::MIN_POSITIVE {
405        return p.distance(a);
406    }
407    let t = ((p - a).dot(along) / length).clamp(0.0, 1.0);
408    p.distance(a + along * t)
409}
410
411/// Follow the intersection from a starting point, in both directions.
412///
413/// # Errors
414///
415/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if the settings are
416/// unusable; [`OgeomError::NotDone`](ogeom_core::OgeomError::NotDone) if the two surfaces
417/// are tangent at the seed, where there is no single direction to follow.
418pub fn trace(
419    a: &SurfaceGeometry,
420    b: &SurfaceGeometry,
421    from: Contact,
422    options: Marching,
423    tol: Tolerances,
424) -> OgeomResult<Traced> {
425    options.validate()?;
426    if tangent_at(a, b, from, tol).is_none() {
427        ogeom_bail!(
428            NotDone,
429            "the surfaces are tangent here, so the intersection has no single \
430             direction to follow; that is a branch point and needs the seed \
431             moved off it"
432        );
433    }
434
435    // Forwards first. If it closes, that is the whole branch and there is
436    // nothing behind us.
437    let ahead = walk(a, b, from, 1.0, options, tol)?;
438    if ahead.stopped == Stopped::Closed {
439        return Ok(ahead);
440    }
441    let behind = walk(a, b, from, -1.0, options, tol)?;
442    let last_step = |walked: &[Point]| -> f64 {
443        walked
444            .windows(2)
445            .last()
446            .map_or(0.0, |w| w[0].distance(w[1]))
447    };
448    let steps = last_step(&ahead.points).max(last_step(&behind.points));
449
450    // Join them, the backward half reversed and its shared first point dropped.
451    let mut points = behind.points;
452    let mut on_a = behind.on_a;
453    let mut on_b = behind.on_b;
454    points.reverse();
455    on_a.reverse();
456    on_b.reverse();
457    points.pop();
458    on_a.pop();
459    on_b.pop();
460    points.extend(ahead.points);
461    on_a.extend(ahead.on_a);
462    on_b.extend(ahead.on_b);
463
464    // The worse of the two reasons: a branch truncated at either end is
465    // truncated.
466    let mut stopped = if ahead.stopped == Stopped::RanOut || behind.stopped == Stopped::RanOut {
467        Stopped::RanOut
468    } else if ahead.stopped == Stopped::Stalled || behind.stopped == Stopped::Stalled {
469        Stopped::Stalled
470    } else {
471        Stopped::LeftTheDomain
472    };
473    // A loop cut at a seam. A closed patch is clamped, not periodic: a
474    // section that runs round it (a rim's circle on a converted drum)
475    // is walked from the seed to the seam one way and to the seam the
476    // other, each walk stopping a fraction of a step short of it, and the
477    // two ends meet where the surface closes on itself. That is the whole
478    // loop, and it is closed: left open, the arrangement downstream held a
479    // circle with two ends at one point and found no face piece to keep.
480    // The ends are within a couple of the walks' own last steps of each
481    // other, and the loop is closed exactly on its first point.
482    if stopped == Stopped::LeftTheDomain && points.len() > 3 {
483        let gap = points[0].distance(points[points.len() - 1]);
484        if gap <= (steps * 2.0).max(tol.confusion() * 10.0) {
485            points.push(points[0]);
486            on_a.push(on_a[0]);
487            on_b.push(on_b[0]);
488            stopped = Stopped::Closed;
489        }
490    }
491    Ok(Traced {
492        points,
493        on_a,
494        on_b,
495        stopped,
496    })
497}
498
499/// Two surfaces, as a condition for the walker: four unknowns and three
500/// equations saying the two points coincide.
501///
502/// The intersector's own walk goes through [`crate::walk`] like everything
503/// else, and what is *not* generic lives here: the domain clamps a surface
504/// pair needs, and the tangent, which the intersector computes from the two
505/// normals rather than from the null space so that it can refuse a crossing
506/// too shallow to be more than the correction's own noise.
507struct SurfacePair<'s> {
508    a: &'s SurfaceGeometry,
509    b: &'s SurfaceGeometry,
510}
511
512impl crate::walk::Condition for SurfacePair<'_> {
513    fn unknowns(&self) -> usize {
514        4
515    }
516
517    fn position(&self, x: &[f64], tol: Tolerances) -> Option<Point> {
518        self.a.point_at(x[0], x[1], tol).ok()
519    }
520
521    fn position_gradient(&self, x: &[f64], tol: Tolerances) -> Option<Vec<Vector>> {
522        let (au, av) = self.a.d1_at(x[0], x[1], tol).ok()?;
523        // The point is taken from the first surface, so it does not move with
524        // the second's parameters at all.
525        Some(vec![au, av, Vector::ZERO, Vector::ZERO])
526    }
527
528    fn system(&self, x: &[f64], tol: Tolerances) -> Option<(Vec<f64>, Vec<Vec<f64>>)> {
529        Some(self.system_at(x, tol)?.0)
530    }
531
532    fn system_at(
533        &self,
534        x: &[f64],
535        tol: Tolerances,
536    ) -> Option<((Vec<f64>, Vec<Vec<f64>>), Point, Vec<Vector>)> {
537        let pa = self.a.point_at(x[0], x[1], tol).ok()?;
538        let pb = self.b.point_at(x[2], x[3], tol).ok()?;
539        let (au, av) = self.a.d1_at(x[0], x[1], tol).ok()?;
540        let (bu, bv) = self.b.d1_at(x[2], x[3], tol).ok()?;
541        let gap = pa - pb;
542        Some((
543            (
544                vec![gap.x, gap.y, gap.z],
545                vec![
546                    vec![au.x, av.x, -bu.x, -bv.x],
547                    vec![au.y, av.y, -bu.y, -bv.y],
548                    vec![au.z, av.z, -bu.z, -bv.z],
549                ],
550            ),
551            pa,
552            vec![au, av, Vector::ZERO, Vector::ZERO],
553        ))
554    }
555
556    fn clamp(&self, x: &mut [f64]) {
557        let (ua, va) = clamp(self.a, x[0], x[1]);
558        let (ub, vb) = clamp(self.b, x[2], x[3]);
559        x[0] = ua;
560        x[1] = va;
561        x[2] = ub;
562        x[3] = vb;
563    }
564
565    fn outside(&self, x: &[f64], tol: Tolerances) -> bool {
566        outside(self.a, (x[0], x[1]), tol) || outside(self.b, (x[2], x[3]), tol)
567    }
568
569    fn near_edge(&self, x: &[f64]) -> bool {
570        near_edge(self.a, (x[0], x[1])) || near_edge(self.b, (x[2], x[3]))
571    }
572
573    fn extent(&self) -> f64 {
574        span(self.a).max(span(self.b))
575    }
576
577    fn tangent_is_oriented(&self) -> bool {
578        // The cross product of the two normals, whose sign is the surfaces'
579        // own and whose flip at a tangency is what stops the march.
580        true
581    }
582
583    fn tangent(&self, x: &[f64], tol: Tolerances) -> Option<Vector> {
584        tangent_at(
585            self.a,
586            self.b,
587            Contact {
588                on_a: (x[0], x[1]),
589                on_b: (x[2], x[3]),
590                point: Point::ORIGIN,
591            },
592            tol,
593        )
594    }
595}
596
597/// Walk one way from a seed.
598fn walk(
599    a: &SurfaceGeometry,
600    b: &SurfaceGeometry,
601    from: Contact,
602    sense: f64,
603    options: Marching,
604    tol: Tolerances,
605) -> OgeomResult<Traced> {
606    let pair = SurfacePair { a, b };
607    let start = [from.on_a.0, from.on_a.1, from.on_b.0, from.on_b.1];
608    let walked = crate::walk::walk_one_way(&pair, &start, sense, options, tol)?;
609    Ok(Traced {
610        on_a: walked.states.iter().map(|x| (x[0], x[1])).collect(),
611        on_b: walked.states.iter().map(|x| (x[2], x[3])).collect(),
612        points: walked.points,
613        stopped: walked.stopped,
614    })
615}
616
617/// The sine of the shallowest crossing angle the marcher will follow.
618///
619/// One microradian, and the number is set by the *correction*, not by taste.
620/// `correct` accepts a residual up to the confusion tolerance, so the two
621/// parameter points of a contact can disagree by that much in space, and on
622/// coincident or near-coincident surfaces, that disagreement shows up as a
623/// spurious angle between the two computed normals of about the residual over
624/// the local feature size. A gate below that floor reads the correction's own
625/// noise as a direction and marches along it: identical spheres came back as
626/// six confident little curves that existed nowhere but in rounding.
627///
628/// So below this angle the marcher cannot tell an ultra-shallow crossing from
629/// coincidence, and refuses both rather than guessing. A genuine crossing
630/// shallower than a microradian is also one the Newton correction cannot
631/// reliably follow (its travel constraint becomes numerically dependent on
632/// the surface-gap rows at exactly the same rate), so the gate refuses what
633/// could not have been followed anyway.
634const SHALLOWEST: f64 = 1e-6;
635
636/// The direction the intersection runs at a contact.
637///
638/// The cross product of the two normals: the one direction lying in both
639/// tangent planes. `None` where the normals are parallel to within
640/// [`SHALLOWEST`]: the surfaces are tangent or coincident there, and the
641/// intersection has no direction the marcher can trust.
642fn tangent_at(
643    a: &SurfaceGeometry,
644    b: &SurfaceGeometry,
645    at: Contact,
646    tol: Tolerances,
647) -> Option<Vector> {
648    let na = normal_at(a, at.on_a, tol)?;
649    let nb = normal_at(b, at.on_b, tol)?;
650    let cross = na.cross(nb);
651    let length = cross.magnitude();
652    // The decision is made through intervals rather than a bare compare:
653    // each normal component carries the correction's stated residual as an
654    // uncertainty, the squared cross magnitude is computed as an enclosure,
655    // and only a crossing *certainly* above the floor is followed. A sine
656    // inside the enclosure's undecided band is exactly the case the floor
657    // exists for (the correction's own noise masquerading as an angle),
658    // and it is refused with a certificate instead of a guess.
659    let floor = tol.angular().max(SHALLOWEST);
660    let widen = |value: f64| ogeom_math::Interval::about(value, tol.confusion());
661    let (ax, ay, az) = (widen(na.x), widen(na.y), widen(na.z));
662    let (bx, by, bz) = (widen(nb.x), widen(nb.y), widen(nb.z));
663    let cx = ay.mul(&bz).sub(&az.mul(&by));
664    let cy = az.mul(&bx).sub(&ax.mul(&bz));
665    let cz = ax.mul(&by).sub(&ay.mul(&bx));
666    let magnitude2 = cx.square().add(&cy.square()).add(&cz.square());
667    let above = magnitude2.sub(&ogeom_math::Interval::point(floor * floor));
668    if above.certain_sign() != Some(ogeom_core::Sign::Positive) || length <= f64::MIN_POSITIVE {
669        return None;
670    }
671    Some(cross * (1.0 / length))
672}
673
674/// A surface's unit normal at a parameter.
675fn normal_at(surface: &SurfaceGeometry, at: (f64, f64), tol: Tolerances) -> Option<Vector> {
676    let (du, dv) = surface.d1_at(at.0, at.1, tol).ok()?;
677    let cross = du.cross(dv);
678    let length = cross.magnitude();
679    if length <= tol.confusion() {
680        return None;
681    }
682    Some(cross * (1.0 / length))
683}
684
685/// Bring a parameter guess onto both surfaces.
686///
687/// Three equations say the two surface points coincide; the fourth says how far
688/// along the direction of travel to land. Without that fourth the system is
689/// underdetermined (its solution set *is* the curve), and Newton would wander
690/// along it instead of converging to a point.
691///
692/// `constraint` is `(anchor, direction, distance)`. Without one, the guess
693/// itself is used as the anchor and the direction is the intersection tangent,
694/// which is what seeding wants: land anywhere on the curve near here.
695fn correct(
696    a: &SurfaceGeometry,
697    b: &SurfaceGeometry,
698    start: [f64; 4],
699    guess: Point,
700    constraint: Option<(Point, Vector, f64)>,
701    tol: Tolerances,
702) -> Option<Contact> {
703    let (anchor, along, reach) = match constraint {
704        Some(given) => given,
705        None => {
706            // No direction to travel: hold the guess still along whichever way
707            // the curve runs, so the solve slides onto the curve rather than
708            // along it.
709            let at = Contact {
710                on_a: (start[0], start[1]),
711                on_b: (start[2], start[3]),
712                point: guess,
713            };
714            (guess, tangent_at(a, b, at, tol).unwrap_or(Vector::X), 0.0)
715        }
716    };
717
718    let system = |x: &[f64; 4]| {
719        let (ua, va) = clamp(a, x[0], x[1]);
720        let (ub, vb) = clamp(b, x[2], x[3]);
721        // Where either surface cannot be evaluated the residual is
722        // infinite, so the damped step backs off rather than reading a
723        // made-up point as a root.
724        let (Ok(pa), Ok(pb), Ok((au, av)), Ok((bu, bv))) = (
725            a.point_at(ua, va, tol),
726            b.point_at(ub, vb, tol),
727            a.d1_at(ua, va, tol),
728            b.d1_at(ub, vb, tol),
729        ) else {
730            return ([f64::INFINITY; 4], [[0.0; 4]; 4]);
731        };
732
733        let gap = pa - pb;
734        let residual = [gap.x, gap.y, gap.z, (pa - anchor).dot(along) - reach];
735        let jacobian = [
736            [au.x, av.x, -bu.x, -bv.x],
737            [au.y, av.y, -bu.y, -bv.y],
738            [au.z, av.z, -bu.z, -bv.z],
739            [au.dot(along), av.dot(along), 0.0, 0.0],
740        ];
741        (residual, jacobian)
742    };
743
744    let criteria = solve::Criteria {
745        residual: tol.confusion() * 0.01,
746        step: tol.parametric(),
747        max_iterations: 40,
748    };
749    let found = solve::newton_system_fixed(system, start, criteria).ok()?;
750    if found.1 > tol.confusion() {
751        return None;
752    }
753    let (ua, va) = clamp(a, found.0[0], found.0[1]);
754    let (ub, vb) = clamp(b, found.0[2], found.0[3]);
755    Some(Contact {
756        on_a: (ua, va),
757        on_b: (ub, vb),
758        point: a.point_at(ua, va, tol).ok()?,
759    })
760}
761
762/// Hold a parameter inside a surface's domain.
763///
764/// A periodic direction wraps instead, so a curve crossing a cylinder's seam
765/// keeps going rather than stopping at a boundary that is not one.
766fn clamp(surface: &SurfaceGeometry, u: f64, v: f64) -> (f64, f64) {
767    let ((ua, ub), (va, vb)) = surface.domain();
768    let fold = |x: f64, lo: f64, hi: f64, periodic: bool| {
769        if !periodic {
770            return x.clamp(lo, hi);
771        }
772        let span = hi - lo;
773        if span <= 0.0 {
774            return x;
775        }
776        lo + (x - lo).rem_euclid(span)
777    };
778    (
779        fold(u, ua, ub, surface.is_periodic_u()),
780        fold(v, va, vb, surface.is_periodic_v()),
781    )
782}
783
784/// Whether a parameter sits close enough to a non-periodic edge that a stalled
785/// walk there means the edge rather than a singularity.
786///
787/// The band is a fraction of the domain's own span: a walk stalls within a
788/// step of the boundary, and the step is far larger than the strict band
789/// [`outside`] uses to decide a point has actually crossed.
790fn near_edge(surface: &SurfaceGeometry, at: (f64, f64)) -> bool {
791    let ((ua, ub), (va, vb)) = surface.domain();
792    let close = |x: f64, lo: f64, hi: f64, periodic: bool| {
793        !periodic && {
794            let band = (hi - lo).abs() * 1e-4;
795            x <= lo + band || x >= hi - band
796        }
797    };
798    close(at.0, ua, ub, surface.is_periodic_u()) || close(at.1, va, vb, surface.is_periodic_v())
799}
800
801/// Whether a parameter has left a surface's domain, in a direction that has one.
802fn outside(surface: &SurfaceGeometry, at: (f64, f64), tol: Tolerances) -> bool {
803    let ((ua, ub), (va, vb)) = surface.domain();
804    let past = |x: f64, lo: f64, hi: f64, periodic: bool| {
805        !periodic && (x <= lo + tol.parametric() || x >= hi - tol.parametric())
806    };
807    past(at.0, ua, ub, surface.is_periodic_u()) || past(at.1, va, vb, surface.is_periodic_v())
808}
809
810/// A surface's rough size, for spacing seeds.
811fn span(surface: &SurfaceGeometry) -> f64 {
812    let ((ua, ub), (va, vb)) = surface.domain();
813    let tol = Tolerances::millimetres();
814    let corners = [(ua, va), (ub, va), (ua, vb), (ub, vb)];
815    let mut low = Point::new(f64::MAX, f64::MAX, f64::MAX);
816    let mut high = Point::new(f64::MIN, f64::MIN, f64::MIN);
817    for (u, v) in corners {
818        if let Ok(p) = surface.point_at(u, v, tol) {
819            low = Point::new(low.x.min(p.x), low.y.min(p.y), low.z.min(p.z));
820            high = Point::new(high.x.max(p.x), high.y.max(p.y), high.z.max(p.z));
821        }
822    }
823    let size = (high - low).magnitude();
824    if size.is_finite() && size > 0.0 {
825        size
826    } else {
827        1.0
828    }
829}
830
831/// One sampled triangle of a surface, with the parameters it came from.
832pub(crate) struct Cell {
833    pub(crate) corners: [Point; 3],
834    pub(crate) at: (f64, f64),
835    pub(crate) low: Point,
836    pub(crate) high: Point,
837    /// How far the surface bows from the flat cell: its middle's distance
838    /// from the middle of the diagonal the two triangles share.
839    pub(crate) sag: f64,
840    /// The parameters of the three corners, in `corners` order.
841    pub(crate) params: [(f64, f64); 3],
842}
843
844/// Sample a surface into triangles.
845pub(crate) fn sample(surface: &SurfaceGeometry, grid: usize, tol: Tolerances) -> Vec<Cell> {
846    sample_by(surface, (grid, grid), tol)
847}
848
849/// Sample a surface into triangles, `counts` cells along `u` and along `v`.
850pub(crate) fn sample_by(
851    surface: &SurfaceGeometry,
852    counts: (usize, usize),
853    tol: Tolerances,
854) -> Vec<Cell> {
855    let ((ua, ub), (va, vb)) = surface.domain();
856    // An unbounded domain would put the samples a billion units apart and find
857    // nothing. Clamped to something a real model lives inside.
858    let limit = 1.0e6;
859    let (ua, ub) = (ua.max(-limit), ub.min(limit));
860    let (va, vb) = (va.max(-limit), vb.min(limit));
861
862    let mut out = Vec::new();
863    #[allow(clippy::cast_precision_loss)]
864    let (nu, nv) = (counts.0 as f64, counts.1 as f64);
865    for i in 0..counts.0 {
866        for j in 0..counts.1 {
867            #[allow(clippy::cast_precision_loss)]
868            let (s0, s1) = (i as f64 / nu, (i + 1) as f64 / nu);
869            #[allow(clippy::cast_precision_loss)]
870            let (t0, t1) = (j as f64 / nv, (j + 1) as f64 / nv);
871            let at = |s: f64, t: f64| {
872                let (u, v) = (ua + (ub - ua) * s, va + (vb - va) * t);
873                surface.point_at(u, v, tol).map(|p| ((u, v), p))
874            };
875            let (Ok((p00, a00)), Ok((p10, a10)), Ok((p01, a01)), Ok((p11, a11))) =
876                (at(s0, t0), at(s1, t0), at(s0, t1), at(s1, t1))
877            else {
878                continue;
879            };
880            let sag = at(f64::midpoint(s0, s1), f64::midpoint(t0, t1))
881                .map_or(0.0, |(_, middle)| middle.distance(a00.midpoint(a11)));
882            for (corners, params) in [
883                ([a00, a10, a11], [p00, p10, p11]),
884                ([a00, a11, a01], [p00, p11, p01]),
885            ] {
886                let low = Point::new(
887                    corners.iter().map(|p| p.x).fold(f64::MAX, f64::min),
888                    corners.iter().map(|p| p.y).fold(f64::MAX, f64::min),
889                    corners.iter().map(|p| p.z).fold(f64::MAX, f64::min),
890                );
891                let high = Point::new(
892                    corners.iter().map(|p| p.x).fold(f64::MIN, f64::max),
893                    corners.iter().map(|p| p.y).fold(f64::MIN, f64::max),
894                    corners.iter().map(|p| p.z).fold(f64::MIN, f64::max),
895                );
896                out.push(Cell {
897                    corners,
898                    at: p00,
899                    low,
900                    high,
901                    sag,
902                    params,
903                });
904            }
905        }
906    }
907    out
908}
909
910/// Whether two cells' boxes come within a margin of each other.
911/// Sampled cells binned by their boxes on a coarse grid, for finding the
912/// cells whose boxes could meet a given one without testing them all.
913struct CellBins {
914    low: Point,
915    size: f64,
916    counts: [usize; 3],
917    bins: Vec<Vec<usize>>,
918    margin: f64,
919}
920
921impl CellBins {
922    /// At most this many bins a side: a grid on a clamped plane's million
923    /// units would otherwise be all empty bins.
924    const MOST: usize = 48;
925
926    fn over(cells: &[Cell], margin: f64) -> Self {
927        let mut low = Point::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
928        let mut high = Point::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
929        for c in cells {
930            low = Point::new(low.x.min(c.low.x), low.y.min(c.low.y), low.z.min(c.low.z));
931            high = Point::new(
932                high.x.max(c.high.x),
933                high.y.max(c.high.y),
934                high.z.max(c.high.z),
935            );
936        }
937        let extent = (high - low).magnitude();
938        #[allow(clippy::cast_precision_loss)]
939        let size = if extent.is_finite() && extent > 0.0 {
940            (extent / Self::MOST as f64).max(margin)
941        } else {
942            1.0
943        };
944        let count = |lo: f64, hi: f64| -> usize {
945            #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
946            let n = ((hi - lo) / size).floor() as usize + 1;
947            n.clamp(1, Self::MOST + 1)
948        };
949        let counts = if extent.is_finite() {
950            [
951                count(low.x, high.x),
952                count(low.y, high.y),
953                count(low.z, high.z),
954            ]
955        } else {
956            [1, 1, 1]
957        };
958        let mut bins = vec![Vec::new(); counts[0] * counts[1] * counts[2]];
959        let mut this = Self {
960            low,
961            size,
962            counts,
963            bins: Vec::new(),
964            margin,
965        };
966        for (i, c) in cells.iter().enumerate() {
967            this.each_bin(c.low, c.high, |k| bins[k].push(i));
968        }
969        this.bins = bins;
970        this
971    }
972
973    /// Every bin a box grown by the margin touches.
974    fn each_bin(&self, low: Point, high: Point, mut visit: impl FnMut(usize)) {
975        let index = |x: f64, lo: f64, n: usize| -> usize {
976            if !x.is_finite() {
977                return if x > 0.0 { n - 1 } else { 0 };
978            }
979            #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
980            let k = ((x - lo) / self.size).floor().max(0.0) as usize;
981            k.min(n - 1)
982        };
983        let m = self.margin;
984        let [nx, ny, nz] = self.counts;
985        let (x0, x1) = (
986            index(low.x - m, self.low.x, nx),
987            index(high.x + m, self.low.x, nx),
988        );
989        let (y0, y1) = (
990            index(low.y - m, self.low.y, ny),
991            index(high.y + m, self.low.y, ny),
992        );
993        let (z0, z1) = (
994            index(low.z - m, self.low.z, nz),
995            index(high.z + m, self.low.z, nz),
996        );
997        for x in x0..=x1 {
998            for y in y0..=y1 {
999                for z in z0..=z1 {
1000                    visit((x * ny + y) * nz + z);
1001                }
1002            }
1003        }
1004    }
1005
1006    /// The cells whose boxes could meet `cell`'s, lowest index first.
1007    fn candidates(&self, cell: &Cell, out: &mut Vec<usize>) {
1008        out.clear();
1009        self.each_bin(cell.low, cell.high, |k| {
1010            out.extend_from_slice(&self.bins[k])
1011        });
1012        out.sort_unstable();
1013        out.dedup();
1014    }
1015}
1016
1017fn overlap(a: &Cell, b: &Cell, margin: f64) -> bool {
1018    a.low.x <= b.high.x + margin
1019        && b.low.x <= a.high.x + margin
1020        && a.low.y <= b.high.y + margin
1021        && b.low.y <= a.high.y + margin
1022        && a.low.z <= b.high.z + margin
1023        && b.low.z <= a.high.z + margin
1024}
1025
1026/// A point where two triangles cross, if they do.
1027///
1028/// Each triangle's edges are tested against the other's plane and then against
1029/// the triangle itself. Only an approximate answer is needed: it is a seed, and
1030/// the Newton correction that follows is what makes it a point on the curve.
1031fn triangles_cross(a: &Cell, b: &Cell) -> Option<Point> {
1032    for (edges, target) in [(a, b), (b, a)] {
1033        for k in 0..3 {
1034            let (from, to) = (edges.corners[k], edges.corners[(k + 1) % 3]);
1035            if let Some(hit) = segment_meets_triangle(from, to, target.corners) {
1036                return Some(hit);
1037            }
1038        }
1039    }
1040    None
1041}
1042
1043/// Where a segment crosses a triangle.
1044pub(crate) fn segment_meets_triangle(from: Point, to: Point, t: [Point; 3]) -> Option<Point> {
1045    let direction = to - from;
1046    let (e1, e2) = (t[1] - t[0], t[2] - t[0]);
1047    let h = direction.cross(e2);
1048    let determinant = e1.dot(h);
1049    if determinant.abs() <= f64::MIN_POSITIVE {
1050        return None;
1051    }
1052    let inverse = 1.0 / determinant;
1053    let s = from - t[0];
1054    let u = inverse * s.dot(h);
1055    if !(0.0..=1.0).contains(&u) {
1056        return None;
1057    }
1058    let q = s.cross(e1);
1059    let v = inverse * direction.dot(q);
1060    if v < 0.0 || u + v > 1.0 {
1061        return None;
1062    }
1063    let along = inverse * e2.dot(q);
1064    if !(0.0..=1.0).contains(&along) {
1065        return None;
1066    }
1067    Some(from + direction * along)
1068}
1069
1070/// One arc between branch points, cut from a stalled fragment.
1071struct Arc {
1072    points: Vec<Point>,
1073    on_a: Vec<(f64, f64)>,
1074    on_b: Vec<(f64, f64)>,
1075    /// The branch-point cluster each end attaches to, if any.
1076    head_bp: Option<usize>,
1077    tail_bp: Option<usize>,
1078}
1079
1080impl Arc {
1081    fn length(&self) -> f64 {
1082        self.points
1083            .windows(2)
1084            .map(|pair| pair[0].distance(pair[1]))
1085            .sum()
1086    }
1087
1088    /// The direction the arc leaves its end, read across a window deep
1089    /// enough to stand clear of any residual wander.
1090    fn outgoing(&self, tail: bool) -> Option<Vector> {
1091        let n = self.points.len();
1092        if n < 2 {
1093            return None;
1094        }
1095        let window = (n - 1).min(24);
1096        let (at, back) = if tail {
1097            (n - 1, n - 1 - window)
1098        } else {
1099            (0, window)
1100        };
1101        let out = self.points[at] - self.points[back];
1102        let m = out.magnitude();
1103        (m > f64::MIN_POSITIVE).then(|| out / m)
1104    }
1105}
1106
1107/// Whether a stalled branch is transversal *somewhere* in its interior:
1108/// the certificate that it is a curve passing branch points rather than
1109/// tangential-contact debris. A plane resting on a torus produces
1110/// fragments tangent along their whole length; a real curve through a
1111/// branch point is tangent only in passing.
1112fn interior_is_transversal(
1113    branch: &Traced,
1114    a: &SurfaceGeometry,
1115    b: &SurfaceGeometry,
1116    tol: Tolerances,
1117) -> bool {
1118    let n = branch.points.len();
1119    if n < 5 {
1120        return false;
1121    }
1122    [n / 4, n / 2, 3 * n / 4]
1123        .into_iter()
1124        .any(|i| crossing_sine(a, b, branch.on_a[i], branch.on_b[i], tol) > BRANCH_POINT_SINE)
1125}
1126
1127/// Join stalled fragments that meet at branch points into the curves they
1128/// belong to: the stitching an earlier plan owed, now delivered.
1129///
1130/// Where the two normals become parallel the intersection has no single
1131/// direction: the walk stalls there, wanders in place while the correction
1132/// gives out, and a curve that passes *through* the singularity comes back
1133/// as fragments with parked ends. The reassembly is geometric, not
1134/// bookkeeping: cluster the near-tangent stall ends into branch points;
1135/// cut every fragment at its branch-point visits, which trims the wander
1136/// and separates the arcs a walk-through glued together; drop debris and
1137/// duplicate coverage; then, at each branch point, pair arc ends whose
1138/// tangents continue one another (a smooth curve crosses the singularity
1139/// collinearly, and the crossing curve turns through the crossing angle)
1140/// and chain the pairs into whole curves, closing the loops that close.
1141///
1142/// Only fragments transversal somewhere in their interior participate:
1143/// tangential contact along a whole curve is a different phenomenon and
1144/// keeps its honest fragments.
1145fn stitch_stalled(
1146    found: Vec<Traced>,
1147    a: &SurfaceGeometry,
1148    b: &SurfaceGeometry,
1149    options: Marching,
1150    tol: Tolerances,
1151) -> Vec<Traced> {
1152    let reach = options.chord.max(tol.confusion()) * 60.0;
1153    const CONTINUES: f64 = 0.5;
1154
1155    let (candidates, mut out): (Vec<Traced>, Vec<Traced>) = found.into_iter().partition(|branch| {
1156        branch.stopped == Stopped::Stalled && interior_is_transversal(branch, a, b, tol)
1157    });
1158    if candidates.is_empty() {
1159        return out;
1160    }
1161
1162    // Branch points: the near-tangent stall ends, clustered.
1163    let mut bps: Vec<Point> = Vec::new();
1164    for branch in &candidates {
1165        let n = branch.points.len();
1166        for at in [0, n - 1] {
1167            if crossing_sine(a, b, branch.on_a[at], branch.on_b[at], tol) < BRANCH_POINT_SINE {
1168                let p = branch.points[at];
1169                if !bps.iter().any(|held| held.distance(p) <= reach) {
1170                    bps.push(p);
1171                }
1172            }
1173        }
1174    }
1175    if bps.is_empty() {
1176        out.extend(candidates);
1177        return out;
1178    }
1179    let bp_of =
1180        |p: Point| -> Option<usize> { bps.iter().position(|held| held.distance(p) <= reach) };
1181
1182    // Cut each fragment at its branch-point visits: maximal runs of points
1183    // clear of every branch point become arcs, attached to the branch
1184    // points beside them.
1185    let mut arcs: Vec<Arc> = Vec::new();
1186    for branch in &candidates {
1187        let n = branch.points.len();
1188        let mut run_start: Option<usize> = None;
1189        for i in 0..=n {
1190            let near = i < n && bp_of(branch.points[i]).is_some();
1191            match (run_start, near, i == n) {
1192                (None, false, false) => run_start = Some(i),
1193                (Some(s), true, _) | (Some(s), _, true) => {
1194                    let e = i;
1195                    if e > s + 1 {
1196                        let head_bp = if s > 0 {
1197                            bp_of(branch.points[s - 1])
1198                        } else {
1199                            None
1200                        };
1201                        let tail_bp = if e < n { bp_of(branch.points[e]) } else { None };
1202                        arcs.push(Arc {
1203                            points: branch.points[s..e].to_vec(),
1204                            on_a: branch.on_a[s..e].to_vec(),
1205                            on_b: branch.on_b[s..e].to_vec(),
1206                            head_bp,
1207                            tail_bp,
1208                        });
1209                    }
1210                    run_start = None;
1211                }
1212                _ => {}
1213            }
1214        }
1215    }
1216
1217    // Debris and duplicate coverage out; longest first so the fuller
1218    // tracing of a doubly-walked arc is the one kept.
1219    arcs.retain(|arc| arc.length() > options.chord * 10.0 && arc.points.len() >= 4);
1220    arcs.sort_by(|x, y| {
1221        y.length()
1222            .partial_cmp(&x.length())
1223            .unwrap_or(core::cmp::Ordering::Equal)
1224    });
1225    let mut kept: Vec<Arc> = Vec::new();
1226    'candidate: for arc in arcs {
1227        let n = arc.points.len();
1228        for probe in [n / 4, n / 2, 3 * n / 4] {
1229            let p = arc.points[probe];
1230            if kept.iter().any(|held| {
1231                held.points
1232                    .windows(2)
1233                    .any(|pair| distance_to_segment(p, pair[0], pair[1]) <= reach)
1234            }) {
1235                continue 'candidate;
1236            }
1237        }
1238        kept.push(arc);
1239    }
1240
1241    // Pair arc ends at each branch point by tangent continuation: the
1242    // smooth curve runs straight through, the crossing one turns.
1243    let ends: Vec<(usize, bool, usize, Vector)> = kept
1244        .iter()
1245        .enumerate()
1246        .flat_map(|(i, arc)| {
1247            [(false, arc.head_bp), (true, arc.tail_bp)]
1248                .into_iter()
1249                .filter_map(move |(tail, bp)| Some((i, tail, bp?, arc.outgoing(tail)?)))
1250        })
1251        .collect();
1252    let mut partner: Vec<Option<usize>> = vec![None; ends.len()];
1253    for bp in 0..bps.len() {
1254        loop {
1255            let mut best: Option<(usize, usize, f64)> = None;
1256            for x in 0..ends.len() {
1257                if partner[x].is_some() || ends[x].2 != bp {
1258                    continue;
1259                }
1260                for y in (x + 1)..ends.len() {
1261                    if partner[y].is_some() || ends[y].2 != bp {
1262                        continue;
1263                    }
1264                    let score = -ends[x].3.dot(ends[y].3);
1265                    if score > CONTINUES && best.is_none_or(|(_, _, held)| score > held) {
1266                        best = Some((x, y, score));
1267                    }
1268                }
1269            }
1270            let Some((x, y, _)) = best else { break };
1271            partner[x] = Some(y);
1272            partner[y] = Some(x);
1273        }
1274    }
1275
1276    // Chain the arcs through the pairings into whole curves.
1277    let end_index = |arc: usize, tail: bool| -> Option<usize> {
1278        ends.iter().position(|e| e.0 == arc && e.1 == tail)
1279    };
1280    let mut used = vec![false; kept.len()];
1281    for start in 0..kept.len() {
1282        if used[start] {
1283            continue;
1284        }
1285        // Walk backwards first to a free entry, unless the chain loops.
1286        let mut first = start;
1287        let mut first_reversed = false;
1288        let mut seen_back = vec![false; kept.len()];
1289        loop {
1290            seen_back[first] = true;
1291            // Forward traversal enters an arc at its head, reversed at its
1292            // tail, so the entry end is named by the orientation flag.
1293            let Some(entry) = end_index(first, first_reversed) else {
1294                break;
1295            };
1296            let Some(p) = partner[entry] else { break };
1297            let (prev, prev_tail, _, _) = ends[p];
1298            if seen_back[prev] {
1299                break; // the chain is a loop; any start serves
1300            }
1301            first = prev;
1302            // The previous arc *leaves* through the paired end: leaving at
1303            // its tail means it runs forward.
1304            first_reversed = !prev_tail;
1305        }
1306
1307        // Now walk forwards from `first`, consuming arcs.
1308        let mut points: Vec<Point> = Vec::new();
1309        let mut on_a: Vec<(f64, f64)> = Vec::new();
1310        let mut on_b: Vec<(f64, f64)> = Vec::new();
1311        let mut current = first;
1312        let mut reversed = first_reversed;
1313        let mut closed = false;
1314        loop {
1315            used[current] = true;
1316            let arc = &kept[current];
1317            type Run = (Vec<Point>, Vec<(f64, f64)>, Vec<(f64, f64)>);
1318            let (pts, pa, pb): Run = if reversed {
1319                (
1320                    arc.points.iter().rev().copied().collect(),
1321                    arc.on_a.iter().rev().copied().collect(),
1322                    arc.on_b.iter().rev().copied().collect(),
1323                )
1324            } else {
1325                (arc.points.clone(), arc.on_a.clone(), arc.on_b.clone())
1326            };
1327            // Insert the branch point itself at the junction.
1328            if !points.is_empty() {
1329                let joint_bp = if reversed { arc.tail_bp } else { arc.head_bp };
1330                if let Some(bp) = joint_bp {
1331                    points.push(bps[bp]);
1332                    on_a.push(pa[0]);
1333                    on_b.push(pb[0]);
1334                }
1335            }
1336            points.extend(pts);
1337            on_a.extend(pa);
1338            on_b.extend(pb);
1339
1340            let leaving = end_index(current, !reversed);
1341            let Some(l) = leaving else { break };
1342            let Some(p) = partner[l] else { break };
1343            let (next, next_tail, _, _) = ends[p];
1344            if used[next] {
1345                closed = next == first;
1346                break;
1347            }
1348            current = next;
1349            reversed = next_tail;
1350        }
1351        if closed && points.len() > 3 {
1352            let bridge = points[0];
1353            let ba = on_a[0];
1354            let bb = on_b[0];
1355            points.push(bridge);
1356            on_a.push(ba);
1357            on_b.push(bb);
1358        }
1359        out.push(Traced {
1360            points,
1361            on_a,
1362            on_b,
1363            stopped: if closed {
1364                Stopped::Closed
1365            } else {
1366                Stopped::Stalled
1367            },
1368        });
1369    }
1370    out
1371}
1372
1373/// Newton projection of a point onto a surface, warm-started: the local
1374/// tool the tangential walker corrects with.
1375fn nearest_on(
1376    surface: &SurfaceGeometry,
1377    seed: (f64, f64),
1378    target: Point,
1379    tol: Tolerances,
1380) -> Option<((f64, f64), Point)> {
1381    let (mut u, mut v) = seed;
1382    for _ in 0..16 {
1383        let (u_ok, v_ok) = surface.normalize_parameters(u, v, tol).ok()?;
1384        u = u_ok;
1385        v = v_ok;
1386        let p = surface.point_at(u, v, tol).ok()?;
1387        let (su, sv) = surface.d1_at(u, v, tol).ok()?;
1388        let r = p - target;
1389        let (a11, a12, a22) = (su.dot(su), su.dot(sv), sv.dot(sv));
1390        let det = a11.mul_add(a22, -(a12 * a12));
1391        if det.abs() <= f64::MIN_POSITIVE {
1392            break;
1393        }
1394        let (b1, b2) = (-su.dot(r), -sv.dot(r));
1395        let du = b1.mul_add(a22, -(b2 * a12)) / det;
1396        let dv = a11.mul_add(b2, -(a12 * b1)) / det;
1397        u += du;
1398        v += dv;
1399        if du.hypot(dv) < 1e-14 {
1400            break;
1401        }
1402    }
1403    let (u, v) = surface.normalize_parameters(u, v, tol).ok()?;
1404    Some(((u, v), surface.point_at(u, v, tol).ok()?))
1405}
1406
1407/// Trace tangential contact along a curve: the walker an earlier plan
1408/// owed, following the valley of the gap function rather than a crossing.
1409///
1410/// Where two surfaces touch along a whole curve there is no transversal
1411/// direction to march: the crossing angle is zero along the entire
1412/// contact, and the crossing walker honestly stalls. But the contact is
1413/// still a curve, and it is the locus where the *gap* between the surfaces
1414/// stays zero. This walker steps along the contact and corrects each step
1415/// transversally: project the candidate onto the first surface, project
1416/// that onto the second, and slide on the first surface to close the gap:
1417/// a minimization, not a root-find, because at tangency the gap touches
1418/// zero without crossing it.
1419///
1420/// The seed must be a genuine contact: on both surfaces within tolerance
1421/// and near-tangent there. A transversal crossing is refused; the
1422/// ordinary walker owns those.
1423///
1424/// # Errors
1425///
1426/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if the
1427/// settings are unusable or the seed is not a tangential contact.
1428pub fn trace_tangential(
1429    a: &SurfaceGeometry,
1430    b: &SurfaceGeometry,
1431    from: Contact,
1432    options: Marching,
1433    tol: Tolerances,
1434) -> OgeomResult<Traced> {
1435    options.validate()?;
1436    let accept = tol.confusion() * 100.0;
1437    let sine = crossing_sine(a, b, from.on_a, from.on_b, tol);
1438    if sine > BRANCH_POINT_SINE {
1439        ogeom_bail!(
1440            Construction,
1441            "the surfaces cross here at sine {sine}; tangential tracing wants a contact"
1442        );
1443    }
1444    let reach = span(a).max(span(b));
1445    let step = (options.chord * reach)
1446        .sqrt()
1447        .clamp(tol.confusion(), reach / 16.0);
1448
1449    type Walked = (Vec<Point>, Vec<(f64, f64)>, Vec<(f64, f64)>, Stopped);
1450    let walk_one = |sense: f64| -> OgeomResult<Walked> {
1451        let mut points = vec![from.point];
1452        let mut on_a = vec![from.on_a];
1453        let mut on_b = vec![from.on_b];
1454        let mut at = from;
1455        let mut previous: Option<Vector> = None;
1456        let mut stopped = Stopped::RanOut;
1457        while points.len() < options.max_points {
1458            ogeom_core::progress::checkpoint()?;
1459            // The contact direction: in the common tangent plane. With the
1460            // normals parallel, one surface's normal serves for both; the
1461            // step direction is the previous one projected back into the
1462            // tangent plane, or any tangent direction to begin with.
1463            let Some(normal) = normal_at(a, at.on_a, tol) else {
1464                stopped = Stopped::Stalled;
1465                break;
1466            };
1467            let direction = match previous {
1468                Some(d) => {
1469                    let flat = d - normal * d.dot(normal);
1470                    let m = flat.magnitude();
1471                    if m <= f64::MIN_POSITIVE {
1472                        stopped = Stopped::Stalled;
1473                        break;
1474                    }
1475                    flat / m
1476                }
1477                None => {
1478                    // First step: the tangent direction along which the gap
1479                    // grows least, found by sampling the tangent circle.
1480                    let (su, _) = a.d1_at(at.on_a.0, at.on_a.1, tol).map_err(|_| {
1481                        ogeom_core::ogeom_err!(Construction, "the seed cannot be evaluated")
1482                    })?;
1483                    let t1 = {
1484                        let flat = su - normal * su.dot(normal);
1485                        let m = flat.magnitude();
1486                        if m <= f64::MIN_POSITIVE {
1487                            stopped = Stopped::Stalled;
1488                            break;
1489                        }
1490                        flat / m
1491                    };
1492                    let t2 = normal.cross(t1);
1493                    let mut best = (f64::INFINITY, t1);
1494                    for k in 0..16 {
1495                        let angle = core::f64::consts::TAU * f64::from(k) / 16.0;
1496                        let dir = t1 * angle.cos() + t2 * angle.sin();
1497                        let probe = at.point + dir * step;
1498                        let Some((_, qa)) = nearest_on(a, at.on_a, probe, tol) else {
1499                            continue;
1500                        };
1501                        let Some((_, qb)) = nearest_on(b, at.on_b, qa, tol) else {
1502                            continue;
1503                        };
1504                        let gap = qa.distance(qb);
1505                        if gap < best.0 {
1506                            best = (gap, dir);
1507                        }
1508                    }
1509                    best.1 * sense
1510                }
1511            };
1512
1513            // Step and correct: onto a, gap closed against b by sliding on
1514            // a a few times.
1515            let mut candidate = at.point + direction * step;
1516            let mut pa = at.on_a;
1517            let mut pb = at.on_b;
1518            let mut gap = f64::INFINITY;
1519            for _ in 0..8 {
1520                let Some((ua, qa)) = nearest_on(a, pa, candidate, tol) else {
1521                    break;
1522                };
1523                let Some((ub, qb)) = nearest_on(b, pb, qa, tol) else {
1524                    break;
1525                };
1526                pa = ua;
1527                pb = ub;
1528                gap = qa.distance(qb);
1529                if gap <= tol.confusion() {
1530                    candidate = qa;
1531                    break;
1532                }
1533                // Slide the working point toward the midpoint of the gap.
1534                candidate = qa + (qb - qa) * 0.5;
1535            }
1536            if gap > accept {
1537                stopped = Stopped::Stalled;
1538                break;
1539            }
1540            let next = Contact {
1541                on_a: pa,
1542                on_b: pb,
1543                point: candidate,
1544            };
1545            if points.len() > 3 && next.point.distance(from.point) <= step {
1546                points.push(from.point);
1547                on_a.push(from.on_a);
1548                on_b.push(from.on_b);
1549                stopped = Stopped::Closed;
1550                break;
1551            }
1552            if next.point.distance(at.point) <= step * 1e-3 {
1553                stopped = Stopped::Stalled;
1554                break;
1555            }
1556            previous = Some(next.point - at.point);
1557            points.push(next.point);
1558            on_a.push(next.on_a);
1559            on_b.push(next.on_b);
1560            at = next;
1561        }
1562        Ok((points, on_a, on_b, stopped))
1563    };
1564
1565    let (points, on_a, on_b, stopped) = walk_one(1.0)?;
1566    if stopped == Stopped::Closed {
1567        return Ok(Traced {
1568            points,
1569            on_a,
1570            on_b,
1571            stopped,
1572        });
1573    }
1574    let (mut back_points, mut back_a, mut back_b, back_stopped) = walk_one(-1.0)?;
1575    back_points.reverse();
1576    back_a.reverse();
1577    back_b.reverse();
1578    back_points.pop();
1579    back_a.pop();
1580    back_b.pop();
1581    back_points.extend(points);
1582    back_a.extend(on_a);
1583    back_b.extend(on_b);
1584    let stopped = if stopped == Stopped::RanOut || back_stopped == Stopped::RanOut {
1585        Stopped::RanOut
1586    } else if stopped == Stopped::Stalled || back_stopped == Stopped::Stalled {
1587        Stopped::Stalled
1588    } else {
1589        Stopped::LeftTheDomain
1590    };
1591    Ok(Traced {
1592        points: back_points,
1593        on_a: back_a,
1594        on_b: back_b,
1595        stopped,
1596    })
1597}
1598
1599#[cfg(test)]
1600#[allow(clippy::unwrap_used, clippy::print_stdout)]
1601mod tests {
1602    use super::*;
1603    use ogeom_geom::{CylinderSurface, PlaneSurface, SphereSurface};
1604    use ogeom_math::{Cylinder, Direction, Frame, Plane, Sphere};
1605
1606    const T: Tolerances = Tolerances::millimetres();
1607
1608    fn cylinder(origin: Point, axis: Vector, radius: f64, height: (f64, f64)) -> SurfaceGeometry {
1609        let frame = Frame::new(
1610            origin,
1611            Direction::new(axis, T).unwrap(),
1612            Direction::from_cross(axis, Vector::new(0.3, 0.5, 0.9), T).unwrap(),
1613            T,
1614        )
1615        .unwrap();
1616        CylinderSurface::new(Cylinder::new(frame, radius, T).unwrap(), height)
1617            .unwrap()
1618            .into()
1619    }
1620
1621    fn sphere(centre: Point, radius: f64) -> SurfaceGeometry {
1622        SphereSurface::new(Sphere::centred(centre, radius, T).unwrap()).into()
1623    }
1624
1625    fn plane(origin: Point, normal: Vector) -> SurfaceGeometry {
1626        PlaneSurface::over(
1627            Plane::through(origin, Direction::new(normal, T).unwrap()),
1628            (-8.0, 8.0),
1629            (-8.0, 8.0),
1630        )
1631        .unwrap()
1632        .into()
1633    }
1634
1635    /// How far a point is from a quadric, in closed form.
1636    fn off(surface: &SurfaceGeometry, p: Point) -> f64 {
1637        match surface {
1638            SurfaceGeometry::Plane(x) => x.plane().distance_to(p),
1639            SurfaceGeometry::Sphere(x) => x.sphere().distance_to(p),
1640            SurfaceGeometry::Cylinder(x) => x.cylinder().distance_to(p),
1641            _ => 0.0,
1642        }
1643    }
1644
1645    /// The worst distance from a traced branch to either surface.
1646    fn deviation(a: &SurfaceGeometry, b: &SurfaceGeometry, traced: &Traced) -> f64 {
1647        traced
1648            .points
1649            .iter()
1650            .map(|p| off(a, *p).abs().max(off(b, *p).abs()))
1651            .fold(0.0_f64, f64::max)
1652    }
1653
1654    #[test]
1655    fn a_plane_through_a_bent_strip_seeds_both_branches() {
1656        // A cubic strip bent into an arch crosses a level plane twice. The
1657        // plane's domain is the unbounded one a face carries, and its
1658        // sampling cells span a million units; merging seeds at *its*
1659        // spacing would call the two crossings one branch and trace only
1660        // one of them. Seeds merge at the finer surface's spacing instead.
1661        use ogeom_geom::BSplineSurface;
1662        use ogeom_math::{ControlGrid, KnotVector};
1663        let mut points = Vec::new();
1664        for i in 0..7 {
1665            let a = core::f64::consts::PI * f64::from(i) / 6.0;
1666            for j in 0..2 {
1667                points.push(Point::new(2.0 * a.cos(), f64::from(j), 2.0 * a.sin()));
1668            }
1669        }
1670        let grid = ControlGrid::new(points, 7, 2).unwrap();
1671        let strip: SurfaceGeometry = BSplineSurface::new(
1672            KnotVector::clamped_uniform(3, 7).unwrap(),
1673            KnotVector::clamped_uniform(1, 2).unwrap(),
1674            &grid,
1675            T,
1676        )
1677        .unwrap()
1678        .into();
1679        let level: SurfaceGeometry = PlaneSurface::over(
1680            Plane::through(Point::new(0.0, 0.0, 1.0), Direction::Z),
1681            (-1.0e9, 1.0e9),
1682            (-1.0e9, 1.0e9),
1683        )
1684        .unwrap()
1685        .into();
1686        let options = Marching {
1687            chord: 1e-5,
1688            ..Marching::default()
1689        };
1690        let found = branches(&strip, &level, options, T).unwrap();
1691        assert_eq!(
1692            found.len(),
1693            2,
1694            "the arch crosses the level twice: {}",
1695            found.len()
1696        );
1697        for branch in &found {
1698            assert!(!branch.closed());
1699            for p in &branch.points {
1700                assert!((p.z - 1.0).abs() < 1e-4, "on the level: {p:?}");
1701            }
1702        }
1703    }
1704
1705    #[test]
1706    fn two_crossed_cylinders_are_traced_onto_both_of_them() {
1707        // The case with no closed form, and the one the analytic module
1708        // explicitly refuses: two cylinders on perpendicular axes meet in a
1709        // quartic space curve. Every point of the trace must be on both.
1710        let a = cylinder(Point::ORIGIN, Vector::Z, 1.0, (-4.0, 4.0));
1711        let b = cylinder(Point::ORIGIN, Vector::X, 1.0, (-4.0, 4.0));
1712        let options = Marching {
1713            chord: 1e-5,
1714            ..Marching::default()
1715        };
1716
1717        let found = branches(&a, &b, options, T).unwrap();
1718        assert_eq!(
1719            found.len(),
1720            2,
1721            "two equal cylinders crossing at right angles meet in two closed \
1722             curves: the Steinmetz solid's seams"
1723        );
1724
1725        let mut worst = 0.0_f64;
1726        for branch in &found {
1727            assert!(branch.closed(), "each seam is a closed loop");
1728            assert!(
1729                branch.points.len() > 100,
1730                "a branch of only {} points",
1731                branch.points.len()
1732            );
1733            worst = worst.max(deviation(&a, &b, branch));
1734        }
1735        println!(
1736            "crossed cylinders: {} branches, worst deviation {worst:e}",
1737            found.len()
1738        );
1739        assert!(worst < 1e-7, "traced off the surfaces by {worst:e}");
1740    }
1741
1742    #[test]
1743    fn unequal_crossed_cylinders_meet_in_two_curves_as_well() {
1744        // The non-degenerate cousin. Equal radii put the two curves through
1745        // each other at the tangency points, so getting the count right there
1746        // says less than getting it right here, where there is no singularity
1747        // for a tracer to be lucky about.
1748        let a = cylinder(Point::ORIGIN, Vector::Z, 1.0, (-4.0, 4.0));
1749        let b = cylinder(Point::ORIGIN, Vector::X, 1.6, (-4.0, 4.0));
1750        let options = Marching {
1751            chord: 1e-5,
1752            ..Marching::default()
1753        };
1754
1755        let found = branches(&a, &b, options, T).unwrap();
1756        assert_eq!(found.len(), 2);
1757        for branch in &found {
1758            assert!(branch.closed());
1759            assert!(deviation(&a, &b, branch) < 1e-7);
1760        }
1761    }
1762
1763    #[test]
1764    fn a_traced_circle_agrees_with_the_circle_it_should_be() {
1765        // A sphere cut by a plane through its centre is a circle of known
1766        // radius, and the marcher does not know that. Tracing it and checking
1767        // against the closed form is the strongest single check there is: it
1768        // tests the tracer against an answer derived independently of it.
1769        let s = sphere(Point::ORIGIN, 3.0);
1770        let cut = plane(Point::ORIGIN, Vector::Z);
1771        let options = Marching {
1772            chord: 1e-6,
1773            ..Marching::default()
1774        };
1775
1776        let found = seeds(&s, &cut, options, T).unwrap();
1777        assert!(!found.is_empty());
1778        let branch = trace(&s, &cut, found[0], options, T).unwrap();
1779
1780        assert!(
1781            branch.closed(),
1782            "a plane through a sphere gives a closed loop"
1783        );
1784        for p in &branch.points {
1785            let radius = (p.x * p.x + p.y * p.y).sqrt();
1786            assert!(
1787                (radius - 3.0).abs() < 1e-7,
1788                "a point at radius {radius} on a circle of 3"
1789            );
1790            assert!(p.z.abs() < 1e-7, "off the cutting plane by {}", p.z);
1791        }
1792    }
1793
1794    #[test]
1795    fn a_branch_that_leaves_the_surface_says_so_rather_than_stopping_quietly() {
1796        // A truncated branch and a finished one are different answers, and a
1797        // caller that cannot tell them apart treats the first as the second.
1798        let s = sphere(Point::ORIGIN, 3.0);
1799        let cut = plane(Point::new(0.0, 0.0, 0.0), Vector::Z);
1800        let options = Marching {
1801            chord: 1e-4,
1802            max_points: 8,
1803            ..Marching::default()
1804        };
1805        let found = seeds(&s, &cut, options, T).unwrap();
1806        let branch = trace(&s, &cut, found[0], options, T).unwrap();
1807        assert_eq!(branch.stopped, Stopped::RanOut);
1808        assert!(!branch.complete(), "a truncated branch is not complete");
1809    }
1810
1811    #[test]
1812    fn tangent_surfaces_are_refused_rather_than_followed_onto_a_guess() {
1813        // Where the normals are parallel the intersection has no single
1814        // direction, and marching through such a point is how a tracer changes
1815        // branch without noticing. A sphere resting on a plane is the case.
1816        let s = sphere(Point::new(0.0, 0.0, 3.0), 3.0);
1817        let ground = plane(Point::ORIGIN, Vector::Z);
1818        let touch = Contact {
1819            on_a: (0.0, -core::f64::consts::FRAC_PI_2),
1820            on_b: (0.0, 0.0),
1821            point: Point::ORIGIN,
1822        };
1823        let err = trace(&s, &ground, touch, Marching::default(), T).unwrap_err();
1824        assert!(err.to_string().contains("tangent"), "unexpected: {err}");
1825    }
1826
1827    #[test]
1828    fn the_number_of_branches_is_the_number_there_are() {
1829        // The failure the accuracy measure cannot see. Every point of one
1830        // circle is on both surfaces, so returning one of two scores perfectly;
1831        // the count is the only thing that catches it.
1832        let options = Marching {
1833            chord: 1e-5,
1834            ..Marching::default()
1835        };
1836
1837        // A sphere cut by a plane off its centre: one circle.
1838        let one = branches(
1839            &sphere(Point::ORIGIN, 3.0),
1840            &plane(Point::new(0.0, 0.0, 1.0), Vector::Z),
1841            options,
1842            T,
1843        )
1844        .unwrap();
1845        assert_eq!(one.len(), 1, "one plane through a sphere cuts one circle");
1846        assert!(one[0].closed());
1847
1848        // A sphere and a coaxial cylinder narrower than it: two circles, one
1849        // above and one below.
1850        let two = branches(
1851            &sphere(Point::ORIGIN, 3.0),
1852            &cylinder(Point::ORIGIN, Vector::Z, 1.5, (-4.0, 4.0)),
1853            options,
1854            T,
1855        )
1856        .unwrap();
1857        assert_eq!(two.len(), 2, "a coaxial cylinder cuts a sphere twice");
1858        for branch in &two {
1859            assert!(branch.closed(), "each is a closed circle");
1860        }
1861        // And they are on opposite sides, rather than the same one twice.
1862        let heights: Vec<f64> = two.iter().map(|b| b.points[0].z).collect();
1863        assert!(
1864            heights[0] * heights[1] < 0.0,
1865            "both branches came back on the same side: {heights:?}"
1866        );
1867    }
1868
1869    #[test]
1870    fn a_branch_thinner_than_the_sampling_is_missed_and_the_knob_finds_it() {
1871        // The stated limitation of polyhedral seeding, pinned so it is a known
1872        // boundary rather than a surprise. Two spheres barely overlapping meet
1873        // in a small circle; a coarse grid steps over it entirely.
1874        let a = sphere(Point::ORIGIN, 3.0);
1875        let b = sphere(Point::new(5.98, 0.0, 0.0), 3.0);
1876
1877        let coarse = seeds(
1878            &a,
1879            &b,
1880            Marching {
1881                grid: 6,
1882                ..Marching::default()
1883            },
1884            T,
1885        )
1886        .unwrap();
1887        let fine = seeds(
1888            &a,
1889            &b,
1890            Marching {
1891                grid: 120,
1892                ..Marching::default()
1893            },
1894            T,
1895        )
1896        .unwrap();
1897        assert!(
1898            coarse.len() < fine.len(),
1899            "a finer grid should find what a coarse one steps over: {} against {}",
1900            coarse.len(),
1901            fine.len()
1902        );
1903        assert!(!fine.is_empty(), "the branch is there to be found");
1904    }
1905
1906    #[test]
1907    fn settings_that_could_not_work_are_refused() {
1908        let a = sphere(Point::ORIGIN, 1.0);
1909        let b = plane(Point::ORIGIN, Vector::Z);
1910        for options in [
1911            Marching {
1912                chord: 0.0,
1913                ..Marching::default()
1914            },
1915            Marching {
1916                grid: 1,
1917                ..Marching::default()
1918            },
1919            Marching {
1920                max_points: 1,
1921                ..Marching::default()
1922            },
1923        ] {
1924            assert!(seeds(&a, &b, options, T).is_err());
1925        }
1926    }
1927}