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();
|
||||
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,
|
||||
self.vector_edit_handle,
|
||||
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()
|
||||
|
||||
@@ -72,6 +72,12 @@ struct OverlayProgram {
|
||||
gradient_drag: Option<((f32, f32), (f32, f32))>,
|
||||
/// Extracted edges for marching ants rendering.
|
||||
marching_ants_edges: Option<std::sync::Arc<Vec<(u32, u32, u32, u32)>>>,
|
||||
/// Normalized bounds of the selected vector shape in canvas-space (x1, y1, x2, y2).
|
||||
selected_vector_bounds: Option<(f32, f32, f32, f32)>,
|
||||
/// Rotation angle (radians) of the selected vector shape.
|
||||
selected_vector_angle: f32,
|
||||
/// Which vector edit handle is active (hovered or being dragged).
|
||||
vector_edit_handle: hcie_engine_api::VectorEditHandle,
|
||||
}
|
||||
|
||||
/// Overlay state — tracks hover and cursor position for crosshair.
|
||||
@@ -325,6 +331,195 @@ impl OverlayProgram {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw transform handles for selected vector shapes.
|
||||
///
|
||||
/// Renders a blue selection outline, 8 white resize handles at corners/edges,
|
||||
/// and a rotation handle (line + blue circle) above the top-center. All
|
||||
/// geometry is rotated by `angle` around the bounding-box center.
|
||||
fn draw_vector_edit_handles(
|
||||
&self,
|
||||
frame: &mut Frame,
|
||||
x1: f32, y1: f32, x2: f32, y2: f32,
|
||||
angle: f32,
|
||||
origin_x: f32,
|
||||
origin_y: f32,
|
||||
) {
|
||||
let cx = (x1 + x2) / 2.0;
|
||||
let cy = (y1 + y2) / 2.0;
|
||||
let w = (x2 - x1).abs();
|
||||
let h = (y2 - y1).abs();
|
||||
if w < 0.5 || h < 0.5 { return; }
|
||||
|
||||
let cos_a = angle.cos();
|
||||
let sin_a = angle.sin();
|
||||
|
||||
// Helper: rotate a canvas-space point around (cx, cy) by angle.
|
||||
let rotate = |px: f32, py: f32| -> (f32, f32) {
|
||||
let dx = px - cx;
|
||||
let dy = py - cy;
|
||||
(cx + dx * cos_a - dy * sin_a, cy + dx * sin_a + dy * cos_a)
|
||||
};
|
||||
|
||||
let line_width = 1.5 / self.zoom.max(1.0);
|
||||
|
||||
// ── Blue selection outline ─────────────────────────────────────────
|
||||
let outline_color = iced::Color::from_rgb(0.2, 0.5, 1.0);
|
||||
let corners: [(f32, f32); 4] = [(x1, y1), (x2, y1), (x2, y2), (x1, y2)];
|
||||
let rotated_corners: Vec<(f32, f32)> = corners.iter().map(|&(px, py)| rotate(px, py)).collect();
|
||||
let outline = Path::new(|b| {
|
||||
for (i, &(px, py)) in rotated_corners.iter().enumerate() {
|
||||
let sx = origin_x + px * self.zoom;
|
||||
let sy = origin_y + py * self.zoom;
|
||||
if i == 0 { b.move_to(Point::new(sx, sy)); }
|
||||
else { b.line_to(Point::new(sx, sy)); }
|
||||
}
|
||||
b.close();
|
||||
});
|
||||
frame.stroke(&outline, Stroke {
|
||||
style: canvas::stroke::Style::Solid(outline_color),
|
||||
width: line_width,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
// ── 8 resize handles (white squares with black stroke) ─────────────
|
||||
let handle_size = 8.0 / self.zoom.max(1.0);
|
||||
let half = handle_size / 2.0;
|
||||
let white = iced::Color::from_rgb(1.0, 1.0, 1.0);
|
||||
let black = iced::Color::from_rgb(0.0, 0.0, 0.0);
|
||||
|
||||
let handle_points: [(f32, f32); 8] = [
|
||||
(x1, y1), // TopLeft
|
||||
(x2, y1), // TopRight
|
||||
(x1, y2), // BottomLeft
|
||||
(x2, y2), // BottomRight
|
||||
(cx, y1), // Top
|
||||
(cx, y2), // Bottom
|
||||
(x1, cy), // Left
|
||||
(x2, cy), // Right
|
||||
];
|
||||
|
||||
for &(px, py) in &handle_points {
|
||||
let (rpx, rpy) = rotate(px, py);
|
||||
let sx = origin_x + rpx * self.zoom;
|
||||
let sy = origin_y + rpy * self.zoom;
|
||||
let rect = Path::rectangle(
|
||||
Point::new(sx - half, sy - half),
|
||||
Size::new(handle_size, handle_size),
|
||||
);
|
||||
frame.fill(&rect, white);
|
||||
frame.stroke(&rect, Stroke {
|
||||
style: canvas::stroke::Style::Solid(black),
|
||||
width: line_width,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
// ── Rotation handle (line from top-center upward + blue circle) ────
|
||||
let rotate_offset = 24.0 / self.zoom.max(1.0);
|
||||
let (rot_line_start_x, rot_line_start_y) = rotate(cx, y1);
|
||||
let (rot_circ_x, rot_circ_y) = rotate(cx, y1 - rotate_offset);
|
||||
let rot_circ_radius = 4.0 / self.zoom.max(1.0);
|
||||
|
||||
// Line from top-center to rotation circle
|
||||
let rot_line = Path::line(
|
||||
Point::new(origin_x + rot_line_start_x * self.zoom, origin_y + rot_line_start_y * self.zoom),
|
||||
Point::new(origin_x + rot_circ_x * self.zoom, origin_y + rot_circ_y * self.zoom),
|
||||
);
|
||||
frame.stroke(&rot_line, Stroke {
|
||||
style: canvas::stroke::Style::Solid(black),
|
||||
width: line_width,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
// Blue rotation circle
|
||||
let rot_circle = Path::circle(
|
||||
Point::new(origin_x + rot_circ_x * self.zoom, origin_y + rot_circ_y * self.zoom),
|
||||
rot_circ_radius,
|
||||
);
|
||||
let blue = iced::Color::from_rgb(0.2, 0.5, 1.0);
|
||||
frame.fill(&rot_circle, blue);
|
||||
frame.stroke(&rot_circle, Stroke {
|
||||
style: canvas::stroke::Style::Solid(black),
|
||||
width: line_width,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
/// Hit-test which vector edit handle is at the given viewport position.
|
||||
///
|
||||
/// Checks rotation handle first (circle area), then resize handles (square
|
||||
/// hit-box), then the move handle (expanded bounding box). Returns the
|
||||
/// matching `VectorEditHandle` variant.
|
||||
pub fn hit_test_vector_handle(
|
||||
&self,
|
||||
viewport_x: f32,
|
||||
viewport_y: f32,
|
||||
) -> hcie_engine_api::VectorEditHandle {
|
||||
use hcie_engine_api::VectorEditHandle as VEH;
|
||||
|
||||
let Some((x1, y1, x2, y2)) = self.selected_vector_bounds else {
|
||||
return VEH::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 cx = (x1 + x2) / 2.0;
|
||||
let cy = (y1 + y2) / 2.0;
|
||||
let angle = self.selected_vector_angle;
|
||||
let cos_a = angle.cos();
|
||||
let sin_a = angle.sin();
|
||||
|
||||
// Inverse-rotate a canvas-space point around (cx, cy) by -angle.
|
||||
let unrotate = |px: f32, py: f32| -> (f32, f32) {
|
||||
let dx = px - cx;
|
||||
let dy = py - cy;
|
||||
(cx + dx * cos_a + dy * sin_a, cy - dx * sin_a + dy * cos_a)
|
||||
};
|
||||
|
||||
let (lx, ly) = unrotate(canvas_x, canvas_y);
|
||||
|
||||
// ── Check rotation handle first (outside the box) ──────────────────
|
||||
let rotate_offset = 24.0 / self.zoom.max(1.0);
|
||||
let rot_circ_x = cx;
|
||||
let rot_circ_y = y1 - rotate_offset;
|
||||
let rot_radius = 6.0 / self.zoom.max(1.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 / self.zoom.max(1.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, cx, y1),
|
||||
(VEH::Bottom, cx, y2),
|
||||
(VEH::Left, x1, cy),
|
||||
(VEH::Right, x2, cy),
|
||||
];
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
impl canvas::Program<Message> for OverlayProgram {
|
||||
@@ -568,6 +763,11 @@ impl canvas::Program<Message> for OverlayProgram {
|
||||
}
|
||||
}
|
||||
|
||||
// Draw vector edit handles for selected shape (blue outline, resize + rotation)
|
||||
if let Some((x1, y1, x2, y2)) = self.selected_vector_bounds {
|
||||
self.draw_vector_edit_handles(&mut frame, x1, y1, x2, y2, self.selected_vector_angle, origin_x, origin_y);
|
||||
}
|
||||
|
||||
// Draw gradient endpoint preview line.
|
||||
if let Some(((gx0, gy0), (gx1, gy1))) = self.gradient_drag {
|
||||
let p0 = Point::new(origin_x + gx0 * self.zoom, origin_y + gy0 * self.zoom);
|
||||
@@ -735,6 +935,24 @@ pub fn view<'a>(
|
||||
.height(Length::Fill);
|
||||
|
||||
// ── Overlay canvas (selection, vector preview, crosshair, pressure HUD) ──────
|
||||
// Compute selected vector shape bounds for handle rendering.
|
||||
let (sel_vec_bounds, sel_vec_angle) = if let Some(idx) = doc.selected_vector_shape {
|
||||
let layer_id = doc.engine.active_layer_id();
|
||||
if let Some(shapes) = doc.engine.active_vector_shapes() {
|
||||
if let Some(shape) = shapes.get(idx) {
|
||||
let (x1, y1, x2, y2) = shape.normalized_bounds();
|
||||
let angle = doc.engine.vector_shape_angle(layer_id, idx);
|
||||
(Some((x1, y1, x2, y2)), angle)
|
||||
} else {
|
||||
(None, 0.0)
|
||||
}
|
||||
} else {
|
||||
(None, 0.0)
|
||||
}
|
||||
} else {
|
||||
(None, 0.0)
|
||||
};
|
||||
|
||||
let overlay_program = OverlayProgram {
|
||||
engine_w,
|
||||
engine_h,
|
||||
@@ -751,6 +969,9 @@ pub fn view<'a>(
|
||||
active_tool: tool_state.active_tool,
|
||||
gradient_drag: doc.gradient_drag,
|
||||
marching_ants_edges: doc.marching_ants_edges.clone(),
|
||||
selected_vector_bounds: sel_vec_bounds,
|
||||
selected_vector_angle: sel_vec_angle,
|
||||
vector_edit_handle: doc.vector_edit_handle,
|
||||
};
|
||||
|
||||
let overlay_canvas = canvas(overlay_program)
|
||||
|
||||
@@ -104,7 +104,7 @@ pub fn dock_view<'a>(
|
||||
}
|
||||
PaneType::Geometry => {
|
||||
let shapes = app.documents[app.active_doc].engine.active_vector_shapes().unwrap_or_default();
|
||||
crate::panels::geometry::view(shapes, colors, app.selected_vector_shape)
|
||||
crate::panels::geometry::view(shapes, colors, app.documents[app.active_doc].selected_vector_shape)
|
||||
}
|
||||
PaneType::ToolSettings => {
|
||||
let fg = app.fg_color;
|
||||
|
||||
Reference in New Issue
Block a user