Skip to main content

geop_core_topology/edit/
transform_body.rs

1use geop_core_geometry::{aabb::union_aabb, deform::Deformation};
2use geop_core_math::{
3    geop_error::{GeopError, GeopResult, WithContext},
4    vector::Vector3,
5};
6
7use crate::{
8    Body, Model, boundary::BoundaryType, contains::face::face_interior_point,
9    validation::ValidationParameters,
10};
11
12/// Seed of the ray casting that finds a point of a face for a map made of
13/// pieces, whose answer does not depend on it.
14const SEED: u64 = 0x7A5F_0D1E_2B3C_4D5E;
15
16impl Model {
17    /// Moves the whole of `body` by `map` in place — a rigid motion, a
18    /// translation, a mirror, a scaling
19    /// ([`geop_core_math::primitives::Motion`]) or a deformation about an
20    /// axis ([`geop_core_geometry::deform::AxialMap`]): every vertex, every
21    /// edge's curve and every face's surface. The pcurves stay as they are:
22    /// every map keeps the surfaces' parametrization, so each pcurve still
23    /// traces the same patch, now moved. Nothing is created or deleted, and
24    /// the body stays connected exactly as it was.
25    ///
26    /// A map made of pieces moves each edge and face by the piece it lies
27    /// in, asking for a point of it (the curve's middle, a point inside the
28    /// face) only where its control points straddle two pieces: the body
29    /// has to be divided first where it crosses from one to the next.
30    ///
31    /// A mirror turns every surface's normal into the body (see
32    /// [`geop_core_geometry::nurb_surface::NurbSurface::transform`]), so
33    /// it also turns every face around ([`Model::reverse_face`]): a mirrored
34    /// solid is a valid solid again, its normals pointing out. Each edge is
35    /// shared by two faces of the body, both turned, so their coedges stay
36    /// opposite.
37    ///
38    /// A translation moves every point by one addition per coordinate;
39    /// a rotation or a mirror encloses its result honestly, widening every
40    /// point by what its rounded matrix leaves open.
41    pub fn transform_body(
42        &mut self,
43        body: impl Into<Body>,
44        map: &dyn Deformation,
45    ) -> GeopResult<()> {
46        let body = body.into();
47        let ctx = |e: GeopError| e.with_context(format!("Model::transform_body(body={body})"));
48        let vertices: Vec<_> = self.iter_body_vertices(body).with_context(&ctx)?.collect();
49        let edges: Vec<_> = self.iter_body_edges(body).with_context(&ctx)?.collect();
50        let faces = self.body_faces(body).with_context(&ctx)?;
51        // Everything moved from the body as it is, then written.
52        let points = (vertices.iter())
53            .map(|v| map.point(&self.vertices[v].point))
54            .collect::<GeopResult<Vec<_>>>()
55            .with_context(&ctx)?;
56        let curves = (edges.iter())
57            .map(|&edge| {
58                let curve = &self.edges[&edge].curve;
59                let at = || curve.evaluate(curve.mid_parameter()?);
60                map.curve(curve, &at)
61                    .map_err(|e| e.with_context(format!("moving edge {edge}")))
62            })
63            .collect::<GeopResult<Vec<_>>>()
64            .with_context(&ctx)?;
65        let surfaces = (faces.iter())
66            .map(|&face| {
67                let whole = &self.faces[&face].surface;
68                // Cut down to the box in `(u, v)` around the face's outer
69                // loop, which the face lies in: the parametrization stays.
70                let outer = match self.faces[&face].outer {
71                    BoundaryType::Loop(anchor) => (self.iterate_loop_coedges(anchor))
72                        .map(|c| Ok(*self.get_coedge(c)?.pcurve.aabb()))
73                        .collect::<GeopResult<Vec<_>>>()?,
74                    BoundaryType::Vertex(_) => Vec::new(),
75                };
76                let restricted = match union_aabb(&outer) {
77                    Some(uv) if !map.affine() => Some(whole.sub_surface(
78                        (uv[0].lower(), uv[0].upper()),
79                        (uv[1].lower(), uv[1].upper()),
80                    )?),
81                    _ => None,
82                };
83                let surface = restricted.as_ref().unwrap_or(whole);
84                let at = || -> GeopResult<Vector3> {
85                    let params = ValidationParameters::default();
86                    let (u, v) = face_interior_point(
87                        self,
88                        face,
89                        params.max_nodes,
90                        params.min_subdivision_size,
91                        SEED,
92                    )?;
93                    surface.evaluate(u.sharpen(), v.sharpen())
94                };
95                map.surface(surface, &at)
96                    .map_err(|e| e.with_context(format!("moving face {face}")))
97            })
98            .collect::<GeopResult<Vec<_>>>()
99            .with_context(&ctx)?;
100        for (vertex, point) in vertices.into_iter().zip(points) {
101            self.get_vertex_mut(vertex).with_context(&ctx)?.point = point;
102        }
103        for (edge, curve) in edges.into_iter().zip(curves) {
104            self.get_edge_mut(edge).with_context(&ctx)?.curve = curve;
105        }
106        for (&face, surface) in faces.iter().zip(surfaces) {
107            self.get_face_mut(face).with_context(&ctx)?.surface = surface;
108        }
109        if map.mirrors() {
110            for face in faces {
111                self.reverse_face(face).with_context(&ctx)?;
112            }
113        }
114        Ok(())
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use geop_core_math::{
121        primitives::{Motion, Pose},
122        scalars::Scalar,
123        vector::Vector3,
124    };
125
126    use crate::{
127        Model,
128        test_fixtures::test_cube_solid,
129        validation::{ValidationParameters, validate},
130    };
131
132    fn v(x: f64, y: f64, z: f64) -> Vector3 {
133        Vector3::from_array([x, y, z].map(Scalar::from_f64))
134    }
135
136    fn assert_valid(model: &Model) {
137        if let Err(errors) = validate(&ValidationParameters::default(), model) {
138            let messages: Vec<String> = errors.iter().map(|e| format!("{e:?}")).collect();
139            panic!("{}", messages.join("\n"));
140        }
141    }
142
143    /// A translation moves every corner by exactly the offset: one
144    /// addition, nothing multiplied.
145    #[test]
146    fn translation_is_exact() {
147        let mut model = Model::new();
148        let cube = test_cube_solid(&mut model);
149        let before = model.clone();
150        let offset = v(2.0, -3.0, 0.5);
151        model
152            .transform_body(cube, &Motion::translation(offset))
153            .unwrap();
154        assert_valid(&model);
155        for (id, vertex) in &model.vertices {
156            let expected = before.vertices[id].point.add(&offset);
157            assert_eq!(
158                format!("{:?}", vertex.point),
159                format!("{expected:?}"),
160                "moved by one addition"
161            );
162        }
163    }
164
165    /// A rotation at an awkward angle about a slanted axis leaves a valid
166    /// solid, its corners where the pose puts them.
167    #[test]
168    fn rotated_cube_stays_valid() {
169        let mut model = Model::new();
170        let cube = test_cube_solid(&mut model);
171        let before = model.clone();
172        let axis = v(1.0, 2.0, 3.0).normalize().unwrap();
173        let pose =
174            Pose::rotation_about(&v(0.5, -1.0, 2.0), &axis, Scalar::from_f64(1.234)).unwrap();
175        model.transform_body(cube, &pose.motion()).unwrap();
176        assert_valid(&model);
177        for (id, vertex) in &model.vertices {
178            let expected = pose.apply(&before.vertices[id].point);
179            assert!(vertex.point.could_be_equal(&expected));
180        }
181    }
182
183    /// Mirrored, the cube is turned inside out unless its faces are turned
184    /// around: validation checks that every normal points out.
185    #[test]
186    fn mirrored_cube_stays_valid() {
187        let mut model = Model::new();
188        let cube = test_cube_solid(&mut model);
189        let before = model.clone();
190        let mirror = Motion::mirror(&v(2.0, 0.0, 0.0), &v(1.0, 0.0, 0.0)).unwrap();
191        model.transform_body(cube, &mirror).unwrap();
192        assert_valid(&model);
193        for (id, vertex) in &model.vertices {
194            let p = before.vertices[id].point;
195            let expected = v(4.0, 0.0, 0.0).add(&Vector3::from_array([p[0].neg(), p[1], p[2]]));
196            assert!(vertex.point.could_be_equal(&expected), "{vertex:?}");
197        }
198    }
199}