1use geometry_coords::CoordinateScalar;
23use geometry_cs::CoordinateSystem;
24use geometry_model::{MultiPolygon, Polygon, Ring};
25use geometry_strategy::{AreaStrategy, ShoelaceArea};
26use geometry_trait::{Closure, Point as PointTrait, Ring as RingTrait};
27
28pub fn correct<G: Correct>(g: &mut G) {
33 g.correct();
34}
35
36#[doc(hidden)]
38pub trait Correct {
39 fn correct(&mut self);
40}
41
42fn fix_closure<P, const CW: bool, const CL: bool>(r: &mut Ring<P, CW, CL>)
45where
46 P: PointTrait + Copy,
47{
48 if r.0.len() <= 2 {
52 return;
53 }
54 let first = r.0[0];
55 let last = *r.0.last().unwrap();
56 let should_be_closed = matches!(r.closure(), Closure::Closed);
57 let already_closed = coords_equal(&first, &last);
58 match (should_be_closed, already_closed) {
59 (true, false) => r.0.push(first), (false, true) => {
61 r.0.pop(); }
63 _ => {}
64 }
65}
66
67fn coords_equal<P: PointTrait>(a: &P, b: &P) -> bool {
71 geometry_trait::fold_dims(true, a, |acc, _p, d| {
72 acc && match d {
73 0 => a.get::<0>() == b.get::<0>(),
74 1 => a.get::<1>() == b.get::<1>(),
75 2 => a.get::<2>() == b.get::<2>(),
76 3 => a.get::<3>() == b.get::<3>(),
77 _ => unreachable!("fold_dims caps at MAX_DIM"),
78 }
79 })
80}
81
82fn fix_orientation<P, const CW: bool, const CL: bool>(r: &mut Ring<P, CW, CL>, want_positive: bool)
84where
85 P: PointTrait,
86 ShoelaceArea: AreaStrategy<Ring<P, CW, CL>, Out = P::Scalar>,
87{
88 let a = ShoelaceArea.area(&*r);
89 let zero = <P::Scalar as CoordinateScalar>::ZERO;
90 let is_positive = a > zero;
91 let is_negative = a < zero;
92 if (want_positive && is_negative) || (!want_positive && is_positive) {
95 r.0.reverse();
96 }
97}
98
99impl<P, const CW: bool, const CL: bool> Correct for Ring<P, CW, CL>
100where
101 P: PointTrait + Copy,
102 P::Cs: CoordinateSystem,
103 ShoelaceArea: AreaStrategy<Ring<P, CW, CL>, Out = P::Scalar>,
104{
105 fn correct(&mut self) {
106 fix_closure(self);
107 fix_orientation(self, true);
115 }
116}
117
118impl<P, const CW: bool, const CL: bool> Correct for Polygon<P, CW, CL>
119where
120 P: PointTrait + Copy,
121 P::Cs: CoordinateSystem,
122 ShoelaceArea: AreaStrategy<Ring<P, CW, CL>, Out = P::Scalar>,
123{
124 fn correct(&mut self) {
125 fix_closure(&mut self.outer);
126 fix_orientation(&mut self.outer, true);
129 for inner in &mut self.inners {
130 fix_closure(inner);
131 fix_orientation(inner, false);
136 }
137 }
138}
139
140impl<Pg: Correct + geometry_trait::Polygon> Correct for MultiPolygon<Pg> {
141 fn correct(&mut self) {
142 for p in &mut self.0 {
143 p.correct();
144 }
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 #![allow(clippy::float_cmp, reason = "Areas are exact integer literals.")]
156
157 use super::correct;
158 use crate::area::ring_area;
159 use geometry_cs::Cartesian;
160 use geometry_model::{Point2D, Ring};
161
162 type P = Point2D<f64, Cartesian>;
163
164 #[test]
165 fn ccw_exterior_of_cw_ring_is_reversed() {
166 let mut r: Ring<P> = Ring::from_vec(vec![
169 P::new(0.0, 0.0),
170 P::new(2.0, 0.0),
171 P::new(2.0, 2.0),
172 P::new(0.0, 2.0),
173 P::new(0.0, 0.0),
174 ]);
175 assert!(ring_area(&r) < 0.0, "precondition: CCW ring is negative");
176 correct(&mut r);
177 assert_eq!(ring_area(&r), 4.0);
178 }
179
180 #[test]
181 fn already_correct_ring_is_unchanged() {
182 let mut r: Ring<P> = Ring::from_vec(vec![
185 P::new(0.0, 0.0),
186 P::new(0.0, 2.0),
187 P::new(2.0, 2.0),
188 P::new(2.0, 0.0),
189 P::new(0.0, 0.0),
190 ]);
191 assert_eq!(ring_area(&r), 4.0);
192 correct(&mut r);
193 assert_eq!(ring_area(&r), 4.0);
194 }
195
196 #[test]
197 fn two_point_ring_is_left_untouched() {
198 use geometry_trait::Ring as _;
202 let mut r: Ring<P> = Ring::from_vec(vec![P::new(0.0, 0.0), P::new(1.0, 1.0)]);
203 correct(&mut r);
204 assert_eq!(r.points().count(), 2, "2-point ring must stay 2 points");
205 }
206
207 #[test]
211 fn open_declared_ring_drops_redundant_closing_vertex() {
212 use geometry_trait::Ring as _;
213 let mut r: Ring<P, true, false> = Ring::from_vec(vec![
215 P::new(0.0, 0.0),
216 P::new(0.0, 2.0),
217 P::new(2.0, 2.0),
218 P::new(2.0, 0.0),
219 P::new(0.0, 0.0),
220 ]);
221 correct(&mut r);
222 assert_eq!(r.points().count(), 4);
224 assert_eq!(ring_area(&r), 4.0);
226 }
227
228 #[test]
231 fn multipolygon_corrects_each_member() {
232 use geometry_model::{MultiPolygon, Polygon};
233 let ccw_square = || {
234 Polygon::<P>::new(Ring::from_vec(vec![
235 P::new(0.0, 0.0),
236 P::new(2.0, 0.0),
237 P::new(2.0, 2.0),
238 P::new(0.0, 2.0),
239 P::new(0.0, 0.0),
240 ]))
241 };
242 let mut mpg: MultiPolygon<Polygon<P>> = MultiPolygon(vec![ccw_square(), ccw_square()]);
243 assert!(ring_area(&mpg.0[0].outer) < 0.0);
245 correct(&mut mpg);
246 assert_eq!(ring_area(&mpg.0[0].outer), 4.0);
247 assert_eq!(ring_area(&mpg.0[1].outer), 4.0);
248 }
249
250 #[test]
254 fn coords_equal_compares_the_third_ordinate() {
255 use geometry_model::Point3D;
256 use geometry_trait::Ring as _;
257 type P3 = Point3D<f64, Cartesian>;
258 let mut r: Ring<P3, true, false> = Ring::from_vec(vec![
262 P3::new(0.0, 0.0, 5.0),
263 P3::new(1.0, 0.0, 5.0),
264 P3::new(1.0, 1.0, 5.0),
265 P3::new(0.0, 0.0, 5.0),
266 ]);
267 let before = r.points().count();
271 super::fix_closure(&mut r);
272 assert_eq!(before, 4);
273 assert_eq!(r.points().count(), 3, "closing vertex dropped");
274
275 let mut open: Ring<P3, true, false> = Ring::from_vec(vec![
277 P3::new(0.0, 0.0, 5.0),
278 P3::new(1.0, 0.0, 5.0),
279 P3::new(1.0, 1.0, 5.0),
280 P3::new(0.0, 0.0, 9.0), ]);
282 super::fix_closure(&mut open);
283 assert_eq!(open.points().count(), 4, "z differs → not closed → kept");
284 }
285
286 #[test]
287 fn ccw_ring_correctly_wound_is_a_noop() {
288 let mut r: Ring<P, false> = Ring::from_vec(vec![
294 P::new(1.0, 0.0),
295 P::new(0.0, 1.0),
296 P::new(-1.0, 0.0),
297 P::new(0.0, -1.0),
298 P::new(1.0, 0.0),
299 ]);
300 assert_eq!(ring_area(&r), 2.0, "precondition: correctly wound");
301 correct(&mut r);
302 assert_eq!(ring_area(&r), 2.0, "correct() must be a no-op");
303 }
304
305 #[test]
306 fn ccw_ring_wrongly_wound_is_reversed() {
307 let mut r: Ring<P, false> = Ring::from_vec(vec![
310 P::new(1.0, 0.0),
311 P::new(0.0, -1.0),
312 P::new(-1.0, 0.0),
313 P::new(0.0, 1.0),
314 P::new(1.0, 0.0),
315 ]);
316 assert_eq!(ring_area(&r), -2.0, "precondition: wrongly wound");
317 correct(&mut r);
318 assert_eq!(ring_area(&r), 2.0);
319 }
320
321 #[test]
322 fn ccw_polygon_with_hole_correctly_wound_is_a_noop() {
323 use geometry_model::Polygon;
328 let outer: Ring<P, false> = Ring::from_vec(vec![
329 P::new(0.0, 0.0),
330 P::new(4.0, 0.0),
331 P::new(4.0, 4.0),
332 P::new(0.0, 4.0),
333 P::new(0.0, 0.0),
334 ]);
335 let hole: Ring<P, false> = Ring::from_vec(vec![
336 P::new(1.0, 1.0),
337 P::new(1.0, 2.0),
338 P::new(2.0, 2.0),
339 P::new(2.0, 1.0),
340 P::new(1.0, 1.0),
341 ]);
342 let mut pg: Polygon<P, false> = Polygon::new(outer);
343 pg.inners.push(hole);
344 assert_eq!(ring_area(&pg.outer), 16.0, "precondition: outer CCW-stored");
345 assert_eq!(
346 ring_area(&pg.inners[0]),
347 -1.0,
348 "precondition: hole CW-stored"
349 );
350 correct(&mut pg);
351 assert_eq!(ring_area(&pg.outer), 16.0, "outer must be untouched");
352 assert_eq!(ring_area(&pg.inners[0]), -1.0, "hole must be untouched");
353 }
354
355 #[test]
356 fn ccw_polygon_wrongly_wound_is_fixed() {
357 use geometry_model::Polygon;
360 let outer: Ring<P, false> = Ring::from_vec(vec![
361 P::new(0.0, 0.0),
362 P::new(0.0, 4.0),
363 P::new(4.0, 4.0),
364 P::new(4.0, 0.0),
365 P::new(0.0, 0.0),
366 ]);
367 let hole: Ring<P, false> = Ring::from_vec(vec![
368 P::new(1.0, 1.0),
369 P::new(2.0, 1.0),
370 P::new(2.0, 2.0),
371 P::new(1.0, 2.0),
372 P::new(1.0, 1.0),
373 ]);
374 let mut pg: Polygon<P, false> = Polygon::new(outer);
375 pg.inners.push(hole);
376 assert_eq!(ring_area(&pg.outer), -16.0, "precondition: outer wrong");
377 assert_eq!(ring_area(&pg.inners[0]), 1.0, "precondition: hole wrong");
378 correct(&mut pg);
379 assert_eq!(ring_area(&pg.outer), 16.0);
380 assert_eq!(ring_area(&pg.inners[0]), -1.0);
381 }
382}