Skip to content

Add alignment overlays #462

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 22 commits into from
Jan 20, 2022
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