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

Fixes 9392658955.

* 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

Fixes 1336221441.

* 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:
mTvare 2025-02-05 16:27:47 +05:30 committed by GitHub
parent da752e5324
commit 4de65c292a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 211 additions and 154 deletions

View file

@ -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.;

View file

@ -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>) {

View file

@ -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

View file

@ -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)

View file

@ -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 });
}
}
}

View file

@ -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)
}
}
}