Skip to content
2 changes: 2 additions & 0 deletions editor/src/consts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ pub const VIEWPORT_SCROLL_RATE: f64 = 0.6;

pub const VIEWPORT_ROTATE_SNAP_INTERVAL: f64 = 15.;

pub const SNAP_TOLERANCE: f64 = 3.;

// TRANSFORMING LAYER
pub const ROTATE_SNAP_ANGLE: f64 = 15.;
pub const SCALE_SNAP_INTERVAL: f64 = 0.1;
Expand Down
8 changes: 8 additions & 0 deletions editor/src/document/document_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ pub struct DocumentMessageHandler {
pub layer_data: HashMap<Vec<LayerId>, LayerData>,
movement_handler: MovementMessageHandler,
transform_layer_handler: TransformLayerMessageHandler,
pub snapping_enabled: bool,
}

impl Default for DocumentMessageHandler {
Expand All @@ -77,6 +78,7 @@ impl Default for DocumentMessageHandler {
layer_data: vec![(vec![], LayerData::new(true))].into_iter().collect(),
movement_handler: MovementMessageHandler::default(),
transform_layer_handler: TransformLayerMessageHandler::default(),
snapping_enabled: true,
}
}
}
Expand Down Expand Up @@ -127,6 +129,7 @@ pub enum DocumentMessage {
insert_index: isize,
},
ReorderSelectedLayers(i32), // relative_position,
SetSnapping(bool),
}

impl From<DocumentOperation> for DocumentMessage {
Expand Down Expand Up @@ -308,6 +311,7 @@ impl DocumentMessageHandler {
layer_data: vec![(vec![], LayerData::new(true))].into_iter().collect(),
movement_handler: MovementMessageHandler::default(),
transform_layer_handler: TransformLayerMessageHandler::default(),
snapping_enabled: true,
}
}

Expand Down Expand Up @@ -772,6 +776,9 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
}
}
RenameLayer(path, name) => responses.push_back(DocumentOperation::RenameLayer { path, name }.into()),
SetSnapping(new_status) => {
self.snapping_enabled = new_status;
}
}
}

Expand All @@ -784,6 +791,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
RenderDocument,
ExportDocument,
SaveDocument,
SetSnapping,
);

if self.layer_data.values().any(|data| data.selected) {
Expand Down
1 change: 1 addition & 0 deletions editor/src/tool/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod snapping;
pub mod tool_message_handler;
pub mod tool_options;
pub mod tools;
Expand Down
112 changes: 112 additions & 0 deletions editor/src/tool/snapping.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
use glam::DVec2;
use graphene::LayerId;

use crate::consts::SNAP_TOLERANCE;

use super::DocumentMessageHandler;

#[derive(Debug, Clone)]
pub struct SnapHandler {
snap_targets: Option<(Vec<f64>, Vec<f64>)>,
}
impl Default for SnapHandler {
fn default() -> Self {
Self { snap_targets: None }
}
}

impl SnapHandler {
/// 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<Vec<LayerId>>, ignore_layers: &[Vec<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.contains(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())
.unzip(),
);
}
}

/// 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 {
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
.iter()
.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())
.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.),
);

// 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 },
);

clamped_closest_move
} else {
DVec2::ZERO
}
} else {
DVec2::ZERO
}
}

/// Handles snapping of a viewport position, returning another viewport position.
pub fn snap_position(&self, 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.),
);

// 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 },
);

position_viewport + clamped_closest_move
} else {
position_viewport
}
} else {
position_viewport
}
}

pub fn cleanup(&mut self) {
self.snap_targets = None;
}
}
11 changes: 5 additions & 6 deletions editor/src/tool/tools/ellipse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ impl Default for EllipseToolFsmState {
}
#[derive(Clone, Debug, Default)]
struct EllipseToolData {
sides: u8,
data: Resize,
}

