Remove most of document-legacy (#1519)

* Remove boolean ops and unused doc-legacy Operations

* Remove Shape legacy layers

* Remove legacy layer Properties panel code

* Remove additional unused doc-legacy Operations

* Removed unused rendering-related legacy-layer code

* Upgrade dep so CI builds

* Remove various additional unused functions and messages

* Remove the LayerData trait

* Remove RenderData struct and usages

* Banish the Operations system

* Further removals
This commit is contained in:
Keavon Chambers
2023-12-19 04:36:19 -08:00
parent c42d030f18
commit 9a7d7de8fa
64 changed files with 330 additions and 5868 deletions
-811
View File
@@ -1,811 +0,0 @@
use crate::consts::F64PRECISE;
use crate::intersection::{intersections, line_curve_intersections, valid_t, Intersect, Origin};
use crate::layers::shape_layer::ShapeLegacyLayer;
use crate::layers::style::PathStyle;
use kurbo::{BezPath, CubicBez, Line, ParamCurve, ParamCurveArclen, ParamCurveArea, ParamCurveExtrema, PathEl, PathSeg, Point, QuadBez, Rect};
use serde::{Deserialize, Serialize};
use std::cell::RefCell;
use std::fmt::{self, Debug, Formatter};
use std::mem::swap;
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, specta::Type)]
pub enum BooleanOperation {
Union,
Difference,
Intersection,
SubtractFront,
SubtractBack,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
pub enum BooleanOperationError {
InvalidSelection,
InvalidIntersections,
NoIntersections,
NothingDone, // Not necessarily an error
DirectionUndefined,
NoResult,
Unexpected, // For debugging, when complete nothing should be unexpected
}
struct Edge {
pub from: Origin,
pub destination: usize,
pub curve: BezPath,
}
impl Debug for Edge {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str(format!("\n To: {}, Type: {:?}", self.destination, self.from).as_str())?;
f.write_str(format!(" {:?}", self.curve).as_str())
}
}
struct Vertex {
pub intersect: Intersect,
pub edges: Vec<Edge>,
}
impl Debug for Vertex {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(
format!(
"\n Intersect Point: {:?} Segment index of A: {:?}, Segment index of B: {:?} t value of A: {:?} t value of B: {:?}",
self.intersect.point,
self.intersect.segment_index(Origin::Alpha),
self.intersect.segment_index(Origin::Beta),
self.intersect.t_value(Origin::Alpha),
self.intersect.t_value(Origin::Beta),
)
.as_str(),
)?;
f.debug_list().entries(self.edges.iter()).finish()
}
}
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
enum Direction {
Ccw,
Cw,
}
/// Behavior: Intersection and Union cases are distinguished between by cycle area magnitude.
/// This only affects shapes whose intersection is a single shape, and the intersection is similarly sized to the union.
/// Can be solved by first computing at low accuracy, and if the values are close recomputing.
#[derive(Clone)]
struct Cycle {
vertices: Vec<(usize, Origin)>,
direction: Option<Direction>,
area: f64,
}
impl Cycle {
pub fn new(start_vertex_index: usize, edge_origin: Origin) -> Self {
Cycle {
vertices: vec![(start_vertex_index, edge_origin)],
direction: None,
area: 0.0,
}
}
/// Returns true when the cycle is complete, a cycle is complete when it revisits its first vertex where edge is the edge traversed in order to get to vertex.
/// For purposes of computing direction this function assumes vertices are traversed in order
fn extend(&mut self, vertex: usize, edge_origin: Origin, edge_curve: &BezPath) -> bool {
self.vertices.push((vertex, edge_origin));
self.area += path_area(edge_curve);
vertex == self.vertices[0].0
}
/// Returns number of vertices == number of edges in cycle.
fn len(&self) -> usize {
self.vertices.len() - 1
}
pub fn prev_edge_origin(&self) -> Origin {
self.vertices.last().unwrap().1
}
pub fn prev_vertex(&self) -> usize {
self.vertices.last().unwrap().0
}
pub fn vertices(&self) -> &Vec<(usize, Origin)> {
&self.vertices
}
pub fn area(&self) -> f64 {
self.area
}
pub fn direction(&mut self) -> Result<Direction, BooleanOperationError> {
match self.direction {
Some(direction) => Ok(direction),
None => {
if self.area > 0.0 {
self.direction = Some(Direction::Ccw);
Ok(Direction::Ccw)
} else if self.area < 0.0 {
self.direction = Some(Direction::Cw);
Ok(Direction::Cw)
} else {
Err(BooleanOperationError::DirectionUndefined)
}
}
}
}
/// If the path is empty (has no segments), the function `Err`s.
/// If the path crosses itself, the computed direction may (or probably will) be wrong, on account of it not really being defined.
pub fn direction_for_path(path: &BezPath) -> Result<Direction, BooleanOperationError> {
let mut area = 0.0;
path.segments().for_each(|path_segment| area += path_segment.signed_area());
if area > 0.0 {
Ok(Direction::Ccw)
} else if area < 0.0 {
Ok(Direction::Cw)
} else {
Err(BooleanOperationError::DirectionUndefined)
}
}
}
/// Optimization: store computed segment bounding boxes, or even edge bounding boxes to prevent recomputation.
#[derive(Debug)]
struct PathGraph {
vertices: Vec<Vertex>,
}
/// # Boolean Operation Algorithm
/// `PathGraph` represents a directional graph with edges "colored" by `Origin`.
/// Each edge also represents a portion of a visible shape.
/// Has somewhat (totally?) undefined behavior when shapes have self intersections.
impl PathGraph {
pub fn from_paths(alpha: &BezPath, beta: &BezPath) -> Result<PathGraph, BooleanOperationError> {
let mut new = PathGraph {
vertices: intersections(alpha, beta).into_iter().map(|i| Vertex { intersect: i, edges: Vec::new() }).collect(),
};
// We only consider graphs with even numbers of intersections.
// An odd number of intersections occurs when either:
// 1. There exists a tangential intersection (which shouldn't affect boolean ops)
// 2. The algorithm has found an extra intersection or missed an intersection
if new.size() == 0 {
return Err(BooleanOperationError::NoIntersections);
}
if new.size() % 2 != 0 {
return Err(BooleanOperationError::InvalidIntersections);
}
new.add_edges_from_path(alpha, Origin::Alpha);
new.add_edges_from_path(beta, Origin::Beta);
Ok(new)
}
// TODO: NOTE: about intersection time_val order
/// Expects `path` (and all subpaths in `path`) to be closed.
/// # Panics
/// This function panics when `path` is empty.
fn add_edges_from_path(&mut self, path: &BezPath, origin: Origin) {
struct AlgorithmState {
//current_start holds the index of the vertex the current edge is starting from
current_start: Option<usize>,
current: Vec<PathSeg>,
// in order to iterate through once, store information for incomplete first edge
beginning: Vec<PathSeg>,
start_index: Option<usize>,
// seg index != el_index
seg_index: i32,
}
impl AlgorithmState {
fn new() -> Self {
AlgorithmState {
current_start: None,
current: Vec::new(),
beginning: Vec::new(),
start_index: None,
seg_index: 0,
}
}
fn reset(&mut self) {
self.current_start = None;
self.current = Vec::new();
self.beginning = Vec::new();
self.start_index = None;
}
fn advance_by_seg(&mut self, graph: &mut PathGraph, seg: PathSeg, origin: Origin) {
let (vertex_ids, mut t_values) = graph.intersects_in_seg(self.seg_index, origin);
if !vertex_ids.is_empty() {
let subdivided = subdivide_path_seg(&seg, &mut t_values);
for (vertex_id, sub_seg) in vertex_ids.into_iter().zip(subdivided.iter()) {
match self.current_start {
Some(index) => {
sub_seg.map(|end_of_edge| self.current.push(end_of_edge));
graph.add_edge(origin, index, vertex_id, self.current.clone());
self.current_start = Some(vertex_id);
self.current = Vec::new();
}
None => {
self.current_start = Some(vertex_id);
self.start_index = Some(vertex_id);
sub_seg.map(|end_of_beginning| self.beginning.push(end_of_beginning));
}
}
}
subdivided.last().unwrap().map(|start_of_edge| self.current.push(start_of_edge));
} else {
match self.current_start {
Some(_) => self.current.push(seg),
None => self.beginning.push(seg),
}
}
self.seg_index += 1;
}
fn advance_by_closepath(&mut self, graph: &mut PathGraph, initial_point: &mut Point, origin: Origin) {
// When a curve ends in a closepath and its start point does not equal its endpoint they should be connected with a line
let last_line = match self.current.last() {
Some(start_of_final_edge) => Line {
p0: start_of_final_edge.end(),
p1: *initial_point,
},
None => {
// When None occurs the current edge has been connected to a vertex.
// Either self.beginning is Some or None, if self.beginning is Some there may be a dangling edge to connect
// if self.beginning is None, the end of the current edge may not have closed the path
match self.beginning.last() {
Some(end_of_first_edge) => Line {
p0: end_of_first_edge.end(),
p1: *initial_point,
},
None => Line {
// should never panic, either a intersection has been encountered, so self.current_start is Some.
// or no vertex has been encountered so self.beginning.last() is Some
p0: graph.vertex(self.current_start.unwrap()).intersect.point,
p1: *initial_point,
},
}
}
};
if last_line.length() > F64PRECISE {
// A closepath implicitly defines a line which closes the path and the closepath line may contain intersections
self.advance_by_seg(graph, PathSeg::Line(last_line), origin);
}
}
fn finalize_sub_path(&mut self, graph: &mut PathGraph, origin: Origin) {
if let (Some(current_start_), Some(start_index_)) = (self.current_start, self.start_index) {
// Complete the current path
self.current.append(&mut self.beginning);
graph.add_edge(origin, current_start_, start_index_, self.current.clone());
} else {
// Path has a subpath with no intersects.
// Create a dummy vertex with single edge which will be identified as cycle.
let dumb_id = graph.add_vertex(Intersect::new(self.beginning[0].start(), 0.0, 0.0, -1, -1));
graph.add_edge(origin, dumb_id, dumb_id, self.beginning.clone());
}
}
}
let mut algorithm_state = AlgorithmState::new();
// All valid SVG paths start with a moveto, so this will always be initialized
let mut initial_point = Point::new(0.0, 0.0);
for (el_index, el) in path.iter().enumerate() {
match el {
PathEl::MoveTo(p) => initial_point = p,
PathEl::ClosePath => {
algorithm_state.advance_by_closepath(self, &mut initial_point, origin);
algorithm_state.finalize_sub_path(self, origin);
algorithm_state.reset();
}
_ => {
algorithm_state.advance_by_seg(self, path.get_seg(el_index).unwrap(), origin);
}
}
}
}
fn add_vertex(&mut self, intersect: Intersect) -> usize {
self.vertices.push(Vertex { intersect, edges: Vec::new() });
self.vertices.len() - 1
}
fn add_edge(&mut self, origin: Origin, vertex: usize, destination: usize, curve: Vec<PathSeg>) {
let new_edge = Edge {
from: origin,
destination,
curve: BezPath::from_path_segments(curve.into_iter()),
};
self.vertices[vertex].edges.push(new_edge);
}
/// Returns the `Vertex` index and intersect `t_value` for all intersects in the segment identified by `seg_index` from `origin`.
/// Sorts both lists for ascending `t_value`.
fn intersects_in_seg(&self, seg_index: i32, origin: Origin) -> (Vec<usize>, Vec<f64>) {
let mut vertex_index = Vec::new();
let mut t_values = Vec::new();
for (v_index, vertex) in self.vertices.iter().enumerate() {
if vertex.intersect.segment_index(origin) == seg_index {
let next_t = vertex.intersect.t_value(origin);
let insert_index = match t_values.binary_search_by(|val: &f64| (*val).partial_cmp(&next_t).unwrap_or(std::cmp::Ordering::Less)) {
Ok(val) | Err(val) => val,
};
t_values.insert(insert_index, next_t);
vertex_index.insert(insert_index, v_index)
}
}
(vertex_index, t_values)
}
/// Returns the number of vertices in the graph. This is equivalent to the number of intersections.
pub fn size(&self) -> usize {
self.vertices.len()
}
pub fn vertex(&self, index: usize) -> &Vertex {
&self.vertices[index]
}
/// A properly constructed `PathGraph` has no duplicate edges of the same `Origin`.
pub fn edge(&self, from: usize, to: usize, origin: Origin) -> Option<&Edge> {
// With a data structure restructure, or a hashmap, the `find()` here could be avoided, but it probably has a minimal performance impact
self.vertex(from).edges.iter().find(|edge| edge.destination == to && edge.from == origin)
}
/// Where a valid cycle alternates edge `Origin`.
/// Single edge/single vertex "dummy" cycles are also valid.
fn get_cycle(&self, cycle: &mut Cycle, marker_map: &mut Vec<u8>) {
if cycle.prev_edge_origin() == Origin::Alpha {
marker_map[cycle.prev_vertex()] |= 1;
} else {
marker_map[cycle.prev_vertex()] |= 2;
}
if let Some(next_edge) = self.vertex(cycle.prev_vertex()).edges.iter().find(|edge| edge.from != cycle.prev_edge_origin()) {
if !cycle.extend(next_edge.destination, next_edge.from, &next_edge.curve) {
self.get_cycle(cycle, marker_map)
}
}
}
pub fn get_cycles(&self) -> Vec<Cycle> {
let mut cycles = Vec::new();
let mut markers = vec![0; self.size()];
self.vertices.iter().enumerate().for_each(|(vertex_index, _vertex)| {
if (markers[vertex_index] & 1) == 0 {
let mut temp = Cycle::new(vertex_index, Origin::Alpha);
self.get_cycle(&mut temp, &mut markers);
if temp.len() > 0 {
cycles.push(temp);
}
}
if (markers[vertex_index] & 2) == 0 {
let mut temp = Cycle::new(vertex_index, Origin::Beta);
self.get_cycle(&mut temp, &mut markers);
if temp.len() > 0 {
cycles.push(temp);
}
}
});
cycles
}
pub fn get_shape(&self, cycle: &Cycle, style: &PathStyle) -> ShapeLegacyLayer {
let mut curve = Vec::new();
let vertices = cycle.vertices();
for index in 1..vertices.len() {
// We expect the cycle to be valid so this should not panic
concat_paths(&mut curve, &self.edge(vertices[index - 1].0, vertices[index].0, vertices[index].1).unwrap().curve);
}
curve.push(PathEl::ClosePath);
ShapeLegacyLayer::new(BezPath::from_vec(curve).iter().into(), style.clone())
}
}
/// If `t` is on `(0, 1)`, returns the split curve.
/// If `t` is outside `[0, 1]`, returns `(None, None)`
/// If `t` is 0 returns `(None, p)`.
/// If `t` is 1 returns `(p, None)`.
pub fn split_path_seg(p: &PathSeg, t: f64) -> (Option<PathSeg>, Option<PathSeg>) {
if t <= -F64PRECISE || t >= 1.0 + F64PRECISE {
return (None, None);
}
if t <= F64PRECISE {
return (None, Some(*p));
}
if t >= 1.0 - F64PRECISE {
return (Some(*p), None);
}
match p {
PathSeg::Cubic(cubic) => {
let a1 = Line::new(cubic.p0, cubic.p1).eval(t);
let a2 = Line::new(cubic.p1, cubic.p2).eval(t);
let a3 = Line::new(cubic.p2, cubic.p3).eval(t);
let b1 = Line::new(a1, a2).eval(t);
let b2 = Line::new(a2, a3).eval(t);
let c1 = Line::new(b1, b2).eval(t);
(
Some(PathSeg::Cubic(CubicBez { p0: cubic.p0, p1: a1, p2: b1, p3: c1 })),
Some(PathSeg::Cubic(CubicBez { p0: c1, p1: b2, p2: a3, p3: cubic.p3 })),
)
}
PathSeg::Quad(quad) => {
let b1 = Line::new(quad.p0, quad.p1).eval(t);
let b2 = Line::new(quad.p1, quad.p2).eval(t);
let c1 = Line::new(b1, b2).eval(t);
(
Some(PathSeg::Quad(QuadBez { p0: quad.p0, p1: b1, p2: c1 })),
Some(PathSeg::Quad(QuadBez { p0: c1, p1: b2, p2: quad.p2 })),
)
}
PathSeg::Line(line) => {
let split = line.eval(t);
(Some(PathSeg::Line(Line { p0: line.p0, p1: split })), Some(PathSeg::Line(Line { p0: split, p1: line.p1 })))
}
}
}
/// Splits `p` at each of `t_values`.
/// `t_values` should be sorted in ascending order.
/// The length of the returned `Vec` is always equal to `1 + t_values.len()`.
pub fn subdivide_path_seg(p: &PathSeg, t_values: &mut [f64]) -> Vec<Option<PathSeg>> {
let mut sub_segments = Vec::new();
let mut to_split = Some(*p);
let mut prev_split = 0.0;
for split in t_values {
if let Some(to_split_next) = to_split {
let (sub_seg, _to_split) = split_path_seg(&to_split_next, (*split - prev_split) / (1.0 - prev_split));
to_split = _to_split;
sub_segments.push(sub_seg);
prev_split = *split;
} else {
sub_segments.push(None);
}
}
sub_segments.push(to_split);
sub_segments
}
pub fn composite_boolean_operation(mut select: BooleanOperation, shapes: &mut Vec<RefCell<ShapeLegacyLayer>>) -> Result<Vec<ShapeLegacyLayer>, BooleanOperationError> {
if select == BooleanOperation::SubtractFront {
select = BooleanOperation::SubtractBack;
let temp_len = shapes.len();
shapes.swap(0, temp_len - 1);
}
match select {
BooleanOperation::Union | BooleanOperation::Intersection => {
// We must attempt to union each shape with every other shape
let mut subject_idx = 0;
while subject_idx < shapes.len() {
let mut shape_idx = 0;
while shape_idx < shapes.len() && subject_idx < shapes.len() {
if shape_idx == subject_idx {
shape_idx += 1;
continue;
}
let partial_union = boolean_operation(select, &mut shapes[subject_idx].borrow_mut(), &mut shapes[shape_idx].borrow_mut());
match partial_union {
Ok(temp_union) => {
// The result of a successful union will be exactly one shape
if let Some(result) = temp_union.into_iter().next() {
shapes.push(RefCell::new(result));
shapes.swap_remove(subject_idx);
shapes.swap_remove(shape_idx);
} else {
return Err(BooleanOperationError::NoResult);
}
}
Err(BooleanOperationError::NothingDone) => shape_idx += 1,
Err(err) => return Err(err),
}
}
subject_idx += 1;
}
Ok(shapes.iter().map(|ref_shape_layer| ref_shape_layer.borrow().clone()).collect())
}
BooleanOperation::SubtractBack => {
let mut result = vec![shapes[0].borrow().clone()];
for shape_idx in shapes.iter().skip(1) {
let mut temp = Vec::new();
for mut partial in result {
match boolean_operation(select, &mut partial, &mut shape_idx.borrow_mut()) {
Ok(mut partial_result) => temp.append(&mut partial_result),
Err(BooleanOperationError::NothingDone) => temp.push(partial),
Err(err) => return Err(err),
}
}
result = temp; // This move should be done without copying
}
Ok(result)
}
BooleanOperation::Difference => {
let mut difference = Vec::new();
for shape_idx in 0..shapes.len() {
shapes.swap(0, shape_idx);
difference.append(&mut composite_boolean_operation(BooleanOperation::SubtractBack, shapes)?);
}
Ok(difference)
}
BooleanOperation::SubtractFront => unreachable!("composite boolean operation: unreachable subtract from back"),
}
}
// TODO: check if shapes are filled
// TODO: Bug: shape with at least two subpaths and comprised of many unions sometimes has erroneous movetos embedded in edges
pub fn boolean_operation(mut select: BooleanOperation, alpha: &mut ShapeLegacyLayer, beta: &mut ShapeLegacyLayer) -> Result<Vec<ShapeLegacyLayer>, BooleanOperationError> {
if alpha.shape.manipulator_groups().is_empty() || beta.shape.manipulator_groups().is_empty() {
return Err(BooleanOperationError::InvalidSelection);
}
if select == BooleanOperation::SubtractFront {
select = BooleanOperation::SubtractBack;
swap(alpha, beta);
}
let mut alpha_shape = close_path(&(&alpha.shape).into());
let beta_shape = close_path(&(&beta.shape).into());
let beta_reverse = close_path(&reverse_path(&beta_shape));
let alpha_dir = Cycle::direction_for_path(&alpha_shape)?;
let beta_dir = Cycle::direction_for_path(&beta_shape)?;
match select {
BooleanOperation::Union => {
match if beta_dir == alpha_dir {
PathGraph::from_paths(&alpha_shape, &beta_shape)
} else {
PathGraph::from_paths(&alpha_shape, &beta_reverse)
} {
Ok(graph) => {
let mut cycles = graph.get_cycles();
// "extra calls to ParamCurveArea::area here"
let mut boolean_union = graph.get_shape(
cycles.iter().reduce(|max, cycle| if cycle.area().abs() >= max.area().abs() { cycle } else { max }).unwrap(),
&alpha.style,
);
for interior in collect_shapes(&graph, &mut cycles, |dir| dir != alpha_dir, |_| &alpha.style)? {
//TODO: this is not very efficient or nice to read
let mut a_path: BezPath = (&boolean_union.shape).into();
let b_path: BezPath = (&interior.shape).into();
add_subpath(&mut a_path, b_path);
boolean_union.shape = a_path.iter().into();
}
Ok(vec![boolean_union])
}
Err(BooleanOperationError::NoIntersections) => {
// If shape is inside the other the Union is just the larger
// Check could also be done with area and single ray cast
if cast_horizontal_ray(point_on_curve(&beta_shape), &alpha_shape) % 2 != 0 {
Ok(vec![alpha.clone()])
} else if cast_horizontal_ray(point_on_curve(&alpha_shape), &beta_shape) % 2 != 0 {
beta.style = alpha.style.clone();
Ok(vec![beta.clone()])
} else {
Err(BooleanOperationError::NothingDone)
}
}
Err(err) => Err(err),
}
}
BooleanOperation::Difference => {
let graph = if beta_dir != alpha_dir {
PathGraph::from_paths(&alpha_shape, &beta_shape)?
} else {
PathGraph::from_paths(&alpha_shape, &beta_reverse)?
};
collect_shapes(&graph, &mut graph.get_cycles(), |_| true, |dir| if dir == alpha_dir { &alpha.style } else { &beta.style })
}
BooleanOperation::Intersection => {
match if beta_dir == alpha_dir {
PathGraph::from_paths(&alpha_shape, &beta_shape)
} else {
PathGraph::from_paths(&alpha_shape, &beta_reverse)
} {
Ok(graph) => {
let mut cycles = graph.get_cycles();
// "extra calls to ParamCurveArea::area here"
cycles.remove(
cycles
.iter()
.enumerate()
.reduce(|(max_index, max), (index, cycle)| if cycle.area().abs() >= max.area().abs() { (index, cycle) } else { (max_index, max) })
.unwrap()
.0,
);
collect_shapes(&graph, &mut cycles, |dir| dir == alpha_dir, |_| &alpha.style)
}
Err(BooleanOperationError::NoIntersections) => {
// Check could also be done with area and single ray cast
if cast_horizontal_ray(point_on_curve(&beta_shape), &alpha_shape) % 2 != 0 {
beta.style = alpha.style.clone();
Ok(vec![beta.clone()])
} else if cast_horizontal_ray(point_on_curve(&alpha_shape), &beta_shape) % 2 != 0 {
Ok(vec![alpha.clone()])
} else {
Err(BooleanOperationError::NothingDone)
}
}
Err(err) => Err(err),
}
}
BooleanOperation::SubtractFront => {
unreachable!("Boolean operation: unreachable subtract from back");
}
BooleanOperation::SubtractBack => {
match if beta_dir != alpha_dir {
PathGraph::from_paths(&alpha_shape, &beta_shape)
} else {
PathGraph::from_paths(&alpha_shape, &beta_reverse)
} {
Ok(graph) => collect_shapes(&graph, &mut graph.get_cycles(), |dir| dir == alpha_dir, |_| &alpha.style),
Err(BooleanOperationError::NoIntersections) => {
if cast_horizontal_ray(point_on_curve(&beta_shape), &alpha_shape) % 2 != 0 {
add_subpath(&mut alpha_shape, if beta_dir == alpha_dir { reverse_path(&beta_shape) } else { beta_shape });
Ok(vec![alpha.clone()])
} else {
Err(BooleanOperationError::NothingDone)
}
}
Err(err) => Err(err),
}
}
}
}
// TODO check bounding boxes more rigorously
pub fn cast_horizontal_ray(from: Point, into: &BezPath) -> usize {
let mut ray = PathSeg::Line(Line {
p0: from,
p1: Point { x: from.x + 1.0, y: from.y },
});
let mut intersects = Vec::new();
for ref mut seg in into.segments() {
if kurbo::ParamCurveExtrema::bounding_box(seg).x1 > from.x {
line_curve_intersections((&mut ray, seg), |_, b| valid_t(b), &mut intersects);
}
}
intersects.len()
}
/// Uses curve start point as point on the curve.
/// # Panics
/// This function panics if the `curve` is empty.
pub fn point_on_curve(curve: &BezPath) -> Point {
curve.segments().next().unwrap().start()
}
/// # Panics
/// This function panics if the curve has no `PathSeg`s.
pub fn bounding_box(curve: &BezPath) -> Rect {
curve
.segments()
.map(|seg| <PathSeg as ParamCurveExtrema>::bounding_box(&seg))
.reduce(|bounds, rect| bounds.union(rect))
.unwrap()
}
fn collect_shapes<'a, F, G>(graph: &PathGraph, cycles: &mut Vec<Cycle>, predicate: F, style: G) -> Result<Vec<ShapeLegacyLayer>, BooleanOperationError>
where
F: Fn(Direction) -> bool,
G: Fn(Direction) -> &'a PathStyle,
{
let mut shapes = Vec::new();
if cycles.is_empty() {
return Err(BooleanOperationError::Unexpected);
}
for cycle in cycles {
match cycle.direction() {
Ok(dir) => {
if predicate(dir) {
shapes.push(graph.get_shape(cycle, style(dir)));
}
}
// Exclude cycles with 0.0 area
Err(_err) => (),
}
}
Ok(shapes)
}
pub fn reverse_path_segment(seg: &mut PathSeg) {
match seg {
PathSeg::Line(line) => std::mem::swap(&mut line.p0, &mut line.p1),
PathSeg::Quad(quad) => std::mem::swap(&mut quad.p0, &mut quad.p2),
PathSeg::Cubic(cubic) => {
std::mem::swap(&mut cubic.p0, &mut cubic.p3);
std::mem::swap(&mut cubic.p1, &mut cubic.p2);
}
}
}
/// Reverses `path` by reversing each `PathSeg`, and reversing the order of `PathSegs` within each subpath.
/// Note: a closed path might no longer be closed after applying this function.
pub fn reverse_path(path: &BezPath) -> BezPath {
let mut curve = Vec::new();
let mut temp = Vec::new();
let mut path_segments = path.segments();
for element in path.iter() {
match element {
PathEl::MoveTo(_) => {
curve.append(&mut temp.into_iter().rev().collect());
temp = Vec::new();
}
_ => {
if let Some(mut seg) = path_segments.next() {
reverse_path_segment(&mut seg);
temp.push(seg);
}
}
}
}
curve.append(&mut temp.into_iter().rev().collect());
BezPath::from_path_segments(curve.into_iter())
}
/// Close off all sub-paths in curve by inserting a `ClosePath` whenever a `MoveTo` is not preceded by one.
pub fn close_path(curve: &BezPath) -> BezPath {
let mut new = BezPath::new();
let mut path_closed_flag = true;
for el in curve.iter() {
match el {
PathEl::MoveTo(p) => {
if !path_closed_flag {
new.push(PathEl::ClosePath);
}
new.push(PathEl::MoveTo(p));
path_closed_flag = false;
}
PathEl::ClosePath => {
path_closed_flag = true;
new.push(PathEl::ClosePath);
}
element => {
new.push(element);
}
}
}
if !path_closed_flag {
new.push(PathEl::ClosePath);
}
new
}
/// Concatenate `b` to `a`, where `b` is not a new subpath but a continuation of `a`.
pub fn concat_paths(a: &mut Vec<PathEl>, b: &BezPath) {
if a.is_empty() {
a.append(&mut b.elements().to_vec());
return;
}
// Remove closepath
if let Some(PathEl::ClosePath) = a.last() {
a.remove(a.len() - 1);
}
// Skip initial `MoveTo`, which should be guaranteed to exist
b.iter().skip(1).for_each(|element| a.push(element));
}
/// Concatenate `b` to `a`, where `b` is a new subpath.
pub fn add_subpath(a: &mut BezPath, b: BezPath) {
b.into_iter().for_each(|el| a.push(el));
}
pub fn path_length(a: &BezPath, accuracy: Option<f64>) -> f64 {
let mut sum = 0.0;
// Computing arc length with `F64PRECISE` accuracy is probably ridiculous
match accuracy {
Some(val) => a.segments().for_each(|seg| sum += seg.arclen(val)),
None => a.segments().for_each(|seg| sum += seg.arclen(F64PRECISE)),
}
sum
}
pub fn path_area(a: &BezPath) -> f64 {
a.segments().fold(0.0, |mut area, seg| {
area += seg.signed_area();
area
})
}
-5
View File
@@ -1,5 +0,0 @@
// BOOLEAN OPERATIONS
// Bezier curve intersection algorithm
pub const F64PRECISE: f64 = f64::EPSILON * ((1 << 7) as f64); // ~= 2^(-45) - For f64 comparisons to allow for rounding error; note that f64::EPSILON ~= 2^(-52)
pub const F64LOOSE: f64 = f64::EPSILON * ((1 << 20) as f64); // ~= 2^(-32) - For comparisons between values that are a result of complex computations where error accumulates
+17 -851
View File
@@ -1,11 +1,7 @@
use crate::document_metadata::{is_artboard, DocumentMetadata, LayerNodeIdentifier};
use crate::intersection::Quad;
use crate::layers::folder_layer::FolderLegacyLayer;
use crate::layers::layer_info::{LayerData, LayerDataTypeDiscriminant, LegacyLayer, LegacyLayerType};
use crate::layers::layer_layer::{CachedOutputData, LayerLegacyLayer};
use crate::layers::shape_layer::ShapeLegacyLayer;
use crate::layers::style::RenderData;
use crate::{DocumentError, DocumentResponse, Operation};
use crate::layers::layer_info::{LegacyLayer, LegacyLayerType};
use crate::DocumentError;
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeNetwork, NodeOutput};
use graphene_core::renderer::ClickTarget;
@@ -13,12 +9,10 @@ use graphene_core::transform::Footprint;
use graphene_core::{concrete, generic, ProtoNodeIdentifier};
use graphene_std::wasm_application_io::WasmEditorApi;
use glam::{DAffine2, DVec2};
use glam::DVec2;
use serde::{Deserialize, Serialize};
use std::cmp::max;
use std::collections::hash_map::DefaultHasher;
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use std::hash::Hasher;
use std::vec;
/// A number that identifies a layer.
@@ -49,7 +43,11 @@ impl PartialEq for Document {
impl Default for Document {
fn default() -> Self {
Self {
root: LegacyLayer::new(LegacyLayerType::Folder(FolderLegacyLayer::default()), DAffine2::IDENTITY.to_cols_array()),
root: LegacyLayer {
name: None,
visible: true,
data: LegacyLayerType::Folder(FolderLegacyLayer::default()),
},
state_identifier: DefaultHasher::new(),
document_network: {
use graph_craft::document::{value::TaggedValue, NodeInput};
@@ -155,78 +153,10 @@ impl Document {
.reduce(graphene_core::renderer::Quad::combine_bounds)
}
/// Wrapper around render, that returns the whole document as a Response.
pub fn render_root(&mut self, render_data: &RenderData) -> String {
// Render and append to the defs section
let mut svg_defs = String::from("<defs>");
self.root.render(&mut vec![], &mut svg_defs, render_data);
svg_defs.push_str("</defs>");
// Append the cached rendered SVG
svg_defs.push_str(&self.root.cache);
svg_defs
}
/// Renders everything below the given layer contained within its parent folder.
pub fn render_layers_below(&mut self, below_layer_path: &[LayerId], render_data: &RenderData) -> Option<String> {
// Split the path into the layer ID and its parent folder
let (layer_id_to_render_below, parent_folder_path) = below_layer_path.split_last()?;
// Note: it is bad practice to directly clone and modify the document structure, this is a temporary hack until this whole system is replaced by the node graph
let mut temp_subset_folder = self.layer_mut(parent_folder_path).ok()?.clone();
if let LegacyLayerType::Folder(ref mut folder) = temp_subset_folder.data {
// Remove the upper layers to leave behind the lower subset for rendering
let count_of_layers_below = folder.layer_ids.iter().position(|id| id == layer_id_to_render_below).unwrap();
folder.layer_ids.truncate(count_of_layers_below);
folder.layers.truncate(count_of_layers_below);
// Render and append to the defs section
let mut svg_defs = String::from("<defs>");
temp_subset_folder.render(&mut vec![], &mut svg_defs, render_data);
svg_defs.push_str("</defs>");
// Append the cached rendered SVG
svg_defs.push_str(&temp_subset_folder.cache);
Some(svg_defs)
} else {
None
}
}
/// Renders a layer and its children
pub fn render_layer(&mut self, layer_path: &[LayerId], render_data: &RenderData) -> Option<String> {
// Note: it is bad practice to directly clone and modify the document structure, this is a temporary hack until this whole system is replaced by the node graph
let mut temp_clone = self.layer_mut(layer_path).ok()?.clone();
// Render and append to the defs section
let mut svg_defs = String::from("<defs>");
temp_clone.render(&mut vec![], &mut svg_defs, render_data);
svg_defs.push_str("</defs>");
// Append the cached rendered SVG
svg_defs.push_str(&temp_clone.cache);
Some(svg_defs)
}
pub fn current_state_identifier(&self) -> u64 {
self.state_identifier.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>>, render_data: &RenderData) {
self.layer(path).unwrap().intersects_quad(quad, path, intersections, render_data);
}
/// 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, render_data: &RenderData) -> Vec<Vec<LayerId>> {
let mut intersections = Vec::new();
self.intersects_quad(quad, &mut vec![], &mut intersections, render_data);
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: impl AsRef<[LayerId]>) -> Result<&FolderLegacyLayer, DocumentError> {
@@ -239,7 +169,6 @@ impl Document {
/// 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.
fn folder_mut(&mut self, path: &[LayerId]) -> Result<&mut FolderLegacyLayer, DocumentError> {
let mut root = &mut self.root;
for id in path {
@@ -270,15 +199,6 @@ impl Document {
layers.reduce(|a, b| &a[..a.iter().zip(b.iter()).take_while(|&(a, b)| a == b).count()]).unwrap_or_default()
}
/// Filters out the non folders from an iterator of paths.
/// Takes and Iterator over &[LayerId] or &Vec<LayerId>.
pub fn folders<'a, T>(&'a self, layers: impl Iterator<Item = T> + 'a) -> impl Iterator<Item = T> + 'a
where
T: AsRef<[LayerId]> + std::cmp::Ord + 'a,
{
layers.filter(|layer| self.is_folder(layer.as_ref()))
}
/// Returns the shallowest folder given the selection, even if the selection doesn't contain any folders
pub fn shallowest_common_folder<'a>(&self, layers: impl Iterator<Item = &'a [LayerId]>) -> Result<&'a [LayerId], DocumentError> {
let common_prefix_of_path = self.common_layer_path_prefix(layers);
@@ -289,15 +209,6 @@ impl Document {
})
}
/// Returns all folders that are not contained in any other of the given folders
/// Takes and Iterator over &[LayerId] or &Vec<LayerId>.
pub fn shallowest_folders<'a, T>(&'a self, layers: impl Iterator<Item = T>) -> Vec<T>
where
T: AsRef<[LayerId]> + std::cmp::Ord + 'a,
{
Self::shallowest_unique_layers(self.folders(layers))
}
/// Returns all layers that are not contained in any other of the given folders
/// Takes and Iterator over &[LayerId] or &Vec<LayerId>.
pub fn shallowest_unique_layers<'a, T>(layers: impl Iterator<Item = T>) -> Vec<T>
@@ -310,71 +221,6 @@ impl Document {
sorted_layers.dedup_by(|a, b| a.as_ref().starts_with(b.as_ref()));
sorted_layers
}
/// Deepest to shallowest (longest to shortest path length)
/// Takes and Iterator over &[LayerId] or &Vec<LayerId>.
pub fn sorted_folders_by_depth<'a, T>(&'a self, layers: impl Iterator<Item = T>) -> Vec<T>
where
T: AsRef<[LayerId]> + std::cmp::Ord + 'a,
{
let mut folders: Vec<_> = self.folders(layers).collect();
folders.sort_by_key(|a| std::cmp::Reverse(a.as_ref().len()));
folders
}
pub fn folder_children_paths(&self, path: &[LayerId]) -> Vec<Vec<LayerId>> {
if let Ok(folder) = self.folder(path) {
folder.list_layers().iter().map(|f| [path, &[*f]].concat()).collect()
} else {
vec![]
}
}
pub fn is_folder(&self, path: impl AsRef<[LayerId]>) -> bool {
return self.folder(path.as_ref()).is_ok();
}
// Determines which layer is closer to the root, if path_a return true, if path_b return false
// Answers the question: Is A closer to the root than B?
pub fn layer_closer_to_root(&self, path_a: &[u64], path_b: &[u64]) -> bool {
// Convert UUIDs to indices
let indices_for_path_a = self.indices_for_path(path_a).unwrap();
let indices_for_path_b = self.indices_for_path(path_b).unwrap();
let longest = max(indices_for_path_a.len(), indices_for_path_b.len());
for i in 0..longest {
// usize::MAX becomes negative one here, sneaky. So folders are compared as [X, -1]. This is intentional.
let index_a = *indices_for_path_a.get(i).unwrap_or(&usize::MAX) as i32;
let index_b = *indices_for_path_b.get(i).unwrap_or(&usize::MAX) as i32;
// At the point at which the two paths first differ, compare to see which is closer to the root
if index_a != index_b {
// If index_a is smaller, index_a is closer to the root
return index_a < index_b;
}
}
false
}
// Is the target layer between a <-> b layers, inclusive
pub fn layer_is_between(&self, target: &[u64], path_a: &[u64], path_b: &[u64]) -> bool {
// If the target is the root, it isn't between
if target.is_empty() {
return false;
}
// This function is inclusive, so we consider path_a, path_b to be between themselves
if target == path_a || target == path_b {
return true;
};
// These can't both be true and be between two values
let layer_vs_a = self.layer_closer_to_root(target, path_a);
let layer_vs_b = self.layer_closer_to_root(target, path_b);
// To be in-between you need to be above A and below B or vice versa
layer_vs_a != layer_vs_b
}
/// 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
@@ -387,703 +233,23 @@ impl Document {
for id in path {
let pos = root.layer_ids.iter().position(|x| *x == *id).ok_or_else(|| DocumentError::LayerNotFound(path.into()))?;
indices.push(pos);
root = root.folder(*id).ok_or_else(|| DocumentError::LayerNotFound(path.into()))?;
root = match root.layer(*id) {
Some(LegacyLayer {
data: LegacyLayerType::Folder(folder),
..
}) => Some(folder),
_ => None,
}
.ok_or_else(|| DocumentError::LayerNotFound(path.into()))?;
}
indices.push(root.layer_ids.iter().position(|x| *x == layer_id).ok_or_else(|| DocumentError::LayerNotFound(path.into()))?);
Ok(indices)
}
/// Replaces the layer at the specified `path` with `layer`.
pub fn set_layer(&mut self, path: &[LayerId], layer: LegacyLayer, 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(())
}
/// Visit each layer recursively, marks all children as dirty
pub fn mark_children_as_dirty(layer: &mut LegacyLayer) -> bool {
match layer.data {
LegacyLayerType::Folder(ref mut folder) => {
for sub_layer in folder.layers_mut() {
if Document::mark_children_as_dirty(sub_layer) {
layer.cache_dirty = true;
}
}
}
_ => layer.cache_dirty = true,
}
layer.cache_dirty
}
/// 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: LegacyLayer, 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], render_data: &RenderData) -> Result<Option<[DVec2; 2]>, DocumentError> {
let layer = self.layer(path)?;
let transform = self.multiply_transforms(path)?;
Ok(layer.data.bounding_box(transform, render_data))
}
pub fn bounding_box_and_transform(&self, path: &[LayerId], render_data: &RenderData) -> Result<Option<([DVec2; 2], DAffine2)>, DocumentError> {
let layer = self.layer(path)?;
let transform = self.multiply_transforms(&path[..path.len() - 1])?;
Ok(layer.data.bounding_box(layer.transform, render_data).map(|bounds| (bounds, transform)))
}
/// Compute the center of transformation multiplied with `Document::multiply_transforms`.
pub fn pivot(&self, path: &[LayerId], render_data: &RenderData) -> Option<DVec2> {
let layer = self.layer(path).ok()?;
Some(self.multiply_transforms(path).unwrap_or_default().transform_point2(layer.layerspace_pivot(render_data)))
}
pub fn visible_layers_bounding_box(&self, render_data: &RenderData) -> 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()), render_data)
}
pub fn combined_viewport_bounding_box<'a>(&self, paths: impl Iterator<Item = &'a [LayerId]>, render_data: &RenderData) -> Option<[DVec2; 2]> {
let boxes = paths.filter_map(|path| self.viewport_bounding_box(path, render_data).ok()?);
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;
for id in path {
root = root.as_folder_mut()?.layer_mut(*id).ok_or_else(|| DocumentError::LayerNotFound(path.into()))?;
root.cache_dirty = true;
}
Ok(())
}
pub fn mark_downstream_as_dirty(&mut self, path: &[LayerId]) -> Result<(), DocumentError> {
let 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(())
}
/// For the purposes of rendering, this invalidates the render cache for the layer so it must be re-rendered next time.
pub fn mark_as_dirty(&mut self, path: &[LayerId]) -> Result<(), DocumentError> {
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_else(|| DocumentError::LayerNotFound(path.into()))?;
}
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_else(|| DocumentError::LayerNotFound(path.into()))?;
}
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 generate_transform_relative_to_viewport(&self, from: &[LayerId]) -> Result<DAffine2, DocumentError> {
self.generate_transform_across_scope(from, None)
}
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> {
use DocumentResponse::*;
operation.pseudo_hash().hash(&mut self.state_identifier);
let responses = match operation {
Operation::AddEllipse { path, insert_index, transform, style } => {
let layer = LegacyLayer::new(LegacyLayerType::Shape(ShapeLegacyLayer::ellipse(style)), transform);
self.set_layer(&path, layer, insert_index)?;
let mut responses = vec![
DocumentChanged,
CreatedLayer {
path: path.clone(),
is_selected: true,
},
];
responses.extend(update_thumbnails_upstream(&path));
Some(responses)
}
Operation::AddRect { path, insert_index, transform, style } => {
let layer = LegacyLayer::new(LegacyLayerType::Shape(ShapeLegacyLayer::rectangle(style)), transform);
self.set_layer(&path, layer, insert_index)?;
let mut responses = vec![
DocumentChanged,
CreatedLayer {
path: path.clone(),
is_selected: true,
},
];
responses.extend(update_thumbnails_upstream(&path));
Some(responses)
}
Operation::AddLine { path, insert_index, transform, style } => {
let layer = LegacyLayer::new(LegacyLayerType::Shape(ShapeLegacyLayer::line(style)), transform);
self.set_layer(&path, layer, insert_index)?;
let mut responses = vec![
DocumentChanged,
CreatedLayer {
path: path.clone(),
is_selected: true,
},
];
responses.extend(update_thumbnails_upstream(&path));
Some(responses)
}
// TODO: Remove
Operation::AddFrame {
path,
insert_index,
transform,
network,
} => {
let layer = LegacyLayer::new(LegacyLayerType::Layer(LayerLegacyLayer { network, ..Default::default() }), transform);
self.set_layer(&path, layer, insert_index)?;
let mut responses = vec![
DocumentChanged,
CreatedLayer {
path: path.clone(),
is_selected: true,
},
];
responses.extend(update_thumbnails_upstream(&path));
Some(responses)
}
Operation::SetLayerPreserveAspect { layer_path, preserve_aspect } => {
if let Ok(layer) = self.layer_mut(&layer_path) {
layer.preserve_aspect = preserve_aspect;
}
Some(vec![LayerChanged { path: layer_path.clone() }])
}
Operation::AddShape {
path,
transform,
insert_index,
style,
subpath,
} => {
let shape = ShapeLegacyLayer::new(subpath, style);
self.set_layer(&path, LegacyLayer::new(LegacyLayerType::Shape(shape), transform), insert_index)?;
Some(vec![DocumentChanged, CreatedLayer { path, is_selected: true }])
}
Operation::AddPolyline {
path,
insert_index,
points,
transform,
style,
} => {
let points: Vec<glam::DVec2> = points.iter().map(|&it| it.into()).collect();
self.set_layer(&path, LegacyLayer::new(LegacyLayerType::Shape(ShapeLegacyLayer::poly_line(points, style)), transform), insert_index)?;
let mut responses = vec![
DocumentChanged,
CreatedLayer {
path: path.clone(),
is_selected: true,
},
];
responses.extend(update_thumbnails_upstream(&path));
Some(responses)
}
Operation::DeleteLayer { path } => {
fn aggregate_deletions(folder: &FolderLegacyLayer, path: &mut Vec<LayerId>, responses: &mut Vec<DocumentResponse>) {
for (id, layer) in folder.layer_ids.iter().zip(folder.layers()) {
path.push(*id);
responses.push(DocumentResponse::DeletedLayer { path: path.clone() });
if let LegacyLayerType::Folder(f) = &layer.data {
aggregate_deletions(f, path, responses);
}
path.pop();
}
}
let mut responses = Vec::new();
if let Ok(folder) = self.folder(&path) {
aggregate_deletions(folder, &mut path.clone(), &mut responses)
};
self.delete(&path)?;
let (folder, _) = split_path(path.as_slice()).unwrap_or((&[], 0));
responses.extend([DocumentChanged, DeletedLayer { path: path.clone() }, FolderChanged { path: folder.to_vec() }]);
responses.extend(update_thumbnails_upstream(folder));
Some(responses)
}
Operation::InsertLayer {
destination_path,
layer,
insert_index,
duplicating,
} => {
let (folder_path, layer_id) = split_path(&destination_path)?;
let mut responses = vec![DocumentChanged];
// If we are duplicating, use the parent layer path as the folder we insert to
let (created_layer_path, folder_changed_path) = if duplicating {
let folder = self.folder_mut(&destination_path)?;
let new_layer_id = folder.add_layer(*layer, None, insert_index).ok_or(DocumentError::IndexOutOfBounds)?;
([destination_path.as_slice(), &[new_layer_id]].concat(), destination_path.clone())
} else {
let folder = self.folder_mut(folder_path)?;
folder.add_layer(*layer, Some(layer_id), insert_index).ok_or(DocumentError::IndexOutOfBounds)?;
(destination_path.clone(), folder_path.to_vec())
};
responses.push(CreatedLayer {
path: created_layer_path,
is_selected: !duplicating,
});
responses.push(FolderChanged { path: folder_changed_path.to_vec() });
responses.extend(update_thumbnails_upstream(&destination_path));
self.mark_as_dirty(&destination_path)?;
// Recursively iterate through each layer in a folder and add it to the responses vector
fn aggregate_insertions(folder: &FolderLegacyLayer, path: &mut Vec<LayerId>, responses: &mut Vec<DocumentResponse>, duplicating: bool) {
for (id, layer) in folder.layer_ids.iter().zip(folder.layers()) {
path.push(*id);
responses.push(DocumentResponse::CreatedLayer {
path: path.clone(),
is_selected: !duplicating,
});
if let LegacyLayerType::Folder(f) = &layer.data {
aggregate_insertions(f, path, responses, duplicating);
}
path.pop();
}
}
if let Ok(folder) = self.folder(&destination_path) {
aggregate_insertions(folder, &mut destination_path.as_slice().to_vec(), &mut responses, duplicating);
};
Some(responses)
}
Operation::DuplicateLayer { path } => {
// Notes for review: I wasn't sure on how to apply unwrap_or() to lines of code that use the function self.layer()
let layer = self.layer(&path)?.clone();
let layer_is_folder = layer.as_folder().is_ok();
let (folder_path, _) = split_path(path.as_slice()).unwrap_or((&[], 0));
// Recursively collect each of the nested folders and shapes if the layer is a folder
fn recursive_collect(document: &mut Document, layer_path: &[u64]) -> Vec<Vec<u64>> {
let mut duplicated_layers_so_far = Vec::new();
let children = document.folder_children_paths(layer_path);
for child in children {
if document.is_folder(&child) {
duplicated_layers_so_far.push(child.to_vec());
duplicated_layers_so_far.append(&mut recursive_collect(document, &child));
} else {
duplicated_layers_so_far.push(child);
}
}
duplicated_layers_so_far
}
let mut duplicated_layers = if layer_is_folder { recursive_collect(self, &path) } else { Vec::new() };
// Iterate through each layer path and collect the corresponding Layer objects into one vector
let duplicated_layers_objects = duplicated_layers
.iter()
.map(|layer_path| self.layer(layer_path.as_slice()).cloned().ok())
.collect::<Option<Vec<_>>>()
.ok_or(DocumentError::InvalidPath)?;
// Sort both vectors by the layer path depth, from shallowest (fewest) to deepest (most)
let mut indices: Vec<usize> = (0..duplicated_layers.len()).collect();
indices.sort_by_key(|&i| duplicated_layers[i].len());
duplicated_layers.sort_by_key(|a| a.len());
let duplicate_layer_objects_sorted: Vec<&LegacyLayer> = indices.iter().map(|&i| &duplicated_layers_objects[i]).collect();
let folder = self.folder_mut(folder_path)?;
let selected_id = path.last().copied().unwrap_or_default();
let insert_index = folder.layer_ids.iter().position(|&id| id == selected_id).unwrap_or(0) as isize + 1;
if let Some(new_layer_id) = folder.add_layer(layer, None, insert_index) {
let new_path = [folder_path, &[new_layer_id]].concat();
let mut responses = vec![
DocumentChanged,
CreatedLayer {
path: new_path.clone(),
is_selected: true,
},
FolderChanged { path: folder_path.to_vec() },
];
responses.extend(update_thumbnails_upstream(path.as_slice()));
if layer_is_folder {
let new_folder = self.folder_mut(&new_path)?;
// Clear the new folders layer_ids/layers because they contain the layer_ids/layers of the layers that were duplicated
new_folder.layer_ids = vec![];
new_folder.layers = vec![];
// Generate a new next assignment ID to avoid collision
new_folder.generate_new_folder_ids();
let mut old_to_new_layer_id: HashMap<LayerId, LayerId> = HashMap::new();
for (i, duplicate_layer) in duplicated_layers.into_iter().enumerate() {
let Some(old_layer_id) = duplicate_layer.last().cloned() else {
continue;
};
// Iterate through each ID of the current duplicate layer
// If the dictionary contains the ID, we know the duplicate folder has been created already. Use the existing layer ID instead of creating a new one
let sub_layer = &duplicate_layer[new_path.len()..];
let new_sub_path: Vec<u64> = sub_layer.iter().filter_map(|id| old_to_new_layer_id.get(id).cloned()).collect();
// Combine the new path with the IDs of the duplicate layer path to create the path where we insert the duplicate layer
let mut updated_layer = duplicate_layer_objects_sorted.get(i).unwrap().to_owned().clone();
let updated_layer_path_parent = [new_path.clone(), new_sub_path].concat();
// Clear the new folder's layer_ids and layers because they contain the layer_ids/layers of the layer were duplicated
if self.is_folder(duplicate_layer) {
let updated_layer_as_folder: &mut FolderLegacyLayer = updated_layer.as_folder_mut()?;
updated_layer_as_folder.layer_ids = vec![];
updated_layer_as_folder.layers = vec![];
updated_layer_as_folder.generate_new_folder_ids()
}
let result = self
.handle_operation(Operation::InsertLayer {
layer: Box::new(updated_layer),
destination_path: updated_layer_path_parent,
insert_index: -1,
duplicating: true,
})
.ok()
.flatten()
.unwrap_or_default();
// Collect the new ID of the duplicated layer from the InsertLayer Operation
// Map the layer ID of the layer we're duplicating to the new ID
if let DocumentResponse::CreatedLayer { path, .. } = result.get(1).unwrap() {
if let Some(new_layer_id) = path.last() {
old_to_new_layer_id.entry(old_layer_id).or_insert(*new_layer_id);
}
}
responses.extend(result);
}
}
self.mark_as_dirty(folder_path)?;
Some(responses)
} else {
return Err(DocumentError::IndexOutOfBounds);
}
}
Operation::RenameLayer { layer_path: path, new_name: name } => {
self.layer_mut(&path)?.name = Some(name);
Some(vec![LayerChanged { path }])
}
Operation::CreateFolder { path, insert_index } => {
self.set_layer(
&path,
LegacyLayer::new(LegacyLayerType::Folder(FolderLegacyLayer::default()), DAffine2::IDENTITY.to_cols_array()),
insert_index,
)?;
self.mark_as_dirty(&path)?;
let mut responses = vec![
DocumentChanged,
CreatedLayer {
path: path.clone(),
is_selected: true,
},
];
responses.extend(update_thumbnails_upstream(&path));
Some(responses)
}
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![DocumentChanged], update_thumbnails_upstream(&path)].concat())
}
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![DocumentChanged], update_thumbnails_upstream(&path)].concat())
}
Operation::SetLayerBlobUrl { layer_path, blob_url, resolution: _ } => {
let layer = self.layer_mut(&layer_path).unwrap_or_else(|_| panic!("Blob URL for invalid layer with path '{layer_path:?}'"));
let LegacyLayerType::Layer(layer) = &mut layer.data else {
panic!("Incorrectly trying to set the image blob URL for a layer that is not a 'Layer' layer type");
};
layer.cached_output_data = CachedOutputData::BlobURL(blob_url);
self.mark_as_dirty(&layer_path)?;
Some([vec![DocumentChanged, LayerChanged { path: layer_path.clone() }], update_thumbnails_upstream(&layer_path)].concat())
}
Operation::ClearBlobURL { path } => {
let layer = self.layer_mut(&path).expect("Clearing node graph image for invalid layer");
match &mut layer.data {
LegacyLayerType::Layer(layer) => {
if matches!(layer.cached_output_data, CachedOutputData::BlobURL(_)) {
layer.cached_output_data = CachedOutputData::None;
}
}
e => panic!("Incorrectly trying to clear the blob URL for layer of type {}", LayerDataTypeDiscriminant::from(&*e)),
}
self.mark_as_dirty(&path)?;
Some([vec![DocumentChanged, LayerChanged { path: path.clone() }], update_thumbnails_upstream(&path)].concat())
}
Operation::SetPivot { layer_path, pivot } => {
let layer = self.layer_mut(&layer_path).expect("Setting pivot for invalid layer");
layer.pivot = pivot.into();
self.mark_as_dirty(&layer_path)?;
Some([vec![DocumentChanged, LayerChanged { path: layer_path.clone() }], update_thumbnails_upstream(&layer_path)].concat())
}
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![DocumentChanged], update_thumbnails_upstream(&path)].concat())
}
Operation::SetShapePath { path, subpath } => {
self.mark_as_dirty(&path)?;
if let LegacyLayerType::Shape(shape) = &mut self.layer_mut(&path)?.data {
shape.shape = subpath;
}
Some(vec![DocumentChanged, LayerChanged { path }])
}
Operation::SetVectorData { path, vector_data } => {
if let LegacyLayerType::Layer(layer) = &mut self.layer_mut(&path)?.data {
layer.cached_output_data = CachedOutputData::VectorPath(Box::new(vector_data));
}
Some(Vec::new())
}
Operation::SetSurface { path, surface_id } => {
if let LegacyLayerType::Layer(layer) = &mut self.layer_mut(&path)?.data {
layer.cached_output_data = CachedOutputData::SurfaceId(surface_id);
}
Some(Vec::new())
}
Operation::SetSvg { path, svg } => {
if let LegacyLayerType::Layer(layer) = &mut self.layer_mut(&path)?.data {
layer.cached_output_data = CachedOutputData::Svg(svg);
}
Some(Vec::new())
}
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![DocumentChanged], update_thumbnails_upstream(&path)].concat())
}
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![DocumentChanged], update_thumbnails_upstream(&path)].concat())
}
Operation::SetLayerScaleAroundPivot { path, new_scale } => {
let layer = self.layer_mut(&path)?;
let matrix = layer.transform.to_cols_array();
let old_scale = (matrix[0], matrix[3]);
let scale_factor = DVec2::from(new_scale) / DVec2::from(old_scale);
let offset = DAffine2::from_translation(-layer.pivot);
let scale = DAffine2::from_scale(scale_factor);
let offset_back = DAffine2::from_translation(layer.pivot);
layer.transform = layer.transform * offset_back * scale * offset;
self.mark_as_dirty(&path)?;
Some([vec![DocumentChanged], update_thumbnails_upstream(&path)].concat())
}
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![DocumentChanged], update_thumbnails_upstream(&path)].concat())
}
Operation::SetLayerVisibility { path, visible } => {
self.mark_as_dirty(&path)?;
let layer = self.layer_mut(&path)?;
layer.visible = visible;
Some([vec![DocumentChanged], update_thumbnails_upstream(&path)].concat())
}
Operation::SetLayerBlendMode { path, blend_mode } => {
self.mark_as_dirty(&path)?;
self.layer_mut(&path)?.blend_mode = blend_mode;
Some([vec![DocumentChanged], update_thumbnails_upstream(&path)].concat())
}
Operation::SetLayerOpacity { path, opacity } => {
self.mark_as_dirty(&path)?;
self.layer_mut(&path)?.opacity = opacity;
Some([vec![DocumentChanged], update_thumbnails_upstream(&path)].concat())
}
Operation::SetLayerStyle { path, style } => {
let layer = self.layer_mut(&path)?;
match &mut layer.data {
LegacyLayerType::Shape(s) => s.style = style,
_ => return Err(DocumentError::NotShape),
}
self.mark_as_dirty(&path)?;
Some([vec![DocumentChanged, LayerChanged { path: path.clone() }], update_thumbnails_upstream(&path)].concat())
}
Operation::SetLayerStroke { path, stroke } => {
let layer = self.layer_mut(&path)?;
layer.style_mut()?.set_stroke(stroke);
self.mark_as_dirty(&path)?;
Some([vec![DocumentChanged], update_thumbnails_upstream(&path)].concat())
}
Operation::SetLayerFill { path, fill } => {
let layer = self.layer_mut(&path)?;
layer.style_mut()?.set_fill(fill);
self.mark_as_dirty(&path)?;
Some([vec![DocumentChanged], update_thumbnails_upstream(&path)].concat())
}
};
Ok(responses)
}
}
fn split_path(path: &[LayerId]) -> Result<(&[LayerId], LayerId), DocumentError> {
let (id, path) = path.split_last().ok_or(DocumentError::InvalidPath)?;
Ok((path, *id))
}
fn update_thumbnails_upstream(path: &[LayerId]) -> Vec<DocumentResponse> {
let length = path.len();
let mut responses = Vec::with_capacity(length);
for i in 0..length {
responses.push(DocumentResponse::LayerChanged { path: path[0..(length - i)].to_vec() });
}
responses
}
pub fn pick_layer_safe_imaginate_resolution(layer: &LegacyLayer, render_data: &RenderData) -> (u64, u64) {
let layer_bounds = layer.bounding_transform(render_data);
let layer_bounds_size = (layer_bounds.transform_vector2((1., 0.).into()).length(), layer_bounds.transform_vector2((0., 1.).into()).length());
graphene_std::imaginate::pick_safe_imaginate_resolution(layer_bounds_size)
}
+1 -23
View File
@@ -338,7 +338,7 @@ impl DocumentMetadata {
.reduce(Quad::combine_bounds)
}
pub fn layer_outline<'a>(&'a self, layer: LayerNodeIdentifier) -> impl Iterator<Item = &'a bezier_rs::Subpath<ManipulatorGroupId>> {
pub fn layer_outline(&self, layer: LayerNodeIdentifier) -> impl Iterator<Item = &bezier_rs::Subpath<ManipulatorGroupId>> {
static EMPTY: Vec<ClickTarget> = Vec::new();
let click_targets = self.click_targets.get(&layer).unwrap_or(&EMPTY);
click_targets.iter().map(|click_target| &click_target.subpath)
@@ -361,12 +361,6 @@ impl core::fmt::Debug for LayerNodeIdentifier {
}
}
impl core::fmt::Display for LayerNodeIdentifier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("Layer(node_id={})", self.to_node()))
}
}
impl LayerNodeIdentifier {
pub const ROOT: Self = LayerNodeIdentifier::new_unchecked(0);
@@ -387,10 +381,6 @@ impl LayerNodeIdentifier {
Self::new_unchecked(node_id)
}
pub fn from_path(path: &[u64], network: &NodeNetwork) -> Self {
Self::new(*path.last().unwrap(), network)
}
/// Access the node id of this layer
pub fn to_node(self) -> NodeId {
u64::from(self.0) - 1
@@ -572,18 +562,6 @@ impl LayerNodeIdentifier {
}
}
impl From<NodeId> for LayerNodeIdentifier {
fn from(node_id: NodeId) -> Self {
Self::new_unchecked(node_id)
}
}
impl From<LayerNodeIdentifier> for NodeId {
fn from(identifier: LayerNodeIdentifier) -> Self {
identifier.to_node()
}
}
/// Iterator over specified axis.
#[derive(Clone)]
pub struct AxisIter<'a> {
-13
View File
@@ -1,13 +0,0 @@
use super::LayerId;
/// A set of different errors that can occur when using this crate.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DocumentError {
LayerNotFound(Vec<LayerId>),
InvalidPath,
IndexOutOfBounds,
NotFolder,
NotShape,
NotLayer,
InvalidFile(String),
}
File diff suppressed because it is too large Load Diff
@@ -1,23 +0,0 @@
//! Basic wrapper for [`serde`] for [`base64`] encoding
use base64::Engine;
use serde::{Deserialize, Deserializer, Serializer};
pub fn as_base64<S>(key: &std::sync::Arc<Vec<u8>>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&base64::engine::general_purpose::STANDARD.encode(key.as_slice()))
}
pub fn from_base64<'a, D>(deserializer: D) -> Result<std::sync::Arc<Vec<u8>>, D::Error>
where
D: Deserializer<'a>,
{
use serde::de::Error;
String::deserialize(deserializer)
.and_then(|string| base64::engine::general_purpose::STANDARD.decode(string).map_err(|err| Error::custom(err.to_string())))
.map(std::sync::Arc::new)
.map_err(serde::de::Error::custom)
}
+9 -224
View File
@@ -1,242 +1,27 @@
use super::layer_info::{LayerData, LegacyLayer, LegacyLayerType};
use super::style::RenderData;
use crate::intersection::Quad;
use crate::{DocumentError, LayerId};
use super::layer_info::LegacyLayer;
use crate::document::LayerId;
use crate::DocumentError;
use graphene_core::uuid::generate_uuid;
use glam::DVec2;
use serde::{Deserialize, Serialize};
/// A layer that encapsulates other layers, including potentially more folders.
/// The contained layers are rendered in the same order they are stored.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
pub struct FolderLegacyLayer {
/// 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
pub layers: Vec<LegacyLayer>,
}
impl LayerData for FolderLegacyLayer {
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<glam::DAffine2>, render_data: &RenderData) -> bool {
let mut any_child_requires_redraw = false;
for layer in &mut self.layers {
let (svg_value, requires_redraw) = layer.render(transforms, svg_defs, render_data);
*svg += svg_value;
any_child_requires_redraw = any_child_requires_redraw || requires_redraw;
}
any_child_requires_redraw
}
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, render_data: &RenderData) {
for (layer, layer_id) in self.layers().iter().zip(&self.layer_ids) {
path.push(*layer_id);
layer.intersects_quad(quad, path, intersections, render_data);
path.pop();
}
}
fn bounding_box(&self, transform: glam::DAffine2, render_data: &RenderData) -> Option<[DVec2; 2]> {
self.layers
.iter()
.filter_map(|layer| layer.data.bounding_box(transform * layer.transform, render_data))
.reduce(|a, b| [a[0].min(b[0]), a[1].max(b[1])])
}
}
impl FolderLegacyLayer {
/// 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_document_legacy::layers::shape_layer::ShapeLegacyLayer;
/// # use graphite_document_legacy::layers::folder_layer::FolderLegacyLayer;
/// # use graphite_document_legacy::layers::style::PathStyle;
/// # use graphite_document_legacy::layers::layer_info::LegacyLayerType;
/// let mut folder = FolderLegacyLayer::default();
///
/// // Create two layers to be added to the folder
/// let mut shape_layer = ShapeLegacyLayer::rectangle(PathStyle::default());
/// let mut folder_layer = FolderLegacyLayer::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: LegacyLayer, id: Option<LayerId>, insert_index: isize) -> Option<LayerId> {
let mut insert_index = insert_index as i128;
// Bounds check for the insert index
if insert_index < 0 {
insert_index = self.layers.len() as i128 + insert_index + 1;
}
if insert_index > self.layers.len() as i128 || insert_index < 0 {
return None;
}
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)
pub fn layer(&self, layer_id: LayerId) -> Option<&LegacyLayer> {
let index = self.layer_ids.iter().position(|x| *x == layer_id).ok_or_else(|| DocumentError::LayerNotFound([layer_id].into())).ok()?;
Some(&self.layers[index])
}
/// 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_document_legacy::layers::folder_layer::FolderLegacyLayer;
/// let mut folder = FolderLegacyLayer::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(FolderLegacyLayer::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);
self.layer_ids.remove(pos);
Ok(())
}
/// 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) -> &[LegacyLayer] {
self.layers.as_slice()
}
/// Get mutable references to all the [Layer]s in the folder.
pub fn layers_mut(&mut self) -> &mut [LegacyLayer] {
self.layers.as_mut_slice()
}
pub fn layer(&self, id: LayerId) -> Option<&LegacyLayer> {
let pos = self.position_of_layer(id).ok()?;
Some(&self.layers[pos])
}
pub fn layer_mut(&mut self, id: LayerId) -> Option<&mut LegacyLayer> {
let pos = self.position_of_layer(id).ok()?;
Some(&mut self.layers[pos])
}
pub fn generate_new_folder_ids(&mut self) {
self.next_assignment_id = generate_uuid();
}
/// Returns `true` if the folder contains a layer with the given [LayerId].
///
/// # Example
/// ```
/// # use graphite_document_legacy::layers::folder_layer::FolderLegacyLayer;
/// let mut folder = FolderLegacyLayer::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(FolderLegacyLayer::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_document_legacy::layers::folder_layer::FolderLegacyLayer;
/// let mut folder = FolderLegacyLayer::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(FolderLegacyLayer::default().into(), Some(123), -1);
/// folder.add_layer(FolderLegacyLayer::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_document_legacy::layers::folder_layer::FolderLegacyLayer;
/// # use graphite_document_legacy::layers::shape_layer::ShapeLegacyLayer;
/// # use graphite_document_legacy::layers::style::PathStyle;
/// let mut folder = FolderLegacyLayer::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(FolderLegacyLayer::default().into(), Some(123), -1);
/// assert!(folder.folder(123).is_some());
///
/// // add a non-folder layer and search for it
/// folder.add_layer(ShapeLegacyLayer::rectangle(PathStyle::default()).into(), Some(42), -1);
/// assert!(folder.folder(42).is_none());
/// ```
pub fn folder(&self, id: LayerId) -> Option<&FolderLegacyLayer> {
match self.layer(id) {
Some(LegacyLayer {
data: LegacyLayerType::Folder(folder),
..
}) => Some(folder),
_ => None,
}
}
/// 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 [FolderLegacyLayer::folder] method for a usage example.
pub fn folder_mut(&mut self, id: LayerId) -> Option<&mut FolderLegacyLayer> {
match self.layer_mut(id) {
Some(LegacyLayer {
data: LegacyLayerType::Folder(folder),
..
}) => Some(folder),
_ => None,
}
pub fn layer_mut(&mut self, layer_id: LayerId) -> Option<&mut LegacyLayer> {
let index = self.layer_ids.iter().position(|x| *x == layer_id).ok_or_else(|| DocumentError::LayerNotFound([layer_id].into())).ok()?;
Some(&mut self.layers[index])
}
}
+18 -486
View File
@@ -1,70 +1,44 @@
use super::folder_layer::FolderLegacyLayer;
use super::layer_layer::LayerLegacyLayer;
use super::shape_layer::ShapeLegacyLayer;
use super::style::{PathStyle, RenderData};
use crate::intersection::Quad;
use crate::DocumentError;
use crate::LayerId;
use graphene_core::raster::BlendMode;
use graphene_core::vector::VectorData;
use graphene_std::vector::subpath::Subpath;
use core::fmt;
use glam::{DAffine2, DMat2, DVec2};
use serde::{Deserialize, Serialize};
use std::fmt::Write;
// ===============
// LegacyLayerType
// ===============
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
/// Represents different types of layers.
pub enum LegacyLayerType {
/// A layer that wraps a [FolderLegacyLayer] struct.
Folder(FolderLegacyLayer),
/// A layer that wraps a [ShapeLegacyLayer] struct. Still used by the overlays system, but will be removed in the future.
Shape(ShapeLegacyLayer),
/// A layer that wraps an [LayerLegacyLayer] struct.
Layer(LayerLegacyLayer),
}
impl Default for LegacyLayerType {
fn default() -> Self {
LegacyLayerType::Folder(FolderLegacyLayer::default())
LegacyLayerType::Layer(Default::default())
}
}
impl LegacyLayerType {
pub fn inner(&self) -> &dyn LayerData {
match self {
LegacyLayerType::Shape(shape) => shape,
LegacyLayerType::Folder(folder) => folder,
LegacyLayerType::Layer(layer) => layer,
}
}
pub fn inner_mut(&mut self) -> &mut dyn LayerData {
match self {
LegacyLayerType::Shape(shape) => shape,
LegacyLayerType::Folder(folder) => folder,
LegacyLayerType::Layer(layer) => layer,
}
}
}
// =========================
// LayerDataTypeDiscriminant
// =========================
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash, specta::Type)]
pub enum LayerDataTypeDiscriminant {
Folder,
Shape,
Layer,
Artboard,
}
impl fmt::Display for LayerDataTypeDiscriminant {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
LayerDataTypeDiscriminant::Folder => write!(f, "Folder"),
LayerDataTypeDiscriminant::Shape => write!(f, "Shape"),
LayerDataTypeDiscriminant::Layer => write!(f, "Layer"),
LayerDataTypeDiscriminant::Artboard => write!(f, "Artboard"),
}
}
}
@@ -75,385 +49,33 @@ impl From<&LegacyLayerType> for LayerDataTypeDiscriminant {
match data {
Folder(_) => LayerDataTypeDiscriminant::Folder,
Shape(_) => LayerDataTypeDiscriminant::Shape,
Layer(_) => LayerDataTypeDiscriminant::Layer,
}
}
}
// ** CONVERSIONS **
// ===========
// LegacyLayer
// ===========
impl<'a> TryFrom<&'a mut LegacyLayer> for &'a mut Subpath {
type Error = &'static str;
/// Convert a mutable layer into a mutable [Subpath].
fn try_from(layer: &'a mut LegacyLayer) -> Result<&'a mut Subpath, Self::Error> {
match &mut layer.data {
LegacyLayerType::Shape(layer) => Ok(&mut layer.shape),
_ => Err("Did not find any shape data in the layer"),
}
}
}
impl<'a> TryFrom<&'a LegacyLayer> for &'a Subpath {
type Error = &'static str;
/// Convert a reference to a layer into a reference of a [Subpath].
fn try_from(layer: &'a LegacyLayer) -> Result<&'a Subpath, Self::Error> {
match &layer.data {
LegacyLayerType::Shape(layer) => Ok(&layer.shape),
_ => Err("Did not find any shape data in the layer"),
}
}
}
/// Defines shared behavior for every layer type.
pub trait LayerData {
/// Render the layer as an SVG tag to a given string, returning a boolean to indicate if a redraw is required next frame.
///
/// # Example
/// ```
/// # use graphite_document_legacy::layers::shape_layer::ShapeLegacyLayer;
/// # use graphite_document_legacy::layers::style::{Fill, PathStyle, ViewMode, RenderData};
/// # use graphite_document_legacy::layers::layer_info::LayerData;
/// # use std::collections::HashMap;
///
/// let mut shape = ShapeLegacyLayer::rectangle(PathStyle::new(None, Fill::None));
/// let mut svg = String::new();
///
/// // Render the shape without any transforms, in normal view mode
/// # let font_cache = Default::default();
/// let render_data = RenderData::new(&font_cache, ViewMode::Normal, None);
/// shape.render(&mut svg, &mut String::new(), &mut vec![], &render_data);
///
/// assert_eq!(
/// svg,
/// "<g transform=\"matrix(\n1,-0,-0,1,-0,-0)\">\
/// <path d=\"M0,0L0,1L1,1L1,0Z\" fill=\"none\" />\
/// </g>"
/// );
/// ```
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<glam::DAffine2>, render_data: &RenderData) -> bool;
/// Determine the layers within this layer that intersect a given quad.
/// # Example
/// ```
/// # use graphite_document_legacy::layers::shape_layer::ShapeLegacyLayer;
/// # use graphite_document_legacy::layers::style::{Fill, PathStyle, ViewMode, RenderData};
/// # use graphite_document_legacy::layers::layer_info::LayerData;
/// # use graphite_document_legacy::intersection::Quad;
/// # use glam::f64::{DAffine2, DVec2};
/// # use std::collections::HashMap;
///
/// let mut shape = ShapeLegacyLayer::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![];
///
/// let font_cache = Default::default();
/// let render_data = RenderData::new(&font_cache, Default::default(), None);
/// shape.intersects_quad(quad, &mut vec![shape_id], &mut intersections, &render_data);
///
/// assert_eq!(intersections, vec![vec![shape_id]]);
/// ```
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, render_data: &RenderData);
// 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_document_legacy::layers::shape_layer::ShapeLegacyLayer;
/// # use graphite_document_legacy::layers::style::{Fill, PathStyle, RenderData};
/// # use graphite_document_legacy::layers::layer_info::LayerData;
/// # use glam::f64::{DAffine2, DVec2};
/// # use std::collections::HashMap;
/// let shape = ShapeLegacyLayer::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 font_cache = Default::default();
/// let render_data = RenderData::new(&font_cache, Default::default(), None);
/// let bounding_box = shape.bounding_box(transform, &render_data);
///
/// assert_eq!(bounding_box, Some([DVec2::ZERO, DVec2::ONE]));
/// ```
fn bounding_box(&self, transform: glam::DAffine2, render_data: &RenderData) -> Option<[DVec2; 2]>;
}
impl LayerData for LegacyLayerType {
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<glam::DAffine2>, render_data: &RenderData) -> bool {
self.inner_mut().render(svg, svg_defs, transforms, render_data)
}
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, render_data: &RenderData) {
self.inner().intersects_quad(quad, path, intersections, render_data)
}
fn bounding_box(&self, transform: glam::DAffine2, render_data: &RenderData) -> Option<[DVec2; 2]> {
self.inner().bounding_box(transform, render_data)
}
}
#[derive(Serialize, Deserialize)]
#[serde(remote = "glam::DAffine2")]
struct DAffine2Ref {
pub matrix2: DMat2,
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)]
#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)]
pub struct LegacyLayer {
/// Whether the layer is currently visible or hidden.
pub visible: bool,
/// The user-given name of the layer.
pub name: Option<String>,
/// Whether the layer is currently visible or hidden.
pub visible: bool,
/// The type of layer, such as folder or shape.
pub data: LegacyLayerType,
/// A transformation applied to the layer (translation, rotation, scaling, and shear).
#[serde(with = "DAffine2Ref")]
pub transform: glam::DAffine2,
/// Should the aspect ratio of this layer be preserved?
#[serde(default = "return_true")]
pub preserve_aspect: bool,
/// The center of transformations like rotation or scaling with the shift key.
/// This is in local space (so the layer's transform should be applied).
pub pivot: DVec2,
/// 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,
}
impl Default for LegacyLayer {
fn default() -> Self {
Self {
visible: Default::default(),
name: Default::default(),
data: Default::default(),
transform: Default::default(),
preserve_aspect: Default::default(),
pivot: Default::default(),
thumbnail_cache: Default::default(),
cache: Default::default(),
svg_defs_cache: Default::default(),
cache_dirty: Default::default(),
blend_mode: Default::default(),
opacity: Default::default(),
}
}
}
impl LegacyLayer {
pub fn new(data: LegacyLayerType, transform: [f64; 6]) -> Self {
Self {
visible: true,
name: None,
data,
transform: glam::DAffine2::from_cols_array(&transform),
preserve_aspect: true,
pivot: DVec2::splat(0.5),
cache: String::new(),
thumbnail_cache: String::new(),
svg_defs_cache: String::new(),
cache_dirty: true,
blend_mode: BlendMode::Normal,
opacity: 1.,
}
}
/// Gets a child layer of this layer, by a path. If the layer with id 1 is inside a folder with id 0, the path will be [0, 1].
pub fn child(&self, path: &[LayerId]) -> Option<&LegacyLayer> {
let mut layer = self;
for id in path {
layer = layer.as_folder().ok()?.layer(*id)?;
}
Some(layer)
}
/// Gets a child layer of this layer, by a path. If the layer with id 1 is inside a folder with id 0, the path will be [0, 1].
pub fn child_mut(&mut self, path: &[LayerId]) -> Option<&mut LegacyLayer> {
let mut layer = self;
for id in path {
layer = layer.as_folder_mut().ok()?.layer_mut(*id)?;
}
Some(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](LegacyLayerType::Folder), the iterator will recursively yield all the layers contained in the folder as well as potential sub-folders.
///
/// # Example
/// ```
/// # use graphite_document_legacy::layers::shape_layer::ShapeLegacyLayer;
/// # use graphite_document_legacy::layers::layer_info::Layer;
/// # use graphite_document_legacy::layers::style::PathStyle;
/// # use graphite_document_legacy::layers::folder_layer::FolderLegacyLayer;
/// let mut root_folder = FolderLegacyLayer::default();
///
/// // Add a shape to the root folder
/// let child_1: Layer = ShapeLegacyLayer::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 = FolderLegacyLayer::default();
/// let grandchild: Layer = ShapeLegacyLayer::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] }
}
/// Renders the layer, returning the result and if a redraw is required
pub fn render(&mut self, transforms: &mut Vec<DAffine2>, svg_defs: &mut String, render_data: &RenderData) -> (&str, bool) {
if !self.visible {
return ("", false);
}
transforms.push(self.transform);
// Skip rendering if outside the viewport bounds
if let Some(viewport_bounds) = render_data.culling_bounds {
if let Some(bounding_box) = self.data.bounding_box(transforms.iter().cloned().reduce(|a, b| a * b).unwrap_or(DAffine2::IDENTITY), render_data) {
let is_overlapping =
viewport_bounds[0].x < bounding_box[1].x && bounding_box[0].x < viewport_bounds[1].x && viewport_bounds[0].y < bounding_box[1].y && bounding_box[0].y < viewport_bounds[1].y;
if !is_overlapping {
transforms.pop();
self.cache.clear();
self.cache_dirty = true;
return ("", true);
}
}
}
let mut requires_redraw = false;
if self.cache_dirty {
self.thumbnail_cache.clear();
self.svg_defs_cache.clear();
requires_redraw = self.data.render(&mut self.thumbnail_cache, &mut self.svg_defs_cache, transforms, render_data);
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="opacity: {};{}">{}</g>"#, self.opacity, self.blend_mode.render(), self.thumbnail_cache.as_str());
self.cache_dirty = false;
}
transforms.pop();
svg_defs.push_str(&self.svg_defs_cache);
// If a redraw is required then set the cache to dirty.
if requires_redraw {
self.cache_dirty = true;
}
(self.cache.as_str(), requires_redraw)
}
pub fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, render_data: &RenderData) {
if !self.visible {
return;
}
let transformed_quad = self.transform.inverse() * quad;
self.data.intersects_quad(transformed_quad, path, intersections, render_data)
}
/// Compute the bounding box of the layer after applying a transform to it.
///
/// # Example
/// ```
/// # use graphite_document_legacy::layers::shape_layer::ShapeLegacyLayer;
/// # use graphite_document_legacy::layers::layer_info::Layer;
/// # use graphite_document_legacy::layers::style::{PathStyle, RenderData};
/// # use glam::DVec2;
/// # use glam::f64::DAffine2;
/// # use std::collections::HashMap;
/// // Create a rectangle with the default dimensions, from `(0|0)` to `(1|1)`
/// let layer: Layer = ShapeLegacyLayer::rectangle(PathStyle::default()).into();
///
/// // Apply the Identity transform, which leaves the points unchanged
/// let transform = DAffine2::IDENTITY;
/// let font_cache = Default::default();
/// let render_data = RenderData::new(&font_cache, Default::default(), None);
/// assert_eq!(
/// layer.aabb_for_transform(transform, &render_data),
/// 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.aabb_for_transform(transform, &render_data),
/// Some([DVec2::ZERO, DVec2::ONE * 2.]),
/// );
pub fn aabb_for_transform(&self, transform: DAffine2, render_data: &RenderData) -> Option<[DVec2; 2]> {
self.data.bounding_box(transform, render_data)
}
pub fn aabb(&self, render_data: &RenderData) -> Option<[DVec2; 2]> {
self.aabb_for_transform(self.transform, render_data)
}
pub fn bounding_transform(&self, render_data: &RenderData) -> DAffine2 {
let scale = match self.aabb_for_transform(DAffine2::IDENTITY, render_data) {
Some([a, b]) => {
let dimensions = b - a;
DAffine2::from_scale(dimensions)
}
None => DAffine2::IDENTITY,
};
self.transform * scale
}
pub fn layerspace_pivot(&self, render_data: &RenderData) -> DVec2 {
let [mut min, max] = self.aabb_for_transform(DAffine2::IDENTITY, render_data).unwrap_or([DVec2::ZERO, DVec2::ONE]);
// If the layer bounds are 0 in either axis then set them to one (to avoid div 0)
if (max.x - min.x) < f64::EPSILON * 1000. {
min.x = max.x - 1.;
}
if (max.y - min.y) < f64::EPSILON * 1000. {
min.y = max.y - 1.;
}
self.pivot * (max - min) + min
}
/// Get a mutable reference to the Folder wrapped by the layer.
/// This operation will fail if the [Layer type](Layer::data) is not `LegacyLayerType::Folder`.
pub fn as_folder_mut(&mut self) -> Result<&mut FolderLegacyLayer, DocumentError> {
@@ -463,20 +85,6 @@ impl LegacyLayer {
}
}
pub fn as_vector_data(&self) -> Option<&VectorData> {
match &self.data {
LegacyLayerType::Layer(layer) => layer.as_vector_data(),
_ => None,
}
}
pub fn as_subpath_mut(&mut self) -> Option<&mut Subpath> {
match &mut self.data {
LegacyLayerType::Shape(s) => Some(&mut s.shape),
_ => None,
}
}
/// Get a reference to the Folder wrapped by the layer.
/// This operation will fail if the [Layer type](Layer::data) is not `LegacyLayerType::Folder`.
pub fn as_folder(&self) -> Result<&FolderLegacyLayer, DocumentError> {
@@ -485,87 +93,11 @@ impl LegacyLayer {
_ => Err(DocumentError::NotFolder),
}
}
/// Get a mutable reference to the NodeNetwork
/// This operation will fail if the [Layer type](Layer::data) is not `LegacyLayerType::Layer`.
pub fn as_layer_network_mut(&mut self) -> Result<&mut graph_craft::document::NodeNetwork, DocumentError> {
match &mut self.data {
LegacyLayerType::Layer(layer) => Ok(&mut layer.network),
_ => Err(DocumentError::NotLayer),
}
}
/// Get a reference to the NodeNetwork
/// This operation will fail if the [Layer type](Layer::data) is not `LegacyLayerType::Layer`.
pub fn as_layer_network(&self) -> Result<&graph_craft::document::NodeNetwork, DocumentError> {
match &self.data {
LegacyLayerType::Layer(layer) => Ok(&layer.network),
_ => Err(DocumentError::NotLayer),
}
}
pub fn as_layer(&self) -> Result<&LayerLegacyLayer, DocumentError> {
match &self.data {
LegacyLayerType::Layer(layer) => Ok(layer),
_ => Err(DocumentError::NotLayer),
}
}
pub fn style(&self) -> Result<&PathStyle, DocumentError> {
match &self.data {
LegacyLayerType::Shape(shape) => Ok(&shape.style),
LegacyLayerType::Layer(layer) => layer.as_vector_data().map(|vector| &vector.style).ok_or(DocumentError::NotShape),
_ => Err(DocumentError::NotShape),
}
}
pub fn style_mut(&mut self) -> Result<&mut PathStyle, DocumentError> {
match &mut self.data {
LegacyLayerType::Shape(s) => Ok(&mut s.style),
_ => Err(DocumentError::NotShape),
}
}
}
impl Clone for LegacyLayer {
fn clone(&self) -> Self {
Self {
visible: self.visible,
name: self.name.clone(),
data: self.data.clone(),
transform: self.transform,
preserve_aspect: self.preserve_aspect,
pivot: self.pivot,
cache: String::new(),
thumbnail_cache: String::new(),
svg_defs_cache: String::new(),
cache_dirty: true,
blend_mode: self.blend_mode,
opacity: self.opacity,
}
}
}
impl From<FolderLegacyLayer> for LegacyLayer {
fn from(from: FolderLegacyLayer) -> LegacyLayer {
LegacyLayer::new(LegacyLayerType::Folder(from), DAffine2::IDENTITY.to_cols_array())
}
}
impl From<ShapeLegacyLayer> for LegacyLayer {
fn from(from: ShapeLegacyLayer) -> LegacyLayer {
LegacyLayer::new(LegacyLayerType::Shape(from), DAffine2::IDENTITY.to_cols_array())
}
}
impl<'a> IntoIterator for &'a LegacyLayer {
type Item = &'a LegacyLayer;
type IntoIter = LayerIter<'a>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
// =========
// LayerIter
// =========
/// An iterator over the layers encapsulated by this layer.
/// See [Layer::iter] for more information.
@@ -581,7 +113,7 @@ impl<'a> Iterator for LayerIter<'a> {
match self.stack.pop() {
Some(layer) => {
if let LegacyLayerType::Folder(folder) = &layer.data {
let layers = folder.layers();
let layers = folder.layers.as_slice();
self.stack.extend(layers);
};
Some(layer)
+3 -164
View File
@@ -1,172 +1,11 @@
use super::layer_info::LayerData;
use super::style::{RenderData, ViewMode};
use crate::intersection::{intersect_quad_bez_path, intersect_quad_subpath, Quad};
use crate::LayerId;
use glam::{DAffine2, DMat2, DVec2};
use graphene_core::vector::VectorData;
use graphene_core::SurfaceId;
use kurbo::{Affine, BezPath, Shape as KurboShape};
use serde::{Deserialize, Serialize};
use std::fmt::Write;
#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)]
pub enum CachedOutputData {
#[default]
None,
BlobURL(String),
VectorPath(Box<VectorData>),
SurfaceId(SurfaceId),
Svg(String),
}
// ================
// LayerLegacyLayer
// ================
#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)]
pub struct LayerLegacyLayer {
/// The document node network that this layer contains
pub network: graph_craft::document::NodeNetwork,
#[serde(skip)]
pub cached_output_data: CachedOutputData,
}
impl LayerData for LayerLegacyLayer {
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<DAffine2>, render_data: &RenderData) -> bool {
let transform = self.transform(transforms, render_data.view_mode);
let inverse = transform.inverse();
let (width, height) = (transform.transform_vector2(DVec2::new(1., 0.)).length(), transform.transform_vector2(DVec2::new(0., 1.)).length());
if !inverse.is_finite() {
let _ = write!(svg, "<!-- SVG shape has an invalid transform -->");
return false;
}
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 matrix = (transform * DAffine2::from_scale((width, height).into()).inverse())
.to_cols_array()
.iter()
.enumerate()
.fold(String::new(), |val, (i, entry)| val + &(entry.to_string() + if i == 5 { "" } else { "," }));
// Render any paths if they exist
match &self.cached_output_data {
CachedOutputData::VectorPath(vector_data) => {
let layer_bounds = vector_data.bounding_box().unwrap_or_default();
let transformed_bounds = vector_data.bounding_box_with_transform(transform).unwrap_or_default();
let _ = write!(svg, "<path d=\"");
for subpath in &vector_data.subpaths {
let _ = subpath.subpath_to_svg(svg, transform);
}
svg.push('"');
svg.push_str(&vector_data.style.render(render_data.view_mode, svg_defs, transform, layer_bounds, transformed_bounds));
let _ = write!(svg, "/>");
}
CachedOutputData::BlobURL(blob_url) => {
// Render the image if it exists
let _ = write!(
svg,
r#"<image width="{}" height="{}" preserveAspectRatio="none" href="{}" transform="matrix({})" />"#,
width.abs(),
height.abs(),
blob_url,
matrix
);
}
CachedOutputData::SurfaceId(SurfaceId(id)) => {
// Render the image if it exists
let _ = write!(
svg,
r#"
<foreignObject width="{}" height="{}" transform="matrix({})"><div data-canvas-placeholder="canvas{}"></div></foreignObject>
"#,
width.abs(),
height.abs(),
matrix,
id
);
}
CachedOutputData::Svg(new_svg) => svg.push_str(new_svg),
_ => {
// Render a dotted blue outline if there is no image or vector data
let _ = write!(
svg,
r#"<rect width="{}" height="{}" fill="none" stroke="var(--color-data-vector)" stroke-width="3" stroke-dasharray="8" transform="matrix({})" />"#,
width.abs(),
height.abs(),
matrix,
);
}
}
let _ = svg.write_str(r#"</g>"#);
false
}
fn bounding_box(&self, transform: glam::DAffine2, _render_data: &RenderData) -> Option<[DVec2; 2]> {
if let CachedOutputData::VectorPath(vector_data) = &self.cached_output_data {
return vector_data.bounding_box_with_transform(transform);
}
let mut path = self.bounds();
if transform.matrix2 == DMat2::ZERO {
return None;
}
path.apply_affine(glam_to_kurbo(transform));
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>>, _render_data: &RenderData) {
if let CachedOutputData::VectorPath(vector_data) = &self.cached_output_data {
let filled_style = vector_data.style.fill().is_some();
if vector_data.subpaths.iter().any(|subpath| intersect_quad_subpath(quad, subpath, filled_style || subpath.closed())) {
intersections.push(path.clone());
}
} else if intersect_quad_bez_path(quad, &self.bounds(), true) {
intersections.push(path.clone());
}
}
}
impl LayerLegacyLayer {
pub fn transform(&self, transforms: &[DAffine2], mode: ViewMode) -> DAffine2 {
let start = match mode {
ViewMode::Outline => 0,
_ => (transforms.len() as i32 - 1).max(0) as usize,
};
transforms.iter().skip(start).cloned().reduce(|a, b| a * b).unwrap_or(DAffine2::IDENTITY)
}
fn bounds(&self) -> BezPath {
kurbo::Rect::from_origin_size(kurbo::Point::ZERO, kurbo::Size::new(1., 1.)).to_path(0.)
}
pub fn as_vector_data(&self) -> Option<&VectorData> {
if let CachedOutputData::VectorPath(vector_data) = &self.cached_output_data {
Some(vector_data)
} else {
None
}
}
pub fn as_blob_url(&self) -> Option<&String> {
if let CachedOutputData::BlobURL(blob_url) = &self.cached_output_data {
Some(blob_url)
} else {
None
}
}
}
fn glam_to_kurbo(transform: DAffine2) -> Affine {
Affine::new(transform.to_cols_array())
}
-13
View File
@@ -3,7 +3,6 @@
//! Layers allow the user to mutate part of the document while leaving the rest unchanged.
//! There are currently these different types of layers:
//! * [Folder layers](folder_layer::FolderLegacyLayer), which encapsulate sub-layers
//! * [Shape layers](shape_layer::ShapeLegacyLayer), which contain generic SVG [`<path>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path)s (deprecated but still used by the overlays system).
//! * [Layer layers](layer_layer::LayerLegacyLayer), which contain a node graph layer
//!
//! Refer to the module-level documentation for detailed information on each layer.
@@ -13,21 +12,9 @@
//! 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.
pub mod base64_serde;
/// Contains the [FolderLegacyLayer](folder_layer::FolderLegacyLayer) type that encapsulates other layers, including more folders.
pub mod folder_layer;
/// Contains the base [Layer](layer_info::Layer) type, an abstraction over the different types of layers.
pub mod layer_info;
/// Contains the [LayerLegacyLayer](nodegraph_layer::LayerLegacyLayer) type that contains a node graph.
pub mod layer_layer;
// TODO: Remove shape layers after rewriting the overlay system
/// Contains the [ShapeLegacyLayer](shape_layer::ShapeLegacyLayer) type, a generic SVG element defined using Bezier paths.
pub mod shape_layer;
mod render_data;
pub use render_data::RenderData;
pub mod style {
pub use super::RenderData;
pub use graphene_core::vector::style::*;
}
-22
View File
@@ -1,22 +0,0 @@
use super::style::ViewMode;
use graphene_std::text::FontCache;
use glam::DVec2;
/// Contains metadata for rendering the document as an svg
#[derive(Debug, Clone, Copy)]
pub struct RenderData<'a> {
pub font_cache: &'a FontCache,
pub view_mode: ViewMode,
pub culling_bounds: Option<[DVec2; 2]>,
}
impl<'a> RenderData<'a> {
pub fn new(font_cache: &'a FontCache, view_mode: ViewMode, culling_bounds: Option<[DVec2; 2]>) -> Self {
Self {
font_cache,
view_mode,
culling_bounds,
}
}
}
-176
View File
@@ -1,176 +0,0 @@
use super::layer_info::LayerData;
use super::style::{self, PathStyle, RenderData, ViewMode};
use crate::intersection::{intersect_quad_bez_path, Quad};
use crate::LayerId;
use graphene_std::vector::subpath::Subpath;
use glam::{DAffine2, DMat2, DVec2};
use serde::{Deserialize, Serialize};
use std::fmt::Write;
/// 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, specta::Type)]
pub struct ShapeLegacyLayer {
/// The geometry of the layer.
pub shape: Subpath,
/// The visual style of the shape.
pub style: style::PathStyle,
// TODO: We might be able to remove this in a future refactor
pub render_index: i32,
}
impl LayerData for ShapeLegacyLayer {
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<DAffine2>, render_data: &RenderData) -> bool {
let mut subpath = self.shape.clone();
let layer_bounds = subpath.bounding_box().unwrap_or_default();
let transform = self.transform(transforms, render_data.view_mode);
if !transform.is_finite() || transform.matrix2.determinant() == 0. {
let _ = write!(svg, "<!-- SVG shape has an invalid transform -->");
return false;
}
let inverse = transform.inverse();
subpath.apply_affine(transform);
let transformed_bounds = subpath.bounding_box().unwrap_or_default();
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="{}" {} />"#,
subpath.to_svg(),
self.style.render(render_data.view_mode, svg_defs, transform, layer_bounds, transformed_bounds)
);
let _ = svg.write_str("</g>");
false
}
fn bounding_box(&self, transform: glam::DAffine2, _render_data: &RenderData) -> Option<[DVec2; 2]> {
let mut subpath = self.shape.clone();
if transform.matrix2 == DMat2::ZERO || !transform.is_finite() {
return None;
}
subpath.apply_affine(transform);
subpath.bounding_box()
}
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, _render_data: &RenderData) {
let filled = self.style.fill().is_some() || self.shape.manipulator_groups().last().filter(|manipulator_group| manipulator_group.is_close()).is_some();
if intersect_quad_bez_path(quad, &(&self.shape).into(), filled) {
intersections.push(path.clone());
}
}
}
impl ShapeLegacyLayer {
/// Construct a new [ShapeLegacyLayer] with the specified [Subpath] and [PathStyle]
pub fn new(shape: Subpath, style: PathStyle) -> Self {
Self { shape, style, render_index: 1 }
}
pub fn transform(&self, transforms: &[DAffine2], mode: ViewMode) -> DAffine2 {
let start = match (mode, self.render_index) {
(ViewMode::Outline, _) => 0,
(_, -1) => 0,
(_, x) => (transforms.len() as i32 - x).max(0) as usize,
};
transforms.iter().skip(start).fold(DAffine2::IDENTITY, |a, b| a * *b)
}
// TODO The behavior of ngon changed from the previous iteration slightly, match original behavior
/// Create an N-gon.
///
/// # Panics
/// This function panics if `sides` is zero.
pub fn ngon(sides: u32, 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 {
shape: Subpath::new_ngon(DVec2::new(0., 0.), sides.into(), 1.),
style,
render_index: 1,
}
}
/// Create a rectangular shape.
pub fn rectangle(style: PathStyle) -> Self {
Self {
shape: Subpath::new_rect(DVec2::new(0., 0.), DVec2::new(1., 1.)),
style,
render_index: 1,
}
}
/// Create an elliptical shape.
pub fn ellipse(style: PathStyle) -> Self {
Self {
shape: Subpath::new_ellipse(DVec2::new(0., 0.), DVec2::new(1., 1.)),
style,
render_index: 1,
}
}
/// Create a straight line from (0, 0) to (1, 0).
pub fn line(style: PathStyle) -> Self {
Self {
shape: Subpath::new_line(DVec2::new(0., 0.), DVec2::new(1., 0.)),
style,
render_index: 1,
}
}
/// Create a polygonal line that visits each provided point.
pub fn poly_line(points: Vec<impl Into<glam::DVec2>>, style: PathStyle) -> Self {
Self {
shape: Subpath::new_poly_line(points),
style,
render_index: 0,
}
}
/// 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/>
pub fn spline(points: Vec<impl Into<glam::DVec2>>, style: PathStyle) -> Self {
Self {
shape: Subpath::new_spline(points),
style,
render_index: 0,
}
}
}
+9 -11
View File
@@ -1,18 +1,16 @@
// `macro_use` puts the log macros (`error!`, `warn!`, `debug!`, `info!` and `trace!`) in scope for the crate
#[macro_use]
// #[macro_use]
extern crate log;
pub mod boolean_ops;
pub mod consts;
pub mod document;
pub mod document_metadata;
pub mod error;
pub mod intersection;
pub mod layers;
pub mod operation;
pub mod response;
pub use document::LayerId;
pub use error::DocumentError;
pub use operation::Operation;
pub use response::DocumentResponse;
/// A set of different errors that can occur when using this crate.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DocumentError {
LayerNotFound(Vec<document::LayerId>),
InvalidPath,
NotFolder,
InvalidFile(String),
}
-176
View File
@@ -1,176 +0,0 @@
use crate::layers::layer_info::LegacyLayer;
use crate::layers::style::{self, Stroke};
use crate::LayerId;
use graphene_core::raster::BlendMode;
use graphene_std::vector::subpath::Subpath;
use serde::{Deserialize, Serialize};
use std::collections::hash_map::DefaultHasher;
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 {
// TODO: Remove
AddFrame {
path: Vec<LayerId>,
insert_index: isize,
transform: [f64; 6],
network: graph_craft::document::NodeNetwork,
},
/// Sets a blob URL as the image source for an Image or Imaginate layer type.
/// **Be sure to call `FrontendMessage::TriggerRevokeBlobUrl` together with this.**
SetLayerBlobUrl {
layer_path: Vec<LayerId>,
blob_url: String,
resolution: (f64, f64),
},
/// Clears the image to leave the layer un-rendered.
/// **Be sure to call `FrontendMessage::TriggerRevokeBlobUrl` together with this.**
ClearBlobURL {
path: Vec<LayerId>,
},
SetPivot {
layer_path: Vec<LayerId>,
pivot: (f64, f64),
},
DeleteLayer {
path: Vec<LayerId>,
},
DuplicateLayer {
path: Vec<LayerId>,
},
RenameLayer {
layer_path: Vec<LayerId>,
new_name: String,
},
InsertLayer {
layer: Box<LegacyLayer>,
destination_path: Vec<LayerId>,
insert_index: isize,
duplicating: bool,
},
CreateFolder {
path: Vec<LayerId>,
insert_index: isize,
},
TransformLayer {
path: Vec<LayerId>,
transform: [f64; 6],
},
TransformLayerInViewport {
path: Vec<LayerId>,
transform: [f64; 6],
},
SetLayerTransformInViewport {
path: Vec<LayerId>,
transform: [f64; 6],
},
SetShapePath {
path: Vec<LayerId>,
subpath: Subpath,
},
SetVectorData {
path: Vec<LayerId>,
vector_data: graphene_core::vector::VectorData,
},
SetSurface {
path: Vec<LayerId>,
surface_id: graphene_core::SurfaceId,
},
SetSvg {
path: Vec<LayerId>,
svg: String,
},
TransformLayerInScope {
path: Vec<LayerId>,
transform: [f64; 6],
scope: [f64; 6],
},
SetLayerTransformInScope {
path: Vec<LayerId>,
transform: [f64; 6],
scope: [f64; 6],
},
SetLayerScaleAroundPivot {
path: Vec<LayerId>,
new_scale: (f64, f64),
},
SetLayerTransform {
path: Vec<LayerId>,
transform: [f64; 6],
},
SetLayerVisibility {
path: Vec<LayerId>,
visible: bool,
},
SetLayerPreserveAspect {
layer_path: Vec<LayerId>,
preserve_aspect: bool,
},
SetLayerBlendMode {
path: Vec<LayerId>,
blend_mode: BlendMode,
},
SetLayerOpacity {
path: Vec<LayerId>,
opacity: f64,
},
SetLayerFill {
path: Vec<LayerId>,
fill: style::Fill,
},
SetLayerStroke {
path: Vec<LayerId>,
stroke: Stroke,
},
// The following are used only by the legacy overlays system
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,
},
AddLine {
path: Vec<LayerId>,
insert_index: isize,
transform: [f64; 6],
style: style::PathStyle,
},
AddPolyline {
path: Vec<LayerId>,
insert_index: isize,
transform: [f64; 6],
style: style::PathStyle,
points: Vec<(f64, f64)>,
},
AddShape {
path: Vec<LayerId>,
insert_index: isize,
transform: [f64; 6],
style: style::PathStyle,
subpath: Subpath,
},
SetLayerStyle {
path: Vec<LayerId>,
style: style::PathStyle,
},
}
impl Operation {
pub fn pseudo_hash(&self) -> u64 {
let mut s = DefaultHasher::new();
std::mem::discriminant(self).hash(&mut s);
s.finish()
}
}
-39
View File
@@ -1,39 +0,0 @@
use crate::LayerId;
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[repr(C)]
pub enum DocumentResponse {
/// For the purposes of rendering, this triggers a re-render of the entire document.
DocumentChanged,
FolderChanged {
path: Vec<LayerId>,
},
CreatedLayer {
path: Vec<LayerId>,
is_selected: bool,
},
DeletedLayer {
path: Vec<LayerId>,
},
/// Triggers an update of the layer in the layer panel.
LayerChanged {
path: Vec<LayerId>,
},
DeletedSelectedManipulatorPoints,
}
impl fmt::Display for DocumentResponse {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
DocumentResponse::DocumentChanged { .. } => write!(f, "DocumentChanged"),
DocumentResponse::FolderChanged { .. } => write!(f, "FolderChanged"),
DocumentResponse::CreatedLayer { .. } => write!(f, "CreatedLayer"),
DocumentResponse::LayerChanged { .. } => write!(f, "LayerChanged"),
DocumentResponse::DeletedLayer { .. } => write!(f, "DeleteLayer"),
DocumentResponse::DeletedSelectedManipulatorPoints { .. } => write!(f, "DeletedSelectedManipulatorPoints"),
}
}
}