Skip to main content

geop_core_topology/edit/
reverse_face.rs

1use geop_core_math::{
2    geop_error::{GeopError, GeopResult, WithContext},
3    scalars::Scalar,
4};
5
6use crate::{FaceId, Model, Sense};
7
8impl<S: Scalar> Model<S> {
9    /// Turn `face_id`'s material side around, so its normal points the other
10    /// way.
11    ///
12    /// Orientation in this kernel lives in the surface's parametrization —
13    /// there is no flag on a face — so flipping it means mirroring the
14    /// surface's `u` (see [`geop_core_geometry::nurb_surface::NurbSurface::reverse_u`])
15    /// and applying the identical mirror to every pcurve drawn on it. The
16    /// domain is unchanged by the mirror, so the trim loops still describe
17    /// the same region of the same surface; only `Su x Sv` reverses.
18    ///
19    /// Deliberately touches nothing but this face's surface and its own
20    /// pcurves. The 3-D curves, the edges, the vertices and every coedge on
21    /// the *other* side of those edges are untouched, which is what lets a
22    /// boolean flip one operand's faces while they stay glued to the other's
23    /// along shared edges.
24    pub fn reverse_face(&mut self, face_id: FaceId) -> GeopResult<()> {
25        let ctx = |e: GeopError| e.with_context(format!("Model::reverse_face(face={face_id})"));
26
27        let face = self.get_face(face_id).with_context(&ctx)?;
28        let (u_lo, u_hi) = face.surface.domain_u();
29        let span = u_lo.add(u_hi);
30        let reversed = face.surface.reverse_u();
31        self.get_face_mut(face_id).with_context(&ctx)?.surface = reversed;
32
33        for coedge_id in self.iterate_face_coedges(face_id).collect::<Vec<_>>() {
34            let coedge = self.get_coedge_mut(coedge_id).with_context(&ctx)?;
35            // A pcurve's control points are homogeneous `[u*w, v*w, w]`, so
36            // mirroring `u -> span - u` is `x -> span*w - x`. Applying it to
37            // the control points rather than to evaluated points keeps the
38            // curve exact: a mirror is affine, and an affine map of a NURBS
39            // curve is the same map applied to its control net.
40            for point in &mut coedge.pcurve.control_points {
41                point[0] = span.mul(point[2]).sub(point[0]);
42            }
43            // The mutation above bypasses every constructor that would
44            // otherwise keep the pcurve's cached bounding box (used by the
45            // intersection search's `aabb_could_overlap` prefilter) in
46            // sync — refresh it explicitly or it goes stale and starts
47            // pruning real overlaps involving this pcurve.
48            coedge.pcurve.recompute_aabb();
49
50            // Then reverse the loop: swap `next`/`prev`, run the pcurve the
51            // other way, and flip the sense.
52            //
53            // Mirroring `u` flips every loop's winding on its own — a
54            // counter-clockwise outer loop comes back clockwise — which puts
55            // the material on the *right* of each coedge instead of its left.
56            // Nothing structural notices (the loop still closes and its
57            // pcurves still join), but `splice_edge_into_face` reads winding
58            // to tell a new face from a new hole, and the debug renderer reads
59            // it to inset a trim curve inward, so a reversed face renders with
60            // its coedges outside it. Reversing the traversal flips the
61            // winding back, leaving only the normal reversed — which is the
62            // whole point of the operation.
63            //
64            // The three go together: reversing traversal without reversing the
65            // pcurves would break `pcurve_loop_continuity`, and without
66            // flipping the sense a coedge's start vertex would no longer be
67            // the vertex its pcurve now starts at.
68            std::mem::swap(&mut coedge.next, &mut coedge.prev);
69            coedge.pcurve = coedge.pcurve.reverse();
70            coedge.sense = match coedge.sense {
71                Sense::Forward => Sense::Reversed,
72                Sense::Reversed => Sense::Forward,
73            };
74        }
75        Ok(())
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use geop_core_geometry::{
82        nurb_curve::{NurbCurve, NurbCurve2D},
83        nurb_surface::NurbSurface3D,
84    };
85    use geop_core_math::{
86        for_all_scalars,
87        scalars::Scalar,
88        vector::{Vector3, Vector4},
89    };
90
91    use crate::{
92        Coedge, CoedgeGeometry, CoedgeId, Edge, Face, Model, Sense, ShellId, Vertex, VertexId,
93        boundary::BoundaryType,
94    };
95
96    /// A face whose surface is a saddle (so the normal genuinely varies) with
97    /// one triangular trim loop.
98    fn saddle_face<S: Scalar>(model: &mut Model<S>) -> crate::FaceId {
99        let p = |x: f64, y: f64, z: f64| {
100            Vector4::from_array([S::from_f64(x), S::from_f64(y), S::from_f64(z), S::ONE])
101        };
102        let surface = NurbSurface3D::try_new(
103            1,
104            1,
105            vec![p(0., 0., 0.), p(0., 2., 1.), p(2., 0., 1.), p(2., 2., 0.)],
106            vec![S::ZERO, S::ZERO, S::ONE, S::ONE],
107            vec![S::ZERO, S::ZERO, S::ONE, S::ONE],
108        )
109        .unwrap();
110        let face_id = model.insert_face(Face {
111            surface,
112            outer: BoundaryType::Vertex(VertexId(0)),
113            holes: Vec::new(),
114            shell: ShellId(999),
115        });
116
117        let corners = [(0.2, 0.2), (0.8, 0.2), (0.5, 0.8)];
118        let line2 = |a: (f64, f64), b: (f64, f64)| -> NurbCurve2D<S> {
119            NurbCurve::try_new(
120                1,
121                vec![
122                    Vector3::from_array([S::from_f64(a.0), S::from_f64(a.1), S::ONE]),
123                    Vector3::from_array([S::from_f64(b.0), S::from_f64(b.1), S::ONE]),
124                ],
125                vec![S::ZERO, S::ZERO, S::ONE, S::ONE],
126            )
127            .unwrap()
128        };
129        let coedges: Vec<CoedgeId> = (0..3)
130            .map(|i| {
131                let a = corners[i];
132                let b = corners[(i + 1) % 3];
133                let v0 = model.insert_vertex(Vertex {
134                    point: Vector3::from_array([S::from_f64(a.0), S::from_f64(a.1), S::ZERO]),
135                });
136                let v1 = model.insert_vertex(Vertex {
137                    point: Vector3::from_array([S::from_f64(b.0), S::from_f64(b.1), S::ZERO]),
138                });
139                let edge = model.insert_edge(Edge {
140                    curve: NurbCurve::try_new(
141                        1,
142                        vec![
143                            Vector4::from_array([
144                                S::from_f64(a.0),
145                                S::from_f64(a.1),
146                                S::ZERO,
147                                S::ONE,
148                            ]),
149                            Vector4::from_array([
150                                S::from_f64(b.0),
151                                S::from_f64(b.1),
152                                S::ZERO,
153                                S::ONE,
154                            ]),
155                        ],
156                        vec![S::ZERO, S::ZERO, S::ONE, S::ONE],
157                    )
158                    .unwrap(),
159                    start_vertex: v0,
160                    end_vertex: v1,
161                });
162                model.insert_coedge(Coedge {
163                    geometry: CoedgeGeometry::Edge(edge),
164                    sense: Sense::Forward,
165                    pcurve: line2(a, b),
166                    next: CoedgeId(0),
167                    prev: CoedgeId(0),
168                    face: face_id,
169                })
170            })
171            .collect();
172        for i in 0..3 {
173            model.coedges.get_mut(&coedges[i]).unwrap().next = coedges[(i + 1) % 3];
174            model.coedges.get_mut(&coedges[i]).unwrap().prev = coedges[(i + 2) % 3];
175        }
176        model.faces.get_mut(&face_id).unwrap().outer = BoundaryType::Loop(coedges[0]);
177        face_id
178    }
179
180    /// Reversing flips the normal while every trim loop still lands on the
181    /// same 3-D points — the face covers the same patch, facing the other way.
182    fn check_reverse_face_flips_normal_and_keeps_the_patch<S: Scalar>() {
183        let mut model = Model::<S>::new();
184        let face_id = saddle_face(&mut model);
185
186        let anchor = match model.get_face(face_id).unwrap().outer {
187            BoundaryType::Loop(a) => a,
188            BoundaryType::Vertex(_) => unreachable!(),
189        };
190        let sample_t = S::from_f64(0.3);
191        let before_uv = model
192            .get_coedge(anchor)
193            .unwrap()
194            .pcurve
195            .evaluate(sample_t)
196            .unwrap();
197        let surface_before = model.get_face(face_id).unwrap().surface.clone();
198        let before_point = surface_before.evaluate(before_uv[0], before_uv[1]).unwrap();
199        let before_normal = surface_before.normal(before_uv[0], before_uv[1]).unwrap();
200
201        model.reverse_face(face_id).unwrap();
202
203        // The pcurve now runs the other way (the loop's traversal is
204        // reversed), so the same point sits at the mirrored parameter.
205        let (t0, t1) = model.get_coedge(anchor).unwrap().pcurve.domain();
206        let after_uv = model
207            .get_coedge(anchor)
208            .unwrap()
209            .pcurve
210            .evaluate(t0.add(t1).sub(sample_t))
211            .unwrap();
212        let surface_after = &model.get_face(face_id).unwrap().surface;
213        let after_point = surface_after.evaluate(after_uv[0], after_uv[1]).unwrap();
214        let after_normal = surface_after.normal(after_uv[0], after_uv[1]).unwrap();
215
216        for c in 0..3 {
217            assert!(
218                before_point[c].could_be_equal(after_point[c]),
219                "coord {c}: the trim curve moved: {before_point:?} vs {after_point:?}"
220            );
221            assert!(
222                before_normal[c].could_be_equal(after_normal[c].neg()),
223                "coord {c}: the normal did not flip: {before_normal:?} vs {after_normal:?}"
224            );
225        }
226    }
227    #[test]
228    fn reverse_face_flips_normal_and_keeps_the_patch() {
229        for_all_scalars!(check_reverse_face_flips_normal_and_keeps_the_patch);
230    }
231
232    /// Reversing a face must not change which side of its loops the material
233    /// is on. The kernel's convention is that an outer loop runs
234    /// counter-clockwise in `(u, v)` and a hole runs clockwise — that is what
235    /// `splice_edge_into_face` uses to tell a new face from a new hole, and
236    /// what the debug renderer uses to inset a coedge's trim curve *inward*.
237    ///
238    /// Mirroring `u` flips the winding on its own, so reversing has to undo
239    /// that by also reversing each loop's traversal. Without it the outer loop
240    /// comes back clockwise: still continuous, still structurally valid, but
241    /// with every coedge now running with the material on its right.
242    fn check_reverse_face_preserves_loop_winding<S: Scalar>() {
243        let mut model = Model::<S>::new();
244        let face_id = saddle_face(&mut model);
245
246        let area = |model: &Model<S>| {
247            let BoundaryType::Loop(anchor) = model.get_face(face_id).unwrap().outer else {
248                unreachable!()
249            };
250            let polygon = crate::loop_sampling::sample_loop_to_polygon(model, anchor, 8).unwrap();
251            geop_core_math::polygon::polygon_signed_area(&polygon)
252        };
253
254        let before = area(&model);
255        model.reverse_face(face_id).unwrap();
256        let after = area(&model);
257
258        assert!(
259            before.definitely_greater(S::ZERO) == after.definitely_greater(S::ZERO)
260                && before.definitely_less(S::ZERO) == after.definitely_less(S::ZERO),
261            "winding flipped: signed area went from {before:?} to {after:?}"
262        );
263    }
264    #[test]
265    fn reverse_face_preserves_loop_winding() {
266        for_all_scalars!(check_reverse_face_preserves_loop_winding);
267    }
268
269    /// Reversing twice is the identity.
270    fn check_reverse_face_twice_is_identity<S: Scalar>() {
271        let mut model = Model::<S>::new();
272        let face_id = saddle_face(&mut model);
273        let anchor = match model.get_face(face_id).unwrap().outer {
274            BoundaryType::Loop(a) => a,
275            BoundaryType::Vertex(_) => unreachable!(),
276        };
277        let t = S::from_f64(0.4);
278        let before = model
279            .get_coedge(anchor)
280            .unwrap()
281            .pcurve
282            .evaluate(t)
283            .unwrap();
284        let before_sense = model.get_coedge(anchor).unwrap().sense;
285
286        model.reverse_face(face_id).unwrap();
287        model.reverse_face(face_id).unwrap();
288
289        let after = model
290            .get_coedge(anchor)
291            .unwrap()
292            .pcurve
293            .evaluate(t)
294            .unwrap();
295        for c in 0..2 {
296            assert!(before[c].could_be_equal(after[c]), "coord {c} drifted");
297        }
298        assert_eq!(
299            before_sense,
300            model.get_coedge(anchor).unwrap().sense,
301            "sense must come back to where it started"
302        );
303    }
304    #[test]
305    fn reverse_face_twice_is_identity() {
306        for_all_scalars!(check_reverse_face_twice_is_identity);
307    }
308}