Restructure project directories (#333)

`/client/web` -> `/frontend`
`/client/cli` -> *delete for now*
`/client/native` -> *delete for now*
`/core/editor` -> `/editor`
`/core/document` -> `/graphene`
`/core/renderer` -> `/charcoal`
`/core/proc-macro` -> `/proc-macros` *(now plural)*
This commit is contained in:
Keavon Chambers
2021-08-07 05:17:18 -07:00
parent 434695d578
commit 53ad105f57
239 changed files with 197 additions and 224 deletions

165
graphene/src/color.rs Normal file
View File

@@ -0,0 +1,165 @@
use serde::{Deserialize, Serialize};
/// Structure that represent a color.
/// Internally alpha is stored as `f32` that ranges from `0.0` (transparent) to `1.0` (opaque).
/// The other components (RGB) are stored as `f32` that range from `0.0` up to `f32::MAX`,
/// the values encode the brightness of each channel proportional to the light intensity in cd/m² (nits) in HDR, and `0.0` (black) to `1.0` (white) in SDR color.
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
pub struct Color {
red: f32,
green: f32,
blue: f32,
alpha: f32,
}
impl Color {
pub const BLACK: Color = Color::from_unsafe(0., 0., 0.);
pub const WHITE: Color = Color::from_unsafe(1., 1., 1.);
pub const RED: Color = Color::from_unsafe(1., 0., 0.);
pub const GREEN: Color = Color::from_unsafe(0., 1., 0.);
pub const BLUE: Color = Color::from_unsafe(0., 0., 1.);
/// Returns `Some(Color)` if `red`, `green`, `blue` and `alpha` have a valid value. Negative numbers (including `-0.0`), NaN, and infinity are not valid values and return `None`.
/// Alpha values greater than `1.0` are not valid.
///
/// # Examples
/// ```
/// use graphite_graphene::color::Color;
/// let color = Color::from_rgbaf32(0.3, 0.14, 0.15, 0.92).unwrap();
/// assert!(color.components() == (0.3, 0.14, 0.15, 0.92));
///
/// let color = Color::from_rgbaf32(1.0, 1.0, 1.0, f32::NAN);
/// assert!(color == None);
/// ```
pub fn from_rgbaf32(red: f32, green: f32, blue: f32, alpha: f32) -> Option<Color> {
if alpha > 1. || [red, green, blue, alpha].iter().any(|c| c.is_sign_negative() || !c.is_finite()) {
return None;
}
Some(Color { red, green, blue, alpha })
}
/// Return an opaque `Color` from given `f32` RGB channels.
const fn from_unsafe(red: f32, green: f32, blue: f32) -> Color {
Color { red, green, blue, alpha: 1. }
}
/// Return an opaque SDR `Color` given RGB channels from `0` to `255`.
///
/// # Examples
/// ```
/// use graphite_graphene::color::Color;
/// let color = Color::from_rgb8(0x72, 0x67, 0x62);
/// let color2 = Color::from_rgba8(0x72, 0x67, 0x62, 0xFF);
/// assert!(color == color2)
/// ```
pub fn from_rgb8(red: u8, green: u8, blue: u8) -> Color {
Color::from_rgba8(red, green, blue, 255)
}
/// Return an SDR `Color` given RGBA channels from `0` to `255`.
///
/// # Examples
/// ```
/// use graphite_graphene::color::Color;
/// let color = Color::from_rgba8(0x72, 0x67, 0x62, 0x61);
/// assert!("72676261" == color.rgba_hex())
/// ```
pub fn from_rgba8(red: u8, green: u8, blue: u8, alpha: u8) -> Color {
let map_range = |int_color| int_color as f32 / 255.0;
Color {
red: map_range(red),
green: map_range(green),
blue: map_range(blue),
alpha: map_range(alpha),
}
}
/// Return the `red` component.
///
/// # Examples
/// ```
/// use graphite_graphene::color::Color;
/// let color = Color::from_rgbaf32(0.114, 0.103, 0.98, 0.97).unwrap();
/// assert!(color.r() == 0.114);
/// ```
pub fn r(&self) -> f32 {
self.red
}
/// Return the `green` component.
///
/// # Examples
/// ```
/// use graphite_graphene::color::Color;
/// let color = Color::from_rgbaf32(0.114, 0.103, 0.98, 0.97).unwrap();
/// assert!(color.g() == 0.103);
/// ```
pub fn g(&self) -> f32 {
self.green
}
/// Return the `blue` component.
///
/// # Examples
/// ```
/// use graphite_graphene::color::Color;
/// let color = Color::from_rgbaf32(0.114, 0.103, 0.98, 0.97).unwrap();
/// assert!(color.b() == 0.98);
/// ```
pub fn b(&self) -> f32 {
self.blue
}
/// Return the `alpha` component without checking its expected `0.0` to `1.0` range.
///
/// # Examples
/// ```
/// use graphite_graphene::color::Color;
/// let color = Color::from_rgbaf32(0.114, 0.103, 0.98, 0.97).unwrap();
/// assert!(color.a() == 0.97);
/// ```
pub fn a(&self) -> f32 {
self.alpha
}
/// Return the all components as a tuple, first component is red, followed by green, followed by blue, followed by alpha.
///
/// # Examples
/// ```
/// use graphite_graphene::color::Color;
/// let color = Color::from_rgbaf32(0.114, 0.103, 0.98, 0.97).unwrap();
/// assert!(color.components() == (0.114, 0.103, 0.98, 0.97));
/// ```
pub fn components(&self) -> (f32, f32, f32, f32) {
(self.red, self.green, self.blue, self.alpha)
}
/// Return an 8-character RGBA hex string (without a # prefix).
///
/// # Examples
/// ```
/// use graphite_graphene::color::Color;
/// let color = Color::from_rgba8(0x7C, 0x67, 0xFA, 0x61);
/// assert!("7C67FA61" == color.rgba_hex())
/// ```
pub fn rgba_hex(&self) -> String {
format!(
"{:02X?}{:02X?}{:02X?}{:02X?}",
(self.r() * 255.) as u8,
(self.g() * 255.) as u8,
(self.b() * 255.) as u8,
(self.a() * 255.) as u8,
)
}
/// Return a 6-character RGB hex string (without a # prefix).
/// ```
/// use graphite_graphene::color::Color;
/// let color = Color::from_rgba8(0x7C, 0x67, 0xFA, 0x61);
/// assert!("7C67FA" == color.rgb_hex())
/// ```
pub fn rgb_hex(&self) -> String {
format!("{:02X?}{:02X?}{:02X?}", (self.r() * 255.) as u8, (self.g() * 255.) as u8, (self.b() * 255.) as u8,)
}
}

418
graphene/src/document.rs Normal file
View File

@@ -0,0 +1,418 @@
use std::{
collections::hash_map::DefaultHasher,
hash::{Hash, Hasher},
};
use glam::{DAffine2, DVec2};
use crate::{
layers::{self, Folder, Layer, LayerData, LayerDataType, Shape},
DocumentError, DocumentResponse, LayerId, Operation, Quad,
};
#[derive(Debug, Clone)]
pub struct Document {
pub root: Layer,
pub hasher: DefaultHasher,
}
impl Default for Document {
fn default() -> Self {
Self {
root: Layer::new(LayerDataType::Folder(Folder::default()), DAffine2::IDENTITY.to_cols_array()),
hasher: DefaultHasher::new(),
}
}
}
fn split_path(path: &[LayerId]) -> Result<(&[LayerId], LayerId), DocumentError> {
let (id, path) = path.split_last().ok_or(DocumentError::InvalidPath)?;
Ok((path, *id))
}
impl Document {
/// Wrapper around render, that returns the whole document as a Response.
pub fn render_root(&mut self) -> String {
self.root.render(&mut vec![]);
self.root.cache.clone()
}
pub fn hash(&self) -> u64 {
self.hasher.finish()
}
/// Checks whether each layer under `path` intersects with the provided `quad` and adds all intersection layers as paths to `intersections`.
pub fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>) {
self.layer(path).unwrap().intersects_quad(quad, path, intersections);
}
/// Checks whether each layer under the root path intersects with the provided `quad` and returns the paths to all intersecting layers.
pub fn intersects_quad_root(&self, quad: Quad) -> Vec<Vec<LayerId>> {
let mut intersections = Vec::new();
self.intersects_quad(quad, &mut vec![], &mut intersections);
intersections
}
/// Returns a reference to the requested folder. Fails if the path does not exist,
/// or if the requested layer is not of type folder.
pub fn folder(&self, path: &[LayerId]) -> Result<&Folder, DocumentError> {
let mut root = &self.root;
for id in path {
root = root.as_folder()?.layer(*id).ok_or(DocumentError::LayerNotFound)?;
}
root.as_folder()
}
/// Returns a mutable reference to the requested folder. Fails if the path does not exist,
/// or if the requested layer is not of type folder.
/// If you manually edit the folder you have to set the cache_dirty flag yourself.
pub fn folder_mut(&mut self, path: &[LayerId]) -> Result<&mut Folder, DocumentError> {
let mut root = &mut self.root;
for id in path {
root = root.as_folder_mut()?.layer_mut(*id).ok_or(DocumentError::LayerNotFound)?;
}
root.as_folder_mut()
}
/// Returns a reference to the layer or folder at the path.
pub fn layer(&self, path: &[LayerId]) -> Result<&Layer, DocumentError> {
if path.is_empty() {
return Ok(&self.root);
}
let (path, id) = split_path(path)?;
self.folder(path)?.layer(id).ok_or(DocumentError::LayerNotFound)
}
/// Returns a mutable reference to the layer or folder at the path.
pub fn layer_mut(&mut self, path: &[LayerId]) -> Result<&mut Layer, DocumentError> {
if path.is_empty() {
return Ok(&mut self.root);
}
let (path, id) = split_path(path)?;
self.folder_mut(path)?.layer_mut(id).ok_or(DocumentError::LayerNotFound)
}
/// Given a path to a layer, returns a vector of the indices in the layer tree
/// These indices can be used to order a list of layers
pub fn indices_for_path(&self, path: &[LayerId]) -> Result<Vec<usize>, DocumentError> {
let mut root = self.root.as_folder()?;
let mut indices = vec![];
let (path, layer_id) = split_path(path)?;
for id in path {
let pos = root.layer_ids.iter().position(|x| *x == *id).ok_or(DocumentError::LayerNotFound)?;
indices.push(pos);
root = root.folder(*id).ok_or(DocumentError::LayerNotFound)?;
}
indices.push(root.layer_ids.iter().position(|x| *x == layer_id).ok_or(DocumentError::LayerNotFound)?);
Ok(indices)
}
/// Replaces the layer at the specified `path` with `layer`.
pub fn set_layer(&mut self, path: &[LayerId], layer: Layer, insert_index: isize) -> Result<(), DocumentError> {
let mut folder = self.root.as_folder_mut()?;
let mut layer_id = None;
if let Ok((path, id)) = split_path(path) {
layer_id = Some(id);
self.mark_as_dirty(path)?;
folder = self.folder_mut(path)?;
if let Some(folder_layer) = folder.layer_mut(id) {
*folder_layer = layer;
return Ok(());
}
}
folder.add_layer(layer, layer_id, insert_index).ok_or(DocumentError::IndexOutOfBounds)?;
Ok(())
}
/// Adds a new layer to the folder specified by `path`.
/// 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<LayerId, DocumentError> {
let folder = self.folder_mut(path)?;
folder.add_layer(layer, None, insert_index).ok_or(DocumentError::IndexOutOfBounds)
}
/// Deletes the layer specified by `path`.
pub fn delete(&mut self, path: &[LayerId]) -> Result<(), DocumentError> {
let (path, id) = split_path(path)?;
self.mark_as_dirty(path)?;
self.folder_mut(path)?.remove_layer(id)
}
pub fn visible_layers(&self, path: &mut Vec<LayerId>, paths: &mut Vec<Vec<LayerId>>) -> Result<(), DocumentError> {
if !self.layer(path)?.visible {
return Ok(());
}
if let Ok(folder) = self.folder(path) {
for layer in folder.layer_ids.iter() {
path.push(*layer);
self.visible_layers(path, paths)?;
path.pop();
}
} else {
paths.push(path.clone());
}
Ok(())
}
pub fn viewport_bounding_box(&self, path: &[LayerId]) -> Result<Option<[DVec2; 2]>, DocumentError> {
let layer = self.layer(path)?;
let transform = self.multiply_transforms(path)?;
Ok(layer.data.bounding_box(transform))
}
pub fn visible_layers_bounding_box(&self) -> Option<[DVec2; 2]> {
let mut paths = vec![];
self.visible_layers(&mut vec![], &mut paths).ok()?;
self.combined_viewport_bounding_box(paths.iter().map(|x| x.as_slice()))
}
pub fn combined_viewport_bounding_box<'a>(&self, paths: impl Iterator<Item = &'a [LayerId]>) -> Option<[DVec2; 2]> {
let boxes = paths.filter_map(|path| self.viewport_bounding_box(path).ok()?);
boxes.reduce(|a, b| [a[0].min(b[0]), a[1].max(b[1])])
}
pub fn mark_upstream_as_dirty(&mut self, path: &[LayerId]) -> Result<(), DocumentError> {
let mut root = &mut self.root;
root.cache_dirty = true;
for id in path {
root = root.as_folder_mut()?.layer_mut(*id).ok_or(DocumentError::LayerNotFound)?;
root.cache_dirty = true;
}
Ok(())
}
pub fn mark_downstream_as_dirty(&mut self, path: &[LayerId]) -> Result<(), DocumentError> {
let mut layer = self.layer_mut(path)?;
layer.cache_dirty = true;
let mut path = path.to_vec();
let len = path.len();
path.push(0);
if let Some(ids) = layer.as_folder().ok().map(|f| f.layer_ids.clone()) {
for id in ids {
path[len] = id;
self.mark_downstream_as_dirty(&path)?
}
}
Ok(())
}
pub fn mark_as_dirty(&mut self, path: &[LayerId]) -> Result<(), DocumentError> {
self.mark_downstream_as_dirty(path)?;
self.mark_upstream_as_dirty(path)?;
Ok(())
}
pub fn transforms(&self, path: &[LayerId]) -> Result<Vec<DAffine2>, DocumentError> {
let mut root = &self.root;
let mut transforms = vec![self.root.transform];
for id in path {
if let Ok(folder) = root.as_folder() {
root = folder.layer(*id).ok_or(DocumentError::LayerNotFound)?;
}
transforms.push(root.transform);
}
Ok(transforms)
}
pub fn multiply_transforms(&self, path: &[LayerId]) -> Result<DAffine2, DocumentError> {
let mut root = &self.root;
let mut trans = self.root.transform;
for id in path {
if let Ok(folder) = root.as_folder() {
root = folder.layer(*id).ok_or(DocumentError::LayerNotFound)?;
}
trans = trans * root.transform;
}
Ok(trans)
}
pub fn generate_transform_across_scope(&self, from: &[LayerId], to: Option<DAffine2>) -> Result<DAffine2, DocumentError> {
let from_rev = self.multiply_transforms(from)?;
let scope = to.unwrap_or(DAffine2::IDENTITY);
Ok(scope * from_rev)
}
pub fn transform_relative_to_scope(&mut self, layer: &[LayerId], scope: Option<DAffine2>, transform: DAffine2) -> Result<(), DocumentError> {
let to = self.generate_transform_across_scope(&layer[..layer.len() - 1], scope)?;
let layer = self.layer_mut(layer)?;
layer.transform = to.inverse() * transform * to * layer.transform;
Ok(())
}
pub fn set_transform_relative_to_scope(&mut self, layer: &[LayerId], scope: Option<DAffine2>, transform: DAffine2) -> Result<(), DocumentError> {
let to = self.generate_transform_across_scope(&layer[..layer.len() - 1], scope)?;
let layer = self.layer_mut(layer)?;
layer.transform = to.inverse() * transform;
Ok(())
}
pub fn apply_transform_relative_to_viewport(&mut self, layer: &[LayerId], transform: DAffine2) -> Result<(), DocumentError> {
self.transform_relative_to_scope(layer, None, transform)
}
pub fn set_transform_relative_to_viewport(&mut self, layer: &[LayerId], transform: DAffine2) -> Result<(), DocumentError> {
self.set_transform_relative_to_scope(layer, None, transform)
}
/// Mutate the document by applying the `operation` to it. If the operation necessitates a
/// reaction from the frontend, responses may be returned.
pub fn handle_operation(&mut self, operation: &Operation) -> Result<Option<Vec<DocumentResponse>>, DocumentError> {
operation.pseudo_hash().hash(&mut self.hasher);
let responses = match &operation {
Operation::AddEllipse { path, insert_index, transform, style } => {
self.set_layer(path, Layer::new(LayerDataType::Shape(Shape::ellipse(*style)), *transform), *insert_index)?;
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::CreatedLayer { path: path.clone() }])
}
Operation::AddRect { path, insert_index, transform, style } => {
self.set_layer(path, Layer::new(LayerDataType::Shape(Shape::rectangle(*style)), *transform), *insert_index)?;
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::CreatedLayer { path: path.clone() }])
}
Operation::AddBoundingBox { path, transform, style } => {
let mut rect = Shape::rectangle(*style);
rect.render_index = -1;
self.set_layer(path, Layer::new(LayerDataType::Shape(rect), *transform), -1)?;
Some(vec![DocumentResponse::DocumentChanged])
}
Operation::AddShape {
path,
insert_index,
transform,
style,
sides,
} => {
self.set_layer(path, Layer::new(LayerDataType::Shape(Shape::shape(*sides, *style)), *transform), *insert_index)?;
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::CreatedLayer { path: path.clone() }])
}
Operation::AddLine { path, insert_index, transform, style } => {
self.set_layer(path, Layer::new(LayerDataType::Shape(Shape::line(*style)), *transform), *insert_index)?;
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::CreatedLayer { path: path.clone() }])
}
Operation::AddPen {
path,
insert_index,
points,
transform,
style,
} => {
let points: Vec<glam::DVec2> = points.iter().map(|&it| it.into()).collect();
self.set_layer(path, Layer::new(LayerDataType::Shape(Shape::poly_line(points, *style)), *transform), *insert_index)?;
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::CreatedLayer { path: path.clone() }])
}
Operation::DeleteLayer { path } => {
self.delete(path)?;
let (folder, _) = split_path(path.as_slice()).unwrap_or_else(|_| (&[], 0));
Some(vec![
DocumentResponse::DocumentChanged,
DocumentResponse::DeletedLayer { path: path.clone() },
DocumentResponse::FolderChanged { path: folder.to_vec() },
])
}
Operation::PasteLayer { path, layer, insert_index } => {
let folder = self.folder_mut(path)?;
let id = folder.add_layer(layer.clone(), None, *insert_index).ok_or(DocumentError::IndexOutOfBounds)?;
let full_path = [path.clone(), vec![id]].concat();
self.mark_as_dirty(&full_path)?;
Some(vec![
DocumentResponse::DocumentChanged,
DocumentResponse::CreatedLayer { path: full_path },
DocumentResponse::FolderChanged { path: path.clone() },
])
}
Operation::DuplicateLayer { path } => {
let layer = self.layer(path)?.clone();
let (folder_path, _) = split_path(path.as_slice()).unwrap_or_else(|_| (&[], 0));
let folder = self.folder_mut(folder_path)?;
folder.add_layer(layer, None, -1).ok_or(DocumentError::IndexOutOfBounds)?;
self.mark_as_dirty(&path[..path.len() - 1])?;
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::FolderChanged { path: folder_path.to_vec() }])
}
Operation::RenameLayer { path, name } => {
self.layer_mut(path)?.name = Some(name.clone());
Some(vec![DocumentResponse::LayerChanged { path: path.clone() }])
}
Operation::AddFolder { path } => {
self.set_layer(path, Layer::new(LayerDataType::Folder(Folder::default()), DAffine2::IDENTITY.to_cols_array()), -1)?;
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::FolderChanged { path: path.clone() }])
}
Operation::TransformLayer { path, transform } => {
let layer = self.layer_mut(path).unwrap();
let transform = DAffine2::from_cols_array(transform) * layer.transform;
layer.transform = transform;
self.mark_as_dirty(path)?;
Some(vec![DocumentResponse::DocumentChanged])
}
Operation::TransformLayerInViewport { path, transform } => {
let transform = DAffine2::from_cols_array(transform);
self.apply_transform_relative_to_viewport(path, transform)?;
self.mark_as_dirty(path)?;
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::LayerChanged { path: path.clone() }])
}
Operation::SetLayerTransformInViewport { path, transform } => {
let transform = DAffine2::from_cols_array(transform);
self.set_transform_relative_to_viewport(path, transform)?;
self.mark_as_dirty(path)?;
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::LayerChanged { path: path.clone() }])
}
Operation::TransformLayerInScope { path, transform, scope } => {
let transform = DAffine2::from_cols_array(transform);
let scope = DAffine2::from_cols_array(scope);
self.transform_relative_to_scope(path, Some(scope), transform)?;
self.mark_as_dirty(path)?;
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::LayerChanged { path: path.clone() }])
}
Operation::SetLayerTransformInScope { path, transform, scope } => {
let transform = DAffine2::from_cols_array(transform);
let scope = DAffine2::from_cols_array(scope);
self.set_transform_relative_to_scope(path, Some(scope), transform)?;
self.mark_as_dirty(path)?;
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::LayerChanged { path: path.clone() }])
}
Operation::SetLayerTransform { path, transform } => {
let transform = DAffine2::from_cols_array(transform);
let layer = self.layer_mut(path)?;
layer.transform = transform;
self.mark_as_dirty(path)?;
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::LayerChanged { path: path.clone() }])
}
Operation::ToggleVisibility { path } => {
self.mark_as_dirty(path)?;
if let Ok(layer) = self.layer_mut(path) {
layer.visible = !layer.visible;
}
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::LayerChanged { path: path.clone() }])
}
Operation::SetLayerBlendMode { path, blend_mode } => {
self.mark_as_dirty(path)?;
self.layer_mut(path)?.blend_mode = *blend_mode;
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::LayerChanged { path: path.clone() }])
}
Operation::SetLayerOpacity { path, opacity } => {
self.mark_as_dirty(path)?;
self.layer_mut(path)?.opacity = *opacity;
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::LayerChanged { path: path.clone() }])
}
Operation::FillLayer { path, color } => {
let layer = self.layer_mut(path)?;
match &mut layer.data {
LayerDataType::Shape(s) => s.style.set_fill(layers::style::Fill::new(*color)),
_ => return Err(DocumentError::NotAShape),
}
self.mark_as_dirty(path)?;
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::LayerChanged { path: path.clone() }])
}
};
Ok(responses)
}
}

