fix paint bucket angle priority

This commit is contained in:
Skyler Lehmkuhl 2025-11-19 05:54:51 -05:00
parent b7c382586e
commit 502bae0947
3 changed files with 116 additions and 47 deletions

View File

@ -71,10 +71,10 @@ 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 (Planar Graph Approach) ===");
// Step 1: Extract curves from stroked shapes only (not filled regions) // Step 1: Extract curves from all shapes (rectangles, ellipses, paths, etc.)
let all_curves = extract_curves_from_stroked_shapes(document, &self.layer_id); 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() { if all_curves.is_empty() {
println!("No curves found, returning"); 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. /// Includes rectangles, ellipses, paths, and even previous paint bucket fills.
/// Stroked shapes define boundaries for the planar graph. /// The planar graph builder will handle deduplication of overlapping edges.
fn extract_curves_from_stroked_shapes( fn extract_curves_from_all_shapes(
document: &Document, document: &Document,
layer_id: &Uuid, layer_id: &Uuid,
) -> Vec<CurveSegment> { ) -> Vec<CurveSegment> {
@ -179,25 +179,26 @@ fn extract_curves_from_stroked_shapes(
// Extract curves only from this vector layer // Extract curves only from this vector layer
if let AnyLayer::Vector(vector_layer) = 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) // 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 // Find the shape for this object
let shape = match vector_layer.shapes.iter().find(|s| s.id == object.shape_id) { let shape = match vector_layer.shapes.iter().find(|s| s.id == object.shape_id) {
Some(s) => s, Some(s) => s,
None => continue, None => continue,
}; };
// Skip shapes without strokes (these are filled regions, not boundaries) // Include all shapes - planar graph will handle deduplication
if shape.stroke_color.is_none() { // (Rectangles, ellipses, paths, and even previous paint bucket fills)
continue;
}
// Get the transform matrix from the object // Get the transform matrix from the object
let transform_affine = object.transform.to_affine(); let transform_affine = object.transform.to_affine();
let path = shape.path(); let path = shape.path();
let mut current_point = Point::ZERO; let mut current_point = Point::ZERO;
let mut subpath_start = Point::ZERO; // Track start of current subpath
let mut segment_index = 0; let mut segment_index = 0;
let mut curves_in_shape = 0;
for element in path.elements() { for element in path.elements() {
// Extract curve segment from path element // Extract curve segment from path element
@ -214,17 +215,41 @@ fn extract_curves_from_stroked_shapes(
all_curves.push(segment); all_curves.push(segment);
segment_index += 1; segment_index += 1;
curves_in_shape += 1;
} }
// Update current point for next iteration (keep in local space) // Update current point for next iteration (keep in local space)
match element { 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::LineTo(p) => current_point = *p,
vello::kurbo::PathEl::QuadTo(_, p) => current_point = *p, vello::kurbo::PathEl::QuadTo(_, p) => current_point = *p,
vello::kurbo::PathEl::CurveTo(_, _, 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);
} }
} }

View File

@ -135,10 +135,22 @@ impl PlanarGraph {
} }
// Find curve-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 i in 0..curves.len() {
for j in (i + 1)..curves.len() { for j in (i + 1)..curves.len() {
let curve_i_intersections = find_curve_intersections(&curves[i], &curves[j]); 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 { for intersection in curve_i_intersections {
// Add to curve i // Add to curve i
intersections intersections
@ -172,6 +184,8 @@ impl PlanarGraph {
} }
} }
println!("Total curve-curve intersections found: {}", total_intersections);
// Sort and deduplicate intersections for each curve // Sort and deduplicate intersections for each curve
for curve_intersections in intersections.values_mut() { for curve_intersections in intersections.values_mut() {
curve_intersections.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap()); 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 // Find the next edge in counterclockwise order around end_node
let next = self.find_next_ccw_edge(current_edge, current_forward, 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 { if let Some((next_edge, next_forward)) = next {
current_edge = next_edge; current_edge = next_edge;
current_forward = next_forward; current_forward = next_forward;
@ -566,16 +561,19 @@ impl PlanarGraph {
) -> Option<(usize, bool)> { ) -> Option<(usize, bool)> {
let node = &self.nodes[node_idx]; 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 edge = &self.edges[incoming_edge];
let incoming_dir = if incoming_forward { let incoming_dir = if incoming_forward {
let start_pos = self.nodes[edge.start_node].position; let start_pos = self.nodes[edge.start_node].position;
let end_pos = self.nodes[edge.end_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 { } else {
let start_pos = self.nodes[edge.start_node].position; let start_pos = self.nodes[edge.start_node].position;
let end_pos = self.nodes[edge.end_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 // 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_edge = None;
let mut best_angle = std::f64::MAX; let mut best_angle = 0.0;
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!(" Edge {} {} -> SKIP (reverse of incoming)", edge_idx, if forward { "fwd" } else { "bwd" });
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);
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_angle = angle;
best_edge = Some((edge_idx, forward)); best_edge = Some((edge_idx, forward));
} }
} }
if best_edge.is_none() { if best_edge.is_none() {
let incoming_edge_obj = &self.edges[incoming_edge]; println!(" FAILED: No valid next edge found!");
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" });
} }
best_edge best_edge

View File

@ -2743,6 +2743,35 @@ 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 {