883 lines
34 KiB
Rust
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
|
|
}
|