Add documentation to many parts of the Rust codebase (#552)

* add lots of doccomments

* add conversion traits from layerdatatypes to layers

* add suggested doc improvements

* Code review changes

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Alaska 2022-04-09 08:56:58 +02:00 committed by Keavon Chambers
parent 1455ac3dce
commit 5c99cdef7f
13 changed files with 470 additions and 21 deletions

View File

@ -16,10 +16,13 @@ use std::cmp::max;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
/// A number that identifies a layer.
/// This does not technically need to be unique globally, only within a folder.
pub type LayerId = u64;
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Document {
/// The root layer, usually a [FolderLayer](layers::folder_layer::FolderLayer) that contains all other [Layers](layers::layer_info::Layer).
pub root: Layer,
/// The state_identifier serves to provide a way to uniquely identify a particular state that the document is in.
/// This identifier is not a hash and is not guaranteed to be equal for equivalent documents.
@ -338,6 +341,7 @@ impl Document {
boxes.reduce(|a, b| [a[0].min(b[0]), a[1].max(b[1])])
}
/// Mark the layer at the provided path, as well as all the folders containing it, as dirty.
pub fn mark_upstream_as_dirty(&mut self, path: &[LayerId]) -> Result<(), DocumentError> {
let mut root = &mut self.root;
root.cache_dirty = true;

View File

@ -1,6 +1,7 @@
use super::LayerId;
use crate::boolean_ops::BooleanOperationError;
/// A set of different errors that can occur when using Graphene.
#[derive(Debug, Clone, PartialEq)]
pub enum DocumentError {
LayerNotFound(Vec<LayerId>),

View File

@ -10,14 +10,17 @@ use glam::{DAffine2, DMat2, DVec2};
use kurbo::{BezPath, CubicBez, Line, ParamCurve, ParamCurveExtrema, PathSeg, Point, QuadBez, Rect, Shape, Vec2};
#[derive(Debug, Clone, Default, Copy)]
/// A quad defined by four vertices.
pub struct Quad([DVec2; 4]);
impl Quad {
/// Convert a box defined by two corner points to a quad.
pub fn from_box(bbox: [DVec2; 2]) -> Self {
let size = bbox[1] - bbox[0];
Self([bbox[0], bbox[0] + size * DVec2::X, bbox[1], bbox[0] + size * DVec2::Y])
}
/// Get all the edges in the quad.
pub fn lines(&self) -> [Line; 4] {
[
Line::new(to_point(self.0[0]), to_point(self.0[1])),
@ -27,6 +30,7 @@ impl Quad {
]
}
/// Generate a [BezPath] of the quad
pub fn path(&self) -> BezPath {
let mut path = kurbo::BezPath::new();
path.move_to(to_point(self.0[0]));
@ -54,6 +58,11 @@ fn to_point(vec: DVec2) -> Point {
Point::new(vec.x, vec.y)
}
/// Return `true` if `quad` intersects `shape`.
/// This is the case if any of the following conditions are true:
/// * the edges of `quad` and `shape` intersect
/// * `shape` is entirely contained within `quad`
/// * `filled` is `true` and `quad` is entirely contained within `shape`.
pub fn intersect_quad_bez_path(quad: Quad, shape: &BezPath, filled: bool) -> bool {
let mut shape = shape.clone();
// for filled shapes act like shape was closed even if it isn't
@ -74,6 +83,8 @@ pub fn intersect_quad_bez_path(quad: Quad, shape: &BezPath, filled: bool) -> boo
get_arbitrary_point_on_path(&shape).map(|shape_point| quad.path().contains(shape_point)).unwrap_or_default()
}
/// Returns a point on `path`.
/// This function will usually return the first point from the path's first segment, but callers should not rely on this behavior.
pub fn get_arbitrary_point_on_path(path: &BezPath) -> Option<Point> {
path.segments().next().map(|seg| match seg {
PathSeg::Line(line) => line.p0,
@ -640,7 +651,7 @@ pub fn quad_line_intersect(a: &Line, b: &QuadBez) -> [Option<f64>; 2] {
}
/// Returns real roots to cubic equation: `f(t) = a0 + t*a1 + t^2*a2 + t^3*a3`.
/// This function uses the Cardano-Viete and Numerical Recipes algorithm, found here: https://quarticequations.com/Cubic.pdf
/// This function uses the Cardano-Viete and Numerical Recipes algorithm, found here: <https://quarticequations.com/Cubic.pdf>
pub fn cubic_real_roots(mut a0: f64, mut a1: f64, mut a2: f64, a3: f64) -> [Option<f64>; 3] {
use std::f64::consts::FRAC_PI_3 as PI_3;

View File

@ -1,5 +1,7 @@
use serde::{Deserialize, Serialize};
/// Describes how overlapping SVG elements should be blended together.
/// See the [MDN Docs](https://developer.mozilla.org/en-US/docs/Web/CSS/blend-mode#examples) for examples.
#[derive(PartialEq, Copy, Clone, Debug, Serialize, Deserialize)]
pub enum BlendMode {
Normal,
@ -21,6 +23,8 @@ pub enum BlendMode {
}
impl BlendMode {
/// Convert the enum to the CSS string for the blend mode.
/// [Read more](https://developer.mozilla.org/en-US/docs/Web/CSS/blend-mode#values)
pub fn to_svg_style_name(&self) -> &str {
match self {
BlendMode::Normal => "normal",

View File

@ -7,10 +7,16 @@ use glam::DVec2;
use serde::{Deserialize, Serialize};
use std::fmt::Write;
/// A layer that encapsulates other layers, including potentially more folders.
/// The contained layers are rendered in the same order they are
/// stored in the [layers](FolderLayer::layers) field.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
pub struct FolderLayer {
/// The ID that will be assigned to the next layer that is added to the folder
next_assignment_id: LayerId,
/// The IDs of the [Layer]s contained within the Folder
pub layer_ids: Vec<LayerId>,
/// The [Layer]s contained in the folder
layers: Vec<Layer>,
}
@ -38,12 +44,29 @@ impl LayerData for FolderLayer {
}
impl FolderLayer {
/// 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.
/// Negative values for insert_index represent distance from the end
/// 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.
/// Negative values for `insert_index` represent distance from the end
///
/// # Example
/// ```
/// # use graphite_graphene::layers::shape_layer::ShapeLayer;
/// # use graphite_graphene::layers::folder_layer::FolderLayer;
/// # use graphite_graphene::layers::style::PathStyle;
/// # use graphite_graphene::layers::layer_info::LayerDataType;
/// let mut folder = FolderLayer::default();
///
/// // Create two layers to be added to the folder
/// let mut shape_layer = ShapeLayer::rectangle(PathStyle::default());
/// let mut folder_layer = FolderLayer::default();
///
/// folder.add_layer(shape_layer.into(), None, -1);
/// folder.add_layer(folder_layer.into(), Some(123), 0);
/// ```
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;
}
@ -71,6 +94,24 @@ impl FolderLayer {
}
}
/// Remove a layer with a given ID from the folder.
/// This operation will fail if `id` is not present in the folder.
///
/// # Example
/// ```
/// # use graphite_graphene::layers::folder_layer::FolderLayer;
/// let mut folder = FolderLayer::default();
///
/// // Try to remove a layer that does not exist
/// assert!(folder.remove_layer(123).is_err());
///
/// // Add another folder to the folder
/// folder.add_layer(FolderLayer::default().into(), Some(123), -1);
///
/// // Try to remove that folder again
/// assert!(folder.remove_layer(123).is_ok());
/// assert_eq!(folder.layers().len(), 0)
/// ```
pub fn remove_layer(&mut self, id: LayerId) -> Result<(), DocumentError> {
let pos = self.position_of_layer(id)?;
self.layers.remove(pos);
@ -78,15 +119,17 @@ impl FolderLayer {
Ok(())
}
/// Returns a list of layers in the folder
/// Returns a list of [LayerId]s in the folder.
pub fn list_layers(&self) -> &[LayerId] {
self.layer_ids.as_slice()
}
/// Get references to all the [Layer]s in the folder.
pub fn layers(&self) -> &[Layer] {
self.layers.as_slice()
}
/// Get mutable references to all the [Layer]s in the folder.
pub fn layers_mut(&mut self) -> &mut [Layer] {
self.layers.as_mut_slice()
}
@ -101,14 +144,70 @@ impl FolderLayer {
Some(&mut self.layers[pos])
}
/// Returns `true` if the folder contains a layer with the given [LayerId].
///
/// # Example
/// ```
/// # use graphite_graphene::layers::folder_layer::FolderLayer;
/// let mut folder = FolderLayer::default();
///
/// // Search for an id that does not exist
/// assert!(!folder.folder_contains(123));
///
/// // Add layer with the id "123" to the folder
/// folder.add_layer(FolderLayer::default().into(), Some(123), -1);
///
/// // Search for the id "123"
/// assert!(folder.folder_contains(123));
/// ```
pub fn folder_contains(&self, id: LayerId) -> bool {
self.layer_ids.contains(&id)
}
/// Tries to find the index of a layer with the given [LayerId] within the folder.
/// This operation will fail if no layer with a matching ID is present in the folder.
///
/// # Example
/// ```
/// # use graphite_graphene::layers::folder_layer::FolderLayer;
/// let mut folder = FolderLayer::default();
///
/// // Search for an id that does not exist
/// assert!(folder.position_of_layer(123).is_err());
///
/// // Add layer with the id "123" to the folder
/// folder.add_layer(FolderLayer::default().into(), Some(123), -1);
/// folder.add_layer(FolderLayer::default().into(), Some(42), -1);
///
/// assert_eq!(folder.position_of_layer(123), Ok(0));
/// assert_eq!(folder.position_of_layer(42), Ok(1));
/// ```
pub fn position_of_layer(&self, layer_id: LayerId) -> Result<usize, DocumentError> {
self.layer_ids.iter().position(|x| *x == layer_id).ok_or_else(|| DocumentError::LayerNotFound([layer_id].into()))
}
/// Tries to get a reference to a folder with the given [LayerId].
/// This operation will return `None` if either no layer with `id` exists
/// in the folder, or the layer with matching ID is not a folder.
///
/// # Example
/// ```
/// # use graphite_graphene::layers::folder_layer::FolderLayer;
/// # use graphite_graphene::layers::shape_layer::ShapeLayer;
/// # use graphite_graphene::layers::style::PathStyle;
/// let mut folder = FolderLayer::default();
///
/// // Search for an id that does not exist
/// assert!(folder.folder(132).is_none());
///
/// // add a folder and search for it
/// folder.add_layer(FolderLayer::default().into(), Some(123), -1);
/// assert!(folder.folder(123).is_some());
///
/// // add a non-folder layer and search for it
/// folder.add_layer(ShapeLayer::rectangle(PathStyle::default()).into(), Some(42), -1);
/// assert!(folder.folder(42).is_none());
/// ```
pub fn folder(&self, id: LayerId) -> Option<&FolderLayer> {
match self.layer(id) {
Some(Layer {
@ -118,6 +217,10 @@ impl FolderLayer {
}
}
/// Tries to get a mutable reference to folder with the given `id`.
/// This operation will return `None` if either no layer with `id` exists
/// in the folder or the layer with matching ID is not a folder.
/// See the [FolderLayer::folder] method for a usage example.
pub fn folder_mut(&mut self, id: LayerId) -> Option<&mut FolderLayer> {
match self.layer_mut(id) {
Some(Layer {

View File

@ -13,10 +13,15 @@ use serde::{Deserialize, Serialize};
use std::fmt::Write;
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
/// Represents different types of layers.
pub enum LayerDataType {
/// A layer that wraps a [FolderLayer] struct.
Folder(FolderLayer),
/// A layer that wraps a [ShapeLayer] struct.
Shape(ShapeLayer),
/// A layer that wraps a [TextLayer] struct.
Text(TextLayer),
/// A layer that wraps an [ImageLayer] struct.
Image(ImageLayer),
}
@ -40,9 +45,70 @@ impl LayerDataType {
}
}
/// Defines shared behavior for every layer type.
pub trait LayerData {
/// Render the layer as an SVG tag to a given string.
///
/// # Example
/// ```
/// # use graphite_graphene::layers::shape_layer::ShapeLayer;
/// # use graphite_graphene::layers::style::{Fill, PathStyle, ViewMode};
/// # use graphite_graphene::layers::layer_info::LayerData;
///
/// let mut shape = ShapeLayer::rectangle(PathStyle::new(None, Fill::None));
/// let mut svg = String::new();
///
/// // Render the shape without any transforms, in normal view mode
/// shape.render(&mut svg, &mut String::new(), &mut vec![], ViewMode::Normal);
///
/// assert_eq!(
/// svg,
/// "<g transform=\"matrix(\n1,-0,-0,1,-0,-0)\">\
/// <path d=\"M0 0L1 0L1 1L0 1Z\" fill=\"none\" />\
/// </g>"
/// );
/// ```
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<glam::DAffine2>, view_mode: ViewMode);
/// Determine the layers within this layer that intersect a given quad.
/// # Example
/// ```
/// # use graphite_graphene::layers::shape_layer::ShapeLayer;
/// # use graphite_graphene::layers::style::{Fill, PathStyle, ViewMode};
/// # use graphite_graphene::layers::layer_info::LayerData;
/// # use graphite_graphene::intersection::Quad;
/// # use glam::f64::{DAffine2, DVec2};
///
/// let mut shape = ShapeLayer::ellipse(PathStyle::new(None, Fill::None));
/// let shape_id = 42;
/// let mut svg = String::new();
///
/// let quad = Quad::from_box([DVec2::ZERO, DVec2::ONE]);
/// let mut intersections = vec![];
///
/// shape.intersects_quad(quad, &mut vec![shape_id], &mut intersections);
///
/// assert_eq!(intersections, vec![vec![shape_id]]);
/// ```
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>);
// TODO: this doctest fails because 0 != 1e-32, maybe assert difference < epsilon?
/// Calculate the bounding box for the layer's contents after applying a given transform.
/// # Example
/// ```no_run
/// # use graphite_graphene::layers::shape_layer::ShapeLayer;
/// # use graphite_graphene::layers::style::{Fill, PathStyle};
/// # use graphite_graphene::layers::layer_info::LayerData;
/// # use glam::f64::{DAffine2, DVec2};
/// let shape = ShapeLayer::ellipse(PathStyle::new(None, Fill::None));
///
/// // Calculate the bounding box without applying any transformations.
/// // (The identity transform maps every vector to itself.)
/// let transform = DAffine2::IDENTITY;
/// let bounding_box = shape.bounding_box(transform);
///
/// assert_eq!(bounding_box, Some([DVec2::ZERO, DVec2::ONE]));
/// ```
fn bounding_box(&self, transform: glam::DAffine2) -> Option<[DVec2; 2]>;
}
@ -67,26 +133,38 @@ struct DAffine2Ref {
pub translation: DVec2,
}
/// Utility function for providing a default boolean value to serde.
#[inline(always)]
fn return_true() -> bool {
true
}
#[derive(Debug, PartialEq, Deserialize, Serialize)]
pub struct Layer {
/// Whether the layer is currently visible or hidden.
pub visible: bool,
/// The user-given name of the layer.
pub name: Option<String>,
/// The type of layer, such as folder or shape.
pub data: LayerDataType,
/// A transformation applied to the layer (translation, rotation, scaling, and shear).
#[serde(with = "DAffine2Ref")]
pub transform: glam::DAffine2,
#[serde(skip)]
pub cache: String,
/// The cached SVG thumbnail view of the layer.
#[serde(skip)]
pub thumbnail_cache: String,
/// The cached SVG render of the layer.
#[serde(skip)]
pub cache: String,
/// The cached definition(s) used by the layer's SVG tag, placed at the top in the SVG defs tag.
#[serde(skip)]
pub svg_defs_cache: String,
/// Whether or not the [Cache](Layer::cache) and [Thumbnail Cache](Layer::thumbnail_cache) need to be updated.
#[serde(skip, default = "return_true")]
pub cache_dirty: bool,
/// The blend mode describing how this layer should composite with others underneath it.
pub blend_mode: BlendMode,
/// The opacity, in the range of 0 to 1.
pub opacity: f64,
}
@ -106,6 +184,37 @@ impl Layer {
}
}
/// Iterate over the layers encapsulated by this layer.
/// If the [Layer type](Layer::data) is not a folder, the only item in the iterator will be the layer itself.
/// If the [Layer type](Layer::data) wraps a [Folder](LayerDataType::Folder), the iterator will recursively yield all the layers contained in the folder as well as potential sub-folders.
///
/// # Example
/// ```
/// # use graphite_graphene::layers::shape_layer::ShapeLayer;
/// # use graphite_graphene::layers::layer_info::Layer;
/// # use graphite_graphene::layers::style::PathStyle;
/// # use graphite_graphene::layers::folder_layer::FolderLayer;
/// let mut root_folder = FolderLayer::default();
///
/// // Add a shape to the root folder
/// let child_1: Layer = ShapeLayer::rectangle(PathStyle::default()).into();
/// root_folder.add_layer(child_1.clone(), None, -1);
///
/// // Add a folder containing another shape to the root layer
/// let mut child_folder = FolderLayer::default();
/// let grandchild: Layer = ShapeLayer::rectangle(PathStyle::default()).into();
/// child_folder.add_layer(grandchild.clone(), None, -1);
/// let child_2: Layer = child_folder.into();
/// root_folder.add_layer(child_2.clone(), None, -1);
/// let root: Layer = root_folder.into();
///
/// let mut iter = root.iter();
/// assert_eq!(iter.next(), Some(&root));
/// assert_eq!(iter.next(), Some(&child_2));
/// assert_eq!(iter.next(), Some(&grandchild));
/// assert_eq!(iter.next(), Some(&child_1));
/// assert_eq!(iter.next(), None);
/// ```
pub fn iter(&self) -> LayerIter<'_> {
LayerIter { stack: vec![self] }
}
@ -150,6 +259,30 @@ impl Layer {
self.data.intersects_quad(transformed_quad, path, intersections)
}
/// Compute the bounding box of the layer after applying a transform to it.
///
/// # Example
/// ```
/// # use graphite_graphene::layers::shape_layer::ShapeLayer;
/// # use graphite_graphene::layers::layer_info::Layer;
/// # use graphite_graphene::layers::style::PathStyle;
/// # use glam::DVec2;
/// # use glam::f64::DAffine2;
/// // Create a rectangle with the default dimensions, from `(0|0)` to `(1|1)`
/// let layer: Layer = ShapeLayer::rectangle(PathStyle::default()).into();
///
/// // Apply the Identity transform, which leaves the points unchanged
/// assert_eq!(
/// layer.aabounding_box_for_transform(DAffine2::IDENTITY),
/// Some([DVec2::ZERO, DVec2::ONE]),
/// );
///
/// // Apply a transform that scales every point by a factor of two
/// let transform = DAffine2::from_scale(DVec2::ONE * 2.);
/// assert_eq!(
/// layer.aabounding_box_for_transform(transform),
/// Some([DVec2::ZERO, DVec2::ONE * 2.]),
/// );
pub fn aabounding_box_for_transform(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
self.data.bounding_box(transform)
}
@ -157,6 +290,7 @@ impl Layer {
pub fn aabounding_box(&self) -> Option<[DVec2; 2]> {
self.aabounding_box_for_transform(self.transform)
}
pub fn bounding_transform(&self) -> DAffine2 {
let scale = match self.aabounding_box_for_transform(DAffine2::IDENTITY) {
Some([a, b]) => {
@ -169,6 +303,8 @@ impl Layer {
self.transform * scale
}
/// Get a mutable reference to the Folder wrapped by the layer.
/// This operation will fail if the [Layer type](Layer::data) is not `LayerDataType::Folder`.
pub fn as_folder_mut(&mut self) -> Result<&mut FolderLayer, DocumentError> {
match &mut self.data {
LayerDataType::Folder(f) => Ok(f),
@ -176,6 +312,8 @@ impl Layer {
}
}
/// Get a reference to the Folder wrapped by the layer.
/// This operation will fail if the [Layer type](Layer::data) is not `LayerDataType::Folder`.
pub fn as_folder(&self) -> Result<&FolderLayer, DocumentError> {
match &self.data {
LayerDataType::Folder(f) => Ok(f),
@ -183,6 +321,8 @@ impl Layer {
}
}
/// Get a mutable reference to the Text element wrapped by the layer.
/// This operation will fail if the [Layer type](Layer::data) is not `LayerDataType::Text`.
pub fn as_text_mut(&mut self) -> Result<&mut TextLayer, DocumentError> {
match &mut self.data {
LayerDataType::Text(t) => Ok(t),
@ -190,6 +330,8 @@ impl Layer {
}
}
/// Get a reference to the Text element wrapped by the layer.
/// This operation will fail if the [Layer type](Layer::data) is not `LayerDataType::Text`.
pub fn as_text(&self) -> Result<&TextLayer, DocumentError> {
match &self.data {
LayerDataType::Text(t) => Ok(t),
@ -197,6 +339,8 @@ impl Layer {
}
}
/// Get a mutable reference to the Image element wrapped by the layer.
/// This operation will fail if the [Layer type](Layer::data) is not `LayerDataType::Image`.
pub fn as_image_mut(&mut self) -> Result<&mut ImageLayer, DocumentError> {
match &mut self.data {
LayerDataType::Image(img) => Ok(img),
@ -204,6 +348,15 @@ impl Layer {
}
}
/// Get a reference to the Image element wrapped by the layer.
/// This operation will fail if the [Layer type](Layer::data) is not `LayerDataType::Image`.
pub fn as_image(&self) -> Result<&ImageLayer, DocumentError> {
match &self.data {
LayerDataType::Image(img) => Ok(img),
_ => Err(DocumentError::NotAnImage),
}
}
pub fn style(&self) -> Result<&PathStyle, DocumentError> {
match &self.data {
LayerDataType::Shape(s) => Ok(&s.style),
@ -238,6 +391,30 @@ impl Clone for Layer {
}
}
impl From<FolderLayer> for Layer {
fn from(from: FolderLayer) -> Layer {
Layer::new(LayerDataType::Folder(from), DAffine2::IDENTITY.to_cols_array())
}
}
impl From<ShapeLayer> for Layer {
fn from(from: ShapeLayer) -> Layer {
Layer::new(LayerDataType::Shape(from), DAffine2::IDENTITY.to_cols_array())
}
}
impl From<TextLayer> for Layer {
fn from(from: TextLayer) -> Layer {
Layer::new(LayerDataType::Text(from), DAffine2::IDENTITY.to_cols_array())
}
}
impl From<ImageLayer> for Layer {
fn from(from: ImageLayer) -> Layer {
Layer::new(LayerDataType::Image(from), DAffine2::IDENTITY.to_cols_array())
}
}
impl<'a> IntoIterator for &'a Layer {
type Item = &'a Layer;
type IntoIter = LayerIter<'a>;
@ -247,6 +424,8 @@ impl<'a> IntoIterator for &'a Layer {
}
}
/// An iterator over the layers encapsulated by this layer.
/// See [Layer::iter] for more information.
#[derive(Debug, Default)]
pub struct LayerIter<'a> {
pub stack: Vec<&'a Layer>,

View File

@ -1,7 +1,29 @@
//! # Layers
//! A document consists of a set of [Layers](layer_info::Layer).
//! Layers allow the user to mutate part of the document while leaving the rest unchanged.
//! Graphene currently includes these different types of layers:
//! * [Folder layers](folder_layer::FolderLayer), which encapsulate sub-layers
//! * [Shape layers](shape_layer::ShapeLayer), which contain generic SVG [`<path>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path)s
//! * [Text layers](text_layer::TextLayer), which contain a description of laid out text
//! * [Image layers](image_layer::ImageLayer), which contain a bitmap image
//!
//! Refer to the module-level documentation for detailed information on each layer.
//!
//! ## Overlapping layers
//! Layers are rendered on top of each other.
//! When different layers overlap, they are blended together according to the [BlendMode](blend_mode::BlendMode)
//! using the CSS [`mix-blend-mode`](https://developer.mozilla.org/en-US/docs/Web/CSS/mix-blend-mode) property and the layer opacity.
/// Different ways of combining overlapping SVG elements.
pub mod blend_mode;
/// Contains the [FolderLayer](folder_layer::FolderLayer) type that encapsulates other layers, including more folders.
pub mod folder_layer;
/// Contains the [ImageLayer](image_layer::ImageLayer) type that contains a bitmap image.
pub mod image_layer;
/// Contains the base [Layer](layer_info::Layer) type, an abstraction over the different types of layers.
pub mod layer_info;
/// Contains the [ShapeLayer](shape_layer::ShapeLayer) type, a generic SVG element defined using Bezier paths.
pub mod shape_layer;
pub mod style;
/// Contains the [TextLayer](text_layer::TextLayer) type.
pub mod text_layer;

View File

@ -12,11 +12,20 @@ fn glam_to_kurbo(transform: DAffine2) -> Affine {
Affine::new(transform.to_cols_array())
}
/// A generic SVG element defined using Bezier paths.
/// Shapes are rendered as
/// [`<path>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path)
/// elements inside a
/// [`<g>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/g)
/// group that the transformation matrix is applied to.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct ShapeLayer {
/// A Bezier path.
pub path: BezPath,
/// The visual style of the shape.
pub style: style::PathStyle,
pub render_index: i32,
/// Whether or not the [path](ShapeLayer::path) connects to itself.
pub closed: bool,
}
@ -79,6 +88,10 @@ impl ShapeLayer {
}
}
/// Create an N-gon.
///
/// # Panics
/// This function panics if `sides` is zero.
pub fn ngon(sides: u8, style: PathStyle) -> Self {
use std::f64::consts::{FRAC_PI_2, TAU};
@ -112,6 +125,7 @@ impl ShapeLayer {
}
}
/// Create a rectangular shape.
pub fn rectangle(style: PathStyle) -> Self {
Self {
path: kurbo::Rect::new(0., 0., 1., 1.).to_path(0.01),
@ -121,6 +135,7 @@ impl ShapeLayer {
}
}
/// Create an elliptical shape.
pub fn ellipse(style: PathStyle) -> Self {
Self {
path: kurbo::Ellipse::from_rect(kurbo::Rect::new(0., 0., 1., 1.)).to_path(0.01),
@ -130,6 +145,7 @@ impl ShapeLayer {
}
}
/// Create a straight line from (0, 0) to (1, 0).
pub fn line(style: PathStyle) -> Self {
Self {
path: kurbo::Line::new((0., 0.), (1., 0.)).to_path(0.01),
@ -139,6 +155,7 @@ impl ShapeLayer {
}
}
/// Create a polygonal line that visits each provided point.
pub fn poly_line(points: Vec<impl Into<glam::DVec2>>, style: PathStyle) -> Self {
let mut path = kurbo::BezPath::new();
points
@ -157,7 +174,7 @@ impl ShapeLayer {
}
/// Creates a smooth bezier spline that passes through all given points.
/// The algorithm used in this implementation is described here: https://www.particleincell.com/2012/bezier-splines/
/// The algorithm used in this implementation is described here: <https://www.particleincell.com/2012/bezier-splines/>
pub fn spline(points: Vec<impl Into<glam::DVec2>>, style: PathStyle) -> Self {
let mut path = kurbo::BezPath::new();

View File

@ -1,12 +1,14 @@
use std::fmt::Write;
//! Contains stylistic options for SVG elements.
use crate::color::Color;
use crate::consts::{LAYER_OUTLINE_STROKE_COLOR, LAYER_OUTLINE_STROKE_WIDTH};
use serde::{Deserialize, Serialize};
use glam::{DAffine2, DVec2};
use serde::{Deserialize, Serialize};
use std::fmt::Write;
/// Precision of the opacity value in digits after the decimal point.
/// A value of 3 would correspond to a precision of 10^-3.
const OPACITY_PRECISION: usize = 3;
fn format_opacity(name: &str, opacity: f32) -> String {
@ -17,10 +19,14 @@ fn format_opacity(name: &str, opacity: f32) -> String {
}
}
/// Represents different ways of rendering an object
#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize)]
pub enum ViewMode {
/// Render with normal coloration at the current viewport resolution
Normal,
/// Render only the outlines of shapes at the current viewport resolution
Outline,
/// Render with normal coloration at the document resolution, showing the pixels when the current viewport resolution is higher
Pixels,
}
@ -83,7 +89,7 @@ impl Gradient {
/// Describes the fill of a layer.
///
/// Can be None, solid or potentially some sort of image or pattern
/// Can be None, a solid [Color], a linear [Gradient], or potentially some sort of image or pattern in the future
#[repr(C)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Fill {
@ -99,17 +105,17 @@ impl Default for Fill {
}
impl Fill {
/// Construct a new solid fill
/// Construct a new solid [Fill] from a [Color].
pub fn solid(color: Color) -> Self {
Self::Solid(color)
}
/// Evaluate the color at some point on the fill
/// Evaluate the color at some point on the fill. Doesn't currently work for LinearGradient.
pub fn color(&self) -> Color {
match self {
Self::None => Color::BLACK,
Self::Solid(color) => *color,
// ToDo: Should correctly sample the gradient
// TODO: Should correctly sample the gradient
Self::LinearGradient(Gradient { positions, .. }) => positions[0].1,
}
}
@ -132,10 +138,13 @@ impl Fill {
}
}
/// The stroke (outline) style of an SVG element.
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Stroke {
/// Stroke color
color: Color,
/// Line thickness
width: f32,
}
@ -144,14 +153,17 @@ impl Stroke {
Self { color, width }
}
/// Get the current stroke color.
pub fn color(&self) -> Color {
self.color
}
/// Get the current stroke width.
pub fn width(&self) -> f32 {
self.width
}
/// Provide the SVG attributes for the stroke.
pub fn render(&self) -> String {
format!(r##" stroke="#{}"{} stroke-width="{}""##, self.color.rgb_hex(), format_opacity("stroke", self.color.a()), self.width)
}
@ -179,26 +191,106 @@ impl PathStyle {
Self { stroke, fill }
}
/// Get the current path's [Fill].
///
/// # Example
/// ```
/// # use graphite_graphene::layers::style::{Fill, PathStyle};
/// # use graphite_graphene::color::Color;
/// let fill = Fill::solid(Color::RED);
/// let style = PathStyle::new(None, fill.clone());
///
/// assert_eq!(*style.fill(), fill);
/// ```
pub fn fill(&self) -> &Fill {
&self.fill
}
/// Get the current path's [Stroke].
///
/// # Example
/// ```
/// # use graphite_graphene::layers::style::{Fill, Stroke, PathStyle};
/// # use graphite_graphene::color::Color;
/// let stroke = Stroke::new(Color::GREEN, 42.);
/// let style = PathStyle::new(Some(stroke), Fill::None);
///
/// assert_eq!(style.stroke(), Some(stroke));
/// ```
pub fn stroke(&self) -> Option<Stroke> {
self.stroke
}
/// Replace the path's [Fill] with a provided one.
///
/// # Example
/// ```
/// # use graphite_graphene::layers::style::{Fill, PathStyle};
/// # use graphite_graphene::color::Color;
/// let mut style = PathStyle::default();
///
/// assert_eq!(*style.fill(), Fill::None);
///
/// let fill = Fill::solid(Color::RED);
/// style.set_fill(fill.clone());
///
/// assert_eq!(*style.fill(), fill);
/// ```
pub fn set_fill(&mut self, fill: Fill) {
self.fill = fill;
}
/// Replace the path's [Stroke] with a provided one.
///
/// # Example
/// ```
/// # use graphite_graphene::layers::style::{Stroke, PathStyle};
/// # use graphite_graphene::color::Color;
/// let mut style = PathStyle::default();
///
/// assert_eq!(style.stroke(), None);
///
/// let stroke = Stroke::new(Color::GREEN, 42.);
/// style.set_stroke(stroke);
///
/// assert_eq!(style.stroke(), Some(stroke));
/// ```
pub fn set_stroke(&mut self, stroke: Stroke) {
self.stroke = Some(stroke);
}
/// Set the path's fill to None.
///
/// # Example
/// ```
/// # use graphite_graphene::layers::style::{Fill, PathStyle};
/// # use graphite_graphene::color::Color;
/// let mut style = PathStyle::new(None, Fill::Solid(Color::RED));
///
/// assert!(style.fill().is_some());
///
/// style.clear_fill();
///
/// assert!(!style.fill().is_some());
/// ```
pub fn clear_fill(&mut self) {
self.fill = Fill::None;
}
/// Set the path's stroke to None.
///
/// # Example
/// ```
/// # use graphite_graphene::layers::style::{Fill, Stroke, PathStyle};
/// # use graphite_graphene::color::Color;
/// let mut style = PathStyle::new(Some(Stroke::new(Color::GREEN, 42.)), Fill::None);
///
/// assert!(style.stroke().is_some());
///
/// style.clear_stroke();
///
/// assert!(!style.stroke().is_some());
/// ```
pub fn clear_stroke(&mut self) {
self.stroke = None;
}

View File

@ -1,5 +1,5 @@
use super::layer_info::LayerData;
use super::style::{self, PathStyle, ViewMode};
use super::style::{PathStyle, ViewMode};
use crate::intersection::{intersect_quad_bez_path, Quad};
use crate::LayerId;
@ -14,10 +14,17 @@ fn glam_to_kurbo(transform: DAffine2) -> Affine {
Affine::new(transform.to_cols_array())
}
/// A line, or multiple lines, of text drawn in the document.
/// Like [ShapeLayers](super::shape_layer::ShapeLayer), [TextLayer] are rendered as
/// [`<path>`s](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path).
/// Currently, the only supported font is `SourceSansPro-Regular`.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct TextLayer {
/// The string of text, encompassing one or multiple lines.
pub text: String,
pub style: style::PathStyle,
/// Fill color and stroke used to render the text.
pub style: PathStyle,
/// Font size in pixels.
pub size: f64,
pub line_width: Option<f64>,
#[serde(skip)]
@ -30,10 +37,12 @@ impl LayerData for TextLayer {
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<DAffine2>, view_mode: ViewMode) {
let transform = self.transform(transforms, view_mode);
let inverse = transform.inverse();
if !inverse.is_finite() {
let _ = write!(svg, "<!-- SVG shape has an invalid transform -->");
return;
}
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 { "," }));
@ -104,7 +113,7 @@ impl TextLayer {
new
}
/// Converts to a BezPath, populating the cache if necessary
/// Converts to a [BezPath], populating the cache if necessary.
#[inline]
pub fn to_bez_path(&mut self) -> BezPath {
if self.cached_path.is_none() {
@ -113,12 +122,14 @@ impl TextLayer {
self.cached_path.clone().unwrap()
}
/// Converts to a bezpath, without populating the cache
/// Converts to a [BezPath], without populating the cache.
#[inline]
pub fn to_bez_path_nonmut(&self) -> BezPath {
self.cached_path.clone().unwrap_or_else(|| self.generate_path())
}
/// Get the font face for `SourceSansPro-Regular`.
/// For now, the font is hardcoded in the wasm binary.
#[inline]
fn font_face() -> rustybuzz::Face<'static> {
rustybuzz::Face::from_slice(include_bytes!("SourceSansPro/SourceSansPro-Regular.ttf"), 0).unwrap()
@ -135,6 +146,7 @@ impl TextLayer {
Rect::new(0., 0., far.x, far.y)
}
/// Populate the cache.
pub fn regenerate_path(&mut self) {
self.cached_path = Some(self.generate_path());
}

View File

@ -82,7 +82,6 @@ pub fn to_kurbo(str: &str, buzz_face: rustybuzz::Face, font_size: f64, line_widt
let length = line.split(' ').count();
for (index, word) in line.split(' ').enumerate() {
push_str(&mut buffer, word, index != length - 1);
let glyph_buffer = rustybuzz::shape(&buzz_face, &[], buffer);
if wrap_word(line_width, &glyph_buffer, scale, builder.pos.x) {

View File

@ -1,8 +1,12 @@
pub mod boolean_ops;
/// Contains the [Color](color::Color) type.
pub mod color;
/// Contains constant values used by Graphene.
pub mod consts;
pub mod document;
/// Defines errors that can occur when using Graphene.
pub mod error;
/// Utilities for computing intersections.
pub mod intersection;
pub mod layers;
pub mod operation;

View File

@ -11,6 +11,7 @@ use std::hash::{Hash, Hasher};
#[repr(C)]
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
// TODO: Rename all instances of `path` to `layer_path`
/// Operations that can be performed to mutate the document.
pub enum Operation {
AddEllipse {
path: Vec<LayerId>,