View File

@@ -0,0 +1,80 @@
use std::ops::Mul;
use glam::{DAffine2, DVec2};
use kurbo::{BezPath, Line, PathSeg, Point, Shape, Vec2};
#[derive(Debug, Clone, Default, Copy)]
pub struct Quad([DVec2; 4]);
impl Quad {
pub fn from_box(bbox: [DVec2; 2]) -> Self {
let size = bbox[1] - bbox[0];
Self([bbox[0], bbox[0] + size * DVec2::X, bbox[0] + size * DVec2::Y, bbox[1]])
}
pub fn lines(&self) -> [Line; 4] {
[
Line::new(to_point(self.0[0]), to_point(self.0[1])),
Line::new(to_point(self.0[1]), to_point(self.0[2])),
Line::new(to_point(self.0[2]), to_point(self.0[3])),
Line::new(to_point(self.0[3]), to_point(self.0[0])),
]
}
}
impl Mul<Quad> for DAffine2 {
type Output = Quad;
fn mul(self, rhs: Quad) -> Self::Output {
let mut output = Quad::default();
for (i, point) in rhs.0.iter().enumerate() {
output.0[i] = self.transform_point2(*point);
}
output
}
}
fn to_point(vec: DVec2) -> Point {
Point::new(vec.x, vec.y)
}
pub fn intersect_quad_bez_path(quad: Quad, shape: &BezPath, closed: bool) -> bool {
// check if outlines intersect
if shape.segments().any(|path_segment| quad.lines().iter().any(|line| !path_segment.intersect_line(*line).is_empty())) {
return true;
}
// check if selection is entirely within the shape
if closed && quad.0.iter().any(|q| shape.contains(to_point(*q))) {
return true;
}
// check if shape is entirely within the selection
if let Some(shape_point) = get_arbitrary_point_on_path(shape) {
let mut pos = 0;
let mut neg = 0;
for line in quad.lines() {
if line.p0 == shape_point {
return true;
};
let line_vec = Vec2::new(line.p1.x - line.p0.x, line.p1.y - line.p0.y);
let point_vec = Vec2::new(line.p1.x - shape_point.x, line.p1.y - shape_point.y);
let cross = line_vec.cross(point_vec);
if cross > 0.0 {
pos += 1;
} else if cross < 0.0 {
neg += 1;
}
if pos > 0 && neg > 0 {
return false;
}
}
}
true
}
pub fn get_arbitrary_point_on_path(path: &BezPath) -> Option<Point> {
path.segments().next().map(|seg| match seg {
PathSeg::Line(line) => line.p0,
PathSeg::Quad(quad) => quad.p0,
PathSeg::Cubic(cubic) => cubic.p0,
})
}

