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 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 for point in &mut coedge.pcurve.control_points {
41 point[0] = span.mul(point[2]).sub(point[0]);
42 }
43 coedge.pcurve.recompute_aabb();
49
50 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 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 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 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 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 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}