Skip to main content

geop_core_geometry/nurb_curve/
translate.rs

1use geop_core_math::{
2    geop_error::GeopResult,
3    primitives::Motion,
4    scalars::Scalar,
5    vector::{Vector2, Vector3, Vector4},
6};
7
8use super::NurbCurve;
9use crate::aabb::compute_aabb;
10
11impl NurbCurve<4> {
12    /// This 3-D curve shifted by `offset` — same shape and parametrization,
13    /// every point moved by `offset`. Translates each (homogeneous) control
14    /// point by `offset * weight`, leaving weights and the knot vector
15    /// untouched.
16    pub fn translate(&self, offset: Vector3) -> Self {
17        let control_points: Vec<_> = self
18            .control_points
19            .iter()
20            .map(|cp| {
21                let w = cp[3];
22                geop_core_math::vector::Vector4::from_array([
23                    cp[0].add(offset[0].mul(w)),
24                    cp[1].add(offset[1].mul(w)),
25                    cp[2].add(offset[2].mul(w)),
26                    w,
27                ])
28            })
29            .collect();
30        let aabb = compute_aabb(&control_points);
31        Self {
32            degree: self.degree,
33            control_points,
34            knot_vector: self.knot_vector.clone(),
35            // Of the control points as moved: see `NurbSurface::translate`.
36            aabb,
37        }
38    }
39}
40
41impl NurbCurve<3> {
42    /// This 2-D curve shifted by `offset` (see [`NurbCurve::translate`] for
43    /// 3-D). Shifted by an exact zero, it is the curve itself, not widened
44    /// by adding zeros.
45    pub fn translate(&self, offset: Vector2) -> Self {
46        let zero = |x: Scalar| x.is_sharp() && x.could_be_equal(Scalar::ZERO);
47        if zero(offset[0]) && zero(offset[1]) {
48            return self.clone();
49        }
50        let control_points: Vec<_> = self
51            .control_points
52            .iter()
53            .map(|cp| {
54                let w = cp[2];
55                Vector3::from_array([cp[0].add(offset[0].mul(w)), cp[1].add(offset[1].mul(w)), w])
56            })
57            .collect();
58        let aabb = compute_aabb(&control_points);
59        Self {
60            degree: self.degree,
61            control_points,
62            knot_vector: self.knot_vector.clone(),
63            aabb,
64        }
65    }
66
67    /// This 2-D curve as a 3-D one in the plane `z = 0`.
68    pub fn lifted(&self) -> GeopResult<NurbCurve<4>> {
69        NurbCurve::try_new(
70            self.degree,
71            self.control_points
72                .iter()
73                .map(|q| Vector4::from_array([q[0], q[1], Scalar::ZERO, q[2]]))
74                .collect(),
75            self.knot_vector.clone(),
76        )
77    }
78
79    /// This 2-D curve as the planar 3-D curve `origin + x e1 + y e2` for
80    /// `(x, y)` along it: a sketch's curve where its plane puts it. An
81    /// affine map of the homogeneous control points `(w x, w y, w)` to
82    /// `(w origin + (w x) e1 + (w y) e2, w)`, so it is exact for any NURBS
83    /// and divides by no weight.
84    pub fn embed(&self, origin: &Vector3, e1: &Vector3, e2: &Vector3) -> GeopResult<NurbCurve<4>> {
85        let control_points: Vec<_> = self
86            .control_points
87            .iter()
88            .map(|cp| embed_point(cp, origin, e1, e2))
89            .collect();
90        NurbCurve::try_new(self.degree, control_points, self.knot_vector.clone())
91    }
92}
93
94/// Homogeneous `(w (origin + x e1 + y e2), w)` for the homogeneous 2-D point
95/// `cp = (w x, w y, w)`: linear in `cp`, so no division — the control point
96/// [`NurbCurve::embed`] makes of `cp`.
97pub fn embed_point(cp: &Vector3, origin: &Vector3, e1: &Vector3, e2: &Vector3) -> Vector4 {
98    let p = origin
99        .prod_scalar(cp[2])
100        .add(&e1.prod_scalar(cp[0]))
101        .add(&e2.prod_scalar(cp[1]));
102    Vector4::from_array([p[0], p[1], p[2], cp[2]])
103}
104
105/// The homogeneous 3-D point `cp = (w p, w)` in the plane through `origin`
106/// along the orthonormal `e1` and `e2`, homogeneous: `(w x, w y, w)` for
107/// `x = (p - origin) . e1`, `y = (p - origin) . e2` — the inverse of
108/// [`embed_point`] for a point of the plane, and like it without division.
109pub fn flatten_point(cp: &Vector4, origin: &Vector3, e1: &Vector3, e2: &Vector3) -> Vector3 {
110    let w = cp[3];
111    let d = Vector3::from_array([cp[0], cp[1], cp[2]]).sub(&origin.prod_scalar(w));
112    Vector3::from_array([d.prod_dot(e1), d.prod_dot(e2), w])
113}
114
115/// The homogeneous control point `cp = (w p, w)` moved by `motion`: an
116/// isometry is affine, so it goes to `(w (A p + t), w) = (A (w p) + w t, w)`
117/// without dividing by the weight. A weight of exactly one — every control
118/// point of a polynomial curve or patch — multiplies nothing: rounding
119/// `1 t` outward would only widen the point. For a translation `A` is the
120/// identity and is not applied either, so the point moves by one addition
121/// per coordinate.
122pub(crate) fn transform_control_point(cp: &Vector4, motion: &Motion) -> Vector4 {
123    let w = cp[3];
124    let p = motion.rotate(&Vector3::from_array([cp[0], cp[1], cp[2]]));
125    let mut t = motion.position();
126    if !(w.is_sharp() && w.to_f64() == 1.0) {
127        t = t.map(|x| x.mul(w));
128    }
129    Vector4::from_array([p[0].add(t[0]), p[1].add(t[1]), p[2].add(t[2]), w])
130}
131
132impl NurbCurve<4> {
133    /// This 3-D curve moved by `motion` — a rigid motion, a translation or
134    /// a mirror — with the same parametrization: each control point moved
135    /// (see [`transform_control_point`]), weights and knots untouched.
136    pub fn transform(&self, motion: &Motion) -> Self {
137        let mut curve = Self {
138            degree: self.degree,
139            control_points: self
140                .control_points
141                .iter()
142                .map(|cp| transform_control_point(cp, motion))
143                .collect(),
144            knot_vector: self.knot_vector.clone(),
145            aabb: self.aabb,
146        };
147        curve.recompute_aabb();
148        curve
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use geop_core_math::{
155        primitives::Pose,
156        scalars::Scalar,
157        vector::{Vector3, Vector4},
158    };
159
160    use super::super::NurbCurve3D;
161
162    #[test]
163    fn translate_shifts_evaluated_points() {
164        let curve = NurbCurve3D::try_new(
165            1,
166            vec![
167                geop_core_math::vector::Vector4::from_array([
168                    Scalar::ZERO,
169                    Scalar::ZERO,
170                    Scalar::ZERO,
171                    Scalar::ONE,
172                ]),
173                geop_core_math::vector::Vector4::from_array([
174                    Scalar::ONE,
175                    Scalar::ZERO,
176                    Scalar::ZERO,
177                    Scalar::ONE,
178                ]),
179            ],
180            vec![Scalar::ZERO, Scalar::ZERO, Scalar::ONE, Scalar::ONE],
181        )
182        .unwrap();
183        let offset = Vector3::from_array([
184            Scalar::from_f64(2.0),
185            Scalar::from_f64(3.0),
186            Scalar::from_f64(-1.0),
187        ]);
188        let shifted = curve.translate(offset);
189        for t in [0.0, 0.3, 1.0] {
190            let a = shifted.evaluate(Scalar::from_f64(t)).unwrap();
191            let b = curve.evaluate(Scalar::from_f64(t)).unwrap().add(&offset);
192            assert!(a.could_be_equal(&b));
193        }
194    }
195
196    /// A rational curve (a weight other than one) placed by a pose
197    /// evaluates to the pose applied to its points.
198    #[test]
199    fn transform_moves_evaluated_points() {
200        let curve = NurbCurve3D::try_new(
201            2,
202            vec![
203                Vector4::from_array([Scalar::ONE, Scalar::ZERO, Scalar::ZERO, Scalar::ONE]),
204                Vector4::from_array([
205                    Scalar::from_f64(0.5),
206                    Scalar::from_f64(0.5),
207                    Scalar::ZERO,
208                    Scalar::from_f64(0.5),
209                ]),
210                Vector4::from_array([Scalar::ZERO, Scalar::ONE, Scalar::ZERO, Scalar::ONE]),
211            ],
212            vec![
213                Scalar::ZERO,
214                Scalar::ZERO,
215                Scalar::ZERO,
216                Scalar::ONE,
217                Scalar::ONE,
218                Scalar::ONE,
219            ],
220        )
221        .unwrap();
222        let pose = Pose::from_euler(
223            Vector3::from_array([1.0, -2.0, 0.5].map(Scalar::from_f64)),
224            [30.0, -10.0, 75.0].map(Scalar::from_f64),
225        )
226        .unwrap();
227        let placed = curve.transform(&pose.motion());
228        for t in [0.0, 0.3, 0.8, 1.0] {
229            let a = placed.evaluate(Scalar::from_f64(t)).unwrap();
230            let b = pose
231                .motion()
232                .apply(&curve.evaluate(Scalar::from_f64(t)).unwrap());
233            assert!(a.could_be_equal(&b));
234        }
235    }
236}