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:
parent
1455ac3dce
commit
5c99cdef7f
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
|
|
|
|||
Loading…
Reference in New Issue