mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-19 10:58:04 +08:00
Migrate vector data and tools to use nodes (#1065)
* Add rendering to vector nodes * Add line, shape, rectange and freehand tool * Fix transforms, strokes and fills * Migrate spline tool * Remove blank lines * Fix test * Fix fill in properties * Select layers when filling * Properties panel transform around pivot * Fix select tool outlines * Select tool modifies node graph pivot * Add the pivot assist to the properties * Improve setting non existant fill UX * Cleanup hash function * Path and pen tools * Bug fixes * Disable boolean ops * Fix default handle smoothing on ellipses * Fix test and warnings --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
@@ -8,8 +8,6 @@ use crate::layers::style::RenderData;
|
||||
use crate::layers::text_layer::{Font, TextLayer};
|
||||
use crate::{DocumentError, DocumentResponse, Operation};
|
||||
|
||||
use graphene_std::vector::subpath::Subpath;
|
||||
|
||||
use glam::{DAffine2, DVec2};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::cell::RefCell;
|
||||
@@ -177,43 +175,6 @@ impl Document {
|
||||
Ok(shapes)
|
||||
}
|
||||
|
||||
/// Return a copy of all [Subpath]s currently in the document.
|
||||
pub fn all_subpaths(&self) -> Vec<Subpath> {
|
||||
self.root.iter().flat_map(|layer| layer.as_subpath_copy()).collect::<Vec<Subpath>>()
|
||||
}
|
||||
|
||||
/// Returns references to all [Subpath]s currently in the document.
|
||||
pub fn all_subpaths_ref(&self) -> Vec<&Subpath> {
|
||||
self.root.iter().flat_map(|layer| layer.as_subpath()).collect::<Vec<&Subpath>>()
|
||||
}
|
||||
|
||||
/// Returns a reference to the requested [Subpath] by providing a path to its owner layer.
|
||||
pub fn subpath_ref<'a>(&'a self, path: &[LayerId]) -> Option<&'a Subpath> {
|
||||
self.layer(path).ok()?.as_subpath()
|
||||
}
|
||||
|
||||
/// Returns a mutable reference of the requested [Subpath] by providing a path to its owner layer.
|
||||
pub fn subpath_mut<'a>(&'a mut self, path: &'a [LayerId]) -> Option<&'a mut Subpath> {
|
||||
self.layer_mut(path).ok()?.as_subpath_mut()
|
||||
}
|
||||
|
||||
/// Set a [Subpath] at the specified path.
|
||||
pub fn set_subpath(&mut self, path: &[LayerId], shape: Subpath) {
|
||||
let layer = self.layer_mut(path);
|
||||
if let Ok(layer) = layer {
|
||||
if let LayerDataType::Shape(shape_layer) = &mut layer.data {
|
||||
shape_layer.shape = shape;
|
||||
// Is this needed?
|
||||
layer.cache_dirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Set [Subpath]s for multiple paths at once.
|
||||
pub fn set_subpaths<'a>(&'a mut self, paths: impl Iterator<Item = &'a [LayerId]>, shapes: Vec<Subpath>) {
|
||||
paths.zip(shapes).for_each(|(path, shape)| self.set_subpath(path, shape));
|
||||
}
|
||||
|
||||
pub fn common_layer_path_prefix<'a>(&self, layers: impl Iterator<Item = &'a [LayerId]>) -> &'a [LayerId] {
|
||||
layers.reduce(|a, b| &a[..a.iter().zip(b.iter()).take_while(|&(a, b)| a == b).count()]).unwrap_or_default()
|
||||
}
|
||||
@@ -865,6 +826,12 @@ impl Document {
|
||||
}
|
||||
Some(vec![DocumentChanged, LayerChanged { path }])
|
||||
}
|
||||
Operation::SetVectorData { path, vector_data } => {
|
||||
if let LayerDataType::NodeGraphFrame(graph) = &mut self.layer_mut(&path)?.data {
|
||||
graph.vector_data = Some(vector_data);
|
||||
}
|
||||
Some(Vec::new())
|
||||
}
|
||||
Operation::InsertManipulatorGroup {
|
||||
layer_path,
|
||||
manipulator_group,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::boolean_ops::{split_path_seg, subdivide_path_seg};
|
||||
use crate::consts::{F64LOOSE, F64PRECISE};
|
||||
|
||||
use graphene_core::uuid::ManipulatorGroupId;
|
||||
use graphene_std::vector::subpath::Subpath;
|
||||
|
||||
use glam::{DAffine2, DMat2, DVec2};
|
||||
@@ -19,6 +20,13 @@ impl Quad {
|
||||
Self([bbox[0], bbox[0] + size * DVec2::X, bbox[1], bbox[0] + size * DVec2::Y])
|
||||
}
|
||||
|
||||
/// Get all the edges in the quad.
|
||||
pub fn lines_glam(&self) -> impl Iterator<Item = bezier_rs::Bezier> + '_ {
|
||||
[[self.0[0], self.0[1]], [self.0[1], self.0[2]], [self.0[2], self.0[3]], [self.0[3], self.0[0]]]
|
||||
.into_iter()
|
||||
.map(|[start, end]| bezier_rs::Bezier::from_linear_dvec2(start, end))
|
||||
}
|
||||
|
||||
/// Get all the edges in the quad.
|
||||
pub fn lines(&self) -> [Line; 4] {
|
||||
[
|
||||
@@ -101,6 +109,35 @@ pub fn intersect_quad_bez_path(quad: Quad, shape: &BezPath, filled: bool) -> boo
|
||||
get_arbitrary_point_on_path(&shape).map(|shape_point| quad.path().contains(shape_point)).unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn intersect_quad_subpath(quad: Quad, subpath: &bezier_rs::Subpath<ManipulatorGroupId>, close_subpath: bool) -> bool {
|
||||
let mut subpath = subpath.clone();
|
||||
|
||||
// For close_subpath shapes act like shape was closed even if it isn't
|
||||
if close_subpath && !subpath.closed() {
|
||||
subpath.set_closed(true);
|
||||
}
|
||||
|
||||
// Check if outlines intersect
|
||||
if subpath
|
||||
.iter()
|
||||
.any(|path_segment| quad.lines_glam().any(|line| !path_segment.intersections(&line, None, None).is_empty()))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// Check if selection is entirely within the shape
|
||||
if close_subpath && subpath.contains_point(quad.center()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if shape is entirely within selection
|
||||
subpath
|
||||
.manipulator_groups()
|
||||
.first()
|
||||
.map(|group| group.anchor)
|
||||
.map(|shape_point| quad.path().contains(to_point(shape_point)))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Returns a point on `path`.
|
||||
/// This function will usually return the first point from the path's first segment, but callers should not rely on this behavior.
|
||||
pub fn get_arbitrary_point_on_path(path: &BezPath) -> Option<Point> {
|
||||
|
||||
@@ -8,6 +8,7 @@ use crate::intersection::Quad;
|
||||
use crate::DocumentError;
|
||||
use crate::LayerId;
|
||||
|
||||
use graphene_core::vector::VectorData;
|
||||
use graphene_std::vector::subpath::Subpath;
|
||||
|
||||
use core::fmt;
|
||||
@@ -437,16 +438,9 @@ impl Layer {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_subpath(&self) -> Option<&Subpath> {
|
||||
pub fn as_vector_data(&self) -> Option<&VectorData> {
|
||||
match &self.data {
|
||||
LayerDataType::Shape(s) => Some(&s.shape),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_subpath_copy(&self) -> Option<Subpath> {
|
||||
match &self.data {
|
||||
LayerDataType::Shape(s) => Some(s.shape.clone()),
|
||||
LayerDataType::NodeGraphFrame(NodeGraphFrameLayer { vector_data: Some(vector_data), .. }) => Some(vector_data),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -503,10 +497,18 @@ impl Layer {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_graph_frame(&self) -> Result<&NodeGraphFrameLayer, DocumentError> {
|
||||
match &self.data {
|
||||
LayerDataType::NodeGraphFrame(frame) => Ok(frame),
|
||||
_ => Err(DocumentError::NotNodeGraph),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn style(&self) -> Result<&PathStyle, DocumentError> {
|
||||
match &self.data {
|
||||
LayerDataType::Shape(s) => Ok(&s.style),
|
||||
LayerDataType::Text(t) => Ok(&t.path_style),
|
||||
LayerDataType::NodeGraphFrame(t) => t.vector_data.as_ref().map(|vector| &vector.style).ok_or(DocumentError::NotShape),
|
||||
_ => Err(DocumentError::NotShape),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use super::base64_serde;
|
||||
use super::layer_info::LayerData;
|
||||
use super::style::{RenderData, ViewMode};
|
||||
use crate::intersection::{intersect_quad_bez_path, Quad};
|
||||
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 kurbo::{Affine, BezPath, Shape as KurboShape};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Write;
|
||||
@@ -23,6 +24,7 @@ pub struct NodeGraphFrameLayer {
|
||||
#[serde(skip)]
|
||||
pub dimensions: DVec2,
|
||||
pub image_data: Option<ImageData>,
|
||||
pub vector_data: Option<VectorData>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize, specta::Type)]
|
||||
@@ -33,7 +35,7 @@ pub struct ImageData {
|
||||
}
|
||||
|
||||
impl LayerData for NodeGraphFrameLayer {
|
||||
fn render(&mut self, svg: &mut String, _svg_defs: &mut String, transforms: &mut Vec<DAffine2>, render_data: &RenderData) -> bool {
|
||||
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();
|
||||
|
||||
@@ -56,7 +58,21 @@ impl LayerData for NodeGraphFrameLayer {
|
||||
.enumerate()
|
||||
.fold(String::new(), |val, (i, entry)| val + &(entry.to_string() + if i == 5 { "" } else { "," }));
|
||||
|
||||
if let Some(blob_url) = &self.blob_url {
|
||||
// Render any paths if they exist
|
||||
if let Some(vector_data) = &self.vector_data {
|
||||
let layer_bounds = vector_data.bounding_box().unwrap_or_default();
|
||||
let transfomed_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, transfomed_bounds));
|
||||
let _ = write!(svg, "/>");
|
||||
} else if let Some(blob_url) = &self.blob_url {
|
||||
// Render the image if it exists
|
||||
let _ = write!(
|
||||
svg,
|
||||
r#"<image width="{}" height="{}" preserveAspectRatio="none" href="{}" transform="matrix({})" />"#,
|
||||
@@ -66,6 +82,7 @@ impl LayerData for NodeGraphFrameLayer {
|
||||
matrix
|
||||
);
|
||||
} else {
|
||||
// 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({})" />"#,
|
||||
@@ -81,6 +98,10 @@ impl LayerData for NodeGraphFrameLayer {
|
||||
}
|
||||
|
||||
fn bounding_box(&self, transform: glam::DAffine2, _render_data: &RenderData) -> Option<[DVec2; 2]> {
|
||||
if let Some(vector_data) = &self.vector_data {
|
||||
return vector_data.bounding_box_with_transform(transform);
|
||||
}
|
||||
|
||||
let mut path = self.bounds();
|
||||
|
||||
if transform.matrix2 == DMat2::ZERO {
|
||||
@@ -93,7 +114,12 @@ impl LayerData for NodeGraphFrameLayer {
|
||||
}
|
||||
|
||||
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, _render_data: &RenderData) {
|
||||
if intersect_quad_bez_path(quad, &self.bounds(), true) {
|
||||
if let Some(vector_data) = &self.vector_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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,6 +187,10 @@ pub enum Operation {
|
||||
path: Vec<LayerId>,
|
||||
subpath: Subpath,
|
||||
},
|
||||
SetVectorData {
|
||||
path: Vec<LayerId>,
|
||||
vector_data: graphene_core::vector::VectorData,
|
||||
},
|
||||
InsertManipulatorGroup {
|
||||
layer_path: Vec<LayerId>,
|
||||
manipulator_group: ManipulatorGroup,
|
||||
|
||||
Reference in New Issue
Block a user