View File

@@ -0,0 +1,44 @@
use serde::{Deserialize, Serialize};
#[derive(PartialEq, Copy, Clone, Debug, Serialize, Deserialize)]
pub enum BlendMode {
Normal,
Multiply,
Darken,
ColorBurn,
Screen,
Lighten,
ColorDodge,
Overlay,
SoftLight,
HardLight,
Difference,
Exclusion,
Hue,
Saturation,
Color,
Luminosity,
}
impl BlendMode {
pub fn to_svg_style_name(&self) -> &str {
match self {
BlendMode::Normal => "normal",
BlendMode::Multiply => "multiply",
BlendMode::Darken => "darken",
BlendMode::ColorBurn => "color-burn",
BlendMode::Screen => "screen",
BlendMode::Lighten => "lighten",
BlendMode::ColorDodge => "color-dodge",
BlendMode::Overlay => "overlay",
BlendMode::SoftLight => "soft-light",
BlendMode::HardLight => "hard-light",
BlendMode::Difference => "difference",
BlendMode::Exclusion => "exclusion",
BlendMode::Hue => "hue",
BlendMode::Saturation => "saturation",
BlendMode::Color => "color",
BlendMode::Luminosity => "luminosity",
}
}
}

View File

@@ -0,0 +1,121 @@
use glam::DVec2;
use crate::{DocumentError, LayerId, Quad};
use super::{Layer, LayerData, LayerDataType};
use serde::{Deserialize, Serialize};
use std::fmt::Write;
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
pub struct Folder {
next_assignment_id: LayerId,
pub layer_ids: Vec<LayerId>,
layers: Vec<Layer>,
}
impl LayerData for Folder {
fn render(&mut self, svg: &mut String, transforms: &mut Vec<glam::DAffine2>) {
for layer in &mut self.layers {
let _ = writeln!(svg, "{}", layer.render(transforms));
}
}
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>) {
for (layer, layer_id) in self.layers().iter().zip(&self.layer_ids) {
path.push(*layer_id);
layer.intersects_quad(quad, path, intersections);
path.pop();
}
}
fn bounding_box(&self, transform: glam::DAffine2) -> Option<[DVec2; 2]> {
self.layers
.iter()
.filter_map(|layer| layer.data.bounding_box(transform * layer.transform))
.reduce(|a, b| [a[0].min(b[0]), a[1].max(b[1])])
}
}
impl Folder {
/// When a insertion id is provided, try to insert the layer with the given id.
/// If that id is already used, return None.
/// When no insertion id is provided, search for the next free id and insert it with that.
pub fn add_layer(&mut self, layer: Layer, id: Option<LayerId>, insert_index: isize) -> Option<LayerId> {
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 {
if let Some(id) = id {
self.next_assignment_id = id;
}
if self.layer_ids.contains(&self.next_assignment_id) {
return None;
}
let id = self.next_assignment_id;
self.layers.insert(insert_index as usize, layer);
self.layer_ids.insert(insert_index as usize, id);
// Linear probing for collision avoidance
while self.layer_ids.contains(&self.next_assignment_id) {
self.next_assignment_id += 1;
}
Some(id)
} else {
None
}
}
pub fn remove_layer(&mut self, id: LayerId) -> Result<(), DocumentError> {
let pos = self.position_of_layer(id)?;
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()
}
pub fn layers(&self) -> &[Layer] {
self.layers.as_slice()
}
pub fn layers_mut(&mut self) -> &mut [Layer] {
self.layers.as_mut_slice()
}
pub fn layer(&self, id: LayerId) -> Option<&Layer> {
let pos = self.position_of_layer(id).ok()?;
Some(&self.layers[pos])
}
pub fn layer_mut(&mut self, id: LayerId) -> Option<&mut Layer> {
let pos = self.position_of_layer(id).ok()?;
Some(&mut self.layers[pos])
}
pub fn position_of_layer(&self, layer_id: LayerId) -> Result<usize, DocumentError> {
self.layer_ids.iter().position(|x| *x == layer_id).ok_or(DocumentError::LayerNotFound)
}
pub fn folder(&self, id: LayerId) -> Option<&Folder> {
match self.layer(id) {
Some(Layer {
data: LayerDataType::Folder(folder), ..
}) => Some(folder),
_ => None,
}
}
pub fn folder_mut(&mut self, id: LayerId) -> Option<&mut Folder> {
match self.layer_mut(id) {
Some(Layer {
data: LayerDataType::Folder(folder), ..
}) => Some(folder),
_ => None,
}
}
}

