Skip to content

Commit 9ec1dec

Browse files
committed
Miscellaneous improvements
1 parent 326e00c commit 9ec1dec

File tree

1 file changed

+44
-36
lines changed

1 file changed

+44
-36
lines changed

node-graph/gcore/src/vector/vector_attributes.rs

Lines changed: 44 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use crate::vector::vector_types::Vector;
44
use dyn_any::DynAny;
55
use glam::{DAffine2, DVec2};
66
use kurbo::{CubicBez, Line, PathSeg, QuadBez};
7+
use log::debug;
78
use std::collections::{HashMap, HashSet};
89
use std::hash::{Hash, Hasher};
910
use 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

Comments
 (0)