Snapping system improvements and refactor (#621)

* Snap to points and refactor

* Improve dot position on bounds

* Add snap matrix

* Cleanup

* Code review

* Half axis fade rather than increase it

* Fix fmt

* Hide snap to point overlay when active

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
0HyperCube 2022-05-09 04:13:57 +01:00 committed by Keavon Chambers
parent 4b7d8b7ab0
commit e3e506ecfb
14 changed files with 303 additions and 183 deletions

View file

@ -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.;

View file

@ -1,41 +1,69 @@
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<f64>, Vec<f64>)>,
overlay_paths: Vec<Vec<LayerId>>,
struct SnapOverlays {
axis_overlay_paths: Vec<Vec<LayerId>>,
point_overlay_paths: Vec<Vec<LayerId>>,
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<Vec<LayerId>>,
responses: &mut VecDeque<Message>,
viewport_bounds: DVec2,
positions_and_distances: (impl Iterator<Item = (f64, f64)>, impl Iterator<Item = (f64, f64)>),
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<Message>, transform: [f64; 6], opacity: f64, index: usize, overlay_paths: &mut Vec<Vec<LayerId>>) {
/// Handles snapping and snap overlays
#[derive(Debug, Clone, Default)]
pub struct SnapHandler {
point_targets: Option<Vec<DVec2>>,
bound_targets: Option<Vec<DVec2>>,
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<Message>, transform: [f64; 6], opacity: Option<f64>, index: usize, overlay_paths: &mut Vec<Vec<LayerId>>) {
// 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),
}
} else {
Operation::AddOverlayEllipse {
path: layer_path.clone(),
transform,
style: style::PathStyle::new(None, style::Fill::Solid(COLOR_ACCENT)),
}
}
.into(),
)
.into(),
@ -43,7 +71,7 @@ impl SnapHandler {
overlay_paths.push(layer_path.clone());
layer_path
}
// Otherwise, reuse an overlay line from the pool and update its new transform
// 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());
@ -51,39 +79,87 @@ impl SnapHandler {
};
// 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<Item = (DVec2, DVec2, f64)>, responses: &mut VecDeque<Message>, 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<Item = (DVec2, DVec2, f64)>, responses: &mut VecDeque<Message>, 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<X, Y, P>(&mut self, responses: &mut VecDeque<Message>, positions_and_distances: (X, Y, P), closest_distance: DVec2)
where
X: Iterator<Item = (DVec2, DVec2, f64)>,
Y: Iterator<Item = (DVec2, DVec2, f64)>,
P: Iterator<Item = (DVec2, DVec2, f64)>,
{
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<Message>) {
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<R>(&mut self, targets: R, responses: &mut VecDeque<Message>) -> DVec2
where
R: Iterator<Item = DVec2> + 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<Item = [DVec2; 2]>, 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<DVec2>) {
pub fn add_snap_points(&mut self, document_message_handler: &DocumentMessageHandler, snap_points: impl Iterator<Item = DVec2>) {
if document_message_handler.snapping_enabled {
let (mut x_targets, mut y_targets): (Vec<f64>, Vec<f64>) = 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<Message>,
document_message_handler: &DocumentMessageHandler,
(snap_x, snap_y): (Vec<f64>, Vec<f64>),
viewport_bounds: DVec2,
mouse_delta: DVec2,
) -> DVec2 {
pub fn snap_layers(&mut self, responses: &mut VecDeque<Message>, document_message_handler: &DocumentMessageHandler, snap_anchors: Vec<DVec2>, 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<Message>, viewport_bounds: DVec2, document_message_handler: &DocumentMessageHandler, position_viewport: DVec2) -> DVec2 {
pub fn snap_position(&mut self, responses: &mut VecDeque<Message>, 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<Message>) {
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;
}
}

View file

@ -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();

View file

@ -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);
}

View file

@ -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

View file

@ -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 {

View file

@ -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::<Vec<_>>();
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::<Vec<_>>();
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
}

View file

@ -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

View file

@ -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);
}

View file

@ -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::<Vec<_>>());
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);

View file

@ -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);
}

View file

@ -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<Message>, viewport_bounds: DVec2, document: &DocumentMessageHandler, mouse_position: DVec2) {
pub fn start(&mut self, responses: &mut VecDeque<Message>, 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<Message>,
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) {

View file

@ -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;

View file

@ -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;