Skip to main content

geop_core_geometry/nurb_surface/
flat.rs

1//! A flat face's surface around the curves bounding it ([`FlatPatch`]): the
2//! cap of a sweep, of a blend's tool, the face filling a flat loop.
3
4use geop_core_math::{
5    geop_error::{GeopError, GeopResult},
6    scalars::Scalar,
7    vector::{Vector2, Vector3},
8};
9
10use super::NurbSurface3D;
11use crate::{
12    aabb::union_aabb,
13    nurb_curve::{NurbCurve, NurbCurve2D},
14};
15
16/// The box around curves drawn in a plane, as a bilinear patch of it: a
17/// plane point `(x, y)` lies at `origin + x e1 + y e2`, and the patch is
18/// parametrized over `[0, 1]²` along `(x, y)` — or along `(y, x)` if
19/// `swapped`, whichever makes its loops wind the way a face's must.
20///
21/// The box encloses every curve: a NURBS curve stays within the convex hull
22/// of its control points, and the box is the outer bounds of their
23/// enclosures. A curve drawn in the plane is mapped into the patch's
24/// parameters by an affine map of its control points ([`FlatPatch::pcurve`]),
25/// exact for any NURBS.
26#[derive(Clone, Debug)]
27pub struct FlatPatch {
28    origin: Vector3,
29    e1: Vector3,
30    e2: Vector3,
31    lo: Vector2,
32    size: Vector2,
33    swapped: bool,
34}
35
36impl FlatPatch {
37    /// The patch of the plane `(origin, e1, e2)` around `curves`, given in
38    /// its coordinates.
39    pub fn around<'a>(
40        origin: Vector3,
41        e1: Vector3,
42        e2: Vector3,
43        curves: impl IntoIterator<Item = &'a NurbCurve2D>,
44        swapped: bool,
45    ) -> GeopResult<Self> {
46        let bounds = union_aabb(curves.into_iter().map(NurbCurve::aabb))
47            .ok_or_else(|| GeopError::new("FlatPatch::around: no curves to span"))?;
48        let lo: [f64; 2] = std::array::from_fn(|k| bounds[k].lower().to_f64());
49        let hi: [f64; 2] = std::array::from_fn(|k| bounds[k].upper().to_f64());
50        let size = std::array::from_fn(|k| reaching(lo[k], hi[k]));
51        Ok(Self {
52            origin,
53            e1,
54            e2,
55            lo: Vector2::from_array(lo.map(Scalar::from_f64)),
56            size: Vector2::from_array(size),
57            swapped,
58        })
59    }
60
61    /// The patch grown to reach past `[low, high]`, a box in the plane's
62    /// coordinates, on every side by its own larger size.
63    ///
64    /// The box's sides pass through the curves' extreme points, and a line
65    /// of another body lying in the plane through such a point, along the
66    /// box's side, runs along the patch's boundary to within rounding: where
67    /// the two enclosures miss each other by less than their widths, no
68    /// search can tell that apart from an overlap. Where the patch ends is a
69    /// free choice. Past the extent of everything that could lie in the
70    /// plane, nothing lies along its sides, whatever the dimensions.
71    pub fn past(&self, [low, high]: [Vector2; 2]) -> Self {
72        let margin = self.size[0].to_f64().max(self.size[1].to_f64());
73        let lo: [f64; 2] =
74            std::array::from_fn(|k| self.lo[k].to_f64().min(low[k].lower().to_f64()) - margin);
75        let hi: [f64; 2] = std::array::from_fn(|k| {
76            let own = self.lo[k].add(self.size[k]).upper().to_f64();
77            own.max(high[k].upper().to_f64()) + margin
78        });
79        let size = std::array::from_fn(|k| reaching(lo[k], hi[k]));
80        Self {
81            lo: Vector2::from_array(lo.map(Scalar::from_f64)),
82            size: Vector2::from_array(size),
83            ..self.clone()
84        }
85    }
86
87    /// How far the patch reaches along the plane's two axes: its lowest
88    /// corner and its highest, in the plane's coordinates.
89    pub fn reach(&self) -> [Vector2; 2] {
90        [self.lo, self.lo.add(&self.size)]
91    }
92
93    /// `curve`, drawn in the plane, in the patch's parameters.
94    pub fn pcurve(&self, curve: &NurbCurve2D) -> GeopResult<NurbCurve2D> {
95        let control_points = curve
96            .control_points
97            .iter()
98            .map(|cp| {
99                Ok(Vector3::from_array([
100                    cp[0].sub(cp[2].mul(self.lo[0])).div(self.size[0])?,
101                    cp[1].sub(cp[2].mul(self.lo[1])).div(self.size[1])?,
102                    cp[2],
103                ]))
104            })
105            .collect::<GeopResult<Vec<_>>>()?;
106        let pcurve = NurbCurve::try_new(curve.degree, control_points, curve.knot_vector.clone())?;
107        Ok(if self.swapped {
108            pcurve.swap_xy()
109        } else {
110            pcurve
111        })
112    }
113
114    /// The patch itself.
115    pub fn surface(&self) -> GeopResult<NurbSurface3D> {
116        let corner = |i: usize, j: usize| {
117            let x = if i == 1 {
118                self.lo[0].add(self.size[0])
119            } else {
120                self.lo[0]
121            };
122            let y = if j == 1 {
123                self.lo[1].add(self.size[1])
124            } else {
125                self.lo[1]
126            };
127            self.origin
128                .add(&self.e1.prod_scalar(x))
129                .add(&self.e2.prod_scalar(y))
130        };
131        if self.swapped {
132            NurbSurface3D::bilinear(corner(0, 0), corner(0, 1), corner(1, 1), corner(1, 0))
133        } else {
134            NurbSurface3D::bilinear(corner(0, 0), corner(1, 0), corner(1, 1), corner(0, 1))
135        }
136    }
137}
138
139/// The size of a patch side from `lo` that reaches `hi`. The corners are a
140/// free choice, so sharp; the size has to reach `hi`: `hi - lo` in plain
141/// numbers, unless that rounds so far down that `lo + size` falls
142/// definitely short of `hi`, and then the upper bound of `hi - lo`.
143fn reaching(lo: f64, hi: f64) -> Scalar {
144    let (from, to) = (Scalar::from_f64(lo), Scalar::from_f64(hi));
145    let plain = Scalar::from_f64(hi - lo);
146    if from.add(plain).definitely_less(to) {
147        to.sub(from).upper()
148    } else {
149        plain
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    /// A square reaching far one way along each axis and only a little the
158    /// other: its box's size in plain numbers, `0.001 - -3`, rounds below
159    /// what the box spans. The patch must still reach every point of it.
160    #[test]
161    fn patch_encloses_its_curves() {
162        let corners = [(-3.0, -3.0), (0.001, -3.0), (0.001, 0.001), (-3.0, 0.001)]
163            .map(|(x, y)| Vector2::from_array([Scalar::from_f64(x), Scalar::from_f64(y)]));
164        let sides: Vec<NurbCurve2D> = (0..4)
165            .map(|k| NurbCurve2D::line(corners[k], corners[(k + 1) % 4]).unwrap())
166            .collect();
167        let patch = FlatPatch::around(
168            Vector3::zero(),
169            Vector3::axis(0),
170            Vector3::axis(1),
171            &sides,
172            false,
173        )
174        .unwrap();
175        let reach = Scalar::from_f64(0.001);
176        let [_, far] = patch.reach();
177        for k in 0..2 {
178            assert!(
179                !far[k].definitely_less(reach),
180                "along axis {k} the patch reaches {:?}, short of the curves' {reach:?}",
181                far[k]
182            );
183        }
184    }
185}