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
|
|
@ -15,6 +15,8 @@ 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.;
|
||||||
|
|
||||||
// TRANSFORMING LAYER
|
// TRANSFORMING LAYER
|
||||||
pub const ROTATE_SNAP_ANGLE: f64 = 15.;
|
pub const ROTATE_SNAP_ANGLE: f64 = 15.;
|
||||||
pub const SCALE_SNAP_INTERVAL: f64 = 0.1;
|
pub const SCALE_SNAP_INTERVAL: f64 = 0.1;
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,7 @@ pub struct DocumentMessageHandler {
|
||||||
pub layer_data: HashMap<Vec<LayerId>, LayerData>,
|
pub layer_data: HashMap<Vec<LayerId>, LayerData>,
|
||||||
movement_handler: MovementMessageHandler,
|
movement_handler: MovementMessageHandler,
|
||||||
transform_layer_handler: TransformLayerMessageHandler,
|
transform_layer_handler: TransformLayerMessageHandler,
|
||||||
|
pub snapping_enabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for DocumentMessageHandler {
|
impl Default for DocumentMessageHandler {
|
||||||
|
|
@ -77,6 +78,7 @@ impl Default for DocumentMessageHandler {
|
||||||
layer_data: vec![(vec![], LayerData::new(true))].into_iter().collect(),
|
layer_data: vec![(vec![], LayerData::new(true))].into_iter().collect(),
|
||||||
movement_handler: MovementMessageHandler::default(),
|
movement_handler: MovementMessageHandler::default(),
|
||||||
transform_layer_handler: TransformLayerMessageHandler::default(),
|
transform_layer_handler: TransformLayerMessageHandler::default(),
|
||||||
|
snapping_enabled: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -127,6 +129,7 @@ pub enum DocumentMessage {
|
||||||
insert_index: isize,
|
insert_index: isize,
|
||||||
},
|
},
|
||||||
ReorderSelectedLayers(i32), // relative_position,
|
ReorderSelectedLayers(i32), // relative_position,
|
||||||
|
SetSnapping(bool),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<DocumentOperation> for DocumentMessage {
|
impl From<DocumentOperation> for DocumentMessage {
|
||||||
|
|
@ -308,6 +311,7 @@ impl DocumentMessageHandler {
|
||||||
layer_data: vec![(vec![], LayerData::new(true))].into_iter().collect(),
|
layer_data: vec![(vec![], LayerData::new(true))].into_iter().collect(),
|
||||||
movement_handler: MovementMessageHandler::default(),
|
movement_handler: MovementMessageHandler::default(),
|
||||||
transform_layer_handler: TransformLayerMessageHandler::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()),
|
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,
|
RenderDocument,
|
||||||
ExportDocument,
|
ExportDocument,
|
||||||
SaveDocument,
|
SaveDocument,
|
||||||
|
SetSnapping,
|
||||||
);
|
);
|
||||||
|
|
||||||
if self.layer_data.values().any(|data| data.selected) {
|
if self.layer_data.values().any(|data| data.selected) {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
mod snapping;
|
||||||
pub mod tool_message_handler;
|
pub mod tool_message_handler;
|
||||||
pub mod tool_options;
|
pub mod tool_options;
|
||||||
pub mod tools;
|
pub mod tools;
|
||||||
|
|
|
||||||
|
|
@ -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)]
|
#[derive(Clone, Debug, Default)]
|
||||||
struct EllipseToolData {
|
struct EllipseToolData {
|
||||||
sides: u8,
|
|
||||||
data: Resize,
|
data: Resize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -59,7 +58,7 @@ impl Fsm for EllipseToolFsmState {
|
||||||
fn transition(
|
fn transition(
|
||||||
self,
|
self,
|
||||||
event: ToolMessage,
|
event: ToolMessage,
|
||||||
_document: &DocumentMessageHandler,
|
document: &DocumentMessageHandler,
|
||||||
tool_data: &DocumentToolData,
|
tool_data: &DocumentToolData,
|
||||||
data: &mut Self::ToolData,
|
data: &mut Self::ToolData,
|
||||||
input: &InputPreprocessor,
|
input: &InputPreprocessor,
|
||||||
|
|
@ -71,7 +70,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.drag_start = input.mouse.position;
|
shape_data.start(document, input.mouse.position);
|
||||||
responses.push_back(DocumentMessage::StartTransaction.into());
|
responses.push_back(DocumentMessage::StartTransaction.into());
|
||||||
shape_data.path = Some(vec![generate_uuid()]);
|
shape_data.path = Some(vec![generate_uuid()]);
|
||||||
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
||||||
|
|
@ -89,7 +88,7 @@ impl Fsm for EllipseToolFsmState {
|
||||||
Dragging
|
Dragging
|
||||||
}
|
}
|
||||||
(state, Resize { center, lock_ratio }) => {
|
(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);
|
responses.push_back(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -102,12 +101,12 @@ impl Fsm for EllipseToolFsmState {
|
||||||
false => responses.push_back(DocumentMessage::CommitTransaction.into()),
|
false => responses.push_back(DocumentMessage::CommitTransaction.into()),
|
||||||
}
|
}
|
||||||
|
|
||||||
shape_data.path = None;
|
shape_data.cleanup();
|
||||||
Ready
|
Ready
|
||||||
}
|
}
|
||||||
(Dragging, Abort) => {
|
(Dragging, Abort) => {
|
||||||
responses.push_back(DocumentMessage::AbortTransaction.into());
|
responses.push_back(DocumentMessage::AbortTransaction.into());
|
||||||
shape_data.path = None;
|
shape_data.cleanup();
|
||||||
|
|
||||||
Ready
|
Ready
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use crate::consts::LINE_ROTATE_SNAP_ANGLE;
|
use crate::consts::LINE_ROTATE_SNAP_ANGLE;
|
||||||
use crate::input::keyboard::Key;
|
use crate::input::keyboard::Key;
|
||||||
use crate::input::{mouse::ViewportPosition, InputPreprocessor};
|
use crate::input::{mouse::ViewportPosition, InputPreprocessor};
|
||||||
|
use crate::tool::snapping::SnapHandler;
|
||||||
use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData, ToolOptions, ToolType};
|
use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData, ToolOptions, ToolType};
|
||||||
use crate::{document::DocumentMessageHandler, message_prelude::*};
|
use crate::{document::DocumentMessageHandler, message_prelude::*};
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
|
|
@ -53,6 +54,7 @@ struct LineToolData {
|
||||||
angle: f64,
|
angle: f64,
|
||||||
weight: u32,
|
weight: u32,
|
||||||
path: Option<Vec<LayerId>>,
|
path: Option<Vec<LayerId>>,
|
||||||
|
snap_handler: SnapHandler,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Fsm for LineToolFsmState {
|
impl Fsm for LineToolFsmState {
|
||||||
|
|
@ -61,7 +63,7 @@ impl Fsm for LineToolFsmState {
|
||||||
fn transition(
|
fn transition(
|
||||||
self,
|
self,
|
||||||
event: ToolMessage,
|
event: ToolMessage,
|
||||||
_document: &DocumentMessageHandler,
|
document: &DocumentMessageHandler,
|
||||||
tool_data: &DocumentToolData,
|
tool_data: &DocumentToolData,
|
||||||
data: &mut Self::ToolData,
|
data: &mut Self::ToolData,
|
||||||
input: &InputPreprocessor,
|
input: &InputPreprocessor,
|
||||||
|
|
@ -72,7 +74,9 @@ impl Fsm for LineToolFsmState {
|
||||||
if let ToolMessage::Line(event) = event {
|
if let ToolMessage::Line(event) = event {
|
||||||
match (self, event) {
|
match (self, event) {
|
||||||
(Ready, DragStart) => {
|
(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());
|
responses.push_back(DocumentMessage::StartTransaction.into());
|
||||||
data.path = Some(vec![generate_uuid()]);
|
data.path = Some(vec![generate_uuid()]);
|
||||||
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
||||||
|
|
@ -95,7 +99,7 @@ impl Fsm for LineToolFsmState {
|
||||||
Dragging
|
Dragging
|
||||||
}
|
}
|
||||||
(Dragging, Redraw { center, snap_angle, lock_angle }) => {
|
(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();
|
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]));
|
||||||
|
|
@ -103,7 +107,8 @@ impl Fsm for LineToolFsmState {
|
||||||
Dragging
|
Dragging
|
||||||
}
|
}
|
||||||
(Dragging, DragStop) => {
|
(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)
|
// TODO; introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100)
|
||||||
match data.drag_start == input.mouse.position {
|
match data.drag_start == input.mouse.position {
|
||||||
|
|
@ -116,6 +121,7 @@ impl Fsm for LineToolFsmState {
|
||||||
Ready
|
Ready
|
||||||
}
|
}
|
||||||
(Dragging, Abort) => {
|
(Dragging, Abort) => {
|
||||||
|
data.snap_handler.cleanup();
|
||||||
responses.push_back(DocumentMessage::AbortTransaction.into());
|
responses.push_back(DocumentMessage::AbortTransaction.into());
|
||||||
data.path = None;
|
data.path = None;
|
||||||
Ready
|
Ready
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use crate::input::InputPreprocessor;
|
use crate::input::InputPreprocessor;
|
||||||
|
use crate::tool::snapping::SnapHandler;
|
||||||
use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData, ToolOptions, ToolType};
|
use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData, ToolOptions, ToolType};
|
||||||
use crate::{document::DocumentMessageHandler, message_prelude::*};
|
use crate::{document::DocumentMessageHandler, message_prelude::*};
|
||||||
use glam::DAffine2;
|
use glam::DAffine2;
|
||||||
|
|
@ -52,6 +53,7 @@ struct PenToolData {
|
||||||
next_point: DAffine2,
|
next_point: DAffine2,
|
||||||
weight: u32,
|
weight: u32,
|
||||||
path: Option<Vec<LayerId>>,
|
path: Option<Vec<LayerId>>,
|
||||||
|
snap_handler: SnapHandler,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Fsm for PenToolFsmState {
|
impl Fsm for PenToolFsmState {
|
||||||
|
|
@ -67,7 +69,6 @@ impl Fsm for PenToolFsmState {
|
||||||
responses: &mut VecDeque<Message>,
|
responses: &mut VecDeque<Message>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let transform = document.graphene_document.root.transform;
|
let transform = document.graphene_document.root.transform;
|
||||||
let pos = transform.inverse() * DAffine2::from_translation(input.mouse.position);
|
|
||||||
|
|
||||||
use PenMessage::*;
|
use PenMessage::*;
|
||||||
use PenToolFsmState::*;
|
use PenToolFsmState::*;
|
||||||
|
|
@ -78,6 +79,11 @@ impl Fsm for PenToolFsmState {
|
||||||
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
||||||
data.path = Some(vec![generate_uuid()]);
|
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.points.push(pos);
|
||||||
data.next_point = pos;
|
data.next_point = pos;
|
||||||
|
|
||||||
|
|
@ -89,6 +95,9 @@ impl Fsm for PenToolFsmState {
|
||||||
Dragging
|
Dragging
|
||||||
}
|
}
|
||||||
(Dragging, DragStop) => {
|
(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)
|
// TODO: introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100)
|
||||||
if data.points.last() != Some(&pos) {
|
if data.points.last() != Some(&pos) {
|
||||||
data.points.push(pos);
|
data.points.push(pos);
|
||||||
|
|
@ -100,6 +109,8 @@ impl Fsm for PenToolFsmState {
|
||||||
Dragging
|
Dragging
|
||||||
}
|
}
|
||||||
(Dragging, PointerMove) => {
|
(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;
|
data.next_point = pos;
|
||||||
|
|
||||||
responses.extend(make_operation(data, tool_data, true));
|
responses.extend(make_operation(data, tool_data, true));
|
||||||
|
|
@ -117,6 +128,7 @@ impl Fsm for PenToolFsmState {
|
||||||
|
|
||||||
data.path = None;
|
data.path = None;
|
||||||
data.points.clear();
|
data.points.clear();
|
||||||
|
data.snap_handler.cleanup();
|
||||||
|
|
||||||
Ready
|
Ready
|
||||||
}
|
}
|
||||||
|
|
@ -124,6 +136,7 @@ impl Fsm for PenToolFsmState {
|
||||||
responses.push_back(DocumentMessage::AbortTransaction.into());
|
responses.push_back(DocumentMessage::AbortTransaction.into());
|
||||||
data.points.clear();
|
data.points.clear();
|
||||||
data.path = None;
|
data.path = None;
|
||||||
|
data.snap_handler.cleanup();
|
||||||
|
|
||||||
Ready
|
Ready
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,6 @@ impl Default for RectangleToolFsmState {
|
||||||
}
|
}
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
struct RectangleToolData {
|
struct RectangleToolData {
|
||||||
sides: u8,
|
|
||||||
data: Resize,
|
data: Resize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -59,7 +58,7 @@ impl Fsm for RectangleToolFsmState {
|
||||||
fn transition(
|
fn transition(
|
||||||
self,
|
self,
|
||||||
event: ToolMessage,
|
event: ToolMessage,
|
||||||
_document: &DocumentMessageHandler,
|
document: &DocumentMessageHandler,
|
||||||
tool_data: &DocumentToolData,
|
tool_data: &DocumentToolData,
|
||||||
data: &mut Self::ToolData,
|
data: &mut Self::ToolData,
|
||||||
input: &InputPreprocessor,
|
input: &InputPreprocessor,
|
||||||
|
|
@ -71,7 +70,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.drag_start = input.mouse.position;
|
shape_data.start(document, input.mouse.position);
|
||||||
responses.push_back(DocumentMessage::StartTransaction.into());
|
responses.push_back(DocumentMessage::StartTransaction.into());
|
||||||
shape_data.path = Some(vec![generate_uuid()]);
|
shape_data.path = Some(vec![generate_uuid()]);
|
||||||
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
||||||
|
|
@ -89,7 +88,7 @@ impl Fsm for RectangleToolFsmState {
|
||||||
Dragging
|
Dragging
|
||||||
}
|
}
|
||||||
(state, Resize { center, lock_ratio }) => {
|
(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);
|
responses.push_back(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -102,12 +101,12 @@ impl Fsm for RectangleToolFsmState {
|
||||||
false => responses.push_back(DocumentMessage::CommitTransaction.into()),
|
false => responses.push_back(DocumentMessage::CommitTransaction.into()),
|
||||||
}
|
}
|
||||||
|
|
||||||
shape_data.path = None;
|
shape_data.cleanup();
|
||||||
Ready
|
Ready
|
||||||
}
|
}
|
||||||
(Dragging, Abort) => {
|
(Dragging, Abort) => {
|
||||||
responses.push_back(DocumentMessage::AbortTransaction.into());
|
responses.push_back(DocumentMessage::AbortTransaction.into());
|
||||||
shape_data.path = None;
|
shape_data.cleanup();
|
||||||
|
|
||||||
Ready
|
Ready
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,54 @@
|
||||||
use crate::input::keyboard::Key;
|
use crate::input::keyboard::Key;
|
||||||
use crate::input::{mouse::ViewportPosition, InputPreprocessor};
|
use crate::input::{mouse::ViewportPosition, InputPreprocessor};
|
||||||
use crate::message_prelude::*;
|
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;
|
use graphene::Operation;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub struct Resize {
|
pub struct Resize {
|
||||||
pub drag_start: ViewportPosition,
|
pub drag_start: ViewportPosition,
|
||||||
pub path: Option<Vec<LayerId>>,
|
pub path: Option<Vec<LayerId>>,
|
||||||
|
snap_handler: SnapHandler,
|
||||||
}
|
}
|
||||||
impl Resize {
|
impl Resize {
|
||||||
pub fn calculate_transform(&self, center: Key, lock_ratio: Key, ipp: &InputPreprocessor) -> Option<Message> {
|
/// Starts a resize, assigning the snap targets and snapping the starting position.
|
||||||
let mut start = self.drag_start;
|
pub fn start(&mut self, document: &DocumentMessageHandler, mouse_position: DVec2) {
|
||||||
let stop = ipp.mouse.position;
|
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;
|
pub fn calculate_transform(&self, document: &DocumentMessageHandler, center: Key, lock_ratio: Key, ipp: &InputPreprocessor) -> Option<Message> {
|
||||||
if ipp.keyboard.get(lock_ratio as usize) {
|
if let Some(path) = &self.path {
|
||||||
size = size.abs().max(size.abs().yx()) * size.signum();
|
let mut start = self.drag_start;
|
||||||
}
|
|
||||||
if ipp.keyboard.get(center as usize) {
|
|
||||||
start -= size;
|
|
||||||
size *= 2.;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.path.clone().map(|path| {
|
let stop = self.snap_handler.snap_position(document, ipp.mouse.position);
|
||||||
Operation::SetLayerTransformInViewport {
|
|
||||||
path,
|
let mut size = stop - start;
|
||||||
transform: DAffine2::from_scale_angle_translation(size, 0., start).to_cols_array(),
|
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::Operation;
|
||||||
use graphene::Quad;
|
use graphene::Quad;
|
||||||
|
|
||||||
use glam::{DAffine2, DVec2};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::consts::COLOR_ACCENT;
|
use crate::consts::COLOR_ACCENT;
|
||||||
use crate::input::keyboard::Key;
|
use crate::input::keyboard::Key;
|
||||||
use crate::input::{mouse::ViewportPosition, InputPreprocessor};
|
use crate::input::{mouse::ViewportPosition, InputPreprocessor};
|
||||||
|
use crate::tool::snapping::SnapHandler;
|
||||||
use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
|
use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
|
||||||
use crate::{
|
use crate::{
|
||||||
consts::SELECTION_TOLERANCE,
|
consts::SELECTION_TOLERANCE,
|
||||||
document::{AlignAggregate, AlignAxis, DocumentMessageHandler, FlipAxis},
|
document::{AlignAggregate, AlignAxis, DocumentMessageHandler, FlipAxis},
|
||||||
message_prelude::*,
|
message_prelude::*,
|
||||||
};
|
};
|
||||||
|
use glam::{DAffine2, DVec2};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct Select {
|
pub struct Select {
|
||||||
|
|
@ -71,6 +71,7 @@ struct SelectToolData {
|
||||||
layers_dragging: Vec<Vec<LayerId>>, // Paths and offsets
|
layers_dragging: Vec<Vec<LayerId>>, // Paths and offsets
|
||||||
drag_box_id: Option<Vec<LayerId>>,
|
drag_box_id: Option<Vec<LayerId>>,
|
||||||
bounding_box_path: Option<Vec<LayerId>>,
|
bounding_box_path: Option<Vec<LayerId>>,
|
||||||
|
snap_handler: SnapHandler,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SelectToolData {
|
impl SelectToolData {
|
||||||
|
|
@ -173,20 +174,31 @@ impl Fsm for SelectToolFsmState {
|
||||||
DrawingBox
|
DrawingBox
|
||||||
};
|
};
|
||||||
buffer.into_iter().rev().for_each(|message| responses.push_front(message));
|
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
|
state
|
||||||
}
|
}
|
||||||
(Dragging, MouseMove) => {
|
(Dragging, MouseMove) => {
|
||||||
responses.push_front(SelectMessage::UpdateSelectionBoundingBox.into());
|
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() {
|
for path in data.layers_dragging.iter() {
|
||||||
responses.push_front(
|
responses.push_front(
|
||||||
Operation::TransformLayerInViewport {
|
Operation::TransformLayerInViewport {
|
||||||
path: path.clone(),
|
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(),
|
.into(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
data.drag_current = input.mouse.position;
|
data.drag_current = input.mouse.position + closest_move;
|
||||||
Dragging
|
Dragging
|
||||||
}
|
}
|
||||||
(DrawingBox, MouseMove) => {
|
(DrawingBox, MouseMove) => {
|
||||||
|
|
@ -209,6 +221,7 @@ impl Fsm for SelectToolFsmState {
|
||||||
true => DocumentMessage::Undo,
|
true => DocumentMessage::Undo,
|
||||||
false => DocumentMessage::CommitTransaction,
|
false => DocumentMessage::CommitTransaction,
|
||||||
};
|
};
|
||||||
|
data.snap_handler.cleanup();
|
||||||
responses.push_front(response.into());
|
responses.push_front(response.into());
|
||||||
Ready
|
Ready
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ impl Fsm for ShapeToolFsmState {
|
||||||
fn transition(
|
fn transition(
|
||||||
self,
|
self,
|
||||||
event: ToolMessage,
|
event: ToolMessage,
|
||||||
_document: &DocumentMessageHandler,
|
document: &DocumentMessageHandler,
|
||||||
tool_data: &DocumentToolData,
|
tool_data: &DocumentToolData,
|
||||||
data: &mut Self::ToolData,
|
data: &mut Self::ToolData,
|
||||||
input: &InputPreprocessor,
|
input: &InputPreprocessor,
|
||||||
|
|
@ -71,7 +71,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.drag_start = input.mouse.position;
|
shape_data.start(document, input.mouse.position);
|
||||||
responses.push_back(DocumentMessage::StartTransaction.into());
|
responses.push_back(DocumentMessage::StartTransaction.into());
|
||||||
shape_data.path = Some(vec![generate_uuid()]);
|
shape_data.path = Some(vec![generate_uuid()]);
|
||||||
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
||||||
|
|
@ -96,7 +96,7 @@ impl Fsm for ShapeToolFsmState {
|
||||||
Dragging
|
Dragging
|
||||||
}
|
}
|
||||||
(state, Resize { center, lock_ratio }) => {
|
(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);
|
responses.push_back(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -109,12 +109,12 @@ impl Fsm for ShapeToolFsmState {
|
||||||
false => responses.push_back(DocumentMessage::CommitTransaction.into()),
|
false => responses.push_back(DocumentMessage::CommitTransaction.into()),
|
||||||
}
|
}
|
||||||
|
|
||||||
shape_data.path = None;
|
shape_data.cleanup();
|
||||||
Ready
|
Ready
|
||||||
}
|
}
|
||||||
(Dragging, Abort) => {
|
(Dragging, Abort) => {
|
||||||
responses.push_back(DocumentMessage::AbortTransaction.into());
|
responses.push_back(DocumentMessage::AbortTransaction.into());
|
||||||
shape_data.path = None;
|
shape_data.cleanup();
|
||||||
|
|
||||||
Ready
|
Ready
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
<div class="right side">
|
<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>
|
<PopoverButton>
|
||||||
<h3>Snapping</h3>
|
<h3>Snapping</h3>
|
||||||
<p>The contents of this popover menu are coming soon</p>
|
<p>The contents of this popover menu are coming soon</p>
|
||||||
|
|
@ -263,6 +263,9 @@ const viewModeEntries: RadioEntries = [
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
methods: {
|
methods: {
|
||||||
|
async setSnap(newStatus: boolean) {
|
||||||
|
(await wasm).set_snapping(newStatus);
|
||||||
|
},
|
||||||
async viewportResize() {
|
async viewportResize() {
|
||||||
const canvas = this.$refs.canvas as HTMLElement;
|
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
|
// 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);
|
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
|
/// Sets the zoom to the value
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn set_canvas_zoom(new_zoom: f64) {
|
pub fn set_canvas_zoom(new_zoom: f64) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue