Skip to main content

geop_core_geometry/nurb_surface/
reverse.rs

1use geop_core_math::{scalars::Scalar, vector::Vector};
2
3use super::NurbSurface;
4
5impl<const D: usize> NurbSurface<D> {
6    /// Mirror the `u` parametrization: the point at `u` moves to
7    /// `u_lo + u_hi - u`, leaving `v` alone.
8    ///
9    /// The surface traces exactly the same set of points, but `Su` reverses,
10    /// so `Su x Sv` — the normal — flips. That is the only way to turn a
11    /// face's material side around in this kernel, since orientation lives in
12    /// the parametrization rather than in a flag on the face.
13    ///
14    /// Mirroring rather than merely reordering matters: the domain is
15    /// unchanged, so every pcurve drawn on this surface stays in range and
16    /// only needs the same mirror applied to its own `u` coordinate (see
17    /// `Model::reverse_face`).
18    pub fn reverse_u(&self) -> Self {
19        let (lo, hi) = self.domain_u();
20        let span = lo.add(hi);
21
22        // Knots run the other way and are mirrored about the domain, which
23        // keeps them non-decreasing and keeps the domain itself fixed.
24        let knot_vector_u: Vec<Scalar> = self
25            .knot_vector_u
26            .iter()
27            .rev()
28            .map(|&k| span.sub(k))
29            .collect();
30
31        // Control points are stored u-major, so reversing the u index is a
32        // reversal of whole rows of length `num_v`.
33        let mut control_points: Vec<Vector<D>> = Vec::with_capacity(self.control_points.len());
34        for i in (0..self.num_u).rev() {
35            for j in 0..self.num_v {
36                control_points.push(self.control_points[i * self.num_v + j]);
37            }
38        }
39
40        NurbSurface {
41            degree_u: self.degree_u,
42            degree_v: self.degree_v,
43            num_u: self.num_u,
44            num_v: self.num_v,
45            control_points,
46            knot_vector_u,
47            knot_vector_v: self.knot_vector_v.clone(),
48            // Same control points, just reordered — the bounding box they
49            // enclose is unchanged.
50            aabb: self.aabb,
51        }
52    }
53}
54
55#[cfg(test)]
56mod tests {
57    use crate::nurb_surface::NurbSurface3D;
58    use geop_core_math::{scalars::Scalar, vector::Vector4};
59
60    /// A non-planar bilinear patch, so `u` and `v` are genuinely independent.
61    fn saddle() -> NurbSurface3D {
62        let p = |x: f64, y: f64, z: f64| {
63            Vector4::from_array([
64                Scalar::from_f64(x),
65                Scalar::from_f64(y),
66                Scalar::from_f64(z),
67                Scalar::ONE,
68            ])
69        };
70        NurbSurface3D::try_new(
71            1,
72            1,
73            vec![p(0., 0., 0.), p(0., 2., 1.), p(2., 0., 1.), p(2., 2., 0.)],
74            vec![Scalar::ZERO, Scalar::ZERO, Scalar::ONE, Scalar::ONE],
75            vec![Scalar::ZERO, Scalar::ZERO, Scalar::ONE, Scalar::ONE],
76        )
77        .unwrap()
78    }
79
80    /// The reversed surface passes through the same points, reached at the
81    /// mirrored `u`.
82    #[test]
83    fn reverse_u_traces_the_same_surface() {
84        let s = saddle();
85        let r = s.reverse_u();
86        let (lo, hi) = s.domain_u();
87        assert!(r.domain_u().0.could_be_equal(lo) && r.domain_u().1.could_be_equal(hi));
88
89        for &(u, v) in &[(0.25, 0.4), (0.5, 0.5), (0.8, 0.1)] {
90            let (u, v) = (Scalar::from_f64(u), Scalar::from_f64(v));
91            let original = s.evaluate(u, v).unwrap();
92            let mirrored = r.evaluate(lo.add(hi).sub(u), v).unwrap();
93            for c in 0..3 {
94                assert!(
95                    original[c].could_be_equal(mirrored[c]),
96                    "coord {c}: {original:?} vs {mirrored:?}"
97                );
98            }
99        }
100    }
101
102    /// …and its normal points the other way, which is the whole point.
103    #[test]
104    fn reverse_u_flips_the_normal() {
105        let s = saddle();
106        let r = s.reverse_u();
107        let (lo, hi) = s.domain_u();
108        let (u, v) = (Scalar::from_f64(0.3), Scalar::from_f64(0.7));
109        let n0 = s.normal(u, v).unwrap();
110        let n1 = r.normal(lo.add(hi).sub(u), v).unwrap();
111        for c in 0..3 {
112            assert!(
113                n0[c].could_be_equal(n1[c].neg()),
114                "coord {c}: {n0:?} vs {n1:?}"
115            );
116        }
117    }
118}