diff --git a/Cargo.lock b/Cargo.lock index e6a513ba01..74e2147412 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -42,6 +42,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "glam" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4126c0479ccf7e8664c36a2d719f5f2c140fbb4f9090008098d2c291fa5b3f16" + [[package]] name = "graphite-cli" version = "0.1.0" @@ -50,6 +56,7 @@ version = "0.1.0" name = "graphite-document-core" version = "0.1.0" dependencies = [ + "glam", "kurbo", "log", "serde", @@ -60,6 +67,7 @@ name = "graphite-editor-core" version = "0.1.0" dependencies = [ "bitflags", + "glam", "graphite-document-core", "graphite-proc-macros", "log", @@ -111,8 +119,7 @@ dependencies = [ [[package]] name = "kurbo" version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e30b1df631d23875f230ed3ddd1a88c231f269a04b2044eb6ca87e763b5f4c42" +source = "git+https://github.com/GraphiteEditor/kurbo.git?branch=bezpath-partial-eq#3f86bda9ba77d3b1279612bc5779e425f2d8714d" dependencies = [ "arrayvec", ] diff --git a/core/document/Cargo.toml b/core/document/Cargo.toml index 74f5e1ecd5..0f7882eab1 100644 --- a/core/document/Cargo.toml +++ b/core/document/Cargo.toml @@ -10,5 +10,8 @@ license = "Apache-2.0" [dependencies] log = "0.4" -kurbo = "0.8.0" +# TODO: Swich to kurbo release when derive `PartialEq` is available for BezPath +#kurbo = "0.8.0" +kurbo = {git="https://github.com/GraphiteEditor/kurbo.git", branch="bezpath-partial-eq"} serde = { version = "1.0", features = ["derive"] } +glam = "0.16" diff --git a/core/document/src/document.rs b/core/document/src/document.rs index 7dc43e48fb..5ccbf6f6b9 100644 --- a/core/document/src/document.rs +++ b/core/document/src/document.rs @@ -1,12 +1,14 @@ +use glam::DAffine2; + use crate::{ - layers::{self, Folder, Layer, LayerData, LayerDataTypes, Line, PolyLine, Rect, Shape}, + layers::{self, style::PathStyle, Folder, Layer, LayerDataTypes, Line, PolyLine, Rect, Shape}, DocumentError, DocumentResponse, LayerId, Operation, }; #[derive(Debug, Clone, PartialEq)] pub struct Document { - pub root: layers::Folder, - pub work: Folder, + pub root: Layer, + pub work: Layer, pub work_mount_path: Vec, pub work_operations: Vec, pub work_mounted: bool, @@ -15,8 +17,8 @@ pub struct Document { impl Default for Document { fn default() -> Self { Self { - root: Folder::default(), - work: Folder::default(), + root: Layer::new(LayerDataTypes::Folder(Folder::default()), DAffine2::IDENTITY.to_cols_array(), PathStyle::default()), + work: Layer::new(LayerDataTypes::Folder(Folder::default()), DAffine2::IDENTITY.to_cols_array(), PathStyle::default()), work_mount_path: Vec::new(), work_operations: Vec::new(), work_mounted: false, @@ -41,8 +43,11 @@ impl Document { return; } if path.as_slice() == self.work_mount_path { - self.document_folder_mut(path).unwrap().render(svg); - self.work.render(svg); + // TODO: Handle if mounted in nested folders + let transform = self.document_folder(path).unwrap().transform; + self.document_folder_mut(path).unwrap().render_as_folder(svg); + self.work.transform = transform; + self.work.render_as_folder(svg); path.pop(); } let ids = self.folder(path).unwrap().layer_ids.clone(); @@ -68,10 +73,10 @@ impl Document { /// This function respects mounted folders and will thus not contain the layers already /// present in the document if a temporary folder is mounted on top. pub fn folder(&self, mut path: &[LayerId]) -> Result<&Folder, DocumentError> { - let mut root = &self.root; + let mut root = self.root.as_folder()?; if self.is_mounted(self.work_mount_path.as_slice(), path) { path = &path[self.work_mount_path.len()..]; - root = &self.work; + root = self.work.as_folder()?; } for id in path { root = root.folder(*id).ok_or(DocumentError::LayerNotFound)?; @@ -87,9 +92,9 @@ impl Document { pub fn folder_mut(&mut self, mut path: &[LayerId]) -> Result<&mut Folder, DocumentError> { let mut root = if self.is_mounted(self.work_mount_path.as_slice(), path) { path = &path[self.work_mount_path.len()..]; - &mut self.work + self.work.as_folder_mut()? } else { - &mut self.root + self.root.as_folder_mut()? }; for id in path { root = root.folder_mut(*id).ok_or(DocumentError::LayerNotFound)?; @@ -101,10 +106,10 @@ impl Document { /// or if the requested layer is not of type folder. /// This function does **not** respect mounted folders and will always return the current /// state of the document, disregarding any temporary modifications. - pub fn document_folder(&self, path: &[LayerId]) -> Result<&Folder, DocumentError> { + pub fn document_folder(&self, path: &[LayerId]) -> Result<&Layer, DocumentError> { let mut root = &self.root; for id in path { - root = root.folder(*id).ok_or(DocumentError::LayerNotFound)?; + root = root.as_folder()?.layer(*id).ok_or(DocumentError::LayerNotFound)?; } Ok(root) } @@ -114,10 +119,10 @@ impl Document { /// This function does **not** respect mounted folders and will always return the current /// state of the document, disregarding any temporary modifications. /// If you manually edit the folder you have to set the cache_dirty flag yourself. - pub fn document_folder_mut(&mut self, path: &[LayerId]) -> Result<&mut Folder, DocumentError> { + pub fn document_folder_mut(&mut self, path: &[LayerId]) -> Result<&mut Layer, DocumentError> { let mut root = &mut self.root; for id in path { - root = root.folder_mut(*id).ok_or(DocumentError::LayerNotFound)?; + root = root.as_folder_mut()?.layer_mut(*id).ok_or(DocumentError::LayerNotFound)?; } Ok(root) } @@ -137,7 +142,7 @@ impl Document { /// Replaces the layer at the specified `path` with `layer`. pub fn set_layer(&mut self, path: &[LayerId], layer: Layer) -> Result<(), DocumentError> { - let mut folder = &mut self.root; + let mut folder = self.root.as_folder_mut()?; if let Ok((path, id)) = split_path(path) { self.layer_mut(path)?.cache_dirty = true; folder = self.folder_mut(path)?; @@ -163,7 +168,7 @@ impl Document { pub fn delete(&mut self, path: &[LayerId]) -> Result<(), DocumentError> { let (path, id) = split_path(path)?; let _ = self.layer_mut(path).map(|x| x.cache_dirty = true); - self.document_folder_mut(path)?.remove_layer(id)?; + self.document_folder_mut(path)?.as_folder_mut()?.remove_layer(id)?; Ok(()) } @@ -171,73 +176,46 @@ impl Document { /// reaction from the frontend, responses may be returned. pub fn handle_operation(&mut self, operation: Operation) -> Result>, DocumentError> { let responses = match &operation { - Operation::AddCircle { path, insert_index, cx, cy, r, style } => { - let id = self.add_layer(&path, Layer::new(LayerDataTypes::Circle(layers::Circle::new((*cx, *cy), *r, *style))), *insert_index)?; + Operation::AddEllipse { path, insert_index, transform, style } => { + let id = self.add_layer(&path, Layer::new(LayerDataTypes::Ellipse(layers::Ellipse::new()), *transform, *style), *insert_index)?; let path = [path.clone(), vec![id]].concat(); Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::SelectLayer { path }]) } - Operation::AddEllipse { - path, - insert_index, - cx, - cy, - rx, - ry, - rot, - style, - } => { - let id = self.add_layer(&path, Layer::new(LayerDataTypes::Ellipse(layers::Ellipse::new((*cx, *cy), (*rx, *ry), *rot, *style))), *insert_index)?; + Operation::AddRect { path, insert_index, transform, style } => { + let id = self.add_layer(&path, Layer::new(LayerDataTypes::Rect(Rect::new()), *transform, *style), *insert_index)?; let path = [path.clone(), vec![id]].concat(); Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::SelectLayer { path }]) } - Operation::AddRect { - path, - insert_index, - x0, - y0, - x1, - y1, - style, - } => { - let id = self.add_layer(&path, Layer::new(LayerDataTypes::Rect(Rect::new((*x0, *y0), (*x1, *y1), *style))), *insert_index)?; + Operation::AddLine { path, insert_index, transform, style } => { + let id = self.add_layer(&path, Layer::new(LayerDataTypes::Line(Line::new()), *transform, *style), *insert_index)?; let path = [path.clone(), vec![id]].concat(); Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::SelectLayer { path }]) } - Operation::AddLine { + Operation::AddPen { path, insert_index, - x0, - y0, - x1, - y1, + points, + transform, style, } => { - let id = self.add_layer(&path, Layer::new(LayerDataTypes::Line(Line::new((*x0, *y0), (*x1, *y1), *style))), *insert_index)?; - let path = [path.clone(), vec![id]].concat(); - - Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::SelectLayer { path }]) - } - Operation::AddPen { path, insert_index, points, style } => { - let points: Vec = points.iter().map(|&it| it.into()).collect(); - let polyline = PolyLine::new(points, *style); - self.add_layer(&path, Layer::new(LayerDataTypes::PolyLine(polyline)), *insert_index)?; + let points: Vec = points.iter().map(|&it| it.into()).collect(); + let polyline = PolyLine::new(points); + self.add_layer(&path, Layer::new(LayerDataTypes::PolyLine(polyline), *transform, *style), *insert_index)?; Some(vec![DocumentResponse::DocumentChanged]) } Operation::AddShape { path, insert_index, - x0, - y0, - x1, - y1, + transform, + equal_sides, sides, style, } => { - let s = Shape::new((*x0, *y0), (*x1, *y1), *sides, *style); - let id = self.add_layer(&path, Layer::new(LayerDataTypes::Shape(s)), *insert_index)?; + let s = Shape::new(*equal_sides, *sides); + let id = self.add_layer(&path, Layer::new(LayerDataTypes::Shape(s), *transform, *style), *insert_index)?; let path = [path.clone(), vec![id]].concat(); Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::SelectLayer { path }]) @@ -256,27 +234,34 @@ impl Document { Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::FolderChanged { path: folder_path.to_vec() }]) } Operation::AddFolder { path } => { - self.set_layer(&path, Layer::new(LayerDataTypes::Folder(Folder::default())))?; + self.set_layer(&path, Layer::new(LayerDataTypes::Folder(Folder::default()), DAffine2::IDENTITY.to_cols_array(), PathStyle::default()))?; Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::FolderChanged { path: path.clone() }]) } Operation::MountWorkingFolder { path } => { self.work_mount_path = path.clone(); self.work_operations.clear(); - self.work = Folder::default(); + self.work = Layer::new(LayerDataTypes::Folder(Folder::default()), DAffine2::IDENTITY.to_cols_array(), PathStyle::default()); self.work_mounted = true; None } + Operation::TransformLayer { path, transform } => { + let transform = self.root.transform * DAffine2::from_cols_array(&transform); + let layer = self.document_folder_mut(path).unwrap(); + layer.transform = transform; + layer.cache_dirty = true; + Some(vec![DocumentResponse::DocumentChanged]) + } Operation::DiscardWorkingFolder => { self.work_operations.clear(); self.work_mount_path = vec![]; - self.work = Folder::default(); + self.work = Layer::new(LayerDataTypes::Folder(Folder::default()), DAffine2::IDENTITY.to_cols_array(), PathStyle::default()); self.work_mounted = false; Some(vec![DocumentResponse::DocumentChanged]) } Operation::ClearWorkingFolder => { self.work_operations.clear(); - self.work = Folder::default(); + self.work = Layer::new(LayerDataTypes::Folder(Folder::default()), DAffine2::IDENTITY.to_cols_array(), PathStyle::default()); Some(vec![DocumentResponse::DocumentChanged]) } Operation::CommitTransaction => { @@ -286,7 +271,7 @@ impl Document { std::mem::swap(&mut ops, &mut self.work_operations); self.work_mounted = false; self.work_mount_path = vec![]; - self.work = Folder::default(); + self.work = Layer::new(LayerDataTypes::Folder(Folder::default()), DAffine2::IDENTITY.to_cols_array(), PathStyle::default()); let mut responses = vec![]; for operation in ops.into_iter() { if let Some(mut op_responses) = self.handle_operation(operation)? { diff --git a/core/document/src/layers/circle.rs b/core/document/src/layers/circle.rs deleted file mode 100644 index ad9f066f2a..0000000000 --- a/core/document/src/layers/circle.rs +++ /dev/null @@ -1,32 +0,0 @@ -use super::style; -use super::LayerData; - -use std::fmt::Write; - -#[derive(Debug, Clone, Copy, PartialEq, Default)] -pub struct Circle { - shape: kurbo::Circle, - style: style::PathStyle, -} - -impl Circle { - pub fn new(center: impl Into, radius: f64, style: style::PathStyle) -> Circle { - Circle { - shape: kurbo::Circle::new(center, radius), - style, - } - } -} - -impl LayerData for Circle { - fn render(&mut self, svg: &mut String) { - let _ = write!( - svg, - r#""#, - self.shape.center.x, - self.shape.center.y, - self.shape.radius, - self.style.render(), - ); - } -} diff --git a/core/document/src/layers/ellipse.rs b/core/document/src/layers/ellipse.rs index fe22124f50..56a07157c0 100644 --- a/core/document/src/layers/ellipse.rs +++ b/core/document/src/layers/ellipse.rs @@ -1,37 +1,24 @@ +use kurbo::Shape; + use super::style; use super::LayerData; use std::fmt::Write; #[derive(Debug, Clone, Copy, PartialEq, Default)] -pub struct Ellipse { - shape: kurbo::Ellipse, - style: style::PathStyle, -} +pub struct Ellipse {} impl Ellipse { - pub fn new(center: impl Into, radii: impl Into, rotation: f64, style: style::PathStyle) -> Ellipse { - Ellipse { - shape: kurbo::Ellipse::new(center, radii, rotation), - style, - } + pub fn new() -> Ellipse { + Ellipse {} } } impl LayerData for Ellipse { - fn render(&mut self, svg: &mut String) { - let kurbo::Vec2 { x: rx, y: ry } = self.shape.radii(); - let kurbo::Point { x: cx, y: cy } = self.shape.center(); - - let _ = write!( - svg, - r#""#, - rx, - ry, - cx, - cy, - self.shape.rotation().to_degrees(), - self.style.render(), - ); + fn to_kurbo_path(&mut self, transform: glam::DAffine2, _style: style::PathStyle) -> kurbo::BezPath { + kurbo::Ellipse::from_affine(kurbo::Affine::new(transform.to_cols_array())).to_path(0.1) + } + fn render(&mut self, svg: &mut String, transform: glam::DAffine2, style: style::PathStyle) { + let _ = write!(svg, r#""#, self.to_kurbo_path(transform, style).to_svg(), style.render()); } } diff --git a/core/document/src/layers/folder.rs b/core/document/src/layers/folder.rs index 71896e2ca4..bafff20fd2 100644 --- a/core/document/src/layers/folder.rs +++ b/core/document/src/layers/folder.rs @@ -1,6 +1,6 @@ use crate::{DocumentError, LayerId}; -use super::{Layer, LayerData, LayerDataTypes}; +use super::{style, Layer, LayerData, LayerDataTypes}; use std::fmt::Write; @@ -12,10 +12,21 @@ pub struct Folder { } impl LayerData for Folder { - fn render(&mut self, svg: &mut String) { + fn render(&mut self, svg: &mut String, transform: glam::DAffine2, _style: style::PathStyle) { + let _ = writeln!(svg, r#""#); + for layer in &mut self.layers { let _ = writeln!(svg, "{}", layer.render()); } + let _ = writeln!(svg, ""); + } + + fn to_kurbo_path(&mut self, _: glam::DAffine2, _: style::PathStyle) -> kurbo::BezPath { + unimplemented!() } } diff --git a/core/document/src/layers/line.rs b/core/document/src/layers/line.rs index e4b62aeabf..7595380fce 100644 --- a/core/document/src/layers/line.rs +++ b/core/document/src/layers/line.rs @@ -1,28 +1,34 @@ +use glam::DVec2; +use kurbo::Point; + use super::style; use super::LayerData; use std::fmt::Write; #[derive(Debug, Clone, Copy, PartialEq)] -pub struct Line { - shape: kurbo::Line, - style: style::PathStyle, -} +pub struct Line {} impl Line { - pub fn new(p0: impl Into, p1: impl Into, style: style::PathStyle) -> Line { - Line { - shape: kurbo::Line::new(p0, p1), - style, - } + pub fn new() -> Line { + Line {} } } impl LayerData for Line { - fn render(&mut self, svg: &mut String) { - let kurbo::Point { x: x1, y: y1 } = self.shape.p0; - let kurbo::Point { x: x2, y: y2 } = self.shape.p1; + fn to_kurbo_path(&mut self, transform: glam::DAffine2, _style: style::PathStyle) -> kurbo::BezPath { + fn new_point(a: DVec2) -> Point { + Point::new(a.x, a.y) + } + let mut path = kurbo::BezPath::new(); + path.move_to(new_point(transform.translation)); + path.line_to(new_point(transform.transform_point2(DVec2::ONE))); + path + } + fn render(&mut self, svg: &mut String, transform: glam::DAffine2, style: style::PathStyle) { + let [x1, y1] = transform.translation.to_array(); + let [x2, y2] = transform.transform_point2(DVec2::ONE).to_array(); - let _ = write!(svg, r#""#, x1, y1, x2, y2, self.style.render(),); + let _ = write!(svg, r#""#, x1, y1, x2, y2, style.render(),); } } diff --git a/core/document/src/layers/mod.rs b/core/document/src/layers/mod.rs index e107249da1..2ee4ad8c10 100644 --- a/core/document/src/layers/mod.rs +++ b/core/document/src/layers/mod.rs @@ -1,12 +1,10 @@ pub mod style; -pub mod circle; -pub use circle::Circle; - pub mod ellipse; pub use ellipse::Ellipse; pub mod line; +use kurbo::BezPath; pub use line::Line; pub mod rect; @@ -21,14 +19,16 @@ pub use shape::Shape; pub mod folder; pub use folder::Folder; +use crate::DocumentError; + pub trait LayerData { - fn render(&mut self, svg: &mut String); + fn render(&mut self, svg: &mut String, transform: glam::DAffine2, style: style::PathStyle); + fn to_kurbo_path(&mut self, transform: glam::DAffine2, style: style::PathStyle) -> BezPath; } #[derive(Debug, Clone, PartialEq)] pub enum LayerDataTypes { Folder(Folder), - Circle(Circle), Ellipse(Ellipse), Rect(Rect), Line(Line), @@ -37,19 +37,36 @@ pub enum LayerDataTypes { } macro_rules! call_render { - ($self:ident.render($svg:ident) { $($variant:ident),* }) => { + ($self:ident.render($svg:ident, $transform:ident, $style:ident) { $($variant:ident),* }) => { match $self { - $(Self::$variant(x) => x.render($svg)),* + $(Self::$variant(x) => x.render($svg, $transform, $style)),* + } + }; +} +macro_rules! call_kurbo_path { + ($self:ident.to_kurbo_path($transform:ident, $style:ident) { $($variant:ident),* }) => { + match $self { + $(Self::$variant(x) => x.to_kurbo_path($transform, $style)),* } }; } - impl LayerDataTypes { - pub fn render(&mut self, svg: &mut String) { + pub fn render(&mut self, svg: &mut String, transform: glam::DAffine2, style: style::PathStyle) { call_render! { - self.render(svg) { + self.render(svg, transform, style) { + Folder, + Ellipse, + Rect, + Line, + PolyLine, + Shape + } + } + } + pub fn to_kurbo_path(&mut self, transform: glam::DAffine2, style: style::PathStyle) -> BezPath { + call_kurbo_path! { + self.to_kurbo_path(transform, style) { Folder, - Circle, Ellipse, Rect, Line, @@ -65,16 +82,20 @@ pub struct Layer { pub visible: bool, pub name: Option, pub data: LayerDataTypes, + pub transform: glam::DAffine2, + pub style: style::PathStyle, pub cache: String, pub cache_dirty: bool, } impl Layer { - pub fn new(data: LayerDataTypes) -> Self { + pub fn new(data: LayerDataTypes, transform: [f64; 6], style: style::PathStyle) -> Self { Self { visible: true, name: None, data, + transform: glam::DAffine2::from_cols_array(&transform), + style: style, cache: String::new(), cache_dirty: true, } @@ -86,9 +107,36 @@ impl Layer { } if self.cache_dirty { self.cache.clear(); - self.data.render(&mut self.cache); + self.data.render(&mut self.cache, self.transform, self.style); self.cache_dirty = false; } self.cache.as_str() } + + pub fn render_on(&mut self, svg: &mut String) { + *svg += self.render(); + } + + pub fn to_kurbo_path(&mut self) -> BezPath { + self.data.to_kurbo_path(self.transform, self.style) + } + pub fn as_folder_mut(&mut self) -> Result<&mut Folder, DocumentError> { + match &mut self.data { + LayerDataTypes::Folder(f) => Ok(f), + _ => Err(DocumentError::NotAFolder), + } + } + pub fn as_folder(&self) -> Result<&Folder, DocumentError> { + match &self.data { + LayerDataTypes::Folder(f) => Ok(&f), + _ => Err(DocumentError::NotAFolder), + } + } + + pub fn render_as_folder(&mut self, svg: &mut String) { + match &mut self.data { + LayerDataTypes::Folder(f) => f.render(svg, self.transform, self.style), + _ => {} + } + } } diff --git a/core/document/src/layers/polyline.rs b/core/document/src/layers/polyline.rs index 38e4364581..4892e0320f 100644 --- a/core/document/src/layers/polyline.rs +++ b/core/document/src/layers/polyline.rs @@ -1,48 +1,56 @@ -use super::style; -use super::LayerData; - use std::fmt::Write; +use super::{style, LayerData}; + #[derive(Debug, Clone, PartialEq)] pub struct PolyLine { - points: Vec, - style: style::PathStyle, + points: Vec, } impl PolyLine { - pub fn new(points: Vec>, style: style::PathStyle) -> PolyLine { + pub fn new(points: Vec>) -> PolyLine { PolyLine { points: points.into_iter().map(|it| it.into()).collect(), - style, } } } impl LayerData for PolyLine { - fn render(&mut self, svg: &mut String) { + fn to_kurbo_path(&mut self, transform: glam::DAffine2, _style: style::PathStyle) -> kurbo::BezPath { + let mut path = kurbo::BezPath::new(); + self.points + .iter() + .map(|v| transform.transform_point2(*v)) + .map(|v| kurbo::Point { x: v.x, y: v.y }) + .enumerate() + .for_each(|(i, p)| if i == 0 { path.move_to(p) } else { path.line_to(p) }); + path + } + fn render(&mut self, svg: &mut String, transform: glam::DAffine2, style: style::PathStyle) { if self.points.is_empty() { return; } let _ = write!(svg, r#""#, self.style.render()); + let _ = write!(svg, r#""{} />"#, style.render()); } } #[cfg(test)] #[test] fn polyline_should_render() { + use super::style::PathStyle; + use glam::DVec2; let mut polyline = PolyLine { - points: vec![kurbo::Point::new(3.0, 4.12354), kurbo::Point::new(1.0, 5.54)], - style: style::PathStyle::new(Some(style::Stroke::new(crate::color::Color::GREEN, 0.4)), None), + points: vec![DVec2::new(3.0, 4.12354), DVec2::new(1.0, 5.54)], }; let mut svg = String::new(); - polyline.render(&mut svg); - assert_eq!(r##""##, svg); + polyline.render(&mut svg, glam::DAffine2::IDENTITY, PathStyle::default()); + assert_eq!(r##""##, svg); } diff --git a/core/document/src/layers/rect.rs b/core/document/src/layers/rect.rs index e38bcd1294..503430ddad 100644 --- a/core/document/src/layers/rect.rs +++ b/core/document/src/layers/rect.rs @@ -1,33 +1,34 @@ +use glam::DVec2; +use kurbo::Point; + use super::style; use super::LayerData; use std::fmt::Write; -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct Rect { - shape: kurbo::Rect, - style: style::PathStyle, -} +#[derive(Debug, Clone, PartialEq)] +pub struct Rect {} impl Rect { - pub fn new(p0: impl Into, p1: impl Into, style: style::PathStyle) -> Rect { - Rect { - shape: kurbo::Rect::from_points(p0, p1), - style, - } + pub fn new() -> Rect { + Rect {} } } impl LayerData for Rect { - fn render(&mut self, svg: &mut String) { - let _ = write!( - svg, - r#""#, - self.shape.min_x(), - self.shape.min_y(), - self.shape.width(), - self.shape.height(), - self.style.render(), - ); + fn to_kurbo_path(&mut self, transform: glam::DAffine2, _style: style::PathStyle) -> kurbo::BezPath { + fn new_point(a: DVec2) -> Point { + Point::new(a.x, a.y) + } + let mut path = kurbo::BezPath::new(); + path.move_to(new_point(transform.translation)); + + // TODO: Use into_iter when new impls get added in rust 2021 + [(1., 0.), (1., 1.), (0., 1.)].iter().for_each(|v| path.line_to(new_point(transform.transform_point2((*v).into())))); + path.close_path(); + path + } + fn render(&mut self, svg: &mut String, transform: glam::DAffine2, style: style::PathStyle) { + let _ = write!(svg, r#""#, self.to_kurbo_path(transform, style).to_svg(), style.render()); } } diff --git a/core/document/src/layers/shape.rs b/core/document/src/layers/shape.rs index c48ad514f4..d00134287a 100644 --- a/core/document/src/layers/shape.rs +++ b/core/document/src/layers/shape.rs @@ -1,38 +1,68 @@ -use crate::shape_points; +use kurbo::BezPath; +use kurbo::Vec2; use super::style; use super::LayerData; use std::fmt::Write; -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, PartialEq)] pub struct Shape { - bounding_rect: kurbo::Rect, - shape: shape_points::ShapePoints, - style: style::PathStyle, + equal_sides: bool, + sides: u8, } impl Shape { - pub fn new(p0: impl Into, p1: impl Into, sides: u8, style: style::PathStyle) -> Shape { - Shape { - bounding_rect: kurbo::Rect::from_points(p0, p1), - shape: shape_points::ShapePoints::new(kurbo::Point::new(0.5, 0.5), kurbo::Vec2::new(0.5, 0.0), sides), - style, - } + pub fn new(equal_sides: bool, sides: u8) -> Shape { + Shape { equal_sides, sides } } } impl LayerData for Shape { - fn render(&mut self, svg: &mut String) { - let _ = write!( - svg, - r#""#, - self.shape, - self.bounding_rect.origin().x, - self.bounding_rect.origin().y, - self.bounding_rect.width(), - self.bounding_rect.height(), - self.style.render(), - ); + fn to_kurbo_path(&mut self, transform: glam::DAffine2, _style: style::PathStyle) -> BezPath { + fn unit_rotation(theta: f64) -> Vec2 { + Vec2::new(-theta.sin(), theta.cos()) + } + let extent = Vec2::new((transform.x_axis.x + transform.x_axis.y) / 2., (transform.y_axis.x + transform.y_axis.y) / 2.); + let translation = transform.translation; + let mut path = kurbo::BezPath::new(); + let apothem_offset_angle = std::f64::consts::PI / (self.sides as f64); + + let relative_points = (0..self.sides) + .map(|i| apothem_offset_angle * ((i * 2 + ((self.sides + 1) % 2)) as f64)) + .map(|radians| unit_rotation(radians)); + + let (mut min_x, mut min_y, mut max_x, mut max_y) = (f64::MAX, f64::MAX, f64::MIN, f64::MIN); + relative_points.clone().for_each(|p| { + min_x = min_x.min(p.x); + min_y = min_y.min(p.y); + max_x = max_x.max(p.x); + max_y = max_y.max(p.y); + }); + + relative_points + .map(|p| { + if self.equal_sides { + p + } else { + Vec2::new((p.x - min_x) / (max_x - min_x) * 2. - 1., (p.y - min_y) / (max_y - min_y) * 2. - 1.) + } + }) + .map(|unit| Vec2::new(-unit.x * extent.x + translation.x + extent.x, -unit.y * extent.y + translation.y + extent.y)) + .map(|pos| (pos).to_point()) + .enumerate() + .for_each(|(i, p)| { + if i == 0 { + path.move_to(p); + } else { + path.line_to(p); + } + }); + + path.close_path(); + path + } + fn render(&mut self, svg: &mut String, transform: glam::DAffine2, style: style::PathStyle) { + let _ = write!(svg, r#""#, self.to_kurbo_path(transform, style).to_svg(), style.render()); } } diff --git a/core/document/src/lib.rs b/core/document/src/lib.rs index 171983d053..d933bf42b1 100644 --- a/core/document/src/lib.rs +++ b/core/document/src/lib.rs @@ -3,7 +3,6 @@ pub mod document; pub mod layers; pub mod operation; pub mod response; -mod shape_points; pub use operation::Operation; pub use response::DocumentResponse; @@ -15,4 +14,5 @@ pub enum DocumentError { LayerNotFound, InvalidPath, IndexOutOfBounds, + NotAFolder, } diff --git a/core/document/src/operation.rs b/core/document/src/operation.rs index e63ada01ae..960ce35ce7 100644 --- a/core/document/src/operation.rs +++ b/core/document/src/operation.rs @@ -5,44 +5,27 @@ use serde::{Deserialize, Serialize}; #[repr(C)] #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub enum Operation { - AddCircle { - path: Vec, - insert_index: isize, - cx: f64, - cy: f64, - r: f64, - style: style::PathStyle, - }, AddEllipse { path: Vec, insert_index: isize, - cx: f64, - cy: f64, - rx: f64, - ry: f64, - rot: f64, + transform: [f64; 6], style: style::PathStyle, }, AddRect { path: Vec, insert_index: isize, - x0: f64, - y0: f64, - x1: f64, - y1: f64, + transform: [f64; 6], style: style::PathStyle, }, AddLine { path: Vec, insert_index: isize, - x0: f64, - y0: f64, - x1: f64, - y1: f64, + transform: [f64; 6], style: style::PathStyle, }, AddPen { path: Vec, + transform: [f64; 6], insert_index: isize, points: Vec<(f64, f64)>, style: style::PathStyle, @@ -50,10 +33,8 @@ pub enum Operation { AddShape { path: Vec, insert_index: isize, - x0: f64, - y0: f64, - x1: f64, - y1: f64, + transform: [f64; 6], + equal_sides: bool, sides: u8, style: style::PathStyle, }, @@ -69,6 +50,10 @@ pub enum Operation { MountWorkingFolder { path: Vec, }, + TransformLayer { + path: Vec, + transform: [f64; 6], + }, DiscardWorkingFolder, ClearWorkingFolder, CommitTransaction, diff --git a/core/document/src/shape_points.rs b/core/document/src/shape_points.rs deleted file mode 100644 index 4b351bd568..0000000000 --- a/core/document/src/shape_points.rs +++ /dev/null @@ -1,128 +0,0 @@ -use std::{fmt, ops::Add}; - -use kurbo::{PathEl, Point, Vec2}; - -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct ShapePoints { - center: kurbo::Point, - extent: kurbo::Vec2, - sides: u8, -} - -impl ShapePoints { - /// A new shape from center, a point and the number of points. - #[inline] - pub fn new(center: impl Into, extent: impl Into, sides: u8) -> ShapePoints { - ShapePoints { - center: center.into(), - extent: extent.into(), - sides, - } - } - - // Gets the angle in radians between the longest line from the center and the apothem. - #[inline] - pub fn apothem_offset_angle(&self) -> f64 { - std::f64::consts::PI / (self.sides as f64) - } - - // Gets the apothem (the shortest distance from the center to the edge) - #[inline] - pub fn apothem(&self) -> f64 { - self.apothem_offset_angle().cos() * (self.sides as f64) - } - - // Gets the length of one side - #[inline] - pub fn side_length(&self) -> f64 { - self.apothem_offset_angle().sin() * (self.sides as f64) * 2f64 - } -} - -// TODO: The display impl and iter impl share large amounts of code and should be refactored. (Display should use the Iterator) -// TODO: Once that is done, the trailing space from the display impl should be removed -// Also consider implementing index -impl std::fmt::Display for ShapePoints { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fn rotate(v: &Vec2, theta: f64) -> Vec2 { - let cosine = theta.cos(); - let sine = theta.sin(); - Vec2::new(v.x * cosine - v.y * sine, v.x * sine + v.y * cosine) - } - for i in 0..self.sides { - let radians = self.apothem_offset_angle() * ((i * 2 + (self.sides % 2)) as f64); - let offset = rotate(&self.extent, radians); - let point = self.center + offset; - write!(f, "{},{} ", point.x, point.y)?; - } - - Ok(()) - } -} - -#[doc(hidden)] -pub struct ShapePathIter { - shape: ShapePoints, - index: usize, -} - -impl Iterator for ShapePathIter { - type Item = PathEl; - - fn next(&mut self) -> Option { - fn rotate(v: &Vec2, theta: f64) -> Vec2 { - let cosine = theta.cos(); - let sine = theta.sin(); - Vec2::new(v.x * cosine - v.y * sine, v.x * sine + v.y * cosine) - } - self.index += 1; - match self.index { - 1 => Some(PathEl::MoveTo(self.shape.center + self.shape.extent)), - _ => { - let radians = self.shape.apothem_offset_angle() * ((self.index * 2 + (self.shape.sides % 2) as usize) as f64); - let offset = rotate(&self.shape.extent, radians); - let point = self.shape.center + offset; - Some(PathEl::LineTo(point)) - } - } - } -} - -impl Add for ShapePoints { - type Output = ShapePoints; - - #[inline] - fn add(self, movement: Vec2) -> ShapePoints { - ShapePoints { - center: self.center + movement, - extent: self.extent, - sides: self.sides, - } - } -} - -impl kurbo::Shape for ShapePoints { - type PathElementsIter = ShapePathIter; - - fn path_elements(&self, _tolerance: f64) -> Self::PathElementsIter { - todo!() - } - - #[inline] - fn area(&self) -> f64 { - self.apothem() * self.perimeter(2.1) - } - - #[inline] - fn perimeter(&self, _accuracy: f64) -> f64 { - self.side_length() * (self.sides as f64) - } - - fn winding(&self, _pt: Point) -> i32 { - todo!() - } - - fn bounding_box(&self) -> kurbo::Rect { - todo!() - } -} diff --git a/core/editor/Cargo.toml b/core/editor/Cargo.toml index fe90d2043d..aa701dc168 100644 --- a/core/editor/Cargo.toml +++ b/core/editor/Cargo.toml @@ -14,6 +14,7 @@ bitflags = "1.2.1" thiserror = "1.0.24" serde = { version = "1.0", features = ["derive"] } graphite-proc-macros = {path = "../proc-macro"} +glam = "0.16" [dependencies.document-core] path = "../document" diff --git a/core/editor/src/communication/dispatcher.rs b/core/editor/src/communication/dispatcher.rs index 644bf93891..7081b17ca0 100644 --- a/core/editor/src/communication/dispatcher.rs +++ b/core/editor/src/communication/dispatcher.rs @@ -34,7 +34,7 @@ impl Dispatcher { } match message { NoOp => (), - Document(message) => self.document_message_handler.process_action(message, (), &mut self.messages), + Document(message) => self.document_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 diff --git a/core/editor/src/document/document_file.rs b/core/editor/src/document/document_file.rs index d724a91363..fb32e35610 100644 --- a/core/editor/src/document/document_file.rs +++ b/core/editor/src/document/document_file.rs @@ -60,9 +60,10 @@ impl Document { let folder = self.document.document_folder(path)?; let self_layer_data = &mut self.layer_data; let entries = folder + .as_folder()? .layers() .iter() - .zip(folder.layer_ids.iter()) + .zip(folder.as_folder()?.layer_ids.iter()) .rev() .map(|(layer, id)| { let path = [path, &[*id]].concat(); diff --git a/core/editor/src/document/document_message_handler.rs b/core/editor/src/document/document_message_handler.rs index 81b642053a..b449ca6509 100644 --- a/core/editor/src/document/document_message_handler.rs +++ b/core/editor/src/document/document_message_handler.rs @@ -1,5 +1,10 @@ -use crate::message_prelude::*; +use crate::{ + input::{mouse::ViewportPosition, InputPreprocessor}, + message_prelude::*, +}; use document_core::{DocumentResponse, LayerId, Operation as DocumentOperation}; +use glam::{DAffine2, DVec2}; +use log::info; use crate::document::Document; use std::collections::VecDeque; @@ -25,6 +30,9 @@ pub enum DocumentMessage { ExportDocument, RenderDocument, Undo, + MouseMove, + TranslateDown, + TranslateUp, } impl From for DocumentMessage { @@ -42,6 +50,8 @@ impl From for Message { pub struct DocumentMessageHandler { documents: Vec, active_document: usize, + mmb_down: bool, + mouse_pos: ViewportPosition, } impl DocumentMessageHandler { @@ -78,12 +88,14 @@ impl Default for DocumentMessageHandler { Self { documents: vec![Document::default()], active_document: 0, + mmb_down: false, + mouse_pos: ViewportPosition::default(), } } } -impl MessageHandler for DocumentMessageHandler { - fn process_action(&mut self, message: DocumentMessage, _data: (), responses: &mut VecDeque) { +impl MessageHandler for DocumentMessageHandler { + fn process_action(&mut self, message: DocumentMessage, ipp: &InputPreprocessor, responses: &mut VecDeque) { use DocumentMessage::*; match message { DeleteLayer(path) => responses.push_back(DocumentOperation::DeleteLayer { path }.into()), @@ -224,7 +236,7 @@ impl MessageHandler for DocumentMessageHandler { } Undo => { // this is a temporary fix and will be addressed by #123 - if let Some(id) = self.active_document().document.root.list_layers().last() { + if let Some(id) = self.active_document().document.root.as_folder().unwrap().list_layers().last() { responses.push_back(DocumentOperation::DeleteLayer { path: vec![*id] }.into()) } } @@ -259,14 +271,32 @@ impl MessageHandler for DocumentMessageHandler { } .into(), ), + TranslateDown => { + self.mmb_down = true; + self.mouse_pos = ipp.mouse.position; + } + TranslateUp => { + self.mmb_down = false; + } + MouseMove => { + if self.mmb_down { + let delta = DVec2::new(ipp.mouse.position.x as f64 - self.mouse_pos.x as f64, ipp.mouse.position.y as f64 - self.mouse_pos.y as f64); + let operation = DocumentOperation::TransformLayer { + path: vec![], + transform: DAffine2::from_translation(delta).to_cols_array(), + }; + responses.push_back(operation.into()); + self.mouse_pos = ipp.mouse.position; + } + } message => todo!("document_action_handler does not implement: {}", message.to_discriminant().global_name()), } } fn actions(&self) -> ActionList { if self.active_document().layer_data.values().any(|data| data.selected) { - actions!(DocumentMessageDiscriminant; Undo, DeleteSelectedLayers, DuplicateSelectedLayers, RenderDocument, ExportDocument, NewDocument, CloseActiveDocument, NextDocument, PrevDocument) + actions!(DocumentMessageDiscriminant; Undo, DeleteSelectedLayers, DuplicateSelectedLayers, RenderDocument, ExportDocument, NewDocument, CloseActiveDocument, NextDocument, PrevDocument, MouseMove, TranslateUp, TranslateDown) } else { - actions!(DocumentMessageDiscriminant; Undo, RenderDocument, ExportDocument, NewDocument, CloseActiveDocument, NextDocument, PrevDocument) + actions!(DocumentMessageDiscriminant; Undo, RenderDocument, ExportDocument, NewDocument, CloseActiveDocument, NextDocument, PrevDocument, MouseMove, TranslateUp, TranslateDown) } } } diff --git a/core/editor/src/frontend/layer_panel.rs b/core/editor/src/frontend/layer_panel.rs index f60f2156e1..01cf24beb9 100644 --- a/core/editor/src/frontend/layer_panel.rs +++ b/core/editor/src/frontend/layer_panel.rs @@ -45,7 +45,6 @@ impl From<&LayerDataTypes> for LayerType { match data { Folder(_) => LayerType::Folder, Shape(_) => LayerType::Shape, - Circle(_) => LayerType::Circle, Rect(_) => LayerType::Rect, Line(_) => LayerType::Line, PolyLine(_) => LayerType::PolyLine, diff --git a/core/editor/src/input/input_mapper.rs b/core/editor/src/input/input_mapper.rs index 1abb4f406d..91d06e307f 100644 --- a/core/editor/src/input/input_mapper.rs +++ b/core/editor/src/input/input_mapper.rs @@ -168,6 +168,9 @@ impl Default for Mapping { entry! {action=DocumentMessage::DeleteSelectedLayers, key_down=KeyBackspace}, entry! {action=DocumentMessage::ExportDocument, key_down=KeyS, modifiers=[KeyControl, KeyShift]}, entry! {action=DocumentMessage::ExportDocument, key_down=KeyE, modifiers=[KeyControl]}, + entry! {action=DocumentMessage::MouseMove, message=InputMapperMessage::PointerMove}, + entry! {action=DocumentMessage::TranslateDown, key_down=Mmb}, + entry! {action=DocumentMessage::TranslateUp, key_up=Mmb}, entry! {action=DocumentMessage::NewDocument, key_down=KeyN, modifiers=[KeyShift]}, entry! {action=DocumentMessage::NextDocument, key_down=KeyTab, modifiers=[KeyShift]}, entry! {action=DocumentMessage::CloseActiveDocument, key_down=KeyW, modifiers=[KeyShift]}, diff --git a/core/editor/src/tool/tools/ellipse.rs b/core/editor/src/tool/tools/ellipse.rs index dc16f0eb37..f792710916 100644 --- a/core/editor/src/tool/tools/ellipse.rs +++ b/core/editor/src/tool/tools/ellipse.rs @@ -2,6 +2,7 @@ use crate::input::{mouse::ViewportPosition, InputPreprocessor}; use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData}; use crate::{message_prelude::*, SvgDocument}; use document_core::{layers::style, Operation}; +use glam::{DAffine2, DVec2}; #[derive(Default)] pub struct Ellipse { @@ -58,7 +59,8 @@ struct EllipseToolData { impl Fsm for EllipseToolFsmState { type ToolData = EllipseToolData; - fn transition(self, event: ToolMessage, _document: &SvgDocument, tool_data: &DocumentToolData, data: &mut Self::ToolData, input: &InputPreprocessor, responses: &mut VecDeque) -> Self { + fn transition(self, event: ToolMessage, document: &SvgDocument, tool_data: &DocumentToolData, data: &mut Self::ToolData, input: &InputPreprocessor, responses: &mut VecDeque) -> Self { + let transform = document.root.transform; use EllipseMessage::*; use EllipseToolFsmState::*; if let ToolMessage::Ellipse(event) = event { @@ -73,7 +75,7 @@ impl Fsm for EllipseToolFsmState { data.drag_current = input.mouse.position; responses.push_back(Operation::ClearWorkingFolder.into()); - responses.push_back(make_operation(data, tool_data)); + responses.push_back(make_operation(data, tool_data, transform)); Dragging } @@ -83,7 +85,7 @@ impl Fsm for EllipseToolFsmState { responses.push_back(Operation::ClearWorkingFolder.into()); // TODO - introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100) if data.drag_start != data.drag_current { - responses.push_back(make_operation(data, tool_data)); + responses.push_back(make_operation(data, tool_data, transform)); responses.push_back(Operation::CommitTransaction.into()); } @@ -97,13 +99,13 @@ impl Fsm for EllipseToolFsmState { } (Ready, LockAspectRatio) => update_state_no_op(&mut data.constrain_to_circle, true, Ready), (Ready, UnlockAspectRatio) => update_state_no_op(&mut data.constrain_to_circle, false, Ready), - (Dragging, LockAspectRatio) => update_state(|data| &mut data.constrain_to_circle, true, tool_data, data, responses, Dragging), - (Dragging, UnlockAspectRatio) => update_state(|data| &mut data.constrain_to_circle, false, tool_data, data, responses, Dragging), + (Dragging, LockAspectRatio) => update_state(|data| &mut data.constrain_to_circle, true, tool_data, data, responses, Dragging, transform), + (Dragging, UnlockAspectRatio) => update_state(|data| &mut data.constrain_to_circle, false, tool_data, data, responses, Dragging, transform), (Ready, Center) => update_state_no_op(&mut data.center_around_cursor, true, Ready), (Ready, UnCenter) => update_state_no_op(&mut data.center_around_cursor, false, Ready), - (Dragging, Center) => update_state(|data| &mut data.center_around_cursor, true, tool_data, data, responses, Dragging), - (Dragging, UnCenter) => update_state(|data| &mut data.center_around_cursor, false, tool_data, data, responses, Dragging), + (Dragging, Center) => update_state(|data| &mut data.center_around_cursor, true, tool_data, data, responses, Dragging, transform), + (Dragging, UnCenter) => update_state(|data| &mut data.center_around_cursor, false, tool_data, data, responses, Dragging, transform), _ => self, } } else { @@ -124,16 +126,17 @@ fn update_state( data: &mut EllipseToolData, responses: &mut VecDeque, new_state: EllipseToolFsmState, + transform: DAffine2, ) -> EllipseToolFsmState { *(state(data)) = value; responses.push_back(Operation::ClearWorkingFolder.into()); - responses.push_back(make_operation(&data, tool_data)); + responses.push_back(make_operation(&data, tool_data, transform)); new_state } -fn make_operation(data: &EllipseToolData, tool_data: &DocumentToolData) -> Message { +fn make_operation(data: &EllipseToolData, tool_data: &DocumentToolData, transform: DAffine2) -> Message { let x0 = data.drag_start.x as f64; let y0 = data.drag_start.y as f64; let x1 = data.drag_current.x as f64; @@ -147,12 +150,10 @@ fn make_operation(data: &EllipseToolData, tool_data: &DocumentToolData) -> Messa let (x2, y2) = (x0 + (x1 - x0).signum() * diameter, y0 + (y1 - y0).signum() * diameter); ((x0 + x2) * 0.5, (y0 + y2) * 0.5, diameter * 0.5) }; - Operation::AddCircle { + Operation::AddEllipse { path: vec![], insert_index: -1, - cx, - cy, - r, + transform: (transform.inverse() * glam::DAffine2::from_scale_angle_translation(DVec2::new(r, r), 0., DVec2::new(cx, cy))).to_cols_array(), style: style::PathStyle::new(None, Some(style::Fill::new(tool_data.primary_color))), } } else { @@ -161,11 +162,7 @@ fn make_operation(data: &EllipseToolData, tool_data: &DocumentToolData) -> Messa Operation::AddEllipse { path: vec![], insert_index: -1, - cx, - cy, - rx, - ry, - rot: 0.0, + transform: (transform.inverse() * glam::DAffine2::from_scale_angle_translation(DVec2::new(rx, ry), 0., DVec2::new(cx, cy))).to_cols_array(), style: style::PathStyle::new(None, Some(style::Fill::new(tool_data.primary_color))), } } diff --git a/core/editor/src/tool/tools/line.rs b/core/editor/src/tool/tools/line.rs index 27d392645e..61383c7672 100644 --- a/core/editor/src/tool/tools/line.rs +++ b/core/editor/src/tool/tools/line.rs @@ -2,6 +2,7 @@ use crate::input::{mouse::ViewportPosition, InputPreprocessor}; use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData}; use crate::{message_prelude::*, SvgDocument}; use document_core::{layers::style, Operation}; +use glam::{DAffine2, DVec2}; use std::f64::consts::PI; @@ -63,7 +64,8 @@ struct LineToolData { impl Fsm for LineToolFsmState { type ToolData = LineToolData; - fn transition(self, event: ToolMessage, _document: &SvgDocument, tool_data: &DocumentToolData, data: &mut Self::ToolData, input: &InputPreprocessor, responses: &mut VecDeque) -> Self { + fn transition(self, event: ToolMessage, document: &SvgDocument, tool_data: &DocumentToolData, data: &mut Self::ToolData, input: &InputPreprocessor, responses: &mut VecDeque) -> Self { + let transform = document.root.transform; use LineMessage::*; use LineToolFsmState::*; if let ToolMessage::Line(event) = event { @@ -80,7 +82,7 @@ impl Fsm for LineToolFsmState { data.drag_current = input.mouse.position; responses.push_back(Operation::ClearWorkingFolder.into()); - responses.push_back(make_operation(data, tool_data)); + responses.push_back(make_operation(data, tool_data, transform)); Dragging } @@ -90,7 +92,7 @@ impl Fsm for LineToolFsmState { responses.push_back(Operation::ClearWorkingFolder.into()); // TODO - introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100) if data.drag_start != data.drag_current { - responses.push_back(make_operation(data, tool_data)); + responses.push_back(make_operation(data, tool_data, transform)); responses.push_back(Operation::CommitTransaction.into()); } @@ -104,18 +106,18 @@ impl Fsm for LineToolFsmState { } (Ready, LockAngle) => update_state_no_op(&mut data.lock_angle, true, Ready), (Ready, UnlockAngle) => update_state_no_op(&mut data.lock_angle, false, Ready), - (Dragging, LockAngle) => update_state(|data| &mut data.lock_angle, true, tool_data, data, responses, Dragging), - (Dragging, UnlockAngle) => update_state(|data| &mut data.lock_angle, false, tool_data, data, responses, Dragging), + (Dragging, LockAngle) => update_state(|data| &mut data.lock_angle, true, tool_data, data, responses, Dragging, transform), + (Dragging, UnlockAngle) => update_state(|data| &mut data.lock_angle, false, tool_data, data, responses, Dragging, transform), (Ready, SnapToAngle) => update_state_no_op(&mut data.snap_angle, true, Ready), (Ready, UnSnapToAngle) => update_state_no_op(&mut data.snap_angle, false, Ready), - (Dragging, SnapToAngle) => update_state(|data| &mut data.snap_angle, true, tool_data, data, responses, Dragging), - (Dragging, UnSnapToAngle) => update_state(|data| &mut data.snap_angle, false, tool_data, data, responses, Dragging), + (Dragging, SnapToAngle) => update_state(|data| &mut data.snap_angle, true, tool_data, data, responses, Dragging, transform), + (Dragging, UnSnapToAngle) => update_state(|data| &mut data.snap_angle, false, tool_data, data, responses, Dragging, transform), (Ready, Center) => update_state_no_op(&mut data.center_around_cursor, true, Ready), (Ready, UnCenter) => update_state_no_op(&mut data.center_around_cursor, false, Ready), - (Dragging, Center) => update_state(|data| &mut data.center_around_cursor, true, tool_data, data, responses, Dragging), - (Dragging, UnCenter) => update_state(|data| &mut data.center_around_cursor, false, tool_data, data, responses, Dragging), + (Dragging, Center) => update_state(|data| &mut data.center_around_cursor, true, tool_data, data, responses, Dragging, transform), + (Dragging, UnCenter) => update_state(|data| &mut data.center_around_cursor, false, tool_data, data, responses, Dragging, transform), _ => self, } } else { @@ -136,16 +138,17 @@ fn update_state( data: &mut LineToolData, responses: &mut VecDeque, new_state: LineToolFsmState, + transform: DAffine2, ) -> LineToolFsmState { *(state(data)) = value; responses.push_back(Operation::ClearWorkingFolder.into()); - responses.push_back(make_operation(data, tool_data)); + responses.push_back(make_operation(data, tool_data, transform)); new_state } -fn make_operation(data: &mut LineToolData, tool_data: &DocumentToolData) -> Message { +fn make_operation(data: &mut LineToolData, tool_data: &DocumentToolData, transform: DAffine2) -> Message { let x0 = data.drag_start.x as f64; let y0 = data.drag_start.y as f64; let x1 = data.drag_current.x as f64; @@ -174,10 +177,7 @@ fn make_operation(data: &mut LineToolData, tool_data: &DocumentToolData) -> Mess Operation::AddLine { path: vec![], insert_index: -1, - x0, - y0, - x1, - y1, + transform: (transform.inverse() * glam::DAffine2::from_scale_angle_translation(DVec2::new(x1 - x0, y1 - y0), 0., DVec2::new(x0, y0))).to_cols_array(), style: style::PathStyle::new(Some(style::Stroke::new(tool_data.primary_color, 5.)), None), } .into() diff --git a/core/editor/src/tool/tools/pen.rs b/core/editor/src/tool/tools/pen.rs index eae68b8c94..6a11b8bc78 100644 --- a/core/editor/src/tool/tools/pen.rs +++ b/core/editor/src/tool/tools/pen.rs @@ -1,7 +1,8 @@ -use crate::input::{mouse::ViewportPosition, InputPreprocessor}; +use crate::input::InputPreprocessor; use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData}; use crate::{message_prelude::*, SvgDocument}; use document_core::{layers::style, Operation}; +use glam::{DAffine2, DVec2}; #[derive(Default)] pub struct Pen { @@ -46,14 +47,17 @@ impl Default for PenToolFsmState { } #[derive(Clone, Debug, Default)] struct PenToolData { - points: Vec, - next_point: ViewportPosition, + points: Vec, + next_point: DAffine2, } impl Fsm for PenToolFsmState { type ToolData = PenToolData; - fn transition(self, event: ToolMessage, _document: &SvgDocument, tool_data: &DocumentToolData, data: &mut Self::ToolData, input: &InputPreprocessor, responses: &mut VecDeque) -> Self { + fn transition(self, event: ToolMessage, document: &SvgDocument, tool_data: &DocumentToolData, data: &mut Self::ToolData, input: &InputPreprocessor, responses: &mut VecDeque) -> Self { + let transform = document.root.transform; + let pos = transform.inverse() * DAffine2::from_translation(DVec2::new(input.mouse.position.x as f64, input.mouse.position.y as f64)); + use PenMessage::*; use PenToolFsmState::*; if let ToolMessage::Pen(event) = event { @@ -61,16 +65,16 @@ impl Fsm for PenToolFsmState { (Ready, DragStart) => { responses.push_back(Operation::MountWorkingFolder { path: vec![] }.into()); - data.points.push(input.mouse.position); - data.next_point = input.mouse.position; + data.points.push(pos); + data.next_point = pos; Dragging } (Dragging, DragStop) => { // TODO - introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100) - if data.points.last() != Some(&input.mouse.position) { - data.points.push(input.mouse.position); - data.next_point = input.mouse.position; + if data.points.last() != Some(&pos) { + data.points.push(pos); + data.next_point = pos; } responses.push_back(Operation::ClearWorkingFolder.into()); @@ -79,7 +83,7 @@ impl Fsm for PenToolFsmState { Dragging } (Dragging, MouseMove) => { - data.next_point = input.mouse.position; + data.next_point = pos; responses.push_back(Operation::ClearWorkingFolder.into()); responses.push_back(make_operation(data, tool_data, true)); @@ -116,13 +120,14 @@ impl Fsm for PenToolFsmState { } fn make_operation(data: &PenToolData, tool_data: &DocumentToolData, show_preview: bool) -> Message { - let mut points: Vec<(f64, f64)> = data.points.iter().map(|p| (p.x as f64, p.y as f64)).collect(); + let mut points: Vec<(f64, f64)> = data.points.iter().map(|p| (p.translation.x, p.translation.y)).collect(); if show_preview { - points.push((data.next_point.x as f64, data.next_point.y as f64)) + points.push((data.next_point.translation.x, data.next_point.translation.y)) } Operation::AddPen { path: vec![], insert_index: -1, + transform: DAffine2::IDENTITY.to_cols_array(), points, style: style::PathStyle::new(Some(style::Stroke::new(tool_data.primary_color, 5.)), Some(style::Fill::none())), } diff --git a/core/editor/src/tool/tools/rectangle.rs b/core/editor/src/tool/tools/rectangle.rs index 0b3c1aa3b0..5f9fee452f 100644 --- a/core/editor/src/tool/tools/rectangle.rs +++ b/core/editor/src/tool/tools/rectangle.rs @@ -2,6 +2,7 @@ use crate::input::{mouse::ViewportPosition, InputPreprocessor}; use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData}; use crate::{message_prelude::*, SvgDocument}; use document_core::{layers::style, Operation}; +use glam::{DAffine2, DVec2}; #[derive(Default)] pub struct Rectangle { @@ -57,7 +58,8 @@ struct RectangleToolData { impl Fsm for RectangleToolFsmState { type ToolData = RectangleToolData; - fn transition(self, event: ToolMessage, _document: &SvgDocument, tool_data: &DocumentToolData, data: &mut Self::ToolData, input: &InputPreprocessor, responses: &mut VecDeque) -> Self { + fn transition(self, event: ToolMessage, document: &SvgDocument, tool_data: &DocumentToolData, data: &mut Self::ToolData, input: &InputPreprocessor, responses: &mut VecDeque) -> Self { + let transform = document.root.transform; use RectangleMessage::*; use RectangleToolFsmState::*; if let ToolMessage::Rectangle(event) = event { @@ -72,7 +74,7 @@ impl Fsm for RectangleToolFsmState { data.drag_current = input.mouse.position; responses.push_back(Operation::ClearWorkingFolder.into()); - responses.push_back(make_operation(data, tool_data)); + responses.push_back(make_operation(data, tool_data, transform)); Dragging } @@ -82,7 +84,7 @@ impl Fsm for RectangleToolFsmState { responses.push_back(Operation::ClearWorkingFolder.into()); // TODO - introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100) if data.drag_start != data.drag_current { - responses.push_back(make_operation(data, tool_data)); + responses.push_back(make_operation(data, tool_data, transform)); responses.push_back(Operation::CommitTransaction.into()); } @@ -96,13 +98,13 @@ impl Fsm for RectangleToolFsmState { } (Ready, LockAspectRatio) => update_state_no_op(&mut data.constrain_to_square, true, Ready), (Ready, UnlockAspectRatio) => update_state_no_op(&mut data.constrain_to_square, false, Ready), - (Dragging, LockAspectRatio) => update_state(|data| &mut data.constrain_to_square, true, tool_data, data, responses, Dragging), - (Dragging, UnlockAspectRatio) => update_state(|data| &mut data.constrain_to_square, false, tool_data, data, responses, Dragging), + (Dragging, LockAspectRatio) => update_state(|data| &mut data.constrain_to_square, true, tool_data, data, responses, Dragging, transform), + (Dragging, UnlockAspectRatio) => update_state(|data| &mut data.constrain_to_square, false, tool_data, data, responses, Dragging, transform), (Ready, Center) => update_state_no_op(&mut data.center_around_cursor, true, Ready), (Ready, UnCenter) => update_state_no_op(&mut data.center_around_cursor, false, Ready), - (Dragging, Center) => update_state(|data| &mut data.center_around_cursor, true, tool_data, data, responses, Dragging), - (Dragging, UnCenter) => update_state(|data| &mut data.center_around_cursor, false, tool_data, data, responses, Dragging), + (Dragging, Center) => update_state(|data| &mut data.center_around_cursor, true, tool_data, data, responses, Dragging, transform), + (Dragging, UnCenter) => update_state(|data| &mut data.center_around_cursor, false, tool_data, data, responses, Dragging, transform), _ => self, } } else { @@ -123,16 +125,17 @@ fn update_state( data: &mut RectangleToolData, responses: &mut VecDeque, new_state: RectangleToolFsmState, + transform: DAffine2, ) -> RectangleToolFsmState { *(state(data)) = value; responses.push_back(Operation::ClearWorkingFolder.into()); - responses.push_back(make_operation(data, tool_data)); + responses.push_back(make_operation(data, tool_data, transform)); new_state } -fn make_operation(data: &RectangleToolData, tool_data: &DocumentToolData) -> Message { +fn make_operation(data: &RectangleToolData, tool_data: &DocumentToolData, transform: DAffine2) -> Message { let x0 = data.drag_start.x as f64; let y0 = data.drag_start.y as f64; let x1 = data.drag_current.x as f64; @@ -161,10 +164,7 @@ fn make_operation(data: &RectangleToolData, tool_data: &DocumentToolData) -> Mes Operation::AddRect { path: vec![], insert_index: -1, - x0, - y0, - x1, - y1, + transform: (transform.inverse() * glam::DAffine2::from_scale_angle_translation(DVec2::new(x1 - x0, y1 - y0), 0., DVec2::new(x0, y0))).to_cols_array(), style: style::PathStyle::new(None, Some(style::Fill::new(tool_data.primary_color))), } .into() diff --git a/core/editor/src/tool/tools/shape.rs b/core/editor/src/tool/tools/shape.rs index 3e929b4901..da34d752e7 100644 --- a/core/editor/src/tool/tools/shape.rs +++ b/core/editor/src/tool/tools/shape.rs @@ -2,6 +2,7 @@ use crate::input::{mouse::ViewportPosition, InputPreprocessor}; use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData}; use crate::{message_prelude::*, SvgDocument}; use document_core::{layers::style, Operation}; +use glam::{DAffine2, DVec2}; #[derive(Default)] pub struct Shape { @@ -59,7 +60,8 @@ struct ShapeToolData { impl Fsm for ShapeToolFsmState { type ToolData = ShapeToolData; - fn transition(self, event: ToolMessage, _document: &SvgDocument, tool_data: &DocumentToolData, data: &mut Self::ToolData, input: &InputPreprocessor, responses: &mut VecDeque) -> Self { + fn transition(self, event: ToolMessage, document: &SvgDocument, tool_data: &DocumentToolData, data: &mut Self::ToolData, input: &InputPreprocessor, responses: &mut VecDeque) -> Self { + let transform = document.root.transform; use ShapeMessage::*; use ShapeToolFsmState::*; if let ToolMessage::Shape(event) = event { @@ -76,7 +78,7 @@ impl Fsm for ShapeToolFsmState { (Dragging, MouseMove) => { data.drag_current = input.mouse.position; responses.push_back(Operation::ClearWorkingFolder.into()); - responses.push_back(make_operation(data, tool_data)); + responses.push_back(make_operation(data, tool_data, transform)); Dragging } @@ -85,7 +87,7 @@ impl Fsm for ShapeToolFsmState { responses.push_back(Operation::ClearWorkingFolder.into()); // TODO - introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100) if data.drag_start != data.drag_current { - responses.push_back(make_operation(data, tool_data)); + responses.push_back(make_operation(data, tool_data, transform)); responses.push_back(Operation::CommitTransaction.into()); } @@ -99,13 +101,13 @@ impl Fsm for ShapeToolFsmState { (Ready, LockAspectRatio) => update_state_no_op(&mut data.constrain_to_square, true, Ready), (Ready, UnlockAspectRatio) => update_state_no_op(&mut data.constrain_to_square, false, Ready), - (Dragging, LockAspectRatio) => update_state(|data| &mut data.constrain_to_square, true, tool_data, data, responses, Dragging), - (Dragging, UnlockAspectRatio) => update_state(|data| &mut data.constrain_to_square, false, tool_data, data, responses, Dragging), + (Dragging, LockAspectRatio) => update_state(|data| &mut data.constrain_to_square, true, tool_data, data, responses, Dragging, transform), + (Dragging, UnlockAspectRatio) => update_state(|data| &mut data.constrain_to_square, false, tool_data, data, responses, Dragging, transform), (Ready, Center) => update_state_no_op(&mut data.center_around_cursor, true, Ready), (Ready, UnCenter) => update_state_no_op(&mut data.center_around_cursor, false, Ready), - (Dragging, Center) => update_state(|data| &mut data.center_around_cursor, true, tool_data, data, responses, Dragging), - (Dragging, UnCenter) => update_state(|data| &mut data.center_around_cursor, false, tool_data, data, responses, Dragging), + (Dragging, Center) => update_state(|data| &mut data.center_around_cursor, true, tool_data, data, responses, Dragging, transform), + (Dragging, UnCenter) => update_state(|data| &mut data.center_around_cursor, false, tool_data, data, responses, Dragging, transform), _ => self, } } else { @@ -126,28 +128,30 @@ fn update_state( data: &mut ShapeToolData, responses: &mut VecDeque, new_state: ShapeToolFsmState, + transform: DAffine2, ) -> ShapeToolFsmState { *(state(data)) = value; responses.push_back(Operation::ClearWorkingFolder.into()); - responses.push_back(make_operation(data, tool_data)); + responses.push_back(make_operation(data, tool_data, transform)); new_state } -fn make_operation(data: &ShapeToolData, tool_data: &DocumentToolData) -> Message { +fn make_operation(data: &ShapeToolData, tool_data: &DocumentToolData, transform: DAffine2) -> Message { let x0 = data.drag_start.x as f64; let y0 = data.drag_start.y as f64; let x1 = data.drag_current.x as f64; let y1 = data.drag_current.y as f64; - let (x0, y0, x1, y1) = if data.constrain_to_square { + // TODO: Use regular polygon's aspect ration for constraining rather than a square. + let (x0, y0, x1, y1, equal_sides) = if data.constrain_to_square { let (x_dir, y_dir) = ((x1 - x0).signum(), (y1 - y0).signum()); let max_dist = f64::max((x1 - x0).abs(), (y1 - y0).abs()); if data.center_around_cursor { - (x0 - max_dist * x_dir, y0 - max_dist * y_dir, x0 + max_dist * x_dir, y0 + max_dist * y_dir) + (x0 - max_dist * x_dir, y0 - max_dist * y_dir, x0 + max_dist * x_dir, y0 + max_dist * y_dir, true) } else { - (x0, y0, x0 + max_dist * x_dir, y0 + max_dist * y_dir) + (x0, y0, x0 + max_dist * x_dir, y0 + max_dist * y_dir, true) } } else { let (x0, y0) = if data.center_around_cursor { @@ -158,16 +162,14 @@ fn make_operation(data: &ShapeToolData, tool_data: &DocumentToolData) -> Message } else { (x0, y0) }; - (x0, y0, x1, y1) + (x0, y0, x1, y1, false) }; Operation::AddShape { path: vec![], insert_index: -1, - x0, - y0, - x1, - y1, + transform: (transform.inverse() * glam::DAffine2::from_scale_angle_translation(DVec2::new(x1 - x0, y1 - y0), 0., DVec2::new(x0, y0))).to_cols_array(), + equal_sides, sides: data.sides, style: style::PathStyle::new(None, Some(style::Fill::new(tool_data.primary_color))), }