Skip to main content

geop_core_geometry/
shape.rs

1//! Recognizing the elementary shapes a NURBS curve or surface can be — a
2//! straight line, a circular arc, a plane, a surface of revolution — for
3//! whatever needs to know *what* an entity is, not just where it is: a
4//! reference axis built along an edge, a sketch placed on a face.
5//!
6//! Every test is a question about the control net, answered with the
7//! kernel's three-valued comparisons: a shape is recognized when the net
8//! *could* be exactly that shape — rounding can't rule it out — and never
9//! because it is merely close to it. A curve that bends by a micron is not a
10//! line, however short.
11//!
12//! The shapes themselves ([`Axis`], [`Circle`], [`Arc`], [`Plane`]) carry
13//! the few constructions everything built on them needs: projecting onto
14//! them, intersecting them.
15
16use geop_core_math::{
17    geop_error::{GeopError, GeopResult},
18    scalars::Scalar,
19    vector::Vector3,
20};
21
22use crate::{
23    nurb_curve::{NurbCurve, NurbCurve3D, dehomogenize},
24    nurb_surface::NurbSurface3D,
25};
26
27/// Whether `v` could be the zero vector.
28fn could_be_zero<S: Scalar>(v: &Vector3<S>) -> bool {
29    v.norm_sq().could_be_equal(S::ZERO)
30}
31
32/// Whether the unit vectors `a` and `b` could be parallel, in either sense.
33fn could_be_parallel<S: Scalar>(a: &Vector3<S>, b: &Vector3<S>) -> bool {
34    could_be_zero(&a.prod_cross(b))
35}
36
37/// A straight line: through `point`, along the unit vector `direction`.
38#[derive(Clone, Debug)]
39pub struct Axis<S: Scalar> {
40    pub point: Vector3<S>,
41    pub direction: Vector3<S>,
42}
43
44impl<S: Scalar> Axis<S> {
45    /// The line through `point` along `direction`, which need not be unit
46    /// length but must not be zero.
47    pub fn try_new(point: Vector3<S>, direction: Vector3<S>) -> GeopResult<Self> {
48        Ok(Self {
49            point,
50            direction: direction.normalize()?,
51        })
52    }
53
54    /// The point of the line closest to `p`: the foot of the perpendicular
55    /// from `p`.
56    pub fn project(&self, p: &Vector3<S>) -> Vector3<S> {
57        let along = p.sub(&self.point).prod_dot(&self.direction);
58        self.point.add(&self.direction.prod_scalar(along))
59    }
60
61    /// Whether `p` could lie on the line.
62    pub fn could_contain(&self, p: &Vector3<S>) -> bool {
63        could_be_zero(&p.sub(&self.point).prod_cross(&self.direction))
64    }
65
66    /// Whether `other` could run parallel to this line, either way.
67    pub fn could_be_parallel(&self, other: &Axis<S>) -> bool {
68        could_be_parallel(&self.direction, &other.direction)
69    }
70
71    /// Where the two lines come closest: halfway between the point of each
72    /// nearest the other — where they cross, if they do. Fails if they could
73    /// be parallel: then every point is as near as any other.
74    pub fn nearest(&self, other: &Axis<S>) -> GeopResult<Vector3<S>> {
75        let n = self.direction.prod_cross(&other.direction);
76        let n2 = n.norm_sq();
77        if n2.could_be_equal(S::ZERO) {
78            return Err(GeopError::new(
79                "the lines are parallel, so no point of them is nearer the other than any other",
80            ));
81        }
82        let r = other.point.sub(&self.point);
83        let s = r.prod_cross(&other.direction).prod_dot(&n).div(n2)?;
84        let t = r.prod_cross(&self.direction).prod_dot(&n).div(n2)?;
85        let a = self.point.add(&self.direction.prod_scalar(s));
86        let b = other.point.add(&other.direction.prod_scalar(t));
87        Ok(Vector3::interpolate(&a, &b, S::ONE.div(S::TWO)?))
88    }
89}
90
91/// A circle: around `center`, in the plane normal to the unit vector
92/// `normal`, of `radius`.
93#[derive(Clone, Debug)]
94pub struct Circle<S: Scalar> {
95    pub center: Vector3<S>,
96    pub normal: Vector3<S>,
97    pub radius: S,
98}
99
100impl<S: Scalar> Circle<S> {
101    /// The line through the center along the normal: what the circle turns
102    /// around.
103    pub fn axis(&self) -> Axis<S> {
104        Axis {
105            point: self.center,
106            direction: self.normal,
107        }
108    }
109
110    fn could_be_equal(&self, other: &Circle<S>) -> bool {
111        self.center.could_be_equal(&other.center)
112            && self.normal.could_be_equal(&other.normal)
113            && self.radius.could_be_equal(other.radius)
114    }
115
116    /// The smallest circle description enclosing both — two enclosures of
117    /// one circle, combined (see `AGENTS.md` on `union`).
118    fn union(&self, other: &Circle<S>) -> Circle<S> {
119        Circle {
120            center: self.center.union(&other.center),
121            normal: self.normal.union(&other.normal),
122            radius: self.radius.union(other.radius),
123        }
124    }
125}
126
127/// A circular arc: the part of `circle` from `start` counter-clockwise
128/// (seen with `circle.normal` towards the viewer) to `end` — all of it when
129/// `start` and `end` coincide.
130#[derive(Clone, Debug)]
131pub struct Arc<S: Scalar> {
132    pub circle: Circle<S>,
133    pub start: Vector3<S>,
134    pub end: Vector3<S>,
135}
136
137impl<S: Scalar> Arc<S> {
138    /// Whether the arc could be the whole circle.
139    pub fn could_be_closed(&self) -> bool {
140        self.start.could_be_equal(&self.end)
141    }
142
143    /// The angle the arc turns through, in radians, in `(0, 2 pi]`.
144    ///
145    /// A plain `f64`: `Scalar` has no inverse trigonometry, and this is
146    /// only ever used to *choose* a point along the arc (see
147    /// [`Arc::point_at`]).
148    pub fn sweep(&self) -> f64 {
149        if self.could_be_closed() {
150            return std::f64::consts::TAU;
151        }
152        let c = &self.circle;
153        let x = self.start.sub(&c.center);
154        let e = self.end.sub(&c.center);
155        let cos = x.prod_dot(&e).to_f64();
156        let sin = c.normal.prod_dot(&x.prod_cross(&e)).to_f64();
157        let angle = sin.atan2(cos);
158        if angle > 0.0 {
159            angle
160        } else {
161            angle + std::f64::consts::TAU
162        }
163    }
164
165    /// The point `fraction` of the way along the arc, by angle: `start` at
166    /// 0, `end` at 1.
167    ///
168    /// The angle is computed in `f64` (see [`Arc::sweep`]) and taken as
169    /// sharp — legitimately: which point is "0.3 of the way" is a choice
170    /// the caller makes, and any angle within rounding of it serves that
171    /// choice equally well. What the result has to be *honestly* is a point
172    /// on the circle, and it is, since it is built from the circle itself.
173    pub fn point_at(&self, fraction: f64) -> GeopResult<Vector3<S>> {
174        let c = &self.circle;
175        let x = self.start.sub(&c.center);
176        let y = c.normal.prod_cross(&x);
177        let angle = S::from_f64(fraction * self.sweep());
178        Ok(c.center
179            .add(&x.prod_scalar(angle.cos()))
180            .add(&y.prod_scalar(angle.sin())))
181    }
182
183    /// The unit tangent at `p`, a point of the arc, pointing the way the arc
184    /// runs.
185    pub fn tangent_at(&self, p: &Vector3<S>) -> GeopResult<Vector3<S>> {
186        self.circle
187            .normal
188            .prod_cross(&p.sub(&self.circle.center))
189            .normalize()
190    }
191}
192
193/// A plane: through `point`, normal to the unit vector `normal`.
194#[derive(Clone, Debug)]
195pub struct Plane<S: Scalar> {
196    pub point: Vector3<S>,
197    pub normal: Vector3<S>,
198}
199
200impl<S: Scalar> Plane<S> {
201    /// The plane through `point` normal to `normal`, which need not be unit
202    /// length but must not be zero.
203    pub fn try_new(point: Vector3<S>, normal: Vector3<S>) -> GeopResult<Self> {
204        Ok(Self {
205            point,
206            normal: normal.normalize()?,
207        })
208    }
209
210    /// How far `p` lies in front of the plane (along the normal); negative
211    /// behind it.
212    pub fn signed_distance(&self, p: &Vector3<S>) -> S {
213        p.sub(&self.point).prod_dot(&self.normal)
214    }
215
216    /// The point of the plane closest to `p`: the foot of the perpendicular
217    /// from `p`.
218    pub fn project(&self, p: &Vector3<S>) -> Vector3<S> {
219        p.sub(&self.normal.prod_scalar(self.signed_distance(p)))
220    }
221
222    /// Where `axis` pierces the plane. Fails if it could run parallel to it.
223    pub fn intersect_axis(&self, axis: &Axis<S>) -> GeopResult<Vector3<S>> {
224        let along = axis.direction.prod_dot(&self.normal);
225        if along.could_be_equal(S::ZERO) {
226            return Err(GeopError::new(
227                "the line runs parallel to the plane, so it does not pierce it",
228            ));
229        }
230        let t = self.signed_distance(&axis.point).div(along)?;
231        Ok(axis.point.sub(&axis.direction.prod_scalar(t)))
232    }
233
234    /// The line the two planes meet in, running along `self.normal x
235    /// other.normal`. Fails if they could be parallel.
236    pub fn intersect_plane(&self, other: &Plane<S>) -> GeopResult<Axis<S>> {
237        let direction = self.normal.prod_cross(&other.normal);
238        let n2 = direction.norm_sq();
239        if n2.could_be_equal(S::ZERO) {
240            return Err(GeopError::new(
241                "the planes are parallel, so they do not meet",
242            ));
243        }
244        // The point of the line closest to the origin: the combination of
245        // both normals that lies on both planes.
246        let (d1, d2) = (
247            self.point.prod_dot(&self.normal),
248            other.point.prod_dot(&other.normal),
249        );
250        let point = other
251            .normal
252            .prod_cross(&direction)
253            .prod_scalar(d1)
254            .add(&direction.prod_cross(&self.normal).prod_scalar(d2))
255            .prod_scalar(S::ONE.div(n2)?);
256        Axis::try_new(point, direction)
257    }
258}
259
260// ── curves ────────────────────────────────────────────────────────────────────
261
262impl<S: Scalar> NurbCurve3D<S> {
263    /// The line the curve runs along, if it is straight: every control point
264    /// on the line from the first to the last, which is then the curve's
265    /// direction. `None` for a curve that bends, or whose ends coincide.
266    pub fn as_line(&self) -> GeopResult<Option<Axis<S>>> {
267        let points = dehomogenize::<S, 4, 3>(&self.control_points)?;
268        let (first, last) = (points[0], points[points.len() - 1]);
269        let d = last.sub(&first);
270        if could_be_zero(&d) {
271            return Ok(None);
272        }
273        let axis = Axis::try_new(first, d)?;
274        Ok(points.iter().all(|p| axis.could_contain(p)).then_some(axis))
275    }
276
277    /// The arc the curve traces, if it is a circular one: rational quadratic
278    /// pieces, each an exact arc, all of one circle — how the kernel builds
279    /// every arc and circle, and what splitting one leaves. `None` for any
280    /// other curve.
281    pub fn as_arc(&self) -> GeopResult<Option<Arc<S>>> {
282        if self.degree != 2 {
283            return Ok(None);
284        }
285        let mut circle: Option<Circle<S>> = None;
286        for piece in self.bezier_pieces()? {
287            let Some(c) = bezier_circle(&piece)? else {
288                return Ok(None);
289            };
290            circle = Some(match circle {
291                None => c,
292                Some(prev) if prev.could_be_equal(&c) => prev.union(&c),
293                Some(_) => return Ok(None),
294            });
295        }
296        let Some(circle) = circle else {
297            return Ok(None);
298        };
299        let (t0, t1) = self.domain();
300        Ok(Some(Arc {
301            circle,
302            start: self.evaluate(t0)?,
303            end: self.evaluate(t1)?,
304        }))
305    }
306
307    /// The curve cut at every interior knot: its polynomial (or rational)
308    /// pieces, in order.
309    fn bezier_pieces(&self) -> GeopResult<Vec<Self>> {
310        let end = self.domain().1;
311        let interior = &self.knot_vector[self.degree + 1..self.control_points.len()];
312        let mut rest = self.clone();
313        let mut pieces = Vec::new();
314        for &k in interior {
315            if k.definitely_greater(rest.domain().0) && k.definitely_less(end) {
316                let (left, right) = rest.split(k)?;
317                pieces.push(left);
318                rest = right;
319            }
320        }
321        pieces.push(rest);
322        Ok(pieces)
323    }
324}
325
326/// The circle a rational quadratic Bézier piece traces, if it is an exact
327/// circular arc.
328///
329/// With end points `P0`, `P2`, middle control point `P1` and weights `w0`,
330/// `w1`, `w2`, the piece is an arc exactly when `P1` is where the arc's end
331/// tangents meet — equally far from both ends, `|P1 - P0| = |P1 - P2|` — and
332/// the weight normalized to `w0 = w2 = 1`, `w1 / sqrt(w0 w2)`, is the cosine
333/// of the angle `theta` between the chord and those tangents:
334/// `cos theta = |P2 - P0| / (2 |P1 - P0|)`. Squared, so no root is taken.
335fn bezier_circle<S: Scalar>(piece: &NurbCurve3D<S>) -> GeopResult<Option<Circle<S>>> {
336    let [h0, h1, h2] = match piece.control_points.as_slice() {
337        [a, b, c] => [*a, *b, *c],
338        _ => return Ok(None),
339    };
340    let points = dehomogenize::<S, 4, 3>(&[h0, h1, h2])?;
341    let (p0, p1, p2) = (points[0], points[1], points[2]);
342    let (w0, w1, w2) = (h0[3], h1[3], h2[3]);
343    let tangent = p1.sub(&p0).norm_sq();
344    if !tangent.could_be_equal(p1.sub(&p2).norm_sq()) {
345        return Ok(None);
346    }
347    let chord = p2.sub(&p0).norm_sq();
348    let four = S::TWO.add(S::TWO);
349    if !four
350        .mul(w1)
351        .mul(w1)
352        .mul(tangent)
353        .could_be_equal(w0.mul(w2).mul(chord))
354    {
355        return Ok(None);
356    }
357    // The center lies on the line from `P1` through the chord's midpoint
358    // `M`, `|P1 - P0|^2 / |P1 - M|^2` times as far from `P1` as `M` is: the
359    // triangle `P0 P1 center` has its right angle at `P0`.
360    let m = p0.add(&p2).prod_scalar(S::ONE.div(S::TWO)?);
361    let h = m.sub(&p1).norm_sq();
362    if h.could_be_equal(S::ZERO) {
363        return Ok(None);
364    }
365    let center = p1.add(&m.sub(&p1).prod_scalar(tangent.div(h)?));
366    let radial = p0.sub(&center);
367    // The piece leaves `P0` towards `P1`: turning counter-clockwise about
368    // `radial x (P1 - P0)`.
369    let normal = radial.prod_cross(&p1.sub(&p0)).normalize()?;
370    Ok(Some(Circle {
371        center,
372        normal,
373        radius: radial.norm(),
374    }))
375}
376
377// ── surfaces ──────────────────────────────────────────────────────────────────
378
379impl<S: Scalar> NurbSurface3D<S> {
380    /// The plane the surface lies in, if it is flat: every control point on
381    /// the plane through its middle, normal to it there — the normal pointing
382    /// the way the surface's own does. `None` for a surface that bends.
383    pub fn as_plane(&self) -> GeopResult<Option<Plane<S>>> {
384        let ((u0, u1), (v0, v1)) = (self.domain_u(), self.domain_v());
385        let (u, v) = (u0.add(u1).div(S::TWO)?, v0.add(v1).div(S::TWO)?);
386        let plane = Plane {
387            point: self.evaluate(u, v)?,
388            normal: self.normal(u, v)?,
389        };
390        let points = dehomogenize::<S, 4, 3>(&self.control_points)?;
391        Ok(points
392            .iter()
393            .all(|p| plane.signed_distance(p).could_be_equal(S::ZERO))
394            .then_some(plane))
395    }
396
397    /// The axis the surface turns around, if it is a surface of revolution —
398    /// a cylinder, a cone, a sphere, a torus, a disc: one of its parameter
399    /// directions sweeps circular arcs around a common axis. `None` for any
400    /// other surface.
401    ///
402    /// Checked on the control net: every row of control points along that
403    /// direction is an arc (see [`NurbCurve3D::as_arc`]) around the one
404    /// axis, starting at the same angle, with weights proportional to every
405    /// other row's — or a single point on the axis, as at a sphere's pole.
406    /// Then every row turns through the same angles at the same parameters,
407    /// and a blend of them across the other direction is the blended
408    /// profile, turned: a surface of revolution.
409    pub fn axis_of_revolution(&self) -> GeopResult<Option<Axis<S>>> {
410        for along_u in [true, false] {
411            if let Some(axis) = self.revolution_along(along_u)? {
412                return Ok(Some(axis));
413            }
414        }
415        Ok(None)
416    }
417
418    /// [`NurbSurface3D::axis_of_revolution`], for rows along `u` or along `v`.
419    fn revolution_along(&self, along_u: bool) -> GeopResult<Option<Axis<S>>> {
420        let (rows, len, degree, knots) = if along_u {
421            (self.num_v, self.num_u, self.degree_u, &self.knot_vector_u)
422        } else {
423            (self.num_u, self.num_v, self.degree_v, &self.knot_vector_v)
424        };
425        let row = |j: usize| -> Vec<_> {
426            (0..len)
427                .map(|i| {
428                    let index = if along_u {
429                        i * self.num_v + j
430                    } else {
431                        j * self.num_v + i
432                    };
433                    self.control_points[index]
434                })
435                .collect()
436        };
437        // The first row that is an arc sets the axis, the angle every row
438        // starts at, and the weights every row is proportional to.
439        let mut reference: Option<(Arc<S>, Vec<S>)> = None;
440        let mut poles = Vec::new();
441        for j in 0..rows {
442            let cps = row(j);
443            let weights: Vec<S> = cps.iter().map(|p| p[3]).collect();
444            if let Some((_, reference_weights)) = &reference {
445                let proportional = (0..len).all(|i| {
446                    weights[i]
447                        .mul(reference_weights[0])
448                        .could_be_equal(reference_weights[i].mul(weights[0]))
449                });
450                if !proportional {
451                    return Ok(None);
452                }
453            }
454            let points = dehomogenize::<S, 4, 3>(&cps)?;
455            if points.iter().all(|p| p.could_be_equal(&points[0])) {
456                poles.push(points[0]);
457                continue;
458            }
459            let Some(arc) = NurbCurve::try_new(degree, cps, knots.clone())?.as_arc()? else {
460                return Ok(None);
461            };
462            match &reference {
463                None => reference = Some((arc, weights)),
464                Some((first, _)) => {
465                    let axis = first.circle.axis();
466                    let same_angle = arc
467                        .start
468                        .sub(&arc.circle.center)
469                        .normalize()?
470                        .could_be_equal(&first.start.sub(&first.circle.center).normalize()?);
471                    if !arc.circle.normal.could_be_equal(&axis.direction)
472                        || !axis.could_contain(&arc.circle.center)
473                        || !same_angle
474                    {
475                        return Ok(None);
476                    }
477                }
478            }
479        }
480        let Some((first, _)) = reference else {
481            return Ok(None);
482        };
483        let axis = first.circle.axis();
484        Ok(poles.iter().all(|p| axis.could_contain(p)).then_some(axis))
485    }
486}
487
488#[cfg(test)]
489mod tests {
490    use geop_core_math::{
491        for_all_scalars,
492        scalars::Scalar,
493        vector::{Vector3, Vector4},
494    };
495
496    use super::*;
497    use crate::nurb_surface::NurbSurface;
498
499    fn v<S: Scalar>(x: f64, y: f64, z: f64) -> Vector3<S> {
500        Vector3::from_array([x, y, z].map(S::from_f64))
501    }
502
503    /// `(x, y, z)` with weight `w`, homogeneous.
504    fn h<S: Scalar>(x: f64, y: f64, z: f64, w: f64) -> Vector4<S> {
505        Vector4::from_array([x * w, y * w, z * w, w].map(S::from_f64))
506    }
507
508    fn knots<S: Scalar>(ks: &[f64]) -> Vec<S> {
509        ks.iter().map(|&k| S::from_f64(k)).collect()
510    }
511
512    const R2: f64 = std::f64::consts::FRAC_1_SQRT_2;
513
514    /// The unit circle around the origin in the xy plane, counter-clockwise
515    /// from `(1, 0, 0)`: four quarter arcs, the kernel's usual form.
516    fn unit_circle<S: Scalar>() -> NurbCurve3D<S> {
517        let corners = [
518            (1., 0.),
519            (1., 1.),
520            (0., 1.),
521            (-1., 1.),
522            (-1., 0.),
523            (-1., -1.),
524            (0., -1.),
525            (1., -1.),
526            (1., 0.),
527        ];
528        let cps = corners
529            .iter()
530            .enumerate()
531            .map(|(i, &(x, y))| h(x, y, 0., if i % 2 == 1 { R2 } else { 1. }))
532            .collect();
533        NurbCurve::try_new(
534            2,
535            cps,
536            knots(&[0., 0., 0., 1., 1., 2., 2., 3., 3., 4., 4., 4.]),
537        )
538        .unwrap()
539    }
540
541    fn check_line<S: Scalar>() {
542        let line = NurbCurve::<S, 4>::try_new(
543            2,
544            vec![h(0., 0., 0., 1.), h(1., 2., 2., 0.5), h(3., 6., 6., 1.)],
545            knots(&[0., 0., 0., 1., 1., 1.]),
546        )
547        .unwrap();
548        let axis = line.as_line().unwrap().expect("collinear control points");
549        assert!(axis.direction.could_be_equal(&v(1. / 3., 2. / 3., 2. / 3.)));
550        assert!(unit_circle::<S>().as_line().unwrap().is_none());
551    }
552    #[test]
553    fn straight_curves_are_lines() {
554        for_all_scalars!(check_line);
555    }
556
557    fn check_circle<S: Scalar>() {
558        let arc = unit_circle::<S>().as_arc().unwrap().expect("a circle");
559        assert!(arc.circle.center.could_be_equal(&v(0., 0., 0.)));
560        assert!(arc.circle.normal.could_be_equal(&v(0., 0., 1.)));
561        assert!(arc.circle.radius.could_be_equal(S::ONE));
562        assert!(arc.could_be_closed());
563        assert!(arc.point_at(0.5).unwrap().could_be_equal(&v(-1., 0., 0.)));
564        let ninety = arc.point_at(0.25).unwrap();
565        assert!(ninety.could_be_equal(&v(0., 1., 0.)));
566        assert!(
567            arc.tangent_at(&ninety)
568                .unwrap()
569                .could_be_equal(&v(-1., 0., 0.))
570        );
571    }
572    #[test]
573    fn a_circle_is_recognized_with_its_turning_sense() {
574        for_all_scalars!(check_circle);
575    }
576
577    /// A piece split off an arc — as a boolean leaves one — is still that
578    /// arc's circle, with its own ends.
579    fn check_split_arc<S: Scalar>() {
580        let (left, _) = unit_circle::<S>().split(S::from_f64(1.3)).unwrap();
581        let arc = left.as_arc().unwrap().expect("still circular");
582        assert!(arc.circle.radius.could_be_equal(S::ONE));
583        assert!(!arc.could_be_closed());
584        assert!(arc.sweep() > std::f64::consts::FRAC_PI_2);
585        assert!(arc.sweep() < std::f64::consts::PI);
586        // Clockwise, seen from below: the same circle, the other normal.
587        let reversed = left.reverse().as_arc().unwrap().expect("still circular");
588        assert!(reversed.circle.normal.could_be_equal(&v(0., 0., -1.)));
589    }
590    #[test]
591    fn a_split_arc_is_still_an_arc() {
592        for_all_scalars!(check_split_arc);
593    }
594
595    /// A conic that is not a circle: the right corner, weighted wrong.
596    fn check_not_circle<S: Scalar>() {
597        let conic = NurbCurve::<S, 4>::try_new(
598            2,
599            vec![h(1., 0., 0., 1.), h(1., 1., 0., 0.5), h(0., 1., 0., 1.)],
600            knots(&[0., 0., 0., 1., 1., 1.]),
601        )
602        .unwrap();
603        assert!(conic.as_arc().unwrap().is_none());
604        let lopsided = NurbCurve::<S, 4>::try_new(
605            2,
606            vec![h(1., 0., 0., 1.), h(1., 2., 0., R2), h(0., 1., 0., 1.)],
607            knots(&[0., 0., 0., 1., 1., 1.]),
608        )
609        .unwrap();
610        assert!(lopsided.as_arc().unwrap().is_none());
611    }
612    #[test]
613    fn other_conics_are_not_arcs() {
614        for_all_scalars!(check_not_circle);
615    }
616
617    /// A quarter of a cylinder of radius 2 around the z axis, `u` around it
618    /// and `v` along it, 3 high.
619    fn quarter_cylinder<S: Scalar>() -> NurbSurface3D<S> {
620        let ring = [(2., 0., 1.), (2., 2., R2), (0., 2., 1.)];
621        let cps = ring
622            .iter()
623            .flat_map(|&(x, y, w)| [h(x, y, 0., w), h(x, y, 3., w)])
624            .collect();
625        NurbSurface::try_new(
626            2,
627            1,
628            cps,
629            knots(&[0., 0., 0., 1., 1., 1.]),
630            knots(&[0., 0., 1., 1.]),
631        )
632        .unwrap()
633    }
634
635    fn check_revolution<S: Scalar>() {
636        let cylinder = quarter_cylinder::<S>();
637        let axis = cylinder.axis_of_revolution().unwrap().expect("a cylinder");
638        assert!(axis.could_contain(&v(0., 0., 7.)));
639        assert!(could_be_parallel(&axis.direction, &v(0., 0., 1.)));
640        assert!(cylinder.as_plane().unwrap().is_none());
641
642        // Twisted: the top ring starts a little further round.
643        let mut twisted = cylinder.clone();
644        twisted.control_points[1] = h(2., 0.1, 3., 1.);
645        assert!(twisted.axis_of_revolution().unwrap().is_none());
646    }
647    #[test]
648    fn a_cylinder_turns_around_its_axis() {
649        for_all_scalars!(check_revolution);
650    }
651
652    fn check_plane<S: Scalar>() {
653        let flat = NurbSurface::<S, 4>::try_new(
654            1,
655            1,
656            vec![
657                h(0., 0., 1., 1.),
658                h(0., 1., 1., 1.),
659                h(1., 0., 1., 1.),
660                h(1., 1., 1., 1.),
661            ],
662            knots(&[0., 0., 1., 1.]),
663            knots(&[0., 0., 1., 1.]),
664        )
665        .unwrap();
666        let plane = flat.as_plane().unwrap().expect("flat");
667        assert!(plane.normal.could_be_equal(&v(0., 0., 1.)));
668        assert!(
669            plane
670                .signed_distance(&v(5., 5., 1.))
671                .could_be_equal(S::ZERO)
672        );
673        let mut bent = flat.clone();
674        bent.control_points[3] = h(1., 1., 1.2, 1.);
675        assert!(bent.as_plane().unwrap().is_none());
676    }
677    #[test]
678    fn flat_surfaces_are_planes() {
679        for_all_scalars!(check_plane);
680    }
681
682    fn check_constructions<S: Scalar>() {
683        let z = Plane::<S>::try_new(v(0., 0., 2.), v(0., 0., 3.)).unwrap();
684        let x = Plane::try_new(v(1., 0., 0.), v(1., 0., 0.)).unwrap();
685        assert!(z.project(&v(4., 5., 6.)).could_be_equal(&v(4., 5., 2.)));
686        let line = z.intersect_plane(&x).unwrap();
687        assert!(line.could_contain(&v(1., 7., 2.)));
688        assert!(could_be_parallel(&line.direction, &v(0., 1., 0.)));
689        let slanted = Axis::try_new(v(0., 0., 0.), v(1., 1., 1.)).unwrap();
690        assert!(
691            z.intersect_axis(&slanted)
692                .unwrap()
693                .could_be_equal(&v(2., 2., 2.))
694        );
695        assert!(
696            slanted
697                .project(&v(3., 0., 0.))
698                .could_be_equal(&v(1., 1., 1.))
699        );
700        let crossing = Axis::try_new(v(2., 0., 2.), v(0., 1., 0.)).unwrap();
701        assert!(
702            slanted
703                .nearest(&crossing)
704                .unwrap()
705                .could_be_equal(&v(2., 2., 2.))
706        );
707        // Missing each other: halfway along the common perpendicular, from
708        // (2.5, 2.5, 2.5) to (2, 2.5, 3).
709        let skew = Axis::try_new(v(2., 0., 3.), v(0., 1., 0.)).unwrap();
710        assert!(
711            slanted
712                .nearest(&skew)
713                .unwrap()
714                .could_be_equal(&v(2.25, 2.5, 2.75))
715        );
716        assert!(slanted.nearest(&slanted).is_err());
717        assert!(z.intersect_plane(&z).is_err());
718    }
719    #[test]
720    fn planes_and_axes_meet_where_they_should() {
721        for_all_scalars!(check_constructions);
722    }
723}