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<S: Scalar, const D: usize> NurbSurface<S, 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<S> = 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<S, 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::for_all_scalars;
59    use geop_core_math::{scalars::Scalar, vector::Vector4};
60
61    /// A non-planar bilinear patch, so `u` and `v` are genuinely independent.
62    fn saddle<S: Scalar>() -> NurbSurface3D<S> {
63        let p = |x: f64, y: f64, z: f64| {
64            Vector4::from_array([S::from_f64(x), S::from_f64(y), S::from_f64(z), S::ONE])
65        };
66        NurbSurface3D::try_new(
67            1,
68            1,
69            vec![p(0., 0., 0.), p(0., 2., 1.), p(2., 0., 1.), p(2., 2., 0.)],
70            vec![S::ZERO, S::ZERO, S::ONE, S::ONE],
71            vec![S::ZERO, S::ZERO, S::ONE, S::ONE],
72        )
73        .unwrap()
74    }
75
76    /// The reversed surface passes through the same points, reached at the
77    /// mirrored `u`.
78    fn check_reverse_u_traces_the_same_surface<S: Scalar>() {
79        let s = saddle::<S>();
80        let r = s.reverse_u();
81        let (lo, hi) = s.domain_u();
82        assert!(r.domain_u().0.could_be_equal(lo) && r.domain_u().1.could_be_equal(hi));
83
84        for &(u, v) in &[(0.25, 0.4), (0.5, 0.5), (0.8, 0.1)] {
85            let (u, v) = (S::from_f64(u), S::from_f64(v));
86            let original = s.evaluate(u, v).unwrap();
87            let mirrored = r.evaluate(lo.add(hi).sub(u), v).unwrap();
88            for c in 0..3 {
89                assert!(
90                    original[c].could_be_equal(mirrored[c]),
91                    "coord {c}: {original:?} vs {mirrored:?}"
92                );
93            }
94        }
95    }
96    #[test]
97    fn reverse_u_traces_the_same_surface() {
98        for_all_scalars!(check_reverse_u_traces_the_same_surface);
99    }
100
101    /// …and its normal points the other way, which is the whole point.
102    fn check_reverse_u_flips_the_normal<S: Scalar>() {
103        let s = saddle::<S>();
104        let r = s.reverse_u();
105        let (lo, hi) = s.domain_u();
106        let (u, v) = (S::from_f64(0.3), S::from_f64(0.7));
107        let n0 = s.normal(u, v).unwrap();
108        let n1 = r.normal(lo.add(hi).sub(u), v).unwrap();
109        for c in 0..3 {
110            assert!(
111                n0[c].could_be_equal(n1[c].neg()),
112                "coord {c}: {n0:?} vs {n1:?}"
113            );
114        }
115    }
116    #[test]
117    fn reverse_u_flips_the_normal() {
118        for_all_scalars!(check_reverse_u_flips_the_normal);
119    }
120}