MiMo 2.5 feat: enhance vector shape handling with angle snapping and color state management
This commit is contained in:
@@ -6200,11 +6200,26 @@ impl HcieIcedApp {
|
||||
};
|
||||
|
||||
if let Some(mut shape) = shape {
|
||||
if matches!(shape, hcie_engine_api::VectorShape::SvgShape { .. }) {
|
||||
// For arrow shapes, derive the angle from the drag
|
||||
// direction so the arrow points along the drawn line.
|
||||
if matches!(
|
||||
self.tool_state.active_tool,
|
||||
hcie_engine_api::Tool::VectorArrow
|
||||
) {
|
||||
let drag_angle = (y1 - y0).atan2(x1 - x0);
|
||||
let drag_angle = if self.modifiers.shift() {
|
||||
crate::vector_edit::snap_to_15deg(drag_angle)
|
||||
} else {
|
||||
drag_angle
|
||||
};
|
||||
shape.set_angle(drag_angle);
|
||||
} else if matches!(shape, hcie_engine_api::VectorShape::SvgShape { .. }) {
|
||||
shape.set_angle(svg_angle);
|
||||
}
|
||||
engine.add_vector_shape(shape);
|
||||
self.documents[self.active_doc].modified = true;
|
||||
// Reset angle so every new drawing starts at 0°.
|
||||
self.settings.tool_settings.vector_angle = 0.0;
|
||||
}
|
||||
self.refresh_composite_if_needed();
|
||||
}
|
||||
@@ -6380,6 +6395,7 @@ impl HcieIcedApp {
|
||||
(x, y),
|
||||
self.modifiers.shift(),
|
||||
self.modifiers.alt(),
|
||||
self.modifiers.shift(),
|
||||
);
|
||||
let (x1, y1, x2, y2) = shape.normalized_bounds();
|
||||
let angle = shape.angle();
|
||||
|
||||
@@ -220,6 +220,13 @@ pub fn panel_body<'a>(
|
||||
let layer_height = layer_info.map(|l| l.height).unwrap_or(0);
|
||||
let vector_shapes = doc.engine.active_vector_shapes().unwrap_or_default();
|
||||
let ts2 = app.settings.tool_settings.clone();
|
||||
// Read the selected shape's angle directly from the engine so
|
||||
// the slider updates instantly when the selection changes.
|
||||
let effective_vector_angle = doc
|
||||
.selected_vector_shape
|
||||
.and_then(|idx| vector_shapes.get(idx))
|
||||
.map(|s| s.angle().to_degrees())
|
||||
.unwrap_or(ts2.vector_angle);
|
||||
crate::panels::properties::view(
|
||||
&app.tool_state.active_tool,
|
||||
app.tool_state.brush_size,
|
||||
@@ -243,7 +250,7 @@ pub fn panel_body<'a>(
|
||||
ts2.vector_radius,
|
||||
ts2.vector_points,
|
||||
ts2.vector_sides,
|
||||
ts2.vector_angle,
|
||||
effective_vector_angle,
|
||||
active_id,
|
||||
app.fg_color,
|
||||
app.bg_color,
|
||||
|
||||
@@ -142,12 +142,16 @@ pub fn sync_tool_from_shape(app: &mut HcieIcedApp) {
|
||||
if let Some(shape) = shapes.get(shape_idx) {
|
||||
let ts = &mut app.settings.tool_settings;
|
||||
|
||||
// Common properties
|
||||
// Common properties — note: angle is intentionally NOT synced here
|
||||
// so that the "default angle for new shapes" stays independent from
|
||||
// the selected shape's rotation. The angle slider in the Properties
|
||||
// panel writes directly to `tool_settings.vector_angle` via
|
||||
// VectorAngleChanged and is pushed to the shape via
|
||||
// sync_shape_from_tool.
|
||||
ts.vector_stroke_width = shape.stroke();
|
||||
ts.vector_opacity = shape.shape_opacity();
|
||||
app.tool_state.brush_hardness = shape.hardness();
|
||||
ts.vector_fill = shape.is_filled();
|
||||
ts.vector_angle = shape.angle().to_degrees();
|
||||
|
||||
// Shape-specific properties
|
||||
match shape {
|
||||
@@ -178,12 +182,10 @@ pub fn sync_tool_from_shape(app: &mut HcieIcedApp) {
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Colors
|
||||
app.fg_color = shape.color();
|
||||
if let Some(fc) = shape.fill_color() {
|
||||
app.bg_color = fc;
|
||||
}
|
||||
app.colors_dirty = true;
|
||||
// Colors are intentionally NOT synced from the shape into the
|
||||
// active palette. The geometry / properties panels read the
|
||||
// shape's colors directly from the engine for display, and the
|
||||
// user picks new colors via the color palette or panel pickers.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,15 +114,18 @@ pub fn hit_test_handle(
|
||||
|
||||
/// Derives a safe shape for the current pointer position in a drag.
|
||||
///
|
||||
/// Arguments: `session` is the immutable drag basis, `pointer` is canvas-space, `lock_aspect`
|
||||
/// corresponds to Shift, and `from_center` corresponds to Alt. Returns: A finite shape with
|
||||
/// dimensions at least `MIN_VECTOR_SIZE`; invalid pointer input returns the original shape.
|
||||
/// Arguments: `session` is the immutable drag basis, `pointer` is canvas-space,
|
||||
/// `lock_aspect` corresponds to Shift (aspect-lock on resize), `from_center`
|
||||
/// corresponds to Alt, and `snap_angle` corresponds to Shift on rotation.
|
||||
/// Returns: A finite shape with dimensions at least `MIN_VECTOR_SIZE`; invalid
|
||||
/// pointer input returns the original shape.
|
||||
/// Side Effects: None.
|
||||
pub fn transform_shape(
|
||||
session: &VectorDragSession,
|
||||
pointer: (f32, f32),
|
||||
lock_aspect: bool,
|
||||
from_center: bool,
|
||||
snap_angle: bool,
|
||||
) -> VectorShape {
|
||||
use VectorEditHandle as H;
|
||||
|
||||
@@ -152,8 +155,13 @@ pub fn transform_shape(
|
||||
(session.pointer_start.1 - center.1).atan2(session.pointer_start.0 - center.0);
|
||||
let current_angle = (pointer.1 - center.1).atan2(pointer.0 - center.0);
|
||||
let angle = session.original.angle() + normalize_angle(current_angle - start_angle);
|
||||
let angle = if snap_angle {
|
||||
snap_to_15deg(angle)
|
||||
} else {
|
||||
normalize_angle(angle)
|
||||
};
|
||||
if angle.is_finite() {
|
||||
result.set_angle(normalize_angle(angle));
|
||||
result.set_angle(angle);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -302,6 +310,16 @@ pub fn normalize_angle(angle: f32) -> f32 {
|
||||
(angle + std::f32::consts::PI).rem_euclid(two_pi) - std::f32::consts::PI
|
||||
}
|
||||
|
||||
/// Snaps an angle (radians) to the nearest 15° increment.
|
||||
///
|
||||
/// Used for Shift-constrained drawing and rotation. 15° = PI/12.
|
||||
/// Returns the nearest snapped angle within [-PI, PI].
|
||||
pub fn snap_to_15deg(angle: f32) -> f32 {
|
||||
let step = std::f32::consts::PI / 12.0; // 15°
|
||||
let snapped = (angle / step).round() * step;
|
||||
normalize_angle(snapped)
|
||||
}
|
||||
|
||||
/// Inverse-rotates a point around a center. Arguments and return are canvas-space coordinates.
|
||||
/// Side Effects: None.
|
||||
fn inverse_rotate(point: (f32, f32), center: (f32, f32), angle: f32) -> (f32, f32) {
|
||||
@@ -467,10 +485,10 @@ mod tests {
|
||||
pointer_start: (5.0, 5.0),
|
||||
original: rect(),
|
||||
};
|
||||
let moved = transform_shape(&session, (15.0, 25.0), false, false);
|
||||
let moved = transform_shape(&session, (15.0, 25.0), false, false, false);
|
||||
assert_eq!(moved.normalized_bounds(), (20.0, 40.0, 120.0, 90.0));
|
||||
assert_eq!(
|
||||
transform_shape(&session, (f32::NAN, 0.0), false, false),
|
||||
transform_shape(&session, (f32::NAN, 0.0), false, false, false),
|
||||
rect()
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user