Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions editor/src/consts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ pub const VIEWPORT_SCROLL_RATE: f64 = 0.6;
pub const VIEWPORT_ROTATE_SNAP_INTERVAL: f64 = 15.;

pub const SNAP_TOLERANCE: f64 = 3.;
pub const SNAP_OVERLAY_FADE_DISTANCE: f64 = 20.;
pub const SNAP_OVERLAY_UNSNAPPED_OPACITY: f64 = 0.4;

// Transforming layer
pub const ROTATE_SNAP_ANGLE: f64 = 15.;
Expand Down
15 changes: 13 additions & 2 deletions editor/src/document/document_message_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,10 @@ impl DocumentMessageHandler {
self.layer_metadata.iter().filter_map(|(path, data)| data.selected.then(|| path.as_slice()))
}

pub fn non_selected_layers(&self) -> impl Iterator<Item = &[LayerId]> {
self.layer_metadata.iter().filter_map(|(path, data)| (!data.selected).then(|| path.as_slice()))
}

pub fn selected_layers_without_children(&self) -> Vec<&[LayerId]> {
let unique_layers = GrapheneDocument::shallowest_unique_layers(self.selected_layers());

Expand All @@ -200,6 +204,13 @@ impl DocumentMessageHandler {
})
}

pub fn visible_layers(&self) -> impl Iterator<Item = &[LayerId]> {
self.all_layers().filter(|path| match self.graphene_document.layer(path) {
Ok(layer) => layer.visible,
Err(_) => false,
})
}

fn serialize_structure(&self, folder: &Folder, structure: &mut Vec<u64>, data: &mut Vec<LayerId>, path: &mut Vec<LayerId>) {
let mut space = 0;
for (id, layer) in folder.layer_ids.iter().zip(folder.layers()).rev() {
Expand Down Expand Up @@ -267,7 +278,7 @@ impl DocumentMessageHandler {
self.layer_metadata.keys().filter_map(|path| (!path.is_empty()).then(|| path.as_slice()))
}

/// Returns the paths to all layers in order, optionally including only selected or non-selected layers.
/// Returns the paths to all layers in order
fn sort_layers<'a>(&self, paths: impl Iterator<Item = &'a [LayerId]>) -> Vec<&'a [LayerId]> {
// Compute the indices for each layer to be able to sort them
let mut layers_with_indices: Vec<(&[LayerId], Vec<usize>)> = paths
Expand Down Expand Up @@ -302,7 +313,7 @@ impl DocumentMessageHandler {
/// Returns the paths to all non_selected layers in order
#[allow(dead_code)] // used for test cases
pub fn non_selected_layers_sorted(&self) -> Vec<&[LayerId]> {
self.sort_layers(self.all_layers().filter(|layer| !self.selected_layers().any(|path| &path == layer)))
self.sort_layers(self.non_selected_layers())
}

pub fn layer_metadata(&self, path: &[LayerId]) -> &LayerMetadata {
Expand Down
3 changes: 3 additions & 0 deletions editor/src/document/transformation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ impl Translation {
}
}

#[must_use]
pub fn increment_amount(self, delta: DVec2) -> Self {
Self {
dragged_distance: self.dragged_distance + delta,
Expand Down Expand Up @@ -87,6 +88,7 @@ impl Rotation {
}
}

#[must_use]
pub fn increment_amount(self, delta: f64) -> Self {
Self {
dragged_angle: self.dragged_angle + delta,
Expand Down Expand Up @@ -124,6 +126,7 @@ impl Scale {
}
}

#[must_use]
pub fn increment_amount(self, delta: f64) -> Self {
Self {
dragged_factor: self.dragged_factor + delta,
Expand Down
10 changes: 0 additions & 10 deletions editor/src/input/input_preprocessor_message_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,6 @@ impl MessageHandler<InputPreprocessorMessage, ()> for InputPreprocessorMessageHa
}
.into(),
);
responses.push_back(
DocumentMessage::Overlays(
graphene::Operation::TransformLayer {
path: vec![],
transform: glam::DAffine2::from_translation(translation).to_cols_array(),
}
.into(),
)
.into(),
);
responses.push_back(
DocumentMessage::Artboard(
graphene::Operation::TransformLayer {
Expand Down
169 changes: 126 additions & 43 deletions editor/src/viewport_tools/snapping.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,105 @@
use crate::consts::SNAP_TOLERANCE;
use crate::consts::{COLOR_ACCENT, SNAP_OVERLAY_FADE_DISTANCE, SNAP_OVERLAY_UNSNAPPED_OPACITY, SNAP_TOLERANCE};
use crate::document::DocumentMessageHandler;
use crate::message_prelude::*;

use graphene::LayerId;
use graphene::layers::style::{self, Stroke};
use graphene::{LayerId, Operation};

use glam::DVec2;
use glam::{DAffine2, DVec2};
use std::f64::consts::PI;

#[derive(Debug, Clone, Default)]
pub struct SnapHandler {
snap_targets: Option<(Vec<f64>, Vec<f64>)>,
overlay_paths: Vec<Vec<LayerId>>,
}

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>>) {
// If there isn't one in the pool to ruse, add a new alignment line to the pool with the intended transform
let layer_path = if index >= overlay_paths.len() {
let layer_path = vec![generate_uuid()];
responses.push_back(
DocumentMessage::Overlays(
Operation::AddOverlayLine {
path: layer_path.clone(),
transform,
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 1.0)), None),
}
.into(),
)
.into(),
);
overlay_paths.push(layer_path.clone());
layer_path
}
// Otherwise, reuse an overlay line from the pool and update its new transform
else {
let layer_path = overlay_paths[index].clone();
responses.push_back(DocumentMessage::Overlays(Operation::SetLayerTransform { path: layer_path.clone(), transform }.into()).into());
layer_path
};

// Then set its opacity to the fade amount
responses.push_back(DocumentMessage::Overlays(Operation::SetLayerOpacity { path: layer_path, opacity }.into()).into());
}

let (positions, distances) = positions_and_distances;
let mut index = 0;

// Draw the vertical alignment lines
for (x_target, distance) in positions.filter(|(_pos, dist)| dist.abs() < SNAP_OVERLAY_FADE_DISTANCE) {
let transform = DAffine2::from_scale_angle_translation(DVec2::new(viewport_bounds.y, 1.), PI / 2., DVec2::new((x_target).round() - 0.5, 0.)).to_cols_array();

let opacity = if closest_distance.x == distance {
1.
} else {
SNAP_OVERLAY_UNSNAPPED_OPACITY - distance.abs() / (SNAP_OVERLAY_FADE_DISTANCE / SNAP_OVERLAY_UNSNAPPED_OPACITY)
};

add_overlay_line(responses, transform, opacity, index, overlay_paths);
index += 1;
}
// Draw the horizontal alignment lines
for (y_target, distance) in distances.filter(|(_pos, dist)| dist.abs() < SNAP_OVERLAY_FADE_DISTANCE) {
let transform = DAffine2::from_scale_angle_translation(DVec2::new(viewport_bounds.x, 1.), 0., DVec2::new(0., (y_target).round() - 0.5)).to_cols_array();

let opacity = if closest_distance.y == distance {
1.
} else {
SNAP_OVERLAY_UNSNAPPED_OPACITY - distance.abs() / (SNAP_OVERLAY_FADE_DISTANCE / SNAP_OVERLAY_UNSNAPPED_OPACITY)
};

add_overlay_line(responses, transform, opacity, index, overlay_paths);
index += 1;
}
Self::remove_unused_overlays(overlay_paths, responses, index);
}

/// Remove overlays from the pool beyond a given index. Pool entries up through that index will be kept.
fn remove_unused_overlays(overlay_paths: &mut Vec<Vec<LayerId>>, responses: &mut VecDeque<Message>, remove_after_index: usize) {
while overlay_paths.len() > remove_after_index {
responses.push_back(DocumentMessage::Overlays(Operation::DeleteLayer { path: overlay_paths.pop().unwrap() }.into()).into());
}
}

/// Gets a list of snap targets for the X and Y axes in Viewport coords for the target layers (usually all layers or all non-selected layers.)
/// This should be called at the start of a drag.
pub fn start_snap(&mut self, document_message_handler: &DocumentMessageHandler, target_layers: Vec<&[LayerId]>, ignore_layers: &[Vec<LayerId>]) {
pub fn start_snap<'a>(&mut self, document_message_handler: &DocumentMessageHandler, target_layers: impl Iterator<Item = &'a [LayerId]>) {
if document_message_handler.snapping_enabled {
// Could be made into sorted Vec or a HashSet for more performant lookups.
self.snap_targets = Some(
target_layers
.iter()
.filter(|path| !ignore_layers.iter().any(|layer| layer.as_slice() == **path))
.filter_map(|path| document_message_handler.graphene_document.viewport_bounding_box(path).ok()?)
.flat_map(|[bound1, bound2]| [bound1, bound2, ((bound1 + bound2) / 2.)])
.map(|vec| vec.into())
Expand All @@ -30,7 +110,14 @@ impl SnapHandler {

/// Finds the closest snap from an array of layers to the specified snap targets in viewport coords.
/// Returns 0 for each axis that there is no snap less than the snap tolerance.
pub fn snap_layers(&self, document_message_handler: &DocumentMessageHandler, selected_layers: &[Vec<LayerId>], mouse_delta: DVec2) -> DVec2 {
pub fn snap_layers(
&mut self,
responses: &mut VecDeque<Message>,
document_message_handler: &DocumentMessageHandler,
selected_layers: &[Vec<LayerId>],
viewport_bounds: DVec2,
mouse_delta: DVec2,
) -> DVec2 {
if document_message_handler.snapping_enabled {
if let Some((targets_x, targets_y)) = &self.snap_targets {
let (snap_x, snap_y): (Vec<f64>, Vec<f64>) = selected_layers
Expand All @@ -40,24 +127,23 @@ impl SnapHandler {
.map(|vec| vec.into())
.unzip();

let closest_move = DVec2::new(
targets_x
.iter()
.flat_map(|target| snap_x.iter().map(move |snap| target - mouse_delta.x - snap))
.min_by(|a, b| a.abs().partial_cmp(&b.abs()).expect("Could not compare document bounds."))
.unwrap_or(0.),
targets_y
.iter()
.flat_map(|target| snap_y.iter().map(move |snap| target - mouse_delta.y - snap))
.min_by(|a, b| a.abs().partial_cmp(&b.abs()).expect("Could not compare document bounds."))
.unwrap_or(0.),
let positions = targets_x.iter().flat_map(|&target| snap_x.iter().map(move |&snap| (target, target - mouse_delta.x - snap)));
let distances = targets_y.iter().flat_map(|&target| snap_y.iter().map(move |&snap| (target, target - mouse_delta.y - snap)));

let min_positions = positions.clone().min_by(|a, b| a.1.abs().partial_cmp(&b.1.abs()).expect("Could not compare position."));
let min_distances = distances.clone().min_by(|a, b| a.1.abs().partial_cmp(&b.1.abs()).expect("Could not compare position."));

let closest_distance = DVec2::new(min_positions.map_or(0., |(_pos, dist)| dist), min_distances.map_or(0., |(_pos, dist)| dist));

// Clamp, do not move, if above snap tolerance
let clamped_closest_distance = DVec2::new(
if closest_distance.x.abs() > SNAP_TOLERANCE { 0. } else { closest_distance.x },
if closest_distance.y.abs() > SNAP_TOLERANCE { 0. } else { closest_distance.y },
);

// Clamp, do not move if over snap tolerance
DVec2::new(
if closest_move.x.abs() > SNAP_TOLERANCE { 0. } else { closest_move.x },
if closest_move.y.abs() > SNAP_TOLERANCE { 0. } else { closest_move.y },
)
Self::update_overlays(&mut self.overlay_paths, responses, viewport_bounds, (positions, distances), clamped_closest_distance);

clamped_closest_distance
} else {
DVec2::ZERO
}
Expand All @@ -67,30 +153,26 @@ impl SnapHandler {
}

/// Handles snapping of a viewport position, returning another viewport position.
pub fn snap_position(&self, document_message_handler: &DocumentMessageHandler, position_viewport: DVec2) -> DVec2 {
pub fn snap_position(&mut self, responses: &mut VecDeque<Message>, viewport_bounds: DVec2, document_message_handler: &DocumentMessageHandler, position_viewport: DVec2) -> DVec2 {
if document_message_handler.snapping_enabled {
if let Some((targets_x, targets_y)) = &self.snap_targets {
// For each list of snap targets, find the shortest distance to move the point to that target.
let closest_move = DVec2::new(
targets_x
.iter()
.map(|x| (x - position_viewport.x))
.min_by(|a, b| a.abs().partial_cmp(&b.abs()).expect("Could not compare document bounds."))
.unwrap_or(0.),
targets_y
.iter()
.map(|y| (y - position_viewport.y))
.min_by(|a, b| a.abs().partial_cmp(&b.abs()).expect("Could not compare document bounds."))
.unwrap_or(0.),
);
let positions = targets_x.iter().map(|&x| (x, x - position_viewport.x));
let distances = targets_y.iter().map(|&y| (y, y - position_viewport.y));

let min_positions = positions.clone().min_by(|a, b| a.1.abs().partial_cmp(&b.1.abs()).expect("Could not compare position."));
let min_distances = distances.clone().min_by(|a, b| a.1.abs().partial_cmp(&b.1.abs()).expect("Could not compare position."));

let closest_distance = DVec2::new(min_positions.map_or(0., |(_pos, dist)| dist), min_distances.map_or(0., |(_pos, dist)| dist));

// Do not move if over snap tolerance
let clamped_closest_move = DVec2::new(
if closest_move.x.abs() > SNAP_TOLERANCE { 0. } else { closest_move.x },
if closest_move.y.abs() > SNAP_TOLERANCE { 0. } else { closest_move.y },
let clamped_closest_distance = DVec2::new(
if closest_distance.x.abs() > SNAP_TOLERANCE { 0. } else { closest_distance.x },
if closest_distance.y.abs() > SNAP_TOLERANCE { 0. } else { closest_distance.y },
);

position_viewport + clamped_closest_move
Self::update_overlays(&mut self.overlay_paths, responses, viewport_bounds, (positions, distances), clamped_closest_distance);

position_viewport + clamped_closest_distance
} else {
position_viewport
}
Expand All @@ -99,8 +181,9 @@ impl SnapHandler {
}
}

/// Removes snap target data. Call this when snapping is done.
pub fn cleanup(&mut self) {
/// Removes snap target data and overlays. Call this when snapping is done.
pub fn cleanup(&mut self, responses: &mut VecDeque<Message>) {
Self::remove_unused_overlays(&mut self.overlay_paths, responses, 0);
self.snap_targets = None;
}
}
1 change: 1 addition & 0 deletions editor/src/viewport_tools/tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ pub type ToolActionHandlerData<'a> = (&'a DocumentMessageHandler, &'a DocumentTo
pub trait Fsm {
type ToolData;

#[must_use]
fn transition(
self,
message: ToolMessage,
Expand Down
8 changes: 4 additions & 4 deletions editor/src/viewport_tools/tools/ellipse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ impl Fsm for EllipseToolFsmState {
if let ToolMessage::Ellipse(event) = event {
match (self, event) {
(Ready, DragStart) => {
shape_data.start(document, input.mouse.position);
shape_data.start(responses, input.viewport_bounds.size(), document, input.mouse.position);
responses.push_back(DocumentMessage::StartTransaction.into());
shape_data.path = Some(vec![generate_uuid()]);
responses.push_back(DocumentMessage::DeselectAllLayers.into());
Expand All @@ -122,7 +122,7 @@ impl Fsm for EllipseToolFsmState {
Drawing
}
(state, Resize { center, lock_ratio }) => {
if let Some(message) = shape_data.calculate_transform(document, center, lock_ratio, input) {
if let Some(message) = shape_data.calculate_transform(responses, input.viewport_bounds.size(), document, center, lock_ratio, input) {
responses.push_back(message);
}

Expand All @@ -135,12 +135,12 @@ impl Fsm for EllipseToolFsmState {
false => responses.push_back(DocumentMessage::CommitTransaction.into()),
}

shape_data.cleanup();
shape_data.cleanup(responses);
Ready
}
(Drawing, Abort) => {
responses.push_back(DocumentMessage::AbortTransaction.into());
shape_data.cleanup();
shape_data.cleanup(responses);

Ready
}
Expand Down
12 changes: 6 additions & 6 deletions editor/src/viewport_tools/tools/line.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,8 @@ impl Fsm for LineToolFsmState {
if let ToolMessage::Line(event) = event {
match (self, event) {
(Ready, DragStart) => {
data.snap_handler.start_snap(document, document.all_layers_sorted(), &[]);
data.drag_start = data.snap_handler.snap_position(document, input.mouse.position);
data.snap_handler.start_snap(document, document.visible_layers());
data.drag_start = data.snap_handler.snap_position(responses, input.viewport_bounds.size(), document, input.mouse.position);

responses.push_back(DocumentMessage::StartTransaction.into());
data.path = Some(vec![generate_uuid()]);
Expand All @@ -136,16 +136,16 @@ impl Fsm for LineToolFsmState {
Drawing
}
(Drawing, Redraw { center, snap_angle, lock_angle }) => {
data.drag_current = data.snap_handler.snap_position(document, input.mouse.position);
data.drag_current = data.snap_handler.snap_position(responses, input.viewport_bounds.size(), document, input.mouse.position);

let values: Vec<_> = [lock_angle, snap_angle, center].iter().map(|k| input.keyboard.get(*k as usize)).collect();
responses.push_back(generate_transform(data, values[0], values[1], values[2]));

Drawing
}
(Drawing, DragStop) => {
data.drag_current = data.snap_handler.snap_position(document, input.mouse.position);
data.snap_handler.cleanup();
data.drag_current = data.snap_handler.snap_position(responses, input.viewport_bounds.size(), document, input.mouse.position);
data.snap_handler.cleanup(responses);

// TODO: introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100)
match data.drag_start == input.mouse.position {
Expand All @@ -158,7 +158,7 @@ impl Fsm for LineToolFsmState {
Ready
}
(Drawing, Abort) => {
data.snap_handler.cleanup();
data.snap_handler.cleanup(responses);
responses.push_back(DocumentMessage::AbortTransaction.into());
data.path = None;
Ready
Expand Down
Loading