mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-10-05 08:28:13 +08:00
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:
+9
@@ -2617,6 +2617,15 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
|||||||
properties: node_properties::node_no_properties,
|
properties: node_properties::node_no_properties,
|
||||||
..Default::default()
|
..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 {
|
DocumentNodeDefinition {
|
||||||
name: "Repeat",
|
name: "Repeat",
|
||||||
category: "Vector",
|
category: "Vector",
|
||||||
|
|||||||
@@ -189,11 +189,19 @@ pub enum Fill {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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 {
|
pub fn solid(color: Color) -> Self {
|
||||||
Self::Solid(color)
|
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.
|
/// Evaluate the color at some point on the fill. Doesn't currently work for Gradient.
|
||||||
pub fn color(&self) -> Color {
|
pub fn color(&self) -> Color {
|
||||||
match self {
|
match self {
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ use super::style::{Fill, FillType, Gradient, GradientType, Stroke};
|
|||||||
use super::{PointId, SegmentId, StrokeId, VectorData};
|
use super::{PointId, SegmentId, StrokeId, VectorData};
|
||||||
use crate::renderer::GraphicElementRendered;
|
use crate::renderer::GraphicElementRendered;
|
||||||
use crate::transform::{Footprint, Transform, TransformMut};
|
use crate::transform::{Footprint, Transform, TransformMut};
|
||||||
|
use crate::uuid::ManipulatorGroupId;
|
||||||
use crate::{Color, GraphicGroup, Node};
|
use crate::{Color, GraphicGroup, Node};
|
||||||
use core::future::Future;
|
use core::future::Future;
|
||||||
|
|
||||||
use bezier_rs::{Subpath, SubpathTValue, TValue};
|
use bezier_rs::{Cap, Join, Subpath, SubpathTValue, TValue};
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
use rand::{Rng, SeedableRng};
|
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 {
|
fn repeat_vector_data(vector_data: VectorData, direction: DVec2, count: u32) -> VectorData {
|
||||||
// Repeat the vector data
|
// Repeat the vector data
|
||||||
let mut result = VectorData::empty();
|
let mut result = VectorData::empty();
|
||||||
let inverse = vector_data.transform.inverse();
|
|
||||||
let direction = inverse.transform_vector2(direction);
|
|
||||||
for i in 0..count {
|
for i in 0..count {
|
||||||
let transform = DAffine2::from_translation(direction * i as f64);
|
let transform = DAffine2::from_translation(direction * i as f64);
|
||||||
result.concat(&vector_data, transform);
|
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 {
|
pub trait ConcatElement {
|
||||||
fn concat(&mut self, other: &Self, transform: DAffine2);
|
fn concat(&mut self, other: &Self, transform: DAffine2);
|
||||||
}
|
}
|
||||||
@@ -493,6 +543,19 @@ mod test {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[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() {
|
fn circle_repeat() {
|
||||||
let repeated = CircularRepeatNode {
|
let repeated = CircularRepeatNode {
|
||||||
angle_offset: ClonedNode::new(45.),
|
angle_offset: ClonedNode::new(45.),
|
||||||
|
|||||||
@@ -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::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::RepeatNode<_, _>, input: VectorData, params: [DVec2, u32]),
|
||||||
register_node!(graphene_core::vector::BoundingBoxNode, input: VectorData, params: []),
|
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]),
|
register_node!(graphene_core::vector::CircularRepeatNode<_, _, _>, input: VectorData, params: [f64, f64, u32]),
|
||||||
vec![(
|
vec![(
|
||||||
ProtoNodeIdentifier::new("graphene_core::transform::CullNode<_>"),
|
ProtoNodeIdentifier::new("graphene_core::transform::CullNode<_>"),
|
||||||
|
|||||||
Reference in New Issue
Block a user