@@ -4,6 +4,7 @@ use crate::vector::vector_types::Vector;
44use dyn_any:: DynAny ;
55use glam:: { DAffine2 , DVec2 } ;
66use kurbo:: { CubicBez , Line , PathSeg , QuadBez } ;
7+ use log:: debug;
78use std:: collections:: { HashMap , HashSet } ;
89use std:: hash:: { Hash , Hasher } ;
910use std:: iter:: zip;
@@ -1036,6 +1037,18 @@ impl Vector {
10361037 false
10371038 }
10381039
1040+ /// Compute signed area of a face using the shoelace formula.
1041+ /// Negative area indicates clockwise winding, positive indicates counterclockwise.
1042+ fn compute_signed_area ( & self , face : & FoundSubpath ) -> f64 {
1043+ let mut area = 0.0 ;
1044+ for edge in & face. edges {
1045+ let start_pos = self . point_domain . positions ( ) [ edge. start ] ;
1046+ let end_pos = self . point_domain . positions ( ) [ edge. end ] ;
1047+ area += ( end_pos. x - start_pos. x ) * ( end_pos. y + start_pos. y ) ;
1048+ }
1049+ area * 0.5
1050+ }
1051+
10391052 /// Find all minimal closed regions (faces) in a branching mesh vector.
10401053 pub fn find_closed_regions ( & self ) -> Vec < FoundSubpath > {
10411054 let mut regions = Vec :: new ( ) ;
@@ -1122,6 +1135,19 @@ impl Vector {
11221135 }
11231136 }
11241137
1138+ // Filter out outer (counterclockwise) faces, keeping only inner (clockwise) faces
1139+ regions. retain ( |face| {
1140+ // Always include 2-edge faces (floating point issues with area calculation)
1141+ if face. edges . len ( ) == 2 {
1142+ return true ;
1143+ }
1144+
1145+ let area = self . compute_signed_area ( face) ;
1146+ // Keep clockwise faces (negative area), exclude counterclockwise (positive area)
1147+ area < 0.0
1148+ } ) ;
1149+
1150+ debug ! ( "Found {} closed regions" , regions. len( ) ) ;
11251151 regions
11261152 }
11271153
@@ -1140,62 +1166,44 @@ impl Vector {
11401166 let target = from_point;
11411167 let mut prev_segment = start_segment;
11421168
1143- let mut iteration = 0 ;
11441169 let max_iterations = adjacency. len ( ) * 2 ;
11451170
11461171 // Follow the "rightmost" edge at each vertex to find minimal face
1147- loop {
1148- iteration += 1 ;
1172+ for _iteration in 1 ..=max_iterations {
1173+ let neighbors = adjacency . get ( & current ) ? ;
11491174
1150- if iteration > max_iterations {
1151- return None ;
1152- }
1175+ // Since neighbors are pre-sorted by angle, find the index of the edge we came from
1176+ // and take the next edge in the sorted circular list
1177+ let prev_index = neighbors . iter ( ) . position ( | ( seg , _next , _rev ) | * seg == prev_segment ) ? ;
11531178
1154- let neighbors = adjacency . get ( & current ) ? ;
1155- // Find the next edge in counterclockwise order (rightmost turn)
1156- let prev_direction = self . point_domain . positions ( ) [ current ] - self . point_domain . positions ( ) [ path . last ( ) ? . start ] ;
1179+ // The next edge in the sorted list is the minimal face edge (rightmost turn)
1180+ // Wrap around using modulo to handle circular list
1181+ let next = neighbors [ ( prev_index + 1 ) % neighbors . len ( ) ] ;
11571182
1158- let angle_between = |v1 : DVec2 , v2 : DVec2 | -> f64 {
1159- let angle = v2. y . atan2 ( v2. x ) - v1. y . atan2 ( v1. x ) ;
1160- if angle < 0.0 { angle + 2.0 * std:: f64:: consts:: PI } else { angle }
1161- } ;
1183+ let ( next_segment, next_point, next_reversed) = next;
11621184
1163- let next = neighbors. iter ( ) . filter ( |( seg, _next, _rev) | * seg != prev_segment) . min_by ( |a, b| {
1164- let dir_a = self . point_domain . positions ( ) [ a. 1 ] - self . point_domain . positions ( ) [ current] ;
1165- let dir_b = self . point_domain . positions ( ) [ b. 1 ] - self . point_domain . positions ( ) [ current] ;
1166- let angle_a = angle_between ( prev_direction, dir_a) ;
1167- let angle_b = angle_between ( prev_direction, dir_b) ;
1168- angle_a. partial_cmp ( & angle_b) . unwrap_or ( std:: cmp:: Ordering :: Equal )
1169- } ) ?;
1185+ // Check if we've created a cycle (might not be back to start)
1186+ if path. iter ( ) . any ( |e| e. end == next_point && e. id != next_segment) {
1187+ return None ;
1188+ }
11701189
1171- let ( next_segment, next_point, next_reversed) = * next ;
1190+ path . push ( HalfEdge :: new ( next_segment, current , next_point, next_reversed) ) ;
11721191
1192+ // Check if we've completed the face
11731193 if next_point == target {
1174- // Completed the cycle
1175- path. push ( HalfEdge :: new ( next_segment, current, next_point, next_reversed) ) ;
1176-
11771194 // Mark all half-edges as used
11781195 for edge in & path {
11791196 used_half_edges. insert ( ( edge. id , edge. reverse ) ) ;
11801197 }
1181-
11821198 return Some ( FoundSubpath :: new ( path) ) ;
11831199 }
11841200
1185- // Check if we've created a cycle (might not be back to start)
1186- if path. iter ( ) . any ( |e| e. end == next_point && e. id != next_segment) {
1187- return None ;
1188- }
1189-
1190- path. push ( HalfEdge :: new ( next_segment, current, next_point, next_reversed) ) ;
11911201 prev_segment = next_segment;
11921202 current = next_point;
1193-
1194- // Prevent infinite loops
1195- if path. len ( ) > adjacency. len ( ) {
1196- return None ;
1197- }
11981203 }
1204+
1205+ // If we exit the loop without returning, the path didn't close
1206+ None
11991207 }
12001208}
12011209
0 commit comments