mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-12-23 10:11:54 +00:00
Layer snapping
* Test snapping * Snap new shapes * Fix snapping when zoomed * Refactor to use viewport bounds * Reduce snap tolerance to 3 * Snap line and path tool * Add disable snapping and refactor * new_snap -> new status * Rearrange import * Cleanup * Fix incorrect variable name * Store snap data in tool data
This commit is contained in:
parent
26835d8d29
commit
0e33498b9b
13 changed files with 229 additions and 46 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -127,6 +129,7 @@ pub enum DocumentMessage {
|
|||
insert_index: isize,
|
||||
},
|
||||
ReorderSelectedLayers(i32), // relative_position,
|
||||
SetSnapping(bool),
|
||||
}
|
||||
|
||||
impl From<DocumentOperation> for DocumentMessage {
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -784,6 +791,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
|||
RenderDocument,
|
||||
ExportDocument,
|
||||
SaveDocument,
|
||||
SetSnapping,
|
||||
);
|
||||
|
||||
if self.layer_data.values().any(|data| data.selected) {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
mod snapping;
|
||||
pub mod tool_message_handler;
|
||||
pub mod tool_options;
|
||||
pub mod tools;
|
||||
|
|
|
|||
112
editor/src/tool/snapping.rs
Normal file
112
editor/src/tool/snapping.rs
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -49,7 +49,6 @@ impl Default for EllipseToolFsmState {
|
|||
}
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct EllipseToolData {
|
||||
sides: u8,
|
||||
data: Resize,
|
||||
}
|
||||
|
||||
|
|
@ -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,
|
||||
|
|
@ -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());
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
|
@ -53,6 +54,7 @@ struct LineToolData {
|
|||
angle: f64,
|
||||
weight: u32,
|
||||
path: Option<Vec<LayerId>>,
|
||||
snap_handler: SnapHandler,
|
||||
}
|
||||
|
||||
impl Fsm for LineToolFsmState {
|
||||
|
|
@ -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,
|
||||
|
|
@ -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());
|
||||
|
|
@ -95,7 +99,7 @@ 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]));
|
||||
|
|
@ -103,7 +107,8 @@ impl Fsm for LineToolFsmState {
|
|||
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 {
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -52,6 +53,7 @@ struct PenToolData {
|
|||
next_point: DAffine2,
|
||||
weight: u32,
|
||||
path: Option<Vec<LayerId>>,
|
||||
snap_handler: SnapHandler,
|
||||
}
|
||||
|
||||
impl Fsm for PenToolFsmState {
|
||||
|
|
@ -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::*;
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
@ -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);
|
||||
|
|
@ -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));
|
||||
|
|
@ -117,6 +128,7 @@ impl Fsm for PenToolFsmState {
|
|||
|
||||
data.path = None;
|
||||
data.points.clear();
|
||||
data.snap_handler.cleanup();
|
||||
|
||||
Ready
|
||||
}
|
||||
|
|
@ -124,6 +136,7 @@ impl Fsm for PenToolFsmState {
|
|||
responses.push_back(DocumentMessage::AbortTransaction.into());
|
||||
data.points.clear();
|
||||
data.path = None;
|
||||
data.snap_handler.cleanup();
|
||||
|
||||
Ready
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,7 +49,6 @@ impl Default for RectangleToolFsmState {
|
|||
}
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct RectangleToolData {
|
||||
sides: u8,
|
||||
data: Resize,
|
||||
}
|
||||
|
||||
|
|
@ -59,7 +58,7 @@ impl Fsm for RectangleToolFsmState {
|
|||
fn transition(
|
||||
self,
|
||||
event: ToolMessage,
|
||||
_document: &DocumentMessageHandler,
|
||||
document: &DocumentMessageHandler,
|
||||
tool_data: &DocumentToolData,
|
||||
data: &mut Self::ToolData,
|
||||
input: &InputPreprocessor,
|
||||
|
|
@ -71,7 +70,7 @@ impl Fsm for RectangleToolFsmState {
|
|||
if let ToolMessage::Rectangle(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());
|
||||
|
|
@ -89,7 +88,7 @@ impl Fsm for RectangleToolFsmState {
|
|||
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);
|
||||
}
|
||||
|
||||
|
|
@ -102,12 +101,12 @@ impl Fsm for RectangleToolFsmState {
|
|||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,34 +1,54 @@
|
|||
use crate::input::keyboard::Key;
|
||||
use crate::input::{mouse::ViewportPosition, InputPreprocessor};
|
||||
use crate::message_prelude::*;
|
||||
use glam::{DAffine2, Vec2Swizzles};
|
||||
use crate::tool::snapping::SnapHandler;
|
||||
use crate::tool::DocumentMessageHandler;
|
||||
use glam::{DAffine2, DVec2, Vec2Swizzles};
|
||||
use graphene::Operation;
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Resize {
|
||||
pub drag_start: ViewportPosition,
|
||||
pub path: Option<Vec<LayerId>>,
|
||||
snap_handler: SnapHandler,
|
||||
}
|
||||
impl Resize {
|
||||
pub fn calculate_transform(&self, center: Key, lock_ratio: Key, ipp: &InputPreprocessor) -> Option<Message> {
|
||||
let mut start = self.drag_start;
|
||||
let stop = ipp.mouse.position;
|
||||
/// Starts a resize, assigning the snap targets and snapping the starting position.
|
||||
pub fn start(&mut self, document: &DocumentMessageHandler, mouse_position: DVec2) {
|
||||
let layers = document.all_layers_sorted();
|
||||
self.snap_handler.start_snap(document, layers, &[]);
|
||||
self.drag_start = self.snap_handler.snap_position(document, mouse_position);
|
||||
}
|
||||
|
||||
let mut size = stop - start;
|
||||
if ipp.keyboard.get(lock_ratio as usize) {
|
||||
size = size.abs().max(size.abs().yx()) * size.signum();
|
||||
}
|
||||
if ipp.keyboard.get(center as usize) {
|
||||
start -= size;
|
||||
size *= 2.;
|
||||
}
|
||||
pub fn calculate_transform(&self, document: &DocumentMessageHandler, center: Key, lock_ratio: Key, ipp: &InputPreprocessor) -> Option<Message> {
|
||||
if let Some(path) = &self.path {
|
||||
let mut start = self.drag_start;
|
||||
|
||||
self.path.clone().map(|path| {
|
||||
Operation::SetLayerTransformInViewport {
|
||||
path,
|
||||
transform: DAffine2::from_scale_angle_translation(size, 0., start).to_cols_array(),
|
||||
let stop = self.snap_handler.snap_position(document, ipp.mouse.position);
|
||||
|
||||
let mut size = stop - start;
|
||||
if ipp.keyboard.get(lock_ratio as usize) {
|
||||
size = size.abs().max(size.abs().yx()) * size.signum();
|
||||
}
|
||||
.into()
|
||||
})
|
||||
if ipp.keyboard.get(center as usize) {
|
||||
start -= size;
|
||||
size *= 2.;
|
||||
}
|
||||
|
||||
Some(
|
||||
Operation::SetLayerTransformInViewport {
|
||||
path: path.to_vec(),
|
||||
transform: DAffine2::from_scale_angle_translation(size, 0., start).to_cols_array(),
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cleanup(&mut self) {
|
||||
self.snap_handler.cleanup();
|
||||
self.path = None;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,18 +4,18 @@ use graphene::layers::style::Stroke;
|
|||
use graphene::Operation;
|
||||
use graphene::Quad;
|
||||
|
||||
use glam::{DAffine2, DVec2};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::consts::COLOR_ACCENT;
|
||||
use crate::input::keyboard::Key;
|
||||
use crate::input::{mouse::ViewportPosition, InputPreprocessor};
|
||||
use crate::tool::snapping::SnapHandler;
|
||||
use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
|
||||
use crate::{
|
||||
consts::SELECTION_TOLERANCE,
|
||||
document::{AlignAggregate, AlignAxis, DocumentMessageHandler, FlipAxis},
|
||||
message_prelude::*,
|
||||
};
|
||||
use glam::{DAffine2, DVec2};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Select {
|
||||
|
|
@ -71,6 +71,7 @@ struct SelectToolData {
|
|||
layers_dragging: Vec<Vec<LayerId>>, // Paths and offsets
|
||||
drag_box_id: Option<Vec<LayerId>>,
|
||||
bounding_box_path: Option<Vec<LayerId>>,
|
||||
snap_handler: SnapHandler,
|
||||
}
|
||||
|
||||
impl SelectToolData {
|
||||
|
|
@ -173,20 +174,31 @@ impl Fsm for SelectToolFsmState {
|
|||
DrawingBox
|
||||
};
|
||||
buffer.into_iter().rev().for_each(|message| responses.push_front(message));
|
||||
|
||||
let ignore_layers = if let Some(bounding_box) = &data.bounding_box_path {
|
||||
vec![bounding_box.clone()]
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
data.snap_handler.start_snap(document, document.non_selected_layers_sorted(), &ignore_layers);
|
||||
state
|
||||
}
|
||||
(Dragging, MouseMove) => {
|
||||
responses.push_front(SelectMessage::UpdateSelectionBoundingBox.into());
|
||||
|
||||
let mouse_delta = input.mouse.position - data.drag_current;
|
||||
|
||||
let closest_move = data.snap_handler.snap_layers(document, &data.layers_dragging, mouse_delta);
|
||||
for path in data.layers_dragging.iter() {
|
||||
responses.push_front(
|
||||
Operation::TransformLayerInViewport {
|
||||
path: path.clone(),
|
||||
transform: DAffine2::from_translation(input.mouse.position - data.drag_current).to_cols_array(),
|
||||
transform: DAffine2::from_translation(input.mouse.position - data.drag_current + closest_move).to_cols_array(),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
data.drag_current = input.mouse.position;
|
||||
data.drag_current = input.mouse.position + closest_move;
|
||||
Dragging
|
||||
}
|
||||
(DrawingBox, MouseMove) => {
|
||||
|
|
@ -209,6 +221,7 @@ impl Fsm for SelectToolFsmState {
|
|||
true => DocumentMessage::Undo,
|
||||
false => DocumentMessage::CommitTransaction,
|
||||
};
|
||||
data.snap_handler.cleanup();
|
||||
responses.push_front(response.into());
|
||||
Ready
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ impl Fsm for ShapeToolFsmState {
|
|||
fn transition(
|
||||
self,
|
||||
event: ToolMessage,
|
||||
_document: &DocumentMessageHandler,
|
||||
document: &DocumentMessageHandler,
|
||||
tool_data: &DocumentToolData,
|
||||
data: &mut Self::ToolData,
|
||||
input: &InputPreprocessor,
|
||||
|
|
@ -71,7 +71,7 @@ impl Fsm for ShapeToolFsmState {
|
|||
if let ToolMessage::Shape(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());
|
||||
|
|
@ -96,7 +96,7 @@ impl Fsm for ShapeToolFsmState {
|
|||
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);
|
||||
}
|
||||
|
||||
|
|
@ -109,12 +109,12 @@ impl Fsm for ShapeToolFsmState {
|
|||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
</div>
|
||||
<div class="spacer"></div>
|
||||
<div class="right side">
|
||||
<OptionalInput v-model:checked="snappingEnabled" @update:checked="comingSoon(200)" :icon="'Snapping'" title="Snapping" />
|
||||
<OptionalInput v-model:checked="snappingEnabled" @update:checked="setSnap" :icon="'Snapping'" title="Snapping" />
|
||||
<PopoverButton>
|
||||
<h3>Snapping</h3>
|
||||
<p>The contents of this popover menu are coming soon</p>
|
||||
|
|
@ -263,6 +263,9 @@ const viewModeEntries: RadioEntries = [
|
|||
|
||||
export default defineComponent({
|
||||
methods: {
|
||||
async setSnap(newStatus: boolean) {
|
||||
(await wasm).set_snapping(newStatus);
|
||||
},
|
||||
async viewportResize() {
|
||||
const canvas = this.$refs.canvas as HTMLElement;
|
||||
// Get the width and height rounded up to the nearest even number because resizing is centered and dividing an odd number by 2 for centering causes antialiasing
|
||||
|
|
|
|||
|
|
@ -326,6 +326,13 @@ pub fn export_document() {
|
|||
dispatch(message);
|
||||
}
|
||||
|
||||
/// Set snapping disabled / enabled
|
||||
#[wasm_bindgen]
|
||||
pub fn set_snapping(new_status: bool) {
|
||||
let message = DocumentMessage::SetSnapping(new_status);
|
||||
dispatch(message);
|
||||
}
|
||||
|
||||
/// Sets the zoom to the value
|
||||
#[wasm_bindgen]
|
||||
pub fn set_canvas_zoom(new_zoom: f64) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue