diff --git a/lightningbeam-ui/lightningbeam-core/src/actions/paint_bucket.rs b/lightningbeam-ui/lightningbeam-core/src/actions/paint_bucket.rs index f0ccb2f..67c64f9 100644 --- a/lightningbeam-ui/lightningbeam-core/src/actions/paint_bucket.rs +++ b/lightningbeam-ui/lightningbeam-core/src/actions/paint_bucket.rs @@ -55,22 +55,28 @@ impl Action for PaintBucketAction { // Record for debug test generation (if recording is active) dcel.record_paint_point(self.click_point); - // Hit-test to find which face was clicked - let face_id = dcel.find_face_containing_point(self.click_point); + // Find the enclosing cycle for the click point + let query = dcel.find_face_at_point(self.click_point); // Dump cumulative test to stderr after every paint click (if recording) - // Do this before the early return so failed clicks are captured too. if dcel.is_recording() { - eprintln!("\n--- DCEL debug test (cumulative, face={:?}) ---", face_id); + eprintln!("\n--- DCEL debug test (cumulative, face={:?}) ---", query.face); dcel.debug_recorder.as_ref().unwrap().dump_test("test_recorded"); eprintln!("--- end test ---\n"); } - if face_id.0 == 0 { - // FaceId(0) is the unbounded exterior face — nothing to fill + if query.cycle_he.is_none() { + // No edges at all — nothing to fill return Err("No face at click point".to_string()); } + // If the cycle is in F0 (no face created yet), create one now + let face_id = if query.face.0 == 0 { + dcel.create_face_at_cycle(query.cycle_he) + } else { + query.face + }; + // Store for undo self.hit_face = Some(face_id); self.old_fill_color = Some(dcel.face(face_id).fill_color.clone()); diff --git a/lightningbeam-ui/lightningbeam-core/src/dcel2/topology.rs b/lightningbeam-ui/lightningbeam-core/src/dcel2/topology.rs index 940b927..e077933 100644 --- a/lightningbeam-ui/lightningbeam-core/src/dcel2/topology.rs +++ b/lightningbeam-ui/lightningbeam-core/src/dcel2/topology.rs @@ -528,6 +528,19 @@ impl Dcel { HalfEdgeId::NONE } + /// Create a face from an existing cycle of half-edges in F0. + /// + /// Use this when a closed boundary exists but no face was created + /// (e.g. paint bucket on a region that hasn't been filled yet). + /// The cycle's half-edges are assigned to the new face. + /// Returns the new FaceId. + pub fn create_face_at_cycle(&mut self, cycle_he: HalfEdgeId) -> FaceId { + let face = self.alloc_face(); + self.faces[face.idx()].outer_half_edge = cycle_he; + self.assign_cycle_face(cycle_he, face); + face + } + /// Re-sort all outgoing half-edges at a vertex by angle and fix the /// fan linkage (`twin.next` / `prev`). Call this after operations that /// add outgoing half-edges to an existing vertex without maintaining