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 265219c6..66487425 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 @@ -2170,6 +2170,27 @@ fn static_nodes() -> Vec { properties: node_properties::circular_repeat_properties, ..Default::default() }, + DocumentNodeType { + name: "Resample Points", + category: "Vector", + identifier: NodeImplementation::proto("graphene_core::vector::ResamplePoints<_>"), + inputs: vec![ + DocumentInputType::value("Vector Data", TaggedValue::VectorData(graphene_core::vector::VectorData::empty()), true), + DocumentInputType::value("Spacing", TaggedValue::F64(100.), false), + ], + outputs: vec![DocumentOutputType::new("Vector", FrontendGraphDataType::Subpath)], + properties: node_properties::resample_points_properties, + ..Default::default() + }, + DocumentNodeType { + name: "Spline from Points", + category: "Vector", + identifier: NodeImplementation::proto("graphene_core::vector::SplineFromPointsNode"), + inputs: vec![DocumentInputType::value("Vector Data", TaggedValue::VectorData(graphene_core::vector::VectorData::empty()), true)], + outputs: vec![DocumentOutputType::new("Vector", FrontendGraphDataType::Subpath)], + properties: node_properties::no_properties, + ..Default::default() + }, DocumentNodeType { name: "Image Segmentation", category: "Image Adjustments", diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs index 8c3e929f..ed30ca2d 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs @@ -1868,6 +1868,12 @@ pub fn circular_repeat_properties(document_node: &DocumentNode, node_id: NodeId, vec![LayoutGroup::Row { widgets: angle_offset }, LayoutGroup::Row { widgets: radius }, LayoutGroup::Row { widgets: count }] } +pub fn resample_points_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { + let spacing = number_widget(document_node, node_id, 1, "Spacing", NumberInput::default().min(1.), true); + + vec![LayoutGroup::Row { widgets: spacing }] +} + /// Fill Node Widgets LayoutGroup pub fn fill_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { let fill_type_index = 1; diff --git a/libraries/bezier-rs/src/bezier/lookup.rs b/libraries/bezier-rs/src/bezier/lookup.rs index 68c45754..5706514a 100644 --- a/libraries/bezier-rs/src/bezier/lookup.rs +++ b/libraries/bezier-rs/src/bezier/lookup.rs @@ -6,6 +6,13 @@ use super::*; impl Bezier { /// Convert a euclidean distance ratio along the `Bezier` curve to a parametric `t`-value. pub fn euclidean_to_parametric(&self, ratio: f64, error: f64) -> f64 { + if ratio < error { + return 0.; + } + if 1. - ratio < error { + return 1.; + } + let mut low = 0.; let mut mid = 0.; let mut high = 1.; diff --git a/libraries/bezier-rs/src/subpath/core.rs b/libraries/bezier-rs/src/subpath/core.rs index 3abf507a..6798bf8b 100644 --- a/libraries/bezier-rs/src/subpath/core.rs +++ b/libraries/bezier-rs/src/subpath/core.rs @@ -112,6 +112,11 @@ impl Subpath { &self.manipulator_groups } + /// Returns a vector of all the anchors (DVec2) for this `Subpath`. + pub fn anchors(&self) -> Vec { + self.manipulator_groups().iter().map(|group| group.anchor).collect() + } + /// Returns if the Subpath is equivalent to a single point. pub fn is_point(&self) -> bool { if self.is_empty() { @@ -254,7 +259,7 @@ impl Subpath { /// Construct a cubic spline from a list of points. /// Based on . pub fn new_cubic_spline(points: Vec) -> Self { - if points.is_empty() { + if points.len() < 2 { return Self::new(Vec::new(), false); } diff --git a/libraries/bezier-rs/src/subpath/lookup.rs b/libraries/bezier-rs/src/subpath/lookup.rs index 81b89e3f..acc0e306 100644 --- a/libraries/bezier-rs/src/subpath/lookup.rs +++ b/libraries/bezier-rs/src/subpath/lookup.rs @@ -252,4 +252,16 @@ mod tests { assert_eq!(closed_subpath.t_value_to_parametric(SubpathTValue::GlobalParametric(0.)), (0, 0.)); assert_eq!(closed_subpath.t_value_to_parametric(SubpathTValue::GlobalParametric(1.)), (4, 1.)); } + + #[test] + fn exact_start_end() { + let start = DVec2::new(20., 30.); + let end = DVec2::new(60., 45.); + let handle = DVec2::new(75., 85.); + + let subpath: Subpath = Subpath::from_bezier(&Bezier::from_quadratic_dvec2(start, handle, end)); + + assert_eq!(subpath.evaluate(SubpathTValue::GlobalEuclidean(0.0)), start); + assert_eq!(subpath.evaluate(SubpathTValue::GlobalEuclidean(1.0)), end); + } } diff --git a/node-graph/gcore/src/vector/vector_nodes.rs b/node-graph/gcore/src/vector/vector_nodes.rs index 87f653e2..50e7a580 100644 --- a/node-graph/gcore/src/vector/vector_nodes.rs +++ b/node-graph/gcore/src/vector/vector_nodes.rs @@ -2,9 +2,9 @@ use super::style::{Fill, FillType, Gradient, GradientType, Stroke}; use super::VectorData; use crate::{Color, Node}; -use bezier_rs::Subpath; - +use bezier_rs::{Subpath, SubpathTValue}; use glam::{DAffine2, DVec2}; +use num_traits::Zero; #[derive(Debug, Clone, Copy)] pub struct SetFillNode { @@ -147,3 +147,41 @@ fn generate_bounding_box(vector_data: VectorData) -> VectorData { vector_data.transform.transform_point2(bounding_box[1]), )]) } + +#[derive(Debug, Clone, Copy)] +pub struct ResamplePoints { + spacing: Spacing, +} + +#[node_macro::node_fn(ResamplePoints)] +fn resample_points(mut vector_data: VectorData, spacing: f64) -> VectorData { + for subpath in &mut vector_data.subpaths { + if subpath.is_empty() || spacing.is_zero() || !spacing.is_finite() { + continue; + } + + subpath.apply_transform(vector_data.transform); + let length = subpath.length(None); + let rounded_count = (length / spacing).round(); + + if rounded_count >= 1. { + let new_anchors = (0..=rounded_count as usize).map(|c| subpath.evaluate(SubpathTValue::GlobalEuclidean(c as f64 / rounded_count))); + *subpath = Subpath::from_anchors(new_anchors, subpath.closed() && rounded_count as usize > 1); + } + + subpath.apply_transform(vector_data.transform.inverse()); + } + vector_data +} + +#[derive(Debug, Clone, Copy)] +pub struct SplineFromPointsNode {} + +#[node_macro::node_fn(SplineFromPointsNode)] +fn spline_from_points(mut vector_data: VectorData) -> VectorData { + for subpath in &mut vector_data.subpaths { + *subpath = Subpath::new_cubic_spline(subpath.anchors()); + } + + vector_data +} diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index 662714f7..484568c6 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -634,6 +634,8 @@ fn node_registry() -> HashMap, input: VectorData, params: [DVec2, u32]), register_node!(graphene_core::vector::BoundingBoxNode, input: VectorData, params: []), register_node!(graphene_core::vector::CircularRepeatNode<_, _, _>, input: VectorData, params: [f32, f32, u32]), + register_node!(graphene_core::vector::ResamplePoints<_>, input: VectorData, params: [f64]), + register_node!(graphene_core::vector::SplineFromPointsNode, input: VectorData, params: []), register_node!(graphene_core::vector::generator_nodes::CircleGenerator<_>, input: (), params: [f32]), register_node!(graphene_core::vector::generator_nodes::EllipseGenerator<_, _>, input: (), params: [f32, f32]), register_node!(graphene_core::vector::generator_nodes::RectangleGenerator<_, _>, input: (), params: [f32, f32]),