diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs index 037b1a91..ea2e3b3d 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs @@ -2617,6 +2617,15 @@ fn static_nodes() -> Vec { properties: node_properties::node_no_properties, ..Default::default() }, + DocumentNodeDefinition { + name: "Solidify Stroke", + category: "Vector", + implementation: DocumentNodeImplementation::proto("graphene_core::vector::SolidifyStrokeNode"), + inputs: vec![DocumentInputType::value("Vector Data", TaggedValue::VectorData(graphene_core::vector::VectorData::empty()), true)], + outputs: vec![DocumentOutputType::new("Vector", FrontendGraphDataType::Subpath)], + properties: node_properties::node_no_properties, + ..Default::default() + }, DocumentNodeDefinition { name: "Repeat", category: "Vector", diff --git a/node-graph/gcore/src/vector/style.rs b/node-graph/gcore/src/vector/style.rs index 9399324d..4fbabb4a 100644 --- a/node-graph/gcore/src/vector/style.rs +++ b/node-graph/gcore/src/vector/style.rs @@ -189,11 +189,19 @@ pub enum Fill { } impl Fill { - /// Construct a new solid [Fill] from a [Color]. + /// Construct a new [Fill::Solid] from a [Color]. pub fn solid(color: Color) -> Self { Self::Solid(color) } + /// Construct a new [Fill::Solid] or [Fill::None] from an optional [Color]. + pub fn solid_or_none(color: Option) -> Self { + match color { + Some(color) => Self::Solid(color), + None => Self::None, + } + } + /// Evaluate the color at some point on the fill. Doesn't currently work for Gradient. pub fn color(&self) -> Color { match self { diff --git a/node-graph/gcore/src/vector/vector_nodes.rs b/node-graph/gcore/src/vector/vector_nodes.rs index 5a171274..b6f54d3c 100644 --- a/node-graph/gcore/src/vector/vector_nodes.rs +++ b/node-graph/gcore/src/vector/vector_nodes.rs @@ -2,10 +2,11 @@ use super::style::{Fill, FillType, Gradient, GradientType, Stroke}; use super::{PointId, SegmentId, StrokeId, VectorData}; use crate::renderer::GraphicElementRendered; use crate::transform::{Footprint, Transform, TransformMut}; +use crate::uuid::ManipulatorGroupId; use crate::{Color, GraphicGroup, Node}; use core::future::Future; -use bezier_rs::{Subpath, SubpathTValue, TValue}; +use bezier_rs::{Cap, Join, Subpath, SubpathTValue, TValue}; use glam::{DAffine2, DVec2}; use rand::{Rng, SeedableRng}; @@ -88,8 +89,6 @@ pub struct RepeatNode { fn repeat_vector_data(vector_data: VectorData, direction: DVec2, count: u32) -> VectorData { // Repeat the vector data let mut result = VectorData::empty(); - let inverse = vector_data.transform.inverse(); - let direction = inverse.transform_vector2(direction); for i in 0..count { let transform = DAffine2::from_translation(direction * i as f64); result.concat(&vector_data, transform); @@ -136,6 +135,57 @@ fn generate_bounding_box(vector_data: VectorData) -> VectorData { )) } +#[derive(Debug, Clone, Copy)] +pub struct SolidifyStrokeNode; + +#[node_macro::node_fn(SolidifyStrokeNode)] +fn solidify_stroke(vector_data: VectorData) -> VectorData { + // Grab what we need from original data. + let VectorData { transform, style, .. } = &vector_data; + let subpaths = vector_data.stroke_bezier_paths(); + let mut result = VectorData::empty(); + + // Perform operation on all subpaths in this shape. + for mut subpath in subpaths { + let stroke = style.stroke().unwrap(); + let transform = transform.clone(); + subpath.apply_transform(transform); + + // Taking the existing stroke data and passing it to Bezier-rs to generate new paths. + let subpath_out = subpath.outline( + stroke.weight / 2., // Diameter to radius. + match stroke.line_join { + crate::vector::style::LineJoin::Miter => Join::Miter(Some(stroke.line_join_miter_limit)), + crate::vector::style::LineJoin::Bevel => Join::Bevel, + crate::vector::style::LineJoin::Round => Join::Round, + }, + match stroke.line_cap { + crate::vector::style::LineCap::Butt => Cap::Butt, + crate::vector::style::LineCap::Round => Cap::Round, + crate::vector::style::LineCap::Square => Cap::Square, + }, + ); + + // This is where we determine whether we have a closed or open path. Ex: Oval vs line segment. + if subpath_out.1.is_some() { + // Two closed subpaths, closed shape. Add both subpaths. + result.append_subpath(subpath_out.0); + result.append_subpath(subpath_out.1.unwrap()); + } else { + // One closed subpath, open path. + result.append_subpath(subpath_out.0); + } + } + + // We set our fill to our stroke's color, then clear our stroke. + if let Some(stroke) = vector_data.style.stroke() { + result.style.set_fill(Fill::solid_or_none(stroke.color)); + result.style.set_stroke(Stroke::default()); + } + + result +} + pub trait ConcatElement { fn concat(&mut self, other: &Self, transform: DAffine2); } @@ -493,6 +543,19 @@ mod test { } } #[test] + fn repeat_transform_position() { + let direction = DVec2::new(12., 10.); + let repeated = RepeatNode { + direction: ClonedNode::new(direction), + count: ClonedNode::new(8), + } + .eval(VectorData::from_subpath(Subpath::new_rect(DVec2::ZERO, DVec2::ONE))); + assert_eq!(repeated.region_bezier_paths().count(), 8); + for (index, (_, subpath)) in repeated.region_bezier_paths().enumerate() { + assert_eq!(subpath.manipulator_groups()[0].anchor, direction * index as f64); + } + } + #[test] fn circle_repeat() { let repeated = CircularRepeatNode { angle_offset: ClonedNode::new(45.), diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index ecc7b08d..94993af5 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -704,6 +704,7 @@ fn node_registry() -> HashMap, input: VectorData, params: [Option, f64, Vec, f64, graphene_core::vector::style::LineCap, graphene_core::vector::style::LineJoin, f64]), register_node!(graphene_core::vector::RepeatNode<_, _>, input: VectorData, params: [DVec2, u32]), register_node!(graphene_core::vector::BoundingBoxNode, input: VectorData, params: []), + register_node!(graphene_core::vector::SolidifyStrokeNode, input: VectorData, params: []), register_node!(graphene_core::vector::CircularRepeatNode<_, _, _>, input: VectorData, params: [f64, f64, u32]), vec![( ProtoNodeIdentifier::new("graphene_core::transform::CullNode<_>"),