diff --git a/editor/src/consts.rs b/editor/src/consts.rs index e617a710..7ee5fde6 100644 --- a/editor/src/consts.rs +++ b/editor/src/consts.rs @@ -28,6 +28,10 @@ pub const SLOWING_DIVISOR: f64 = 10.; pub const SELECTION_TOLERANCE: f64 = 1.; pub const SELECTION_DRAG_ANGLE: f64 = 90.; +// Transformation cage +pub const BOUNDS_SELECT_THRESHOLD: f64 = 10.; +pub const BOUNDS_ROTATE_THRESHOLD: f64 = 40.; + // Path tool pub const VECTOR_MANIPULATOR_ANCHOR_MARKER_SIZE: f64 = 5.; diff --git a/editor/src/document/movement_message_handler.rs b/editor/src/document/movement_message_handler.rs index 1c71f1d9..5f5695ac 100644 --- a/editor/src/document/movement_message_handler.rs +++ b/editor/src/document/movement_message_handler.rs @@ -191,9 +191,9 @@ impl MessageHandler ActionList { let mut common = actions!(MovementMessageDiscriminant; - MouseMove, TranslateCanvasBegin, RotateCanvasBegin, ZoomCanvasBegin, @@ -345,6 +344,7 @@ impl MessageHandler, LayerMeta use TransformLayerMessage::*; let (layer_metadata, document, ipp) = data; - let mut selected = Selected::new(&mut self.original_transforms, &mut self.pivot, layer_metadata, responses, document); + + let selected_layers = layer_metadata.iter().filter_map(|(layer_path, data)| data.selected.then(|| layer_path)).collect::>(); + let mut selected = Selected::new(&mut self.original_transforms, &mut self.pivot, &selected_layers, responses, document); let mut begin_operation = |operation: TransformOperation, typing: &mut Typing, mouse_position: &mut DVec2, start_mouse: &mut DVec2| { if operation != TransformOperation::None { @@ -128,10 +130,10 @@ impl MessageHandler, LayerMeta TransformOperation::Rotating(rotation) => { let selected_pivot = selected.calculate_pivot(); let angle = { - let start_vec = self.mouse_position - selected_pivot; - let end_vec = ipp.mouse.position - selected_pivot; + let start_offset = self.mouse_position - selected_pivot; + let end_offset = ipp.mouse.position - selected_pivot; - start_vec.angle_between(end_vec) + start_offset.angle_between(end_offset) }; let change = if self.slow { angle / SLOWING_DIVISOR } else { angle }; diff --git a/editor/src/document/transformation.rs b/editor/src/document/transformation.rs index e4e57415..c78d7b79 100644 --- a/editor/src/document/transformation.rs +++ b/editor/src/document/transformation.rs @@ -1,4 +1,3 @@ -use super::layer_panel::LayerMetadata; use crate::consts::{ROTATE_SNAP_ANGLE, SCALE_SNAP_INTERVAL}; use crate::message_prelude::*; @@ -188,25 +187,18 @@ impl TransformOperation { } pub struct Selected<'a> { - pub selected: Vec>, + pub selected: &'a [&'a Vec], pub responses: &'a mut VecDeque, - pub document: &'a mut Document, + pub document: &'a Document, pub original_transforms: &'a mut OriginalTransforms, pub pivot: &'a mut DVec2, } impl<'a> Selected<'a> { - pub fn new( - original_transforms: &'a mut OriginalTransforms, - pivot: &'a mut DVec2, - layer_metadata: &'a mut HashMap, LayerMetadata>, - responses: &'a mut VecDeque, - document: &'a mut Document, - ) -> Self { - let selected = layer_metadata.iter().filter_map(|(layer_path, data)| data.selected.then(|| layer_path.to_owned())).collect(); - for path in &selected { - if !original_transforms.contains_key::>(path) { - original_transforms.insert(path.clone(), document.layer(path).unwrap().transform); + pub fn new(original_transforms: &'a mut OriginalTransforms, pivot: &'a mut DVec2, selected: &'a [&'a Vec], responses: &'a mut VecDeque, document: &'a Document) -> Self { + for path in selected { + if !original_transforms.contains_key(*path) { + original_transforms.insert(path.to_vec(), document.layer(path).unwrap().transform); } } Self { @@ -247,7 +239,7 @@ impl<'a> Selected<'a> { // TODO: Cache the result of `shallowest_unique_layers` to avoid this heavy computation every frame of movement, see https://github.com/GraphiteEditor/Graphite/pull/481 for layer_path in Document::shallowest_unique_layers(self.selected.iter()) { let parent_folder_path = &layer_path[..layer_path.len() - 1]; - let original_layer_transforms = *self.original_transforms.get(layer_path).unwrap(); + let original_layer_transforms = *self.original_transforms.get(*layer_path).unwrap(); let to = self.document.generate_transform_across_scope(parent_folder_path, None).unwrap(); let new = to.inverse() * transformation * to * original_layer_transforms; @@ -266,11 +258,11 @@ impl<'a> Selected<'a> { } pub fn revert_operation(&mut self) { - for path in &self.selected { + for path in self.selected { self.responses.push_back( DocumentOperation::SetLayerTransform { path: path.to_vec(), - transform: (*self.original_transforms.get(path).unwrap()).to_cols_array(), + transform: (*self.original_transforms.get(*path).unwrap()).to_cols_array(), } .into(), ); diff --git a/editor/src/frontend/utility_types.rs b/editor/src/frontend/utility_types.rs index cf0c2fd2..027d03c1 100644 --- a/editor/src/frontend/utility_types.rs +++ b/editor/src/frontend/utility_types.rs @@ -15,4 +15,14 @@ pub enum MouseCursorIcon { Grabbing, Crosshair, Text, + NSResize, + EWResize, + NESWResize, + NWSEResize, +} + +impl Default for MouseCursorIcon { + fn default() -> Self { + Self::Default + } } diff --git a/editor/src/input/input_mapper.rs b/editor/src/input/input_mapper.rs index e5b924f7..66744235 100644 --- a/editor/src/input/input_mapper.rs +++ b/editor/src/input/input_mapper.rs @@ -29,6 +29,7 @@ impl Default for Mapping { let mappings = mapping![ // Higher priority than entries in sections below entry! {action=PortfolioMessage::Paste { clipboard: Clipboard::User }, key_down=KeyV, modifiers=[KeyControl]}, + entry! {action=MovementMessage::MouseMove { snap_angle: KeyControl, wait_for_snap_angle_release: true, snap_zoom: KeyControl, zoom_from_viewport: None }, message=InputMapperMessage::PointerMove}, // Transform layers entry! {action=TransformLayerMessage::ApplyTransformOperation, key_down=KeyEnter}, entry! {action=TransformLayerMessage::ApplyTransformOperation, key_down=Lmb}, @@ -42,7 +43,7 @@ impl Default for Mapping { entry! {action=TransformLayerMessage::TypeDecimalPoint, key_down=KeyPeriod}, entry! {action=TransformLayerMessage::MouseMove { slow_key: KeyShift, snap_key: KeyControl }, triggers=[KeyShift, KeyControl]}, // Select - entry! {action=SelectMessage::MouseMove { snap_angle: KeyShift }, message=InputMapperMessage::PointerMove}, + entry! {action=SelectMessage::MouseMove { axis_align: KeyShift, snap_angle: KeyControl }, message=InputMapperMessage::PointerMove}, entry! {action=SelectMessage::DragStart { add_to_selection: KeyShift }, key_down=Lmb}, entry! {action=SelectMessage::DragStop, key_up=Lmb}, entry! {action=SelectMessage::EditText, message=InputMapperMessage::DoubleClick}, @@ -145,7 +146,6 @@ impl Default for Mapping { entry! {action=TransformLayerMessage::BeginRotate, key_down=KeyR}, entry! {action=TransformLayerMessage::BeginScale, key_down=KeyS}, // Document movement - entry! {action=MovementMessage::MouseMove { snap_angle: KeyControl, wait_for_snap_angle_release: true, snap_zoom: KeyControl, zoom_from_viewport: None }, message=InputMapperMessage::PointerMove}, entry! {action=MovementMessage::RotateCanvasBegin, key_down=Mmb, modifiers=[KeyControl]}, entry! {action=MovementMessage::ZoomCanvasBegin, key_down=Mmb, modifiers=[KeyShift]}, entry! {action=MovementMessage::TranslateCanvasBegin, key_down=Mmb}, diff --git a/editor/src/viewport_tools/snapping.rs b/editor/src/viewport_tools/snapping.rs index 3103f0a8..8ffdf7f4 100644 --- a/editor/src/viewport_tools/snapping.rs +++ b/editor/src/viewport_tools/snapping.rs @@ -93,18 +93,18 @@ impl SnapHandler { } } - /// Gets a list of snap targets for the X and Y axes in Viewport coords for the target layers (usually all layers or all non-selected layers.) + /// Gets a list of snap targets for the X and Y axes (if specified) in Viewport coords for the target layers (usually all layers or all non-selected layers.) /// This should be called at the start of a drag. - pub fn start_snap<'a>(&mut self, document_message_handler: &DocumentMessageHandler, target_layers: impl Iterator) { + pub fn start_snap<'a>(&mut self, document_message_handler: &DocumentMessageHandler, target_layers: impl Iterator, snap_x: bool, snap_y: bool) { if document_message_handler.snapping_enabled { + let (x_targets, y_targets) = target_layers + .filter_map(|path| document_message_handler.graphene_document.viewport_bounding_box(path).ok()?) + .flat_map(|[bound1, bound2]| [bound1, bound2, ((bound1 + bound2) / 2.)]) + .map(|vec| vec.into()) + .unzip(); + // Could be made into sorted Vec or a HashSet for more performant lookups. - self.snap_targets = Some( - target_layers - .filter_map(|path| document_message_handler.graphene_document.viewport_bounding_box(path).ok()?) - .flat_map(|[bound1, bound2]| [bound1, bound2, ((bound1 + bound2) / 2.)]) - .map(|vec| vec.into()) - .unzip(), - ); + self.snap_targets = Some((if snap_x { x_targets } else { Vec::new() }, if snap_y { y_targets } else { Vec::new() })); } } diff --git a/editor/src/viewport_tools/tools/line.rs b/editor/src/viewport_tools/tools/line.rs index 377ebebc..f46fd790 100644 --- a/editor/src/viewport_tools/tools/line.rs +++ b/editor/src/viewport_tools/tools/line.rs @@ -155,7 +155,7 @@ impl Fsm for LineToolFsmState { if let ToolMessage::Line(event) = event { match (self, event) { (Ready, DragStart) => { - data.snap_handler.start_snap(document, document.visible_layers()); + data.snap_handler.start_snap(document, document.visible_layers(), true, true); data.drag_start = data.snap_handler.snap_position(responses, input.viewport_bounds.size(), document, input.mouse.position); responses.push_back(DocumentMessage::StartTransaction.into()); diff --git a/editor/src/viewport_tools/tools/pen.rs b/editor/src/viewport_tools/tools/pen.rs index 5e1db78e..e81e4adb 100644 --- a/editor/src/viewport_tools/tools/pen.rs +++ b/editor/src/viewport_tools/tools/pen.rs @@ -155,7 +155,7 @@ impl Fsm for PenToolFsmState { responses.push_back(DocumentMessage::DeselectAllLayers.into()); data.path = Some(vec![generate_uuid()]); - data.snap_handler.start_snap(document, document.visible_layers()); + data.snap_handler.start_snap(document, document.visible_layers(), true, true); let snapped_position = data.snap_handler.snap_position(responses, input.viewport_bounds.size(), document, input.mouse.position); let pos = transform.inverse().transform_point2(snapped_position); diff --git a/editor/src/viewport_tools/tools/select.rs b/editor/src/viewport_tools/tools/select.rs index 06b17dd9..b7c141d1 100644 --- a/editor/src/viewport_tools/tools/select.rs +++ b/editor/src/viewport_tools/tools/select.rs @@ -1,4 +1,5 @@ -use crate::consts::{COLOR_ACCENT, SELECTION_DRAG_ANGLE, SELECTION_TOLERANCE}; +use crate::consts::{BOUNDS_ROTATE_THRESHOLD, BOUNDS_SELECT_THRESHOLD, COLOR_ACCENT, ROTATE_SNAP_ANGLE, SELECTION_DRAG_ANGLE, SELECTION_TOLERANCE, VECTOR_MANIPULATOR_ANCHOR_MARKER_SIZE}; +use crate::document::transformation::{OriginalTransforms, Selected}; use crate::document::utility_types::{AlignAggregate, AlignAxis, FlipAxis}; use crate::document::DocumentMessageHandler; use crate::frontend::utility_types::MouseCursorIcon; @@ -11,6 +12,7 @@ use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup}; use crate::viewport_tools::snapping::SnapHandler; use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData, ToolType}; +use graphene::color::Color; use graphene::document::Document; use graphene::intersection::Quad; use graphene::layers::style::{self, Fill, Stroke}; @@ -48,6 +50,7 @@ pub enum SelectMessage { FlipHorizontal, FlipVertical, MouseMove { + axis_align: Key, snap_angle: Key, }, } @@ -233,7 +236,7 @@ impl<'a> MessageHandler> for Select { } if action == ToolMessage::UpdateCursor { - self.fsm_state.update_cursor(responses); + responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }.into()); return; } @@ -242,7 +245,6 @@ impl<'a> MessageHandler> for Select { if self.fsm_state != new_state { self.fsm_state = new_state; self.fsm_state.update_hints(responses); - self.fsm_state.update_cursor(responses); } } @@ -250,9 +252,11 @@ impl<'a> MessageHandler> for Select { use SelectToolFsmState::*; match self.fsm_state { - Ready => actions!(SelectMessageDiscriminant; DragStart, EditText), + Ready => actions!(SelectMessageDiscriminant; DragStart, MouseMove, EditText), Dragging => actions!(SelectMessageDiscriminant; DragStop, MouseMove, EditText), DrawingBox => actions!(SelectMessageDiscriminant; DragStop, MouseMove, Abort, EditText), + ResizingBounds => actions!(SelectMessageDiscriminant; DragStop, MouseMove, Abort, EditText), + RotatingBounds => actions!(SelectMessageDiscriminant; DragStop, MouseMove, Abort, EditText), } } } @@ -262,6 +266,8 @@ enum SelectToolFsmState { Ready, Dragging, DrawingBox, + ResizingBounds, + RotatingBounds, } impl Default for SelectToolFsmState { @@ -276,8 +282,9 @@ struct SelectToolData { drag_current: ViewportPosition, layers_dragging: Vec>, // Paths and offsets drag_box_overlay_layer: Option>, - bounding_box_overlay_layer: Option>, + bounding_box_overlays: Option, snap_handler: SnapHandler, + cursor: MouseCursorIcon, } impl SelectToolData { @@ -296,6 +303,63 @@ impl SelectToolData { } } +/// Handles the selected edges whilst dragging the layer bounds +#[derive(Clone, Debug, Default)] +struct SelectedEdges { + bounds: [DVec2; 2], + top: bool, + bottom: bool, + left: bool, + right: bool, +} + +impl SelectedEdges { + fn new(top: bool, bottom: bool, left: bool, right: bool, bounds: [DVec2; 2]) -> Self { + Self { top, bottom, left, right, bounds } + } + + /// Calculate the pivot for the operation (the opposite point to the edge dragged) + fn calculate_pivot(&self) -> DVec2 { + let min = self.bounds[0]; + let max = self.bounds[1]; + + let x = if self.left { + max.x + } else if self.right { + min.x + } else { + (min.x + max.x) / 2. + }; + + let y = if self.top { + max.y + } else if self.bottom { + min.y + } else { + (min.y + max.y) / 2. + }; + + DVec2::new(x, y) + } + + /// Calculates the required scaling to resize the bounding box + fn pos_to_scale_transform(&self, mouse: DVec2) -> DAffine2 { + let mut min = self.bounds[0]; + let mut max = self.bounds[1]; + if self.top { + min.y = mouse.y; + } else if self.bottom { + max.y = mouse.y; + } + if self.left { + min.x = mouse.x + } else if self.right { + max.x = mouse.x; + } + DAffine2::from_scale((max - min) / (self.bounds[1] - self.bounds[0])) + } +} + fn add_bounding_box(responses: &mut Vec) -> Vec { let path = vec![generate_uuid()]; @@ -309,10 +373,150 @@ fn add_bounding_box(responses: &mut Vec) -> Vec { path } +fn evaluate_transform_handle_positions((left, top): (f64, f64), (right, bottom): (f64, f64)) -> [DVec2; 8] { + [ + DVec2::new(left, top), + DVec2::new(left, (top + bottom) / 2.), + DVec2::new(left, bottom), + DVec2::new((left + right) / 2., top), + DVec2::new((left + right) / 2., bottom), + DVec2::new(right, top), + DVec2::new(right, (top + bottom) / 2.), + DVec2::new(right, bottom), + ] +} + +fn add_transform_handles(responses: &mut Vec) -> [Vec; 8] { + const EMPTY_VEC: Vec = Vec::new(); + let mut transform_handle_paths = [EMPTY_VEC; 8]; + + for item in &mut transform_handle_paths { + let current_path = vec![generate_uuid()]; + + let operation = Operation::AddOverlayRect { + path: current_path.clone(), + transform: DAffine2::ZERO.to_cols_array(), + style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 2.0)), Some(Fill::new(Color::WHITE))), + }; + responses.push(DocumentMessage::Overlays(operation.into()).into()); + + *item = current_path; + } + + transform_handle_paths +} + fn transform_from_box(pos1: DVec2, pos2: DVec2) -> [f64; 6] { DAffine2::from_scale_angle_translation((pos2 - pos1).round(), 0., pos1.round() - DVec2::splat(0.5)).to_cols_array() } +/// Contains info on the overlays for the bounding box and transform handles +#[derive(Clone, Debug, Default)] +struct BoundingBoxOverlays { + pub bounding_box: Vec, + pub transform_handles: [Vec; 8], + pub bounds: [DVec2; 2], + pub selected_edges: Option, + pub original_transforms: OriginalTransforms, + pub pivot: DVec2, +} + +impl BoundingBoxOverlays { + #[must_use] + pub fn new(buffer: &mut Vec) -> Self { + Self { + bounding_box: add_bounding_box(buffer), + transform_handles: add_transform_handles(buffer), + ..Default::default() + } + } + + /// Update the position of the bounding box and transform handles + pub fn transform(&mut self, buffer: &mut Vec) { + let transform = transform_from_box(self.bounds[0], self.bounds[1]); + let path = self.bounding_box.clone(); + buffer.push(DocumentMessage::Overlays(Operation::SetLayerTransformInViewport { path, transform }.into()).into()); + + // Helps push values that end in approximately half, plus or minus some floating point imprecision, towards the same side of the round() function + const BIAS: f64 = 0.0001; + + for (position, path) in evaluate_transform_handle_positions(self.bounds[0].into(), self.bounds[1].into()) + .into_iter() + .zip(&self.transform_handles) + { + let scale = DVec2::splat(VECTOR_MANIPULATOR_ANCHOR_MARKER_SIZE); + let translation = (position - (scale / 2.) - 0.5 + BIAS).round(); + let transform = DAffine2::from_scale_angle_translation(scale, 0., translation).to_cols_array(); + let path = path.clone(); + buffer.push(DocumentMessage::Overlays(Operation::SetLayerTransformInViewport { path, transform }.into()).into()); + } + } + + /// Check if the user has selected the edge for dragging (returns which edge in order top, bottom, left, right) + pub fn check_selected_edges(&self, cursor: DVec2) -> Option<(bool, bool, bool, bool)> { + let min = self.bounds[0].min(self.bounds[1]); + let max = self.bounds[0].max(self.bounds[1]); + if min.x - cursor.x < BOUNDS_SELECT_THRESHOLD && min.y - cursor.y < BOUNDS_SELECT_THRESHOLD && cursor.x - max.x < BOUNDS_SELECT_THRESHOLD && cursor.y - max.y < BOUNDS_SELECT_THRESHOLD { + let mut top = (cursor.y - min.y).abs() < BOUNDS_SELECT_THRESHOLD; + let mut bottom = (max.y - cursor.y).abs() < BOUNDS_SELECT_THRESHOLD; + let mut left = (cursor.x - min.x).abs() < BOUNDS_SELECT_THRESHOLD; + let mut right = (max.x - cursor.x).abs() < BOUNDS_SELECT_THRESHOLD; + if cursor.y - min.y + max.y - cursor.y < BOUNDS_SELECT_THRESHOLD * 2. && (left || right) { + top = false; + bottom = false; + } + if cursor.x - min.x + max.x - cursor.x < BOUNDS_SELECT_THRESHOLD * 2. && (top || bottom) { + left = false; + right = false; + } + + if top || bottom || left || right { + return Some((top, bottom, left, right)); + } + } + + None + } + + /// Check if the user is rotating with the bounds + pub fn check_rotate(&self, cursor: DVec2) -> bool { + let min = self.bounds[0].min(self.bounds[1]); + let max = self.bounds[0].max(self.bounds[1]); + + let outside_bounds = (min.x > cursor.x || cursor.x > max.x) || (min.y > cursor.y || cursor.y > max.y); + let inside_extended_bounds = + min.x - cursor.x < BOUNDS_ROTATE_THRESHOLD && min.y - cursor.y < BOUNDS_ROTATE_THRESHOLD && cursor.x - max.x < BOUNDS_ROTATE_THRESHOLD && cursor.y - max.y < BOUNDS_ROTATE_THRESHOLD; + + outside_bounds & inside_extended_bounds + } + + pub fn get_cursor(&self, input: &InputPreprocessorMessageHandler) -> MouseCursorIcon { + if let Some(directions) = self.check_selected_edges(input.mouse.position) { + match directions { + (true, false, false, false) | (false, true, false, false) => MouseCursorIcon::NSResize, + (false, false, true, false) | (false, false, false, true) => MouseCursorIcon::EWResize, + (true, false, true, false) | (false, true, false, true) => MouseCursorIcon::NWSEResize, + (true, false, false, true) | (false, true, true, false) => MouseCursorIcon::NESWResize, + _ => MouseCursorIcon::Default, + } + } else if self.check_rotate(input.mouse.position) { + MouseCursorIcon::Grabbing + } else { + MouseCursorIcon::Default + } + } + + /// Removes the overlays + pub fn delete(self, buffer: &mut impl Extend) { + buffer.extend([DocumentMessage::Overlays(Operation::DeleteLayer { path: self.bounding_box }.into()).into()]); + buffer.extend( + self.transform_handles + .iter() + .map(|path| DocumentMessage::Overlays(Operation::DeleteLayer { path: path.clone() }.into()).into()), + ); + } +} + impl Fsm for SelectToolFsmState { type ToolData = SelectToolData; type ToolOptions = (); @@ -334,19 +538,18 @@ impl Fsm for SelectToolFsmState { match (self, event) { (_, DocumentIsDirty) => { let mut buffer = Vec::new(); - let response = match (document.selected_visible_layers_bounding_box(), data.bounding_box_overlay_layer.take()) { - (None, Some(path)) => DocumentMessage::Overlays(Operation::DeleteLayer { path }.into()).into(), - (Some([pos1, pos2]), path) => { - let path = path.unwrap_or_else(|| add_bounding_box(&mut buffer)); + match (document.selected_visible_layers_bounding_box(), data.bounding_box_overlays.take()) { + (None, Some(bounding_box_overlays)) => bounding_box_overlays.delete(&mut buffer), + (Some(bounds), paths) => { + let mut bounding_box_overlays = paths.unwrap_or_else(|| BoundingBoxOverlays::new(&mut buffer)); - data.bounding_box_overlay_layer = Some(path.clone()); + bounding_box_overlays.bounds = bounds; + bounding_box_overlays.transform(&mut buffer); - let transform = transform_from_box(pos1, pos2); - DocumentMessage::Overlays(Operation::SetLayerTransformInViewport { path, transform }.into()).into() + data.bounding_box_overlays = Some(bounding_box_overlays); } - (_, _) => Message::NoOp, + (_, _) => {} }; - responses.push_front(response); buffer.into_iter().rev().for_each(|message| responses.push_front(message)); self } @@ -372,18 +575,62 @@ impl Fsm for SelectToolFsmState { data.drag_start = input.mouse.position; data.drag_current = input.mouse.position; let mut buffer = Vec::new(); + + let dragging_bounds = if let Some(bounding_box) = &mut data.bounding_box_overlays { + let edges = bounding_box.check_selected_edges(input.mouse.position); + + bounding_box.selected_edges = edges.map(|(top, bottom, left, right)| { + let edges = SelectedEdges::new(top, bottom, left, right, bounding_box.bounds); + bounding_box.pivot = edges.calculate_pivot(); + edges + }); + + edges + } else { + None + }; + + let rotating_bounds = if let Some(bounding_box) = &mut data.bounding_box_overlays { + bounding_box.check_rotate(input.mouse.position) + } else { + false + }; + let mut selected: Vec<_> = document.selected_visible_layers().map(|path| path.to_vec()).collect(); let quad = data.selection_quad(); let mut intersection = document.graphene_document.intersects_quad_root(quad); + // If the user is dragging the bounding box bounds, go into ResizingBounds mode. + // If the user is dragging the rotate trigger, go into RotatingBounds mode. // If the user clicks on a layer that is in their current selection, go into the dragging mode. // If the user clicks on new shape, make that layer their new selection. // Otherwise enter the box select mode - let state = if selected.iter().any(|path| intersection.contains(path)) { + let state = if let Some(selected_edges) = dragging_bounds { + let snap_x = selected_edges.2 || selected_edges.3; + let snap_y = selected_edges.0 || selected_edges.1; + + data.snap_handler + .start_snap(document, document.visible_layers().filter(|layer| !selected.iter().any(|path| path == layer)), snap_x, snap_y); + + data.layers_dragging = selected; + + ResizingBounds + } else if rotating_bounds { + if let Some(bounds) = &mut data.bounding_box_overlays { + let selected = selected.iter().collect::>(); + let mut selected = Selected::new(&mut bounds.original_transforms, &mut bounds.pivot, &selected, responses, &document.graphene_document); + + *selected.pivot = selected.calculate_pivot(); + } + + data.layers_dragging = selected; + + RotatingBounds + } else if selected.iter().any(|path| intersection.contains(path)) { buffer.push(DocumentMessage::StartTransaction.into()); data.layers_dragging = selected; data.snap_handler - .start_snap(document, document.visible_layers().filter(|layer| !data.layers_dragging.iter().any(|path| path == layer))); + .start_snap(document, document.visible_layers().filter(|layer| !data.layers_dragging.iter().any(|path| path == layer)), true, true); Dragging } else { @@ -398,7 +645,7 @@ impl Fsm for SelectToolFsmState { buffer.push(DocumentMessage::StartTransaction.into()); data.layers_dragging.append(&mut selected); data.snap_handler - .start_snap(document, document.visible_layers().filter(|layer| !data.layers_dragging.iter().any(|path| path == layer))); + .start_snap(document, document.visible_layers().filter(|layer| !data.layers_dragging.iter().any(|path| path == layer)), true, true); Dragging } else { @@ -410,11 +657,11 @@ impl Fsm for SelectToolFsmState { state } - (Dragging, MouseMove { snap_angle }) => { + (Dragging, MouseMove { axis_align, .. }) => { // TODO: This is a cheat. Break out the relevant functionality from the handler above and call it from there and here. responses.push_front(SelectMessage::DocumentIsDirty.into()); - let mouse_position = if input.keyboard.get(snap_angle as usize) { + let mouse_position = if input.keyboard.get(axis_align as usize) { let mouse_position = input.mouse.position - data.drag_start; let snap_resolution = SELECTION_DRAG_ANGLE.to_radians(); let angle = -mouse_position.angle_between(DVec2::X); @@ -440,6 +687,49 @@ impl Fsm for SelectToolFsmState { data.drag_current = mouse_position + closest_move; Dragging } + (ResizingBounds, MouseMove { .. }) => { + if let Some(bounds) = &mut data.bounding_box_overlays { + if let Some(movement) = &mut bounds.selected_edges { + let mouse_position = input.mouse.position; + + let snapped_mouse_position = data.snap_handler.snap_position(responses, input.viewport_bounds.size(), document, mouse_position); + + let delta = movement.pos_to_scale_transform(snapped_mouse_position); + + let selected = data.layers_dragging.iter().collect::>(); + let mut selected = Selected::new(&mut bounds.original_transforms, &mut bounds.pivot, &selected, responses, &document.graphene_document); + + selected.update_transforms(delta); + } + } + ResizingBounds + } + (RotatingBounds, MouseMove { snap_angle, .. }) => { + if let Some(bounds) = &mut data.bounding_box_overlays { + let angle = { + let start_offset = data.drag_start - bounds.pivot; + let end_offset = input.mouse.position - bounds.pivot; + + start_offset.angle_between(end_offset) + }; + + let snapped_angle = if input.keyboard.get(snap_angle as usize) { + let snap_resolution = ROTATE_SNAP_ANGLE.to_radians(); + (angle / snap_resolution).round() * snap_resolution + } else { + angle + }; + + let delta = DAffine2::from_angle(snapped_angle); + + let selected = data.layers_dragging.iter().collect::>(); + let mut selected = Selected::new(&mut bounds.original_transforms, &mut bounds.pivot, &selected, responses, &document.graphene_document); + + selected.update_transforms(delta); + } + + RotatingBounds + } (DrawingBox, MouseMove { .. }) => { data.drag_current = input.mouse.position; @@ -455,6 +745,16 @@ impl Fsm for SelectToolFsmState { ); DrawingBox } + (Ready, MouseMove { .. }) => { + let cursor = data.bounding_box_overlays.as_ref().map_or(MouseCursorIcon::Default, |bounds| bounds.get_cursor(input)); + + if data.cursor != cursor { + data.cursor = cursor; + responses.push_back(FrontendMessage::UpdateMouseCursor { cursor }.into()); + } + + Ready + } (Dragging, DragStop) => { let response = match input.mouse.position.distance(data.drag_start) < 10. * f64::EPSILON { true => DocumentMessage::Undo, @@ -464,6 +764,22 @@ impl Fsm for SelectToolFsmState { responses.push_front(response.into()); Ready } + (ResizingBounds, DragStop) => { + data.snap_handler.cleanup(responses); + + if let Some(bounds) = &mut data.bounding_box_overlays { + bounds.original_transforms.clear(); + } + + Ready + } + (RotatingBounds, DragStop) => { + if let Some(bounds) = &mut data.bounding_box_overlays { + bounds.original_transforms.clear(); + } + + Ready + } (DrawingBox, DragStop) => { let quad = data.selection_quad(); responses.push_front( @@ -484,9 +800,13 @@ impl Fsm for SelectToolFsmState { Ready } (_, Abort) => { - let mut delete = |path: &mut Option>| path.take().map(|path| responses.push_front(DocumentMessage::Overlays(Operation::DeleteLayer { path }.into()).into())); - delete(&mut data.drag_box_overlay_layer); - delete(&mut data.bounding_box_overlay_layer); + if let Some(path) = data.drag_box_overlay_layer.take() { + responses.push_front(DocumentMessage::Overlays(Operation::DeleteLayer { path }.into()).into()) + }; + if let Some(bounding_box_overlays) = data.bounding_box_overlays.take() { + bounding_box_overlays.delete(responses); + } + data.snap_handler.cleanup(responses); Ready } @@ -624,6 +944,13 @@ impl Fsm for SelectToolFsmState { }, ])]), SelectToolFsmState::DrawingBox => HintData(vec![]), + SelectToolFsmState::ResizingBounds => HintData(vec![]), + SelectToolFsmState::RotatingBounds => HintData(vec![HintGroup(vec![HintInfo { + key_groups: vec![KeysGroup(vec![Key::KeyControl])], + mouse: None, + label: String::from("Snap 15°"), + plus: false, + }])]), }; responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into()); diff --git a/editor/src/viewport_tools/tools/shared/resize.rs b/editor/src/viewport_tools/tools/shared/resize.rs index 3026cdba..d5a2ed48 100644 --- a/editor/src/viewport_tools/tools/shared/resize.rs +++ b/editor/src/viewport_tools/tools/shared/resize.rs @@ -18,7 +18,7 @@ pub struct Resize { impl Resize { /// Starts a resize, assigning the snap targets and snapping the starting position. pub fn start(&mut self, responses: &mut VecDeque, viewport_bounds: DVec2, document: &DocumentMessageHandler, mouse_position: DVec2) { - self.snap_handler.start_snap(document, document.visible_layers()); + self.snap_handler.start_snap(document, document.visible_layers(), true, true); self.drag_start = self.snap_handler.snap_position(responses, viewport_bounds, document, mouse_position); } diff --git a/frontend/src/dispatcher/js-messages.ts b/frontend/src/dispatcher/js-messages.ts index c1152293..06a86c25 100644 --- a/frontend/src/dispatcher/js-messages.ts +++ b/frontend/src/dispatcher/js-messages.ts @@ -199,7 +199,7 @@ export class UpdateDocumentRulers extends JsMessage { readonly interval!: number; } -export type MouseCursorIcon = "default" | "zoom-in" | "zoom-out" | "grabbing" | "crosshair" | "text"; +export type MouseCursorIcon = "default" | "zoom-in" | "zoom-out" | "grabbing" | "crosshair" | "text" | "ns-resize" | "ew-resize" | "nesw-resize" | "nwse-resize"; const ToCssCursorProperty = Transform(({ value }) => { const cssNames: Record = { @@ -208,6 +208,10 @@ const ToCssCursorProperty = Transform(({ value }) => { Grabbing: "grabbing", Crosshair: "crosshair", Text: "text", + NSResize: "ns-resize", + EWResize: "ew-resize", + NESWResize: "nesw-resize", + NWSEResize: "nwse-resize", }; return cssNames[value] || "default";