From 56bbcdf356ca0ca5f40899de5fc16f6e7258cc2c Mon Sep 17 00:00:00 2001 From: Halit Can Date: Wed, 15 Jul 2026 03:09:03 +0300 Subject: [PATCH] feat(iced): add transform handle hit-testing --- .../crates/hcie-iced-gui/src/canvas/mod.rs | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/canvas/mod.rs b/hcie-iced-app/crates/hcie-iced-gui/src/canvas/mod.rs index b726e16..903edc2 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/canvas/mod.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/canvas/mod.rs @@ -66,6 +66,8 @@ struct OverlayState { cursor_pos: Option, /// Whether the mouse is over the overlay. is_hovered: bool, + /// Current handle being hovered (for cursor changes). + hovered_handle: TransformHandle, } impl OverlayProgram { @@ -84,6 +86,69 @@ impl OverlayProgram { (ox, oy, display_w, display_h) } + /// Test which transform handle is at the given viewport position. + pub fn hit_test_handle( + &self, + viewport_x: f32, + viewport_y: f32, + ) -> TransformHandle { + let Some(ref tr) = self.selection_transform else { + return TransformHandle::None; + }; + if tr.is_empty() { + return TransformHandle::None; + } + + let (origin_x, origin_y, _, _) = self.canvas_origin(iced::Size::new(0.0, 0.0)); + + // Convert viewport to canvas coordinates + let canvas_x = (viewport_x - origin_x) / self.zoom; + let canvas_y = (viewport_y - origin_y) / self.zoom; + + let handle_size = TransformHandle::HANDLE_SIZE / self.zoom; + let half = handle_size / 2.0; + + let x = tr.pos.x; + let y = tr.pos.y; + let w = tr.size.x; + let h = tr.size.y; + + // Check rotation handle first (it's outside the box) + let rotate_y = y - TransformHandle::ROTATE_OFFSET / self.zoom; + let rotate_x = x + w / 2.0; + let rotate_radius = 4.0 / self.zoom; + let dx = canvas_x - rotate_x; + let dy = canvas_y - rotate_y; + if dx * dx + dy * dy <= rotate_radius * rotate_radius { + return TransformHandle::Rotate; + } + + // Check corner and edge handles + let handles = [ + (TransformHandle::TopLeft, x, y), + (TransformHandle::TopRight, x + w, y), + (TransformHandle::BottomLeft, x, y + h), + (TransformHandle::BottomRight, x + w, y + h), + (TransformHandle::Top, x + w / 2.0, y), + (TransformHandle::Bottom, x + w / 2.0, y + h), + (TransformHandle::Left, x, y + h / 2.0), + (TransformHandle::Right, x + w, y + h / 2.0), + ]; + + for (handle, hx, hy) in &handles { + if (canvas_x - hx).abs() <= half && (canvas_y - hy).abs() <= half { + return *handle; + } + } + + // Check if inside the selection box (for move) + if canvas_x >= x && canvas_x <= x + w && canvas_y >= y && canvas_y <= y + h { + return TransformHandle::Move; + } + + TransformHandle::None + } + /// Draw transform handles for selection. fn draw_transform_handles( &self, @@ -291,11 +356,19 @@ impl canvas::Program for OverlayProgram { ); state.cursor_pos = Some(local_pos); state.is_hovered = bounds.contains(position); + + // Update hovered handle + if self.selection_transform.is_some() { + state.hovered_handle = self.hit_test_handle(position.x, position.y); + } else { + state.hovered_handle = TransformHandle::None; + } } _ => { if state.is_hovered && !cursor.is_over(bounds) { state.is_hovered = false; state.cursor_pos = None; + state.hovered_handle = TransformHandle::None; } } }