Add a movable canvas with matricies (#175)

* Convert polygon and rectangle tool to kurbo::BezPath

* Add glam

* Add affine transform to elipse and remove circle

* Format

* Add svg group and add matrix for group

* Convert all operations to use matricies

* Work uses same transform as root

* Format

* Frontend fixed to render changes to working colors when changed from backend (#180)

* Backend and Frontend modification to show working color mods

* Remove comments & change precedence for tool and doc actions

* Add keybind for resetting work colors

* Minor Frontend changes

* Remove early sample "greet" code

* Add a contributing section to the project README

* Add moving document around

* Add document transform for tools

* Update to GraphiteEditor's fork

* Use write in foreach for rendering group / folder

* Add missing TranslateDown action

* Use points for line operation

* Format

* Add todo to change to shape's aspect ratio

* Remove empty if

* Initial pass at refactor

* Fix polyline test

* Use document message to modify document transform

* Messages -> Operations

* Transform layer

* Format

* Use DAffine2::IDENTITY

* Clean up kurbo generation for line and rect

* Use .into for rectangle points

* Rename cols to transform

* Rename other cols to transform

* Add todo for into_iter

* Remove unnecessary clone

Co-authored-by: akshay1992kalbhor <akshay1992kalbhor@gmail.com>
Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
0HyperCube
2021-06-26 21:44:48 +01:00
committed by Keavon Chambers
parent 923e63c045
commit bb3293af43
25 changed files with 401 additions and 454 deletions

View File

@@ -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<LayerId>,
pub work_operations: Vec<Operation>,
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<Option<Vec<DocumentResponse>>, 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 {
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::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::AddPen {
path,
insert_index,
cx,
cy,
rx,
ry,
rot,
points,
transform,
style,
} => {
let id = self.add_layer(&path, Layer::new(LayerDataTypes::Ellipse(layers::Ellipse::new((*cx, *cy), (*rx, *ry), *rot, *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)?;
let path = [path.clone(), vec![id]].concat();
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::SelectLayer { path }])
}
Operation::AddLine {
path,
insert_index,
x0,
y0,
x1,
y1,
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<kurbo::Point> = 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<glam::DVec2> = 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)? {

View File

@@ -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<kurbo::Point>, 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#"<circle cx="{}" cy="{}" r="{}"{} />"#,
self.shape.center.x,
self.shape.center.y,
self.shape.radius,
self.style.render(),
);
}
}

View File

@@ -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<kurbo::Point>, radii: impl Into<kurbo::Vec2>, 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#"<ellipse cx="0" cy="0" rx="{}" ry="{}" transform="translate({} {}) rotate({})"{} />"#,
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#"<path d="{}" {} />"#, self.to_kurbo_path(transform, style).to_svg(), style.render());
}
}

View File

@@ -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#"<g transform="matrix("#);
transform.to_cols_array().iter().enumerate().for_each(|(i, f)| {
let _ = svg.write_str(&(f.to_string() + if i != 5 { "," } else { "" }));
});
let _ = svg.write_str(r#")">"#);
for layer in &mut self.layers {
let _ = writeln!(svg, "{}", layer.render());
}
let _ = writeln!(svg, "</g>");
}
fn to_kurbo_path(&mut self, _: glam::DAffine2, _: style::PathStyle) -> kurbo::BezPath {
unimplemented!()
}
}

View File

@@ -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<kurbo::Point>, p1: impl Into<kurbo::Point>, 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#"<line x1="{}" y1="{}" x2="{}" y2="{}"{} />"#, x1, y1, x2, y2, self.style.render(),);
let _ = write!(svg, r#"<line x1="{}" y1="{}" x2="{}" y2="{}"{} />"#, x1, y1, x2, y2, style.render(),);
}
}

View File

@@ -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<String>,
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),
_ => {}
}
}
}

View File

@@ -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<kurbo::Point>,
style: style::PathStyle,
points: Vec<glam::DVec2>,
}
impl PolyLine {
pub fn new(points: Vec<impl Into<kurbo::Point>>, style: style::PathStyle) -> PolyLine {
pub fn new(points: Vec<impl Into<glam::DVec2>>) -> 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#"<polyline points=""#);
let mut points = self.points.iter();
let mut points = self.points.iter().map(|v| transform.transform_point2(*v));
let first = points.next().unwrap();
let _ = write!(svg, "{:.3} {:.3}", first.x, first.y);
for point in points {
let _ = write!(svg, " {:.3} {:.3}", point.x, point.y);
}
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##"<polyline points="3.000 4.124 1.000 5.540" stroke="#00FF00" stroke-width="0.4" />"##, svg);
polyline.render(&mut svg, glam::DAffine2::IDENTITY, PathStyle::default());
assert_eq!(r##"<polyline points="3.000 4.124 1.000 5.540" />"##, svg);
}

View File

@@ -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<kurbo::Point>, p1: impl Into<kurbo::Point>, 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#"<rect x="{}" y="{}" width="{}" height="{}"{} />"#,
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#"<path d="{}" {} />"#, self.to_kurbo_path(transform, style).to_svg(), style.render());
}
}

View File

@@ -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<kurbo::Point>, p1: impl Into<kurbo::Point>, 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#"<polygon points="{}" transform="translate({} {}) scale({} {})"{} />"#,
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#"<path d="{}" {} />"#, self.to_kurbo_path(transform, style).to_svg(), style.render());
}
}

View File

@@ -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,
}

View File

@@ -5,44 +5,27 @@ use serde::{Deserialize, Serialize};
#[repr(C)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub enum Operation {
AddCircle {
path: Vec<LayerId>,
insert_index: isize,
cx: f64,
cy: f64,
r: f64,
style: style::PathStyle,
},
AddEllipse {
path: Vec<LayerId>,
insert_index: isize,
cx: f64,
cy: f64,
rx: f64,
ry: f64,
rot: f64,
transform: [f64; 6],
style: style::PathStyle,
},
AddRect {
path: Vec<LayerId>,
insert_index: isize,
x0: f64,
y0: f64,
x1: f64,
y1: f64,
transform: [f64; 6],
style: style::PathStyle,
},
AddLine {
path: Vec<LayerId>,
insert_index: isize,
x0: f64,
y0: f64,
x1: f64,
y1: f64,
transform: [f64; 6],
style: style::PathStyle,
},
AddPen {
path: Vec<LayerId>,
transform: [f64; 6],
insert_index: isize,
points: Vec<(f64, f64)>,
style: style::PathStyle,
@@ -50,10 +33,8 @@ pub enum Operation {
AddShape {
path: Vec<LayerId>,
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<LayerId>,
},
TransformLayer {
path: Vec<LayerId>,
transform: [f64; 6],
},
DiscardWorkingFolder,
ClearWorkingFolder,
CommitTransaction,

View File

@@ -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<Point>, extent: impl Into<Vec2>, 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<PathEl> {
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<Vec2> 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!()
}
}