Initial work migrating vector layers to document graph

* Fix pen tool (except overlays)
* Thumbnail of only the layer and not the composite
* Fix occasional transform breakages
* Constrain size of thumbnail
* Insert new layers at the top
* Broken layer tree
* Fix crash when drawing
* Reduce calls to send graph
* Reduce calls to updating properties
* Store cached transforms upon the document
* Fix missing node UI updates
* Fix fill tool and clean up imports and indentation
* Error on overide existing layer
* Fix pen tool (partially)
* Fix some lints
This commit is contained in:
0hypercube 2023-07-30 20:16:08 +01:00 committed by Keavon Chambers
parent fc6cee372a
commit 4cd72edb64
50 changed files with 3585 additions and 3053 deletions

View file

@ -97,6 +97,13 @@ fn construct_layer<Data: Into<GraphicElementData>>(
stack
}
pub struct ToGraphicElementData {}
#[node_fn(ToGraphicElementData)]
fn to_graphic_element_data<Data: Into<GraphicElementData>>(graphic_element_data: Data) -> GraphicElementData {
graphic_element_data.into()
}
pub struct ConstructArtboardNode<Location, Dimensions, Background, Clip> {
location: Location,
dimensions: Dimensions,

View file

@ -1,11 +1,56 @@
use crate::raster::{Image, ImageFrame};
use crate::{uuid::generate_uuid, vector::VectorData, Artboard, Color, GraphicElementData, GraphicGroup};
use quad::Quad;
use crate::uuid::{generate_uuid, ManipulatorGroupId};
use crate::{vector::VectorData, Artboard, Color, GraphicElementData, GraphicGroup};
use bezier_rs::Subpath;
pub use quad::Quad;
use glam::{DAffine2, DVec2};
mod quad;
/// Represents a clickable target for the layer
#[derive(Clone, Debug)]
pub struct ClickTarget {
pub subpath: bezier_rs::Subpath<ManipulatorGroupId>,
pub stroke_width: f64,
}
impl ClickTarget {
/// Does the click target intersect the rectangle
pub fn intersect_rectangle(&self, document_quad: Quad, layer_transform: DAffine2) -> bool {
let quad = layer_transform.inverse() * document_quad;
// Check if outlines intersect
if self
.subpath
.iter()
.any(|path_segment| quad.bezier_lines().any(|line| !path_segment.intersections(&line, None, None).is_empty()))
{
return true;
}
// Check if selection is entirely within the shape
if self.subpath.closed() && self.subpath.contains_point(quad.center()) {
return true;
}
// Check if shape is entirely within selection
self.subpath
.manipulator_groups()
.first()
.map(|group| group.anchor)
.map(|shape_point| quad.contains(shape_point))
.unwrap_or_default()
}
/// Does the click target intersect the point (accounting for stroke size)
pub fn intersect_point(&self, point: DVec2, layer_transform: DAffine2) -> bool {
// Allows for selecting lines
// TODO: actual intersection of stroke
let inflated_quad = Quad::from_box([point - DVec2::splat(self.stroke_width / 2.), point + DVec2::splat(self.stroke_width / 2.)]);
self.intersect_rectangle(inflated_quad, layer_transform)
}
}
/// Mutable state used whilst rendering to an SVG
pub struct SvgRender {
pub svg: SvgSegmentList,
@ -109,6 +154,7 @@ pub fn format_transform_matrix(transform: DAffine2) -> String {
pub trait GraphicElementRendered {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams);
fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]>;
fn add_click_targets(&self, click_targets: &mut Vec<ClickTarget>);
}
impl GraphicElementRendered for GraphicGroup {
@ -118,6 +164,7 @@ impl GraphicElementRendered for GraphicGroup {
fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
self.iter().filter_map(|element| element.graphic_element_data.bounding_box(transform)).reduce(Quad::combine_bounds)
}
fn add_click_targets(&self, _click_targets: &mut Vec<ClickTarget>) {}
}
impl GraphicElementRendered for VectorData {
@ -140,6 +187,14 @@ impl GraphicElementRendered for VectorData {
fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
self.bounding_box_with_transform(self.transform * transform)
}
fn add_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
let stroke_width = self.style.stroke().as_ref().map_or(0., crate::vector::style::Stroke::weight);
let update_closed = |mut subpath: bezier_rs::Subpath<ManipulatorGroupId>| {
subpath.set_closed(self.style.fill().is_some());
subpath
};
click_targets.extend(self.subpaths.iter().cloned().map(update_closed).map(|subpath| ClickTarget { stroke_width, subpath }))
}
}
impl GraphicElementRendered for Artboard {
@ -195,7 +250,15 @@ impl GraphicElementRendered for Artboard {
}
fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
let artboard_bounds = (transform * Quad::from_box([self.location.as_dvec2(), self.location.as_dvec2() + self.dimensions.as_dvec2()])).bounding_box();
[self.graphic_group.bounding_box(transform), Some(artboard_bounds)].into_iter().flatten().reduce(Quad::combine_bounds)
if self.clip {
Some(artboard_bounds)
} else {
[self.graphic_group.bounding_box(transform), Some(artboard_bounds)].into_iter().flatten().reduce(Quad::combine_bounds)
}
}
fn add_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
let subpath = Subpath::new_rect(self.location.as_dvec2(), self.location.as_dvec2() + self.dimensions.as_dvec2());
click_targets.push(ClickTarget { stroke_width: 0., subpath });
}
}
@ -216,6 +279,10 @@ impl GraphicElementRendered for ImageFrame<Color> {
let transform = self.transform * transform;
(transform.matrix2 != glam::DMat2::ZERO).then(|| (transform * Quad::from_box([DVec2::ZERO, DVec2::ONE])).bounding_box())
}
fn add_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
let subpath = Subpath::new_rect(DVec2::ZERO, DVec2::ONE);
click_targets.push(ClickTarget { subpath, stroke_width: 0. });
}
}
impl GraphicElementRendered for GraphicElementData {
@ -238,6 +305,16 @@ impl GraphicElementRendered for GraphicElementData {
GraphicElementData::Artboard(artboard) => artboard.bounding_box(transform),
}
}
fn add_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
match self {
GraphicElementData::VectorShape(vector_data) => vector_data.add_click_targets(click_targets),
GraphicElementData::ImageFrame(image_frame) => image_frame.add_click_targets(click_targets),
GraphicElementData::Text(_) => todo!("click target for text GraphicElementData"),
GraphicElementData::GraphicGroup(graphic_group) => graphic_group.add_click_targets(click_targets),
GraphicElementData::Artboard(artboard) => artboard.add_click_targets(click_targets),
}
}
}
/// A segment of an svg string to allow for embedding blob urls