Expand All @@ -59,7 +58,7 @@ impl Fsm for EllipseToolFsmState {
fn transition(
self,
event: ToolMessage,
_document: &DocumentMessageHandler,
document: &DocumentMessageHandler,
tool_data: &DocumentToolData,
data: &mut Self::ToolData,
input: &InputPreprocessor,
Expand All @@ -71,7 +70,7 @@ impl Fsm for EllipseToolFsmState {
if let ToolMessage::Ellipse(event) = event {
match (self, event) {
(Ready, DragStart) => {
shape_data.drag_start = input.mouse.position;
shape_data.start(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 @@ -89,7 +88,7 @@ impl Fsm for EllipseToolFsmState {
Dragging
}
(state, Resize { center, lock_ratio }) => {
if let Some(message) = shape_data.calculate_transform(center, lock_ratio, input) {
if let Some(message) = shape_data.calculate_transform(document, center, lock_ratio, input) {
responses.push_back(message);
}

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

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

Ready
}
Expand Down
14 changes: 10 additions & 4 deletions editor/src/tool/tools/line.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::consts::LINE_ROTATE_SNAP_ANGLE;
use crate::input::keyboard::Key;
use crate::input::{mouse::ViewportPosition, InputPreprocessor};
use crate::tool::snapping::SnapHandler;
use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData, ToolOptions, ToolType};
use crate::{document::DocumentMessageHandler, message_prelude::*};
use glam::{DAffine2, DVec2};
Expand Down Expand Up @@ -53,6 +54,7 @@ struct LineToolData {
angle: f64,
weight: u32,
path: Option<Vec<LayerId>>,
snap_handler: SnapHandler,
}

impl Fsm for LineToolFsmState {
Expand All @@ -61,7 +63,7 @@ impl Fsm for LineToolFsmState {
fn transition(
self,
event: ToolMessage,
_document: &DocumentMessageHandler,
document: &DocumentMessageHandler,
tool_data: &DocumentToolData,
data: &mut Self::ToolData,
input: &InputPreprocessor,
Expand All @@ -72,7 +74,9 @@ impl Fsm for LineToolFsmState {
if let ToolMessage::Line(event) = event {
match (self, event) {
(Ready, DragStart) => {
data.drag_start = input.mouse.position;
data.snap_handler.start_snap(document, document.all_layers_sorted(), &[]);
data.drag_start = data.snap_handler.snap_position(document, input.mouse.position);

responses.push_back(DocumentMessage::StartTransaction.into());
data.path = Some(vec![generate_uuid()]);
responses.push_back(DocumentMessage::DeselectAllLayers.into());
Expand All @@ -95,15 +99,16 @@ impl Fsm for LineToolFsmState {
Dragging
}
(Dragging, Redraw { center, snap_angle, lock_angle }) => {
data.drag_current = input.mouse.position;
data.drag_current = data.snap_handler.snap_position(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]));

Dragging
}
(Dragging, DragStop) => {
data.drag_current = input.mouse.position;
data.drag_current = data.snap_handler.snap_position(document, input.mouse.position);
data.snap_handler.cleanup();

// 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 @@ -116,6 +121,7 @@ impl Fsm for LineToolFsmState {
Ready
}
(Dragging, Abort) => {
data.snap_handler.cleanup();
responses.push_back(DocumentMessage::AbortTransaction.into());
data.path = None;
Ready
Expand Down
15 changes: 14 additions & 1 deletion editor/src/tool/tools/pen.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::input::InputPreprocessor;
use crate::tool::snapping::SnapHandler;
use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData, ToolOptions, ToolType};
use crate::{document::DocumentMessageHandler, message_prelude::*};
use glam::DAffine2;
Expand Down Expand Up @@ -52,6 +53,7 @@ struct PenToolData {
next_point: DAffine2,
weight: u32,
path: Option<Vec<LayerId>>,
snap_handler: SnapHandler,
}

impl Fsm for PenToolFsmState {
Expand All @@ -67,7 +69,6 @@ impl Fsm for PenToolFsmState {
responses: &mut VecDeque<Message>,
) -> Self {
let transform = document.graphene_document.root.transform;
let pos = transform.inverse() * DAffine2::from_translation(input.mouse.position);

use PenMessage::*;
use PenToolFsmState::*;
Expand All @@ -78,6 +79,11 @@ impl Fsm for PenToolFsmState {
responses.push_back(DocumentMessage::DeselectAllLayers.into());
data.path = Some(vec![generate_uuid()]);

data.snap_handler.start_snap(document, document.all_layers_sorted(), &[]);
let snapped_position = data.snap_handler.snap_position(document, input.mouse.position);

let pos = transform.inverse() * DAffine2::from_translation(snapped_position);

data.points.push(pos);
data.next_point = pos;

Expand All @@ -89,6 +95,9 @@ impl Fsm for PenToolFsmState {
Dragging
}
(Dragging, DragStop) => {
let snapped_position = data.snap_handler.snap_position(document, input.mouse.position);
let pos = transform.inverse() * DAffine2::from_translation(snapped_position);

// TODO: introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100)
if data.points.last() != Some(&pos) {
data.points.push(pos);
Expand All @@ -100,6 +109,8 @@ impl Fsm for PenToolFsmState {
Dragging
}
(Dragging, PointerMove) => {
let snapped_position = data.snap_handler.snap_position(document, input.mouse.position);
let pos = transform.inverse() * DAffine2::from_translation(snapped_position);
data.next_point = pos;

responses.extend(make_operation(data, tool_data, true));
Expand All @@ -117,13 +128,15 @@ impl Fsm for PenToolFsmState {

data.path = None;
data.points.clear();
data.snap_handler.cleanup();

Ready
}
(Dragging, Abort) => {
responses.push_back(DocumentMessage::AbortTransaction.into());
data.points.clear();
data.path = None;
data.snap_handler.cleanup();

Ready
}
Expand Down
Loading