diff --git a/editor/src/messages/input_mapper/input_mappings.rs b/editor/src/messages/input_mapper/input_mappings.rs index e049e206..d9320e63 100644 --- a/editor/src/messages/input_mapper/input_mappings.rs +++ b/editor/src/messages/input_mapper/input_mappings.rs @@ -97,7 +97,7 @@ pub fn input_mappings() -> Mapping { // // SelectToolMessage entry!(PointerMove; refresh_keys=[Control, Alt, Shift], action_dispatch=SelectToolMessage::PointerMove(SelectToolPointerKeys { axis_align: Shift, snap_angle: Control, center: Alt, duplicate: Alt })), - entry!(KeyDown(MouseLeft); action_dispatch=SelectToolMessage::DragStart { extend_selection: Shift, remove_from_selection: Alt, select_deepest: Accel, lasso_select: Control }), + entry!(KeyDown(MouseLeft); action_dispatch=SelectToolMessage::DragStart { extend_selection: Shift, remove_from_selection: Alt, select_deepest: Accel, lasso_select: Control, skew: Control }), entry!(KeyUp(MouseLeft); action_dispatch=SelectToolMessage::DragStop { remove_from_selection: Alt }), entry!(KeyDown(Enter); action_dispatch=SelectToolMessage::Enter), entry!(DoubleClick(MouseButton::Left); action_dispatch=SelectToolMessage::EditLayer), diff --git a/editor/src/messages/tool/common_functionality/transformation_cage.rs b/editor/src/messages/tool/common_functionality/transformation_cage.rs index 2ab1d2bc..42d932ef 100644 --- a/editor/src/messages/tool/common_functionality/transformation_cage.rs +++ b/editor/src/messages/tool/common_functionality/transformation_cage.rs @@ -11,7 +11,7 @@ use crate::messages::tool::common_functionality::snapping::SnapTypeConfiguration use graphene_core::renderer::Quad; use graphene_std::renderer::Rect; -use glam::{DAffine2, DVec2}; +use glam::{DAffine2, DMat2, DVec2}; use super::snapping::{self, SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnappedPoint}; @@ -241,6 +241,42 @@ impl SelectedEdges { } (DAffine2::from_scale(enlargement_factor), pivot) } + + pub fn skew_transform(&self, mouse: DVec2, to_viewport_transform: DAffine2) -> 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; + } + + let opposite = self.pivot_from_bounds(self.bounds[0], self.bounds[1]); + // This is the current handle that goes under the mouse. + let dragging_point = self.pivot_from_bounds(self.bounds[1], self.bounds[0]); + + let mut new_dragging_point = to_viewport_transform.transform_point2(dragging_point); + let parallel_to_x = self.top || self.bottom; + let parallel_to_y = !parallel_to_x && (self.left || self.right); + + // The target point is the projection in viewport space onto the line that the skew is parallel to. + if parallel_to_x { + new_dragging_point += (mouse - new_dragging_point).project_onto(to_viewport_transform.transform_vector2(DVec2::X)); + } else if parallel_to_y { + new_dragging_point += (mouse - new_dragging_point).project_onto(to_viewport_transform.transform_vector2(DVec2::Y)); + } + new_dragging_point = to_viewport_transform.inverse().transform_point2(new_dragging_point); + + let movement = new_dragging_point - dragging_point; + + // Produce a skew that moves the dragging point to the new dragging point (assuming the opposite is origin). + let skew = DAffine2::from_mat2(DMat2::from_cols_array(&[ + 1., + if parallel_to_y { movement.y / (dragging_point - opposite).x } else { 0. }, + if parallel_to_x { movement.x / (dragging_point - opposite).y } else { 0. }, + 1., + ])); + + // Combine that with a transform that makes opposite the origin. + DAffine2::from_translation(opposite) * skew * DAffine2::from_translation(-opposite) + } } /// Aligns the mouse position to the closest axis @@ -497,3 +533,52 @@ impl BoundingBoxManager { } } } + +#[test] +fn skew_transform_singular() { + for edge in [ + SelectedEdges::new(true, false, false, false, [DVec2::NEG_ONE, DVec2::ONE]), + SelectedEdges::new(false, true, false, false, [DVec2::NEG_ONE, DVec2::ONE]), + SelectedEdges::new(false, false, true, false, [DVec2::NEG_ONE, DVec2::ONE]), + SelectedEdges::new(false, false, false, true, [DVec2::NEG_ONE, DVec2::ONE]), + ] { + // 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); + } +} + +#[test] +fn skew_transform_correct() { + for edge in [ + SelectedEdges::new(true, false, false, false, [DVec2::NEG_ONE, DVec2::ONE]), + SelectedEdges::new(false, true, false, false, [DVec2::NEG_ONE, DVec2::ONE]), + SelectedEdges::new(false, false, true, false, [DVec2::NEG_ONE, DVec2::ONE]), + SelectedEdges::new(false, false, false, true, [DVec2::NEG_ONE, DVec2::ONE]), + ] { + // Random transform with det != 0. + 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); + + // This is the current handle that goes under the mouse. + let dragging_point = edge.pivot_from_bounds(edge.bounds[1], edge.bounds[0]); + + let parallel_to_x = edge.top || edge.bottom; + let parallel_to_y = !parallel_to_x && (edge.left || edge.right); + + // The target point is the projection in viewport space onto the line that the skew is parallel to. + let mut target_dragging_point = to_viewport_transform.transform_point2(dragging_point); + if parallel_to_x { + target_dragging_point += (mouse - target_dragging_point).project_onto(to_viewport_transform.transform_vector2(DVec2::X)); + } else if parallel_to_y { + target_dragging_point += (mouse - target_dragging_point).project_onto(to_viewport_transform.transform_vector2(DVec2::Y)); + } + + // Compute the final point in viewport space. + let final_dragging_point = to_viewport_transform.transform_point2(final_transform.transform_point2(dragging_point)); + assert_eq!(final_dragging_point, target_dragging_point); + } +} diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index f74a306e..6b290c99 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -81,6 +81,7 @@ pub enum SelectToolMessage { remove_from_selection: Key, select_deepest: Key, lasso_select: Key, + skew: Key, }, DragStop { remove_from_selection: Key, @@ -274,6 +275,7 @@ enum SelectToolFsmState { Drawing { selection_shape: SelectionShapeType }, Dragging, ResizingBounds, + SkewingBounds, RotatingBounds, DraggingPivot, } @@ -624,6 +626,7 @@ impl Fsm for SelectToolFsmState { remove_from_selection, select_deepest, lasso_select, + skew, }, ) => { tool_data.drag_start = input.mouse.position; @@ -705,7 +708,11 @@ impl Fsm for SelectToolFsmState { } tool_data.get_snap_candidates(document, input); - SelectToolFsmState::ResizingBounds + if input.keyboard.key(skew) { + SelectToolFsmState::SkewingBounds + }else{ + SelectToolFsmState::ResizingBounds + } } // Dragging near the transform cage bounding box to rotate it else if rotating_bounds { @@ -877,6 +884,37 @@ impl Fsm for SelectToolFsmState { } SelectToolFsmState::ResizingBounds } + (SelectToolFsmState::SkewingBounds, 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); + + tool_data.layers_dragging.retain(|layer| { + if *layer != LayerNodeIdentifier::ROOT_PARENT { + document.network_interface.network(&[]).unwrap().nodes.contains_key(&layer.to_node()) + } else { + log::error!("ROOT_PARENT should not be part of layers_dragging"); + false + } + }); + let selected = &tool_data.layers_dragging; + let mut pivot = DVec2::ZERO; + let mut selected = Selected::new( + &mut bounds.original_transforms, + &mut pivot, + selected, + responses, + &document.network_interface, + None, + &ToolType::Select, + None, + ); + + selected.apply_transformation(bounds.original_bound_transform * transformation * bounds.original_bound_transform.inverse()); + } + } + SelectToolFsmState::SkewingBounds + } (SelectToolFsmState::RotatingBounds, SelectToolMessage::PointerMove(modifier_keys)) => { if let Some(bounds) = &mut tool_data.bounding_box_manager { let angle = { @@ -978,7 +1016,7 @@ impl Fsm for SelectToolFsmState { SelectToolFsmState::Dragging } - (SelectToolFsmState::ResizingBounds, 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 { @@ -1081,7 +1119,7 @@ impl Fsm for SelectToolFsmState { let selection = tool_data.nested_selection_behavior; SelectToolFsmState::Ready { selection } } - (SelectToolFsmState::ResizingBounds, 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,