feat(iced): implement VectorEditHandle rendering and drag/resize/rotate
This commit is contained in:
@@ -256,12 +256,8 @@ pub struct HcieIcedApp {
|
||||
/// Dirty flag set whenever fg/bg/recent colors change, so colors can be
|
||||
/// persisted to disk (egui persists these under the `hcie_colors` key).
|
||||
pub colors_dirty: bool,
|
||||
/// Index of the currently selected vector shape (None = no selection).
|
||||
pub selected_vector_shape: Option<usize>,
|
||||
/// Snapshot of vector shapes before editing (for undo/Cancel).
|
||||
pub vector_shapes_snapshot: Option<Vec<hcie_engine_api::VectorShape>>,
|
||||
/// Which transform handle is being dragged on the selected vector shape.
|
||||
pub vector_edit_handle: hcie_engine_api::VectorEditHandle,
|
||||
/// Last drag position during vector shape editing (canvas coordinates).
|
||||
pub vector_drag_last: Option<(f32, f32)>,
|
||||
}
|
||||
@@ -344,6 +340,10 @@ pub struct IcedDocument {
|
||||
pub selection_edge_cache: crate::canvas::edge_cache::SelectionEdgeCache,
|
||||
/// Extracted selection edges for overlay rendering.
|
||||
pub marching_ants_edges: Option<Arc<Vec<(u32, u32, u32, u32)>>>,
|
||||
/// Index of the currently selected vector shape (None = no selection).
|
||||
pub selected_vector_shape: Option<usize>,
|
||||
/// Which vector edit handle is active (hovered or being dragged).
|
||||
pub vector_edit_handle: hcie_engine_api::VectorEditHandle,
|
||||
}
|
||||
|
||||
/// In-progress text layer being authored with the Text tool.
|
||||
@@ -992,6 +992,8 @@ impl HcieIcedApp {
|
||||
text_draft: None,
|
||||
selection_edge_cache: Default::default(),
|
||||
marching_ants_edges: None,
|
||||
selected_vector_shape: None,
|
||||
vector_edit_handle: hcie_engine_api::VectorEditHandle::None,
|
||||
};
|
||||
|
||||
// Load saved settings
|
||||
@@ -1078,9 +1080,7 @@ impl HcieIcedApp {
|
||||
show_dock_profile_dialog: false,
|
||||
language: i18n::Language::default(),
|
||||
colors_dirty: false,
|
||||
selected_vector_shape: None,
|
||||
vector_shapes_snapshot: None,
|
||||
vector_edit_handle: hcie_engine_api::VectorEditHandle::None,
|
||||
vector_drag_last: None,
|
||||
};
|
||||
|
||||
@@ -1191,6 +1191,86 @@ impl HcieIcedApp {
|
||||
}
|
||||
}
|
||||
|
||||
/// Hit-test which vector edit handle is at the given canvas-space position.
|
||||
///
|
||||
/// Checks rotation handle first (circle area), then resize handles (square
|
||||
/// hit-box), then the move handle (inside bounding box). Returns the
|
||||
/// matching `VectorEditHandle` variant. Coordinates are in canvas-space.
|
||||
fn vector_hit_test_handle(&self, cx: f32, cy: f32) -> hcie_engine_api::VectorEditHandle {
|
||||
use hcie_engine_api::VectorEditHandle as VEH;
|
||||
|
||||
let doc = &self.documents[self.active_doc];
|
||||
let Some(idx) = doc.selected_vector_shape else {
|
||||
return VEH::None;
|
||||
};
|
||||
let layer_id = doc.engine.active_layer_id();
|
||||
let shapes = match doc.engine.active_vector_shapes() {
|
||||
Some(s) => s,
|
||||
None => return VEH::None,
|
||||
};
|
||||
let shape = match shapes.get(idx) {
|
||||
Some(s) => s,
|
||||
None => return VEH::None,
|
||||
};
|
||||
|
||||
let (x1, y1, x2, y2) = shape.normalized_bounds();
|
||||
let angle = doc.engine.vector_shape_angle(layer_id, idx);
|
||||
let w = (x2 - x1).abs();
|
||||
let h = (y2 - y1).abs();
|
||||
if w < 0.5 || h < 0.5 { return VEH::None; }
|
||||
|
||||
let bcx = (x1 + x2) / 2.0;
|
||||
let bcy = (y1 + y2) / 2.0;
|
||||
let cos_a = angle.cos();
|
||||
let sin_a = angle.sin();
|
||||
|
||||
// Inverse-rotate a canvas-space point around the bounding-box center.
|
||||
let unrotate = |px: f32, py: f32| -> (f32, f32) {
|
||||
let dx = px - bcx;
|
||||
let dy = py - bcy;
|
||||
(bcx + dx * cos_a + dy * sin_a, bcy - dx * sin_a + dy * cos_a)
|
||||
};
|
||||
|
||||
let (lx, ly) = unrotate(cx, cy);
|
||||
|
||||
// Check rotation handle first (outside the box).
|
||||
let rotate_offset = 24.0;
|
||||
let rot_circ_x = bcx;
|
||||
let rot_circ_y = y1 - rotate_offset;
|
||||
let rot_radius = 6.0;
|
||||
let drx = lx - rot_circ_x;
|
||||
let dry = ly - rot_circ_y;
|
||||
if drx * drx + dry * dry <= rot_radius * rot_radius {
|
||||
return VEH::Rotate;
|
||||
}
|
||||
|
||||
// Check 8 resize handles.
|
||||
let handle_half = 7.0;
|
||||
let handle_points: [(VEH, f32, f32); 8] = [
|
||||
(VEH::TopLeft, x1, y1),
|
||||
(VEH::TopRight, x2, y1),
|
||||
(VEH::BottomLeft, x1, y2),
|
||||
(VEH::BottomRight, x2, y2),
|
||||
(VEH::Top, bcx, y1),
|
||||
(VEH::Bottom, bcx, y2),
|
||||
(VEH::Left, x1, bcy),
|
||||
(VEH::Right, x2, bcy),
|
||||
];
|
||||
|
||||
for &(handle, hx, hy) in &handle_points {
|
||||
if (lx - hx).abs() <= handle_half && (ly - hy).abs() <= handle_half {
|
||||
return handle;
|
||||
}
|
||||
}
|
||||
|
||||
// Check move handle (inside bounding box).
|
||||
if lx >= x1 && lx <= x2 && ly >= y1 && ly <= y2 {
|
||||
return VEH::Move;
|
||||
}
|
||||
|
||||
VEH::None
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// Add a file to the recent files list (most recent first, max 20) and persist to disk.
|
||||
@@ -2962,6 +3042,8 @@ impl HcieIcedApp {
|
||||
text_draft: None,
|
||||
selection_edge_cache: Default::default(),
|
||||
marching_ants_edges: None,
|
||||
selected_vector_shape: None,
|
||||
vector_edit_handle: hcie_engine_api::VectorEditHandle::None,
|
||||
});
|
||||
self.active_doc = self.documents.len() - 1;
|
||||
self.active_dialog = ActiveDialog::None;
|
||||
@@ -3356,55 +3438,87 @@ impl HcieIcedApp {
|
||||
|
||||
// ── Vector select / edit ─────────────────────
|
||||
Message::VectorSelectClick { x, y } => {
|
||||
use hcie_engine_api::VectorEditHandle as VEH;
|
||||
|
||||
// If a shape is already selected, hit-test its edit handles first.
|
||||
if self.documents[self.active_doc].selected_vector_shape.is_some() {
|
||||
let handle = self.vector_hit_test_handle(x, y);
|
||||
if handle != VEH::None && handle != VEH::Move {
|
||||
// Clicked on a resize/rotate handle — start drag with that handle.
|
||||
self.documents[self.active_doc].vector_edit_handle = handle;
|
||||
self.vector_drag_last = Some((x, y));
|
||||
return Task::none();
|
||||
}
|
||||
}
|
||||
|
||||
// Hit-test vector shapes under cursor; select the topmost one.
|
||||
if let Some(idx) = self.documents[self.active_doc].engine.find_vector_shape_at(x, y) {
|
||||
// Save snapshot before editing (for undo).
|
||||
let snapshot = self.documents[self.active_doc].engine.active_vector_shapes();
|
||||
self.vector_shapes_snapshot = snapshot;
|
||||
self.selected_vector_shape = Some(idx);
|
||||
// Start a drag from this position (move on drag, deselect on release without drag).
|
||||
let doc = &mut self.documents[self.active_doc];
|
||||
doc.selected_vector_shape = Some(idx);
|
||||
doc.vector_draw = None; // clear any preview
|
||||
doc.vector_edit_handle = VEH::Move;
|
||||
self.vector_drag_last = Some((x, y));
|
||||
self.vector_edit_handle = hcie_engine_api::VectorEditHandle::Move;
|
||||
} else {
|
||||
// No shape under cursor — deselect.
|
||||
self.selected_vector_shape = None;
|
||||
let doc = &mut self.documents[self.active_doc];
|
||||
doc.selected_vector_shape = None;
|
||||
doc.vector_edit_handle = VEH::None;
|
||||
self.vector_shapes_snapshot = None;
|
||||
self.vector_edit_handle = hcie_engine_api::VectorEditHandle::None;
|
||||
self.vector_drag_last = None;
|
||||
}
|
||||
}
|
||||
Message::VectorSelectDragStart { x, y } => {
|
||||
if self.selected_vector_shape.is_some() {
|
||||
if self.documents[self.active_doc].selected_vector_shape.is_some() {
|
||||
// Hit-test vector edit handles to determine drag mode.
|
||||
let handle = self.vector_hit_test_handle(x, y);
|
||||
self.documents[self.active_doc].vector_edit_handle = handle;
|
||||
self.vector_drag_last = Some((x, y));
|
||||
}
|
||||
}
|
||||
Message::VectorSelectDragMove { x, y } => {
|
||||
if let (Some(idx), Some((last_x, last_y))) = (self.selected_vector_shape, self.vector_drag_last) {
|
||||
let idx = self.documents[self.active_doc].selected_vector_shape;
|
||||
if let (Some(idx), Some((last_x, last_y))) = (idx, self.vector_drag_last) {
|
||||
let dx = x - last_x;
|
||||
let dy = y - last_y;
|
||||
let layer_id = self.documents[self.active_doc].engine.active_layer_id();
|
||||
self.documents[self.active_doc].engine.modify_vector_shape(
|
||||
layer_id,
|
||||
idx,
|
||||
self.vector_edit_handle,
|
||||
dx,
|
||||
dy,
|
||||
);
|
||||
let handle = self.documents[self.active_doc].vector_edit_handle;
|
||||
match handle {
|
||||
hcie_engine_api::VectorEditHandle::Rotate => {
|
||||
// Compute rotation delta from mouse movement.
|
||||
if let Some((cx, cy)) = self.documents[self.active_doc].engine.get_vector_shape_center(layer_id, idx) {
|
||||
let angle_start = (last_y - cy).atan2(last_x - cx);
|
||||
let angle_end = (y - cy).atan2(x - cx);
|
||||
let delta = angle_end - angle_start;
|
||||
let current = self.documents[self.active_doc].engine.vector_shape_angle(layer_id, idx);
|
||||
self.documents[self.active_doc].engine.set_vector_shape_angle(layer_id, idx, current + delta);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
self.documents[self.active_doc].engine.modify_vector_shape(
|
||||
layer_id,
|
||||
idx,
|
||||
handle,
|
||||
dx,
|
||||
dy,
|
||||
);
|
||||
}
|
||||
}
|
||||
self.vector_drag_last = Some((x, y));
|
||||
self.refresh_composite_if_needed();
|
||||
}
|
||||
}
|
||||
Message::VectorSelectDragEnd => {
|
||||
if self.vector_drag_last.is_some() {
|
||||
// Commit the move: if shapes changed, push history snapshot.
|
||||
// Commit the move/resize/rotate: if shapes changed, push history snapshot.
|
||||
self.vector_drag_last = None;
|
||||
self.vector_edit_handle = hcie_engine_api::VectorEditHandle::None;
|
||||
self.documents[self.active_doc].vector_edit_handle = hcie_engine_api::VectorEditHandle::None;
|
||||
}
|
||||
}
|
||||
Message::VectorShapeDelete => {
|
||||
if let Some(idx) = self.selected_vector_shape {
|
||||
if let Some(idx) = self.documents[self.active_doc].selected_vector_shape {
|
||||
let layer_id = self.documents[self.active_doc].engine.active_layer_id();
|
||||
// Remove the shape from the layer's shape list.
|
||||
if let Some(shapes) = self.documents[self.active_doc].engine.get_layer_shapes_direct(layer_id) {
|
||||
@@ -3412,7 +3526,7 @@ impl HcieIcedApp {
|
||||
shapes.remove(idx);
|
||||
}
|
||||
}
|
||||
self.selected_vector_shape = None;
|
||||
self.documents[self.active_doc].selected_vector_shape = None;
|
||||
self.vector_shapes_snapshot = None;
|
||||
self.documents[self.active_doc].engine.mark_composite_dirty();
|
||||
self.refresh_composite_if_needed();
|
||||
@@ -3420,7 +3534,7 @@ impl HcieIcedApp {
|
||||
}
|
||||
}
|
||||
Message::VectorShapeSelect(idx) => {
|
||||
self.selected_vector_shape = idx;
|
||||
self.documents[self.active_doc].selected_vector_shape = idx;
|
||||
if idx.is_some() {
|
||||
let snapshot = self.documents[self.active_doc].engine.active_vector_shapes();
|
||||
self.vector_shapes_snapshot = snapshot;
|
||||
@@ -4193,6 +4307,8 @@ impl HcieIcedApp {
|
||||
text_draft: None,
|
||||
selection_edge_cache: Default::default(),
|
||||
marching_ants_edges: None,
|
||||
selected_vector_shape: None,
|
||||
vector_edit_handle: hcie_engine_api::VectorEditHandle::None,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4760,10 +4876,10 @@ impl HcieIcedApp {
|
||||
return self.update(Message::CropCancel);
|
||||
} else if self.documents[self.active_doc].selection_transform.is_some() {
|
||||
return self.update(Message::TransformCancel);
|
||||
} else if self.selected_vector_shape.is_some() {
|
||||
self.selected_vector_shape = None;
|
||||
} else if self.documents[self.active_doc].selected_vector_shape.is_some() {
|
||||
self.documents[self.active_doc].selected_vector_shape = None;
|
||||
self.vector_shapes_snapshot = None;
|
||||
self.vector_edit_handle = hcie_engine_api::VectorEditHandle::None;
|
||||
self.documents[self.active_doc].vector_edit_handle = hcie_engine_api::VectorEditHandle::None;
|
||||
self.vector_drag_last = None;
|
||||
} else if self.documents[self.active_doc].selection_bounds.is_some()
|
||||
|| self.documents[self.active_doc].selection_rect.is_some()
|
||||
|
||||
Reference in New Issue
Block a user