//! Paint bucket fill action //! //! This action performs a paint bucket fill operation starting from a click point, //! using planar graph face detection to identify the region to fill. use crate::action::Action; use crate::curve_segment::CurveSegment; use crate::document::Document; use crate::gap_handling::GapHandlingMode; use crate::layer::AnyLayer; use crate::planar_graph::PlanarGraph; use crate::shape::ShapeColor; use uuid::Uuid; use vello::kurbo::Point; /// Action that performs a paint bucket fill operation pub struct PaintBucketAction { /// Layer ID to add the filled shape to layer_id: Uuid, /// Time of the keyframe to operate on time: f64, /// Click point where fill was initiated click_point: Point, /// Fill color for the shape fill_color: ShapeColor, /// Tolerance for gap bridging (in pixels) _tolerance: f64, /// Gap handling mode _gap_mode: GapHandlingMode, /// ID of the created shape (set after execution) created_shape_id: Option, } impl PaintBucketAction { /// Create a new paint bucket action pub fn new( layer_id: Uuid, time: f64, click_point: Point, fill_color: ShapeColor, tolerance: f64, gap_mode: GapHandlingMode, ) -> Self { Self { layer_id, time, click_point, fill_color, _tolerance: tolerance, _gap_mode: gap_mode, created_shape_id: None, } } } impl Action for PaintBucketAction { fn execute(&mut self, document: &mut Document) -> Result<(), String> { println!("=== PaintBucketAction::execute ==="); // Optimization: Check if we're clicking on an existing shape first if let Some(AnyLayer::Vector(vector_layer)) = document.get_layer_mut(&self.layer_id) { // Iterate through shapes in the keyframe in reverse order (topmost first) let shapes = vector_layer.shapes_at_time(self.time); for shape in shapes.iter().rev() { // Skip shapes without fill color if shape.fill_color.is_none() { continue; } use vello::kurbo::PathEl; let is_closed = shape.path().elements().iter().any(|el| matches!(el, PathEl::ClosePath)); if !is_closed { continue; } // Apply the shape's transform let transform_affine = shape.transform.to_affine(); let inverse_transform = transform_affine.inverse(); let local_point = inverse_transform * self.click_point; use vello::kurbo::Shape as KurboShape; let winding = shape.path().winding(local_point); if winding != 0 { println!("Clicked on existing shape, changing fill color"); let shape_id = shape.id; // Now get mutable access to change the fill if let Some(shape_mut) = vector_layer.get_shape_in_keyframe_mut(&shape_id, self.time) { shape_mut.fill_color = Some(self.fill_color); } return Ok(()); } } println!("No existing shape at click point, creating new fill region"); } // Step 1: Extract curves from all shapes in the keyframe let all_curves = extract_curves_from_keyframe(document, &self.layer_id, self.time); println!("Extracted {} curves from all shapes", all_curves.len()); if all_curves.is_empty() { println!("No curves found, returning"); return Ok(()); } // Step 2: Build planar graph println!("Building planar graph..."); let graph = PlanarGraph::build(&all_curves); // Step 3: Trace the face containing the click point 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!"); let face_path = graph.build_face_path(&face); let face_shape = crate::shape::Shape::new(face_path) .with_fill(self.fill_color); self.created_shape_id = Some(face_shape.id); if let Some(AnyLayer::Vector(vector_layer)) = document.get_layer_mut(&self.layer_id) { vector_layer.add_shape_to_keyframe(face_shape, self.time); println!("DEBUG: Added filled shape to keyframe"); } } else { println!("Click point is not inside any face!"); } println!("=== Paint Bucket Complete ==="); Ok(()) } fn rollback(&mut self, document: &mut Document) -> Result<(), String> { if let Some(shape_id) = self.created_shape_id { if let Some(AnyLayer::Vector(vector_layer)) = document.get_layer_mut(&self.layer_id) { vector_layer.remove_shape_from_keyframe(&shape_id, self.time); } self.created_shape_id = None; } Ok(()) } fn description(&self) -> String { "Paint bucket fill".to_string() } } /// Extract curves from all shapes in the keyframe at the given time fn extract_curves_from_keyframe( document: &Document, layer_id: &Uuid, time: f64, ) -> Vec { let mut all_curves = Vec::new(); let layer = match document.get_layer(layer_id) { Some(l) => l, None => return all_curves, }; if let AnyLayer::Vector(vector_layer) = layer { let shapes = vector_layer.shapes_at_time(time); println!("Extracting curves from {} shapes in keyframe", shapes.len()); for (shape_idx, shape) in shapes.iter().enumerate() { let transform_affine = shape.transform.to_affine(); let path = shape.path(); let mut current_point = Point::ZERO; let mut subpath_start = Point::ZERO; let mut segment_index = 0; let mut curves_in_shape = 0; for element in path.elements() { if let Some(mut segment) = CurveSegment::from_path_element( shape.id.as_u128() as usize, segment_index, element, current_point, ) { 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; } match element { vello::kurbo::PathEl::MoveTo(p) => { current_point = *p; subpath_start = *p; } 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 => { 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, ) { 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; } } } println!(" Shape {}: Extracted {} curves", shape_idx, curves_in_shape); } } all_curves } #[cfg(test)] mod tests { use super::*; use crate::layer::VectorLayer; use crate::shape::Shape; use vello::kurbo::{Rect, Shape as KurboShape}; #[test] fn test_paint_bucket_action_basic() { let mut document = Document::new("Test"); let mut layer = VectorLayer::new("Layer 1"); // Create a simple rectangle shape (boundary for fill) let rect = Rect::new(0.0, 0.0, 100.0, 100.0); let path = rect.to_path(0.1); let shape = Shape::new(path); layer.add_shape_to_keyframe(shape, 0.0); let layer_id = document.root_mut().add_child(AnyLayer::Vector(layer)); // Create and execute paint bucket action let mut action = PaintBucketAction::new( layer_id, 0.0, Point::new(50.0, 50.0), ShapeColor::rgb(255, 0, 0), 2.0, GapHandlingMode::BridgeSegment, ); action.execute(&mut document).unwrap(); // Verify a filled shape was created (or existing shape was recolored) if let Some(AnyLayer::Vector(layer)) = document.get_layer(&layer_id) { assert!(layer.shapes_at_time(0.0).len() >= 1); } else { panic!("Layer not found or not a vector layer"); } // Test rollback action.rollback(&mut document).unwrap(); } #[test] fn test_paint_bucket_action_description() { let action = PaintBucketAction::new( Uuid::new_v4(), 0.0, Point::ZERO, ShapeColor::rgb(0, 0, 255), 2.0, GapHandlingMode::BridgeSegment, ); assert_eq!(action.description(), "Paint bucket fill"); } }