mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-08-04 13:30:48 +00:00
Further polishing of G/R/S visualization and features (#2243)
* Further polishing of G/R/S visualisation and features Followup to #2229. * Begin typing only if constrained or not in G * Prevent adding empty group in R mode. Order fn alphabetically as was before * Always show typing hints unless can't begin typing * Fix one frame bug * Add cancel and confirm groups for GRS hints * Fix inconsistency in call increments, snaps * Use top/bottom left/right methods with quads where more readable * Fix inconsistent use of narrow/flat * Add hints to transform cage Fixes9392658955
. * Rename some hints * Fix scale radial behaviour, grab constraints and local edge orientation * Fix not being able to remove the whole selection with delete modifier Fixes1336221441
. * Fix compiling * Fix crash when single point bbox Fixes #2267 * Fix the same crash in scale and use better name for bbox * cargo fmt --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
da752e5324
commit
4de65c292a
6 changed files with 211 additions and 154 deletions
|
@ -41,8 +41,8 @@ pub const MAX_LAYER_SNAP_POINTS: usize = 100;
|
|||
pub const DRAG_THRESHOLD: f64 = 1.;
|
||||
|
||||
// TRANSFORMING LAYER
|
||||
pub const ROTATE_SNAP_ANGLE: f64 = 15.;
|
||||
pub const SCALE_SNAP_INTERVAL: f64 = 0.1;
|
||||
pub const ROTATE_INCREMENT: f64 = 15.;
|
||||
pub const SCALE_INCREMENT: f64 = 0.1;
|
||||
pub const SLOWING_DIVISOR: f64 = 10.;
|
||||
pub const NUDGE_AMOUNT: f64 = 1.;
|
||||
pub const BIG_NUDGE_AMOUNT: f64 = 10.;
|
||||
|
|
|
@ -45,7 +45,7 @@ impl OverlayContext {
|
|||
}
|
||||
|
||||
pub fn polygon(&mut self, polygon: &[DVec2], color_fill: Option<&str>) {
|
||||
self.dashed_polygon(&polygon, color_fill, None, None, None);
|
||||
self.dashed_polygon(polygon, color_fill, None, None, None);
|
||||
}
|
||||
|
||||
pub fn dashed_polygon(&mut self, polygon: &[DVec2], color_fill: Option<&str>, dash_width: Option<f64>, dash_gap_width: Option<f64>, dash_offset: Option<f64>) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use super::network_interface::NodeNetworkInterface;
|
||||
use crate::consts::{ROTATE_SNAP_ANGLE, SCALE_SNAP_INTERVAL};
|
||||
use crate::consts::{ROTATE_INCREMENT, SCALE_INCREMENT};
|
||||
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
|
||||
use crate::messages::prelude::*;
|
||||
|
@ -12,7 +12,7 @@ use graphene_core::vector::ManipulatorPointId;
|
|||
use graphene_core::vector::VectorModificationType;
|
||||
use graphene_std::vector::{HandleId, PointId};
|
||||
|
||||
use glam::{DAffine2, DVec2};
|
||||
use glam::{DAffine2, DMat2, DVec2};
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
|
@ -143,10 +143,13 @@ pub struct Translation {
|
|||
}
|
||||
|
||||
impl Translation {
|
||||
pub fn to_dvec(self, transform: DAffine2, snap: bool) -> DVec2 {
|
||||
pub fn to_dvec(self, transform: DAffine2, increment_mode: bool) -> DVec2 {
|
||||
let displacement = if let Some(value) = self.typed_distance {
|
||||
let document_displacement = if self.constraint == Axis::Y { DVec2::new(0., value) } else { DVec2::new(value, 0.) };
|
||||
transform.transform_vector2(document_displacement)
|
||||
match self.constraint {
|
||||
Axis::X => transform.transform_vector2(DVec2::new(value, 0.)),
|
||||
Axis::Y => transform.transform_vector2(DVec2::new(0., value)),
|
||||
Axis::Both => self.dragged_distance,
|
||||
}
|
||||
} else {
|
||||
match self.constraint {
|
||||
Axis::Both => self.dragged_distance,
|
||||
|
@ -155,7 +158,7 @@ impl Translation {
|
|||
}
|
||||
};
|
||||
let displacement = transform.inverse().transform_vector2(displacement);
|
||||
if snap {
|
||||
if increment_mode {
|
||||
displacement.round()
|
||||
} else {
|
||||
displacement
|
||||
|
@ -196,12 +199,12 @@ pub struct Rotation {
|
|||
}
|
||||
|
||||
impl Rotation {
|
||||
pub fn to_f64(self, snap: bool) -> f64 {
|
||||
pub fn to_f64(self, increment_mode: bool) -> f64 {
|
||||
if let Some(value) = self.typed_angle {
|
||||
value.to_radians()
|
||||
} else if snap {
|
||||
let snap_resolution = ROTATE_SNAP_ANGLE.to_radians();
|
||||
(self.dragged_angle / snap_resolution).round() * snap_resolution
|
||||
} else if increment_mode {
|
||||
let increment_resolution = ROTATE_INCREMENT.to_radians();
|
||||
(self.dragged_angle / increment_resolution).round() * increment_resolution
|
||||
} else {
|
||||
self.dragged_angle
|
||||
}
|
||||
|
@ -245,17 +248,17 @@ impl Default for Scale {
|
|||
}
|
||||
|
||||
impl Scale {
|
||||
pub fn to_f64(self, snap: bool) -> f64 {
|
||||
pub fn to_f64(self, increment: bool) -> f64 {
|
||||
let factor = if let Some(value) = self.typed_factor { value } else { self.dragged_factor };
|
||||
if snap {
|
||||
(factor / SCALE_SNAP_INTERVAL).round() * SCALE_SNAP_INTERVAL
|
||||
if increment {
|
||||
(factor / SCALE_INCREMENT).round() * SCALE_INCREMENT
|
||||
} else {
|
||||
factor
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_dvec(self, snap: bool) -> DVec2 {
|
||||
let factor = self.to_f64(snap);
|
||||
pub fn to_dvec(self, increment_mode: bool) -> DVec2 {
|
||||
let factor = self.to_f64(increment_mode);
|
||||
|
||||
match self.constraint {
|
||||
Axis::Both => DVec2::splat(factor),
|
||||
|
@ -272,7 +275,7 @@ impl Scale {
|
|||
#[must_use]
|
||||
pub fn increment_amount(self, delta: f64) -> Self {
|
||||
Self {
|
||||
dragged_factor: self.dragged_factor + delta,
|
||||
dragged_factor: (self.dragged_factor + delta),
|
||||
typed_factor: None,
|
||||
constraint: self.constraint,
|
||||
}
|
||||
|
@ -302,25 +305,29 @@ pub enum TransformOperation {
|
|||
}
|
||||
|
||||
impl TransformOperation {
|
||||
pub fn apply_transform_operation(&self, selected: &mut Selected, snapping: bool, local: bool, quad: Quad, transform: DAffine2) {
|
||||
let quad = quad.0;
|
||||
let edge = quad[1] - quad[0];
|
||||
pub fn apply_transform_operation(&self, selected: &mut Selected, increment_mode: bool, local: bool, quad: Quad, transform: DAffine2) {
|
||||
let local_axis_transform_angle = (quad.top_left() - quad.top_right()).to_angle();
|
||||
if self != &TransformOperation::None {
|
||||
let transformation = match self {
|
||||
TransformOperation::Grabbing(translation) => {
|
||||
let translate = DAffine2::from_translation(transform.transform_vector2(translation.to_dvec(transform, snapping)));
|
||||
let translate = DAffine2::from_translation(transform.transform_vector2(translation.to_dvec(transform, increment_mode)));
|
||||
if local {
|
||||
DAffine2::from_angle(edge.to_angle()) * translate * DAffine2::from_angle(-edge.to_angle())
|
||||
let resolved_angle = if local_axis_transform_angle > 0. {
|
||||
local_axis_transform_angle - std::f64::consts::PI
|
||||
} else {
|
||||
local_axis_transform_angle
|
||||
};
|
||||
DAffine2::from_angle(resolved_angle) * translate * DAffine2::from_angle(-resolved_angle)
|
||||
} else {
|
||||
translate
|
||||
}
|
||||
}
|
||||
TransformOperation::Rotating(rotation) => DAffine2::from_angle(rotation.to_f64(snapping)),
|
||||
TransformOperation::Rotating(rotation) => DAffine2::from_angle(rotation.to_f64(increment_mode)),
|
||||
TransformOperation::Scaling(scale) => {
|
||||
if local {
|
||||
DAffine2::from_angle(edge.to_angle()) * DAffine2::from_scale(scale.to_dvec(snapping)) * DAffine2::from_angle(-edge.to_angle())
|
||||
DAffine2::from_angle(local_axis_transform_angle) * DAffine2::from_scale(scale.to_dvec(increment_mode)) * DAffine2::from_angle(-local_axis_transform_angle)
|
||||
} else {
|
||||
DAffine2::from_scale(scale.to_dvec(snapping))
|
||||
DAffine2::from_scale(scale.to_dvec(increment_mode))
|
||||
}
|
||||
}
|
||||
TransformOperation::None => unreachable!(),
|
||||
|
@ -331,16 +338,19 @@ impl TransformOperation {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn is_typing(&self) -> bool {
|
||||
pub fn axis_constraint(&self) -> Axis {
|
||||
match self {
|
||||
TransformOperation::None => false,
|
||||
TransformOperation::Grabbing(translation) => translation.typed_distance.is_some(),
|
||||
TransformOperation::Rotating(rotation) => rotation.typed_angle.is_some(),
|
||||
TransformOperation::Scaling(scale) => scale.typed_factor.is_some(),
|
||||
TransformOperation::Grabbing(grabbing) => grabbing.constraint,
|
||||
TransformOperation::Scaling(scaling) => scaling.constraint,
|
||||
_ => Axis::Both,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn constrain_axis(&mut self, axis: Axis, selected: &mut Selected, snapping: bool, mut local: bool, quad: Quad, transform: DAffine2) -> bool {
|
||||
pub fn can_begin_typing(&self) -> bool {
|
||||
self.is_constraint_to_axis() || !matches!(self, TransformOperation::Grabbing(_))
|
||||
}
|
||||
|
||||
pub fn constrain_axis(&mut self, axis: Axis, selected: &mut Selected, increment_mode: bool, mut local: bool, quad: Quad, transform: DAffine2) -> bool {
|
||||
(*self, local) = match self {
|
||||
TransformOperation::Grabbing(translation) => {
|
||||
let (translation, local) = translation.with_constraint(axis, local);
|
||||
|
@ -352,11 +362,11 @@ impl TransformOperation {
|
|||
}
|
||||
_ => (*self, false),
|
||||
};
|
||||
self.apply_transform_operation(selected, snapping, local, quad, transform);
|
||||
self.apply_transform_operation(selected, increment_mode, local, quad, transform);
|
||||
local
|
||||
}
|
||||
|
||||
pub fn grs_typed(&mut self, typed: Option<f64>, selected: &mut Selected, snapping: bool, local: bool, quad: Quad, transform: DAffine2) {
|
||||
pub fn grs_typed(&mut self, typed: Option<f64>, selected: &mut Selected, increment_mode: bool, local: bool, quad: Quad, transform: DAffine2) {
|
||||
match self {
|
||||
TransformOperation::None => (),
|
||||
TransformOperation::Grabbing(translation) => translation.typed_distance = typed,
|
||||
|
@ -364,45 +374,34 @@ impl TransformOperation {
|
|||
TransformOperation::Scaling(scale) => scale.typed_factor = typed,
|
||||
};
|
||||
|
||||
self.apply_transform_operation(selected, snapping, local, quad, transform);
|
||||
self.apply_transform_operation(selected, increment_mode, local, quad, transform);
|
||||
}
|
||||
|
||||
pub fn hints(&self, responses: &mut VecDeque<Message>, local: bool) {
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::Key;
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, MouseMotion};
|
||||
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
|
||||
|
||||
let mut input_hints = Vec::new();
|
||||
if self.is_typing() {
|
||||
input_hints.push(HintInfo::keys([Key::Minus], "Negate Direction"));
|
||||
input_hints.push(HintInfo::keys([Key::Backspace], "Delete Digit"));
|
||||
input_hints.push(HintInfo::keys([Key::NumKeys], "Enter Number"));
|
||||
} else if matches!(self, TransformOperation::Grabbing(_) | TransformOperation::Scaling(_)) {
|
||||
let axis_constraint = match self {
|
||||
TransformOperation::Grabbing(grabbing) => grabbing.constraint,
|
||||
TransformOperation::Scaling(scaling) => scaling.constraint,
|
||||
_ => Axis::Both,
|
||||
};
|
||||
let clear_constraint = "Clear Constraint";
|
||||
match axis_constraint {
|
||||
Axis::Both => {
|
||||
input_hints.push(HintInfo::keys([Key::KeyX], "Along X Axis"));
|
||||
input_hints.push(HintInfo::keys([Key::KeyY], "Along Y Axis"));
|
||||
let clear_constraint = "Clear Constraint";
|
||||
match self.axis_constraint() {
|
||||
Axis::Both => {
|
||||
input_hints.push(HintInfo::keys([Key::KeyX], "X-Axis Constraint"));
|
||||
input_hints.push(HintInfo::keys([Key::KeyY], "Y-Axis Constraint"));
|
||||
}
|
||||
Axis::X => {
|
||||
let x_label = if local { clear_constraint } else { "Local X-Axis Constraint" };
|
||||
input_hints.push(HintInfo::keys([Key::KeyX], x_label));
|
||||
input_hints.push(HintInfo::keys([Key::KeyY], "Y-Axis Constraint"));
|
||||
if !local {
|
||||
input_hints.push(HintInfo::keys([Key::KeyX, Key::KeyX], clear_constraint));
|
||||
}
|
||||
Axis::X => {
|
||||
let x_label = if local { clear_constraint } else { "Along Local X Axis" };
|
||||
input_hints.push(HintInfo::keys([Key::KeyX], x_label));
|
||||
input_hints.push(HintInfo::keys([Key::KeyY], "Along Y Axis"));
|
||||
if !local {
|
||||
input_hints.push(HintInfo::keys([Key::KeyX, Key::KeyX], clear_constraint));
|
||||
}
|
||||
}
|
||||
Axis::Y => {
|
||||
let y_label = if local { clear_constraint } else { "Along Local Y Axis" };
|
||||
input_hints.push(HintInfo::keys([Key::KeyX], "Along X Axis"));
|
||||
input_hints.push(HintInfo::keys([Key::KeyY], y_label));
|
||||
if !local {
|
||||
input_hints.push(HintInfo::keys([Key::KeyY, Key::KeyY], clear_constraint));
|
||||
}
|
||||
}
|
||||
Axis::Y => {
|
||||
let y_label = if local { clear_constraint } else { "Local Y-Axis Constraint" };
|
||||
input_hints.push(HintInfo::keys([Key::KeyX], "X-Axis Constraint"));
|
||||
input_hints.push(HintInfo::keys([Key::KeyY], y_label));
|
||||
if !local {
|
||||
input_hints.push(HintInfo::keys([Key::KeyY, Key::KeyY], clear_constraint));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -414,18 +413,50 @@ impl TransformOperation {
|
|||
TransformOperation::Rotating(_) => HintGroup(vec![HintInfo::multi_keys([[Key::KeyG], [Key::KeyS]], "Grab/Scale Selected")]),
|
||||
};
|
||||
|
||||
let mut hint_groups = vec![grs_hint_group];
|
||||
let confirm_and_cancel_group = HintGroup(vec![
|
||||
HintInfo::mouse(MouseMotion::Lmb, ""),
|
||||
HintInfo::keys([Key::Enter], "Confirm").prepend_slash(),
|
||||
HintInfo::mouse(MouseMotion::Rmb, ""),
|
||||
HintInfo::keys([Key::Escape], "Cancel").prepend_slash(),
|
||||
]);
|
||||
let mut hint_groups = vec![confirm_and_cancel_group, grs_hint_group];
|
||||
if !self.is_typing() {
|
||||
let modifiers = vec![HintInfo::keys([Key::Shift], "Slow"), HintInfo::keys([Key::Control], "Increments")];
|
||||
let modifiers = vec![
|
||||
HintInfo::keys([Key::Shift], "Slow"),
|
||||
HintInfo::keys([Key::Control], if matches!(self, TransformOperation::Rotating(_)) { "15° Increments" } else { "Increments" }),
|
||||
];
|
||||
hint_groups.push(HintGroup(modifiers));
|
||||
}
|
||||
hint_groups.push(HintGroup(input_hints));
|
||||
if !matches!(self, TransformOperation::Rotating(_)) {
|
||||
hint_groups.push(HintGroup(input_hints));
|
||||
}
|
||||
let mut typing_hints = vec![HintInfo::keys([Key::Minus], "Negate Direction")];
|
||||
if self.can_begin_typing() {
|
||||
typing_hints.push(HintInfo::keys([Key::NumKeys], "Enter Number"));
|
||||
if self.is_typing() {
|
||||
typing_hints.push(HintInfo::keys([Key::Backspace], "Delete Digit"));
|
||||
}
|
||||
}
|
||||
hint_groups.push(HintGroup(typing_hints));
|
||||
|
||||
let hint_data = HintData(hint_groups);
|
||||
responses.add(FrontendMessage::UpdateInputHints { hint_data });
|
||||
}
|
||||
|
||||
pub fn negate(&mut self, selected: &mut Selected, snapping: bool, local: bool, quad: Quad, transform: DAffine2) {
|
||||
pub fn is_constraint_to_axis(&self) -> bool {
|
||||
self.axis_constraint() != Axis::Both
|
||||
}
|
||||
|
||||
pub fn is_typing(&self) -> bool {
|
||||
match self {
|
||||
TransformOperation::None => false,
|
||||
TransformOperation::Grabbing(translation) => translation.typed_distance.is_some(),
|
||||
TransformOperation::Rotating(rotation) => rotation.typed_angle.is_some(),
|
||||
TransformOperation::Scaling(scale) => scale.typed_factor.is_some(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn negate(&mut self, selected: &mut Selected, increment_mode: bool, local: bool, quad: Quad, transform: DAffine2) {
|
||||
if *self != TransformOperation::None {
|
||||
*self = match self {
|
||||
TransformOperation::Scaling(scale) => TransformOperation::Scaling(scale.negate()),
|
||||
|
@ -433,7 +464,7 @@ impl TransformOperation {
|
|||
TransformOperation::Grabbing(translation) => TransformOperation::Grabbing(translation.negate()),
|
||||
_ => *self,
|
||||
};
|
||||
self.apply_transform_operation(selected, snapping, local, quad, transform);
|
||||
self.apply_transform_operation(selected, increment_mode, local, quad, transform);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -505,7 +536,7 @@ impl<'a> Selected<'a> {
|
|||
pub fn bounding_box(&mut self) -> Quad {
|
||||
let metadata = self.network_interface.document_metadata();
|
||||
|
||||
let transform = self
|
||||
let mut transform = self
|
||||
.network_interface
|
||||
.selected_nodes(&[])
|
||||
.unwrap()
|
||||
|
@ -514,8 +545,8 @@ impl<'a> Selected<'a> {
|
|||
.map(|layer| metadata.transform_to_viewport(layer))
|
||||
.unwrap_or(DAffine2::IDENTITY);
|
||||
|
||||
if transform.matrix2.determinant() == 0. {
|
||||
return Default::default();
|
||||
if transform.matrix2.determinant().abs() <= f64::EPSILON {
|
||||
transform.matrix2 += DMat2::IDENTITY * 1e-4;
|
||||
}
|
||||
|
||||
let bounds = self
|
||||
|
|
|
@ -507,9 +507,9 @@ impl BoundingBoxManager {
|
|||
let cursor = self.transform.inverse().transform_point2(cursor);
|
||||
let [threshold_x, threshold_y] = self.compute_viewport_threshold(BOUNDS_ROTATE_THRESHOLD);
|
||||
|
||||
let narrow = (self.bounds[0] - self.bounds[1]).abs().cmple(DVec2::splat(1e-4)).any();
|
||||
let flat = (self.bounds[0] - self.bounds[1]).abs().cmple(DVec2::splat(1e-4)).any();
|
||||
let within_square_bounds = |center: &DVec2| center.x - threshold_x < cursor.x && cursor.x < center.x + threshold_x && center.y - threshold_y < cursor.y && cursor.y < center.y + threshold_y;
|
||||
if narrow {
|
||||
if flat {
|
||||
[self.bounds[0], self.bounds[1]].iter().any(within_square_bounds)
|
||||
} else {
|
||||
self.evaluate_transform_handle_positions().iter().any(within_square_bounds)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#![allow(clippy::too_many_arguments)]
|
||||
|
||||
use super::tool_prelude::*;
|
||||
use crate::consts::{DRAG_DIRECTION_MODE_DETERMINATION_THRESHOLD, ROTATE_SNAP_ANGLE, SELECTION_TOLERANCE};
|
||||
use crate::consts::{DRAG_DIRECTION_MODE_DETERMINATION_THRESHOLD, ROTATE_INCREMENT, SELECTION_TOLERANCE};
|
||||
use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition;
|
||||
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||
|
@ -935,7 +935,7 @@ impl Fsm for SelectToolFsmState {
|
|||
};
|
||||
|
||||
let snapped_angle = if input.keyboard.key(modifier_keys.snap_angle) {
|
||||
let snap_resolution = ROTATE_SNAP_ANGLE.to_radians();
|
||||
let snap_resolution = ROTATE_INCREMENT.to_radians();
|
||||
(angle / snap_resolution).round() * snap_resolution
|
||||
} else {
|
||||
angle
|
||||
|
@ -1194,24 +1194,27 @@ impl Fsm for SelectToolFsmState {
|
|||
};
|
||||
|
||||
let current_selected: HashSet<_> = document.network_interface.selected_nodes(&[]).unwrap().selected_layers(document.metadata()).collect();
|
||||
if new_selected != current_selected {
|
||||
// Negative selection when both Shift and Ctrl are pressed
|
||||
if input.keyboard.key(remove_from_selection) {
|
||||
let updated_selection = current_selected
|
||||
.into_iter()
|
||||
.filter(|layer| !new_selected.iter().any(|selected| layer.starts_with(*selected, document.metadata())))
|
||||
.collect();
|
||||
tool_data.layers_dragging = updated_selection;
|
||||
} else {
|
||||
let parent_selected: HashSet<_> = new_selected
|
||||
.into_iter()
|
||||
.map(|layer| {
|
||||
// Find the parent node
|
||||
layer.ancestors(document.metadata()).filter(not_artboard(document)).last().unwrap_or(layer)
|
||||
})
|
||||
.collect();
|
||||
tool_data.layers_dragging.extend(parent_selected.iter().copied());
|
||||
}
|
||||
let negative_selection = input.keyboard.key(remove_from_selection);
|
||||
let selection_modified = new_selected != current_selected;
|
||||
// Negative selection when both Shift and Ctrl are pressed
|
||||
if negative_selection {
|
||||
let updated_selection = current_selected
|
||||
.into_iter()
|
||||
.filter(|layer| !new_selected.iter().any(|selected| layer.starts_with(*selected, document.metadata())))
|
||||
.collect();
|
||||
tool_data.layers_dragging = updated_selection;
|
||||
} else if selection_modified {
|
||||
let parent_selected: HashSet<_> = new_selected
|
||||
.into_iter()
|
||||
.map(|layer| {
|
||||
// Find the parent node
|
||||
layer.ancestors(document.metadata()).filter(not_artboard(document)).last().unwrap_or(layer)
|
||||
})
|
||||
.collect();
|
||||
tool_data.layers_dragging.extend(parent_selected.iter().copied());
|
||||
}
|
||||
|
||||
if negative_selection || selection_modified {
|
||||
responses.add(NodeGraphMessage::SelectedNodesSet {
|
||||
nodes: tool_data
|
||||
.layers_dragging
|
||||
|
@ -1354,7 +1357,25 @@ impl Fsm for SelectToolFsmState {
|
|||
]);
|
||||
responses.add(FrontendMessage::UpdateInputHints { hint_data });
|
||||
}
|
||||
_ => {}
|
||||
SelectToolFsmState::Drawing { .. } | SelectToolFsmState::Dragging => {}
|
||||
SelectToolFsmState::ResizingBounds => {
|
||||
let hint_data = HintData(vec![
|
||||
HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]),
|
||||
HintGroup(vec![HintInfo::keys([Key::Alt], "From Pivot"), HintInfo::keys([Key::Shift], "Preserve Aspect Ratio")]),
|
||||
]);
|
||||
responses.add(FrontendMessage::UpdateInputHints { hint_data });
|
||||
}
|
||||
SelectToolFsmState::RotatingBounds => {
|
||||
let hint_data = HintData(vec![
|
||||
HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]),
|
||||
HintGroup(vec![HintInfo::keys([Key::Control], "Snap")]),
|
||||
]);
|
||||
responses.add(FrontendMessage::UpdateInputHints { hint_data });
|
||||
}
|
||||
SelectToolFsmState::DraggingPivot | SelectToolFsmState::SkewingBounds => {
|
||||
let hint_data = HintData(vec![HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])]);
|
||||
responses.add(FrontendMessage::UpdateInputHints { hint_data });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ pub struct TransformLayerMessageHandler {
|
|||
slow: bool,
|
||||
increments: bool,
|
||||
local: bool,
|
||||
fixed_bbox: Quad,
|
||||
layer_bounding_box: Quad,
|
||||
typing: Typing,
|
||||
|
||||
mouse_position: ViewportPosition,
|
||||
|
@ -79,18 +79,17 @@ fn calculate_pivot(selected_points: &Vec<&ManipulatorPointId>, vector_data: &Vec
|
|||
}
|
||||
|
||||
fn project_edge_to_quad(edge: DVec2, quad: &Quad, local: bool, axis_constraint: Axis) -> DVec2 {
|
||||
let quad = quad.0;
|
||||
match axis_constraint {
|
||||
Axis::X => {
|
||||
if local {
|
||||
edge.project_onto(quad[1] - quad[0])
|
||||
edge.project_onto(quad.top_right() - quad.top_left())
|
||||
} else {
|
||||
edge.with_y(0.)
|
||||
}
|
||||
}
|
||||
Axis::Y => {
|
||||
if local {
|
||||
edge.project_onto(quad[3] - quad[0])
|
||||
edge.project_onto(quad.bottom_left() - quad.top_left())
|
||||
} else {
|
||||
edge.with_x(0.)
|
||||
}
|
||||
|
@ -177,14 +176,10 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
|
|||
};
|
||||
|
||||
let viewport_box = input.viewport_bounds.size();
|
||||
let axis_constraint = match self.transform_operation {
|
||||
TransformOperation::Grabbing(grabbing) => grabbing.constraint,
|
||||
TransformOperation::Scaling(scaling) => scaling.constraint,
|
||||
_ => Axis::Both,
|
||||
};
|
||||
let axis_constraint = self.transform_operation.axis_constraint();
|
||||
|
||||
let format_rounded = |value: f64, precision: usize| {
|
||||
if self.typing.digits.is_empty() {
|
||||
if self.typing.digits.is_empty() || !self.transform_operation.can_begin_typing() {
|
||||
format!("{:.*}", precision, value).trim_end_matches('0').trim_end_matches('.').to_string()
|
||||
} else {
|
||||
self.typing.string.clone()
|
||||
|
@ -197,7 +192,7 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
|
|||
let translation = translation.to_dvec(document_to_viewport, self.increments);
|
||||
let viewport_translate = document_to_viewport.transform_vector2(translation);
|
||||
let quad = Quad::from_box([self.grab_target, self.grab_target + viewport_translate]).0;
|
||||
let e1 = (self.fixed_bbox.0[1] - self.fixed_bbox.0[0]).normalize();
|
||||
let e1 = (self.layer_bounding_box.0[1] - self.layer_bounding_box.0[0]).normalize_or(DVec2::X);
|
||||
|
||||
if matches!(axis_constraint, Axis::Both | Axis::X) && translation.x != 0. {
|
||||
let end = if self.local {
|
||||
|
@ -221,7 +216,7 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
|
|||
let x_parameter = viewport_translate.x.clamp(-1., 1.);
|
||||
let y_transform = DAffine2::from_translation((quad[0] + end) / 2. + x_parameter * DVec2::X * 0.);
|
||||
let pivot_selection = if x_parameter >= 0. { Pivot::Start } else { Pivot::End };
|
||||
if axis_constraint != Axis::Both || self.typing.digits.is_empty() {
|
||||
if axis_constraint != Axis::Both || self.typing.digits.is_empty() || !self.transform_operation.can_begin_typing() {
|
||||
overlay_context.text(&format_rounded(translation.y, 2), COLOR_OVERLAY_BLUE, None, y_transform, 3., [pivot_selection, Pivot::Middle]);
|
||||
}
|
||||
}
|
||||
|
@ -231,16 +226,10 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
|
|||
}
|
||||
}
|
||||
TransformOperation::Scaling(scale) => {
|
||||
let to_mouse_final = self.mouse_position - self.pivot;
|
||||
let to_mouse_start = self.start_mouse - self.pivot;
|
||||
|
||||
let to_mouse_final = project_edge_to_quad(to_mouse_final, &self.fixed_bbox, self.local, axis_constraint);
|
||||
let to_mouse_start = project_edge_to_quad(to_mouse_start, &self.fixed_bbox, self.local, axis_constraint);
|
||||
|
||||
let scale = scale.to_f64(self.increments) * to_mouse_final.dot(to_mouse_start).signum();
|
||||
let scale = scale.to_f64(self.increments);
|
||||
let text = format!("{}x", format_rounded(scale, 3));
|
||||
let local_edge = self.start_mouse - self.pivot;
|
||||
let local_edge = project_edge_to_quad(local_edge, &self.fixed_bbox, self.local, axis_constraint);
|
||||
let local_edge = project_edge_to_quad(local_edge, &self.layer_bounding_box, self.local, axis_constraint);
|
||||
let boundary_point = self.pivot + local_edge * scale.min(1.);
|
||||
let end_point = self.pivot + local_edge * scale.max(1.);
|
||||
|
||||
|
@ -249,18 +238,17 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
|
|||
}
|
||||
overlay_context.line(boundary_point, end_point, None);
|
||||
|
||||
let transform = DAffine2::from_translation(boundary_point.midpoint(self.pivot) + local_edge.perp().normalize() * local_edge.element_product().signum() * 24.);
|
||||
let transform = DAffine2::from_translation(boundary_point.midpoint(self.pivot) + local_edge.perp().normalize_or(DVec2::X) * local_edge.element_product().signum() * 24.);
|
||||
overlay_context.text(&text, COLOR_OVERLAY_BLUE, None, transform, 16., [Pivot::Middle, Pivot::Middle]);
|
||||
}
|
||||
TransformOperation::Rotating(rotation) => {
|
||||
let angle = rotation.to_f64(self.increments);
|
||||
let quad = self.fixed_bbox.0;
|
||||
let offset_angle = if self.grs_pen_handle {
|
||||
self.handle - self.last_point
|
||||
} else if using_path_tool {
|
||||
self.start_mouse - self.pivot
|
||||
} else {
|
||||
quad[1] - quad[0]
|
||||
self.layer_bounding_box.top_right() - self.layer_bounding_box.top_right()
|
||||
};
|
||||
let offset_angle = offset_angle.to_angle();
|
||||
let width = viewport_box.max_element();
|
||||
|
@ -316,7 +304,7 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
|
|||
let top_left = DVec2::new(last_point.x, handle.y);
|
||||
let bottom_right = DVec2::new(handle.x, last_point.y);
|
||||
self.local = false;
|
||||
self.fixed_bbox = Quad::from_box([top_left, bottom_right]);
|
||||
self.layer_bounding_box = Quad::from_box([top_left, bottom_right]);
|
||||
self.grab_target = handle;
|
||||
self.pivot = last_point;
|
||||
self.handle = handle;
|
||||
|
@ -351,7 +339,7 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
|
|||
|
||||
self.transform_operation = TransformOperation::Grabbing(Default::default());
|
||||
self.local = false;
|
||||
self.fixed_bbox = selected.bounding_box();
|
||||
self.layer_bounding_box = selected.bounding_box();
|
||||
|
||||
selected.original_transforms.clear();
|
||||
|
||||
|
@ -405,7 +393,7 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
|
|||
self.transform_operation = TransformOperation::Rotating(Default::default());
|
||||
|
||||
self.local = false;
|
||||
self.fixed_bbox = selected.bounding_box();
|
||||
self.layer_bounding_box = selected.bounding_box();
|
||||
|
||||
selected.original_transforms.clear();
|
||||
|
||||
|
@ -458,7 +446,7 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
|
|||
self.transform_operation = TransformOperation::Scaling(Default::default());
|
||||
|
||||
self.local = false;
|
||||
self.fixed_bbox = selected.bounding_box();
|
||||
self.layer_bounding_box = selected.bounding_box();
|
||||
|
||||
selected.original_transforms.clear();
|
||||
|
||||
|
@ -477,6 +465,7 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
|
|||
self.handle = DVec2::ZERO;
|
||||
|
||||
responses.add(PenToolMessage::Abort);
|
||||
responses.add(ToolMessage::UpdateHints);
|
||||
} else {
|
||||
selected.revert_operation();
|
||||
selected.original_transforms.clear();
|
||||
|
@ -492,12 +481,16 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
|
|||
TransformLayerMessage::ConstrainX => {
|
||||
self.local = self
|
||||
.transform_operation
|
||||
.constrain_axis(Axis::X, &mut selected, self.increments, self.local, self.fixed_bbox, document_to_viewport)
|
||||
.constrain_axis(Axis::X, &mut selected, self.increments, self.local, self.layer_bounding_box, document_to_viewport);
|
||||
self.transform_operation
|
||||
.grs_typed(self.typing.evaluate(), &mut selected, self.increments, self.local, self.layer_bounding_box, document_to_viewport);
|
||||
}
|
||||
TransformLayerMessage::ConstrainY => {
|
||||
self.local = self
|
||||
.transform_operation
|
||||
.constrain_axis(Axis::Y, &mut selected, self.increments, self.local, self.fixed_bbox, document_to_viewport)
|
||||
.constrain_axis(Axis::Y, &mut selected, self.increments, self.local, self.layer_bounding_box, document_to_viewport);
|
||||
self.transform_operation
|
||||
.grs_typed(self.typing.evaluate(), &mut selected, self.increments, self.local, self.layer_bounding_box, document_to_viewport);
|
||||
}
|
||||
TransformLayerMessage::PointerMove { slow_key, increments_key } => {
|
||||
self.slow = input.keyboard.get(slow_key as usize);
|
||||
|
@ -506,10 +499,10 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
|
|||
if new_increments != self.increments {
|
||||
self.increments = new_increments;
|
||||
self.transform_operation
|
||||
.apply_transform_operation(&mut selected, self.increments, self.local, self.fixed_bbox, document_to_viewport);
|
||||
.apply_transform_operation(&mut selected, self.increments, self.local, self.layer_bounding_box, document_to_viewport);
|
||||
}
|
||||
|
||||
if self.typing.digits.is_empty() {
|
||||
if self.typing.digits.is_empty() || !self.transform_operation.can_begin_typing() {
|
||||
let delta_pos = input.mouse.position - self.mouse_position;
|
||||
|
||||
match self.transform_operation {
|
||||
|
@ -518,7 +511,7 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
|
|||
let change = if self.slow { delta_pos / SLOWING_DIVISOR } else { delta_pos };
|
||||
self.transform_operation = TransformOperation::Grabbing(translation.increment_amount(change));
|
||||
self.transform_operation
|
||||
.apply_transform_operation(&mut selected, self.increments, self.local, self.fixed_bbox, document_to_viewport);
|
||||
.apply_transform_operation(&mut selected, self.increments, self.local, self.layer_bounding_box, document_to_viewport);
|
||||
}
|
||||
TransformOperation::Rotating(rotation) => {
|
||||
let start_offset = *selected.pivot - self.mouse_position;
|
||||
|
@ -529,33 +522,31 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
|
|||
|
||||
self.transform_operation = TransformOperation::Rotating(rotation.increment_amount(change));
|
||||
self.transform_operation
|
||||
.apply_transform_operation(&mut selected, self.increments, self.local, self.fixed_bbox, document_to_viewport);
|
||||
.apply_transform_operation(&mut selected, self.increments, self.local, self.layer_bounding_box, document_to_viewport);
|
||||
}
|
||||
TransformOperation::Scaling(scale) => {
|
||||
TransformOperation::Scaling(mut scale) => {
|
||||
let axis_constraint = scale.constraint;
|
||||
let to_mouse_final = self.mouse_position - *selected.pivot;
|
||||
let to_mouse_final_old = input.mouse.position - *selected.pivot;
|
||||
let to_mouse_start = self.start_mouse - *selected.pivot;
|
||||
|
||||
let to_mouse_final = project_edge_to_quad(to_mouse_final, &self.fixed_bbox, self.local, axis_constraint);
|
||||
let to_mouse_final_old = project_edge_to_quad(to_mouse_final_old, &self.fixed_bbox, self.local, axis_constraint);
|
||||
let to_mouse_start = project_edge_to_quad(to_mouse_start, &self.fixed_bbox, self.local, axis_constraint);
|
||||
let to_mouse_final = project_edge_to_quad(to_mouse_final, &self.layer_bounding_box, self.local, axis_constraint);
|
||||
let to_mouse_final_old = project_edge_to_quad(to_mouse_final_old, &self.layer_bounding_box, self.local, axis_constraint);
|
||||
let to_mouse_start = project_edge_to_quad(to_mouse_start, &self.layer_bounding_box, self.local, axis_constraint);
|
||||
|
||||
let change = {
|
||||
let previous_frame_dist = to_mouse_final.length();
|
||||
let current_frame_dist = to_mouse_final_old.length();
|
||||
let start_transform_dist = to_mouse_start.length();
|
||||
let previous_frame_dist = to_mouse_final.dot(to_mouse_start);
|
||||
let current_frame_dist = to_mouse_final_old.dot(to_mouse_start);
|
||||
let start_transform_dist = to_mouse_start.length_squared();
|
||||
|
||||
(current_frame_dist - previous_frame_dist) / start_transform_dist
|
||||
};
|
||||
let change = if self.slow { change / SLOWING_DIVISOR } else { change };
|
||||
|
||||
let sign = to_mouse_final.dot(to_mouse_start).signum();
|
||||
let scale = scale.increment_amount(change * scale.to_f64(self.increments).signum());
|
||||
scale = scale.increment_amount(change);
|
||||
self.transform_operation = TransformOperation::Scaling(scale);
|
||||
|
||||
let op = TransformOperation::Scaling(if sign > 0. { scale } else { scale.negate() });
|
||||
op.apply_transform_operation(&mut selected, self.increments, self.local, self.fixed_bbox, document_to_viewport);
|
||||
self.transform_operation
|
||||
.apply_transform_operation(&mut selected, self.increments, self.local, self.layer_bounding_box, document_to_viewport);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -568,30 +559,44 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
|
|||
}
|
||||
TransformLayerMessage::TypeBackspace => {
|
||||
if self.typing.digits.is_empty() && self.typing.negative {
|
||||
self.transform_operation.negate(&mut selected, self.increments, self.local, self.fixed_bbox, document_to_viewport);
|
||||
self.transform_operation
|
||||
.negate(&mut selected, self.increments, self.local, self.layer_bounding_box, document_to_viewport);
|
||||
self.typing.type_negate();
|
||||
}
|
||||
self.transform_operation
|
||||
.grs_typed(self.typing.type_backspace(), &mut selected, self.increments, self.local, self.fixed_bbox, document_to_viewport);
|
||||
.grs_typed(self.typing.type_backspace(), &mut selected, self.increments, self.local, self.layer_bounding_box, document_to_viewport);
|
||||
}
|
||||
TransformLayerMessage::TypeDecimalPoint => {
|
||||
if self.typing.digits.is_empty() {
|
||||
self.typing.negative = false;
|
||||
} else {
|
||||
self.transform_operation
|
||||
.grs_typed(self.typing.type_decimal_point(), &mut selected, self.increments, self.local, self.fixed_bbox, document_to_viewport)
|
||||
if self.transform_operation.can_begin_typing() {
|
||||
self.transform_operation.grs_typed(
|
||||
self.typing.type_decimal_point(),
|
||||
&mut selected,
|
||||
self.increments,
|
||||
self.local,
|
||||
self.layer_bounding_box,
|
||||
document_to_viewport,
|
||||
)
|
||||
}
|
||||
}
|
||||
TransformLayerMessage::TypeDigit { digit } => {
|
||||
self.transform_operation
|
||||
.grs_typed(self.typing.type_number(digit), &mut selected, self.increments, self.local, self.fixed_bbox, document_to_viewport)
|
||||
if self.transform_operation.can_begin_typing() {
|
||||
self.transform_operation.grs_typed(
|
||||
self.typing.type_number(digit),
|
||||
&mut selected,
|
||||
self.increments,
|
||||
self.local,
|
||||
self.layer_bounding_box,
|
||||
document_to_viewport,
|
||||
)
|
||||
}
|
||||
}
|
||||
TransformLayerMessage::TypeNegate => {
|
||||
if self.typing.digits.is_empty() {
|
||||
self.transform_operation.negate(&mut selected, self.increments, self.local, self.fixed_bbox, document_to_viewport);
|
||||
self.transform_operation
|
||||
.negate(&mut selected, self.increments, self.local, self.layer_bounding_box, document_to_viewport);
|
||||
}
|
||||
self.transform_operation
|
||||
.grs_typed(self.typing.type_negate(), &mut selected, self.increments, self.local, self.fixed_bbox, document_to_viewport)
|
||||
.grs_typed(self.typing.type_negate(), &mut selected, self.increments, self.local, self.layer_bounding_box, document_to_viewport)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue