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