From 08f3c30b290be8ff64af21f91b0d690020579f22 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Wed, 19 Nov 2025 09:01:27 -0500 Subject: [PATCH] paint bucket mostly working --- .../src/actions/paint_bucket.rs | 88 +++++--- .../lightningbeam-core/src/planar_graph.rs | 199 ++++++++++++++---- .../lightningbeam-editor/src/panes/stage.rs | 29 --- 3 files changed, 213 insertions(+), 103 deletions(-) diff --git a/lightningbeam-ui/lightningbeam-core/src/actions/paint_bucket.rs b/lightningbeam-ui/lightningbeam-core/src/actions/paint_bucket.rs index 9a7ae56..fc20eac 100644 --- a/lightningbeam-ui/lightningbeam-core/src/actions/paint_bucket.rs +++ b/lightningbeam-ui/lightningbeam-core/src/actions/paint_bucket.rs @@ -69,7 +69,46 @@ impl PaintBucketAction { impl Action for PaintBucketAction { 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.) let all_curves = extract_curves_from_all_shapes(document, &self.layer_id); @@ -85,41 +124,22 @@ impl Action for PaintBucketAction { println!("Building planar graph..."); let graph = PlanarGraph::build(&all_curves); - // Store graph for debug visualization - if let Ok(mut debug_graph) = crate::planar_graph::DEBUG_GRAPH.lock() { - *debug_graph = Some(graph.clone()); - } - - // 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); + // Step 3: Trace the face containing the click point (optimized - only traces one face) + println!("Tracing face from click point {:?}...", self.click_point); + if let Some(face) = graph.trace_face_from_point(self.click_point) { + println!("Successfully traced face containing click point!"); // 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) .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); // Store the created IDs for rollback @@ -127,9 +147,15 @@ impl Action for PaintBucketAction { self.created_object_id = Some(face_object.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_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 { println!("Click point is not inside any face!"); diff --git a/lightningbeam-ui/lightningbeam-core/src/planar_graph.rs b/lightningbeam-ui/lightningbeam-core/src/planar_graph.rs index f74b004..b49cd72 100644 --- a/lightningbeam-ui/lightningbeam-core/src/planar_graph.rs +++ b/lightningbeam-ui/lightningbeam-core/src/planar_graph.rs @@ -10,13 +10,8 @@ use crate::curve_intersections::{find_curve_intersections, find_self_intersections}; use crate::curve_segment::CurveSegment; -use crate::shape::{Shape, ShapeColor, StrokeStyle}; use std::collections::{HashMap, HashSet}; -use std::sync::Mutex; -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> = Mutex::new(None); +use vello::kurbo::{BezPath, CubicBez, Point}; /// A node in the planar graph (intersection point or endpoint) #[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 pub fn find_faces(&self) -> Vec { let mut faces = Vec::new(); @@ -728,6 +686,161 @@ impl PlanarGraph { 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 { + 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) diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs index 546ddf1..2a78fff 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs @@ -2743,35 +2743,6 @@ impl PaneRenderer for StagePane { egui::FontId::proportional(14.0), 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 {