diff --git a/client/web/wasm/src/wrappers.rs b/client/web/wasm/src/wrappers.rs index e018bb78..332fcd1a 100644 --- a/client/web/wasm/src/wrappers.rs +++ b/client/web/wasm/src/wrappers.rs @@ -82,6 +82,7 @@ pub fn translate_key(name: &str) -> events::Key { "r" => K::KeyR, "m" => K::KeyM, "x" => K::KeyX, + "z" => K::KeyZ, "0" => K::Key0, "1" => K::Key1, "2" => K::Key2, diff --git a/core/document/src/lib.rs b/core/document/src/lib.rs index 77e49525..d3093baf 100644 --- a/core/document/src/lib.rs +++ b/core/document/src/lib.rs @@ -4,14 +4,16 @@ pub use kurbo::{Circle, Point, Rect}; pub use operation::Operation; #[derive(Debug, Clone, PartialEq)] -pub enum SvgElement { +pub enum LayerType { + Folder(Folder), Circle(Circle), Rect(Rect), } -impl SvgElement { +impl LayerType { pub fn render(&self) -> String { match self { + Self::Folder(f) => f.render(), Self::Circle(c) => { format!(r#""#, c.center.x, c.center.y, c.radius) } @@ -22,31 +24,200 @@ impl SvgElement { } } -#[derive(Default, Debug, Clone, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum DocumentError { + LayerNotFound, + InvalidPath, + IndexOutOfBounds, +} + +type LayerId = u64; + +#[derive(Debug, Clone, PartialEq)] +pub struct Layer { + visible: bool, + name: Option, + data: LayerType, +} + +impl Layer { + pub fn new(data: LayerType) -> Self { + Self { visible: true, name: None, data } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Folder { + next_assignment_id: LayerId, + layer_ids: Vec, + layers: Vec, +} + +impl Folder { + pub fn render(&self) -> String { + self.layers + .iter() + .filter(|layer| layer.visible) + .map(|layer| layer.data.render()) + .fold(String::with_capacity(self.layers.len() * 30), |s, n| s + "\n" + &n) + } + + fn add_layer(&mut self, layer: Layer, insert_index: isize) -> Option { + let mut insert_index = insert_index as i128; + if insert_index < 0 { + insert_index = self.layers.len() as i128 + insert_index as i128 + 1; + } + + if insert_index <= self.layers.len() as i128 && insert_index >= 0 { + self.layers.insert(insert_index as usize, layer); + self.layer_ids.insert(insert_index as usize, self.next_assignment_id); + self.next_assignment_id += 1; + Some(self.next_assignment_id - 1) + } else { + None + } + } + + fn remove_layer(&mut self, id: LayerId) -> Result<(), DocumentError> { + let pos = self.layer_ids.iter().position(|x| *x == id).ok_or(DocumentError::LayerNotFound)?; + self.layers.remove(pos); + self.layer_ids.remove(pos); + Ok(()) + } + + /// Returns a list of layers in the folder + pub fn list_layers(&self) -> &[LayerId] { + self.layer_ids.as_slice() + } + + fn layer(&self, id: LayerId) -> Option<&Layer> { + let pos = self.layer_ids.iter().position(|x| *x == id)?; + Some(&self.layers[pos]) + } + + fn layer_mut(&mut self, id: LayerId) -> Option<&mut Layer> { + let pos = self.layer_ids.iter().position(|x| *x == id)?; + Some(&mut self.layers[pos]) + } + + fn folder(&self, id: LayerId) -> Option<&Folder> { + match self.layer(id) { + Some(Layer { data: LayerType::Folder(folder), .. }) => Some(&folder), + _ => None, + } + } + + fn folder_mut(&mut self, id: LayerId) -> Option<&mut Folder> { + match self.layer_mut(id) { + Some(Layer { data: LayerType::Folder(folder), .. }) => Some(folder), + _ => None, + } + } +} + +impl Default for Folder { + fn default() -> Self { + Self { + layer_ids: vec![], + layers: vec![], + next_assignment_id: 0, + } + } +} + +#[derive(Debug, Clone, PartialEq)] pub struct Document { - pub svg: Vec, + pub root: Folder, +} + +impl Default for Document { + fn default() -> Self { + Self { root: Folder::default() } + } +} + +fn split_path(path: &[LayerId]) -> Result<(&[LayerId], LayerId), DocumentError> { + let id = path.last().ok_or(DocumentError::InvalidPath)?; + let folder_path = &path[0..path.len() - 1]; + Ok((folder_path, *id)) } impl Document { pub fn render(&self) -> String { - self.svg.iter().map(|element| element.render()).collect::>().join("\n") + self.root.render() } - pub fn handle_operation(&mut self, operation: &Operation, update_frontend: F) { - match *operation { - Operation::AddCircle { cx, cy, r } => { - self.svg.push(SvgElement::Circle(Circle { - center: Point { x: cx, y: cy }, - radius: r, - })); + pub fn folder(&self, path: &[LayerId]) -> Result<&Folder, DocumentError> { + let mut root = &self.root; + for id in path { + root = root.folder(*id).ok_or(DocumentError::LayerNotFound)?; + } + Ok(root) + } - update_frontend(self.render()); - } - Operation::AddRect { x0, y0, x1, y1 } => { - self.svg.push(SvgElement::Rect(Rect::from_points(Point::new(x0, y0), Point::new(x1, y1)))); + pub fn folder_mut(&mut self, path: &[LayerId]) -> Result<&mut Folder, DocumentError> { + let mut root = &mut self.root; + for id in path { + root = root.folder_mut(*id).ok_or(DocumentError::LayerNotFound)?; + } + Ok(root) + } - update_frontend(self.render()); + pub fn layer(&self, path: &[LayerId]) -> Result<&Layer, DocumentError> { + let (path, id) = split_path(path)?; + self.folder(path)?.layer(id).ok_or(DocumentError::LayerNotFound) + } + + pub fn layer_mut(&mut self, path: &[LayerId]) -> Result<&mut Layer, DocumentError> { + let (path, id) = split_path(path)?; + self.folder_mut(path)?.layer_mut(id).ok_or(DocumentError::LayerNotFound) + } + + pub fn set_layer(&mut self, path: &[LayerId], layer: Layer) -> Result<(), DocumentError> { + let mut folder = &mut self.root; + if let Ok((path, id)) = split_path(path) { + folder = self.folder_mut(path)?; + if let Some(folder_layer) = folder.layer_mut(id) { + *folder_layer = layer; + return Ok(()); } } + folder.add_layer(layer, -1).ok_or(DocumentError::IndexOutOfBounds)?; + Ok(()) + } + + /// Passing a negative `insert_index` indexes relative to the end + /// -1 is equivalent to adding the layer to the top + pub fn add_layer(&mut self, path: &[LayerId], layer: Layer, insert_index: isize) -> Result { + let folder = self.folder_mut(path)?; + folder.add_layer(layer, insert_index).ok_or(DocumentError::IndexOutOfBounds) + } + + pub fn delete(&mut self, path: &[LayerId]) -> Result<(), DocumentError> { + let (path, id) = split_path(path)?; + self.folder_mut(path)?.remove_layer(id)?; + Ok(()) + } + + pub fn handle_operation(&mut self, operation: Operation, update_frontend: F) -> Result<(), DocumentError> { + match operation { + Operation::AddCircle { path, insert_index, cx, cy, r } => { + self.add_layer(&path, Layer::new(LayerType::Circle(Circle::new(Point::new(cx, cy), r))), insert_index)?; + + update_frontend(self.render()); + } + Operation::AddRect { path, insert_index, x0, y0, x1, y1 } => { + self.add_layer(&path, Layer::new(LayerType::Rect(Rect::from_points(Point::new(x0, y0), Point::new(x1, y1)))), insert_index)?; + + update_frontend(self.render()); + } + Operation::DeleteLayer { path } => { + self.delete(&path)?; + + update_frontend(self.render()); + } + Operation::AddFolder { path } => self.set_layer(&path, Layer::new(LayerType::Folder(Folder::default())))?, + } + Ok(()) } } diff --git a/core/document/src/operation.rs b/core/document/src/operation.rs index 08b5eb23..df1d1fce 100644 --- a/core/document/src/operation.rs +++ b/core/document/src/operation.rs @@ -1,4 +1,25 @@ +use crate::LayerId; + pub enum Operation { - AddCircle { cx: f64, cy: f64, r: f64 }, - AddRect { x0: f64, y0: f64, x1: f64, y1: f64 }, + AddCircle { + path: Vec, + insert_index: isize, + cx: f64, + cy: f64, + r: f64, + }, + AddRect { + path: Vec, + insert_index: isize, + x0: f64, + y0: f64, + x1: f64, + y1: f64, + }, + DeleteLayer { + path: Vec, + }, + AddFolder { + path: Vec, + }, } diff --git a/core/editor/src/dispatcher/events.rs b/core/editor/src/dispatcher/events.rs index 764cf79c..fb6501c6 100644 --- a/core/editor/src/dispatcher/events.rs +++ b/core/editor/src/dispatcher/events.rs @@ -113,6 +113,7 @@ pub enum Key { KeyE, KeyV, KeyX, + KeyZ, Key0, Key1, Key2, diff --git a/core/editor/src/dispatcher/mod.rs b/core/editor/src/dispatcher/mod.rs index 0c430d7f..e342db17 100644 --- a/core/editor/src/dispatcher/mod.rs +++ b/core/editor/src/dispatcher/mod.rs @@ -85,28 +85,28 @@ impl Dispatcher { let (responses, operations) = editor_state.tool_state.active_tool()?.handle_input(event, &editor_state.document); - self.dispatch_operations(&mut editor_state.document, &operations); + self.dispatch_operations(&mut editor_state.document, operations); // TODO - Dispatch Responses Ok(()) } - fn dispatch_operations(&self, document: &mut Document, operations: &[Operation]) { + fn dispatch_operations>(&self, document: &mut Document, operations: I) { for operation in operations { - self.dispatch_operation(document, operation); + if let Err(error) = self.dispatch_operation(document, operation) { + log::error!("{}", error); + } } } - fn dispatch_operation(&self, document: &mut Document, operation: &Operation) { - document.handle_operation(operation, |svg: String| { - self.dispatch_response(Response::UpdateCanvas { document: svg }); - }); + fn dispatch_operation(&self, document: &mut Document, operation: Operation) -> Result<(), EditorError> { + document.handle_operation(operation, |svg: String| self.dispatch_response(Response::UpdateCanvas { document: svg }))?; + Ok(()) } - pub fn dispatch_responses(&self, responses: &[Response]) { + pub fn dispatch_responses>(&self, responses: I) { for response in responses { - // TODO - Remove clone when Response is Copy - self.dispatch_response(response.clone()); + self.dispatch_response(response); } } diff --git a/core/editor/src/error.rs b/core/editor/src/error.rs index 5b355fb9..acf55b62 100644 --- a/core/editor/src/error.rs +++ b/core/editor/src/error.rs @@ -1,5 +1,6 @@ use crate::events::Event; use crate::Color; +use document_core::DocumentError; use thiserror::Error; /// The error type used by the Graphite editor. @@ -15,6 +16,8 @@ pub enum EditorError { Color(String), #[error("The requested tool does not exist")] UnknownTool, + #[error("The operation caused a document error {0:?}")] + Document(String), } macro_rules! derive_from { @@ -31,3 +34,4 @@ derive_from!(&str, Misc); derive_from!(String, Misc); derive_from!(Color, Color); derive_from!(Event, InvalidEvent); +derive_from!(DocumentError, Document); diff --git a/core/editor/src/tools/ellipse.rs b/core/editor/src/tools/ellipse.rs index 915d3370..dab1fb8d 100644 --- a/core/editor/src/tools/ellipse.rs +++ b/core/editor/src/tools/ellipse.rs @@ -1,5 +1,5 @@ use crate::events::{Event, Response}; -use crate::events::{MouseKeys, ViewportPosition}; +use crate::events::{Key, MouseKeys, ViewportPosition}; use crate::tools::{Fsm, Tool}; use crate::Document; use document_core::Operation; @@ -45,12 +45,20 @@ impl Fsm for EllipseToolFsmState { data.drag_start = mouse_state.position; EllipseToolFsmState::LmbDown } + (EllipseToolFsmState::Ready, Event::KeyDown(Key::KeyZ)) => { + if let Some(id) = document.root.list_layers().last() { + operations.push(Operation::DeleteLayer { path: vec![*id] }) + } + EllipseToolFsmState::Ready + } // TODO - Check for left mouse button (EllipseToolFsmState::LmbDown, Event::MouseUp(mouse_state)) => { let r = data.drag_start.distance(&mouse_state.position); log::info!("draw ellipse with radius: {:.2}", r); operations.push(Operation::AddCircle { + path: vec![], + insert_index: -1, cx: data.drag_start.x as f64, cy: data.drag_start.y as f64, r: data.drag_start.distance(&mouse_state.position), diff --git a/core/editor/src/tools/rectangle.rs b/core/editor/src/tools/rectangle.rs index 679030b0..cd27e3db 100644 --- a/core/editor/src/tools/rectangle.rs +++ b/core/editor/src/tools/rectangle.rs @@ -1,5 +1,5 @@ use crate::events::{Event, Response}; -use crate::events::{MouseKeys, ViewportPosition}; +use crate::events::{Key, MouseKeys, ViewportPosition}; use crate::tools::{Fsm, Tool}; use crate::Document; use document_core::Operation; @@ -45,6 +45,12 @@ impl Fsm for RectangleToolFsmState { data.drag_start = mouse_state.position; RectangleToolFsmState::LmbDown } + (RectangleToolFsmState::Ready, Event::KeyDown(Key::KeyZ)) => { + if let Some(id) = document.root.list_layers().last() { + operations.push(Operation::DeleteLayer { path: vec![*id] }) + } + RectangleToolFsmState::Ready + } // TODO - Check for left mouse button (RectangleToolFsmState::LmbDown, Event::MouseUp(mouse_state)) => { @@ -53,6 +59,8 @@ impl Fsm for RectangleToolFsmState { let start = data.drag_start; let end = mouse_state.position; operations.push(Operation::AddRect { + path: vec![], + insert_index: -1, x0: start.x as f64, y0: start.y as f64, x1: end.x as f64,