Lightningbeam/lightningbeam-ui/lightningbeam-core/src/planar_graph.rs

883 lines
34 KiB
Rust

//! Planar graph construction for paint bucket fill
//!
//! This module builds a planar graph from a collection of curves by:
//! 1. Finding all intersections (curve-curve and self-intersections)
//! 2. Splitting curves at intersection points to create graph edges
//! 3. Creating nodes at all intersection points and curve endpoints
//! 4. Connecting edges to form a complete planar graph
//!
//! The resulting graph can be used for face detection to identify regions for filling.
use crate::curve_intersections::{find_curve_intersections, find_self_intersections};
use crate::curve_segment::CurveSegment;
use std::collections::{HashMap, HashSet};
use vello::kurbo::{BezPath, CubicBez, Point};
/// A node in the planar graph (intersection point or endpoint)
#[derive(Debug, Clone)]
pub struct GraphNode {
/// Position of the node
pub position: Point,
/// Indices of edges connected to this node
pub edge_indices: Vec<usize>,
}
impl GraphNode {
pub fn new(position: Point) -> Self {
Self {
position,
edge_indices: Vec::new(),
}
}
}
/// An edge in the planar graph (curve segment between two nodes)
#[derive(Debug, Clone)]
pub struct GraphEdge {
/// Index of start node
pub start_node: usize,
/// Index of end node
pub end_node: usize,
/// Original curve ID
pub curve_id: usize,
/// Parameter at start of this edge on the original curve [0, 1]
pub t_start: f64,
/// Parameter at end of this edge on the original curve [0, 1]
pub t_end: f64,
}
impl GraphEdge {
pub fn new(
start_node: usize,
end_node: usize,
curve_id: usize,
t_start: f64,
t_end: f64,
) -> Self {
Self {
start_node,
end_node,
curve_id,
t_start,
t_end,
}
}
}
/// Planar graph structure
#[derive(Clone)]
pub struct PlanarGraph {
/// All nodes in the graph
pub nodes: Vec<GraphNode>,
/// All edges in the graph
pub edges: Vec<GraphEdge>,
/// Original curves (referenced by edges)
pub curves: Vec<CubicBez>,
}
impl PlanarGraph {
/// Build a planar graph from a collection of curve segments
///
/// # Arguments
///
/// * `curve_segments` - The input curve segments
///
/// # Returns
///
/// A complete planar graph with nodes at all intersections and edges connecting them
pub fn build(curve_segments: &[CurveSegment]) -> Self {
println!("PlanarGraph::build started with {} curves", curve_segments.len());
// Convert curve segments to cubic beziers
let curves: Vec<CubicBez> = curve_segments
.iter()
.map(|seg| seg.to_cubic_bez())
.collect();
// Find all intersection points
let intersections = Self::find_all_intersections(&curves);
println!("Found {} intersection points", intersections.len());
// Create nodes and edges
let (nodes, edges) = Self::build_nodes_and_edges(&curves, intersections);
println!("Created {} nodes and {} edges", nodes.len(), edges.len());
let mut graph = Self {
nodes,
edges,
curves,
};
// Prune dangling nodes
graph.prune_dangling_nodes();
graph
}
/// Find all intersections between curves
///
/// Returns a map from curve_id to sorted list of (t_value, point) intersections
fn find_all_intersections(curves: &[CubicBez]) -> HashMap<usize, Vec<(f64, Point)>> {
let mut intersections: HashMap<usize, Vec<(f64, Point)>> = HashMap::new();
// Initialize with endpoints for all curves
for (i, curve) in curves.iter().enumerate() {
let curve_intersections = vec![
(0.0, curve.p0),
(1.0, curve.p3),
];
intersections.insert(i, curve_intersections);
}
// Find curve-curve intersections
println!("Checking {} curve pairs for intersections...", (curves.len() * (curves.len() - 1)) / 2);
let mut total_intersections = 0;
for i in 0..curves.len() {
for j in (i + 1)..curves.len() {
let curve_i_intersections = find_curve_intersections(&curves[i], &curves[j]);
if !curve_i_intersections.is_empty() {
println!(" Curves {} and {} intersect at {} points:", i, j, curve_i_intersections.len());
for (idx, intersection) in curve_i_intersections.iter().enumerate() {
println!(" {} - t1={:.3}, t2={:.3}, point=({:.1}, {:.1})",
idx, intersection.t1, intersection.t2.unwrap_or(0.0),
intersection.point.x, intersection.point.y);
}
total_intersections += curve_i_intersections.len();
}
for intersection in curve_i_intersections {
// Add to curve i
intersections
.get_mut(&i)
.unwrap()
.push((intersection.t1, intersection.point));
// Add to curve j
if let Some(t2) = intersection.t2 {
intersections
.get_mut(&j)
.unwrap()
.push((t2, intersection.point));
}
}
}
// Find self-intersections
let self_intersections = find_self_intersections(&curves[i]);
for intersection in self_intersections {
intersections
.get_mut(&i)
.unwrap()
.push((intersection.t1, intersection.point));
if let Some(t2) = intersection.t2 {
intersections
.get_mut(&i)
.unwrap()
.push((t2, intersection.point));
}
}
}
println!("Total curve-curve intersections found: {}", total_intersections);
// Sort and deduplicate intersections for each curve
for curve_intersections in intersections.values_mut() {
curve_intersections.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
// Remove duplicates (points very close together)
let mut i = 0;
while i + 1 < curve_intersections.len() {
let dist = (curve_intersections[i].1 - curve_intersections[i + 1].1).hypot();
if dist < 0.5 {
curve_intersections.remove(i + 1);
} else {
i += 1;
}
}
}
intersections
}
/// Build nodes and edges from curves and their intersections
fn build_nodes_and_edges(
_curves: &[CubicBez],
intersections: HashMap<usize, Vec<(f64, Point)>>,
) -> (Vec<GraphNode>, Vec<GraphEdge>) {
let mut nodes = Vec::new();
let mut edges = Vec::new();
// Helper to get or create node at a position
// Uses distance-based deduplication with 0.5 pixel tolerance
const NODE_TOLERANCE: f64 = 0.5;
let get_or_create_node = |position: Point, nodes: &mut Vec<GraphNode>| -> usize {
// Check if there's already a node within tolerance
for (idx, node) in nodes.iter().enumerate() {
let dist = (position - node.position).hypot();
if dist < NODE_TOLERANCE {
return idx;
}
}
// No nearby node found, create new one
let node_idx = nodes.len();
nodes.push(GraphNode::new(position));
node_idx
};
// Create edges for each curve
for (curve_id, curve_intersections) in intersections.iter() {
// Create edges between consecutive intersection points
for i in 0..(curve_intersections.len() - 1) {
let (t_start, p_start) = curve_intersections[i];
let (t_end, p_end) = curve_intersections[i + 1];
// Get or create nodes
let start_node = get_or_create_node(p_start, &mut nodes);
let end_node = get_or_create_node(p_end, &mut nodes);
// Create edge
let edge_idx = edges.len();
edges.push(GraphEdge::new(
start_node,
end_node,
*curve_id,
t_start,
t_end,
));
// Add edge to nodes
nodes[start_node].edge_indices.push(edge_idx);
nodes[end_node].edge_indices.push(edge_idx);
}
}
(nodes, edges)
}
/// Prune dangling nodes (nodes with only one edge) from the graph
///
/// This is useful for cleaning up the graph structure by removing dead ends
/// that cannot be part of any face. Nodes are pruned iteratively until only
/// nodes that are part of face loops remain (or the graph becomes empty).
fn prune_dangling_nodes(&mut self) {
println!("Starting graph pruning...");
let mut iteration = 0;
loop {
// Find nodes with only 1 edge
let mut nodes_to_remove = Vec::new();
for (idx, node) in self.nodes.iter().enumerate() {
if node.edge_indices.len() == 1 {
nodes_to_remove.push(idx);
}
}
if nodes_to_remove.is_empty() {
println!("Pruning complete after {} iterations", iteration);
break;
}
iteration += 1;
println!("Pruning iteration {}: removing {} nodes", iteration, nodes_to_remove.len());
// Find edges connected to these nodes
let mut edges_to_remove = HashSet::new();
for &node_idx in &nodes_to_remove {
for &edge_idx in &self.nodes[node_idx].edge_indices {
edges_to_remove.insert(edge_idx);
}
}
// Remove the edges and nodes
// We need to rebuild the structure since indices change
// Create new nodes list (excluding removed ones)
let mut new_nodes = Vec::new();
let mut old_to_new_node: HashMap<usize, usize> = HashMap::new();
for (old_idx, node) in self.nodes.iter().enumerate() {
if !nodes_to_remove.contains(&old_idx) {
let new_idx = new_nodes.len();
old_to_new_node.insert(old_idx, new_idx);
new_nodes.push(node.clone());
}
}
// Create new edges list (excluding removed ones and updating node indices)
let mut new_edges = Vec::new();
for (old_idx, edge) in self.edges.iter().enumerate() {
if !edges_to_remove.contains(&old_idx) {
// Update node indices
if let (Some(&new_start), Some(&new_end)) =
(old_to_new_node.get(&edge.start_node), old_to_new_node.get(&edge.end_node)) {
let mut new_edge = edge.clone();
new_edge.start_node = new_start;
new_edge.end_node = new_end;
new_edges.push(new_edge);
}
}
}
// Rebuild edge_indices in nodes
for node in &mut new_nodes {
node.edge_indices.clear();
}
for (edge_idx, edge) in new_edges.iter().enumerate() {
new_nodes[edge.start_node].edge_indices.push(edge_idx);
new_nodes[edge.end_node].edge_indices.push(edge_idx);
}
// Update graph
self.nodes = new_nodes;
self.edges = new_edges;
println!("After pruning: {} nodes, {} edges", self.nodes.len(), self.edges.len());
}
}
/// Find all faces in the planar graph
pub fn find_faces(&self) -> Vec<Face> {
let mut faces = Vec::new();
let mut used_half_edges = HashSet::new();
println!("Finding faces: trying {} edges in both directions", self.edges.len());
// Try starting from each edge in both directions
for edge_idx in 0..self.edges.len() {
// Try forward direction
if !used_half_edges.contains(&(edge_idx, true)) {
if let Some(face) = self.trace_face(edge_idx, true, &mut used_half_edges) {
let start_edge = &self.edges[edge_idx];
print!("Successfully traced face {} starting from {} -> {} (edge {} fwd) with {} edges: ",
faces.len(), start_edge.start_node, start_edge.end_node, edge_idx, face.edges.len());
for (idx, (e, fwd)) in face.edges.iter().enumerate() {
let e_obj: &GraphEdge = &self.edges[*e];
let (n1, n2) = if *fwd {
(e_obj.start_node, e_obj.end_node)
} else {
(e_obj.end_node, e_obj.start_node)
};
print!("{} -> {}{}", n1, n2, if idx < face.edges.len() - 1 { " -> " } else { "" });
}
println!();
faces.push(face);
}
}
// Try backward direction
if !used_half_edges.contains(&(edge_idx, false)) {
if let Some(face) = self.trace_face(edge_idx, false, &mut used_half_edges) {
let start_edge = &self.edges[edge_idx];
print!("Successfully traced face {} starting from {} -> {} (edge {} bwd) with {} edges: ",
faces.len(), start_edge.end_node, start_edge.start_node, edge_idx, face.edges.len());
for (idx, (e, fwd)) in face.edges.iter().enumerate() {
let e_obj: &GraphEdge = &self.edges[*e];
let (n1, n2) = if *fwd {
(e_obj.start_node, e_obj.end_node)
} else {
(e_obj.end_node, e_obj.start_node)
};
print!("{} -> {}{}", n1, n2, if idx < face.edges.len() - 1 { " -> " } else { "" });
}
println!();
faces.push(face);
}
}
}
println!("Found {} faces", faces.len());
faces
}
/// Trace a face starting from an edge in a given direction
/// Returns None if the face is already traced or invalid
fn trace_face(
&self,
start_edge: usize,
forward: bool,
used_half_edges: &mut HashSet<(usize, bool)>,
) -> Option<Face> {
// Use a local set for this trace attempt
// Only add to global set if we successfully complete a face
let mut temp_used = HashSet::new();
let mut edge_sequence = Vec::new();
let mut visited_nodes = HashSet::new();
let mut current_edge = start_edge;
let mut current_forward = forward;
// Get start node info for logging
let start_edge_obj = &self.edges[start_edge];
let (start_node, start_end_node) = if forward {
(start_edge_obj.start_node, start_edge_obj.end_node)
} else {
(start_edge_obj.end_node, start_edge_obj.start_node)
};
println!("trace_face: Starting from node {} -> {} (edge {} {})",
start_node, start_end_node, start_edge, if forward { "fwd" } else { "bwd" });
// Mark the starting node as visited
visited_nodes.insert(start_node);
loop {
// Check if this half-edge is already used (globally or in this trace)
if used_half_edges.contains(&(current_edge, current_forward))
|| temp_used.contains(&(current_edge, current_forward)) {
// Already traced this half-edge
let current_edge_obj = &self.edges[current_edge];
let (curr_start, curr_end) = if current_forward {
(current_edge_obj.start_node, current_edge_obj.end_node)
} else {
(current_edge_obj.end_node, current_edge_obj.start_node)
};
println!("trace_face: Found already-used edge: {} -> {} (edge {} {}) after {} steps",
curr_start, curr_end, current_edge, if current_forward { "fwd" } else { "bwd" },
edge_sequence.len());
// Print the full edge sequence to understand the sub-cycle
print!(" Full sequence: ");
for (idx, (e, fwd)) in edge_sequence.iter().enumerate() {
let e_obj: &GraphEdge = &self.edges[*e];
let (n1, n2) = if *fwd {
(e_obj.start_node, e_obj.end_node)
} else {
(e_obj.end_node, e_obj.start_node)
};
print!("{} -> {}{}", n1, n2, if idx < edge_sequence.len() - 1 { " -> " } else { "" });
}
println!(" -> {} -> {} (already used)", curr_start, curr_end);
return None;
}
edge_sequence.push((current_edge, current_forward));
temp_used.insert((current_edge, current_forward));
// Get the end node of this half-edge
let edge = &self.edges[current_edge];
let end_node = if current_forward {
edge.end_node
} else {
edge.start_node
};
// Check if we've returned to the starting node - if so, we've completed the face!
if end_node == start_node && edge_sequence.len() >= 2 {
println!("trace_face: Completed cycle back to starting node {} after {} edges", start_node, edge_sequence.len());
// Success! Add all edges from this trace to the global used set
for &half_edge in &temp_used {
used_half_edges.insert(half_edge);
}
return Some(Face { edges: edge_sequence });
}
// Check if we've visited this end node before (it's not the start, so it's a self-intersection)
if visited_nodes.contains(&end_node) {
println!("trace_face: Detected node revisit at node {} - rejecting self-intersecting path", end_node);
return None;
}
// Mark this node as visited
visited_nodes.insert(end_node);
// Find the next edge in counterclockwise order around end_node
let next = self.find_next_ccw_edge(current_edge, current_forward, end_node);
if let Some((next_edge, next_forward)) = next {
current_edge = next_edge;
current_forward = next_forward;
// Continue to next iteration
} else {
// Dead end - not a valid face
println!("trace_face: Dead end at node {}", end_node);
return None;
}
// Safety check to prevent infinite loops
if edge_sequence.len() > self.edges.len() * 2 {
println!("Warning: Potential infinite loop detected in face tracing");
return None;
}
}
}
/// Find the next edge in counterclockwise order around a node
fn find_next_ccw_edge(
&self,
incoming_edge: usize,
incoming_forward: bool,
node_idx: usize,
) -> Option<(usize, bool)> {
let node = &self.nodes[node_idx];
// Get the reverse of the incoming direction (pointing back to where we came FROM)
// This way, angle 0 = going back, and we measure CCW turns from the incoming edge
let edge = &self.edges[incoming_edge];
let incoming_dir = if incoming_forward {
let start_pos = self.nodes[edge.start_node].position;
let end_pos = self.nodes[edge.end_node].position;
// Reverse: point from end back to start
(start_pos.x - end_pos.x, start_pos.y - end_pos.y)
} else {
let start_pos = self.nodes[edge.start_node].position;
let end_pos = self.nodes[edge.end_node].position;
// Reverse: point from start back to end
(end_pos.x - start_pos.x, end_pos.y - start_pos.y)
};
// Find all outgoing edges from this node
let mut candidates = Vec::new();
for &edge_idx in &node.edge_indices {
let edge = &self.edges[edge_idx];
// Check if this edge goes out from node_idx
if edge.start_node == node_idx {
// Forward direction
let end_pos = self.nodes[edge.end_node].position;
let node_pos = node.position;
let out_dir = (end_pos.x - node_pos.x, end_pos.y - node_pos.y);
candidates.push((edge_idx, true, out_dir));
}
if edge.end_node == node_idx {
// Backward direction
let start_pos = self.nodes[edge.start_node].position;
let node_pos = node.position;
let out_dir = (start_pos.x - node_pos.x, start_pos.y - node_pos.y);
candidates.push((edge_idx, false, out_dir));
}
}
// Debug: show incoming edge info
let incoming_edge_obj = &self.edges[incoming_edge];
let (inc_start, inc_end) = if incoming_forward {
(incoming_edge_obj.start_node, incoming_edge_obj.end_node)
} else {
(incoming_edge_obj.end_node, incoming_edge_obj.start_node)
};
println!(" find_next_ccw_edge at node {} (incoming: {} -> {}, edge {} {})",
node_idx, inc_start, inc_end, incoming_edge, if incoming_forward { "fwd" } else { "bwd" });
println!(" Available edges ({} candidates):", candidates.len());
// Find the edge with the largest CCW angle (rightmost turn for face tracing)
// Since incoming_dir points back to where we came from, the largest angle
// gives us the rightmost turn, which traces faces correctly.
let mut best_edge = None;
let mut best_angle = 0.0;
for &(edge_idx, forward, out_dir) in &candidates {
// Skip the edge we came from (in opposite direction)
if edge_idx == incoming_edge && forward == !incoming_forward {
println!(" Edge {} {} -> SKIP (reverse of incoming)", edge_idx, if forward { "fwd" } else { "bwd" });
continue;
}
// Compute angle from incoming to outgoing (counterclockwise)
let angle = angle_between_ccw(incoming_dir, out_dir);
// Get the destination node for this candidate
let cand_edge = &self.edges[edge_idx];
let dest_node = if forward { cand_edge.end_node } else { cand_edge.start_node };
println!(" Edge {} {} -> node {} (angle: {:.3} rad = {:.1}°){}",
edge_idx, if forward { "fwd" } else { "bwd" }, dest_node,
angle, angle.to_degrees(),
if angle > best_angle { " <- BEST" } else { "" });
if angle > best_angle {
best_angle = angle;
best_edge = Some((edge_idx, forward));
}
}
if best_edge.is_none() {
println!(" FAILED: No valid next edge found!");
}
best_edge
}
/// Find which face contains a given point
pub fn find_face_containing_point(&self, point: Point, faces: &[Face]) -> Option<usize> {
for (i, face) in faces.iter().enumerate() {
// Build polygon for debugging
let mut polygon_points = Vec::new();
for &(edge_idx, forward) in &face.edges {
let edge = &self.edges[edge_idx];
let node_idx = if forward { edge.start_node } else { edge.end_node };
polygon_points.push(self.nodes[node_idx].position);
}
// Calculate bounding box
let mut min_x = f64::MAX;
let mut max_x = f64::MIN;
let mut min_y = f64::MAX;
let mut max_y = f64::MIN;
for p in &polygon_points {
min_x = min_x.min(p.x);
max_x = max_x.max(p.x);
min_y = min_y.min(p.y);
max_y = max_y.max(p.y);
}
println!("Face {}: {} edges, {} points, bbox: ({:.1},{:.1}) to ({:.1},{:.1})",
i, face.edges.len(), polygon_points.len(), min_x, min_y, max_x, max_y);
if self.point_in_face(point, face) {
return Some(i);
}
}
None
}
/// Test if a point is inside a face using ray casting
fn point_in_face(&self, point: Point, face: &Face) -> bool {
// Build polygon from face edges
let mut polygon_points = Vec::new();
for &(edge_idx, forward) in &face.edges {
let edge = &self.edges[edge_idx];
let node_idx = if forward { edge.start_node } else { edge.end_node };
polygon_points.push(self.nodes[node_idx].position);
}
// Ray casting algorithm
point_in_polygon(point, &polygon_points)
}
/// Build a BezPath from a face using the actual curve segments
pub fn build_face_path(&self, face: &Face) -> BezPath {
use vello::kurbo::ParamCurve;
let mut path = BezPath::new();
let mut first = true;
for &(edge_idx, forward) in &face.edges {
let edge = &self.edges[edge_idx];
let orig_curve = &self.curves[edge.curve_id];
// Get the curve segment for this edge
let segment = if forward {
orig_curve.subsegment(edge.t_start..edge.t_end)
} else {
// Reverse the segment
orig_curve.subsegment(edge.t_end..edge.t_start)
};
if first {
path.move_to(segment.p0);
first = false;
}
// Add the curve segment
path.curve_to(segment.p1, segment.p2, segment.p3);
}
path.close_path();
path
}
/// Find the closest edge to a given point
///
/// Returns (edge_index, closest_point_on_edge, distance)
fn find_closest_edge_to_point(&self, point: Point) -> Option<(usize, Point, f64)> {
let mut best_edge = None;
let mut best_distance = f64::MAX;
let mut best_point = Point::ZERO;
for (edge_idx, edge) in self.edges.iter().enumerate() {
// Get the edge endpoints
let start_pos = self.nodes[edge.start_node].position;
let end_pos = self.nodes[edge.end_node].position;
// Compute closest point on line segment manually
// Vector from start to end
let dx = end_pos.x - start_pos.x;
let dy = end_pos.y - start_pos.y;
// Squared length of segment
let len_sq = dx * dx + dy * dy;
let closest = if len_sq < 1e-10 {
// Degenerate segment (start == end), closest point is start
start_pos
} else {
// Parameter t of closest point on line
let t = ((point.x - start_pos.x) * dx + (point.y - start_pos.y) * dy) / len_sq;
// Clamp t to [0, 1] to keep it on the segment
let t_clamped = t.max(0.0).min(1.0);
// Compute closest point
Point::new(
start_pos.x + t_clamped * dx,
start_pos.y + t_clamped * dy
)
};
let distance = (point - closest).hypot();
if distance < best_distance {
best_distance = distance;
best_point = closest;
best_edge = Some(edge_idx);
}
}
best_edge.map(|idx| (idx, best_point, best_distance))
}
/// Determine the starting node and direction for face traversal
///
/// Uses cross product to determine which side of the edge the click point is on,
/// then picks the traversal direction that keeps the point on the "inside" of the face.
///
/// Returns (starting_node, starting_edge, forward)
fn determine_face_traversal_start(&self, edge_idx: usize, click_point: Point) -> (usize, usize, bool) {
let edge = &self.edges[edge_idx];
let start_pos = self.nodes[edge.start_node].position;
let end_pos = self.nodes[edge.end_node].position;
// Vector along the edge (forward direction: start -> end)
let edge_vec = (end_pos.x - start_pos.x, end_pos.y - start_pos.y);
// Vector from edge start to click point
let to_point = (click_point.x - start_pos.x, click_point.y - start_pos.y);
// Cross product: positive if point is to the left of edge, negative if to the right
let cross = edge_vec.0 * to_point.1 - edge_vec.1 * to_point.0;
if cross > 0.0 {
// Point is to the left of the edge (when going start -> end)
// To keep the point on our right (inside the face), we should traverse start -> end
// This means starting from start_node and going forward
(edge.start_node, edge_idx, true)
} else {
// Point is to the right of the edge (when going start -> end)
// To keep the point on our right (inside the face), we should traverse end -> start
// This means starting from end_node and going backward
(edge.end_node, edge_idx, false)
}
}
/// Trace a single face from a click point
///
/// This is an optimized version that only traces the one face containing the click point,
/// rather than finding all faces in the graph.
///
/// Returns the face if successful, None if no valid face contains the click point
pub fn trace_face_from_point(&self, click_point: Point) -> Option<Face> {
println!("trace_face_from_point: Finding face containing {:?}", click_point);
// Step 1: Find closest edge to the click point
let (closest_edge_idx, closest_point, distance) = self.find_closest_edge_to_point(click_point)?;
println!(" Closest edge: {} at distance {:.2}, point: {:?}", closest_edge_idx, distance, closest_point);
// Step 2: Determine starting node and direction
let (start_node, start_edge, start_forward) = self.determine_face_traversal_start(closest_edge_idx, click_point);
println!(" Starting from node {}, edge {} {}", start_node, start_edge, if start_forward { "forward" } else { "backward" });
// Step 3: Trace the face using CCW traversal
let mut edge_sequence = Vec::new();
let mut visited_nodes = HashSet::new();
let mut current_edge = start_edge;
let mut current_forward = start_forward;
// Mark the starting node as visited
visited_nodes.insert(start_node);
loop {
// Add this edge to the sequence
edge_sequence.push((current_edge, current_forward));
// Get the end node of this half-edge
let edge = &self.edges[current_edge];
let end_node = if current_forward {
edge.end_node
} else {
edge.start_node
};
// Check if we've returned to the starting node - if so, we've completed the face!
if end_node == start_node && edge_sequence.len() >= 2 {
println!(" Completed face with {} edges", edge_sequence.len());
return Some(Face { edges: edge_sequence });
}
// Check if we've visited this end node before (not the start, so it's an error)
if visited_nodes.contains(&end_node) {
println!(" Error: Visited node {} twice before completing loop", end_node);
return None;
}
// Mark this node as visited
visited_nodes.insert(end_node);
// Find the next edge in counterclockwise order
let next = self.find_next_ccw_edge(current_edge, current_forward, end_node);
if let Some((next_edge, next_forward)) = next {
current_edge = next_edge;
current_forward = next_forward;
} else {
println!(" Dead end at node {}", end_node);
return None;
}
// Safety check to prevent infinite loops
if edge_sequence.len() > self.edges.len() {
println!(" Error: Potential infinite loop detected");
return None;
}
}
}
}
/// A face in the planar graph (bounded region)
#[derive(Debug, Clone)]
pub struct Face {
/// Sequence of (edge_index, is_forward) pairs that form the boundary
pub edges: Vec<(usize, bool)>,
}
/// Compute the counterclockwise angle from v1 to v2
fn angle_between_ccw(v1: (f64, f64), v2: (f64, f64)) -> f64 {
let angle1 = v1.1.atan2(v1.0);
let angle2 = v2.1.atan2(v2.0);
let mut diff = angle2 - angle1;
// Normalize to [0, 2π)
while diff < 0.0 {
diff += 2.0 * std::f64::consts::PI;
}
while diff >= 2.0 * std::f64::consts::PI {
diff -= 2.0 * std::f64::consts::PI;
}
diff
}
/// Test if a point is inside a polygon using ray casting
fn point_in_polygon(point: Point, polygon: &[Point]) -> bool {
let mut inside = false;
let n = polygon.len();
for i in 0..n {
let j = (i + 1) % n;
let pi = polygon[i];
let pj = polygon[j];
if ((pi.y > point.y) != (pj.y > point.y)) &&
(point.x < (pj.x - pi.x) * (point.y - pi.y) / (pj.y - pi.y) + pi.x) {
inside = !inside;
}
}
inside
}