diff --git a/editor/src/consts.rs b/editor/src/consts.rs index c0d693fdf..97f54cb0a 100644 --- a/editor/src/consts.rs +++ b/editor/src/consts.rs @@ -15,9 +15,17 @@ 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; +// Snapping axis +pub const SNAP_AXIS_TOLERANCE: f64 = 3.; +pub const SNAP_AXIS_OVERLAY_FADE_DISTANCE: f64 = 15.; +pub const SNAP_AXIS_UNSNAPPED_OPACITY: f64 = 0.4; + +// Snapping point +pub const SNAP_POINT_OVERLAY_FADE_NEAR: f64 = 20.; +pub const SNAP_POINT_OVERLAY_FADE_FAR: f64 = 40.; +pub const SNAP_POINT_UNSNAPPED_OPACITY: f64 = 0.4; +pub const SNAP_POINT_TOLERANCE: f64 = 5.; +pub const SNAP_POINT_SIZE: f64 = 5.; pub const DRAG_THRESHOLD: f64 = 1.; diff --git a/editor/src/viewport_tools/snapping.rs b/editor/src/viewport_tools/snapping.rs index bf3d5fcc6..f47842995 100644 --- a/editor/src/viewport_tools/snapping.rs +++ b/editor/src/viewport_tools/snapping.rs @@ -1,89 +1,165 @@ -use crate::consts::{COLOR_ACCENT, SNAP_OVERLAY_FADE_DISTANCE, SNAP_OVERLAY_UNSNAPPED_OPACITY, SNAP_TOLERANCE}; +use crate::consts::{ + COLOR_ACCENT, SNAP_AXIS_OVERLAY_FADE_DISTANCE, SNAP_AXIS_TOLERANCE, SNAP_AXIS_UNSNAPPED_OPACITY, SNAP_POINT_OVERLAY_FADE_FAR, SNAP_POINT_OVERLAY_FADE_NEAR, SNAP_POINT_SIZE, SNAP_POINT_TOLERANCE, + SNAP_POINT_UNSNAPPED_OPACITY, +}; use crate::document::DocumentMessageHandler; use crate::message_prelude::*; +use graphene::layers::layer_info::{Layer, LayerDataType}; use graphene::layers::style::{self, Stroke}; use graphene::{LayerId, Operation}; use glam::{DAffine2, DVec2}; use std::f64::consts::PI; +// Handles snap overlays #[derive(Debug, Clone, Default)] -pub struct SnapHandler { - snap_targets: Option<(Vec, Vec)>, - overlay_paths: Vec>, +struct SnapOverlays { + axis_overlay_paths: Vec>, + point_overlay_paths: Vec>, + axis_index: usize, + point_index: usize, } -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( +/// Handles snapping and snap overlays +#[derive(Debug, Clone, Default)] +pub struct SnapHandler { + point_targets: Option>, + bound_targets: Option>, + snap_overlays: SnapOverlays, + snap_x: bool, + snap_y: bool, +} + +/// Converts a bounding box into a set of points for snapping +/// +/// Puts a point in the middle of each edge (top, bottom, left, right) +pub fn expand_bounds([bound1, bound2]: [DVec2; 2]) -> [DVec2; 4] { + [ + DVec2::new((bound1.x + bound2.x) / 2., bound1.y), + DVec2::new((bound1.x + bound2.x) / 2., bound2.y), + DVec2::new(bound1.x, (bound1.y + bound2.y) / 2.), + DVec2::new(bound2.x, (bound1.y + bound2.y) / 2.), + ] +} + +impl SnapOverlays { + /// Draws an overlay (axis or point) with the correct transform and fade opacity, reusing lines from the pool if available. + fn add_overlay(is_axis: bool, responses: &mut VecDeque, transform: [f64; 6], opacity: Option, 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( + if is_axis { Operation::AddOverlayLine { path: layer_path.clone(), transform, style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 1.0)), style::Fill::None), } - .into(), - ) + } else { + Operation::AddOverlayEllipse { + path: layer_path.clone(), + transform, + style: style::PathStyle::new(None, style::Fill::Solid(COLOR_ACCENT)), + } + } .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 - }; + ) + .into(), + ); + overlay_paths.push(layer_path.clone()); + layer_path + } + // Otherwise, reuse an overlay 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 + // Then set its opacity to the fade amount + if let Some(opacity) = opacity { 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 alignment lines for an axis + /// Note: horizontal refers to the overlay line being horizontal and the snap being along the Y axis + fn draw_alignment_lines(&mut self, is_horizontal: bool, distances: impl Iterator, responses: &mut VecDeque, closest_distance: DVec2) { + for (target, goal, distance) in distances.filter(|(_target, _pos, dist)| dist.abs() < SNAP_AXIS_OVERLAY_FADE_DISTANCE) { + let offset = if is_horizontal { target.y } else { target.x }.round() - 0.5; + let offset_other = if is_horizontal { target.x } else { target.y }.round() - 0.5; + let goal_axis = if is_horizontal { goal.x } else { goal.y }.round() - 0.5; - // 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 scale = DVec2::new(offset_other - goal_axis, 1.); + let angle = if is_horizontal { 0. } else { PI / 2. }; + let translation = if is_horizontal { DVec2::new(goal_axis, offset) } else { DVec2::new(offset, goal_axis) }; - let opacity = if closest_distance.x == distance { + let transform = DAffine2::from_scale_angle_translation(scale, angle, translation).to_cols_array(); + let closest = if is_horizontal { closest_distance.y } else { closest_distance.x }; + + let opacity = if (closest - distance).abs() < 1. { 1. } else { - SNAP_OVERLAY_UNSNAPPED_OPACITY - distance.abs() / (SNAP_OVERLAY_FADE_DISTANCE / SNAP_OVERLAY_UNSNAPPED_OPACITY) + SNAP_AXIS_UNSNAPPED_OPACITY - distance.abs() / (SNAP_AXIS_OVERLAY_FADE_DISTANCE / SNAP_AXIS_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(); + // Add line + Self::add_overlay(true, responses, transform, Some(opacity), self.axis_index, &mut self.axis_overlay_paths); + self.axis_index += 1; - let opacity = if closest_distance.y == distance { - 1. - } else { - SNAP_OVERLAY_UNSNAPPED_OPACITY - distance.abs() / (SNAP_OVERLAY_FADE_DISTANCE / SNAP_OVERLAY_UNSNAPPED_OPACITY) - }; + let size = DVec2::splat(SNAP_POINT_SIZE); - add_overlay_line(responses, transform, opacity, index, overlay_paths); - index += 1; + // Add point at target + let transform = DAffine2::from_scale_angle_translation(size, 0., target - size / 2.).to_cols_array(); + Self::add_overlay(false, responses, transform, Some(opacity), self.point_index, &mut self.point_overlay_paths); + self.point_index += 1; + + // Add point along line but towards goal + let translation = if is_horizontal { DVec2::new(goal.x, target.y) } else { DVec2::new(target.x, goal.y) }; + let transform = DAffine2::from_scale_angle_translation(size, 0., translation - size / 2.).to_cols_array(); + Self::add_overlay(false, responses, transform, Some(opacity), self.point_index, &mut self.point_overlay_paths); + self.point_index += 1 } - Self::remove_unused_overlays(overlay_paths, responses, index); + } + + /// Draw the snap points + fn draw_snap_points(&mut self, distances: impl Iterator, responses: &mut VecDeque, closest_distance: DVec2) { + for (target, offset, distance) in distances.filter(|(_pos, _offset, dist)| dist.abs() < SNAP_POINT_OVERLAY_FADE_FAR) { + let active = (closest_distance - offset).length_squared() < 1.; + + if active { + continue; + } + + let opacity = (1. - (distance - SNAP_POINT_OVERLAY_FADE_NEAR) / (SNAP_POINT_OVERLAY_FADE_FAR - SNAP_POINT_OVERLAY_FADE_NEAR)).min(1.) / SNAP_POINT_UNSNAPPED_OPACITY; + + let size = DVec2::splat(SNAP_POINT_SIZE); + let transform = DAffine2::from_scale_angle_translation(size, 0., target - size / 2.).to_cols_array(); + Self::add_overlay(false, responses, transform, Some(opacity), self.point_index, &mut self.point_overlay_paths); + self.point_index += 1 + } + } + + /// Updates the snapping overlays with the specified distances. + /// `positions_and_distances` is a tuple of `x`, `y` & `point` iterators,, each with `(position, goal, distance)` values. + fn update_overlays(&mut self, responses: &mut VecDeque, positions_and_distances: (X, Y, P), closest_distance: DVec2) + where + X: Iterator, + Y: Iterator, + P: Iterator, + { + self.axis_index = 0; + self.point_index = 0; + + let (x, y, points) = positions_and_distances; + self.draw_alignment_lines(true, y, responses, closest_distance); + self.draw_alignment_lines(false, x, responses, closest_distance); + self.draw_snap_points(points, responses, closest_distance); + + Self::remove_unused_overlays(&mut self.axis_overlay_paths, responses, self.axis_index); + Self::remove_unused_overlays(&mut self.point_overlay_paths, responses, self.point_index); } /// Remove overlays from the pool beyond a given index. Pool entries up through that index will be kept. @@ -93,91 +169,138 @@ impl SnapHandler { } } + /// Deletes all overlays + fn cleanup(&mut self, responses: &mut VecDeque) { + Self::remove_unused_overlays(&mut self.axis_overlay_paths, responses, 0); + Self::remove_unused_overlays(&mut self.point_overlay_paths, responses, 0); + } +} + +impl SnapHandler { + /// Computes the necessary translation to the layer to snap it (as well as updating necessary overlays) + fn calculate_snap(&mut self, targets: R, responses: &mut VecDeque) -> DVec2 + where + R: Iterator + Clone, + { + let empty = Vec::new(); + let snap_points = self.snap_x && self.snap_y; + + let axis = self.bound_targets.as_ref().unwrap_or(&empty); + let points = if snap_points { self.point_targets.as_ref().unwrap_or(&empty) } else { &empty }; + + let x_axis = if self.snap_x { axis } else { &empty } + .iter() + .flat_map(|&pos| targets.clone().map(move |goal| (pos, goal, (pos - goal).x))); + let y_axis = if self.snap_y { axis } else { &empty } + .iter() + .flat_map(|&pos| targets.clone().map(move |goal| (pos, goal, (pos - goal).y))); + let points = points.iter().flat_map(|&pos| targets.clone().map(move |goal| (pos, pos - goal, (pos - goal).length()))); + + let min_x = x_axis.clone().min_by(|a, b| a.2.abs().partial_cmp(&b.2.abs()).expect("Could not compare position.")); + let min_y = y_axis.clone().min_by(|a, b| a.2.abs().partial_cmp(&b.2.abs()).expect("Could not compare position.")); + let min_points = points.clone().min_by(|a, b| a.2.abs().partial_cmp(&b.2.abs()).expect("Could not compare position.")); + + // Snap to a point if possible + let clamped_closest_distance = if let Some(min_points) = min_points.filter(|&(_, _, dist)| dist <= SNAP_POINT_TOLERANCE) { + min_points.1 + } else { + // Do not move if over snap tolerance + let closest_distance = DVec2::new(min_x.unwrap_or_default().2, min_y.unwrap_or_default().2); + DVec2::new( + if closest_distance.x.abs() > SNAP_AXIS_TOLERANCE { 0. } else { closest_distance.x }, + if closest_distance.y.abs() > SNAP_AXIS_TOLERANCE { 0. } else { closest_distance.y }, + ) + }; + + self.snap_overlays.update_overlays(responses, (x_axis, y_axis, points), clamped_closest_distance); + + clamped_closest_distance + } + /// 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(&mut self, document_message_handler: &DocumentMessageHandler, bounding_boxes: impl Iterator, snap_x: bool, snap_y: bool) { if document_message_handler.snapping_enabled { - let (x_targets, y_targets) = bounding_boxes.flat_map(|[bound1, bound2]| [bound1, bound2, ((bound1 + bound2) / 2.)]).map(|vec| vec.into()).unzip(); + self.snap_x = snap_x; + self.snap_y = snap_y; // Could be made into sorted Vec or a HashSet for more performant lookups. - self.snap_targets = Some((if snap_x { x_targets } else { Vec::new() }, if snap_y { y_targets } else { Vec::new() })); + self.bound_targets = Some(bounding_boxes.flat_map(expand_bounds).collect()); + self.point_targets = None; } } /// Add arbitrary snapping points + /// /// This should be called after start_snap - pub fn add_snap_points(&mut self, document_message_handler: &DocumentMessageHandler, snap_points: Vec) { + pub fn add_snap_points(&mut self, document_message_handler: &DocumentMessageHandler, snap_points: impl Iterator) { if document_message_handler.snapping_enabled { - let (mut x_targets, mut y_targets): (Vec, Vec) = snap_points.into_iter().map(|vec| vec.into()).unzip(); - if let Some((new_x_targets, new_y_targets)) = &mut self.snap_targets { - x_targets.append(new_x_targets); - y_targets.append(new_y_targets); - self.snap_targets = Some((x_targets, y_targets)); + if let Some(targets) = &mut self.point_targets { + targets.extend(snap_points); + } else { + self.point_targets = Some(snap_points.collect()); + } + } + } + + /// Add the control points (optionally including bézier handles) of the specified shape layer to the snapping points + /// + /// This should be called after start_snap + pub fn add_snap_path(&mut self, document_message_handler: &DocumentMessageHandler, layer: &Layer, path: &[LayerId], include_handles: bool) { + if let LayerDataType::Shape(s) = &layer.data { + let transform = document_message_handler.graphene_document.multiply_transforms(path).unwrap(); + let snap_points = s + .path + .iter() + .flat_map(|shape| { + if include_handles { + match shape { + kurbo::PathEl::MoveTo(point) => vec![point], + kurbo::PathEl::LineTo(point) => vec![point], + kurbo::PathEl::QuadTo(handle1, point) => vec![handle1, point], + kurbo::PathEl::CurveTo(handle1, handle2, point) => vec![handle1, handle2, point], + kurbo::PathEl::ClosePath => vec![], + } + } else { + match shape { + kurbo::PathEl::MoveTo(point) => vec![point], + kurbo::PathEl::LineTo(point) => vec![point], + kurbo::PathEl::QuadTo(_, point) => vec![point], + kurbo::PathEl::CurveTo(_, _, point) => vec![point], + kurbo::PathEl::ClosePath => vec![], + } + } + }) + .map(|point| DVec2::new(point.x, point.y)) + .map(|pos| transform.transform_point2(pos)); + self.add_snap_points(document_message_handler, snap_points); + } + } + + /// Adds all of the shape handles in the document, including bézier handles of the points specified + pub fn add_all_document_handles(&mut self, document_message_handler: &DocumentMessageHandler, include_handles: &[&[LayerId]], exclude: &[&[LayerId]]) { + for path in document_message_handler.all_layers() { + if !exclude.contains(&path) { + let layer = document_message_handler.graphene_document.layer(path).expect("Could not get layer for snapping"); + self.add_snap_path(document_message_handler, layer, path, include_handles.contains(&path)); } } } /// 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( - &mut self, - responses: &mut VecDeque, - document_message_handler: &DocumentMessageHandler, - (snap_x, snap_y): (Vec, Vec), - viewport_bounds: DVec2, - mouse_delta: DVec2, - ) -> DVec2 { + pub fn snap_layers(&mut self, responses: &mut VecDeque, document_message_handler: &DocumentMessageHandler, snap_anchors: Vec, mouse_delta: DVec2) -> DVec2 { if document_message_handler.snapping_enabled { - if let Some((targets_x, targets_y)) = &self.snap_targets { - 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 }, - ); - - Self::update_overlays(&mut self.overlay_paths, responses, viewport_bounds, (positions, distances), clamped_closest_distance); - - clamped_closest_distance - } else { - DVec2::ZERO - } + self.calculate_snap(snap_anchors.iter().map(move |&snap| mouse_delta + snap), responses) } else { DVec2::ZERO } } /// Handles snapping of a viewport position, returning another viewport position. - pub fn snap_position(&mut self, responses: &mut VecDeque, viewport_bounds: DVec2, document_message_handler: &DocumentMessageHandler, position_viewport: DVec2) -> DVec2 { + pub fn snap_position(&mut self, responses: &mut VecDeque, document_message_handler: &DocumentMessageHandler, position_viewport: DVec2) -> DVec2 { if document_message_handler.snapping_enabled { - if let Some((targets_x, targets_y)) = &self.snap_targets { - 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_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 }, - ); - - Self::update_overlays(&mut self.overlay_paths, responses, viewport_bounds, (positions, distances), clamped_closest_distance); - - position_viewport + clamped_closest_distance - } else { - position_viewport - } + self.calculate_snap([position_viewport].into_iter(), responses) + position_viewport } else { position_viewport } @@ -185,7 +308,8 @@ impl SnapHandler { /// 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; + self.snap_overlays.cleanup(responses); + self.bound_targets = None; + self.point_targets = None; } } diff --git a/editor/src/viewport_tools/tools/artboard_tool.rs b/editor/src/viewport_tools/tools/artboard_tool.rs index 7ee1057e7..408cfcd42 100644 --- a/editor/src/viewport_tools/tools/artboard_tool.rs +++ b/editor/src/viewport_tools/tools/artboard_tool.rs @@ -163,6 +163,7 @@ impl Fsm for ArtboardToolFsmState { data.snap_handler .start_snap(document, document.bounding_boxes(None, Some(data.selected_board.unwrap())), snap_x, snap_y); + data.snap_handler.add_all_document_handles(document, &[], &[]); ArtboardToolFsmState::ResizingBounds } else { @@ -175,6 +176,7 @@ impl Fsm for ArtboardToolFsmState { data.selected_board = Some(intersection[0]); data.snap_handler.start_snap(document, document.bounding_boxes(None, Some(intersection[0])), true, true); + data.snap_handler.add_all_document_handles(document, &[], &[]); responses.push_back( PropertiesPanelMessage::SetActiveLayers { @@ -190,6 +192,7 @@ impl Fsm for ArtboardToolFsmState { data.selected_board = Some(id); data.snap_handler.start_snap(document, document.bounding_boxes(None, Some(id)), true, true); + data.snap_handler.add_all_document_handles(document, &[], &[]); responses.push_back( ArtboardMessage::AddArtboard { @@ -213,7 +216,7 @@ impl Fsm for ArtboardToolFsmState { let constrain_square = input.keyboard.get(constrain_axis_or_aspect as usize); let mouse_position = input.mouse.position; - let snapped_mouse_position = data.snap_handler.snap_position(responses, input.viewport_bounds.size(), document, mouse_position); + let snapped_mouse_position = data.snap_handler.snap_position(responses, document, mouse_position); let [position, size] = movement.new_size(snapped_mouse_position, bounds.transform, from_center, constrain_square); let position = movement.center_position(position, size, from_center); @@ -239,8 +242,8 @@ impl Fsm for ArtboardToolFsmState { let mouse_position = axis_align_drag(axis_align, input.mouse.position, data.drag_start); let mouse_delta = mouse_position - data.drag_current; - let snap = bounds.evaluate_transform_handle_positions().iter().map(|v| (v.x, v.y)).unzip(); - let closest_move = data.snap_handler.snap_layers(responses, document, snap, input.viewport_bounds.size(), mouse_delta); + let snap = bounds.evaluate_transform_handle_positions().into_iter().collect(); + let closest_move = data.snap_handler.snap_layers(responses, document, snap, mouse_delta); let size = bounds.bounds[1] - bounds.bounds[0]; @@ -263,7 +266,7 @@ impl Fsm for ArtboardToolFsmState { } (ArtboardToolFsmState::Drawing, ArtboardToolMessage::PointerMove { constrain_axis_or_aspect, center }) => { let mouse_position = input.mouse.position; - let snapped_mouse_position = data.snap_handler.snap_position(responses, input.viewport_bounds.size(), document, mouse_position); + let snapped_mouse_position = data.snap_handler.snap_position(responses, document, mouse_position); let root_transform = document.graphene_document.root.transform.inverse(); diff --git a/editor/src/viewport_tools/tools/ellipse_tool.rs b/editor/src/viewport_tools/tools/ellipse_tool.rs index f351f6499..07e9922fe 100644 --- a/editor/src/viewport_tools/tools/ellipse_tool.rs +++ b/editor/src/viewport_tools/tools/ellipse_tool.rs @@ -110,7 +110,7 @@ impl Fsm for EllipseToolFsmState { if let ToolMessage::Ellipse(event) = event { match (self, event) { (Ready, DragStart) => { - shape_data.start(responses, input.viewport_bounds.size(), document, input.mouse.position); + shape_data.start(responses, document, input.mouse.position); responses.push_back(DocumentMessage::StartTransaction.into()); shape_data.path = Some(document.get_path_for_new_layer()); responses.push_back(DocumentMessage::DeselectAllLayers.into()); @@ -128,7 +128,7 @@ impl Fsm for EllipseToolFsmState { Drawing } (state, Resize { center, lock_ratio }) => { - if let Some(message) = shape_data.calculate_transform(responses, input.viewport_bounds.size(), document, center, lock_ratio, input) { + if let Some(message) = shape_data.calculate_transform(responses, document, center, lock_ratio, input) { responses.push_back(message); } diff --git a/editor/src/viewport_tools/tools/gradient_tool.rs b/editor/src/viewport_tools/tools/gradient_tool.rs index 6a03a718d..c0a0a25d4 100644 --- a/editor/src/viewport_tools/tools/gradient_tool.rs +++ b/editor/src/viewport_tools/tools/gradient_tool.rs @@ -11,7 +11,7 @@ use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData}; use graphene::color::Color; use graphene::intersection::Quad; -use graphene::layers::layer_info::{Layer, LayerDataType}; +use graphene::layers::layer_info::Layer; use graphene::layers::style::{Fill, Gradient, PathStyle, Stroke}; use graphene::Operation; @@ -241,25 +241,9 @@ struct GradientToolData { snap_handler: SnapHandler, } -pub fn start_snap(snap_handler: &mut SnapHandler, document: &DocumentMessageHandler, layer: &Layer, path: &[LayerId]) { +pub fn start_snap(snap_handler: &mut SnapHandler, document: &DocumentMessageHandler) { snap_handler.start_snap(document, document.bounding_boxes(None, None), true, true); - if let LayerDataType::Shape(s) = &layer.data { - let transform = document.graphene_document.multiply_transforms(path).unwrap(); - let snap_points = s - .path - .iter() - .filter_map(|shape| match shape { - kurbo::PathEl::MoveTo(point) => Some(point), - kurbo::PathEl::LineTo(point) => Some(point), - kurbo::PathEl::QuadTo(_, point) => Some(point), - kurbo::PathEl::CurveTo(_, _, point) => Some(point), - kurbo::PathEl::ClosePath => None, - }) - .map(|point| DVec2::new(point.x, point.y)) - .map(|pos| transform.transform_point2(pos)) - .collect(); - snap_handler.add_snap_points(document, snap_points); - } + snap_handler.add_all_document_handles(document, &[], &[]); } impl Fsm for GradientToolFsmState { @@ -307,7 +291,7 @@ impl Fsm for GradientToolFsmState { for overlay in &data.gradient_overlays { if overlay.evaluate_gradient_start().distance_squared(mouse) < tolerance { dragging = true; - start_snap(&mut data.snap_handler, document, document.graphene_document.layer(&overlay.path).unwrap(), &overlay.path); + start_snap(&mut data.snap_handler, document); data.selected_gradient = Some(SelectedGradient { path: overlay.path.clone(), transform: overlay.transform, @@ -317,7 +301,7 @@ impl Fsm for GradientToolFsmState { } if overlay.evaluate_gradient_end().distance_squared(mouse) < tolerance { dragging = true; - start_snap(&mut data.snap_handler, document, document.graphene_document.layer(&overlay.path).unwrap(), &overlay.path); + start_snap(&mut data.snap_handler, document); data.selected_gradient = Some(SelectedGradient { path: overlay.path.clone(), transform: overlay.transform, @@ -348,7 +332,7 @@ impl Fsm for GradientToolFsmState { data.selected_gradient = Some(selected_gradient); - start_snap(&mut data.snap_handler, document, layer, &intersection); + start_snap(&mut data.snap_handler, document); GradientToolFsmState::Drawing } else { @@ -358,7 +342,7 @@ impl Fsm for GradientToolFsmState { } (GradientToolFsmState::Drawing, GradientToolMessage::PointerMove { constrain_axis }) => { if let Some(selected_gradient) = &mut data.selected_gradient { - let mouse = data.snap_handler.snap_position(responses, input.viewport_bounds.size(), document, input.mouse.position); + let mouse = data.snap_handler.snap_position(responses, document, input.mouse.position); selected_gradient.update_gradient(mouse, responses, input.keyboard.get(constrain_axis as usize)); } GradientToolFsmState::Drawing diff --git a/editor/src/viewport_tools/tools/line_tool.rs b/editor/src/viewport_tools/tools/line_tool.rs index d3384b2cf..4abdcec0c 100644 --- a/editor/src/viewport_tools/tools/line_tool.rs +++ b/editor/src/viewport_tools/tools/line_tool.rs @@ -155,7 +155,8 @@ impl Fsm for LineToolFsmState { match (self, event) { (Ready, DragStart) => { data.snap_handler.start_snap(document, document.bounding_boxes(None, None), true, true); - data.drag_start = data.snap_handler.snap_position(responses, input.viewport_bounds.size(), document, input.mouse.position); + data.snap_handler.add_all_document_handles(document, &[], &[]); + data.drag_start = data.snap_handler.snap_position(responses, document, input.mouse.position); responses.push_back(DocumentMessage::StartTransaction.into()); data.path = Some(document.get_path_for_new_layer()); @@ -176,7 +177,7 @@ impl Fsm for LineToolFsmState { Drawing } (Drawing, Redraw { center, snap_angle, lock_angle }) => { - data.drag_current = data.snap_handler.snap_position(responses, input.viewport_bounds.size(), document, input.mouse.position); + data.drag_current = data.snap_handler.snap_position(responses, 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])); @@ -184,7 +185,7 @@ impl Fsm for LineToolFsmState { Drawing } (Drawing, DragStop) => { - data.drag_current = data.snap_handler.snap_position(responses, input.viewport_bounds.size(), document, input.mouse.position); + data.drag_current = data.snap_handler.snap_position(responses, document, input.mouse.position); data.snap_handler.cleanup(responses); match data.drag_start.distance(input.mouse.position) <= DRAG_THRESHOLD { diff --git a/editor/src/viewport_tools/tools/path_tool.rs b/editor/src/viewport_tools/tools/path_tool.rs index 16c8f3dbe..858e23a72 100644 --- a/editor/src/viewport_tools/tools/path_tool.rs +++ b/editor/src/viewport_tools/tools/path_tool.rs @@ -144,15 +144,13 @@ impl Fsm for PathToolFsmState { // Select the first point within the threshold (in pixels) if data.shape_editor.select_point(input.mouse.position, SELECTION_THRESHOLD, add_to_selection, responses) { responses.push_back(DocumentMessage::StartTransaction.into()); - data.snap_handler.start_snap(document, document.bounding_boxes(None, None), true, true); - let snap_points = data - .shape_editor - .shapes_to_modify - .iter() - .flat_map(|shape| shape.anchors.iter().flat_map(|anchor| anchor.points[0].as_ref())) - .map(|point| point.position) - .collect(); - data.snap_handler.add_snap_points(document, snap_points); + + let ignore_document = data.shape_editor.shapes_to_modify.iter().map(|shape| shape.layer_path.clone()).collect::>(); + data.snap_handler.start_snap(document, document.bounding_boxes(Some(&ignore_document), None), true, true); + + let include_handles = data.shape_editor.shapes_to_modify.iter().map(|shape| shape.layer_path.as_slice()).collect::>(); + data.snap_handler.add_all_document_handles(document, &include_handles, &[]); + data.drag_start_pos = input.mouse.position; Dragging } @@ -209,7 +207,7 @@ impl Fsm for PathToolFsmState { } // Move the selected points by the mouse position - let snapped_position = data.snap_handler.snap_position(responses, input.viewport_bounds.size(), document, input.mouse.position); + let snapped_position = data.snap_handler.snap_position(responses, document, input.mouse.position); data.shape_editor.move_selected_points(snapped_position - data.drag_start_pos, true, responses); Dragging } diff --git a/editor/src/viewport_tools/tools/pen_tool.rs b/editor/src/viewport_tools/tools/pen_tool.rs index 01128f7ba..457e52600 100644 --- a/editor/src/viewport_tools/tools/pen_tool.rs +++ b/editor/src/viewport_tools/tools/pen_tool.rs @@ -169,7 +169,8 @@ impl Fsm for PenToolFsmState { // Create a new layer and prep snap system data.path = Some(document.get_path_for_new_layer()); data.snap_handler.start_snap(document, document.bounding_boxes(None, None), true, true); - let snapped_position = data.snap_handler.snap_position(responses, input.viewport_bounds.size(), document, input.mouse.position); + data.snap_handler.add_all_document_handles(document, &[], &[]); + let snapped_position = data.snap_handler.snap_position(responses, document, input.mouse.position); // Get the position and set properties let start_position = transform.inverse().transform_point2(snapped_position); @@ -217,14 +218,14 @@ impl Fsm for PenToolFsmState { } // Move the newly selected points to the cursor - let snapped_position = data.snap_handler.snap_position(responses, input.viewport_bounds.size(), document, input.mouse.position); + let snapped_position = data.snap_handler.snap_position(responses, document, input.mouse.position); data.shape_editor.move_selected_points(snapped_position, false, responses); Drawing } (Drawing, PointerMove) => { // Move selected points - let snapped_position = data.snap_handler.snap_position(responses, input.viewport_bounds.size(), document, input.mouse.position); + let snapped_position = data.snap_handler.snap_position(responses, document, input.mouse.position); data.shape_editor.move_selected_points(snapped_position, false, responses); Drawing @@ -302,7 +303,7 @@ fn add_to_curve(data: &mut PenToolData, input: &InputPreprocessorMessageHandler, update_path_representation(data); // Setup our position params - let snapped_position = data.snap_handler.snap_position(responses, input.viewport_bounds.size(), document, input.mouse.position); + let snapped_position = data.snap_handler.snap_position(responses, document, input.mouse.position); let position = transform.inverse().transform_point2(snapped_position); // Add a curve to the path diff --git a/editor/src/viewport_tools/tools/rectangle_tool.rs b/editor/src/viewport_tools/tools/rectangle_tool.rs index 8e9df1929..5393aad2f 100644 --- a/editor/src/viewport_tools/tools/rectangle_tool.rs +++ b/editor/src/viewport_tools/tools/rectangle_tool.rs @@ -109,7 +109,7 @@ impl Fsm for RectangleToolFsmState { if let ToolMessage::Rectangle(event) = event { match (self, event) { (Ready, DragStart) => { - shape_data.start(responses, input.viewport_bounds.size(), document, input.mouse.position); + shape_data.start(responses, document, input.mouse.position); responses.push_back(DocumentMessage::StartTransaction.into()); shape_data.path = Some(document.get_path_for_new_layer()); responses.push_back(DocumentMessage::DeselectAllLayers.into()); @@ -127,7 +127,7 @@ impl Fsm for RectangleToolFsmState { Drawing } (state, Resize { center, lock_ratio }) => { - if let Some(message) = shape_data.calculate_transform(responses, input.viewport_bounds.size(), document, center, lock_ratio, input) { + if let Some(message) = shape_data.calculate_transform(responses, document, center, lock_ratio, input) { responses.push_back(message); } diff --git a/editor/src/viewport_tools/tools/select_tool.rs b/editor/src/viewport_tools/tools/select_tool.rs index 294f34259..d5ce0eab2 100644 --- a/editor/src/viewport_tools/tools/select_tool.rs +++ b/editor/src/viewport_tools/tools/select_tool.rs @@ -9,7 +9,7 @@ use crate::input::InputPreprocessorMessageHandler; use crate::layout::widgets::{IconButton, LayoutRow, PopoverButton, PropertyHolder, Separator, SeparatorDirection, SeparatorType, Widget, WidgetCallback, WidgetHolder, WidgetLayout}; use crate::message_prelude::*; use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup}; -use crate::viewport_tools::snapping::SnapHandler; +use crate::viewport_tools::snapping::{self, SnapHandler}; use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData, ToolType}; use graphene::boolean_ops::BooleanOperation; use graphene::document::Document; @@ -404,6 +404,7 @@ impl Fsm for SelectToolFsmState { let snap_y = selected_edges.0 || selected_edges.1; data.snap_handler.start_snap(document, document.bounding_boxes(Some(&selected), None), snap_x, snap_y); + data.snap_handler.add_all_document_handles(document, &[], &selected.iter().map(|x| x.as_slice()).collect::>()); data.layers_dragging = selected; @@ -461,11 +462,10 @@ impl Fsm for SelectToolFsmState { .layers_dragging .iter() .filter_map(|path| document.graphene_document.viewport_bounding_box(path).ok()?) - .flat_map(|[bound1, bound2]| [bound1, bound2, (bound1 + bound2) / 2.]) - .map(|vec| vec.into()) - .unzip(); + .flat_map(snapping::expand_bounds) + .collect(); - let closest_move = data.snap_handler.snap_layers(responses, document, snap, input.viewport_bounds.size(), mouse_delta); + let closest_move = data.snap_handler.snap_layers(responses, document, snap, 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( @@ -486,7 +486,7 @@ impl Fsm for SelectToolFsmState { let mouse_position = input.mouse.position; - let snapped_mouse_position = data.snap_handler.snap_position(responses, input.viewport_bounds.size(), document, mouse_position); + let snapped_mouse_position = data.snap_handler.snap_position(responses, document, mouse_position); let [_position, size] = movement.new_size(snapped_mouse_position, bounds.transform, center, axis_align); let delta = movement.bounds_to_scale_transform(center, size); diff --git a/editor/src/viewport_tools/tools/shape_tool.rs b/editor/src/viewport_tools/tools/shape_tool.rs index 03141946b..5a6029ea4 100644 --- a/editor/src/viewport_tools/tools/shape_tool.rs +++ b/editor/src/viewport_tools/tools/shape_tool.rs @@ -149,7 +149,7 @@ impl Fsm for ShapeToolFsmState { if let ToolMessage::Shape(event) = event { match (self, event) { (Ready, DragStart) => { - shape_data.start(responses, input.viewport_bounds.size(), document, input.mouse.position); + shape_data.start(responses, document, input.mouse.position); responses.push_back(DocumentMessage::StartTransaction.into()); shape_data.path = Some(document.get_path_for_new_layer()); responses.push_back(DocumentMessage::DeselectAllLayers.into()); @@ -169,7 +169,7 @@ impl Fsm for ShapeToolFsmState { Drawing } (state, Resize { center, lock_ratio }) => { - if let Some(message) = shape_data.calculate_transform(responses, input.viewport_bounds.size(), document, center, lock_ratio, input) { + if let Some(message) = shape_data.calculate_transform(responses, document, center, lock_ratio, input) { responses.push_back(message); } diff --git a/editor/src/viewport_tools/tools/shared/resize.rs b/editor/src/viewport_tools/tools/shared/resize.rs index 360d2a74e..3500a87fd 100644 --- a/editor/src/viewport_tools/tools/shared/resize.rs +++ b/editor/src/viewport_tools/tools/shared/resize.rs @@ -18,15 +18,15 @@ 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) { + pub fn start(&mut self, responses: &mut VecDeque, document: &DocumentMessageHandler, mouse_position: DVec2) { self.snap_handler.start_snap(document, document.bounding_boxes(None, None), true, true); - self.drag_start = self.snap_handler.snap_position(responses, viewport_bounds, document, mouse_position); + self.snap_handler.add_all_document_handles(document, &[], &[]); + self.drag_start = self.snap_handler.snap_position(responses, document, mouse_position); } pub fn calculate_transform( &mut self, responses: &mut VecDeque, - viewport_bounds: DVec2, document: &DocumentMessageHandler, center: Key, lock_ratio: Key, @@ -35,7 +35,7 @@ impl Resize { if let Some(path) = &self.path { let mut start = self.drag_start; - let stop = self.snap_handler.snap_position(responses, viewport_bounds, document, ipp.mouse.position); + let stop = self.snap_handler.snap_position(responses, document, ipp.mouse.position); let mut size = stop - start; if ipp.keyboard.get(lock_ratio as usize) { diff --git a/editor/src/viewport_tools/tools/spline_tool.rs b/editor/src/viewport_tools/tools/spline_tool.rs index f1d4c2b10..fe38aa0dd 100644 --- a/editor/src/viewport_tools/tools/spline_tool.rs +++ b/editor/src/viewport_tools/tools/spline_tool.rs @@ -156,7 +156,8 @@ impl Fsm for SplineToolFsmState { data.path = Some(document.get_path_for_new_layer()); data.snap_handler.start_snap(document, document.bounding_boxes(None, None), true, true); - let snapped_position = data.snap_handler.snap_position(responses, input.viewport_bounds.size(), document, input.mouse.position); + data.snap_handler.add_all_document_handles(document, &[], &[]); + let snapped_position = data.snap_handler.snap_position(responses, document, input.mouse.position); let pos = transform.inverse().transform_point2(snapped_position); @@ -170,7 +171,7 @@ impl Fsm for SplineToolFsmState { Drawing } (Drawing, DragStop) => { - let snapped_position = data.snap_handler.snap_position(responses, input.viewport_bounds.size(), document, input.mouse.position); + let snapped_position = data.snap_handler.snap_position(responses, document, input.mouse.position); let pos = transform.inverse().transform_point2(snapped_position); if let Some(last_pos) = data.points.last() { @@ -186,7 +187,7 @@ impl Fsm for SplineToolFsmState { Drawing } (Drawing, PointerMove) => { - let snapped_position = data.snap_handler.snap_position(responses, input.viewport_bounds.size(), document, input.mouse.position); + let snapped_position = data.snap_handler.snap_position(responses, document, input.mouse.position); let pos = transform.inverse().transform_point2(snapped_position); data.next_point = pos; diff --git a/frontend/src/lifetime/input.ts b/frontend/src/lifetime/input.ts index 2ad9faa30..7c7650489 100644 --- a/frontend/src/lifetime/input.ts +++ b/frontend/src/lifetime/input.ts @@ -65,7 +65,7 @@ export function createInputManager(editor: EditorState, container: HTMLElement, if (key === "f5") return false; // Don't redirect debugging tools - if (key === "f12") return false; + if (key === "f12" || key === "f8") return false; if (e.ctrlKey && e.shiftKey && key === "c") return false; if (e.ctrlKey && e.shiftKey && key === "i") return false; if (e.ctrlKey && e.shiftKey && key === "j") return false;