mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-07-31 19:43:45 +00:00
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:
parent
4b7d8b7ab0
commit
e3e506ecfb
14 changed files with 303 additions and 183 deletions
|
@ -15,9 +15,17 @@ pub const VIEWPORT_SCROLL_RATE: f64 = 0.6;
|
||||||
|
|
||||||
pub const VIEWPORT_ROTATE_SNAP_INTERVAL: f64 = 15.;
|
pub const VIEWPORT_ROTATE_SNAP_INTERVAL: f64 = 15.;
|
||||||
|
|
||||||
pub const SNAP_TOLERANCE: f64 = 3.;
|
// Snapping axis
|
||||||
pub const SNAP_OVERLAY_FADE_DISTANCE: f64 = 20.;
|
pub const SNAP_AXIS_TOLERANCE: f64 = 3.;
|
||||||
pub const SNAP_OVERLAY_UNSNAPPED_OPACITY: f64 = 0.4;
|
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.;
|
pub const DRAG_THRESHOLD: f64 = 1.;
|
||||||
|
|
||||||
|
|
|
@ -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::document::DocumentMessageHandler;
|
||||||
use crate::message_prelude::*;
|
use crate::message_prelude::*;
|
||||||
|
|
||||||
|
use graphene::layers::layer_info::{Layer, LayerDataType};
|
||||||
use graphene::layers::style::{self, Stroke};
|
use graphene::layers::style::{self, Stroke};
|
||||||
use graphene::{LayerId, Operation};
|
use graphene::{LayerId, Operation};
|
||||||
|
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
use std::f64::consts::PI;
|
use std::f64::consts::PI;
|
||||||
|
|
||||||
|
// Handles snap overlays
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct SnapHandler {
|
struct SnapOverlays {
|
||||||
snap_targets: Option<(Vec<f64>, Vec<f64>)>,
|
axis_overlay_paths: Vec<Vec<LayerId>>,
|
||||||
overlay_paths: Vec<Vec<LayerId>>,
|
point_overlay_paths: Vec<Vec<LayerId>>,
|
||||||
|
axis_index: usize,
|
||||||
|
point_index: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SnapHandler {
|
/// Handles snapping and snap overlays
|
||||||
/// Updates the snapping overlays with the specified distances.
|
#[derive(Debug, Clone, Default)]
|
||||||
/// `positions_and_distances` is a tuple of `position` and `distance` iterators, respectively, each with `(x, y)` values.
|
pub struct SnapHandler {
|
||||||
fn update_overlays(
|
point_targets: Option<Vec<DVec2>>,
|
||||||
overlay_paths: &mut Vec<Vec<LayerId>>,
|
bound_targets: Option<Vec<DVec2>>,
|
||||||
responses: &mut VecDeque<Message>,
|
snap_overlays: SnapOverlays,
|
||||||
viewport_bounds: DVec2,
|
snap_x: bool,
|
||||||
positions_and_distances: (impl Iterator<Item = (f64, f64)>, impl Iterator<Item = (f64, f64)>),
|
snap_y: bool,
|
||||||
closest_distance: DVec2,
|
}
|
||||||
) {
|
|
||||||
/// Draws an alignment line overlay with the correct transform and fade opacity, reusing lines from the pool if available.
|
/// Converts a bounding box into a set of points for snapping
|
||||||
fn add_overlay_line(responses: &mut VecDeque<Message>, transform: [f64; 6], opacity: 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
|
/// Puts a point in the middle of each edge (top, bottom, left, right)
|
||||||
let layer_path = if index >= overlay_paths.len() {
|
pub fn expand_bounds([bound1, bound2]: [DVec2; 2]) -> [DVec2; 4] {
|
||||||
let layer_path = vec![generate_uuid()];
|
[
|
||||||
responses.push_back(
|
DVec2::new((bound1.x + bound2.x) / 2., bound1.y),
|
||||||
DocumentMessage::Overlays(
|
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 {
|
Operation::AddOverlayLine {
|
||||||
path: layer_path.clone(),
|
path: layer_path.clone(),
|
||||||
transform,
|
transform,
|
||||||
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 1.0)), style::Fill::None),
|
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(),
|
.into(),
|
||||||
);
|
)
|
||||||
overlay_paths.push(layer_path.clone());
|
.into(),
|
||||||
layer_path
|
);
|
||||||
}
|
overlay_paths.push(layer_path.clone());
|
||||||
// Otherwise, reuse an overlay line from the pool and update its new transform
|
layer_path
|
||||||
else {
|
}
|
||||||
let layer_path = overlay_paths[index].clone();
|
// Otherwise, reuse an overlay from the pool and update its new transform
|
||||||
responses.push_back(DocumentMessage::Overlays(Operation::SetLayerTransform { path: layer_path.clone(), transform }.into()).into());
|
else {
|
||||||
layer_path
|
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());
|
responses.push_back(DocumentMessage::Overlays(Operation::SetLayerOpacity { path: layer_path, opacity }.into()).into());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let (positions, distances) = positions_and_distances;
|
/// Draw the alignment lines for an axis
|
||||||
let mut index = 0;
|
/// 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
|
let scale = DVec2::new(offset_other - goal_axis, 1.);
|
||||||
for (x_target, distance) in positions.filter(|(_pos, dist)| dist.abs() < SNAP_OVERLAY_FADE_DISTANCE) {
|
let angle = if is_horizontal { 0. } else { PI / 2. };
|
||||||
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 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.
|
1.
|
||||||
} else {
|
} 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);
|
// Add line
|
||||||
index += 1;
|
Self::add_overlay(true, responses, transform, Some(opacity), self.axis_index, &mut self.axis_overlay_paths);
|
||||||
}
|
self.axis_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 {
|
let size = DVec2::splat(SNAP_POINT_SIZE);
|
||||||
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);
|
// Add point at target
|
||||||
index += 1;
|
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.
|
/// 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.)
|
/// 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.
|
/// 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) {
|
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 {
|
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.
|
// 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
|
/// Add arbitrary snapping points
|
||||||
|
///
|
||||||
/// This should be called after start_snap
|
/// 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 {
|
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(targets) = &mut self.point_targets {
|
||||||
if let Some((new_x_targets, new_y_targets)) = &mut self.snap_targets {
|
targets.extend(snap_points);
|
||||||
x_targets.append(new_x_targets);
|
} else {
|
||||||
y_targets.append(new_y_targets);
|
self.point_targets = Some(snap_points.collect());
|
||||||
self.snap_targets = Some((x_targets, y_targets));
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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.
|
/// 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.
|
/// Returns 0 for each axis that there is no snap less than the snap tolerance.
|
||||||
pub fn snap_layers(
|
pub fn snap_layers(&mut self, responses: &mut VecDeque<Message>, document_message_handler: &DocumentMessageHandler, snap_anchors: Vec<DVec2>, mouse_delta: DVec2) -> DVec2 {
|
||||||
&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 {
|
|
||||||
if document_message_handler.snapping_enabled {
|
if document_message_handler.snapping_enabled {
|
||||||
if let Some((targets_x, targets_y)) = &self.snap_targets {
|
self.calculate_snap(snap_anchors.iter().map(move |&snap| mouse_delta + snap), responses)
|
||||||
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
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
DVec2::ZERO
|
DVec2::ZERO
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handles snapping of a viewport position, returning another viewport position.
|
/// 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 document_message_handler.snapping_enabled {
|
||||||
if let Some((targets_x, targets_y)) = &self.snap_targets {
|
self.calculate_snap([position_viewport].into_iter(), responses) + position_viewport
|
||||||
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
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
position_viewport
|
position_viewport
|
||||||
}
|
}
|
||||||
|
@ -185,7 +308,8 @@ impl SnapHandler {
|
||||||
|
|
||||||
/// Removes snap target data and overlays. Call this when snapping is done.
|
/// Removes snap target data and overlays. Call this when snapping is done.
|
||||||
pub fn cleanup(&mut self, responses: &mut VecDeque<Message>) {
|
pub fn cleanup(&mut self, responses: &mut VecDeque<Message>) {
|
||||||
Self::remove_unused_overlays(&mut self.overlay_paths, responses, 0);
|
self.snap_overlays.cleanup(responses);
|
||||||
self.snap_targets = None;
|
self.bound_targets = None;
|
||||||
|
self.point_targets = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -163,6 +163,7 @@ impl Fsm for ArtboardToolFsmState {
|
||||||
|
|
||||||
data.snap_handler
|
data.snap_handler
|
||||||
.start_snap(document, document.bounding_boxes(None, Some(data.selected_board.unwrap())), snap_x, snap_y);
|
.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
|
ArtboardToolFsmState::ResizingBounds
|
||||||
} else {
|
} else {
|
||||||
|
@ -175,6 +176,7 @@ impl Fsm for ArtboardToolFsmState {
|
||||||
data.selected_board = Some(intersection[0]);
|
data.selected_board = Some(intersection[0]);
|
||||||
|
|
||||||
data.snap_handler.start_snap(document, document.bounding_boxes(None, Some(intersection[0])), true, true);
|
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(
|
responses.push_back(
|
||||||
PropertiesPanelMessage::SetActiveLayers {
|
PropertiesPanelMessage::SetActiveLayers {
|
||||||
|
@ -190,6 +192,7 @@ impl Fsm for ArtboardToolFsmState {
|
||||||
data.selected_board = Some(id);
|
data.selected_board = Some(id);
|
||||||
|
|
||||||
data.snap_handler.start_snap(document, document.bounding_boxes(None, Some(id)), true, true);
|
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(
|
responses.push_back(
|
||||||
ArtboardMessage::AddArtboard {
|
ArtboardMessage::AddArtboard {
|
||||||
|
@ -213,7 +216,7 @@ impl Fsm for ArtboardToolFsmState {
|
||||||
let constrain_square = input.keyboard.get(constrain_axis_or_aspect as usize);
|
let constrain_square = input.keyboard.get(constrain_axis_or_aspect as usize);
|
||||||
|
|
||||||
let mouse_position = input.mouse.position;
|
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, size] = movement.new_size(snapped_mouse_position, bounds.transform, from_center, constrain_square);
|
||||||
let position = movement.center_position(position, size, from_center);
|
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_position = axis_align_drag(axis_align, input.mouse.position, data.drag_start);
|
||||||
let mouse_delta = mouse_position - data.drag_current;
|
let mouse_delta = mouse_position - data.drag_current;
|
||||||
|
|
||||||
let snap = bounds.evaluate_transform_handle_positions().iter().map(|v| (v.x, v.y)).unzip();
|
let snap = bounds.evaluate_transform_handle_positions().into_iter().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);
|
||||||
|
|
||||||
let size = bounds.bounds[1] - bounds.bounds[0];
|
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 }) => {
|
(ArtboardToolFsmState::Drawing, ArtboardToolMessage::PointerMove { constrain_axis_or_aspect, center }) => {
|
||||||
let mouse_position = input.mouse.position;
|
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();
|
let root_transform = document.graphene_document.root.transform.inverse();
|
||||||
|
|
||||||
|
|
|
@ -110,7 +110,7 @@ impl Fsm for EllipseToolFsmState {
|
||||||
if let ToolMessage::Ellipse(event) = event {
|
if let ToolMessage::Ellipse(event) = event {
|
||||||
match (self, event) {
|
match (self, event) {
|
||||||
(Ready, DragStart) => {
|
(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());
|
responses.push_back(DocumentMessage::StartTransaction.into());
|
||||||
shape_data.path = Some(document.get_path_for_new_layer());
|
shape_data.path = Some(document.get_path_for_new_layer());
|
||||||
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
||||||
|
@ -128,7 +128,7 @@ impl Fsm for EllipseToolFsmState {
|
||||||
Drawing
|
Drawing
|
||||||
}
|
}
|
||||||
(state, Resize { center, lock_ratio }) => {
|
(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);
|
responses.push_back(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
|
||||||
|
|
||||||
use graphene::color::Color;
|
use graphene::color::Color;
|
||||||
use graphene::intersection::Quad;
|
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::layers::style::{Fill, Gradient, PathStyle, Stroke};
|
||||||
use graphene::Operation;
|
use graphene::Operation;
|
||||||
|
|
||||||
|
@ -241,25 +241,9 @@ struct GradientToolData {
|
||||||
snap_handler: SnapHandler,
|
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);
|
snap_handler.start_snap(document, document.bounding_boxes(None, None), true, true);
|
||||||
if let LayerDataType::Shape(s) = &layer.data {
|
snap_handler.add_all_document_handles(document, &[], &[]);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Fsm for GradientToolFsmState {
|
impl Fsm for GradientToolFsmState {
|
||||||
|
@ -307,7 +291,7 @@ impl Fsm for GradientToolFsmState {
|
||||||
for overlay in &data.gradient_overlays {
|
for overlay in &data.gradient_overlays {
|
||||||
if overlay.evaluate_gradient_start().distance_squared(mouse) < tolerance {
|
if overlay.evaluate_gradient_start().distance_squared(mouse) < tolerance {
|
||||||
dragging = true;
|
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 {
|
data.selected_gradient = Some(SelectedGradient {
|
||||||
path: overlay.path.clone(),
|
path: overlay.path.clone(),
|
||||||
transform: overlay.transform,
|
transform: overlay.transform,
|
||||||
|
@ -317,7 +301,7 @@ impl Fsm for GradientToolFsmState {
|
||||||
}
|
}
|
||||||
if overlay.evaluate_gradient_end().distance_squared(mouse) < tolerance {
|
if overlay.evaluate_gradient_end().distance_squared(mouse) < tolerance {
|
||||||
dragging = true;
|
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 {
|
data.selected_gradient = Some(SelectedGradient {
|
||||||
path: overlay.path.clone(),
|
path: overlay.path.clone(),
|
||||||
transform: overlay.transform,
|
transform: overlay.transform,
|
||||||
|
@ -348,7 +332,7 @@ impl Fsm for GradientToolFsmState {
|
||||||
|
|
||||||
data.selected_gradient = Some(selected_gradient);
|
data.selected_gradient = Some(selected_gradient);
|
||||||
|
|
||||||
start_snap(&mut data.snap_handler, document, layer, &intersection);
|
start_snap(&mut data.snap_handler, document);
|
||||||
|
|
||||||
GradientToolFsmState::Drawing
|
GradientToolFsmState::Drawing
|
||||||
} else {
|
} else {
|
||||||
|
@ -358,7 +342,7 @@ impl Fsm for GradientToolFsmState {
|
||||||
}
|
}
|
||||||
(GradientToolFsmState::Drawing, GradientToolMessage::PointerMove { constrain_axis }) => {
|
(GradientToolFsmState::Drawing, GradientToolMessage::PointerMove { constrain_axis }) => {
|
||||||
if let Some(selected_gradient) = &mut data.selected_gradient {
|
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));
|
selected_gradient.update_gradient(mouse, responses, input.keyboard.get(constrain_axis as usize));
|
||||||
}
|
}
|
||||||
GradientToolFsmState::Drawing
|
GradientToolFsmState::Drawing
|
||||||
|
|
|
@ -155,7 +155,8 @@ impl Fsm for LineToolFsmState {
|
||||||
match (self, event) {
|
match (self, event) {
|
||||||
(Ready, DragStart) => {
|
(Ready, DragStart) => {
|
||||||
data.snap_handler.start_snap(document, document.bounding_boxes(None, None), true, true);
|
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());
|
responses.push_back(DocumentMessage::StartTransaction.into());
|
||||||
data.path = Some(document.get_path_for_new_layer());
|
data.path = Some(document.get_path_for_new_layer());
|
||||||
|
@ -176,7 +177,7 @@ impl Fsm for LineToolFsmState {
|
||||||
Drawing
|
Drawing
|
||||||
}
|
}
|
||||||
(Drawing, Redraw { center, snap_angle, lock_angle }) => {
|
(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();
|
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]));
|
responses.push_back(generate_transform(data, values[0], values[1], values[2]));
|
||||||
|
@ -184,7 +185,7 @@ impl Fsm for LineToolFsmState {
|
||||||
Drawing
|
Drawing
|
||||||
}
|
}
|
||||||
(Drawing, DragStop) => {
|
(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);
|
data.snap_handler.cleanup(responses);
|
||||||
|
|
||||||
match data.drag_start.distance(input.mouse.position) <= DRAG_THRESHOLD {
|
match data.drag_start.distance(input.mouse.position) <= DRAG_THRESHOLD {
|
||||||
|
|
|
@ -144,15 +144,13 @@ impl Fsm for PathToolFsmState {
|
||||||
// Select the first point within the threshold (in pixels)
|
// Select the first point within the threshold (in pixels)
|
||||||
if data.shape_editor.select_point(input.mouse.position, SELECTION_THRESHOLD, add_to_selection, responses) {
|
if data.shape_editor.select_point(input.mouse.position, SELECTION_THRESHOLD, add_to_selection, responses) {
|
||||||
responses.push_back(DocumentMessage::StartTransaction.into());
|
responses.push_back(DocumentMessage::StartTransaction.into());
|
||||||
data.snap_handler.start_snap(document, document.bounding_boxes(None, None), true, true);
|
|
||||||
let snap_points = data
|
let ignore_document = data.shape_editor.shapes_to_modify.iter().map(|shape| shape.layer_path.clone()).collect::<Vec<_>>();
|
||||||
.shape_editor
|
data.snap_handler.start_snap(document, document.bounding_boxes(Some(&ignore_document), None), true, true);
|
||||||
.shapes_to_modify
|
|
||||||
.iter()
|
let include_handles = data.shape_editor.shapes_to_modify.iter().map(|shape| shape.layer_path.as_slice()).collect::<Vec<_>>();
|
||||||
.flat_map(|shape| shape.anchors.iter().flat_map(|anchor| anchor.points[0].as_ref()))
|
data.snap_handler.add_all_document_handles(document, &include_handles, &[]);
|
||||||
.map(|point| point.position)
|
|
||||||
.collect();
|
|
||||||
data.snap_handler.add_snap_points(document, snap_points);
|
|
||||||
data.drag_start_pos = input.mouse.position;
|
data.drag_start_pos = input.mouse.position;
|
||||||
Dragging
|
Dragging
|
||||||
}
|
}
|
||||||
|
@ -209,7 +207,7 @@ impl Fsm for PathToolFsmState {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move the selected points by the mouse position
|
// 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);
|
data.shape_editor.move_selected_points(snapped_position - data.drag_start_pos, true, responses);
|
||||||
Dragging
|
Dragging
|
||||||
}
|
}
|
||||||
|
|
|
@ -169,7 +169,8 @@ impl Fsm for PenToolFsmState {
|
||||||
// Create a new layer and prep snap system
|
// Create a new layer and prep snap system
|
||||||
data.path = Some(document.get_path_for_new_layer());
|
data.path = Some(document.get_path_for_new_layer());
|
||||||
data.snap_handler.start_snap(document, document.bounding_boxes(None, None), true, true);
|
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
|
// Get the position and set properties
|
||||||
let start_position = transform.inverse().transform_point2(snapped_position);
|
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
|
// 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);
|
data.shape_editor.move_selected_points(snapped_position, false, responses);
|
||||||
|
|
||||||
Drawing
|
Drawing
|
||||||
}
|
}
|
||||||
(Drawing, PointerMove) => {
|
(Drawing, PointerMove) => {
|
||||||
// Move selected points
|
// 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);
|
data.shape_editor.move_selected_points(snapped_position, false, responses);
|
||||||
|
|
||||||
Drawing
|
Drawing
|
||||||
|
@ -302,7 +303,7 @@ fn add_to_curve(data: &mut PenToolData, input: &InputPreprocessorMessageHandler,
|
||||||
update_path_representation(data);
|
update_path_representation(data);
|
||||||
|
|
||||||
// Setup our position params
|
// 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);
|
let position = transform.inverse().transform_point2(snapped_position);
|
||||||
|
|
||||||
// Add a curve to the path
|
// Add a curve to the path
|
||||||
|
|
|
@ -109,7 +109,7 @@ impl Fsm for RectangleToolFsmState {
|
||||||
if let ToolMessage::Rectangle(event) = event {
|
if let ToolMessage::Rectangle(event) = event {
|
||||||
match (self, event) {
|
match (self, event) {
|
||||||
(Ready, DragStart) => {
|
(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());
|
responses.push_back(DocumentMessage::StartTransaction.into());
|
||||||
shape_data.path = Some(document.get_path_for_new_layer());
|
shape_data.path = Some(document.get_path_for_new_layer());
|
||||||
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
||||||
|
@ -127,7 +127,7 @@ impl Fsm for RectangleToolFsmState {
|
||||||
Drawing
|
Drawing
|
||||||
}
|
}
|
||||||
(state, Resize { center, lock_ratio }) => {
|
(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);
|
responses.push_back(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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::layout::widgets::{IconButton, LayoutRow, PopoverButton, PropertyHolder, Separator, SeparatorDirection, SeparatorType, Widget, WidgetCallback, WidgetHolder, WidgetLayout};
|
||||||
use crate::message_prelude::*;
|
use crate::message_prelude::*;
|
||||||
use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup};
|
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 crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData, ToolType};
|
||||||
use graphene::boolean_ops::BooleanOperation;
|
use graphene::boolean_ops::BooleanOperation;
|
||||||
use graphene::document::Document;
|
use graphene::document::Document;
|
||||||
|
@ -404,6 +404,7 @@ impl Fsm for SelectToolFsmState {
|
||||||
let snap_y = selected_edges.0 || selected_edges.1;
|
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.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;
|
data.layers_dragging = selected;
|
||||||
|
|
||||||
|
@ -461,11 +462,10 @@ impl Fsm for SelectToolFsmState {
|
||||||
.layers_dragging
|
.layers_dragging
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|path| document.graphene_document.viewport_bounding_box(path).ok()?)
|
.filter_map(|path| document.graphene_document.viewport_bounding_box(path).ok()?)
|
||||||
.flat_map(|[bound1, bound2]| [bound1, bound2, (bound1 + bound2) / 2.])
|
.flat_map(snapping::expand_bounds)
|
||||||
.map(|vec| vec.into())
|
.collect();
|
||||||
.unzip();
|
|
||||||
|
|
||||||
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
|
// 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()) {
|
for path in Document::shallowest_unique_layers(data.layers_dragging.iter()) {
|
||||||
responses.push_front(
|
responses.push_front(
|
||||||
|
@ -486,7 +486,7 @@ impl Fsm for SelectToolFsmState {
|
||||||
|
|
||||||
let mouse_position = input.mouse.position;
|
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 [_position, size] = movement.new_size(snapped_mouse_position, bounds.transform, center, axis_align);
|
||||||
let delta = movement.bounds_to_scale_transform(center, size);
|
let delta = movement.bounds_to_scale_transform(center, size);
|
||||||
|
|
|
@ -149,7 +149,7 @@ impl Fsm for ShapeToolFsmState {
|
||||||
if let ToolMessage::Shape(event) = event {
|
if let ToolMessage::Shape(event) = event {
|
||||||
match (self, event) {
|
match (self, event) {
|
||||||
(Ready, DragStart) => {
|
(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());
|
responses.push_back(DocumentMessage::StartTransaction.into());
|
||||||
shape_data.path = Some(document.get_path_for_new_layer());
|
shape_data.path = Some(document.get_path_for_new_layer());
|
||||||
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
||||||
|
@ -169,7 +169,7 @@ impl Fsm for ShapeToolFsmState {
|
||||||
Drawing
|
Drawing
|
||||||
}
|
}
|
||||||
(state, Resize { center, lock_ratio }) => {
|
(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);
|
responses.push_back(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,15 +18,15 @@ pub struct Resize {
|
||||||
|
|
||||||
impl Resize {
|
impl Resize {
|
||||||
/// Starts a resize, assigning the snap targets and snapping the starting position.
|
/// 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.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(
|
pub fn calculate_transform(
|
||||||
&mut self,
|
&mut self,
|
||||||
responses: &mut VecDeque<Message>,
|
responses: &mut VecDeque<Message>,
|
||||||
viewport_bounds: DVec2,
|
|
||||||
document: &DocumentMessageHandler,
|
document: &DocumentMessageHandler,
|
||||||
center: Key,
|
center: Key,
|
||||||
lock_ratio: Key,
|
lock_ratio: Key,
|
||||||
|
@ -35,7 +35,7 @@ impl Resize {
|
||||||
if let Some(path) = &self.path {
|
if let Some(path) = &self.path {
|
||||||
let mut start = self.drag_start;
|
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;
|
let mut size = stop - start;
|
||||||
if ipp.keyboard.get(lock_ratio as usize) {
|
if ipp.keyboard.get(lock_ratio as usize) {
|
||||||
|
|
|
@ -156,7 +156,8 @@ impl Fsm for SplineToolFsmState {
|
||||||
data.path = Some(document.get_path_for_new_layer());
|
data.path = Some(document.get_path_for_new_layer());
|
||||||
|
|
||||||
data.snap_handler.start_snap(document, document.bounding_boxes(None, None), true, true);
|
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);
|
let pos = transform.inverse().transform_point2(snapped_position);
|
||||||
|
|
||||||
|
@ -170,7 +171,7 @@ impl Fsm for SplineToolFsmState {
|
||||||
Drawing
|
Drawing
|
||||||
}
|
}
|
||||||
(Drawing, DragStop) => {
|
(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);
|
let pos = transform.inverse().transform_point2(snapped_position);
|
||||||
|
|
||||||
if let Some(last_pos) = data.points.last() {
|
if let Some(last_pos) = data.points.last() {
|
||||||
|
@ -186,7 +187,7 @@ impl Fsm for SplineToolFsmState {
|
||||||
Drawing
|
Drawing
|
||||||
}
|
}
|
||||||
(Drawing, PointerMove) => {
|
(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);
|
let pos = transform.inverse().transform_point2(snapped_position);
|
||||||
data.next_point = pos;
|
data.next_point = pos;
|
||||||
|
|
||||||
|
|
|
@ -65,7 +65,7 @@ export function createInputManager(editor: EditorState, container: HTMLElement,
|
||||||
if (key === "f5") return false;
|
if (key === "f5") return false;
|
||||||
|
|
||||||
// Don't redirect debugging tools
|
// 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 === "c") return false;
|
||||||
if (e.ctrlKey && e.shiftKey && key === "i") return false;
|
if (e.ctrlKey && e.shiftKey && key === "i") return false;
|
||||||
if (e.ctrlKey && e.shiftKey && key === "j") return false;
|
if (e.ctrlKey && e.shiftKey && key === "j") return false;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue