Skip to main content

axiolid_nurbs/
implicit_ops.rs

1//! Working with section curves through their implicit form (ADR 0077):
2//! pcurves of a section on any analytic face, and a curve of the section
3//! families against a surface.
4//!
5//! Every section family is a field's zero set on a carrier: an
6//! `ImplicitSection3` is one by definition. A ruled section (ADR 0076) is
7//! the root branch of `a v^2 + b v + c = 0` on its ruled carrier, and a
8//! torus section is where `A cos u + B sin u = C` on its torus. Each is
9//! traced once in its own carrier's parameters. The stretch that matches
10//! the curve is cut out by its end points, so the operations below have one
11//! implementation for all of them.
12
13use axiolid_core::{Interval, Point2, Scalar, Vec2};
14use axiolid_curve::implicit::Cell;
15use axiolid_curve::{
16    Axis, Basis, Carrier, Curve3, Field2, ImplicitCurve2, ImplicitSection3, SeriesField2, Trig2,
17};
18use axiolid_surface::Surface;
19use core::f64::consts::{PI, TAU};
20
21use crate::curve_tower::CurveTower;
22use crate::exact_curve_intersection::{
23    ExactCurveHit, ExactCurveIntersection, ExactCurveParameter, ExactCurveRefusal, Isolated,
24};
25use crate::exact_surface_intersection::ExactIntersectionRefusal;
26use crate::field::{carrier_of, section_field};
27use crate::implicit_trace::Periodic;
28use crate::root_chain::{isolate, Budget, End, RootRefusal, Zone, ROOT_BUDGET};
29
30/// Every component of the section of `surface` by `other` in a window of
31/// `surface`'s parameters, as curves in those parameters.
32///
33/// A window a whole turn wide in a periodic parameter wraps.
34///
35/// # Errors
36///
37/// As [`crate::implicit_surface_intersection`]; `Disjoint` is not an
38/// error here (the list is empty).
39pub fn trace_section_pcurves(
40    surface: &Surface,
41    other: &Surface,
42    window: (Point2, Point2),
43) -> Result<Vec<ImplicitCurve2>, ExactIntersectionRefusal> {
44    let carrier = carrier_of(surface).ok_or(ExactIntersectionRefusal::UnsupportedPair)?;
45    let field = section_field(&carrier, other).ok_or(ExactIntersectionRefusal::UnsupportedPair)?;
46    // A quarter of a section's budget: the section itself is traced
47    // already, and a face without these pcurves reads it back instead.
48    trace_field(&carrier, &field, window, crate::implicit_trace::BUDGET / 4)
49}
50
51fn trace_field(
52    carrier: &Carrier,
53    field: &Field2,
54    window: (Point2, Point2),
55    budget: usize,
56) -> Result<Vec<ImplicitCurve2>, ExactIntersectionRefusal> {
57    let (pu, pv) = carrier.periodic();
58    let (lo, hi) = window;
59    let wraps = |periodic: bool, a: Scalar, b: Scalar| {
60        periodic && ((b - a) - TAU).abs() <= 1e-12 * (1.0 + a.abs())
61    };
62    let periodic = Periodic {
63        u: wraps(pu, lo.x, hi.x),
64        v: wraps(pv, lo.y, hi.y),
65    };
66    crate::implicit_trace::trace_within(field, Cell { lo, hi }, periodic, budget)
67        .map_err(crate::implicit_section::refusal_of)
68}
69
70/// The stretch of one of `curves` that runs from `start` through `first`
71/// and then `second` to `end` (all in parameters, each possibly a whole
72/// period off), as a curve of its own running from `start` to `end`.
73/// `closed` asks for a whole loop from `start` back to itself, in the
74/// direction that meets `first` before `second`.
75#[must_use]
76pub fn extract_stretch(
77    curves: &[ImplicitCurve2],
78    periodic: (bool, bool),
79    start: Point2,
80    [first, second]: [Point2; 2],
81    end: Point2,
82    closed: bool,
83) -> Option<ImplicitCurve2> {
84    let shifts = |p: Point2| -> Vec<Point2> {
85        let ku: &[Scalar] = if periodic.0 {
86            &[0.0, -1.0, 1.0, -2.0, 2.0]
87        } else {
88            &[0.0]
89        };
90        let kv: &[Scalar] = if periodic.1 {
91            &[0.0, -1.0, 1.0, -2.0, 2.0]
92        } else {
93            &[0.0]
94        };
95        let mut out = Vec::new();
96        for a in ku {
97            for b in kv {
98                out.push(Point2::new(p.x + a * TAU, p.y + b * TAU));
99            }
100        }
101        out
102    };
103    let find = |curve: &ImplicitCurve2, p: Point2| -> Option<Scalar> {
104        let eps = 1e-7 * (1.0 + p.x.abs().max(p.y.abs()));
105        let mut best: Option<(Scalar, Scalar)> = None;
106        for q in shifts(p) {
107            let Some(t) = curve.parameter_of(q) else {
108                continue;
109            };
110            let Some(on) = curve.point(t) else {
111                continue;
112            };
113            // Compare up to whole periods.
114            let mut d = on - q;
115            if periodic.0 {
116                d.x -= (d.x / TAU).round() * TAU;
117            }
118            if periodic.1 {
119                d.y -= (d.y / TAU).round() * TAU;
120            }
121            let miss = d.length();
122            if miss <= eps && best.is_none_or(|(m, _)| miss < m) {
123                best = Some((miss, t));
124            }
125        }
126        best.map(|(_, t)| t)
127    };
128    for curve in curves {
129        let (Some(ts), Some(t1), Some(t2), Some(te)) = (
130            find(curve, start),
131            find(curve, first),
132            find(curve, second),
133            find(curve, end),
134        ) else {
135            continue;
136        };
137        let n = curve.end();
138        let closure = curve.closure(periodic.0, periodic.1);
139        let ahead = |t: Scalar| (t - ts).rem_euclid(n);
140        let stretch = if closed {
141            // The whole loop from `start` round to itself.
142            let whole = curve.rotated(ts, closure?)?;
143            if ahead(t1) < ahead(t2) {
144                whole
145            } else {
146                whole.reversed()
147            }
148        } else if let Some(closure) = closure {
149            // On a loop: forward if the inner points come in order before
150            // `end` going forward from `start`.
151            if ahead(t1) < ahead(t2) && ahead(t2) < ahead(te) {
152                curve.sub(ts, te, Some(closure))?
153            } else {
154                curve.sub(te, ts, Some(closure))?.reversed()
155            }
156        } else if ts <= t1 && t1 <= t2 && t2 <= te {
157            curve.sub(ts, te, None)?
158        } else if te <= t2 && t2 <= t1 && t1 <= ts {
159            curve.sub(te, ts, None)?.reversed()
160        } else {
161            continue;
162        };
163        return Some(stretch);
164    }
165    None
166}
167
168/// The carrier and defining field of a section family, or `None` for
169/// other curves.
170fn defining_field(curve: &Curve3) -> Option<(Carrier, Field2)> {
171    let fourier = |t: &Trig2| vec![t.constant, t.cos, t.sin, t.cos2, t.sin2];
172    match curve {
173        Curve3::ImplicitSection(s) => Some((s.carrier.clone(), s.curve.field.clone())),
174        Curve3::RuledSection(r) => {
175            // c(u) + b(u) v + a(u) v^2, u harmonic, v a power.
176            let (a, b, c) = (
177                fourier(&r.graph.a),
178                fourier(&r.graph.b),
179                fourier(&r.graph.c),
180            );
181            let coefficients = (0..5).map(|i| vec![c[i], b[i], a[i]]).collect();
182            Some((
183                Carrier::Ruled(r.carrier),
184                Field2::Series(SeriesField2 {
185                    u: Basis::Fourier,
186                    v: Basis::Power,
187                    coefficients,
188                }),
189            ))
190        }
191        Curve3::TorusSection(t) => {
192            // A(v) cos u + B(v) sin u - C(v), both harmonic.
193            let (a, b, c) = (
194                fourier(&t.graph.a),
195                fourier(&t.graph.b),
196                fourier(&t.graph.c),
197            );
198            let coefficients = vec![c.iter().map(|x| -x).collect(), a, b];
199            Some((
200                Carrier::Torus(t.torus),
201                Field2::Series(SeriesField2 {
202                    u: Basis::Fourier,
203                    v: Basis::Fourier,
204                    coefficients,
205                }),
206            ))
207        }
208        _ => None,
209    }
210}
211
212/// A section-family curve over `span` as an [`ImplicitSection3`], with the
213/// same point set and direction; `None` for other families or where the
214/// trace is refused.
215#[must_use]
216pub fn implicit_view(curve: &Curve3, span: Interval) -> Option<ImplicitSection3> {
217    if let Curve3::ImplicitSection(s) = curve {
218        let closed = (span.end - span.start).abs() >= s.curve.end() - 1e-12;
219        let sub = if closed {
220            s.curve.clone()
221        } else {
222            let (a, b) = (span.start.min(span.end), span.start.max(span.end));
223            let piece = s.curve.sub(a, b, None)?;
224            if span.end < span.start {
225                piece.reversed()
226            } else {
227                piece
228            }
229        };
230        return Some(ImplicitSection3 {
231            carrier: s.carrier.clone(),
232            curve: sub,
233        });
234    }
235    let (carrier, field) = defining_field(curve)?;
236    let at = |t: Scalar| -> Option<Point2> {
237        let p = axiolid_evaluate::evaluate3(curve, t).ok()?;
238        let (u, v) = carrier.parameters(p);
239        Some(Point2::new(u, v))
240    };
241    // Carrier parameters along the span, unwrapped, to size the window.
242    let n = 64;
243    let mut samples = Vec::with_capacity(n + 1);
244    for i in 0..=n {
245        let t = span.start + (span.end - span.start) * i as Scalar / n as Scalar;
246        let mut p = at(t)?;
247        if let Some(q) = samples.last().copied() {
248            let q: Point2 = q;
249            let (pu, pv) = carrier.periodic();
250            if pu {
251                p.x += ((q.x - p.x) / TAU).round() * TAU;
252            }
253            if pv {
254                p.y += ((q.y - p.y) / TAU).round() * TAU;
255            }
256        }
257        samples.push(p);
258    }
259    let (mut lo, mut hi) = (samples[0], samples[0]);
260    for p in &samples {
261        lo = lo.min(*p);
262        hi = hi.max(*p);
263    }
264    let pad = (hi - lo) * 0.25 + Vec2::splat(0.05);
265    let (mut lo, mut hi) = (lo - pad, hi + pad);
266    // Never more than a turn in a periodic parameter.
267    let (pu, pv) = carrier.periodic();
268    if pu && hi.x - lo.x >= TAU {
269        let c = 0.5 * (lo.x + hi.x);
270        (lo.x, hi.x) = (c - PI, c + PI);
271    }
272    if pv && hi.y - lo.y >= TAU {
273        let c = 0.5 * (lo.y + hi.y);
274        (lo.y, hi.y) = (c - PI, c + PI);
275    }
276    let curves = trace_field(&carrier, &field, (lo, hi), crate::implicit_trace::BUDGET).ok()?;
277    // A loop closes in space, whatever whole turns its parameters make.
278    let (first, last) = (
279        axiolid_evaluate::evaluate3(curve, span.start).ok()?,
280        axiolid_evaluate::evaluate3(curve, span.end).ok()?,
281    );
282    let closed = (first - last).length() <= 1e-9 * (1.0 + first.length());
283    let stretch = extract_stretch(
284        &curves,
285        (pu, pv),
286        samples[0],
287        [samples[n / 3], samples[2 * n / 3]],
288        samples[n],
289        closed,
290    )?;
291    Some(ImplicitSection3 {
292        carrier,
293        curve: stretch,
294    })
295}
296
297/// Where a section-family curve meets a surface, with parameters on the
298/// curve itself.
299///
300/// The curve is read as an implicit curve on its carrier (over `span`, or
301/// its whole traced extent for an [`ImplicitSection3`]); the surface's
302/// equation on that carrier is a second field, and its roots along the
303/// curve are isolated with interval bounds by a Rolle chain on the
304/// derivatives along the curve (ADR 0077). A root where the second field
305/// changes sign is a crossing (multiplicity 1); a root of higher
306/// multiplicity, up to eight, is a touch or a flat crossing and reports its
307/// order; a curve on which the second field vanishes identically is
308/// `Contained`.
309///
310/// # Errors
311///
312/// `UnsupportedCurve` for a curve that is not a section family or whose
313/// trace was refused, `UnsupportedSurface` for a B-spline surface,
314/// `Undecided` where the roots could not all be isolated with their
315/// multiplicity.
316pub fn section_curve_surface_intersection(
317    curve: &Curve3,
318    span: Interval,
319    surface: &Surface,
320) -> Result<ExactCurveIntersection, ExactCurveRefusal> {
321    let view = implicit_view(curve, span).ok_or(ExactCurveRefusal::UnsupportedCurve)?;
322    let other =
323        section_field(&view.carrier, surface).ok_or(ExactCurveRefusal::UnsupportedSurface)?;
324    let own = &view.curve;
325    // Contained: the second field vanishes along the whole curve, to the
326    // rounding of its own size.
327    let scale = 1e-9 * other.magnitude().max(1.0);
328    let samples: Vec<Scalar> = (0..=32)
329        .filter_map(|i| {
330            let p = own.point(own.end() * i as Scalar / 32.0)?;
331            Some(other.value(p))
332        })
333        .collect();
334    if samples.len() == 33 && samples.iter().all(|h| h.abs() <= scale) {
335        return Ok(ExactCurveIntersection::Contained);
336    }
337    let (pu, pv) = view.carrier.periodic();
338    let closed = own.closure(pu, pv).is_some();
339    let roots = roots_along(own, &other, closed).map_err(|_| ExactCurveRefusal::Undecided)?;
340    let mut hits = Vec::new();
341    for (t, multiplicity) in roots {
342        let Some(p) = own.point(t) else { continue };
343        let point = view.carrier.jet(p.x, p.y).point;
344        // Back to the curve's own parameter.
345        let parameter = match curve {
346            Curve3::ImplicitSection(_) => {
347                if span.end < span.start {
348                    span.start - t
349                } else {
350                    span.start + t
351                }
352            }
353            _ => {
354                match axiolid_evaluate::curve::invert3(curve, point, axiolid_core::Tolerance::METRE)
355                {
356                    Ok(x) => nearest_turn(x, span),
357                    Err(_) => continue,
358                }
359            }
360        };
361        hits.push(ExactCurveHit {
362            parameter: ExactCurveParameter::Certified(Isolated::new(parameter)),
363            multiplicity,
364            point,
365        });
366    }
367    Ok(ExactCurveIntersection::Points(hits))
368}
369
370/// The roots of `other` along an implicit curve, by the curve's parameter,
371/// each with its multiplicity, ascending: 1 where the field changes sign,
372/// more where it touches zero or crosses it with a flat contact.
373///
374/// The stretches of consecutive cells are isolated by a Rolle chain on the
375/// intrinsic derivatives along the curve ([`crate::root_chain`],
376/// [`crate::curve_tower`]); a bridge cell is scanned for sign changes (its
377/// roots are not certified). A `closed` curve's start and end are one
378/// point, so a root there is reported once.
379///
380/// # Errors
381///
382/// The refusal of the isolation: a contact above order eight, one hidden by
383/// rounding, or an exhausted work budget.
384pub(crate) fn roots_along(
385    own: &ImplicitCurve2,
386    other: &Field2,
387    closed: bool,
388) -> Result<Vec<(Scalar, usize)>, RootRefusal> {
389    roots_along_within(own, other, closed, ROOT_BUDGET)
390}
391
392/// [`roots_along`] within a work budget of `budget` enclosures.
393pub(crate) fn roots_along_within(
394    own: &ImplicitCurve2,
395    other: &Field2,
396    closed: bool,
397    budget: usize,
398) -> Result<Vec<(Scalar, usize)>, RootRefusal> {
399    let tower = CurveTower::new(own, other);
400    let mut budget = Budget::new(budget);
401    let cells = own.cells.len();
402    let mut zones: Vec<Zone> = Vec::new();
403    let mut index = 0;
404    while index < cells {
405        if own.cells[index].bridge.is_some() {
406            for (t, m) in scan_bridge(own, other, index) {
407                zones.push(Zone {
408                    lo: t,
409                    hi: t,
410                    at: t,
411                    m,
412                    end: None,
413                });
414            }
415            index += 1;
416            continue;
417        }
418        let start = index;
419        while index < cells && own.cells[index].bridge.is_none() {
420            index += 1;
421        }
422        let ends = [
423            if start == 0 { End::Curve } else { End::Inner },
424            if index == cells {
425                End::Curve
426            } else {
427                End::Inner
428            },
429        ];
430        zones.extend(isolate(
431            &tower,
432            start as Scalar,
433            index as Scalar,
434            ends,
435            &mut budget,
436        )?);
437    }
438    zones.sort_by(|a, b| a.at.total_cmp(&b.at));
439    // A loop's seam is one point, met at both ends of the parameter.
440    if closed && zones.len() > 1 {
441        let (first, last) = (zones[0], zones[zones.len() - 1]);
442        if first.lo == 0.0 && last.hi == cells as Scalar {
443            zones.pop();
444            zones[0].m = first.m.max(last.m);
445        }
446    }
447    let roots: Vec<(Scalar, usize)> = zones.iter().map(|z| (z.at, z.m)).collect();
448    debug_assert!(roots.windows(2).all(|w| w[0].0 < w[1].0));
449    Ok(roots)
450}
451
452/// The sign changes of `other` along a bridge cell, by 64 samples. A
453/// bridge into a crossing is too short and too close to the singular point
454/// for bounds, so its roots are scanned and not certified.
455// TODO(#441): certify the roots along bridge cells.
456fn scan_bridge(own: &ImplicitCurve2, other: &Field2, index: usize) -> Vec<(Scalar, usize)> {
457    let cell = &own.cells[index];
458    let h = |s: Scalar| -> Option<Scalar> {
459        let free = cell.from + (cell.to - cell.from) * s;
460        let solved = own.solve_cell(cell, free)?;
461        let p = match cell.axis {
462            Axis::U => Point2::new(free, solved),
463            Axis::V => Point2::new(solved, free),
464        };
465        Some(other.value(p))
466    };
467    let mut out = Vec::new();
468    let n = 64;
469    let at = |k: usize| k as Scalar / n as Scalar;
470    let mut last = h(at(0));
471    for k in 1..=n {
472        let now = h(at(k));
473        if let (Some(a), Some(b)) = (last, now) {
474            if a == 0.0 && k == 1 {
475                out.push((at(0), 1));
476            } else if (a < 0.0) != (b < 0.0) && b != 0.0 {
477                out.push((bisect_bridge(&h, at(k - 1), at(k), a), 1));
478            } else if b == 0.0 {
479                out.push((at(k), 1));
480            }
481        }
482        last = now;
483    }
484    let mut roots: Vec<(Scalar, usize)> = out
485        .into_iter()
486        .map(|(s, m)| (index as Scalar + s, m))
487        .collect();
488    roots.dedup_by(|a, b| (a.0 - b.0).abs() <= 1e-9);
489    roots
490}
491
492fn bisect_bridge(
493    h: &dyn Fn(Scalar) -> Option<Scalar>,
494    mut s0: Scalar,
495    mut s1: Scalar,
496    a: Scalar,
497) -> Scalar {
498    let negative = a < 0.0;
499    for _ in 0..100 {
500        let m = 0.5 * (s0 + s1);
501        if m <= s0 || m >= s1 {
502            break;
503        }
504        match h(m) {
505            Some(0.0) => return m,
506            Some(v) if (v < 0.0) == negative => s0 = m,
507            Some(_) => s1 = m,
508            None => break,
509        }
510    }
511    0.5 * (s0 + s1)
512}
513
514/// `x` moved by whole turns into `span` when that is possible.
515fn nearest_turn(x: Scalar, span: Interval) -> Scalar {
516    let (lo, hi) = (span.start.min(span.end), span.start.max(span.end));
517    for k in [0.0, 1.0, -1.0, 2.0, -2.0] {
518        let y = x + k * TAU;
519        if y >= lo - 1e-9 && y <= hi + 1e-9 {
520            return y;
521        }
522    }
523    x
524}
525
526/// A line, circle or ellipse against a B-spline surface: the curve is the
527/// meeting of two analytic surfaces (a line of two planes, a circle of its
528/// plane and a sphere, an ellipse of its plane and an elliptical cylinder).
529/// The first is traced on the spline (certified, over its Bernstein
530/// patches); the second's roots along each traced piece are the hits.
531///
532/// # Errors
533///
534/// `UnsupportedCurve` for other families, `UnsupportedSurface` for a
535/// surface that is not a B-spline or a spline whose trace is refused.
536pub fn conic_spline_intersection(
537    curve: &Curve3,
538    surface: &Surface,
539) -> Result<ExactCurveIntersection, ExactCurveRefusal> {
540    use axiolid_core::Frame3;
541    use axiolid_surface::{EllipticalCylinder, Plane, Sphere};
542    if !matches!(surface, Surface::BSpline(_)) {
543        return Err(ExactCurveRefusal::UnsupportedSurface);
544    }
545    let plane = |origin: axiolid_core::Point3, z: axiolid_core::Vec3| -> Surface {
546        let z = z.normalize();
547        let helper = if z.x.abs() < 0.9 {
548            axiolid_core::Vec3::X
549        } else {
550            axiolid_core::Vec3::Y
551        };
552        let x = helper.cross(z).normalize();
553        Surface::Plane(Plane {
554            frame: Frame3 {
555                origin,
556                x,
557                y: z.cross(x),
558                z,
559            },
560        })
561    };
562    let (first, second) = match curve {
563        Curve3::Line(l) => {
564            let d = l.direction.normalize();
565            let helper = if d.x.abs() < 0.9 {
566                axiolid_core::Vec3::X
567            } else {
568                axiolid_core::Vec3::Y
569            };
570            let n1 = d.cross(helper).normalize();
571            (plane(l.origin, n1), plane(l.origin, d.cross(n1)))
572        }
573        Curve3::Circle(c) => (
574            plane(c.frame.origin, c.frame.x.cross(c.frame.y)),
575            Surface::Sphere(Sphere {
576                frame: c.frame,
577                radius: c.radius,
578            }),
579        ),
580        Curve3::Ellipse(e) => (
581            plane(e.frame.origin, e.frame.x.cross(e.frame.y)),
582            Surface::EllipticalCylinder(EllipticalCylinder {
583                frame: Frame3 {
584                    z: e.frame.x.cross(e.frame.y).normalize(),
585                    ..e.frame
586                },
587                semi_axis_x: e.semi_axis_x,
588                semi_axis_y: e.semi_axis_y,
589            }),
590        ),
591        _ => return Err(ExactCurveRefusal::UnsupportedCurve),
592    };
593    let pieces = match crate::implicit_section::implicit_surface_intersection(surface, &first, None)
594    {
595        Ok(pieces) => pieces,
596        Err(ExactIntersectionRefusal::Disjoint) => {
597            return Ok(ExactCurveIntersection::Points(Vec::new()))
598        }
599        Err(_) => return Err(ExactCurveRefusal::UnsupportedSurface),
600    };
601    let mut hits = Vec::new();
602    for piece in pieces {
603        let end = piece.curve.end();
604        let traced = Curve3::ImplicitSection(piece);
605        match section_curve_surface_intersection(&traced, Interval::new(0.0, end), &second)? {
606            ExactCurveIntersection::Points(found) => {
607                for hit in found {
608                    let t = match curve {
609                        Curve3::Line(l) => {
610                            (hit.point - l.origin).dot(l.direction) / l.direction.length_squared()
611                        }
612                        _ => match axiolid_evaluate::curve::invert3(
613                            curve,
614                            hit.point,
615                            axiolid_core::Tolerance::METRE,
616                        ) {
617                            Ok(t) => t,
618                            Err(_) => continue,
619                        },
620                    };
621                    hits.push(ExactCurveHit {
622                        parameter: ExactCurveParameter::Certified(Isolated::new(t)),
623                        ..hit
624                    });
625                }
626            }
627            // The traced piece lies on the second surface too: the curve
628            // runs in the spline.
629            _ => return Ok(ExactCurveIntersection::Contained),
630        }
631    }
632    hits.sort_by(|a, b| a.parameter.approx().total_cmp(&b.parameter.approx()));
633    Ok(ExactCurveIntersection::Points(hits))
634}
635
636#[cfg(test)]
637mod tests {
638    use super::*;
639    use crate::implicit_trace::trace_within;
640    use axiolid_curve::implicit::Basis;
641
642    fn power(terms: &[(usize, usize, Scalar)]) -> Field2 {
643        let mut c = vec![vec![0.0; 10]; 10];
644        for &(i, j, x) in terms {
645            c[i][j] += x;
646        }
647        Field2::Series(SeriesField2 {
648            u: Basis::Power,
649            v: Basis::Power,
650            coefficients: c,
651        })
652    }
653
654    fn traced(field: &Field2, lo: Point2, hi: Point2) -> ImplicitCurve2 {
655        let mut curves = trace_within(
656            field,
657            Cell { lo, hi },
658            Periodic { u: false, v: false },
659            1 << 20,
660        )
661        .expect("a traced curve");
662        assert_eq!(curves.len(), 1);
663        curves.remove(0)
664    }
665
666    fn window() -> (Point2, Point2) {
667        (Point2::new(-1.0, -1.0), Point2::new(1.0, 1.0))
668    }
669
670    /// `v - 0.3 u^2` and `F + u^k`: on the curve the second field is `u^k`.
671    fn graph(k: usize) -> (ImplicitCurve2, Field2) {
672        let f = [(0, 1, 1.0), (2, 0, -0.3)];
673        let mut h = f.to_vec();
674        h.push((k, 0, 1.0));
675        let (lo, hi) = window();
676        (traced(&power(&f), lo, hi), power(&h))
677    }
678
679    #[test]
680    fn a_contact_of_any_order_up_to_eight_reports_its_multiplicity() {
681        for k in 2..=8 {
682            let (curve, h) = graph(k);
683            let roots = roots_along(&curve, &h, false).expect("decided");
684            assert_eq!(roots.len(), 1, "u^{k}: {roots:?}");
685            assert_eq!(roots[0].1, k, "u^{k}");
686            let p = curve.point(roots[0].0).unwrap();
687            assert!(p.x.abs() < 1e-6 && p.y.abs() < 1e-6, "u^{k}: {p:?}");
688        }
689    }
690
691    #[test]
692    fn a_contact_above_the_cap_is_undecided() {
693        let (curve, h) = graph(9);
694        assert!(roots_along(&curve, &h, false).is_err());
695    }
696
697    #[test]
698    fn a_contact_at_the_end_of_the_curve_is_reported() {
699        let f = power(&[(0, 1, 1.0), (2, 0, -0.3)]);
700        let h = power(&[(0, 1, 1.0), (2, 0, -0.3), (4, 0, 1.0)]);
701        let curve = traced(&f, Point2::new(-1.0, -1.0), Point2::new(0.0, 1.0));
702        let roots = roots_along(&curve, &h, false).expect("decided");
703        assert_eq!(roots.len(), 1, "{roots:?}");
704        assert_eq!(roots[0].1, 4);
705        assert!(curve.point(roots[0].0).unwrap().x.abs() < 1e-6);
706    }
707
708    #[test]
709    fn the_tangent_at_a_loops_seam_touches_it_once() {
710        let f = power(&[(2, 0, 1.0), (0, 2, 1.0), (0, 0, -0.25)]);
711        let (lo, hi) = window();
712        let curve = traced(&f, lo, hi);
713        let p = curve.point(0.0).unwrap();
714        let h = power(&[(1, 0, p.x), (0, 1, p.y), (0, 0, -(p.x * p.x + p.y * p.y))]);
715        let roots = roots_along(&curve, &h, true).expect("decided");
716        assert_eq!(roots.len(), 1, "{roots:?}");
717        assert_eq!(roots[0].1, 2);
718    }
719
720    #[test]
721    fn the_budget_refuses_by_name() {
722        // v = 0 against u times four factors u^2 - r^2: nine simple roots,
723        // more than the cap of the chain, so the leaves must split.
724        let f = power(&[(0, 1, 1.0)]);
725        let mut p = vec![0.0, 1.0];
726        for r in [0.2f64, 0.5, 0.8, 0.9] {
727            let mut q = vec![0.0; p.len() + 2];
728            for (i, c) in p.iter().enumerate() {
729                q[i + 2] += c;
730                q[i] -= c * r * r;
731            }
732            p = q;
733        }
734        let terms: Vec<(usize, usize, Scalar)> =
735            p.iter().enumerate().map(|(i, c)| (i, 0, *c)).collect();
736        let h = power(&terms);
737        let (lo, hi) = window();
738        let curve = traced(&f, lo, hi);
739        assert_eq!(roots_along(&curve, &h, false).expect("decided").len(), 9);
740        assert_eq!(
741            roots_along_within(&curve, &h, false, 2),
742            Err(RootRefusal::Budget)
743        );
744    }
745
746    /// Prints the most enclosures a decided case of this module's tests
747    /// needed, for the calibration of `ROOT_BUDGET` (sixteen times the peak
748    /// of every suite must stay below it). Run with
749    /// `cargo test -p axiolid-nurbs --lib peak_enclosures -- --ignored --nocapture`.
750    #[test]
751    #[ignore = "calibration, not a check of behaviour"]
752    fn peak_enclosures_of_decided_cases() {
753        for k in 2..=8 {
754            let (curve, h) = graph(k);
755            roots_along(&curve, &h, false).expect("decided");
756        }
757        let peak = crate::root_chain::PEAK.load(std::sync::atomic::Ordering::Relaxed);
758        println!("peak enclosures of the section cases: {peak}");
759        assert!(16 * peak <= ROOT_BUDGET, "{peak}");
760    }
761}