axiolid_nurbs/
implicit_section.rs

1//! Sections of analytic surfaces with no closed form, as implicit curves
2//! (ADR 0077).
3//!
4//! The section, read in one surface's parameters, is the zero set of the
5//! other surface's equation composed with the first one's point: an exact
6//! [`Field2`]. [`implicit_surface_intersection`] traces every component of
7//! it in a parameter window with certified topology
8//! (`implicit_trace`), each as an [`ImplicitSection3`] on the first
9//! surface. A torus against a cylinder, cone or torus off its axis takes
10//! this path from `exact_surface_intersection`.
11
12use axiolid_core::{Interval, Point2, Scalar, Vec3};
13use axiolid_curve::{Carrier, Curve3, Field2, ImplicitSection3};
14use axiolid_surface::Surface;
15use core::f64::consts::{FRAC_PI_2, PI};
16
17use crate::exact_surface_intersection::{
18    Derivation, ExactIntersectionCurve, ExactIntersectionRefusal,
19};
20use crate::field::{carrier_of, section_field};
21use crate::implicit_trace::{trace, Periodic, TraceRefusal};
22use axiolid_curve::implicit::Cell;
23
24/// `other`'s implicit equation read in `carrier`'s parameters, where both
25/// are analytic: zero exactly where `carrier`'s point lies on `other` (for
26/// a cone, on either nappe).
27#[must_use]
28pub fn section_field_of(carrier: &Surface, other: &Surface) -> Option<Field2> {
29    section_field(&carrier_of(carrier)?, other)
30}
31
32/// The refusal a trace's own refusal amounts to: touching only at isolated
33/// points is `NotRegularCurve`, as for the closed forms; anything else is
34/// `Undecided`.
35pub(crate) fn refusal_of(refusal: TraceRefusal) -> ExactIntersectionRefusal {
36    match refusal {
37        TraceRefusal::Touching(_) => ExactIntersectionRefusal::NotRegularCurve,
38        TraceRefusal::Singular(_) | TraceRefusal::Many(_) | TraceRefusal::Budget => {
39            ExactIntersectionRefusal::Undecided
40        }
41    }
42}
43
44/// Every component of the section of `carrier` by `other` in a window of
45/// `carrier`'s parameters, as implicit curves on `carrier`.
46///
47/// Without a `window`, the whole of a compact carrier (a sphere or torus)
48/// is searched, or the stretch of an unbounded one that a compact `other`
49/// can reach.
50///
51/// # Errors
52///
53/// - `UnsupportedPair`: a B-spline operand, or an unbounded pair with no
54///   window.
55/// - `Undecided`: the trace ran out of budget, or met a singular point it
56///   could not match to the field's sign changes about it.
57/// - `NotRegularCurve`: the surfaces only touch in the window (a
58///   singular point of the section), or the trace exhausted its budget.
59/// - `Disjoint`: no section in the window.
60pub fn implicit_surface_intersection(
61    carrier: &Surface,
62    other: &Surface,
63    window: Option<(Point2, Point2)>,
64) -> Result<Vec<ImplicitSection3>, ExactIntersectionRefusal> {
65    let form = carrier_of(carrier).ok_or(ExactIntersectionRefusal::UnsupportedPair)?;
66    let field = section_field(&form, other).ok_or(ExactIntersectionRefusal::UnsupportedPair)?;
67    let (periodic_u, periodic_v) = form.periodic();
68    let (lo, hi) = match window {
69        Some(w) => w,
70        None => default_window(&form, other).ok_or(ExactIntersectionRefusal::UnsupportedPair)?,
71    };
72    // A window a whole turn wide in a periodic parameter wraps.
73    let wraps = |periodic: bool, a: Scalar, b: Scalar| {
74        periodic && ((b - a) - 2.0 * PI).abs() <= 1e-12 * (1.0 + a.abs())
75    };
76    let periodic = Periodic {
77        u: wraps(periodic_u, lo.x, hi.x),
78        v: wraps(periodic_v, lo.y, hi.y),
79    };
80    let curves = trace(&field, Cell { lo, hi }, periodic).map_err(refusal_of)?;
81    // A B-spline ends at its domain: its sections do too.
82    let curves: Vec<_> = match &form {
83        Carrier::Spline(b) => match b.domain() {
84            Some(((u0, u1), (v0, v1))) => curves
85                .iter()
86                .flat_map(|c| c.clipped(Point2::new(u0, v0), Point2::new(u1, v1)))
87                .collect(),
88            None => curves,
89        },
90        _ => curves,
91    };
92    if curves.is_empty() {
93        return Err(ExactIntersectionRefusal::Disjoint);
94    }
95    Ok(curves
96        .into_iter()
97        .map(|curve| ImplicitSection3 {
98            carrier: form.clone(),
99            curve,
100        })
101        .collect())
102}
103
104/// Where a whole-turn window starts, past `-pi`: an irrational fraction of
105/// a radian, so the window's edge is never where a symmetric section
106/// crosses or turns.
107pub(crate) const TURN_OFFSET: Scalar = 0.123_456_789_012_345_67;
108
109/// A centre and radius holding a compact surface.
110fn ball(surface: &Surface) -> Option<(Vec3, Scalar)> {
111    match surface {
112        Surface::Sphere(s) => Some((s.frame.origin, s.radius)),
113        Surface::Torus(t) => Some((t.frame.origin, t.major_radius + t.minor_radius)),
114        _ => None,
115    }
116}
117
118/// The window a trace needs on `carrier` to find every section with
119/// `other`.
120fn default_window(carrier: &Carrier, other: &Surface) -> Option<(Point2, Point2)> {
121    // A whole turn, starting off the round angles a symmetric input puts
122    // its special points on.
123    let o = TURN_OFFSET;
124    match carrier {
125        Carrier::Torus(_) => Some((Point2::new(-PI + o, -PI + o), Point2::new(PI + o, PI + o))),
126        Carrier::Sphere { .. } => Some((
127            Point2::new(-PI + o, -FRAC_PI_2),
128            Point2::new(PI + o, FRAC_PI_2),
129        )),
130        Carrier::Ruled(k) => {
131            let (centre, radius) = ball(other)?;
132            let z = k.frame.z.normalize();
133            let along = (centre - k.frame.origin).dot(z);
134            let margin = 1e-6 * (1.0 + radius + along.abs());
135            let (mut v0, mut v1) = (along - radius - margin, along + radius + margin);
136            // One nappe of a cone: stop short of the apex.
137            if k.slope != 0.0 {
138                let apex = -k.x_radius / k.slope;
139                let clear = 1e-9 * (1.0 + apex.abs());
140                if k.slope > 0.0 {
141                    v0 = v0.max(apex + clear);
142                } else {
143                    v1 = v1.min(apex - clear);
144                }
145                if v0 >= v1 {
146                    return None;
147                }
148            }
149            Some((Point2::new(-PI + o, v0), Point2::new(PI + o, v1)))
150        }
151        Carrier::Spline(b) => {
152            // A little past the domain, so a section reaching its edge is
153            // found there and not on the window's own boundary.
154            let ((u0, u1), (v0, v1)) = b.domain()?;
155            let (pu, pv) = (1e-6 * (u1 - u0), 1e-6 * (v1 - v0));
156            Some((Point2::new(u0 - pu, v0 - pv), Point2::new(u1 + pu, v1 + pv)))
157        }
158        Carrier::Plane(f) => {
159            let (centre, radius) = ball(other)?;
160            let d = centre - f.origin;
161            let (x, y) = (d.dot(f.x.normalize()), d.dot(f.y.normalize()));
162            let r = radius * (1.0 + 1e-6);
163            Some((Point2::new(x - r, y - r), Point2::new(x + r, y + r)))
164        }
165    }
166}
167
168/// The traced fallback of `exact_surface_intersection`: analytic pairs with
169/// no closed form, carried on the compact surface (a torus first). `None`
170/// when the pair has no compact surface to carry the trace.
171pub(crate) fn traced_section(
172    first: &Surface,
173    second: &Surface,
174) -> Result<Option<ExactIntersectionCurve>, ExactIntersectionRefusal> {
175    if carrier_of(first).is_none() || carrier_of(second).is_none() {
176        return Ok(None);
177    }
178    let rank = |s: &Surface| match s {
179        Surface::Torus(_) => 2,
180        Surface::Sphere(_) => 1,
181        _ => 0,
182    };
183    let (carrier, other) = if rank(first) >= rank(second) {
184        (first, second)
185    } else {
186        (second, first)
187    };
188    if rank(carrier) == 0 {
189        return Ok(None);
190    }
191    let sections = implicit_surface_intersection(carrier, other, None)?;
192    let spans = sections
193        .iter()
194        .map(|s| Some(Interval::new(0.0, s.curve.end())))
195        .collect();
196    Ok(Some(ExactIntersectionCurve {
197        branches: sections.into_iter().map(Curve3::ImplicitSection).collect(),
198        derivation: Derivation::ImplicitTrace,
199        spans,
200    }))
201}