Skip to main content

geop_core_geometry/nurb_surface/
translate.rs

1use geop_core_math::{primitives::Motion, vector::Vector3};
2
3use crate::{aabb::compute_aabb, nurb_curve::transform_control_point};
4
5use super::NurbSurface;
6
7impl NurbSurface<4> {
8    /// This surface shifted by `offset` — same shape and parametrization,
9    /// every point moved by `offset`. Translates each (homogeneous) control
10    /// point by `offset * weight`, leaving weights and knot vectors
11    /// untouched.
12    pub fn translate(&self, offset: Vector3) -> Self {
13        let control_points: Vec<_> = self
14            .control_points
15            .iter()
16            .map(|cp| {
17                let w = cp[3];
18                geop_core_math::vector::Vector4::from_array([
19                    cp[0].add(offset[0].mul(w)),
20                    cp[1].add(offset[1].mul(w)),
21                    cp[2].add(offset[2].mul(w)),
22                    w,
23                ])
24            })
25            .collect();
26        let aabb = compute_aabb(&control_points);
27        Self {
28            degree_u: self.degree_u,
29            degree_v: self.degree_v,
30            num_u: self.num_u,
31            num_v: self.num_v,
32            control_points,
33            knot_vector_u: self.knot_vector_u.clone(),
34            knot_vector_v: self.knot_vector_v.clone(),
35            // Of the control points as moved, rounding and all: the box
36            // moved by `offset` misses what rounding the move adds to a
37            // point, and is not the box the same points read back have.
38            aabb,
39        }
40    }
41
42    /// This surface moved by `motion` — a rigid motion, a translation or a
43    /// mirror — with the same parametrization, so every pcurve drawn on it
44    /// still traces the same patch: each control point moved (see
45    /// [`crate::nurb_curve::transform_control_point`]), weights and knots
46    /// untouched.
47    ///
48    /// A mirror turns `Su x Sv` against the mirrored surface's material
49    /// side: the normal of the result is the mirrored normal, negated. What
50    /// the surface bounds has to be turned around for that (see
51    /// `Model::transform_body`).
52    pub fn transform(&self, motion: &Motion) -> Self {
53        let mut surface = Self {
54            degree_u: self.degree_u,
55            degree_v: self.degree_v,
56            num_u: self.num_u,
57            num_v: self.num_v,
58            control_points: self
59                .control_points
60                .iter()
61                .map(|cp| transform_control_point(cp, motion))
62                .collect(),
63            knot_vector_u: self.knot_vector_u.clone(),
64            knot_vector_v: self.knot_vector_v.clone(),
65            aabb: self.aabb,
66        };
67        surface.recompute_aabb();
68        surface
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use geop_core_math::{primitives::Motion, scalars::Scalar, vector::Vector3};
75
76    use super::super::NurbSurface3D;
77
78    /// A saddle mirrored in a slanted plane: every point goes to its mirror
79    /// image, and the normal to the mirrored normal, negated.
80    #[test]
81    fn mirror_moves_points_and_turns_normals() {
82        let p = |x: f64, y: f64, z: f64| {
83            geop_core_math::vector::Vector4::from_array([
84                Scalar::from_f64(x),
85                Scalar::from_f64(y),
86                Scalar::from_f64(z),
87                Scalar::ONE,
88            ])
89        };
90        let surface = NurbSurface3D::try_new(
91            1,
92            1,
93            vec![p(0., 0., 0.), p(0., 2., 1.), p(2., 0., 1.), p(2., 2., 0.)],
94            vec![Scalar::ZERO, Scalar::ZERO, Scalar::ONE, Scalar::ONE],
95            vec![Scalar::ZERO, Scalar::ZERO, Scalar::ONE, Scalar::ONE],
96        )
97        .unwrap();
98        let v = |x: f64, y: f64, z: f64| Vector3::from_array([x, y, z].map(Scalar::from_f64));
99        let mirror = Motion::mirror(&v(1.0, 0.5, -0.25), &v(1.0, 2.0, -0.5)).unwrap();
100        let mirrored = surface.transform(&mirror);
101        let (u, w) = (Scalar::from_f64(0.3), Scalar::from_f64(0.7));
102        let a = mirrored.evaluate(u, w).unwrap();
103        let b = mirror.apply(&surface.evaluate(u, w).unwrap());
104        assert!(a.could_be_equal(&b), "{a:?} vs {b:?}");
105        let n = mirrored.normal(u, w).unwrap();
106        let m = mirror.rotate(&surface.normal(u, w).unwrap()).neg();
107        assert!(n.could_be_equal(&m), "{n:?} vs {m:?}");
108    }
109
110    /// A surface shifted holds the box of its control points as shifted,
111    /// rounding and all: the box a surface of the same points has — what
112    /// one kept and read back has — and one that encloses every point.
113    #[test]
114    fn a_surface_shifted_boxes_its_points_as_shifted() {
115        let p = |x: f64, y: f64, z: f64, w: f64| {
116            geop_core_math::vector::Vector4::from_array(
117                [x * w, y * w, z * w, w].map(Scalar::from_f64),
118            )
119        };
120        let surface = NurbSurface3D::try_new(
121            1,
122            1,
123            vec![
124                p(0.1, 0., 0.3, 0.7),
125                p(0., 2.2, 1., 1.3),
126                p(1.9, 0., 1., 0.9),
127                p(2., 2., 0.1, 1.),
128            ],
129            vec![Scalar::ZERO, Scalar::ZERO, Scalar::ONE, Scalar::ONE],
130            vec![Scalar::ZERO, Scalar::ZERO, Scalar::ONE, Scalar::ONE],
131        )
132        .unwrap();
133        let by = Vector3::from_array([1.7, -0.3, 1e3].map(Scalar::from_f64));
134        let shifted = surface.translate(by);
135        let mut boxed = shifted.clone();
136        boxed.recompute_aabb();
137        assert_eq!(shifted, boxed);
138    }
139
140    #[test]
141    fn translate_shifts_evaluated_points() {
142        let p = |x: f64, y: f64, z: f64| {
143            geop_core_math::vector::Vector4::from_array([
144                Scalar::from_f64(x),
145                Scalar::from_f64(y),
146                Scalar::from_f64(z),
147                Scalar::ONE,
148            ])
149        };
150        let surface = NurbSurface3D::try_new(
151            1,
152            1,
153            vec![
154                p(0.0, 0.0, 0.0),
155                p(0.0, 1.0, 0.0),
156                p(1.0, 0.0, 0.0),
157                p(1.0, 1.0, 0.0),
158            ],
159            vec![Scalar::ZERO, Scalar::ZERO, Scalar::ONE, Scalar::ONE],
160            vec![Scalar::ZERO, Scalar::ZERO, Scalar::ONE, Scalar::ONE],
161        )
162        .unwrap();
163        let offset = Vector3::from_array([
164            Scalar::from_f64(2.0),
165            Scalar::from_f64(3.0),
166            Scalar::from_f64(-1.0),
167        ]);
168        let shifted = surface.translate(offset);
169        let a = shifted
170            .evaluate(Scalar::from_f64(0.3), Scalar::from_f64(0.7))
171            .unwrap();
172        let b = surface
173            .evaluate(Scalar::from_f64(0.3), Scalar::from_f64(0.7))
174            .unwrap()
175            .add(&offset);
176        assert!(a.could_be_equal(&b));
177    }
178
179    /// A placed surface evaluates to the placed points of the original.
180    #[test]
181    fn place_moves_evaluated_points() {
182        use geop_core_math::{primitives::Pose, scalars::Scalar};
183        let p = |x: f64, y: f64, z: f64, w: f64| {
184            geop_core_math::vector::Vector4::from_array(
185                [x * w, y * w, z * w, w].map(Scalar::from_f64),
186            )
187        };
188        let surface = NurbSurface3D::try_new(
189            1,
190            1,
191            vec![
192                p(0.0, 0.0, 0.0, 1.0),
193                p(0.0, 1.0, 0.0, 2.0),
194                p(1.0, 0.0, 1.0, 1.0),
195                p(1.0, 1.0, 0.0, 1.0),
196            ],
197            vec![Scalar::ZERO, Scalar::ZERO, Scalar::ONE, Scalar::ONE],
198            vec![Scalar::ZERO, Scalar::ZERO, Scalar::ONE, Scalar::ONE],
199        )
200        .unwrap();
201        let pose = Pose::from_euler(
202            Vector3::from_array([1.0, -2.0, 0.5].map(Scalar::from_f64)),
203            [30.0, 10.0, -45.0].map(Scalar::from_f64),
204        )
205        .unwrap();
206        let placed = surface.transform(&pose.motion());
207        let (u, v) = (Scalar::from_f64(0.3), Scalar::from_f64(0.7));
208        let a = placed.evaluate(u, v).unwrap();
209        let b = pose.apply(&surface.evaluate(u, v).unwrap());
210        assert!(a.could_be_equal(&b));
211    }
212}