Skip to main content

geop_core_geometry/nurb_curve/
within.rs

1//! Where a plane curve lies with respect to a convex polygon: wholly in
2//! it, touching it, or clear of it — what a box drawn over a sketch
3//! selects. Decided on the curve itself by subdivision, never from
4//! samples: a piece whose control points all lie in the polygon lies in
5//! it, and one whose control points all lie past one side lies clear of it
6//! (the convex hull property, the weights being positive).
7
8use geop_core_math::{
9    geop_error::{GeopError, GeopResult},
10    polygon::Convex,
11    scalars::Scalar,
12    solvers::subdivide::{Order, Searched, Split, subdivide},
13    vector::Vector2,
14};
15
16use super::{NurbCurve, dehomogenize};
17
18/// Where a curve lies with respect to a convex polygon (see
19/// [`NurbCurve::within`]). Where a piece of the curve runs along the
20/// polygon's boundary closer than the search resolves, it counts as
21/// touching, and as no point outside.
22#[derive(Clone, Copy, Debug, PartialEq, Eq)]
23pub struct Within {
24    /// No point of the curve lies definitely outside the polygon.
25    pub wholly: bool,
26    /// Some point of the curve could lie in the polygon.
27    pub touches: bool,
28}
29
30impl NurbCurve<3> {
31    /// Where the curve lies with respect to `region`: split until each
32    /// piece lies in it, lies past one of its sides, or has a point on
33    /// either side — or is smaller than `min_size`, along the boundary.
34    /// Visits at most `max_nodes` pieces: exhausting them is an error, the
35    /// answer being unknown.
36    pub fn within(
37        &self,
38        region: &Convex,
39        min_size: Scalar,
40        max_nodes: usize,
41    ) -> GeopResult<Within> {
42        let (mut inside, mut outside) = (false, false);
43        let pieces = self.bezier_pieces()?;
44        let searched = subdivide(pieces, Order::DepthFirst, max_nodes, |piece, _| {
45            if inside && outside {
46                return Ok(Split::Done);
47            }
48            let points: Vec<Vector2> = dehomogenize(&piece.control_points);
49            // Past one side with every control point: clear of the polygon.
50            let clear = (region.sides.iter()).any(|(n, c)| {
51                (points.iter()).all(|p| n.prod_dot(p).sub(*c).definitely_greater(Scalar::ZERO))
52            });
53            if clear {
54                outside = true;
55                return Ok(Split::Done);
56            }
57            if points.iter().all(|p| region.could_contain(p)) {
58                inside = true;
59                return Ok(Split::Done);
60            }
61            // A Bézier piece passes through its end control points.
62            for end in [points.first(), points.last()].into_iter().flatten() {
63                match region.could_contain(end) {
64                    true => inside = true,
65                    false => outside = true,
66                }
67            }
68            let [x, y, _] = *piece.aabb();
69            if !x.width().max(y.width()).definitely_greater(min_size) {
70                // Along the boundary, closer than resolved.
71                inside = true;
72                return Ok(Split::Done);
73            }
74            let (left, right) = piece.split_mid()?;
75            Ok::<_, GeopError>(Split::Halves(left, right))
76        })?;
77        if let Searched::Exhausted { pending } = searched {
78            return Err(GeopError::new(format!(
79                "NurbCurve::within: exhausted max_nodes={max_nodes} with {pending} pieces left, \
80                 min_size={min_size:?}, region={region:?}"
81            )));
82        }
83        Ok(Within {
84            wholly: !outside,
85            touches: inside,
86        })
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93    use crate::nurb_curve::NurbCurve2D;
94
95    fn v(x: f64, y: f64) -> Vector2 {
96        Vector2::from_array([Scalar::from_f64(x), Scalar::from_f64(y)])
97    }
98
99    fn square(x0: f64, y0: f64, x1: f64, y1: f64) -> Convex {
100        Convex::new(&[v(x0, y0), v(x1, y0), v(x1, y1), v(x0, y1)]).unwrap()
101    }
102
103    fn within(curve: &NurbCurve2D, region: &Convex) -> Within {
104        curve
105            .within(region, Scalar::from_f64(1e-9), 10_000)
106            .unwrap()
107    }
108
109    /// The quarter arc of the unit circle from `(1, 0)` to `(0, 1)`.
110    fn quarter() -> NurbCurve2D {
111        let w = Scalar::ONE.div(Scalar::TWO).unwrap().sqrt().unwrap();
112        NurbCurve2D::arc(v(1.0, 0.0), v(1.0, 1.0), v(0.0, 1.0), w).unwrap()
113    }
114
115    #[test]
116    fn a_line_inside_across_and_clear() {
117        let line = NurbCurve2D::line(v(1.0, 1.0), v(2.0, 2.0)).unwrap();
118        let both = Within {
119            wholly: true,
120            touches: true,
121        };
122        assert_eq!(within(&line, &square(0.0, 0.0, 3.0, 3.0)), both);
123        let across = within(&line, &square(1.5, 0.0, 3.0, 3.0));
124        assert!(!across.wholly && across.touches);
125        // Through the box with both ends outside it.
126        let through = NurbCurve2D::line(v(-1.0, 0.5), v(4.0, 0.5)).unwrap();
127        let crossed = within(&through, &square(0.0, 0.0, 3.0, 3.0));
128        assert!(!crossed.wholly && crossed.touches);
129        let clear = within(&line, &square(5.0, 5.0, 6.0, 6.0));
130        assert!(!clear.wholly && !clear.touches);
131    }
132
133    /// An arc's control polygon reaches its corner `(1, 1)`, the arc only
134    /// `√½` of the way: a box over the corner alone is clear of the arc,
135    /// and one the arc bulges into, but not its control point, holds it.
136    #[test]
137    fn an_arc_is_judged_by_itself_not_its_control_points() {
138        let arc = quarter();
139        let corner = within(&arc, &square(0.8, 0.8, 1.2, 1.2));
140        assert!(!corner.touches, "{corner:?}");
141        let holds = within(&arc, &square(-0.01, -0.01, 1.01, 1.01));
142        assert!(holds.wholly && holds.touches, "{holds:?}");
143        // The arc's middle is at `0.7071`: a box to `0.72` takes it whole,
144        // one to `0.70` only touches it.
145        let bulge = within(&arc, &square(0.6, 0.6, 0.72, 0.72));
146        assert!(bulge.touches && !bulge.wholly, "{bulge:?}");
147        let near = within(&arc, &square(0.72, 0.72, 0.9, 0.9));
148        assert!(!near.touches, "{near:?}");
149    }
150}