prune paint bucket node graph

This commit is contained in:
Skyler Lehmkuhl 2025-11-19 02:45:38 -05:00
parent e1d9514472
commit 71f9283356
1 changed files with 119 additions and 38 deletions

View File

@ -102,11 +102,16 @@ impl PlanarGraph {
let (nodes, edges) = Self::build_nodes_and_edges(&curves, intersections); let (nodes, edges) = Self::build_nodes_and_edges(&curves, intersections);
println!("Created {} nodes and {} edges", nodes.len(), edges.len()); println!("Created {} nodes and {} edges", nodes.len(), edges.len());
Self { let mut graph = Self {
nodes, nodes,
edges, edges,
curves, curves,
} };
// Prune dangling nodes
graph.prune_dangling_nodes();
graph
} }
/// Find all intersections between curves /// Find all intersections between curves
@ -189,25 +194,22 @@ impl PlanarGraph {
let mut nodes = Vec::new(); let mut nodes = Vec::new();
let mut edges = Vec::new(); let mut edges = Vec::new();
// Map from position to node index (to avoid duplicate nodes)
let mut position_to_node: HashMap<(i32, i32), usize> = HashMap::new();
// Helper to get or create node at a position // Helper to get or create node at a position
let mut get_or_create_node = |position: Point, // Uses distance-based deduplication with 0.5 pixel tolerance
nodes: &mut Vec<GraphNode>, const NODE_TOLERANCE: f64 = 0.5;
position_to_node: &mut HashMap<(i32, i32), usize>| let get_or_create_node = |position: Point, nodes: &mut Vec<GraphNode>| -> usize {
-> usize { // Check if there's already a node within tolerance
// Round to nearest pixel for lookup for (idx, node) in nodes.iter().enumerate() {
let key = (position.x.round() as i32, position.y.round() as i32); let dist = (position - node.position).hypot();
if dist < NODE_TOLERANCE {
if let Some(&node_idx) = position_to_node.get(&key) { return idx;
node_idx }
} else {
let node_idx = nodes.len();
nodes.push(GraphNode::new(position));
position_to_node.insert(key, node_idx);
node_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 // Create edges for each curve
@ -218,8 +220,8 @@ impl PlanarGraph {
let (t_end, p_end) = curve_intersections[i + 1]; let (t_end, p_end) = curve_intersections[i + 1];
// Get or create nodes // Get or create nodes
let start_node = get_or_create_node(p_start, &mut nodes, &mut position_to_node); let start_node = get_or_create_node(p_start, &mut nodes);
let end_node = get_or_create_node(p_end, &mut nodes, &mut position_to_node); let end_node = get_or_create_node(p_end, &mut nodes);
// Create edge // Create edge
let edge_idx = edges.len(); let edge_idx = edges.len();
@ -240,6 +242,88 @@ impl PlanarGraph {
(nodes, edges) (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());
}
}
/// Render debug visualization of the planar graph /// Render debug visualization of the planar graph
/// ///
/// Returns two shapes: one for nodes (red circles) and one for edges (yellow lines) /// Returns two shapes: one for nodes (red circles) and one for edges (yellow lines)
@ -279,25 +363,18 @@ impl PlanarGraph {
/// Find all faces in the planar graph /// Find all faces in the planar graph
pub fn find_faces(&self) -> Vec<Face> { pub fn find_faces(&self) -> Vec<Face> {
// Debug: Print graph structure
println!("\n=== GRAPH STRUCTURE DEBUG ===");
for (node_idx, node) in self.nodes.iter().enumerate() {
println!("Node {}: pos=({:.1}, {:.1}), edges={:?}",
node_idx, node.position.x, node.position.y, node.edge_indices);
}
for (edge_idx, edge) in self.edges.iter().enumerate() {
println!("Edge {}: {} -> {}", edge_idx, edge.start_node, edge.end_node);
}
println!("=== END GRAPH STRUCTURE ===\n");
let mut faces = Vec::new(); let mut faces = Vec::new();
let mut used_half_edges = HashSet::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 // Try starting from each edge in both directions
for edge_idx in 0..self.edges.len() { for edge_idx in 0..self.edges.len() {
// Try forward direction // Try forward direction
if !used_half_edges.contains(&(edge_idx, true)) { if !used_half_edges.contains(&(edge_idx, true)) {
if let Some(face) = self.trace_face(edge_idx, true, &mut used_half_edges) { if let Some(face) = self.trace_face(edge_idx, true, &mut used_half_edges) {
println!("Successfully traced face {} starting from edge {} fwd with {} edges",
faces.len(), edge_idx, face.edges.len());
faces.push(face); faces.push(face);
} }
} }
@ -305,6 +382,8 @@ impl PlanarGraph {
// Try backward direction // Try backward direction
if !used_half_edges.contains(&(edge_idx, false)) { if !used_half_edges.contains(&(edge_idx, false)) {
if let Some(face) = self.trace_face(edge_idx, false, &mut used_half_edges) { if let Some(face) = self.trace_face(edge_idx, false, &mut used_half_edges) {
println!("Successfully traced face {} starting from edge {} bwd with {} edges",
faces.len(), edge_idx, face.edges.len());
faces.push(face); faces.push(face);
} }
} }
@ -357,6 +436,8 @@ impl PlanarGraph {
} }
} else { } else {
// Dead end - not a valid face // Dead end - not a valid face
println!("trace_face: Dead end at node {} (from edge {} {})",
end_node, current_edge, if current_forward { "fwd" } else { "bwd" });
return None; return None;
} }
@ -412,22 +493,18 @@ impl PlanarGraph {
} }
} }
println!("find_next_ccw_edge: node {} has {} candidates", node_idx, candidates.len());
// Find the edge that makes the smallest left turn (most counterclockwise) // Find the edge that makes the smallest left turn (most counterclockwise)
let mut best_edge = None; let mut best_edge = None;
let mut best_angle = std::f64::MAX; let mut best_angle = std::f64::MAX;
for (edge_idx, forward, out_dir) in candidates { for &(edge_idx, forward, out_dir) in &candidates {
// Skip the edge we came from (in opposite direction) // Skip the edge we came from (in opposite direction)
if edge_idx == incoming_edge && forward == !incoming_forward { if edge_idx == incoming_edge && forward == !incoming_forward {
println!(" Skipping edge {} (came from there)", edge_idx);
continue; continue;
} }
// Compute angle from incoming to outgoing (counterclockwise) // Compute angle from incoming to outgoing (counterclockwise)
let angle = angle_between_ccw(incoming_dir, out_dir); let angle = angle_between_ccw(incoming_dir, out_dir);
println!(" Edge {} dir={} angle={}", edge_idx, if forward { "fwd" } else { "bwd" }, angle);
if angle < best_angle { if angle < best_angle {
best_angle = angle; best_angle = angle;
@ -435,7 +512,11 @@ impl PlanarGraph {
} }
} }
println!(" Best: {:?} angle={}", best_edge, best_angle); if best_edge.is_none() {
println!("find_next_ccw_edge FAILED at node {}: {} candidates total, incoming edge {} {}, found no valid next edge",
node_idx, candidates.len(), incoming_edge, if incoming_forward { "fwd" } else { "bwd" });
}
best_edge best_edge
} }