Transform API (#301)

* Enforce cache dirtification

* Turn all shapes into one struct

* Remove working folder

* Remove old shapes

* Major restructuring

* Refactor Ellipse, Rectangle and ShapeTool

* Simplify bounding box calculation for folder

* Fix panic in select tool

*  Refactorselect tool

* Refactor Align

* Refactor flipping layers

* Zoom to fit all

* Refactor tools to avoid state keeping

* Refactor more tools to use state that is passed along

* Fix whitespace + change selection box style

* Set viewbox of svg export based on the contents
This commit is contained in:
TrueDoctor
2021-08-06 12:34:30 +02:00
committed by Keavon Chambers
parent a9364f5122
commit da290f1e63
55 changed files with 2059 additions and 2251 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ bitflags = "1.2.1"
thiserror = "1.0.24"
serde = { version = "1.0", features = ["derive"] }
graphite-proc-macros = { path = "../proc-macro" }
glam = { version="0.16", features = ["serde"] }
glam = { version="0.17", features = ["serde"] }
[dependencies.document-core]
path = "../document"
+49 -54
View File
@@ -1,6 +1,6 @@
use crate::{frontend::FrontendMessageHandler, message_prelude::*, Callback, EditorError};
pub use crate::document::DocumentMessageHandler;
pub use crate::document::DocumentsMessageHandler;
pub use crate::input::{InputMapper, InputPreprocessor};
pub use crate::tool::ToolMessageHandler;
@@ -13,7 +13,7 @@ pub struct Dispatcher {
input_mapper: InputMapper,
global_message_handler: GlobalMessageHandler,
tool_message_handler: ToolMessageHandler,
document_message_handler: DocumentMessageHandler,
documents_message_handler: DocumentsMessageHandler,
messages: VecDeque<Message>,
}
@@ -25,23 +25,23 @@ impl Dispatcher {
message,
Message::InputPreprocessor(_)
| Message::InputMapper(_)
| Message::Document(DocumentMessage::RenderDocument)
| Message::Documents(DocumentsMessage::Document(DocumentMessage::RenderDocument))
| Message::Frontend(FrontendMessage::UpdateCanvas { .. })
| Message::Frontend(FrontendMessage::SetCanvasZoom { .. })
| Message::Frontend(FrontendMessage::SetCanvasRotation { .. })
| Message::Document(DocumentMessage::DispatchOperation { .. })
| Message::Documents(DocumentsMessage::Document(DocumentMessage::DispatchOperation { .. }))
) || MessageDiscriminant::from(&message).local_name().ends_with("MouseMove"))
{
log::trace!("Message: {}", message.to_discriminant().local_name());
log::trace!("Hints:{}", self.input_mapper.hints(self.collect_actions()));
log::trace!("Message: {:?}", message);
//log::trace!("Hints:{:?}", self.input_mapper.hints(self.collect_actions()));
}
match message {
NoOp => (),
Document(message) => self.document_message_handler.process_action(message, &self.input_preprocessor, &mut self.messages),
Documents(message) => self.documents_message_handler.process_action(message, &self.input_preprocessor, &mut self.messages),
Global(message) => self.global_message_handler.process_action(message, (), &mut self.messages),
Tool(message) => self
.tool_message_handler
.process_action(message, (&self.document_message_handler.active_document(), &self.input_preprocessor), &mut self.messages),
.process_action(message, (self.documents_message_handler.active_document(), &self.input_preprocessor), &mut self.messages),
Frontend(message) => self.frontend_message_handler.process_action(message, (), &mut self.messages),
InputPreprocessor(message) => self.input_preprocessor.process_action(message, (), &mut self.messages),
InputMapper(message) => {
@@ -63,7 +63,7 @@ impl Dispatcher {
list.extend(self.input_mapper.actions());
list.extend(self.global_message_handler.actions());
list.extend(self.tool_message_handler.actions());
list.extend(self.document_message_handler.actions());
list.extend(self.documents_message_handler.actions());
list
}
@@ -73,7 +73,7 @@ impl Dispatcher {
input_preprocessor: InputPreprocessor::default(),
global_message_handler: GlobalMessageHandler::new(),
input_mapper: InputMapper::default(),
document_message_handler: DocumentMessageHandler::default(),
documents_message_handler: DocumentsMessageHandler::default(),
tool_message_handler: ToolMessageHandler::default(),
messages: VecDeque::new(),
}
@@ -82,12 +82,7 @@ impl Dispatcher {
#[cfg(test)]
mod test {
use crate::{
communication::DocumentMessageHandler,
message_prelude::{DocumentMessage, Message},
misc::test_utils::EditorTestUtils,
Editor,
};
use crate::{document::DocumentMessageHandler, message_prelude::*, misc::test_utils::EditorTestUtils, Editor};
use document_core::{color::Color, Operation};
use log::info;
@@ -123,10 +118,10 @@ mod test {
init_logger();
let mut editor = create_editor_with_three_layers();
let document_before_copy = editor.dispatcher.document_message_handler.active_document().document.clone();
editor.handle_message(Message::Document(DocumentMessage::CopySelectedLayers)).unwrap();
editor.handle_message(Message::Document(DocumentMessage::PasteLayers { path: vec![], insert_index: -1 })).unwrap();
let document_after_copy = editor.dispatcher.document_message_handler.active_document().document.clone();
let document_before_copy = editor.dispatcher.documents_message_handler.active_document().document.clone();
editor.handle_message(DocumentsMessage::CopySelectedLayers).unwrap();
editor.handle_message(DocumentsMessage::PasteLayers { path: vec![], insert_index: -1 }).unwrap();
let document_after_copy = editor.dispatcher.documents_message_handler.active_document().document.clone();
let layers_before_copy = document_before_copy.root.as_folder().unwrap().layers();
let layers_after_copy = document_after_copy.root.as_folder().unwrap().layers();
@@ -153,14 +148,14 @@ mod test {
init_logger();
let mut editor = create_editor_with_three_layers();
let document_before_copy = editor.dispatcher.document_message_handler.active_document().document.clone();
let document_before_copy = editor.dispatcher.documents_message_handler.active_document().document.clone();
let shape_id = document_before_copy.root.as_folder().unwrap().layer_ids[1];
editor.handle_message(Message::Document(DocumentMessage::SelectLayers(vec![vec![shape_id]]))).unwrap();
editor.handle_message(Message::Document(DocumentMessage::CopySelectedLayers)).unwrap();
editor.handle_message(Message::Document(DocumentMessage::PasteLayers { path: vec![], insert_index: -1 })).unwrap();
editor.handle_message(DocumentMessage::SelectLayers(vec![vec![shape_id]])).unwrap();
editor.handle_message(DocumentsMessage::CopySelectedLayers).unwrap();
editor.handle_message(DocumentsMessage::PasteLayers { path: vec![], insert_index: -1 }).unwrap();
let document_after_copy = editor.dispatcher.document_message_handler.active_document().document.clone();
let document_after_copy = editor.dispatcher.documents_message_handler.active_document().document.clone();
let layers_before_copy = document_before_copy.root.as_folder().unwrap().layers();
let layers_after_copy = document_after_copy.root.as_folder().unwrap().layers();
@@ -190,42 +185,42 @@ mod test {
const LINE_INDEX: usize = 0;
const PEN_INDEX: usize = 1;
editor.handle_message(Message::Document(DocumentMessage::AddFolder(vec![]))).unwrap();
editor.handle_message(DocumentMessage::AddFolder(vec![])).unwrap();
let document_before_added_shapes = editor.dispatcher.document_message_handler.active_document().document.clone();
let document_before_added_shapes = editor.dispatcher.documents_message_handler.active_document().document.clone();
let folder_id = document_before_added_shapes.root.as_folder().unwrap().layer_ids[FOLDER_INDEX];
// TODO: This adding of a Line and Pen should be rewritten using the corresponding functions in EditorTestUtils.
// This has not been done yet as the line and pen tool are not yet able to add layers to the currently selected folder
editor
.handle_message(Message::Document(DocumentMessage::DispatchOperation(Operation::AddLine {
path: vec![folder_id],
.handle_message(Operation::AddLine {
path: vec![folder_id, LINE_INDEX as u64],
insert_index: 0,
transform: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
style: Default::default(),
})))
})
.unwrap();
editor
.handle_message(Message::Document(DocumentMessage::DispatchOperation(Operation::AddPen {
path: vec![folder_id],
.handle_message(Operation::AddPen {
path: vec![folder_id, PEN_INDEX as u64],
insert_index: 0,
transform: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
style: Default::default(),
points: vec![(10.0, 20.0), (30.0, 40.0)],
})))
})
.unwrap();
editor.handle_message(Message::Document(DocumentMessage::SelectLayers(vec![vec![folder_id]]))).unwrap();
editor.handle_message(DocumentMessage::SelectLayers(vec![vec![folder_id]])).unwrap();
let document_before_copy = editor.dispatcher.document_message_handler.active_document().document.clone();
let document_before_copy = editor.dispatcher.documents_message_handler.active_document().document.clone();
editor.handle_message(Message::Document(DocumentMessage::CopySelectedLayers)).unwrap();
editor.handle_message(Message::Document(DocumentMessage::DeleteSelectedLayers)).unwrap();
editor.handle_message(Message::Document(DocumentMessage::PasteLayers { path: vec![], insert_index: -1 })).unwrap();
editor.handle_message(Message::Document(DocumentMessage::PasteLayers { path: vec![], insert_index: -1 })).unwrap();
editor.handle_message(DocumentsMessage::CopySelectedLayers).unwrap();
editor.handle_message(DocumentMessage::DeleteSelectedLayers).unwrap();
editor.handle_message(DocumentsMessage::PasteLayers { path: vec![], insert_index: -1 }).unwrap();
editor.handle_message(DocumentsMessage::PasteLayers { path: vec![], insert_index: -1 }).unwrap();
let document_after_copy = editor.dispatcher.document_message_handler.active_document().document.clone();
let document_after_copy = editor.dispatcher.documents_message_handler.active_document().document.clone();
let layers_before_copy = document_before_copy.root.as_folder().unwrap().layers();
let layers_after_copy = document_after_copy.root.as_folder().unwrap().layers();
@@ -276,18 +271,18 @@ mod test {
const SHAPE_INDEX: usize = 1;
const RECT_INDEX: usize = 0;
let document_before_copy = editor.dispatcher.document_message_handler.active_document().document.clone();
let document_before_copy = editor.dispatcher.documents_message_handler.active_document().document.clone();
let rect_id = document_before_copy.root.as_folder().unwrap().layer_ids[RECT_INDEX];
let ellipse_id = document_before_copy.root.as_folder().unwrap().layer_ids[ELLIPSE_INDEX];
editor.handle_message(Message::Document(DocumentMessage::SelectLayers(vec![vec![rect_id], vec![ellipse_id]]))).unwrap();
editor.handle_message(Message::Document(DocumentMessage::CopySelectedLayers)).unwrap();
editor.handle_message(Message::Document(DocumentMessage::DeleteSelectedLayers)).unwrap();
editor.handle_message(DocumentMessage::SelectLayers(vec![vec![rect_id], vec![ellipse_id]])).unwrap();
editor.handle_message(DocumentsMessage::CopySelectedLayers).unwrap();
editor.handle_message(DocumentMessage::DeleteSelectedLayers).unwrap();
editor.draw_rect(0, 800, 12, 200);
editor.handle_message(Message::Document(DocumentMessage::PasteLayers { path: vec![], insert_index: -1 })).unwrap();
editor.handle_message(Message::Document(DocumentMessage::PasteLayers { path: vec![], insert_index: -1 })).unwrap();
editor.handle_message(DocumentsMessage::PasteLayers { path: vec![], insert_index: -1 }).unwrap();
editor.handle_message(DocumentsMessage::PasteLayers { path: vec![], insert_index: -1 }).unwrap();
let document_after_copy = editor.dispatcher.document_message_handler.active_document().document.clone();
let document_after_copy = editor.dispatcher.documents_message_handler.active_document().document.clone();
let layers_before_copy = document_before_copy.root.as_folder().unwrap().layers();
let layers_after_copy = document_after_copy.root.as_folder().unwrap().layers();
@@ -314,18 +309,18 @@ mod test {
let verify_order = |handler: &mut DocumentMessageHandler| (handler.all_layers_sorted(), handler.non_selected_layers_sorted(), handler.selected_layers_sorted());
editor.handle_message(Message::Document(DocumentMessage::SelectLayers(vec![vec![0], vec![2]]))).unwrap();
editor.handle_message(DocumentMessage::SelectLayers(vec![vec![0], vec![2]])).unwrap();
editor.handle_message(Message::Document(DocumentMessage::ReorderSelectedLayers(1))).unwrap();
let (all, non_selected, selected) = verify_order(&mut editor.dispatcher.document_message_handler);
editor.handle_message(DocumentMessage::ReorderSelectedLayers(1)).unwrap();
let (all, non_selected, selected) = verify_order(&mut editor.dispatcher.documents_message_handler.active_document_mut());
assert_eq!(all, non_selected.into_iter().chain(selected.into_iter()).collect::<Vec<_>>());
editor.handle_message(Message::Document(DocumentMessage::ReorderSelectedLayers(-1))).unwrap();
let (all, non_selected, selected) = verify_order(&mut editor.dispatcher.document_message_handler);
editor.handle_message(DocumentMessage::ReorderSelectedLayers(-1)).unwrap();
let (all, non_selected, selected) = verify_order(&mut editor.dispatcher.documents_message_handler.active_document_mut());
assert_eq!(all, selected.into_iter().chain(non_selected.into_iter()).collect::<Vec<_>>());
editor.handle_message(Message::Document(DocumentMessage::ReorderSelectedLayers(i32::MAX))).unwrap();
let (all, non_selected, selected) = verify_order(&mut editor.dispatcher.document_message_handler);
editor.handle_message(DocumentMessage::ReorderSelectedLayers(i32::MAX)).unwrap();
let (all, non_selected, selected) = verify_order(&mut editor.dispatcher.documents_message_handler.active_document_mut());
assert_eq!(all, non_selected.into_iter().chain(selected.into_iter()).collect::<Vec<_>>());
}
}
+27 -2
View File
@@ -1,5 +1,9 @@
use crate::message_prelude::*;
use graphite_proc_macros::*;
use std::{
collections::hash_map::DefaultHasher,
hash::{Hash, Hasher},
};
pub trait AsMessage: TransitiveChild
where
@@ -12,11 +16,11 @@ where
}
#[impl_message]
#[derive(PartialEq, Clone, Debug)]
#[derive(Clone, Debug, PartialEq)]
pub enum Message {
NoOp,
#[child]
Document(DocumentMessage),
Documents(DocumentsMessage),
#[child]
Global(GlobalMessage),
#[child]
@@ -28,3 +32,24 @@ pub enum Message {
#[child]
InputMapper(InputMapperMessage),
}
impl Message {
/// Returns the byte representation of the message.
///
/// # Safety
/// This function reads from uninitialized memory!!!
/// Only use if you know what you are doing
unsafe fn as_slice(&self) -> &[u8] {
core::slice::from_raw_parts(self as *const Message as *const u8, std::mem::size_of::<Message>())
}
/// Returns a pseudo hash that should uniquely identify the message.
/// This is needed because `Hash` is not implemented for f64s
///
/// # Safety
/// This function reads from uninitialized memory but the generated value should be fine.
pub fn pseudo_hash(&self) -> u64 {
let mut s = DefaultHasher::new();
unsafe { self.as_slice() }.hash(&mut s);
s.finish()
}
}
+13
View File
@@ -1,3 +1,6 @@
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
pub mod dispatcher;
pub mod message;
use crate::message_prelude::*;
@@ -21,3 +24,13 @@ where
fn process_action(&mut self, action: A, data: T, responses: &mut VecDeque<Message>);
fn actions(&self) -> ActionList;
}
pub fn generate_hash<'a>(messages: impl IntoIterator<Item = &'a Message>, ipp: &InputPreprocessor, document_hash: u64) -> u64 {
let mut s = DefaultHasher::new();
document_hash.hash(&mut s);
ipp.hash(&mut s);
for message in messages {
message.pseudo_hash().hash(&mut s);
}
s.finish()
}
+3 -1
View File
@@ -11,4 +11,6 @@ pub const MOUSE_ZOOM_RATE: f64 = 1. / 400.;
pub const ROTATE_SNAP_INTERVAL: f64 = 15.;
pub const SELECTION_TOLERANCE: f64 = 5.0;
pub const LINE_ROTATE_SNAP_ANGLE: f64 = 15.;
pub const SELECTION_TOLERANCE: f64 = 1.0;
+458 -100
View File
@@ -1,143 +1,501 @@
use crate::{consts::ROTATE_SNAP_INTERVAL, frontend::layer_panel::*, EditorError};
use document_core::{document::Document as InternalDocument, layers::Layer, LayerId};
pub use super::layer_panel::*;
use crate::{frontend::layer_panel::*, EditorError};
use document_core::{document::Document as InternalDocument, LayerId};
use glam::{DAffine2, DVec2};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Clone, Debug)]
pub struct Document {
pub document: InternalDocument,
pub name: String,
pub layer_data: HashMap<Vec<LayerId>, LayerData>,
use crate::input::InputPreprocessor;
use crate::message_prelude::*;
use document_core::layers::BlendMode;
use document_core::{DocumentResponse, Operation as DocumentOperation};
use log::warn;
use std::collections::VecDeque;
use super::movement_handler::{MovementMessage, MovementMessageHandler};
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, Hash)]
pub enum FlipAxis {
X,
Y,
}
impl Default for Document {
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, Hash)]
pub enum AlignAxis {
X,
Y,
}
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, Hash)]
pub enum AlignAggregate {
Min,
Max,
Center,
Average,
}
#[derive(Clone, Debug)]
pub struct DocumentMessageHandler {
pub document: InternalDocument,
pub document_backup: Option<InternalDocument>,
pub name: String,
pub layer_data: HashMap<Vec<LayerId>, LayerData>,
movement_handler: MovementMessageHandler,
}
impl Default for DocumentMessageHandler {
fn default() -> Self {
Self {
document: InternalDocument::default(),
document_backup: None,
name: String::from("Untitled Document"),
layer_data: vec![(vec![], LayerData::new(true))].into_iter().collect(),
movement_handler: MovementMessageHandler::default(),
}
}
}
impl Document {
#[impl_message(Message, DocumentsMessage, Document)]
#[derive(PartialEq, Clone, Debug)]
pub enum DocumentMessage {
#[child]
Movement(MovementMessage),
DispatchOperation(Box<DocumentOperation>),
SelectLayers(Vec<Vec<LayerId>>),
SelectAllLayers,
DeselectAllLayers,
DeleteLayer(Vec<LayerId>),
DeleteSelectedLayers,
DuplicateSelectedLayers,
SetBlendModeForSelectedLayers(BlendMode),
SetOpacityForSelectedLayers(f64),
AddFolder(Vec<LayerId>),
RenameLayer(Vec<LayerId>, String),
ToggleLayerVisibility(Vec<LayerId>),
FlipSelectedLayers(FlipAxis),
ToggleLayerExpansion(Vec<LayerId>),
FolderChanged(Vec<LayerId>),
StartTransaction,
RollbackTransaction,
AbortTransaction,
CommitTransaction,
ExportDocument,
RenderDocument,
Undo,
NudgeSelectedLayers(f64, f64),
AlignSelectedLayers(AlignAxis, AlignAggregate),
MoveSelectedLayersTo {
path: Vec<LayerId>,
insert_index: isize,
},
ReorderSelectedLayers(i32), // relative_position,
}
impl From<DocumentOperation> for DocumentMessage {
fn from(operation: DocumentOperation) -> DocumentMessage {
Self::DispatchOperation(Box::new(operation))
}
}
impl From<DocumentOperation> for Message {
fn from(operation: DocumentOperation) -> Message {
DocumentMessage::DispatchOperation(Box::new(operation)).into()
}
}
impl DocumentMessageHandler {
pub fn active_document(&self) -> &DocumentMessageHandler {
self
}
pub fn active_document_mut(&mut self) -> &mut DocumentMessageHandler {
self
}
fn filter_document_responses(&self, document_responses: &mut Vec<DocumentResponse>) -> bool {
let len = document_responses.len();
document_responses.retain(|response| !matches!(response, DocumentResponse::DocumentChanged));
document_responses.len() != len
}
fn handle_folder_changed(&mut self, path: Vec<LayerId>) -> Option<Message> {
let _ = self.document.render_root();
self.layer_data(&path).expanded.then(|| {
let children = self.layer_panel(path.as_slice()).expect("The provided Path was not valid");
FrontendMessage::ExpandFolder { path, children }.into()
})
}
fn clear_selection(&mut self) {
self.layer_data.values_mut().for_each(|layer_data| layer_data.selected = false);
}
fn select_layer(&mut self, path: &[LayerId]) -> Option<Message> {
self.layer_data(path).selected = true;
// TODO: Add deduplication
(!path.is_empty()).then(|| self.handle_folder_changed(path[..path.len() - 1].to_vec())).flatten()
}
pub fn layerdata(&self, path: &[LayerId]) -> &LayerData {
self.layer_data.get(path).expect("Layerdata does not exist")
}
pub fn layerdata_mut(&mut self, path: &[LayerId]) -> &mut LayerData {
self.layer_data.entry(path.to_vec()).or_insert_with(|| LayerData::new(true))
}
pub fn selected_layers(&self) -> impl Iterator<Item = &Vec<LayerId>> {
self.layer_data.iter().filter_map(|(path, data)| data.selected.then(|| path))
}
/// Returns the paths to all layers in order, optionally including only selected or non-selected layers.
fn layers_sorted(&self, selected: Option<bool>) -> Vec<Vec<LayerId>> {
// Compute the indices for each layer to be able to sort them
let mut layers_with_indices: Vec<(Vec<LayerId>, Vec<usize>)> = self
.layer_data
.iter()
// 'path.len() > 0' filters out root layer since it has no indices
.filter_map(|(path, data)| (!path.is_empty() && (data.selected == selected.unwrap_or(data.selected))).then(|| path.clone()))
.filter_map(|path| {
// Currently it is possible that layer_data contains layers that are don't actually exist (has been partially fixed in #281)
// and thus indices_for_path can return an error. We currently skip these layers and log a warning.
// Once this problem is solved this code can be simplified
match self.document.indices_for_path(&path) {
Err(err) => {
warn!("layers_sorted: Could not get indices for the layer {:?}: {:?}", path, err);
None
}
Ok(indices) => Some((path, indices)),
}
})
.collect();
layers_with_indices.sort_by_key(|(_, indices)| indices.clone());
layers_with_indices.into_iter().map(|(path, _)| path).collect()
}
/// Returns the paths to all layers in order
pub fn all_layers_sorted(&self) -> Vec<Vec<LayerId>> {
self.layers_sorted(None)
}
/// Returns the paths to all selected layers in order
pub fn selected_layers_sorted(&self) -> Vec<Vec<LayerId>> {
self.layers_sorted(Some(true))
}
/// 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<Vec<LayerId>> {
self.layers_sorted(Some(false))
}
pub fn with_name(name: String) -> Self {
Self {
document: InternalDocument::default(),
document_backup: None,
name,
layer_data: vec![(vec![], LayerData::new(true))].into_iter().collect(),
movement_handler: MovementMessageHandler::default(),
}
}
}
fn layer_data<'a>(layer_data: &'a mut HashMap<Vec<LayerId>, LayerData>, path: &[LayerId]) -> &'a mut LayerData {
if !layer_data.contains_key(path) {
layer_data.insert(path.to_vec(), LayerData::new(false));
}
layer_data.get_mut(path).unwrap()
}
pub fn layer_panel_entry(layer_data: &mut LayerData, layer: &mut Layer, path: Vec<LayerId>) -> LayerPanelEntry {
let blend_mode = layer.blend_mode;
let opacity = layer.opacity;
let layer_type: LayerType = (&layer.data).into();
let name = layer.name.clone().unwrap_or_else(|| format!("Unnamed {}", layer_type));
let arr = layer.current_bounding_box().unwrap_or([DVec2::ZERO, DVec2::ZERO]);
let arr = arr.iter().map(|x| (*x).into()).collect::<Vec<(f64, f64)>>();
if layer.cache_dirty {
layer.render();
}
let thumbnail = if let [(x_min, y_min), (x_max, y_max)] = arr.as_slice() {
format!(
r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="{} {} {} {}">{}</svg>"#,
x_min,
y_min,
x_max - x_min,
y_max - y_min,
layer.thumbnail_cache.clone()
)
} else {
String::new()
};
LayerPanelEntry {
name,
visible: layer.visible,
blend_mode,
opacity,
layer_type,
layer_data: *layer_data,
path,
thumbnail,
}
}
impl Document {
pub fn layer_data(&mut self, path: &[LayerId]) -> &mut LayerData {
layer_data(&mut self.layer_data, path)
}
pub fn backup(&mut self) {
self.document_backup = Some(self.document.clone())
}
pub fn rollback(&mut self) -> Result<(), EditorError> {
self.backup();
self.reset()
}
pub fn reset(&mut self) -> Result<(), EditorError> {
match self.document_backup.take() {
Some(backup) => {
self.document = backup;
Ok(())
}
None => Err(EditorError::NoTransactionInProgress),
}
}
pub fn layer_panel_entry(&mut self, path: Vec<LayerId>) -> Result<LayerPanelEntry, EditorError> {
self.document.render_root();
let data: LayerData = *layer_data(&mut self.layer_data, &path);
let layer = self.document.layer(&path)?;
let entry = layer_panel_entry(&data, self.document.multiply_transforms(&path).unwrap(), layer, path);
Ok(entry)
}
/// Returns a list of `LayerPanelEntry`s intended for display purposes. These don't contain
/// any actual data, but rather metadata such as visibility and names of the layers.
/// any actual data, but ratfolderch as visibility and names of the layers.
pub fn layer_panel(&mut self, path: &[LayerId]) -> Result<Vec<LayerPanelEntry>, EditorError> {
let folder = self.document.document_folder_mut(path)?;
let ids = folder.as_folder()?.layer_ids.clone();
let self_layer_data = &mut self.layer_data;
let folder = self.document.folder(path)?;
let paths: Vec<Vec<LayerId>> = folder.layer_ids.iter().map(|id| [path, &[*id]].concat()).collect();
let data: Vec<LayerData> = paths.iter().map(|path| *layer_data(&mut self.layer_data, path)).collect();
let folder = self.document.folder(path)?;
let entries = folder
.as_folder_mut()?
.layers_mut()
.iter_mut()
.zip(ids)
.layers()
.iter()
.zip(paths.iter().zip(data))
.rev()
.map(|(layer, id)| {
let path = [path, &[id]].concat();
layer_panel_entry(layer_data(self_layer_data, &path), layer, path)
.map(|(layer, (path, data))| {
layer_panel_entry(
&data,
self.document.generate_transform_across_scope(path, Some(self.document.root.transform.inverse())).unwrap(),
layer,
path.to_vec(),
)
})
.collect();
Ok(entries)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Copy)]
pub struct LayerData {
pub selected: bool,
pub expanded: bool,
pub translation: DVec2,
pub rotation: f64,
pub snap_rotate: bool,
pub scale: f64,
}
impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHandler {
fn process_action(&mut self, message: DocumentMessage, ipp: &InputPreprocessor, responses: &mut VecDeque<Message>) {
use DocumentMessage::*;
match message {
Movement(message) => self.movement_handler.process_action(message, (layer_data(&mut self.layer_data, &[]), &self.document, ipp), responses),
DeleteLayer(path) => responses.push_back(DocumentOperation::DeleteLayer { path }.into()),
AddFolder(path) => responses.push_back(DocumentOperation::AddFolder { path }.into()),
StartTransaction => self.backup(),
RollbackTransaction => {
self.rollback().unwrap_or_else(|e| log::warn!("{}", e));
responses.extend([DocumentMessage::RenderDocument.into(), self.handle_folder_changed(vec![]).unwrap()]);
}
AbortTransaction => {
self.reset().unwrap_or_else(|e| log::warn!("{}", e));
responses.extend([DocumentMessage::RenderDocument.into(), self.handle_folder_changed(vec![]).unwrap()]);
}
CommitTransaction => self.document_backup = None,
ExportDocument => {
let bbox = self.document.visible_layers_bounding_box().unwrap_or([DVec2::ZERO, ipp.viewport_size.as_f64()]);
let size = bbox[1] - bbox[0];
responses.push_back(
FrontendMessage::ExportDocument {
document: format!(
r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="{} {} {} {}">{}{}</svg>"#,
bbox[0].x,
bbox[0].y,
size.x,
size.y,
"\n",
self.document.render_root()
),
}
.into(),
)
}
SetBlendModeForSelectedLayers(blend_mode) => {
let active_document = self;
impl LayerData {
pub fn new(expanded: bool) -> LayerData {
LayerData {
selected: false,
expanded,
translation: DVec2::ZERO,
rotation: 0.,
snap_rotate: false,
scale: 1.,
for path in active_document.layer_data.iter().filter_map(|(path, data)| data.selected.then(|| path.clone())) {
responses.push_back(DocumentOperation::SetLayerBlendMode { path, blend_mode }.into());
}
}
SetOpacityForSelectedLayers(opacity) => {
let opacity = opacity.clamp(0., 1.);
for path in self.selected_layers().cloned() {
responses.push_back(DocumentOperation::SetLayerOpacity { path, opacity }.into());
}
}
ToggleLayerVisibility(path) => {
responses.push_back(DocumentOperation::ToggleVisibility { path }.into());
}
ToggleLayerExpansion(path) => {
self.layer_data(&path).expanded ^= true;
responses.extend(self.handle_folder_changed(path));
}
DeleteSelectedLayers => {
for path in self.selected_layers().cloned() {
responses.push_back(DocumentOperation::DeleteLayer { path }.into())
}
}
DuplicateSelectedLayers => {
for path in self.selected_layers_sorted() {
responses.push_back(DocumentOperation::DuplicateLayer { path }.into())
}
}
SelectLayers(paths) => {
self.clear_selection();
for path in paths {
responses.extend(self.select_layer(&path));
}
// TODO: Correctly update layer panel in clear_selection instead of here
responses.extend(self.handle_folder_changed(Vec::new()));
}
SelectAllLayers => {
let all_layer_paths = self.layer_data.keys().filter(|path| !path.is_empty()).cloned().collect::<Vec<_>>();
for path in all_layer_paths {
responses.extend(self.select_layer(&path));
}
}
DeselectAllLayers => {
self.clear_selection();
let children = self.layer_panel(&[]).expect("The provided Path was not valid");
responses.push_back(FrontendMessage::ExpandFolder { path: vec![], children }.into());
}
Undo => {
// this is a temporary fix and will be addressed by #123
if let Some(id) = self.document.root.as_folder().unwrap().list_layers().last() {
responses.push_back(DocumentOperation::DeleteLayer { path: vec![*id] }.into())
}
}
FolderChanged(path) => responses.extend(self.handle_folder_changed(path)),
DispatchOperation(op) => match self.document.handle_operation(&op) {
Ok(Some(mut document_responses)) => {
let canvas_dirty = self.filter_document_responses(&mut document_responses);
responses.extend(
document_responses
.into_iter()
.map(|response| match response {
DocumentResponse::FolderChanged { path } => self.handle_folder_changed(path),
DocumentResponse::DeletedLayer { path } => {
self.layer_data.remove(&path);
None
}
DocumentResponse::LayerChanged { path } => Some(
FrontendMessage::UpdateLayer {
path: path.clone(),
data: self.layer_panel_entry(path).unwrap(),
}
.into(),
),
DocumentResponse::CreatedLayer { path } => self.select_layer(&path),
DocumentResponse::DocumentChanged => unreachable!(),
})
.flatten(),
);
if canvas_dirty {
responses.push_back(RenderDocument.into())
}
}
Err(e) => log::error!("DocumentError: {:?}", e),
Ok(_) => (),
},
RenderDocument => responses.push_back(
FrontendMessage::UpdateCanvas {
document: self.document.render_root(),
}
.into(),
),
NudgeSelectedLayers(x, y) => {
for path in self.selected_layers().cloned() {
let operation = DocumentOperation::TransformLayerInViewport {
path,
transform: DAffine2::from_translation((x, y).into()).to_cols_array(),
};
responses.push_back(operation.into());
}
}
MoveSelectedLayersTo { path, insert_index } => {
responses.push_back(DocumentsMessage::CopySelectedLayers.into());
responses.push_back(DocumentMessage::DeleteSelectedLayers.into());
responses.push_back(DocumentsMessage::PasteLayers { path, insert_index }.into());
}
ReorderSelectedLayers(relative_position) => {
let all_layer_paths = self.all_layers_sorted();
let selected_layers = self.selected_layers_sorted();
if let Some(pivot) = match relative_position.signum() {
-1 => selected_layers.first(),
1 => selected_layers.last(),
_ => unreachable!(),
} {
if let Some(pos) = all_layer_paths.iter().position(|path| path == pivot) {
let max = all_layer_paths.len() as i64 - 1;
let insert_pos = (pos as i64 + relative_position as i64).clamp(0, max) as usize;
let insert = all_layer_paths.get(insert_pos);
if let Some(insert_path) = insert {
let (id, path) = insert_path.split_last().expect("Can't move the root folder");
if let Some(folder) = self.document.layer(path).ok().map(|layer| layer.as_folder().ok()).flatten() {
let selected: Vec<_> = selected_layers
.iter()
.filter(|layer| layer.starts_with(path) && layer.len() == path.len() + 1)
.map(|x| x.last().unwrap())
.collect();
let non_selected: Vec<_> = folder.layer_ids.iter().filter(|id| selected.iter().all(|x| x != id)).collect();
let offset = if relative_position < 0 || non_selected.is_empty() { 0 } else { 1 };
let fallback = offset * (non_selected.len());
let insert_index = non_selected.iter().position(|x| *x == id).map(|x| x + offset).unwrap_or(fallback) as isize;
responses.push_back(DocumentMessage::MoveSelectedLayersTo { path: path.to_vec(), insert_index }.into())
}
}
}
}
}
FlipSelectedLayers(axis) => {
let scale = match axis {
FlipAxis::X => DVec2::new(-1., 1.),
FlipAxis::Y => DVec2::new(1., -1.),
};
if let Some([min, max]) = self.document.combined_viewport_bounding_box(self.selected_layers().map(|x| x.as_slice())) {
let center = (max + min) / 2.;
let bbox_trans = DAffine2::from_translation(-center);
for path in self.selected_layers() {
responses.push_back(
DocumentOperation::TransformLayerInScope {
path: path.clone(),
transform: DAffine2::from_scale(scale).to_cols_array(),
scope: bbox_trans.to_cols_array(),
}
.into(),
);
}
}
}
AlignSelectedLayers(axis, aggregate) => {
let (paths, boxes): (Vec<_>, Vec<_>) = self.selected_layers().filter_map(|path| self.document.viewport_bounding_box(path).ok()?.map(|b| (path, b))).unzip();
let axis = match axis {
AlignAxis::X => DVec2::X,
AlignAxis::Y => DVec2::Y,
};
let lerp = |bbox: &[DVec2; 2]| bbox[0].lerp(bbox[1], 0.5);
if let Some(combined_box) = self.document.combined_viewport_bounding_box(self.selected_layers().map(|x| x.as_slice())) {
let aggregated = match aggregate {
AlignAggregate::Min => combined_box[0],
AlignAggregate::Max => combined_box[1],
AlignAggregate::Center => lerp(&combined_box),
AlignAggregate::Average => boxes.iter().map(|b| lerp(b)).reduce(|a, b| a + b).map(|b| b / boxes.len() as f64).unwrap(),
};
for (path, bbox) in paths.into_iter().zip(boxes) {
let center = match aggregate {
AlignAggregate::Min => bbox[0],
AlignAggregate::Max => bbox[1],
_ => lerp(&bbox),
};
let translation = (aggregated - center) * axis;
responses.push_back(
DocumentOperation::TransformLayerInViewport {
path: path.clone(),
transform: DAffine2::from_translation(translation).to_cols_array(),
}
.into(),
);
}
}
}
RenameLayer(path, name) => responses.push_back(DocumentOperation::RenameLayer { path, name }.into()),
}
}
pub fn snapped_angle(&self) -> f64 {
let increment_radians: f64 = ROTATE_SNAP_INTERVAL.to_radians();
if self.snap_rotate {
(self.rotation / increment_radians).round() * increment_radians
} else {
self.rotation
fn actions(&self) -> ActionList {
let mut common = actions!(DocumentMessageDiscriminant;
Undo,
SelectAllLayers,
DeselectAllLayers,
RenderDocument,
ExportDocument,
);
if self.layer_data.values().any(|data| data.selected) {
let select = actions!(DocumentMessageDiscriminant;
DeleteSelectedLayers,
DuplicateSelectedLayers,
NudgeSelectedLayers,
ReorderSelectedLayers,
);
common.extend(select);
}
}
pub fn calculate_offset_transform(&self, offset: DVec2) -> DAffine2 {
let offset_transform = DAffine2::from_translation(offset);
let scale_transform = DAffine2::from_scale(DVec2::new(self.scale, self.scale));
let angle_transform = DAffine2::from_angle(self.snapped_angle());
let translation_transform = DAffine2::from_translation(self.translation);
scale_transform * offset_transform * angle_transform * scale_transform * translation_transform
}
pub fn calculate_transform(&self) -> DAffine2 {
self.calculate_offset_transform(DVec2::ZERO)
common.extend(self.movement_handler.actions());
common
}
}
@@ -1,40 +1,25 @@
use crate::input::InputPreprocessor;
use crate::message_prelude::*;
use crate::{
consts::{MOUSE_ZOOM_RATE, VIEWPORT_SCROLL_RATE, VIEWPORT_ZOOM_SCALE_MAX, VIEWPORT_ZOOM_SCALE_MIN, WHEEL_ZOOM_RATE},
input::{mouse::ViewportPosition, InputPreprocessor},
};
use document_core::layers::BlendMode;
use document_core::layers::Layer;
use document_core::{DocumentResponse, LayerId, Operation as DocumentOperation};
use glam::{DAffine2, DVec2};
use document_core::{LayerId, Operation as DocumentOperation};
use log::warn;
use serde::{Deserialize, Serialize};
use crate::document::Document;
use std::collections::VecDeque;
use super::LayerData;
use super::DocumentMessageHandler;
#[impl_message(Message, Document)]
#[impl_message(Message, Documents)]
#[derive(PartialEq, Clone, Debug)]
pub enum DocumentMessage {
DispatchOperation(DocumentOperation),
SelectLayers(Vec<Vec<LayerId>>),
SelectAllLayers,
DeselectAllLayers,
DeleteLayer(Vec<LayerId>),
DeleteSelectedLayers,
DuplicateSelectedLayers,
pub enum DocumentsMessage {
CopySelectedLayers,
SetBlendModeForSelectedLayers(BlendMode),
SetOpacityForSelectedLayers(f64),
PasteLayers { path: Vec<LayerId>, insert_index: isize },
AddFolder(Vec<LayerId>),
RenameLayer(Vec<LayerId>, String),
ToggleLayerVisibility(Vec<LayerId>),
ToggleLayerExpansion(Vec<LayerId>),
PasteLayers {
path: Vec<LayerId>,
insert_index: isize,
},
SelectDocument(usize),
CloseDocument(usize),
#[child]
Document(DocumentMessage),
CloseActiveDocumentWithConfirmation,
CloseAllDocumentsWithConfirmation,
CloseAllDocuments,
@@ -42,187 +27,40 @@ pub enum DocumentMessage {
GetOpenDocumentsList,
NextDocument,
PrevDocument,
ExportDocument,
RenderDocument,
Undo,
MouseMove,
TranslateCanvasBegin,
WheelCanvasTranslate { use_y_as_x: bool },
RotateCanvasBegin { snap: bool },
EnableSnapping,
DisableSnapping,
ZoomCanvasBegin,
TranslateCanvasEnd,
SetCanvasZoom(f64),
MultiplyCanvasZoom(f64),
WheelCanvasZoom,
SetCanvasRotation(f64),
NudgeSelectedLayers(f64, f64),
FlipSelectedLayers(FlipAxis),
AlignSelectedLayers(AlignAxis, AlignAggregate),
DragLayer(Vec<LayerId>, DVec2),
MoveSelectedLayersTo { path: Vec<LayerId>, insert_index: isize },
ReorderSelectedLayers(i32), // relative_position,
SetLayerTranslation(Vec<LayerId>, Option<f64>, Option<f64>),
}
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub enum FlipAxis {
X,
Y,
}
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub enum AlignAxis {
X,
Y,
}
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub enum AlignAggregate {
Min,
Max,
Center,
Average,
}
impl From<DocumentOperation> for DocumentMessage {
fn from(operation: DocumentOperation) -> DocumentMessage {
Self::DispatchOperation(operation)
}
}
impl From<DocumentOperation> for Message {
fn from(operation: DocumentOperation) -> Message {
DocumentMessage::DispatchOperation(operation).into()
}
}
#[derive(Debug, Clone)]
pub struct DocumentMessageHandler {
documents: Vec<Document>,
pub struct DocumentsMessageHandler {
documents: Vec<DocumentMessageHandler>,
active_document_index: usize,
translating: bool,
rotating: bool,
zooming: bool,
snapping: bool,
mouse_pos: ViewportPosition,
copy_buffer: Vec<Layer>,
}
impl DocumentMessageHandler {
pub fn active_document(&self) -> &Document {
impl DocumentsMessageHandler {
pub fn active_document(&self) -> &DocumentMessageHandler {
&self.documents[self.active_document_index]
}
pub fn active_document_mut(&mut self) -> &mut Document {
pub fn active_document_mut(&mut self) -> &mut DocumentMessageHandler {
&mut self.documents[self.active_document_index]
}
fn filter_document_responses(&self, document_responses: &mut Vec<DocumentResponse>) -> bool {
let len = document_responses.len();
document_responses.retain(|response| !matches!(response, DocumentResponse::DocumentChanged));
document_responses.len() != len
}
fn handle_folder_changed(&mut self, path: Vec<LayerId>) -> Option<Message> {
let document = self.active_document_mut();
document.layer_data(&path).expanded.then(|| {
let children = document.layer_panel(path.as_slice()).expect("The provided Path was not valid");
FrontendMessage::ExpandFolder { path, children }.into()
})
}
fn clear_selection(&mut self) {
self.active_document_mut().layer_data.values_mut().for_each(|layer_data| layer_data.selected = false);
}
fn select_layer(&mut self, path: &[LayerId]) -> Option<Message> {
self.active_document_mut().layer_data(&path).selected = true;
// TODO: Add deduplication
(!path.is_empty()).then(|| self.handle_folder_changed(path[..path.len() - 1].to_vec())).flatten()
}
fn selected_layers(&self) -> impl Iterator<Item = &Vec<LayerId>> {
self.active_document().layer_data.iter().filter_map(|(path, data)| data.selected.then(|| path))
}
fn layerdata(&self, path: &[LayerId]) -> &LayerData {
self.active_document().layer_data.get(path).expect("Layerdata does not exist")
}
fn layerdata_mut(&mut self, path: &[LayerId]) -> &mut LayerData {
self.active_document_mut().layer_data.entry(path.to_vec()).or_insert_with(|| LayerData::new(true))
}
fn create_document_transform_from_layerdata(&self, viewport_size: &ViewportPosition, responses: &mut VecDeque<Message>) {
let half_viewport = viewport_size.as_dvec2() / 2.;
let layerdata = self.layerdata(&[]);
let scaled_half_viewport = half_viewport / layerdata.scale;
responses.push_back(
DocumentOperation::SetLayerTransform {
path: vec![],
transform: layerdata.calculate_offset_transform(scaled_half_viewport).to_cols_array(),
}
.into(),
);
}
/// Returns the paths to all layers in order, optionally including only selected or non-selected layers.
fn layers_sorted(&self, selected: Option<bool>) -> Vec<Vec<LayerId>> {
// Compute the indices for each layer to be able to sort them
let mut layers_with_indices: Vec<(Vec<LayerId>, Vec<usize>)> = self
.active_document()
.layer_data
.iter()
// 'path.len() > 0' filters out root layer since it has no indices
.filter_map(|(path, data)| (!path.is_empty() && (data.selected == selected.unwrap_or(data.selected))).then(|| path.clone()))
.filter_map(|path| {
// Currently it is possible that layer_data contains layers that are don't actually exist (has been partially fixed in #281)
// and thus indices_for_path can return an error. We currently skip these layers and log a warning.
// Once this problem is solved this code can be simplified
match self.active_document().document.indices_for_path(&path) {
Err(err) => {
warn!("layers_sorted: Could not get indices for the layer {:?}: {:?}", path, err);
None
}
Ok(indices) => Some((path, indices)),
}
})
.collect();
layers_with_indices.sort_by_key(|(_, indices)| indices.clone());
layers_with_indices.into_iter().map(|(path, _)| path).collect()
}
/// Returns the paths to all layers in order
pub fn all_layers_sorted(&self) -> Vec<Vec<LayerId>> {
self.layers_sorted(None)
}
/// Returns the paths to all selected layers in order
pub fn selected_layers_sorted(&self) -> Vec<Vec<LayerId>> {
self.layers_sorted(Some(true))
}
/// 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<Vec<LayerId>> {
self.layers_sorted(Some(false))
}
}
impl Default for DocumentMessageHandler {
impl Default for DocumentsMessageHandler {
fn default() -> Self {
Self {
documents: vec![Document::default()],
documents: vec![DocumentMessageHandler::default()],
active_document_index: 0,
translating: false,
rotating: false,
zooming: false,
snapping: false,
mouse_pos: ViewportPosition::default(),
copy_buffer: vec![],
}
}
}
impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHandler {
fn process_action(&mut self, message: DocumentMessage, ipp: &InputPreprocessor, responses: &mut VecDeque<Message>) {
impl MessageHandler<DocumentsMessage, &InputPreprocessor> for DocumentsMessageHandler {
fn process_action(&mut self, message: DocumentsMessage, ipp: &InputPreprocessor, responses: &mut VecDeque<Message>) {
use DocumentMessage::*;
use DocumentsMessage::*;
match message {
DeleteLayer(path) => responses.push_back(DocumentOperation::DeleteLayer { path }.into()),
AddFolder(path) => responses.push_back(DocumentOperation::AddFolder { path }.into()),
Document(message) => self.active_document_mut().process_action(message, ipp, responses),
SelectDocument(id) => {
assert!(id < self.documents.len(), "Tried to select a document that was not initialized");
self.active_document_index = id;
@@ -250,7 +88,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
self.documents.clear();
// Create a new blank document
responses.push_back(DocumentMessage::NewDocument.into());
responses.push_back(NewDocument.into());
}
CloseDocument(id) => {
assert!(id < self.documents.len(), "Tried to select a document that was not initialized");
@@ -264,7 +102,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
// Last tab was closed, so create a new blank tab
if self.documents.is_empty() {
self.active_document_index = 0;
responses.push_back(DocumentMessage::NewDocument.into());
responses.push_back(NewDocument.into());
}
// The currently selected doc is being closed
else if id == self.active_document_index {
@@ -327,7 +165,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
};
self.active_document_index = self.documents.len();
let new_document = Document::with_name(name);
let new_document = DocumentMessageHandler::with_name(name);
self.documents.push(new_document);
// Send the new list of document tab names
@@ -356,48 +194,8 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
let id = (self.active_document_index + self.documents.len() - 1) % self.documents.len();
responses.push_back(SelectDocument(id).into());
}
ExportDocument => responses.push_back(
FrontendMessage::ExportDocument {
//TODO: Add canvas size instead of using 1920x1080 by default
document: format!(
r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1920 1080">{}{}</svg>"#,
"\n",
self.active_document_mut().document.render_root(),
),
}
.into(),
),
SetBlendModeForSelectedLayers(blend_mode) => {
for path in self.selected_layers().cloned() {
responses.push_back(DocumentOperation::SetLayerBlendMode { path, blend_mode }.into());
}
}
SetOpacityForSelectedLayers(opacity) => {
let opacity = opacity.clamp(0., 1.);
for path in self.selected_layers().cloned() {
responses.push_back(DocumentOperation::SetLayerOpacity { path, opacity }.into());
}
}
ToggleLayerVisibility(path) => {
responses.push_back(DocumentOperation::ToggleVisibility { path }.into());
}
ToggleLayerExpansion(path) => {
self.active_document_mut().layer_data(&path).expanded ^= true;
responses.extend(self.handle_folder_changed(path));
}
DeleteSelectedLayers => {
for path in self.selected_layers().cloned() {
responses.push_back(DocumentOperation::DeleteLayer { path }.into())
}
}
DuplicateSelectedLayers => {
for path in self.selected_layers_sorted() {
responses.push_back(DocumentOperation::DuplicateLayer { path }.into())
}
}
CopySelectedLayers => {
let paths = self.selected_layers_sorted();
let paths = self.active_document().selected_layers_sorted();
self.copy_buffer.clear();
for path in paths {
match self.active_document().document.layer(&path).map(|t| t.clone()) {
@@ -430,405 +228,26 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
}
}
}
SelectLayers(paths) => {
self.clear_selection();
for path in paths {
responses.extend(self.select_layer(&path));
}
// TODO: Correctly update layer panel in clear_selection instead of here
responses.extend(self.handle_folder_changed(Vec::new()));
}
SelectAllLayers => {
let all_layer_paths = self.active_document().layer_data.keys().filter(|path| !path.is_empty()).cloned().collect::<Vec<_>>();
for path in all_layer_paths {
responses.extend(self.select_layer(&path));
}
}
DeselectAllLayers => {
self.clear_selection();
let children = self.active_document_mut().layer_panel(&[]).expect("The provided Path was not valid");
responses.push_back(FrontendMessage::ExpandFolder { path: vec![], children }.into());
}
Undo => {
// this is a temporary fix and will be addressed by #123
if let Some(id) = self.active_document().document.root.as_folder().unwrap().list_layers().last() {
responses.push_back(DocumentOperation::DeleteLayer { path: vec![*id] }.into())
}
}
DispatchOperation(op) => {
if let Ok(Some(mut document_responses)) = self.active_document_mut().document.handle_operation(op) {
let canvas_dirty = self.filter_document_responses(&mut document_responses);
responses.extend(
document_responses
.into_iter()
.map(|response| match response {
DocumentResponse::FolderChanged { path } => self.handle_folder_changed(path),
DocumentResponse::DeletedLayer { path } => {
self.active_document_mut().layer_data.remove(&path);
None
}
DocumentResponse::CreatedLayer { path } => {
if !self.active_document().document.work_mounted {
self.select_layer(&path)
} else {
None
}
}
DocumentResponse::DocumentChanged => unreachable!(),
})
.flatten(),
);
if canvas_dirty {
responses.push_back(RenderDocument.into())
}
}
}
RenderDocument => responses.push_back(
FrontendMessage::UpdateCanvas {
document: self.active_document_mut().document.render_root(),
}
.into(),
),
TranslateCanvasBegin => {
self.translating = true;
self.mouse_pos = ipp.mouse.position;
}
RotateCanvasBegin { snap } => {
self.rotating = true;
self.snapping = snap;
let layerdata = self.layerdata_mut(&[]);
layerdata.snap_rotate = snap;
self.mouse_pos = ipp.mouse.position;
}
EnableSnapping => self.snapping = true,
DisableSnapping => self.snapping = false,
ZoomCanvasBegin => {
self.zooming = true;
self.mouse_pos = ipp.mouse.position;
}
TranslateCanvasEnd => {
let layerdata = self.layerdata_mut(&[]);
layerdata.rotation = layerdata.snapped_angle();
layerdata.snap_rotate = false;
self.translating = false;
self.rotating = false;
self.zooming = false;
}
MouseMove => {
if self.translating {
let delta = ipp.mouse.position.as_dvec2() - self.mouse_pos.as_dvec2();
let transformed_delta = self.active_document().document.root.transform.inverse().transform_vector2(delta);
let layerdata = self.layerdata_mut(&[]);
layerdata.translation += transformed_delta;
self.create_document_transform_from_layerdata(&ipp.viewport_size, responses);
}
if self.rotating {
let half_viewport = ipp.viewport_size.as_dvec2() / 2.;
let rotation = {
let start_vec = self.mouse_pos.as_dvec2() - half_viewport;
let end_vec = ipp.mouse.position.as_dvec2() - half_viewport;
start_vec.angle_between(end_vec)
};
let snapping = self.snapping;
let layerdata = self.layerdata_mut(&[]);
layerdata.rotation += rotation;
layerdata.snap_rotate = snapping;
responses.push_back(
FrontendMessage::SetCanvasRotation {
new_radians: layerdata.snapped_angle(),
}
.into(),
);
self.create_document_transform_from_layerdata(&ipp.viewport_size, responses);
}
if self.zooming {
let difference = self.mouse_pos.y as f64 - ipp.mouse.position.y as f64;
let amount = 1. + difference * MOUSE_ZOOM_RATE;
let layerdata = self.layerdata_mut(&[]);
let new = (layerdata.scale * amount).clamp(VIEWPORT_ZOOM_SCALE_MIN, VIEWPORT_ZOOM_SCALE_MAX);
layerdata.scale = new;
responses.push_back(FrontendMessage::SetCanvasZoom { new_zoom: layerdata.scale }.into());
self.create_document_transform_from_layerdata(&ipp.viewport_size, responses);
}
self.mouse_pos = ipp.mouse.position;
}
SetCanvasZoom(new) => {
let layerdata = self.layerdata_mut(&[]);
layerdata.scale = new.clamp(VIEWPORT_ZOOM_SCALE_MIN, VIEWPORT_ZOOM_SCALE_MAX);
responses.push_back(FrontendMessage::SetCanvasZoom { new_zoom: layerdata.scale }.into());
self.create_document_transform_from_layerdata(&ipp.viewport_size, responses);
}
MultiplyCanvasZoom(multiplier) => {
let layerdata = self.layerdata_mut(&[]);
let new = (layerdata.scale * multiplier).clamp(VIEWPORT_ZOOM_SCALE_MIN, VIEWPORT_ZOOM_SCALE_MAX);
layerdata.scale = new;
responses.push_back(FrontendMessage::SetCanvasZoom { new_zoom: layerdata.scale }.into());
self.create_document_transform_from_layerdata(&ipp.viewport_size, responses);
}
WheelCanvasZoom => {
let scroll = ipp.mouse.scroll_delta.scroll_delta();
let mouse = ipp.mouse.position.as_dvec2();
let viewport_size = ipp.viewport_size.as_dvec2();
let mut zoom_factor = 1. + scroll.abs() * WHEEL_ZOOM_RATE;
if ipp.mouse.scroll_delta.y > 0 {
zoom_factor = 1. / zoom_factor
};
let new_viewport_size = viewport_size * (1. / zoom_factor);
let delta_size = viewport_size - new_viewport_size;
let mouse_percent = mouse / viewport_size;
let delta = delta_size * -2. * (mouse_percent - (0.5, 0.5).into());
let transformed_delta = self.active_document().document.root.transform.inverse().transform_vector2(delta);
let layerdata = self.layerdata_mut(&[]);
let new = (layerdata.scale * zoom_factor).clamp(VIEWPORT_ZOOM_SCALE_MIN, VIEWPORT_ZOOM_SCALE_MAX);
layerdata.scale = new;
layerdata.translation += transformed_delta;
responses.push_back(FrontendMessage::SetCanvasZoom { new_zoom: layerdata.scale }.into());
self.create_document_transform_from_layerdata(&ipp.viewport_size, responses);
}
WheelCanvasTranslate { use_y_as_x } => {
let delta = match use_y_as_x {
false => -ipp.mouse.scroll_delta.as_dvec2(),
true => (-ipp.mouse.scroll_delta.y as f64, 0.).into(),
} * VIEWPORT_SCROLL_RATE;
let transformed_delta = self.active_document().document.root.transform.inverse().transform_vector2(delta);
let layerdata = self.layerdata_mut(&[]);
layerdata.translation += transformed_delta;
self.create_document_transform_from_layerdata(&ipp.viewport_size, responses);
}
SetCanvasRotation(new) => {
let layerdata = self.layerdata_mut(&[]);
layerdata.rotation = new;
self.create_document_transform_from_layerdata(&ipp.viewport_size, responses);
responses.push_back(FrontendMessage::SetCanvasRotation { new_radians: new }.into());
}
NudgeSelectedLayers(x, y) => {
let delta = {
let root_layer_rotation = self.layerdata_mut(&[]).rotation;
let rotate_to_viewport_space = DAffine2::from_angle(root_layer_rotation).inverse();
rotate_to_viewport_space.transform_point2((x, y).into())
};
for path in self.selected_layers().cloned() {
let operation = DocumentOperation::TransformLayer {
path,
transform: DAffine2::from_translation(delta).to_cols_array(),
};
responses.push_back(operation.into());
}
}
MoveSelectedLayersTo { path, insert_index } => {
responses.push_back(DocumentMessage::CopySelectedLayers.into());
responses.push_back(DocumentMessage::DeleteSelectedLayers.into());
responses.push_back(DocumentMessage::PasteLayers { path, insert_index }.into());
}
ReorderSelectedLayers(relative_position) => {
let all_layer_paths = self.all_layers_sorted();
let selected_layers = self.selected_layers_sorted();
if let Some(pivot) = match relative_position.signum() {
-1 => selected_layers.first(),
1 => selected_layers.last(),
_ => unreachable!(),
} {
if let Some(pos) = all_layer_paths.iter().position(|path| path == pivot) {
let max = all_layer_paths.len() as i64 - 1;
let insert_pos = (pos as i64 + relative_position as i64).clamp(0, max) as usize;
let insert = all_layer_paths.get(insert_pos);
if let Some(insert_path) = insert {
let (id, path) = insert_path.split_last().expect("Can't move the root folder");
if let Some(folder) = self.active_document().document.document_layer(path).ok().map(|layer| layer.as_folder().ok()).flatten() {
let selected: Vec<_> = selected_layers
.iter()
.filter(|layer| layer.starts_with(path) && layer.len() == path.len() + 1)
.map(|x| x.last().unwrap())
.collect();
let non_selected: Vec<_> = folder.layer_ids.iter().filter(|id| selected.iter().all(|x| x != id)).collect();
let offset = if relative_position < 0 || non_selected.is_empty() { 0 } else { 1 };
let fallback = offset * (non_selected.len());
let insert_index = non_selected.iter().position(|x| *x == id).map(|x| x + offset).unwrap_or(fallback) as isize;
responses.push_back(DocumentMessage::MoveSelectedLayersTo { path: path.to_vec(), insert_index }.into())
}
}
}
}
}
FlipSelectedLayers(axis) => {
// TODO: Handle folder nested transforms with the transforms API
let selected_paths = self.selected_layers_sorted();
if selected_paths.is_empty() {
return;
}
let selected_layers = selected_paths.iter().filter_map(|path| {
let layer = self.active_document().document.layer(path).ok()?;
// TODO: Refactor with `reduce` and `merge_bounding_boxes` once the latter is added
let (min, max) = {
let bounding_box = layer.bounding_box(layer.transform, layer.style)?;
match axis {
FlipAxis::X => (bounding_box[0].x, bounding_box[1].x),
FlipAxis::Y => (bounding_box[0].y, bounding_box[1].y),
}
};
Some((path.clone(), (min, max)))
});
let (min, max) = selected_layers
.clone()
.map(|(_, extrema)| extrema)
.reduce(|(min_a, max_a), (min_b, max_b)| (min_a.min(min_b), max_a.max(max_b)))
.unwrap();
let middle = (min + max) / 2.;
for (path, _) in selected_layers {
let layer = self.active_document().document.layer(&path).unwrap();
let mut transform = layer.transform;
let scale = match axis {
FlipAxis::X => DVec2::new(-1., 1.),
FlipAxis::Y => DVec2::new(1., -1.),
};
transform = transform * DAffine2::from_scale(scale);
let coord = match axis {
FlipAxis::X => &mut transform.translation.x,
FlipAxis::Y => &mut transform.translation.y,
};
*coord = *coord - 2. * (*coord - middle);
responses.push_back(
DocumentOperation::SetLayerTransform {
path,
transform: transform.to_cols_array(),
}
.into(),
);
}
}
AlignSelectedLayers(axis, aggregate) => {
// TODO: Handle folder nested transforms with the transforms API
if self.selected_layers().next().is_none() {
return;
}
let selected_layers = self.selected_layers().cloned().filter_map(|path| {
let layer = self.active_document().document.layer(&path).ok()?;
let point = {
let bounding_box = layer.bounding_box(layer.transform, layer.style)?;
match aggregate {
AlignAggregate::Min => bounding_box[0],
AlignAggregate::Max => bounding_box[1],
AlignAggregate::Center => bounding_box[0].lerp(bounding_box[1], 0.5),
AlignAggregate::Average => bounding_box[0].lerp(bounding_box[1], 0.5),
}
};
let (bounding_box_coord, translation_coord) = match axis {
AlignAxis::X => (point.x, layer.transform.translation.x),
AlignAxis::Y => (point.y, layer.transform.translation.y),
};
Some((path, bounding_box_coord, translation_coord))
});
let selected_layers: Vec<_> = selected_layers.collect();
let bounding_box_coords = selected_layers.iter().map(|(_, bounding_box_coord, _)| bounding_box_coord).cloned();
if let Some(aggregated_coord) = match aggregate {
AlignAggregate::Min => bounding_box_coords.reduce(|a, b| a.min(b)),
AlignAggregate::Max => bounding_box_coords.reduce(|a, b| a.max(b)),
AlignAggregate::Center => {
// TODO: Refactor with `reduce` and `merge_bounding_boxes` once the latter is added
self.selected_layers()
.filter_map(|path| self.active_document().document.layer(path).ok().map(|layer| layer.bounding_box(layer.transform, layer.style)).flatten())
.map(|bbox| match axis {
AlignAxis::X => (bbox[0].x, bbox[1].x),
AlignAxis::Y => (bbox[0].y, bbox[1].y),
})
.reduce(|(a, b), (c, d)| (a.min(c), b.max(d)))
.map(|(min, max)| (min + max) / 2.)
}
AlignAggregate::Average => Some(bounding_box_coords.sum::<f64>() / selected_layers.len() as f64),
} {
for (path, bounding_box_coord, translation_coord) in selected_layers {
let new_coord = aggregated_coord - (bounding_box_coord - translation_coord);
match axis {
AlignAxis::X => responses.push_back(DocumentMessage::SetLayerTranslation(path, Some(new_coord), None).into()),
AlignAxis::Y => responses.push_back(DocumentMessage::SetLayerTranslation(path, None, Some(new_coord)).into()),
}
}
}
}
DragLayer(path, offset) => {
// TODO: Replace root transformations with functions of the transform api
// and do the same with all instances of `root.transform.inverse()` in other messages
let transformed_mouse_pos = self.active_document().document.root.transform.inverse().transform_vector2(ipp.mouse.position.as_dvec2());
let translation = offset + transformed_mouse_pos;
if let Ok(layer) = self.active_document_mut().document.layer_mut(&path) {
let transform = {
let mut transform = layer.transform;
transform.translation = translation;
transform.to_cols_array()
};
responses.push_back(DocumentOperation::SetLayerTransform { path, transform }.into());
}
}
SetLayerTranslation(path, x_option, y_option) => {
if let Ok(layer) = self.active_document_mut().document.layer_mut(&path) {
let mut transform = layer.transform;
transform.translation = DVec2::new(x_option.unwrap_or(transform.translation.x), y_option.unwrap_or(transform.translation.y));
responses.push_back(
DocumentOperation::SetLayerTransform {
path,
transform: transform.to_cols_array(),
}
.into(),
);
}
}
message => todo!("document_action_handler does not implement: {}", message.to_discriminant().global_name()),
}
}
fn actions(&self) -> ActionList {
let mut common = actions!(DocumentMessageDiscriminant;
Undo,
SelectAllLayers,
DeselectAllLayers,
RenderDocument,
ExportDocument,
let mut common = actions!(DocumentsMessageDiscriminant;
NewDocument,
CloseActiveDocumentWithConfirmation,
CloseAllDocumentsWithConfirmation,
CloseAllDocuments,
NextDocument,
PrevDocument,
MouseMove,
TranslateCanvasEnd,
TranslateCanvasBegin,
PasteLayers,
RotateCanvasBegin,
ZoomCanvasBegin,
SetCanvasZoom,
MultiplyCanvasZoom,
SetCanvasRotation,
WheelCanvasZoom,
WheelCanvasTranslate,
);
if self.active_document().layer_data.values().any(|data| data.selected) {
let select = actions!(DocumentMessageDiscriminant;
DeleteSelectedLayers,
DuplicateSelectedLayers,
let select = actions!(DocumentsMessageDiscriminant;
CopySelectedLayers,
NudgeSelectedLayers,
ReorderSelectedLayers,
);
common.extend(select);
}
if self.rotating {
let snapping = actions!(DocumentMessageDiscriminant;
EnableSnapping,
DisableSnapping,
);
common.extend(snapping);
}
common.extend(self.active_document().actions());
common
}
}
+94
View File
@@ -0,0 +1,94 @@
use crate::{consts::ROTATE_SNAP_INTERVAL, frontend::layer_panel::*};
use document_core::{
layers::{Layer, LayerData as DocumentLayerData},
LayerId,
};
use glam::{DAffine2, DVec2};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Copy)]
pub struct LayerData {
pub selected: bool,
pub expanded: bool,
pub translation: DVec2,
pub rotation: f64,
pub snap_rotate: bool,
pub scale: f64,
}
impl LayerData {
pub fn new(expanded: bool) -> LayerData {
LayerData {
selected: false,
expanded,
translation: DVec2::ZERO,
rotation: 0.,
snap_rotate: false,
scale: 1.,
}
}
pub fn snapped_angle(&self) -> f64 {
let increment_radians: f64 = ROTATE_SNAP_INTERVAL.to_radians();
if self.snap_rotate {
(self.rotation / increment_radians).round() * increment_radians
} else {
self.rotation
}
}
pub fn calculate_offset_transform(&self, offset: DVec2) -> DAffine2 {
// TODO: replace with DAffine2::from_scale_angle_translation and fix the errors
let offset_transform = DAffine2::from_translation(offset);
let scale_transform = DAffine2::from_scale(DVec2::new(self.scale, self.scale));
let angle_transform = DAffine2::from_angle(self.snapped_angle());
let translation_transform = DAffine2::from_translation(self.translation);
scale_transform * offset_transform * angle_transform * translation_transform
}
}
pub fn layer_data<'a>(layer_data: &'a mut HashMap<Vec<LayerId>, LayerData>, path: &[LayerId]) -> &'a mut LayerData {
if !layer_data.contains_key(path) {
layer_data.insert(path.to_vec(), LayerData::new(false));
}
layer_data.get_mut(path).unwrap()
}
pub fn layer_panel_entry(layer_data: &LayerData, transform: DAffine2, layer: &Layer, path: Vec<LayerId>) -> LayerPanelEntry {
let layer_type: LayerType = (&layer.data).into();
let name = layer.name.clone().unwrap_or_else(|| format!("Unnamed {}", layer_type));
let arr = layer.data.bounding_box(transform).unwrap_or([DVec2::ZERO, DVec2::ZERO]);
let arr = arr.iter().map(|x| (*x).into()).collect::<Vec<(f64, f64)>>();
let mut thumbnail = String::new();
layer.data.clone().render(&mut thumbnail, &mut vec![transform]);
let transform = transform.to_cols_array().iter().map(ToString::to_string).collect::<Vec<_>>().join(",");
let thumbnail = if let [(x_min, y_min), (x_max, y_max)] = arr.as_slice() {
format!(
r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="{} {} {} {}"><g transform="matrix({})">{}</g></svg>"#,
x_min,
y_min,
x_max - x_min,
y_max - y_min,
transform,
thumbnail,
)
} else {
String::new()
};
// LayerIds are sent as (u32, u32) because jsond does not support u64s
let path = path.iter().map(|id| ((id >> 32) as u32, (id << 32 >> 32) as u32)).collect::<Vec<_>>();
LayerPanelEntry {
name,
visible: layer.visible,
blend_mode: layer.blend_mode,
opacity: layer.opacity,
layer_type: (&layer.data).into(),
layer_data: *layer_data,
path,
thumbnail,
}
}
+8 -2
View File
@@ -1,8 +1,14 @@
mod document_file;
mod document_message_handler;
mod layer_panel;
mod movement_handler;
#[doc(inline)]
pub use document_file::{Document, LayerData};
pub use document_file::LayerData;
#[doc(inline)]
pub use document_message_handler::{AlignAggregate, AlignAxis, DocumentMessage, DocumentMessageDiscriminant, DocumentMessageHandler, FlipAxis};
pub use document_file::{AlignAggregate, AlignAxis, DocumentMessage, DocumentMessageDiscriminant, DocumentMessageHandler, FlipAxis};
#[doc(inline)]
pub use document_message_handler::{DocumentsMessage, DocumentsMessageDiscriminant, DocumentsMessageHandler};
#[doc(inline)]
pub use movement_handler::{MovementMessage, MovementMessageDiscriminant};
@@ -0,0 +1,214 @@
pub use super::layer_panel::*;
use super::LayerData;
use crate::message_prelude::*;
use crate::{
consts::{MOUSE_ZOOM_RATE, VIEWPORT_SCROLL_RATE, VIEWPORT_ZOOM_SCALE_MAX, VIEWPORT_ZOOM_SCALE_MIN, WHEEL_ZOOM_RATE},
input::{mouse::ViewportPosition, InputPreprocessor},
};
use document_core::document::Document;
use document_core::Operation as DocumentOperation;
use glam::DVec2;
use std::collections::VecDeque;
#[impl_message(Message, DocumentMessage, Movement)]
#[derive(PartialEq, Clone, Debug)]
pub enum MovementMessage {
MouseMove,
TranslateCanvasBegin,
WheelCanvasTranslate { use_y_as_x: bool },
RotateCanvasBegin { snap: bool },
EnableSnapping,
DisableSnapping,
ZoomCanvasBegin,
TranslateCanvasEnd,
SetCanvasZoom(f64),
MultiplyCanvasZoom(f64),
WheelCanvasZoom,
SetCanvasRotation(f64),
ZoomCanvasToFitAll,
}
#[derive(Debug, Clone, Hash, Default, PartialEq)]
pub struct MovementMessageHandler {
translating: bool,
rotating: bool,
zooming: bool,
snapping: bool,
mouse_pos: ViewportPosition,
}
impl MovementMessageHandler {
fn create_document_transform_from_layerdata(&self, layerdata: &LayerData, viewport_size: &ViewportPosition, responses: &mut VecDeque<Message>) {
let half_viewport = viewport_size.as_f64() / 2.;
let scaled_half_viewport = half_viewport / layerdata.scale;
responses.push_back(
DocumentOperation::SetLayerTransform {
path: vec![],
transform: layerdata.calculate_offset_transform(scaled_half_viewport).to_cols_array(),
}
.into(),
);
}
}
impl MessageHandler<MovementMessage, (&mut LayerData, &Document, &InputPreprocessor)> for MovementMessageHandler {
fn process_action(&mut self, message: MovementMessage, data: (&mut LayerData, &Document, &InputPreprocessor), responses: &mut VecDeque<Message>) {
let (layerdata, document, ipp) = data;
use MovementMessage::*;
match message {
TranslateCanvasBegin => {
self.translating = true;
self.mouse_pos = ipp.mouse.position;
}
RotateCanvasBegin { snap } => {
self.rotating = true;
self.snapping = snap;
layerdata.snap_rotate = snap;
self.mouse_pos = ipp.mouse.position;
}
EnableSnapping => self.snapping = true,
DisableSnapping => self.snapping = false,
ZoomCanvasBegin => {
self.zooming = true;
self.mouse_pos = ipp.mouse.position;
}
TranslateCanvasEnd => {
layerdata.rotation = layerdata.snapped_angle();
layerdata.snap_rotate = false;
self.translating = false;
self.rotating = false;
self.zooming = false;
}
MouseMove => {
if self.translating {
let delta = ipp.mouse.position.as_f64() - self.mouse_pos.as_f64();
let transformed_delta = document.root.transform.inverse().transform_vector2(delta);
layerdata.translation += transformed_delta;
self.create_document_transform_from_layerdata(layerdata, &ipp.viewport_size, responses);
}
if self.rotating {
let half_viewport = ipp.viewport_size.as_f64() / 2.;
let rotation = {
let start_vec = self.mouse_pos.as_f64() - half_viewport;
let end_vec = ipp.mouse.position.as_f64() - half_viewport;
start_vec.angle_between(end_vec)
};
let snapping = self.snapping;
layerdata.rotation += rotation;
layerdata.snap_rotate = snapping;
responses.push_back(
FrontendMessage::SetCanvasRotation {
new_radians: layerdata.snapped_angle(),
}
.into(),
);
self.create_document_transform_from_layerdata(layerdata, &ipp.viewport_size, responses);
}
if self.zooming {
let difference = self.mouse_pos.y as f64 - ipp.mouse.position.y as f64;
let amount = 1. + difference * MOUSE_ZOOM_RATE;
let new = (layerdata.scale * amount).clamp(VIEWPORT_ZOOM_SCALE_MIN, VIEWPORT_ZOOM_SCALE_MAX);
layerdata.scale = new;
responses.push_back(FrontendMessage::SetCanvasZoom { new_zoom: layerdata.scale }.into());
self.create_document_transform_from_layerdata(layerdata, &ipp.viewport_size, responses);
}
self.mouse_pos = ipp.mouse.position;
}
SetCanvasZoom(new) => {
layerdata.scale = new.clamp(VIEWPORT_ZOOM_SCALE_MIN, VIEWPORT_ZOOM_SCALE_MAX);
responses.push_back(FrontendMessage::SetCanvasZoom { new_zoom: layerdata.scale }.into());
self.create_document_transform_from_layerdata(layerdata, &ipp.viewport_size, responses);
}
MultiplyCanvasZoom(multiplier) => {
let new = (layerdata.scale * multiplier).clamp(VIEWPORT_ZOOM_SCALE_MIN, VIEWPORT_ZOOM_SCALE_MAX);
layerdata.scale = new;
responses.push_back(FrontendMessage::SetCanvasZoom { new_zoom: layerdata.scale }.into());
self.create_document_transform_from_layerdata(layerdata, &ipp.viewport_size, responses);
}
WheelCanvasZoom => {
let scroll = ipp.mouse.scroll_delta.scroll_delta();
let mouse = ipp.mouse.position.as_f64();
let viewport_size = ipp.viewport_size.as_f64();
let mut zoom_factor = 1. + scroll.abs() * WHEEL_ZOOM_RATE;
if ipp.mouse.scroll_delta.y > 0 {
zoom_factor = 1. / zoom_factor
};
let new_viewport_size = viewport_size * (1. / zoom_factor);
let delta_size = viewport_size - new_viewport_size;
let mouse_percent = mouse / viewport_size;
let delta = (delta_size * -2.) * (mouse_percent - DVec2::splat(0.5));
let transformed_delta = document.root.transform.inverse().transform_vector2(delta);
let new = (layerdata.scale * zoom_factor).clamp(VIEWPORT_ZOOM_SCALE_MIN, VIEWPORT_ZOOM_SCALE_MAX);
layerdata.scale = new;
layerdata.translation += transformed_delta;
responses.push_back(FrontendMessage::SetCanvasZoom { new_zoom: layerdata.scale }.into());
self.create_document_transform_from_layerdata(layerdata, &ipp.viewport_size, responses);
}
WheelCanvasTranslate { use_y_as_x } => {
let delta = match use_y_as_x {
false => -ipp.mouse.scroll_delta.as_dvec2(),
true => (-ipp.mouse.scroll_delta.y as f64, 0.).into(),
} * VIEWPORT_SCROLL_RATE;
let transformed_delta = document.root.transform.inverse().transform_vector2(delta);
layerdata.translation += transformed_delta;
self.create_document_transform_from_layerdata(layerdata, &ipp.viewport_size, responses);
}
SetCanvasRotation(new) => {
layerdata.rotation = new;
self.create_document_transform_from_layerdata(layerdata, &ipp.viewport_size, responses);
responses.push_back(FrontendMessage::SetCanvasRotation { new_radians: new }.into());
}
ZoomCanvasToFitAll => {
if let Some([pos1, pos2]) = document.visible_layers_bounding_box() {
let pos1 = document.root.transform.inverse().transform_point2(pos1);
let pos2 = document.root.transform.inverse().transform_point2(pos2);
let v1 = document.root.transform.inverse().transform_point2(DVec2::ZERO);
let v2 = document.root.transform.inverse().transform_point2(ipp.viewport_size.as_f64());
let center = v1.lerp(v2, 0.5) - pos1.lerp(pos2, 0.5);
let size = (pos2 - pos1) / (v2 - v1);
let size = 1. / size;
let new_scale = size.min_element();
layerdata.translation += center;
layerdata.scale *= new_scale;
responses.push_back(FrontendMessage::SetCanvasZoom { new_zoom: layerdata.scale }.into());
self.create_document_transform_from_layerdata(layerdata, &ipp.viewport_size, responses);
}
}
}
}
fn actions(&self) -> ActionList {
let mut common = actions!(MovementMessageDiscriminant;
MouseMove,
TranslateCanvasEnd,
TranslateCanvasBegin,
RotateCanvasBegin,
ZoomCanvasBegin,
SetCanvasZoom,
MultiplyCanvasZoom,
SetCanvasRotation,
WheelCanvasZoom,
WheelCanvasTranslate,
ZoomCanvasToFitAll,
);
if self.rotating {
let snapping = actions!(MovementMessageDiscriminant;
EnableSnapping,
DisableSnapping,
);
common.extend(snapping);
}
common
}
}
@@ -16,6 +16,7 @@ pub enum FrontendMessage {
DisplayConfirmationToCloseDocument { document_index: usize },
DisplayConfirmationToCloseAllDocuments,
UpdateCanvas { document: String },
UpdateLayer { path: Vec<LayerId>, data: LayerPanelEntry },
ExportDocument { document: String },
EnableTextInput,
DisableTextInput,
+6 -22
View File
@@ -1,8 +1,5 @@
use crate::document::LayerData;
use document_core::{
layers::{BlendMode, LayerDataTypes},
LayerId,
};
use document_core::layers::{BlendMode, LayerDataType};
use serde::{Deserialize, Serialize};
use std::fmt;
@@ -14,7 +11,8 @@ pub struct LayerPanelEntry {
pub opacity: f64,
pub layer_type: LayerType,
pub layer_data: LayerData,
pub path: Vec<LayerId>,
// TODO: instead of turning the u64 into (u32, u32)s here, do that in the wasm wrapper
pub path: Vec<(u32, u32)>,
pub thumbnail: String,
}
@@ -22,11 +20,6 @@ pub struct LayerPanelEntry {
pub enum LayerType {
Folder,
Shape,
Circle,
Rect,
Line,
PolyLine,
Ellipse,
}
impl fmt::Display for LayerType {
@@ -34,27 +27,18 @@ impl fmt::Display for LayerType {
let name = match self {
LayerType::Folder => "Folder",
LayerType::Shape => "Shape",
LayerType::Rect => "Rectangle",
LayerType::Line => "Line",
LayerType::Circle => "Circle",
LayerType::PolyLine => "Polyline",
LayerType::Ellipse => "Ellipse",
};
formatter.write_str(name)
}
}
impl From<&LayerDataTypes> for LayerType {
fn from(data: &LayerDataTypes) -> Self {
use LayerDataTypes::*;
impl From<&LayerDataType> for LayerType {
fn from(data: &LayerDataType) -> Self {
use LayerDataType::*;
match data {
Folder(_) => LayerType::Folder,
Shape(_) => LayerType::Shape,
Rect(_) => LayerType::Rect,
Line(_) => LayerType::Line,
PolyLine(_) => LayerType::PolyLine,
Ellipse(_) => LayerType::Ellipse,
}
}
}
@@ -2,7 +2,7 @@ use crate::message_prelude::*;
use std::collections::VecDeque;
#[impl_message(Message, Global)]
#[derive(PartialEq, Clone, Debug)]
#[derive(PartialEq, Clone, Debug, Hash)]
pub enum GlobalMessage {
LogInfo,
LogDebug,
+56 -64
View File
@@ -12,7 +12,7 @@ const NUDGE_AMOUNT: f64 = 1.;
const SHIFT_NUDGE_AMOUNT: f64 = 10.;
#[impl_message(Message, InputMapper)]
#[derive(PartialEq, Clone, Debug)]
#[derive(PartialEq, Clone, Debug, Hash)]
pub enum InputMapperMessage {
PointerMove,
MouseScroll,
@@ -87,35 +87,47 @@ macro_rules! entry {
entry!{action=$action, message=InputMapperMessage::KeyUp(Key::$key) $(, modifiers=[$($m),* ])?}
}};
{action=$action:expr, message=$message:expr $(, modifiers=[$($m:ident),* $(,)?])?} => {{
MappingEntry {trigger: $message, modifiers: modifiers!($($($m),*)?), action: $action.into()}
&[MappingEntry {trigger: $message, modifiers: modifiers!($($($m),*)?), action: $action.into()}]
}};
{action=$action:expr, triggers=[$($m:ident),* $(,)?]} => {{
&[
MappingEntry {trigger:InputMapperMessage::PointerMove, action: $action.into(), modifiers: modifiers!()},
$(
MappingEntry {trigger:InputMapperMessage::KeyDown(Key::$m), action: $action.into(), modifiers: modifiers!()},
MappingEntry {trigger:InputMapperMessage::KeyUp(Key::$m), action: $action.into(), modifiers: modifiers!()},
)*
]
}};
}
macro_rules! mapping {
//[$(<action=$action:expr; message=$key:expr; $(modifiers=[$($m:ident),* $(,)?];)?>)*] => {{
[$($entry:expr),* $(,)?] => {{
let mut key_up = KeyMappingEntries::key_array();
let mut key_up = KeyMappingEntries::key_array();
let mut key_down = KeyMappingEntries::key_array();
let mut pointer_move: KeyMappingEntries = Default::default();
let mut mouse_scroll: KeyMappingEntries = Default::default();
$(
let arr = match $entry.trigger {
InputMapperMessage::KeyDown(key) => &mut key_down[key as usize],
InputMapperMessage::KeyUp(key) => &mut key_up[key as usize],
InputMapperMessage::PointerMove => &mut pointer_move,
InputMapperMessage::MouseScroll => &mut mouse_scroll,
};
arr.push($entry);
)*
(key_up, key_down, pointer_move, mouse_scroll)
for entry in $entry {
let arr = match entry.trigger {
InputMapperMessage::KeyDown(key) => &mut key_down[key as usize],
InputMapperMessage::KeyUp(key) => &mut key_up[key as usize],
InputMapperMessage::PointerMove => &mut pointer_move,
InputMapperMessage::MouseScroll => &mut mouse_scroll,
};
arr.push(entry.clone());
}
)*
(key_up, key_down, pointer_move, mouse_scroll)
}};
}
impl Default for Mapping {
fn default() -> Self {
use Key::*;
let mappings = mapping![
entry! {action=DocumentMessage::PasteLayers{path: vec![], insert_index: -1}, key_down=KeyV, modifiers=[KeyControl]},
entry! {action=DocumentMessage::EnableSnapping, key_down=KeyShift},
entry! {action=DocumentMessage::DisableSnapping, key_up=KeyShift},
entry! {action=DocumentsMessage::PasteLayers{path: vec![], insert_index: -1}, key_down=KeyV, modifiers=[KeyControl]},
entry! {action=MovementMessage::EnableSnapping, key_down=KeyShift},
entry! {action=MovementMessage::DisableSnapping, key_up=KeyShift},
// Select
entry! {action=SelectMessage::MouseMove, message=InputMapperMessage::PointerMove},
entry! {action=SelectMessage::DragStart, key_down=Lmb},
@@ -126,49 +138,31 @@ impl Default for Mapping {
entry! {action=EyedropperMessage::LeftMouseDown, key_down=Lmb},
entry! {action=EyedropperMessage::RightMouseDown, key_down=Rmb},
// Rectangle
entry! {action=RectangleMessage::Center, key_down=KeyAlt},
entry! {action=RectangleMessage::UnCenter, key_up=KeyAlt},
entry! {action=RectangleMessage::MouseMove, message=InputMapperMessage::PointerMove},
entry! {action=RectangleMessage::DragStart, key_down=Lmb},
entry! {action=RectangleMessage::DragStop, key_up=Lmb},
entry! {action=RectangleMessage::Abort, key_down=Rmb},
entry! {action=RectangleMessage::Abort, key_down=KeyEscape},
entry! {action=RectangleMessage::LockAspectRatio, key_down=KeyShift},
entry! {action=RectangleMessage::UnlockAspectRatio, key_up=KeyShift},
entry! {action=RectangleMessage::Resize{center: KeyAlt, lock_ratio: KeyShift}, triggers=[KeyAlt, KeyShift]},
// Ellipse
entry! {action=EllipseMessage::Center, key_down=KeyAlt},
entry! {action=EllipseMessage::UnCenter, key_up=KeyAlt},
entry! {action=EllipseMessage::MouseMove, message=InputMapperMessage::PointerMove},
entry! {action=EllipseMessage::DragStart, key_down=Lmb},
entry! {action=EllipseMessage::DragStop, key_up=Lmb},
entry! {action=EllipseMessage::Abort, key_down=Rmb},
entry! {action=EllipseMessage::Abort, key_down=KeyEscape},
entry! {action=EllipseMessage::LockAspectRatio, key_down=KeyShift},
entry! {action=EllipseMessage::UnlockAspectRatio, key_up=KeyShift},
entry! {action=EllipseMessage::Resize{center: KeyAlt, lock_ratio: KeyShift}, triggers=[KeyAlt, KeyShift]},
// Shape
entry! {action=ShapeMessage::Center, key_down=KeyAlt},
entry! {action=ShapeMessage::UnCenter, key_up=KeyAlt},
entry! {action=ShapeMessage::MouseMove, message=InputMapperMessage::PointerMove},
entry! {action=ShapeMessage::DragStart, key_down=Lmb},
entry! {action=ShapeMessage::DragStop, key_up=Lmb},
entry! {action=ShapeMessage::Abort, key_down=Rmb},
entry! {action=ShapeMessage::Abort, key_down=KeyEscape},
entry! {action=ShapeMessage::LockAspectRatio, key_down=KeyShift},
entry! {action=ShapeMessage::UnlockAspectRatio, key_up=KeyShift},
entry! {action=ShapeMessage::Resize{center: KeyAlt, lock_ratio: KeyShift}, triggers=[KeyAlt, KeyShift]},
// Line
entry! {action=LineMessage::Center, key_down=KeyAlt},
entry! {action=LineMessage::UnCenter, key_up=KeyAlt},
entry! {action=LineMessage::MouseMove, message=InputMapperMessage::PointerMove},
entry! {action=LineMessage::DragStart, key_down=Lmb},
entry! {action=LineMessage::DragStop, key_up=Lmb},
entry! {action=LineMessage::Abort, key_down=Rmb},
entry! {action=LineMessage::Abort, key_down=KeyEscape},
entry! {action=LineMessage::LockAngle, key_down=KeyControl},
entry! {action=LineMessage::UnlockAngle, key_up=KeyControl},
entry! {action=LineMessage::SnapToAngle, key_down=KeyShift},
entry! {action=LineMessage::UnSnapToAngle, key_up=KeyShift},
entry! {action=LineMessage::Redraw{center: KeyAlt, lock_angle: KeyControl, snap_angle: KeyShift}, triggers=[KeyAlt, KeyShift, KeyControl]},
// Pen
entry! {action=PenMessage::MouseMove, message=InputMapperMessage::PointerMove},
entry! {action=PenMessage::PointerMove, message=InputMapperMessage::PointerMove},
entry! {action=PenMessage::DragStart, key_down=Lmb},
entry! {action=PenMessage::DragStop, key_up=Lmb},
entry! {action=PenMessage::Confirm, key_down=Rmb},
@@ -195,27 +189,28 @@ impl Default for Mapping {
entry! {action=DocumentMessage::DeleteSelectedLayers, key_down=KeyX},
entry! {action=DocumentMessage::DeleteSelectedLayers, key_down=KeyBackspace},
entry! {action=DocumentMessage::ExportDocument, key_down=KeyE, modifiers=[KeyControl]},
entry! {action=DocumentMessage::MouseMove, message=InputMapperMessage::PointerMove},
entry! {action=DocumentMessage::RotateCanvasBegin{snap:false}, key_down=Mmb, modifiers=[KeyControl]},
entry! {action=DocumentMessage::RotateCanvasBegin{snap:true}, key_down=Mmb, modifiers=[KeyControl, KeyShift]},
entry! {action=DocumentMessage::ZoomCanvasBegin, key_down=Mmb, modifiers=[KeyShift]},
entry! {action=DocumentMessage::TranslateCanvasBegin, key_down=Mmb},
entry! {action=DocumentMessage::TranslateCanvasEnd, key_up=Mmb},
entry! {action=DocumentMessage::MultiplyCanvasZoom(PLUS_KEY_ZOOM_RATE), key_down=KeyPlus, modifiers=[KeyControl]},
entry! {action=DocumentMessage::MultiplyCanvasZoom(PLUS_KEY_ZOOM_RATE), key_down=KeyEquals, modifiers=[KeyControl]},
entry! {action=DocumentMessage::MultiplyCanvasZoom(MINUS_KEY_ZOOM_RATE), key_down=KeyMinus, modifiers=[KeyControl]},
entry! {action=DocumentMessage::SetCanvasZoom(1.), key_down=Key1, modifiers=[KeyControl]},
entry! {action=DocumentMessage::SetCanvasZoom(2.), key_down=Key2, modifiers=[KeyControl]},
entry! {action=DocumentMessage::WheelCanvasZoom, message=InputMapperMessage::MouseScroll, modifiers=[KeyControl]},
entry! {action=DocumentMessage::WheelCanvasTranslate{use_y_as_x: true}, message=InputMapperMessage::MouseScroll, modifiers=[KeyShift]},
entry! {action=DocumentMessage::WheelCanvasTranslate{use_y_as_x: false}, message=InputMapperMessage::MouseScroll},
entry! {action=DocumentMessage::NewDocument, key_down=KeyN, modifiers=[KeyControl]},
entry! {action=DocumentMessage::NextDocument, key_down=KeyTab, modifiers=[KeyControl]},
entry! {action=DocumentMessage::PrevDocument, key_down=KeyTab, modifiers=[KeyControl, KeyShift]},
entry! {action=DocumentMessage::CloseAllDocumentsWithConfirmation, key_down=KeyW, modifiers=[KeyControl, KeyAlt]},
entry! {action=DocumentMessage::CloseActiveDocumentWithConfirmation, key_down=KeyW, modifiers=[KeyControl]},
entry! {action=MovementMessage::MouseMove, message=InputMapperMessage::PointerMove},
entry! {action=MovementMessage::RotateCanvasBegin{snap:false}, key_down=Mmb, modifiers=[KeyControl]},
entry! {action=MovementMessage::RotateCanvasBegin{snap:true}, key_down=Mmb, modifiers=[KeyControl, KeyShift]},
entry! {action=MovementMessage::ZoomCanvasBegin, key_down=Mmb, modifiers=[KeyShift]},
entry! {action=MovementMessage::ZoomCanvasToFitAll, key_down=Key0, modifiers=[KeyControl]},
entry! {action=MovementMessage::TranslateCanvasBegin, key_down=Mmb},
entry! {action=MovementMessage::TranslateCanvasEnd, key_up=Mmb},
entry! {action=MovementMessage::MultiplyCanvasZoom(PLUS_KEY_ZOOM_RATE), key_down=KeyPlus, modifiers=[KeyControl]},
entry! {action=MovementMessage::MultiplyCanvasZoom(PLUS_KEY_ZOOM_RATE), key_down=KeyEquals, modifiers=[KeyControl]},
entry! {action=MovementMessage::MultiplyCanvasZoom(MINUS_KEY_ZOOM_RATE), key_down=KeyMinus, modifiers=[KeyControl]},
entry! {action=MovementMessage::SetCanvasZoom(1.), key_down=Key1, modifiers=[KeyControl]},
entry! {action=MovementMessage::SetCanvasZoom(2.), key_down=Key2, modifiers=[KeyControl]},
entry! {action=MovementMessage::WheelCanvasZoom, message=InputMapperMessage::MouseScroll, modifiers=[KeyControl]},
entry! {action=MovementMessage::WheelCanvasTranslate{use_y_as_x: true}, message=InputMapperMessage::MouseScroll, modifiers=[KeyShift]},
entry! {action=MovementMessage::WheelCanvasTranslate{use_y_as_x: false}, message=InputMapperMessage::MouseScroll},
entry! {action=DocumentsMessage::NewDocument, key_down=KeyN, modifiers=[KeyControl]},
entry! {action=DocumentsMessage::NextDocument, key_down=KeyTab, modifiers=[KeyControl]},
entry! {action=DocumentsMessage::PrevDocument, key_down=KeyTab, modifiers=[KeyControl, KeyShift]},
entry! {action=DocumentsMessage::CloseAllDocumentsWithConfirmation, key_down=KeyW, modifiers=[KeyControl, KeyAlt]},
entry! {action=DocumentsMessage::CloseActiveDocumentWithConfirmation, key_down=KeyW, modifiers=[KeyControl]},
entry! {action=DocumentMessage::DuplicateSelectedLayers, key_down=KeyD, modifiers=[KeyControl]},
entry! {action=DocumentMessage::CopySelectedLayers, key_down=KeyC, modifiers=[KeyControl]},
entry! {action=DocumentsMessage::CopySelectedLayers, key_down=KeyC, modifiers=[KeyControl]},
entry! {action=DocumentMessage::NudgeSelectedLayers(-SHIFT_NUDGE_AMOUNT, -SHIFT_NUDGE_AMOUNT), key_down=KeyArrowUp, modifiers=[KeyShift, KeyArrowLeft]},
entry! {action=DocumentMessage::NudgeSelectedLayers(SHIFT_NUDGE_AMOUNT, -SHIFT_NUDGE_AMOUNT), key_down=KeyArrowUp, modifiers=[KeyShift, KeyArrowRight]},
entry! {action=DocumentMessage::NudgeSelectedLayers(0., -SHIFT_NUDGE_AMOUNT), key_down=KeyArrowUp, modifiers=[KeyShift]},
@@ -289,19 +284,16 @@ pub struct InputMapper {
impl InputMapper {
pub fn hints(&self, actions: ActionList) -> String {
let mut output = String::new();
let actions: Vec<MessageDiscriminant> = actions
let mut actions = actions
.into_iter()
.flatten()
.filter(|a| !matches!(*a, MessageDiscriminant::Tool(ToolMessageDiscriminant::SelectTool) | MessageDiscriminant::Global(_)))
.collect();
.filter(|a| !matches!(*a, MessageDiscriminant::Tool(ToolMessageDiscriminant::SelectTool) | MessageDiscriminant::Global(_)));
self.mapping
.key_down
.iter()
.enumerate()
.filter_map(|(i, m)| {
let ma =
m.0.iter()
.find_map(|m| actions.iter().find_map(|a| (a == &m.action.to_discriminant()).then(|| m.action.to_discriminant())));
let ma = m.0.iter().find_map(|m| actions.find_map(|a| (a == m.action.to_discriminant()).then(|| m.action.to_discriminant())));
ma.map(|a| unsafe { (std::mem::transmute_copy::<usize, Key>(&i), a) })
})
+3 -4
View File
@@ -7,7 +7,6 @@ use bitflags::bitflags;
#[doc(inline)]
pub use document_core::DocumentResponse;
use glam::DVec2;
#[impl_message(Message, InputPreprocessor)]
#[derive(PartialEq, Clone, Debug)]
@@ -31,7 +30,7 @@ bitflags! {
}
}
#[derive(Debug, Default)]
#[derive(Debug, Default, Hash)]
pub struct InputPreprocessor {
pub keyboard: KeyStates,
pub mouse: MouseState,
@@ -78,7 +77,7 @@ impl MessageHandler<InputPreprocessorMessage, ()> for InputPreprocessor {
responses.push_back(
document_core::Operation::TransformLayer {
path: vec![],
transform: glam::DAffine2::from_translation(DVec2::new((size.x as f64 - self.viewport_size.x as f64) / 2., (size.y as f64 - self.viewport_size.y as f64) / 2.)).to_cols_array(),
transform: glam::DAffine2::from_translation((size.as_f64() - self.viewport_size.as_f64()) / 2.).to_cols_array(),
}
.into(),
);
@@ -137,7 +136,7 @@ mod test {
#[test]
fn process_action_mouse_move_handle_modifier_keys() {
let mut input_preprocessor = InputPreprocessor::default();
let message = InputPreprocessorMessage::MouseMove(ViewportPosition { x: 4, y: 809 }, ModifierKeys::ALT);
let message = InputPreprocessorMessage::MouseMove((4, 809).into(), ModifierKeys::ALT);
let mut responses = VecDeque::new();
input_preprocessor.process_action(message, (), &mut responses);
+1 -1
View File
@@ -112,7 +112,7 @@ impl<const LENGTH: usize> BitVector<LENGTH> {
let (offset, bit) = Self::convert_index(bitvector_index);
self.0[offset] ^= bit;
}
pub fn get(&mut self, bitvector_index: usize) -> bool {
pub fn get(&self, bitvector_index: usize) -> bool {
let (offset, bit) = Self::convert_index(bitvector_index);
(self.0[offset] & bit) != 0
}
+4 -19
View File
@@ -2,24 +2,9 @@ use bitflags::bitflags;
use glam::DVec2;
// origin is top left
#[derive(Debug, Copy, Clone, Default, Eq, PartialEq)]
pub struct ViewportPosition {
pub x: u32,
pub y: u32,
}
pub type ViewportPosition = glam::UVec2;
impl ViewportPosition {
pub fn distance(&self, other: &Self) -> f64 {
let x_diff = other.x as i64 - self.x as i64;
let y_diff = other.y as i64 - self.y as i64;
f64::sqrt((x_diff * x_diff + y_diff * y_diff) as f64)
}
pub fn as_dvec2(&self) -> DVec2 {
DVec2::new(self.x as f64, self.y as f64)
}
}
#[derive(Debug, Copy, Clone, Default, Eq, PartialEq)]
#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Hash)]
pub struct ScrollDelta {
pub x: i32,
pub y: i32,
@@ -38,7 +23,7 @@ impl ScrollDelta {
}
}
#[derive(Debug, Copy, Clone, Default, Eq, PartialEq)]
#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Hash)]
pub struct MouseState {
pub position: ViewportPosition,
pub mouse_keys: MouseKeys,
@@ -52,7 +37,7 @@ impl MouseState {
pub fn from_pos(x: u32, y: u32) -> MouseState {
MouseState {
position: ViewportPosition { x, y },
position: (x, y).into(),
mouse_keys: MouseKeys::default(),
scroll_delta: ScrollDelta::default(),
}
+3
View File
@@ -57,9 +57,12 @@ impl Editor {
}
pub mod message_prelude {
pub use crate::communication::generate_hash;
pub use crate::communication::message::{AsMessage, Message, MessageDiscriminant};
pub use crate::communication::{ActionList, MessageHandler};
pub use crate::document::{DocumentMessage, DocumentMessageDiscriminant};
pub use crate::document::{DocumentsMessage, DocumentsMessageDiscriminant};
pub use crate::document::{MovementMessage, MovementMessageDiscriminant};
pub use crate::frontend::{FrontendMessage, FrontendMessageDiscriminant};
pub use crate::global::{GlobalMessage, GlobalMessageDiscriminant};
pub use crate::input::{InputMapperMessage, InputMapperMessageDiscriminant, InputPreprocessorMessage, InputPreprocessorMessageDiscriminant};
+2
View File
@@ -15,6 +15,8 @@ pub enum EditorError {
UnknownTool,
#[error("The operation caused a document error {0:?}")]
Document(String),
#[error("A Rollback was initated but no transaction was in progress")]
NoTransactionInProgress,
}
macro_rules! derive_from {
+4 -4
View File
@@ -1,6 +1,6 @@
use crate::{
input::{
mouse::{MouseKeys, MouseState, ScrollDelta, ViewportPosition},
mouse::{MouseKeys, MouseState, ScrollDelta},
InputPreprocessorMessage, ModifierKeys,
},
message_prelude::{Message, ToolMessage},
@@ -45,14 +45,14 @@ impl EditorTestUtils for Editor {
self.lmb_mousedown(x1, y1);
self.move_mouse(x2, y2);
self.mouseup(MouseState {
position: ViewportPosition { x: x2, y: y2 },
position: (x2, y2).into(),
mouse_keys: MouseKeys::empty(),
scroll_delta: ScrollDelta::default(),
});
}
fn move_mouse(&mut self, x: u32, y: u32) {
self.input(InputPreprocessorMessage::MouseMove(ViewportPosition { x, y }, ModifierKeys::default()));
self.input(InputPreprocessorMessage::MouseMove((x, y).into(), ModifierKeys::default()));
}
fn mousedown(&mut self, state: MouseState) {
@@ -65,7 +65,7 @@ impl EditorTestUtils for Editor {
fn lmb_mousedown(&mut self, x: u32, y: u32) {
self.mousedown(MouseState {
position: ViewportPosition { x, y },
position: (x, y).into(),
mouse_keys: MouseKeys::LEFT,
scroll_delta: ScrollDelta::default(),
})
+11 -3
View File
@@ -2,7 +2,7 @@ pub mod tool_message_handler;
pub mod tool_options;
pub mod tools;
use crate::document::Document;
use crate::document::DocumentMessageHandler;
use crate::input::InputPreprocessor;
use crate::message_prelude::*;
use crate::{
@@ -25,12 +25,20 @@ pub mod tool_messages {
pub use super::tools::rectangle::{RectangleMessage, RectangleMessageDiscriminant};
}
pub type ToolActionHandlerData<'a> = (&'a Document, &'a DocumentToolData, &'a InputPreprocessor);
pub type ToolActionHandlerData<'a> = (&'a DocumentMessageHandler, &'a DocumentToolData, &'a InputPreprocessor);
pub trait Fsm {
type ToolData;
fn transition(self, message: ToolMessage, document: &Document, tool_data: &DocumentToolData, data: &mut Self::ToolData, input: &InputPreprocessor, messages: &mut VecDeque<Message>) -> Self;
fn transition(
self,
message: ToolMessage,
document: &DocumentMessageHandler,
tool_data: &DocumentToolData,
data: &mut Self::ToolData,
input: &InputPreprocessor,
messages: &mut VecDeque<Message>,
) -> Self;
}
#[derive(Debug, Clone)]
+4 -4
View File
@@ -3,7 +3,7 @@ use document_core::color::Color;
use crate::input::InputPreprocessor;
use crate::{
document::Document,
document::DocumentMessageHandler,
tool::{tool_options::ToolOptions, DocumentToolData, ToolFsmState, ToolType},
};
use std::collections::VecDeque;
@@ -45,8 +45,8 @@ pub enum ToolMessage {
pub struct ToolMessageHandler {
tool_state: ToolFsmState,
}
impl MessageHandler<ToolMessage, (&Document, &InputPreprocessor)> for ToolMessageHandler {
fn process_action(&mut self, message: ToolMessage, data: (&Document, &InputPreprocessor), responses: &mut VecDeque<Message>) {
impl MessageHandler<ToolMessage, (&DocumentMessageHandler, &InputPreprocessor)> for ToolMessageHandler {
fn process_action(&mut self, message: ToolMessage, data: (&DocumentMessageHandler, &InputPreprocessor), responses: &mut VecDeque<Message>) {
let (document, input) = data;
use ToolMessage::*;
match message {
@@ -103,7 +103,7 @@ impl MessageHandler<ToolMessage, (&Document, &InputPreprocessor)> for ToolMessag
_ => unreachable!(),
};
if let Some(tool) = self.tool_state.tool_data.tools.get_mut(&tool_type) {
tool.process_action(message, (&document, &self.tool_state.document_tool_data, input), responses);
tool.process_action(message, (document, &self.tool_state.document_tool_data, input), responses);
}
}
}
+3 -3
View File
@@ -1,14 +1,14 @@
use serde::{Deserialize, Serialize};
// TODO: Rename this `ToolOption` to not be plural in a separate commit (together with `enum LayerDataTypes`)
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Hash)]
pub enum ToolOptions {
Select { append_mode: SelectAppendMode },
Ellipse,
Shape { shape_type: ShapeType },
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Hash)]
pub enum SelectAppendMode {
New,
Add,
@@ -16,7 +16,7 @@ pub enum SelectAppendMode {
Intersect,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Hash)]
pub enum ShapeType {
Star { vertices: u32 },
Polygon { vertices: u32 },
+1 -1
View File
@@ -5,7 +5,7 @@ use crate::tool::ToolActionHandlerData;
pub struct Crop;
#[impl_message(Message, ToolMessage, Crop)]
#[derive(PartialEq, Clone, Debug)]
#[derive(PartialEq, Clone, Debug, Hash)]
pub enum CropMessage {
MouseMove,
}
+50 -102
View File
@@ -1,8 +1,11 @@
use crate::input::{mouse::ViewportPosition, InputPreprocessor};
use crate::input::keyboard::Key;
use crate::input::InputPreprocessor;
use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
use crate::{document::Document, message_prelude::*};
use crate::{document::DocumentMessageHandler, message_prelude::*};
use document_core::{layers::style, Operation};
use glam::{DAffine2, DVec2};
use glam::DAffine2;
use super::resize::*;
#[derive(Default)]
pub struct Ellipse {
@@ -11,17 +14,12 @@ pub struct Ellipse {
}
#[impl_message(Message, ToolMessage, Ellipse)]
#[derive(PartialEq, Clone, Debug)]
#[derive(PartialEq, Clone, Debug, Hash)]
pub enum EllipseMessage {
Undo,
DragStart,
DragStop,
MouseMove,
Resize { center: Key, lock_ratio: Key },
Abort,
Center,
UnCenter,
LockAspectRatio,
UnlockAspectRatio,
}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Ellipse {
@@ -31,8 +29,8 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Ellipse {
fn actions(&self) -> ActionList {
use EllipseToolFsmState::*;
match self.fsm_state {
Ready => actions!(EllipseMessageDiscriminant; Undo, DragStart, Center, UnCenter, LockAspectRatio, UnlockAspectRatio),
Dragging => actions!(EllipseMessageDiscriminant; DragStop, Center, UnCenter, LockAspectRatio, UnlockAspectRatio, MouseMove, Abort),
Ready => actions!(EllipseMessageDiscriminant; DragStart),
Dragging => actions!(EllipseMessageDiscriminant; DragStop, Abort, Resize),
}
}
}
@@ -50,63 +48,68 @@ impl Default for EllipseToolFsmState {
}
#[derive(Clone, Debug, Default)]
struct EllipseToolData {
drag_start: ViewportPosition,
drag_current: ViewportPosition,
constrain_to_circle: bool,
center_around_cursor: bool,
sides: u8,
data: Resize,
}
impl Fsm for EllipseToolFsmState {
type ToolData = EllipseToolData;
fn transition(self, event: ToolMessage, document: &Document, tool_data: &DocumentToolData, data: &mut Self::ToolData, input: &InputPreprocessor, responses: &mut VecDeque<Message>) -> Self {
let transform = document.document.root.transform;
fn transition(
self,
event: ToolMessage,
document: &DocumentMessageHandler,
tool_data: &DocumentToolData,
data: &mut Self::ToolData,
input: &InputPreprocessor,
responses: &mut VecDeque<Message>,
) -> Self {
let mut shape_data = &mut data.data;
use EllipseMessage::*;
use EllipseToolFsmState::*;
if let ToolMessage::Ellipse(event) = event {
match (self, event) {
(Ready, DragStart) => {
data.drag_start = input.mouse.position;
data.drag_current = input.mouse.position;
responses.push_back(Operation::MountWorkingFolder { path: vec![] }.into());
Dragging
}
(Dragging, MouseMove) => {
data.drag_current = input.mouse.position;
shape_data.drag_start = input.mouse.position;
responses.push_back(DocumentMessage::StartTransaction.into());
shape_data.path = Some(vec![generate_hash(&*responses, input, document.document.hash())]);
responses.push_back(DocumentMessage::DeselectAllLayers.into());
responses.push_back(Operation::ClearWorkingFolder.into());
responses.push_back(make_operation(data, tool_data, transform));
responses.push_back(
Operation::AddEllipse {
path: shape_data.path.clone().unwrap(),
insert_index: -1,
transform: DAffine2::ZERO.to_cols_array(),
style: style::PathStyle::new(None, Some(style::Fill::new(tool_data.primary_color))),
}
.into(),
);
Dragging
}
(Dragging, DragStop) => {
data.drag_current = input.mouse.position;
responses.push_back(Operation::ClearWorkingFolder.into());
// TODO - introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100)
if data.drag_start != data.drag_current {
responses.push_back(make_operation(data, tool_data, transform));
responses.push_back(DocumentMessage::DeselectAllLayers.into());
responses.push_back(Operation::CommitTransaction.into());
(state, Resize { center, lock_ratio }) => {
if let Some(message) = shape_data.calculate_transform(center, lock_ratio, input) {
responses.push_back(message);
}
state
}
(Dragging, DragStop) => {
// TODO: introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100)
match shape_data.drag_start == input.mouse.position {
true => responses.push_back(DocumentMessage::AbortTransaction.into()),
false => responses.push_back(DocumentMessage::CommitTransaction.into()),
}
shape_data.path = None;
Ready
}
// TODO - simplify with or_patterns when rust 1.53.0 is stable (https://github.com/rust-lang/rust/issues/54883)
(Dragging, Abort) => {
responses.push_back(Operation::DiscardWorkingFolder.into());
responses.push_back(DocumentMessage::AbortTransaction.into());
shape_data.path = None;
Ready
}
(Ready, LockAspectRatio) => update_state_no_op(&mut data.constrain_to_circle, true, Ready),
(Ready, UnlockAspectRatio) => update_state_no_op(&mut data.constrain_to_circle, false, Ready),
(Dragging, LockAspectRatio) => update_state(|data| &mut data.constrain_to_circle, true, tool_data, data, responses, Dragging, transform),
(Dragging, UnlockAspectRatio) => update_state(|data| &mut data.constrain_to_circle, false, tool_data, data, responses, Dragging, transform),
(Ready, Center) => update_state_no_op(&mut data.center_around_cursor, true, Ready),
(Ready, UnCenter) => update_state_no_op(&mut data.center_around_cursor, false, Ready),
(Dragging, Center) => update_state(|data| &mut data.center_around_cursor, true, tool_data, data, responses, Dragging, transform),
(Dragging, UnCenter) => update_state(|data| &mut data.center_around_cursor, false, tool_data, data, responses, Dragging, transform),
_ => self,
}
} else {
@@ -114,58 +117,3 @@ impl Fsm for EllipseToolFsmState {
}
}
}
fn update_state_no_op(state: &mut bool, value: bool, new_state: EllipseToolFsmState) -> EllipseToolFsmState {
*state = value;
new_state
}
fn update_state(
state: fn(&mut EllipseToolData) -> &mut bool,
value: bool,
tool_data: &DocumentToolData,
data: &mut EllipseToolData,
responses: &mut VecDeque<Message>,
new_state: EllipseToolFsmState,
transform: DAffine2,
) -> EllipseToolFsmState {
*(state(data)) = value;
responses.push_back(Operation::ClearWorkingFolder.into());
responses.push_back(make_operation(&data, tool_data, transform));
new_state
}
fn make_operation(data: &EllipseToolData, tool_data: &DocumentToolData, transform: DAffine2) -> Message {
let x0 = data.drag_start.x as f64;
let y0 = data.drag_start.y as f64;
let x1 = data.drag_current.x as f64;
let y1 = data.drag_current.y as f64;
if data.constrain_to_circle {
let (cx, cy, r) = if data.center_around_cursor {
(x0, y0, f64::hypot(x1 - x0, y1 - y0))
} else {
let diameter = f64::max((x1 - x0).abs(), (y1 - y0).abs());
let (x2, y2) = (x0 + (x1 - x0).signum() * diameter, y0 + (y1 - y0).signum() * diameter);
((x0 + x2) * 0.5, (y0 + y2) * 0.5, diameter * 0.5)
};
Operation::AddEllipse {
path: vec![],
insert_index: -1,
transform: (transform.inverse() * glam::DAffine2::from_scale_angle_translation(DVec2::new(r, r), 0., DVec2::new(cx, cy))).to_cols_array(),
style: style::PathStyle::new(None, Some(style::Fill::new(tool_data.primary_color))),
}
} else {
let (cx, cy, r_scale) = if data.center_around_cursor { (x0, y0, 1.0) } else { ((x0 + x1) * 0.5, (y0 + y1) * 0.5, 0.5) };
let (rx, ry) = ((x1 - x0).abs() * r_scale, (y1 - y0).abs() * r_scale);
Operation::AddEllipse {
path: vec![],
insert_index: -1,
transform: (transform.inverse() * glam::DAffine2::from_scale_angle_translation(DVec2::new(rx, ry), 0., DVec2::new(cx, cy))).to_cols_array(),
style: style::PathStyle::new(None, Some(style::Fill::new(tool_data.primary_color))),
}
}
.into()
}
+13 -19
View File
@@ -1,13 +1,15 @@
use crate::consts::SELECTION_TOLERANCE;
use crate::message_prelude::*;
use crate::tool::{ToolActionHandlerData, ToolMessage};
use document_core::layers::LayerDataType;
use document_core::Quad;
use glam::DVec2;
#[derive(Default)]
pub struct Eyedropper;
#[impl_message(Message, ToolMessage, Eyedropper)]
#[derive(PartialEq, Clone, Debug)]
#[derive(PartialEq, Clone, Debug, Hash)]
pub enum EyedropperMessage {
LeftMouseDown,
RightMouseDown,
@@ -16,27 +18,19 @@ pub enum EyedropperMessage {
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Eyedropper {
fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
let mouse_pos = data.2.mouse.position;
let (x, y) = (mouse_pos.x as f64, mouse_pos.y as f64);
let (point_1, point_2) = (
DVec2::new(x - SELECTION_TOLERANCE, y - SELECTION_TOLERANCE),
DVec2::new(x + SELECTION_TOLERANCE, y + SELECTION_TOLERANCE),
);
let quad = [
DVec2::new(point_1.x, point_1.y),
DVec2::new(point_2.x, point_1.y),
DVec2::new(point_2.x, point_2.y),
DVec2::new(point_1.x, point_2.y),
];
let tolerance = DVec2::splat(SELECTION_TOLERANCE);
let quad = Quad::from_box([mouse_pos.as_f64() - tolerance, mouse_pos.as_f64() + tolerance]);
if let Some(path) = data.0.document.intersects_quad_root(quad).last() {
if let Ok(layer) = data.0.document.layer(path) {
if let Some(color) = layer.style.fill().and_then(|fill| fill.color()) {
match action {
ToolMessage::Eyedropper(EyedropperMessage::LeftMouseDown) => responses.push_back(ToolMessage::SelectPrimaryColor(color).into()),
ToolMessage::Eyedropper(EyedropperMessage::RightMouseDown) => responses.push_back(ToolMessage::SelectSecondaryColor(color).into()),
_ => {}
}
if let LayerDataType::Shape(s) = &layer.data {
s.style.fill().and_then(|fill| {
fill.color().map(|color| match action {
ToolMessage::Eyedropper(EyedropperMessage::LeftMouseDown) => responses.push_back(ToolMessage::SelectPrimaryColor(color).into()),
ToolMessage::Eyedropper(EyedropperMessage::RightMouseDown) => responses.push_back(ToolMessage::SelectSecondaryColor(color).into()),
_ => {}
})
});
}
}
}
+4 -14
View File
@@ -1,14 +1,14 @@
use crate::consts::SELECTION_TOLERANCE;
use crate::message_prelude::*;
use crate::tool::ToolActionHandlerData;
use document_core::Operation;
use document_core::{Operation, Quad};
use glam::DVec2;
#[derive(Default)]
pub struct Fill;
#[impl_message(Message, ToolMessage, Fill)]
#[derive(PartialEq, Clone, Debug)]
#[derive(PartialEq, Clone, Debug, Hash)]
pub enum FillMessage {
MouseDown,
}
@@ -16,18 +16,8 @@ pub enum FillMessage {
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Fill {
fn process_action(&mut self, _action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
let mouse_pos = data.2.mouse.position;
let (x, y) = (mouse_pos.x as f64, mouse_pos.y as f64);
let (point_1, point_2) = (
DVec2::new(x - SELECTION_TOLERANCE, y - SELECTION_TOLERANCE),
DVec2::new(x + SELECTION_TOLERANCE, y + SELECTION_TOLERANCE),
);
let quad = [
DVec2::new(point_1.x, point_1.y),
DVec2::new(point_2.x, point_1.y),
DVec2::new(point_2.x, point_2.y),
DVec2::new(point_1.x, point_2.y),
];
let tolerance = DVec2::splat(SELECTION_TOLERANCE);
let quad = Quad::from_box([mouse_pos.as_f64() - tolerance, mouse_pos.as_f64() + tolerance]);
if let Some(path) = data.0.document.intersects_quad_root(quad).last() {
responses.push_back(
+63 -87
View File
@@ -1,11 +1,13 @@
use std::f64::consts::PI;
use crate::consts::LINE_ROTATE_SNAP_ANGLE;
use crate::input::keyboard::Key;
use crate::input::{mouse::ViewportPosition, InputPreprocessor};
use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
use crate::{document::Document, message_prelude::*};
use crate::{document::DocumentMessageHandler, message_prelude::*};
use document_core::{layers::style, Operation};
use glam::{DAffine2, DVec2};
use std::f64::consts::PI;
#[derive(Default)]
pub struct Line {
fsm_state: LineToolFsmState,
@@ -13,18 +15,12 @@ pub struct Line {
}
#[impl_message(Message, ToolMessage, Line)]
#[derive(PartialEq, Clone, Debug)]
#[derive(PartialEq, Clone, Debug, Hash)]
pub enum LineMessage {
DragStart,
DragStop,
MouseMove,
Redraw { center: Key, lock_angle: Key, snap_angle: Key },
Abort,
Center,
UnCenter,
LockAngle,
UnlockAngle,
SnapToAngle,
UnSnapToAngle,
}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Line {
@@ -34,8 +30,8 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Line {
fn actions(&self) -> ActionList {
use LineToolFsmState::*;
match self.fsm_state {
Ready => actions!(LineMessageDiscriminant; DragStart, Center, UnCenter, LockAngle, UnlockAngle, SnapToAngle, UnSnapToAngle),
Dragging => actions!(LineMessageDiscriminant; DragStop, MouseMove, Abort, Center, UnCenter, LockAngle, UnlockAngle, SnapToAngle, UnSnapToAngle),
Ready => actions!(LineMessageDiscriminant; DragStart),
Dragging => actions!(LineMessageDiscriminant; DragStop, Redraw, Abort),
}
}
}
@@ -56,69 +52,69 @@ struct LineToolData {
drag_start: ViewportPosition,
drag_current: ViewportPosition,
angle: f64,
snap_angle: bool,
lock_angle: bool,
center_around_cursor: bool,
path: Option<Vec<LayerId>>,
}
impl Fsm for LineToolFsmState {
type ToolData = LineToolData;
fn transition(self, event: ToolMessage, document: &Document, tool_data: &DocumentToolData, data: &mut Self::ToolData, input: &InputPreprocessor, responses: &mut VecDeque<Message>) -> Self {
let transform = document.document.root.transform;
fn transition(
self,
event: ToolMessage,
document: &DocumentMessageHandler,
tool_data: &DocumentToolData,
data: &mut Self::ToolData,
input: &InputPreprocessor,
responses: &mut VecDeque<Message>,
) -> Self {
use LineMessage::*;
use LineToolFsmState::*;
if let ToolMessage::Line(event) = event {
match (self, event) {
(Ready, DragStart) => {
data.drag_start = input.mouse.position;
data.drag_current = input.mouse.position;
responses.push_back(DocumentMessage::StartTransaction.into());
data.path = Some(vec![generate_hash(&*responses, input, document.document.hash())]);
responses.push_back(DocumentMessage::DeselectAllLayers.into());
responses.push_back(Operation::MountWorkingFolder { path: vec![] }.into());
responses.push_back(
Operation::AddLine {
path: data.path.clone().unwrap(),
insert_index: -1,
transform: DAffine2::ZERO.to_cols_array(),
style: style::PathStyle::new(Some(style::Stroke::new(tool_data.primary_color, 5.)), None),
}
.into(),
);
Dragging
}
(Dragging, MouseMove) => {
(Dragging, Redraw { center, snap_angle, lock_angle }) => {
data.drag_current = input.mouse.position;
responses.push_back(Operation::ClearWorkingFolder.into());
responses.push_back(make_operation(data, tool_data, transform));
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;
responses.push_back(Operation::ClearWorkingFolder.into());
// TODO - introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100)
if data.drag_start != data.drag_current {
responses.push_back(make_operation(data, tool_data, transform));
responses.push_back(DocumentMessage::DeselectAllLayers.into());
responses.push_back(Operation::CommitTransaction.into());
// TODO; introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100)
match data.drag_start == input.mouse.position {
true => responses.push_back(DocumentMessage::AbortTransaction.into()),
false => responses.push_back(DocumentMessage::CommitTransaction.into()),
}
data.path = None;
Ready
}
// TODO - simplify with or_patterns when rust 1.53.0 is stable (https://github.com/rust-lang/rust/issues/54883)
(Dragging, Abort) => {
responses.push_back(Operation::DiscardWorkingFolder.into());
responses.push_back(DocumentMessage::AbortTransaction.into());
data.path = None;
Ready
}
(Ready, LockAngle) => update_state_no_op(&mut data.lock_angle, true, Ready),
(Ready, UnlockAngle) => update_state_no_op(&mut data.lock_angle, false, Ready),
(Dragging, LockAngle) => update_state(|data| &mut data.lock_angle, true, tool_data, data, responses, Dragging, transform),
(Dragging, UnlockAngle) => update_state(|data| &mut data.lock_angle, false, tool_data, data, responses, Dragging, transform),
(Ready, SnapToAngle) => update_state_no_op(&mut data.snap_angle, true, Ready),
(Ready, UnSnapToAngle) => update_state_no_op(&mut data.snap_angle, false, Ready),
(Dragging, SnapToAngle) => update_state(|data| &mut data.snap_angle, true, tool_data, data, responses, Dragging, transform),
(Dragging, UnSnapToAngle) => update_state(|data| &mut data.snap_angle, false, tool_data, data, responses, Dragging, transform),
(Ready, Center) => update_state_no_op(&mut data.center_around_cursor, true, Ready),
(Ready, UnCenter) => update_state_no_op(&mut data.center_around_cursor, false, Ready),
(Dragging, Center) => update_state(|data| &mut data.center_around_cursor, true, tool_data, data, responses, Dragging, transform),
(Dragging, UnCenter) => update_state(|data| &mut data.center_around_cursor, false, tool_data, data, responses, Dragging, transform),
_ => self,
}
} else {
@@ -127,59 +123,39 @@ impl Fsm for LineToolFsmState {
}
}
fn update_state_no_op(state: &mut bool, value: bool, new_state: LineToolFsmState) -> LineToolFsmState {
*state = value;
new_state
}
fn generate_transform(data: &mut LineToolData, lock: bool, snap: bool, center: bool) -> Message {
let mut start = data.drag_start.as_f64();
let stop = data.drag_current.as_f64();
fn update_state(
state: fn(&mut LineToolData) -> &mut bool,
value: bool,
tool_data: &DocumentToolData,
data: &mut LineToolData,
responses: &mut VecDeque<Message>,
new_state: LineToolFsmState,
transform: DAffine2,
) -> LineToolFsmState {
*(state(data)) = value;
let dir = stop - start;
responses.push_back(Operation::ClearWorkingFolder.into());
responses.push_back(make_operation(data, tool_data, transform));
let mut angle = -dir.angle_between(DVec2::X);
new_state
}
fn make_operation(data: &mut LineToolData, tool_data: &DocumentToolData, transform: DAffine2) -> Message {
let x0 = data.drag_start.x as f64;
let y0 = data.drag_start.y as f64;
let x1 = data.drag_current.x as f64;
let y1 = data.drag_current.y as f64;
let (dx, dy) = (x1 - x0, y1 - y0);
let mut angle = f64::atan2(dx, dy);
if data.lock_angle {
if lock {
angle = data.angle
};
if data.snap_angle {
let snap_resolution = 12.0;
angle = (angle * snap_resolution / PI).round() / snap_resolution * PI;
if snap {
let snap_resolution = LINE_ROTATE_SNAP_ANGLE.to_radians();
angle = (angle / snap_resolution).round() * snap_resolution;
}
data.angle = angle;
let (dir_x, dir_y) = (f64::sin(angle), f64::cos(angle));
let projected_length = dx * dir_x + dy * dir_y;
let (x1, y1) = (x0 + dir_x * projected_length, y0 + dir_y * projected_length);
let mut scale = dir.length();
let (x0, y0) = if data.center_around_cursor { (x0 - (x1 - x0), y0 - (y1 - y0)) } else { (x0, y0) };
if lock {
let angle_vec = DVec2::new(angle.cos(), angle.sin());
scale = dir.dot(angle_vec);
}
Operation::AddLine {
path: vec![],
insert_index: -1,
transform: (transform.inverse() * glam::DAffine2::from_scale_angle_translation(DVec2::new(x1 - x0, y1 - y0), 0., DVec2::new(x0, y0))).to_cols_array(),
style: style::PathStyle::new(Some(style::Stroke::new(tool_data.primary_color, 5.)), None),
if center {
start -= dir / 2.;
}
Operation::SetLayerTransformInViewport {
path: data.path.clone().unwrap(),
transform: glam::DAffine2::from_scale_angle_translation(DVec2::splat(scale), angle, start).to_cols_array(),
}
.into()
}
+1
View File
@@ -4,6 +4,7 @@ pub mod fill;
pub mod line;
pub mod pen;
pub mod rectangle;
pub mod resize;
pub mod shape;
// not implemented yet
+1 -1
View File
@@ -5,7 +5,7 @@ use crate::tool::ToolActionHandlerData;
pub struct Navigate;
#[impl_message(Message, ToolMessage, Navigate)]
#[derive(PartialEq, Clone, Debug)]
#[derive(PartialEq, Clone, Debug, Hash)]
pub enum NavigateMessage {
MouseMove,
}
+1 -1
View File
@@ -5,7 +5,7 @@ use crate::tool::ToolActionHandlerData;
pub struct Path;
#[impl_message(Message, ToolMessage, Path)]
#[derive(PartialEq, Clone, Debug)]
#[derive(PartialEq, Clone, Debug, Hash)]
pub enum PathMessage {
MouseMove,
}
+40 -29
View File
@@ -1,6 +1,6 @@
use crate::input::InputPreprocessor;
use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
use crate::{document::Document, message_prelude::*};
use crate::{document::DocumentMessageHandler, message_prelude::*};
use document_core::{layers::style, Operation};
use glam::DAffine2;
@@ -11,12 +11,12 @@ pub struct Pen {
}
#[impl_message(Message, ToolMessage, Pen)]
#[derive(PartialEq, Clone, Debug)]
#[derive(PartialEq, Clone, Debug, Hash)]
pub enum PenMessage {
Undo,
DragStart,
DragStop,
MouseMove,
PointerMove,
Confirm,
Abort,
}
@@ -35,7 +35,7 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Pen {
use PenToolFsmState::*;
match self.fsm_state {
Ready => actions!(PenMessageDiscriminant; Undo, DragStart, DragStop, Confirm, Abort),
Dragging => actions!(PenMessageDiscriminant; DragStop, MouseMove, Confirm, Abort),
Dragging => actions!(PenMessageDiscriminant; DragStop, PointerMove, Confirm, Abort),
}
}
}
@@ -49,21 +49,32 @@ impl Default for PenToolFsmState {
struct PenToolData {
points: Vec<DAffine2>,
next_point: DAffine2,
path: Option<Vec<LayerId>>,
}
impl Fsm for PenToolFsmState {
type ToolData = PenToolData;
fn transition(self, event: ToolMessage, document: &Document, tool_data: &DocumentToolData, data: &mut Self::ToolData, input: &InputPreprocessor, responses: &mut VecDeque<Message>) -> Self {
fn transition(
self,
event: ToolMessage,
document: &DocumentMessageHandler,
tool_data: &DocumentToolData,
data: &mut Self::ToolData,
input: &InputPreprocessor,
responses: &mut VecDeque<Message>,
) -> Self {
let transform = document.document.root.transform;
let pos = transform.inverse() * DAffine2::from_translation(input.mouse.position.as_dvec2());
let pos = transform.inverse() * DAffine2::from_translation(input.mouse.position.as_f64());
use PenMessage::*;
use PenToolFsmState::*;
if let ToolMessage::Pen(event) = event {
match (self, event) {
(Ready, DragStart) => {
responses.push_back(Operation::MountWorkingFolder { path: vec![] }.into());
responses.push_back(DocumentMessage::StartTransaction.into());
responses.push_back(DocumentMessage::DeselectAllLayers.into());
data.path = Some(vec![generate_hash(&*responses, input, document.document.hash())]);
data.points.push(pos);
data.next_point = pos;
@@ -71,44 +82,41 @@ impl Fsm for PenToolFsmState {
Dragging
}
(Dragging, DragStop) => {
// 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) {
data.points.push(pos);
data.next_point = pos;
}
responses.push_back(Operation::ClearWorkingFolder.into());
responses.push_back(make_operation(data, tool_data, true));
responses.extend(make_operation(data, tool_data, true));
Dragging
}
(Dragging, MouseMove) => {
(Dragging, PointerMove) => {
data.next_point = pos;
responses.push_back(Operation::ClearWorkingFolder.into());
responses.push_back(make_operation(data, tool_data, true));
responses.extend(make_operation(data, tool_data, true));
Dragging
}
// TODO - simplify with or_patterns when rust 1.53.0 is stable (https://github.com/rust-lang/rust/issues/54883)
(Dragging, Confirm) => {
responses.push_back(Operation::ClearWorkingFolder.into());
if data.points.len() >= 2 {
responses.push_back(make_operation(data, tool_data, false));
responses.push_back(DocumentMessage::DeselectAllLayers.into());
responses.push_back(Operation::CommitTransaction.into());
responses.extend(make_operation(data, tool_data, false));
responses.push_back(DocumentMessage::CommitTransaction.into());
} else {
responses.push_back(Operation::DiscardWorkingFolder.into());
responses.push_back(DocumentMessage::AbortTransaction.into());
}
data.path = None;
data.points.clear();
Ready
}
(Dragging, Abort) => {
responses.push_back(Operation::DiscardWorkingFolder.into());
responses.push_back(DocumentMessage::AbortTransaction.into());
data.points.clear();
data.path = None;
Ready
}
@@ -120,17 +128,20 @@ impl Fsm for PenToolFsmState {
}
}
fn make_operation(data: &PenToolData, tool_data: &DocumentToolData, show_preview: bool) -> Message {
fn make_operation(data: &PenToolData, tool_data: &DocumentToolData, show_preview: bool) -> [Message; 2] {
let mut points: Vec<(f64, f64)> = data.points.iter().map(|p| (p.translation.x, p.translation.y)).collect();
if show_preview {
points.push((data.next_point.translation.x, data.next_point.translation.y))
}
Operation::AddPen {
path: vec![],
insert_index: -1,
transform: DAffine2::IDENTITY.to_cols_array(),
points,
style: style::PathStyle::new(Some(style::Stroke::new(tool_data.primary_color, 5.)), Some(style::Fill::none())),
}
.into()
[
Operation::DeleteLayer { path: data.path.clone().unwrap() }.into(),
Operation::AddPen {
path: data.path.clone().unwrap(),
insert_index: -1,
transform: DAffine2::IDENTITY.to_cols_array(),
points,
style: style::PathStyle::new(Some(style::Stroke::new(tool_data.primary_color, 5.)), Some(style::Fill::none())),
}
.into(),
]
}
+50 -103
View File
@@ -1,8 +1,11 @@
use crate::input::{mouse::ViewportPosition, InputPreprocessor};
use crate::input::keyboard::Key;
use crate::input::InputPreprocessor;
use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
use crate::{document::Document, message_prelude::*};
use crate::{document::DocumentMessageHandler, message_prelude::*};
use document_core::{layers::style, Operation};
use glam::{DAffine2, DVec2};
use glam::DAffine2;
use super::resize::*;
#[derive(Default)]
pub struct Rectangle {
@@ -11,16 +14,12 @@ pub struct Rectangle {
}
#[impl_message(Message, ToolMessage, Rectangle)]
#[derive(PartialEq, Clone, Debug)]
#[derive(PartialEq, Clone, Debug, Hash)]
pub enum RectangleMessage {
DragStart,
DragStop,
MouseMove,
Resize { center: Key, lock_ratio: Key },
Abort,
Center,
UnCenter,
LockAspectRatio,
UnlockAspectRatio,
}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Rectangle {
@@ -30,8 +29,8 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Rectangle {
fn actions(&self) -> ActionList {
use RectangleToolFsmState::*;
match self.fsm_state {
Ready => actions!(RectangleMessageDiscriminant; DragStart, Center, UnCenter, LockAspectRatio, UnlockAspectRatio),
Dragging => actions!(RectangleMessageDiscriminant; DragStop, Center, UnCenter, LockAspectRatio, UnlockAspectRatio, MouseMove, Abort),
Ready => actions!(RectangleMessageDiscriminant; DragStart),
Dragging => actions!(RectangleMessageDiscriminant; DragStop, Abort, Resize),
}
}
}
@@ -49,63 +48,68 @@ impl Default for RectangleToolFsmState {
}
#[derive(Clone, Debug, Default)]
struct RectangleToolData {
drag_start: ViewportPosition,
drag_current: ViewportPosition,
constrain_to_square: bool,
center_around_cursor: bool,
sides: u8,
data: Resize,
}
impl Fsm for RectangleToolFsmState {
type ToolData = RectangleToolData;
fn transition(self, event: ToolMessage, document: &Document, tool_data: &DocumentToolData, data: &mut Self::ToolData, input: &InputPreprocessor, responses: &mut VecDeque<Message>) -> Self {
let transform = document.document.root.transform;
fn transition(
self,
event: ToolMessage,
document: &DocumentMessageHandler,
tool_data: &DocumentToolData,
data: &mut Self::ToolData,
input: &InputPreprocessor,
responses: &mut VecDeque<Message>,
) -> Self {
let mut shape_data = &mut data.data;
use RectangleMessage::*;
use RectangleToolFsmState::*;
if let ToolMessage::Rectangle(event) = event {
match (self, event) {
(Ready, DragStart) => {
data.drag_start = input.mouse.position;
data.drag_current = input.mouse.position;
responses.push_back(Operation::MountWorkingFolder { path: vec![] }.into());
Dragging
}
(Dragging, MouseMove) => {
data.drag_current = input.mouse.position;
shape_data.drag_start = input.mouse.position;
responses.push_back(DocumentMessage::StartTransaction.into());
shape_data.path = Some(vec![generate_hash(&*responses, input, document.document.hash())]);
responses.push_back(DocumentMessage::DeselectAllLayers.into());
responses.push_back(Operation::ClearWorkingFolder.into());
responses.push_back(make_operation(data, tool_data, transform));
responses.push_back(
Operation::AddRect {
path: shape_data.path.clone().unwrap(),
insert_index: -1,
transform: DAffine2::ZERO.to_cols_array(),
style: style::PathStyle::new(None, Some(style::Fill::new(tool_data.primary_color))),
}
.into(),
);
Dragging
}
(Dragging, DragStop) => {
data.drag_current = input.mouse.position;
responses.push_back(Operation::ClearWorkingFolder.into());
// TODO - introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100)
if data.drag_start != data.drag_current {
responses.push_back(make_operation(data, tool_data, transform));
responses.push_back(DocumentMessage::DeselectAllLayers.into());
responses.push_back(Operation::CommitTransaction.into());
(state, Resize { center, lock_ratio }) => {
if let Some(message) = shape_data.calculate_transform(center, lock_ratio, input) {
responses.push_back(message);
}
state
}
(Dragging, DragStop) => {
// TODO: introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100)
match shape_data.drag_start == input.mouse.position {
true => responses.push_back(DocumentMessage::AbortTransaction.into()),
false => responses.push_back(DocumentMessage::CommitTransaction.into()),
}
shape_data.path = None;
Ready
}
// TODO - simplify with or_patterns when rust 1.53.0 is stable (https://github.com/rust-lang/rust/issues/54883)
(Dragging, Abort) => {
responses.push_back(Operation::DiscardWorkingFolder.into());
responses.push_back(DocumentMessage::AbortTransaction.into());
shape_data.path = None;
Ready
}
(Ready, LockAspectRatio) => update_state_no_op(&mut data.constrain_to_square, true, Ready),
(Ready, UnlockAspectRatio) => update_state_no_op(&mut data.constrain_to_square, false, Ready),
(Dragging, LockAspectRatio) => update_state(|data| &mut data.constrain_to_square, true, tool_data, data, responses, Dragging, transform),
(Dragging, UnlockAspectRatio) => update_state(|data| &mut data.constrain_to_square, false, tool_data, data, responses, Dragging, transform),
(Ready, Center) => update_state_no_op(&mut data.center_around_cursor, true, Ready),
(Ready, UnCenter) => update_state_no_op(&mut data.center_around_cursor, false, Ready),
(Dragging, Center) => update_state(|data| &mut data.center_around_cursor, true, tool_data, data, responses, Dragging, transform),
(Dragging, UnCenter) => update_state(|data| &mut data.center_around_cursor, false, tool_data, data, responses, Dragging, transform),
_ => self,
}
} else {
@@ -113,60 +117,3 @@ impl Fsm for RectangleToolFsmState {
}
}
}
fn update_state_no_op(state: &mut bool, value: bool, new_state: RectangleToolFsmState) -> RectangleToolFsmState {
*state = value;
new_state
}
fn update_state(
state: fn(&mut RectangleToolData) -> &mut bool,
value: bool,
tool_data: &DocumentToolData,
data: &mut RectangleToolData,
responses: &mut VecDeque<Message>,
new_state: RectangleToolFsmState,
transform: DAffine2,
) -> RectangleToolFsmState {
*(state(data)) = value;
responses.push_back(Operation::ClearWorkingFolder.into());
responses.push_back(make_operation(data, tool_data, transform));
new_state
}
fn make_operation(data: &RectangleToolData, tool_data: &DocumentToolData, transform: DAffine2) -> Message {
let x0 = data.drag_start.x as f64;
let y0 = data.drag_start.y as f64;
let x1 = data.drag_current.x as f64;
let y1 = data.drag_current.y as f64;
let (x0, y0, x1, y1) = if data.constrain_to_square {
let (x_dir, y_dir) = ((x1 - x0).signum(), (y1 - y0).signum());
let max_dist = f64::max((x1 - x0).abs(), (y1 - y0).abs());
if data.center_around_cursor {
(x0 - max_dist * x_dir, y0 - max_dist * y_dir, x0 + max_dist * x_dir, y0 + max_dist * y_dir)
} else {
(x0, y0, x0 + max_dist * x_dir, y0 + max_dist * y_dir)
}
} else {
let (x0, y0) = if data.center_around_cursor {
let delta_x = x1 - x0;
let delta_y = y1 - y0;
(x0 - delta_x, y0 - delta_y)
} else {
(x0, y0)
};
(x0, y0, x1, y1)
};
Operation::AddRect {
path: vec![],
insert_index: -1,
transform: (transform.inverse() * glam::DAffine2::from_scale_angle_translation(DVec2::new(x1 - x0, y1 - y0), 0., DVec2::new(x0, y0))).to_cols_array(),
style: style::PathStyle::new(None, Some(style::Fill::new(tool_data.primary_color))),
}
.into()
}
+34
View File
@@ -0,0 +1,34 @@
use crate::input::keyboard::Key;
use crate::input::{mouse::ViewportPosition, InputPreprocessor};
use crate::message_prelude::*;
use document_core::Operation;
use glam::{DAffine2, Vec2Swizzles};
#[derive(Clone, Debug, Default)]
pub struct Resize {
pub drag_start: ViewportPosition,
pub path: Option<Vec<LayerId>>,
}
impl Resize {
pub fn calculate_transform(&self, center: Key, lock_ratio: Key, ipp: &InputPreprocessor) -> Option<Message> {
let mut start = self.drag_start.as_f64();
let stop = ipp.mouse.position.as_f64();
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.;
}
self.path.clone().map(|path| {
Operation::SetLayerTransformInViewport {
path,
transform: DAffine2::from_scale_angle_translation(size, 0., start).to_cols_array(),
}
.into()
})
}
}
+88 -98
View File
@@ -3,6 +3,8 @@ use document_core::layers::style;
use document_core::layers::style::Fill;
use document_core::layers::style::Stroke;
use document_core::Operation;
use document_core::Quad;
use glam::{DAffine2, DVec2};
use serde::{Deserialize, Serialize};
@@ -10,7 +12,7 @@ use crate::input::{mouse::ViewportPosition, InputPreprocessor};
use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
use crate::{
consts::SELECTION_TOLERANCE,
document::{AlignAggregate, AlignAxis, Document, FlipAxis},
document::{AlignAggregate, AlignAxis, DocumentMessageHandler, FlipAxis},
message_prelude::*,
};
@@ -21,7 +23,7 @@ pub struct Select {
}
#[impl_message(Message, ToolMessage, Select)]
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, Hash)]
pub enum SelectMessage {
DragStart,
DragStop,
@@ -40,8 +42,9 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Select {
fn actions(&self) -> ActionList {
use SelectToolFsmState::*;
match self.fsm_state {
Ready => actions!(SelectMessageDiscriminant; DragStart),
Dragging => actions!(SelectMessageDiscriminant; DragStop, MouseMove, Abort),
Ready => actions!(SelectMessageDiscriminant; DragStart),
Dragging => actions!(SelectMessageDiscriminant; DragStop, MouseMove),
DrawingBox => actions!(SelectMessageDiscriminant; DragStop, MouseMove, Abort),
}
}
}
@@ -50,6 +53,7 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Select {
enum SelectToolFsmState {
Ready,
Dragging,
DrawingBox,
}
impl Default for SelectToolFsmState {
@@ -62,14 +66,38 @@ impl Default for SelectToolFsmState {
struct SelectToolData {
drag_start: ViewportPosition,
drag_current: ViewportPosition,
layers_dragging: Vec<(Vec<LayerId>, DVec2)>, // Paths and offsets
layers_dragging: Vec<Vec<LayerId>>, // Paths and offsets
box_id: Option<Vec<LayerId>>,
}
impl SelectToolData {
fn selection_quad(&self) -> Quad {
let bbox = self.selection_box();
Quad::from_box(bbox)
}
fn selection_box(&self) -> [DVec2; 2] {
if self.drag_current == self.drag_start {
let tolerance = DVec2::splat(SELECTION_TOLERANCE);
[self.drag_start.as_f64() - tolerance, self.drag_start.as_f64() + tolerance]
} else {
[self.drag_start.as_f64(), self.drag_current.as_f64()]
}
}
}
impl Fsm for SelectToolFsmState {
type ToolData = SelectToolData;
fn transition(self, event: ToolMessage, document: &Document, tool_data: &DocumentToolData, data: &mut Self::ToolData, input: &InputPreprocessor, responses: &mut VecDeque<Message>) -> Self {
let transform = document.document.root.transform;
fn transition(
self,
event: ToolMessage,
document: &DocumentMessageHandler,
_tool_data: &DocumentToolData,
data: &mut Self::ToolData,
input: &InputPreprocessor,
responses: &mut VecDeque<Message>,
) -> Self {
use SelectMessage::*;
use SelectToolFsmState::*;
if let ToolMessage::Select(event) = event {
@@ -77,94 +105,71 @@ impl Fsm for SelectToolFsmState {
(Ready, DragStart) => {
data.drag_start = input.mouse.position;
data.drag_current = input.mouse.position;
let (point_1, point_2) = {
let (x, y) = (data.drag_start.x as f64, data.drag_start.y as f64);
(
DVec2::new(x - SELECTION_TOLERANCE, y - SELECTION_TOLERANCE),
DVec2::new(x + SELECTION_TOLERANCE, y + SELECTION_TOLERANCE),
)
};
let quad = [
DVec2::new(point_1.x, point_1.y),
DVec2::new(point_2.x, point_1.y),
DVec2::new(point_2.x, point_2.y),
DVec2::new(point_1.x, point_2.y),
];
if let Some(intersection) = document.document.intersects_quad_root(quad).last() {
// TODO: Replace root transformations with functions of the transform api
let transformed_start = document.document.root.transform.inverse().transform_vector2(data.drag_start.as_dvec2());
if document.layer_data.get(intersection).map_or(false, |layer_data| layer_data.selected) {
data.layers_dragging = document
.layer_data
.iter()
.filter_map(|(path, layer_data)| {
layer_data
.selected
.then(|| (path.clone(), document.document.layer(path).unwrap().transform.translation - transformed_start))
})
.collect();
} else {
responses.push_back(DocumentMessage::SelectLayers(vec![intersection.clone()]).into());
data.layers_dragging = vec![(intersection.clone(), document.document.layer(intersection).unwrap().transform.translation - transformed_start)]
let mut selected: Vec<_> = document.selected_layers().cloned().collect();
let quad = data.selection_quad();
let intersection = document.document.intersects_quad_root(quad);
// If no layer is currently selected and the user clicks on a shape, select that.
if selected.is_empty() {
if let Some(layer) = intersection.last() {
selected.push(layer.clone());
responses.push_back(DocumentMessage::SelectLayers(selected.clone()).into());
}
} else {
responses.push_back(Operation::MountWorkingFolder { path: vec![] }.into());
data.layers_dragging = Vec::new();
}
Dragging
// If the user clicks on a layer that is in their current selection, go into the dragging mode.
// Otherwise enter the box select mode
if selected.iter().any(|path| intersection.contains(path)) {
data.layers_dragging = selected;
Dragging
} else {
responses.push_back(DocumentMessage::DeselectAllLayers.into());
data.box_id = Some(vec![generate_hash(&*responses, input, document.document.hash())]);
responses.push_back(
Operation::AddBoundingBox {
path: data.box_id.clone().unwrap(),
transform: DAffine2::ZERO.to_cols_array(),
style: style::PathStyle::new(Some(Stroke::new(Color::from_rgb8(0x00, 0xA8, 0xFF), 1.0)), Some(Fill::none())),
}
.into(),
);
DrawingBox
}
}
(Dragging, MouseMove) => {
data.drag_current = input.mouse.position;
if data.layers_dragging.is_empty() {
responses.push_back(Operation::ClearWorkingFolder.into());
responses.push_back(make_operation(data, tool_data, transform));
} else {
for (path, offset) in &data.layers_dragging {
responses.push_back(DocumentMessage::DragLayer(path.clone(), *offset).into());
}
for path in data.layers_dragging.iter() {
responses.push_back(
Operation::TransformLayerInViewport {
path: path.clone(),
transform: DAffine2::from_translation(input.mouse.position.as_f64() - data.drag_current.as_f64()).to_cols_array(),
}
.into(),
);
}
data.drag_current = input.mouse.position;
Dragging
}
(Dragging, DragStop) => {
(DrawingBox, MouseMove) => {
data.drag_current = input.mouse.position;
let start = data.drag_start.as_f64();
let size = data.drag_current.as_f64() - start;
if data.layers_dragging.is_empty() {
responses.push_back(Operation::ClearWorkingFolder.into());
responses.push_back(Operation::DiscardWorkingFolder.into());
if data.drag_start == data.drag_current {
responses.push_back(DocumentMessage::SelectLayers(vec![]).into());
} else {
let (point_1, point_2) = (
DVec2::new(data.drag_start.x as f64, data.drag_start.y as f64),
DVec2::new(data.drag_current.x as f64, data.drag_current.y as f64),
);
let quad = [
DVec2::new(point_1.x, point_1.y),
DVec2::new(point_2.x, point_1.y),
DVec2::new(point_2.x, point_2.y),
DVec2::new(point_1.x, point_2.y),
];
responses.push_back(DocumentMessage::SelectLayers(document.document.intersects_quad_root(quad)).into());
responses.push_back(
Operation::SetLayerTransformInViewport {
path: data.box_id.clone().unwrap(),
transform: DAffine2::from_scale_angle_translation(size, 0., start).to_cols_array(),
}
} else {
data.layers_dragging = Vec::new();
}
.into(),
);
DrawingBox
}
(Dragging, DragStop) => Ready,
(DrawingBox, Abort) => {
responses.push_back(Operation::DeleteLayer { path: data.box_id.take().unwrap() }.into());
Ready
}
(Dragging, Abort) => {
responses.push_back(Operation::DiscardWorkingFolder.into());
data.layers_dragging = Vec::new();
(DrawingBox, DragStop) => {
let quad = data.selection_quad();
responses.push_back(DocumentMessage::SelectLayers(document.document.intersects_quad_root(quad)).into());
responses.push_back(Operation::DeleteLayer { path: data.box_id.take().unwrap() }.into());
Ready
}
(_, Align(axis, aggregate)) => {
@@ -189,18 +194,3 @@ impl Fsm for SelectToolFsmState {
}
}
}
fn make_operation(data: &SelectToolData, _tool_data: &DocumentToolData, transform: DAffine2) -> Message {
let x0 = data.drag_start.x as f64;
let y0 = data.drag_start.y as f64;
let x1 = data.drag_current.x as f64;
let y1 = data.drag_current.y as f64;
Operation::AddRect {
path: vec![],
insert_index: -1,
transform: (transform.inverse() * glam::DAffine2::from_scale_angle_translation(DVec2::new(x1 - x0, y1 - y0), 0., DVec2::new(x0, y0))).to_cols_array(),
style: style::PathStyle::new(Some(Stroke::new(Color::from_rgb8(0x31, 0x94, 0xD6), 2.0)), Some(Fill::none())),
}
.into()
}
+50 -107
View File
@@ -1,8 +1,11 @@
use crate::input::{mouse::ViewportPosition, InputPreprocessor};
use crate::input::keyboard::Key;
use crate::input::InputPreprocessor;
use crate::tool::{DocumentToolData, Fsm, ShapeType, ToolActionHandlerData, ToolOptions, ToolType};
use crate::{document::Document, message_prelude::*};
use crate::{document::DocumentMessageHandler, message_prelude::*};
use document_core::{layers::style, Operation};
use glam::{DAffine2, DVec2};
use glam::DAffine2;
use super::resize::*;
#[derive(Default)]
pub struct Shape {
@@ -11,17 +14,12 @@ pub struct Shape {
}
#[impl_message(Message, ToolMessage, Shape)]
#[derive(PartialEq, Clone, Debug)]
#[derive(PartialEq, Clone, Debug, Hash)]
pub enum ShapeMessage {
Undo,
DragStart,
DragStop,
MouseMove,
Resize { center: Key, lock_ratio: Key },
Abort,
Center,
UnCenter,
LockAspectRatio,
UnlockAspectRatio,
}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Shape {
@@ -31,8 +29,8 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Shape {
fn actions(&self) -> ActionList {
use ShapeToolFsmState::*;
match self.fsm_state {
Ready => actions!(ShapeMessageDiscriminant; Undo, DragStart, Center, UnCenter, LockAspectRatio, UnlockAspectRatio),
Dragging => actions!(ShapeMessageDiscriminant; DragStop, Center, UnCenter, LockAspectRatio, UnlockAspectRatio, MouseMove, Abort),
Ready => actions!(ShapeMessageDiscriminant; DragStart),
Dragging => actions!(ShapeMessageDiscriminant; DragStop, Abort, Resize),
}
}
}
@@ -50,26 +48,32 @@ impl Default for ShapeToolFsmState {
}
#[derive(Clone, Debug, Default)]
struct ShapeToolData {
drag_start: ViewportPosition,
drag_current: ViewportPosition,
constrain_to_square: bool,
center_around_cursor: bool,
sides: u8,
data: Resize,
}
impl Fsm for ShapeToolFsmState {
type ToolData = ShapeToolData;
fn transition(self, event: ToolMessage, document: &Document, tool_data: &DocumentToolData, data: &mut Self::ToolData, input: &InputPreprocessor, responses: &mut VecDeque<Message>) -> Self {
let transform = document.document.root.transform;
fn transition(
self,
event: ToolMessage,
document: &DocumentMessageHandler,
tool_data: &DocumentToolData,
data: &mut Self::ToolData,
input: &InputPreprocessor,
responses: &mut VecDeque<Message>,
) -> Self {
let mut shape_data = &mut data.data;
use ShapeMessage::*;
use ShapeToolFsmState::*;
if let ToolMessage::Shape(event) = event {
match (self, event) {
(Ready, DragStart) => {
data.drag_start = input.mouse.position;
data.drag_current = input.mouse.position;
shape_data.drag_start = input.mouse.position;
responses.push_back(DocumentMessage::StartTransaction.into());
shape_data.path = Some(vec![generate_hash(&*responses, input, document.document.hash())]);
responses.push_back(DocumentMessage::DeselectAllLayers.into());
data.sides = match tool_data.tool_options.get(&ToolType::Shape) {
Some(&ToolOptions::Shape {
shape_type: ShapeType::Polygon { vertices },
@@ -77,43 +81,42 @@ impl Fsm for ShapeToolFsmState {
_ => 6,
};
responses.push_back(Operation::MountWorkingFolder { path: vec![] }.into());
Dragging
}
(Dragging, MouseMove) => {
data.drag_current = input.mouse.position;
responses.push_back(Operation::ClearWorkingFolder.into());
responses.push_back(make_operation(data, tool_data, transform));
responses.push_back(
Operation::AddShape {
path: shape_data.path.clone().unwrap(),
insert_index: -1,
transform: DAffine2::ZERO.to_cols_array(),
sides: data.sides,
style: style::PathStyle::new(None, Some(style::Fill::new(tool_data.primary_color))),
}
.into(),
);
Dragging
}
(Dragging, DragStop) => {
data.drag_current = input.mouse.position;
responses.push_back(Operation::ClearWorkingFolder.into());
// TODO - introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100)
if data.drag_start != data.drag_current {
responses.push_back(make_operation(data, tool_data, transform));
responses.push_back(DocumentMessage::DeselectAllLayers.into());
responses.push_back(Operation::CommitTransaction.into());
(state, Resize { center, lock_ratio }) => {
if let Some(message) = shape_data.calculate_transform(center, lock_ratio, input) {
responses.push_back(message);
}
state
}
(Dragging, DragStop) => {
// TODO: introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100)
match shape_data.drag_start == input.mouse.position {
true => responses.push_back(DocumentMessage::AbortTransaction.into()),
false => responses.push_back(DocumentMessage::CommitTransaction.into()),
}
shape_data.path = None;
Ready
}
(Dragging, Abort) => {
responses.push_back(Operation::DiscardWorkingFolder.into());
responses.push_back(DocumentMessage::AbortTransaction.into());
shape_data.path = None;
Ready
}
(Ready, LockAspectRatio) => update_state_no_op(&mut data.constrain_to_square, true, Ready),
(Ready, UnlockAspectRatio) => update_state_no_op(&mut data.constrain_to_square, false, Ready),
(Dragging, LockAspectRatio) => update_state(|data| &mut data.constrain_to_square, true, tool_data, data, responses, Dragging, transform),
(Dragging, UnlockAspectRatio) => update_state(|data| &mut data.constrain_to_square, false, tool_data, data, responses, Dragging, transform),
(Ready, Center) => update_state_no_op(&mut data.center_around_cursor, true, Ready),
(Ready, UnCenter) => update_state_no_op(&mut data.center_around_cursor, false, Ready),
(Dragging, Center) => update_state(|data| &mut data.center_around_cursor, true, tool_data, data, responses, Dragging, transform),
(Dragging, UnCenter) => update_state(|data| &mut data.center_around_cursor, false, tool_data, data, responses, Dragging, transform),
_ => self,
}
} else {
@@ -121,63 +124,3 @@ impl Fsm for ShapeToolFsmState {
}
}
}
fn update_state_no_op(state: &mut bool, value: bool, new_state: ShapeToolFsmState) -> ShapeToolFsmState {
*state = value;
new_state
}
fn update_state(
state: fn(&mut ShapeToolData) -> &mut bool,
value: bool,
tool_data: &DocumentToolData,
data: &mut ShapeToolData,
responses: &mut VecDeque<Message>,
new_state: ShapeToolFsmState,
transform: DAffine2,
) -> ShapeToolFsmState {
*(state(data)) = value;
responses.push_back(Operation::ClearWorkingFolder.into());
responses.push_back(make_operation(data, tool_data, transform));
new_state
}
fn make_operation(data: &ShapeToolData, tool_data: &DocumentToolData, transform: DAffine2) -> Message {
let x0 = data.drag_start.x as f64;
let y0 = data.drag_start.y as f64;
let x1 = data.drag_current.x as f64;
let y1 = data.drag_current.y as f64;
// TODO: Use regular polygon's aspect ration for constraining rather than a square.
let (x0, y0, x1, y1, equal_sides) = if data.constrain_to_square {
let (x_dir, y_dir) = ((x1 - x0).signum(), (y1 - y0).signum());
let max_dist = f64::max((x1 - x0).abs(), (y1 - y0).abs());
if data.center_around_cursor {
(x0 - max_dist * x_dir, y0 - max_dist * y_dir, x0 + max_dist * x_dir, y0 + max_dist * y_dir, true)
} else {
(x0, y0, x0 + max_dist * x_dir, y0 + max_dist * y_dir, true)
}
} else {
let (x0, y0) = if data.center_around_cursor {
let delta_x = x1 - x0;
let delta_y = y1 - y0;
(x0 - delta_x, y0 - delta_y)
} else {
(x0, y0)
};
(x0, y0, x1, y1, false)
};
Operation::AddShape {
path: vec![],
insert_index: -1,
transform: (transform.inverse() * glam::DAffine2::from_scale_angle_translation(DVec2::new(x1 - x0, y1 - y0), 0., DVec2::new(x0, y0))).to_cols_array(),
equal_sides,
sides: data.sides,
style: style::PathStyle::new(None, Some(style::Fill::new(tool_data.primary_color))),
}
.into()
}