Skip to main content

geop_core_topology/euler/
replace_face.rs

1use crate::{
2    FaceId, Model, argument_validation::validate_pcurve_start_and_end, boundary::BoundaryType,
3};
4use geop_core_geometry::nurb_surface::NurbSurface3D;
5use geop_core_math::{geop_error::GeopResult, scalars::Scalar};
6
7impl<S: Scalar> Model<S> {
8    // Swap `face_id`'s surface for `surface` — e.g. dropping in a real
9    // parameterization once a face's boundary rings are complete, in place
10    // of the placeholder `NurbSurface3D::everything()` it was built with.
11    // Every existing coedge's pcurve must still land on its edge's actual
12    // 3-D endpoints under the new surface, checked before anything is
13    // mutated, so a rejected swap leaves the model untouched.
14    pub fn replace_face(
15        self: &mut Model<S>,
16        face_id: FaceId,
17        surface: NurbSurface3D<S>,
18    ) -> GeopResult<()> {
19        let face = self.get_face(face_id)?.clone();
20        for boundary in face.boundaries() {
21            let BoundaryType::Loop(anchor) = boundary else {
22                continue;
23            };
24            let mut cursor = anchor;
25            loop {
26                let coedge = self.get_coedge(cursor)?.clone();
27                let start = self.coedge_start_vertex(cursor)?.clone();
28                let end = self.coedge_end_vertex(cursor)?.clone();
29                validate_pcurve_start_and_end(&surface, &coedge.pcurve, &start.point, &end.point)?;
30
31                cursor = coedge.next;
32                if cursor == anchor {
33                    break;
34                }
35            }
36        }
37
38        self.get_face_mut(face_id)?.surface = surface;
39        Ok(())
40    }
41}
42
43#[cfg(test)]
44mod tests {
45    use crate::{
46        Coedge, CoedgeGeometry, CoedgeId, Edge, Face, Model, Sense, ShellId, Vertex,
47        boundary::BoundaryType,
48    };
49    use geop_core_geometry::{
50        nurb_curve::{NurbCurve, NurbCurve2D, NurbCurve3D},
51        nurb_surface::NurbSurface3D,
52    };
53    use geop_core_math::{
54        for_all_scalars,
55        scalars::Scalar,
56        vector::{Vector3, Vector4},
57    };
58
59    fn line3<S: Scalar>(a: (f64, f64, f64), b: (f64, f64, f64)) -> NurbCurve3D<S> {
60        let p = |x: f64, y: f64, z: f64| {
61            Vector4::from_array([S::from_f64(x), S::from_f64(y), S::from_f64(z), S::ONE])
62        };
63        NurbCurve3D::try_new(
64            1,
65            vec![p(a.0, a.1, a.2), p(b.0, b.1, b.2)],
66            vec![S::ZERO, S::ZERO, S::ONE, S::ONE],
67        )
68        .unwrap()
69    }
70
71    /// A pcurve matching the unit-square bilinear surface built below:
72    /// `pcurve.evaluate(t) == (a + (b-a)*t, ...)` in `(u, v)`, exactly
73    /// tracing the 3-D straight line `a -> b` since the surface is planar
74    /// and bilinear.
75    fn line2<S: Scalar>(a: (f64, f64), b: (f64, f64)) -> NurbCurve2D<S> {
76        let p = |x: f64, y: f64| Vector3::from_array([S::from_f64(x), S::from_f64(y), S::ONE]);
77        NurbCurve::try_new(
78            1,
79            vec![p(a.0, a.1), p(b.0, b.1)],
80            vec![S::ZERO, S::ZERO, S::ONE, S::ONE],
81        )
82        .unwrap()
83    }
84
85    fn unit_square_surface<S: Scalar>() -> NurbSurface3D<S> {
86        let p =
87            |x: f64, y: f64| Vector4::from_array([S::from_f64(x), S::from_f64(y), S::ZERO, S::ONE]);
88        NurbSurface3D::try_new(
89            1,
90            1,
91            vec![p(0.0, 0.0), p(0.0, 1.0), p(1.0, 0.0), p(1.0, 1.0)],
92            vec![S::ZERO, S::ZERO, S::ONE, S::ONE],
93            vec![S::ZERO, S::ZERO, S::ONE, S::ONE],
94        )
95        .unwrap()
96    }
97
98    /// A triangular face on `NurbSurface3D::everything()` with corners
99    /// `(0,0,0) -> (1,0,0) -> (0,1,0)`, whose pcurves are consistent with
100    /// `unit_square_surface` (so swapping to it should succeed).
101    fn triangle_face_on_everything<S: Scalar>(model: &mut Model<S>) -> crate::FaceId {
102        let face_id = model.insert_face(Face {
103            surface: NurbSurface3D::everything(),
104            outer: BoundaryType::Vertex(crate::VertexId(0)),
105            holes: Vec::new(),
106            shell: ShellId(999),
107        });
108
109        let points_3d = [(0.0, 0.0, 0.0), (1.0, 0.0, 0.0), (0.0, 1.0, 0.0)];
110        let points_2d = [(0.0, 0.0), (1.0, 0.0), (0.0, 1.0)];
111        let n = points_3d.len();
112        let verts: Vec<_> = points_3d
113            .iter()
114            .map(|&(x, y, z)| {
115                model.insert_vertex(Vertex {
116                    point: Vector3::from_array([S::from_f64(x), S::from_f64(y), S::from_f64(z)]),
117                })
118            })
119            .collect();
120        let edges: Vec<_> = (0..n)
121            .map(|i| {
122                model.insert_edge(Edge {
123                    curve: line3(points_3d[i], points_3d[(i + 1) % n]),
124                    start_vertex: verts[i],
125                    end_vertex: verts[(i + 1) % n],
126                })
127            })
128            .collect();
129        let coedges: Vec<_> = (0..n)
130            .map(|i| {
131                model.insert_coedge(Coedge {
132                    geometry: CoedgeGeometry::Edge(edges[i]),
133                    sense: Sense::Forward,
134                    pcurve: line2(points_2d[i], points_2d[(i + 1) % n]),
135                    next: CoedgeId(0),
136                    prev: CoedgeId(0),
137                    face: face_id,
138                })
139            })
140            .collect();
141        for i in 0..n {
142            model.coedges.get_mut(&coedges[i]).unwrap().next = coedges[(i + 1) % n];
143            model.coedges.get_mut(&coedges[i]).unwrap().prev = coedges[(i + n - 1) % n];
144        }
145        model.faces.get_mut(&face_id).unwrap().outer = BoundaryType::Loop(coedges[0]);
146
147        face_id
148    }
149
150    fn check_replace_face_with_matching_surface_succeeds<S: Scalar>() {
151        let mut model = Model::<S>::new();
152        let face_id = triangle_face_on_everything(&mut model);
153        model.replace_face(face_id, unit_square_surface()).unwrap();
154        // Sharpness check: evaluating a pcurve endpoint now goes through the
155        // real surface, not `everything()`, and must still land exactly on
156        // its edge's vertex.
157        let p = model
158            .get_face(face_id)
159            .unwrap()
160            .surface
161            .evaluate(S::ONE, S::ZERO)
162            .unwrap();
163        assert!(p.could_be_equal(&Vector3::from_array([S::ONE, S::ZERO, S::ZERO])));
164    }
165    #[test]
166    fn replace_face_with_matching_surface_succeeds() {
167        for_all_scalars!(check_replace_face_with_matching_surface_succeeds);
168    }
169
170    fn check_replace_face_with_mismatched_surface_fails_and_does_not_mutate<S: Scalar>() {
171        let mut model = Model::<S>::new();
172        let face_id = triangle_face_on_everything(&mut model);
173
174        let p = |x: f64, y: f64| {
175            Vector4::from_array([S::from_f64(x), S::from_f64(y), S::from_f64(5.0), S::ONE])
176        };
177        let shifted_surface = NurbSurface3D::try_new(
178            1,
179            1,
180            vec![p(0.0, 0.0), p(0.0, 1.0), p(1.0, 0.0), p(1.0, 1.0)],
181            vec![S::ZERO, S::ZERO, S::ONE, S::ONE],
182            vec![S::ZERO, S::ZERO, S::ONE, S::ONE],
183        )
184        .unwrap();
185
186        assert!(model.replace_face(face_id, shifted_surface).is_err());
187        // Rejected swap must leave the original `everything()` surface in
188        // place, which evaluates to `ENTIRE` (equal to any point) anywhere.
189        let p_check = model
190            .get_face(face_id)
191            .unwrap()
192            .surface
193            .evaluate(S::ZERO, S::ZERO)
194            .unwrap();
195        assert!(p_check.could_be_equal(&Vector3::from_array([
196            S::from_f64(123.0),
197            S::from_f64(456.0),
198            S::from_f64(789.0)
199        ])));
200    }
201    #[test]
202    fn replace_face_with_mismatched_surface_fails_and_does_not_mutate() {
203        for_all_scalars!(check_replace_face_with_mismatched_surface_fails_and_does_not_mutate);
204    }
205}