mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-25 15:48:12 +08:00
Rename the legacy Graphene crate to document-legacy (#899)
* Rename /graphene to /document-legacy * Update names in code
This commit is contained in:
@@ -0,0 +1,807 @@
|
||||
use crate::consts::F64PRECISE;
|
||||
use crate::intersection::{intersections, line_curve_intersections, valid_t, Intersect, Origin};
|
||||
use crate::layers::shape_layer::ShapeLayer;
|
||||
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)]
|
||||
pub enum BooleanOperation {
|
||||
Union,
|
||||
Difference,
|
||||
Intersection,
|
||||
SubtractFront,
|
||||
SubtractBack,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
|
||||
pub enum BooleanOperationError {
|
||||
InvalidSelection,
|
||||
InvalidIntersections,
|
||||
NoIntersections,
|
||||
NothingDone, // Not necessarily an error
|
||||
DirectionUndefined,
|
||||
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::new();
|
||||
markers.resize(self.size(), 0);
|
||||
|
||||
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) -> ShapeLayer {
|
||||
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);
|
||||
ShapeLayer::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<ShapeLayer>>) -> Result<Vec<ShapeLayer>, 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
|
||||
shapes.push(RefCell::new(temp_union.into_iter().next().unwrap()));
|
||||
shapes.swap_remove(subject_idx);
|
||||
shapes.swap_remove(shape_idx);
|
||||
}
|
||||
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 ShapeLayer, beta: &mut ShapeLayer) -> Result<Vec<ShapeLayer>, 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<ShapeLayer>, 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
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Structure that represents a color.
|
||||
/// Internally alpha is stored as `f32` that ranges from `0.0` (transparent) to `1.0` (opaque).
|
||||
/// The other components (RGB) are stored as `f32` that range from `0.0` up to `f32::MAX`,
|
||||
/// the values encode the brightness of each channel proportional to the light intensity in cd/m² (nits) in HDR, and `0.0` (black) to `1.0` (white) in SDR color.
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
|
||||
pub struct Color {
|
||||
red: f32,
|
||||
green: f32,
|
||||
blue: f32,
|
||||
alpha: f32,
|
||||
}
|
||||
|
||||
impl Color {
|
||||
pub const BLACK: Color = Color::from_unsafe(0., 0., 0.);
|
||||
pub const WHITE: Color = Color::from_unsafe(1., 1., 1.);
|
||||
pub const RED: Color = Color::from_unsafe(1., 0., 0.);
|
||||
pub const GREEN: Color = Color::from_unsafe(0., 1., 0.);
|
||||
pub const BLUE: Color = Color::from_unsafe(0., 0., 1.);
|
||||
pub const TRANSPARENT: Color = Self {
|
||||
red: 0.,
|
||||
green: 0.,
|
||||
blue: 0.,
|
||||
alpha: 0.,
|
||||
};
|
||||
|
||||
/// Returns `Some(Color)` if `red`, `green`, `blue` and `alpha` have a valid value. Negative numbers (including `-0.0`), NaN, and infinity are not valid values and return `None`.
|
||||
/// Alpha values greater than `1.0` are not valid.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use graphite_document_legacy::color::Color;
|
||||
/// let color = Color::from_rgbaf32(0.3, 0.14, 0.15, 0.92).unwrap();
|
||||
/// assert!(color.components() == (0.3, 0.14, 0.15, 0.92));
|
||||
///
|
||||
/// let color = Color::from_rgbaf32(1.0, 1.0, 1.0, f32::NAN);
|
||||
/// assert!(color == None);
|
||||
/// ```
|
||||
pub fn from_rgbaf32(red: f32, green: f32, blue: f32, alpha: f32) -> Option<Color> {
|
||||
if alpha > 1. || [red, green, blue, alpha].iter().any(|c| c.is_sign_negative() || !c.is_finite()) {
|
||||
return None;
|
||||
}
|
||||
Some(Color { red, green, blue, alpha })
|
||||
}
|
||||
|
||||
/// Return an opaque `Color` from given `f32` RGB channels.
|
||||
pub const fn from_unsafe(red: f32, green: f32, blue: f32) -> Color {
|
||||
Color { red, green, blue, alpha: 1. }
|
||||
}
|
||||
|
||||
/// Return an opaque SDR `Color` given RGB channels from `0` to `255`.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use graphite_document_legacy::color::Color;
|
||||
/// let color = Color::from_rgb8(0x72, 0x67, 0x62);
|
||||
/// let color2 = Color::from_rgba8(0x72, 0x67, 0x62, 0xFF);
|
||||
/// assert!(color == color2)
|
||||
/// ```
|
||||
pub fn from_rgb8(red: u8, green: u8, blue: u8) -> Color {
|
||||
Color::from_rgba8(red, green, blue, 255)
|
||||
}
|
||||
|
||||
/// Return an SDR `Color` given RGBA channels from `0` to `255`.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use graphite_document_legacy::color::Color;
|
||||
/// let color = Color::from_rgba8(0x72, 0x67, 0x62, 0x61);
|
||||
/// assert!("72676261" == color.rgba_hex())
|
||||
/// ```
|
||||
pub fn from_rgba8(red: u8, green: u8, blue: u8, alpha: u8) -> Color {
|
||||
let map_range = |int_color| int_color as f32 / 255.0;
|
||||
Color {
|
||||
red: map_range(red),
|
||||
green: map_range(green),
|
||||
blue: map_range(blue),
|
||||
alpha: map_range(alpha),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the `red` component.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use graphite_document_legacy::color::Color;
|
||||
/// let color = Color::from_rgbaf32(0.114, 0.103, 0.98, 0.97).unwrap();
|
||||
/// assert!(color.r() == 0.114);
|
||||
/// ```
|
||||
pub fn r(&self) -> f32 {
|
||||
self.red
|
||||
}
|
||||
|
||||
/// Return the `green` component.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use graphite_document_legacy::color::Color;
|
||||
/// let color = Color::from_rgbaf32(0.114, 0.103, 0.98, 0.97).unwrap();
|
||||
/// assert!(color.g() == 0.103);
|
||||
/// ```
|
||||
pub fn g(&self) -> f32 {
|
||||
self.green
|
||||
}
|
||||
|
||||
/// Return the `blue` component.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use graphite_document_legacy::color::Color;
|
||||
/// let color = Color::from_rgbaf32(0.114, 0.103, 0.98, 0.97).unwrap();
|
||||
/// assert!(color.b() == 0.98);
|
||||
/// ```
|
||||
pub fn b(&self) -> f32 {
|
||||
self.blue
|
||||
}
|
||||
|
||||
/// Return the `alpha` component without checking its expected `0.0` to `1.0` range.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use graphite_document_legacy::color::Color;
|
||||
/// let color = Color::from_rgbaf32(0.114, 0.103, 0.98, 0.97).unwrap();
|
||||
/// assert!(color.a() == 0.97);
|
||||
/// ```
|
||||
pub fn a(&self) -> f32 {
|
||||
self.alpha
|
||||
}
|
||||
|
||||
/// Return the all components as a tuple, first component is red, followed by green, followed by blue, followed by alpha.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use graphite_document_legacy::color::Color;
|
||||
/// let color = Color::from_rgbaf32(0.114, 0.103, 0.98, 0.97).unwrap();
|
||||
/// assert!(color.components() == (0.114, 0.103, 0.98, 0.97));
|
||||
/// ```
|
||||
pub fn components(&self) -> (f32, f32, f32, f32) {
|
||||
(self.red, self.green, self.blue, self.alpha)
|
||||
}
|
||||
|
||||
/// Return an 8-character RGBA hex string (without a # prefix).
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use graphite_document_legacy::color::Color;
|
||||
/// let color = Color::from_rgba8(0x7C, 0x67, 0xFA, 0x61);
|
||||
/// assert!("7C67FA61" == color.rgba_hex())
|
||||
/// ```
|
||||
pub fn rgba_hex(&self) -> String {
|
||||
format!(
|
||||
"{:02X?}{:02X?}{:02X?}{:02X?}",
|
||||
(self.r() * 255.) as u8,
|
||||
(self.g() * 255.) as u8,
|
||||
(self.b() * 255.) as u8,
|
||||
(self.a() * 255.) as u8,
|
||||
)
|
||||
}
|
||||
|
||||
/// Return a 6-character RGB hex string (without a # prefix).
|
||||
/// ```
|
||||
/// use graphite_document_legacy::color::Color;
|
||||
/// let color = Color::from_rgba8(0x7C, 0x67, 0xFA, 0x61);
|
||||
/// assert!("7C67FA" == color.rgb_hex())
|
||||
/// ```
|
||||
pub fn rgb_hex(&self) -> String {
|
||||
format!("{:02X?}{:02X?}{:02X?}", (self.r() * 255.) as u8, (self.g() * 255.) as u8, (self.b() * 255.) as u8,)
|
||||
}
|
||||
|
||||
/// Creates a color from a 8-character RGBA hex string (without a # prefix).
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use graphite_document_legacy::color::Color;
|
||||
/// let color = Color::from_rgba_str("7C67FA61").unwrap();
|
||||
/// assert!("7C67FA61" == color.rgba_hex())
|
||||
/// ```
|
||||
pub fn from_rgba_str(color_str: &str) -> Option<Color> {
|
||||
if color_str.len() != 8 {
|
||||
return None;
|
||||
}
|
||||
let r = u8::from_str_radix(&color_str[0..2], 16).ok()?;
|
||||
let g = u8::from_str_radix(&color_str[2..4], 16).ok()?;
|
||||
let b = u8::from_str_radix(&color_str[4..6], 16).ok()?;
|
||||
let a = u8::from_str_radix(&color_str[6..8], 16).ok()?;
|
||||
|
||||
Some(Color::from_rgba8(r, g, b, a))
|
||||
}
|
||||
|
||||
/// Creates a color from a 6-character RGB hex string (without a # prefix).
|
||||
/// ```
|
||||
/// use graphite_document_legacy::color::Color;
|
||||
/// let color = Color::from_rgb_str("7C67FA").unwrap();
|
||||
/// assert!("7C67FA" == color.rgb_hex())
|
||||
/// ```
|
||||
pub fn from_rgb_str(color_str: &str) -> Option<Color> {
|
||||
if color_str.len() != 6 {
|
||||
return None;
|
||||
}
|
||||
let r = u8::from_str_radix(&color_str[0..2], 16).ok()?;
|
||||
let g = u8::from_str_radix(&color_str[2..4], 16).ok()?;
|
||||
let b = u8::from_str_radix(&color_str[4..6], 16).ok()?;
|
||||
|
||||
Some(Color::from_rgb8(r, g, b))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
use crate::color::Color;
|
||||
|
||||
// RENDERING
|
||||
pub const LAYER_OUTLINE_STROKE_COLOR: Color = Color::BLACK;
|
||||
pub const LAYER_OUTLINE_STROKE_WEIGHT: f64 = 1.;
|
||||
|
||||
// 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
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,24 @@
|
||||
use super::LayerId;
|
||||
use crate::boolean_ops::BooleanOperationError;
|
||||
|
||||
/// 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,
|
||||
NotAFolder,
|
||||
NonReorderableSelection,
|
||||
NotAShape,
|
||||
NotText,
|
||||
NotAnImage,
|
||||
NotAnImaginate,
|
||||
InvalidFile(String),
|
||||
}
|
||||
|
||||
// TODO: change how BooleanOperationErrors are handled
|
||||
impl From<BooleanOperationError> for DocumentError {
|
||||
fn from(err: BooleanOperationError) -> Self {
|
||||
DocumentError::InvalidFile(format!("{:?}", err))
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
||||
//! Basic wrapper for [`serde`] for [`base64`] encoding
|
||||
|
||||
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::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::decode(string).map_err(|err| Error::custom(err.to_string())))
|
||||
.map(std::sync::Arc::new)
|
||||
.map_err(serde::de::Error::custom)
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
|
||||
/// Describes how overlapping SVG elements should be blended together.
|
||||
/// See the [MDN Docs](https://developer.mozilla.org/en-US/docs/Web/CSS/blend-mode#examples) for examples.
|
||||
#[derive(PartialEq, Eq, Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum BlendMode {
|
||||
// Basic group
|
||||
Normal,
|
||||
// Not supported by SVG, but we should someday support: Dissolve
|
||||
|
||||
// Darken group
|
||||
Multiply,
|
||||
Darken,
|
||||
ColorBurn,
|
||||
// Not supported by SVG, but we should someday support: Linear Burn, Darker Color
|
||||
|
||||
// Lighten group
|
||||
Screen,
|
||||
Lighten,
|
||||
ColorDodge,
|
||||
// Not supported by SVG, but we should someday support: Linear Dodge (Add), Lighter Color
|
||||
|
||||
// Contrast group
|
||||
Overlay,
|
||||
SoftLight,
|
||||
HardLight,
|
||||
// Not supported by SVG, but we should someday support: Vivid Light, Linear Light, Pin Light, Hard Mix
|
||||
|
||||
// Inversion group
|
||||
Difference,
|
||||
Exclusion,
|
||||
// Not supported by SVG, but we should someday support: Subtract, Divide
|
||||
|
||||
// Component group
|
||||
Hue,
|
||||
Saturation,
|
||||
Color,
|
||||
Luminosity,
|
||||
}
|
||||
|
||||
impl fmt::Display for BlendMode {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
BlendMode::Normal => write!(f, "Normal"),
|
||||
|
||||
BlendMode::Multiply => write!(f, "Multiply"),
|
||||
BlendMode::Darken => write!(f, "Darken"),
|
||||
BlendMode::ColorBurn => write!(f, "Color Burn"),
|
||||
|
||||
BlendMode::Screen => write!(f, "Screen"),
|
||||
BlendMode::Lighten => write!(f, "Lighten"),
|
||||
BlendMode::ColorDodge => write!(f, "Color Dodge"),
|
||||
|
||||
BlendMode::Overlay => write!(f, "Overlay"),
|
||||
BlendMode::SoftLight => write!(f, "Soft Light"),
|
||||
BlendMode::HardLight => write!(f, "Hard Light"),
|
||||
|
||||
BlendMode::Difference => write!(f, "Difference"),
|
||||
BlendMode::Exclusion => write!(f, "Exclusion"),
|
||||
|
||||
BlendMode::Hue => write!(f, "Hue"),
|
||||
BlendMode::Saturation => write!(f, "Saturation"),
|
||||
BlendMode::Color => write!(f, "Color"),
|
||||
BlendMode::Luminosity => write!(f, "Luminosity"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BlendMode {
|
||||
/// Convert the enum to the CSS string for the blend mode.
|
||||
/// [Read more](https://developer.mozilla.org/en-US/docs/Web/CSS/blend-mode#values)
|
||||
pub fn to_svg_style_name(&self) -> &str {
|
||||
match self {
|
||||
BlendMode::Normal => "normal",
|
||||
BlendMode::Multiply => "multiply",
|
||||
BlendMode::Darken => "darken",
|
||||
BlendMode::ColorBurn => "color-burn",
|
||||
BlendMode::Screen => "screen",
|
||||
BlendMode::Lighten => "lighten",
|
||||
BlendMode::ColorDodge => "color-dodge",
|
||||
BlendMode::Overlay => "overlay",
|
||||
BlendMode::SoftLight => "soft-light",
|
||||
BlendMode::HardLight => "hard-light",
|
||||
BlendMode::Difference => "difference",
|
||||
BlendMode::Exclusion => "exclusion",
|
||||
BlendMode::Hue => "hue",
|
||||
BlendMode::Saturation => "saturation",
|
||||
BlendMode::Color => "color",
|
||||
BlendMode::Luminosity => "luminosity",
|
||||
}
|
||||
}
|
||||
|
||||
/// List of all the blend modes in their conventional ordering and grouping.
|
||||
pub fn list_modes_in_groups() -> [&'static [BlendMode]; 6] {
|
||||
[
|
||||
&[BlendMode::Normal],
|
||||
&[BlendMode::Multiply, BlendMode::Darken, BlendMode::ColorBurn],
|
||||
&[BlendMode::Screen, BlendMode::Lighten, BlendMode::ColorDodge],
|
||||
&[BlendMode::Overlay, BlendMode::SoftLight, BlendMode::HardLight],
|
||||
&[BlendMode::Difference, BlendMode::Exclusion],
|
||||
&[BlendMode::Hue, BlendMode::Saturation, BlendMode::Color, BlendMode::Luminosity],
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
use super::layer_info::{Layer, LayerData, LayerDataType};
|
||||
use super::style::RenderData;
|
||||
use crate::intersection::Quad;
|
||||
use crate::layers::text_layer::FontCache;
|
||||
use crate::{DocumentError, LayerId};
|
||||
|
||||
use glam::DVec2;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Write;
|
||||
|
||||
/// A layer that encapsulates other layers, including potentially more folders.
|
||||
/// The contained layers are rendered in the same order they are
|
||||
/// stored in the [layers](FolderLayer::layers) field.
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
|
||||
pub struct FolderLayer {
|
||||
/// The ID that will be assigned to the next layer that is added to the folder
|
||||
next_assignment_id: LayerId,
|
||||
/// The IDs of the [Layer]s contained within the Folder
|
||||
pub layer_ids: Vec<LayerId>,
|
||||
/// The [Layer]s contained in the folder
|
||||
pub layers: Vec<Layer>,
|
||||
}
|
||||
|
||||
impl LayerData for FolderLayer {
|
||||
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<glam::DAffine2>, render_data: RenderData) {
|
||||
for layer in &mut self.layers {
|
||||
let _ = writeln!(svg, "{}", layer.render(transforms, svg_defs, render_data));
|
||||
}
|
||||
}
|
||||
|
||||
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, font_cache: &FontCache) {
|
||||
for (layer, layer_id) in self.layers().iter().zip(&self.layer_ids) {
|
||||
path.push(*layer_id);
|
||||
layer.intersects_quad(quad, path, intersections, font_cache);
|
||||
path.pop();
|
||||
}
|
||||
}
|
||||
|
||||
fn bounding_box(&self, transform: glam::DAffine2, font_cache: &FontCache) -> Option<[DVec2; 2]> {
|
||||
self.layers
|
||||
.iter()
|
||||
.filter_map(|layer| layer.data.bounding_box(transform * layer.transform, font_cache))
|
||||
.reduce(|a, b| [a[0].min(b[0]), a[1].max(b[1])])
|
||||
}
|
||||
}
|
||||
|
||||
impl FolderLayer {
|
||||
/// When a insertion ID is provided, try to insert the layer with the given ID.
|
||||
/// If that ID is already used, return `None`.
|
||||
/// When no insertion ID is provided, search for the next free ID and insert it with that.
|
||||
/// Negative values for `insert_index` represent distance from the end
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use graphite_document_legacy::layers::shape_layer::ShapeLayer;
|
||||
/// # use graphite_document_legacy::layers::folder_layer::FolderLayer;
|
||||
/// # use graphite_document_legacy::layers::style::PathStyle;
|
||||
/// # use graphite_document_legacy::layers::layer_info::LayerDataType;
|
||||
/// let mut folder = FolderLayer::default();
|
||||
///
|
||||
/// // Create two layers to be added to the folder
|
||||
/// let mut shape_layer = ShapeLayer::rectangle(PathStyle::default());
|
||||
/// let mut folder_layer = FolderLayer::default();
|
||||
///
|
||||
/// folder.add_layer(shape_layer.into(), None, -1);
|
||||
/// folder.add_layer(folder_layer.into(), Some(123), 0);
|
||||
/// ```
|
||||
pub fn add_layer(&mut self, layer: Layer, id: Option<LayerId>, insert_index: isize) -> Option<LayerId> {
|
||||
let mut insert_index = insert_index as i128;
|
||||
|
||||
if insert_index < 0 {
|
||||
insert_index = self.layers.len() as i128 + insert_index as i128 + 1;
|
||||
}
|
||||
|
||||
if insert_index <= self.layers.len() as i128 && insert_index >= 0 {
|
||||
if let Some(id) = id {
|
||||
self.next_assignment_id = id;
|
||||
}
|
||||
if self.layer_ids.contains(&self.next_assignment_id) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let id = self.next_assignment_id;
|
||||
self.layers.insert(insert_index as usize, layer);
|
||||
self.layer_ids.insert(insert_index as usize, id);
|
||||
|
||||
// Linear probing for collision avoidance
|
||||
while self.layer_ids.contains(&self.next_assignment_id) {
|
||||
self.next_assignment_id += 1;
|
||||
}
|
||||
|
||||
Some(id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// 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::FolderLayer;
|
||||
/// let mut folder = FolderLayer::default();
|
||||
///
|
||||
/// // Try to remove a layer that does not exist
|
||||
/// assert!(folder.remove_layer(123).is_err());
|
||||
///
|
||||
/// // Add another folder to the folder
|
||||
/// folder.add_layer(FolderLayer::default().into(), Some(123), -1);
|
||||
///
|
||||
/// // Try to remove that folder again
|
||||
/// assert!(folder.remove_layer(123).is_ok());
|
||||
/// assert_eq!(folder.layers().len(), 0)
|
||||
/// ```
|
||||
pub fn remove_layer(&mut self, id: LayerId) -> Result<(), DocumentError> {
|
||||
let pos = self.position_of_layer(id)?;
|
||||
self.layers.remove(pos);
|
||||
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) -> &[Layer] {
|
||||
self.layers.as_slice()
|
||||
}
|
||||
|
||||
/// Get mutable references to all the [Layer]s in the folder.
|
||||
pub fn layers_mut(&mut self) -> &mut [Layer] {
|
||||
self.layers.as_mut_slice()
|
||||
}
|
||||
|
||||
pub fn layer(&self, id: LayerId) -> Option<&Layer> {
|
||||
let pos = self.position_of_layer(id).ok()?;
|
||||
Some(&self.layers[pos])
|
||||
}
|
||||
|
||||
pub fn layer_mut(&mut self, id: LayerId) -> Option<&mut Layer> {
|
||||
let pos = self.position_of_layer(id).ok()?;
|
||||
Some(&mut self.layers[pos])
|
||||
}
|
||||
|
||||
/// Returns `true` if the folder contains a layer with the given [LayerId].
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use graphite_document_legacy::layers::folder_layer::FolderLayer;
|
||||
/// let mut folder = FolderLayer::default();
|
||||
///
|
||||
/// // Search for an id that does not exist
|
||||
/// assert!(!folder.folder_contains(123));
|
||||
///
|
||||
/// // Add layer with the id "123" to the folder
|
||||
/// folder.add_layer(FolderLayer::default().into(), Some(123), -1);
|
||||
///
|
||||
/// // Search for the id "123"
|
||||
/// assert!(folder.folder_contains(123));
|
||||
/// ```
|
||||
pub fn folder_contains(&self, id: LayerId) -> bool {
|
||||
self.layer_ids.contains(&id)
|
||||
}
|
||||
|
||||
/// Tries to find the index of a layer with the given [LayerId] within the folder.
|
||||
/// This operation will fail if no layer with a matching ID is present in the folder.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use graphite_document_legacy::layers::folder_layer::FolderLayer;
|
||||
/// let mut folder = FolderLayer::default();
|
||||
///
|
||||
/// // Search for an id that does not exist
|
||||
/// assert!(folder.position_of_layer(123).is_err());
|
||||
///
|
||||
/// // Add layer with the id "123" to the folder
|
||||
/// folder.add_layer(FolderLayer::default().into(), Some(123), -1);
|
||||
/// folder.add_layer(FolderLayer::default().into(), Some(42), -1);
|
||||
///
|
||||
/// assert_eq!(folder.position_of_layer(123), Ok(0));
|
||||
/// assert_eq!(folder.position_of_layer(42), Ok(1));
|
||||
/// ```
|
||||
pub fn position_of_layer(&self, layer_id: LayerId) -> Result<usize, DocumentError> {
|
||||
self.layer_ids.iter().position(|x| *x == layer_id).ok_or_else(|| DocumentError::LayerNotFound([layer_id].into()))
|
||||
}
|
||||
|
||||
/// Tries to get a reference to a folder with the given [LayerId].
|
||||
/// This operation will return `None` if either no layer with `id` exists
|
||||
/// in the folder, or the layer with matching ID is not a folder.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use graphite_document_legacy::layers::folder_layer::FolderLayer;
|
||||
/// # use graphite_document_legacy::layers::shape_layer::ShapeLayer;
|
||||
/// # use graphite_document_legacy::layers::style::PathStyle;
|
||||
/// let mut folder = FolderLayer::default();
|
||||
///
|
||||
/// // Search for an id that does not exist
|
||||
/// assert!(folder.folder(132).is_none());
|
||||
///
|
||||
/// // add a folder and search for it
|
||||
/// folder.add_layer(FolderLayer::default().into(), Some(123), -1);
|
||||
/// assert!(folder.folder(123).is_some());
|
||||
///
|
||||
/// // add a non-folder layer and search for it
|
||||
/// folder.add_layer(ShapeLayer::rectangle(PathStyle::default()).into(), Some(42), -1);
|
||||
/// assert!(folder.folder(42).is_none());
|
||||
/// ```
|
||||
pub fn folder(&self, id: LayerId) -> Option<&FolderLayer> {
|
||||
match self.layer(id) {
|
||||
Some(Layer {
|
||||
data: LayerDataType::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 [FolderLayer::folder] method for a usage example.
|
||||
pub fn folder_mut(&mut self, id: LayerId) -> Option<&mut FolderLayer> {
|
||||
match self.layer_mut(id) {
|
||||
Some(Layer {
|
||||
data: LayerDataType::Folder(folder), ..
|
||||
}) => Some(folder),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
use super::base64_serde;
|
||||
use super::layer_info::LayerData;
|
||||
use super::style::{RenderData, ViewMode};
|
||||
use crate::intersection::{intersect_quad_bez_path, Quad};
|
||||
use crate::layers::text_layer::FontCache;
|
||||
use crate::LayerId;
|
||||
|
||||
use glam::{DAffine2, DMat2, DVec2};
|
||||
use kurbo::{Affine, BezPath, Shape as KurboShape};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Write;
|
||||
|
||||
#[derive(Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct ImageLayer {
|
||||
pub mime: String,
|
||||
#[serde(serialize_with = "base64_serde::as_base64", deserialize_with = "base64_serde::from_base64")]
|
||||
pub image_data: std::sync::Arc<Vec<u8>>,
|
||||
// TODO: Have the browser dispose of this blob URL when this is dropped (like when the layer is deleted)
|
||||
#[serde(skip)]
|
||||
pub blob_url: Option<String>,
|
||||
#[serde(skip)]
|
||||
pub dimensions: DVec2,
|
||||
}
|
||||
|
||||
impl LayerData for ImageLayer {
|
||||
fn render(&mut self, svg: &mut String, _svg_defs: &mut String, transforms: &mut Vec<DAffine2>, render_data: RenderData) {
|
||||
let transform = self.transform(transforms, render_data.view_mode);
|
||||
let inverse = transform.inverse();
|
||||
|
||||
if !inverse.is_finite() {
|
||||
let _ = write!(svg, "<!-- SVG shape has an invalid transform -->");
|
||||
return;
|
||||
}
|
||||
|
||||
let _ = writeln!(svg, r#"<g transform="matrix("#);
|
||||
inverse.to_cols_array().iter().enumerate().for_each(|(i, entry)| {
|
||||
let _ = svg.write_str(&(entry.to_string() + if i == 5 { "" } else { "," }));
|
||||
});
|
||||
let _ = svg.write_str(r#")">"#);
|
||||
|
||||
let svg_transform = transform
|
||||
.to_cols_array()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, entry)| entry.to_string() + if i == 5 { "" } else { "," })
|
||||
.collect::<String>();
|
||||
let _ = write!(
|
||||
svg,
|
||||
r#"<image width="{}" height="{}" transform="matrix({})" href="{}"/>"#,
|
||||
self.dimensions.x,
|
||||
self.dimensions.y,
|
||||
svg_transform,
|
||||
self.blob_url.as_ref().unwrap_or(&String::new())
|
||||
);
|
||||
let _ = svg.write_str("</g>");
|
||||
}
|
||||
|
||||
fn bounding_box(&self, transform: glam::DAffine2, _font_cache: &FontCache) -> Option<[DVec2; 2]> {
|
||||
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>>, _font_cache: &FontCache) {
|
||||
if intersect_quad_bez_path(quad, &self.bounds(), true) {
|
||||
intersections.push(path.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ImageLayer {
|
||||
pub fn new(mime: String, image_data: std::sync::Arc<Vec<u8>>) -> Self {
|
||||
Self {
|
||||
mime,
|
||||
image_data,
|
||||
blob_url: None,
|
||||
dimensions: DVec2::ONE,
|
||||
}
|
||||
}
|
||||
|
||||
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(self.dimensions.x, self.dimensions.y)).to_path(0.)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ImageLayer {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("ImageLayer")
|
||||
.field("mime", &self.mime)
|
||||
.field("image_data", &"...")
|
||||
.field("blob_url", &self.blob_url)
|
||||
.field("dimensions", &self.dimensions)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
fn glam_to_kurbo(transform: DAffine2) -> Affine {
|
||||
Affine::new(transform.to_cols_array())
|
||||
}
|
||||
@@ -0,0 +1,584 @@
|
||||
use super::blend_mode::BlendMode;
|
||||
use super::folder_layer::FolderLayer;
|
||||
use super::image_layer::ImageLayer;
|
||||
use super::nodegraph_layer::NodeGraphFrameLayer;
|
||||
use super::shape_layer::ShapeLayer;
|
||||
use super::style::{PathStyle, RenderData};
|
||||
use super::text_layer::TextLayer;
|
||||
use crate::intersection::Quad;
|
||||
use crate::layers::text_layer::FontCache;
|
||||
use crate::DocumentError;
|
||||
use crate::LayerId;
|
||||
|
||||
use graphene_std::vector::subpath::Subpath;
|
||||
|
||||
use core::fmt;
|
||||
use glam::{DAffine2, DMat2, DVec2};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Write;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
/// Represents different types of layers.
|
||||
pub enum LayerDataType {
|
||||
/// A layer that wraps a [FolderLayer] struct.
|
||||
Folder(FolderLayer),
|
||||
/// A layer that wraps a [ShapeLayer] struct.
|
||||
Shape(ShapeLayer),
|
||||
/// A layer that wraps a [TextLayer] struct.
|
||||
Text(TextLayer),
|
||||
/// A layer that wraps an [ImageLayer] struct.
|
||||
Image(ImageLayer),
|
||||
/// A layer that wraps an [NodeGraphFrameLayer] struct.
|
||||
NodeGraphFrame(NodeGraphFrameLayer),
|
||||
}
|
||||
|
||||
impl LayerDataType {
|
||||
pub fn inner(&self) -> &dyn LayerData {
|
||||
match self {
|
||||
LayerDataType::Shape(s) => s,
|
||||
LayerDataType::Folder(f) => f,
|
||||
LayerDataType::Text(t) => t,
|
||||
LayerDataType::Image(i) => i,
|
||||
LayerDataType::NodeGraphFrame(n) => n,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn inner_mut(&mut self) -> &mut dyn LayerData {
|
||||
match self {
|
||||
LayerDataType::Shape(s) => s,
|
||||
LayerDataType::Folder(f) => f,
|
||||
LayerDataType::Text(t) => t,
|
||||
LayerDataType::Image(i) => i,
|
||||
LayerDataType::NodeGraphFrame(n) => n,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||
pub enum LayerDataTypeDiscriminant {
|
||||
Folder,
|
||||
Shape,
|
||||
Text,
|
||||
Image,
|
||||
NodeGraphFrame,
|
||||
}
|
||||
|
||||
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::Text => write!(f, "Text"),
|
||||
LayerDataTypeDiscriminant::Image => write!(f, "Image"),
|
||||
LayerDataTypeDiscriminant::NodeGraphFrame => write!(f, "Node Graph Frame"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&LayerDataType> for LayerDataTypeDiscriminant {
|
||||
fn from(data: &LayerDataType) -> Self {
|
||||
use LayerDataType::*;
|
||||
|
||||
match data {
|
||||
Folder(_) => LayerDataTypeDiscriminant::Folder,
|
||||
Shape(_) => LayerDataTypeDiscriminant::Shape,
|
||||
Text(_) => LayerDataTypeDiscriminant::Text,
|
||||
Image(_) => LayerDataTypeDiscriminant::Image,
|
||||
NodeGraphFrame(_) => LayerDataTypeDiscriminant::NodeGraphFrame,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ** CONVERSIONS **
|
||||
|
||||
impl<'a> TryFrom<&'a mut Layer> for &'a mut Subpath {
|
||||
type Error = &'static str;
|
||||
/// Convert a mutable layer into a mutable [Subpath].
|
||||
fn try_from(layer: &'a mut Layer) -> Result<&'a mut Subpath, Self::Error> {
|
||||
match &mut layer.data {
|
||||
LayerDataType::Shape(layer) => Ok(&mut layer.shape),
|
||||
// TODO Resolve converting text into a Subpath at the layer level
|
||||
// LayerDataType::Text(text) => Some(Subpath::new(path_to_shape.to_vec(), viewport_transform, true)),
|
||||
_ => Err("Did not find any shape data in the layer"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a Layer> for &'a Subpath {
|
||||
type Error = &'static str;
|
||||
/// Convert a reference to a layer into a reference of a [Subpath].
|
||||
fn try_from(layer: &'a Layer) -> Result<&'a Subpath, Self::Error> {
|
||||
match &layer.data {
|
||||
LayerDataType::Shape(layer) => Ok(&layer.shape),
|
||||
// TODO Resolve converting text into a Subpath at the layer level
|
||||
// LayerDataType::Text(text) => Some(Subpath::new(path_to_shape.to_vec(), viewport_transform, true)),
|
||||
_ => 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.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use graphite_document_legacy::layers::shape_layer::ShapeLayer;
|
||||
/// # 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 = ShapeLayer::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(ViewMode::Normal, &font_cache, 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);
|
||||
|
||||
/// Determine the layers within this layer that intersect a given quad.
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use graphite_document_legacy::layers::shape_layer::ShapeLayer;
|
||||
/// # use graphite_document_legacy::layers::style::{Fill, PathStyle, ViewMode};
|
||||
/// # 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 = ShapeLayer::ellipse(PathStyle::new(None, Fill::None));
|
||||
/// let shape_id = 42;
|
||||
/// let mut svg = String::new();
|
||||
///
|
||||
/// let quad = Quad::from_box([DVec2::ZERO, DVec2::ONE]);
|
||||
/// let mut intersections = vec![];
|
||||
///
|
||||
/// shape.intersects_quad(quad, &mut vec![shape_id], &mut intersections, &Default::default());
|
||||
///
|
||||
/// assert_eq!(intersections, vec![vec![shape_id]]);
|
||||
/// ```
|
||||
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, font_cache: &FontCache);
|
||||
|
||||
// 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::ShapeLayer;
|
||||
/// # use graphite_document_legacy::layers::style::{Fill, PathStyle};
|
||||
/// # use graphite_document_legacy::layers::layer_info::LayerData;
|
||||
/// # use glam::f64::{DAffine2, DVec2};
|
||||
/// # use std::collections::HashMap;
|
||||
/// let shape = ShapeLayer::ellipse(PathStyle::new(None, Fill::None));
|
||||
///
|
||||
/// // Calculate the bounding box without applying any transformations.
|
||||
/// // (The identity transform maps every vector to itself.)
|
||||
/// let transform = DAffine2::IDENTITY;
|
||||
/// let bounding_box = shape.bounding_box(transform, &Default::default());
|
||||
///
|
||||
/// assert_eq!(bounding_box, Some([DVec2::ZERO, DVec2::ONE]));
|
||||
/// ```
|
||||
fn bounding_box(&self, transform: glam::DAffine2, font_cache: &FontCache) -> Option<[DVec2; 2]>;
|
||||
}
|
||||
|
||||
impl LayerData for LayerDataType {
|
||||
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<glam::DAffine2>, render_data: RenderData) {
|
||||
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>>, font_cache: &FontCache) {
|
||||
self.inner().intersects_quad(quad, path, intersections, font_cache)
|
||||
}
|
||||
|
||||
fn bounding_box(&self, transform: glam::DAffine2, font_cache: &FontCache) -> Option<[DVec2; 2]> {
|
||||
self.inner().bounding_box(transform, font_cache)
|
||||
}
|
||||
}
|
||||
|
||||
#[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)]
|
||||
pub struct Layer {
|
||||
/// Whether the layer is currently visible or hidden.
|
||||
pub visible: bool,
|
||||
/// The user-given name of the layer.
|
||||
pub name: Option<String>,
|
||||
/// The type of layer, such as folder or shape.
|
||||
pub data: LayerDataType,
|
||||
/// A transformation applied to the layer (translation, rotation, scaling, and shear).
|
||||
#[serde(with = "DAffine2Ref")]
|
||||
pub transform: glam::DAffine2,
|
||||
/// 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 Layer {
|
||||
pub fn new(data: LayerDataType, transform: [f64; 6]) -> Self {
|
||||
Self {
|
||||
visible: true,
|
||||
name: None,
|
||||
data,
|
||||
transform: glam::DAffine2::from_cols_array(&transform),
|
||||
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.,
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterate over the layers encapsulated by this layer.
|
||||
/// If the [Layer type](Layer::data) is not a folder, the only item in the iterator will be the layer itself.
|
||||
/// If the [Layer type](Layer::data) wraps a [Folder](LayerDataType::Folder), the iterator will recursively yield all the layers contained in the folder as well as potential sub-folders.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use graphite_document_legacy::layers::shape_layer::ShapeLayer;
|
||||
/// # use graphite_document_legacy::layers::layer_info::Layer;
|
||||
/// # use graphite_document_legacy::layers::style::PathStyle;
|
||||
/// # use graphite_document_legacy::layers::folder_layer::FolderLayer;
|
||||
/// let mut root_folder = FolderLayer::default();
|
||||
///
|
||||
/// // Add a shape to the root folder
|
||||
/// let child_1: Layer = ShapeLayer::rectangle(PathStyle::default()).into();
|
||||
/// root_folder.add_layer(child_1.clone(), None, -1);
|
||||
///
|
||||
/// // Add a folder containing another shape to the root layer
|
||||
/// let mut child_folder = FolderLayer::default();
|
||||
/// let grandchild: Layer = ShapeLayer::rectangle(PathStyle::default()).into();
|
||||
/// child_folder.add_layer(grandchild.clone(), None, -1);
|
||||
/// let child_2: Layer = child_folder.into();
|
||||
/// root_folder.add_layer(child_2.clone(), None, -1);
|
||||
/// let root: Layer = root_folder.into();
|
||||
///
|
||||
/// let mut iter = root.iter();
|
||||
/// assert_eq!(iter.next(), Some(&root));
|
||||
/// assert_eq!(iter.next(), Some(&child_2));
|
||||
/// assert_eq!(iter.next(), Some(&grandchild));
|
||||
/// assert_eq!(iter.next(), Some(&child_1));
|
||||
/// assert_eq!(iter.next(), None);
|
||||
/// ```
|
||||
pub fn iter(&self) -> LayerIter<'_> {
|
||||
LayerIter { stack: vec![self] }
|
||||
}
|
||||
|
||||
pub fn render(&mut self, transforms: &mut Vec<DAffine2>, svg_defs: &mut String, render_data: RenderData) -> &str {
|
||||
if !self.visible {
|
||||
return "";
|
||||
}
|
||||
|
||||
transforms.push(self.transform);
|
||||
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.font_cache)
|
||||
{
|
||||
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 "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.cache_dirty {
|
||||
self.thumbnail_cache.clear();
|
||||
self.svg_defs_cache.clear();
|
||||
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="mix-blend-mode: {}; opacity: {}">{}</g>"#,
|
||||
self.blend_mode.to_svg_style_name(),
|
||||
self.opacity,
|
||||
self.thumbnail_cache.as_str()
|
||||
);
|
||||
|
||||
self.cache_dirty = false;
|
||||
}
|
||||
transforms.pop();
|
||||
svg_defs.push_str(&self.svg_defs_cache);
|
||||
|
||||
self.cache.as_str()
|
||||
}
|
||||
|
||||
pub fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, font_cache: &FontCache) {
|
||||
if !self.visible {
|
||||
return;
|
||||
}
|
||||
|
||||
let transformed_quad = self.transform.inverse() * quad;
|
||||
self.data.intersects_quad(transformed_quad, path, intersections, font_cache)
|
||||
}
|
||||
|
||||
/// Compute the bounding box of the layer after applying a transform to it.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use graphite_document_legacy::layers::shape_layer::ShapeLayer;
|
||||
/// # use graphite_document_legacy::layers::layer_info::Layer;
|
||||
/// # use graphite_document_legacy::layers::style::PathStyle;
|
||||
/// # 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 = ShapeLayer::rectangle(PathStyle::default()).into();
|
||||
///
|
||||
/// // Apply the Identity transform, which leaves the points unchanged
|
||||
/// assert_eq!(
|
||||
/// layer.aabb_for_transform(DAffine2::IDENTITY, &Default::default()),
|
||||
/// 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, &Default::default()),
|
||||
/// Some([DVec2::ZERO, DVec2::ONE * 2.]),
|
||||
/// );
|
||||
pub fn aabb_for_transform(&self, transform: DAffine2, font_cache: &FontCache) -> Option<[DVec2; 2]> {
|
||||
self.data.bounding_box(transform, font_cache)
|
||||
}
|
||||
|
||||
pub fn aabb(&self, font_cache: &FontCache) -> Option<[DVec2; 2]> {
|
||||
self.aabb_for_transform(self.transform, font_cache)
|
||||
}
|
||||
|
||||
pub fn bounding_transform(&self, font_cache: &FontCache) -> DAffine2 {
|
||||
let scale = match self.aabb_for_transform(DAffine2::IDENTITY, font_cache) {
|
||||
Some([a, b]) => {
|
||||
let dimensions = b - a;
|
||||
DAffine2::from_scale(dimensions)
|
||||
}
|
||||
None => DAffine2::IDENTITY,
|
||||
};
|
||||
|
||||
self.transform * scale
|
||||
}
|
||||
|
||||
pub fn layerspace_pivot(&self, font_cache: &FontCache) -> DVec2 {
|
||||
let [mut min, max] = self.aabb_for_transform(DAffine2::IDENTITY, font_cache).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 `LayerDataType::Folder`.
|
||||
pub fn as_folder_mut(&mut self) -> Result<&mut FolderLayer, DocumentError> {
|
||||
match &mut self.data {
|
||||
LayerDataType::Folder(f) => Ok(f),
|
||||
_ => Err(DocumentError::NotAFolder),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_subpath(&self) -> Option<&Subpath> {
|
||||
match &self.data {
|
||||
LayerDataType::Shape(s) => Some(&s.shape),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_subpath_copy(&self) -> Option<Subpath> {
|
||||
match &self.data {
|
||||
LayerDataType::Shape(s) => Some(s.shape.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_subpath_mut(&mut self) -> Option<&mut Subpath> {
|
||||
match &mut self.data {
|
||||
LayerDataType::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 `LayerDataType::Folder`.
|
||||
pub fn as_folder(&self) -> Result<&FolderLayer, DocumentError> {
|
||||
match &self.data {
|
||||
LayerDataType::Folder(f) => Ok(f),
|
||||
_ => Err(DocumentError::NotAFolder),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a mutable reference to the Text element wrapped by the layer.
|
||||
/// This operation will fail if the [Layer type](Layer::data) is not `LayerDataType::Text`.
|
||||
pub fn as_text_mut(&mut self) -> Result<&mut TextLayer, DocumentError> {
|
||||
match &mut self.data {
|
||||
LayerDataType::Text(t) => Ok(t),
|
||||
_ => Err(DocumentError::NotText),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a reference to the Text element wrapped by the layer.
|
||||
/// This operation will fail if the [Layer type](Layer::data) is not `LayerDataType::Text`.
|
||||
pub fn as_text(&self) -> Result<&TextLayer, DocumentError> {
|
||||
match &self.data {
|
||||
LayerDataType::Text(t) => Ok(t),
|
||||
_ => Err(DocumentError::NotText),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a mutable reference to the Image element wrapped by the layer.
|
||||
/// This operation will fail if the [Layer type](Layer::data) is not `LayerDataType::Image`.
|
||||
pub fn as_image_mut(&mut self) -> Result<&mut ImageLayer, DocumentError> {
|
||||
match &mut self.data {
|
||||
LayerDataType::Image(img) => Ok(img),
|
||||
_ => Err(DocumentError::NotAnImage),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a reference to the Image element wrapped by the layer.
|
||||
/// This operation will fail if the [Layer type](Layer::data) is not `LayerDataType::Image`.
|
||||
pub fn as_image(&self) -> Result<&ImageLayer, DocumentError> {
|
||||
match &self.data {
|
||||
LayerDataType::Image(img) => Ok(img),
|
||||
_ => Err(DocumentError::NotAnImage),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn style(&self) -> Result<&PathStyle, DocumentError> {
|
||||
match &self.data {
|
||||
LayerDataType::Shape(s) => Ok(&s.style),
|
||||
LayerDataType::Text(t) => Ok(&t.path_style),
|
||||
_ => Err(DocumentError::NotAShape),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn style_mut(&mut self) -> Result<&mut PathStyle, DocumentError> {
|
||||
match &mut self.data {
|
||||
LayerDataType::Shape(s) => Ok(&mut s.style),
|
||||
LayerDataType::Text(t) => Ok(&mut t.path_style),
|
||||
_ => Err(DocumentError::NotAShape),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for Layer {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
visible: self.visible,
|
||||
name: self.name.clone(),
|
||||
data: self.data.clone(),
|
||||
transform: self.transform,
|
||||
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<FolderLayer> for Layer {
|
||||
fn from(from: FolderLayer) -> Layer {
|
||||
Layer::new(LayerDataType::Folder(from), DAffine2::IDENTITY.to_cols_array())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ShapeLayer> for Layer {
|
||||
fn from(from: ShapeLayer) -> Layer {
|
||||
Layer::new(LayerDataType::Shape(from), DAffine2::IDENTITY.to_cols_array())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TextLayer> for Layer {
|
||||
fn from(from: TextLayer) -> Layer {
|
||||
Layer::new(LayerDataType::Text(from), DAffine2::IDENTITY.to_cols_array())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ImageLayer> for Layer {
|
||||
fn from(from: ImageLayer) -> Layer {
|
||||
Layer::new(LayerDataType::Image(from), DAffine2::IDENTITY.to_cols_array())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a Layer {
|
||||
type Item = &'a Layer;
|
||||
type IntoIter = LayerIter<'a>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.iter()
|
||||
}
|
||||
}
|
||||
|
||||
/// An iterator over the layers encapsulated by this layer.
|
||||
/// See [Layer::iter] for more information.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct LayerIter<'a> {
|
||||
pub stack: Vec<&'a Layer>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for LayerIter<'a> {
|
||||
type Item = &'a Layer;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match self.stack.pop() {
|
||||
Some(layer) => {
|
||||
if let LayerDataType::Folder(folder) = &layer.data {
|
||||
let layers = folder.layers();
|
||||
self.stack.extend(layers);
|
||||
};
|
||||
Some(layer)
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
//! # Layers
|
||||
//! A document consists of a set of [Layers](layer_info::Layer).
|
||||
//! Layers allow the user to mutate part of the document while leaving the rest unchanged.
|
||||
//! There are currently these different types of layers:
|
||||
//! * [Folder layers](folder_layer::FolderLayer), which encapsulate sub-layers
|
||||
//! * [Shape layers](shape_layer::ShapeLayer), which contain generic SVG [`<path>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path)s
|
||||
//! * [Text layers](text_layer::TextLayer), which contain a description of laid out text
|
||||
//! * [Image layers](image_layer::ImageLayer), which contain a bitmap image
|
||||
//! * [Nodegraph layers](nodegraph_layer::NodegraphLayer), which contain a node graph frame
|
||||
//!
|
||||
//! Refer to the module-level documentation for detailed information on each layer.
|
||||
//!
|
||||
//! ## Overlapping layers
|
||||
//! Layers are rendered on top of each other.
|
||||
//! When different layers overlap, they are blended together according to the [BlendMode](blend_mode::BlendMode)
|
||||
//! using the CSS [`mix-blend-mode`](https://developer.mozilla.org/en-US/docs/Web/CSS/mix-blend-mode) property and the layer opacity.
|
||||
|
||||
pub mod base64_serde;
|
||||
/// Different ways of combining overlapping SVG elements.
|
||||
pub mod blend_mode;
|
||||
/// Contains the [FolderLayer](folder_layer::FolderLayer) type that encapsulates other layers, including more folders.
|
||||
pub mod folder_layer;
|
||||
/// Contains the [ImageLayer](image_layer::ImageLayer) type that contains a bitmap image.
|
||||
pub mod image_layer;
|
||||
/// Contains the base [Layer](layer_info::Layer) type, an abstraction over the different types of layers.
|
||||
pub mod layer_info;
|
||||
/// Contains the [NodegraphLayer](nodegraph_layer::NodegraphLayer) type that contains a node graph.
|
||||
pub mod nodegraph_layer;
|
||||
/// Contains the [ShapeLayer](shape_layer::ShapeLayer) type, a generic SVG element defined using Bezier paths.
|
||||
pub mod shape_layer;
|
||||
pub mod style;
|
||||
/// Contains the [TextLayer](text_layer::TextLayer) type.
|
||||
pub mod text_layer;
|
||||
@@ -0,0 +1,115 @@
|
||||
use super::base64_serde;
|
||||
use super::layer_info::LayerData;
|
||||
use super::style::{RenderData, ViewMode};
|
||||
use crate::intersection::{intersect_quad_bez_path, Quad};
|
||||
use crate::layers::text_layer::FontCache;
|
||||
use crate::LayerId;
|
||||
|
||||
use glam::{DAffine2, DMat2, DVec2};
|
||||
use kurbo::{Affine, BezPath, Shape as KurboShape};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Write;
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)]
|
||||
pub struct NodeGraphFrameLayer {
|
||||
// Image stored in layer after generation completes
|
||||
pub mime: String,
|
||||
|
||||
/// The document node network that this layer contains
|
||||
pub network: graph_craft::document::NodeNetwork,
|
||||
|
||||
// TODO: Have the browser dispose of this blob URL when this is dropped (like when the layer is deleted)
|
||||
#[serde(skip)]
|
||||
pub blob_url: Option<String>,
|
||||
#[serde(skip)]
|
||||
pub dimensions: DVec2,
|
||||
pub image_data: Option<ImageData>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
|
||||
pub struct ImageData {
|
||||
#[serde(serialize_with = "base64_serde::as_base64", deserialize_with = "base64_serde::from_base64")]
|
||||
pub image_data: std::sync::Arc<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl LayerData for NodeGraphFrameLayer {
|
||||
fn render(&mut self, svg: &mut String, _svg_defs: &mut String, transforms: &mut Vec<DAffine2>, render_data: RenderData) {
|
||||
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;
|
||||
}
|
||||
|
||||
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 { "," }));
|
||||
|
||||
if let Some(blob_url) = &self.blob_url {
|
||||
let _ = write!(
|
||||
svg,
|
||||
r#"<image width="{}" height="{}" preserveAspectRatio="none" href="{}" transform="matrix({})" />"#,
|
||||
width.abs(),
|
||||
height.abs(),
|
||||
blob_url,
|
||||
matrix
|
||||
);
|
||||
}
|
||||
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>"#);
|
||||
}
|
||||
|
||||
fn bounding_box(&self, transform: glam::DAffine2, _font_cache: &FontCache) -> Option<[DVec2; 2]> {
|
||||
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>>, _font_cache: &FontCache) {
|
||||
if intersect_quad_bez_path(quad, &self.bounds(), true) {
|
||||
intersections.push(path.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NodeGraphFrameLayer {
|
||||
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.)
|
||||
}
|
||||
}
|
||||
|
||||
fn glam_to_kurbo(transform: DAffine2) -> Affine {
|
||||
Affine::new(transform.to_cols_array())
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
use super::layer_info::LayerData;
|
||||
use super::style::{self, PathStyle, RenderData, ViewMode};
|
||||
use crate::intersection::{intersect_quad_bez_path, Quad};
|
||||
use crate::layers::text_layer::FontCache;
|
||||
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)]
|
||||
pub struct ShapeLayer {
|
||||
/// 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 ShapeLayer {
|
||||
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<DAffine2>, render_data: RenderData) {
|
||||
let mut subpath = self.shape.clone();
|
||||
|
||||
let layer_bounds = subpath.bounding_box().unwrap_or_default();
|
||||
|
||||
let transform = self.transform(transforms, render_data.view_mode);
|
||||
let inverse = transform.inverse();
|
||||
if !inverse.is_finite() {
|
||||
let _ = write!(svg, "<!-- SVG shape has an invalid transform -->");
|
||||
return;
|
||||
}
|
||||
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>");
|
||||
}
|
||||
|
||||
fn bounding_box(&self, transform: glam::DAffine2, _font_cache: &FontCache) -> Option<[DVec2; 2]> {
|
||||
let mut subpath = self.shape.clone();
|
||||
if transform.matrix2 == DMat2::ZERO {
|
||||
return None;
|
||||
}
|
||||
subpath.apply_affine(transform);
|
||||
|
||||
subpath.bounding_box()
|
||||
}
|
||||
|
||||
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, _font_cache: &FontCache) {
|
||||
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 ShapeLayer {
|
||||
/// Construct a new [ShapeLayer] 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,492 @@
|
||||
//! Contains stylistic options for SVG elements.
|
||||
|
||||
use super::text_layer::FontCache;
|
||||
use crate::color::Color;
|
||||
use crate::consts::{LAYER_OUTLINE_STROKE_COLOR, LAYER_OUTLINE_STROKE_WEIGHT};
|
||||
|
||||
use glam::{DAffine2, DVec2};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{self, Display, Write};
|
||||
|
||||
/// Precision of the opacity value in digits after the decimal point.
|
||||
/// A value of 3 would correspond to a precision of 10^-3.
|
||||
const OPACITY_PRECISION: usize = 3;
|
||||
|
||||
fn format_opacity(name: &str, opacity: f32) -> String {
|
||||
if (opacity - 1.).abs() > 10_f32.powi(-(OPACITY_PRECISION as i32)) {
|
||||
format!(r#" {}-opacity="{:.precision$}""#, name, opacity, precision = OPACITY_PRECISION)
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents different ways of rendering an object
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub enum ViewMode {
|
||||
/// Render with normal coloration at the current viewport resolution
|
||||
Normal,
|
||||
/// Render only the outlines of shapes at the current viewport resolution
|
||||
Outline,
|
||||
/// Render with normal coloration at the document resolution, showing the pixels when the current viewport resolution is higher
|
||||
Pixels,
|
||||
}
|
||||
|
||||
impl Default for ViewMode {
|
||||
fn default() -> Self {
|
||||
ViewMode::Normal
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains metadata for rendering the document as an svg
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct RenderData<'a> {
|
||||
pub view_mode: ViewMode,
|
||||
pub font_cache: &'a FontCache,
|
||||
pub culling_bounds: Option<[DVec2; 2]>,
|
||||
}
|
||||
|
||||
impl<'a> RenderData<'a> {
|
||||
pub fn new(view_mode: ViewMode, font_cache: &'a FontCache, culling_bounds: Option<[DVec2; 2]>) -> Self {
|
||||
Self {
|
||||
view_mode,
|
||||
font_cache,
|
||||
culling_bounds,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Copy, Debug, Hash, Serialize, Deserialize)]
|
||||
pub enum GradientType {
|
||||
Linear,
|
||||
Radial,
|
||||
}
|
||||
|
||||
impl Default for GradientType {
|
||||
fn default() -> Self {
|
||||
GradientType::Linear
|
||||
}
|
||||
}
|
||||
|
||||
/// A gradient fill.
|
||||
///
|
||||
/// Contains the start and end points, along with the colors at varying points along the length.
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
|
||||
pub struct Gradient {
|
||||
pub start: DVec2,
|
||||
pub end: DVec2,
|
||||
pub transform: DAffine2,
|
||||
pub positions: Vec<(f64, Option<Color>)>,
|
||||
uuid: u64,
|
||||
pub gradient_type: GradientType,
|
||||
}
|
||||
|
||||
impl Gradient {
|
||||
/// Constructs a new gradient with the colors at 0 and 1 specified.
|
||||
pub fn new(start: DVec2, start_color: Color, end: DVec2, end_color: Color, transform: DAffine2, uuid: u64, gradient_type: GradientType) -> Self {
|
||||
Gradient {
|
||||
start,
|
||||
end,
|
||||
positions: vec![(0., Some(start_color)), (1., Some(end_color))],
|
||||
transform,
|
||||
uuid,
|
||||
gradient_type,
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds the gradient def with the uuid specified
|
||||
fn render_defs(&self, svg_defs: &mut String, multiplied_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2]) {
|
||||
let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
|
||||
let transformed_bound_transform = DAffine2::from_scale_angle_translation(transformed_bounds[1] - transformed_bounds[0], 0., transformed_bounds[0]);
|
||||
let updated_transform = multiplied_transform * bound_transform;
|
||||
|
||||
let positions = self
|
||||
.positions
|
||||
.iter()
|
||||
.filter_map(|(pos, color)| color.map(|color| (pos, color)))
|
||||
.map(|(position, color)| format!(r##"<stop offset="{}" stop-color="#{}" />"##, position, color.rgba_hex()))
|
||||
.collect::<String>();
|
||||
|
||||
let mod_gradient = transformed_bound_transform.inverse();
|
||||
let mod_points = mod_gradient.inverse() * transformed_bound_transform.inverse() * updated_transform;
|
||||
|
||||
let start = mod_points.transform_point2(self.start);
|
||||
let end = mod_points.transform_point2(self.end);
|
||||
|
||||
let transform = mod_gradient
|
||||
.to_cols_array()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, entry)| entry.to_string() + if i == 5 { "" } else { "," })
|
||||
.collect::<String>();
|
||||
|
||||
match self.gradient_type {
|
||||
GradientType::Linear => {
|
||||
let _ = write!(
|
||||
svg_defs,
|
||||
r#"<linearGradient id="{}" x1="{}" x2="{}" y1="{}" y2="{}" gradientTransform="matrix({})">{}</linearGradient>"#,
|
||||
self.uuid, start.x, end.x, start.y, end.y, transform, positions
|
||||
);
|
||||
}
|
||||
GradientType::Radial => {
|
||||
let radius = (f64::powi(start.x - end.x, 2) + f64::powi(start.y - end.y, 2)).sqrt();
|
||||
let _ = write!(
|
||||
svg_defs,
|
||||
r#"<radialGradient id="{}" cx="{}" cy="{}" r="{}" gradientTransform="matrix({})">{}</radialGradient>"#,
|
||||
self.uuid, start.x, start.y, radius, transform, positions
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Describes the fill of a layer.
|
||||
///
|
||||
/// Can be None, a solid [Color], a linear [Gradient], a radial [Gradient] or potentially some sort of image or pattern in the future
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum Fill {
|
||||
None,
|
||||
Solid(Color),
|
||||
Gradient(Gradient),
|
||||
}
|
||||
|
||||
impl Default for Fill {
|
||||
fn default() -> Self {
|
||||
Self::None
|
||||
}
|
||||
}
|
||||
|
||||
impl Fill {
|
||||
/// Construct a new solid [Fill] from a [Color].
|
||||
pub fn solid(color: Color) -> Self {
|
||||
Self::Solid(color)
|
||||
}
|
||||
|
||||
/// Evaluate the color at some point on the fill. Doesn't currently work for Gradient.
|
||||
pub fn color(&self) -> Color {
|
||||
match self {
|
||||
Self::None => Color::BLACK,
|
||||
Self::Solid(color) => *color,
|
||||
// TODO: Should correctly sample the gradient
|
||||
Self::Gradient(Gradient { positions, .. }) => positions[0].1.unwrap_or(Color::BLACK),
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders the fill, adding necessary defs.
|
||||
pub fn render(&self, svg_defs: &mut String, multiplied_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2]) -> String {
|
||||
match self {
|
||||
Self::None => r#" fill="none""#.to_string(),
|
||||
Self::Solid(color) => format!(r##" fill="#{}"{}"##, color.rgb_hex(), format_opacity("fill", color.a())),
|
||||
Self::Gradient(gradient) => {
|
||||
gradient.render_defs(svg_defs, multiplied_transform, bounds, transformed_bounds);
|
||||
format!(r##" fill="url('#{}')""##, gradient.uuid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the fill is not none
|
||||
pub fn is_some(&self) -> bool {
|
||||
*self != Self::None
|
||||
}
|
||||
}
|
||||
|
||||
/// The stroke (outline) style of an SVG element.
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum LineCap {
|
||||
Butt,
|
||||
Round,
|
||||
Square,
|
||||
}
|
||||
|
||||
impl Display for LineCap {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
LineCap::Butt => write!(f, "butt"),
|
||||
LineCap::Round => write!(f, "round"),
|
||||
LineCap::Square => write!(f, "square"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum LineJoin {
|
||||
Miter,
|
||||
Bevel,
|
||||
Round,
|
||||
}
|
||||
|
||||
impl Display for LineJoin {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
LineJoin::Bevel => write!(f, "bevel"),
|
||||
LineJoin::Miter => write!(f, "miter"),
|
||||
LineJoin::Round => write!(f, "round"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Stroke {
|
||||
/// Stroke color
|
||||
color: Option<Color>,
|
||||
/// Line thickness
|
||||
weight: f64,
|
||||
dash_lengths: Vec<f32>,
|
||||
dash_offset: f64,
|
||||
line_cap: LineCap,
|
||||
line_join: LineJoin,
|
||||
line_join_miter_limit: f64,
|
||||
}
|
||||
|
||||
impl Stroke {
|
||||
pub fn new(color: Color, weight: f64) -> Self {
|
||||
Self {
|
||||
color: Some(color),
|
||||
weight,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current stroke color.
|
||||
pub fn color(&self) -> Option<Color> {
|
||||
self.color
|
||||
}
|
||||
|
||||
/// Get the current stroke weight.
|
||||
pub fn weight(&self) -> f64 {
|
||||
self.weight
|
||||
}
|
||||
|
||||
pub fn dash_lengths(&self) -> String {
|
||||
self.dash_lengths.iter().map(|v| v.to_string()).collect::<Vec<_>>().join(", ")
|
||||
}
|
||||
|
||||
pub fn dash_offset(&self) -> f64 {
|
||||
self.dash_offset
|
||||
}
|
||||
|
||||
pub fn line_cap_index(&self) -> u32 {
|
||||
self.line_cap as u32
|
||||
}
|
||||
|
||||
pub fn line_join_index(&self) -> u32 {
|
||||
self.line_join as u32
|
||||
}
|
||||
|
||||
pub fn line_join_miter_limit(&self) -> f32 {
|
||||
self.line_join_miter_limit as f32
|
||||
}
|
||||
|
||||
/// Provide the SVG attributes for the stroke.
|
||||
pub fn render(&self) -> String {
|
||||
if let Some(color) = self.color {
|
||||
format!(
|
||||
r##" stroke="#{}"{} stroke-width="{}" stroke-dasharray="{}" stroke-dashoffset="{}" stroke-linecap="{}" stroke-linejoin="{}" stroke-miterlimit="{}" "##,
|
||||
color.rgb_hex(),
|
||||
format_opacity("stroke", color.a()),
|
||||
self.weight,
|
||||
self.dash_lengths(),
|
||||
self.dash_offset,
|
||||
self.line_cap,
|
||||
self.line_join,
|
||||
self.line_join_miter_limit
|
||||
)
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_color(mut self, color: &Option<Color>) -> Option<Self> {
|
||||
self.color = *color;
|
||||
|
||||
Some(self)
|
||||
}
|
||||
|
||||
pub fn with_weight(mut self, weight: f64) -> Self {
|
||||
self.weight = weight;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_dash_lengths(mut self, dash_lengths: &str) -> Option<Self> {
|
||||
dash_lengths
|
||||
.split(&[',', ' '])
|
||||
.filter(|x| !x.is_empty())
|
||||
.map(str::parse::<f32>)
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.ok()
|
||||
.map(|lengths| {
|
||||
self.dash_lengths = lengths;
|
||||
self
|
||||
})
|
||||
}
|
||||
|
||||
pub fn with_dash_offset(mut self, dash_offset: f64) -> Self {
|
||||
self.dash_offset = dash_offset;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_line_cap(mut self, line_cap: LineCap) -> Self {
|
||||
self.line_cap = line_cap;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_line_join(mut self, line_join: LineJoin) -> Self {
|
||||
self.line_join = line_join;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_line_join_miter_limit(mut self, limit: f64) -> Self {
|
||||
self.line_join_miter_limit = limit;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// Having an alpha of 1 to start with leads to a better experience with the properties panel
|
||||
impl Default for Stroke {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
weight: 0.,
|
||||
color: Some(Color::from_rgba8(0, 0, 0, 255)),
|
||||
dash_lengths: vec![0.],
|
||||
dash_offset: 0.,
|
||||
line_cap: LineCap::Butt,
|
||||
line_join: LineJoin::Miter,
|
||||
line_join_miter_limit: 4.,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
|
||||
pub struct PathStyle {
|
||||
stroke: Option<Stroke>,
|
||||
fill: Fill,
|
||||
}
|
||||
|
||||
impl PathStyle {
|
||||
pub fn new(stroke: Option<Stroke>, fill: Fill) -> Self {
|
||||
Self { stroke, fill }
|
||||
}
|
||||
|
||||
/// Get the current path's [Fill].
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use graphite_document_legacy::layers::style::{Fill, PathStyle};
|
||||
/// # use graphite_document_legacy::color::Color;
|
||||
/// let fill = Fill::solid(Color::RED);
|
||||
/// let style = PathStyle::new(None, fill.clone());
|
||||
///
|
||||
/// assert_eq!(*style.fill(), fill);
|
||||
/// ```
|
||||
pub fn fill(&self) -> &Fill {
|
||||
&self.fill
|
||||
}
|
||||
|
||||
/// Get the current path's [Stroke].
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use graphite_document_legacy::layers::style::{Fill, Stroke, PathStyle};
|
||||
/// # use graphite_document_legacy::color::Color;
|
||||
/// let stroke = Stroke::new(Color::GREEN, 42.);
|
||||
/// let style = PathStyle::new(Some(stroke.clone()), Fill::None);
|
||||
///
|
||||
/// assert_eq!(style.stroke(), Some(stroke));
|
||||
/// ```
|
||||
pub fn stroke(&self) -> Option<Stroke> {
|
||||
self.stroke.clone()
|
||||
}
|
||||
|
||||
/// Replace the path's [Fill] with a provided one.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use graphite_document_legacy::layers::style::{Fill, PathStyle};
|
||||
/// # use graphite_document_legacy::color::Color;
|
||||
/// let mut style = PathStyle::default();
|
||||
///
|
||||
/// assert_eq!(*style.fill(), Fill::None);
|
||||
///
|
||||
/// let fill = Fill::solid(Color::RED);
|
||||
/// style.set_fill(fill.clone());
|
||||
///
|
||||
/// assert_eq!(*style.fill(), fill);
|
||||
/// ```
|
||||
pub fn set_fill(&mut self, fill: Fill) {
|
||||
self.fill = fill;
|
||||
}
|
||||
|
||||
/// Replace the path's [Stroke] with a provided one.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use graphite_document_legacy::layers::style::{Stroke, PathStyle};
|
||||
/// # use graphite_document_legacy::color::Color;
|
||||
/// let mut style = PathStyle::default();
|
||||
///
|
||||
/// assert_eq!(style.stroke(), None);
|
||||
///
|
||||
/// let stroke = Stroke::new(Color::GREEN, 42.);
|
||||
/// style.set_stroke(stroke.clone());
|
||||
///
|
||||
/// assert_eq!(style.stroke(), Some(stroke));
|
||||
/// ```
|
||||
pub fn set_stroke(&mut self, stroke: Stroke) {
|
||||
self.stroke = Some(stroke);
|
||||
}
|
||||
|
||||
/// Set the path's fill to None.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use graphite_document_legacy::layers::style::{Fill, PathStyle};
|
||||
/// # use graphite_document_legacy::color::Color;
|
||||
/// let mut style = PathStyle::new(None, Fill::Solid(Color::RED));
|
||||
///
|
||||
/// assert!(style.fill().is_some());
|
||||
///
|
||||
/// style.clear_fill();
|
||||
///
|
||||
/// assert!(!style.fill().is_some());
|
||||
/// ```
|
||||
pub fn clear_fill(&mut self) {
|
||||
self.fill = Fill::None;
|
||||
}
|
||||
|
||||
/// Set the path's stroke to None.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use graphite_document_legacy::layers::style::{Fill, Stroke, PathStyle};
|
||||
/// # use graphite_document_legacy::color::Color;
|
||||
/// let mut style = PathStyle::new(Some(Stroke::new(Color::GREEN, 42.)), Fill::None);
|
||||
///
|
||||
/// assert!(style.stroke().is_some());
|
||||
///
|
||||
/// style.clear_stroke();
|
||||
///
|
||||
/// assert!(!style.stroke().is_some());
|
||||
/// ```
|
||||
pub fn clear_stroke(&mut self) {
|
||||
self.stroke = None;
|
||||
}
|
||||
|
||||
pub fn render(&self, view_mode: ViewMode, svg_defs: &mut String, multiplied_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2]) -> String {
|
||||
let fill_attribute = match (view_mode, &self.fill) {
|
||||
(ViewMode::Outline, _) => Fill::None.render(svg_defs, multiplied_transform, bounds, transformed_bounds),
|
||||
(_, fill) => fill.render(svg_defs, multiplied_transform, bounds, transformed_bounds),
|
||||
};
|
||||
let stroke_attribute = match (view_mode, &self.stroke) {
|
||||
(ViewMode::Outline, _) => Stroke::new(LAYER_OUTLINE_STROKE_COLOR, LAYER_OUTLINE_STROKE_WEIGHT).render(),
|
||||
(_, Some(stroke)) => stroke.render(),
|
||||
(_, None) => String::new(),
|
||||
};
|
||||
|
||||
format!("{}{}", fill_attribute, stroke_attribute)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
use super::layer_info::LayerData;
|
||||
use super::style::{PathStyle, RenderData, ViewMode};
|
||||
use crate::intersection::{intersect_quad_bez_path, Quad};
|
||||
use crate::LayerId;
|
||||
pub use font_cache::{Font, FontCache};
|
||||
|
||||
use graphene_std::vector::subpath::Subpath;
|
||||
|
||||
use glam::{DAffine2, DMat2, DVec2};
|
||||
use rustybuzz::Face;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Write;
|
||||
|
||||
mod font_cache;
|
||||
mod to_path;
|
||||
|
||||
/// A line, or multiple lines, of text drawn in the document.
|
||||
/// Like [ShapeLayers](super::shape_layer::ShapeLayer), [TextLayer] are rendered as
|
||||
/// [`<path>`s](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path).
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct TextLayer {
|
||||
/// The string of text, encompassing one or multiple lines.
|
||||
pub text: String,
|
||||
/// Fill color and stroke used to render the text.
|
||||
pub path_style: PathStyle,
|
||||
/// Font size in pixels.
|
||||
pub size: f64,
|
||||
pub line_width: Option<f64>,
|
||||
pub font: Font,
|
||||
#[serde(skip)]
|
||||
pub editable: bool,
|
||||
#[serde(skip)]
|
||||
pub cached_path: Option<Subpath>,
|
||||
}
|
||||
|
||||
impl LayerData for TextLayer {
|
||||
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<DAffine2>, render_data: RenderData) {
|
||||
let transform = self.transform(transforms, render_data.view_mode);
|
||||
let inverse = transform.inverse();
|
||||
|
||||
if !inverse.is_finite() {
|
||||
let _ = write!(svg, "<!-- SVG shape has an invalid transform -->");
|
||||
return;
|
||||
}
|
||||
|
||||
let _ = writeln!(svg, r#"<g transform="matrix("#);
|
||||
inverse.to_cols_array().iter().enumerate().for_each(|(i, entry)| {
|
||||
let _ = svg.write_str(&(entry.to_string() + if i == 5 { "" } else { "," }));
|
||||
});
|
||||
let _ = svg.write_str(r#")">"#);
|
||||
|
||||
if self.editable {
|
||||
let font = render_data.font_cache.resolve_font(&self.font);
|
||||
if let Some(url) = font.and_then(|font| render_data.font_cache.get_preview_url(font)) {
|
||||
let _ = write!(svg, r#"<style>@font-face {{font-family: local-font;src: url({});}}")</style>"#, url);
|
||||
}
|
||||
|
||||
let _ = write!(
|
||||
svg,
|
||||
r#"<foreignObject transform="matrix({})"{}></foreignObject>"#,
|
||||
transform
|
||||
.to_cols_array()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, entry)| { entry.to_string() + if i == 5 { "" } else { "," } })
|
||||
.collect::<String>(),
|
||||
font.map(|_| r#" style="font-family: local-font;""#).unwrap_or_default()
|
||||
);
|
||||
} else {
|
||||
let buzz_face = self.load_face(render_data.font_cache);
|
||||
|
||||
let mut path = self.to_subpath(buzz_face);
|
||||
|
||||
let bounds = path.bounding_box().unwrap_or_default();
|
||||
|
||||
path.apply_affine(transform);
|
||||
|
||||
let transformed_bounds = path.bounding_box().unwrap_or_default();
|
||||
|
||||
let _ = write!(
|
||||
svg,
|
||||
r#"<path d="{}" {} />"#,
|
||||
path.to_svg(),
|
||||
self.path_style.render(render_data.view_mode, svg_defs, transform, bounds, transformed_bounds)
|
||||
);
|
||||
}
|
||||
let _ = svg.write_str("</g>");
|
||||
}
|
||||
|
||||
fn bounding_box(&self, transform: glam::DAffine2, font_cache: &FontCache) -> Option<[DVec2; 2]> {
|
||||
let buzz_face = Some(self.load_face(font_cache)?);
|
||||
|
||||
if transform.matrix2 == DMat2::ZERO {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some((transform * self.bounding_box(&self.text, buzz_face)).bounding_box())
|
||||
}
|
||||
|
||||
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, font_cache: &FontCache) {
|
||||
let buzz_face = self.load_face(font_cache);
|
||||
|
||||
if intersect_quad_bez_path(quad, &self.bounding_box(&self.text, buzz_face).path(), true) {
|
||||
intersections.push(path.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TextLayer {
|
||||
pub fn load_face<'a>(&self, font_cache: &'a FontCache) -> Option<Face<'a>> {
|
||||
font_cache.get(&self.font).map(|data| rustybuzz::Face::from_slice(data, 0).expect("Loading font failed"))
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
pub fn new(text: String, style: PathStyle, size: f64, font: Font, font_cache: &FontCache) -> Self {
|
||||
let mut new = Self {
|
||||
text,
|
||||
path_style: style,
|
||||
size,
|
||||
line_width: None,
|
||||
font,
|
||||
editable: false,
|
||||
cached_path: None,
|
||||
};
|
||||
|
||||
new.cached_path = Some(new.generate_path(new.load_face(font_cache)));
|
||||
|
||||
new
|
||||
}
|
||||
|
||||
/// Converts to a [Subpath], populating the cache if necessary.
|
||||
#[inline]
|
||||
pub fn to_subpath(&mut self, buzz_face: Option<Face>) -> Subpath {
|
||||
if self.cached_path.as_ref().filter(|subpath| !subpath.manipulator_groups().is_empty()).is_none() {
|
||||
let path = self.generate_path(buzz_face);
|
||||
self.cached_path = Some(path.clone());
|
||||
return path;
|
||||
}
|
||||
self.cached_path.clone().unwrap()
|
||||
}
|
||||
|
||||
/// Converts to a [Subpath], without populating the cache.
|
||||
#[inline]
|
||||
pub fn to_subpath_nonmut(&self, font_cache: &FontCache) -> Subpath {
|
||||
let buzz_face = self.load_face(font_cache);
|
||||
|
||||
self.cached_path
|
||||
.clone()
|
||||
.filter(|subpath| !subpath.manipulator_groups().is_empty())
|
||||
.unwrap_or_else(|| self.generate_path(buzz_face))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn generate_path(&self, buzz_face: Option<Face>) -> Subpath {
|
||||
to_path::to_path(&self.text, buzz_face, self.size, self.line_width)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn bounding_box(&self, text: &str, buzz_face: Option<Face>) -> Quad {
|
||||
let far = to_path::bounding_box(text, buzz_face, self.size, self.line_width);
|
||||
Quad::from_box([DVec2::ZERO, far])
|
||||
}
|
||||
|
||||
pub fn update_text(&mut self, text: String, font_cache: &FontCache) {
|
||||
let buzz_face = self.load_face(font_cache);
|
||||
|
||||
self.text = text;
|
||||
self.cached_path = Some(self.generate_path(buzz_face));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// A font type (storing font family and font style and an optional preview URL)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Hash, PartialEq, Eq)]
|
||||
pub struct Font {
|
||||
#[serde(rename = "fontFamily")]
|
||||
pub font_family: String,
|
||||
#[serde(rename = "fontStyle")]
|
||||
pub font_style: String,
|
||||
}
|
||||
impl Font {
|
||||
pub fn new(font_family: String, font_style: String) -> Self {
|
||||
Self { font_family, font_style }
|
||||
}
|
||||
}
|
||||
|
||||
/// A cache of all loaded font data and preview urls along with the default font (send from `init_app` in `editor_api.rs`)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct FontCache {
|
||||
/// Actual font file data used for rendering a font with ttf_parser and rustybuzz
|
||||
font_file_data: HashMap<Font, Vec<u8>>,
|
||||
/// Web font preview URLs used for showing fonts when live editing
|
||||
preview_urls: HashMap<Font, String>,
|
||||
/// The default font (used as a fallback)
|
||||
default_font: Option<Font>,
|
||||
}
|
||||
impl FontCache {
|
||||
/// Returns the font family name if the font is cached, otherwise returns the default font family name if that is cached
|
||||
pub fn resolve_font<'a>(&'a self, font: &'a Font) -> Option<&'a Font> {
|
||||
if self.loaded_font(font) {
|
||||
Some(font)
|
||||
} else {
|
||||
self.default_font.as_ref().filter(|font| self.loaded_font(font))
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to get the bytes for a font
|
||||
pub fn get<'a>(&'a self, font: &Font) -> Option<&'a Vec<u8>> {
|
||||
self.resolve_font(font).and_then(|font| self.font_file_data.get(font))
|
||||
}
|
||||
|
||||
/// Check if the font is already loaded
|
||||
pub fn loaded_font(&self, font: &Font) -> bool {
|
||||
self.font_file_data.contains_key(font)
|
||||
}
|
||||
|
||||
/// Insert a new font into the cache
|
||||
pub fn insert(&mut self, font: Font, perview_url: String, data: Vec<u8>, is_default: bool) {
|
||||
if is_default {
|
||||
self.default_font = Some(font.clone());
|
||||
}
|
||||
self.font_file_data.insert(font.clone(), data);
|
||||
self.preview_urls.insert(font, perview_url);
|
||||
}
|
||||
|
||||
/// Checks if the font cache has a default font
|
||||
pub fn has_default(&self) -> bool {
|
||||
self.default_font.is_some()
|
||||
}
|
||||
|
||||
/// Gets the preview URL for showing in text field when live editing
|
||||
pub fn get_preview_url(&self, font: &Font) -> Option<&String> {
|
||||
self.preview_urls.get(font)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
use graphene_std::vector::consts::ManipulatorType;
|
||||
use graphene_std::vector::manipulator_group::ManipulatorGroup;
|
||||
use graphene_std::vector::manipulator_point::ManipulatorPoint;
|
||||
use graphene_std::vector::subpath::Subpath;
|
||||
|
||||
use glam::DVec2;
|
||||
use rustybuzz::{GlyphBuffer, UnicodeBuffer};
|
||||
use ttf_parser::{GlyphId, OutlineBuilder};
|
||||
|
||||
struct Builder {
|
||||
path: Subpath,
|
||||
pos: DVec2,
|
||||
offset: DVec2,
|
||||
ascender: f64,
|
||||
scale: f64,
|
||||
}
|
||||
|
||||
impl Builder {
|
||||
fn point(&self, x: f32, y: f32) -> DVec2 {
|
||||
self.pos + self.offset + DVec2::new(x as f64, self.ascender - y as f64) * self.scale
|
||||
}
|
||||
}
|
||||
|
||||
impl OutlineBuilder for Builder {
|
||||
fn move_to(&mut self, x: f32, y: f32) {
|
||||
let anchor = self.point(x, y);
|
||||
if self.path.manipulator_groups().last().filter(|el| el.points.iter().any(Option::is_some)).is_some() {
|
||||
self.path.manipulator_groups_mut().push_end(ManipulatorGroup::closed());
|
||||
}
|
||||
self.path.manipulator_groups_mut().push_end(ManipulatorGroup::new_with_anchor(anchor));
|
||||
}
|
||||
|
||||
fn line_to(&mut self, x: f32, y: f32) {
|
||||
let anchor = self.point(x, y);
|
||||
self.path.manipulator_groups_mut().push_end(ManipulatorGroup::new_with_anchor(anchor));
|
||||
}
|
||||
|
||||
fn quad_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32) {
|
||||
let [handle, anchor] = [self.point(x1, y1), self.point(x2, y2)];
|
||||
self.path.manipulator_groups_mut().last_mut().unwrap().points[ManipulatorType::OutHandle] = Some(ManipulatorPoint::new(handle, ManipulatorType::OutHandle));
|
||||
self.path.manipulator_groups_mut().push_end(ManipulatorGroup::new_with_anchor(anchor));
|
||||
}
|
||||
|
||||
fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x3: f32, y3: f32) {
|
||||
let [handle1, handle2, anchor] = [self.point(x1, y1), self.point(x2, y2), self.point(x3, y3)];
|
||||
self.path.manipulator_groups_mut().last_mut().unwrap().points[ManipulatorType::OutHandle] = Some(ManipulatorPoint::new(handle1, ManipulatorType::OutHandle));
|
||||
self.path.manipulator_groups_mut().push_end(ManipulatorGroup::new_with_anchor(anchor));
|
||||
self.path.manipulator_groups_mut().last_mut().unwrap().points[ManipulatorType::InHandle] = Some(ManipulatorPoint::new(handle2, ManipulatorType::InHandle));
|
||||
}
|
||||
|
||||
fn close(&mut self) {
|
||||
self.path.manipulator_groups_mut().push_end(ManipulatorGroup::closed());
|
||||
}
|
||||
}
|
||||
|
||||
fn font_properties(buzz_face: &rustybuzz::Face, font_size: f64) -> (f64, f64, UnicodeBuffer) {
|
||||
let scale = (buzz_face.units_per_em() as f64).recip() * font_size;
|
||||
let line_height = font_size;
|
||||
let buffer = UnicodeBuffer::new();
|
||||
(scale, line_height, buffer)
|
||||
}
|
||||
|
||||
fn push_str(buffer: &mut UnicodeBuffer, word: &str, trailing_space: bool) {
|
||||
buffer.push_str(word);
|
||||
|
||||
if trailing_space {
|
||||
buffer.push_str(" ");
|
||||
}
|
||||
}
|
||||
|
||||
fn wrap_word(line_width: Option<f64>, glyph_buffer: &GlyphBuffer, scale: f64, x_pos: f64) -> bool {
|
||||
if let Some(line_width) = line_width {
|
||||
let word_length: i32 = glyph_buffer.glyph_positions().iter().map(|pos| pos.x_advance).sum();
|
||||
let scaled_word_length = word_length as f64 * scale;
|
||||
|
||||
if scaled_word_length + x_pos > line_width {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn to_path(str: &str, buzz_face: Option<rustybuzz::Face>, font_size: f64, line_width: Option<f64>) -> Subpath {
|
||||
let buzz_face = match buzz_face {
|
||||
Some(face) => face,
|
||||
// Show blank layer if font has not loaded
|
||||
None => return Subpath::default(),
|
||||
};
|
||||
|
||||
let (scale, line_height, mut buffer) = font_properties(&buzz_face, font_size);
|
||||
|
||||
let mut builder = Builder {
|
||||
path: Subpath::new(),
|
||||
pos: DVec2::ZERO,
|
||||
offset: DVec2::ZERO,
|
||||
ascender: (buzz_face.ascender() as f64 / buzz_face.height() as f64) * font_size / scale,
|
||||
scale,
|
||||
};
|
||||
|
||||
for line in str.split('\n') {
|
||||
let length = line.split(' ').count();
|
||||
for (index, word) in line.split(' ').enumerate() {
|
||||
push_str(&mut buffer, word, index != length - 1);
|
||||
let glyph_buffer = rustybuzz::shape(&buzz_face, &[], buffer);
|
||||
|
||||
if wrap_word(line_width, &glyph_buffer, scale, builder.pos.x) {
|
||||
builder.pos = DVec2::new(0., builder.pos.y + line_height);
|
||||
}
|
||||
|
||||
for (glyph_position, glyph_info) in glyph_buffer.glyph_positions().iter().zip(glyph_buffer.glyph_infos()) {
|
||||
if let Some(line_width) = line_width {
|
||||
if builder.pos.x + (glyph_position.x_advance as f64 * builder.scale) >= line_width {
|
||||
builder.pos = DVec2::new(0., builder.pos.y + line_height);
|
||||
}
|
||||
}
|
||||
builder.offset = DVec2::new(glyph_position.x_offset as f64, glyph_position.y_offset as f64) * builder.scale;
|
||||
buzz_face.outline_glyph(GlyphId(glyph_info.glyph_id as u16), &mut builder);
|
||||
builder.pos += DVec2::new(glyph_position.x_advance as f64, glyph_position.y_advance as f64) * builder.scale;
|
||||
}
|
||||
|
||||
buffer = glyph_buffer.clear();
|
||||
}
|
||||
builder.pos = DVec2::new(0., builder.pos.y + line_height);
|
||||
}
|
||||
builder.path
|
||||
}
|
||||
|
||||
pub fn bounding_box(str: &str, buzz_face: Option<rustybuzz::Face>, font_size: f64, line_width: Option<f64>) -> DVec2 {
|
||||
let buzz_face = match buzz_face {
|
||||
Some(face) => face,
|
||||
// Show blank layer if font has not loaded
|
||||
None => return DVec2::ZERO,
|
||||
};
|
||||
|
||||
let (scale, line_height, mut buffer) = font_properties(&buzz_face, font_size);
|
||||
|
||||
let mut pos = DVec2::ZERO;
|
||||
let mut bounds = DVec2::ZERO;
|
||||
|
||||
for line in str.split('\n') {
|
||||
let length = line.split(' ').count();
|
||||
for (index, word) in line.split(' ').enumerate() {
|
||||
push_str(&mut buffer, word, index != length - 1);
|
||||
|
||||
let glyph_buffer = rustybuzz::shape(&buzz_face, &[], buffer);
|
||||
|
||||
if wrap_word(line_width, &glyph_buffer, scale, pos.x) {
|
||||
pos = DVec2::new(0., pos.y + line_height);
|
||||
}
|
||||
|
||||
for glyph_position in glyph_buffer.glyph_positions() {
|
||||
if let Some(line_width) = line_width {
|
||||
if pos.x + (glyph_position.x_advance as f64 * scale) >= line_width {
|
||||
pos = DVec2::new(0., pos.y + line_height);
|
||||
}
|
||||
}
|
||||
pos += DVec2::new(glyph_position.x_advance as f64, glyph_position.y_advance as f64) * scale;
|
||||
}
|
||||
bounds = bounds.max(pos + DVec2::new(0., line_height));
|
||||
|
||||
buffer = glyph_buffer.clear();
|
||||
}
|
||||
pos = DVec2::new(0., pos.y + line_height);
|
||||
}
|
||||
|
||||
bounds
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// `macro_use` puts the log macros (`error!`, `warn!`, `debug!`, `info!` and `trace!`) in scope for the crate
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
pub mod boolean_ops;
|
||||
/// Contains the [Color](color::Color) type.
|
||||
pub mod color;
|
||||
/// Contains constant values used by this crate.
|
||||
pub mod consts;
|
||||
pub mod document;
|
||||
/// Defines errors that can occur when using this crate.
|
||||
pub mod error;
|
||||
/// Utilities for computing intersections.
|
||||
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;
|
||||
@@ -0,0 +1,300 @@
|
||||
use crate::boolean_ops::BooleanOperation as BooleanOperationType;
|
||||
use crate::layers::blend_mode::BlendMode;
|
||||
use crate::layers::layer_info::Layer;
|
||||
use crate::layers::style::{self, Stroke};
|
||||
use crate::LayerId;
|
||||
|
||||
use graphene_std::vector::consts::ManipulatorType;
|
||||
use graphene_std::vector::manipulator_group::ManipulatorGroup;
|
||||
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 {
|
||||
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,
|
||||
},
|
||||
AddText {
|
||||
path: Vec<LayerId>,
|
||||
insert_index: isize,
|
||||
transform: [f64; 6],
|
||||
style: style::PathStyle,
|
||||
text: String,
|
||||
size: f64,
|
||||
font_name: String,
|
||||
font_style: String,
|
||||
},
|
||||
AddImage {
|
||||
path: Vec<LayerId>,
|
||||
insert_index: isize,
|
||||
transform: [f64; 6],
|
||||
mime: String,
|
||||
image_data: Vec<u8>,
|
||||
},
|
||||
AddNodeGraphFrame {
|
||||
path: Vec<LayerId>,
|
||||
insert_index: isize,
|
||||
transform: [f64; 6],
|
||||
network: graph_craft::document::NodeNetwork,
|
||||
},
|
||||
SetNodeGraphFrameImageData {
|
||||
layer_path: Vec<LayerId>,
|
||||
image_data: Vec<u8>,
|
||||
},
|
||||
/// 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),
|
||||
},
|
||||
SetTextEditability {
|
||||
path: Vec<LayerId>,
|
||||
editable: bool,
|
||||
},
|
||||
SetTextContent {
|
||||
path: Vec<LayerId>,
|
||||
new_text: String,
|
||||
},
|
||||
AddPolyline {
|
||||
path: Vec<LayerId>,
|
||||
insert_index: isize,
|
||||
transform: [f64; 6],
|
||||
style: style::PathStyle,
|
||||
points: Vec<(f64, f64)>,
|
||||
},
|
||||
AddSpline {
|
||||
path: Vec<LayerId>,
|
||||
insert_index: isize,
|
||||
transform: [f64; 6],
|
||||
style: style::PathStyle,
|
||||
points: Vec<(f64, f64)>,
|
||||
},
|
||||
AddNgon {
|
||||
path: Vec<LayerId>,
|
||||
insert_index: isize,
|
||||
transform: [f64; 6],
|
||||
style: style::PathStyle,
|
||||
sides: u32,
|
||||
},
|
||||
AddShape {
|
||||
path: Vec<LayerId>,
|
||||
insert_index: isize,
|
||||
transform: [f64; 6],
|
||||
style: style::PathStyle,
|
||||
// TODO This will become a compound path once we support them.
|
||||
subpath: Subpath,
|
||||
},
|
||||
BooleanOperation {
|
||||
operation: BooleanOperationType,
|
||||
selected: Vec<Vec<LayerId>>,
|
||||
},
|
||||
DeleteLayer {
|
||||
path: Vec<LayerId>,
|
||||
},
|
||||
DeleteSelectedManipulatorPoints {
|
||||
layer_paths: Vec<Vec<LayerId>>,
|
||||
},
|
||||
DeselectManipulatorPoints {
|
||||
layer_path: Vec<LayerId>,
|
||||
point_ids: Vec<(u64, ManipulatorType)>,
|
||||
},
|
||||
DeselectAllManipulatorPoints {
|
||||
layer_path: Vec<LayerId>,
|
||||
},
|
||||
DuplicateLayer {
|
||||
path: Vec<LayerId>,
|
||||
},
|
||||
ModifyFont {
|
||||
path: Vec<LayerId>,
|
||||
font_family: String,
|
||||
size: f64,
|
||||
font_style: String,
|
||||
},
|
||||
MoveSelectedManipulatorPoints {
|
||||
layer_path: Vec<LayerId>,
|
||||
delta: (f64, f64),
|
||||
},
|
||||
MoveManipulatorPoint {
|
||||
layer_path: Vec<LayerId>,
|
||||
id: u64,
|
||||
manipulator_type: ManipulatorType,
|
||||
position: (f64, f64),
|
||||
},
|
||||
SetManipulatorPoints {
|
||||
layer_path: Vec<LayerId>,
|
||||
id: u64,
|
||||
manipulator_type: ManipulatorType,
|
||||
position: Option<(f64, f64)>,
|
||||
},
|
||||
RenameLayer {
|
||||
layer_path: Vec<LayerId>,
|
||||
new_name: String,
|
||||
},
|
||||
InsertLayer {
|
||||
layer: Box<Layer>,
|
||||
destination_path: Vec<LayerId>,
|
||||
insert_index: isize,
|
||||
},
|
||||
CreateFolder {
|
||||
path: Vec<LayerId>,
|
||||
},
|
||||
TransformLayer {
|
||||
path: Vec<LayerId>,
|
||||
transform: [f64; 6],
|
||||
},
|
||||
TransformLayerInViewport {
|
||||
path: Vec<LayerId>,
|
||||
transform: [f64; 6],
|
||||
},
|
||||
SetLayerTransformInViewport {
|
||||
path: Vec<LayerId>,
|
||||
transform: [f64; 6],
|
||||
},
|
||||
SelectManipulatorPoints {
|
||||
layer_path: Vec<LayerId>,
|
||||
point_ids: Vec<(u64, ManipulatorType)>,
|
||||
add: bool,
|
||||
},
|
||||
SetShapePath {
|
||||
path: Vec<LayerId>,
|
||||
subpath: Subpath,
|
||||
},
|
||||
InsertManipulatorGroup {
|
||||
layer_path: Vec<LayerId>,
|
||||
manipulator_group: ManipulatorGroup,
|
||||
after_id: u64,
|
||||
},
|
||||
PushManipulatorGroup {
|
||||
layer_path: Vec<LayerId>,
|
||||
manipulator_group: ManipulatorGroup,
|
||||
},
|
||||
PushFrontManipulatorGroup {
|
||||
layer_path: Vec<LayerId>,
|
||||
manipulator_group: ManipulatorGroup,
|
||||
},
|
||||
RemoveManipulatorGroup {
|
||||
layer_path: Vec<LayerId>,
|
||||
id: u64,
|
||||
},
|
||||
RemoveManipulatorPoint {
|
||||
layer_path: Vec<LayerId>,
|
||||
id: u64,
|
||||
manipulator_type: ManipulatorType,
|
||||
},
|
||||
TransformLayerInScope {
|
||||
path: Vec<LayerId>,
|
||||
transform: [f64; 6],
|
||||
scope: [f64; 6],
|
||||
},
|
||||
SetLayerTransformInScope {
|
||||
path: Vec<LayerId>,
|
||||
transform: [f64; 6],
|
||||
scope: [f64; 6],
|
||||
},
|
||||
TransformLayerScaleAroundPivot {
|
||||
path: Vec<LayerId>,
|
||||
scale_factor: (f64, f64),
|
||||
},
|
||||
SetLayerScaleAroundPivot {
|
||||
path: Vec<LayerId>,
|
||||
new_scale: (f64, f64),
|
||||
},
|
||||
SetLayerTransform {
|
||||
path: Vec<LayerId>,
|
||||
transform: [f64; 6],
|
||||
},
|
||||
ToggleLayerVisibility {
|
||||
path: Vec<LayerId>,
|
||||
},
|
||||
SetLayerVisibility {
|
||||
path: Vec<LayerId>,
|
||||
visible: bool,
|
||||
},
|
||||
SetLayerName {
|
||||
path: Vec<LayerId>,
|
||||
name: String,
|
||||
},
|
||||
SetLayerBlendMode {
|
||||
path: Vec<LayerId>,
|
||||
blend_mode: BlendMode,
|
||||
},
|
||||
SetLayerOpacity {
|
||||
path: Vec<LayerId>,
|
||||
opacity: f64,
|
||||
},
|
||||
SetLayerStyle {
|
||||
path: Vec<LayerId>,
|
||||
style: style::PathStyle,
|
||||
},
|
||||
SetLayerFill {
|
||||
path: Vec<LayerId>,
|
||||
fill: style::Fill,
|
||||
},
|
||||
SetLayerStroke {
|
||||
path: Vec<LayerId>,
|
||||
stroke: Stroke,
|
||||
},
|
||||
SetManipulatorHandleMirroring {
|
||||
layer_path: Vec<LayerId>,
|
||||
id: u64,
|
||||
mirror_distance: bool,
|
||||
mirror_angle: bool,
|
||||
},
|
||||
SetSelectedHandleMirroring {
|
||||
layer_path: Vec<LayerId>,
|
||||
toggle_distance: bool,
|
||||
toggle_angle: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl Operation {
|
||||
/// Returns the byte representation of the message.
|
||||
///
|
||||
/// # Safety
|
||||
/// This function reads from uninitialized memory!!!
|
||||
/// Only use if you know what you are doing
|
||||
unsafe fn as_slice(&self) -> &[u8] {
|
||||
core::slice::from_raw_parts(self as *const Operation as *const u8, std::mem::size_of::<Operation>())
|
||||
}
|
||||
/// Returns a pseudo hash that should uniquely identify the operation.
|
||||
/// This is needed because `Hash` is not implemented for f64s
|
||||
///
|
||||
/// # Safety
|
||||
/// This function reads from uninitialized memory but the generated value should be fine.
|
||||
pub fn pseudo_hash(&self) -> u64 {
|
||||
let mut s = DefaultHasher::new();
|
||||
unsafe { self.as_slice() }.hash(&mut s);
|
||||
s.finish()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
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>,
|
||||
},
|
||||
DeletedLayer {
|
||||
path: Vec<LayerId>,
|
||||
},
|
||||
/// Triggers an update of the layer in the layer panel.
|
||||
LayerChanged {
|
||||
path: Vec<LayerId>,
|
||||
},
|
||||
}
|
||||
|
||||
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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user