View file

@ -5,6 +5,11 @@ use glam::{DAffine2, DVec2};
pub struct Quad([DVec2; 4]);
impl Quad {
/// Create a zero sized quad at the point
pub fn from_point(point: DVec2) -> Self {
Self([point; 4])
}
/// Convert a box defined by two corner points to a quad.
pub fn from_box(bbox: [DVec2; 2]) -> Self {
let size = bbox[1] - bbox[0];
@ -12,7 +17,7 @@ impl Quad {
}
/// Get all the edges in the quad.
pub fn lines_glam(&self) -> impl Iterator<Item = bezier_rs::Bezier> + '_ {
pub fn bezier_lines(&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))
@ -40,6 +45,33 @@ impl Quad {
pub fn combine_bounds(a: [DVec2; 2], b: [DVec2; 2]) -> [DVec2; 2] {
[a[0].min(b[0]), a[1].max(b[1])]
}
/// Expand a quad by a certain amount on all sides.
///
/// Not currently very optimised
pub fn inflate(&self, offset: f64) -> Quad {
let offset = |index_before, index, index_after| {
let [point_before, point, point_after]: [DVec2; 3] = [self.0[index_before], self.0[index], self.0[index_after]];
let [line_in, line_out] = [point - point_before, point_after - point];
let angle = line_in.angle_between(-line_out);
let offset_length = offset / (std::f64::consts::FRAC_PI_2 - angle / 2.).cos();
point + (line_in.perp().normalize_or_zero() + line_out.perp().normalize_or_zero()).normalize_or_zero() * offset_length
};
Self([offset(3, 0, 1), offset(0, 1, 2), offset(1, 2, 3), offset(2, 3, 0)])
}
/// Does this quad contain a point
///
/// Code from https://wrfranklin.org/Research/Short_Notes/pnpoly.html
pub fn contains(&self, p: DVec2) -> bool {
let mut inside = false;
for (i, j) in (0..4).zip([3, 0, 1, 2]) {
if (self.0[i].y > p.y) != (self.0[j].y > p.y) && p.x < (self.0[j].x - self.0[i].x * (p.y - self.0[i].y) / (self.0[j].y - self.0[i].y) + self.0[i].x) {
inside = !inside;
}
}
inside
}
}
impl core::ops::Mul<Quad> for DAffine2 {
@ -49,3 +81,26 @@ impl core::ops::Mul<Quad> for DAffine2 {
Quad(rhs.0.map(|point| self.transform_point2(point)))
}
}
#[test]
fn offset_quad() {
fn eq(a: Quad, b: Quad) -> bool {
a.0.iter().zip(b.0).all(|(a, b)| a.abs_diff_eq(b, 0.0001))
}
assert!(eq(Quad::from_box([DVec2::ZERO, DVec2::ONE]).inflate(0.5), Quad::from_box([DVec2::splat(-0.5), DVec2::splat(1.5)])));
assert!(eq(Quad::from_box([DVec2::ONE, DVec2::ZERO]).inflate(0.5), Quad::from_box([DVec2::splat(1.5), DVec2::splat(-0.5)])));
assert!(eq(
(DAffine2::from_scale(DVec2::new(-1., 1.)) * Quad::from_box([DVec2::ZERO, DVec2::ONE])).inflate(0.5),
DAffine2::from_scale(DVec2::new(-1., 1.)) * Quad::from_box([DVec2::splat(-0.5), DVec2::splat(1.5)])
));
}
#[test]
fn quad_contains() {
assert!(Quad::from_box([DVec2::ZERO, DVec2::ONE]).contains(DVec2::splat(0.5)));
assert!(Quad::from_box([DVec2::ONE, DVec2::ZERO]).contains(DVec2::splat(0.5)));
assert!((DAffine2::from_scale(DVec2::new(-1., 1.)) * Quad::from_box([DVec2::ZERO, DVec2::ONE])).contains(DVec2::new(-0.5, 0.5)));
assert!(!Quad::from_box([DVec2::ZERO, DVec2::ONE]).contains(DVec2::new(1., 1.1)));
assert!(!Quad::from_box([DVec2::ONE, DVec2::ZERO]).contains(DVec2::new(0.5, -0.01)));
assert!(!(DAffine2::from_scale(DVec2::new(-1., 1.)) * Quad::from_box([DVec2::ZERO, DVec2::ONE])).contains(DVec2::splat(0.5)));
}

View file

@ -5,6 +5,7 @@ use glam::DVec2;
use crate::raster::ImageFrame;
use crate::raster::Pixel;
use crate::vector::VectorData;
use crate::GraphicElementData;
use crate::Node;
pub trait Transform {
@ -42,6 +43,52 @@ impl<P: Pixel> TransformMut for ImageFrame<P> {
&mut self.transform
}
}
impl Transform for GraphicElementData {
fn transform(&self) -> DAffine2 {
match self {
GraphicElementData::VectorShape(vector_shape) => vector_shape.transform(),
GraphicElementData::ImageFrame(image_frame) => image_frame.transform(),
GraphicElementData::Text(_) => todo!("Transform of text"),
GraphicElementData::GraphicGroup(_graphic_group) => DAffine2::IDENTITY,
GraphicElementData::Artboard(_artboard) => DAffine2::IDENTITY,
}
}
fn local_pivot(&self, pivot: DVec2) -> DVec2 {
match self {
GraphicElementData::VectorShape(vector_shape) => vector_shape.local_pivot(pivot),
GraphicElementData::ImageFrame(image_frame) => image_frame.local_pivot(pivot),
GraphicElementData::Text(_) => todo!("Transform of text"),
GraphicElementData::GraphicGroup(_graphic_group) => pivot,
GraphicElementData::Artboard(_artboard) => pivot,
}
}
fn decompose_scale(&self) -> DVec2 {
let standard = || {
DVec2::new(
self.transform().transform_vector2((1., 0.).into()).length(),
self.transform().transform_vector2((0., 1.).into()).length(),
)
};
match self {
GraphicElementData::VectorShape(vector_shape) => vector_shape.decompose_scale(),
GraphicElementData::ImageFrame(image_frame) => image_frame.decompose_scale(),
GraphicElementData::Text(_) => todo!("Transform of text"),
GraphicElementData::GraphicGroup(_graphic_group) => standard(),
GraphicElementData::Artboard(_artboard) => standard(),
}
}
}
impl TransformMut for GraphicElementData {
fn transform_mut(&mut self) -> &mut DAffine2 {
match self {
GraphicElementData::VectorShape(vector_shape) => vector_shape.transform_mut(),
GraphicElementData::ImageFrame(image_frame) => image_frame.transform_mut(),
GraphicElementData::Text(_) => todo!("Transform of text"),
GraphicElementData::GraphicGroup(_graphic_group) => todo!("Mutable transform of graphic group"),
GraphicElementData::Artboard(_artboard) => todo!("Mutable transform of artboard"),
}
}
}
impl Transform for VectorData {
fn transform(&self) -> DAffine2 {

View file

@ -453,9 +453,7 @@ impl NodeNetwork {
return true;
}
// Get the outputs
let Some(mut stack) = self.outputs.iter().map(|&output| self.nodes.get(&output.node_id)).collect::<Option<Vec<_>>>() else {
return false;
};
let mut stack = self.outputs.iter().filter_map(|&output| self.nodes.get(&output.node_id)).collect::<Vec<_>>();
let mut already_visited = HashSet::new();
already_visited.extend(self.outputs.iter().map(|output| output.node_id));

View file

@ -328,7 +328,7 @@ async fn create_compute_pass_descriptor<T: Clone + Pixel + StaticTypeSized>(
#[cfg(feature = "quantization")]
buffers: vec![width_uniform.clone(), storage_buffer.clone(), quantization_uniform.clone()],
#[cfg(not(feature = "quantization"))]
buffers: vec![width_uniform.clone(), storage_buffer.clone()],
buffers: vec![width_uniform, storage_buffer],
};
let shader = gpu_executor::Shader {
@ -343,13 +343,13 @@ async fn create_compute_pass_descriptor<T: Clone + Pixel + StaticTypeSized>(
shader: shader.into(),
entry_point: "eval".to_string(),
bind_group: bind_group.into(),
output_buffer: output_buffer.clone(),
output_buffer,
};
log::debug!("created pipeline");
Ok(ComputePass {
pipeline_layout: pipeline,
readback_buffer: Some(readback_buffer.clone()),
readback_buffer: Some(readback_buffer),
})
}
/*

View file

@ -318,7 +318,7 @@ fn node_registry() -> HashMap<NodeIdentifier, HashMap<NodeIOTypes, NodeConstruct
)],
register_node!(graphene_std::raster::EmptyImageNode<_, _>, input: DAffine2, params: [Color]),
register_node!(graphene_core::memo::MonitorNode<_>, input: ImageFrame<Color>, params: []),
register_node!(graphene_core::memo::MonitorNode<_>, input: graphene_core::GraphicGroup, params: []),
register_node!(graphene_core::memo::MonitorNode<_>, input: graphene_core::GraphicElementData, params: []),
async_node!(graphene_std::wasm_application_io::LoadResourceNode<_>, input: WasmEditorApi, output: Arc<[u8]>, params: [String]),
register_node!(graphene_std::wasm_application_io::DecodeImageNode, input: Arc<[u8]>, params: []),
async_node!(graphene_std::wasm_application_io::CreateSurfaceNode, input: WasmEditorApi, output: Arc<SurfaceHandle<<graphene_std::wasm_application_io::WasmApplicationIo as graphene_core::application_io::ApplicationIo>::Surface>>, params: []),
@ -653,10 +653,11 @@ fn node_registry() -> HashMap<NodeIdentifier, HashMap<NodeIOTypes, NodeConstruct
register_node!(graphene_core::text::TextGenerator<_, _, _>, input: WasmEditorApi, params: [String, graphene_core::text::Font, f64]),
register_node!(graphene_std::brush::VectorPointsNode, input: VectorData, params: []),
register_node!(graphene_core::ExtractImageFrame, input: WasmEditorApi, params: []),
register_node!(graphene_core::ConstructLayerNode<_, _, _, _, _, _, _>, input: graphene_core::vector::VectorData, params: [String, BlendMode, f32, bool, bool, bool, graphene_core::GraphicGroup]),
register_node!(graphene_core::ConstructLayerNode<_, _, _, _, _, _, _>, input: ImageFrame<Color>, params: [String, BlendMode, f32, bool, bool, bool, graphene_core::GraphicGroup]),
register_node!(graphene_core::ConstructLayerNode<_, _, _, _, _, _, _>, input: graphene_core::GraphicGroup, params: [String, BlendMode, f32, bool, bool, bool, graphene_core::GraphicGroup]),
register_node!(graphene_core::ConstructLayerNode<_, _, _, _, _, _, _>, input: graphene_core::Artboard, params: [String, BlendMode, f32, bool, bool, bool, graphene_core::GraphicGroup]),
register_node!(graphene_core::ConstructLayerNode<_, _, _, _, _, _, _>, input: graphene_core::GraphicElementData, params: [String, BlendMode, f32, bool, bool, bool, graphene_core::GraphicGroup]),
register_node!(graphene_core::ToGraphicElementData, input: graphene_core::vector::VectorData, params: []),
register_node!(graphene_core::ToGraphicElementData, input: ImageFrame<Color>, params: []),
register_node!(graphene_core::ToGraphicElementData, input: graphene_core::GraphicGroup, params: []),
register_node!(graphene_core::ToGraphicElementData, input: graphene_core::Artboard, params: []),
register_node!(graphene_core::ConstructArtboardNode<_, _, _, _>, input: graphene_core::GraphicGroup, params: [glam::IVec2, glam::IVec2, Color, bool]),
];
let mut map: HashMap<NodeIdentifier, HashMap<NodeIOTypes, NodeConstructor>> = HashMap::new();