mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
Remove most of document-legacy (#1519)
* Remove boolean ops and unused doc-legacy Operations * Remove Shape legacy layers * Remove legacy layer Properties panel code * Remove additional unused doc-legacy Operations * Removed unused rendering-related legacy-layer code * Upgrade dep so CI builds * Remove various additional unused functions and messages * Remove the LayerData trait * Remove RenderData struct and usages * Banish the Operations system * Further removals
This commit is contained in:
8
Cargo.lock
generated
8
Cargo.lock
generated
@@ -7892,18 +7892,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.7.28"
|
||||
version = "0.7.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d6f15f7ade05d2a4935e34a457b936c23dc70a05cc1d97133dc99e7a3fe0f0e"
|
||||
checksum = "1c4061bedbb353041c12f413700357bec76df2c7e2ca8e4df8bac24c6bf68e3d"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.7.28"
|
||||
version = "0.7.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dbbad221e3f78500350ecbd7dfa4e63ef945c05f4c61cb7f4d3f84cd0bba649b"
|
||||
checksum = "b3c129550b3e6de3fd0ba67ba5c81818f9805e58b8d7fee80a3a59d2c9fc601a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
||||
@@ -1,811 +0,0 @@
|
||||
use crate::consts::F64PRECISE;
|
||||
use crate::intersection::{intersections, line_curve_intersections, valid_t, Intersect, Origin};
|
||||
use crate::layers::shape_layer::ShapeLegacyLayer;
|
||||
use crate::layers::style::PathStyle;
|
||||
|
||||
use kurbo::{BezPath, CubicBez, Line, ParamCurve, ParamCurveArclen, ParamCurveArea, ParamCurveExtrema, PathEl, PathSeg, Point, QuadBez, Rect};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::cell::RefCell;
|
||||
use std::fmt::{self, Debug, Formatter};
|
||||
use std::mem::swap;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, specta::Type)]
|
||||
pub enum BooleanOperation {
|
||||
Union,
|
||||
Difference,
|
||||
Intersection,
|
||||
SubtractFront,
|
||||
SubtractBack,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub enum BooleanOperationError {
|
||||
InvalidSelection,
|
||||
InvalidIntersections,
|
||||
NoIntersections,
|
||||
NothingDone, // Not necessarily an error
|
||||
DirectionUndefined,
|
||||
NoResult,
|
||||
Unexpected, // For debugging, when complete nothing should be unexpected
|
||||
}
|
||||
|
||||
struct Edge {
|
||||
pub from: Origin,
|
||||
pub destination: usize,
|
||||
pub curve: BezPath,
|
||||
}
|
||||
|
||||
impl Debug for Edge {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.write_str(format!("\n To: {}, Type: {:?}", self.destination, self.from).as_str())?;
|
||||
f.write_str(format!(" {:?}", self.curve).as_str())
|
||||
}
|
||||
}
|
||||
|
||||
struct Vertex {
|
||||
pub intersect: Intersect,
|
||||
pub edges: Vec<Edge>,
|
||||
}
|
||||
|
||||
impl Debug for Vertex {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(
|
||||
format!(
|
||||
"\n Intersect Point: {:?} Segment index of A: {:?}, Segment index of B: {:?} t value of A: {:?} t value of B: {:?}",
|
||||
self.intersect.point,
|
||||
self.intersect.segment_index(Origin::Alpha),
|
||||
self.intersect.segment_index(Origin::Beta),
|
||||
self.intersect.t_value(Origin::Alpha),
|
||||
self.intersect.t_value(Origin::Beta),
|
||||
)
|
||||
.as_str(),
|
||||
)?;
|
||||
f.debug_list().entries(self.edges.iter()).finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
|
||||
enum Direction {
|
||||
Ccw,
|
||||
Cw,
|
||||
}
|
||||
|
||||
/// Behavior: Intersection and Union cases are distinguished between by cycle area magnitude.
|
||||
/// This only affects shapes whose intersection is a single shape, and the intersection is similarly sized to the union.
|
||||
/// Can be solved by first computing at low accuracy, and if the values are close recomputing.
|
||||
#[derive(Clone)]
|
||||
struct Cycle {
|
||||
vertices: Vec<(usize, Origin)>,
|
||||
direction: Option<Direction>,
|
||||
area: f64,
|
||||
}
|
||||
|
||||
impl Cycle {
|
||||
pub fn new(start_vertex_index: usize, edge_origin: Origin) -> Self {
|
||||
Cycle {
|
||||
vertices: vec![(start_vertex_index, edge_origin)],
|
||||
direction: None,
|
||||
area: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true when the cycle is complete, a cycle is complete when it revisits its first vertex where edge is the edge traversed in order to get to vertex.
|
||||
/// For purposes of computing direction this function assumes vertices are traversed in order
|
||||
fn extend(&mut self, vertex: usize, edge_origin: Origin, edge_curve: &BezPath) -> bool {
|
||||
self.vertices.push((vertex, edge_origin));
|
||||
self.area += path_area(edge_curve);
|
||||
vertex == self.vertices[0].0
|
||||
}
|
||||
|
||||
/// Returns number of vertices == number of edges in cycle.
|
||||
fn len(&self) -> usize {
|
||||
self.vertices.len() - 1
|
||||
}
|
||||
|
||||
pub fn prev_edge_origin(&self) -> Origin {
|
||||
self.vertices.last().unwrap().1
|
||||
}
|
||||
|
||||
pub fn prev_vertex(&self) -> usize {
|
||||
self.vertices.last().unwrap().0
|
||||
}
|
||||
|
||||
pub fn vertices(&self) -> &Vec<(usize, Origin)> {
|
||||
&self.vertices
|
||||
}
|
||||
|
||||
pub fn area(&self) -> f64 {
|
||||
self.area
|
||||
}
|
||||
|
||||
pub fn direction(&mut self) -> Result<Direction, BooleanOperationError> {
|
||||
match self.direction {
|
||||
Some(direction) => Ok(direction),
|
||||
None => {
|
||||
if self.area > 0.0 {
|
||||
self.direction = Some(Direction::Ccw);
|
||||
Ok(Direction::Ccw)
|
||||
} else if self.area < 0.0 {
|
||||
self.direction = Some(Direction::Cw);
|
||||
Ok(Direction::Cw)
|
||||
} else {
|
||||
Err(BooleanOperationError::DirectionUndefined)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// If the path is empty (has no segments), the function `Err`s.
|
||||
/// If the path crosses itself, the computed direction may (or probably will) be wrong, on account of it not really being defined.
|
||||
pub fn direction_for_path(path: &BezPath) -> Result<Direction, BooleanOperationError> {
|
||||
let mut area = 0.0;
|
||||
path.segments().for_each(|path_segment| area += path_segment.signed_area());
|
||||
if area > 0.0 {
|
||||
Ok(Direction::Ccw)
|
||||
} else if area < 0.0 {
|
||||
Ok(Direction::Cw)
|
||||
} else {
|
||||
Err(BooleanOperationError::DirectionUndefined)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Optimization: store computed segment bounding boxes, or even edge bounding boxes to prevent recomputation.
|
||||
#[derive(Debug)]
|
||||
struct PathGraph {
|
||||
vertices: Vec<Vertex>,
|
||||
}
|
||||
|
||||
/// # Boolean Operation Algorithm
|
||||
/// `PathGraph` represents a directional graph with edges "colored" by `Origin`.
|
||||
/// Each edge also represents a portion of a visible shape.
|
||||
/// Has somewhat (totally?) undefined behavior when shapes have self intersections.
|
||||
impl PathGraph {
|
||||
pub fn from_paths(alpha: &BezPath, beta: &BezPath) -> Result<PathGraph, BooleanOperationError> {
|
||||
let mut new = PathGraph {
|
||||
vertices: intersections(alpha, beta).into_iter().map(|i| Vertex { intersect: i, edges: Vec::new() }).collect(),
|
||||
};
|
||||
// We only consider graphs with even numbers of intersections.
|
||||
// An odd number of intersections occurs when either:
|
||||
// 1. There exists a tangential intersection (which shouldn't affect boolean ops)
|
||||
// 2. The algorithm has found an extra intersection or missed an intersection
|
||||
if new.size() == 0 {
|
||||
return Err(BooleanOperationError::NoIntersections);
|
||||
}
|
||||
if new.size() % 2 != 0 {
|
||||
return Err(BooleanOperationError::InvalidIntersections);
|
||||
}
|
||||
new.add_edges_from_path(alpha, Origin::Alpha);
|
||||
new.add_edges_from_path(beta, Origin::Beta);
|
||||
Ok(new)
|
||||
}
|
||||
|
||||
// TODO: NOTE: about intersection time_val order
|
||||
/// Expects `path` (and all subpaths in `path`) to be closed.
|
||||
/// # Panics
|
||||
/// This function panics when `path` is empty.
|
||||
fn add_edges_from_path(&mut self, path: &BezPath, origin: Origin) {
|
||||
struct AlgorithmState {
|
||||
//current_start holds the index of the vertex the current edge is starting from
|
||||
current_start: Option<usize>,
|
||||
current: Vec<PathSeg>,
|
||||
// in order to iterate through once, store information for incomplete first edge
|
||||
beginning: Vec<PathSeg>,
|
||||
start_index: Option<usize>,
|
||||
// seg index != el_index
|
||||
seg_index: i32,
|
||||
}
|
||||
|
||||
impl AlgorithmState {
|
||||
fn new() -> Self {
|
||||
AlgorithmState {
|
||||
current_start: None,
|
||||
current: Vec::new(),
|
||||
beginning: Vec::new(),
|
||||
start_index: None,
|
||||
seg_index: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.current_start = None;
|
||||
self.current = Vec::new();
|
||||
self.beginning = Vec::new();
|
||||
self.start_index = None;
|
||||
}
|
||||
|
||||
fn advance_by_seg(&mut self, graph: &mut PathGraph, seg: PathSeg, origin: Origin) {
|
||||
let (vertex_ids, mut t_values) = graph.intersects_in_seg(self.seg_index, origin);
|
||||
if !vertex_ids.is_empty() {
|
||||
let subdivided = subdivide_path_seg(&seg, &mut t_values);
|
||||
for (vertex_id, sub_seg) in vertex_ids.into_iter().zip(subdivided.iter()) {
|
||||
match self.current_start {
|
||||
Some(index) => {
|
||||
sub_seg.map(|end_of_edge| self.current.push(end_of_edge));
|
||||
graph.add_edge(origin, index, vertex_id, self.current.clone());
|
||||
self.current_start = Some(vertex_id);
|
||||
self.current = Vec::new();
|
||||
}
|
||||
None => {
|
||||
self.current_start = Some(vertex_id);
|
||||
self.start_index = Some(vertex_id);
|
||||
sub_seg.map(|end_of_beginning| self.beginning.push(end_of_beginning));
|
||||
}
|
||||
}
|
||||
}
|
||||
subdivided.last().unwrap().map(|start_of_edge| self.current.push(start_of_edge));
|
||||
} else {
|
||||
match self.current_start {
|
||||
Some(_) => self.current.push(seg),
|
||||
None => self.beginning.push(seg),
|
||||
}
|
||||
}
|
||||
self.seg_index += 1;
|
||||
}
|
||||
|
||||
fn advance_by_closepath(&mut self, graph: &mut PathGraph, initial_point: &mut Point, origin: Origin) {
|
||||
// When a curve ends in a closepath and its start point does not equal its endpoint they should be connected with a line
|
||||
let last_line = match self.current.last() {
|
||||
Some(start_of_final_edge) => Line {
|
||||
p0: start_of_final_edge.end(),
|
||||
p1: *initial_point,
|
||||
},
|
||||
None => {
|
||||
// When None occurs the current edge has been connected to a vertex.
|
||||
// Either self.beginning is Some or None, if self.beginning is Some there may be a dangling edge to connect
|
||||
// if self.beginning is None, the end of the current edge may not have closed the path
|
||||
match self.beginning.last() {
|
||||
Some(end_of_first_edge) => Line {
|
||||
p0: end_of_first_edge.end(),
|
||||
p1: *initial_point,
|
||||
},
|
||||
None => Line {
|
||||
// should never panic, either a intersection has been encountered, so self.current_start is Some.
|
||||
// or no vertex has been encountered so self.beginning.last() is Some
|
||||
p0: graph.vertex(self.current_start.unwrap()).intersect.point,
|
||||
p1: *initial_point,
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
if last_line.length() > F64PRECISE {
|
||||
// A closepath implicitly defines a line which closes the path and the closepath line may contain intersections
|
||||
self.advance_by_seg(graph, PathSeg::Line(last_line), origin);
|
||||
}
|
||||
}
|
||||
|
||||
fn finalize_sub_path(&mut self, graph: &mut PathGraph, origin: Origin) {
|
||||
if let (Some(current_start_), Some(start_index_)) = (self.current_start, self.start_index) {
|
||||
// Complete the current path
|
||||
self.current.append(&mut self.beginning);
|
||||
graph.add_edge(origin, current_start_, start_index_, self.current.clone());
|
||||
} else {
|
||||
// Path has a subpath with no intersects.
|
||||
// Create a dummy vertex with single edge which will be identified as cycle.
|
||||
let dumb_id = graph.add_vertex(Intersect::new(self.beginning[0].start(), 0.0, 0.0, -1, -1));
|
||||
graph.add_edge(origin, dumb_id, dumb_id, self.beginning.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut algorithm_state = AlgorithmState::new();
|
||||
|
||||
// All valid SVG paths start with a moveto, so this will always be initialized
|
||||
let mut initial_point = Point::new(0.0, 0.0);
|
||||
|
||||
for (el_index, el) in path.iter().enumerate() {
|
||||
match el {
|
||||
PathEl::MoveTo(p) => initial_point = p,
|
||||
PathEl::ClosePath => {
|
||||
algorithm_state.advance_by_closepath(self, &mut initial_point, origin);
|
||||
|
||||
algorithm_state.finalize_sub_path(self, origin);
|
||||
|
||||
algorithm_state.reset();
|
||||
}
|
||||
_ => {
|
||||
algorithm_state.advance_by_seg(self, path.get_seg(el_index).unwrap(), origin);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn add_vertex(&mut self, intersect: Intersect) -> usize {
|
||||
self.vertices.push(Vertex { intersect, edges: Vec::new() });
|
||||
self.vertices.len() - 1
|
||||
}
|
||||
|
||||
fn add_edge(&mut self, origin: Origin, vertex: usize, destination: usize, curve: Vec<PathSeg>) {
|
||||
let new_edge = Edge {
|
||||
from: origin,
|
||||
destination,
|
||||
curve: BezPath::from_path_segments(curve.into_iter()),
|
||||
};
|
||||
self.vertices[vertex].edges.push(new_edge);
|
||||
}
|
||||
|
||||
/// Returns the `Vertex` index and intersect `t_value` for all intersects in the segment identified by `seg_index` from `origin`.
|
||||
/// Sorts both lists for ascending `t_value`.
|
||||
fn intersects_in_seg(&self, seg_index: i32, origin: Origin) -> (Vec<usize>, Vec<f64>) {
|
||||
let mut vertex_index = Vec::new();
|
||||
let mut t_values = Vec::new();
|
||||
for (v_index, vertex) in self.vertices.iter().enumerate() {
|
||||
if vertex.intersect.segment_index(origin) == seg_index {
|
||||
let next_t = vertex.intersect.t_value(origin);
|
||||
let insert_index = match t_values.binary_search_by(|val: &f64| (*val).partial_cmp(&next_t).unwrap_or(std::cmp::Ordering::Less)) {
|
||||
Ok(val) | Err(val) => val,
|
||||
};
|
||||
t_values.insert(insert_index, next_t);
|
||||
vertex_index.insert(insert_index, v_index)
|
||||
}
|
||||
}
|
||||
(vertex_index, t_values)
|
||||
}
|
||||
|
||||
/// Returns the number of vertices in the graph. This is equivalent to the number of intersections.
|
||||
pub fn size(&self) -> usize {
|
||||
self.vertices.len()
|
||||
}
|
||||
|
||||
pub fn vertex(&self, index: usize) -> &Vertex {
|
||||
&self.vertices[index]
|
||||
}
|
||||
|
||||
/// A properly constructed `PathGraph` has no duplicate edges of the same `Origin`.
|
||||
pub fn edge(&self, from: usize, to: usize, origin: Origin) -> Option<&Edge> {
|
||||
// With a data structure restructure, or a hashmap, the `find()` here could be avoided, but it probably has a minimal performance impact
|
||||
self.vertex(from).edges.iter().find(|edge| edge.destination == to && edge.from == origin)
|
||||
}
|
||||
|
||||
/// Where a valid cycle alternates edge `Origin`.
|
||||
/// Single edge/single vertex "dummy" cycles are also valid.
|
||||
fn get_cycle(&self, cycle: &mut Cycle, marker_map: &mut Vec<u8>) {
|
||||
if cycle.prev_edge_origin() == Origin::Alpha {
|
||||
marker_map[cycle.prev_vertex()] |= 1;
|
||||
} else {
|
||||
marker_map[cycle.prev_vertex()] |= 2;
|
||||
}
|
||||
if let Some(next_edge) = self.vertex(cycle.prev_vertex()).edges.iter().find(|edge| edge.from != cycle.prev_edge_origin()) {
|
||||
if !cycle.extend(next_edge.destination, next_edge.from, &next_edge.curve) {
|
||||
self.get_cycle(cycle, marker_map)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_cycles(&self) -> Vec<Cycle> {
|
||||
let mut cycles = Vec::new();
|
||||
let mut markers = vec![0; self.size()];
|
||||
|
||||
self.vertices.iter().enumerate().for_each(|(vertex_index, _vertex)| {
|
||||
if (markers[vertex_index] & 1) == 0 {
|
||||
let mut temp = Cycle::new(vertex_index, Origin::Alpha);
|
||||
self.get_cycle(&mut temp, &mut markers);
|
||||
if temp.len() > 0 {
|
||||
cycles.push(temp);
|
||||
}
|
||||
}
|
||||
if (markers[vertex_index] & 2) == 0 {
|
||||
let mut temp = Cycle::new(vertex_index, Origin::Beta);
|
||||
self.get_cycle(&mut temp, &mut markers);
|
||||
if temp.len() > 0 {
|
||||
cycles.push(temp);
|
||||
}
|
||||
}
|
||||
});
|
||||
cycles
|
||||
}
|
||||
|
||||
pub fn get_shape(&self, cycle: &Cycle, style: &PathStyle) -> ShapeLegacyLayer {
|
||||
let mut curve = Vec::new();
|
||||
let vertices = cycle.vertices();
|
||||
for index in 1..vertices.len() {
|
||||
// We expect the cycle to be valid so this should not panic
|
||||
concat_paths(&mut curve, &self.edge(vertices[index - 1].0, vertices[index].0, vertices[index].1).unwrap().curve);
|
||||
}
|
||||
curve.push(PathEl::ClosePath);
|
||||
ShapeLegacyLayer::new(BezPath::from_vec(curve).iter().into(), style.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// If `t` is on `(0, 1)`, returns the split curve.
|
||||
/// If `t` is outside `[0, 1]`, returns `(None, None)`
|
||||
/// If `t` is 0 returns `(None, p)`.
|
||||
/// If `t` is 1 returns `(p, None)`.
|
||||
pub fn split_path_seg(p: &PathSeg, t: f64) -> (Option<PathSeg>, Option<PathSeg>) {
|
||||
if t <= -F64PRECISE || t >= 1.0 + F64PRECISE {
|
||||
return (None, None);
|
||||
}
|
||||
if t <= F64PRECISE {
|
||||
return (None, Some(*p));
|
||||
}
|
||||
if t >= 1.0 - F64PRECISE {
|
||||
return (Some(*p), None);
|
||||
}
|
||||
match p {
|
||||
PathSeg::Cubic(cubic) => {
|
||||
let a1 = Line::new(cubic.p0, cubic.p1).eval(t);
|
||||
let a2 = Line::new(cubic.p1, cubic.p2).eval(t);
|
||||
let a3 = Line::new(cubic.p2, cubic.p3).eval(t);
|
||||
let b1 = Line::new(a1, a2).eval(t);
|
||||
let b2 = Line::new(a2, a3).eval(t);
|
||||
let c1 = Line::new(b1, b2).eval(t);
|
||||
(
|
||||
Some(PathSeg::Cubic(CubicBez { p0: cubic.p0, p1: a1, p2: b1, p3: c1 })),
|
||||
Some(PathSeg::Cubic(CubicBez { p0: c1, p1: b2, p2: a3, p3: cubic.p3 })),
|
||||
)
|
||||
}
|
||||
PathSeg::Quad(quad) => {
|
||||
let b1 = Line::new(quad.p0, quad.p1).eval(t);
|
||||
let b2 = Line::new(quad.p1, quad.p2).eval(t);
|
||||
let c1 = Line::new(b1, b2).eval(t);
|
||||
(
|
||||
Some(PathSeg::Quad(QuadBez { p0: quad.p0, p1: b1, p2: c1 })),
|
||||
Some(PathSeg::Quad(QuadBez { p0: c1, p1: b2, p2: quad.p2 })),
|
||||
)
|
||||
}
|
||||
PathSeg::Line(line) => {
|
||||
let split = line.eval(t);
|
||||
(Some(PathSeg::Line(Line { p0: line.p0, p1: split })), Some(PathSeg::Line(Line { p0: split, p1: line.p1 })))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Splits `p` at each of `t_values`.
|
||||
/// `t_values` should be sorted in ascending order.
|
||||
/// The length of the returned `Vec` is always equal to `1 + t_values.len()`.
|
||||
pub fn subdivide_path_seg(p: &PathSeg, t_values: &mut [f64]) -> Vec<Option<PathSeg>> {
|
||||
let mut sub_segments = Vec::new();
|
||||
let mut to_split = Some(*p);
|
||||
let mut prev_split = 0.0;
|
||||
for split in t_values {
|
||||
if let Some(to_split_next) = to_split {
|
||||
let (sub_seg, _to_split) = split_path_seg(&to_split_next, (*split - prev_split) / (1.0 - prev_split));
|
||||
to_split = _to_split;
|
||||
sub_segments.push(sub_seg);
|
||||
prev_split = *split;
|
||||
} else {
|
||||
sub_segments.push(None);
|
||||
}
|
||||
}
|
||||
sub_segments.push(to_split);
|
||||
sub_segments
|
||||
}
|
||||
|
||||
pub fn composite_boolean_operation(mut select: BooleanOperation, shapes: &mut Vec<RefCell<ShapeLegacyLayer>>) -> Result<Vec<ShapeLegacyLayer>, BooleanOperationError> {
|
||||
if select == BooleanOperation::SubtractFront {
|
||||
select = BooleanOperation::SubtractBack;
|
||||
let temp_len = shapes.len();
|
||||
shapes.swap(0, temp_len - 1);
|
||||
}
|
||||
match select {
|
||||
BooleanOperation::Union | BooleanOperation::Intersection => {
|
||||
// We must attempt to union each shape with every other shape
|
||||
let mut subject_idx = 0;
|
||||
while subject_idx < shapes.len() {
|
||||
let mut shape_idx = 0;
|
||||
while shape_idx < shapes.len() && subject_idx < shapes.len() {
|
||||
if shape_idx == subject_idx {
|
||||
shape_idx += 1;
|
||||
continue;
|
||||
}
|
||||
let partial_union = boolean_operation(select, &mut shapes[subject_idx].borrow_mut(), &mut shapes[shape_idx].borrow_mut());
|
||||
match partial_union {
|
||||
Ok(temp_union) => {
|
||||
// The result of a successful union will be exactly one shape
|
||||
if let Some(result) = temp_union.into_iter().next() {
|
||||
shapes.push(RefCell::new(result));
|
||||
shapes.swap_remove(subject_idx);
|
||||
shapes.swap_remove(shape_idx);
|
||||
} else {
|
||||
return Err(BooleanOperationError::NoResult);
|
||||
}
|
||||
}
|
||||
Err(BooleanOperationError::NothingDone) => shape_idx += 1,
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
}
|
||||
subject_idx += 1;
|
||||
}
|
||||
Ok(shapes.iter().map(|ref_shape_layer| ref_shape_layer.borrow().clone()).collect())
|
||||
}
|
||||
BooleanOperation::SubtractBack => {
|
||||
let mut result = vec![shapes[0].borrow().clone()];
|
||||
for shape_idx in shapes.iter().skip(1) {
|
||||
let mut temp = Vec::new();
|
||||
for mut partial in result {
|
||||
match boolean_operation(select, &mut partial, &mut shape_idx.borrow_mut()) {
|
||||
Ok(mut partial_result) => temp.append(&mut partial_result),
|
||||
Err(BooleanOperationError::NothingDone) => temp.push(partial),
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
}
|
||||
result = temp; // This move should be done without copying
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
BooleanOperation::Difference => {
|
||||
let mut difference = Vec::new();
|
||||
for shape_idx in 0..shapes.len() {
|
||||
shapes.swap(0, shape_idx);
|
||||
difference.append(&mut composite_boolean_operation(BooleanOperation::SubtractBack, shapes)?);
|
||||
}
|
||||
Ok(difference)
|
||||
}
|
||||
BooleanOperation::SubtractFront => unreachable!("composite boolean operation: unreachable subtract from back"),
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: check if shapes are filled
|
||||
// TODO: Bug: shape with at least two subpaths and comprised of many unions sometimes has erroneous movetos embedded in edges
|
||||
pub fn boolean_operation(mut select: BooleanOperation, alpha: &mut ShapeLegacyLayer, beta: &mut ShapeLegacyLayer) -> Result<Vec<ShapeLegacyLayer>, BooleanOperationError> {
|
||||
if alpha.shape.manipulator_groups().is_empty() || beta.shape.manipulator_groups().is_empty() {
|
||||
return Err(BooleanOperationError::InvalidSelection);
|
||||
}
|
||||
if select == BooleanOperation::SubtractFront {
|
||||
select = BooleanOperation::SubtractBack;
|
||||
swap(alpha, beta);
|
||||
}
|
||||
let mut alpha_shape = close_path(&(&alpha.shape).into());
|
||||
let beta_shape = close_path(&(&beta.shape).into());
|
||||
let beta_reverse = close_path(&reverse_path(&beta_shape));
|
||||
let alpha_dir = Cycle::direction_for_path(&alpha_shape)?;
|
||||
let beta_dir = Cycle::direction_for_path(&beta_shape)?;
|
||||
match select {
|
||||
BooleanOperation::Union => {
|
||||
match if beta_dir == alpha_dir {
|
||||
PathGraph::from_paths(&alpha_shape, &beta_shape)
|
||||
} else {
|
||||
PathGraph::from_paths(&alpha_shape, &beta_reverse)
|
||||
} {
|
||||
Ok(graph) => {
|
||||
let mut cycles = graph.get_cycles();
|
||||
// "extra calls to ParamCurveArea::area here"
|
||||
let mut boolean_union = graph.get_shape(
|
||||
cycles.iter().reduce(|max, cycle| if cycle.area().abs() >= max.area().abs() { cycle } else { max }).unwrap(),
|
||||
&alpha.style,
|
||||
);
|
||||
for interior in collect_shapes(&graph, &mut cycles, |dir| dir != alpha_dir, |_| &alpha.style)? {
|
||||
//TODO: this is not very efficient or nice to read
|
||||
let mut a_path: BezPath = (&boolean_union.shape).into();
|
||||
let b_path: BezPath = (&interior.shape).into();
|
||||
add_subpath(&mut a_path, b_path);
|
||||
boolean_union.shape = a_path.iter().into();
|
||||
}
|
||||
Ok(vec![boolean_union])
|
||||
}
|
||||
Err(BooleanOperationError::NoIntersections) => {
|
||||
// If shape is inside the other the Union is just the larger
|
||||
// Check could also be done with area and single ray cast
|
||||
if cast_horizontal_ray(point_on_curve(&beta_shape), &alpha_shape) % 2 != 0 {
|
||||
Ok(vec![alpha.clone()])
|
||||
} else if cast_horizontal_ray(point_on_curve(&alpha_shape), &beta_shape) % 2 != 0 {
|
||||
beta.style = alpha.style.clone();
|
||||
Ok(vec![beta.clone()])
|
||||
} else {
|
||||
Err(BooleanOperationError::NothingDone)
|
||||
}
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
BooleanOperation::Difference => {
|
||||
let graph = if beta_dir != alpha_dir {
|
||||
PathGraph::from_paths(&alpha_shape, &beta_shape)?
|
||||
} else {
|
||||
PathGraph::from_paths(&alpha_shape, &beta_reverse)?
|
||||
};
|
||||
collect_shapes(&graph, &mut graph.get_cycles(), |_| true, |dir| if dir == alpha_dir { &alpha.style } else { &beta.style })
|
||||
}
|
||||
BooleanOperation::Intersection => {
|
||||
match if beta_dir == alpha_dir {
|
||||
PathGraph::from_paths(&alpha_shape, &beta_shape)
|
||||
} else {
|
||||
PathGraph::from_paths(&alpha_shape, &beta_reverse)
|
||||
} {
|
||||
Ok(graph) => {
|
||||
let mut cycles = graph.get_cycles();
|
||||
// "extra calls to ParamCurveArea::area here"
|
||||
cycles.remove(
|
||||
cycles
|
||||
.iter()
|
||||
.enumerate()
|
||||
.reduce(|(max_index, max), (index, cycle)| if cycle.area().abs() >= max.area().abs() { (index, cycle) } else { (max_index, max) })
|
||||
.unwrap()
|
||||
.0,
|
||||
);
|
||||
collect_shapes(&graph, &mut cycles, |dir| dir == alpha_dir, |_| &alpha.style)
|
||||
}
|
||||
Err(BooleanOperationError::NoIntersections) => {
|
||||
// Check could also be done with area and single ray cast
|
||||
if cast_horizontal_ray(point_on_curve(&beta_shape), &alpha_shape) % 2 != 0 {
|
||||
beta.style = alpha.style.clone();
|
||||
Ok(vec![beta.clone()])
|
||||
} else if cast_horizontal_ray(point_on_curve(&alpha_shape), &beta_shape) % 2 != 0 {
|
||||
Ok(vec![alpha.clone()])
|
||||
} else {
|
||||
Err(BooleanOperationError::NothingDone)
|
||||
}
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
BooleanOperation::SubtractFront => {
|
||||
unreachable!("Boolean operation: unreachable subtract from back");
|
||||
}
|
||||
BooleanOperation::SubtractBack => {
|
||||
match if beta_dir != alpha_dir {
|
||||
PathGraph::from_paths(&alpha_shape, &beta_shape)
|
||||
} else {
|
||||
PathGraph::from_paths(&alpha_shape, &beta_reverse)
|
||||
} {
|
||||
Ok(graph) => collect_shapes(&graph, &mut graph.get_cycles(), |dir| dir == alpha_dir, |_| &alpha.style),
|
||||
Err(BooleanOperationError::NoIntersections) => {
|
||||
if cast_horizontal_ray(point_on_curve(&beta_shape), &alpha_shape) % 2 != 0 {
|
||||
add_subpath(&mut alpha_shape, if beta_dir == alpha_dir { reverse_path(&beta_shape) } else { beta_shape });
|
||||
Ok(vec![alpha.clone()])
|
||||
} else {
|
||||
Err(BooleanOperationError::NothingDone)
|
||||
}
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO check bounding boxes more rigorously
|
||||
pub fn cast_horizontal_ray(from: Point, into: &BezPath) -> usize {
|
||||
let mut ray = PathSeg::Line(Line {
|
||||
p0: from,
|
||||
p1: Point { x: from.x + 1.0, y: from.y },
|
||||
});
|
||||
let mut intersects = Vec::new();
|
||||
for ref mut seg in into.segments() {
|
||||
if kurbo::ParamCurveExtrema::bounding_box(seg).x1 > from.x {
|
||||
line_curve_intersections((&mut ray, seg), |_, b| valid_t(b), &mut intersects);
|
||||
}
|
||||
}
|
||||
intersects.len()
|
||||
}
|
||||
|
||||
/// Uses curve start point as point on the curve.
|
||||
/// # Panics
|
||||
/// This function panics if the `curve` is empty.
|
||||
pub fn point_on_curve(curve: &BezPath) -> Point {
|
||||
curve.segments().next().unwrap().start()
|
||||
}
|
||||
|
||||
/// # Panics
|
||||
/// This function panics if the curve has no `PathSeg`s.
|
||||
pub fn bounding_box(curve: &BezPath) -> Rect {
|
||||
curve
|
||||
.segments()
|
||||
.map(|seg| <PathSeg as ParamCurveExtrema>::bounding_box(&seg))
|
||||
.reduce(|bounds, rect| bounds.union(rect))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn collect_shapes<'a, F, G>(graph: &PathGraph, cycles: &mut Vec<Cycle>, predicate: F, style: G) -> Result<Vec<ShapeLegacyLayer>, BooleanOperationError>
|
||||
where
|
||||
F: Fn(Direction) -> bool,
|
||||
G: Fn(Direction) -> &'a PathStyle,
|
||||
{
|
||||
let mut shapes = Vec::new();
|
||||
|
||||
if cycles.is_empty() {
|
||||
return Err(BooleanOperationError::Unexpected);
|
||||
}
|
||||
|
||||
for cycle in cycles {
|
||||
match cycle.direction() {
|
||||
Ok(dir) => {
|
||||
if predicate(dir) {
|
||||
shapes.push(graph.get_shape(cycle, style(dir)));
|
||||
}
|
||||
}
|
||||
// Exclude cycles with 0.0 area
|
||||
Err(_err) => (),
|
||||
}
|
||||
}
|
||||
Ok(shapes)
|
||||
}
|
||||
|
||||
pub fn reverse_path_segment(seg: &mut PathSeg) {
|
||||
match seg {
|
||||
PathSeg::Line(line) => std::mem::swap(&mut line.p0, &mut line.p1),
|
||||
PathSeg::Quad(quad) => std::mem::swap(&mut quad.p0, &mut quad.p2),
|
||||
PathSeg::Cubic(cubic) => {
|
||||
std::mem::swap(&mut cubic.p0, &mut cubic.p3);
|
||||
std::mem::swap(&mut cubic.p1, &mut cubic.p2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reverses `path` by reversing each `PathSeg`, and reversing the order of `PathSegs` within each subpath.
|
||||
/// Note: a closed path might no longer be closed after applying this function.
|
||||
pub fn reverse_path(path: &BezPath) -> BezPath {
|
||||
let mut curve = Vec::new();
|
||||
let mut temp = Vec::new();
|
||||
let mut path_segments = path.segments();
|
||||
|
||||
for element in path.iter() {
|
||||
match element {
|
||||
PathEl::MoveTo(_) => {
|
||||
curve.append(&mut temp.into_iter().rev().collect());
|
||||
temp = Vec::new();
|
||||
}
|
||||
_ => {
|
||||
if let Some(mut seg) = path_segments.next() {
|
||||
reverse_path_segment(&mut seg);
|
||||
temp.push(seg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
curve.append(&mut temp.into_iter().rev().collect());
|
||||
BezPath::from_path_segments(curve.into_iter())
|
||||
}
|
||||
|
||||
/// Close off all sub-paths in curve by inserting a `ClosePath` whenever a `MoveTo` is not preceded by one.
|
||||
pub fn close_path(curve: &BezPath) -> BezPath {
|
||||
let mut new = BezPath::new();
|
||||
let mut path_closed_flag = true;
|
||||
for el in curve.iter() {
|
||||
match el {
|
||||
PathEl::MoveTo(p) => {
|
||||
if !path_closed_flag {
|
||||
new.push(PathEl::ClosePath);
|
||||
}
|
||||
new.push(PathEl::MoveTo(p));
|
||||
path_closed_flag = false;
|
||||
}
|
||||
PathEl::ClosePath => {
|
||||
path_closed_flag = true;
|
||||
new.push(PathEl::ClosePath);
|
||||
}
|
||||
element => {
|
||||
new.push(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
if !path_closed_flag {
|
||||
new.push(PathEl::ClosePath);
|
||||
}
|
||||
new
|
||||
}
|
||||
|
||||
/// Concatenate `b` to `a`, where `b` is not a new subpath but a continuation of `a`.
|
||||
pub fn concat_paths(a: &mut Vec<PathEl>, b: &BezPath) {
|
||||
if a.is_empty() {
|
||||
a.append(&mut b.elements().to_vec());
|
||||
return;
|
||||
}
|
||||
// Remove closepath
|
||||
if let Some(PathEl::ClosePath) = a.last() {
|
||||
a.remove(a.len() - 1);
|
||||
}
|
||||
// Skip initial `MoveTo`, which should be guaranteed to exist
|
||||
b.iter().skip(1).for_each(|element| a.push(element));
|
||||
}
|
||||
|
||||
/// Concatenate `b` to `a`, where `b` is a new subpath.
|
||||
pub fn add_subpath(a: &mut BezPath, b: BezPath) {
|
||||
b.into_iter().for_each(|el| a.push(el));
|
||||
}
|
||||
|
||||
pub fn path_length(a: &BezPath, accuracy: Option<f64>) -> f64 {
|
||||
let mut sum = 0.0;
|
||||
// Computing arc length with `F64PRECISE` accuracy is probably ridiculous
|
||||
match accuracy {
|
||||
Some(val) => a.segments().for_each(|seg| sum += seg.arclen(val)),
|
||||
None => a.segments().for_each(|seg| sum += seg.arclen(F64PRECISE)),
|
||||
}
|
||||
sum
|
||||
}
|
||||
|
||||
pub fn path_area(a: &BezPath) -> f64 {
|
||||
a.segments().fold(0.0, |mut area, seg| {
|
||||
area += seg.signed_area();
|
||||
area
|
||||
})
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
// BOOLEAN OPERATIONS
|
||||
|
||||
// Bezier curve intersection algorithm
|
||||
pub const F64PRECISE: f64 = f64::EPSILON * ((1 << 7) as f64); // ~= 2^(-45) - For f64 comparisons to allow for rounding error; note that f64::EPSILON ~= 2^(-52)
|
||||
pub const F64LOOSE: f64 = f64::EPSILON * ((1 << 20) as f64); // ~= 2^(-32) - For comparisons between values that are a result of complex computations where error accumulates
|
||||
@@ -1,11 +1,7 @@
|
||||
use crate::document_metadata::{is_artboard, DocumentMetadata, LayerNodeIdentifier};
|
||||
use crate::intersection::Quad;
|
||||
use crate::layers::folder_layer::FolderLegacyLayer;
|
||||
use crate::layers::layer_info::{LayerData, LayerDataTypeDiscriminant, LegacyLayer, LegacyLayerType};
|
||||
use crate::layers::layer_layer::{CachedOutputData, LayerLegacyLayer};
|
||||
use crate::layers::shape_layer::ShapeLegacyLayer;
|
||||
use crate::layers::style::RenderData;
|
||||
use crate::{DocumentError, DocumentResponse, Operation};
|
||||
use crate::layers::layer_info::{LegacyLayer, LegacyLayerType};
|
||||
use crate::DocumentError;
|
||||
|
||||
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeNetwork, NodeOutput};
|
||||
use graphene_core::renderer::ClickTarget;
|
||||
@@ -13,12 +9,10 @@ use graphene_core::transform::Footprint;
|
||||
use graphene_core::{concrete, generic, ProtoNodeIdentifier};
|
||||
use graphene_std::wasm_application_io::WasmEditorApi;
|
||||
|
||||
use glam::{DAffine2, DVec2};
|
||||
use glam::DVec2;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::cmp::max;
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::collections::HashMap;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::hash::Hasher;
|
||||
use std::vec;
|
||||
|
||||
/// A number that identifies a layer.
|
||||
@@ -49,7 +43,11 @@ impl PartialEq for Document {
|
||||
impl Default for Document {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
root: LegacyLayer::new(LegacyLayerType::Folder(FolderLegacyLayer::default()), DAffine2::IDENTITY.to_cols_array()),
|
||||
root: LegacyLayer {
|
||||
name: None,
|
||||
visible: true,
|
||||
data: LegacyLayerType::Folder(FolderLegacyLayer::default()),
|
||||
},
|
||||
state_identifier: DefaultHasher::new(),
|
||||
document_network: {
|
||||
use graph_craft::document::{value::TaggedValue, NodeInput};
|
||||
@@ -155,78 +153,10 @@ impl Document {
|
||||
.reduce(graphene_core::renderer::Quad::combine_bounds)
|
||||
}
|
||||
|
||||
/// Wrapper around render, that returns the whole document as a Response.
|
||||
pub fn render_root(&mut self, render_data: &RenderData) -> String {
|
||||
// Render and append to the defs section
|
||||
let mut svg_defs = String::from("<defs>");
|
||||
self.root.render(&mut vec![], &mut svg_defs, render_data);
|
||||
svg_defs.push_str("</defs>");
|
||||
|
||||
// Append the cached rendered SVG
|
||||
svg_defs.push_str(&self.root.cache);
|
||||
|
||||
svg_defs
|
||||
}
|
||||
|
||||
/// Renders everything below the given layer contained within its parent folder.
|
||||
pub fn render_layers_below(&mut self, below_layer_path: &[LayerId], render_data: &RenderData) -> Option<String> {
|
||||
// Split the path into the layer ID and its parent folder
|
||||
let (layer_id_to_render_below, parent_folder_path) = below_layer_path.split_last()?;
|
||||
|
||||
// Note: it is bad practice to directly clone and modify the document structure, this is a temporary hack until this whole system is replaced by the node graph
|
||||
let mut temp_subset_folder = self.layer_mut(parent_folder_path).ok()?.clone();
|
||||
if let LegacyLayerType::Folder(ref mut folder) = temp_subset_folder.data {
|
||||
// Remove the upper layers to leave behind the lower subset for rendering
|
||||
let count_of_layers_below = folder.layer_ids.iter().position(|id| id == layer_id_to_render_below).unwrap();
|
||||
folder.layer_ids.truncate(count_of_layers_below);
|
||||
folder.layers.truncate(count_of_layers_below);
|
||||
|
||||
// Render and append to the defs section
|
||||
let mut svg_defs = String::from("<defs>");
|
||||
temp_subset_folder.render(&mut vec![], &mut svg_defs, render_data);
|
||||
svg_defs.push_str("</defs>");
|
||||
|
||||
// Append the cached rendered SVG
|
||||
svg_defs.push_str(&temp_subset_folder.cache);
|
||||
|
||||
Some(svg_defs)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders a layer and its children
|
||||
pub fn render_layer(&mut self, layer_path: &[LayerId], render_data: &RenderData) -> Option<String> {
|
||||
// Note: it is bad practice to directly clone and modify the document structure, this is a temporary hack until this whole system is replaced by the node graph
|
||||
let mut temp_clone = self.layer_mut(layer_path).ok()?.clone();
|
||||
|
||||
// Render and append to the defs section
|
||||
let mut svg_defs = String::from("<defs>");
|
||||
temp_clone.render(&mut vec![], &mut svg_defs, render_data);
|
||||
svg_defs.push_str("</defs>");
|
||||
|
||||
// Append the cached rendered SVG
|
||||
svg_defs.push_str(&temp_clone.cache);
|
||||
|
||||
Some(svg_defs)
|
||||
}
|
||||
|
||||
pub fn current_state_identifier(&self) -> u64 {
|
||||
self.state_identifier.finish()
|
||||
}
|
||||
|
||||
/// Checks whether each layer under `path` intersects with the provided `quad` and adds all intersection layers as paths to `intersections`.
|
||||
pub fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, render_data: &RenderData) {
|
||||
self.layer(path).unwrap().intersects_quad(quad, path, intersections, render_data);
|
||||
}
|
||||
|
||||
/// Checks whether each layer under the root path intersects with the provided `quad` and returns the paths to all intersecting layers.
|
||||
pub fn intersects_quad_root(&self, quad: Quad, render_data: &RenderData) -> Vec<Vec<LayerId>> {
|
||||
let mut intersections = Vec::new();
|
||||
self.intersects_quad(quad, &mut vec![], &mut intersections, render_data);
|
||||
intersections
|
||||
}
|
||||
|
||||
/// Returns a reference to the requested folder. Fails if the path does not exist,
|
||||
/// or if the requested layer is not of type folder.
|
||||
pub fn folder(&self, path: impl AsRef<[LayerId]>) -> Result<&FolderLegacyLayer, DocumentError> {
|
||||
@@ -239,7 +169,6 @@ impl Document {
|
||||
|
||||
/// Returns a mutable reference to the requested folder. Fails if the path does not exist,
|
||||
/// or if the requested layer is not of type folder.
|
||||
/// If you manually edit the folder you have to set the cache_dirty flag yourself.
|
||||
fn folder_mut(&mut self, path: &[LayerId]) -> Result<&mut FolderLegacyLayer, DocumentError> {
|
||||
let mut root = &mut self.root;
|
||||
for id in path {
|
||||
@@ -270,15 +199,6 @@ impl Document {
|
||||
layers.reduce(|a, b| &a[..a.iter().zip(b.iter()).take_while(|&(a, b)| a == b).count()]).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Filters out the non folders from an iterator of paths.
|
||||
/// Takes and Iterator over &[LayerId] or &Vec<LayerId>.
|
||||
pub fn folders<'a, T>(&'a self, layers: impl Iterator<Item = T> + 'a) -> impl Iterator<Item = T> + 'a
|
||||
where
|
||||
T: AsRef<[LayerId]> + std::cmp::Ord + 'a,
|
||||
{
|
||||
layers.filter(|layer| self.is_folder(layer.as_ref()))
|
||||
}
|
||||
|
||||
/// Returns the shallowest folder given the selection, even if the selection doesn't contain any folders
|
||||
pub fn shallowest_common_folder<'a>(&self, layers: impl Iterator<Item = &'a [LayerId]>) -> Result<&'a [LayerId], DocumentError> {
|
||||
let common_prefix_of_path = self.common_layer_path_prefix(layers);
|
||||
@@ -289,15 +209,6 @@ impl Document {
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns all folders that are not contained in any other of the given folders
|
||||
/// Takes and Iterator over &[LayerId] or &Vec<LayerId>.
|
||||
pub fn shallowest_folders<'a, T>(&'a self, layers: impl Iterator<Item = T>) -> Vec<T>
|
||||
where
|
||||
T: AsRef<[LayerId]> + std::cmp::Ord + 'a,
|
||||
{
|
||||
Self::shallowest_unique_layers(self.folders(layers))
|
||||
}
|
||||
|
||||
/// Returns all layers that are not contained in any other of the given folders
|
||||
/// Takes and Iterator over &[LayerId] or &Vec<LayerId>.
|
||||
pub fn shallowest_unique_layers<'a, T>(layers: impl Iterator<Item = T>) -> Vec<T>
|
||||
@@ -310,71 +221,6 @@ impl Document {
|
||||
sorted_layers.dedup_by(|a, b| a.as_ref().starts_with(b.as_ref()));
|
||||
sorted_layers
|
||||
}
|
||||
/// Deepest to shallowest (longest to shortest path length)
|
||||
/// Takes and Iterator over &[LayerId] or &Vec<LayerId>.
|
||||
pub fn sorted_folders_by_depth<'a, T>(&'a self, layers: impl Iterator<Item = T>) -> Vec<T>
|
||||
where
|
||||
T: AsRef<[LayerId]> + std::cmp::Ord + 'a,
|
||||
{
|
||||
let mut folders: Vec<_> = self.folders(layers).collect();
|
||||
folders.sort_by_key(|a| std::cmp::Reverse(a.as_ref().len()));
|
||||
folders
|
||||
}
|
||||
|
||||
pub fn folder_children_paths(&self, path: &[LayerId]) -> Vec<Vec<LayerId>> {
|
||||
if let Ok(folder) = self.folder(path) {
|
||||
folder.list_layers().iter().map(|f| [path, &[*f]].concat()).collect()
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_folder(&self, path: impl AsRef<[LayerId]>) -> bool {
|
||||
return self.folder(path.as_ref()).is_ok();
|
||||
}
|
||||
|
||||
// Determines which layer is closer to the root, if path_a return true, if path_b return false
|
||||
// Answers the question: Is A closer to the root than B?
|
||||
pub fn layer_closer_to_root(&self, path_a: &[u64], path_b: &[u64]) -> bool {
|
||||
// Convert UUIDs to indices
|
||||
let indices_for_path_a = self.indices_for_path(path_a).unwrap();
|
||||
let indices_for_path_b = self.indices_for_path(path_b).unwrap();
|
||||
|
||||
let longest = max(indices_for_path_a.len(), indices_for_path_b.len());
|
||||
for i in 0..longest {
|
||||
// usize::MAX becomes negative one here, sneaky. So folders are compared as [X, -1]. This is intentional.
|
||||
let index_a = *indices_for_path_a.get(i).unwrap_or(&usize::MAX) as i32;
|
||||
let index_b = *indices_for_path_b.get(i).unwrap_or(&usize::MAX) as i32;
|
||||
|
||||
// At the point at which the two paths first differ, compare to see which is closer to the root
|
||||
if index_a != index_b {
|
||||
// If index_a is smaller, index_a is closer to the root
|
||||
return index_a < index_b;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
// Is the target layer between a <-> b layers, inclusive
|
||||
pub fn layer_is_between(&self, target: &[u64], path_a: &[u64], path_b: &[u64]) -> bool {
|
||||
// If the target is the root, it isn't between
|
||||
if target.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// This function is inclusive, so we consider path_a, path_b to be between themselves
|
||||
if target == path_a || target == path_b {
|
||||
return true;
|
||||
};
|
||||
|
||||
// These can't both be true and be between two values
|
||||
let layer_vs_a = self.layer_closer_to_root(target, path_a);
|
||||
let layer_vs_b = self.layer_closer_to_root(target, path_b);
|
||||
|
||||
// To be in-between you need to be above A and below B or vice versa
|
||||
layer_vs_a != layer_vs_b
|
||||
}
|
||||
|
||||
/// Given a path to a layer, returns a vector of the indices in the layer tree
|
||||
/// These indices can be used to order a list of layers
|
||||
@@ -387,703 +233,23 @@ impl Document {
|
||||
for id in path {
|
||||
let pos = root.layer_ids.iter().position(|x| *x == *id).ok_or_else(|| DocumentError::LayerNotFound(path.into()))?;
|
||||
indices.push(pos);
|
||||
root = root.folder(*id).ok_or_else(|| DocumentError::LayerNotFound(path.into()))?;
|
||||
root = match root.layer(*id) {
|
||||
Some(LegacyLayer {
|
||||
data: LegacyLayerType::Folder(folder),
|
||||
..
|
||||
}) => Some(folder),
|
||||
_ => None,
|
||||
}
|
||||
.ok_or_else(|| DocumentError::LayerNotFound(path.into()))?;
|
||||
}
|
||||
|
||||
indices.push(root.layer_ids.iter().position(|x| *x == layer_id).ok_or_else(|| DocumentError::LayerNotFound(path.into()))?);
|
||||
|
||||
Ok(indices)
|
||||
}
|
||||
|
||||
/// Replaces the layer at the specified `path` with `layer`.
|
||||
pub fn set_layer(&mut self, path: &[LayerId], layer: LegacyLayer, insert_index: isize) -> Result<(), DocumentError> {
|
||||
let mut folder = self.root.as_folder_mut()?;
|
||||
let mut layer_id = None;
|
||||
if let Ok((path, id)) = split_path(path) {
|
||||
layer_id = Some(id);
|
||||
self.mark_as_dirty(path)?;
|
||||
folder = self.folder_mut(path)?;
|
||||
if let Some(folder_layer) = folder.layer_mut(id) {
|
||||
*folder_layer = layer;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
folder.add_layer(layer, layer_id, insert_index).ok_or(DocumentError::IndexOutOfBounds)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Visit each layer recursively, marks all children as dirty
|
||||
pub fn mark_children_as_dirty(layer: &mut LegacyLayer) -> bool {
|
||||
match layer.data {
|
||||
LegacyLayerType::Folder(ref mut folder) => {
|
||||
for sub_layer in folder.layers_mut() {
|
||||
if Document::mark_children_as_dirty(sub_layer) {
|
||||
layer.cache_dirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => layer.cache_dirty = true,
|
||||
}
|
||||
layer.cache_dirty
|
||||
}
|
||||
|
||||
/// Adds a new layer to the folder specified by `path`.
|
||||
/// Passing a negative `insert_index` indexes relative to the end.
|
||||
/// -1 is equivalent to adding the layer to the top.
|
||||
pub fn add_layer(&mut self, path: &[LayerId], layer: LegacyLayer, insert_index: isize) -> Result<LayerId, DocumentError> {
|
||||
let folder = self.folder_mut(path)?;
|
||||
folder.add_layer(layer, None, insert_index).ok_or(DocumentError::IndexOutOfBounds)
|
||||
}
|
||||
|
||||
/// Deletes the layer specified by `path`.
|
||||
pub fn delete(&mut self, path: &[LayerId]) -> Result<(), DocumentError> {
|
||||
let (path, id) = split_path(path)?;
|
||||
self.mark_as_dirty(path)?;
|
||||
self.folder_mut(path)?.remove_layer(id)
|
||||
}
|
||||
|
||||
pub fn visible_layers(&self, path: &mut Vec<LayerId>, paths: &mut Vec<Vec<LayerId>>) -> Result<(), DocumentError> {
|
||||
if !self.layer(path)?.visible {
|
||||
return Ok(());
|
||||
}
|
||||
if let Ok(folder) = self.folder(&path) {
|
||||
for layer in folder.layer_ids.iter() {
|
||||
path.push(*layer);
|
||||
self.visible_layers(path, paths)?;
|
||||
path.pop();
|
||||
}
|
||||
} else {
|
||||
paths.push(path.clone());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn viewport_bounding_box(&self, path: &[LayerId], render_data: &RenderData) -> Result<Option<[DVec2; 2]>, DocumentError> {
|
||||
let layer = self.layer(path)?;
|
||||
let transform = self.multiply_transforms(path)?;
|
||||
Ok(layer.data.bounding_box(transform, render_data))
|
||||
}
|
||||
|
||||
pub fn bounding_box_and_transform(&self, path: &[LayerId], render_data: &RenderData) -> Result<Option<([DVec2; 2], DAffine2)>, DocumentError> {
|
||||
let layer = self.layer(path)?;
|
||||
let transform = self.multiply_transforms(&path[..path.len() - 1])?;
|
||||
Ok(layer.data.bounding_box(layer.transform, render_data).map(|bounds| (bounds, transform)))
|
||||
}
|
||||
|
||||
/// Compute the center of transformation multiplied with `Document::multiply_transforms`.
|
||||
pub fn pivot(&self, path: &[LayerId], render_data: &RenderData) -> Option<DVec2> {
|
||||
let layer = self.layer(path).ok()?;
|
||||
Some(self.multiply_transforms(path).unwrap_or_default().transform_point2(layer.layerspace_pivot(render_data)))
|
||||
}
|
||||
|
||||
pub fn visible_layers_bounding_box(&self, render_data: &RenderData) -> Option<[DVec2; 2]> {
|
||||
let mut paths = vec![];
|
||||
self.visible_layers(&mut vec![], &mut paths).ok()?;
|
||||
self.combined_viewport_bounding_box(paths.iter().map(|x| x.as_slice()), render_data)
|
||||
}
|
||||
|
||||
pub fn combined_viewport_bounding_box<'a>(&self, paths: impl Iterator<Item = &'a [LayerId]>, render_data: &RenderData) -> Option<[DVec2; 2]> {
|
||||
let boxes = paths.filter_map(|path| self.viewport_bounding_box(path, render_data).ok()?);
|
||||
boxes.reduce(|a, b| [a[0].min(b[0]), a[1].max(b[1])])
|
||||
}
|
||||
|
||||
/// Mark the layer at the provided path, as well as all the folders containing it, as dirty.
|
||||
pub fn mark_upstream_as_dirty(&mut self, path: &[LayerId]) -> Result<(), DocumentError> {
|
||||
let mut root = &mut self.root;
|
||||
root.cache_dirty = true;
|
||||
for id in path {
|
||||
root = root.as_folder_mut()?.layer_mut(*id).ok_or_else(|| DocumentError::LayerNotFound(path.into()))?;
|
||||
root.cache_dirty = true;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn mark_downstream_as_dirty(&mut self, path: &[LayerId]) -> Result<(), DocumentError> {
|
||||
let layer = self.layer_mut(path)?;
|
||||
layer.cache_dirty = true;
|
||||
|
||||
let mut path = path.to_vec();
|
||||
let len = path.len();
|
||||
path.push(0);
|
||||
|
||||
if let Some(ids) = layer.as_folder().ok().map(|f| f.layer_ids.clone()) {
|
||||
for id in ids {
|
||||
path[len] = id;
|
||||
self.mark_downstream_as_dirty(&path)?
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// For the purposes of rendering, this invalidates the render cache for the layer so it must be re-rendered next time.
|
||||
pub fn mark_as_dirty(&mut self, path: &[LayerId]) -> Result<(), DocumentError> {
|
||||
self.mark_upstream_as_dirty(path)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn transforms(&self, path: &[LayerId]) -> Result<Vec<DAffine2>, DocumentError> {
|
||||
let mut root = &self.root;
|
||||
let mut transforms = vec![self.root.transform];
|
||||
for id in path {
|
||||
if let Ok(folder) = root.as_folder() {
|
||||
root = folder.layer(*id).ok_or_else(|| DocumentError::LayerNotFound(path.into()))?;
|
||||
}
|
||||
transforms.push(root.transform);
|
||||
}
|
||||
Ok(transforms)
|
||||
}
|
||||
|
||||
pub fn multiply_transforms(&self, path: &[LayerId]) -> Result<DAffine2, DocumentError> {
|
||||
let mut root = &self.root;
|
||||
let mut trans = self.root.transform;
|
||||
for id in path {
|
||||
if let Ok(folder) = root.as_folder() {
|
||||
root = folder.layer(*id).ok_or_else(|| DocumentError::LayerNotFound(path.into()))?;
|
||||
}
|
||||
trans = trans * root.transform;
|
||||
}
|
||||
Ok(trans)
|
||||
}
|
||||
|
||||
pub fn generate_transform_across_scope(&self, from: &[LayerId], to: Option<DAffine2>) -> Result<DAffine2, DocumentError> {
|
||||
let from_rev = self.multiply_transforms(from)?;
|
||||
let scope = to.unwrap_or(DAffine2::IDENTITY);
|
||||
Ok(scope * from_rev)
|
||||
}
|
||||
|
||||
pub fn transform_relative_to_scope(&mut self, layer: &[LayerId], scope: Option<DAffine2>, transform: DAffine2) -> Result<(), DocumentError> {
|
||||
let to = self.generate_transform_across_scope(&layer[..layer.len() - 1], scope)?;
|
||||
let layer = self.layer_mut(layer)?;
|
||||
layer.transform = to.inverse() * transform * to * layer.transform;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_transform_relative_to_scope(&mut self, layer: &[LayerId], scope: Option<DAffine2>, transform: DAffine2) -> Result<(), DocumentError> {
|
||||
let to = self.generate_transform_across_scope(&layer[..layer.len() - 1], scope)?;
|
||||
let layer = self.layer_mut(layer)?;
|
||||
layer.transform = to.inverse() * transform;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn generate_transform_relative_to_viewport(&self, from: &[LayerId]) -> Result<DAffine2, DocumentError> {
|
||||
self.generate_transform_across_scope(from, None)
|
||||
}
|
||||
|
||||
pub fn apply_transform_relative_to_viewport(&mut self, layer: &[LayerId], transform: DAffine2) -> Result<(), DocumentError> {
|
||||
self.transform_relative_to_scope(layer, None, transform)
|
||||
}
|
||||
|
||||
pub fn set_transform_relative_to_viewport(&mut self, layer: &[LayerId], transform: DAffine2) -> Result<(), DocumentError> {
|
||||
self.set_transform_relative_to_scope(layer, None, transform)
|
||||
}
|
||||
|
||||
/// Mutate the document by applying the `operation` to it. If the operation necessitates a
|
||||
/// reaction from the frontend, responses may be returned.
|
||||
pub fn handle_operation(&mut self, operation: Operation) -> Result<Option<Vec<DocumentResponse>>, DocumentError> {
|
||||
use DocumentResponse::*;
|
||||
|
||||
operation.pseudo_hash().hash(&mut self.state_identifier);
|
||||
|
||||
let responses = match operation {
|
||||
Operation::AddEllipse { path, insert_index, transform, style } => {
|
||||
let layer = LegacyLayer::new(LegacyLayerType::Shape(ShapeLegacyLayer::ellipse(style)), transform);
|
||||
|
||||
self.set_layer(&path, layer, insert_index)?;
|
||||
|
||||
let mut responses = vec![
|
||||
DocumentChanged,
|
||||
CreatedLayer {
|
||||
path: path.clone(),
|
||||
is_selected: true,
|
||||
},
|
||||
];
|
||||
responses.extend(update_thumbnails_upstream(&path));
|
||||
|
||||
Some(responses)
|
||||
}
|
||||
Operation::AddRect { path, insert_index, transform, style } => {
|
||||
let layer = LegacyLayer::new(LegacyLayerType::Shape(ShapeLegacyLayer::rectangle(style)), transform);
|
||||
|
||||
self.set_layer(&path, layer, insert_index)?;
|
||||
|
||||
let mut responses = vec![
|
||||
DocumentChanged,
|
||||
CreatedLayer {
|
||||
path: path.clone(),
|
||||
is_selected: true,
|
||||
},
|
||||
];
|
||||
responses.extend(update_thumbnails_upstream(&path));
|
||||
|
||||
Some(responses)
|
||||
}
|
||||
Operation::AddLine { path, insert_index, transform, style } => {
|
||||
let layer = LegacyLayer::new(LegacyLayerType::Shape(ShapeLegacyLayer::line(style)), transform);
|
||||
|
||||
self.set_layer(&path, layer, insert_index)?;
|
||||
|
||||
let mut responses = vec![
|
||||
DocumentChanged,
|
||||
CreatedLayer {
|
||||
path: path.clone(),
|
||||
is_selected: true,
|
||||
},
|
||||
];
|
||||
responses.extend(update_thumbnails_upstream(&path));
|
||||
|
||||
Some(responses)
|
||||
}
|
||||
// TODO: Remove
|
||||
Operation::AddFrame {
|
||||
path,
|
||||
insert_index,
|
||||
transform,
|
||||
network,
|
||||
} => {
|
||||
let layer = LegacyLayer::new(LegacyLayerType::Layer(LayerLegacyLayer { network, ..Default::default() }), transform);
|
||||
|
||||
self.set_layer(&path, layer, insert_index)?;
|
||||
|
||||
let mut responses = vec![
|
||||
DocumentChanged,
|
||||
CreatedLayer {
|
||||
path: path.clone(),
|
||||
is_selected: true,
|
||||
},
|
||||
];
|
||||
responses.extend(update_thumbnails_upstream(&path));
|
||||
|
||||
Some(responses)
|
||||
}
|
||||
Operation::SetLayerPreserveAspect { layer_path, preserve_aspect } => {
|
||||
if let Ok(layer) = self.layer_mut(&layer_path) {
|
||||
layer.preserve_aspect = preserve_aspect;
|
||||
}
|
||||
Some(vec![LayerChanged { path: layer_path.clone() }])
|
||||
}
|
||||
Operation::AddShape {
|
||||
path,
|
||||
transform,
|
||||
insert_index,
|
||||
style,
|
||||
subpath,
|
||||
} => {
|
||||
let shape = ShapeLegacyLayer::new(subpath, style);
|
||||
self.set_layer(&path, LegacyLayer::new(LegacyLayerType::Shape(shape), transform), insert_index)?;
|
||||
Some(vec![DocumentChanged, CreatedLayer { path, is_selected: true }])
|
||||
}
|
||||
Operation::AddPolyline {
|
||||
path,
|
||||
insert_index,
|
||||
points,
|
||||
transform,
|
||||
style,
|
||||
} => {
|
||||
let points: Vec<glam::DVec2> = points.iter().map(|&it| it.into()).collect();
|
||||
self.set_layer(&path, LegacyLayer::new(LegacyLayerType::Shape(ShapeLegacyLayer::poly_line(points, style)), transform), insert_index)?;
|
||||
|
||||
let mut responses = vec![
|
||||
DocumentChanged,
|
||||
CreatedLayer {
|
||||
path: path.clone(),
|
||||
is_selected: true,
|
||||
},
|
||||
];
|
||||
responses.extend(update_thumbnails_upstream(&path));
|
||||
|
||||
Some(responses)
|
||||
}
|
||||
Operation::DeleteLayer { path } => {
|
||||
fn aggregate_deletions(folder: &FolderLegacyLayer, path: &mut Vec<LayerId>, responses: &mut Vec<DocumentResponse>) {
|
||||
for (id, layer) in folder.layer_ids.iter().zip(folder.layers()) {
|
||||
path.push(*id);
|
||||
responses.push(DocumentResponse::DeletedLayer { path: path.clone() });
|
||||
if let LegacyLayerType::Folder(f) = &layer.data {
|
||||
aggregate_deletions(f, path, responses);
|
||||
}
|
||||
path.pop();
|
||||
}
|
||||
}
|
||||
let mut responses = Vec::new();
|
||||
if let Ok(folder) = self.folder(&path) {
|
||||
aggregate_deletions(folder, &mut path.clone(), &mut responses)
|
||||
};
|
||||
self.delete(&path)?;
|
||||
|
||||
let (folder, _) = split_path(path.as_slice()).unwrap_or((&[], 0));
|
||||
responses.extend([DocumentChanged, DeletedLayer { path: path.clone() }, FolderChanged { path: folder.to_vec() }]);
|
||||
responses.extend(update_thumbnails_upstream(folder));
|
||||
Some(responses)
|
||||
}
|
||||
Operation::InsertLayer {
|
||||
destination_path,
|
||||
layer,
|
||||
insert_index,
|
||||
duplicating,
|
||||
} => {
|
||||
let (folder_path, layer_id) = split_path(&destination_path)?;
|
||||
let mut responses = vec![DocumentChanged];
|
||||
|
||||
// If we are duplicating, use the parent layer path as the folder we insert to
|
||||
let (created_layer_path, folder_changed_path) = if duplicating {
|
||||
let folder = self.folder_mut(&destination_path)?;
|
||||
let new_layer_id = folder.add_layer(*layer, None, insert_index).ok_or(DocumentError::IndexOutOfBounds)?;
|
||||
|
||||
([destination_path.as_slice(), &[new_layer_id]].concat(), destination_path.clone())
|
||||
} else {
|
||||
let folder = self.folder_mut(folder_path)?;
|
||||
folder.add_layer(*layer, Some(layer_id), insert_index).ok_or(DocumentError::IndexOutOfBounds)?;
|
||||
|
||||
(destination_path.clone(), folder_path.to_vec())
|
||||
};
|
||||
|
||||
responses.push(CreatedLayer {
|
||||
path: created_layer_path,
|
||||
is_selected: !duplicating,
|
||||
});
|
||||
responses.push(FolderChanged { path: folder_changed_path.to_vec() });
|
||||
responses.extend(update_thumbnails_upstream(&destination_path));
|
||||
|
||||
self.mark_as_dirty(&destination_path)?;
|
||||
|
||||
// Recursively iterate through each layer in a folder and add it to the responses vector
|
||||
fn aggregate_insertions(folder: &FolderLegacyLayer, path: &mut Vec<LayerId>, responses: &mut Vec<DocumentResponse>, duplicating: bool) {
|
||||
for (id, layer) in folder.layer_ids.iter().zip(folder.layers()) {
|
||||
path.push(*id);
|
||||
|
||||
responses.push(DocumentResponse::CreatedLayer {
|
||||
path: path.clone(),
|
||||
is_selected: !duplicating,
|
||||
});
|
||||
if let LegacyLayerType::Folder(f) = &layer.data {
|
||||
aggregate_insertions(f, path, responses, duplicating);
|
||||
}
|
||||
|
||||
path.pop();
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(folder) = self.folder(&destination_path) {
|
||||
aggregate_insertions(folder, &mut destination_path.as_slice().to_vec(), &mut responses, duplicating);
|
||||
};
|
||||
|
||||
Some(responses)
|
||||
}
|
||||
Operation::DuplicateLayer { path } => {
|
||||
// Notes for review: I wasn't sure on how to apply unwrap_or() to lines of code that use the function self.layer()
|
||||
|
||||
let layer = self.layer(&path)?.clone();
|
||||
let layer_is_folder = layer.as_folder().is_ok();
|
||||
let (folder_path, _) = split_path(path.as_slice()).unwrap_or((&[], 0));
|
||||
|
||||
// Recursively collect each of the nested folders and shapes if the layer is a folder
|
||||
fn recursive_collect(document: &mut Document, layer_path: &[u64]) -> Vec<Vec<u64>> {
|
||||
let mut duplicated_layers_so_far = Vec::new();
|
||||
|
||||
let children = document.folder_children_paths(layer_path);
|
||||
for child in children {
|
||||
if document.is_folder(&child) {
|
||||
duplicated_layers_so_far.push(child.to_vec());
|
||||
duplicated_layers_so_far.append(&mut recursive_collect(document, &child));
|
||||
} else {
|
||||
duplicated_layers_so_far.push(child);
|
||||
}
|
||||
}
|
||||
|
||||
duplicated_layers_so_far
|
||||
}
|
||||
let mut duplicated_layers = if layer_is_folder { recursive_collect(self, &path) } else { Vec::new() };
|
||||
|
||||
// Iterate through each layer path and collect the corresponding Layer objects into one vector
|
||||
let duplicated_layers_objects = duplicated_layers
|
||||
.iter()
|
||||
.map(|layer_path| self.layer(layer_path.as_slice()).cloned().ok())
|
||||
.collect::<Option<Vec<_>>>()
|
||||
.ok_or(DocumentError::InvalidPath)?;
|
||||
|
||||
// Sort both vectors by the layer path depth, from shallowest (fewest) to deepest (most)
|
||||
let mut indices: Vec<usize> = (0..duplicated_layers.len()).collect();
|
||||
indices.sort_by_key(|&i| duplicated_layers[i].len());
|
||||
duplicated_layers.sort_by_key(|a| a.len());
|
||||
let duplicate_layer_objects_sorted: Vec<&LegacyLayer> = indices.iter().map(|&i| &duplicated_layers_objects[i]).collect();
|
||||
|
||||
let folder = self.folder_mut(folder_path)?;
|
||||
|
||||
let selected_id = path.last().copied().unwrap_or_default();
|
||||
let insert_index = folder.layer_ids.iter().position(|&id| id == selected_id).unwrap_or(0) as isize + 1;
|
||||
|
||||
if let Some(new_layer_id) = folder.add_layer(layer, None, insert_index) {
|
||||
let new_path = [folder_path, &[new_layer_id]].concat();
|
||||
|
||||
let mut responses = vec![
|
||||
DocumentChanged,
|
||||
CreatedLayer {
|
||||
path: new_path.clone(),
|
||||
is_selected: true,
|
||||
},
|
||||
FolderChanged { path: folder_path.to_vec() },
|
||||
];
|
||||
responses.extend(update_thumbnails_upstream(path.as_slice()));
|
||||
|
||||
if layer_is_folder {
|
||||
let new_folder = self.folder_mut(&new_path)?;
|
||||
// Clear the new folders layer_ids/layers because they contain the layer_ids/layers of the layers that were duplicated
|
||||
new_folder.layer_ids = vec![];
|
||||
new_folder.layers = vec![];
|
||||
// Generate a new next assignment ID to avoid collision
|
||||
new_folder.generate_new_folder_ids();
|
||||
|
||||
let mut old_to_new_layer_id: HashMap<LayerId, LayerId> = HashMap::new();
|
||||
|
||||
for (i, duplicate_layer) in duplicated_layers.into_iter().enumerate() {
|
||||
let Some(old_layer_id) = duplicate_layer.last().cloned() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Iterate through each ID of the current duplicate layer
|
||||
// If the dictionary contains the ID, we know the duplicate folder has been created already. Use the existing layer ID instead of creating a new one
|
||||
let sub_layer = &duplicate_layer[new_path.len()..];
|
||||
let new_sub_path: Vec<u64> = sub_layer.iter().filter_map(|id| old_to_new_layer_id.get(id).cloned()).collect();
|
||||
|
||||
// Combine the new path with the IDs of the duplicate layer path to create the path where we insert the duplicate layer
|
||||
let mut updated_layer = duplicate_layer_objects_sorted.get(i).unwrap().to_owned().clone();
|
||||
let updated_layer_path_parent = [new_path.clone(), new_sub_path].concat();
|
||||
|
||||
// Clear the new folder's layer_ids and layers because they contain the layer_ids/layers of the layer were duplicated
|
||||
if self.is_folder(duplicate_layer) {
|
||||
let updated_layer_as_folder: &mut FolderLegacyLayer = updated_layer.as_folder_mut()?;
|
||||
updated_layer_as_folder.layer_ids = vec![];
|
||||
updated_layer_as_folder.layers = vec![];
|
||||
updated_layer_as_folder.generate_new_folder_ids()
|
||||
}
|
||||
|
||||
let result = self
|
||||
.handle_operation(Operation::InsertLayer {
|
||||
layer: Box::new(updated_layer),
|
||||
destination_path: updated_layer_path_parent,
|
||||
insert_index: -1,
|
||||
duplicating: true,
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or_default();
|
||||
|
||||
// Collect the new ID of the duplicated layer from the InsertLayer Operation
|
||||
// Map the layer ID of the layer we're duplicating to the new ID
|
||||
if let DocumentResponse::CreatedLayer { path, .. } = result.get(1).unwrap() {
|
||||
if let Some(new_layer_id) = path.last() {
|
||||
old_to_new_layer_id.entry(old_layer_id).or_insert(*new_layer_id);
|
||||
}
|
||||
}
|
||||
|
||||
responses.extend(result);
|
||||
}
|
||||
}
|
||||
|
||||
self.mark_as_dirty(folder_path)?;
|
||||
|
||||
Some(responses)
|
||||
} else {
|
||||
return Err(DocumentError::IndexOutOfBounds);
|
||||
}
|
||||
}
|
||||
Operation::RenameLayer { layer_path: path, new_name: name } => {
|
||||
self.layer_mut(&path)?.name = Some(name);
|
||||
Some(vec![LayerChanged { path }])
|
||||
}
|
||||
Operation::CreateFolder { path, insert_index } => {
|
||||
self.set_layer(
|
||||
&path,
|
||||
LegacyLayer::new(LegacyLayerType::Folder(FolderLegacyLayer::default()), DAffine2::IDENTITY.to_cols_array()),
|
||||
insert_index,
|
||||
)?;
|
||||
self.mark_as_dirty(&path)?;
|
||||
|
||||
let mut responses = vec![
|
||||
DocumentChanged,
|
||||
CreatedLayer {
|
||||
path: path.clone(),
|
||||
is_selected: true,
|
||||
},
|
||||
];
|
||||
responses.extend(update_thumbnails_upstream(&path));
|
||||
|
||||
Some(responses)
|
||||
}
|
||||
Operation::TransformLayer { path, transform } => {
|
||||
let layer = self.layer_mut(&path).unwrap();
|
||||
let transform = DAffine2::from_cols_array(&transform) * layer.transform;
|
||||
layer.transform = transform;
|
||||
self.mark_as_dirty(&path)?;
|
||||
Some([vec![DocumentChanged], update_thumbnails_upstream(&path)].concat())
|
||||
}
|
||||
Operation::TransformLayerInViewport { path, transform } => {
|
||||
let transform = DAffine2::from_cols_array(&transform);
|
||||
self.apply_transform_relative_to_viewport(&path, transform)?;
|
||||
self.mark_as_dirty(&path)?;
|
||||
Some([vec![DocumentChanged], update_thumbnails_upstream(&path)].concat())
|
||||
}
|
||||
Operation::SetLayerBlobUrl { layer_path, blob_url, resolution: _ } => {
|
||||
let layer = self.layer_mut(&layer_path).unwrap_or_else(|_| panic!("Blob URL for invalid layer with path '{layer_path:?}'"));
|
||||
|
||||
let LegacyLayerType::Layer(layer) = &mut layer.data else {
|
||||
panic!("Incorrectly trying to set the image blob URL for a layer that is not a 'Layer' layer type");
|
||||
};
|
||||
|
||||
layer.cached_output_data = CachedOutputData::BlobURL(blob_url);
|
||||
|
||||
self.mark_as_dirty(&layer_path)?;
|
||||
Some([vec![DocumentChanged, LayerChanged { path: layer_path.clone() }], update_thumbnails_upstream(&layer_path)].concat())
|
||||
}
|
||||
Operation::ClearBlobURL { path } => {
|
||||
let layer = self.layer_mut(&path).expect("Clearing node graph image for invalid layer");
|
||||
match &mut layer.data {
|
||||
LegacyLayerType::Layer(layer) => {
|
||||
if matches!(layer.cached_output_data, CachedOutputData::BlobURL(_)) {
|
||||
layer.cached_output_data = CachedOutputData::None;
|
||||
}
|
||||
}
|
||||
e => panic!("Incorrectly trying to clear the blob URL for layer of type {}", LayerDataTypeDiscriminant::from(&*e)),
|
||||
}
|
||||
self.mark_as_dirty(&path)?;
|
||||
Some([vec![DocumentChanged, LayerChanged { path: path.clone() }], update_thumbnails_upstream(&path)].concat())
|
||||
}
|
||||
Operation::SetPivot { layer_path, pivot } => {
|
||||
let layer = self.layer_mut(&layer_path).expect("Setting pivot for invalid layer");
|
||||
layer.pivot = pivot.into();
|
||||
self.mark_as_dirty(&layer_path)?;
|
||||
Some([vec![DocumentChanged, LayerChanged { path: layer_path.clone() }], update_thumbnails_upstream(&layer_path)].concat())
|
||||
}
|
||||
Operation::SetLayerTransformInViewport { path, transform } => {
|
||||
let transform = DAffine2::from_cols_array(&transform);
|
||||
self.set_transform_relative_to_viewport(&path, transform)?;
|
||||
self.mark_as_dirty(&path)?;
|
||||
Some([vec![DocumentChanged], update_thumbnails_upstream(&path)].concat())
|
||||
}
|
||||
Operation::SetShapePath { path, subpath } => {
|
||||
self.mark_as_dirty(&path)?;
|
||||
|
||||
if let LegacyLayerType::Shape(shape) = &mut self.layer_mut(&path)?.data {
|
||||
shape.shape = subpath;
|
||||
}
|
||||
Some(vec![DocumentChanged, LayerChanged { path }])
|
||||
}
|
||||
Operation::SetVectorData { path, vector_data } => {
|
||||
if let LegacyLayerType::Layer(layer) = &mut self.layer_mut(&path)?.data {
|
||||
layer.cached_output_data = CachedOutputData::VectorPath(Box::new(vector_data));
|
||||
}
|
||||
Some(Vec::new())
|
||||
}
|
||||
Operation::SetSurface { path, surface_id } => {
|
||||
if let LegacyLayerType::Layer(layer) = &mut self.layer_mut(&path)?.data {
|
||||
layer.cached_output_data = CachedOutputData::SurfaceId(surface_id);
|
||||
}
|
||||
Some(Vec::new())
|
||||
}
|
||||
Operation::SetSvg { path, svg } => {
|
||||
if let LegacyLayerType::Layer(layer) = &mut self.layer_mut(&path)?.data {
|
||||
layer.cached_output_data = CachedOutputData::Svg(svg);
|
||||
}
|
||||
Some(Vec::new())
|
||||
}
|
||||
Operation::TransformLayerInScope { path, transform, scope } => {
|
||||
let transform = DAffine2::from_cols_array(&transform);
|
||||
let scope = DAffine2::from_cols_array(&scope);
|
||||
self.transform_relative_to_scope(&path, Some(scope), transform)?;
|
||||
self.mark_as_dirty(&path)?;
|
||||
Some([vec![DocumentChanged], update_thumbnails_upstream(&path)].concat())
|
||||
}
|
||||
Operation::SetLayerTransformInScope { path, transform, scope } => {
|
||||
let transform = DAffine2::from_cols_array(&transform);
|
||||
let scope = DAffine2::from_cols_array(&scope);
|
||||
self.set_transform_relative_to_scope(&path, Some(scope), transform)?;
|
||||
self.mark_as_dirty(&path)?;
|
||||
Some([vec![DocumentChanged], update_thumbnails_upstream(&path)].concat())
|
||||
}
|
||||
Operation::SetLayerScaleAroundPivot { path, new_scale } => {
|
||||
let layer = self.layer_mut(&path)?;
|
||||
|
||||
let matrix = layer.transform.to_cols_array();
|
||||
let old_scale = (matrix[0], matrix[3]);
|
||||
|
||||
let scale_factor = DVec2::from(new_scale) / DVec2::from(old_scale);
|
||||
|
||||
let offset = DAffine2::from_translation(-layer.pivot);
|
||||
let scale = DAffine2::from_scale(scale_factor);
|
||||
let offset_back = DAffine2::from_translation(layer.pivot);
|
||||
layer.transform = layer.transform * offset_back * scale * offset;
|
||||
|
||||
self.mark_as_dirty(&path)?;
|
||||
Some([vec![DocumentChanged], update_thumbnails_upstream(&path)].concat())
|
||||
}
|
||||
Operation::SetLayerTransform { path, transform } => {
|
||||
let transform = DAffine2::from_cols_array(&transform);
|
||||
let layer = self.layer_mut(&path)?;
|
||||
layer.transform = transform;
|
||||
self.mark_as_dirty(&path)?;
|
||||
Some([vec![DocumentChanged], update_thumbnails_upstream(&path)].concat())
|
||||
}
|
||||
Operation::SetLayerVisibility { path, visible } => {
|
||||
self.mark_as_dirty(&path)?;
|
||||
let layer = self.layer_mut(&path)?;
|
||||
layer.visible = visible;
|
||||
Some([vec![DocumentChanged], update_thumbnails_upstream(&path)].concat())
|
||||
}
|
||||
Operation::SetLayerBlendMode { path, blend_mode } => {
|
||||
self.mark_as_dirty(&path)?;
|
||||
self.layer_mut(&path)?.blend_mode = blend_mode;
|
||||
|
||||
Some([vec![DocumentChanged], update_thumbnails_upstream(&path)].concat())
|
||||
}
|
||||
Operation::SetLayerOpacity { path, opacity } => {
|
||||
self.mark_as_dirty(&path)?;
|
||||
self.layer_mut(&path)?.opacity = opacity;
|
||||
|
||||
Some([vec![DocumentChanged], update_thumbnails_upstream(&path)].concat())
|
||||
}
|
||||
Operation::SetLayerStyle { path, style } => {
|
||||
let layer = self.layer_mut(&path)?;
|
||||
match &mut layer.data {
|
||||
LegacyLayerType::Shape(s) => s.style = style,
|
||||
_ => return Err(DocumentError::NotShape),
|
||||
}
|
||||
self.mark_as_dirty(&path)?;
|
||||
Some([vec![DocumentChanged, LayerChanged { path: path.clone() }], update_thumbnails_upstream(&path)].concat())
|
||||
}
|
||||
Operation::SetLayerStroke { path, stroke } => {
|
||||
let layer = self.layer_mut(&path)?;
|
||||
layer.style_mut()?.set_stroke(stroke);
|
||||
self.mark_as_dirty(&path)?;
|
||||
Some([vec![DocumentChanged], update_thumbnails_upstream(&path)].concat())
|
||||
}
|
||||
Operation::SetLayerFill { path, fill } => {
|
||||
let layer = self.layer_mut(&path)?;
|
||||
layer.style_mut()?.set_fill(fill);
|
||||
self.mark_as_dirty(&path)?;
|
||||
Some([vec![DocumentChanged], update_thumbnails_upstream(&path)].concat())
|
||||
}
|
||||
};
|
||||
Ok(responses)
|
||||
}
|
||||
}
|
||||
|
||||
fn split_path(path: &[LayerId]) -> Result<(&[LayerId], LayerId), DocumentError> {
|
||||
let (id, path) = path.split_last().ok_or(DocumentError::InvalidPath)?;
|
||||
Ok((path, *id))
|
||||
}
|
||||
|
||||
fn update_thumbnails_upstream(path: &[LayerId]) -> Vec<DocumentResponse> {
|
||||
let length = path.len();
|
||||
let mut responses = Vec::with_capacity(length);
|
||||
for i in 0..length {
|
||||
responses.push(DocumentResponse::LayerChanged { path: path[0..(length - i)].to_vec() });
|
||||
}
|
||||
responses
|
||||
}
|
||||
|
||||
pub fn pick_layer_safe_imaginate_resolution(layer: &LegacyLayer, render_data: &RenderData) -> (u64, u64) {
|
||||
let layer_bounds = layer.bounding_transform(render_data);
|
||||
let layer_bounds_size = (layer_bounds.transform_vector2((1., 0.).into()).length(), layer_bounds.transform_vector2((0., 1.).into()).length());
|
||||
|
||||
graphene_std::imaginate::pick_safe_imaginate_resolution(layer_bounds_size)
|
||||
}
|
||||
|
||||
@@ -338,7 +338,7 @@ impl DocumentMetadata {
|
||||
.reduce(Quad::combine_bounds)
|
||||
}
|
||||
|
||||
pub fn layer_outline<'a>(&'a self, layer: LayerNodeIdentifier) -> impl Iterator<Item = &'a bezier_rs::Subpath<ManipulatorGroupId>> {
|
||||
pub fn layer_outline(&self, layer: LayerNodeIdentifier) -> impl Iterator<Item = &bezier_rs::Subpath<ManipulatorGroupId>> {
|
||||
static EMPTY: Vec<ClickTarget> = Vec::new();
|
||||
let click_targets = self.click_targets.get(&layer).unwrap_or(&EMPTY);
|
||||
click_targets.iter().map(|click_target| &click_target.subpath)
|
||||
@@ -361,12 +361,6 @@ impl core::fmt::Debug for LayerNodeIdentifier {
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for LayerNodeIdentifier {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_fmt(format_args!("Layer(node_id={})", self.to_node()))
|
||||
}
|
||||
}
|
||||
|
||||
impl LayerNodeIdentifier {
|
||||
pub const ROOT: Self = LayerNodeIdentifier::new_unchecked(0);
|
||||
|
||||
@@ -387,10 +381,6 @@ impl LayerNodeIdentifier {
|
||||
Self::new_unchecked(node_id)
|
||||
}
|
||||
|
||||
pub fn from_path(path: &[u64], network: &NodeNetwork) -> Self {
|
||||
Self::new(*path.last().unwrap(), network)
|
||||
}
|
||||
|
||||
/// Access the node id of this layer
|
||||
pub fn to_node(self) -> NodeId {
|
||||
u64::from(self.0) - 1
|
||||
@@ -572,18 +562,6 @@ impl LayerNodeIdentifier {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NodeId> for LayerNodeIdentifier {
|
||||
fn from(node_id: NodeId) -> Self {
|
||||
Self::new_unchecked(node_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LayerNodeIdentifier> for NodeId {
|
||||
fn from(identifier: LayerNodeIdentifier) -> Self {
|
||||
identifier.to_node()
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterator over specified axis.
|
||||
#[derive(Clone)]
|
||||
pub struct AxisIter<'a> {
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
use super::LayerId;
|
||||
|
||||
/// A set of different errors that can occur when using this crate.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum DocumentError {
|
||||
LayerNotFound(Vec<LayerId>),
|
||||
InvalidPath,
|
||||
IndexOutOfBounds,
|
||||
NotFolder,
|
||||
NotShape,
|
||||
NotLayer,
|
||||
InvalidFile(String),
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,23 +0,0 @@
|
||||
//! Basic wrapper for [`serde`] for [`base64`] encoding
|
||||
|
||||
use base64::Engine;
|
||||
use serde::{Deserialize, Deserializer, Serializer};
|
||||
|
||||
pub fn as_base64<S>(key: &std::sync::Arc<Vec<u8>>, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&base64::engine::general_purpose::STANDARD.encode(key.as_slice()))
|
||||
}
|
||||
|
||||
pub fn from_base64<'a, D>(deserializer: D) -> Result<std::sync::Arc<Vec<u8>>, D::Error>
|
||||
where
|
||||
D: Deserializer<'a>,
|
||||
{
|
||||
use serde::de::Error;
|
||||
|
||||
String::deserialize(deserializer)
|
||||
.and_then(|string| base64::engine::general_purpose::STANDARD.decode(string).map_err(|err| Error::custom(err.to_string())))
|
||||
.map(std::sync::Arc::new)
|
||||
.map_err(serde::de::Error::custom)
|
||||
}
|
||||
@@ -1,242 +1,27 @@
|
||||
use super::layer_info::{LayerData, LegacyLayer, LegacyLayerType};
|
||||
use super::style::RenderData;
|
||||
use crate::intersection::Quad;
|
||||
use crate::{DocumentError, LayerId};
|
||||
use super::layer_info::LegacyLayer;
|
||||
use crate::document::LayerId;
|
||||
use crate::DocumentError;
|
||||
|
||||
use graphene_core::uuid::generate_uuid;
|
||||
|
||||
use glam::DVec2;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A layer that encapsulates other layers, including potentially more folders.
|
||||
/// The contained layers are rendered in the same order they are stored.
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
|
||||
pub struct FolderLegacyLayer {
|
||||
/// The ID that will be assigned to the next layer that is added to the folder
|
||||
next_assignment_id: LayerId,
|
||||
/// The IDs of the [Layer]s contained within the Folder
|
||||
pub layer_ids: Vec<LayerId>,
|
||||
/// The [Layer]s contained in the folder
|
||||
pub layers: Vec<LegacyLayer>,
|
||||
}
|
||||
|
||||
impl LayerData for FolderLegacyLayer {
|
||||
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<glam::DAffine2>, render_data: &RenderData) -> bool {
|
||||
let mut any_child_requires_redraw = false;
|
||||
for layer in &mut self.layers {
|
||||
let (svg_value, requires_redraw) = layer.render(transforms, svg_defs, render_data);
|
||||
*svg += svg_value;
|
||||
any_child_requires_redraw = any_child_requires_redraw || requires_redraw;
|
||||
}
|
||||
any_child_requires_redraw
|
||||
}
|
||||
|
||||
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, render_data: &RenderData) {
|
||||
for (layer, layer_id) in self.layers().iter().zip(&self.layer_ids) {
|
||||
path.push(*layer_id);
|
||||
layer.intersects_quad(quad, path, intersections, render_data);
|
||||
path.pop();
|
||||
}
|
||||
}
|
||||
|
||||
fn bounding_box(&self, transform: glam::DAffine2, render_data: &RenderData) -> Option<[DVec2; 2]> {
|
||||
self.layers
|
||||
.iter()
|
||||
.filter_map(|layer| layer.data.bounding_box(transform * layer.transform, render_data))
|
||||
.reduce(|a, b| [a[0].min(b[0]), a[1].max(b[1])])
|
||||
}
|
||||
}
|
||||
|
||||
impl FolderLegacyLayer {
|
||||
/// When a insertion ID is provided, try to insert the layer with the given ID.
|
||||
/// If that ID is already used, return `None`.
|
||||
/// When no insertion ID is provided, search for the next free ID and insert it with that.
|
||||
/// Negative values for `insert_index` represent distance from the end
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use graphite_document_legacy::layers::shape_layer::ShapeLegacyLayer;
|
||||
/// # use graphite_document_legacy::layers::folder_layer::FolderLegacyLayer;
|
||||
/// # use graphite_document_legacy::layers::style::PathStyle;
|
||||
/// # use graphite_document_legacy::layers::layer_info::LegacyLayerType;
|
||||
/// let mut folder = FolderLegacyLayer::default();
|
||||
///
|
||||
/// // Create two layers to be added to the folder
|
||||
/// let mut shape_layer = ShapeLegacyLayer::rectangle(PathStyle::default());
|
||||
/// let mut folder_layer = FolderLegacyLayer::default();
|
||||
///
|
||||
/// folder.add_layer(shape_layer.into(), None, -1);
|
||||
/// folder.add_layer(folder_layer.into(), Some(123), 0);
|
||||
/// ```
|
||||
pub fn add_layer(&mut self, layer: LegacyLayer, id: Option<LayerId>, insert_index: isize) -> Option<LayerId> {
|
||||
let mut insert_index = insert_index as i128;
|
||||
|
||||
// Bounds check for the insert index
|
||||
if insert_index < 0 {
|
||||
insert_index = self.layers.len() as i128 + insert_index + 1;
|
||||
}
|
||||
if insert_index > self.layers.len() as i128 || insert_index < 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(id) = id {
|
||||
self.next_assignment_id = id;
|
||||
}
|
||||
if self.layer_ids.contains(&self.next_assignment_id) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let id = self.next_assignment_id;
|
||||
self.layers.insert(insert_index as usize, layer);
|
||||
self.layer_ids.insert(insert_index as usize, id);
|
||||
|
||||
// Linear probing for collision avoidance
|
||||
while self.layer_ids.contains(&self.next_assignment_id) {
|
||||
self.next_assignment_id += 1;
|
||||
}
|
||||
|
||||
Some(id)
|
||||
pub fn layer(&self, layer_id: LayerId) -> Option<&LegacyLayer> {
|
||||
let index = self.layer_ids.iter().position(|x| *x == layer_id).ok_or_else(|| DocumentError::LayerNotFound([layer_id].into())).ok()?;
|
||||
Some(&self.layers[index])
|
||||
}
|
||||
|
||||
/// Remove a layer with a given ID from the folder.
|
||||
/// This operation will fail if `id` is not present in the folder.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use graphite_document_legacy::layers::folder_layer::FolderLegacyLayer;
|
||||
/// let mut folder = FolderLegacyLayer::default();
|
||||
///
|
||||
/// // Try to remove a layer that does not exist
|
||||
/// assert!(folder.remove_layer(123).is_err());
|
||||
///
|
||||
/// // Add another folder to the folder
|
||||
/// folder.add_layer(FolderLegacyLayer::default().into(), Some(123), -1);
|
||||
///
|
||||
/// // Try to remove that folder again
|
||||
/// assert!(folder.remove_layer(123).is_ok());
|
||||
/// assert_eq!(folder.layers().len(), 0)
|
||||
/// ```
|
||||
pub fn remove_layer(&mut self, id: LayerId) -> Result<(), DocumentError> {
|
||||
let pos = self.position_of_layer(id)?;
|
||||
self.layers.remove(pos);
|
||||
self.layer_ids.remove(pos);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns a list of [LayerId]s in the folder.
|
||||
pub fn list_layers(&self) -> &[LayerId] {
|
||||
self.layer_ids.as_slice()
|
||||
}
|
||||
|
||||
/// Get references to all the [Layer]s in the folder.
|
||||
pub fn layers(&self) -> &[LegacyLayer] {
|
||||
self.layers.as_slice()
|
||||
}
|
||||
|
||||
/// Get mutable references to all the [Layer]s in the folder.
|
||||
pub fn layers_mut(&mut self) -> &mut [LegacyLayer] {
|
||||
self.layers.as_mut_slice()
|
||||
}
|
||||
|
||||
pub fn layer(&self, id: LayerId) -> Option<&LegacyLayer> {
|
||||
let pos = self.position_of_layer(id).ok()?;
|
||||
Some(&self.layers[pos])
|
||||
}
|
||||
|
||||
pub fn layer_mut(&mut self, id: LayerId) -> Option<&mut LegacyLayer> {
|
||||
let pos = self.position_of_layer(id).ok()?;
|
||||
Some(&mut self.layers[pos])
|
||||
}
|
||||
|
||||
pub fn generate_new_folder_ids(&mut self) {
|
||||
self.next_assignment_id = generate_uuid();
|
||||
}
|
||||
|
||||
/// Returns `true` if the folder contains a layer with the given [LayerId].
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use graphite_document_legacy::layers::folder_layer::FolderLegacyLayer;
|
||||
/// let mut folder = FolderLegacyLayer::default();
|
||||
///
|
||||
/// // Search for an id that does not exist
|
||||
/// assert!(!folder.folder_contains(123));
|
||||
///
|
||||
/// // Add layer with the id "123" to the folder
|
||||
/// folder.add_layer(FolderLegacyLayer::default().into(), Some(123), -1);
|
||||
///
|
||||
/// // Search for the id "123"
|
||||
/// assert!(folder.folder_contains(123));
|
||||
/// ```
|
||||
pub fn folder_contains(&self, id: LayerId) -> bool {
|
||||
self.layer_ids.contains(&id)
|
||||
}
|
||||
|
||||
/// Tries to find the index of a layer with the given [LayerId] within the folder.
|
||||
/// This operation will fail if no layer with a matching ID is present in the folder.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use graphite_document_legacy::layers::folder_layer::FolderLegacyLayer;
|
||||
/// let mut folder = FolderLegacyLayer::default();
|
||||
///
|
||||
/// // Search for an id that does not exist
|
||||
/// assert!(folder.position_of_layer(123).is_err());
|
||||
///
|
||||
/// // Add layer with the id "123" to the folder
|
||||
/// folder.add_layer(FolderLegacyLayer::default().into(), Some(123), -1);
|
||||
/// folder.add_layer(FolderLegacyLayer::default().into(), Some(42), -1);
|
||||
///
|
||||
/// assert_eq!(folder.position_of_layer(123), Ok(0));
|
||||
/// assert_eq!(folder.position_of_layer(42), Ok(1));
|
||||
/// ```
|
||||
pub fn position_of_layer(&self, layer_id: LayerId) -> Result<usize, DocumentError> {
|
||||
self.layer_ids.iter().position(|x| *x == layer_id).ok_or_else(|| DocumentError::LayerNotFound([layer_id].into()))
|
||||
}
|
||||
|
||||
/// Tries to get a reference to a folder with the given [LayerId].
|
||||
/// This operation will return `None` if either no layer with `id` exists
|
||||
/// in the folder, or the layer with matching ID is not a folder.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use graphite_document_legacy::layers::folder_layer::FolderLegacyLayer;
|
||||
/// # use graphite_document_legacy::layers::shape_layer::ShapeLegacyLayer;
|
||||
/// # use graphite_document_legacy::layers::style::PathStyle;
|
||||
/// let mut folder = FolderLegacyLayer::default();
|
||||
///
|
||||
/// // Search for an id that does not exist
|
||||
/// assert!(folder.folder(132).is_none());
|
||||
///
|
||||
/// // add a folder and search for it
|
||||
/// folder.add_layer(FolderLegacyLayer::default().into(), Some(123), -1);
|
||||
/// assert!(folder.folder(123).is_some());
|
||||
///
|
||||
/// // add a non-folder layer and search for it
|
||||
/// folder.add_layer(ShapeLegacyLayer::rectangle(PathStyle::default()).into(), Some(42), -1);
|
||||
/// assert!(folder.folder(42).is_none());
|
||||
/// ```
|
||||
pub fn folder(&self, id: LayerId) -> Option<&FolderLegacyLayer> {
|
||||
match self.layer(id) {
|
||||
Some(LegacyLayer {
|
||||
data: LegacyLayerType::Folder(folder),
|
||||
..
|
||||
}) => Some(folder),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Tries to get a mutable reference to folder with the given `id`.
|
||||
/// This operation will return `None` if either no layer with `id` exists
|
||||
/// in the folder or the layer with matching ID is not a folder.
|
||||
/// See the [FolderLegacyLayer::folder] method for a usage example.
|
||||
pub fn folder_mut(&mut self, id: LayerId) -> Option<&mut FolderLegacyLayer> {
|
||||
match self.layer_mut(id) {
|
||||
Some(LegacyLayer {
|
||||
data: LegacyLayerType::Folder(folder),
|
||||
..
|
||||
}) => Some(folder),
|
||||
_ => None,
|
||||
}
|
||||
pub fn layer_mut(&mut self, layer_id: LayerId) -> Option<&mut LegacyLayer> {
|
||||
let index = self.layer_ids.iter().position(|x| *x == layer_id).ok_or_else(|| DocumentError::LayerNotFound([layer_id].into())).ok()?;
|
||||
Some(&mut self.layers[index])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,70 +1,44 @@
|
||||
use super::folder_layer::FolderLegacyLayer;
|
||||
use super::layer_layer::LayerLegacyLayer;
|
||||
use super::shape_layer::ShapeLegacyLayer;
|
||||
use super::style::{PathStyle, RenderData};
|
||||
use crate::intersection::Quad;
|
||||
use crate::DocumentError;
|
||||
use crate::LayerId;
|
||||
|
||||
use graphene_core::raster::BlendMode;
|
||||
use graphene_core::vector::VectorData;
|
||||
use graphene_std::vector::subpath::Subpath;
|
||||
|
||||
use core::fmt;
|
||||
use glam::{DAffine2, DMat2, DVec2};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Write;
|
||||
|
||||
// ===============
|
||||
// LegacyLayerType
|
||||
// ===============
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
/// Represents different types of layers.
|
||||
pub enum LegacyLayerType {
|
||||
/// A layer that wraps a [FolderLegacyLayer] struct.
|
||||
Folder(FolderLegacyLayer),
|
||||
/// A layer that wraps a [ShapeLegacyLayer] struct. Still used by the overlays system, but will be removed in the future.
|
||||
Shape(ShapeLegacyLayer),
|
||||
/// A layer that wraps an [LayerLegacyLayer] struct.
|
||||
Layer(LayerLegacyLayer),
|
||||
}
|
||||
|
||||
impl Default for LegacyLayerType {
|
||||
fn default() -> Self {
|
||||
LegacyLayerType::Folder(FolderLegacyLayer::default())
|
||||
LegacyLayerType::Layer(Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl LegacyLayerType {
|
||||
pub fn inner(&self) -> &dyn LayerData {
|
||||
match self {
|
||||
LegacyLayerType::Shape(shape) => shape,
|
||||
LegacyLayerType::Folder(folder) => folder,
|
||||
LegacyLayerType::Layer(layer) => layer,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn inner_mut(&mut self) -> &mut dyn LayerData {
|
||||
match self {
|
||||
LegacyLayerType::Shape(shape) => shape,
|
||||
LegacyLayerType::Folder(folder) => folder,
|
||||
LegacyLayerType::Layer(layer) => layer,
|
||||
}
|
||||
}
|
||||
}
|
||||
// =========================
|
||||
// LayerDataTypeDiscriminant
|
||||
// =========================
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash, specta::Type)]
|
||||
pub enum LayerDataTypeDiscriminant {
|
||||
Folder,
|
||||
Shape,
|
||||
Layer,
|
||||
Artboard,
|
||||
}
|
||||
|
||||
impl fmt::Display for LayerDataTypeDiscriminant {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
LayerDataTypeDiscriminant::Folder => write!(f, "Folder"),
|
||||
LayerDataTypeDiscriminant::Shape => write!(f, "Shape"),
|
||||
LayerDataTypeDiscriminant::Layer => write!(f, "Layer"),
|
||||
LayerDataTypeDiscriminant::Artboard => write!(f, "Artboard"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,385 +49,33 @@ impl From<&LegacyLayerType> for LayerDataTypeDiscriminant {
|
||||
|
||||
match data {
|
||||
Folder(_) => LayerDataTypeDiscriminant::Folder,
|
||||
Shape(_) => LayerDataTypeDiscriminant::Shape,
|
||||
Layer(_) => LayerDataTypeDiscriminant::Layer,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ** CONVERSIONS **
|
||||
// ===========
|
||||
// LegacyLayer
|
||||
// ===========
|
||||
|
||||
impl<'a> TryFrom<&'a mut LegacyLayer> for &'a mut Subpath {
|
||||
type Error = &'static str;
|
||||
/// Convert a mutable layer into a mutable [Subpath].
|
||||
fn try_from(layer: &'a mut LegacyLayer) -> Result<&'a mut Subpath, Self::Error> {
|
||||
match &mut layer.data {
|
||||
LegacyLayerType::Shape(layer) => Ok(&mut layer.shape),
|
||||
_ => Err("Did not find any shape data in the layer"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a LegacyLayer> for &'a Subpath {
|
||||
type Error = &'static str;
|
||||
/// Convert a reference to a layer into a reference of a [Subpath].
|
||||
fn try_from(layer: &'a LegacyLayer) -> Result<&'a Subpath, Self::Error> {
|
||||
match &layer.data {
|
||||
LegacyLayerType::Shape(layer) => Ok(&layer.shape),
|
||||
_ => Err("Did not find any shape data in the layer"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines shared behavior for every layer type.
|
||||
pub trait LayerData {
|
||||
/// Render the layer as an SVG tag to a given string, returning a boolean to indicate if a redraw is required next frame.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use graphite_document_legacy::layers::shape_layer::ShapeLegacyLayer;
|
||||
/// # use graphite_document_legacy::layers::style::{Fill, PathStyle, ViewMode, RenderData};
|
||||
/// # use graphite_document_legacy::layers::layer_info::LayerData;
|
||||
/// # use std::collections::HashMap;
|
||||
///
|
||||
/// let mut shape = ShapeLegacyLayer::rectangle(PathStyle::new(None, Fill::None));
|
||||
/// let mut svg = String::new();
|
||||
///
|
||||
/// // Render the shape without any transforms, in normal view mode
|
||||
/// # let font_cache = Default::default();
|
||||
/// let render_data = RenderData::new(&font_cache, ViewMode::Normal, None);
|
||||
/// shape.render(&mut svg, &mut String::new(), &mut vec![], &render_data);
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// svg,
|
||||
/// "<g transform=\"matrix(\n1,-0,-0,1,-0,-0)\">\
|
||||
/// <path d=\"M0,0L0,1L1,1L1,0Z\" fill=\"none\" />\
|
||||
/// </g>"
|
||||
/// );
|
||||
/// ```
|
||||
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<glam::DAffine2>, render_data: &RenderData) -> bool;
|
||||
|
||||
/// Determine the layers within this layer that intersect a given quad.
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use graphite_document_legacy::layers::shape_layer::ShapeLegacyLayer;
|
||||
/// # use graphite_document_legacy::layers::style::{Fill, PathStyle, ViewMode, RenderData};
|
||||
/// # use graphite_document_legacy::layers::layer_info::LayerData;
|
||||
/// # use graphite_document_legacy::intersection::Quad;
|
||||
/// # use glam::f64::{DAffine2, DVec2};
|
||||
/// # use std::collections::HashMap;
|
||||
///
|
||||
/// let mut shape = ShapeLegacyLayer::ellipse(PathStyle::new(None, Fill::None));
|
||||
/// let shape_id = 42;
|
||||
/// let mut svg = String::new();
|
||||
///
|
||||
/// let quad = Quad::from_box([DVec2::ZERO, DVec2::ONE]);
|
||||
/// let mut intersections = vec![];
|
||||
///
|
||||
/// let font_cache = Default::default();
|
||||
/// let render_data = RenderData::new(&font_cache, Default::default(), None);
|
||||
/// shape.intersects_quad(quad, &mut vec![shape_id], &mut intersections, &render_data);
|
||||
///
|
||||
/// assert_eq!(intersections, vec![vec![shape_id]]);
|
||||
/// ```
|
||||
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, render_data: &RenderData);
|
||||
|
||||
// TODO: this doctest fails because 0 != 1e-32, maybe assert difference < epsilon?
|
||||
/// Calculate the bounding box for the layer's contents after applying a given transform.
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// # use graphite_document_legacy::layers::shape_layer::ShapeLegacyLayer;
|
||||
/// # use graphite_document_legacy::layers::style::{Fill, PathStyle, RenderData};
|
||||
/// # use graphite_document_legacy::layers::layer_info::LayerData;
|
||||
/// # use glam::f64::{DAffine2, DVec2};
|
||||
/// # use std::collections::HashMap;
|
||||
/// let shape = ShapeLegacyLayer::ellipse(PathStyle::new(None, Fill::None));
|
||||
///
|
||||
/// // Calculate the bounding box without applying any transformations.
|
||||
/// // (The identity transform maps every vector to itself.)
|
||||
/// let transform = DAffine2::IDENTITY;
|
||||
/// let font_cache = Default::default();
|
||||
/// let render_data = RenderData::new(&font_cache, Default::default(), None);
|
||||
/// let bounding_box = shape.bounding_box(transform, &render_data);
|
||||
///
|
||||
/// assert_eq!(bounding_box, Some([DVec2::ZERO, DVec2::ONE]));
|
||||
/// ```
|
||||
fn bounding_box(&self, transform: glam::DAffine2, render_data: &RenderData) -> Option<[DVec2; 2]>;
|
||||
}
|
||||
|
||||
impl LayerData for LegacyLayerType {
|
||||
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<glam::DAffine2>, render_data: &RenderData) -> bool {
|
||||
self.inner_mut().render(svg, svg_defs, transforms, render_data)
|
||||
}
|
||||
|
||||
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, render_data: &RenderData) {
|
||||
self.inner().intersects_quad(quad, path, intersections, render_data)
|
||||
}
|
||||
|
||||
fn bounding_box(&self, transform: glam::DAffine2, render_data: &RenderData) -> Option<[DVec2; 2]> {
|
||||
self.inner().bounding_box(transform, render_data)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(remote = "glam::DAffine2")]
|
||||
struct DAffine2Ref {
|
||||
pub matrix2: DMat2,
|
||||
pub translation: DVec2,
|
||||
}
|
||||
|
||||
/// Utility function for providing a default boolean value to serde.
|
||||
#[inline(always)]
|
||||
fn return_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Deserialize, Serialize)]
|
||||
#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct LegacyLayer {
|
||||
/// Whether the layer is currently visible or hidden.
|
||||
pub visible: bool,
|
||||
/// The user-given name of the layer.
|
||||
pub name: Option<String>,
|
||||
/// Whether the layer is currently visible or hidden.
|
||||
pub visible: bool,
|
||||
/// The type of layer, such as folder or shape.
|
||||
pub data: LegacyLayerType,
|
||||
/// A transformation applied to the layer (translation, rotation, scaling, and shear).
|
||||
#[serde(with = "DAffine2Ref")]
|
||||
pub transform: glam::DAffine2,
|
||||
/// Should the aspect ratio of this layer be preserved?
|
||||
#[serde(default = "return_true")]
|
||||
pub preserve_aspect: bool,
|
||||
/// The center of transformations like rotation or scaling with the shift key.
|
||||
/// This is in local space (so the layer's transform should be applied).
|
||||
pub pivot: DVec2,
|
||||
/// The cached SVG thumbnail view of the layer.
|
||||
#[serde(skip)]
|
||||
pub thumbnail_cache: String,
|
||||
/// The cached SVG render of the layer.
|
||||
#[serde(skip)]
|
||||
pub cache: String,
|
||||
/// The cached definition(s) used by the layer's SVG tag, placed at the top in the SVG defs tag.
|
||||
#[serde(skip)]
|
||||
pub svg_defs_cache: String,
|
||||
/// Whether or not the [Cache](Layer::cache) and [Thumbnail Cache](Layer::thumbnail_cache) need to be updated.
|
||||
#[serde(skip, default = "return_true")]
|
||||
pub cache_dirty: bool,
|
||||
/// The blend mode describing how this layer should composite with others underneath it.
|
||||
pub blend_mode: BlendMode,
|
||||
/// The opacity, in the range of 0 to 1.
|
||||
pub opacity: f64,
|
||||
}
|
||||
|
||||
impl Default for LegacyLayer {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
visible: Default::default(),
|
||||
name: Default::default(),
|
||||
data: Default::default(),
|
||||
transform: Default::default(),
|
||||
preserve_aspect: Default::default(),
|
||||
pivot: Default::default(),
|
||||
thumbnail_cache: Default::default(),
|
||||
cache: Default::default(),
|
||||
svg_defs_cache: Default::default(),
|
||||
cache_dirty: Default::default(),
|
||||
blend_mode: Default::default(),
|
||||
opacity: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LegacyLayer {
|
||||
pub fn new(data: LegacyLayerType, transform: [f64; 6]) -> Self {
|
||||
Self {
|
||||
visible: true,
|
||||
name: None,
|
||||
data,
|
||||
transform: glam::DAffine2::from_cols_array(&transform),
|
||||
preserve_aspect: true,
|
||||
pivot: DVec2::splat(0.5),
|
||||
cache: String::new(),
|
||||
thumbnail_cache: String::new(),
|
||||
svg_defs_cache: String::new(),
|
||||
cache_dirty: true,
|
||||
blend_mode: BlendMode::Normal,
|
||||
opacity: 1.,
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets a child layer of this layer, by a path. If the layer with id 1 is inside a folder with id 0, the path will be [0, 1].
|
||||
pub fn child(&self, path: &[LayerId]) -> Option<&LegacyLayer> {
|
||||
let mut layer = self;
|
||||
for id in path {
|
||||
layer = layer.as_folder().ok()?.layer(*id)?;
|
||||
}
|
||||
Some(layer)
|
||||
}
|
||||
|
||||
/// Gets a child layer of this layer, by a path. If the layer with id 1 is inside a folder with id 0, the path will be [0, 1].
|
||||
pub fn child_mut(&mut self, path: &[LayerId]) -> Option<&mut LegacyLayer> {
|
||||
let mut layer = self;
|
||||
for id in path {
|
||||
layer = layer.as_folder_mut().ok()?.layer_mut(*id)?;
|
||||
}
|
||||
Some(layer)
|
||||
}
|
||||
|
||||
/// Iterate over the layers encapsulated by this layer.
|
||||
/// If the [Layer type](Layer::data) is not a folder, the only item in the iterator will be the layer itself.
|
||||
/// If the [Layer type](Layer::data) wraps a [Folder](LegacyLayerType::Folder), the iterator will recursively yield all the layers contained in the folder as well as potential sub-folders.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use graphite_document_legacy::layers::shape_layer::ShapeLegacyLayer;
|
||||
/// # use graphite_document_legacy::layers::layer_info::Layer;
|
||||
/// # use graphite_document_legacy::layers::style::PathStyle;
|
||||
/// # use graphite_document_legacy::layers::folder_layer::FolderLegacyLayer;
|
||||
/// let mut root_folder = FolderLegacyLayer::default();
|
||||
///
|
||||
/// // Add a shape to the root folder
|
||||
/// let child_1: Layer = ShapeLegacyLayer::rectangle(PathStyle::default()).into();
|
||||
/// root_folder.add_layer(child_1.clone(), None, -1);
|
||||
///
|
||||
/// // Add a folder containing another shape to the root layer
|
||||
/// let mut child_folder = FolderLegacyLayer::default();
|
||||
/// let grandchild: Layer = ShapeLegacyLayer::rectangle(PathStyle::default()).into();
|
||||
/// child_folder.add_layer(grandchild.clone(), None, -1);
|
||||
/// let child_2: Layer = child_folder.into();
|
||||
/// root_folder.add_layer(child_2.clone(), None, -1);
|
||||
/// let root: Layer = root_folder.into();
|
||||
///
|
||||
/// let mut iter = root.iter();
|
||||
/// assert_eq!(iter.next(), Some(&root));
|
||||
/// assert_eq!(iter.next(), Some(&child_2));
|
||||
/// assert_eq!(iter.next(), Some(&grandchild));
|
||||
/// assert_eq!(iter.next(), Some(&child_1));
|
||||
/// assert_eq!(iter.next(), None);
|
||||
/// ```
|
||||
pub fn iter(&self) -> LayerIter<'_> {
|
||||
LayerIter { stack: vec![self] }
|
||||
}
|
||||
|
||||
/// Renders the layer, returning the result and if a redraw is required
|
||||
pub fn render(&mut self, transforms: &mut Vec<DAffine2>, svg_defs: &mut String, render_data: &RenderData) -> (&str, bool) {
|
||||
if !self.visible {
|
||||
return ("", false);
|
||||
}
|
||||
|
||||
transforms.push(self.transform);
|
||||
|
||||
// Skip rendering if outside the viewport bounds
|
||||
if let Some(viewport_bounds) = render_data.culling_bounds {
|
||||
if let Some(bounding_box) = self.data.bounding_box(transforms.iter().cloned().reduce(|a, b| a * b).unwrap_or(DAffine2::IDENTITY), render_data) {
|
||||
let is_overlapping =
|
||||
viewport_bounds[0].x < bounding_box[1].x && bounding_box[0].x < viewport_bounds[1].x && viewport_bounds[0].y < bounding_box[1].y && bounding_box[0].y < viewport_bounds[1].y;
|
||||
if !is_overlapping {
|
||||
transforms.pop();
|
||||
self.cache.clear();
|
||||
self.cache_dirty = true;
|
||||
return ("", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut requires_redraw = false;
|
||||
|
||||
if self.cache_dirty {
|
||||
self.thumbnail_cache.clear();
|
||||
self.svg_defs_cache.clear();
|
||||
requires_redraw = self.data.render(&mut self.thumbnail_cache, &mut self.svg_defs_cache, transforms, render_data);
|
||||
|
||||
self.cache.clear();
|
||||
let _ = writeln!(self.cache, r#"<g transform="matrix("#);
|
||||
self.transform.to_cols_array().iter().enumerate().for_each(|(i, f)| {
|
||||
let _ = self.cache.write_str(&(f.to_string() + if i == 5 { "" } else { "," }));
|
||||
});
|
||||
let _ = write!(self.cache, r#")" style="opacity: {};{}">{}</g>"#, self.opacity, self.blend_mode.render(), self.thumbnail_cache.as_str());
|
||||
|
||||
self.cache_dirty = false;
|
||||
}
|
||||
|
||||
transforms.pop();
|
||||
svg_defs.push_str(&self.svg_defs_cache);
|
||||
|
||||
// If a redraw is required then set the cache to dirty.
|
||||
if requires_redraw {
|
||||
self.cache_dirty = true;
|
||||
}
|
||||
|
||||
(self.cache.as_str(), requires_redraw)
|
||||
}
|
||||
|
||||
pub fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, render_data: &RenderData) {
|
||||
if !self.visible {
|
||||
return;
|
||||
}
|
||||
|
||||
let transformed_quad = self.transform.inverse() * quad;
|
||||
self.data.intersects_quad(transformed_quad, path, intersections, render_data)
|
||||
}
|
||||
|
||||
/// Compute the bounding box of the layer after applying a transform to it.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use graphite_document_legacy::layers::shape_layer::ShapeLegacyLayer;
|
||||
/// # use graphite_document_legacy::layers::layer_info::Layer;
|
||||
/// # use graphite_document_legacy::layers::style::{PathStyle, RenderData};
|
||||
/// # use glam::DVec2;
|
||||
/// # use glam::f64::DAffine2;
|
||||
/// # use std::collections::HashMap;
|
||||
/// // Create a rectangle with the default dimensions, from `(0|0)` to `(1|1)`
|
||||
/// let layer: Layer = ShapeLegacyLayer::rectangle(PathStyle::default()).into();
|
||||
///
|
||||
/// // Apply the Identity transform, which leaves the points unchanged
|
||||
/// let transform = DAffine2::IDENTITY;
|
||||
/// let font_cache = Default::default();
|
||||
/// let render_data = RenderData::new(&font_cache, Default::default(), None);
|
||||
/// assert_eq!(
|
||||
/// layer.aabb_for_transform(transform, &render_data),
|
||||
/// Some([DVec2::ZERO, DVec2::ONE]),
|
||||
/// );
|
||||
///
|
||||
/// // Apply a transform that scales every point by a factor of two
|
||||
/// let transform = DAffine2::from_scale(DVec2::ONE * 2.);
|
||||
/// assert_eq!(
|
||||
/// layer.aabb_for_transform(transform, &render_data),
|
||||
/// Some([DVec2::ZERO, DVec2::ONE * 2.]),
|
||||
/// );
|
||||
pub fn aabb_for_transform(&self, transform: DAffine2, render_data: &RenderData) -> Option<[DVec2; 2]> {
|
||||
self.data.bounding_box(transform, render_data)
|
||||
}
|
||||
|
||||
pub fn aabb(&self, render_data: &RenderData) -> Option<[DVec2; 2]> {
|
||||
self.aabb_for_transform(self.transform, render_data)
|
||||
}
|
||||
|
||||
pub fn bounding_transform(&self, render_data: &RenderData) -> DAffine2 {
|
||||
let scale = match self.aabb_for_transform(DAffine2::IDENTITY, render_data) {
|
||||
Some([a, b]) => {
|
||||
let dimensions = b - a;
|
||||
DAffine2::from_scale(dimensions)
|
||||
}
|
||||
None => DAffine2::IDENTITY,
|
||||
};
|
||||
|
||||
self.transform * scale
|
||||
}
|
||||
|
||||
pub fn layerspace_pivot(&self, render_data: &RenderData) -> DVec2 {
|
||||
let [mut min, max] = self.aabb_for_transform(DAffine2::IDENTITY, render_data).unwrap_or([DVec2::ZERO, DVec2::ONE]);
|
||||
|
||||
// If the layer bounds are 0 in either axis then set them to one (to avoid div 0)
|
||||
if (max.x - min.x) < f64::EPSILON * 1000. {
|
||||
min.x = max.x - 1.;
|
||||
}
|
||||
if (max.y - min.y) < f64::EPSILON * 1000. {
|
||||
min.y = max.y - 1.;
|
||||
}
|
||||
|
||||
self.pivot * (max - min) + min
|
||||
}
|
||||
|
||||
/// Get a mutable reference to the Folder wrapped by the layer.
|
||||
/// This operation will fail if the [Layer type](Layer::data) is not `LegacyLayerType::Folder`.
|
||||
pub fn as_folder_mut(&mut self) -> Result<&mut FolderLegacyLayer, DocumentError> {
|
||||
@@ -463,20 +85,6 @@ impl LegacyLayer {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_vector_data(&self) -> Option<&VectorData> {
|
||||
match &self.data {
|
||||
LegacyLayerType::Layer(layer) => layer.as_vector_data(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_subpath_mut(&mut self) -> Option<&mut Subpath> {
|
||||
match &mut self.data {
|
||||
LegacyLayerType::Shape(s) => Some(&mut s.shape),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a reference to the Folder wrapped by the layer.
|
||||
/// This operation will fail if the [Layer type](Layer::data) is not `LegacyLayerType::Folder`.
|
||||
pub fn as_folder(&self) -> Result<&FolderLegacyLayer, DocumentError> {
|
||||
@@ -485,87 +93,11 @@ impl LegacyLayer {
|
||||
_ => Err(DocumentError::NotFolder),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a mutable reference to the NodeNetwork
|
||||
/// This operation will fail if the [Layer type](Layer::data) is not `LegacyLayerType::Layer`.
|
||||
pub fn as_layer_network_mut(&mut self) -> Result<&mut graph_craft::document::NodeNetwork, DocumentError> {
|
||||
match &mut self.data {
|
||||
LegacyLayerType::Layer(layer) => Ok(&mut layer.network),
|
||||
_ => Err(DocumentError::NotLayer),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a reference to the NodeNetwork
|
||||
/// This operation will fail if the [Layer type](Layer::data) is not `LegacyLayerType::Layer`.
|
||||
pub fn as_layer_network(&self) -> Result<&graph_craft::document::NodeNetwork, DocumentError> {
|
||||
match &self.data {
|
||||
LegacyLayerType::Layer(layer) => Ok(&layer.network),
|
||||
_ => Err(DocumentError::NotLayer),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_layer(&self) -> Result<&LayerLegacyLayer, DocumentError> {
|
||||
match &self.data {
|
||||
LegacyLayerType::Layer(layer) => Ok(layer),
|
||||
_ => Err(DocumentError::NotLayer),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn style(&self) -> Result<&PathStyle, DocumentError> {
|
||||
match &self.data {
|
||||
LegacyLayerType::Shape(shape) => Ok(&shape.style),
|
||||
LegacyLayerType::Layer(layer) => layer.as_vector_data().map(|vector| &vector.style).ok_or(DocumentError::NotShape),
|
||||
_ => Err(DocumentError::NotShape),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn style_mut(&mut self) -> Result<&mut PathStyle, DocumentError> {
|
||||
match &mut self.data {
|
||||
LegacyLayerType::Shape(s) => Ok(&mut s.style),
|
||||
_ => Err(DocumentError::NotShape),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for LegacyLayer {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
visible: self.visible,
|
||||
name: self.name.clone(),
|
||||
data: self.data.clone(),
|
||||
transform: self.transform,
|
||||
preserve_aspect: self.preserve_aspect,
|
||||
pivot: self.pivot,
|
||||
cache: String::new(),
|
||||
thumbnail_cache: String::new(),
|
||||
svg_defs_cache: String::new(),
|
||||
cache_dirty: true,
|
||||
blend_mode: self.blend_mode,
|
||||
opacity: self.opacity,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FolderLegacyLayer> for LegacyLayer {
|
||||
fn from(from: FolderLegacyLayer) -> LegacyLayer {
|
||||
LegacyLayer::new(LegacyLayerType::Folder(from), DAffine2::IDENTITY.to_cols_array())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ShapeLegacyLayer> for LegacyLayer {
|
||||
fn from(from: ShapeLegacyLayer) -> LegacyLayer {
|
||||
LegacyLayer::new(LegacyLayerType::Shape(from), DAffine2::IDENTITY.to_cols_array())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a LegacyLayer {
|
||||
type Item = &'a LegacyLayer;
|
||||
type IntoIter = LayerIter<'a>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.iter()
|
||||
}
|
||||
}
|
||||
// =========
|
||||
// LayerIter
|
||||
// =========
|
||||
|
||||
/// An iterator over the layers encapsulated by this layer.
|
||||
/// See [Layer::iter] for more information.
|
||||
@@ -581,7 +113,7 @@ impl<'a> Iterator for LayerIter<'a> {
|
||||
match self.stack.pop() {
|
||||
Some(layer) => {
|
||||
if let LegacyLayerType::Folder(folder) = &layer.data {
|
||||
let layers = folder.layers();
|
||||
let layers = folder.layers.as_slice();
|
||||
self.stack.extend(layers);
|
||||
};
|
||||
Some(layer)
|
||||
|
||||
@@ -1,172 +1,11 @@
|
||||
use super::layer_info::LayerData;
|
||||
use super::style::{RenderData, ViewMode};
|
||||
use crate::intersection::{intersect_quad_bez_path, intersect_quad_subpath, Quad};
|
||||
use crate::LayerId;
|
||||
|
||||
use glam::{DAffine2, DMat2, DVec2};
|
||||
use graphene_core::vector::VectorData;
|
||||
use graphene_core::SurfaceId;
|
||||
use kurbo::{Affine, BezPath, Shape as KurboShape};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Write;
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)]
|
||||
pub enum CachedOutputData {
|
||||
#[default]
|
||||
None,
|
||||
BlobURL(String),
|
||||
VectorPath(Box<VectorData>),
|
||||
SurfaceId(SurfaceId),
|
||||
Svg(String),
|
||||
}
|
||||
// ================
|
||||
// LayerLegacyLayer
|
||||
// ================
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)]
|
||||
pub struct LayerLegacyLayer {
|
||||
/// The document node network that this layer contains
|
||||
pub network: graph_craft::document::NodeNetwork,
|
||||
|
||||
#[serde(skip)]
|
||||
pub cached_output_data: CachedOutputData,
|
||||
}
|
||||
|
||||
impl LayerData for LayerLegacyLayer {
|
||||
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<DAffine2>, render_data: &RenderData) -> bool {
|
||||
let transform = self.transform(transforms, render_data.view_mode);
|
||||
let inverse = transform.inverse();
|
||||
|
||||
let (width, height) = (transform.transform_vector2(DVec2::new(1., 0.)).length(), transform.transform_vector2(DVec2::new(0., 1.)).length());
|
||||
|
||||
if !inverse.is_finite() {
|
||||
let _ = write!(svg, "<!-- SVG shape has an invalid transform -->");
|
||||
return false;
|
||||
}
|
||||
|
||||
let _ = writeln!(svg, r#"<g transform="matrix("#);
|
||||
inverse.to_cols_array().iter().enumerate().for_each(|(i, entry)| {
|
||||
let _ = svg.write_str(&(entry.to_string() + if i == 5 { "" } else { "," }));
|
||||
});
|
||||
let _ = svg.write_str(r#")">"#);
|
||||
|
||||
let matrix = (transform * DAffine2::from_scale((width, height).into()).inverse())
|
||||
.to_cols_array()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.fold(String::new(), |val, (i, entry)| val + &(entry.to_string() + if i == 5 { "" } else { "," }));
|
||||
|
||||
// Render any paths if they exist
|
||||
match &self.cached_output_data {
|
||||
CachedOutputData::VectorPath(vector_data) => {
|
||||
let layer_bounds = vector_data.bounding_box().unwrap_or_default();
|
||||
let transformed_bounds = vector_data.bounding_box_with_transform(transform).unwrap_or_default();
|
||||
|
||||
let _ = write!(svg, "<path d=\"");
|
||||
for subpath in &vector_data.subpaths {
|
||||
let _ = subpath.subpath_to_svg(svg, transform);
|
||||
}
|
||||
svg.push('"');
|
||||
|
||||
svg.push_str(&vector_data.style.render(render_data.view_mode, svg_defs, transform, layer_bounds, transformed_bounds));
|
||||
let _ = write!(svg, "/>");
|
||||
}
|
||||
CachedOutputData::BlobURL(blob_url) => {
|
||||
// Render the image if it exists
|
||||
let _ = write!(
|
||||
svg,
|
||||
r#"<image width="{}" height="{}" preserveAspectRatio="none" href="{}" transform="matrix({})" />"#,
|
||||
width.abs(),
|
||||
height.abs(),
|
||||
blob_url,
|
||||
matrix
|
||||
);
|
||||
}
|
||||
CachedOutputData::SurfaceId(SurfaceId(id)) => {
|
||||
// Render the image if it exists
|
||||
let _ = write!(
|
||||
svg,
|
||||
r#"
|
||||
<foreignObject width="{}" height="{}" transform="matrix({})"><div data-canvas-placeholder="canvas{}"></div></foreignObject>
|
||||
"#,
|
||||
width.abs(),
|
||||
height.abs(),
|
||||
matrix,
|
||||
id
|
||||
);
|
||||
}
|
||||
CachedOutputData::Svg(new_svg) => svg.push_str(new_svg),
|
||||
_ => {
|
||||
// Render a dotted blue outline if there is no image or vector data
|
||||
let _ = write!(
|
||||
svg,
|
||||
r#"<rect width="{}" height="{}" fill="none" stroke="var(--color-data-vector)" stroke-width="3" stroke-dasharray="8" transform="matrix({})" />"#,
|
||||
width.abs(),
|
||||
height.abs(),
|
||||
matrix,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let _ = svg.write_str(r#"</g>"#);
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn bounding_box(&self, transform: glam::DAffine2, _render_data: &RenderData) -> Option<[DVec2; 2]> {
|
||||
if let CachedOutputData::VectorPath(vector_data) = &self.cached_output_data {
|
||||
return vector_data.bounding_box_with_transform(transform);
|
||||
}
|
||||
|
||||
let mut path = self.bounds();
|
||||
|
||||
if transform.matrix2 == DMat2::ZERO {
|
||||
return None;
|
||||
}
|
||||
path.apply_affine(glam_to_kurbo(transform));
|
||||
|
||||
let kurbo::Rect { x0, y0, x1, y1 } = path.bounding_box();
|
||||
Some([(x0, y0).into(), (x1, y1).into()])
|
||||
}
|
||||
|
||||
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, _render_data: &RenderData) {
|
||||
if let CachedOutputData::VectorPath(vector_data) = &self.cached_output_data {
|
||||
let filled_style = vector_data.style.fill().is_some();
|
||||
if vector_data.subpaths.iter().any(|subpath| intersect_quad_subpath(quad, subpath, filled_style || subpath.closed())) {
|
||||
intersections.push(path.clone());
|
||||
}
|
||||
} else if intersect_quad_bez_path(quad, &self.bounds(), true) {
|
||||
intersections.push(path.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LayerLegacyLayer {
|
||||
pub fn transform(&self, transforms: &[DAffine2], mode: ViewMode) -> DAffine2 {
|
||||
let start = match mode {
|
||||
ViewMode::Outline => 0,
|
||||
_ => (transforms.len() as i32 - 1).max(0) as usize,
|
||||
};
|
||||
transforms.iter().skip(start).cloned().reduce(|a, b| a * b).unwrap_or(DAffine2::IDENTITY)
|
||||
}
|
||||
|
||||
fn bounds(&self) -> BezPath {
|
||||
kurbo::Rect::from_origin_size(kurbo::Point::ZERO, kurbo::Size::new(1., 1.)).to_path(0.)
|
||||
}
|
||||
|
||||
pub fn as_vector_data(&self) -> Option<&VectorData> {
|
||||
if let CachedOutputData::VectorPath(vector_data) = &self.cached_output_data {
|
||||
Some(vector_data)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
pub fn as_blob_url(&self) -> Option<&String> {
|
||||
if let CachedOutputData::BlobURL(blob_url) = &self.cached_output_data {
|
||||
Some(blob_url)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn glam_to_kurbo(transform: DAffine2) -> Affine {
|
||||
Affine::new(transform.to_cols_array())
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
//! Layers allow the user to mutate part of the document while leaving the rest unchanged.
|
||||
//! There are currently these different types of layers:
|
||||
//! * [Folder layers](folder_layer::FolderLegacyLayer), which encapsulate sub-layers
|
||||
//! * [Shape layers](shape_layer::ShapeLegacyLayer), which contain generic SVG [`<path>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path)s (deprecated but still used by the overlays system).
|
||||
//! * [Layer layers](layer_layer::LayerLegacyLayer), which contain a node graph layer
|
||||
//!
|
||||
//! Refer to the module-level documentation for detailed information on each layer.
|
||||
@@ -13,21 +12,9 @@
|
||||
//! When different layers overlap, they are blended together according to the [BlendMode](blend_mode::BlendMode)
|
||||
//! using the CSS [`mix-blend-mode`](https://developer.mozilla.org/en-US/docs/Web/CSS/mix-blend-mode) property and the layer opacity.
|
||||
|
||||
pub mod base64_serde;
|
||||
/// Contains the [FolderLegacyLayer](folder_layer::FolderLegacyLayer) type that encapsulates other layers, including more folders.
|
||||
pub mod folder_layer;
|
||||
/// Contains the base [Layer](layer_info::Layer) type, an abstraction over the different types of layers.
|
||||
pub mod layer_info;
|
||||
/// Contains the [LayerLegacyLayer](nodegraph_layer::LayerLegacyLayer) type that contains a node graph.
|
||||
pub mod layer_layer;
|
||||
// TODO: Remove shape layers after rewriting the overlay system
|
||||
/// Contains the [ShapeLegacyLayer](shape_layer::ShapeLegacyLayer) type, a generic SVG element defined using Bezier paths.
|
||||
pub mod shape_layer;
|
||||
|
||||
mod render_data;
|
||||
pub use render_data::RenderData;
|
||||
|
||||
pub mod style {
|
||||
pub use super::RenderData;
|
||||
pub use graphene_core::vector::style::*;
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
use super::style::ViewMode;
|
||||
use graphene_std::text::FontCache;
|
||||
|
||||
use glam::DVec2;
|
||||
|
||||
/// Contains metadata for rendering the document as an svg
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct RenderData<'a> {
|
||||
pub font_cache: &'a FontCache,
|
||||
pub view_mode: ViewMode,
|
||||
pub culling_bounds: Option<[DVec2; 2]>,
|
||||
}
|
||||
|
||||
impl<'a> RenderData<'a> {
|
||||
pub fn new(font_cache: &'a FontCache, view_mode: ViewMode, culling_bounds: Option<[DVec2; 2]>) -> Self {
|
||||
Self {
|
||||
font_cache,
|
||||
view_mode,
|
||||
culling_bounds,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,176 +0,0 @@
|
||||
use super::layer_info::LayerData;
|
||||
use super::style::{self, PathStyle, RenderData, ViewMode};
|
||||
use crate::intersection::{intersect_quad_bez_path, Quad};
|
||||
use crate::LayerId;
|
||||
|
||||
use graphene_std::vector::subpath::Subpath;
|
||||
|
||||
use glam::{DAffine2, DMat2, DVec2};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Write;
|
||||
|
||||
/// A generic SVG element defined using Bezier paths.
|
||||
/// Shapes are rendered as
|
||||
/// [`<path>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path)
|
||||
/// elements inside a
|
||||
/// [`<g>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/g)
|
||||
/// group that the transformation matrix is applied to.
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, specta::Type)]
|
||||
pub struct ShapeLegacyLayer {
|
||||
/// The geometry of the layer.
|
||||
pub shape: Subpath,
|
||||
/// The visual style of the shape.
|
||||
pub style: style::PathStyle,
|
||||
// TODO: We might be able to remove this in a future refactor
|
||||
pub render_index: i32,
|
||||
}
|
||||
|
||||
impl LayerData for ShapeLegacyLayer {
|
||||
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<DAffine2>, render_data: &RenderData) -> bool {
|
||||
let mut subpath = self.shape.clone();
|
||||
|
||||
let layer_bounds = subpath.bounding_box().unwrap_or_default();
|
||||
|
||||
let transform = self.transform(transforms, render_data.view_mode);
|
||||
if !transform.is_finite() || transform.matrix2.determinant() == 0. {
|
||||
let _ = write!(svg, "<!-- SVG shape has an invalid transform -->");
|
||||
return false;
|
||||
}
|
||||
let inverse = transform.inverse();
|
||||
|
||||
subpath.apply_affine(transform);
|
||||
|
||||
let transformed_bounds = subpath.bounding_box().unwrap_or_default();
|
||||
|
||||
let _ = writeln!(svg, r#"<g transform="matrix("#);
|
||||
inverse.to_cols_array().iter().enumerate().for_each(|(i, entry)| {
|
||||
let _ = svg.write_str(&(entry.to_string() + if i == 5 { "" } else { "," }));
|
||||
});
|
||||
let _ = svg.write_str(r#")">"#);
|
||||
let _ = write!(
|
||||
svg,
|
||||
r#"<path d="{}" {} />"#,
|
||||
subpath.to_svg(),
|
||||
self.style.render(render_data.view_mode, svg_defs, transform, layer_bounds, transformed_bounds)
|
||||
);
|
||||
let _ = svg.write_str("</g>");
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn bounding_box(&self, transform: glam::DAffine2, _render_data: &RenderData) -> Option<[DVec2; 2]> {
|
||||
let mut subpath = self.shape.clone();
|
||||
if transform.matrix2 == DMat2::ZERO || !transform.is_finite() {
|
||||
return None;
|
||||
}
|
||||
subpath.apply_affine(transform);
|
||||
|
||||
subpath.bounding_box()
|
||||
}
|
||||
|
||||
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, _render_data: &RenderData) {
|
||||
let filled = self.style.fill().is_some() || self.shape.manipulator_groups().last().filter(|manipulator_group| manipulator_group.is_close()).is_some();
|
||||
if intersect_quad_bez_path(quad, &(&self.shape).into(), filled) {
|
||||
intersections.push(path.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ShapeLegacyLayer {
|
||||
/// Construct a new [ShapeLegacyLayer] with the specified [Subpath] and [PathStyle]
|
||||
pub fn new(shape: Subpath, style: PathStyle) -> Self {
|
||||
Self { shape, style, render_index: 1 }
|
||||
}
|
||||
|
||||
pub fn transform(&self, transforms: &[DAffine2], mode: ViewMode) -> DAffine2 {
|
||||
let start = match (mode, self.render_index) {
|
||||
(ViewMode::Outline, _) => 0,
|
||||
(_, -1) => 0,
|
||||
(_, x) => (transforms.len() as i32 - x).max(0) as usize,
|
||||
};
|
||||
transforms.iter().skip(start).fold(DAffine2::IDENTITY, |a, b| a * *b)
|
||||
}
|
||||
|
||||
// TODO The behavior of ngon changed from the previous iteration slightly, match original behavior
|
||||
/// Create an N-gon.
|
||||
///
|
||||
/// # Panics
|
||||
/// This function panics if `sides` is zero.
|
||||
pub fn ngon(sides: u32, style: PathStyle) -> Self {
|
||||
use std::f64::consts::{FRAC_PI_2, TAU};
|
||||
|
||||
fn unit_rotation(theta: f64) -> DVec2 {
|
||||
DVec2::new(theta.sin(), theta.cos())
|
||||
}
|
||||
|
||||
let mut path = kurbo::BezPath::new();
|
||||
|
||||
let apothem_offset_angle = TAU / (sides as f64);
|
||||
// Rotate odd sided shapes by 90 degrees
|
||||
let offset = ((sides + 1) % 2) as f64 * FRAC_PI_2;
|
||||
|
||||
let relative_points = (0..sides).map(|i| apothem_offset_angle * i as f64 + offset).map(unit_rotation);
|
||||
|
||||
let min = relative_points.clone().reduce(|a, b| a.min(b)).unwrap_or_default();
|
||||
let transform = DAffine2::from_scale_angle_translation(DVec2::ONE / 2., 0., -min / 2.);
|
||||
let point = |vec: DVec2| kurbo::Point::new(vec.x, vec.y);
|
||||
|
||||
let mut relative_points = relative_points.map(|p| point(transform.transform_point2(p)));
|
||||
path.move_to(relative_points.next().expect("Tried to create an ngon with 0 sides"));
|
||||
relative_points.for_each(|p| path.line_to(p));
|
||||
|
||||
path.close_path();
|
||||
|
||||
Self {
|
||||
shape: Subpath::new_ngon(DVec2::new(0., 0.), sides.into(), 1.),
|
||||
style,
|
||||
render_index: 1,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a rectangular shape.
|
||||
pub fn rectangle(style: PathStyle) -> Self {
|
||||
Self {
|
||||
shape: Subpath::new_rect(DVec2::new(0., 0.), DVec2::new(1., 1.)),
|
||||
style,
|
||||
render_index: 1,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an elliptical shape.
|
||||
pub fn ellipse(style: PathStyle) -> Self {
|
||||
Self {
|
||||
shape: Subpath::new_ellipse(DVec2::new(0., 0.), DVec2::new(1., 1.)),
|
||||
style,
|
||||
render_index: 1,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a straight line from (0, 0) to (1, 0).
|
||||
pub fn line(style: PathStyle) -> Self {
|
||||
Self {
|
||||
shape: Subpath::new_line(DVec2::new(0., 0.), DVec2::new(1., 0.)),
|
||||
style,
|
||||
render_index: 1,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a polygonal line that visits each provided point.
|
||||
pub fn poly_line(points: Vec<impl Into<glam::DVec2>>, style: PathStyle) -> Self {
|
||||
Self {
|
||||
shape: Subpath::new_poly_line(points),
|
||||
style,
|
||||
render_index: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a smooth bezier spline that passes through all given points.
|
||||
/// The algorithm used in this implementation is described here: <https://www.particleincell.com/2012/bezier-splines/>
|
||||
pub fn spline(points: Vec<impl Into<glam::DVec2>>, style: PathStyle) -> Self {
|
||||
Self {
|
||||
shape: Subpath::new_spline(points),
|
||||
style,
|
||||
render_index: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,16 @@
|
||||
// `macro_use` puts the log macros (`error!`, `warn!`, `debug!`, `info!` and `trace!`) in scope for the crate
|
||||
#[macro_use]
|
||||
// #[macro_use]
|
||||
extern crate log;
|
||||
|
||||
pub mod boolean_ops;
|
||||
pub mod consts;
|
||||
pub mod document;
|
||||
pub mod document_metadata;
|
||||
pub mod error;
|
||||
pub mod intersection;
|
||||
pub mod layers;
|
||||
pub mod operation;
|
||||
pub mod response;
|
||||
|
||||
pub use document::LayerId;
|
||||
pub use error::DocumentError;
|
||||
pub use operation::Operation;
|
||||
pub use response::DocumentResponse;
|
||||
/// A set of different errors that can occur when using this crate.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum DocumentError {
|
||||
LayerNotFound(Vec<document::LayerId>),
|
||||
InvalidPath,
|
||||
NotFolder,
|
||||
InvalidFile(String),
|
||||
}
|
||||
|
||||
@@ -1,176 +0,0 @@
|
||||
use crate::layers::layer_info::LegacyLayer;
|
||||
use crate::layers::style::{self, Stroke};
|
||||
use crate::LayerId;
|
||||
|
||||
use graphene_core::raster::BlendMode;
|
||||
use graphene_std::vector::subpath::Subpath;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
// TODO: Rename all instances of `path` to `layer_path`
|
||||
/// Operations that can be performed to mutate the document.
|
||||
pub enum Operation {
|
||||
// TODO: Remove
|
||||
AddFrame {
|
||||
path: Vec<LayerId>,
|
||||
insert_index: isize,
|
||||
transform: [f64; 6],
|
||||
network: graph_craft::document::NodeNetwork,
|
||||
},
|
||||
/// Sets a blob URL as the image source for an Image or Imaginate layer type.
|
||||
/// **Be sure to call `FrontendMessage::TriggerRevokeBlobUrl` together with this.**
|
||||
SetLayerBlobUrl {
|
||||
layer_path: Vec<LayerId>,
|
||||
blob_url: String,
|
||||
resolution: (f64, f64),
|
||||
},
|
||||
/// Clears the image to leave the layer un-rendered.
|
||||
/// **Be sure to call `FrontendMessage::TriggerRevokeBlobUrl` together with this.**
|
||||
ClearBlobURL {
|
||||
path: Vec<LayerId>,
|
||||
},
|
||||
SetPivot {
|
||||
layer_path: Vec<LayerId>,
|
||||
pivot: (f64, f64),
|
||||
},
|
||||
DeleteLayer {
|
||||
path: Vec<LayerId>,
|
||||
},
|
||||
DuplicateLayer {
|
||||
path: Vec<LayerId>,
|
||||
},
|
||||
RenameLayer {
|
||||
layer_path: Vec<LayerId>,
|
||||
new_name: String,
|
||||
},
|
||||
InsertLayer {
|
||||
layer: Box<LegacyLayer>,
|
||||
destination_path: Vec<LayerId>,
|
||||
insert_index: isize,
|
||||
duplicating: bool,
|
||||
},
|
||||
CreateFolder {
|
||||
path: Vec<LayerId>,
|
||||
insert_index: isize,
|
||||
},
|
||||
TransformLayer {
|
||||
path: Vec<LayerId>,
|
||||
transform: [f64; 6],
|
||||
},
|
||||
TransformLayerInViewport {
|
||||
path: Vec<LayerId>,
|
||||
transform: [f64; 6],
|
||||
},
|
||||
SetLayerTransformInViewport {
|
||||
path: Vec<LayerId>,
|
||||
transform: [f64; 6],
|
||||
},
|
||||
SetShapePath {
|
||||
path: Vec<LayerId>,
|
||||
subpath: Subpath,
|
||||
},
|
||||
SetVectorData {
|
||||
path: Vec<LayerId>,
|
||||
vector_data: graphene_core::vector::VectorData,
|
||||
},
|
||||
SetSurface {
|
||||
path: Vec<LayerId>,
|
||||
surface_id: graphene_core::SurfaceId,
|
||||
},
|
||||
SetSvg {
|
||||
path: Vec<LayerId>,
|
||||
svg: String,
|
||||
},
|
||||
TransformLayerInScope {
|
||||
path: Vec<LayerId>,
|
||||
transform: [f64; 6],
|
||||
scope: [f64; 6],
|
||||
},
|
||||
SetLayerTransformInScope {
|
||||
path: Vec<LayerId>,
|
||||
transform: [f64; 6],
|
||||
scope: [f64; 6],
|
||||
},
|
||||
SetLayerScaleAroundPivot {
|
||||
path: Vec<LayerId>,
|
||||
new_scale: (f64, f64),
|
||||
},
|
||||
SetLayerTransform {
|
||||
path: Vec<LayerId>,
|
||||
transform: [f64; 6],
|
||||
},
|
||||
SetLayerVisibility {
|
||||
path: Vec<LayerId>,
|
||||
visible: bool,
|
||||
},
|
||||
SetLayerPreserveAspect {
|
||||
layer_path: Vec<LayerId>,
|
||||
preserve_aspect: bool,
|
||||
},
|
||||
SetLayerBlendMode {
|
||||
path: Vec<LayerId>,
|
||||
blend_mode: BlendMode,
|
||||
},
|
||||
SetLayerOpacity {
|
||||
path: Vec<LayerId>,
|
||||
opacity: f64,
|
||||
},
|
||||
SetLayerFill {
|
||||
path: Vec<LayerId>,
|
||||
fill: style::Fill,
|
||||
},
|
||||
SetLayerStroke {
|
||||
path: Vec<LayerId>,
|
||||
stroke: Stroke,
|
||||
},
|
||||
|
||||
// The following are used only by the legacy overlays system
|
||||
AddEllipse {
|
||||
path: Vec<LayerId>,
|
||||
insert_index: isize,
|
||||
transform: [f64; 6],
|
||||
style: style::PathStyle,
|
||||
},
|
||||
AddRect {
|
||||
path: Vec<LayerId>,
|
||||
insert_index: isize,
|
||||
transform: [f64; 6],
|
||||
style: style::PathStyle,
|
||||
},
|
||||
AddLine {
|
||||
path: Vec<LayerId>,
|
||||
insert_index: isize,
|
||||
transform: [f64; 6],
|
||||
style: style::PathStyle,
|
||||
},
|
||||
AddPolyline {
|
||||
path: Vec<LayerId>,
|
||||
insert_index: isize,
|
||||
transform: [f64; 6],
|
||||
style: style::PathStyle,
|
||||
points: Vec<(f64, f64)>,
|
||||
},
|
||||
AddShape {
|
||||
path: Vec<LayerId>,
|
||||
insert_index: isize,
|
||||
transform: [f64; 6],
|
||||
style: style::PathStyle,
|
||||
subpath: Subpath,
|
||||
},
|
||||
SetLayerStyle {
|
||||
path: Vec<LayerId>,
|
||||
style: style::PathStyle,
|
||||
},
|
||||
}
|
||||
|
||||
impl Operation {
|
||||
pub fn pseudo_hash(&self) -> u64 {
|
||||
let mut s = DefaultHasher::new();
|
||||
std::mem::discriminant(self).hash(&mut s);
|
||||
s.finish()
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
use crate::LayerId;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||
#[repr(C)]
|
||||
pub enum DocumentResponse {
|
||||
/// For the purposes of rendering, this triggers a re-render of the entire document.
|
||||
DocumentChanged,
|
||||
FolderChanged {
|
||||
path: Vec<LayerId>,
|
||||
},
|
||||
CreatedLayer {
|
||||
path: Vec<LayerId>,
|
||||
is_selected: bool,
|
||||
},
|
||||
DeletedLayer {
|
||||
path: Vec<LayerId>,
|
||||
},
|
||||
/// Triggers an update of the layer in the layer panel.
|
||||
LayerChanged {
|
||||
path: Vec<LayerId>,
|
||||
},
|
||||
DeletedSelectedManipulatorPoints,
|
||||
}
|
||||
|
||||
impl fmt::Display for DocumentResponse {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
DocumentResponse::DocumentChanged { .. } => write!(f, "DocumentChanged"),
|
||||
DocumentResponse::FolderChanged { .. } => write!(f, "FolderChanged"),
|
||||
DocumentResponse::CreatedLayer { .. } => write!(f, "CreatedLayer"),
|
||||
DocumentResponse::LayerChanged { .. } => write!(f, "LayerChanged"),
|
||||
DocumentResponse::DeletedLayer { .. } => write!(f, "DeleteLayer"),
|
||||
DocumentResponse::DeletedSelectedManipulatorPoints { .. } => write!(f, "DeletedSelectedManipulatorPoints"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ const SIDE_EFFECT_FREE_MESSAGES: &[MessageDiscriminant] = &[
|
||||
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::RenderDocument)),
|
||||
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::NodeGraph(NodeGraphMessageDiscriminant::SendGraph))),
|
||||
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::PropertiesPanel(
|
||||
PropertiesPanelMessageDiscriminant::ResendActiveProperties,
|
||||
PropertiesPanelMessageDiscriminant::Refresh,
|
||||
))),
|
||||
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::FolderChanged)),
|
||||
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::DocumentStructureChanged)),
|
||||
@@ -262,8 +262,8 @@ mod test {
|
||||
use crate::messages::tool::tool_messages::tool_prelude::ToolType;
|
||||
use crate::test_utils::EditorTestUtils;
|
||||
|
||||
use document_legacy::document::LayerId;
|
||||
use document_legacy::document_metadata::LayerNodeIdentifier;
|
||||
use document_legacy::LayerId;
|
||||
use graphene_core::raster::color::Color;
|
||||
|
||||
fn init_logger() {
|
||||
@@ -475,10 +475,6 @@ mod test {
|
||||
)
|
||||
};
|
||||
|
||||
editor.handle_message(DocumentMessage::SetSelectedLayers {
|
||||
replacement_selected_layers: sorted_layers[..2].to_vec(),
|
||||
});
|
||||
|
||||
editor.handle_message(DocumentMessage::SelectedLayersRaise);
|
||||
let (all, non_selected, selected) = verify_order(editor.dispatcher.message_handlers.portfolio_message_handler.active_document_mut().unwrap());
|
||||
assert_eq!(all, non_selected.into_iter().chain(selected).collect::<Vec<_>>());
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use document_legacy::document::LayerId;
|
||||
use document_legacy::document_metadata::LayerNodeIdentifier;
|
||||
use document_legacy::LayerId;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize, specta::Type)]
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use crate::messages::portfolio::utility_types::KeyboardPlatformLayout;
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
pub use document_legacy::DocumentResponse;
|
||||
|
||||
use bitflags::bitflags;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::messages::input_mapper::utility_types::input_keyboard::KeysGroup;
|
||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
use document_legacy::LayerId;
|
||||
use document_legacy::document::LayerId;
|
||||
use graphene_core::raster::color::Color;
|
||||
use graphene_core::text::Font;
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::messages::input_mapper::utility_types::misc::ActionKeys;
|
||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
|
||||
use document_legacy::document::LayerId;
|
||||
use document_legacy::layers::layer_info::LayerDataTypeDiscriminant;
|
||||
use document_legacy::LayerId;
|
||||
use graphene_core::raster::curve::Curve;
|
||||
use graphite_proc_macros::WidgetBuilder;
|
||||
|
||||
|
||||
@@ -4,13 +4,12 @@ use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate,
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
use document_legacy::document::Document as DocumentLegacy;
|
||||
use document_legacy::document::LayerId;
|
||||
use document_legacy::document_metadata::LayerNodeIdentifier;
|
||||
use document_legacy::layers::style::ViewMode;
|
||||
use document_legacy::LayerId;
|
||||
use document_legacy::Operation as DocumentOperation;
|
||||
use graph_craft::document::NodeId;
|
||||
use graphene_core::raster::BlendMode;
|
||||
use graphene_core::raster::Image;
|
||||
use graphene_core::vector::style::ViewMode;
|
||||
use graphene_core::Color;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -20,8 +19,6 @@ use serde::{Deserialize, Serialize};
|
||||
pub enum DocumentMessage {
|
||||
// Sub-messages
|
||||
#[remain::unsorted]
|
||||
DispatchOperation(Box<DocumentOperation>),
|
||||
#[remain::unsorted]
|
||||
#[child]
|
||||
Navigation(NavigationMessage),
|
||||
#[remain::unsorted]
|
||||
@@ -52,9 +49,6 @@ pub enum DocumentMessage {
|
||||
},
|
||||
ClearLayerTree,
|
||||
CommitTransaction,
|
||||
CopyToClipboardLayerImageOutput {
|
||||
layer_path: Vec<LayerId>,
|
||||
},
|
||||
CreateEmptyFolder {
|
||||
parent: LayerNodeIdentifier,
|
||||
},
|
||||
@@ -64,14 +58,9 @@ pub enum DocumentMessage {
|
||||
},
|
||||
DeleteSelectedLayers,
|
||||
DeselectAllLayers,
|
||||
DirtyRenderDocument,
|
||||
DirtyRenderDocumentInOutlineView,
|
||||
DocumentHistoryBackward,
|
||||
DocumentHistoryForward,
|
||||
DocumentStructureChanged,
|
||||
DownloadLayerImageOutput {
|
||||
layer_path: Vec<LayerId>,
|
||||
},
|
||||
DuplicateSelectedLayers,
|
||||
FlipSelectedLayers {
|
||||
flip_axis: FlipAxis,
|
||||
@@ -79,7 +68,6 @@ pub enum DocumentMessage {
|
||||
FolderChanged {
|
||||
affected_folder_path: Vec<LayerId>,
|
||||
},
|
||||
FrameClear,
|
||||
GroupSelectedLayers,
|
||||
ImaginateClear {
|
||||
layer_path: Vec<LayerId>,
|
||||
@@ -116,14 +104,9 @@ pub enum DocumentMessage {
|
||||
RenameDocument {
|
||||
new_name: String,
|
||||
},
|
||||
RenameLayer {
|
||||
layer_path: Vec<LayerId>,
|
||||
new_name: String,
|
||||
},
|
||||
RenderDocument,
|
||||
RenderRulers,
|
||||
RenderScrollbars,
|
||||
RollbackTransaction,
|
||||
SaveDocument,
|
||||
SelectAllLayers,
|
||||
SelectedLayersLower,
|
||||
@@ -141,16 +124,6 @@ pub enum DocumentMessage {
|
||||
SetBlendModeForSelectedLayers {
|
||||
blend_mode: BlendMode,
|
||||
},
|
||||
SetImageBlobUrl {
|
||||
layer_path: Vec<LayerId>,
|
||||
blob_url: String,
|
||||
resolution: (f64, f64),
|
||||
document_id: u64,
|
||||
},
|
||||
SetLayerExpansion {
|
||||
layer_path: Vec<LayerId>,
|
||||
set_expanded: bool,
|
||||
},
|
||||
SetOpacityForSelectedLayers {
|
||||
opacity: f64,
|
||||
},
|
||||
@@ -160,9 +133,6 @@ pub enum DocumentMessage {
|
||||
SetRangeSelectionLayer {
|
||||
new_layer: Option<LayerNodeIdentifier>,
|
||||
},
|
||||
SetSelectedLayers {
|
||||
replacement_selected_layers: Vec<Vec<LayerId>>,
|
||||
},
|
||||
SetSnapping {
|
||||
snapping_enabled: Option<bool>,
|
||||
bounding_box_snapping: Option<bool>,
|
||||
@@ -181,23 +151,7 @@ pub enum DocumentMessage {
|
||||
UpdateDocumentTransform {
|
||||
transform: glam::DAffine2,
|
||||
},
|
||||
UpdateLayerMetadata {
|
||||
layer_path: Vec<LayerId>,
|
||||
layer_metadata: LayerMetadata,
|
||||
},
|
||||
ZoomCanvasTo100Percent,
|
||||
ZoomCanvasTo200Percent,
|
||||
ZoomCanvasToFitAll,
|
||||
}
|
||||
|
||||
impl From<DocumentOperation> for DocumentMessage {
|
||||
fn from(operation: DocumentOperation) -> DocumentMessage {
|
||||
DocumentMessage::DispatchOperation(Box::new(operation))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DocumentOperation> for Message {
|
||||
fn from(operation: DocumentOperation) -> Message {
|
||||
DocumentMessage::DispatchOperation(Box::new(operation)).into()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,14 +17,15 @@ use crate::messages::tool::utility_types::ToolType;
|
||||
use crate::node_graph_executor::NodeGraphExecutor;
|
||||
|
||||
use document_legacy::document::Document as DocumentLegacy;
|
||||
use document_legacy::document::LayerId;
|
||||
use document_legacy::document_metadata::LayerNodeIdentifier;
|
||||
use document_legacy::layers::layer_info::{LayerDataTypeDiscriminant, LegacyLayerType};
|
||||
use document_legacy::layers::style::{RenderData, ViewMode};
|
||||
use document_legacy::{DocumentError, DocumentResponse, LayerId, Operation as DocumentOperation};
|
||||
use document_legacy::layers::layer_info::LayerDataTypeDiscriminant;
|
||||
use document_legacy::DocumentError;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graph_craft::document::{NodeInput, NodeNetwork};
|
||||
use graphene_core::raster::BlendMode;
|
||||
use graphene_core::raster::ImageFrame;
|
||||
use graphene_core::vector::style::ViewMode;
|
||||
|
||||
use glam::{DAffine2, DVec2};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -121,46 +122,10 @@ impl MessageHandler<DocumentMessage, DocumentInputs<'_>> for DocumentMessageHand
|
||||
} = document_inputs;
|
||||
use DocumentMessage::*;
|
||||
|
||||
let render_data = RenderData::new(&persistent_data.font_cache, self.view_mode, Some(ipp.document_bounds()));
|
||||
|
||||
#[remain::sorted]
|
||||
match message {
|
||||
// Sub-messages
|
||||
#[remain::unsorted]
|
||||
DispatchOperation(op) => {
|
||||
match self.document_legacy.handle_operation(*op) {
|
||||
Ok(Some(document_responses)) => {
|
||||
for response in document_responses {
|
||||
match &response {
|
||||
DocumentResponse::FolderChanged { path } => responses.add(FolderChanged { affected_folder_path: path.clone() }),
|
||||
DocumentResponse::DeletedLayer { path } => {
|
||||
self.layer_metadata.remove(path);
|
||||
}
|
||||
DocumentResponse::LayerChanged { path } => responses.add(LayerChanged { affected_layer_path: path.clone() }),
|
||||
DocumentResponse::CreatedLayer { .. } => {
|
||||
unimplemented!("We should no longer be creating layers in the document and should instead be using the node graph.")
|
||||
}
|
||||
DocumentResponse::DocumentChanged => responses.add(RenderDocument),
|
||||
DocumentResponse::DeletedSelectedManipulatorPoints => {
|
||||
// Clear Properties panel after deleting all points by updating backend widget state.
|
||||
responses.add(LayoutMessage::SendLayout {
|
||||
layout: Layout::WidgetLayout(WidgetLayout::new(vec![])),
|
||||
layout_target: LayoutTarget::PropertiesOptions,
|
||||
});
|
||||
responses.add(LayoutMessage::SendLayout {
|
||||
layout: Layout::WidgetLayout(WidgetLayout::new(vec![])),
|
||||
layout_target: LayoutTarget::PropertiesSections,
|
||||
});
|
||||
}
|
||||
};
|
||||
responses.add(BroadcastEvent::DocumentIsDirty);
|
||||
}
|
||||
}
|
||||
Err(e) => error!("DocumentError: {e:?}"),
|
||||
Ok(_) => (),
|
||||
}
|
||||
}
|
||||
#[remain::unsorted]
|
||||
Navigation(message) => {
|
||||
let document_bounds = self.metadata().document_bounds_viewport_space();
|
||||
self.navigation_handler.process_message(
|
||||
@@ -212,7 +177,7 @@ impl MessageHandler<DocumentMessage, DocumentInputs<'_>> for DocumentMessageHand
|
||||
}
|
||||
AddSelectedLayers { additional_layers } => {
|
||||
for layer_path in &additional_layers {
|
||||
responses.extend(self.select_layer(layer_path, &render_data));
|
||||
responses.extend(self.select_layer(layer_path));
|
||||
}
|
||||
|
||||
// TODO: Correctly update layer panel in clear_selection instead of here
|
||||
@@ -269,15 +234,6 @@ impl MessageHandler<DocumentMessage, DocumentInputs<'_>> for DocumentMessageHand
|
||||
});
|
||||
}
|
||||
CommitTransaction => (),
|
||||
CopyToClipboardLayerImageOutput { layer_path } => {
|
||||
let layer = self.document_legacy.layer(&layer_path).ok();
|
||||
|
||||
let blob_url = layer.and_then(|layer| layer.as_layer().ok()).and_then(|layer_layer| layer_layer.as_blob_url()).cloned();
|
||||
|
||||
if let Some(blob_url) = blob_url {
|
||||
responses.add(FrontendMessage::TriggerCopyToClipboardBlobUrl { blob_url });
|
||||
}
|
||||
}
|
||||
CreateEmptyFolder { parent } => {
|
||||
let id = generate_uuid();
|
||||
|
||||
@@ -295,7 +251,6 @@ impl MessageHandler<DocumentMessage, DocumentInputs<'_>> for DocumentMessageHand
|
||||
DeleteLayer { layer_path } => {
|
||||
responses.add(GraphOperationMessage::DeleteLayer { id: layer_path[0] });
|
||||
responses.add_front(BroadcastEvent::ToolAbort);
|
||||
responses.add(PropertiesPanelMessage::CheckSelectedWasDeleted { path: layer_path });
|
||||
}
|
||||
DeleteSelectedLayers => {
|
||||
self.backup(responses);
|
||||
@@ -313,40 +268,19 @@ impl MessageHandler<DocumentMessage, DocumentInputs<'_>> for DocumentMessageHand
|
||||
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![] });
|
||||
self.layer_range_selection_reference = None;
|
||||
}
|
||||
DirtyRenderDocument => {
|
||||
// Mark all non-overlay caches as dirty
|
||||
DocumentLegacy::mark_children_as_dirty(&mut self.document_legacy.root);
|
||||
responses.add(DocumentMessage::RenderDocument);
|
||||
}
|
||||
DirtyRenderDocumentInOutlineView => {
|
||||
if self.view_mode == ViewMode::Outline {
|
||||
responses.add_front(DocumentMessage::DirtyRenderDocument);
|
||||
}
|
||||
}
|
||||
DocumentHistoryBackward => self.undo(responses),
|
||||
DocumentHistoryForward => self.redo(responses),
|
||||
DocumentStructureChanged => {
|
||||
let data_buffer: RawBuffer = self.serialize_root().as_slice().into();
|
||||
responses.add(FrontendMessage::UpdateDocumentLayerTreeStructure { data_buffer })
|
||||
}
|
||||
DownloadLayerImageOutput { layer_path } => {
|
||||
let layer = self.document_legacy.layer(&layer_path).ok();
|
||||
|
||||
let layer_name = layer.map(|layer| layer.name.clone().unwrap_or_else(|| "Untitled Layer".to_string()));
|
||||
|
||||
let blob_url = layer.and_then(|layer| layer.as_layer().ok()).and_then(|layer_layer| layer_layer.as_blob_url()).cloned();
|
||||
|
||||
if let (Some(layer_name), Some(blob_url)) = (layer_name, blob_url) {
|
||||
responses.add(FrontendMessage::TriggerDownloadBlobUrl { layer_name, blob_url });
|
||||
}
|
||||
}
|
||||
DuplicateSelectedLayers => {
|
||||
self.backup(responses);
|
||||
responses.add_front(SetSelectedLayers { replacement_selected_layers: vec![] });
|
||||
self.layer_range_selection_reference = None;
|
||||
for path in self.selected_layers_sorted() {
|
||||
responses.add(DocumentOperation::DuplicateLayer { path: path.to_vec() });
|
||||
}
|
||||
// TODO: Reimplement selected layer duplication
|
||||
// self.backup(responses);
|
||||
// self.layer_range_selection_reference = None;
|
||||
// for path in self.selected_layers_sorted() {
|
||||
// responses.add(DocumentOperation::DuplicateLayer { path: path.to_vec() });
|
||||
// }
|
||||
}
|
||||
FlipSelectedLayers { flip_axis } => {
|
||||
self.backup(responses);
|
||||
@@ -372,27 +306,6 @@ impl MessageHandler<DocumentMessage, DocumentInputs<'_>> for DocumentMessageHand
|
||||
let affected_layer_path = affected_folder_path;
|
||||
responses.extend([LayerChanged { affected_layer_path }.into(), DocumentStructureChanged.into()]);
|
||||
}
|
||||
FrameClear => {
|
||||
let mut selected_frame_layers = self.selected_layers_with_type(LayerDataTypeDiscriminant::Layer);
|
||||
// Get what is hopefully the only selected Layer layer
|
||||
let layer_path = selected_frame_layers.next();
|
||||
// Abort if we didn't have any Layer layer, or if there are additional ones also selected
|
||||
if layer_path.is_none() || selected_frame_layers.next().is_some() {
|
||||
return;
|
||||
}
|
||||
let layer_path = layer_path.unwrap();
|
||||
|
||||
let layer = self.document_legacy.layer(layer_path).expect("Clearing Layer image for invalid layer");
|
||||
let previous_blob_url = match &layer.data {
|
||||
LegacyLayerType::Layer(layer) => layer.as_blob_url(),
|
||||
x => panic!("Cannot find blob url for layer type {}", LayerDataTypeDiscriminant::from(x)),
|
||||
};
|
||||
|
||||
if let Some(url) = previous_blob_url {
|
||||
responses.add(FrontendMessage::TriggerRevokeBlobUrl { url: url.clone() });
|
||||
}
|
||||
responses.add(DocumentOperation::ClearBlobURL { path: layer_path.into() });
|
||||
}
|
||||
GroupSelectedLayers => {
|
||||
// TODO: Add code that changes the insert index of the new folder based on the selected layer
|
||||
let parent = self.metadata().deepest_common_ancestor(self.metadata().selected_layers(), true).unwrap_or(LayerNodeIdentifier::ROOT);
|
||||
@@ -442,10 +355,9 @@ impl MessageHandler<DocumentMessage, DocumentInputs<'_>> for DocumentMessageHand
|
||||
}
|
||||
InputFrameRasterizeRegionBelowLayer { layer_path } => responses.add(PortfolioMessage::SubmitGraphRender { document_id, layer_path }),
|
||||
LayerChanged { affected_layer_path } => {
|
||||
if let Ok(layer_entry) = self.layer_panel_entry(affected_layer_path.clone(), &render_data) {
|
||||
if let Ok(layer_entry) = self.layer_panel_entry(affected_layer_path.clone()) {
|
||||
responses.add(FrontendMessage::UpdateDocumentLayerDetails { data: layer_entry });
|
||||
}
|
||||
responses.add(PropertiesPanelMessage::CheckSelectedWasUpdated { path: affected_layer_path });
|
||||
self.update_layers_panel_options_bar_widgets(responses);
|
||||
}
|
||||
MoveSelectedLayersTo { parent, insert_index } => {
|
||||
@@ -567,11 +479,7 @@ impl MessageHandler<DocumentMessage, DocumentInputs<'_>> for DocumentMessageHand
|
||||
responses.add(PortfolioMessage::UpdateOpenDocumentsList);
|
||||
responses.add(NodeGraphMessage::UpdateNewNodeGraph);
|
||||
}
|
||||
RenameLayer { layer_path, new_name } => responses.add(DocumentOperation::RenameLayer { layer_path, new_name }),
|
||||
RenderDocument => {
|
||||
// responses.add(FrontendMessage::UpdateDocumentArtwork {
|
||||
// svg: self.document_legacy.render_root(&render_data),
|
||||
// });
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
}
|
||||
RenderRulers => {
|
||||
@@ -610,10 +518,6 @@ impl MessageHandler<DocumentMessage, DocumentInputs<'_>> for DocumentMessageHand
|
||||
multiplier: scrollbar_multiplier.into(),
|
||||
});
|
||||
}
|
||||
RollbackTransaction => {
|
||||
self.rollback(responses);
|
||||
responses.extend([RenderDocument.into(), DocumentStructureChanged.into()]);
|
||||
}
|
||||
SaveDocument => {
|
||||
self.set_save_state(true);
|
||||
responses.add(PortfolioMessage::AutoSaveActiveDocument);
|
||||
@@ -699,40 +603,6 @@ impl MessageHandler<DocumentMessage, DocumentInputs<'_>> for DocumentMessageHand
|
||||
responses.add(GraphOperationMessage::BlendModeSet { layer: layer.to_path(), blend_mode });
|
||||
}
|
||||
}
|
||||
SetImageBlobUrl {
|
||||
layer_path,
|
||||
blob_url,
|
||||
resolution,
|
||||
document_id,
|
||||
} => {
|
||||
let Ok(layer) = self.document_legacy.layer(&layer_path) else {
|
||||
warn!("Setting blob URL for invalid layer");
|
||||
return;
|
||||
};
|
||||
|
||||
// Revoke the old blob URL
|
||||
match &layer.data {
|
||||
LegacyLayerType::Layer(layer) => {
|
||||
if let Some(url) = layer.as_blob_url() {
|
||||
responses.add(FrontendMessage::TriggerRevokeBlobUrl { url: url.clone() });
|
||||
}
|
||||
}
|
||||
other => {
|
||||
warn!("Setting blob URL for invalid layer type, which must be a `Layer` layer type. Found: `{other:?}`");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
responses.add(PortfolioMessage::DocumentPassMessage {
|
||||
document_id,
|
||||
message: DocumentOperation::SetLayerBlobUrl { layer_path, blob_url, resolution }.into(),
|
||||
});
|
||||
}
|
||||
SetLayerExpansion { layer_path, set_expanded } => {
|
||||
self.layer_metadata_mut(&layer_path).expanded = set_expanded;
|
||||
responses.add(DocumentStructureChanged);
|
||||
responses.add(LayerChanged { affected_layer_path: layer_path })
|
||||
}
|
||||
SetOpacityForSelectedLayers { opacity } => {
|
||||
self.backup(responses);
|
||||
let opacity = opacity.clamp(0., 1.) as f32;
|
||||
@@ -749,16 +619,6 @@ impl MessageHandler<DocumentMessage, DocumentInputs<'_>> for DocumentMessageHand
|
||||
SetRangeSelectionLayer { new_layer } => {
|
||||
self.layer_range_selection_reference = new_layer;
|
||||
}
|
||||
SetSelectedLayers { replacement_selected_layers } => {
|
||||
let selected = self.layer_metadata.iter_mut().filter(|(_, layer_metadata)| layer_metadata.selected);
|
||||
selected.for_each(|(path, layer_metadata)| {
|
||||
layer_metadata.selected = false;
|
||||
responses.add(LayerChanged { affected_layer_path: path.clone() })
|
||||
});
|
||||
|
||||
let additional_layers = replacement_selected_layers;
|
||||
responses.add_front(AddSelectedLayers { additional_layers });
|
||||
}
|
||||
SetSnapping {
|
||||
snapping_enabled,
|
||||
bounding_box_snapping,
|
||||
@@ -829,9 +689,6 @@ impl MessageHandler<DocumentMessage, DocumentInputs<'_>> for DocumentMessageHand
|
||||
responses.add(DocumentMessage::RenderScrollbars);
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
UpdateLayerMetadata { layer_path, layer_metadata } => {
|
||||
self.layer_metadata.insert(layer_path, layer_metadata);
|
||||
}
|
||||
ZoomCanvasTo100Percent => {
|
||||
responses.add_front(NavigationMessage::SetCanvasZoom { zoom_factor: 1. });
|
||||
}
|
||||
@@ -948,17 +805,15 @@ impl DocumentMessageHandler {
|
||||
&& self.name.starts_with(DEFAULT_DOCUMENT_NAME)
|
||||
}
|
||||
|
||||
fn select_layer(&mut self, path: &[LayerId], render_data: &RenderData) -> Option<Message> {
|
||||
fn select_layer(&mut self, path: &[LayerId]) -> Option<Message> {
|
||||
println!("Select_layer fail: {:?}", self.all_layers_sorted());
|
||||
|
||||
if let Some(layer) = self.layer_metadata.get_mut(path) {
|
||||
let render_data = RenderData::new(render_data.font_cache, self.view_mode, None);
|
||||
|
||||
layer.selected = true;
|
||||
let data = self.layer_panel_entry(path.to_vec(), &render_data).ok()?;
|
||||
let data = self.layer_panel_entry(path.to_vec()).ok()?;
|
||||
(!path.is_empty()).then(|| FrontendMessage::UpdateDocumentLayerDetails { data }.into())
|
||||
} else {
|
||||
warn!("Tried to select non existing layer {path:?}");
|
||||
warn!("Tried to select non-existing layer {path:?}");
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -998,11 +853,11 @@ impl DocumentMessageHandler {
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the bounding boxes for all visible layers, optionally excluding any paths.
|
||||
pub fn bounding_boxes<'a>(&'a self, ignore_document: Option<&'a Vec<Vec<LayerId>>>, _ignore_artboard: Option<LayerId>, render_data: &'a RenderData) -> impl Iterator<Item = [DVec2; 2]> + 'a {
|
||||
self.visible_layers()
|
||||
.filter(move |path| ignore_document.map_or(true, |ignore_document| !ignore_document.iter().any(|ig| ig.as_slice() == *path)))
|
||||
.filter_map(|path| self.document_legacy.viewport_bounding_box(path, render_data).ok()?)
|
||||
/// Returns the bounding boxes for all visible layers.
|
||||
pub fn bounding_boxes<'a>(&'a self) -> impl Iterator<Item = [DVec2; 2]> + 'a {
|
||||
// TODO: Remove this function entirely?
|
||||
// self.visible_layers().filter_map(|path| self.document_legacy.viewport_bounding_box(path, font_cache).ok()?)
|
||||
std::iter::empty()
|
||||
}
|
||||
|
||||
fn serialize_structure(&self, folder: LayerNodeIdentifier, structure: &mut Vec<u64>, data: &mut Vec<LayerId>, path: &mut Vec<LayerId>) {
|
||||
@@ -1159,7 +1014,6 @@ impl DocumentMessageHandler {
|
||||
let old_root = self.metadata().document_to_viewport;
|
||||
let document = std::mem::replace(&mut self.document_legacy, document);
|
||||
self.document_legacy.metadata.document_to_viewport = old_root;
|
||||
self.document_legacy.root.cache_dirty = true;
|
||||
|
||||
let layer_metadata = std::mem::replace(&mut self.layer_metadata, layer_metadata);
|
||||
|
||||
@@ -1260,31 +1114,21 @@ impl DocumentMessageHandler {
|
||||
}
|
||||
|
||||
// TODO: This should probably take a slice not a vec, also why does this even exist when `layer_panel_entry_from_path` also exists?
|
||||
pub fn layer_panel_entry(&mut self, path: Vec<LayerId>, render_data: &RenderData) -> Result<LayerPanelEntry, EditorError> {
|
||||
pub fn layer_panel_entry(&mut self, path: Vec<LayerId>) -> Result<LayerPanelEntry, EditorError> {
|
||||
let data: LayerMetadata = *self
|
||||
.layer_metadata
|
||||
.get_mut(&path)
|
||||
.ok_or_else(|| EditorError::Document(format!("Could not get layer metadata for {path:?}")))?;
|
||||
let layer = self.document_legacy.layer(&path)?;
|
||||
let entry = LayerPanelEntry::new(&data, self.document_legacy.multiply_transforms(&path)?, layer, path, render_data);
|
||||
let entry = LayerPanelEntry::new(&data, layer, path);
|
||||
Ok(entry)
|
||||
}
|
||||
|
||||
/// Returns a list of `LayerPanelEntry`s intended for display purposes. These don't contain
|
||||
/// any actual data, but rather attributes such as visibility and names of the layers.
|
||||
pub fn layer_panel(&mut self, path: &[LayerId], render_data: &RenderData) -> Result<Vec<LayerPanelEntry>, EditorError> {
|
||||
let folder = self.document_legacy.folder(path)?;
|
||||
let paths: Vec<Vec<LayerId>> = folder.layer_ids.iter().map(|id| [path, &[*id]].concat()).collect();
|
||||
let entries = paths.iter().rev().filter_map(|path| self.layer_panel_entry_from_path(path, render_data)).collect();
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
pub fn layer_panel_entry_from_path(&self, path: &[LayerId], render_data: &RenderData) -> Option<LayerPanelEntry> {
|
||||
pub fn layer_panel_entry_from_path(&self, path: &[LayerId]) -> Option<LayerPanelEntry> {
|
||||
let layer_metadata = self.layer_metadata(path);
|
||||
let transform = self.document_legacy.generate_transform_across_scope(path, Some(self.metadata().document_to_viewport.inverse())).ok()?;
|
||||
let layer = self.document_legacy.layer(path).ok()?;
|
||||
|
||||
Some(LayerPanelEntry::new(layer_metadata, transform, layer, path.to_vec(), render_data))
|
||||
Some(LayerPanelEntry::new(layer_metadata, layer, path.to_vec()))
|
||||
}
|
||||
|
||||
/// When working with an insert index, deleting the layers may cause the insert index to point to a different location (if the layer being deleted was located before the insert index).
|
||||
@@ -1297,11 +1141,6 @@ impl DocumentMessageHandler {
|
||||
Ok(new_insert_index)
|
||||
}
|
||||
|
||||
/// Calculates the bounding box of all layers in the document
|
||||
pub fn all_layer_bounds(&self, render_data: &RenderData) -> Option<[DVec2; 2]> {
|
||||
self.document_legacy.viewport_bounding_box(&[], render_data).ok().flatten()
|
||||
}
|
||||
|
||||
/// Calculate the path that new layers should be inserted to.
|
||||
/// Depends on the selected layers as well as their types (Folder/Non-Folder)
|
||||
pub fn get_path_for_new_layer(&self) -> Vec<u64> {
|
||||
|
||||
@@ -103,7 +103,6 @@ impl MessageHandler<NavigationMessage, (&Document, Option<[DVec2; 2]>, &InputPre
|
||||
}
|
||||
|
||||
responses.add(BroadcastEvent::DocumentIsDirty);
|
||||
responses.add(DocumentMessage::DirtyRenderDocumentInOutlineView);
|
||||
responses.add(PortfolioMessage::UpdateDocumentWidgets);
|
||||
self.create_document_transform(ipp.viewport_bounds.center(), responses);
|
||||
}
|
||||
@@ -235,7 +234,6 @@ impl MessageHandler<NavigationMessage, (&Document, Option<[DVec2; 2]>, &InputPre
|
||||
self.zoom = zoom_factor.clamp(VIEWPORT_ZOOM_SCALE_MIN, VIEWPORT_ZOOM_SCALE_MAX);
|
||||
self.zoom *= Self::clamp_zoom(self.zoom, document_bounds, old_zoom, ipp);
|
||||
responses.add(BroadcastEvent::DocumentIsDirty);
|
||||
responses.add(DocumentMessage::DirtyRenderDocumentInOutlineView);
|
||||
responses.add(PortfolioMessage::UpdateDocumentWidgets);
|
||||
self.create_document_transform(ipp.viewport_bounds.center(), responses);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ use graphene_core::{Artboard, Color};
|
||||
|
||||
use glam::{DAffine2, DVec2, IVec2};
|
||||
|
||||
pub type LayerIdentifier = Vec<document_legacy::LayerId>;
|
||||
pub type LayerIdentifier = Vec<document_legacy::document::LayerId>;
|
||||
|
||||
#[impl_message(Message, DocumentMessage, GraphOperation)]
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
|
||||
@@ -3,8 +3,8 @@ use crate::messages::prelude::*;
|
||||
|
||||
use bezier_rs::Subpath;
|
||||
use document_legacy::document::Document;
|
||||
use document_legacy::document::LayerId;
|
||||
use document_legacy::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
|
||||
use document_legacy::{LayerId, Operation};
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graph_craft::document::{generate_uuid, DocumentNode, NodeId, NodeInput, NodeNetwork, NodeOutput};
|
||||
use graphene_core::raster::{BlendMode, ImageFrame};
|
||||
@@ -315,13 +315,13 @@ impl<'a> ModifyInputsContext<'a> {
|
||||
}
|
||||
|
||||
self.node_graph.network.clear();
|
||||
self.responses.add(PropertiesPanelMessage::ResendActiveProperties);
|
||||
self.responses.add(PropertiesPanelMessage::Refresh);
|
||||
let layer_path = self.layer.to_vec();
|
||||
|
||||
if !skip_rerender {
|
||||
self.responses.add(DocumentMessage::InputFrameRasterizeRegionBelowLayer { layer_path });
|
||||
} else {
|
||||
self.responses.add(DocumentMessage::FrameClear);
|
||||
// Code was removed from here which cleared the frame
|
||||
}
|
||||
if existing_node_id.is_none() {
|
||||
self.responses.add(NodeGraphMessage::SendGraph { should_rerender: false });
|
||||
@@ -340,13 +340,13 @@ impl<'a> ModifyInputsContext<'a> {
|
||||
self.modify_existing_node_inputs(existing_node_id, &mut update_input);
|
||||
}
|
||||
|
||||
self.responses.add(PropertiesPanelMessage::ResendActiveProperties);
|
||||
self.responses.add(PropertiesPanelMessage::Refresh);
|
||||
let layer_path = self.layer.to_vec();
|
||||
|
||||
if !skip_rerender {
|
||||
self.responses.add(DocumentMessage::InputFrameRasterizeRegionBelowLayer { layer_path });
|
||||
} else {
|
||||
self.responses.add(DocumentMessage::FrameClear);
|
||||
// Code was removed from here which cleared the frame
|
||||
}
|
||||
}
|
||||
|
||||
@@ -582,8 +582,6 @@ impl MessageHandler<GraphOperationMessage, (&mut Document, &mut Vec<LayerNodeIde
|
||||
GraphOperationMessage::FillSet { layer, fill } => {
|
||||
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(&layer, document, node_graph, responses) {
|
||||
modify_inputs.fill_set(fill);
|
||||
} else {
|
||||
responses.add(Operation::SetLayerFill { path: layer, fill });
|
||||
}
|
||||
}
|
||||
GraphOperationMessage::OpacitySet { layer, opacity } => {
|
||||
@@ -604,8 +602,6 @@ impl MessageHandler<GraphOperationMessage, (&mut Document, &mut Vec<LayerNodeIde
|
||||
GraphOperationMessage::StrokeSet { layer, stroke } => {
|
||||
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(&layer, document, node_graph, responses) {
|
||||
modify_inputs.stroke_set(stroke);
|
||||
} else {
|
||||
responses.add(Operation::SetLayerStroke { path: layer, stroke });
|
||||
}
|
||||
}
|
||||
GraphOperationMessage::TransformChange {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
use document_legacy::LayerId;
|
||||
use document_legacy::document::LayerId;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graph_craft::document::{DocumentNode, NodeId, NodeInput};
|
||||
|
||||
@@ -10,7 +10,6 @@ pub enum NodeGraphMessage {
|
||||
// Messages
|
||||
Init,
|
||||
SelectedNodesUpdated,
|
||||
CloseNodeGraph,
|
||||
ConnectNodesByLink {
|
||||
output_node: u64,
|
||||
output_node_connector_index: usize,
|
||||
@@ -57,9 +56,6 @@ pub enum NodeGraphMessage {
|
||||
displacement_x: i32,
|
||||
displacement_y: i32,
|
||||
},
|
||||
OpenNodeGraph {
|
||||
layer_path: Vec<document_legacy::LayerId>,
|
||||
},
|
||||
PasteNodes {
|
||||
serialized_nodes: String,
|
||||
},
|
||||
|
||||
@@ -6,8 +6,8 @@ use crate::messages::prelude::*;
|
||||
use crate::node_graph_executor::GraphIdentifier;
|
||||
|
||||
use document_legacy::document::Document;
|
||||
use document_legacy::document::LayerId;
|
||||
use document_legacy::document_metadata::LayerNodeIdentifier;
|
||||
use document_legacy::LayerId;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graph_craft::document::{DocumentNode, NodeId, NodeInput, NodeNetwork, NodeOutput};
|
||||
use graphene_core::*;
|
||||
@@ -271,7 +271,7 @@ impl NodeGraphMessageHandler {
|
||||
}
|
||||
|
||||
fn send_graph(network: &NodeNetwork, layer_path: &Option<Vec<LayerId>>, graph_view_overlay_open: bool, responses: &mut VecDeque<Message>) {
|
||||
responses.add(PropertiesPanelMessage::ResendActiveProperties);
|
||||
responses.add(PropertiesPanelMessage::Refresh);
|
||||
|
||||
if !graph_view_overlay_open {
|
||||
return;
|
||||
@@ -489,7 +489,6 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
|
||||
}
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
NodeGraphMessage::CloseNodeGraph => {}
|
||||
NodeGraphMessage::ConnectNodesByLink {
|
||||
output_node,
|
||||
output_node_connector_index,
|
||||
@@ -687,7 +686,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
|
||||
|
||||
let should_rerender = network.connected_to_output(node_id);
|
||||
responses.add(NodeGraphMessage::SendGraph { should_rerender });
|
||||
responses.add(PropertiesPanelMessage::ResendActiveProperties);
|
||||
responses.add(PropertiesPanelMessage::Refresh);
|
||||
}
|
||||
NodeGraphMessage::InsertNode { node_id, document_node } => {
|
||||
if let Some(network) = document.document_network.nested_network_mut(&self.network) {
|
||||
@@ -707,19 +706,6 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
|
||||
}
|
||||
Self::send_graph(network, &self.layer_path, graph_view_overlay_open, responses);
|
||||
}
|
||||
NodeGraphMessage::OpenNodeGraph { layer_path } => {
|
||||
self.layer_path = Some(layer_path);
|
||||
|
||||
if let Some(network) = document.document_network.nested_network(&self.network) {
|
||||
responses.add(document.metadata.clear_selected_nodes());
|
||||
|
||||
Self::send_graph(network, &self.layer_path, graph_view_overlay_open, responses);
|
||||
|
||||
let node_types = document_node_types::collect_node_types();
|
||||
responses.add(FrontendMessage::UpdateNodeTypes { node_types });
|
||||
}
|
||||
self.update_selected(document, responses);
|
||||
}
|
||||
NodeGraphMessage::PasteNodes { serialized_nodes } => {
|
||||
let Some(network) = document.document_network.nested_network(&self.network) else {
|
||||
warn!("No network");
|
||||
@@ -776,7 +762,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
|
||||
}
|
||||
NodeGraphMessage::SelectedNodesSet { nodes } => {
|
||||
responses.add(document.metadata.set_selected_nodes(nodes));
|
||||
responses.add(PropertiesPanelMessage::ResendActiveProperties);
|
||||
responses.add(PropertiesPanelMessage::Refresh);
|
||||
}
|
||||
NodeGraphMessage::SendGraph { should_rerender } => {
|
||||
if let Some(network) = document.document_network.nested_network(&self.network) {
|
||||
@@ -797,7 +783,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
|
||||
|
||||
let input = NodeInput::Value { tagged_value: value, exposed: false };
|
||||
responses.add(NodeGraphMessage::SetNodeInput { node_id, input_index, input });
|
||||
responses.add(PropertiesPanelMessage::ResendActiveProperties);
|
||||
responses.add(PropertiesPanelMessage::Refresh);
|
||||
if (node.name != "Imaginate" || input_index == 0) && network.connected_to_output(node_id) {
|
||||
if let Some(layer_path) = self.layer_path.clone() {
|
||||
responses.add(DocumentMessage::InputFrameRasterizeRegionBelowLayer { layer_path });
|
||||
|
||||
@@ -71,7 +71,7 @@ pub struct NodePropertiesContext<'a> {
|
||||
pub persistent_data: &'a crate::messages::portfolio::utility_types::PersistentData,
|
||||
pub document: &'a document_legacy::document::Document,
|
||||
pub responses: &'a mut VecDeque<crate::messages::prelude::Message>,
|
||||
pub layer_path: &'a [document_legacy::LayerId],
|
||||
pub layer_path: &'a [document_legacy::document::LayerId],
|
||||
pub nested_path: &'a [NodeId],
|
||||
pub executor: &'a mut NodeGraphExecutor,
|
||||
pub network: &'a NodeNetwork,
|
||||
|
||||
@@ -5,8 +5,7 @@ use super::FrontendGraphDataType;
|
||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
use document_legacy::{layers::layer_info::LayerDataTypeDiscriminant, Operation};
|
||||
use graph_craft::concrete;
|
||||
use document_legacy::layers::layer_info::LayerDataTypeDiscriminant;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graph_craft::document::{DocumentNode, NodeId, NodeInput};
|
||||
use graph_craft::imaginate_input::{ImaginateMaskStartingFill, ImaginateSamplingMethod, ImaginateServerStatus, ImaginateStatus};
|
||||
@@ -867,34 +866,10 @@ pub fn load_image_properties(document_node: &DocumentNode, node_id: NodeId, _con
|
||||
vec![LayoutGroup::Row { widgets: url }]
|
||||
}
|
||||
|
||||
pub fn output_properties(_document_node: &DocumentNode, _node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||
let output_type = context.executor.previous_output_type(context.layer_path);
|
||||
let disabled = match output_type {
|
||||
Some(output_type) => output_type != concrete!(ImageFrame<Color>),
|
||||
None => true,
|
||||
};
|
||||
pub fn output_properties(_document_node: &DocumentNode, _node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||
let label = TextLabel::new("Graphics fed into the Output are drawn in the viewport").widget_holder();
|
||||
|
||||
let layer_path_1 = context.layer_path.to_vec();
|
||||
let layer_path_2 = context.layer_path.to_vec();
|
||||
|
||||
let label = TextLabel::new("The graph's output is drawn in the layer").widget_holder();
|
||||
let download_button = TextButton::new("Download Render Output")
|
||||
.tooltip("Download the rendered image output as a PNG file")
|
||||
.disabled(disabled)
|
||||
.on_update(move |_| DocumentMessage::DownloadLayerImageOutput { layer_path: layer_path_1.clone() }.into())
|
||||
.widget_holder();
|
||||
let copy_button = TextButton::new("Copy Render Output")
|
||||
.tooltip("Copy the rendered image output to the clipboard")
|
||||
.disabled(disabled)
|
||||
.on_update(move |_| DocumentMessage::CopyToClipboardLayerImageOutput { layer_path: layer_path_2.clone() }.into())
|
||||
.widget_holder();
|
||||
|
||||
vec![
|
||||
LayoutGroup::Row { widgets: vec![label] },
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![download_button, Separator::new(SeparatorType::Related).widget_holder(), copy_button],
|
||||
},
|
||||
]
|
||||
vec![LayoutGroup::Row { widgets: vec![label] }]
|
||||
}
|
||||
|
||||
pub fn mask_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||
@@ -1738,10 +1713,10 @@ pub fn imaginate_properties(document_node: &DocumentNode, node_id: NodeId, conte
|
||||
LayoutGroup::Row { widgets }.with_tooltip("Seed determines the random outcome, enabling limitless unique variations")
|
||||
};
|
||||
|
||||
let transform = context
|
||||
.executor
|
||||
.introspect_node_in_network(context.network, &imaginate_node, |network| network.inputs.first().copied(), |frame: &ImageFrame<Color>| frame.transform)
|
||||
.unwrap_or_default();
|
||||
// let transform = context
|
||||
// .executor
|
||||
// .introspect_node_in_network(context.network, &imaginate_node, |network| network.inputs.first().copied(), |frame: &ImageFrame<Color>| frame.transform)
|
||||
// .unwrap_or_default();
|
||||
let image_size = context
|
||||
.executor
|
||||
.introspect_node_in_network(
|
||||
@@ -1770,18 +1745,11 @@ pub fn imaginate_properties(document_node: &DocumentNode, node_id: NodeId, conte
|
||||
let dimensions_is_auto = vec2.is_none();
|
||||
let vec2 = vec2.unwrap_or_else(|| round((image_size.0 as f64, image_size.1 as f64).into()));
|
||||
|
||||
let layer_path = context.layer_path.to_vec();
|
||||
widgets.extend_from_slice(&[
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
IconButton::new("Rescale", 24)
|
||||
.tooltip("Set the layer dimensions to this resolution")
|
||||
.on_update(move |_| {
|
||||
Operation::SetLayerScaleAroundPivot {
|
||||
path: layer_path.clone(),
|
||||
new_scale: vec2.into(),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.on_update(move |_| DialogMessage::RequestComingSoonDialog { issue: None }.into())
|
||||
.widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
CheckboxInput::new(!dimensions_is_auto || transform_not_connected)
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
use super::utility_functions::overlay_canvas_element;
|
||||
use super::utility_types::{OverlayContext, OverlayProvider};
|
||||
use super::utility_types::OverlayProvider;
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct OverlaysMessageHandler {
|
||||
pub overlay_providers: HashSet<OverlayProvider>,
|
||||
@@ -16,6 +13,10 @@ impl MessageHandler<OverlaysMessage, (bool, &InputPreprocessorMessageHandler)> f
|
||||
match message {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
OverlaysMessage::Draw => {
|
||||
use super::utility_functions::overlay_canvas_element;
|
||||
use super::utility_types::OverlayContext;
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
let canvas = self.canvas.get_or_insert_with(|| overlay_canvas_element().expect("Failed to get canvas element"));
|
||||
|
||||
let context = self.context.get_or_insert_with(|| {
|
||||
@@ -36,7 +37,10 @@ impl MessageHandler<OverlaysMessage, (bool, &InputPreprocessorMessageHandler)> f
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
OverlaysMessage::Draw => {
|
||||
warn!("Cannot render overlays on non-Wasm targets {overlays_visible} {ipp:?}.");
|
||||
warn!(
|
||||
"Cannot render overlays on non-Wasm targets.\n{responses:?} {overlays_visible} {ipp:?} {:?} {:?}",
|
||||
self.canvas, self.context
|
||||
);
|
||||
}
|
||||
OverlaysMessage::AddProvider(message) => {
|
||||
self.overlay_providers.insert(message);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
mod properties_panel_message;
|
||||
mod properties_panel_message_handler;
|
||||
|
||||
pub mod utility_functions;
|
||||
pub mod utility_types;
|
||||
|
||||
#[doc(inline)]
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
use super::utility_types::TransformOp;
|
||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
use document_legacy::layers::style::{Fill, Stroke};
|
||||
use document_legacy::LayerId;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[remain::sorted]
|
||||
@@ -12,17 +7,6 @@ use serde::{Deserialize, Serialize};
|
||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum PropertiesPanelMessage {
|
||||
// Messages
|
||||
CheckSelectedWasDeleted { path: Vec<LayerId> },
|
||||
CheckSelectedWasUpdated { path: Vec<LayerId> },
|
||||
ClearSelection,
|
||||
Deactivate,
|
||||
Init,
|
||||
ModifyFill { fill: Fill },
|
||||
ModifyPreserveAspect { preserve_aspect: bool },
|
||||
ModifyStroke { stroke: Stroke },
|
||||
ModifyTransform { value: f64, transform_op: TransformOp },
|
||||
ResendActiveProperties,
|
||||
SetActiveLayers { paths: Vec<Vec<LayerId>> },
|
||||
SetPivot { new_position: PivotPosition },
|
||||
UpdateSelectedDocumentProperties,
|
||||
Clear,
|
||||
Refresh,
|
||||
}
|
||||
|
||||
@@ -1,20 +1,13 @@
|
||||
use super::utility_functions::{register_artwork_layer_properties, register_document_graph_properties};
|
||||
use super::utility_types::PropertiesPanelMessageHandlerData;
|
||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::portfolio::document::properties_panel::utility_functions::apply_transform_operation;
|
||||
use crate::messages::portfolio::document::node_graph::NodePropertiesContext;
|
||||
use crate::messages::portfolio::utility_types::PersistentData;
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
use document_legacy::layers::layer_info::LayerDataTypeDiscriminant;
|
||||
use document_legacy::layers::style::{RenderData, ViewMode};
|
||||
use document_legacy::{LayerId, Operation};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct PropertiesPanelMessageHandler {
|
||||
active_selection: Option<Vec<LayerId>>, // TODO: Delete this if it's indeed dead code?
|
||||
}
|
||||
pub struct PropertiesPanelMessageHandler;
|
||||
|
||||
impl<'a> MessageHandler<PropertiesPanelMessage, (&PersistentData, PropertiesPanelMessageHandlerData<'a>)> for PropertiesPanelMessageHandler {
|
||||
#[remain::check]
|
||||
@@ -24,42 +17,13 @@ impl<'a> MessageHandler<PropertiesPanelMessage, (&PersistentData, PropertiesPane
|
||||
let PropertiesPanelMessageHandlerData {
|
||||
document_name,
|
||||
artwork_document,
|
||||
selected_layers,
|
||||
node_graph_message_handler,
|
||||
executor,
|
||||
..
|
||||
} = data;
|
||||
let render_data = RenderData::new(&persistent_data.font_cache, ViewMode::Normal, None);
|
||||
|
||||
match message {
|
||||
SetActiveLayers { paths } => {
|
||||
if paths.len() != 1 {
|
||||
// TODO: Allow for multiple selected layers
|
||||
responses.add(PropertiesPanelMessage::ClearSelection);
|
||||
responses.add(NodeGraphMessage::CloseNodeGraph);
|
||||
} else {
|
||||
let path = paths.into_iter().next().unwrap();
|
||||
if self.active_selection.as_ref() != Some(&path) {
|
||||
// Update the layer visibility
|
||||
if artwork_document
|
||||
.layer(&path)
|
||||
.ok()
|
||||
.filter(|layer| LayerDataTypeDiscriminant::from(&layer.data) == LayerDataTypeDiscriminant::Layer)
|
||||
.is_some()
|
||||
{
|
||||
responses.add(NodeGraphMessage::OpenNodeGraph { layer_path: path.clone() });
|
||||
} else {
|
||||
responses.add(NodeGraphMessage::CloseNodeGraph);
|
||||
}
|
||||
|
||||
self.active_selection = Some(path);
|
||||
responses.add(PropertiesPanelMessage::ResendActiveProperties);
|
||||
}
|
||||
}
|
||||
}
|
||||
ClearSelection => {
|
||||
// This causes the Properties panel to change, so this needs to happen before the following lines clear the Properties panel
|
||||
responses.add(NodeGraphMessage::CloseNodeGraph);
|
||||
|
||||
Clear => {
|
||||
responses.add(LayoutMessage::SendLayout {
|
||||
layout: Layout::WidgetLayout(WidgetLayout::new(vec![])),
|
||||
layout_target: LayoutTarget::PropertiesOptions,
|
||||
@@ -68,84 +32,41 @@ impl<'a> MessageHandler<PropertiesPanelMessage, (&PersistentData, PropertiesPane
|
||||
layout: Layout::WidgetLayout(WidgetLayout::new(vec![])),
|
||||
layout_target: LayoutTarget::PropertiesSections,
|
||||
});
|
||||
self.active_selection = None;
|
||||
}
|
||||
Deactivate => responses.add(BroadcastMessage::UnsubscribeEvent {
|
||||
on: BroadcastEvent::SelectionChanged,
|
||||
message: Box::new(PropertiesPanelMessage::UpdateSelectedDocumentProperties.into()),
|
||||
}),
|
||||
Init => responses.add(BroadcastMessage::SubscribeEvent {
|
||||
on: BroadcastEvent::SelectionChanged,
|
||||
send: Box::new(PropertiesPanelMessage::UpdateSelectedDocumentProperties.into()),
|
||||
}),
|
||||
ModifyTransform { value, transform_op } => {
|
||||
let path = self.active_selection.as_ref().expect("Received update for properties panel with no active layer");
|
||||
let layer = artwork_document.layer(path).unwrap();
|
||||
Refresh => {
|
||||
let mut context = NodePropertiesContext {
|
||||
persistent_data,
|
||||
document: artwork_document,
|
||||
responses,
|
||||
nested_path: &node_graph_message_handler.network,
|
||||
layer_path: &[],
|
||||
executor,
|
||||
network: &artwork_document.document_network,
|
||||
};
|
||||
|
||||
let transform = apply_transform_operation(layer, transform_op, value, &render_data);
|
||||
let properties_sections = node_graph_message_handler.collate_properties(&mut context);
|
||||
|
||||
self.create_document_operation(Operation::SetLayerTransform { path: path.clone(), transform }, true, responses);
|
||||
}
|
||||
ModifyPreserveAspect { preserve_aspect } => {
|
||||
let layer_path = self.active_selection.clone().expect("Received update for properties panel with no active layer");
|
||||
self.create_document_operation(Operation::SetLayerPreserveAspect { layer_path, preserve_aspect }, true, responses);
|
||||
}
|
||||
ModifyFill { fill } => {
|
||||
let path = self.active_selection.clone().expect("Received update for properties panel with no active layer");
|
||||
self.create_document_operation(Operation::SetLayerFill { path, fill }, true, responses);
|
||||
}
|
||||
ModifyStroke { stroke } => {
|
||||
let path = self.active_selection.clone().expect("Received update for properties panel with no active layer");
|
||||
self.create_document_operation(Operation::SetLayerStroke { path, stroke }, true, responses);
|
||||
}
|
||||
SetPivot { new_position } => {
|
||||
let layer = self.active_selection.clone().expect("Received update for properties panel with no active layer");
|
||||
let position: Option<glam::DVec2> = new_position.into();
|
||||
let pivot = position.unwrap();
|
||||
let options_bar = vec![LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
IconLabel::new("File").tooltip("Document").widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
TextInput::new(document_name)
|
||||
.on_update(|text_input| DocumentMessage::RenameDocument { new_name: text_input.value.clone() }.into())
|
||||
.widget_holder(),
|
||||
Separator::new(SeparatorType::Related).widget_holder(),
|
||||
PopoverButton::new("Additional Options", "Coming soon").widget_holder(),
|
||||
],
|
||||
}];
|
||||
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
responses.add(GraphOperationMessage::TransformSetPivot { layer, pivot });
|
||||
context.responses.add(LayoutMessage::SendLayout {
|
||||
layout: Layout::WidgetLayout(WidgetLayout::new(options_bar)),
|
||||
layout_target: LayoutTarget::PropertiesOptions,
|
||||
});
|
||||
context.responses.add(LayoutMessage::SendLayout {
|
||||
layout: Layout::WidgetLayout(WidgetLayout::new(properties_sections)),
|
||||
layout_target: LayoutTarget::PropertiesSections,
|
||||
});
|
||||
}
|
||||
CheckSelectedWasUpdated { path } => {
|
||||
if self.matches_selected(&path) {
|
||||
responses.add(PropertiesPanelMessage::ResendActiveProperties)
|
||||
}
|
||||
}
|
||||
CheckSelectedWasDeleted { path } => {
|
||||
if self.matches_selected(&path) {
|
||||
self.active_selection = None;
|
||||
responses.add(LayoutMessage::SendLayout {
|
||||
layout_target: LayoutTarget::PropertiesOptions,
|
||||
layout: Layout::WidgetLayout(WidgetLayout::default()),
|
||||
});
|
||||
responses.add(LayoutMessage::SendLayout {
|
||||
layout_target: LayoutTarget::PropertiesSections,
|
||||
layout: Layout::WidgetLayout(WidgetLayout::default()),
|
||||
});
|
||||
responses.add(NodeGraphMessage::CloseNodeGraph);
|
||||
}
|
||||
}
|
||||
ResendActiveProperties => {
|
||||
if let Some(path) = self.active_selection.clone() {
|
||||
// TODO: Remove this conditional now that the document graph is the only form of graph? (Also any other related code.)
|
||||
let layer = artwork_document.layer(&path).unwrap();
|
||||
register_artwork_layer_properties(artwork_document, path, layer, responses, persistent_data, node_graph_message_handler, executor);
|
||||
} else {
|
||||
let context = crate::messages::portfolio::document::node_graph::NodePropertiesContext {
|
||||
persistent_data,
|
||||
document: artwork_document,
|
||||
responses,
|
||||
nested_path: &node_graph_message_handler.network,
|
||||
layer_path: &[],
|
||||
executor,
|
||||
network: &artwork_document.document_network,
|
||||
};
|
||||
register_document_graph_properties(context, node_graph_message_handler, document_name);
|
||||
}
|
||||
}
|
||||
UpdateSelectedDocumentProperties => responses.add(PropertiesPanelMessage::SetActiveLayers {
|
||||
paths: selected_layers.map(|path| path.to_vec()).collect(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,21 +74,3 @@ impl<'a> MessageHandler<PropertiesPanelMessage, (&PersistentData, PropertiesPane
|
||||
actions!(PropertiesMessageDiscriminant;)
|
||||
}
|
||||
}
|
||||
|
||||
impl PropertiesPanelMessageHandler {
|
||||
fn matches_selected(&self, path: &[LayerId]) -> bool {
|
||||
let last_active_path_id = self.active_selection.as_ref().and_then(|v| v.last().copied());
|
||||
let last_modified = path.last().copied();
|
||||
matches!((last_active_path_id, last_modified), (Some(active_last), Some(modified_last)) if active_last == modified_last)
|
||||
}
|
||||
|
||||
fn create_document_operation(&self, operation: Operation, commit_history: bool, responses: &mut VecDeque<Message>) {
|
||||
// Commit to history before the modification
|
||||
if commit_history {
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
}
|
||||
|
||||
// Dispatch the relevant operation to the main document
|
||||
responses.add(DocumentMessage::DispatchOperation(Box::new(operation)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,793 +0,0 @@
|
||||
use super::utility_types::TransformOp;
|
||||
use crate::application::generate_uuid;
|
||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::portfolio::document::node_graph::NodePropertiesContext;
|
||||
use crate::messages::portfolio::utility_types::PersistentData;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::node_graph_executor::NodeGraphExecutor;
|
||||
|
||||
use document_legacy::document::Document;
|
||||
use document_legacy::layers::layer_info::{LegacyLayer, LegacyLayerType};
|
||||
use document_legacy::layers::style::{Fill, Gradient, GradientType, LineCap, LineJoin, RenderData, Stroke, ViewMode};
|
||||
use graphene_core::raster::color::Color;
|
||||
|
||||
use glam::{DAffine2, DVec2};
|
||||
use std::f64::consts::PI;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub fn apply_transform_operation(layer: &LegacyLayer, transform_op: TransformOp, value: f64, render_data: &RenderData) -> [f64; 6] {
|
||||
let transformation = match transform_op {
|
||||
TransformOp::X => DAffine2::update_x,
|
||||
TransformOp::Y => DAffine2::update_y,
|
||||
TransformOp::ScaleX | TransformOp::Width => DAffine2::update_scale_x,
|
||||
TransformOp::ScaleY | TransformOp::Height => DAffine2::update_scale_y,
|
||||
TransformOp::Rotation => DAffine2::update_rotation,
|
||||
};
|
||||
|
||||
let scale = match transform_op {
|
||||
TransformOp::Width => layer.bounding_transform(render_data).scale_x() / layer.transform.scale_x(),
|
||||
TransformOp::Height => layer.bounding_transform(render_data).scale_y() / layer.transform.scale_y(),
|
||||
_ => 1.,
|
||||
};
|
||||
|
||||
// Apply the operation
|
||||
let transform = transformation(layer.transform, value / scale);
|
||||
|
||||
// Return this transform if it is not a dimensions change
|
||||
if !matches!(transform_op, TransformOp::ScaleX | TransformOp::Width | TransformOp::ScaleY | TransformOp::Height) {
|
||||
return transform.to_cols_array();
|
||||
}
|
||||
|
||||
// Find the layerspace pivot
|
||||
let pivot = DAffine2::from_translation(layer.transform.transform_point2(layer.layerspace_pivot(render_data)));
|
||||
|
||||
// Find the delta transform
|
||||
let mut delta = layer.transform.inverse() * transform;
|
||||
if !delta.is_finite() {
|
||||
return layer.transform.to_cols_array();
|
||||
}
|
||||
|
||||
// Preserve aspect ratio
|
||||
if matches!(transform_op, TransformOp::ScaleX | TransformOp::Width) && layer.preserve_aspect {
|
||||
let scale_x = layer.transform.scale_x();
|
||||
if scale_x != 0. {
|
||||
delta = DAffine2::from_scale((1., (value / scale) / scale_x).into()) * delta;
|
||||
}
|
||||
} else if layer.preserve_aspect {
|
||||
let scale_y = layer.transform.scale_y();
|
||||
if scale_y != 0. {
|
||||
delta = DAffine2::from_scale(((value / scale) / scale_y, 1.).into()) * delta;
|
||||
}
|
||||
}
|
||||
|
||||
// Transform around pivot
|
||||
((pivot * delta * pivot.inverse()) * layer.transform).to_cols_array()
|
||||
}
|
||||
|
||||
pub fn register_artwork_layer_properties(
|
||||
document: &Document,
|
||||
layer_path: Vec<document_legacy::LayerId>,
|
||||
layer: &LegacyLayer,
|
||||
responses: &mut VecDeque<Message>,
|
||||
persistent_data: &PersistentData,
|
||||
node_graph_message_handler: &NodeGraphMessageHandler,
|
||||
executor: &mut NodeGraphExecutor,
|
||||
) {
|
||||
let options_bar = vec![LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
match &layer.data {
|
||||
LegacyLayerType::Folder(_) => IconLabel::new("Folder").tooltip("Folder").widget_holder(),
|
||||
LegacyLayerType::Shape(_) => IconLabel::new("NodeShape").tooltip("Shape").widget_holder(),
|
||||
LegacyLayerType::Layer(_) => IconLabel::new("Layer").tooltip("Layer").widget_holder(),
|
||||
},
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
TextInput::new(layer.name.clone().unwrap_or_else(|| "Untitled Layer".to_string()))
|
||||
.on_update(|_text_input: &TextInput| panic!("This is presumed to be dead code, but if you are seeing this crash, please file a bug report."))
|
||||
.widget_holder(),
|
||||
Separator::new(SeparatorType::Related).widget_holder(),
|
||||
PopoverButton::new("Additional Options", "Coming soon").widget_holder(),
|
||||
],
|
||||
}];
|
||||
|
||||
let properties_body = match &layer.data {
|
||||
LegacyLayerType::Shape(shape) => {
|
||||
if let Some(fill_layout) = node_section_fill(shape.style.fill()) {
|
||||
vec![
|
||||
node_section_transform(layer, persistent_data),
|
||||
fill_layout,
|
||||
node_section_stroke(&shape.style.stroke().unwrap_or_default()),
|
||||
]
|
||||
} else {
|
||||
vec![node_section_transform(layer, persistent_data), node_section_stroke(&shape.style.stroke().unwrap_or_default())]
|
||||
}
|
||||
}
|
||||
LegacyLayerType::Layer(layer) => {
|
||||
let mut context = NodePropertiesContext {
|
||||
persistent_data,
|
||||
document,
|
||||
responses,
|
||||
nested_path: &node_graph_message_handler.network,
|
||||
layer_path: &layer_path,
|
||||
executor,
|
||||
network: &layer.network,
|
||||
};
|
||||
let properties_sections = node_graph_message_handler.collate_properties(&mut context);
|
||||
|
||||
properties_sections
|
||||
}
|
||||
LegacyLayerType::Folder(_) => {
|
||||
vec![node_section_transform(layer, persistent_data)]
|
||||
}
|
||||
};
|
||||
|
||||
responses.add(LayoutMessage::SendLayout {
|
||||
layout: Layout::WidgetLayout(WidgetLayout::new(options_bar)),
|
||||
layout_target: LayoutTarget::PropertiesOptions,
|
||||
});
|
||||
responses.add(LayoutMessage::SendLayout {
|
||||
layout: Layout::WidgetLayout(WidgetLayout::new(properties_body)),
|
||||
layout_target: LayoutTarget::PropertiesSections,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn register_document_graph_properties(mut context: NodePropertiesContext, node_graph_message_handler: &NodeGraphMessageHandler, document_name: &str) {
|
||||
let properties_sections = node_graph_message_handler.collate_properties(&mut context);
|
||||
|
||||
let options_bar = vec![LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
IconLabel::new("File").tooltip("Document").widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
TextInput::new(document_name)
|
||||
.on_update(|text_input| DocumentMessage::RenameDocument { new_name: text_input.value.clone() }.into())
|
||||
.widget_holder(),
|
||||
Separator::new(SeparatorType::Related).widget_holder(),
|
||||
PopoverButton::new("Additional Options", "Coming soon").widget_holder(),
|
||||
],
|
||||
}];
|
||||
|
||||
context.responses.add(LayoutMessage::SendLayout {
|
||||
layout: Layout::WidgetLayout(WidgetLayout::new(options_bar)),
|
||||
layout_target: LayoutTarget::PropertiesOptions,
|
||||
});
|
||||
context.responses.add(LayoutMessage::SendLayout {
|
||||
layout: Layout::WidgetLayout(WidgetLayout::new(properties_sections)),
|
||||
layout_target: LayoutTarget::PropertiesSections,
|
||||
});
|
||||
}
|
||||
|
||||
fn node_section_transform(layer: &LegacyLayer, persistent_data: &PersistentData) -> LayoutGroup {
|
||||
let render_data = RenderData::new(&persistent_data.font_cache, ViewMode::default(), None);
|
||||
let pivot = layer.transform.transform_vector2(layer.layerspace_pivot(&render_data));
|
||||
LayoutGroup::Section {
|
||||
name: "Transform".into(),
|
||||
layout: vec![
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
TextLabel::new("Location").widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
PivotInput::new(layer.pivot.into())
|
||||
.on_update(|pivot_input: &PivotInput| PropertiesPanelMessage::SetPivot { new_position: pivot_input.position }.into())
|
||||
.widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
NumberInput::new(Some(layer.transform.x() + pivot.x))
|
||||
.label("X")
|
||||
.unit(" px")
|
||||
.min(-((1u64 << std::f64::MANTISSA_DIGITS) as f64))
|
||||
.max((1u64 << std::f64::MANTISSA_DIGITS) as f64)
|
||||
.on_update(move |number_input: &NumberInput| {
|
||||
PropertiesPanelMessage::ModifyTransform {
|
||||
value: number_input.value.unwrap() - pivot.x,
|
||||
transform_op: TransformOp::X,
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_holder(),
|
||||
Separator::new(SeparatorType::Related).widget_holder(),
|
||||
NumberInput::new(Some(layer.transform.y() + pivot.y))
|
||||
.label("Y")
|
||||
.unit(" px")
|
||||
.min(-((1u64 << std::f64::MANTISSA_DIGITS) as f64))
|
||||
.max((1u64 << std::f64::MANTISSA_DIGITS) as f64)
|
||||
.on_update(move |number_input: &NumberInput| {
|
||||
PropertiesPanelMessage::ModifyTransform {
|
||||
value: number_input.value.unwrap() - pivot.y,
|
||||
transform_op: TransformOp::Y,
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_holder(),
|
||||
],
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
TextLabel::new("Rotation").widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(), // TODO: These three separators add up to 24px,
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(), // TODO: which is the width of the Assist area.
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists.
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
NumberInput::new(Some(layer.transform.rotation() * 180. / PI))
|
||||
.unit("°")
|
||||
.mode(NumberInputMode::Range)
|
||||
.range_min(Some(-180.))
|
||||
.range_max(Some(180.))
|
||||
.on_update(|number_input: &NumberInput| {
|
||||
PropertiesPanelMessage::ModifyTransform {
|
||||
value: number_input.value.unwrap() / 180. * PI,
|
||||
transform_op: TransformOp::Rotation,
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_holder(),
|
||||
],
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
TextLabel::new("Scale").widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
Separator::new(SeparatorType::Related).widget_holder(),
|
||||
CheckboxInput::new(layer.preserve_aspect)
|
||||
.icon("Link")
|
||||
.tooltip("Preserve Aspect Ratio")
|
||||
.on_update(|input: &CheckboxInput| PropertiesPanelMessage::ModifyPreserveAspect { preserve_aspect: input.checked }.into())
|
||||
.widget_holder(),
|
||||
Separator::new(SeparatorType::Related).widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
NumberInput::new(Some(layer.transform.scale_x()))
|
||||
.label("X")
|
||||
.unit("")
|
||||
.min(0.)
|
||||
.max((1u64 << std::f64::MANTISSA_DIGITS) as f64)
|
||||
.on_update(|number_input: &NumberInput| {
|
||||
PropertiesPanelMessage::ModifyTransform {
|
||||
value: number_input.value.unwrap(),
|
||||
transform_op: TransformOp::ScaleX,
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_holder(),
|
||||
Separator::new(SeparatorType::Related).widget_holder(),
|
||||
NumberInput::new(Some(layer.transform.scale_y()))
|
||||
.label("Y")
|
||||
.unit("")
|
||||
.max((1u64 << std::f64::MANTISSA_DIGITS) as f64)
|
||||
.on_update(|number_input: &NumberInput| {
|
||||
PropertiesPanelMessage::ModifyTransform {
|
||||
value: number_input.value.unwrap(),
|
||||
transform_op: TransformOp::ScaleY,
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_holder(),
|
||||
],
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
TextLabel::new("Dimensions").widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(), // TODO: These three separators add up to 24px,
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(), // TODO: which is the width of the Assist area.
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists.
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
NumberInput::new(Some(layer.bounding_transform(&render_data).scale_x()))
|
||||
.label("W")
|
||||
.unit(" px")
|
||||
.max((1u64 << f64::MANTISSA_DIGITS) as f64)
|
||||
.on_update(|number_input: &NumberInput| {
|
||||
PropertiesPanelMessage::ModifyTransform {
|
||||
value: number_input.value.unwrap(),
|
||||
transform_op: TransformOp::Width,
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_holder(),
|
||||
Separator::new(SeparatorType::Related).widget_holder(),
|
||||
NumberInput::new(Some(layer.bounding_transform(&render_data).scale_y()))
|
||||
.label("H")
|
||||
.unit(" px")
|
||||
.max((1u64 << f64::MANTISSA_DIGITS) as f64)
|
||||
.on_update(|number_input: &NumberInput| {
|
||||
PropertiesPanelMessage::ModifyTransform {
|
||||
value: number_input.value.unwrap(),
|
||||
transform_op: TransformOp::Height,
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_holder(),
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
fn node_gradient_type(gradient: &Gradient) -> LayoutGroup {
|
||||
let selected_index = match gradient.gradient_type {
|
||||
GradientType::Linear => 0,
|
||||
GradientType::Radial => 1,
|
||||
};
|
||||
let mut cloned_gradient_linear = gradient.clone();
|
||||
cloned_gradient_linear.gradient_type = GradientType::Linear;
|
||||
let mut cloned_gradient_radial = gradient.clone();
|
||||
cloned_gradient_radial.gradient_type = GradientType::Radial;
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
TextLabel::new("Gradient Type").widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(), // TODO: These three separators add up to 24px,
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(), // TODO: which is the width of the Assist area.
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists.
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
RadioInput::new(vec![
|
||||
RadioEntryData::new("linear")
|
||||
.label("Linear")
|
||||
.tooltip("Linear gradient changes colors from one side to the other along a line")
|
||||
.on_update(move |_| {
|
||||
PropertiesPanelMessage::ModifyFill {
|
||||
fill: Fill::Gradient(cloned_gradient_linear.clone()),
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
RadioEntryData::new("radial")
|
||||
.label("Radial")
|
||||
.tooltip("Radial gradient changes colors from the inside to the outside of a circular area")
|
||||
.on_update(move |_| {
|
||||
PropertiesPanelMessage::ModifyFill {
|
||||
fill: Fill::Gradient(cloned_gradient_radial.clone()),
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
])
|
||||
.selected_index(Some(selected_index))
|
||||
.widget_holder(),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
fn node_gradient_color(gradient: &Gradient, position: usize) -> LayoutGroup {
|
||||
let gradient_clone = Arc::new(gradient.clone());
|
||||
let gradient_2 = gradient_clone.clone();
|
||||
let gradient_3 = gradient_clone.clone();
|
||||
let send_fill_message = move |new_gradient: Gradient| PropertiesPanelMessage::ModifyFill { fill: Fill::Gradient(new_gradient) }.into();
|
||||
|
||||
let value = format!("Gradient: {:.0}%", gradient_clone.positions[position].0 * 100.);
|
||||
let mut widgets = vec![
|
||||
TextLabel::new(value)
|
||||
.tooltip("Adjustable by dragging the gradient stops in the viewport with the Gradient tool active")
|
||||
.widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(), // TODO: These three separators add up to 24px,
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(), // TODO: which is the width of the Assist area.
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists.
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
ColorButton::new(gradient_clone.positions[position].1)
|
||||
.on_update(move |text_input: &ColorButton| {
|
||||
let mut new_gradient = (*gradient_clone).clone();
|
||||
new_gradient.positions[position].1 = text_input.value;
|
||||
send_fill_message(new_gradient)
|
||||
})
|
||||
.widget_holder(),
|
||||
];
|
||||
|
||||
let mut skip_separator = false;
|
||||
// Remove button
|
||||
if gradient.positions.len() != position + 1 && position != 0 {
|
||||
let on_update = move |_: &IconButton| {
|
||||
let mut new_gradient = (*gradient_3).clone();
|
||||
new_gradient.positions.remove(position);
|
||||
send_fill_message(new_gradient)
|
||||
};
|
||||
|
||||
skip_separator = true;
|
||||
widgets.push(Separator::new(SeparatorType::Related).widget_holder());
|
||||
widgets.push(IconButton::new("Remove", 16).tooltip("Remove this gradient stop").on_update(on_update).widget_holder());
|
||||
}
|
||||
// Add button
|
||||
if gradient.positions.len() != position + 1 {
|
||||
let on_update = move |_: &IconButton| {
|
||||
let mut gradient = (*gradient_2).clone();
|
||||
|
||||
let get_color = |index: usize| match (gradient.positions[index].1, gradient.positions.get(index + 1).and_then(|x| x.1)) {
|
||||
(Some(a), Some(b)) => Color::from_rgbaf32((a.r() + b.r()) / 2., (a.g() + b.g()) / 2., (a.b() + b.b()) / 2., ((a.a() + b.a()) / 2.).clamp(0., 1.)),
|
||||
(Some(v), _) | (_, Some(v)) => Some(v),
|
||||
_ => Some(Color::WHITE),
|
||||
};
|
||||
let get_pos = |index: usize| (gradient.positions[index].0 + gradient.positions.get(index + 1).map(|v| v.0).unwrap_or(1.)) / 2.;
|
||||
|
||||
gradient.positions.push((get_pos(position), get_color(position)));
|
||||
|
||||
gradient.positions.sort_unstable_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
|
||||
|
||||
send_fill_message(gradient)
|
||||
};
|
||||
|
||||
if !skip_separator {
|
||||
widgets.push(Separator::new(SeparatorType::Related).widget_holder());
|
||||
}
|
||||
widgets.push(IconButton::new("Add", 16).tooltip("Add a gradient stop after this").on_update(on_update).widget_holder());
|
||||
}
|
||||
LayoutGroup::Row { widgets }
|
||||
}
|
||||
|
||||
fn node_section_fill(fill: &Fill) -> Option<LayoutGroup> {
|
||||
let initial_color = if let Fill::Solid(color) = fill { *color } else { Color::BLACK };
|
||||
|
||||
match fill {
|
||||
Fill::Solid(_) | Fill::None => Some(LayoutGroup::Section {
|
||||
name: "Fill".into(),
|
||||
layout: vec![
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
TextLabel::new("Color").widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(), // TODO: These three separators add up to 24px,
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(), // TODO: which is the width of the Assist area.
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists.
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
ColorButton::new(if let Fill::Solid(color) = fill { Some(*color) } else { None })
|
||||
.on_update(|text_input: &ColorButton| {
|
||||
let fill = if let Some(value) = text_input.value { Fill::Solid(value) } else { Fill::None };
|
||||
PropertiesPanelMessage::ModifyFill { fill }.into()
|
||||
})
|
||||
.widget_holder(),
|
||||
],
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
TextLabel::new("").widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(), // TODO: These three separators add up to 24px,
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(), // TODO: which is the width of the Assist area.
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists.
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
TextButton::new("Use Gradient")
|
||||
.tooltip("Change this fill from a solid color to a gradient")
|
||||
.on_update(move |_: &TextButton| {
|
||||
let (r, g, b, _) = initial_color.components();
|
||||
let opposite_color = Color::from_rgbaf32(1. - r, 1. - g, 1. - b, 1.).unwrap();
|
||||
|
||||
PropertiesPanelMessage::ModifyFill {
|
||||
fill: Fill::Gradient(Gradient::new(
|
||||
DVec2::new(0., 0.5),
|
||||
initial_color,
|
||||
DVec2::new(1., 0.5),
|
||||
opposite_color,
|
||||
DAffine2::IDENTITY,
|
||||
generate_uuid(),
|
||||
GradientType::Linear,
|
||||
)),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_holder(),
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
Fill::Gradient(gradient) => Some(LayoutGroup::Section {
|
||||
name: "Fill".into(),
|
||||
layout: {
|
||||
let cloned_gradient = gradient.clone();
|
||||
let first_color = gradient.positions.get(0).unwrap_or(&(0., None)).1;
|
||||
|
||||
let mut layout = vec![node_gradient_type(gradient)];
|
||||
layout.extend((0..gradient.positions.len()).map(|pos| node_gradient_color(gradient, pos)));
|
||||
layout.push(LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
TextLabel::new("").widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(), // TODO: These three separators add up to 24px,
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(), // TODO: which is the width of the Assist area.
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists.
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
TextButton::new("Invert")
|
||||
.icon(Some("Swap".into()))
|
||||
.tooltip("Reverse the order of each color stop")
|
||||
.on_update(move |_: &TextButton| {
|
||||
let mut new_gradient = cloned_gradient.clone();
|
||||
new_gradient.positions = new_gradient.positions.iter().map(|(distance, color)| (1. - distance, *color)).collect();
|
||||
new_gradient.positions.reverse();
|
||||
PropertiesPanelMessage::ModifyFill { fill: Fill::Gradient(new_gradient) }.into()
|
||||
})
|
||||
.widget_holder(),
|
||||
],
|
||||
});
|
||||
layout.push(LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
TextLabel::new("").widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(), // TODO: These three separators add up to 24px,
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(), // TODO: which is the width of the Assist area.
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists.
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
TextButton::new("Use Solid Color")
|
||||
.tooltip("Change this fill from a gradient to a solid color, keeping the 0% stop color")
|
||||
.on_update(move |_: &TextButton| {
|
||||
PropertiesPanelMessage::ModifyFill {
|
||||
fill: Fill::Solid(first_color.unwrap_or_default()),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_holder(),
|
||||
],
|
||||
});
|
||||
layout
|
||||
},
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn node_section_stroke(stroke: &Stroke) -> LayoutGroup {
|
||||
// We have to make multiple variables because they get moved into different closures.
|
||||
let internal_stroke1 = stroke.clone();
|
||||
let internal_stroke2 = stroke.clone();
|
||||
let internal_stroke3 = stroke.clone();
|
||||
let internal_stroke4 = stroke.clone();
|
||||
let internal_stroke5 = stroke.clone();
|
||||
let internal_stroke6 = stroke.clone();
|
||||
let internal_stroke7 = stroke.clone();
|
||||
let internal_stroke8 = stroke.clone();
|
||||
let internal_stroke9 = stroke.clone();
|
||||
let internal_stroke10 = stroke.clone();
|
||||
let internal_stroke11 = stroke.clone();
|
||||
|
||||
LayoutGroup::Section {
|
||||
name: "Stroke".into(),
|
||||
layout: vec![
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
TextLabel::new("Color").widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(), // TODO: These three separators add up to 24px,
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(), // TODO: which is the width of the Assist area.
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists.
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
ColorButton::new(stroke.color())
|
||||
.on_update(move |text_input: &ColorButton| {
|
||||
internal_stroke1
|
||||
.clone()
|
||||
.with_color(&text_input.value)
|
||||
.map_or(PropertiesPanelMessage::ResendActiveProperties.into(), |stroke| PropertiesPanelMessage::ModifyStroke { stroke }.into())
|
||||
})
|
||||
.widget_holder(),
|
||||
],
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
TextLabel::new("Weight").widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(), // TODO: These three separators add up to 24px,
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(), // TODO: which is the width of the Assist area.
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists.
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
NumberInput::new(Some(stroke.weight()))
|
||||
.is_integer(false)
|
||||
.min(0.)
|
||||
.max((1u64 << std::f64::MANTISSA_DIGITS) as f64)
|
||||
.unit(" px")
|
||||
.on_update(move |number_input: &NumberInput| {
|
||||
PropertiesPanelMessage::ModifyStroke {
|
||||
stroke: internal_stroke2.clone().with_weight(number_input.value.unwrap()),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_holder(),
|
||||
],
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
TextLabel::new("Dash Lengths").widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(), // TODO: These three separators add up to 24px,
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(), // TODO: which is the width of the Assist area.
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists.
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
TextInput::new(stroke.dash_lengths())
|
||||
.centered(true)
|
||||
.on_update(move |text_input: &TextInput| {
|
||||
internal_stroke3
|
||||
.clone()
|
||||
.with_dash_lengths(&text_input.value)
|
||||
.map_or(PropertiesPanelMessage::ResendActiveProperties.into(), |stroke| PropertiesPanelMessage::ModifyStroke { stroke }.into())
|
||||
})
|
||||
.widget_holder(),
|
||||
],
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
TextLabel::new("Dash Offset").widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(), // TODO: These three separators add up to 24px,
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(), // TODO: which is the width of the Assist area.
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists.
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
NumberInput::new(Some(stroke.dash_offset()))
|
||||
.is_integer(true)
|
||||
.min(0.)
|
||||
.max((1u64 << std::f64::MANTISSA_DIGITS) as f64)
|
||||
.unit(" px")
|
||||
.on_update(move |number_input: &NumberInput| {
|
||||
PropertiesPanelMessage::ModifyStroke {
|
||||
stroke: internal_stroke4.clone().with_dash_offset(number_input.value.unwrap()),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_holder(),
|
||||
],
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
TextLabel::new("Line Cap").widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(), // TODO: These three separators add up to 24px,
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(), // TODO: which is the width of the Assist area.
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists.
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
RadioInput::new(vec![
|
||||
RadioEntryData::new("Butt").on_update(move |_| {
|
||||
PropertiesPanelMessage::ModifyStroke {
|
||||
stroke: internal_stroke6.clone().with_line_cap(LineCap::Butt),
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
RadioEntryData::new("Round").on_update(move |_| {
|
||||
PropertiesPanelMessage::ModifyStroke {
|
||||
stroke: internal_stroke7.clone().with_line_cap(LineCap::Round),
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
RadioEntryData::new("Square").on_update(move |_| {
|
||||
PropertiesPanelMessage::ModifyStroke {
|
||||
stroke: internal_stroke8.clone().with_line_cap(LineCap::Square),
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
])
|
||||
.selected_index(Some(stroke.line_cap_index()))
|
||||
.widget_holder(),
|
||||
],
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
TextLabel::new("Line Join").widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(), // TODO: These three separators add up to 24px,
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(), // TODO: which is the width of the Assist area.
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists.
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
RadioInput::new(vec![
|
||||
RadioEntryData::new("Miter").on_update(move |_| {
|
||||
PropertiesPanelMessage::ModifyStroke {
|
||||
stroke: internal_stroke9.clone().with_line_join(LineJoin::Miter),
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
RadioEntryData::new("Bevel").on_update(move |_| {
|
||||
PropertiesPanelMessage::ModifyStroke {
|
||||
stroke: internal_stroke10.clone().with_line_join(LineJoin::Bevel),
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
RadioEntryData::new("Round").on_update(move |_| {
|
||||
PropertiesPanelMessage::ModifyStroke {
|
||||
stroke: internal_stroke11.clone().with_line_join(LineJoin::Round),
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
])
|
||||
.selected_index(Some(stroke.line_join_index()))
|
||||
.widget_holder(),
|
||||
],
|
||||
},
|
||||
// TODO: Gray out this row when Line Join isn't set to Miter
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
TextLabel::new("Miter Limit").widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(), // TODO: These three separators add up to 24px,
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(), // TODO: which is the width of the Assist area.
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists.
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
NumberInput::new(Some(stroke.line_join_miter_limit() as f64))
|
||||
.is_integer(true)
|
||||
.min(0.)
|
||||
.max((1u64 << std::f64::MANTISSA_DIGITS) as f64)
|
||||
.unit("")
|
||||
.on_update(move |number_input: &NumberInput| {
|
||||
PropertiesPanelMessage::ModifyStroke {
|
||||
stroke: internal_stroke5.clone().with_line_join_miter_limit(number_input.value.unwrap()),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_holder(),
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
trait DAffine2Utils {
|
||||
fn scale_x(&self) -> f64;
|
||||
fn update_scale_x(self, new_width: f64) -> Self;
|
||||
fn scale_y(&self) -> f64;
|
||||
fn update_scale_y(self, new_height: f64) -> Self;
|
||||
fn x(&self) -> f64;
|
||||
fn update_x(self, new_x: f64) -> Self;
|
||||
fn y(&self) -> f64;
|
||||
fn update_y(self, new_y: f64) -> Self;
|
||||
fn rotation(&self) -> f64;
|
||||
fn update_rotation(self, new_rotation: f64) -> Self;
|
||||
}
|
||||
|
||||
impl DAffine2Utils for DAffine2 {
|
||||
fn scale_x(&self) -> f64 {
|
||||
self.transform_vector2((1., 0.).into()).length()
|
||||
}
|
||||
|
||||
fn update_scale_x(self, new_width: f64) -> Self {
|
||||
let scale_x = self.scale_x();
|
||||
if scale_x != 0. {
|
||||
self * DAffine2::from_scale((new_width / scale_x, 1.).into())
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn scale_y(&self) -> f64 {
|
||||
self.transform_vector2((0., 1.).into()).length()
|
||||
}
|
||||
|
||||
fn update_scale_y(self, new_height: f64) -> Self {
|
||||
let scale_y = self.scale_y();
|
||||
if scale_y != 0. {
|
||||
self * DAffine2::from_scale((1., new_height / scale_y).into())
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn x(&self) -> f64 {
|
||||
self.translation.x
|
||||
}
|
||||
|
||||
fn update_x(mut self, new_x: f64) -> Self {
|
||||
self.translation.x = new_x;
|
||||
self
|
||||
}
|
||||
|
||||
fn y(&self) -> f64 {
|
||||
self.translation.y
|
||||
}
|
||||
|
||||
fn update_y(mut self, new_y: f64) -> Self {
|
||||
self.translation.y = new_y;
|
||||
self
|
||||
}
|
||||
|
||||
fn rotation(&self) -> f64 {
|
||||
if self.scale_x() != 0. {
|
||||
let cos = self.matrix2.col(0).x / self.scale_x();
|
||||
let sin = self.matrix2.col(0).y / self.scale_x();
|
||||
sin.atan2(cos)
|
||||
} else if self.scale_y() != 0. {
|
||||
let sin = -self.matrix2.col(1).x / self.scale_y();
|
||||
let cos = self.matrix2.col(1).y / self.scale_y();
|
||||
sin.atan2(cos)
|
||||
} else {
|
||||
// Rotation information does not exists anymore in the matrix
|
||||
// return 0 for user experience.
|
||||
0.
|
||||
}
|
||||
}
|
||||
|
||||
fn update_rotation(self, new_rotation: f64) -> Self {
|
||||
let width = self.scale_x();
|
||||
let height = self.scale_y();
|
||||
let half_width = width / 2.;
|
||||
let half_height = height / 2.;
|
||||
|
||||
let angle_translation_offset = |angle: f64| DVec2::new(-half_width * angle.cos() + half_height * angle.sin(), -half_width * angle.sin() - half_height * angle.cos());
|
||||
let angle_translation_adjustment = angle_translation_offset(new_rotation) - angle_translation_offset(self.rotation());
|
||||
|
||||
DAffine2::from_scale_angle_translation((width, height).into(), new_rotation, self.translation + angle_translation_adjustment)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
use document_legacy::document::Document as DocumentLegacy;
|
||||
use document_legacy::LayerId;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use document_legacy::document::LayerId;
|
||||
|
||||
use crate::{messages::prelude::NodeGraphMessageHandler, node_graph_executor::NodeGraphExecutor};
|
||||
|
||||
@@ -12,14 +10,3 @@ pub struct PropertiesPanelMessageHandlerData<'a> {
|
||||
pub node_graph_message_handler: &'a NodeGraphMessageHandler,
|
||||
pub executor: &'a mut NodeGraphExecutor,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Copy, Debug, Serialize, Deserialize, specta::Type)]
|
||||
pub enum TransformOp {
|
||||
X,
|
||||
Y,
|
||||
ScaleX,
|
||||
ScaleY,
|
||||
Width,
|
||||
Height,
|
||||
Rotation,
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use document_legacy::layers::layer_info::{LayerData, LayerDataTypeDiscriminant, LegacyLayer};
|
||||
use document_legacy::layers::style::RenderData;
|
||||
use document_legacy::LayerId;
|
||||
use document_legacy::document::LayerId;
|
||||
use document_legacy::layers::layer_info::{LayerDataTypeDiscriminant, LegacyLayer};
|
||||
|
||||
use glam::{DAffine2, DVec2};
|
||||
use serde::ser::SerializeStruct;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -60,45 +58,15 @@ pub struct LayerPanelEntry {
|
||||
impl LayerPanelEntry {
|
||||
// TODO: Deprecate this because it's using document-legacy layer data which is no longer linked to data from the node graph,
|
||||
// TODO: so this doesn't feed `name` (that's fed elsewhere) or `visible` (that's broken entirely), etc.
|
||||
pub fn new(layer_metadata: &LayerMetadata, transform: DAffine2, layer: &LegacyLayer, path: Vec<LayerId>, render_data: &RenderData) -> Self {
|
||||
let name = layer.name.clone().unwrap_or_else(|| String::from(""));
|
||||
|
||||
let mut tooltip = name.clone();
|
||||
if cfg!(debug_assertions) {
|
||||
tooltip += "\nLayer Path: ";
|
||||
tooltip += &path.iter().map(|id| id.to_string()).collect::<Vec<_>>().join(" / ");
|
||||
tooltip = tooltip.trim().to_string();
|
||||
}
|
||||
|
||||
let arr = layer.data.bounding_box(transform, render_data).unwrap_or([DVec2::ZERO, DVec2::ZERO]);
|
||||
let arr = arr.iter().map(|x| (*x).into()).collect::<Vec<(f64, f64)>>();
|
||||
let mut thumbnail = String::new();
|
||||
let mut svg_defs = String::new();
|
||||
layer.data.clone().render(&mut thumbnail, &mut svg_defs, &mut vec![transform], render_data);
|
||||
let transform = transform.to_cols_array().iter().map(ToString::to_string).collect::<Vec<_>>().join(",");
|
||||
let thumbnail = if let [(x_min, y_min), (x_max, y_max)] = arr.as_slice() {
|
||||
format!(
|
||||
r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="{} {} {} {}"><defs>{}</defs><g transform="matrix({})">{}</g></svg>"#,
|
||||
x_min,
|
||||
y_min,
|
||||
x_max - x_min,
|
||||
y_max - y_min,
|
||||
svg_defs,
|
||||
transform,
|
||||
thumbnail,
|
||||
)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
LayerPanelEntry {
|
||||
name,
|
||||
tooltip,
|
||||
pub fn new(layer_metadata: &LayerMetadata, layer: &LegacyLayer, path: Vec<LayerId>) -> Self {
|
||||
Self {
|
||||
name: "".to_string(), // Replaced before it gets used
|
||||
tooltip: "".to_string(), // Replaced before it gets used
|
||||
visible: layer.visible,
|
||||
layer_type: (&layer.data).into(),
|
||||
layer_metadata: *layer_metadata,
|
||||
path,
|
||||
thumbnail,
|
||||
thumbnail: r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 0 0"></svg>"#.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
pub use super::layer_panel::{LayerMetadata, LayerPanelEntry};
|
||||
use document_legacy::document::Document as DocumentLegacy;
|
||||
use document_legacy::LayerId;
|
||||
use document_legacy::document::LayerId;
|
||||
use graphene_core::raster::color::Color;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use crate::messages::frontend::utility_types::{ExportBounds, FileType};
|
||||
use crate::messages::portfolio::document::utility_types::clipboards::Clipboard;
|
||||
use crate::messages::prelude::*;
|
||||
use document_legacy::document::LayerId;
|
||||
use document_legacy::document_metadata::LayerNodeIdentifier;
|
||||
use document_legacy::LayerId;
|
||||
use graph_craft::document::NodeId;
|
||||
use graphene_core::text::Font;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -103,13 +102,6 @@ pub enum PortfolioMessage {
|
||||
SetActiveDocument {
|
||||
document_id: u64,
|
||||
},
|
||||
SetImageBlobUrl {
|
||||
document_id: u64,
|
||||
layer_path: Vec<LayerId>,
|
||||
node_id: Option<NodeId>,
|
||||
blob_url: String,
|
||||
resolution: (f64, f64),
|
||||
},
|
||||
SubmitDocumentExport {
|
||||
file_name: String,
|
||||
file_type: FileType,
|
||||
|
||||
@@ -11,7 +11,6 @@ use crate::messages::prelude::*;
|
||||
use crate::messages::tool::utility_types::{HintData, HintGroup};
|
||||
use crate::node_graph_executor::{ExportConfig, NodeGraphExecutor};
|
||||
|
||||
use document_legacy::layers::style::RenderData;
|
||||
use graph_craft::document::NodeId;
|
||||
use graphene_core::text::Font;
|
||||
|
||||
@@ -105,12 +104,11 @@ impl MessageHandler<PortfolioMessage, (&InputPreprocessorMessageHandler, &Prefer
|
||||
}
|
||||
PortfolioMessage::CloseAllDocuments => {
|
||||
if self.active_document_id.is_some() {
|
||||
responses.add(PropertiesPanelMessage::Deactivate);
|
||||
responses.add(BroadcastEvent::ToolAbort);
|
||||
responses.add(ToolMessage::DeactivateTools);
|
||||
|
||||
// Clear relevant UI layouts if there are no documents
|
||||
responses.add(PropertiesPanelMessage::ClearSelection);
|
||||
responses.add(PropertiesPanelMessage::Clear);
|
||||
responses.add(DocumentMessage::ClearLayerTree);
|
||||
let hint_data = HintData(vec![HintGroup(vec![])]);
|
||||
responses.add(FrontendMessage::UpdateInputHints { hint_data });
|
||||
@@ -134,7 +132,7 @@ impl MessageHandler<PortfolioMessage, (&InputPreprocessorMessageHandler, &Prefer
|
||||
// Is this the last document?
|
||||
if self.documents.len() == 1 && self.document_ids[0] == document_id {
|
||||
// Clear UI layouts that assume the existence of a document
|
||||
responses.add(PropertiesPanelMessage::ClearSelection);
|
||||
responses.add(PropertiesPanelMessage::Clear);
|
||||
responses.add(DocumentMessage::ClearLayerTree);
|
||||
let hint_data = HintData(vec![HintGroup(vec![])]);
|
||||
responses.add(FrontendMessage::UpdateInputHints { hint_data });
|
||||
@@ -306,12 +304,12 @@ impl MessageHandler<PortfolioMessage, (&InputPreprocessorMessageHandler, &Prefer
|
||||
})
|
||||
}
|
||||
if &server_status != self.persistent_data.imaginate.server_status() {
|
||||
responses.add(PropertiesPanelMessage::ResendActiveProperties);
|
||||
responses.add(PropertiesPanelMessage::Refresh);
|
||||
}
|
||||
}
|
||||
PortfolioMessage::ImaginatePollServerStatus => {
|
||||
self.persistent_data.imaginate.poll_server_check();
|
||||
responses.add(PropertiesPanelMessage::ResendActiveProperties);
|
||||
responses.add(PropertiesPanelMessage::Refresh);
|
||||
}
|
||||
PortfolioMessage::ImaginatePreferences => self.executor.update_imaginate_preferences(preferences.get_imaginate_preferences()),
|
||||
PortfolioMessage::ImaginateServerHostname => {
|
||||
@@ -492,25 +490,6 @@ impl MessageHandler<PortfolioMessage, (&InputPreprocessorMessageHandler, &Prefer
|
||||
self.active_document_id = Some(document_id);
|
||||
responses.add(MenuBarMessage::SendLayout);
|
||||
}
|
||||
PortfolioMessage::SetImageBlobUrl {
|
||||
document_id,
|
||||
layer_path,
|
||||
node_id,
|
||||
blob_url,
|
||||
resolution,
|
||||
} => {
|
||||
if let Some(node_id) = node_id {
|
||||
self.executor.insert_thumbnail_blob_url(blob_url, node_id, responses);
|
||||
return;
|
||||
}
|
||||
let message = DocumentMessage::SetImageBlobUrl {
|
||||
layer_path,
|
||||
blob_url,
|
||||
resolution,
|
||||
document_id,
|
||||
};
|
||||
responses.add(PortfolioMessage::DocumentPassMessage { document_id, message });
|
||||
}
|
||||
PortfolioMessage::SubmitDocumentExport {
|
||||
file_name,
|
||||
file_type,
|
||||
@@ -668,15 +647,13 @@ impl PortfolioMessageHandler {
|
||||
|
||||
// TODO: Fix how this doesn't preserve tab order upon loading new document from *File > Load*
|
||||
fn load_document(&mut self, new_document: DocumentMessageHandler, document_id: u64, responses: &mut VecDeque<Message>) {
|
||||
let render_data = RenderData::new(&self.persistent_data.font_cache, new_document.view_mode, None);
|
||||
|
||||
self.document_ids.push(document_id);
|
||||
|
||||
responses.extend(
|
||||
new_document
|
||||
.layer_metadata
|
||||
.keys()
|
||||
.filter_map(|path| new_document.layer_panel_entry_from_path(path, &render_data))
|
||||
.filter_map(|path| new_document.layer_panel_entry_from_path(path))
|
||||
.map(|entry| FrontendMessage::UpdateDocumentLayerDetails { data: entry }.into())
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
@@ -685,7 +662,6 @@ impl PortfolioMessageHandler {
|
||||
self.documents.insert(document_id, new_document);
|
||||
|
||||
if self.active_document().is_some() {
|
||||
responses.add(PropertiesPanelMessage::Deactivate);
|
||||
responses.add(BroadcastEvent::ToolAbort);
|
||||
responses.add(ToolMessage::DeactivateTools);
|
||||
}
|
||||
@@ -696,12 +672,10 @@ impl PortfolioMessageHandler {
|
||||
responses.add(PortfolioMessage::UpdateDocumentWidgets);
|
||||
responses.add(PortfolioMessage::GraphViewOverlay { open: self.graph_view_overlay_open });
|
||||
responses.add(ToolMessage::InitTools);
|
||||
responses.add(PropertiesPanelMessage::Init);
|
||||
responses.add(NodeGraphMessage::Init);
|
||||
responses.add(NavigationMessage::TranslateCanvas { delta: (0., 0.).into() });
|
||||
responses.add(DocumentMessage::DocumentStructureChanged);
|
||||
responses.add(PropertiesPanelMessage::ClearSelection);
|
||||
responses.add(PropertiesPanelMessage::UpdateSelectedDocumentProperties);
|
||||
responses.add(PropertiesPanelMessage::Clear);
|
||||
responses.add(NodeGraphMessage::UpdateNewNodeGraph);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::messages::portfolio::document::node_graph::VectorDataModification;
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
use bezier_rs::{ManipulatorGroup, Subpath};
|
||||
use document_legacy::{document::Document, document_metadata::LayerNodeIdentifier, LayerId, Operation};
|
||||
use document_legacy::{document::Document, document_metadata::LayerNodeIdentifier};
|
||||
use graph_craft::document::{value::TaggedValue, DocumentNode, NodeId, NodeInput, NodeNetwork};
|
||||
use graphene_core::raster::{BlendMode, ImageFrame};
|
||||
use graphene_core::text::Font;
|
||||
@@ -10,7 +10,7 @@ use graphene_core::uuid::ManipulatorGroupId;
|
||||
use graphene_core::vector::style::{FillType, Gradient};
|
||||
use graphene_core::Color;
|
||||
|
||||
use glam::{DAffine2, DVec2};
|
||||
use glam::DVec2;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
/// Create a new vector layer from a vector of [`bezier_rs::Subpath`].
|
||||
@@ -34,18 +34,6 @@ pub fn new_image_layer(image_frame: ImageFrame<Color>, id: NodeId, parent: Layer
|
||||
LayerNodeIdentifier::new_unchecked(id)
|
||||
}
|
||||
|
||||
/// Create a legacy node graph frame TODO: remove
|
||||
pub fn new_custom_layer(network: NodeNetwork, layer_path: Vec<LayerId>, responses: &mut VecDeque<Message>) {
|
||||
responses.add(DocumentMessage::DeselectAllLayers);
|
||||
responses.add(Operation::AddFrame {
|
||||
path: layer_path.clone(),
|
||||
insert_index: -1,
|
||||
transform: DAffine2::ZERO.to_cols_array(),
|
||||
network,
|
||||
});
|
||||
responses.add(DocumentMessage::InputFrameRasterizeRegionBelowLayer { layer_path });
|
||||
}
|
||||
|
||||
/// Batch set all of the manipulator groups to mirror on a specific layer
|
||||
pub fn set_manipulator_mirror_angle(manipulator_groups: &[ManipulatorGroup<ManipulatorGroupId>], layer: LayerNodeIdentifier, mirror_angle: bool, responses: &mut VecDeque<Message>) {
|
||||
for manipulator_group in manipulator_groups {
|
||||
@@ -77,12 +65,6 @@ pub fn get_pivot(layer: LayerNodeIdentifier, document: &Document) -> Option<DVec
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_document_pivot(layer: LayerNodeIdentifier, document: &Document) -> DVec2 {
|
||||
let [min, max] = document.metadata.nonzero_bounding_box(layer);
|
||||
let pivot = get_pivot(layer, document).unwrap_or(DVec2::splat(0.5));
|
||||
document.metadata.transform_to_document(layer).transform_point2(min + (max - min) * pivot)
|
||||
}
|
||||
|
||||
pub fn get_viewport_pivot(layer: LayerNodeIdentifier, document: &Document) -> DVec2 {
|
||||
let [min, max] = document.metadata.nonzero_bounding_box(layer);
|
||||
let pivot = get_pivot(layer, document).unwrap_or(DVec2::splat(0.5));
|
||||
@@ -234,25 +216,6 @@ impl<'a> NodeGraphLayer<'a> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the nearest layer node from the path and the document
|
||||
pub fn new_from_path(layer: &[LayerId], document: &'a document_legacy::document::Document) -> Option<Self> {
|
||||
let node_graph = &document.document_network;
|
||||
let outwards_links = document.document_network.collect_outwards_links();
|
||||
|
||||
let Some(mut layer_node) = layer.last().copied() else {
|
||||
error!("Tried to modify root layer");
|
||||
return None;
|
||||
};
|
||||
while !node_graph.nodes.get(&layer_node)?.is_layer() {
|
||||
layer_node = outwards_links.get(&layer_node)?.first().copied()?;
|
||||
}
|
||||
Some(Self {
|
||||
node_graph,
|
||||
_outwards_links: outwards_links,
|
||||
layer_node,
|
||||
})
|
||||
}
|
||||
|
||||
/// Return an iterator up the primary flow of the layer
|
||||
pub fn primary_layer_flow(&self) -> impl Iterator<Item = (&'a DocumentNode, u64)> {
|
||||
self.node_graph.upstream_flow_back_from_nodes(vec![self.layer_node], true)
|
||||
|
||||
@@ -4,7 +4,6 @@ use crate::messages::prelude::*;
|
||||
use crate::messages::tool::common_functionality::snapping::SnapManager;
|
||||
|
||||
use document_legacy::document_metadata::LayerNodeIdentifier;
|
||||
use document_legacy::layers::style::RenderData;
|
||||
|
||||
use glam::{DAffine2, DVec2, Vec2Swizzles};
|
||||
|
||||
@@ -17,16 +16,16 @@ pub struct Resize {
|
||||
|
||||
impl Resize {
|
||||
/// Starts a resize, assigning the snap targets and snapping the starting position.
|
||||
pub fn start(&mut self, responses: &mut VecDeque<Message>, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, render_data: &RenderData) {
|
||||
self.snap_manager.start_snap(document, input, document.bounding_boxes(None, None, render_data), true, true);
|
||||
pub fn start(&mut self, responses: &mut VecDeque<Message>, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler) {
|
||||
self.snap_manager.start_snap(document, input, document.bounding_boxes(), true, true);
|
||||
self.snap_manager.add_all_document_handles(document, input, &[], &[], &[]);
|
||||
let root_transform = document.metadata().document_to_viewport;
|
||||
self.drag_start = root_transform.inverse().transform_point2(self.snap_manager.snap_position(responses, document, input.mouse.position));
|
||||
}
|
||||
|
||||
/// Recalculates snap targets without snapping the starting position.
|
||||
pub fn recalculate_snaps(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, render_data: &RenderData) {
|
||||
self.snap_manager.start_snap(document, input, document.bounding_boxes(None, None, render_data), true, true);
|
||||
pub fn recalculate_snaps(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler) {
|
||||
self.snap_manager.start_snap(document, input, document.bounding_boxes(), true, true);
|
||||
self.snap_manager.add_all_document_handles(document, input, &[], &[], &[]);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,8 @@ use super::shape_editor::ManipulatorPointInfo;
|
||||
use crate::consts::{SNAP_AXIS_TOLERANCE, SNAP_POINT_TOLERANCE};
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
use document_legacy::document_metadata::LayerNodeIdentifier;
|
||||
use document_legacy::document::LayerId;
|
||||
use document_legacy::layers::layer_info::LegacyLayer;
|
||||
use document_legacy::LayerId;
|
||||
use graphene_core::vector::{ManipulatorPointId, SelectedType};
|
||||
|
||||
use glam::DVec2;
|
||||
|
||||
@@ -109,42 +107,44 @@ impl SnapManager {
|
||||
/// This should be called after start_snap
|
||||
pub fn add_snap_path(
|
||||
&mut self,
|
||||
document_message_handler: &DocumentMessageHandler,
|
||||
input: &InputPreprocessorMessageHandler,
|
||||
layer: &LegacyLayer,
|
||||
path: &[LayerId],
|
||||
include_handles: bool,
|
||||
ignore_points: &[ManipulatorPointInfo],
|
||||
_document_message_handler: &DocumentMessageHandler,
|
||||
_input: &InputPreprocessorMessageHandler,
|
||||
_layer: &LegacyLayer,
|
||||
_path: &[LayerId],
|
||||
_include_handles: bool,
|
||||
_ignore_points: &[ManipulatorPointInfo],
|
||||
) {
|
||||
let Some(vector_data) = &layer.as_vector_data() else { return };
|
||||
todo!();
|
||||
|
||||
if !document_message_handler.snapping_state.node_snapping {
|
||||
return;
|
||||
};
|
||||
// let Some(vector_data) = &layer.as_vector_data() else { return };
|
||||
|
||||
let transform = document_message_handler.document_legacy.multiply_transforms(path).unwrap();
|
||||
let snap_points = vector_data
|
||||
.manipulator_groups()
|
||||
.flat_map(|group| {
|
||||
if include_handles {
|
||||
[
|
||||
Some((ManipulatorPointId::new(group.id, SelectedType::Anchor), group.anchor)),
|
||||
group.in_handle.map(|pos| (ManipulatorPointId::new(group.id, SelectedType::InHandle), pos)),
|
||||
group.out_handle.map(|pos| (ManipulatorPointId::new(group.id, SelectedType::OutHandle), pos)),
|
||||
]
|
||||
} else {
|
||||
[Some((ManipulatorPointId::new(group.id, SelectedType::Anchor), group.anchor)), None, None]
|
||||
}
|
||||
})
|
||||
.flatten()
|
||||
.filter(|&(point_id, _)| {
|
||||
!ignore_points.contains(&ManipulatorPointInfo {
|
||||
layer: LayerNodeIdentifier::from_path(path, document_message_handler.network()),
|
||||
point_id,
|
||||
})
|
||||
})
|
||||
.map(|(_, pos)| transform.transform_point2(pos));
|
||||
self.add_snap_points(document_message_handler, input, snap_points);
|
||||
// if !document_message_handler.snapping_state.node_snapping {
|
||||
// return;
|
||||
// };
|
||||
|
||||
// let transform = document_message_handler.document_legacy.multiply_transforms(path).unwrap();
|
||||
// let snap_points = vector_data
|
||||
// .manipulator_groups()
|
||||
// .flat_map(|group| {
|
||||
// if include_handles {
|
||||
// [
|
||||
// Some((ManipulatorPointId::new(group.id, SelectedType::Anchor), group.anchor)),
|
||||
// group.in_handle.map(|pos| (ManipulatorPointId::new(group.id, SelectedType::InHandle), pos)),
|
||||
// group.out_handle.map(|pos| (ManipulatorPointId::new(group.id, SelectedType::OutHandle), pos)),
|
||||
// ]
|
||||
// } else {
|
||||
// [Some((ManipulatorPointId::new(group.id, SelectedType::Anchor), group.anchor)), None, None]
|
||||
// }
|
||||
// })
|
||||
// .flatten()
|
||||
// .filter(|&(point_id, _)| {
|
||||
// !ignore_points.contains(&ManipulatorPointInfo {
|
||||
// layer: LayerNodeIdentifier::from_path(path, document_message_handler.network()),
|
||||
// point_id,
|
||||
// })
|
||||
// })
|
||||
// .map(|(_, pos)| transform.transform_point2(pos));
|
||||
// self.add_snap_points(document_message_handler, input, snap_points);
|
||||
}
|
||||
|
||||
/// Adds all of the shape handles in the document, including bézier handles of the points specified
|
||||
|
||||
@@ -7,7 +7,6 @@ use crate::messages::prelude::*;
|
||||
use crate::messages::tool::utility_types::ToolType;
|
||||
use crate::node_graph_executor::NodeGraphExecutor;
|
||||
|
||||
use document_legacy::layers::style::RenderData;
|
||||
use graphene_core::raster::color::Color;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
@@ -25,7 +24,7 @@ impl MessageHandler<ToolMessage, (&DocumentMessageHandler, u64, &InputPreprocess
|
||||
responses: &mut VecDeque<Message>,
|
||||
(document, document_id, input, persistent_data, node_graph): (&DocumentMessageHandler, u64, &InputPreprocessorMessageHandler, &PersistentData, &NodeGraphExecutor),
|
||||
) {
|
||||
let render_data = RenderData::new(&persistent_data.font_cache, document.view_mode, None);
|
||||
let font_cache = &persistent_data.font_cache;
|
||||
|
||||
#[remain::sorted]
|
||||
match message {
|
||||
@@ -89,7 +88,7 @@ impl MessageHandler<ToolMessage, (&DocumentMessageHandler, u64, &InputPreprocess
|
||||
document_id,
|
||||
global_tool_data: &self.tool_state.document_tool_data,
|
||||
input,
|
||||
render_data: &render_data,
|
||||
font_cache,
|
||||
shape_editor: &mut self.shape_editor,
|
||||
node_graph,
|
||||
};
|
||||
@@ -171,7 +170,7 @@ impl MessageHandler<ToolMessage, (&DocumentMessageHandler, u64, &InputPreprocess
|
||||
document_id,
|
||||
global_tool_data: &self.tool_state.document_tool_data,
|
||||
input,
|
||||
render_data: &render_data,
|
||||
font_cache,
|
||||
shape_editor: &mut self.shape_editor,
|
||||
node_graph,
|
||||
};
|
||||
@@ -241,7 +240,7 @@ impl MessageHandler<ToolMessage, (&DocumentMessageHandler, u64, &InputPreprocess
|
||||
document_id,
|
||||
global_tool_data: &self.tool_state.document_tool_data,
|
||||
input,
|
||||
render_data: &render_data,
|
||||
font_cache,
|
||||
shape_editor: &mut self.shape_editor,
|
||||
node_graph,
|
||||
};
|
||||
|
||||
@@ -6,7 +6,6 @@ use crate::messages::tool::common_functionality::snapping::SnapManager;
|
||||
use crate::messages::tool::common_functionality::transformation_cage::*;
|
||||
|
||||
use document_legacy::document_metadata::LayerNodeIdentifier;
|
||||
use document_legacy::layers::RenderData;
|
||||
|
||||
use glam::{IVec2, Vec2Swizzles};
|
||||
|
||||
@@ -117,13 +116,11 @@ impl ArtboardToolData {
|
||||
Some(edges)
|
||||
}
|
||||
|
||||
fn start_resizing(&mut self, selected_edges: (bool, bool, bool, bool), document: &DocumentMessageHandler, render_data: &RenderData, input: &InputPreprocessorMessageHandler) {
|
||||
fn start_resizing(&mut self, selected_edges: (bool, bool, bool, bool), document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler) {
|
||||
let snap_x = selected_edges.2 || selected_edges.3;
|
||||
let snap_y = selected_edges.0 || selected_edges.1;
|
||||
|
||||
let artboard = self.selected_artboard.unwrap();
|
||||
self.snap_manager
|
||||
.start_snap(document, input, document.bounding_boxes(None, Some(artboard.to_node()), render_data), snap_x, snap_y);
|
||||
self.snap_manager.start_snap(document, input, document.bounding_boxes(), snap_x, snap_y);
|
||||
self.snap_manager.add_all_document_handles(document, input, &[], &[], &[]);
|
||||
|
||||
if let Some(bounds) = &mut self.bounding_box_manager {
|
||||
@@ -131,7 +128,7 @@ impl ArtboardToolData {
|
||||
}
|
||||
}
|
||||
|
||||
fn select_artboard(&mut self, document: &DocumentMessageHandler, render_data: &RenderData, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) -> bool {
|
||||
fn select_artboard(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) -> bool {
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
|
||||
let mut intersections = document
|
||||
@@ -143,15 +140,14 @@ impl ArtboardToolData {
|
||||
if let Some(intersection) = intersections.next() {
|
||||
self.selected_artboard = Some(intersection);
|
||||
|
||||
self.snap_manager
|
||||
.start_snap(document, input, document.bounding_boxes(None, Some(intersection.to_node()), render_data), true, true);
|
||||
self.snap_manager.start_snap(document, input, document.bounding_boxes(), true, true);
|
||||
self.snap_manager.add_all_document_handles(document, input, &[], &[], &[]);
|
||||
|
||||
true
|
||||
} else {
|
||||
self.selected_artboard = None;
|
||||
|
||||
responses.add(PropertiesPanelMessage::ClearSelection);
|
||||
responses.add(PropertiesPanelMessage::Clear);
|
||||
|
||||
false
|
||||
}
|
||||
@@ -183,7 +179,7 @@ impl Fsm for ArtboardToolFsmState {
|
||||
type ToolOptions = ();
|
||||
|
||||
fn transition(self, event: ToolMessage, tool_data: &mut Self::ToolData, tool_action_data: &mut ToolActionHandlerData, _tool_options: &(), responses: &mut VecDeque<Message>) -> Self {
|
||||
let ToolActionHandlerData { document, input, render_data, .. } = tool_action_data;
|
||||
let ToolActionHandlerData { document, input, .. } = tool_action_data;
|
||||
|
||||
let ToolMessage::Artboard(event) = event else {
|
||||
return self;
|
||||
@@ -210,10 +206,10 @@ impl Fsm for ArtboardToolFsmState {
|
||||
|
||||
if let Some(selected_edges) = tool_data.check_dragging_bounds(input.mouse.position) {
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
tool_data.start_resizing(selected_edges, document, render_data, input);
|
||||
tool_data.start_resizing(selected_edges, document, input);
|
||||
|
||||
ArtboardToolFsmState::ResizingBounds
|
||||
} else if tool_data.select_artboard(document, render_data, input, responses) {
|
||||
} else if tool_data.select_artboard(document, input, responses) {
|
||||
ArtboardToolFsmState::Dragging
|
||||
} else {
|
||||
ArtboardToolFsmState::Drawing
|
||||
@@ -284,7 +280,7 @@ impl Fsm for ArtboardToolFsmState {
|
||||
let id = generate_uuid();
|
||||
tool_data.selected_artboard = Some(LayerNodeIdentifier::new_unchecked(id));
|
||||
|
||||
tool_data.snap_manager.start_snap(document, input, document.bounding_boxes(None, Some(id), render_data), true, true);
|
||||
tool_data.snap_manager.start_snap(document, input, document.bounding_boxes(), true, true);
|
||||
tool_data.snap_manager.add_all_document_handles(document, input, &[], &[], &[]);
|
||||
|
||||
responses.add(GraphOperationMessage::NewArtboard {
|
||||
@@ -367,11 +363,6 @@ impl Fsm for ArtboardToolFsmState {
|
||||
ArtboardToolFsmState::Ready
|
||||
}
|
||||
(_, ArtboardToolMessage::Abort) => {
|
||||
// Register properties when switching back to other tools
|
||||
responses.add(PropertiesPanelMessage::SetActiveLayers {
|
||||
paths: document.selected_layers().map(|path| path.to_vec()).collect(),
|
||||
});
|
||||
|
||||
tool_data.snap_manager.cleanup(responses);
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
ArtboardToolFsmState::Ready
|
||||
|
||||
@@ -185,11 +185,7 @@ impl Fsm for EllipseToolFsmState {
|
||||
|
||||
fn transition(self, event: ToolMessage, tool_data: &mut Self::ToolData, tool_action_data: &mut ToolActionHandlerData, tool_options: &Self::ToolOptions, responses: &mut VecDeque<Message>) -> Self {
|
||||
let ToolActionHandlerData {
|
||||
document,
|
||||
global_tool_data,
|
||||
input,
|
||||
render_data,
|
||||
..
|
||||
document, global_tool_data, input, ..
|
||||
} = tool_action_data;
|
||||
|
||||
let shape_data = &mut tool_data.data;
|
||||
@@ -199,11 +195,11 @@ impl Fsm for EllipseToolFsmState {
|
||||
};
|
||||
match (self, event) {
|
||||
(EllipseToolFsmState::Drawing, EllipseToolMessage::CanvasTransformed) => {
|
||||
tool_data.data.recalculate_snaps(document, input, render_data);
|
||||
tool_data.data.recalculate_snaps(document, input);
|
||||
self
|
||||
}
|
||||
(EllipseToolFsmState::Ready, EllipseToolMessage::DragStart) => {
|
||||
shape_data.start(responses, document, input, render_data);
|
||||
shape_data.start(responses, document, input);
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
|
||||
// Create a new ellipse vector shape
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::tool_prelude::*;
|
||||
use document_legacy::layers::style::Fill;
|
||||
|
||||
use graphene_core::vector::style::Fill;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FillTool {
|
||||
@@ -80,9 +81,6 @@ impl Fsm for FillToolFsmState {
|
||||
let fill = Fill::Solid(color);
|
||||
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
responses.add(DocumentMessage::SetSelectedLayers {
|
||||
replacement_selected_layers: vec![layer.clone()],
|
||||
});
|
||||
responses.add(GraphOperationMessage::FillSet { layer, fill });
|
||||
responses.add(DocumentMessage::CommitTransaction);
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ use crate::messages::tool::common_functionality::graph_modification_utils::get_g
|
||||
use crate::messages::tool::common_functionality::snapping::SnapManager;
|
||||
|
||||
use document_legacy::document_metadata::LayerNodeIdentifier;
|
||||
use document_legacy::layers::style::{Fill, Gradient, GradientType, RenderData};
|
||||
use graphene_core::raster::color::Color;
|
||||
use graphene_core::vector::style::{Fill, Gradient, GradientType};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct GradientTool {
|
||||
@@ -267,8 +267,8 @@ struct GradientToolData {
|
||||
drag_start: DVec2,
|
||||
}
|
||||
|
||||
pub fn start_snap(snap_manager: &mut SnapManager, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, render_data: &RenderData) {
|
||||
snap_manager.start_snap(document, input, document.bounding_boxes(None, None, render_data), true, true);
|
||||
pub fn start_snap(snap_manager: &mut SnapManager, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler) {
|
||||
snap_manager.start_snap(document, input, document.bounding_boxes(), true, true);
|
||||
snap_manager.add_all_document_handles(document, input, &[], &[], &[]);
|
||||
}
|
||||
|
||||
@@ -278,11 +278,7 @@ impl Fsm for GradientToolFsmState {
|
||||
|
||||
fn transition(self, event: ToolMessage, tool_data: &mut Self::ToolData, tool_action_data: &mut ToolActionHandlerData, tool_options: &Self::ToolOptions, responses: &mut VecDeque<Message>) -> Self {
|
||||
let ToolActionHandlerData {
|
||||
document,
|
||||
global_tool_data,
|
||||
input,
|
||||
render_data,
|
||||
..
|
||||
document, global_tool_data, input, ..
|
||||
} = tool_action_data;
|
||||
|
||||
let ToolMessage::Gradient(event) = event else {
|
||||
@@ -434,7 +430,7 @@ impl Fsm for GradientToolFsmState {
|
||||
let pos = transform.transform_point2(pos);
|
||||
if pos.distance_squared(mouse) < tolerance {
|
||||
dragging = true;
|
||||
start_snap(&mut tool_data.snap_manager, document, input, render_data);
|
||||
start_snap(&mut tool_data.snap_manager, document, input);
|
||||
tool_data.selected_gradient = Some(SelectedGradient {
|
||||
layer,
|
||||
transform,
|
||||
@@ -479,7 +475,7 @@ impl Fsm for GradientToolFsmState {
|
||||
|
||||
tool_data.selected_gradient = Some(selected_gradient);
|
||||
|
||||
start_snap(&mut tool_data.snap_manager, document, input, render_data);
|
||||
start_snap(&mut tool_data.snap_manager, document, input);
|
||||
|
||||
GradientToolFsmState::Drawing
|
||||
} else {
|
||||
|
||||
@@ -3,9 +3,7 @@ use crate::messages::portfolio::document::node_graph::{self, IMAGINATE_NODE};
|
||||
use crate::messages::tool::common_functionality::resize::Resize;
|
||||
|
||||
use document_legacy::document_metadata::LayerNodeIdentifier;
|
||||
use document_legacy::Operation;
|
||||
|
||||
use glam::DAffine2;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -105,7 +103,7 @@ impl Fsm for ImaginateToolFsmState {
|
||||
self,
|
||||
event: ToolMessage,
|
||||
tool_data: &mut Self::ToolData,
|
||||
ToolActionHandlerData { document, input, render_data, .. }: &mut ToolActionHandlerData,
|
||||
ToolActionHandlerData { document, input, .. }: &mut ToolActionHandlerData,
|
||||
_tool_options: &Self::ToolOptions,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) -> Self {
|
||||
@@ -116,12 +114,12 @@ impl Fsm for ImaginateToolFsmState {
|
||||
};
|
||||
match (self, event) {
|
||||
(_, ImaginateToolMessage::DocumentIsDirty | ImaginateToolMessage::SelectionChanged) => {
|
||||
//tool_data.path_outlines.update_selected(document.document_legacy.selected_visible_layers(), document, responses, render_data);
|
||||
//tool_data.path_outlines.update_selected(document.document_legacy.selected_visible_layers(), document, responses, font_cache);
|
||||
|
||||
self
|
||||
}
|
||||
(ImaginateToolFsmState::Ready, ImaginateToolMessage::DragStart) => {
|
||||
shape_data.start(responses, document, input, render_data);
|
||||
shape_data.start(responses, document, input);
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
shape_data.layer = Some(LayerNodeIdentifier::new(generate_uuid(), document.network()));
|
||||
responses.add(DocumentMessage::DeselectAllLayers);
|
||||
@@ -156,16 +154,16 @@ impl Fsm for ImaginateToolFsmState {
|
||||
imaginate_node_id,
|
||||
imaginate_node_type.to_document_node_default_inputs([Some(graph_craft::document::NodeInput::node(transform_node_id, 0))], next_pos()),
|
||||
);
|
||||
|
||||
// Add a layer with a frame to the document
|
||||
responses.add(Operation::AddFrame {
|
||||
path: shape_data.layer.unwrap().to_path(),
|
||||
insert_index: -1,
|
||||
transform: DAffine2::ZERO.to_cols_array(),
|
||||
network,
|
||||
});
|
||||
responses.add(NodeGraphMessage::ShiftNode { node_id: imaginate_node_id });
|
||||
|
||||
// // Add a layer with a frame to the document
|
||||
// responses.add(Operation::AddFrame {
|
||||
// path: shape_data.layer.unwrap().to_path(),
|
||||
// insert_index: -1,
|
||||
// transform: DAffine2::ZERO.to_cols_array(),
|
||||
// network,
|
||||
// });
|
||||
|
||||
ImaginateToolFsmState::Drawing
|
||||
}
|
||||
(state, ImaginateToolMessage::Resize { center, lock_ratio }) => {
|
||||
|
||||
@@ -166,11 +166,7 @@ impl Fsm for LineToolFsmState {
|
||||
|
||||
fn transition(self, event: ToolMessage, tool_data: &mut Self::ToolData, tool_action_data: &mut ToolActionHandlerData, tool_options: &Self::ToolOptions, responses: &mut VecDeque<Message>) -> Self {
|
||||
let ToolActionHandlerData {
|
||||
document,
|
||||
global_tool_data,
|
||||
input,
|
||||
render_data,
|
||||
..
|
||||
document, global_tool_data, input, ..
|
||||
} = tool_action_data;
|
||||
|
||||
let ToolMessage::Line(event) = event else {
|
||||
@@ -178,7 +174,7 @@ impl Fsm for LineToolFsmState {
|
||||
};
|
||||
match (self, event) {
|
||||
(LineToolFsmState::Ready, LineToolMessage::DragStart) => {
|
||||
tool_data.snap_manager.start_snap(document, input, document.bounding_boxes(None, None, render_data), true, true);
|
||||
tool_data.snap_manager.start_snap(document, input, document.bounding_boxes(), true, true);
|
||||
tool_data.snap_manager.add_all_document_handles(document, input, &[], &[], &[]);
|
||||
|
||||
let viewport_start = tool_data.snap_manager.snap_position(responses, document, input.mouse.position);
|
||||
|
||||
@@ -257,7 +257,7 @@ impl PathToolData {
|
||||
|
||||
//self
|
||||
// .snap_manager
|
||||
// .start_snap(document, input, document.bounding_boxes(Some(&selected_layers), None, render_data), true, true);
|
||||
// .start_snap(document, input, document.bounding_boxes(Some(&selected_layers), None, font_cache), true, true);
|
||||
|
||||
// Do not snap against handles when anchor is selected
|
||||
let mut additional_selected_points = Vec::new();
|
||||
|
||||
@@ -543,7 +543,6 @@ impl Fsm for PenToolFsmState {
|
||||
document,
|
||||
global_tool_data,
|
||||
input,
|
||||
render_data,
|
||||
shape_editor,
|
||||
..
|
||||
} = tool_action_data;
|
||||
@@ -568,7 +567,7 @@ impl Fsm for PenToolFsmState {
|
||||
};
|
||||
match (self, event) {
|
||||
(_, PenToolMessage::CanvasTransformed) => {
|
||||
tool_data.snap_manager.start_snap(document, input, document.bounding_boxes(None, None, render_data), true, true);
|
||||
tool_data.snap_manager.start_snap(document, input, document.bounding_boxes(), true, true);
|
||||
self
|
||||
}
|
||||
(_, PenToolMessage::SelectionChanged) => {
|
||||
@@ -591,7 +590,7 @@ impl Fsm for PenToolFsmState {
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
|
||||
// Initialize snapping
|
||||
tool_data.snap_manager.start_snap(document, input, document.bounding_boxes(None, None, render_data), true, true);
|
||||
tool_data.snap_manager.start_snap(document, input, document.bounding_boxes(), true, true);
|
||||
tool_data.snap_manager.add_all_document_handles(document, input, &[], &[], &[]);
|
||||
|
||||
// Disable this tool's mirroring
|
||||
|
||||
@@ -225,11 +225,7 @@ impl Fsm for PolygonToolFsmState {
|
||||
|
||||
fn transition(self, event: ToolMessage, tool_data: &mut Self::ToolData, tool_action_data: &mut ToolActionHandlerData, tool_options: &Self::ToolOptions, responses: &mut VecDeque<Message>) -> Self {
|
||||
let ToolActionHandlerData {
|
||||
document,
|
||||
global_tool_data,
|
||||
input,
|
||||
render_data,
|
||||
..
|
||||
document, global_tool_data, input, ..
|
||||
} = tool_action_data;
|
||||
|
||||
let polygon_data = &mut tool_data.data;
|
||||
@@ -239,11 +235,11 @@ impl Fsm for PolygonToolFsmState {
|
||||
};
|
||||
match (self, event) {
|
||||
(PolygonToolFsmState::Drawing, PolygonToolMessage::CanvasTransformed) => {
|
||||
tool_data.data.recalculate_snaps(document, input, render_data);
|
||||
tool_data.data.recalculate_snaps(document, input);
|
||||
self
|
||||
}
|
||||
(PolygonToolFsmState::Ready, PolygonToolMessage::DragStart) => {
|
||||
polygon_data.start(responses, document, input, render_data);
|
||||
polygon_data.start(responses, document, input);
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
|
||||
let subpath = match tool_options.primitive_shape_type {
|
||||
|
||||
@@ -189,11 +189,7 @@ impl Fsm for RectangleToolFsmState {
|
||||
event: ToolMessage,
|
||||
tool_data: &mut Self::ToolData,
|
||||
ToolActionHandlerData {
|
||||
document,
|
||||
global_tool_data,
|
||||
input,
|
||||
render_data,
|
||||
..
|
||||
document, global_tool_data, input, ..
|
||||
}: &mut ToolActionHandlerData,
|
||||
tool_options: &Self::ToolOptions,
|
||||
responses: &mut VecDeque<Message>,
|
||||
@@ -209,11 +205,11 @@ impl Fsm for RectangleToolFsmState {
|
||||
|
||||
match (self, event) {
|
||||
(Drawing, CanvasTransformed) => {
|
||||
tool_data.data.recalculate_snaps(document, input, render_data);
|
||||
tool_data.data.recalculate_snaps(document, input);
|
||||
self
|
||||
}
|
||||
(Ready, DragStart) => {
|
||||
shape_data.start(responses, document, input, render_data);
|
||||
shape_data.start(responses, document, input);
|
||||
|
||||
let subpath = bezier_rs::Subpath::new_rect(DVec2::ZERO, DVec2::ONE);
|
||||
|
||||
|
||||
@@ -382,7 +382,7 @@ impl Fsm for SelectToolFsmState {
|
||||
type ToolOptions = ();
|
||||
|
||||
fn transition(self, event: ToolMessage, tool_data: &mut Self::ToolData, tool_action_data: &mut ToolActionHandlerData, _tool_options: &(), responses: &mut VecDeque<Message>) -> Self {
|
||||
let ToolActionHandlerData { document, input, render_data, .. } = tool_action_data;
|
||||
let ToolActionHandlerData { document, input, .. } = tool_action_data;
|
||||
|
||||
let ToolMessage::Select(event) = event else {
|
||||
return self;
|
||||
@@ -482,7 +482,7 @@ impl Fsm for SelectToolFsmState {
|
||||
let state = if tool_data.pivot.is_over(input.mouse.position) {
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
|
||||
tool_data.snap_manager.start_snap(document, input, document.bounding_boxes(None, None, render_data), true, true);
|
||||
tool_data.snap_manager.start_snap(document, input, document.bounding_boxes(), true, true);
|
||||
tool_data.snap_manager.add_all_document_handles(document, input, &[], &[], &[]);
|
||||
|
||||
SelectToolFsmState::DraggingPivot
|
||||
@@ -494,7 +494,7 @@ impl Fsm for SelectToolFsmState {
|
||||
//
|
||||
// tool_data
|
||||
// .snap_manager
|
||||
// .start_snap(document, input, document.bounding_boxes(Some(&selected), None, render_data), snap_x, snap_y);
|
||||
// .start_snap(document, input, document.bounding_boxes(Some(&selected), None, font_cache), snap_x, snap_y);
|
||||
// tool_data
|
||||
// .snap_manager
|
||||
// .add_all_document_handles(document, input, &[], &selected.iter().map(|x| x.as_slice()).collect::<Vec<_>>(), &[]);
|
||||
@@ -550,7 +550,7 @@ impl Fsm for SelectToolFsmState {
|
||||
|
||||
// tool_data
|
||||
// .snap_manager
|
||||
// .start_snap(document, input, document.bounding_boxes(Some(&tool_data.layers_dragging), None, render_data), true, true);
|
||||
// .start_snap(document, input, document.bounding_boxes(Some(&tool_data.layers_dragging), None, font_cache), true, true);
|
||||
|
||||
SelectToolFsmState::Dragging
|
||||
} else {
|
||||
@@ -922,7 +922,7 @@ fn drag_shallowest_manipulation(responses: &mut VecDeque<Message>, selected: Vec
|
||||
});
|
||||
// tool_data
|
||||
// .snap_manager
|
||||
// .start_snap(document, input, document.bounding_boxes(Some(&tool_data.layers_dragging), None, render_data), true, true);
|
||||
// .start_snap(document, input, document.bounding_boxes(Some(&tool_data.layers_dragging), None, font_cache), true, true);
|
||||
}
|
||||
|
||||
fn drag_deepest_manipulation(responses: &mut VecDeque<Message>, mut selected: Vec<LayerNodeIdentifier>, tool_data: &mut SelectToolData) {
|
||||
@@ -932,7 +932,7 @@ fn drag_deepest_manipulation(responses: &mut VecDeque<Message>, mut selected: Ve
|
||||
});
|
||||
// tool_data
|
||||
// .snap_manager
|
||||
// .start_snap(document, input, document.bounding_boxes(Some(&tool_data.layers_dragging), None, render_data), true, true);
|
||||
// .start_snap(document, input, document.bounding_boxes(Some(&tool_data.layers_dragging), None, font_cache), true, true);
|
||||
}
|
||||
|
||||
fn edit_layer_shallowest_manipulation(document: &DocumentMessageHandler, layer: LayerNodeIdentifier, responses: &mut VecDeque<Message>) {
|
||||
|
||||
@@ -196,11 +196,7 @@ impl Fsm for SplineToolFsmState {
|
||||
|
||||
fn transition(self, event: ToolMessage, tool_data: &mut Self::ToolData, tool_action_data: &mut ToolActionHandlerData, tool_options: &Self::ToolOptions, responses: &mut VecDeque<Message>) -> Self {
|
||||
let ToolActionHandlerData {
|
||||
document,
|
||||
global_tool_data,
|
||||
input,
|
||||
render_data,
|
||||
..
|
||||
document, global_tool_data, input, ..
|
||||
} = tool_action_data;
|
||||
|
||||
let ToolMessage::Spline(event) = event else {
|
||||
@@ -208,7 +204,7 @@ impl Fsm for SplineToolFsmState {
|
||||
};
|
||||
match (self, event) {
|
||||
(_, SplineToolMessage::CanvasTransformed) => {
|
||||
tool_data.snap_manager.start_snap(document, input, document.bounding_boxes(None, None, render_data), true, true);
|
||||
tool_data.snap_manager.start_snap(document, input, document.bounding_boxes(), true, true);
|
||||
self
|
||||
}
|
||||
(SplineToolFsmState::Ready, SplineToolMessage::DragStart) => {
|
||||
@@ -218,7 +214,7 @@ impl Fsm for SplineToolFsmState {
|
||||
let parent = document.new_layer_parent();
|
||||
let transform = document.metadata().transform_to_viewport(parent);
|
||||
|
||||
tool_data.snap_manager.start_snap(document, input, document.bounding_boxes(None, None, render_data), true, true);
|
||||
tool_data.snap_manager.start_snap(document, input, document.bounding_boxes(), true, true);
|
||||
tool_data.snap_manager.add_all_document_handles(document, input, &[], &[], &[]);
|
||||
let snapped_position = tool_data.snap_manager.snap_position(responses, document, input.mouse.position);
|
||||
|
||||
|
||||
@@ -7,10 +7,10 @@ use crate::messages::tool::common_functionality::color_selector::{ToolColorOptio
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::{self, is_layer_fed_by_node_of_name};
|
||||
|
||||
use document_legacy::document_metadata::LayerNodeIdentifier;
|
||||
use document_legacy::layers::style::{Fill, RenderData};
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graphene_core::renderer::Quad;
|
||||
use graphene_core::text::{load_face, Font};
|
||||
use graphene_core::text::{load_face, Font, FontCache};
|
||||
use graphene_core::vector::style::Fill;
|
||||
use graphene_core::Color;
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -224,7 +224,7 @@ struct TextToolData {
|
||||
|
||||
impl TextToolData {
|
||||
/// Set the editing state of the currently modifying layer
|
||||
fn set_editing(&self, editable: bool, render_data: &RenderData, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
fn set_editing(&self, editable: bool, font_cache: &FontCache, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
if let Some(node_id) = graph_modification_utils::get_fill_id(self.layer, &document.document_legacy) {
|
||||
responses.add(NodeGraphMessage::SetHidden { node_id, hidden: editable });
|
||||
}
|
||||
@@ -235,7 +235,7 @@ impl TextToolData {
|
||||
line_width: None,
|
||||
font_size: editing_text.font_size,
|
||||
color: editing_text.color.unwrap_or(Color::BLACK),
|
||||
url: render_data.font_cache.get_preview_url(&editing_text.font).cloned().unwrap_or_default(),
|
||||
url: font_cache.get_preview_url(&editing_text.font).cloned().unwrap_or_default(),
|
||||
transform: editing_text.transform.to_cols_array(),
|
||||
});
|
||||
} else {
|
||||
@@ -258,9 +258,9 @@ impl TextToolData {
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn start_editing_layer(&mut self, layer: LayerNodeIdentifier, tool_state: TextToolFsmState, document: &DocumentMessageHandler, render_data: &RenderData, responses: &mut VecDeque<Message>) {
|
||||
fn start_editing_layer(&mut self, layer: LayerNodeIdentifier, tool_state: TextToolFsmState, document: &DocumentMessageHandler, font_cache: &FontCache, responses: &mut VecDeque<Message>) {
|
||||
if tool_state == TextToolFsmState::Editing {
|
||||
self.set_editing(false, render_data, document, responses);
|
||||
self.set_editing(false, font_cache, document, responses);
|
||||
}
|
||||
|
||||
self.layer = layer;
|
||||
@@ -268,19 +268,19 @@ impl TextToolData {
|
||||
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
|
||||
self.set_editing(true, render_data, document, responses);
|
||||
self.set_editing(true, font_cache, document, responses);
|
||||
|
||||
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![self.layer.to_node()] });
|
||||
}
|
||||
|
||||
fn interact(&mut self, state: TextToolFsmState, mouse: DVec2, document: &DocumentMessageHandler, render_data: &RenderData, responses: &mut VecDeque<Message>) -> TextToolFsmState {
|
||||
fn interact(&mut self, state: TextToolFsmState, mouse: DVec2, document: &DocumentMessageHandler, font_cache: &FontCache, responses: &mut VecDeque<Message>) -> TextToolFsmState {
|
||||
// Check if the user has selected an existing text layer
|
||||
if let Some(clicked_text_layer_path) = document
|
||||
.document_legacy
|
||||
.click(mouse, document.network())
|
||||
.filter(|&layer| is_layer_fed_by_node_of_name(layer, &document.document_legacy, "Text"))
|
||||
{
|
||||
self.start_editing_layer(clicked_text_layer_path, state, document, render_data, responses);
|
||||
self.start_editing_layer(clicked_text_layer_path, state, document, font_cache, responses);
|
||||
|
||||
TextToolFsmState::Editing
|
||||
}
|
||||
@@ -309,32 +309,32 @@ impl TextToolData {
|
||||
skip_rerender: true,
|
||||
});
|
||||
|
||||
self.set_editing(true, render_data, document, responses);
|
||||
self.set_editing(true, font_cache, document, responses);
|
||||
|
||||
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: self.layer.to_path() });
|
||||
|
||||
TextToolFsmState::Editing
|
||||
} else {
|
||||
// Removing old text as editable
|
||||
self.set_editing(false, render_data, document, responses);
|
||||
self.set_editing(false, font_cache, document, responses);
|
||||
|
||||
TextToolFsmState::Ready
|
||||
}
|
||||
}
|
||||
|
||||
fn get_bounds(&self, text: &str, render_data: &RenderData) -> Option<[DVec2; 2]> {
|
||||
fn get_bounds(&self, text: &str, font_cache: &FontCache) -> Option<[DVec2; 2]> {
|
||||
let editing_text = self.editing_text.as_ref()?;
|
||||
let buzz_face = render_data.font_cache.get(&editing_text.font).map(|data| load_face(data));
|
||||
let buzz_face = font_cache.get(&editing_text.font).map(|data| load_face(data));
|
||||
let subpaths = graphene_core::text::to_path(text, buzz_face, editing_text.font_size, None);
|
||||
let bounds = subpaths.iter().filter_map(|subpath| subpath.bounding_box());
|
||||
let combined_bounds = bounds.reduce(|a, b| [a[0].min(b[0]), a[1].max(b[1])]).unwrap_or_default();
|
||||
Some(combined_bounds)
|
||||
}
|
||||
|
||||
fn fix_text_bounds(&self, new_text: &str, _document: &DocumentMessageHandler, render_data: &RenderData, responses: &mut VecDeque<Message>) -> Option<()> {
|
||||
fn fix_text_bounds(&self, new_text: &str, _document: &DocumentMessageHandler, font_cache: &FontCache, responses: &mut VecDeque<Message>) -> Option<()> {
|
||||
let layer = self.layer.to_path();
|
||||
let old_bounds = self.get_bounds(&self.editing_text.as_ref()?.text, render_data)?;
|
||||
let new_bounds = self.get_bounds(new_text, render_data)?;
|
||||
let old_bounds = self.get_bounds(&self.editing_text.as_ref()?.text, font_cache)?;
|
||||
let new_bounds = self.get_bounds(new_text, font_cache)?;
|
||||
responses.add(GraphOperationMessage::UpdateBounds { layer, old_bounds, new_bounds });
|
||||
|
||||
Some(())
|
||||
@@ -366,7 +366,7 @@ impl Fsm for TextToolFsmState {
|
||||
document,
|
||||
global_tool_data,
|
||||
input,
|
||||
render_data,
|
||||
font_cache,
|
||||
..
|
||||
} = transition_data;
|
||||
let ToolMessage::Text(event) = event else {
|
||||
@@ -378,7 +378,7 @@ impl Fsm for TextToolFsmState {
|
||||
transform: document.metadata().transform_to_viewport(tool_data.layer).to_cols_array(),
|
||||
});
|
||||
if let Some(editing_text) = tool_data.editing_text.as_ref() {
|
||||
let buzz_face = render_data.font_cache.get(&editing_text.font).map(|data| load_face(data));
|
||||
let buzz_face = font_cache.get(&editing_text.font).map(|data| load_face(data));
|
||||
let far = graphene_core::text::bounding_box(&tool_data.new_text, buzz_face, editing_text.font_size, None);
|
||||
if far.x != 0. && far.y != 0. {
|
||||
let quad = Quad::from_box([DVec2::ZERO, far]);
|
||||
@@ -394,7 +394,7 @@ impl Fsm for TextToolFsmState {
|
||||
let Some((text, font, font_size)) = graph_modification_utils::get_text(layer, &document.document_legacy) else {
|
||||
continue;
|
||||
};
|
||||
let buzz_face = render_data.font_cache.get(font).map(|data| load_face(data));
|
||||
let buzz_face = font_cache.get(font).map(|data| load_face(data));
|
||||
let far = graphene_core::text::bounding_box(text, buzz_face, font_size, None);
|
||||
let quad = Quad::from_box([DVec2::ZERO, far]);
|
||||
let multiplied = document.metadata().transform_to_viewport(layer) * quad;
|
||||
@@ -413,11 +413,11 @@ impl Fsm for TextToolFsmState {
|
||||
});
|
||||
tool_data.new_text = String::new();
|
||||
|
||||
tool_data.interact(state, input.mouse.position, document, render_data, responses)
|
||||
tool_data.interact(state, input.mouse.position, document, font_cache, responses)
|
||||
}
|
||||
(state, TextToolMessage::EditSelected) => {
|
||||
if let Some(layer) = can_edit_selected(document) {
|
||||
tool_data.start_editing_layer(layer, state, document, render_data, responses);
|
||||
tool_data.start_editing_layer(layer, state, document, font_cache, responses);
|
||||
return TextToolFsmState::Editing;
|
||||
}
|
||||
|
||||
@@ -425,7 +425,7 @@ impl Fsm for TextToolFsmState {
|
||||
}
|
||||
(state, TextToolMessage::Abort) => {
|
||||
if state == TextToolFsmState::Editing {
|
||||
tool_data.set_editing(false, render_data, document, responses);
|
||||
tool_data.set_editing(false, font_cache, document, responses);
|
||||
}
|
||||
|
||||
TextToolFsmState::Ready
|
||||
@@ -436,7 +436,7 @@ impl Fsm for TextToolFsmState {
|
||||
TextToolFsmState::Editing
|
||||
}
|
||||
(TextToolFsmState::Editing, TextToolMessage::TextChange { new_text }) => {
|
||||
tool_data.fix_text_bounds(&new_text, document, render_data, responses);
|
||||
tool_data.fix_text_bounds(&new_text, document, font_cache, responses);
|
||||
responses.add(NodeGraphMessage::SetQualifiedInputValue {
|
||||
layer_path: Vec::new(),
|
||||
node_path: vec![graph_modification_utils::get_text_id(tool_data.layer, &document.document_legacy).unwrap()],
|
||||
@@ -444,7 +444,7 @@ impl Fsm for TextToolFsmState {
|
||||
value: TaggedValue::String(new_text),
|
||||
});
|
||||
|
||||
tool_data.set_editing(false, render_data, document, responses);
|
||||
tool_data.set_editing(false, font_cache, document, responses);
|
||||
|
||||
TextToolFsmState::Ready
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ use crate::messages::portfolio::document::overlays::utility_types::OverlayProvid
|
||||
use crate::messages::prelude::*;
|
||||
use crate::node_graph_executor::NodeGraphExecutor;
|
||||
|
||||
use document_legacy::layers::style::RenderData;
|
||||
use graphene_core::raster::color::Color;
|
||||
use graphene_std::text::FontCache;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{self, Debug};
|
||||
@@ -23,7 +23,7 @@ pub struct ToolActionHandlerData<'a> {
|
||||
pub document_id: u64,
|
||||
pub global_tool_data: &'a DocumentToolData,
|
||||
pub input: &'a InputPreprocessorMessageHandler,
|
||||
pub render_data: &'a RenderData<'a>,
|
||||
pub font_cache: &'a FontCache,
|
||||
pub shape_editor: &'a mut ShapeState,
|
||||
pub node_graph: &'a NodeGraphExecutor,
|
||||
}
|
||||
@@ -33,7 +33,7 @@ impl<'a> ToolActionHandlerData<'a> {
|
||||
document_id: u64,
|
||||
global_tool_data: &'a DocumentToolData,
|
||||
input: &'a InputPreprocessorMessageHandler,
|
||||
render_data: &'a RenderData<'a>,
|
||||
font_cache: &'a FontCache,
|
||||
shape_editor: &'a mut ShapeState,
|
||||
node_graph: &'a NodeGraphExecutor,
|
||||
) -> Self {
|
||||
@@ -42,7 +42,7 @@ impl<'a> ToolActionHandlerData<'a> {
|
||||
document_id,
|
||||
global_tool_data,
|
||||
input,
|
||||
render_data,
|
||||
font_cache,
|
||||
shape_editor,
|
||||
node_graph,
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@ use crate::messages::portfolio::document::utility_types::misc::{LayerMetadata, L
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
use document_legacy::document::Document as DocumentLegacy;
|
||||
use document_legacy::document::LayerId;
|
||||
use document_legacy::document_metadata::LayerNodeIdentifier;
|
||||
use document_legacy::layers::layer_info::{LayerDataTypeDiscriminant, LegacyLayerType};
|
||||
use document_legacy::{LayerId, Operation};
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graph_craft::document::{generate_uuid, DocumentNodeImplementation, NodeId, NodeNetwork};
|
||||
use graph_craft::graphene_compiler::Compiler;
|
||||
@@ -626,9 +626,7 @@ impl NodeGraphExecutor {
|
||||
name: document.document_network.nodes.get(&node_id).map(|node| node.alias.clone()).unwrap_or_default(),
|
||||
tooltip: if cfg!(debug_assertions) { format!("Layer ID: {node_id}") } else { "".into() },
|
||||
visible: !document.document_network.disabled.contains(&layer.to_node()),
|
||||
layer_type: if document.metadata.is_artboard(layer) {
|
||||
LayerDataTypeDiscriminant::Artboard
|
||||
} else if document.metadata.is_folder(layer) {
|
||||
layer_type: if document.metadata.is_folder(layer) {
|
||||
LayerDataTypeDiscriminant::Folder
|
||||
} else {
|
||||
LayerDataTypeDiscriminant::Layer
|
||||
@@ -653,12 +651,9 @@ impl NodeGraphExecutor {
|
||||
responses.add(DocumentMessage::RenderDocument);
|
||||
responses.add(DocumentMessage::DocumentStructureChanged);
|
||||
responses.add(BroadcastEvent::DocumentIsDirty);
|
||||
responses.add(DocumentMessage::DirtyRenderDocument);
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
}
|
||||
NodeGraphUpdate::NodeGraphUpdateMessage(NodeGraphUpdateMessage::ImaginateStatusUpdate) => {
|
||||
responses.add(DocumentMessage::PropertiesPanel(PropertiesPanelMessage::ResendActiveProperties))
|
||||
}
|
||||
NodeGraphUpdate::NodeGraphUpdateMessage(NodeGraphUpdateMessage::ImaginateStatusUpdate) => responses.add(DocumentMessage::PropertiesPanel(PropertiesPanelMessage::Refresh)),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -685,10 +680,8 @@ impl NodeGraphExecutor {
|
||||
fn process_node_graph_output(&mut self, node_graph_output: TaggedValue, layer_path: Vec<LayerId>, transform: DAffine2, responses: &mut VecDeque<Message>) -> Result<(), String> {
|
||||
self.last_output_type.insert(layer_path.clone(), Some(node_graph_output.ty()));
|
||||
match node_graph_output {
|
||||
TaggedValue::SurfaceFrame(SurfaceFrame { surface_id, transform }) => {
|
||||
let transform = transform.to_cols_array();
|
||||
responses.add(Operation::SetLayerTransform { path: layer_path.clone(), transform });
|
||||
responses.add(Operation::SetSurface { path: layer_path, surface_id });
|
||||
TaggedValue::SurfaceFrame(SurfaceFrame { surface_id: _, transform: _ }) => {
|
||||
// TODO: Reimplement this now that document-legacy is gone
|
||||
}
|
||||
TaggedValue::RenderOutput(graphene_std::wasm_application_io::RenderOutput::Svg(svg)) => {
|
||||
// Send to frontend
|
||||
|
||||
@@ -112,7 +112,7 @@ export function createPortfolioState(editor: Editor) {
|
||||
image.src = blobURL;
|
||||
await image.decode();
|
||||
|
||||
editor.instance.setImageBlobURL(updateImageData.documentId, element.path, element.nodeId, blobURL, image.naturalWidth, image.naturalHeight, element.transform);
|
||||
// editor.instance.setImageBlobURL(updateImageData.documentId, element.path, element.nodeId, blobURL, image.naturalWidth, image.naturalHeight, element.transform);
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(TriggerRevokeBlobUrl, async (triggerRevokeBlobUrl) => {
|
||||
|
||||
@@ -10,7 +10,7 @@ export type Editor = Readonly<ReturnType<typeof createEditor>>;
|
||||
// `wasmImport` starts uninitialized because its initialization needs to occur asynchronously, and thus needs to occur by manually calling and awaiting `initWasm()`
|
||||
let wasmImport: WebAssembly.Memory | undefined;
|
||||
|
||||
export async function updateImage(path: BigUint64Array, nodeId: bigint, mime: string, imageData: Uint8Array, transform: Float64Array, documentId: bigint) {
|
||||
export async function updateImage(path: BigUint64Array, nodeId: bigint, mime: string, imageData: Uint8Array, _transform: Float64Array, _documentId: bigint) {
|
||||
const blob = new Blob([imageData], { type: mime });
|
||||
|
||||
const blobURL = URL.createObjectURL(blob);
|
||||
@@ -21,10 +21,10 @@ export async function updateImage(path: BigUint64Array, nodeId: bigint, mime: st
|
||||
await image.decode();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(window as any).editorInstance?.setImageBlobURL(documentId, path, nodeId, blobURL, image.naturalWidth, image.naturalHeight, transform);
|
||||
// (window as any).editorInstance?.setImageBlobURL(documentId, path, nodeId, blobURL, image.naturalWidth, image.naturalHeight, transform);
|
||||
}
|
||||
|
||||
export async function fetchImage(path: BigUint64Array, nodeId: bigint, mime: string, documentId: bigint, url: string) {
|
||||
export async function fetchImage(_path: BigUint64Array, _nodeId: bigint, _mime: string, _documentId: bigint, url: string) {
|
||||
const data = await fetch(url);
|
||||
const blob = await data.blob();
|
||||
|
||||
@@ -36,7 +36,7 @@ export async function fetchImage(path: BigUint64Array, nodeId: bigint, mime: str
|
||||
await image.decode();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(window as any).editorInstance?.setImageBlobURL(documentId, path, nodeId, blobURL, image.naturalWidth, image.naturalHeight, undefined);
|
||||
// (window as any).editorInstance?.setImageBlobURL(documentId, path, nodeId, blobURL, image.naturalWidth, image.naturalHeight, undefined);
|
||||
}
|
||||
|
||||
const tauri = "__TAURI_METADATA__" in window && import("@tauri-apps/api");
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
//! This file is where functions are defined to be called directly from JS.
|
||||
//! It serves as a thin wrapper over the editor backend API that relies
|
||||
//! on the dispatcher messaging system and more complex Rust data types.
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[allow(clippy::non_snake_case)]
|
||||
// This file is where functions are defined to be called directly from JS.
|
||||
// It serves as a thin wrapper over the editor backend API that relies
|
||||
// on the dispatcher messaging system and more complex Rust data types.
|
||||
use crate::helpers::translate_key;
|
||||
use crate::{Error, EDITOR_HAS_CRASHED, EDITOR_INSTANCES, JS_EDITOR_HANDLES};
|
||||
|
||||
use document_legacy::document::LayerId;
|
||||
use document_legacy::document_metadata::LayerNodeIdentifier;
|
||||
use document_legacy::LayerId;
|
||||
use editor::application::generate_uuid;
|
||||
use editor::application::Editor;
|
||||
use editor::consts::{FILE_SAVE_SUFFIX, GRAPHITE_DOCUMENT_VERSION};
|
||||
@@ -103,7 +104,6 @@ async fn poll_node_graph_evaluation() {
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
impl JsEditorHandle {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(frontend_message_handler_callback: js_sys::Function) -> Self {
|
||||
@@ -575,26 +575,19 @@ impl JsEditorHandle {
|
||||
self.dispatch(message);
|
||||
}
|
||||
|
||||
/// Sends the blob URL generated by JS to the Image layer
|
||||
#[wasm_bindgen(js_name = setImageBlobURL)]
|
||||
pub fn set_image_blob_url(&self, document_id: u64, layer_path: Vec<LayerId>, node_id: Option<NodeId>, blob_url: String, width: f64, height: f64, transform: Option<js_sys::Float64Array>) {
|
||||
let resolution = (width, height);
|
||||
let message = PortfolioMessage::SetImageBlobUrl {
|
||||
document_id,
|
||||
layer_path: layer_path.clone(),
|
||||
node_id,
|
||||
blob_url,
|
||||
resolution,
|
||||
};
|
||||
self.dispatch(message);
|
||||
|
||||
if let Some(array) = transform.filter(|array| array.length() == 6) {
|
||||
let mut transform: [f64; 6] = [0.; 6];
|
||||
array.copy_to(&mut transform);
|
||||
let message = document_legacy::Operation::SetLayerTransform { path: layer_path, transform };
|
||||
self.dispatch(message);
|
||||
}
|
||||
}
|
||||
// /// Sends the blob URL generated by JS to the Image layer
|
||||
// #[wasm_bindgen(js_name = setImageBlobURL)]
|
||||
// pub fn set_image_blob_url(&self, document_id: u64, layer_path: Vec<LayerId>, node_id: Option<NodeId>, blob_url: String, width: f64, height: f64, _transform: Option<js_sys::Float64Array>) {
|
||||
// let resolution = (width, height);
|
||||
// let message = PortfolioMessage::SetImageBlobUrl {
|
||||
// document_id,
|
||||
// layer_path: layer_path.clone(),
|
||||
// node_id,
|
||||
// blob_url,
|
||||
// resolution,
|
||||
// };
|
||||
// self.dispatch(message);
|
||||
// }
|
||||
|
||||
/// Notifies the backend that the user connected a node's primary output to one of another node's inputs
|
||||
#[wasm_bindgen(js_name = connectNodesByLink)]
|
||||
|
||||
@@ -89,7 +89,7 @@ fn create_executor(document_string: String) -> Result<DynamicExecutor, Box<dyn E
|
||||
let document: serde_json::Value = serde_json::from_str(&document_string).expect("Failed to parse document");
|
||||
let document = serde_json::from_value::<Document>(document["document_legacy"].clone()).expect("Failed to parse document");
|
||||
let Some(LegacyLayerType::Layer(ref node_graph)) = document.root.iter().find(|layer| matches!(layer.data, LegacyLayerType::Layer(_))).map(|x| &x.data) else {
|
||||
panic!("failed to extract node graph from docmuent")
|
||||
panic!("Failed to extract node graph from document")
|
||||
};
|
||||
let network = &node_graph.network;
|
||||
let wrapped_network = wrap_network_in_scope(network.clone());
|
||||
|
||||
Reference in New Issue
Block a user