From 502bae094733656572a6bcb9310718198a7659d6 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Wed, 19 Nov 2025 05:54:51 -0500 Subject: [PATCH] fix paint bucket angle priority --- .../src/actions/paint_bucket.rs | 53 ++++++++---- .../lightningbeam-core/src/planar_graph.rs | 81 +++++++++++-------- .../lightningbeam-editor/src/panes/stage.rs | 29 +++++++ 3 files changed, 116 insertions(+), 47 deletions(-) diff --git a/lightningbeam-ui/lightningbeam-core/src/actions/paint_bucket.rs b/lightningbeam-ui/lightningbeam-core/src/actions/paint_bucket.rs index 2406e8c..9a7ae56 100644 --- a/lightningbeam-ui/lightningbeam-core/src/actions/paint_bucket.rs +++ b/lightningbeam-ui/lightningbeam-core/src/actions/paint_bucket.rs @@ -71,10 +71,10 @@ impl Action for PaintBucketAction { fn execute(&mut self, document: &mut Document) { println!("=== PaintBucketAction::execute (Planar Graph Approach) ==="); - // Step 1: Extract curves from stroked shapes only (not filled regions) - let all_curves = extract_curves_from_stroked_shapes(document, &self.layer_id); + // Step 1: Extract curves from all shapes (rectangles, ellipses, paths, etc.) + let all_curves = extract_curves_from_all_shapes(document, &self.layer_id); - println!("Extracted {} curves from stroked shapes", all_curves.len()); + println!("Extracted {} curves from all shapes", all_curves.len()); if all_curves.is_empty() { println!("No curves found, returning"); @@ -161,11 +161,11 @@ impl Action for PaintBucketAction { } } -/// Extract curves from stroked shapes only (not filled regions) +/// Extract curves from all shapes in the layer /// -/// This filters out paint bucket filled shapes which have only fills, not strokes. -/// Stroked shapes define boundaries for the planar graph. -fn extract_curves_from_stroked_shapes( +/// Includes rectangles, ellipses, paths, and even previous paint bucket fills. +/// The planar graph builder will handle deduplication of overlapping edges. +fn extract_curves_from_all_shapes( document: &Document, layer_id: &Uuid, ) -> Vec { @@ -179,25 +179,26 @@ fn extract_curves_from_stroked_shapes( // Extract curves only from this vector layer if let AnyLayer::Vector(vector_layer) = layer { + println!("Extracting curves from {} objects in layer", vector_layer.objects.len()); // Extract curves from each object (which applies transforms to shapes) - for object in &vector_layer.objects { + for (obj_idx, object) in vector_layer.objects.iter().enumerate() { // Find the shape for this object let shape = match vector_layer.shapes.iter().find(|s| s.id == object.shape_id) { Some(s) => s, None => continue, }; - // Skip shapes without strokes (these are filled regions, not boundaries) - if shape.stroke_color.is_none() { - continue; - } + // Include all shapes - planar graph will handle deduplication + // (Rectangles, ellipses, paths, and even previous paint bucket fills) // Get the transform matrix from the object let transform_affine = object.transform.to_affine(); let path = shape.path(); let mut current_point = Point::ZERO; + let mut subpath_start = Point::ZERO; // Track start of current subpath let mut segment_index = 0; + let mut curves_in_shape = 0; for element in path.elements() { // Extract curve segment from path element @@ -214,17 +215,41 @@ fn extract_curves_from_stroked_shapes( all_curves.push(segment); segment_index += 1; + curves_in_shape += 1; } // Update current point for next iteration (keep in local space) match element { - vello::kurbo::PathEl::MoveTo(p) => current_point = *p, + vello::kurbo::PathEl::MoveTo(p) => { + current_point = *p; + subpath_start = *p; // Mark start of new subpath + } vello::kurbo::PathEl::LineTo(p) => current_point = *p, vello::kurbo::PathEl::QuadTo(_, p) => current_point = *p, vello::kurbo::PathEl::CurveTo(_, _, p) => current_point = *p, - vello::kurbo::PathEl::ClosePath => {} + vello::kurbo::PathEl::ClosePath => { + // Create closing segment from current_point back to subpath_start + if let Some(mut segment) = CurveSegment::from_path_element( + shape.id.as_u128() as usize, + segment_index, + &vello::kurbo::PathEl::LineTo(subpath_start), + current_point, + ) { + // Apply transform + for control_point in &mut segment.control_points { + *control_point = transform_affine * (*control_point); + } + + all_curves.push(segment); + segment_index += 1; + curves_in_shape += 1; + } + current_point = subpath_start; // ClosePath moves back to start + } } } + + println!(" Object {}: Extracted {} curves from shape", obj_idx, curves_in_shape); } } diff --git a/lightningbeam-ui/lightningbeam-core/src/planar_graph.rs b/lightningbeam-ui/lightningbeam-core/src/planar_graph.rs index b0a7f00..f74b004 100644 --- a/lightningbeam-ui/lightningbeam-core/src/planar_graph.rs +++ b/lightningbeam-ui/lightningbeam-core/src/planar_graph.rs @@ -135,10 +135,22 @@ impl PlanarGraph { } // 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 @@ -172,6 +184,8 @@ impl PlanarGraph { } } + 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()); @@ -520,25 +534,6 @@ impl PlanarGraph { // Find the next edge in counterclockwise order around end_node let next = self.find_next_ccw_edge(current_edge, current_forward, end_node); - if edge_sequence.len() <= 5 { - if let Some((next_edge, next_forward)) = next { - let next_edge_obj = &self.edges[next_edge]; - let next_end_node = if next_forward { - next_edge_obj.end_node - } else { - next_edge_obj.start_node - }; - println!(" Step {}: {} -> {} (edge {} {}) -> next: {} -> {} (edge {} {})", - edge_sequence.len(), start_node_this_edge, end_node, - current_edge, if current_forward { "fwd" } else { "bwd" }, - end_node, next_end_node, next_edge, if next_forward { "fwd" } else { "bwd" }); - } else { - println!(" Step {}: {} -> {} (edge {} {}) -> next: None", - edge_sequence.len(), start_node_this_edge, end_node, - current_edge, if current_forward { "fwd" } else { "bwd" }); - } - } - if let Some((next_edge, next_forward)) = next { current_edge = next_edge; current_forward = next_forward; @@ -566,16 +561,19 @@ impl PlanarGraph { ) -> Option<(usize, bool)> { let node = &self.nodes[node_idx]; - // Get the incoming direction vector (pointing INTO this node) + // 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; - (end_pos.x - start_pos.x, end_pos.y - start_pos.y) + // 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; - (start_pos.x - end_pos.x, start_pos.y - end_pos.y) + // 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 @@ -601,34 +599,51 @@ impl PlanarGraph { } } - // Find the edge that makes the smallest left turn (most counterclockwise) + // 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 = std::f64::MAX; + 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); - if angle < best_angle { + // 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() { - 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 FAILED at node {}: {} candidates total, incoming {} -> {} (edge {} {}), found no valid next edge", - node_idx, candidates.len(), inc_start, inc_end, incoming_edge, if incoming_forward { "fwd" } else { "bwd" }); + println!(" FAILED: No valid next edge found!"); } best_edge diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs index 2a78fff..546ddf1 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs @@ -2743,6 +2743,35 @@ 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 {