fj_core/objects/kinds/
cycle.rs

1use fj_math::{Scalar, Winding};
2
3use crate::{
4    geometry::{Geometry, SurfacePath},
5    objects::{HalfEdge, ObjectSet},
6    storage::Handle,
7};
8
9/// A cycle of connected edges
10#[derive(Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
11pub struct Cycle {
12    half_edges: ObjectSet<HalfEdge>,
13}
14
15impl Cycle {
16    /// Create an instance of `Cycle`
17    pub fn new(half_edges: impl IntoIterator<Item = Handle<HalfEdge>>) -> Self {
18        let half_edges = half_edges.into_iter().collect();
19        Self { half_edges }
20    }
21
22    /// Access the edges that make up the cycle
23    pub fn half_edges(&self) -> &ObjectSet<HalfEdge> {
24        &self.half_edges
25    }
26
27    /// Indicate the cycle's winding, assuming a right-handed coordinate system
28    ///
29    /// Please note that this is not *the* winding of the cycle, only one of the
30    /// two possible windings, depending on the direction you look at the
31    /// surface that the cycle is defined on from.
32    pub fn winding(&self, geometry: &Geometry) -> Winding {
33        // The cycle could be made up of one or two circles. If that is the
34        // case, the winding of the cycle is determined by the winding of the
35        // first circle.
36        if self.half_edges.len() < 3 {
37            let first = self
38                .half_edges()
39                .iter()
40                .next()
41                .expect("Invalid cycle: expected at least one edge");
42
43            let [a, b] = first.boundary().inner;
44            let edge_direction_positive = a < b;
45
46            let circle = match geometry.of_half_edge(first).path {
47                SurfacePath::Circle(circle) => circle,
48                SurfacePath::Line(_) => unreachable!(
49                    "Invalid cycle: less than 3 edges, but not all are circles"
50                ),
51            };
52            let cross_positive = circle.a().cross2d(&circle.b()) > Scalar::ZERO;
53
54            if edge_direction_positive == cross_positive {
55                return Winding::Ccw;
56            } else {
57                return Winding::Cw;
58            }
59        }
60
61        // Now that we got the special case out of the way, we can treat the
62        // cycle as a polygon:
63        // https://stackoverflow.com/a/1165943
64
65        let mut sum = Scalar::ZERO;
66
67        for (a, b) in self.half_edges().pairs() {
68            let [a, b] = [a, b].map(|edge| edge.start_position());
69
70            sum += (b.u - a.u) * (b.v + a.v);
71        }
72
73        if sum > Scalar::ZERO {
74            return Winding::Cw;
75        }
76        if sum < Scalar::ZERO {
77            return Winding::Ccw;
78        }
79
80        unreachable!("Encountered invalid cycle: {self:#?}");
81    }
82}