mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-27 13:28:11 +08:00
Add layer and artboard nudge resizing (#4242)
* Add layer and artboard nudge resizing * Improve bounding box validation for layer resizing to ensure only finite bounds are considered
This commit is contained in:
@@ -586,3 +586,57 @@ pub fn make_path_editable_is_allowed(network_interface: &mut NodeNetworkInterfac
|
||||
|
||||
Some(first_layer)
|
||||
}
|
||||
|
||||
/// Smallest extent, in document units, a nudge-resized box may have per axis (avoids a zero divisor and prevents collapse/inversion).
|
||||
const NUDGE_RESIZE_MIN_EXTENT: f64 = 1.;
|
||||
|
||||
/// The resized box corners and the document-space scale transform produced by [`nudge_resize_bounds`].
|
||||
pub struct NudgeResize {
|
||||
pub min: DVec2,
|
||||
pub max: DVec2,
|
||||
pub transform: DAffine2,
|
||||
}
|
||||
|
||||
/// Resizes the axis-aligned document-space box `[min, max]` by an arrow-key nudge `delta` (screen space).
|
||||
///
|
||||
/// `tilt` (document rotation) is snapped to a quarter turn so the arrow maps onto a box axis. The top-left corner is anchored by default,
|
||||
/// or the bottom-right corner when `resize_opposite` (Control) is held; the other corner moves. The box stays at least one unit per axis.
|
||||
pub fn nudge_resize_bounds(min: DVec2, max: DVec2, delta: DVec2, tilt: f64, resize_opposite: bool) -> NudgeResize {
|
||||
// Snap rotation to a quarter turn so the screen arrow lands on a document-space box axis
|
||||
let doc_delta = match ((tilt / std::f64::consts::FRAC_PI_2).round() as i32).rem_euclid(4) {
|
||||
1 => DVec2::new(delta.y, -delta.x),
|
||||
2 => -delta,
|
||||
3 => DVec2::new(-delta.y, delta.x),
|
||||
_ => delta,
|
||||
};
|
||||
|
||||
// Move one corner by the arrow, keeping the other (the anchor) at least the minimum extent away
|
||||
let mut new_min = min;
|
||||
let mut new_max = max;
|
||||
if resize_opposite {
|
||||
new_min += doc_delta;
|
||||
new_min = new_min.min(new_max - DVec2::splat(NUDGE_RESIZE_MIN_EXTENT));
|
||||
} else {
|
||||
new_max += doc_delta;
|
||||
new_max = new_max.max(new_min + DVec2::splat(NUDGE_RESIZE_MIN_EXTENT));
|
||||
}
|
||||
|
||||
// Ratio of new to old extent, treating a degenerate (sub-unit) original extent as unscaled
|
||||
let old_extent = max - min;
|
||||
let new_extent = new_max - new_min;
|
||||
let scale = DVec2::new(
|
||||
if old_extent.x.abs() < NUDGE_RESIZE_MIN_EXTENT { 1. } else { new_extent.x / old_extent.x },
|
||||
if old_extent.y.abs() < NUDGE_RESIZE_MIN_EXTENT { 1. } else { new_extent.y / old_extent.y },
|
||||
);
|
||||
|
||||
// Scale about the anchored corner, guarding non-finite components to zero
|
||||
let anchor = if resize_opposite { max } else { min };
|
||||
let scale_center = DVec2::new(if anchor.x.is_finite() { anchor.x } else { 0. }, if anchor.y.is_finite() { anchor.y } else { 0. });
|
||||
let transform = DAffine2::from_scale_angle_translation(scale, 0., scale_center - scale * scale_center);
|
||||
|
||||
NudgeResize {
|
||||
min: new_min,
|
||||
max: new_max,
|
||||
transform,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ use crate::messages::tool::common_functionality::snapping;
|
||||
use crate::messages::tool::common_functionality::snapping::SnapCandidatePoint;
|
||||
use crate::messages::tool::common_functionality::snapping::SnapData;
|
||||
use crate::messages::tool::common_functionality::transformation_cage::*;
|
||||
use crate::messages::tool::common_functionality::utility_functions::nudge_resize_bounds;
|
||||
use graph_craft::document::NodeId;
|
||||
use graphene_std::renderer::{Quad, Rect};
|
||||
|
||||
@@ -30,7 +31,7 @@ pub enum ArtboardToolMessage {
|
||||
// Tool-specific messages
|
||||
UpdateSelectedArtboard,
|
||||
DeleteSelected,
|
||||
NudgeSelected { delta_x: f64, delta_y: f64 },
|
||||
NudgeSelected { delta_x: f64, delta_y: f64, resize: Key, resize_opposite: Key },
|
||||
PointerDown,
|
||||
PointerMove { constrain_axis_or_aspect: Key, center: Key },
|
||||
PointerOutsideViewport { constrain_axis_or_aspect: Key, center: Key },
|
||||
@@ -480,8 +481,16 @@ impl Fsm for ArtboardToolFsmState {
|
||||
|
||||
ArtboardToolFsmState::Ready { hovered }
|
||||
}
|
||||
(_, ArtboardToolMessage::NudgeSelected { delta_x, delta_y }) => {
|
||||
let Some(bounds) = &mut tool_data.bounding_box_manager else {
|
||||
(
|
||||
_,
|
||||
ArtboardToolMessage::NudgeSelected {
|
||||
delta_x,
|
||||
delta_y,
|
||||
resize,
|
||||
resize_opposite,
|
||||
},
|
||||
) => {
|
||||
let Some(bounds) = &tool_data.bounding_box_manager else {
|
||||
return ArtboardToolFsmState::Ready { hovered };
|
||||
};
|
||||
let Some(selected_artboard) = tool_data.selected_artboard else {
|
||||
@@ -493,12 +502,24 @@ impl Fsm for ArtboardToolFsmState {
|
||||
}
|
||||
|
||||
let [existing_top_left, existing_bottom_right] = bounds.bounds;
|
||||
let delta = DVec2::from_angle(-document.document_ptz.tilt()).rotate(DVec2::new(delta_x, delta_y));
|
||||
let tilt = document.document_ptz.tilt();
|
||||
|
||||
// The resize key switches from nudging the artboard's position to resizing its box, anchored to the opposite edge by the other key
|
||||
let resize = input.keyboard.key(resize);
|
||||
let (location, dimensions) = if resize {
|
||||
let resize_opposite = input.keyboard.key(resize_opposite);
|
||||
let resized = nudge_resize_bounds(existing_top_left, existing_bottom_right, DVec2::new(delta_x, delta_y), tilt, resize_opposite);
|
||||
(resized.min.round(), (resized.max - resized.min).round())
|
||||
} else {
|
||||
let delta = DVec2::from_angle(-tilt).rotate(DVec2::new(delta_x, delta_y));
|
||||
((existing_top_left + delta).round(), (existing_bottom_right - existing_top_left).round())
|
||||
};
|
||||
|
||||
responses.add(DocumentMessage::AddTransaction);
|
||||
responses.add(GraphOperationMessage::ResizeArtboard {
|
||||
layer: selected_artboard,
|
||||
location: DVec2::new(existing_top_left.x + delta.x, existing_top_left.y + delta.y).round(),
|
||||
dimensions: (existing_bottom_right - existing_top_left).round(),
|
||||
location,
|
||||
dimensions,
|
||||
});
|
||||
|
||||
ArtboardToolFsmState::Ready { hovered }
|
||||
|
||||
@@ -1812,7 +1812,12 @@ impl Fsm for SelectToolFsmState {
|
||||
// TODO: Make all the following hints only appear if there is at least one selected layer
|
||||
HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Drag Selected")]),
|
||||
HintGroup(vec![HintInfo::multi_keys([[Key::KeyG], [Key::KeyR], [Key::KeyS]], "Grab/Rotate/Scale Selected")]),
|
||||
HintGroup(vec![HintInfo::arrow_keys("Nudge Selected"), HintInfo::keys([Key::Shift], "10x").prepend_plus()]),
|
||||
HintGroup(vec![
|
||||
HintInfo::arrow_keys("Nudge Selected"),
|
||||
HintInfo::keys([Key::Shift], "10x").prepend_plus(),
|
||||
HintInfo::keys([Key::Alt], "Resize Corner").prepend_plus(),
|
||||
HintInfo::keys([Key::Control], "Other Corner").prepend_plus(),
|
||||
]),
|
||||
HintGroup(vec![
|
||||
HintInfo::keys_and_mouse([Key::Alt], MouseMotion::LmbDrag, "Move Duplicate"),
|
||||
HintInfo::keys([Key::Control, Key::KeyD], "Duplicate").add_mac_keys([Key::Command, Key::KeyD]),
|
||||
|
||||
@@ -133,7 +133,7 @@ pub enum ShapeToolMessage {
|
||||
IncreaseSides,
|
||||
DecreaseSides,
|
||||
|
||||
NudgeSelectedLayers { delta_x: f64, delta_y: f64 },
|
||||
NudgeSelectedLayers { delta_x: f64, delta_y: f64, resize: Key, resize_opposite: Key },
|
||||
}
|
||||
|
||||
fn create_sides_widget(vertices: u32) -> WidgetInstance {
|
||||
@@ -994,8 +994,21 @@ impl Fsm for ShapeToolFsmState {
|
||||
}
|
||||
self
|
||||
}
|
||||
(ShapeToolFsmState::Ready(_), ShapeToolMessage::NudgeSelectedLayers { delta_x, delta_y }) => {
|
||||
responses.add(DocumentMessage::NudgeSelectedLayers { delta_x, delta_y });
|
||||
(
|
||||
ShapeToolFsmState::Ready(_),
|
||||
ShapeToolMessage::NudgeSelectedLayers {
|
||||
delta_x,
|
||||
delta_y,
|
||||
resize,
|
||||
resize_opposite,
|
||||
},
|
||||
) => {
|
||||
responses.add(DocumentMessage::NudgeSelectedLayers {
|
||||
delta_x,
|
||||
delta_y,
|
||||
resize,
|
||||
resize_opposite,
|
||||
});
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user