fj_core/objects/kinds/
cycle.rs1use fj_math::{Scalar, Winding};
2
3use crate::{
4 geometry::{Geometry, SurfacePath},
5 objects::{HalfEdge, ObjectSet},
6 storage::Handle,
7};
8
9#[derive(Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
11pub struct Cycle {
12 half_edges: ObjectSet<HalfEdge>,
13}
14
15impl Cycle {
16 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 pub fn half_edges(&self) -> &ObjectSet<HalfEdge> {
24 &self.half_edges
25 }
26
27 pub fn winding(&self, geometry: &Geometry) -> Winding {
33 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 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}