From 1510ad820ccd8ad15aa7dffe3b4ba26bcc6e06ba Mon Sep 17 00:00:00 2001 From: mTvare Date: Sat, 1 Mar 2025 15:54:56 +0530 Subject: [PATCH] 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 --- editor/src/consts.rs | 5 + .../document/overlays/utility_types.rs | 24 ++ .../utility_types/network_interface.rs | 2 +- .../document/utility_types/transformation.rs | 4 + .../graph_modification_utils.rs | 15 +- .../tool/common_functionality/snapping.rs | 4 +- .../transformation_cage.rs | 220 ++++++++++++++++-- .../tool/tool_messages/artboard_tool.rs | 5 +- .../tool/tool_messages/select_tool.rs | 61 +++-- .../tool/tool_messages/spline_tool.rs | 15 +- 10 files changed, 304 insertions(+), 51 deletions(-) diff --git a/editor/src/consts.rs b/editor/src/consts.rs index c457fda9..b5959ef9 100644 --- a/editor/src/consts.rs +++ b/editor/src/consts.rs @@ -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.; diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index 125a1ed0..3d29f4d1 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -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, dash_gap_width: Option, dash_offset: Option) { self.dashed_polygon(&quad.0, color_fill, dash_width, dash_gap_width, dash_offset); } diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface.rs b/editor/src/messages/portfolio/document/utility_types/network_interface.rs index da0f6767..f94f3716 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface.rs @@ -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; diff --git a/editor/src/messages/portfolio/document/utility_types/transformation.rs b/editor/src/messages/portfolio/document/utility_types/transformation.rs index 24953b9d..88e3b42e 100644 --- a/editor/src/messages/portfolio/document/utility_types/transformation.rs +++ b/editor/src/messages/portfolio/document/utility_types/transformation.rs @@ -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, 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 { diff --git a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs index c024ecab..82f14d0a 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -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 diff --git a/editor/src/messages/tool/common_functionality/snapping.rs b/editor/src/messages/tool/common_functionality/snapping.rs index e20e02c3..ffc96d23 100644 --- a/editor/src/messages/tool/common_functionality/snapping.rs +++ b/editor/src/messages/tool/common_functionality/snapping.rs @@ -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); } } diff --git a/editor/src/messages/tool/common_functionality/transformation_cage.rs b/editor/src/messages/tool/common_functionality/transformation_cage.rs index cddef227..45a120a9 100644 --- a/editor/src/messages/tool/common_functionality/transformation_cage.rs +++ b/editor/src/messages/tool/common_functionality/transformation_cage.rs @@ -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, @@ -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 { 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) -> 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]); diff --git a/editor/src/messages/tool/tool_messages/artboard_tool.rs b/editor/src/messages/tool/tool_messages/artboard_tool.rs index 3bbe23ab..943a5857 100644 --- a/editor/src/messages/tool/tool_messages/artboard_tool.rs +++ b/editor/src/messages/tool/tool_messages/artboard_tool.rs @@ -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); diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index bcd8b8c5..56a71558 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -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 }); } diff --git a/editor/src/messages/tool/tool_messages/spline_tool.rs b/editor/src/messages/tool/tool_messages/spline_tool.rs index 2fb1a1a6..9e257455 100644 --- a/editor/src/messages/tool/tool_messages/spline_tool.rs +++ b/editor/src/messages/tool/tool_messages/spline_tool.rs @@ -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);