Add draggable skew triangles to the transform cage (#2300)

* Add triangle handles to transform cage for skew transform

Fixes #2299

* Add skew triangles

* Fix conflicts which github didn't show

* cargo fmt

* Fix needed

* remove unreachable

* use the trap and rect logic

* fix quad checks

* cursor fix; no triangles if already dragging and not skewing

* cargo fmt

* Resolve Clippy lints

* Add min length for triangle visibility

* Code review

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
mTvare 2025-03-01 15:54:56 +05:30 committed by GitHub
parent bc6e76208d
commit 1510ad820c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 304 additions and 51 deletions

View File

@ -88,6 +88,11 @@ pub const MIN_LENGTH_FOR_RESIZE_TO_INCLUDE_INTERIOR: f64 = 40.;
/// The motion of the user's cursor by an `x` pixel offset results in `x * scale_factor` pixels of offset on the other side.
pub const MAXIMUM_ALT_SCALE_FACTOR: f64 = 25.;
// SKEW TRIANGLES
pub const SKEW_TRIANGLE_SIZE: f64 = 7.;
pub const SKEW_TRIANGLE_OFFSET: f64 = 4.;
pub const MIN_LENGTH_FOR_SKEW_TRIANGLE_VISIBILITY: f64 = 48.;
// PATH TOOL
pub const MANIPULATOR_GROUP_MARKER_SIZE: f64 = 6.;
pub const SELECTION_THRESHOLD: f64 = 10.;

View File

@ -42,6 +42,30 @@ impl OverlayContext {
self.dashed_polygon(&quad.0, color_fill, None, None, None);
}
pub fn draw_triangle(&mut self, base: DVec2, direction: DVec2, size: f64, color_fill: Option<&str>, color_stroke: Option<&str>) {
let color_fill = color_fill.unwrap_or(COLOR_OVERLAY_WHITE);
let color_stroke = color_stroke.unwrap_or(COLOR_OVERLAY_BLUE);
let normal = direction.perp();
let top = base + direction * size;
let edge1 = base + normal * size / 2.;
let edge2 = base - normal * size / 2.;
self.start_dpi_aware_transform();
self.render_context.begin_path();
self.render_context.move_to(top.x, top.y);
self.render_context.line_to(edge1.x, edge1.y);
self.render_context.line_to(edge2.x, edge2.y);
self.render_context.close_path();
self.render_context.set_fill_style_str(color_fill);
self.render_context.set_stroke_style_str(color_stroke);
self.render_context.fill();
self.render_context.stroke();
self.end_dpi_aware_transform();
}
pub fn dashed_quad(&mut self, quad: Quad, color_fill: Option<&str>, dash_width: Option<f64>, dash_gap_width: Option<f64>, dash_offset: Option<f64>) {
self.dashed_polygon(&quad.0, color_fill, dash_width, dash_gap_width, dash_offset);
}

View File

@ -5334,7 +5334,7 @@ impl NodeNetworkInterface {
// If a non artboard layer is attempted to be connected to the exports, and there is already an artboard connected, then connect the layer to the artboard.
if let Some(first_layer) = LayerNodeIdentifier::ROOT_PARENT.children(&self.document_metadata).next() {
if parent == LayerNodeIdentifier::ROOT_PARENT
&& !self.reference(&layer.to_node(), network_path).is_some_and(|reference| *reference == Some("Artboard".to_string()))
&& self.reference(&layer.to_node(), network_path).is_none_or(|reference| *reference != Some("Artboard".to_string()))
&& self.is_artboard(&first_layer.to_node(), network_path)
{
parent = first_layer;

View File

@ -306,6 +306,7 @@ pub enum TransformOperation {
}
impl TransformOperation {
#[allow(clippy::too_many_arguments)]
pub fn apply_transform_operation(&self, selected: &mut Selected, increment_mode: bool, local: bool, quad: Quad, transform: DAffine2, pivot: DVec2, local_transform: DAffine2) {
let local_axis_transform_angle = (quad.top_left() - quad.top_right()).to_angle();
if self != &TransformOperation::None {
@ -351,6 +352,7 @@ impl TransformOperation {
self.is_constraint_to_axis() || !matches!(self, TransformOperation::Grabbing(_))
}
#[allow(clippy::too_many_arguments)]
pub fn constrain_axis(&mut self, axis: Axis, selected: &mut Selected, increment_mode: bool, mut local: bool, quad: Quad, transform: DAffine2, pivot: DVec2, local_transform: DAffine2) -> bool {
(*self, local) = match self {
TransformOperation::Grabbing(translation) => {
@ -367,6 +369,7 @@ impl TransformOperation {
local
}
#[allow(clippy::too_many_arguments)]
pub fn grs_typed(&mut self, typed: Option<f64>, selected: &mut Selected, increment_mode: bool, local: bool, quad: Quad, transform: DAffine2, pivot: DVec2, local_transform: DAffine2) {
match self {
TransformOperation::None => (),
@ -457,6 +460,7 @@ impl TransformOperation {
}
}
#[allow(clippy::too_many_arguments)]
pub fn negate(&mut self, selected: &mut Selected, increment_mode: bool, local: bool, quad: Quad, transform: DAffine2, pivot: DVec2, local_transform: DAffine2) {
if *self != TransformOperation::None {
*self = match self {

View File

@ -48,15 +48,12 @@ pub fn merge_layers(document: &DocumentMessageHandler, first_layer: LayerNodeIde
let mut current_and_other_layer_is_spline = false;
match (find_spline(document, first_layer), find_spline(document, second_layer)) {
(Some(current_layer_spline), Some(other_layer_spline)) => {
responses.add(NodeGraphMessage::DeleteNodes {
node_ids: [current_layer_spline, other_layer_spline].to_vec(),
delete_children: false,
});
current_and_other_layer_is_spline = true;
}
_ => {}
if let (Some(current_layer_spline), Some(other_layer_spline)) = (find_spline(document, first_layer), find_spline(document, second_layer)) {
responses.add(NodeGraphMessage::DeleteNodes {
node_ids: [current_layer_spline, other_layer_spline].to_vec(),
delete_children: false,
});
current_and_other_layer_is_spline = true;
}
// Move the `second_layer` below the `first_layer` for positioning purposes

View File

@ -324,10 +324,10 @@ impl SnapManager {
let layer_bounds = document.metadata().transform_to_document(layer) * Quad::from_box(bounds);
let screen_bounds = document.metadata().document_to_viewport.inverse() * Quad::from_box([DVec2::ZERO, snap_data.input.viewport_bounds.size()]);
if screen_bounds.intersects(layer_bounds) {
if !self.alignment_candidates.as_ref().is_some_and(|candidates| candidates.len() > 100) {
if self.alignment_candidates.as_ref().is_none_or(|candidates| candidates.len() <= 100) {
self.alignment_candidates.get_or_insert_with(Vec::new).push(layer);
}
if quad.intersects(layer_bounds) && !self.candidates.as_ref().is_some_and(|candidates| candidates.len() > 10) {
if quad.intersects(layer_bounds) && self.candidates.as_ref().is_none_or(|candidates| candidates.len() <= 10) {
self.candidates.get_or_insert_with(Vec::new).push(layer);
}
}

View File

@ -1,6 +1,7 @@
use crate::consts::{
BOUNDS_ROTATE_THRESHOLD, BOUNDS_SELECT_THRESHOLD, COLOR_OVERLAY_WHITE, MAXIMUM_ALT_SCALE_FACTOR, MIN_LENGTH_FOR_CORNERS_VISIBILITY, MIN_LENGTH_FOR_EDGE_RESIZE_PRIORITY_OVER_CORNERS,
MIN_LENGTH_FOR_MIDPOINT_VISIBILITY, MIN_LENGTH_FOR_RESIZE_TO_INCLUDE_INTERIOR, RESIZE_HANDLE_SIZE, SELECTION_DRAG_ANGLE,
MIN_LENGTH_FOR_MIDPOINT_VISIBILITY, MIN_LENGTH_FOR_RESIZE_TO_INCLUDE_INTERIOR, MIN_LENGTH_FOR_SKEW_TRIANGLE_VISIBILITY, RESIZE_HANDLE_SIZE, SELECTION_DRAG_ANGLE, SKEW_TRIANGLE_OFFSET,
SKEW_TRIANGLE_SIZE,
};
use crate::messages::frontend::utility_types::MouseCursorIcon;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
@ -15,6 +16,9 @@ use glam::{DAffine2, DMat2, DVec2};
use super::snapping::{self, SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnappedPoint};
/// (top, bottom, left, right)
pub type EdgeBool = (bool, bool, bool, bool);
pub struct SizeSnapData<'a> {
pub manager: &'a mut SnapManager,
pub points: &'a mut Vec<SnapCandidatePoint>,
@ -250,7 +254,8 @@ impl SelectedEdges {
(DAffine2::from_scale(enlargement_factor), pivot)
}
pub fn skew_transform(&self, mouse: DVec2, to_viewport_transform: DAffine2) -> DAffine2 {
// TODO: Add free movement when Ctrl is pressed to allow dragging the whole edge, not just sliding it
pub fn skew_transform(&self, mouse: DVec2, to_viewport_transform: DAffine2, _free_movement: bool) -> DAffine2 {
// Skip if the matrix is singular (as it isn't really possible to skew).
if !to_viewport_transform.matrix2.determinant().recip().is_finite() {
return DAffine2::IDENTITY;
@ -377,6 +382,171 @@ impl BoundingBoxManager {
]
}
pub fn get_closest_edge(&self, edges: EdgeBool, cursor: DVec2) -> EdgeBool {
if !edges.0 && !edges.1 && !edges.2 && !edges.3 {
return (false, false, false, false);
}
let cursor = self.transform.inverse().transform_point2(cursor);
let min = self.bounds[0].min(self.bounds[1]);
let max = self.bounds[0].max(self.bounds[1]);
let distances = [
edges.0.then(|| (cursor - DVec2::new(cursor.x, min.y)).length_squared()),
edges.1.then(|| (cursor - DVec2::new(cursor.x, max.y)).length_squared()),
edges.2.then(|| (cursor - DVec2::new(min.x, cursor.y)).length_squared()),
edges.3.then(|| (cursor - DVec2::new(max.x, cursor.y)).length_squared()),
];
let min_distance = distances.iter().filter_map(|&x| x).min_by(|a, b| a.partial_cmp(b).unwrap());
match min_distance {
Some(min) => (
edges.0 && distances[0].is_some_and(|d| (d - min).abs() < f64::EPSILON),
edges.1 && distances[1].is_some_and(|d| (d - min).abs() < f64::EPSILON),
edges.2 && distances[2].is_some_and(|d| (d - min).abs() < f64::EPSILON),
edges.3 && distances[3].is_some_and(|d| (d - min).abs() < f64::EPSILON),
),
None => (false, false, false, false),
}
}
pub fn check_skew_handle(&self, cursor: DVec2, edge: EdgeBool) -> bool {
if let Some([start, end]) = self.edge_endpoints_vector_from_edge_bool(edge) {
if (end - start).length() < MIN_LENGTH_FOR_SKEW_TRIANGLE_VISIBILITY {
return false;
}
let touches_triangle = |base: DVec2, direction: DVec2, cursor: DVec2| -> bool {
let normal = direction.perp();
let top = base + direction * SKEW_TRIANGLE_SIZE;
let edge1 = base + normal * SKEW_TRIANGLE_SIZE / 2.;
let edge2 = base - normal * SKEW_TRIANGLE_SIZE / 2.;
let v0 = edge1 - top;
let v1 = edge2 - top;
let v2 = cursor - top;
let d00 = v0.dot(v0);
let d01 = v0.dot(v1);
let d11 = v1.dot(v1);
let d20 = v2.dot(v0);
let d21 = v2.dot(v1);
let denom = d00 * d11 - d01 * d01;
let v = (d11 * d20 - d01 * d21) / denom;
let w = (d00 * d21 - d01 * d20) / denom;
let u = 1. - v - w;
u >= 0. && v >= 0. && w >= 0.
};
let edge_dir = (end - start).normalize();
let mid = end.midpoint(start);
for direction in [edge_dir, -edge_dir] {
let base = mid + direction * (3. + SKEW_TRIANGLE_OFFSET);
if touches_triangle(base, direction, cursor) {
return true;
}
}
}
false
}
pub fn edge_endpoints_vector_from_edge_bool(&self, edges: EdgeBool) -> Option<[DVec2; 2]> {
let quad = self.transform * Quad::from_box(self.bounds);
let category = self.overlay_display_category();
if matches!(
category,
TransformCageSizeCategory::Full | TransformCageSizeCategory::Narrow | TransformCageSizeCategory::ReducedLandscape
) {
if edges.0 {
return Some([quad.top_left(), quad.top_right()]);
}
if edges.1 {
return Some([quad.bottom_left(), quad.bottom_right()]);
}
}
if matches!(
category,
TransformCageSizeCategory::Full | TransformCageSizeCategory::Narrow | TransformCageSizeCategory::ReducedPortrait
) {
if edges.2 {
return Some([quad.top_left(), quad.bottom_left()]);
}
if edges.3 {
return Some([quad.top_right(), quad.bottom_right()]);
}
}
None
}
pub fn render_skew_gizmos(&mut self, overlay_context: &mut OverlayContext, hover_edge: EdgeBool) {
let mut draw_edge_triangles = |start: DVec2, end: DVec2| {
if (end - start).length() < MIN_LENGTH_FOR_SKEW_TRIANGLE_VISIBILITY {
return;
}
let edge_dir = (end - start).normalize();
let mid = end.midpoint(start);
for edge in [edge_dir, -edge_dir] {
overlay_context.draw_triangle(mid + edge * (3. + SKEW_TRIANGLE_OFFSET), edge, SKEW_TRIANGLE_SIZE, None, None);
}
};
if let Some([start, end]) = self.edge_endpoints_vector_from_edge_bool(hover_edge) {
draw_edge_triangles(start, end);
}
}
pub fn over_extended_edge_midpoint(&self, mouse: DVec2, hover_edge: EdgeBool) -> bool {
const HALF_WIDTH_OUTER_RECT: f64 = RESIZE_HANDLE_SIZE / 2. + SKEW_TRIANGLE_OFFSET + SKEW_TRIANGLE_SIZE;
const HALF_WIDTH_INNER_RECT: f64 = SKEW_TRIANGLE_OFFSET + RESIZE_HANDLE_SIZE / 2.;
const INNER_QUAD_CORNER: DVec2 = DVec2::new(HALF_WIDTH_INNER_RECT, RESIZE_HANDLE_SIZE / 2.);
const FULL_QUAD_CORNER: DVec2 = DVec2::new(HALF_WIDTH_OUTER_RECT, BOUNDS_SELECT_THRESHOLD);
let quad = self.transform * Quad::from_box(self.bounds);
let Some([start, end]) = self.edge_endpoints_vector_from_edge_bool(hover_edge) else {
return false;
};
if (end - start).length() < MIN_LENGTH_FOR_SKEW_TRIANGLE_VISIBILITY {
return false;
}
let angle;
let is_compact;
if hover_edge.0 || hover_edge.1 {
angle = (quad.top_left() - quad.top_right()).to_angle();
is_compact = (quad.top_left() - quad.bottom_left()).length_squared() < MIN_LENGTH_FOR_RESIZE_TO_INCLUDE_INTERIOR.powi(2);
} else if hover_edge.2 || hover_edge.3 {
angle = (quad.top_left() - quad.bottom_left()).to_angle();
is_compact = (quad.top_left() - quad.top_right()).length_squared() < MIN_LENGTH_FOR_RESIZE_TO_INCLUDE_INTERIOR.powi(2);
} else {
return false;
};
let has_triangle_hover = self.check_skew_handle(mouse, hover_edge);
let point = start.midpoint(end);
if is_compact {
let upper_rect = DAffine2::from_angle_translation(angle, point) * Quad::from_box([-FULL_QUAD_CORNER.with_y(0.), FULL_QUAD_CORNER]);
let inter_triangle_quad = DAffine2::from_angle_translation(angle, point) * Quad::from_box([-INNER_QUAD_CORNER, INNER_QUAD_CORNER]);
upper_rect.contains(mouse) || has_triangle_hover || inter_triangle_quad.contains(mouse)
} else {
let rect = DAffine2::from_angle_translation(angle, point) * Quad::from_box([-FULL_QUAD_CORNER, FULL_QUAD_CORNER]);
rect.contains(mouse) || has_triangle_hover
}
}
/// Update the position of the bounding box and transform handles
pub fn render_overlays(&mut self, overlay_context: &mut OverlayContext) {
let quad = self.transform * Quad::from_box(self.bounds);
@ -388,18 +558,20 @@ impl BoundingBoxManager {
// Draw the bounding box rectangle
overlay_context.quad(quad, None);
let mut draw_handle = |point: DVec2| {
let quad = DAffine2::from_angle_translation((quad.top_left() - quad.top_right()).to_angle(), point)
* Quad::from_box([DVec2::splat(-RESIZE_HANDLE_SIZE / 2.), DVec2::splat(RESIZE_HANDLE_SIZE / 2.)]);
let mut draw_handle = |point: DVec2, angle: f64| {
let quad = DAffine2::from_angle_translation(angle, point) * Quad::from_box([DVec2::splat(-RESIZE_HANDLE_SIZE / 2.), DVec2::splat(RESIZE_HANDLE_SIZE / 2.)]);
overlay_context.quad(quad, Some(COLOR_OVERLAY_WHITE));
};
let horizontal_angle = (quad.top_left() - quad.bottom_left()).to_angle();
let vertical_angle = (quad.top_left() - quad.top_right()).to_angle();
// Draw the horizontal midpoint drag handles
if matches!(
category,
TransformCageSizeCategory::Full | TransformCageSizeCategory::Narrow | TransformCageSizeCategory::ReducedLandscape
) {
horizontal_edges.map(&mut draw_handle);
horizontal_edges.map(|point| draw_handle(point, horizontal_angle));
}
// Draw the vertical midpoint drag handles
@ -407,21 +579,28 @@ impl BoundingBoxManager {
category,
TransformCageSizeCategory::Full | TransformCageSizeCategory::Narrow | TransformCageSizeCategory::ReducedPortrait
) {
vertical_edges.map(&mut draw_handle);
vertical_edges.map(|point| draw_handle(point, vertical_angle));
}
let angle = quad
.edges()
.map(|[x, y]| x.distance_squared(y))
.into_iter()
.reduce(|horizontal_distance, vertical_distance| if horizontal_distance > vertical_distance { horizontal_angle } else { vertical_angle })
.unwrap_or_default();
// Draw the corner drag handles
if matches!(
category,
TransformCageSizeCategory::Full | TransformCageSizeCategory::ReducedBoth | TransformCageSizeCategory::ReducedLandscape | TransformCageSizeCategory::ReducedPortrait
) {
quad.0.map(&mut draw_handle);
quad.0.map(|point| draw_handle(point, angle));
}
// Draw the flat line endpoint drag handles
if category == TransformCageSizeCategory::Flat {
draw_handle(self.transform.transform_point2(self.bounds[0]));
draw_handle(self.transform.transform_point2(self.bounds[1]));
draw_handle(self.transform.transform_point2(self.bounds[0]), angle);
draw_handle(self.transform.transform_point2(self.bounds[1]), angle);
}
}
@ -482,7 +661,7 @@ impl BoundingBoxManager {
/// Returns which edge in the order:
///
/// `top, bottom, left, right`
pub fn check_selected_edges(&self, cursor: DVec2) -> Option<(bool, bool, bool, bool)> {
pub fn check_selected_edges(&self, cursor: DVec2) -> Option<EdgeBool> {
let cursor = self.transform.inverse().transform_point2(cursor);
let min = self.bounds[0].min(self.bounds[1]);
@ -571,9 +750,22 @@ impl BoundingBoxManager {
}
/// Gets the required mouse cursor to show resizing bounds or optionally rotation
pub fn get_cursor(&self, input: &InputPreprocessorMessageHandler, rotate: bool) -> MouseCursorIcon {
pub fn get_cursor(&self, input: &InputPreprocessorMessageHandler, rotate: bool, dragging_bounds: bool, skew_edge: Option<EdgeBool>) -> MouseCursorIcon {
let edges = self.check_selected_edges(input.mouse.position);
let is_near_square = edges.is_some_and(|hover_edge| self.over_extended_edge_midpoint(input.mouse.position, hover_edge));
if dragging_bounds && is_near_square {
if let Some(skew_edge) = skew_edge {
if self.check_skew_handle(input.mouse.position, skew_edge) {
if skew_edge.0 || skew_edge.1 {
return MouseCursorIcon::EWResize;
} else if skew_edge.2 || skew_edge.3 {
return MouseCursorIcon::NSResize;
}
}
};
}
match edges {
Some((top, bottom, left, right)) if !self.is_bounds_flat() => match (top, bottom, left, right) {
(true, _, false, false) | (_, true, false, false) => MouseCursorIcon::NSResize,
@ -599,7 +791,7 @@ fn skew_transform_singular() {
// The determinant is 0.
let transform = DAffine2::from_cols_array(&[2.; 6]);
// This shouldn't panic. We don't really care about the behavior in this test.
let _ = edge.skew_transform(DVec2::new(1.5, 1.5), transform);
let _ = edge.skew_transform(DVec2::new(1.5, 1.5), transform, false);
}
}
@ -615,7 +807,7 @@ fn skew_transform_correct() {
let to_viewport_transform = DAffine2::from_cols_array(&[2., 1., 0., 1., 2., 3.]);
// Random mouse position.
let mouse = DVec2::new(1.5, 1.5);
let final_transform = edge.skew_transform(mouse, to_viewport_transform);
let final_transform = edge.skew_transform(mouse, to_viewport_transform, false);
// This is the current handle that goes under the mouse.
let dragging_point = edge.pivot_from_bounds(edge.bounds[1], edge.bounds[0]);

View File

@ -393,7 +393,10 @@ impl Fsm for ArtboardToolFsmState {
}
(ArtboardToolFsmState::Ready { .. }, ArtboardToolMessage::PointerMove { .. }) => {
let mut cursor = tool_data.bounding_box_manager.as_ref().map_or(MouseCursorIcon::Default, |bounds| bounds.get_cursor(input, false));
let mut cursor = tool_data
.bounding_box_manager
.as_ref()
.map_or(MouseCursorIcon::Default, |bounds| bounds.get_cursor(input, false, false, None));
if cursor == MouseCursorIcon::Default && !hovered {
tool_data.snap_manager.preview_draw(&SnapData::new(document, input), input.mouse.position);

View File

@ -280,7 +280,7 @@ enum SelectToolFsmState {
Drawing { selection_shape: SelectionShapeType },
Dragging { axis: Axis, using_compass: bool },
ResizingBounds,
SkewingBounds,
SkewingBounds { skew: Key },
RotatingBounds,
DraggingPivot,
}
@ -309,6 +309,7 @@ struct SelectToolData {
cursor: MouseCursorIcon,
pivot: Pivot,
compass_rose: CompassRose,
skew_edge: EdgeBool,
nested_selection_behavior: NestedSelectionBehavior,
selected_layers_count: usize,
selected_layers_changed: bool,
@ -577,8 +578,23 @@ impl Fsm for SelectToolFsmState {
.map(|bounding_box| bounding_box.check_rotate(input.mouse.position))
.unwrap_or_default();
let is_resizing_or_rotating = matches!(self, SelectToolFsmState::ResizingBounds | SelectToolFsmState::SkewingBounds { .. } | SelectToolFsmState::RotatingBounds);
if let Some(bounds) = tool_data.bounding_box_manager.as_mut() {
let edges = bounds.check_selected_edges(input.mouse.position);
let is_skewing = matches!(self, SelectToolFsmState::SkewingBounds { .. });
let is_near_square = edges.is_some_and(|hover_edge| bounds.over_extended_edge_midpoint(input.mouse.position, hover_edge));
if is_skewing || (dragging_bounds && is_near_square && !is_resizing_or_rotating) {
bounds.render_skew_gizmos(&mut overlay_context, tool_data.skew_edge);
}
if !is_skewing && dragging_bounds {
if let Some(edges) = edges {
tool_data.skew_edge = bounds.get_closest_edge(edges, input.mouse.position);
}
}
}
let might_resize_or_rotate = dragging_bounds || rotating_bounds;
let is_resizing_or_rotating = matches!(self, SelectToolFsmState::ResizingBounds { .. } | SelectToolFsmState::SkewingBounds | SelectToolFsmState::RotatingBounds);
let can_get_into_other_states = might_resize_or_rotate && !matches!(self, SelectToolFsmState::Dragging { .. });
let show_compass = !(can_get_into_other_states || is_resizing_or_rotating);
@ -842,14 +858,19 @@ impl Fsm for SelectToolFsmState {
None
);
bounds.center_of_transformation = selected.mean_average_of_pivots();
// Check if we're hovering over a skew triangle
let edges = bounds.check_selected_edges(input.mouse.position);
if let Some(edges) = edges {
let closest_edge = bounds.get_closest_edge(edges, input.mouse.position);
if bounds.check_skew_handle(input.mouse.position, closest_edge) {
tool_data.get_snap_candidates(document, input);
return SelectToolFsmState::SkewingBounds { skew };
}
}
}
tool_data.get_snap_candidates(document, input);
if input.keyboard.key(skew) {
SelectToolFsmState::SkewingBounds
} else {
SelectToolFsmState::ResizingBounds
}
SelectToolFsmState::ResizingBounds
}
// Dragging the selected layers around to transform them
else if can_grab_compass_rose || intersection.is_some_and(|intersection| selected.iter().any(|selected_layer| intersection.starts_with(*selected_layer, document.metadata()))) {
@ -1034,10 +1055,11 @@ impl Fsm for SelectToolFsmState {
}
SelectToolFsmState::ResizingBounds
}
(SelectToolFsmState::SkewingBounds, SelectToolMessage::PointerMove(_)) => {
(SelectToolFsmState::SkewingBounds { skew }, SelectToolMessage::PointerMove(_)) => {
if let Some(ref mut bounds) = &mut tool_data.bounding_box_manager {
if let Some(movement) = &mut bounds.selected_edges {
let transformation = movement.skew_transform(input.mouse.position, bounds.original_bound_transform);
let free_movement = input.keyboard.key(skew);
let transformation = movement.skew_transform(input.mouse.position, bounds.original_bound_transform, free_movement);
tool_data.layers_dragging.retain(|layer| {
if *layer != LayerNodeIdentifier::ROOT_PARENT {
@ -1063,7 +1085,7 @@ impl Fsm for SelectToolFsmState {
selected.apply_transformation(bounds.original_bound_transform * transformation * bounds.original_bound_transform.inverse(), None);
}
}
SelectToolFsmState::SkewingBounds
SelectToolFsmState::SkewingBounds { skew }
}
(SelectToolFsmState::RotatingBounds, SelectToolMessage::PointerMove(modifier_keys)) => {
if let Some(bounds) = &mut tool_data.bounding_box_manager {
@ -1139,7 +1161,16 @@ impl Fsm for SelectToolFsmState {
SelectToolFsmState::Drawing { selection_shape }
}
(SelectToolFsmState::Ready { .. }, SelectToolMessage::PointerMove(_)) => {
let mut cursor = tool_data.bounding_box_manager.as_ref().map_or(MouseCursorIcon::Default, |bounds| bounds.get_cursor(input, true));
let dragging_bounds = tool_data
.bounding_box_manager
.as_mut()
.and_then(|bounding_box| bounding_box.check_selected_edges(input.mouse.position))
.is_some();
let mut cursor = tool_data
.bounding_box_manager
.as_ref()
.map_or(MouseCursorIcon::Default, |bounds| bounds.get_cursor(input, true, dragging_bounds, Some(tool_data.skew_edge)));
// Dragging the pivot overrules the other operations
if tool_data.pivot.is_over(input.mouse.position) {
@ -1166,7 +1197,7 @@ impl Fsm for SelectToolFsmState {
SelectToolFsmState::Dragging { axis, using_compass }
}
(SelectToolFsmState::ResizingBounds | SelectToolFsmState::SkewingBounds, SelectToolMessage::PointerOutsideViewport(_)) => {
(SelectToolFsmState::ResizingBounds | SelectToolFsmState::SkewingBounds { .. }, SelectToolMessage::PointerOutsideViewport(_)) => {
// AutoPanning
if let Some(shift) = tool_data.auto_panning.shift_viewport(input, responses) {
if let Some(ref mut bounds) = &mut tool_data.bounding_box_manager {
@ -1271,7 +1302,7 @@ impl Fsm for SelectToolFsmState {
let selection = tool_data.nested_selection_behavior;
SelectToolFsmState::Ready { selection }
}
(SelectToolFsmState::ResizingBounds | SelectToolFsmState::SkewingBounds, SelectToolMessage::DragStop { .. } | SelectToolMessage::Enter) => {
(SelectToolFsmState::ResizingBounds | SelectToolFsmState::SkewingBounds { .. }, SelectToolMessage::DragStop { .. } | SelectToolMessage::Enter) => {
let response = match input.mouse.position.distance(tool_data.drag_start) < 10. * f64::EPSILON {
true => DocumentMessage::AbortTransaction,
false => DocumentMessage::EndTransaction,
@ -1523,7 +1554,7 @@ impl Fsm for SelectToolFsmState {
]);
responses.add(FrontendMessage::UpdateInputHints { hint_data });
}
SelectToolFsmState::DraggingPivot | SelectToolFsmState::SkewingBounds => {
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

@ -325,17 +325,14 @@ impl Fsm for SplineToolFsmState {
let append_to_selected_layer = input.keyboard.key(append_to_selected);
// Create new path in the selected layer when shift is down
match (selected_layer, append_to_selected_layer) {
(Some(layer), true) => {
tool_data.current_layer = Some(layer);
if let (Some(layer), true) = (selected_layer, append_to_selected_layer) {
tool_data.current_layer = Some(layer);
let transform = document.metadata().transform_to_viewport(layer);
let position = transform.inverse().transform_point2(input.mouse.position);
tool_data.next_point = position;
let transform = document.metadata().transform_to_viewport(layer);
let position = transform.inverse().transform_point2(input.mouse.position);
tool_data.next_point = position;
return SplineToolFsmState::Drawing;
}
_ => {}
return SplineToolFsmState::Drawing;
}
responses.add(DocumentMessage::DeselectAllLayers);