164
graphene/src/layers/mod.rs Normal file
View File

@@ -0,0 +1,164 @@
pub mod style;
use glam::DAffine2;
use glam::{DMat2, DVec2};
pub mod blend_mode;
pub use blend_mode::BlendMode;
pub mod simple_shape;
pub use simple_shape::Shape;
pub mod folder;
use crate::LayerId;
use crate::{DocumentError, Quad};
pub use folder::Folder;
use serde::{Deserialize, Serialize};
use std::fmt::Write;
pub trait LayerData {
fn render(&mut self, svg: &mut String, transforms: &mut Vec<glam::DAffine2>);
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>);
fn bounding_box(&self, transform: glam::DAffine2) -> Option<[DVec2; 2]>;
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub enum LayerDataType {
Folder(Folder),
Shape(Shape),
}
impl LayerDataType {
pub fn inner(&self) -> &dyn LayerData {
match self {
LayerDataType::Shape(s) => s,
LayerDataType::Folder(f) => f,
}
}
pub fn inner_mut(&mut self) -> &mut dyn LayerData {
match self {
LayerDataType::Shape(s) => s,
LayerDataType::Folder(f) => f,
}
}
}
impl LayerData for LayerDataType {
fn render(&mut self, svg: &mut String, transforms: &mut Vec<glam::DAffine2>) {
self.inner_mut().render(svg, transforms)
}
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>) {
self.inner().intersects_quad(quad, path, intersections)
}
fn bounding_box(&self, transform: glam::DAffine2) -> Option<[DVec2; 2]> {
self.inner().bounding_box(transform)
}
}
#[derive(Serialize, Deserialize)]
#[serde(remote = "glam::DAffine2")]
struct DAffine2Ref {
pub matrix2: DMat2,
pub translation: DVec2,
}
#[derive(Debug, PartialEq, Deserialize, Serialize)]
pub struct Layer {
pub visible: bool,
pub name: Option<String>,
pub data: LayerDataType,
#[serde(with = "DAffine2Ref")]
pub transform: glam::DAffine2,
pub cache: String,
pub thumbnail_cache: String,
pub cache_dirty: bool,
pub blend_mode: BlendMode,
pub opacity: f64,
}
impl Layer {
pub fn new(data: LayerDataType, transform: [f64; 6]) -> Self {
Self {
visible: true,
name: None,
data,
transform: glam::DAffine2::from_cols_array(&transform),
cache: String::new(),
thumbnail_cache: String::new(),
cache_dirty: true,
blend_mode: BlendMode::Normal,
opacity: 1.,
}
}
pub fn render(&mut self, transforms: &mut Vec<DAffine2>) -> &str {
if !self.visible {
return "";
}
if self.cache_dirty {
transforms.push(self.transform);
self.thumbnail_cache.clear();
self.data.render(&mut self.thumbnail_cache, transforms);
self.cache.clear();
let _ = writeln!(self.cache, r#"<g transform="matrix("#);
self.transform.to_cols_array().iter().enumerate().for_each(|(i, f)| {
let _ = self.cache.write_str(&(f.to_string() + if i != 5 { "," } else { "" }));
});
let _ = write!(
self.cache,
r#")" style="mix-blend-mode: {}; opacity: {}">{}</g>"#,
self.blend_mode.to_svg_style_name(),
self.opacity,
self.thumbnail_cache.as_str()
);
transforms.pop();
self.cache_dirty = false;
}
self.cache.as_str()
}
pub fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>) {
if !self.visible {
return;
}
let transformed_quad = self.transform.inverse() * quad;
self.data.intersects_quad(transformed_quad, path, intersections)
}
pub fn current_bounding_box(&self) -> Option<[DVec2; 2]> {
self.data.bounding_box(self.transform)
}
pub fn as_folder_mut(&mut self) -> Result<&mut Folder, DocumentError> {
match &mut self.data {
LayerDataType::Folder(f) => Ok(f),
_ => Err(DocumentError::NotAFolder),
}
}
pub fn as_folder(&self) -> Result<&Folder, DocumentError> {
match &self.data {
LayerDataType::Folder(f) => Ok(f),
_ => Err(DocumentError::NotAFolder),
}
}
}
impl Clone for Layer {
fn clone(&self) -> Self {
Self {
visible: self.visible,
name: self.name.clone(),
data: self.data.clone(),
transform: self.transform,
cache: String::new(),
thumbnail_cache: String::new(),
cache_dirty: true,
blend_mode: self.blend_mode,
opacity: self.opacity,
}
}
}

