paint bucket mostly working

This commit is contained in:
Skyler Lehmkuhl 2025-11-19 09:01:27 -05:00
parent 502bae0947
commit 08f3c30b29
3 changed files with 213 additions and 103 deletions

View File

@ -69,7 +69,46 @@ impl PaintBucketAction {
impl Action for PaintBucketAction { impl Action for PaintBucketAction {
fn execute(&mut self, document: &mut Document) { fn execute(&mut self, document: &mut Document) {
println!("=== PaintBucketAction::execute (Planar Graph Approach) ==="); println!("=== PaintBucketAction::execute ===");
// Optimization: Check if we're clicking on an existing shape first
// This is much faster than building a planar graph
if let Some(AnyLayer::Vector(vector_layer)) = document.get_layer_mut(&self.layer_id) {
// Iterate through objects in reverse order (topmost first)
for object in vector_layer.objects.iter().rev() {
// Find the corresponding shape
if let Some(shape) = vector_layer.shapes.iter().find(|s| s.id == object.shape_id) {
// Apply the object's transform to get the transformed path
let transform_affine = object.transform.to_affine();
// Transform the click point to shape's local coordinates (inverse transform)
let inverse_transform = transform_affine.inverse();
let local_point = inverse_transform * self.click_point;
// Test if the local point is inside the shape using winding number
use vello::kurbo::Shape as KurboShape;
let winding = shape.path().winding(local_point);
if winding != 0 {
// Point is inside this shape! Just change its fill color
println!("Clicked on existing shape, changing fill color");
// Store the shape ID before the immutable borrow ends
let shape_id = shape.id;
// Find mutable reference to the shape and update its fill
if let Some(shape_mut) = vector_layer.shapes.iter_mut().find(|s| s.id == shape_id) {
shape_mut.fill_color = Some(self.fill_color);
println!("Updated shape fill color");
}
return; // Done! No need to create a new shape
}
}
}
println!("No existing shape at click point, creating new fill region");
}
// Step 1: Extract curves from all shapes (rectangles, ellipses, paths, etc.) // Step 1: Extract curves from all shapes (rectangles, ellipses, paths, etc.)
let all_curves = extract_curves_from_all_shapes(document, &self.layer_id); let all_curves = extract_curves_from_all_shapes(document, &self.layer_id);
@ -85,41 +124,22 @@ impl Action for PaintBucketAction {
println!("Building planar graph..."); println!("Building planar graph...");
let graph = PlanarGraph::build(&all_curves); let graph = PlanarGraph::build(&all_curves);
// Store graph for debug visualization // Step 3: Trace the face containing the click point (optimized - only traces one face)
if let Ok(mut debug_graph) = crate::planar_graph::DEBUG_GRAPH.lock() { println!("Tracing face from click point {:?}...", self.click_point);
*debug_graph = Some(graph.clone()); if let Some(face) = graph.trace_face_from_point(self.click_point) {
} println!("Successfully traced face containing click point!");
// Step 3: Render debug visualization of planar graph
println!("Rendering planar graph debug visualization...");
let (nodes_shape, edges_shape) = graph.render_debug();
let nodes_object = Object::new(nodes_shape.id);
let edges_object = Object::new(edges_shape.id);
if let Some(AnyLayer::Vector(vector_layer)) = document.get_layer_mut(&self.layer_id) {
vector_layer.add_shape_internal(edges_shape);
vector_layer.add_object_internal(edges_object);
vector_layer.add_shape_internal(nodes_shape);
vector_layer.add_object_internal(nodes_object);
println!("DEBUG: Added graph visualization (yellow=edges, red=nodes)");
}
// Step 4: Find all faces
println!("Finding faces in planar graph...");
let faces = graph.find_faces();
// Step 5: Find which face contains the click point
println!("Finding face containing click point {:?}...", self.click_point);
if let Some(face_idx) = graph.find_face_containing_point(self.click_point, &faces) {
println!("Found face {} containing click point!", face_idx);
// Build the face boundary using actual curve segments // Build the face boundary using actual curve segments
let face = &faces[face_idx]; let face_path = graph.build_face_path(&face);
let face_path = graph.build_face_path(face);
println!("DEBUG: Creating face shape with fill color: r={}, g={}, b={}, a={}",
self.fill_color.r, self.fill_color.g, self.fill_color.b, self.fill_color.a);
let face_shape = crate::shape::Shape::new(face_path) let face_shape = crate::shape::Shape::new(face_path)
.with_fill(self.fill_color); // Use the requested fill color .with_fill(self.fill_color); // Use the requested fill color
println!("DEBUG: Face shape created with fill_color: {:?}", face_shape.fill_color);
let face_object = Object::new(face_shape.id); let face_object = Object::new(face_shape.id);
// Store the created IDs for rollback // Store the created IDs for rollback
@ -127,9 +147,15 @@ impl Action for PaintBucketAction {
self.created_object_id = Some(face_object.id); self.created_object_id = Some(face_object.id);
if let Some(AnyLayer::Vector(vector_layer)) = document.get_layer_mut(&self.layer_id) { if let Some(AnyLayer::Vector(vector_layer)) = document.get_layer_mut(&self.layer_id) {
let shape_id_for_debug = face_shape.id;
vector_layer.add_shape_internal(face_shape); vector_layer.add_shape_internal(face_shape);
vector_layer.add_object_internal(face_object); vector_layer.add_object_internal(face_object);
println!("DEBUG: Added filled shape for face {}", face_idx); println!("DEBUG: Added filled shape");
// Verify the shape still has the fill color after being added
if let Some(added_shape) = vector_layer.shapes.iter().find(|s| s.id == shape_id_for_debug) {
println!("DEBUG: After adding to layer, shape fill_color = {:?}", added_shape.fill_color);
}
} }
} else { } else {
println!("Click point is not inside any face!"); println!("Click point is not inside any face!");

View File

@ -10,13 +10,8 @@
use crate::curve_intersections::{find_curve_intersections, find_self_intersections}; use crate::curve_intersections::{find_curve_intersections, find_self_intersections};
use crate::curve_segment::CurveSegment; use crate::curve_segment::CurveSegment;
use crate::shape::{Shape, ShapeColor, StrokeStyle};
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::sync::Mutex; use vello::kurbo::{BezPath, CubicBez, Point};
use vello::kurbo::{BezPath, Circle, CubicBez, Point, Shape as KurboShape};
/// Global debug storage for the last planar graph (for visualization)
pub static DEBUG_GRAPH: Mutex<Option<PlanarGraph>> = Mutex::new(None);
/// A node in the planar graph (intersection point or endpoint) /// A node in the planar graph (intersection point or endpoint)
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -343,43 +338,6 @@ impl PlanarGraph {
} }
} }
/// Render debug visualization of the planar graph
///
/// Returns two shapes: one for nodes (red circles) and one for edges (yellow lines)
pub fn render_debug(&self) -> (Shape, Shape) {
// Render nodes as red circles
let mut nodes_path = BezPath::new();
for node in &self.nodes {
let circle = Circle::new(node.position, 3.0);
nodes_path.extend(circle.to_path(0.1));
}
let nodes_shape = Shape::new(nodes_path).with_stroke(
ShapeColor::rgb(255, 0, 0),
StrokeStyle {
width: 1.0,
..Default::default()
},
);
// Render edges as yellow straight lines
let mut edges_path = BezPath::new();
for edge in &self.edges {
let start_pos = self.nodes[edge.start_node].position;
let end_pos = self.nodes[edge.end_node].position;
edges_path.move_to(start_pos);
edges_path.line_to(end_pos);
}
let edges_shape = Shape::new(edges_path).with_stroke(
ShapeColor::rgb(255, 255, 0),
StrokeStyle {
width: 0.5,
..Default::default()
},
);
(nodes_shape, edges_shape)
}
/// 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> {
let mut faces = Vec::new(); let mut faces = Vec::new();
@ -728,6 +686,161 @@ impl PlanarGraph {
path.close_path(); path.close_path();
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) /// A face in the planar graph (bounded region)

View File

@ -2743,35 +2743,6 @@ impl PaneRenderer for StagePane {
egui::FontId::proportional(14.0), egui::FontId::proportional(14.0),
egui::Color32::from_gray(200), egui::Color32::from_gray(200),
); );
// Render planar graph debug visualization
if let Ok(debug_graph_opt) = lightningbeam_core::planar_graph::DEBUG_GRAPH.lock() {
if let Some(ref graph) = *debug_graph_opt {
// Draw node labels
for (idx, node) in graph.nodes.iter().enumerate() {
// Transform world coords to screen coords
let screen_x = (node.position.x + self.pan_offset.x as f64) * self.zoom as f64 + rect.min.x as f64;
let screen_y = (node.position.y + self.pan_offset.y as f64) * self.zoom as f64 + rect.min.y as f64;
let screen_pos = egui::pos2(screen_x as f32, screen_y as f32);
// Draw small circle at node
ui.painter().circle_filled(
screen_pos,
4.0,
egui::Color32::from_rgb(255, 100, 100),
);
// Draw node number label
ui.painter().text(
screen_pos + egui::vec2(8.0, -8.0),
egui::Align2::LEFT_BOTTOM,
format!("{}", idx),
egui::FontId::monospace(14.0),
egui::Color32::from_rgb(0, 0, 0),
);
}
}
}
} }
fn name(&self) -> &str { fn name(&self) -> &str {