New node: Solidify Stroke (#1650)

* added struct and registered fill from stroke node. not functional yet.

* properly registered new node

* finished fill-from-stroke feature. works on both closed and open shapes. supports line caps and line joins.

* fixed import issues and miter limit handling

* fix outline function. use both subpaths for solid shapes now

* code quality improvements

* use radius instead of diameter, functionality not yet working for shapes as transform is not properly applied before calculating new shapes.

* Updated to follow new VectorData structure. Fully functional.

* fix repeat node

* remove comment

* add a test, remove unused code.
This commit is contained in:
Roshan P 2024-03-11 20:19:29 -04:00 committed by GitHub
parent c3a886116a
commit 2263b0ab89
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 85 additions and 4 deletions

View File

@ -2617,6 +2617,15 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
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",

View File

@ -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<Color>) -> 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 {

View File

@ -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<Direction, Count> {
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.),

View File

@ -704,6 +704,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
register_node!(graphene_core::vector::SetStrokeNode<_, _, _, _, _, _, _>, input: VectorData, params: [Option<graphene_core::Color>, f64, Vec<f64>, 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<_>"),