View File

@@ -0,0 +1,146 @@
use glam::DAffine2;
use glam::DMat2;
use glam::DVec2;
use kurbo::Affine;
use kurbo::Shape as KurboShape;
use crate::intersection::intersect_quad_bez_path;
use crate::LayerId;
use crate::Quad;
use kurbo::BezPath;
use super::style;
use super::style::PathStyle;
use super::LayerData;
use serde::{Deserialize, Serialize};
use std::fmt::Write;
fn glam_to_kurbo(transform: DAffine2) -> Affine {
Affine::new(transform.to_cols_array())
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct Shape {
pub path: BezPath,
pub style: style::PathStyle,
pub render_index: i32,
pub solid: bool,
}
impl LayerData for Shape {
fn render(&mut self, svg: &mut String, transforms: &mut Vec<DAffine2>) {
let mut path = self.path.clone();
let transform = self.transform(transforms);
let inverse = transform.inverse();
if !inverse.is_finite() {
let _ = write!(svg, "<!-- SVG shape has an invalid transform -->");
return;
}
path.apply_affine(glam_to_kurbo(transform));
let _ = writeln!(svg, r#"<g transform="matrix("#);
inverse.to_cols_array().iter().enumerate().for_each(|(i, entry)| {
let _ = svg.write_str(&(entry.to_string() + if i != 5 { "," } else { "" }));
});
let _ = svg.write_str(r#")">"#);
let _ = write!(svg, r#"<path d="{}" {} />"#, path.to_svg(), self.style.render());
let _ = svg.write_str("</g>");
}
fn bounding_box(&self, transform: glam::DAffine2) -> Option<[DVec2; 2]> {
let mut path = self.path.clone();
if transform.matrix2 == DMat2::ZERO {
return None;
}
path.apply_affine(glam_to_kurbo(transform));
use kurbo::Shape;
let kurbo::Rect { x0, y0, x1, y1 } = path.bounding_box();
Some([(x0, y0).into(), (x1, y1).into()])
}
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>) {
if intersect_quad_bez_path(quad, &self.path, self.solid) {
intersections.push(path.clone());
}
}
}
impl Shape {
pub fn transform(&self, transforms: &[DAffine2]) -> DAffine2 {
let start = match self.render_index {
-1 => 0,
x => (transforms.len() as i32 - x).max(0) as usize,
};
transforms.iter().skip(start).cloned().reduce(|a, b| a * b).unwrap_or(DAffine2::IDENTITY)
}
pub fn shape(sides: u8, style: PathStyle) -> Self {
use std::f64::consts::{FRAC_PI_2, TAU};
fn unit_rotation(theta: f64) -> DVec2 {
DVec2::new(theta.sin(), theta.cos())
}
let mut path = kurbo::BezPath::new();
let apothem_offset_angle = TAU / (sides as f64);
// Rotate odd sided shapes by 90 degrees
let offset = ((sides + 1) % 2) as f64 * FRAC_PI_2;
let relative_points = (0..sides).map(|i| apothem_offset_angle * i as f64 + offset).map(unit_rotation);
let min = relative_points.clone().reduce(|a, b| a.min(b)).unwrap_or_default();
let transform = DAffine2::from_scale_angle_translation(DVec2::ONE / 2., 0., -min / 2.);
let point = |vec: DVec2| kurbo::Point::new(vec.x, vec.y);
let mut relative_points = relative_points.map(|p| point(transform.transform_point2(p)));
path.move_to(relative_points.next().expect("Tried to create an ngon with 0 sides"));
relative_points.for_each(|p| path.line_to(p));
path.close_path();
Self {
path,
style,
render_index: 1,
solid: true,
}
}
pub fn rectangle(style: PathStyle) -> Self {
Self {
path: kurbo::Rect::new(0., 0., 1., 1.).to_path(0.01),
style,
render_index: 1,
solid: true,
}
}
pub fn ellipse(style: PathStyle) -> Self {
Self {
path: kurbo::Ellipse::from_rect(kurbo::Rect::new(0., 0., 1., 1.)).to_path(0.01),
style,
render_index: 1,
solid: true,
}
}
pub fn line(style: PathStyle) -> Self {
Self {
path: kurbo::Line::new((0., 0.), (1., 0.)).to_path(0.01),
style,
render_index: 1,
solid: true,
}
}
pub fn poly_line(points: Vec<impl Into<glam::DVec2>>, style: PathStyle) -> Self {
let mut path = kurbo::BezPath::new();
points
.into_iter()
.map(|v| v.into())
.map(|v: DVec2| 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) });
Self {
path,
style,
render_index: 0,
solid: false,
}
}
}

View File

@@ -0,0 +1,99 @@
use crate::color::Color;
use serde::{Deserialize, Serialize};
const OPACITY_PRECISION: usize = 3;
fn format_opacity(name: &str, opacity: f32) -> String {
if (opacity - 1.).abs() > 10f32.powi(-(OPACITY_PRECISION as i32)) {
format!(r#" {}-opacity="{:.precision$}""#, name, opacity, precision = OPACITY_PRECISION)
} else {
String::new()
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
pub struct Fill {
color: Option<Color>,
}
impl Fill {
pub fn new(color: Color) -> Self {
Self { color: Some(color) }
}
pub fn color(&self) -> Option<Color> {
self.color
}
pub fn none() -> Self {
Self { color: None }
}
pub fn render(&self) -> String {
match self.color {
Some(c) => format!(r##" fill="#{}"{}"##, c.rgb_hex(), format_opacity("fill", c.a())),
None => r#" fill="none""#.to_string(),
}
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
pub struct Stroke {
color: Color,
width: f32,
}
impl Stroke {
pub fn new(color: Color, width: f32) -> Self {
Self { color, width }
}
pub fn color(&self) -> Color {
self.color
}
pub fn width(&self) -> f32 {
self.width
}
pub fn render(&self) -> String {
format!(r##" stroke="#{}"{} stroke-width="{}""##, self.color.rgb_hex(), format_opacity("stroke", self.color.a()), self.width)
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
pub struct PathStyle {
stroke: Option<Stroke>,
fill: Option<Fill>,
}
impl PathStyle {
pub fn new(stroke: Option<Stroke>, fill: Option<Fill>) -> Self {
Self { stroke, fill }
}
pub fn fill(&self) -> Option<Fill> {
self.fill
}
pub fn stroke(&self) -> Option<Stroke> {
self.stroke
}
pub fn set_fill(&mut self, fill: Fill) {
self.fill = Some(fill);
}
pub fn set_stroke(&mut self, stroke: Stroke) {
self.stroke = Some(stroke);
}
pub fn clear_fill(&mut self) {
self.fill = None;
}
pub fn clear_stroke(&mut self) {
self.stroke = None;
}
pub fn render(&self) -> String {
format!(
"{}{}",
match self.fill {
Some(fill) => fill.render(),
None => String::new(),
},
match self.stroke {
Some(stroke) => stroke.render(),
None => String::new(),
},
)
}
}

22
graphene/src/lib.rs Normal file
View File

@@ -0,0 +1,22 @@
pub mod color;
pub mod document;
pub mod intersection;
pub mod layers;
pub mod operation;
pub mod response;
pub use intersection::Quad;
pub use operation::Operation;
pub use response::DocumentResponse;
pub type LayerId = u64;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum DocumentError {
LayerNotFound,
InvalidPath,
IndexOutOfBounds,
NotAFolder,
NonReorderableSelection,
NotAShape,
}

134
graphene/src/operation.rs Normal file
View File

@@ -0,0 +1,134 @@
use std::{
collections::hash_map::DefaultHasher,
hash::{Hash, Hasher},
};
use crate::{
color::Color,
layers::{style, BlendMode, Layer},
LayerId,
};
use serde::{Deserialize, Serialize};
#[repr(C)]
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub enum Operation {
AddEllipse {
path: Vec<LayerId>,
insert_index: isize,
transform: [f64; 6],
style: style::PathStyle,
},
AddRect {
path: Vec<LayerId>,
insert_index: isize,
transform: [f64; 6],
style: style::PathStyle,
},
AddBoundingBox {
path: Vec<LayerId>,
transform: [f64; 6],
style: style::PathStyle,
},
AddLine {
path: Vec<LayerId>,
insert_index: isize,
transform: [f64; 6],
style: style::PathStyle,
},
AddPen {
path: Vec<LayerId>,
transform: [f64; 6],
insert_index: isize,
points: Vec<(f64, f64)>,
style: style::PathStyle,
},
AddShape {
path: Vec<LayerId>,
insert_index: isize,
transform: [f64; 6],
sides: u8,
style: style::PathStyle,
},
DeleteLayer {
path: Vec<LayerId>,
},
DuplicateLayer {
path: Vec<LayerId>,
},
RenameLayer {
path: Vec<LayerId>,
name: String,
},
PasteLayer {
layer: Layer,
path: Vec<LayerId>,
insert_index: isize,
},
AddFolder {
path: Vec<LayerId>,
},
TransformLayer {
path: Vec<LayerId>,
transform: [f64; 6],
},
TransformLayerInViewport {
path: Vec<LayerId>,
transform: [f64; 6],
},
SetLayerTransformInViewport {
path: Vec<LayerId>,
transform: [f64; 6],
},
TransformLayerInScope {
path: Vec<LayerId>,
transform: [f64; 6],
scope: [f64; 6],
},
SetLayerTransformInScope {
path: Vec<LayerId>,
transform: [f64; 6],
scope: [f64; 6],
},
SetLayerTransform {
path: Vec<LayerId>,
transform: [f64; 6],
},
ToggleVisibility {
path: Vec<LayerId>,
},
SetLayerBlendMode {
path: Vec<LayerId>,
blend_mode: BlendMode,
},
SetLayerOpacity {
path: Vec<LayerId>,
opacity: f64,
},
FillLayer {
path: Vec<LayerId>,
color: Color,
},
}
impl Operation {
/// Returns the byte representation of the message.
///
/// # Safety
/// This function reads from uninitialized memory!!!
/// Only use if you know what you are doing
unsafe fn as_slice(&self) -> &[u8] {
core::slice::from_raw_parts(self as *const Operation as *const u8, std::mem::size_of::<Operation>())
}
/// Returns a pseudo hash that should uniquely identify the operation.
/// This is needed because `Hash` is not implemented for f64s
///
/// # Safety
/// This function reads from uninitialized memory but the generated value should be fine.
pub fn pseudo_hash(&self) -> u64 {
let mut s = DefaultHasher::new();
unsafe { self.as_slice() }.hash(&mut s);
s.finish()
}
}

27
graphene/src/response.rs Normal file
View File

@@ -0,0 +1,27 @@
use crate::LayerId;
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[repr(C)]
pub enum DocumentResponse {
DocumentChanged,
FolderChanged { path: Vec<LayerId> },
CreatedLayer { path: Vec<LayerId> },
DeletedLayer { path: Vec<LayerId> },
LayerChanged { path: Vec<LayerId> },
}
impl fmt::Display for DocumentResponse {
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
let name = match self {
DocumentResponse::DocumentChanged { .. } => "DocumentChanged",
DocumentResponse::FolderChanged { .. } => "FolderChanged",
DocumentResponse::CreatedLayer { .. } => "CreatedLayer",
DocumentResponse::LayerChanged { .. } => "LayerChanged",
DocumentResponse::DeletedLayer { .. } => "DeleteLayer",
};
formatter.write_str(name)
}
}