diff --git a/editor/src/consts.rs b/editor/src/consts.rs index 7c436eca44..e617a710dc 100644 --- a/editor/src/consts.rs +++ b/editor/src/consts.rs @@ -16,6 +16,8 @@ pub const VIEWPORT_SCROLL_RATE: f64 = 0.6; pub const VIEWPORT_ROTATE_SNAP_INTERVAL: f64 = 15.; pub const SNAP_TOLERANCE: f64 = 3.; +pub const SNAP_OVERLAY_FADE_DISTANCE: f64 = 20.; +pub const SNAP_OVERLAY_UNSNAPPED_OPACITY: f64 = 0.4; // Transforming layer pub const ROTATE_SNAP_ANGLE: f64 = 15.; diff --git a/editor/src/document/document_message_handler.rs b/editor/src/document/document_message_handler.rs index fa9c17b854..b783e31ea6 100644 --- a/editor/src/document/document_message_handler.rs +++ b/editor/src/document/document_message_handler.rs @@ -182,6 +182,10 @@ impl DocumentMessageHandler { self.layer_metadata.iter().filter_map(|(path, data)| data.selected.then(|| path.as_slice())) } + pub fn non_selected_layers(&self) -> impl Iterator { + self.layer_metadata.iter().filter_map(|(path, data)| (!data.selected).then(|| path.as_slice())) + } + pub fn selected_layers_without_children(&self) -> Vec<&[LayerId]> { let unique_layers = GrapheneDocument::shallowest_unique_layers(self.selected_layers()); @@ -200,6 +204,13 @@ impl DocumentMessageHandler { }) } + pub fn visible_layers(&self) -> impl Iterator { + self.all_layers().filter(|path| match self.graphene_document.layer(path) { + Ok(layer) => layer.visible, + Err(_) => false, + }) + } + fn serialize_structure(&self, folder: &Folder, structure: &mut Vec, data: &mut Vec, path: &mut Vec) { let mut space = 0; for (id, layer) in folder.layer_ids.iter().zip(folder.layers()).rev() { @@ -267,7 +278,7 @@ impl DocumentMessageHandler { self.layer_metadata.keys().filter_map(|path| (!path.is_empty()).then(|| path.as_slice())) } - /// Returns the paths to all layers in order, optionally including only selected or non-selected layers. + /// Returns the paths to all layers in order fn sort_layers<'a>(&self, paths: impl Iterator) -> Vec<&'a [LayerId]> { // Compute the indices for each layer to be able to sort them let mut layers_with_indices: Vec<(&[LayerId], Vec)> = paths @@ -302,7 +313,7 @@ impl DocumentMessageHandler { /// Returns the paths to all non_selected layers in order #[allow(dead_code)] // used for test cases pub fn non_selected_layers_sorted(&self) -> Vec<&[LayerId]> { - self.sort_layers(self.all_layers().filter(|layer| !self.selected_layers().any(|path| &path == layer))) + self.sort_layers(self.non_selected_layers()) } pub fn layer_metadata(&self, path: &[LayerId]) -> &LayerMetadata { diff --git a/editor/src/document/transformation.rs b/editor/src/document/transformation.rs index 1461e2a228..e4e57415c8 100644 --- a/editor/src/document/transformation.rs +++ b/editor/src/document/transformation.rs @@ -60,6 +60,7 @@ impl Translation { } } + #[must_use] pub fn increment_amount(self, delta: DVec2) -> Self { Self { dragged_distance: self.dragged_distance + delta, @@ -87,6 +88,7 @@ impl Rotation { } } + #[must_use] pub fn increment_amount(self, delta: f64) -> Self { Self { dragged_angle: self.dragged_angle + delta, @@ -124,6 +126,7 @@ impl Scale { } } + #[must_use] pub fn increment_amount(self, delta: f64) -> Self { Self { dragged_factor: self.dragged_factor + delta, diff --git a/editor/src/input/input_preprocessor_message_handler.rs b/editor/src/input/input_preprocessor_message_handler.rs index 536c3d5fac..025bc70a55 100644 --- a/editor/src/input/input_preprocessor_message_handler.rs +++ b/editor/src/input/input_preprocessor_message_handler.rs @@ -37,16 +37,6 @@ impl MessageHandler for InputPreprocessorMessageHa } .into(), ); - responses.push_back( - DocumentMessage::Overlays( - graphene::Operation::TransformLayer { - path: vec![], - transform: glam::DAffine2::from_translation(translation).to_cols_array(), - } - .into(), - ) - .into(), - ); responses.push_back( DocumentMessage::Artboard( graphene::Operation::TransformLayer { diff --git a/editor/src/viewport_tools/snapping.rs b/editor/src/viewport_tools/snapping.rs index a25eb28988..32f9cfb0c4 100644 --- a/editor/src/viewport_tools/snapping.rs +++ b/editor/src/viewport_tools/snapping.rs @@ -1,25 +1,105 @@ -use crate::consts::SNAP_TOLERANCE; +use crate::consts::{COLOR_ACCENT, SNAP_OVERLAY_FADE_DISTANCE, SNAP_OVERLAY_UNSNAPPED_OPACITY, SNAP_TOLERANCE}; use crate::document::DocumentMessageHandler; +use crate::message_prelude::*; -use graphene::LayerId; +use graphene::layers::style::{self, Stroke}; +use graphene::{LayerId, Operation}; -use glam::DVec2; +use glam::{DAffine2, DVec2}; +use std::f64::consts::PI; #[derive(Debug, Clone, Default)] pub struct SnapHandler { snap_targets: Option<(Vec, Vec)>, + overlay_paths: Vec>, } impl SnapHandler { + /// Updates the snapping overlays with the specified distances. + /// `positions_and_distances` is a tuple of `position` and `distance` iterators, respectively, each with `(x, y)` values. + fn update_overlays( + overlay_paths: &mut Vec>, + responses: &mut VecDeque, + viewport_bounds: DVec2, + (positions_and_distances): (impl Iterator, impl Iterator), + closest_distance: DVec2, + ) { + /// Draws an alignment line overlay with the correct transform and fade opacity, reusing lines from the pool if available. + fn add_overlay_line(responses: &mut VecDeque, transform: [f64; 6], opacity: f64, index: usize, overlay_paths: &mut Vec>) { + // If there isn't one in the pool to ruse, add a new alignment line to the pool with the intended transform + let layer_path = if index >= overlay_paths.len() { + let layer_path = vec![generate_uuid()]; + responses.push_back( + DocumentMessage::Overlays( + Operation::AddOverlayLine { + path: layer_path.clone(), + transform, + style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 1.0)), None), + } + .into(), + ) + .into(), + ); + overlay_paths.push(layer_path.clone()); + layer_path + } + // Otherwise, reuse an overlay line from the pool and update its new transform + else { + let layer_path = overlay_paths[index].clone(); + responses.push_back(DocumentMessage::Overlays(Operation::SetLayerTransform { path: layer_path.clone(), transform }.into()).into()); + layer_path + }; + + // Then set its opacity to the fade amount + responses.push_back(DocumentMessage::Overlays(Operation::SetLayerOpacity { path: layer_path, opacity }.into()).into()); + } + + let (positions, distances) = positions_and_distances; + let mut index = 0; + + // Draw the vertical alignment lines + for (x_target, distance) in positions.filter(|(_pos, dist)| dist.abs() < SNAP_OVERLAY_FADE_DISTANCE) { + let transform = DAffine2::from_scale_angle_translation(DVec2::new(viewport_bounds.y, 1.), PI / 2., DVec2::new((x_target).round() - 0.5, 0.)).to_cols_array(); + + let opacity = if closest_distance.x == distance { + 1. + } else { + SNAP_OVERLAY_UNSNAPPED_OPACITY - distance.abs() / (SNAP_OVERLAY_FADE_DISTANCE / SNAP_OVERLAY_UNSNAPPED_OPACITY) + }; + + add_overlay_line(responses, transform, opacity, index, overlay_paths); + index += 1; + } + // Draw the horizontal alignment lines + for (y_target, distance) in distances.filter(|(_pos, dist)| dist.abs() < SNAP_OVERLAY_FADE_DISTANCE) { + let transform = DAffine2::from_scale_angle_translation(DVec2::new(viewport_bounds.x, 1.), 0., DVec2::new(0., (y_target).round() - 0.5)).to_cols_array(); + + let opacity = if closest_distance.y == distance { + 1. + } else { + SNAP_OVERLAY_UNSNAPPED_OPACITY - distance.abs() / (SNAP_OVERLAY_FADE_DISTANCE / SNAP_OVERLAY_UNSNAPPED_OPACITY) + }; + + add_overlay_line(responses, transform, opacity, index, overlay_paths); + index += 1; + } + Self::remove_unused_overlays(overlay_paths, responses, index); + } + + /// Remove overlays from the pool beyond a given index. Pool entries up through that index will be kept. + fn remove_unused_overlays(overlay_paths: &mut Vec>, responses: &mut VecDeque, remove_after_index: usize) { + while overlay_paths.len() > remove_after_index { + responses.push_back(DocumentMessage::Overlays(Operation::DeleteLayer { path: overlay_paths.pop().unwrap() }.into()).into()); + } + } + /// 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.) /// This should be called at the start of a drag. - pub fn start_snap(&mut self, document_message_handler: &DocumentMessageHandler, target_layers: Vec<&[LayerId]>, ignore_layers: &[Vec]) { + pub fn start_snap<'a>(&mut self, document_message_handler: &DocumentMessageHandler, target_layers: impl Iterator) { if document_message_handler.snapping_enabled { // Could be made into sorted Vec or a HashSet for more performant lookups. self.snap_targets = Some( target_layers - .iter() - .filter(|path| !ignore_layers.iter().any(|layer| layer.as_slice() == **path)) .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()) @@ -30,7 +110,14 @@ impl SnapHandler { /// Finds the closest snap from an array of layers to the specified snap targets in viewport coords. /// Returns 0 for each axis that there is no snap less than the snap tolerance. - pub fn snap_layers(&self, document_message_handler: &DocumentMessageHandler, selected_layers: &[Vec], mouse_delta: DVec2) -> DVec2 { + pub fn snap_layers( + &mut self, + responses: &mut VecDeque, + document_message_handler: &DocumentMessageHandler, + selected_layers: &[Vec], + viewport_bounds: DVec2, + mouse_delta: DVec2, + ) -> DVec2 { if document_message_handler.snapping_enabled { if let Some((targets_x, targets_y)) = &self.snap_targets { let (snap_x, snap_y): (Vec, Vec) = selected_layers @@ -40,24 +127,23 @@ impl SnapHandler { .map(|vec| vec.into()) .unzip(); - let closest_move = DVec2::new( - targets_x - .iter() - .flat_map(|target| snap_x.iter().map(move |snap| target - mouse_delta.x - snap)) - .min_by(|a, b| a.abs().partial_cmp(&b.abs()).expect("Could not compare document bounds.")) - .unwrap_or(0.), - targets_y - .iter() - .flat_map(|target| snap_y.iter().map(move |snap| target - mouse_delta.y - snap)) - .min_by(|a, b| a.abs().partial_cmp(&b.abs()).expect("Could not compare document bounds.")) - .unwrap_or(0.), + let positions = targets_x.iter().flat_map(|&target| snap_x.iter().map(move |&snap| (target, target - mouse_delta.x - snap))); + let distances = targets_y.iter().flat_map(|&target| snap_y.iter().map(move |&snap| (target, target - mouse_delta.y - snap))); + + let min_positions = positions.clone().min_by(|a, b| a.1.abs().partial_cmp(&b.1.abs()).expect("Could not compare position.")); + let min_distances = distances.clone().min_by(|a, b| a.1.abs().partial_cmp(&b.1.abs()).expect("Could not compare position.")); + + let closest_distance = DVec2::new(min_positions.map_or(0., |(_pos, dist)| dist), min_distances.map_or(0., |(_pos, dist)| dist)); + + // Clamp, do not move, if above snap tolerance + let clamped_closest_distance = DVec2::new( + if closest_distance.x.abs() > SNAP_TOLERANCE { 0. } else { closest_distance.x }, + if closest_distance.y.abs() > SNAP_TOLERANCE { 0. } else { closest_distance.y }, ); - // Clamp, do not move if over snap tolerance - DVec2::new( - if closest_move.x.abs() > SNAP_TOLERANCE { 0. } else { closest_move.x }, - if closest_move.y.abs() > SNAP_TOLERANCE { 0. } else { closest_move.y }, - ) + Self::update_overlays(&mut self.overlay_paths, responses, viewport_bounds, (positions, distances), clamped_closest_distance); + + clamped_closest_distance } else { DVec2::ZERO } @@ -67,30 +153,26 @@ impl SnapHandler { } /// Handles snapping of a viewport position, returning another viewport position. - pub fn snap_position(&self, document_message_handler: &DocumentMessageHandler, position_viewport: DVec2) -> DVec2 { + pub fn snap_position(&mut self, responses: &mut VecDeque, viewport_bounds: DVec2, document_message_handler: &DocumentMessageHandler, position_viewport: DVec2) -> DVec2 { if document_message_handler.snapping_enabled { if let Some((targets_x, targets_y)) = &self.snap_targets { - // For each list of snap targets, find the shortest distance to move the point to that target. - let closest_move = DVec2::new( - targets_x - .iter() - .map(|x| (x - position_viewport.x)) - .min_by(|a, b| a.abs().partial_cmp(&b.abs()).expect("Could not compare document bounds.")) - .unwrap_or(0.), - targets_y - .iter() - .map(|y| (y - position_viewport.y)) - .min_by(|a, b| a.abs().partial_cmp(&b.abs()).expect("Could not compare document bounds.")) - .unwrap_or(0.), - ); + let positions = targets_x.iter().map(|&x| (x, x - position_viewport.x)); + let distances = targets_y.iter().map(|&y| (y, y - position_viewport.y)); + + let min_positions = positions.clone().min_by(|a, b| a.1.abs().partial_cmp(&b.1.abs()).expect("Could not compare position.")); + let min_distances = distances.clone().min_by(|a, b| a.1.abs().partial_cmp(&b.1.abs()).expect("Could not compare position.")); + + let closest_distance = DVec2::new(min_positions.map_or(0., |(_pos, dist)| dist), min_distances.map_or(0., |(_pos, dist)| dist)); // Do not move if over snap tolerance - let clamped_closest_move = DVec2::new( - if closest_move.x.abs() > SNAP_TOLERANCE { 0. } else { closest_move.x }, - if closest_move.y.abs() > SNAP_TOLERANCE { 0. } else { closest_move.y }, + let clamped_closest_distance = DVec2::new( + if closest_distance.x.abs() > SNAP_TOLERANCE { 0. } else { closest_distance.x }, + if closest_distance.y.abs() > SNAP_TOLERANCE { 0. } else { closest_distance.y }, ); - position_viewport + clamped_closest_move + Self::update_overlays(&mut self.overlay_paths, responses, viewport_bounds, (positions, distances), clamped_closest_distance); + + position_viewport + clamped_closest_distance } else { position_viewport } @@ -99,8 +181,9 @@ impl SnapHandler { } } - /// Removes snap target data. Call this when snapping is done. - pub fn cleanup(&mut self) { + /// Removes snap target data and overlays. Call this when snapping is done. + pub fn cleanup(&mut self, responses: &mut VecDeque) { + Self::remove_unused_overlays(&mut self.overlay_paths, responses, 0); self.snap_targets = None; } } diff --git a/editor/src/viewport_tools/tool.rs b/editor/src/viewport_tools/tool.rs index f575287271..715138838a 100644 --- a/editor/src/viewport_tools/tool.rs +++ b/editor/src/viewport_tools/tool.rs @@ -16,6 +16,7 @@ pub type ToolActionHandlerData<'a> = (&'a DocumentMessageHandler, &'a DocumentTo pub trait Fsm { type ToolData; + #[must_use] fn transition( self, message: ToolMessage, diff --git a/editor/src/viewport_tools/tools/ellipse.rs b/editor/src/viewport_tools/tools/ellipse.rs index 032f8861d0..a2f84cd145 100644 --- a/editor/src/viewport_tools/tools/ellipse.rs +++ b/editor/src/viewport_tools/tools/ellipse.rs @@ -104,7 +104,7 @@ impl Fsm for EllipseToolFsmState { if let ToolMessage::Ellipse(event) = event { match (self, event) { (Ready, DragStart) => { - shape_data.start(document, input.mouse.position); + shape_data.start(responses, input.viewport_bounds.size(), document, input.mouse.position); responses.push_back(DocumentMessage::StartTransaction.into()); shape_data.path = Some(vec![generate_uuid()]); responses.push_back(DocumentMessage::DeselectAllLayers.into()); @@ -122,7 +122,7 @@ impl Fsm for EllipseToolFsmState { Drawing } (state, Resize { center, lock_ratio }) => { - if let Some(message) = shape_data.calculate_transform(document, center, lock_ratio, input) { + if let Some(message) = shape_data.calculate_transform(responses, input.viewport_bounds.size(), document, center, lock_ratio, input) { responses.push_back(message); } @@ -135,12 +135,12 @@ impl Fsm for EllipseToolFsmState { false => responses.push_back(DocumentMessage::CommitTransaction.into()), } - shape_data.cleanup(); + shape_data.cleanup(responses); Ready } (Drawing, Abort) => { responses.push_back(DocumentMessage::AbortTransaction.into()); - shape_data.cleanup(); + shape_data.cleanup(responses); Ready } diff --git a/editor/src/viewport_tools/tools/line.rs b/editor/src/viewport_tools/tools/line.rs index 9348fd407f..7f4f9adb5c 100644 --- a/editor/src/viewport_tools/tools/line.rs +++ b/editor/src/viewport_tools/tools/line.rs @@ -111,8 +111,8 @@ impl Fsm for LineToolFsmState { if let ToolMessage::Line(event) = event { match (self, event) { (Ready, DragStart) => { - data.snap_handler.start_snap(document, document.all_layers_sorted(), &[]); - data.drag_start = data.snap_handler.snap_position(document, input.mouse.position); + data.snap_handler.start_snap(document, document.visible_layers()); + data.drag_start = data.snap_handler.snap_position(responses, input.viewport_bounds.size(), document, input.mouse.position); responses.push_back(DocumentMessage::StartTransaction.into()); data.path = Some(vec![generate_uuid()]); @@ -136,7 +136,7 @@ impl Fsm for LineToolFsmState { Drawing } (Drawing, Redraw { center, snap_angle, lock_angle }) => { - data.drag_current = data.snap_handler.snap_position(document, input.mouse.position); + data.drag_current = data.snap_handler.snap_position(responses, input.viewport_bounds.size(), document, input.mouse.position); let values: Vec<_> = [lock_angle, snap_angle, center].iter().map(|k| input.keyboard.get(*k as usize)).collect(); responses.push_back(generate_transform(data, values[0], values[1], values[2])); @@ -144,8 +144,8 @@ impl Fsm for LineToolFsmState { Drawing } (Drawing, DragStop) => { - data.drag_current = data.snap_handler.snap_position(document, input.mouse.position); - data.snap_handler.cleanup(); + data.drag_current = data.snap_handler.snap_position(responses, input.viewport_bounds.size(), document, input.mouse.position); + data.snap_handler.cleanup(responses); // TODO: introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100) match data.drag_start == input.mouse.position { @@ -158,7 +158,7 @@ impl Fsm for LineToolFsmState { Ready } (Drawing, Abort) => { - data.snap_handler.cleanup(); + data.snap_handler.cleanup(responses); responses.push_back(DocumentMessage::AbortTransaction.into()); data.path = None; Ready diff --git a/editor/src/viewport_tools/tools/pen.rs b/editor/src/viewport_tools/tools/pen.rs index 3116fba87f..08d272e614 100644 --- a/editor/src/viewport_tools/tools/pen.rs +++ b/editor/src/viewport_tools/tools/pen.rs @@ -113,8 +113,8 @@ impl Fsm for PenToolFsmState { data.path = Some(vec![generate_uuid()]); data.layer_exists = false; - data.snap_handler.start_snap(document, document.all_layers_sorted(), &[]); - let snapped_position = data.snap_handler.snap_position(document, input.mouse.position); + data.snap_handler.start_snap(document, document.visible_layers()); + let snapped_position = data.snap_handler.snap_position(responses, input.viewport_bounds.size(), document, input.mouse.position); let pos = transform.inverse() * DAffine2::from_translation(snapped_position); @@ -131,7 +131,7 @@ impl Fsm for PenToolFsmState { Drawing } (Drawing, DragStop) => { - let snapped_position = data.snap_handler.snap_position(document, input.mouse.position); + let snapped_position = data.snap_handler.snap_position(responses, input.viewport_bounds.size(), document, input.mouse.position); let pos = transform.inverse() * DAffine2::from_translation(snapped_position); // TODO: introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100) @@ -146,7 +146,7 @@ impl Fsm for PenToolFsmState { Drawing } (Drawing, PointerMove) => { - let snapped_position = data.snap_handler.snap_position(document, input.mouse.position); + let snapped_position = data.snap_handler.snap_position(responses, input.viewport_bounds.size(), document, input.mouse.position); let pos = transform.inverse() * DAffine2::from_translation(snapped_position); data.next_point = pos; @@ -167,7 +167,7 @@ impl Fsm for PenToolFsmState { data.path = None; data.points.clear(); - data.snap_handler.cleanup(); + data.snap_handler.cleanup(responses); Ready } diff --git a/editor/src/viewport_tools/tools/rectangle.rs b/editor/src/viewport_tools/tools/rectangle.rs index 78907158c3..c67f250ff3 100644 --- a/editor/src/viewport_tools/tools/rectangle.rs +++ b/editor/src/viewport_tools/tools/rectangle.rs @@ -103,7 +103,7 @@ impl Fsm for RectangleToolFsmState { if let ToolMessage::Rectangle(event) = event { match (self, event) { (Ready, DragStart) => { - shape_data.start(document, input.mouse.position); + shape_data.start(responses, input.viewport_bounds.size(), document, input.mouse.position); responses.push_back(DocumentMessage::StartTransaction.into()); shape_data.path = Some(vec![generate_uuid()]); responses.push_back(DocumentMessage::DeselectAllLayers.into()); @@ -121,7 +121,7 @@ impl Fsm for RectangleToolFsmState { Drawing } (state, Resize { center, lock_ratio }) => { - if let Some(message) = shape_data.calculate_transform(document, center, lock_ratio, input) { + if let Some(message) = shape_data.calculate_transform(responses, input.viewport_bounds.size(), document, center, lock_ratio, input) { responses.push_back(message); } @@ -134,13 +134,14 @@ impl Fsm for RectangleToolFsmState { false => responses.push_back(DocumentMessage::CommitTransaction.into()), } - shape_data.cleanup(); + shape_data.cleanup(responses); Ready } (Drawing, Abort) => { responses.push_back(DocumentMessage::AbortTransaction.into()); - shape_data.cleanup(); + + shape_data.cleanup(responses); Ready } diff --git a/editor/src/viewport_tools/tools/resize.rs b/editor/src/viewport_tools/tools/resize.rs index abe6057986..5ee7e3e0b3 100644 --- a/editor/src/viewport_tools/tools/resize.rs +++ b/editor/src/viewport_tools/tools/resize.rs @@ -17,9 +17,9 @@ pub struct Resize { } impl Resize { /// Starts a resize, assigning the snap targets and snapping the starting position. - pub fn start(&mut self, document: &DocumentMessageHandler, mouse_position: DVec2) { + pub fn start(&mut self, responses: &mut VecDeque, viewport_bounds: DVec2, document: &DocumentMessageHandler, mouse_position: DVec2) { let layers = document.all_layers_sorted(); - self.snap_handler.start_snap(document, layers, &[]); + self.snap_handler.start_snap(responses, viewport_bounds, document, layers); self.drag_start = self.snap_handler.snap_position(document, mouse_position); } @@ -50,8 +50,8 @@ impl Resize { } } - pub fn cleanup(&mut self) { - self.snap_handler.cleanup(); + pub fn cleanup(&mut self, responses: &mut VecDeque) { + self.snap_handler.cleanup(responses); self.path = None; } } diff --git a/editor/src/viewport_tools/tools/select.rs b/editor/src/viewport_tools/tools/select.rs index dd281564bc..8d3dff87ca 100644 --- a/editor/src/viewport_tools/tools/select.rs +++ b/editor/src/viewport_tools/tools/select.rs @@ -135,7 +135,7 @@ fn add_bounding_box(responses: &mut Vec) -> Vec { } fn transform_from_box(pos1: DVec2, pos2: DVec2) -> [f64; 6] { - DAffine2::from_scale_angle_translation(pos2 - pos1, 0., pos1).to_cols_array() + DAffine2::from_scale_angle_translation((pos2 - pos1).round(), 0., pos1.round() - DVec2::splat(0.5)).to_cols_array() } impl Fsm for SelectToolFsmState { @@ -164,9 +164,6 @@ impl Fsm for SelectToolFsmState { data.bounding_box_overlay_layer = Some(path.clone()); - let half_pixel_offset = DVec2::splat(0.5); - let pos1 = pos1 + half_pixel_offset; - let pos2 = pos2 - half_pixel_offset; let transform = transform_from_box(pos1, pos2); DocumentMessage::Overlays(Operation::SetLayerTransformInViewport { path, transform }.into()).into() } @@ -189,6 +186,10 @@ impl Fsm for SelectToolFsmState { let state = 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))); + Dragging } else { if !input.keyboard.get(add_to_selection as usize) { @@ -201,6 +202,9 @@ impl Fsm for SelectToolFsmState { buffer.push(DocumentMessage::AddSelectedLayers { additional_layers: selected.clone() }.into()); 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))); + Dragging } else { data.drag_box_overlay_layer = Some(add_bounding_box(&mut buffer)); @@ -209,13 +213,6 @@ impl Fsm for SelectToolFsmState { }; buffer.into_iter().rev().for_each(|message| responses.push_front(message)); - // TODO: Probably delete this now that the overlays system has moved to a separate Graphene document? (@0hypercube) - let ignore_layers = if let Some(bounding_box) = &data.bounding_box_overlay_layer { - vec![bounding_box.clone()] - } else { - Vec::new() - }; - data.snap_handler.start_snap(document, document.non_selected_layers_sorted(), &ignore_layers); state } (Dragging, MouseMove { snap_angle }) => { @@ -234,7 +231,7 @@ impl Fsm for SelectToolFsmState { let mouse_delta = mouse_position - data.drag_current; - let closest_move = data.snap_handler.snap_layers(document, &data.layers_dragging, mouse_delta); + let closest_move = data.snap_handler.snap_layers(responses, document, &data.layers_dragging, input.viewport_bounds.size(), mouse_delta); // 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 path in Document::shallowest_unique_layers(data.layers_dragging.iter()) { responses.push_front( @@ -250,15 +247,12 @@ impl Fsm for SelectToolFsmState { } (DrawingBox, MouseMove { .. }) => { data.drag_current = input.mouse.position; - let half_pixel_offset = DVec2::splat(0.5); - let start = data.drag_start + half_pixel_offset; - let size = data.drag_current - start + half_pixel_offset; responses.push_front( DocumentMessage::Overlays( Operation::SetLayerTransformInViewport { path: data.drag_box_overlay_layer.clone().unwrap(), - transform: DAffine2::from_scale_angle_translation(size, 0., start).to_cols_array(), + transform: transform_from_box(data.drag_start, data.drag_current), } .into(), ) @@ -271,7 +265,7 @@ impl Fsm for SelectToolFsmState { true => DocumentMessage::Undo, false => DocumentMessage::CommitTransaction, }; - data.snap_handler.cleanup(); + data.snap_handler.cleanup(responses); responses.push_front(response.into()); Ready } @@ -298,6 +292,7 @@ impl Fsm for SelectToolFsmState { 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); + data.snap_handler.cleanup(responses); Ready } (_, Align { axis, aggregate }) => { diff --git a/editor/src/viewport_tools/tools/shape.rs b/editor/src/viewport_tools/tools/shape.rs index 833015d6ad..1da0331ba2 100644 --- a/editor/src/viewport_tools/tools/shape.rs +++ b/editor/src/viewport_tools/tools/shape.rs @@ -105,7 +105,7 @@ impl Fsm for ShapeToolFsmState { if let ToolMessage::Shape(event) = event { match (self, event) { (Ready, DragStart) => { - shape_data.start(document, input.mouse.position); + shape_data.start(responses, input.viewport_bounds.size(), document, input.mouse.position); responses.push_back(DocumentMessage::StartTransaction.into()); shape_data.path = Some(vec![generate_uuid()]); responses.push_back(DocumentMessage::DeselectAllLayers.into()); @@ -130,7 +130,7 @@ impl Fsm for ShapeToolFsmState { Drawing } (state, Resize { center, lock_ratio }) => { - if let Some(message) = shape_data.calculate_transform(document, center, lock_ratio, input) { + if let Some(message) = shape_data.calculate_transform(responses, input.viewport_bounds.size(), document, center, lock_ratio, input) { responses.push_back(message); } @@ -143,12 +143,14 @@ impl Fsm for ShapeToolFsmState { false => responses.push_back(DocumentMessage::CommitTransaction.into()), } - shape_data.cleanup(); + shape_data.cleanup(responses); + Ready } (Drawing, Abort) => { responses.push_back(DocumentMessage::AbortTransaction.into()); - shape_data.cleanup(); + + shape_data.cleanup(responses); Ready } diff --git a/editor/src/viewport_tools/tools/shared/resize.rs b/editor/src/viewport_tools/tools/shared/resize.rs index abe6057986..3026cdba96 100644 --- a/editor/src/viewport_tools/tools/shared/resize.rs +++ b/editor/src/viewport_tools/tools/shared/resize.rs @@ -17,17 +17,24 @@ pub struct Resize { } impl Resize { /// Starts a resize, assigning the snap targets and snapping the starting position. - pub fn start(&mut self, document: &DocumentMessageHandler, mouse_position: DVec2) { - let layers = document.all_layers_sorted(); - self.snap_handler.start_snap(document, layers, &[]); - self.drag_start = self.snap_handler.snap_position(document, mouse_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.drag_start = self.snap_handler.snap_position(responses, viewport_bounds, document, mouse_position); } - pub fn calculate_transform(&self, document: &DocumentMessageHandler, center: Key, lock_ratio: Key, ipp: &InputPreprocessorMessageHandler) -> Option { + pub fn calculate_transform( + &mut self, + responses: &mut VecDeque, + viewport_bounds: DVec2, + document: &DocumentMessageHandler, + center: Key, + lock_ratio: Key, + ipp: &InputPreprocessorMessageHandler, + ) -> Option { if let Some(path) = &self.path { let mut start = self.drag_start; - let stop = self.snap_handler.snap_position(document, ipp.mouse.position); + let stop = self.snap_handler.snap_position(responses, viewport_bounds, document, ipp.mouse.position); let mut size = stop - start; if ipp.keyboard.get(lock_ratio as usize) { @@ -50,8 +57,8 @@ impl Resize { } } - pub fn cleanup(&mut self) { - self.snap_handler.cleanup(); + pub fn cleanup(&mut self, responses: &mut VecDeque) { + self.snap_handler.cleanup(responses); self.path = None; } }