diff --git a/Cargo.lock b/Cargo.lock index 988d30bc25..b8f294f429 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -97,6 +97,7 @@ dependencies = [ "glam", "graphite-graphene", "graphite-proc-macros", + "kurbo", "log", "rand_chacha", "serde", diff --git a/Cargo.toml b/Cargo.toml index e3769abc6a..f6bfac06ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,4 +8,10 @@ members = [ ] [profile.release.package.graphite-wasm] -opt-level = "s" +opt-level = 3 + +[profile.dev.package.graphite-wasm] +opt-level = 3 + +[profile.dev] +opt-level = 3 diff --git a/editor/Cargo.toml b/editor/Cargo.toml index 318c2aabe5..e855a7a5c6 100644 --- a/editor/Cargo.toml +++ b/editor/Cargo.toml @@ -17,6 +17,9 @@ graphite-proc-macros = { path = "../proc-macros" } glam = { version="0.17", features = ["serde"] } rand_chacha = "0.3.1" spin = "0.9.2" +kurbo = { git = "https://github.com/GraphiteEditor/kurbo.git", features = [ + "serde", +] } [dependencies.graphene] path = "../graphene" diff --git a/editor/src/communication/dispatcher.rs b/editor/src/communication/dispatcher.rs index f00c0a4cdb..4a2cd9b438 100644 --- a/editor/src/communication/dispatcher.rs +++ b/editor/src/communication/dispatcher.rs @@ -1,4 +1,4 @@ -use crate::{frontend::FrontendMessageHandler, message_prelude::*, Callback, EditorError}; +use crate::{message_prelude::*, EditorError}; pub use crate::document::DocumentsMessageHandler; pub use crate::input::{InputMapper, InputPreprocessor}; @@ -8,49 +8,47 @@ use crate::global::GlobalMessageHandler; use std::collections::VecDeque; pub struct Dispatcher { - frontend_message_handler: FrontendMessageHandler, input_preprocessor: InputPreprocessor, input_mapper: InputMapper, global_message_handler: GlobalMessageHandler, tool_message_handler: ToolMessageHandler, documents_message_handler: DocumentsMessageHandler, messages: VecDeque, + pub responses: Vec, } +const GROUP_MESSAGES: &[MessageDiscriminant] = &[ + MessageDiscriminant::Documents(DocumentsMessageDiscriminant::Document(DocumentMessageDiscriminant::RenderDocument)), + MessageDiscriminant::Documents(DocumentsMessageDiscriminant::Document(DocumentMessageDiscriminant::FolderChanged)), + MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateLayer), + MessageDiscriminant::Frontend(FrontendMessageDiscriminant::ExpandFolder), + MessageDiscriminant::Tool(ToolMessageDiscriminant::SelectedLayersChanged), +]; + impl Dispatcher { pub fn handle_message>(&mut self, message: T) -> Result<(), EditorError> { - let message = message.into(); + self.messages.push_back(message.into()); + use Message::*; - if !(matches!( - message, - Message::InputPreprocessor(_) - | Message::InputMapper(_) - | Message::Documents(DocumentsMessage::Document(DocumentMessage::RenderDocument)) - | Message::Frontend(FrontendMessage::UpdateCanvas { .. }) - | Message::Frontend(FrontendMessage::UpdateScrollbars { .. }) - | Message::Frontend(FrontendMessage::SetCanvasZoom { .. }) - | Message::Frontend(FrontendMessage::SetCanvasRotation { .. }) - ) || MessageDiscriminant::from(&message).local_name().ends_with("MouseMove")) - { - log::trace!("Message: {:?}", message); - //log::trace!("Hints:{:?}", self.input_mapper.hints(self.collect_actions())); - } - match message { - NoOp => (), - 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.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) => { - let actions = self.collect_actions(); - self.input_mapper.process_action(message, (&self.input_preprocessor, actions), &mut self.messages) + while let Some(message) = self.messages.pop_front() { + if GROUP_MESSAGES.contains(&message.to_discriminant()) && self.messages.contains(&message) { + continue; + } + log_message(&message); + match message { + NoOp => (), + 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.documents_message_handler.active_document(), &self.input_preprocessor), &mut self.messages), + Frontend(message) => self.responses.push(message), + InputPreprocessor(message) => self.input_preprocessor.process_action(message, (), &mut self.messages), + InputMapper(message) => { + let actions = self.collect_actions(); + self.input_mapper.process_action(message, (&self.input_preprocessor, actions), &mut self.messages) + } } - } - if let Some(message) = self.messages.pop_front() { - self.handle_message(message)?; } Ok(()) } @@ -58,7 +56,6 @@ impl Dispatcher { pub fn collect_actions(&self) -> ActionList { //TODO: reduce the number of heap allocations let mut list = Vec::new(); - list.extend(self.frontend_message_handler.actions()); list.extend(self.input_preprocessor.actions()); list.extend(self.input_mapper.actions()); list.extend(self.global_message_handler.actions()); @@ -67,24 +64,36 @@ impl Dispatcher { list } - pub fn new(callback: Callback) -> Dispatcher { + pub fn new() -> Dispatcher { Dispatcher { - frontend_message_handler: FrontendMessageHandler::new(callback), input_preprocessor: InputPreprocessor::default(), global_message_handler: GlobalMessageHandler::new(), input_mapper: InputMapper::default(), documents_message_handler: DocumentsMessageHandler::default(), tool_message_handler: ToolMessageHandler::default(), messages: VecDeque::new(), + responses: vec![], } } } +fn log_message(message: &Message) { + use Message::*; + if log::max_level() == log::LevelFilter::Trace + && !(matches!( + message, + InputPreprocessor(_) | Frontend(FrontendMessage::SetCanvasZoom { .. }) | Frontend(FrontendMessage::SetCanvasRotation { .. }) + ) || MessageDiscriminant::from(message).local_name().ends_with("MouseMove")) + { + log::trace!("Message: {:?}", message); + //log::trace!("Hints:{:?}", self.input_mapper.hints(self.collect_actions())); + } +} + #[cfg(test)] mod test { use crate::{document::DocumentMessageHandler, message_prelude::*, misc::test_utils::EditorTestUtils, Editor}; use graphene::{color::Color, Operation}; - use log::info; fn init_logger() { let _ = env_logger::builder().is_test(true).try_init(); @@ -95,9 +104,7 @@ mod test { /// 2. A blue shape /// 3. A green ellipse fn create_editor_with_three_layers() -> Editor { - let mut editor = Editor::new(Box::new(|e| { - info!("Got frontend message: {:?}", e); - })); + let mut editor = Editor::new(); editor.select_primary_color(Color::RED); editor.draw_rect(100., 200., 300., 400.); diff --git a/editor/src/communication/message.rs b/editor/src/communication/message.rs index 0aad1a006a..6c7d396a19 100644 --- a/editor/src/communication/message.rs +++ b/editor/src/communication/message.rs @@ -1,5 +1,6 @@ use crate::message_prelude::*; use graphite_proc_macros::*; +use serde::{Deserialize, Serialize}; use std::{ collections::hash_map::DefaultHasher, hash::{Hash, Hasher}, @@ -16,7 +17,7 @@ where } #[impl_message] -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub enum Message { NoOp, #[child] diff --git a/editor/src/consts.rs b/editor/src/consts.rs index c9e42a81d7..cd2122df63 100644 --- a/editor/src/consts.rs +++ b/editor/src/consts.rs @@ -1,3 +1,5 @@ +use graphene::color::Color; + // VIEWPORT pub const VIEWPORT_ZOOM_WHEEL_RATE: f64 = 1. / 600.; pub const VIEWPORT_ZOOM_MOUSE_RATE: f64 = 1. / 400.; @@ -13,12 +15,15 @@ pub const VIEWPORT_SCROLL_RATE: f64 = 0.6; pub const VIEWPORT_ROTATE_SNAP_INTERVAL: f64 = 15.; +// SELECT TOOL +pub const SELECTION_TOLERANCE: f64 = 1.; + +// PATH TOOL +pub const VECTOR_MANIPULATOR_ANCHOR_MARKER_SIZE: f64 = 5.; + // LINE TOOL pub const LINE_ROTATE_SNAP_ANGLE: f64 = 15.; -// SELECT TOOL -pub const SELECTION_TOLERANCE: f64 = 1.0; - // SCROLLBARS pub const SCROLLBAR_SPACING: f64 = 0.1; pub const ASYMPTOTIC_EFFECT: f64 = 0.5; @@ -27,3 +32,6 @@ pub const SCALE_EFFECT: f64 = 0.5; pub const DEFAULT_DOCUMENT_NAME: &str = "Untitled Document"; pub const FILE_SAVE_SUFFIX: &str = ".graphite"; pub const FILE_EXPORT_SUFFIX: &str = ".svg"; + +// COLORS +pub const COLOR_ACCENT: Color = Color::from_unsafe(0x00 as f32 / 255., 0xA8 as f32 / 255., 0xFF as f32 / 255.); diff --git a/editor/src/document/document_file.rs b/editor/src/document/document_file.rs index fd7dd68171..7f51c8ff4a 100644 --- a/editor/src/document/document_file.rs +++ b/editor/src/document/document_file.rs @@ -5,7 +5,8 @@ use crate::{ EditorError, }; use glam::{DAffine2, DVec2}; -use graphene::{document::Document as InternalDocument, DocumentError, LayerId}; +use graphene::{document::Document as InternalDocument, layers::LayerDataType, DocumentError, LayerId}; +use kurbo::PathSeg; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -41,6 +42,20 @@ pub enum AlignAggregate { Average, } +#[derive(PartialEq, Clone, Debug)] +pub enum VectorManipulatorSegment { + Line(DVec2, DVec2), + Quad(DVec2, DVec2, DVec2), + Cubic(DVec2, DVec2, DVec2, DVec2), +} + +#[derive(PartialEq, Clone, Debug)] +pub struct VectorManipulatorShape { + pub path: kurbo::BezPath, + pub segments: Vec, + pub transform: DAffine2, +} + #[derive(Clone, Debug)] pub struct DocumentMessageHandler { pub document: InternalDocument, @@ -65,7 +80,7 @@ impl Default for DocumentMessageHandler { } #[impl_message(Message, DocumentsMessage, Document)] -#[derive(PartialEq, Clone, Debug)] +#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] pub enum DocumentMessage { #[child] Movement(MovementMessage), @@ -112,6 +127,7 @@ impl From for DocumentMessage { Self::DispatchOperation(Box::new(operation)) } } + impl From for Message { fn from(operation: DocumentOperation) -> Message { DocumentMessage::DispatchOperation(Box::new(operation)).into() @@ -119,11 +135,6 @@ impl From for Message { } impl DocumentMessageHandler { - fn filter_document_responses(&self, document_responses: &mut Vec) -> bool { - let len = document_responses.len(); - document_responses.retain(|response| !matches!(response, DocumentResponse::DocumentChanged)); - document_responses.len() != len - } pub fn handle_folder_changed(&mut self, path: Vec) -> Option { let _ = self.document.render_root(); self.layer_data(&path).expanded.then(|| { @@ -131,9 +142,11 @@ impl DocumentMessageHandler { 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 { if self.document.layer(path).ok()?.overlay { return None; @@ -143,13 +156,51 @@ impl DocumentMessageHandler { // TODO: Add deduplication (!path.is_empty()).then(|| FrontendMessage::UpdateLayer { path: path.to_vec(), data }.into()) } + pub fn selected_layers_bounding_box(&self) -> Option<[DVec2; 2]> { let paths = self.selected_layers().map(|vec| &vec[..]); self.document.combined_viewport_bounding_box(paths) } + + // TODO: Consider moving this to some kind of overlay manager in the future + pub fn selected_layers_vector_points(&self) -> Vec { + let shapes = self.selected_layers().filter_map(|path_to_shape| { + let viewport_transform = self.document.generate_transform_relative_to_viewport(path_to_shape.as_slice()).ok()?; + + let shape = match &self.document.layer(path_to_shape.as_slice()).ok()?.data { + LayerDataType::Shape(shape) => Some(shape), + LayerDataType::Folder(_) => None, + }?; + let path = shape.path.clone(); + + let segments = path + .segments() + .map(|segment| -> VectorManipulatorSegment { + let place = |point: kurbo::Point| -> DVec2 { viewport_transform.transform_point2(DVec2::from((point.x, point.y))) }; + + match segment { + PathSeg::Line(line) => VectorManipulatorSegment::Line(place(line.p0), place(line.p1)), + PathSeg::Quad(quad) => VectorManipulatorSegment::Quad(place(quad.p0), place(quad.p1), place(quad.p2)), + PathSeg::Cubic(cubic) => VectorManipulatorSegment::Cubic(place(cubic.p0), place(cubic.p1), place(cubic.p2), place(cubic.p3)), + } + }) + .collect::>(); + + Some(VectorManipulatorShape { + path, + segments, + transform: viewport_transform, + }) + }); + + // TODO: Consider refactoring this in a way that avoids needing to collect() so we can skip the heap allocations + shapes.collect::>() + } + 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)) } @@ -200,6 +251,7 @@ impl DocumentMessageHandler { pub fn non_selected_layers_sorted(&self) -> Vec> { self.layers_sorted(Some(false)) } + pub fn with_name(name: String) -> Self { Self { document: InternalDocument::default(), @@ -210,6 +262,7 @@ impl DocumentMessageHandler { movement_handler: MovementMessageHandler::default(), } } + pub fn with_name_and_content(name: String, serialized_content: String) -> Result { let mut document = Self::with_name(name); let internal_document = InternalDocument::with_content(&serialized_content); @@ -271,7 +324,6 @@ impl DocumentMessageHandler { } pub fn layer_panel_entry(&mut self, path: Vec) -> Result { - 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); @@ -372,22 +424,25 @@ impl MessageHandler for DocumentMessageHand } } ToggleLayerVisibility(path) => { - responses.push_back(DocumentOperation::ToggleVisibility { path }.into()); + responses.push_back(DocumentOperation::ToggleLayerVisibility { path }.into()); } ToggleLayerExpansion(path) => { self.layer_data(&path).expanded ^= true; - responses.extend(self.handle_folder_changed(path)); + responses.push_back(FolderChanged(path).into()); + } + SelectionChanged => { + // TODO: Hoist this duplicated code into wider system + responses.push_back(ToolMessage::SelectedLayersChanged.into()); } - SelectionChanged => responses.push_back(SelectMessage::UpdateSelectionBoundingBox.into()), DeleteSelectedLayers => { self.backup(); - responses.push_front(SelectMessage::UpdateSelectionBoundingBox.into()); + responses.push_front(ToolMessage::SelectedLayersChanged.into()); for path in self.selected_layers().cloned() { responses.push_front(DocumentOperation::DeleteLayer { path }.into()); } } ClearOverlays => { - responses.push_front(SelectMessage::UpdateSelectionBoundingBox.into()); + responses.push_back(ToolMessage::SelectedLayersChanged.into()); for path in self.layer_data.keys().filter(|path| self.document.layer(path).unwrap().overlay).cloned() { responses.push_front(DocumentOperation::DeleteLayer { path }.into()); } @@ -407,8 +462,8 @@ impl MessageHandler for DocumentMessageHand 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())); - responses.push_front(SelectMessage::UpdateSelectionBoundingBox.into()); + responses.push_back(FolderChanged(Vec::new()).into()); + responses.push_back(ToolMessage::SelectedLayersChanged.into()); } SelectAllLayers => { let all_layer_paths = self @@ -427,46 +482,41 @@ impl MessageHandler for DocumentMessageHand Undo => { responses.push_back(SelectMessage::Abort.into()); responses.push_back(DocumentHistoryBackward.into()); - responses.push_back(SelectMessage::UpdateSelectionBoundingBox.into()); + responses.push_back(ToolMessage::SelectedLayersChanged.into()); responses.push_back(RenderDocument.into()); responses.push_back(FolderChanged(vec![]).into()); } Redo => { responses.push_back(SelectMessage::Abort.into()); responses.push_back(DocumentHistoryForward.into()); - responses.push_back(SelectMessage::UpdateSelectionBoundingBox.into()); + responses.push_back(ToolMessage::SelectedLayersChanged.into()); responses.push_back(RenderDocument.into()); responses.push_back(FolderChanged(vec![]).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); + Ok(Some(document_responses)) => { responses.extend( document_responses .into_iter() .map(|response| match response { - DocumentResponse::FolderChanged { path } => self.handle_folder_changed(path), + DocumentResponse::FolderChanged { path } => Some(FolderChanged(path).into()), DocumentResponse::DeletedLayer { path } => { self.layer_data.remove(&path); - - Some(SelectMessage::UpdateSelectionBoundingBox.into()) + Some(ToolMessage::SelectedLayersChanged.into()) } - DocumentResponse::LayerChanged { path } => Some( + DocumentResponse::LayerChanged { path } => (!self.document.layer(&path).unwrap().overlay).then(|| { FrontendMessage::UpdateLayer { path: path.clone(), data: self.layer_panel_entry(path).unwrap(), } - .into(), - ), + .into() + }), DocumentResponse::CreatedLayer { path } => (!self.document.layer(&path).unwrap().overlay).then(|| SetSelectedLayers(vec![path]).into()), - DocumentResponse::DocumentChanged => unreachable!(), + DocumentResponse::DocumentChanged => Some(RenderDocument.into()), }) .flatten(), ); - if canvas_dirty { - responses.push_back(RenderDocument.into()); - } } Err(e) => log::error!("DocumentError: {:?}", e), Ok(_) => (), @@ -478,6 +528,7 @@ impl MessageHandler for DocumentMessageHand } .into(), ); + let scale = 0.5 + ASYMPTOTIC_EFFECT + self.layerdata(&[]).scale * SCALE_EFFECT; let viewport_size = ipp.viewport_bounds.size(); let viewport_mid = ipp.viewport_bounds.center(); @@ -507,7 +558,7 @@ impl MessageHandler for DocumentMessageHand }; responses.push_back(operation.into()); } - responses.push_back(SelectMessage::UpdateSelectionBoundingBox.into()); + responses.push_back(ToolMessage::SelectedLayersChanged.into()); } MoveSelectedLayersTo { path, insert_index } => { responses.push_back(DocumentsMessage::CopySelectedLayers.into()); @@ -564,7 +615,7 @@ impl MessageHandler for DocumentMessageHand .into(), ); } - responses.push_back(SelectMessage::UpdateSelectionBoundingBox.into()); + responses.push_back(ToolMessage::SelectedLayersChanged.into()); } } AlignSelectedLayers(axis, aggregate) => { @@ -598,12 +649,13 @@ impl MessageHandler for DocumentMessageHand .into(), ); } - responses.push_back(SelectMessage::UpdateSelectionBoundingBox.into()); + responses.push_back(ToolMessage::SelectedLayersChanged.into()); } } RenameLayer(path, name) => responses.push_back(DocumentOperation::RenameLayer { path, name }.into()), } } + fn actions(&self) -> ActionList { let mut common = actions!(DocumentMessageDiscriminant; Undo, diff --git a/editor/src/document/document_message_handler.rs b/editor/src/document/document_message_handler.rs index 51eb0725b1..9f66c85885 100644 --- a/editor/src/document/document_message_handler.rs +++ b/editor/src/document/document_message_handler.rs @@ -4,13 +4,14 @@ use graphene::layers::Layer; use graphene::{LayerId, Operation as DocumentOperation}; use log::warn; +use serde::{Deserialize, Serialize}; use std::collections::VecDeque; use super::DocumentMessageHandler; use crate::consts::DEFAULT_DOCUMENT_NAME; #[impl_message(Message, Documents)] -#[derive(PartialEq, Clone, Debug)] +#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] pub enum DocumentsMessage { CopySelectedLayers, PasteLayers { diff --git a/editor/src/document/mod.rs b/editor/src/document/mod.rs index 2799d4da76..aa24342026 100644 --- a/editor/src/document/mod.rs +++ b/editor/src/document/mod.rs @@ -7,7 +7,7 @@ mod movement_handler; pub use document_file::LayerData; #[doc(inline)] -pub use document_file::{AlignAggregate, AlignAxis, DocumentMessage, DocumentMessageDiscriminant, DocumentMessageHandler, FlipAxis}; +pub use document_file::{AlignAggregate, AlignAxis, DocumentMessage, DocumentMessageDiscriminant, DocumentMessageHandler, FlipAxis, VectorManipulatorSegment, VectorManipulatorShape}; #[doc(inline)] pub use document_message_handler::{DocumentsMessage, DocumentsMessageDiscriminant, DocumentsMessageHandler}; #[doc(inline)] diff --git a/editor/src/document/movement_handler.rs b/editor/src/document/movement_handler.rs index 5f892679eb..69bf414256 100644 --- a/editor/src/document/movement_handler.rs +++ b/editor/src/document/movement_handler.rs @@ -11,10 +11,11 @@ use glam::DVec2; use graphene::document::Document; use graphene::Operation as DocumentOperation; +use serde::{Deserialize, Serialize}; use std::collections::VecDeque; #[impl_message(Message, DocumentMessage, Movement)] -#[derive(PartialEq, Clone, Debug)] +#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] pub enum MovementMessage { MouseMove, TranslateCanvasBegin, @@ -91,6 +92,7 @@ impl MessageHandler { layerdata.scale = new.clamp(VIEWPORT_ZOOM_SCALE_MIN, VIEWPORT_ZOOM_SCALE_MAX); responses.push_back(FrontendMessage::SetCanvasZoom { new_zoom: layerdata.scale }.into()); + responses.push_back(ToolMessage::SelectedLayersChanged.into()); self.create_document_transform_from_layerdata(layerdata, &ipp.viewport_bounds, responses); } IncreaseCanvasZoom => { layerdata.scale = *VIEWPORT_ZOOM_LEVELS.iter().find(|scale| **scale > layerdata.scale).unwrap_or(&layerdata.scale); responses.push_back(FrontendMessage::SetCanvasZoom { new_zoom: layerdata.scale }.into()); + responses.push_back(ToolMessage::SelectedLayersChanged.into()); self.create_document_transform_from_layerdata(layerdata, &ipp.viewport_bounds, responses); } DecreaseCanvasZoom => { layerdata.scale = *VIEWPORT_ZOOM_LEVELS.iter().rev().find(|scale| **scale < layerdata.scale).unwrap_or(&layerdata.scale); responses.push_back(FrontendMessage::SetCanvasZoom { new_zoom: layerdata.scale }.into()); + responses.push_back(ToolMessage::SelectedLayersChanged.into()); self.create_document_transform_from_layerdata(layerdata, &ipp.viewport_bounds, responses); } WheelCanvasZoom => { @@ -158,6 +164,7 @@ impl MessageHandler { @@ -167,13 +174,14 @@ impl MessageHandler { layerdata.rotation = new; self.create_document_transform_from_layerdata(layerdata, &ipp.viewport_bounds, responses); responses.push_back(FrontendMessage::SetCanvasRotation { new_radians: new }.into()); - responses.push_back(SelectMessage::UpdateSelectionBoundingBox.into()); + responses.push_back(ToolMessage::SelectedLayersChanged.into()); } ZoomCanvasToFitAll => { if let Some([pos1, pos2]) = document.visible_layers_bounding_box() { @@ -190,6 +198,7 @@ impl MessageHandler { let transformed_delta = document.root.transform.inverse().transform_vector2(delta * ipp.viewport_bounds.size()); layerdata.translation += transformed_delta; + responses.push_back(ToolMessage::SelectedLayersChanged.into()); self.create_document_transform_from_layerdata(layerdata, &ipp.viewport_bounds, responses); } } diff --git a/editor/src/frontend/frontend_message_handler.rs b/editor/src/frontend/frontend_message_handler.rs index d17c208fc9..fddce953bd 100644 --- a/editor/src/frontend/frontend_message_handler.rs +++ b/editor/src/frontend/frontend_message_handler.rs @@ -4,8 +4,6 @@ use crate::tool::tool_options::ToolOptions; use crate::Color; use serde::{Deserialize, Serialize}; -pub type Callback = Box; - #[impl_message(Message, Frontend)] #[derive(PartialEq, Clone, Deserialize, Serialize, Debug)] pub enum FrontendMessage { @@ -29,34 +27,3 @@ pub enum FrontendMessage { SetCanvasZoom { new_zoom: f64 }, SetCanvasRotation { new_radians: f64 }, } - -pub struct FrontendMessageHandler { - callback: crate::Callback, -} - -impl FrontendMessageHandler { - pub fn new(callback: Callback) -> Self { - Self { callback } - } -} - -impl MessageHandler for FrontendMessageHandler { - fn process_action(&mut self, message: FrontendMessage, _data: (), _responses: &mut VecDeque) { - (self.callback)(message) - } - advertise_actions!( - FrontendMessageDiscriminant; - - DisplayError, - CollapseFolder, - ExpandFolder, - SetActiveTool, - UpdateCanvas, - UpdateScrollbars, - EnableTextInput, - DisableTextInput, - SetCanvasZoom, - SetCanvasRotation, - OpenDocumentBrowse, - ); -} diff --git a/editor/src/frontend/mod.rs b/editor/src/frontend/mod.rs index be96641178..3f2d6b3cd0 100644 --- a/editor/src/frontend/mod.rs +++ b/editor/src/frontend/mod.rs @@ -1,4 +1,4 @@ pub mod frontend_message_handler; pub mod layer_panel; -pub use frontend_message_handler::{Callback, FrontendMessage, FrontendMessageDiscriminant, FrontendMessageHandler}; +pub use frontend_message_handler::{FrontendMessage, FrontendMessageDiscriminant}; diff --git a/editor/src/global/global_message_handler.rs b/editor/src/global/global_message_handler.rs index 45fafb08a8..f447a43e94 100644 --- a/editor/src/global/global_message_handler.rs +++ b/editor/src/global/global_message_handler.rs @@ -1,8 +1,9 @@ use crate::message_prelude::*; +use serde::{Deserialize, Serialize}; use std::collections::VecDeque; #[impl_message(Message, Global)] -#[derive(PartialEq, Clone, Debug, Hash)] +#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)] pub enum GlobalMessage { LogInfo, LogDebug, diff --git a/editor/src/input/input_mapper.rs b/editor/src/input/input_mapper.rs index 91189af68d..dcc174b035 100644 --- a/editor/src/input/input_mapper.rs +++ b/editor/src/input/input_mapper.rs @@ -7,13 +7,14 @@ use super::{ use crate::message_prelude::*; use crate::tool::ToolType; +use serde::{Deserialize, Serialize}; use std::fmt::Write; const NUDGE_AMOUNT: f64 = 1.; const SHIFT_NUDGE_AMOUNT: f64 = 10.; #[impl_message(Message, InputMapper)] -#[derive(PartialEq, Clone, Debug, Hash)] +#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)] pub enum InputMapperMessage { PointerMove, MouseScroll, @@ -172,14 +173,16 @@ impl Default for Mapping { // Fill entry! {action=FillMessage::MouseDown, key_down=Lmb}, // Tool Actions - entry! {action=ToolMessage::SelectTool(ToolType::Fill), key_down=KeyF}, - entry! {action=ToolMessage::SelectTool(ToolType::Rectangle), key_down=KeyM}, - entry! {action=ToolMessage::SelectTool(ToolType::Ellipse), key_down=KeyE}, - entry! {action=ToolMessage::SelectTool(ToolType::Select), key_down=KeyV}, - entry! {action=ToolMessage::SelectTool(ToolType::Line), key_down=KeyL}, - entry! {action=ToolMessage::SelectTool(ToolType::Pen), key_down=KeyP}, - entry! {action=ToolMessage::SelectTool(ToolType::Shape), key_down=KeyY}, - entry! {action=ToolMessage::SelectTool(ToolType::Eyedropper), key_down=KeyI}, + entry! {action=ToolMessage::ActivateTool(ToolType::Select), key_down=KeyV}, + entry! {action=ToolMessage::ActivateTool(ToolType::Eyedropper), key_down=KeyI}, + entry! {action=ToolMessage::ActivateTool(ToolType::Fill), key_down=KeyF}, + entry! {action=ToolMessage::ActivateTool(ToolType::Path), key_down=KeyA}, + entry! {action=ToolMessage::ActivateTool(ToolType::Pen), key_down=KeyP}, + entry! {action=ToolMessage::ActivateTool(ToolType::Line), key_down=KeyL}, + entry! {action=ToolMessage::ActivateTool(ToolType::Rectangle), key_down=KeyM}, + entry! {action=ToolMessage::ActivateTool(ToolType::Ellipse), key_down=KeyE}, + entry! {action=ToolMessage::ActivateTool(ToolType::Shape), key_down=KeyY}, + // Colors entry! {action=ToolMessage::ResetColors, key_down=KeyX, modifiers=[KeyShift, KeyControl]}, entry! {action=ToolMessage::SwapColors, key_down=KeyX, modifiers=[KeyShift]}, // Editor Actions @@ -300,7 +303,7 @@ impl InputMapper { let mut actions = actions .into_iter() .flatten() - .filter(|a| !matches!(*a, MessageDiscriminant::Tool(ToolMessageDiscriminant::SelectTool) | MessageDiscriminant::Global(_))); + .filter(|a| !matches!(*a, MessageDiscriminant::Tool(ToolMessageDiscriminant::ActivateTool) | MessageDiscriminant::Global(_))); self.mapping .key_down .iter() diff --git a/editor/src/input/input_preprocessor.rs b/editor/src/input/input_preprocessor.rs index 1b9ea2f13b..0cfc90976b 100644 --- a/editor/src/input/input_preprocessor.rs +++ b/editor/src/input/input_preprocessor.rs @@ -7,9 +7,10 @@ use bitflags::bitflags; #[doc(inline)] pub use graphene::DocumentResponse; +use serde::{Deserialize, Serialize}; #[impl_message(Message, InputPreprocessor)] -#[derive(PartialEq, Clone, Debug)] +#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] pub enum InputPreprocessorMessage { MouseDown(EditorMouseState, ModifierKeys), MouseUp(EditorMouseState, ModifierKeys), @@ -21,7 +22,7 @@ pub enum InputPreprocessorMessage { } bitflags! { - #[derive(Default)] + #[derive(Default, Serialize, Deserialize)] #[repr(transparent)] pub struct ModifierKeys: u8 { const CONTROL = 0b0000_0001; diff --git a/editor/src/input/mouse.rs b/editor/src/input/mouse.rs index 9a16661fa0..ca1d514b83 100644 --- a/editor/src/input/mouse.rs +++ b/editor/src/input/mouse.rs @@ -1,11 +1,12 @@ use bitflags::bitflags; use glam::DVec2; +use serde::{Deserialize, Serialize}; // Origin is top left pub type ViewportPosition = DVec2; pub type EditorPosition = DVec2; -#[derive(PartialEq, Clone, Debug, Default)] +#[derive(PartialEq, Clone, Debug, Default, Serialize, Deserialize)] pub struct ViewportBounds { pub top_left: DVec2, pub bottom_right: DVec2, @@ -28,7 +29,7 @@ impl ViewportBounds { } } -#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Hash)] +#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Hash, Serialize, Deserialize)] pub struct ScrollDelta { pub x: i32, pub y: i32, @@ -47,7 +48,7 @@ impl ScrollDelta { } } -#[derive(Debug, Copy, Clone, Default, PartialEq)] +#[derive(Debug, Copy, Clone, Default, PartialEq, Serialize, Deserialize)] pub struct MouseState { pub position: ViewportPosition, pub mouse_keys: MouseKeys, @@ -77,7 +78,7 @@ impl MouseState { } } -#[derive(Debug, Copy, Clone, Default, PartialEq)] +#[derive(Debug, Copy, Clone, Default, PartialEq, Serialize, Deserialize)] pub struct EditorMouseState { pub editor_position: EditorPosition, pub mouse_keys: MouseKeys, @@ -116,7 +117,7 @@ impl EditorMouseState { } bitflags! { - #[derive(Default)] + #[derive(Default, Serialize, Deserialize)] #[repr(transparent)] pub struct MouseKeys: u8 { const LEFT = 0b0000_0001; diff --git a/editor/src/lib.rs b/editor/src/lib.rs index da77963231..772a95a941 100644 --- a/editor/src/lib.rs +++ b/editor/src/lib.rs @@ -26,9 +26,6 @@ pub use graphene::LayerId; #[doc(inline)] pub use graphene::document::Document as SvgDocument; -#[doc(inline)] -pub use frontend::Callback; - use communication::dispatcher::Dispatcher; // TODO: serialize with serde to save the current editor state pub struct Editor { @@ -38,14 +35,16 @@ pub struct Editor { use message_prelude::*; impl Editor { - pub fn new(callback: Callback) -> Self { - Self { - dispatcher: Dispatcher::new(callback), - } + pub fn new() -> Self { + Self { dispatcher: Dispatcher::new() } } - pub fn handle_message>(&mut self, message: T) -> Result<(), EditorError> { - self.dispatcher.handle_message(message) + pub fn handle_message>(&mut self, message: T) -> Result, EditorError> { + self.dispatcher.handle_message(message).map(|_| { + let mut responses = Vec::new(); + std::mem::swap(&mut responses, &mut self.dispatcher.responses); + responses + }) } } diff --git a/editor/src/misc/test_utils.rs b/editor/src/misc/test_utils.rs index 06d0914638..d7f45dbbb9 100644 --- a/editor/src/misc/test_utils.rs +++ b/editor/src/misc/test_utils.rs @@ -62,7 +62,7 @@ impl EditorTestUtils for Editor { } fn mouseup(&mut self, state: EditorMouseState) { - self.handle_message(InputPreprocessorMessage::MouseUp(state, ModifierKeys::default())).unwrap() + self.handle_message(InputPreprocessorMessage::MouseUp(state, ModifierKeys::default())).unwrap(); } fn lmb_mousedown(&mut self, x: f64, y: f64) { @@ -70,7 +70,7 @@ impl EditorTestUtils for Editor { editor_position: (x, y).into(), mouse_keys: MouseKeys::LEFT, scroll_delta: ScrollDelta::default(), - }) + }); } fn input(&mut self, message: InputPreprocessorMessage) { @@ -78,7 +78,7 @@ impl EditorTestUtils for Editor { } fn select_tool(&mut self, typ: ToolType) { - self.handle_message(Message::Tool(ToolMessage::SelectTool(typ))).unwrap(); + self.handle_message(Message::Tool(ToolMessage::ActivateTool(typ))).unwrap(); } fn select_primary_color(&mut self, color: Color) { diff --git a/editor/src/tool/mod.rs b/editor/src/tool/mod.rs index 7aa8f3e531..60d72081ac 100644 --- a/editor/src/tool/mod.rs +++ b/editor/src/tool/mod.rs @@ -9,6 +9,7 @@ use crate::{ communication::{message::Message, MessageHandler}, Color, }; +use serde::{Deserialize, Serialize}; use std::collections::VecDeque; use std::{ collections::HashMap, @@ -126,7 +127,7 @@ fn default_tool_options() -> HashMap { } #[repr(usize)] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum ToolType { Select, Crop, diff --git a/editor/src/tool/tool_message_handler.rs b/editor/src/tool/tool_message_handler.rs index 72eaf10ea9..a281b8f4fb 100644 --- a/editor/src/tool/tool_message_handler.rs +++ b/editor/src/tool/tool_message_handler.rs @@ -6,14 +6,16 @@ use crate::{ document::DocumentMessageHandler, tool::{tool_options::ToolOptions, DocumentToolData, ToolFsmState, ToolType}, }; +use serde::{Deserialize, Serialize}; use std::collections::VecDeque; #[impl_message(Message, Tool)] -#[derive(PartialEq, Clone, Debug)] +#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] pub enum ToolMessage { - SelectTool(ToolType), + ActivateTool(ToolType), SelectPrimaryColor(Color), SelectSecondaryColor(Color), + SelectedLayersChanged, SwapColors, ResetColors, NoOp, @@ -63,7 +65,7 @@ impl MessageHandler update_working_colors(document_data, responses); } - SelectTool(new_tool) => { + ActivateTool(new_tool) => { let tool_data = &mut self.tool_state.tool_data; let document_data = &self.tool_state.document_tool_data; let old_tool = tool_data.active_tool_type; @@ -75,12 +77,13 @@ impl MessageHandler // Get the Abort state of a tool's FSM let reset_message = |tool| match tool { - ToolType::Ellipse => Some(EllipseMessage::Abort.into()), + ToolType::Select => Some(SelectMessage::Abort.into()), + ToolType::Path => Some(PathMessage::Abort.into()), + ToolType::Pen => Some(PenMessage::Abort.into()), + ToolType::Line => Some(LineMessage::Abort.into()), ToolType::Rectangle => Some(RectangleMessage::Abort.into()), + ToolType::Ellipse => Some(EllipseMessage::Abort.into()), ToolType::Shape => Some(ShapeMessage::Abort.into()), - ToolType::Line => Some(LineMessage::Abort.into()), - ToolType::Pen => Some(PenMessage::Abort.into()), - ToolType::Select => Some(SelectMessage::Abort.into()), _ => None, }; @@ -100,8 +103,9 @@ impl MessageHandler } // Special cases for specific tools - if new_tool == ToolType::Select { - responses.push_back(SelectMessage::UpdateSelectionBoundingBox.into()); + // TODO: Refactor to avoid doing this here + if new_tool == ToolType::Select || new_tool == ToolType::Path { + responses.push_back(ToolMessage::SelectedLayersChanged.into()); } // Store the new active tool @@ -112,6 +116,13 @@ impl MessageHandler let tool_options = self.tool_state.document_tool_data.tool_options.get(&new_tool).map(|tool_options| *tool_options); responses.push_back(FrontendMessage::SetActiveTool { tool_name, tool_options }.into()); } + SelectedLayersChanged => { + match self.tool_state.tool_data.active_tool_type { + ToolType::Select => responses.push_back(SelectMessage::UpdateSelectionBoundingBox.into()), + ToolType::Path => responses.push_back(PathMessage::RedrawOverlay.into()), + _ => (), + }; + } SwapColors => { let document_data = &mut self.tool_state.document_tool_data; @@ -146,7 +157,7 @@ impl MessageHandler } } fn actions(&self) -> ActionList { - let mut list = actions!(ToolMessageDiscriminant; ResetColors, SwapColors, SelectTool, SetToolOptions); + let mut list = actions!(ToolMessageDiscriminant; ResetColors, SwapColors, ActivateTool, SetToolOptions); list.extend(self.tool_state.tool_data.active_tool().actions()); list diff --git a/editor/src/tool/tools/crop.rs b/editor/src/tool/tools/crop.rs index f8b9239015..e3573bae98 100644 --- a/editor/src/tool/tools/crop.rs +++ b/editor/src/tool/tools/crop.rs @@ -1,11 +1,12 @@ use crate::message_prelude::*; use crate::tool::ToolActionHandlerData; +use serde::{Deserialize, Serialize}; #[derive(Default)] pub struct Crop; #[impl_message(Message, ToolMessage, Crop)] -#[derive(PartialEq, Clone, Debug, Hash)] +#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)] pub enum CropMessage { MouseMove, } diff --git a/editor/src/tool/tools/ellipse.rs b/editor/src/tool/tools/ellipse.rs index e1e11d901f..9d62196070 100644 --- a/editor/src/tool/tools/ellipse.rs +++ b/editor/src/tool/tools/ellipse.rs @@ -4,6 +4,7 @@ use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData}; use crate::{document::DocumentMessageHandler, message_prelude::*}; use glam::DAffine2; use graphene::{layers::style, Operation}; +use serde::{Deserialize, Serialize}; use super::resize::*; @@ -14,7 +15,7 @@ pub struct Ellipse { } #[impl_message(Message, ToolMessage, Ellipse)] -#[derive(PartialEq, Clone, Debug, Hash)] +#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)] pub enum EllipseMessage { DragStart, DragStop, diff --git a/editor/src/tool/tools/eyedropper.rs b/editor/src/tool/tools/eyedropper.rs index f61e6bac07..7d09834c3c 100644 --- a/editor/src/tool/tools/eyedropper.rs +++ b/editor/src/tool/tools/eyedropper.rs @@ -4,12 +4,13 @@ use crate::tool::{ToolActionHandlerData, ToolMessage}; use glam::DVec2; use graphene::layers::LayerDataType; use graphene::Quad; +use serde::{Deserialize, Serialize}; #[derive(Default)] pub struct Eyedropper; #[impl_message(Message, ToolMessage, Eyedropper)] -#[derive(PartialEq, Clone, Debug, Hash)] +#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)] pub enum EyedropperMessage { LeftMouseDown, RightMouseDown, diff --git a/editor/src/tool/tools/fill.rs b/editor/src/tool/tools/fill.rs index 0bc6f2fc63..c2359f2fcb 100644 --- a/editor/src/tool/tools/fill.rs +++ b/editor/src/tool/tools/fill.rs @@ -3,12 +3,13 @@ use crate::message_prelude::*; use crate::tool::ToolActionHandlerData; use glam::DVec2; use graphene::{Operation, Quad}; +use serde::{Deserialize, Serialize}; #[derive(Default)] pub struct Fill; #[impl_message(Message, ToolMessage, Fill)] -#[derive(PartialEq, Clone, Debug, Hash)] +#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)] pub enum FillMessage { MouseDown, } @@ -21,7 +22,7 @@ impl<'a> MessageHandler> for Fill { if let Some(path) = data.0.document.intersects_quad_root(quad).last() { responses.push_back( - Operation::FillLayer { + Operation::SetLayerFill { path: path.to_vec(), color: data.1.primary_color, } diff --git a/editor/src/tool/tools/line.rs b/editor/src/tool/tools/line.rs index 6cc6224f14..33da52dfb3 100644 --- a/editor/src/tool/tools/line.rs +++ b/editor/src/tool/tools/line.rs @@ -5,6 +5,7 @@ use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData, ToolOptions, Too use crate::{document::DocumentMessageHandler, message_prelude::*}; use glam::{DAffine2, DVec2}; use graphene::{layers::style, Operation}; +use serde::{Deserialize, Serialize}; #[derive(Default)] pub struct Line { @@ -13,7 +14,7 @@ pub struct Line { } #[impl_message(Message, ToolMessage, Line)] -#[derive(PartialEq, Clone, Debug, Hash)] +#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)] pub enum LineMessage { DragStart, DragStop, diff --git a/editor/src/tool/tools/navigate.rs b/editor/src/tool/tools/navigate.rs index 4c6264795e..e888b97aa6 100644 --- a/editor/src/tool/tools/navigate.rs +++ b/editor/src/tool/tools/navigate.rs @@ -1,11 +1,12 @@ use crate::message_prelude::*; use crate::tool::ToolActionHandlerData; +use serde::{Deserialize, Serialize}; #[derive(Default)] pub struct Navigate; #[impl_message(Message, ToolMessage, Navigate)] -#[derive(PartialEq, Clone, Debug, Hash)] +#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)] pub enum NavigateMessage { MouseMove, } diff --git a/editor/src/tool/tools/path.rs b/editor/src/tool/tools/path.rs index 41d54387e3..a5999352d3 100644 --- a/editor/src/tool/tools/path.rs +++ b/editor/src/tool/tools/path.rs @@ -1,18 +1,306 @@ +use crate::consts::COLOR_ACCENT; +use crate::consts::VECTOR_MANIPULATOR_ANCHOR_MARKER_SIZE; +use crate::document::DocumentMessageHandler; +use crate::document::VectorManipulatorSegment; +use crate::document::VectorManipulatorShape; +use crate::input::InputPreprocessor; use crate::message_prelude::*; use crate::tool::ToolActionHandlerData; +use crate::tool::{DocumentToolData, Fsm}; +use glam::{DAffine2, DVec2}; +use graphene::color::Color; +use graphene::layers::style; +use graphene::layers::style::Fill; +use graphene::layers::style::Stroke; +use graphene::Operation; +use kurbo::BezPath; +use serde::{Deserialize, Serialize}; #[derive(Default)] -pub struct Path; +pub struct Path { + fsm_state: PathToolFsmState, + data: PathToolData, +} #[impl_message(Message, ToolMessage, Path)] -#[derive(PartialEq, Clone, Debug, Hash)] +#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)] pub enum PathMessage { - MouseMove, + RedrawOverlay, + Abort, } impl<'a> MessageHandler> for Path { fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque) { - todo!("{}::handle_input {:?} {:?} {:?} ", module_path!(), action, data, responses); + self.fsm_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, data.2, responses); + } + fn actions(&self) -> ActionList { + use PathToolFsmState::*; + match self.fsm_state { + Ready => actions!(PathMessageDiscriminant;), + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum PathToolFsmState { + Ready, +} + +impl Default for PathToolFsmState { + fn default() -> Self { + PathToolFsmState::Ready } - advertise_actions!(); +} + +#[derive(Clone, Debug, Default)] +struct PathToolData { + anchor_marker_pool: Vec>, + handle_marker_pool: Vec>, + anchor_handle_line_pool: Vec>, + shape_outline_pool: Vec>, +} + +impl PathToolData {} + +impl Fsm for PathToolFsmState { + type ToolData = PathToolData; + + fn transition( + self, + event: ToolMessage, + document: &DocumentMessageHandler, + _tool_data: &DocumentToolData, + data: &mut Self::ToolData, + _input: &InputPreprocessor, + responses: &mut VecDeque, + ) -> Self { + if let ToolMessage::Path(event) = event { + use PathMessage::*; + use PathToolFsmState::*; + match (self, event) { + (_, RedrawOverlay) => { + let (mut anchor_i, mut handle_i, mut line_i, mut shape_i) = (0, 0, 0, 0); + + let shapes_to_draw = document.selected_layers_vector_points(); + // Grow the overlay pools by the shortfall, if any + let (total_anchors, total_handles, total_anchor_handle_lines) = calculate_total_overlays_per_type(&shapes_to_draw); + let total_shapes = shapes_to_draw.len(); + grow_overlay_pool_entries(&mut data.shape_outline_pool, total_shapes, add_shape_outline, responses); + grow_overlay_pool_entries(&mut data.anchor_handle_line_pool, total_anchor_handle_lines, add_anchor_handle_line, responses); + grow_overlay_pool_entries(&mut data.anchor_marker_pool, total_anchors, add_anchor_marker, responses); + grow_overlay_pool_entries(&mut data.handle_marker_pool, total_handles, add_handle_marker, responses); + + // Helps push values that end in approximately half, plus or minus some floating point imprecision, towards the same side of the round() function + const BIAS: f64 = 0.0001; + + // Draw the overlays for each shape + for shape_to_draw in &shapes_to_draw { + let shape_layer_path = &data.shape_outline_pool[shape_i]; + + responses.push_back( + Operation::SetShapePathInViewport { + path: shape_layer_path.clone(), + bez_path: shape_to_draw.path.clone(), + transform: shape_to_draw.transform.to_cols_array(), + } + .into(), + ); + responses.push_back( + Operation::SetLayerVisibility { + path: shape_layer_path.clone(), + visible: true, + } + .into(), + ); + shape_i += 1; + + for segment in &shape_to_draw.segments { + let (anchors, handles, anchor_handle_lines) = match segment { + VectorManipulatorSegment::Line(a1, a2) => (vec![*a1, *a2], vec![], vec![]), + VectorManipulatorSegment::Quad(a1, h1, a2) => (vec![*a1, *a2], vec![*h1], vec![(*h1, *a1)]), + VectorManipulatorSegment::Cubic(a1, h1, h2, a2) => (vec![*a1, *a2], vec![*h1, *h2], vec![(*h1, *a1), (*h2, *a2)]), + }; + + // Draw the line connecting the anchor with handle for cubic and quadratic bezier segments + for anchor_handle_line in anchor_handle_lines { + let marker = data.anchor_handle_line_pool[line_i].clone(); + + let line_vector = anchor_handle_line.0 - anchor_handle_line.1; + + let scale = DVec2::splat(line_vector.length()); + let angle = -line_vector.angle_between(DVec2::X); + let translation = (anchor_handle_line.1 + BIAS).round() + DVec2::splat(0.5); + let transform = DAffine2::from_scale_angle_translation(scale, angle, translation).to_cols_array(); + + responses.push_back(Operation::SetLayerTransformInViewport { path: marker.clone(), transform }.into()); + responses.push_back(Operation::SetLayerVisibility { path: marker, visible: true }.into()); + + line_i += 1; + } + + // Draw the draggable square points on the end of every line segment or bezier curve segment + for anchor in anchors { + let marker = data.anchor_marker_pool[anchor_i].clone(); + + let scale = DVec2::splat(VECTOR_MANIPULATOR_ANCHOR_MARKER_SIZE); + let angle = 0.; + let translation = (anchor - (scale / 2.) + BIAS).round(); + let transform = DAffine2::from_scale_angle_translation(scale, angle, translation).to_cols_array(); + + responses.push_back(Operation::SetLayerTransformInViewport { path: marker.clone(), transform }.into()); + responses.push_back(Operation::SetLayerVisibility { path: marker, visible: true }.into()); + + anchor_i += 1; + } + + // Draw the draggable handle for cubic and quadratic bezier segments + for handle in handles { + let marker = data.handle_marker_pool[handle_i].clone(); + + let scale = DVec2::splat(VECTOR_MANIPULATOR_ANCHOR_MARKER_SIZE); + let angle = 0.; + let translation = (handle - (scale / 2.) + BIAS).round(); + let transform = DAffine2::from_scale_angle_translation(scale, angle, translation).to_cols_array(); + + responses.push_back(Operation::SetLayerTransformInViewport { path: marker.clone(), transform }.into()); + responses.push_back(Operation::SetLayerVisibility { path: marker, visible: true }.into()); + + handle_i += 1; + } + } + } + + // Hide the remaining pooled overlays + for i in anchor_i..data.anchor_marker_pool.len() { + let marker = data.anchor_marker_pool[i].clone(); + responses.push_back(Operation::SetLayerVisibility { path: marker, visible: false }.into()); + } + for i in handle_i..data.handle_marker_pool.len() { + let marker = data.handle_marker_pool[i].clone(); + responses.push_back(Operation::SetLayerVisibility { path: marker, visible: false }.into()); + } + for i in line_i..data.anchor_handle_line_pool.len() { + let line = data.anchor_handle_line_pool[i].clone(); + responses.push_back(Operation::SetLayerVisibility { path: line, visible: false }.into()); + } + for i in shape_i..data.shape_outline_pool.len() { + let shape_i = data.shape_outline_pool[i].clone(); + responses.push_back(Operation::SetLayerVisibility { path: shape_i, visible: false }.into()); + } + + self + } + (_, Abort) => { + // Destory the overlay layer pools + while let Some(layer) = data.anchor_marker_pool.pop() { + responses.push_back(Operation::DeleteLayer { path: layer }.into()); + } + while let Some(layer) = data.handle_marker_pool.pop() { + responses.push_back(Operation::DeleteLayer { path: layer }.into()); + } + while let Some(layer) = data.anchor_handle_line_pool.pop() { + responses.push_back(Operation::DeleteLayer { path: layer }.into()); + } + while let Some(layer) = data.shape_outline_pool.pop() { + responses.push_back(Operation::DeleteLayer { path: layer }.into()); + } + + Ready + } + } + } else { + self + } + } +} + +fn calculate_total_overlays_per_type(shapes_to_draw: &Vec) -> (usize, usize, usize) { + let (mut total_anchors, mut total_handles, mut total_anchor_handle_lines) = (0, 0, 0); + + for shape_to_draw in shapes_to_draw { + for segment in &shape_to_draw.segments { + let (anchors, handles, anchor_handle_lines) = match segment { + VectorManipulatorSegment::Line(_, _) => (2, 0, 0), + VectorManipulatorSegment::Quad(_, _, _) => (2, 1, 1), + VectorManipulatorSegment::Cubic(_, _, _, _) => (2, 2, 2), + }; + total_anchors += anchors; + total_handles += handles; + total_anchor_handle_lines += anchor_handle_lines; + } + } + + (total_anchors, total_handles, total_anchor_handle_lines) +} + +fn grow_overlay_pool_entries(pool: &mut Vec>, total: usize, add_overlay_function: F, responses: &mut VecDeque) +where + F: Fn(&mut VecDeque) -> Vec, +{ + if pool.len() < total { + let additional = total - pool.len(); + + pool.reserve(additional); + + for _ in 0..additional { + let marker = add_overlay_function(responses); + pool.push(marker); + } + } +} + +fn add_anchor_marker(responses: &mut VecDeque) -> Vec { + let layer_path = vec![generate_uuid()]; + responses.push_back( + Operation::AddOverlayRect { + path: layer_path.clone(), + transform: DAffine2::IDENTITY.to_cols_array(), + style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 2.0)), Some(Fill::new(Color::WHITE))), + } + .into(), + ); + + layer_path +} + +fn add_handle_marker(responses: &mut VecDeque) -> Vec { + let layer_path = vec![generate_uuid()]; + responses.push_back( + Operation::AddOverlayEllipse { + path: layer_path.clone(), + transform: DAffine2::IDENTITY.to_cols_array(), + style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 2.0)), Some(Fill::new(Color::WHITE))), + } + .into(), + ); + + layer_path +} + +fn add_anchor_handle_line(responses: &mut VecDeque) -> Vec { + let layer_path = vec![generate_uuid()]; + responses.push_back( + Operation::AddOverlayLine { + path: layer_path.clone(), + transform: DAffine2::IDENTITY.to_cols_array(), + style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 1.0)), Some(Fill::none())), + } + .into(), + ); + + layer_path +} + +fn add_shape_outline(responses: &mut VecDeque) -> Vec { + let layer_path = vec![generate_uuid()]; + responses.push_back( + Operation::AddOverlayShape { + path: layer_path.clone(), + bez_path: BezPath::default(), + style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 1.0)), Some(Fill::none())), + } + .into(), + ); + + layer_path } diff --git a/editor/src/tool/tools/pen.rs b/editor/src/tool/tools/pen.rs index ffd64417b6..e4a86d7481 100644 --- a/editor/src/tool/tools/pen.rs +++ b/editor/src/tool/tools/pen.rs @@ -3,6 +3,7 @@ use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData, ToolOptions, Too use crate::{document::DocumentMessageHandler, message_prelude::*}; use glam::DAffine2; use graphene::{layers::style, Operation}; +use serde::{Deserialize, Serialize}; #[derive(Default)] pub struct Pen { @@ -11,7 +12,7 @@ pub struct Pen { } #[impl_message(Message, ToolMessage, Pen)] -#[derive(PartialEq, Clone, Debug, Hash)] +#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)] pub enum PenMessage { Undo, DragStart, diff --git a/editor/src/tool/tools/rectangle.rs b/editor/src/tool/tools/rectangle.rs index ce562ce7d1..107206e987 100644 --- a/editor/src/tool/tools/rectangle.rs +++ b/editor/src/tool/tools/rectangle.rs @@ -4,6 +4,7 @@ use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData}; use crate::{document::DocumentMessageHandler, message_prelude::*}; use glam::DAffine2; use graphene::{layers::style, Operation}; +use serde::{Deserialize, Serialize}; use super::resize::*; @@ -14,7 +15,7 @@ pub struct Rectangle { } #[impl_message(Message, ToolMessage, Rectangle)] -#[derive(PartialEq, Clone, Debug, Hash)] +#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)] pub enum RectangleMessage { DragStart, DragStop, diff --git a/editor/src/tool/tools/select.rs b/editor/src/tool/tools/select.rs index ccab7a0b19..94a7e51261 100644 --- a/editor/src/tool/tools/select.rs +++ b/editor/src/tool/tools/select.rs @@ -1,4 +1,3 @@ -use graphene::color::Color; use graphene::layers::style; use graphene::layers::style::Fill; use graphene::layers::style::Stroke; @@ -8,6 +7,7 @@ use graphene::Quad; use glam::{DAffine2, DVec2}; use serde::{Deserialize, Serialize}; +use crate::consts::COLOR_ACCENT; use crate::input::keyboard::Key; use crate::input::{mouse::ViewportPosition, InputPreprocessor}; use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData}; @@ -24,7 +24,7 @@ pub struct Select { } #[impl_message(Message, ToolMessage, Select)] -#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, Hash)] +#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)] pub enum SelectMessage { DragStart { add_to_selection: Key }, DragStop, @@ -70,7 +70,7 @@ struct SelectToolData { drag_current: ViewportPosition, layers_dragging: Vec>, // Paths and offsets drag_box_id: Option>, - bounding_box_id: Option>, + bounding_box_path: Option>, } impl SelectToolData { @@ -89,13 +89,13 @@ impl SelectToolData { } } -fn add_boundnig_box(responses: &mut Vec) -> Vec { +fn add_bounding_box(responses: &mut Vec) -> Vec { let path = vec![generate_uuid()]; responses.push( - Operation::AddBoundingBox { + Operation::AddOverlayRect { path: path.clone(), transform: DAffine2::ZERO.to_cols_array(), - style: style::PathStyle::new(Some(Stroke::new(Color::from_rgb8(0x00, 0xA8, 0xFF), 1.0)), Some(Fill::none())), + style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 1.0)), Some(Fill::none())), } .into(), ); @@ -125,11 +125,11 @@ impl Fsm for SelectToolFsmState { match (self, event) { (_, UpdateSelectionBoundingBox) => { let mut buffer = Vec::new(); - let response = match (document.selected_layers_bounding_box(), data.bounding_box_id.take()) { + let response = match (document.selected_layers_bounding_box(), data.bounding_box_path.take()) { (None, Some(path)) => Operation::DeleteLayer { path }.into(), (Some([pos1, pos2]), path) => { - let path = path.unwrap_or_else(|| add_boundnig_box(&mut buffer)); - data.bounding_box_id = Some(path.clone()); + let path = path.unwrap_or_else(|| add_bounding_box(&mut buffer)); + data.bounding_box_path = Some(path.clone()); let transform = transform_from_box(pos1, pos2); Operation::SetLayerTransformInViewport { path, transform }.into() } @@ -163,7 +163,7 @@ impl Fsm for SelectToolFsmState { if !input.keyboard.get(add_to_selection as usize) { buffer.push(DocumentMessage::DeselectAllLayers.into()); } - data.drag_box_id = Some(add_boundnig_box(&mut buffer)); + data.drag_box_id = Some(add_bounding_box(&mut buffer)); DrawingBox }; buffer.into_iter().rev().for_each(|message| responses.push_front(message)); @@ -220,7 +220,7 @@ impl Fsm for SelectToolFsmState { (_, Abort) => { let mut delete = |path: &mut Option>| path.take().map(|path| responses.push_front(Operation::DeleteLayer { path }.into())); delete(&mut data.drag_box_id); - delete(&mut data.bounding_box_id); + delete(&mut data.bounding_box_path); Ready } (_, Align(axis, aggregate)) => { diff --git a/editor/src/tool/tools/shape.rs b/editor/src/tool/tools/shape.rs index c9fbc17952..b921699c08 100644 --- a/editor/src/tool/tools/shape.rs +++ b/editor/src/tool/tools/shape.rs @@ -4,6 +4,7 @@ use crate::tool::{DocumentToolData, Fsm, ShapeType, ToolActionHandlerData, ToolO use crate::{document::DocumentMessageHandler, message_prelude::*}; use glam::DAffine2; use graphene::{layers::style, Operation}; +use serde::{Deserialize, Serialize}; use super::resize::*; @@ -14,7 +15,7 @@ pub struct Shape { } #[impl_message(Message, ToolMessage, Shape)] -#[derive(PartialEq, Clone, Debug, Hash)] +#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)] pub enum ShapeMessage { DragStart, DragStop, @@ -82,7 +83,7 @@ impl Fsm for ShapeToolFsmState { }; responses.push_back( - Operation::AddShape { + Operation::AddNgon { path: shape_data.path.clone().unwrap(), insert_index: -1, transform: DAffine2::ZERO.to_cols_array(), diff --git a/frontend/.gitignore b/frontend/.gitignore index 3bc6c711f5..3b22476aee 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -1,4 +1,3 @@ node_modules/ dist/ -pkg/ -wasm/pkg/* +wasm/pkg/ diff --git a/frontend/src/components/panels/Document.vue b/frontend/src/components/panels/Document.vue index 1834a127a2..c071a04aff 100644 --- a/frontend/src/components/panels/Document.vue +++ b/frontend/src/components/panels/Document.vue @@ -91,7 +91,7 @@ - + diff --git a/frontend/wasm/.gitignore b/frontend/wasm/.gitignore deleted file mode 100644 index b2bcbe85e5..0000000000 --- a/frontend/wasm/.gitignore +++ /dev/null @@ -1 +0,0 @@ -pkg/ \ No newline at end of file diff --git a/frontend/wasm/Cargo.toml b/frontend/wasm/Cargo.toml index e6b509a740..9c513941ca 100644 --- a/frontend/wasm/Cargo.toml +++ b/frontend/wasm/Cargo.toml @@ -23,6 +23,9 @@ log = "0.4" serde = { version = "1.0", features = ["derive"] } wasm-bindgen = { version = "0.2.73", features = ["serde-serialize"] } +[profile.dev] +opt-level = 3 + [dev-dependencies] wasm-bindgen-test = "0.3.22" diff --git a/frontend/wasm/src/document.rs b/frontend/wasm/src/document.rs index 98785e5a04..bd0f6821f6 100644 --- a/frontend/wasm/src/document.rs +++ b/frontend/wasm/src/document.rs @@ -1,6 +1,5 @@ use crate::shims::Error; use crate::wrappers::{translate_key, translate_tool, Color}; -use crate::EDITOR_STATE; use editor::input::input_preprocessor::ModifierKeys; use editor::input::mouse::{EditorMouseState, ScrollDelta, ViewportBounds}; use editor::message_prelude::*; @@ -14,23 +13,31 @@ fn convert_error(err: editor::EditorError) -> JsValue { Error::new(&err.to_string()).into() } +fn dispatch>(message: T) -> Result<(), JsValue> { + let result = crate::EDITOR_STATE.with(|state| state.borrow_mut().handle_message(message.into())); + if let Ok(messages) = result { + crate::handle_responses(messages); + } + Ok(()) +} + /// Modify the currently selected tool in the document state store #[wasm_bindgen] pub fn select_tool(tool: String) -> Result<(), JsValue> { - EDITOR_STATE.with(|editor| match translate_tool(&tool) { - Some(tool) => editor.borrow_mut().handle_message(ToolMessage::SelectTool(tool)).map_err(convert_error), + match translate_tool(&tool) { + Some(tool) => dispatch(ToolMessage::ActivateTool(tool)), None => Err(Error::new(&format!("Couldn't select {} because it was not recognized as a valid tool", tool)).into()), - }) + } } /// Update the options for a given tool #[wasm_bindgen] pub fn set_tool_options(tool: String, options: &JsValue) -> Result<(), JsValue> { match options.into_serde::() { - Ok(options) => EDITOR_STATE.with(|editor| match translate_tool(&tool) { - Some(tool) => editor.borrow_mut().handle_message(ToolMessage::SetToolOptions(tool, options)).map_err(convert_error), + Ok(options) => match translate_tool(&tool) { + Some(tool) => dispatch(ToolMessage::SetToolOptions(tool, options)), None => Err(Error::new(&format!("Couldn't set options for {} because it was not recognized as a valid tool", tool)).into()), - }), + }, Err(err) => Err(Error::new(&format!("Invalid JSON for ToolOptions: {}", err)).into()), } } @@ -48,60 +55,60 @@ pub fn send_tool_message(tool: String, message: &JsValue) -> Result<(), JsValue> }, None => Err(Error::new(&format!("Couldn't send message for {} because it was not recognized as a valid tool", tool)).into()), }; - EDITOR_STATE.with(|editor| match tool_message { - Ok(tool_message) => editor.borrow_mut().handle_message(tool_message).map_err(convert_error), + match tool_message { + Ok(tool_message) => dispatch(tool_message), Err(err) => Err(err), - }) + } } #[wasm_bindgen] pub fn select_document(document: usize) -> Result<(), JsValue> { - EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentsMessage::SelectDocument(document)).map_err(convert_error)) + dispatch(DocumentsMessage::SelectDocument(document)) } #[wasm_bindgen] pub fn get_open_documents_list() -> Result<(), JsValue> { - EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentsMessage::GetOpenDocumentsList).map_err(convert_error)) + dispatch(DocumentsMessage::GetOpenDocumentsList) } #[wasm_bindgen] pub fn new_document() -> Result<(), JsValue> { - EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentsMessage::NewDocument).map_err(convert_error)) + dispatch(DocumentsMessage::NewDocument) } #[wasm_bindgen] pub fn open_document() -> Result<(), JsValue> { - EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentsMessage::OpenDocument).map_err(convert_error)) + dispatch(DocumentsMessage::OpenDocument) } #[wasm_bindgen] pub fn open_document_file(name: String, content: String) -> Result<(), JsValue> { - EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentsMessage::OpenDocumentFile(name, content)).map_err(convert_error)) + dispatch(DocumentsMessage::OpenDocumentFile(name, content)) } #[wasm_bindgen] pub fn save_document() -> Result<(), JsValue> { - EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentMessage::SaveDocument)).map_err(convert_error) + dispatch(DocumentMessage::SaveDocument) } #[wasm_bindgen] pub fn close_document(document: usize) -> Result<(), JsValue> { - EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentsMessage::CloseDocument(document)).map_err(convert_error)) + dispatch(DocumentsMessage::CloseDocument(document)) } #[wasm_bindgen] pub fn close_all_documents() -> Result<(), JsValue> { - EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentsMessage::CloseAllDocuments).map_err(convert_error)) + dispatch(DocumentsMessage::CloseAllDocuments) } #[wasm_bindgen] pub fn close_active_document_with_confirmation() -> Result<(), JsValue> { - EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentsMessage::CloseActiveDocumentWithConfirmation).map_err(convert_error)) + dispatch(DocumentsMessage::CloseActiveDocumentWithConfirmation) } #[wasm_bindgen] pub fn close_all_documents_with_confirmation() -> Result<(), JsValue> { - EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentsMessage::CloseAllDocumentsWithConfirmation).map_err(convert_error)) + dispatch(DocumentsMessage::CloseAllDocumentsWithConfirmation) } /// Send new bounds when document panel viewports get resized or moved within the editor @@ -110,7 +117,7 @@ pub fn close_all_documents_with_confirmation() -> Result<(), JsValue> { pub fn bounds_of_viewports(bounds_of_viewports: &[f64]) -> Result<(), JsValue> { let chunked: Vec<_> = bounds_of_viewports.chunks(4).map(ViewportBounds::from_slice).collect(); let ev = InputPreprocessorMessage::BoundsOfViewports(chunked); - EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(ev)).map_err(convert_error) + dispatch(ev) } /// Mouse movement within the screenspace bounds of the viewport @@ -121,7 +128,7 @@ pub fn on_mouse_move(x: f64, y: f64, mouse_keys: u8, modifiers: u8) -> Result<() let modifier_keys = ModifierKeys::from_bits(modifiers).expect("invalid modifier keys"); let ev = InputPreprocessorMessage::MouseMove(editor_mouse_state, modifier_keys); - EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(ev)).map_err(convert_error) + dispatch(ev) } /// Mouse scrolling within the screenspace bounds of the viewport @@ -133,7 +140,7 @@ pub fn on_mouse_scroll(x: f64, y: f64, mouse_keys: u8, wheel_delta_x: i32, wheel let modifier_keys = ModifierKeys::from_bits(modifiers).expect("invalid modifier keys"); let ev = InputPreprocessorMessage::MouseScroll(editor_mouse_state, modifier_keys); - EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(ev)).map_err(convert_error) + dispatch(ev) } /// A mouse button depressed within screenspace the bounds of the viewport @@ -144,7 +151,7 @@ pub fn on_mouse_down(x: f64, y: f64, mouse_keys: u8, modifiers: u8) -> Result<() let modifier_keys = ModifierKeys::from_bits(modifiers).expect("invalid modifier keys"); let ev = InputPreprocessorMessage::MouseDown(editor_mouse_state, modifier_keys); - EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(ev)).map_err(convert_error) + dispatch(ev) } /// A mouse button released @@ -155,7 +162,7 @@ pub fn on_mouse_up(x: f64, y: f64, mouse_keys: u8, modifiers: u8) -> Result<(), let modifier_keys = ModifierKeys::from_bits(modifiers).expect("invalid modifier keys"); let ev = InputPreprocessorMessage::MouseUp(editor_mouse_state, modifier_keys); - EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(ev)).map_err(convert_error) + dispatch(ev) } /// A keyboard button depressed within screenspace the bounds of the viewport @@ -165,7 +172,7 @@ pub fn on_key_down(name: String, modifiers: u8) -> Result<(), JsValue> { let mods = ModifierKeys::from_bits(modifiers).expect("invalid modifier keys"); log::trace!("key down {:?}, name: {}, modifiers: {:?}", key, name, mods); let ev = InputPreprocessorMessage::KeyDown(key, mods); - EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(ev)).map_err(convert_error) + dispatch(ev) } /// A keyboard button released @@ -175,69 +182,61 @@ pub fn on_key_up(name: String, modifiers: u8) -> Result<(), JsValue> { let mods = ModifierKeys::from_bits(modifiers).expect("invalid modifier keys"); log::trace!("key up {:?}, name: {}, modifiers: {:?}", key, name, mods); let ev = InputPreprocessorMessage::KeyUp(key, mods); - EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(ev)).map_err(convert_error) + dispatch(ev) } /// Update primary color #[wasm_bindgen] pub fn update_primary_color(primary_color: Color) -> Result<(), JsValue> { - EDITOR_STATE - .with(|editor| editor.borrow_mut().handle_message(ToolMessage::SelectPrimaryColor(primary_color.inner()))) - .map_err(convert_error) + dispatch(ToolMessage::SelectPrimaryColor(primary_color.inner())) } /// Update secondary color #[wasm_bindgen] pub fn update_secondary_color(secondary_color: Color) -> Result<(), JsValue> { - EDITOR_STATE - .with(|editor| editor.borrow_mut().handle_message(ToolMessage::SelectSecondaryColor(secondary_color.inner()))) - .map_err(convert_error) + dispatch(ToolMessage::SelectSecondaryColor(secondary_color.inner())) } /// Swap primary and secondary color #[wasm_bindgen] pub fn swap_colors() -> Result<(), JsValue> { - EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(ToolMessage::SwapColors)).map_err(convert_error) + dispatch(ToolMessage::SwapColors) } /// Reset primary and secondary colors to their defaults #[wasm_bindgen] pub fn reset_colors() -> Result<(), JsValue> { - EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(ToolMessage::ResetColors)).map_err(convert_error) + dispatch(ToolMessage::ResetColors) } /// Undo history one step #[wasm_bindgen] pub fn undo() -> Result<(), JsValue> { - EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentMessage::Undo)).map_err(convert_error) + dispatch(DocumentMessage::Undo) } /// Redo history one step #[wasm_bindgen] pub fn redo() -> Result<(), JsValue> { - EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentMessage::Redo)).map_err(convert_error) + dispatch(DocumentMessage::Redo) } /// Select all layers #[wasm_bindgen] pub fn select_all_layers() -> Result<(), JsValue> { - EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentMessage::SelectAllLayers)).map_err(convert_error) + dispatch(DocumentMessage::SelectAllLayers) } /// Deselect all layers #[wasm_bindgen] pub fn deselect_all_layers() -> Result<(), JsValue> { - EDITOR_STATE - .with(|editor| editor.borrow_mut().handle_message(DocumentMessage::DeselectAllLayers)) - .map_err(convert_error) + dispatch(DocumentMessage::DeselectAllLayers) } /// Reorder selected layer #[wasm_bindgen] pub fn reorder_selected_layers(delta: i32) -> Result<(), JsValue> { - EDITOR_STATE - .with(|editor| editor.borrow_mut().handle_message(DocumentMessage::ReorderSelectedLayers(delta))) - .map_err(convert_error) + dispatch(DocumentMessage::ReorderSelectedLayers(delta)) } /// Set the blend mode for the selected layers @@ -263,111 +262,96 @@ pub fn set_blend_mode_for_selected_layers(blend_mode_svg_style_name: String) -> _ => return Err(convert_error(EditorError::Misc("UnknownBlendMode".to_string()))), }; - EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentMessage::SetBlendModeForSelectedLayers(blend_mode)).map_err(convert_error)) + dispatch(DocumentMessage::SetBlendModeForSelectedLayers(blend_mode)) } /// Set the opacity for the selected layers #[wasm_bindgen] pub fn set_opacity_for_selected_layers(opacity_percent: f64) -> Result<(), JsValue> { - EDITOR_STATE.with(|editor| { - editor - .borrow_mut() - .handle_message(DocumentMessage::SetOpacityForSelectedLayers(opacity_percent / 100.)) - .map_err(convert_error) - }) + dispatch(DocumentMessage::SetOpacityForSelectedLayers(opacity_percent / 100.)) } /// Export the document #[wasm_bindgen] pub fn export_document() -> Result<(), JsValue> { - EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentMessage::ExportDocument)).map_err(convert_error) + dispatch(DocumentMessage::ExportDocument) } /// Sets the zoom to the value #[wasm_bindgen] pub fn set_canvas_zoom(new_zoom: f64) -> Result<(), JsValue> { let ev = MovementMessage::SetCanvasZoom(new_zoom); - EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(ev)).map_err(convert_error) + dispatch(ev) } /// Zoom in to the next step #[wasm_bindgen] pub fn increase_canvas_zoom() -> Result<(), JsValue> { let ev = MovementMessage::IncreaseCanvasZoom; - EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(ev)).map_err(convert_error) + dispatch(ev) } /// Zoom out to the next step #[wasm_bindgen] pub fn decrease_canvas_zoom() -> Result<(), JsValue> { let ev = MovementMessage::DecreaseCanvasZoom; - EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(ev)).map_err(convert_error) + dispatch(ev) } /// Sets the rotation to the new value (in radians) #[wasm_bindgen] pub fn set_rotation(new_radians: f64) -> Result<(), JsValue> { let ev = MovementMessage::SetCanvasRotation(new_radians); - EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(ev)).map_err(convert_error) + dispatch(ev) } /// Translates document (in viewport coords) #[wasm_bindgen] pub fn translate_canvas(delta_x: f64, delta_y: f64) -> Result<(), JsValue> { let ev = MovementMessage::TranslateCanvas((delta_x, delta_y).into()); - EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(ev)).map_err(convert_error) + dispatch(ev) } /// Translates document (in viewport coords) #[wasm_bindgen] pub fn translate_canvas_by_fraction(delta_x: f64, delta_y: f64) -> Result<(), JsValue> { let ev = MovementMessage::TranslateCanvasByViewportFraction((delta_x, delta_y).into()); - EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(ev)).map_err(convert_error) + dispatch(ev) } /// Update the list of selected layers. The layer paths have to be stored in one array and are separated by LayerId::MAX #[wasm_bindgen] pub fn select_layers(paths: Vec) -> Result<(), JsValue> { let paths = paths.split(|id| *id == LayerId::MAX).map(|path| path.to_vec()).collect(); - EDITOR_STATE - .with(|editor| editor.borrow_mut().handle_message(DocumentMessage::SetSelectedLayers(paths))) - .map_err(convert_error) + dispatch(DocumentMessage::SetSelectedLayers(paths)) } /// Toggle visibility of a layer from the layer list #[wasm_bindgen] pub fn toggle_layer_visibility(path: Vec) -> Result<(), JsValue> { - EDITOR_STATE - .with(|editor| editor.borrow_mut().handle_message(DocumentMessage::ToggleLayerVisibility(path))) - .map_err(convert_error) + dispatch(DocumentMessage::ToggleLayerVisibility(path)) } /// Toggle expansions state of a layer from the layer list #[wasm_bindgen] pub fn toggle_layer_expansion(path: Vec) -> Result<(), JsValue> { - EDITOR_STATE - .with(|editor| editor.borrow_mut().handle_message(DocumentMessage::ToggleLayerExpansion(path))) - .map_err(convert_error) + dispatch(DocumentMessage::ToggleLayerExpansion(path)) } /// Renames a layer from the layer list #[wasm_bindgen] pub fn rename_layer(path: Vec, new_name: String) -> Result<(), JsValue> { - EDITOR_STATE - .with(|editor| editor.borrow_mut().handle_message(DocumentMessage::RenameLayer(path, new_name))) - .map_err(convert_error) + dispatch(DocumentMessage::RenameLayer(path, new_name)) } /// Deletes a layer from the layer list #[wasm_bindgen] pub fn delete_layer(path: Vec) -> Result<(), JsValue> { - EDITOR_STATE - .with(|editor| editor.borrow_mut().handle_message(DocumentMessage::DeleteLayer(path))) - .map_err(convert_error) + dispatch(DocumentMessage::DeleteLayer(path)) } /// Requests the backend to add a layer to the layer list #[wasm_bindgen] pub fn add_folder(path: Vec) -> Result<(), JsValue> { - EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentMessage::AddFolder(path))).map_err(convert_error) + dispatch(DocumentMessage::AddFolder(path)) } diff --git a/frontend/wasm/src/lib.rs b/frontend/wasm/src/lib.rs index e1f00a95cf..e5c78b7866 100644 --- a/frontend/wasm/src/lib.rs +++ b/frontend/wasm/src/lib.rs @@ -10,7 +10,9 @@ use utils::WasmLog; use wasm_bindgen::prelude::*; // the thread_local macro provides a way to initialize static variables with non-constant functions -thread_local! { pub static EDITOR_STATE: RefCell = RefCell::new(Editor::new(Box::new(handle_response))) } +thread_local! { + pub static EDITOR_STATE: RefCell = RefCell::new(Editor::new()); +} static LOGGER: WasmLog = WasmLog; #[wasm_bindgen(start)] @@ -20,6 +22,12 @@ pub fn init() { log::set_max_level(log::LevelFilter::Debug); } +pub fn handle_responses(responses: Vec) { + for response in responses.into_iter() { + handle_response(response) + } +} + #[wasm_bindgen(module = "/../src/utilities/response-handler-binding.ts")] extern "C" { #[wasm_bindgen(catch)] diff --git a/graphene/src/color.rs b/graphene/src/color.rs index 705a82a77c..12b800835f 100644 --- a/graphene/src/color.rs +++ b/graphene/src/color.rs @@ -40,7 +40,7 @@ impl Color { } /// Return an opaque `Color` from given `f32` RGB channels. - const fn from_unsafe(red: f32, green: f32, blue: f32) -> Color { + pub const fn from_unsafe(red: f32, green: f32, blue: f32) -> Color { Color { red, green, blue, alpha: 1. } } diff --git a/graphene/src/document.rs b/graphene/src/document.rs index 8906a46720..ee01e7a6da 100644 --- a/graphene/src/document.rs +++ b/graphene/src/document.rs @@ -215,7 +215,6 @@ impl Document { } pub fn mark_as_dirty(&mut self, path: &[LayerId]) -> Result<(), DocumentError> { - self.mark_downstream_as_dirty(path)?; self.mark_upstream_as_dirty(path)?; Ok(()) } @@ -264,6 +263,10 @@ impl Document { Ok(()) } + pub fn generate_transform_relative_to_viewport(&self, from: &[LayerId]) -> Result { + self.generate_transform_across_scope(from, None) + } + pub fn apply_transform_relative_to_viewport(&mut self, layer: &[LayerId], transform: DAffine2) -> Result<(), DocumentError> { self.transform_relative_to_scope(layer, None, transform) } @@ -297,33 +300,79 @@ impl Document { let responses = match &operation { Operation::AddEllipse { path, insert_index, transform, style } => { - self.set_layer(path, Layer::new(LayerDataType::Shape(Shape::ellipse(*style)), *transform), *insert_index)?; + let layer = Layer::new(LayerDataType::Shape(Shape::ellipse(*style)), *transform); + + self.set_layer(path, layer, *insert_index)?; + + Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::CreatedLayer { path: path.clone() }]) + } + Operation::AddOverlayEllipse { path, transform, style } => { + let mut ellipse = Shape::ellipse(*style); + ellipse.render_index = -1; + + let mut layer = Layer::new(LayerDataType::Shape(ellipse), *transform); + layer.overlay = true; + + self.set_layer(path, layer, -1)?; + Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::CreatedLayer { path: path.clone() }]) } Operation::AddRect { path, insert_index, transform, style } => { - self.set_layer(path, Layer::new(LayerDataType::Shape(Shape::rectangle(*style)), *transform), *insert_index)?; + let layer = Layer::new(LayerDataType::Shape(Shape::rectangle(*style)), *transform); + + self.set_layer(path, layer, *insert_index)?; + Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::CreatedLayer { path: path.clone() }]) } - Operation::AddBoundingBox { path, transform, style } => { + Operation::AddOverlayRect { path, transform, style } => { let mut rect = Shape::rectangle(*style); rect.render_index = -1; + let mut layer = Layer::new(LayerDataType::Shape(rect), *transform); layer.overlay = true; + + self.set_layer(path, layer, -1)?; + + Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::CreatedLayer { path: path.clone() }]) + } + Operation::AddLine { path, insert_index, transform, style } => { + let layer = Layer::new(LayerDataType::Shape(Shape::line(*style)), *transform); + + self.set_layer(path, layer, *insert_index)?; + + Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::CreatedLayer { path: path.clone() }]) + } + Operation::AddOverlayLine { path, transform, style } => { + let mut line = Shape::line(*style); + line.render_index = -1; + + let mut layer = Layer::new(LayerDataType::Shape(line), *transform); + layer.overlay = true; + self.set_layer(path, layer, -1)?; + Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::CreatedLayer { path: path.clone() }]) } - Operation::AddShape { + Operation::AddNgon { path, insert_index, transform, style, sides, } => { - self.set_layer(path, Layer::new(LayerDataType::Shape(Shape::shape(*sides, *style)), *transform), *insert_index)?; + self.set_layer(path, Layer::new(LayerDataType::Shape(Shape::ngon(*sides, *style)), *transform), *insert_index)?; + Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::CreatedLayer { path: path.clone() }]) } - Operation::AddLine { path, insert_index, transform, style } => { - self.set_layer(path, Layer::new(LayerDataType::Shape(Shape::line(*style)), *transform), *insert_index)?; + Operation::AddOverlayShape { path, style, bez_path } => { + let mut shape = Shape::from_bez_path(bez_path.clone(), *style, false); + shape.render_index = -1; + + let mut layer = Layer::new(LayerDataType::Shape(shape), DAffine2::IDENTITY.to_cols_array()); + layer.overlay = true; + + self.set_layer(path, layer, -1)?; + Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::CreatedLayer { path: path.clone() }]) } Operation::AddPen { @@ -395,6 +444,19 @@ impl Document { self.mark_as_dirty(path)?; Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::LayerChanged { path: path.clone() }]) } + Operation::SetShapePathInViewport { path, bez_path, transform } => { + let transform = DAffine2::from_cols_array(transform); + self.set_transform_relative_to_viewport(path, transform)?; + self.mark_as_dirty(path)?; + + match &mut self.layer_mut(path)?.data { + LayerDataType::Shape(shape) => { + shape.path = bez_path.clone(); + } + LayerDataType::Folder(_) => (), + } + Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::LayerChanged { path: path.clone() }]) + } Operation::TransformLayerInScope { path, transform, scope } => { let transform = DAffine2::from_cols_array(transform); let scope = DAffine2::from_cols_array(scope); @@ -416,11 +478,16 @@ impl Document { self.mark_as_dirty(path)?; Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::LayerChanged { path: path.clone() }]) } - Operation::ToggleVisibility { path } => { + Operation::ToggleLayerVisibility { path } => { self.mark_as_dirty(path)?; - if let Ok(layer) = self.layer_mut(path) { - layer.visible = !layer.visible; - } + let layer = self.layer_mut(path)?; + layer.visible = !layer.visible; + Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::LayerChanged { path: path.clone() }]) + } + Operation::SetLayerVisibility { path, visible } => { + self.mark_as_dirty(path)?; + let layer = self.layer_mut(path)?; + layer.visible = *visible; Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::LayerChanged { path: path.clone() }]) } Operation::SetLayerBlendMode { path, blend_mode } => { @@ -435,7 +502,16 @@ impl Document { Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::LayerChanged { path: path.clone() }]) } - Operation::FillLayer { path, color } => { + Operation::SetLayerStyle { path, style } => { + let layer = self.layer_mut(path)?; + match &mut layer.data { + LayerDataType::Shape(s) => s.style = *style, + _ => return Err(DocumentError::NotAShape), + } + self.mark_as_dirty(path)?; + Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::LayerChanged { path: path.clone() }]) + } + Operation::SetLayerFill { path, color } => { let layer = self.layer_mut(path)?; match &mut layer.data { LayerDataType::Shape(s) => s.style.set_fill(layers::style::Fill::new(*color)), diff --git a/graphene/src/layers/simple_shape.rs b/graphene/src/layers/simple_shape.rs index 84946be9f5..cbe6ca62f4 100644 --- a/graphene/src/layers/simple_shape.rs +++ b/graphene/src/layers/simple_shape.rs @@ -77,7 +77,16 @@ impl Shape { transforms.iter().skip(start).cloned().reduce(|a, b| a * b).unwrap_or(DAffine2::IDENTITY) } - pub fn shape(sides: u8, style: PathStyle) -> Self { + pub fn from_bez_path(bez_path: BezPath, style: PathStyle, solid: bool) -> Self { + Self { + path: bez_path, + style, + render_index: 1, + solid: solid, + } + } + + pub fn ngon(sides: u8, style: PathStyle) -> Self { use std::f64::consts::{FRAC_PI_2, TAU}; fn unit_rotation(theta: f64) -> DVec2 { DVec2::new(theta.sin(), theta.cos()) diff --git a/graphene/src/operation.rs b/graphene/src/operation.rs index 9089ee2f19..b067871815 100644 --- a/graphene/src/operation.rs +++ b/graphene/src/operation.rs @@ -20,13 +20,18 @@ pub enum Operation { transform: [f64; 6], style: style::PathStyle, }, + AddOverlayEllipse { + path: Vec, + transform: [f64; 6], + style: style::PathStyle, + }, AddRect { path: Vec, insert_index: isize, transform: [f64; 6], style: style::PathStyle, }, - AddBoundingBox { + AddOverlayRect { path: Vec, transform: [f64; 6], style: style::PathStyle, @@ -37,6 +42,11 @@ pub enum Operation { transform: [f64; 6], style: style::PathStyle, }, + AddOverlayLine { + path: Vec, + transform: [f64; 6], + style: style::PathStyle, + }, AddPen { path: Vec, transform: [f64; 6], @@ -44,13 +54,18 @@ pub enum Operation { points: Vec<(f64, f64)>, style: style::PathStyle, }, - AddShape { + AddNgon { path: Vec, insert_index: isize, transform: [f64; 6], sides: u8, style: style::PathStyle, }, + AddOverlayShape { + path: Vec, + bez_path: kurbo::BezPath, + style: style::PathStyle, + }, DeleteLayer { path: Vec, }, @@ -81,6 +96,11 @@ pub enum Operation { path: Vec, transform: [f64; 6], }, + SetShapePathInViewport { + path: Vec, + bez_path: kurbo::BezPath, + transform: [f64; 6], + }, TransformLayerInScope { path: Vec, transform: [f64; 6], @@ -95,9 +115,13 @@ pub enum Operation { path: Vec, transform: [f64; 6], }, - ToggleVisibility { + ToggleLayerVisibility { path: Vec, }, + SetLayerVisibility { + path: Vec, + visible: bool, + }, SetLayerBlendMode { path: Vec, blend_mode: BlendMode, @@ -106,7 +130,11 @@ pub enum Operation { path: Vec, opacity: f64, }, - FillLayer { + SetLayerStyle { + path: Vec, + style: style::PathStyle, + }, + SetLayerFill { path: Vec, color: Color, },