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 https://discord.com/channels/731730685944922173/881073965047636018/939265895509925898.

* 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 https://discord.com/channels/731730685944922173/881073965047636018/1336221441716391937.

* 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 02:57:47 -08:00
committed by GitHub
co-authored by Keavon Chambers
parent da752e5324
commit 4de65c292a
6 changed files with 211 additions and 154 deletions
@@ -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)
}
}
}