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 3edcc050..eedbf06d 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 @@ -2580,7 +2580,7 @@ fn static_nodes() -> Vec { DocumentInputType::value("Start", TaggedValue::DVec2(DVec2::new(0., 0.5)), false), DocumentInputType::value("End", TaggedValue::DVec2(DVec2::new(1., 0.5)), false), DocumentInputType::value("Transform", TaggedValue::DAffine2(DAffine2::IDENTITY), false), - DocumentInputType::value("Positions", TaggedValue::GradientPositions(vec![(0., Some(Color::BLACK)), (1., Some(Color::WHITE))]), false), + DocumentInputType::value("Positions", TaggedValue::GradientPositions(vec![(0., Color::BLACK), (1., Color::WHITE)]), false), ], outputs: vec![DocumentOutputType::new("Vector", FrontendGraphDataType::Subpath)], properties: node_properties::fill_properties, @@ -2715,6 +2715,21 @@ fn static_nodes() -> Vec { properties: node_properties::node_no_properties, ..Default::default() }, + DocumentNodeDefinition { + name: "Morph", + category: "Vector", + implementation: NodeImplementation::proto("graphene_core::vector::MorphNode<_, _, _, _>"), + inputs: vec![ + DocumentInputType::value("Source", TaggedValue::VectorData(graphene_core::vector::VectorData::empty()), true), + DocumentInputType::value("Target", TaggedValue::VectorData(graphene_core::vector::VectorData::empty()), true), + DocumentInputType::value("Start Index", TaggedValue::U32(0), false), + DocumentInputType::value("Time", TaggedValue::F64(0.5), false), + ], + outputs: vec![DocumentOutputType::new("Vector", FrontendGraphDataType::Subpath)], + manual_composition: Some(concrete!(Footprint)), + properties: node_properties::morph_properties, + ..Default::default() + }, // TODO: This needs to work with resolution-aware (raster with footprint, post-Cull node) data. DocumentNodeDefinition { name: "Image Segmentation", 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 653c9291..3720c31e 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 @@ -623,18 +623,18 @@ fn gradient_type_widget(document_node: &DocumentNode, node_id: NodeId, index: us LayoutGroup::Row { widgets } } -fn gradient_row(row: &mut Vec, positions: &Vec<(f64, Option)>, index: usize, node_id: NodeId, input_index: usize) { +fn gradient_row(row: &mut Vec, positions: &Vec<(f64, Color)>, index: usize, node_id: NodeId, input_index: usize) { let label = TextLabel::new(format!("Gradient: {:.0}%", positions[index].0 * 100.)).tooltip("Adjustable by dragging the gradient stops in the viewport with the Gradient tool active"); row.push(label.widget_holder()); let on_update = { let positions = positions.clone(); move |color_button: &ColorButton| { let mut new_positions = positions.clone(); - new_positions[index].1 = color_button.value; + new_positions[index].1 = color_button.value.unwrap(); TaggedValue::GradientPositions(new_positions) } }; - let color = ColorButton::new(positions[index].1).on_update(update_value(on_update, node_id, input_index)); + let color = ColorButton::new(Some(positions[index].1)).on_update(update_value(on_update, node_id, input_index)).allow_none(false); add_blank_assist(row); row.push(Separator::new(SeparatorType::Unrelated).widget_holder()); row.push(color.widget_holder()); @@ -668,10 +668,9 @@ fn gradient_row(row: &mut Vec, positions: &Vec<(f64, Option let mut new_positions = positions.clone(); // Blend linearly between the two colors. - let get_color = |index: usize| match (new_positions[index].1, new_positions.get(index + 1).and_then(|x| x.1)) { - (Some(a), Some(b)) => Color::from_rgbaf32((a.r() + b.r()) / 2., (a.g() + b.g()) / 2., (a.b() + b.b()) / 2., ((a.a() + b.a()) / 2.).clamp(0., 1.)), - (Some(v), _) | (_, Some(v)) => Some(v), - _ => Some(Color::WHITE), + let get_color = |index: usize| match (new_positions[index].1, new_positions.get(index + 1).map(|x| x.1)) { + (a, Some(b)) => Color::from_rgbaf32_unchecked((a.r() + b.r()) / 2., (a.g() + b.g()) / 2., (a.b() + b.b()) / 2., ((a.a() + b.a()) / 2.).clamp(0., 1.)), + (_, None) => Color::WHITE, }; let get_pos = |index: usize| (new_positions[index].0 + new_positions.get(index + 1).map(|v| v.0).unwrap_or(1.)) / 2.; @@ -2056,6 +2055,16 @@ pub fn sample_points_properties(document_node: &DocumentNode, node_id: NodeId, _ ] } +pub fn morph_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { + let start_index = number_widget(document_node, node_id, 2, "Start Index", NumberInput::default().min(0.), true); + let time = number_widget(document_node, node_id, 3, "Time", NumberInput::default().min(0.).max(1.).mode_range(), true); + + vec![ + LayoutGroup::Row { widgets: start_index }.with_tooltip("The index of point on the target that morphs to the first point of the source"), + LayoutGroup::Row { widgets: time }.with_tooltip("Linear time of transition - 0. is source, 1. is target"), + ] +} + /// 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/editor/src/messages/tool/tool_messages/gradient_tool.rs b/editor/src/messages/tool/tool_messages/gradient_tool.rs index d2ff888a..f4f4bac7 100644 --- a/editor/src/messages/tool/tool_messages/gradient_tool.rs +++ b/editor/src/messages/tool/tool_messages/gradient_tool.rs @@ -5,7 +5,6 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye use crate::messages::tool::common_functionality::graph_modification_utils::get_gradient; use crate::messages::tool::common_functionality::snapping::SnapManager; -use graphene_core::raster::color::Color; use graphene_core::vector::style::{Fill, Gradient, GradientType}; #[derive(Default)] @@ -335,7 +334,7 @@ impl Fsm for GradientToolFsmState { if selected_gradient.gradient.positions.len() == 1 { responses.add(GraphOperationMessage::FillSet { layer: selected_gradient.layer, - fill: Fill::Solid(selected_gradient.gradient.positions[0].1.unwrap_or(Color::BLACK)), + fill: Fill::Solid(selected_gradient.gradient.positions[0].1), }); return self; } @@ -344,7 +343,7 @@ impl Fsm for GradientToolFsmState { let min_position = selected_gradient.gradient.positions.iter().map(|(pos, _)| *pos).reduce(f64::min).expect("No min"); let max_position = selected_gradient.gradient.positions.iter().map(|(pos, _)| *pos).reduce(f64::max).expect("No max"); - // Recompute the start and end posiiton of the gradient (in viewport transform) + // Recompute the start and end position of the gradient (in viewport transform) let transform = selected_gradient.transform; let (start, end) = (transform.transform_point2(selected_gradient.gradient.start), transform.transform_point2(selected_gradient.gradient.end)); let (new_start, new_end) = (start.lerp(end, min_position), start.lerp(end, max_position)); diff --git a/libraries/bezier-rs/src/subpath/core.rs b/libraries/bezier-rs/src/subpath/core.rs index 437a21d2..595ee0db 100644 --- a/libraries/bezier-rs/src/subpath/core.rs +++ b/libraries/bezier-rs/src/subpath/core.rs @@ -112,7 +112,7 @@ impl Subpath { &self.manipulator_groups } - /// Returns a mutable vec of the [ManipulatorGroup]s in the `Subpath`. + /// Returns a mutable reference to the [ManipulatorGroup]s in the `Subpath`. pub fn manipulator_groups_mut(&mut self) -> &mut Vec> { &mut self.manipulator_groups } diff --git a/libraries/bezier-rs/src/subpath/structs.rs b/libraries/bezier-rs/src/subpath/structs.rs index 8b0ce22f..9bb753b2 100644 --- a/libraries/bezier-rs/src/subpath/structs.rs +++ b/libraries/bezier-rs/src/subpath/structs.rs @@ -111,6 +111,12 @@ impl ManipulatorGroup pub fn is_finite(&self) -> bool { self.anchor.is_finite() && self.in_handle.map_or(true, |handle| handle.is_finite()) && self.out_handle.map_or(true, |handle| handle.is_finite()) } + + /// Reverse directions of handles + pub fn flip(mut self) -> Self { + std::mem::swap(&mut self.in_handle, &mut self.out_handle); + self + } } #[derive(Copy, Clone)] diff --git a/node-graph/gcore/src/raster/color.rs b/node-graph/gcore/src/raster/color.rs index 311b0e7c..f3005709 100644 --- a/node-graph/gcore/src/raster/color.rs +++ b/node-graph/gcore/src/raster/color.rs @@ -853,7 +853,7 @@ impl Color { /// /// T must be between 0 and 1. #[inline(always)] - pub fn lerp(self, other: Color, t: f32) -> Self { + pub fn lerp(&self, other: &Color, t: f32) -> Self { assert!((0. ..=1.).contains(&t)); Color::from_rgbaf32_unchecked( self.red + ((other.red - self.red) * t), diff --git a/node-graph/gcore/src/vector/style.rs b/node-graph/gcore/src/vector/style.rs index e6b17821..6e24bd36 100644 --- a/node-graph/gcore/src/vector/style.rs +++ b/node-graph/gcore/src/vector/style.rs @@ -36,9 +36,10 @@ pub struct Gradient { pub start: DVec2, pub end: DVec2, pub transform: DAffine2, - pub positions: Vec<(f64, Option)>, + pub positions: Vec<(f64, Color)>, pub gradient_type: GradientType, } + impl core::hash::Hash for Gradient { fn hash(&self, state: &mut H) { self.positions.len().hash(state); @@ -52,18 +53,44 @@ impl core::hash::Hash for Gradient { self.gradient_type.hash(state); } } + impl Gradient { /// Constructs a new gradient with the colors at 0 and 1 specified. pub fn new(start: DVec2, start_color: Color, end: DVec2, end_color: Color, transform: DAffine2, gradient_type: GradientType) -> Self { Gradient { start, end, - positions: vec![(0., Some(start_color)), (1., Some(end_color))], + positions: vec![(0., start_color), (1., end_color)], transform, gradient_type, } } + pub fn lerp(&self, other: &Self, time: f64) -> Self { + let start = self.start + (other.start - self.start) * time; + let end = self.end + (other.end - self.end) * time; + let transform = self.transform; + let positions = self + .positions + .iter() + .zip(other.positions.iter()) + .map(|((a_pos, a_color), (b_pos, b_color))| { + let position = a_pos + (b_pos - a_pos) * time; + let color = a_color.lerp(b_color, time as f32); + (position, color) + }) + .collect::>(); + let gradient_type = if time < 0.5 { self.gradient_type } else { other.gradient_type }; + + Self { + start, + end, + transform, + positions, + gradient_type, + } + } + /// Adds the gradient def through mutating the first argument, returning the gradient ID. fn render_defs(&self, svg_defs: &mut String, multiplied_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2]) -> u64 { let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]); @@ -71,7 +98,7 @@ impl Gradient { let updated_transform = multiplied_transform * bound_transform; let mut positions = String::new(); - for (position, color) in self.positions.iter().filter_map(|(pos, color)| color.map(|color| (pos, color))) { + for (position, color) in self.positions.iter() { let _ = write!(positions, r##""##, position, color.with_alpha(color.a()).rgba_hex()); } @@ -124,15 +151,14 @@ impl Gradient { } // Compute the color of the inserted stop - let get_color = |index: usize, time: f64| match (self.positions[index].1, self.positions.get(index + 1).and_then(|x| x.1)) { + let get_color = |index: usize, time: f64| match (self.positions[index].1, self.positions.get(index + 1).map(|(_, c)| *c)) { // Lerp between the nearest colors if applicable - (Some(a), Some(b)) => a.lerp( - b, + (a, Some(b)) => a.lerp( + &b, ((time - self.positions[index].0) / self.positions.get(index + 1).map(|end| end.0 - self.positions[index].0).unwrap_or_default()) as f32, ), // Use the start or the end color if applicable - (Some(v), _) | (_, Some(v)) => v, - _ => Color::WHITE, + (v, _) => v, }; // Compute the correct index to keep the positions in order @@ -144,7 +170,7 @@ impl Gradient { let new_color = get_color(index - 1, new_position); // Insert the new stop - self.positions.insert(index, (new_position, Some(new_color))); + self.positions.insert(index, (new_position, new_color)); Some(index) } @@ -174,7 +200,31 @@ impl Fill { Self::None => Color::BLACK, Self::Solid(color) => *color, // TODO: Should correctly sample the gradient - Self::Gradient(Gradient { positions, .. }) => positions[0].1.unwrap_or(Color::BLACK), + Self::Gradient(Gradient { positions, .. }) => positions[0].1, + } + } + + pub fn lerp(&self, other: &Self, time: f64) -> Self { + let transparent = Self::solid(Color::TRANSPARENT); + let a = if *self == Self::None { &transparent } else { self }; + let b = if *other == Self::None { &transparent } else { other }; + + match (a, b) { + (Self::Solid(a), Self::Solid(b)) => Self::Solid(a.lerp(b, time as f32)), + (Self::Solid(a), Self::Gradient(b)) => { + let mut solid_to_gradient = b.clone(); + solid_to_gradient.positions.iter_mut().for_each(|(_, color)| *color = *a); + let a = &solid_to_gradient; + Self::Gradient(a.lerp(b, time)) + } + (Self::Gradient(a), Self::Solid(b)) => { + let mut gradient_to_solid = a.clone(); + gradient_to_solid.positions.iter_mut().for_each(|(_, color)| *color = *b); + let b = &gradient_to_solid; + Self::Gradient(a.lerp(b, time)) + } + (Self::Gradient(a), Self::Gradient(b)) => Self::Gradient(a.lerp(b, time)), + _ => Self::None, } } @@ -290,6 +340,18 @@ impl Stroke { } } + pub fn lerp(&self, other: &Self, time: f64) -> Self { + Self { + color: self.color.map(|color| color.lerp(&other.color.unwrap_or(color), time as f32)), + weight: self.weight + (other.weight - self.weight) * time, + dash_lengths: self.dash_lengths.iter().zip(other.dash_lengths.iter()).map(|(a, b)| a + (b - a) * time as f32).collect(), + dash_offset: self.dash_offset + (other.dash_offset - self.dash_offset) * time, + line_cap: if time < 0.5 { self.line_cap } else { other.line_cap }, + line_join: if time < 0.5 { self.line_join } else { other.line_join }, + line_join_miter_limit: self.line_join_miter_limit + (other.line_join_miter_limit - self.line_join_miter_limit) * time, + } + } + /// Get the current stroke color. pub fn color(&self) -> Option { self.color @@ -422,6 +484,30 @@ impl PathStyle { Self { stroke, fill } } + pub fn lerp(&self, other: &Self, time: f64) -> Self { + Self { + fill: self.fill.lerp(&other.fill, time), + stroke: match (self.stroke.as_ref(), other.stroke.as_ref()) { + (Some(a), Some(b)) => Some(a.lerp(b, time)), + (Some(a), None) => { + if time < 0.5 { + Some(a.clone()) + } else { + None + } + } + (None, Some(b)) => { + if time < 0.5 { + Some(b.clone()) + } else { + None + } + } + (None, None) => None, + }, + } + } + /// Get the current path's [Fill]. /// /// # Example diff --git a/node-graph/gcore/src/vector/vector_nodes.rs b/node-graph/gcore/src/vector/vector_nodes.rs index 0a8d97d1..53b1833b 100644 --- a/node-graph/gcore/src/vector/vector_nodes.rs +++ b/node-graph/gcore/src/vector/vector_nodes.rs @@ -5,7 +5,7 @@ use crate::transform::{Footprint, Transform, TransformMut}; use crate::{Color, GraphicGroup, Node}; use core::future::Future; -use bezier_rs::{Subpath, TValue}; +use bezier_rs::{Subpath, SubpathTValue, TValue}; use glam::{DAffine2, DVec2}; #[derive(Debug, Clone, Copy)] @@ -28,7 +28,7 @@ fn set_vector_data_fill( start: DVec2, end: DVec2, transform: DAffine2, - positions: Vec<(f64, Option)>, + positions: Vec<(f64, Color)>, ) -> VectorData { vector_data.style.set_fill(match fill_type { FillType::Solid => solid_color.map_or(Fill::None, Fill::Solid), @@ -309,3 +309,98 @@ fn splines_from_points(mut vector_data: VectorData) -> VectorData { vector_data } + +pub struct MorphNode { + source: Source, + target: Target, + start_index: StartIndex, + time: Time, +} + +#[node_macro::node_fn(MorphNode)] +async fn morph, TargetFuture: Future>( + footprint: Footprint, + source: impl Node, + target: impl Node, + start_index: u32, + time: f64, +) -> VectorData { + let mut source = self.source.eval(footprint).await; + let mut target = self.target.eval(footprint).await; + + // Lerp styles + let style = source.style.lerp(&target.style, time); + + for (source_path, target_path) in source.subpaths.iter_mut().zip(target.subpaths.iter_mut()) { + // Deal with mistmatched transforms + source_path.apply_transform(source.transform); + target_path.apply_transform(target.transform); + + // Deal with mismatched start index + for _ in 0..start_index { + let first = target_path.remove_manipulator_group(0); + target_path.push_manipulator_group(first); + } + + // Deal with mismatched closed state + if source_path.closed() && !target_path.closed() { + source_path.set_closed(false); + source_path.push_manipulator_group(source_path.manipulator_groups()[0].flip()); + } + if !source_path.closed() && target_path.closed() { + target_path.set_closed(false); + target_path.push_manipulator_group(target_path.manipulator_groups()[0].flip()); + } + + // Mismatched subpath items + 'outer: loop { + for segment_index in (0..(source_path.len() - 1)).rev() { + if target_path.len() <= source_path.len() { + break 'outer; + } + source_path.insert(SubpathTValue::Parametric { segment_index, t: 0.5 }) + } + } + 'outer: loop { + for segment_index in (0..(target_path.len() - 1)).rev() { + if source_path.len() <= target_path.len() { + break 'outer; + } + target_path.insert(SubpathTValue::Parametric { segment_index, t: 0.5 }) + } + } + } + // Mismatched subpath count + for source_path in source.subpaths.iter_mut().skip(target.subpaths.len()) { + source_path.apply_transform(source.transform); + target.subpaths.push(Subpath::from_anchors( + std::iter::repeat(source_path.manipulator_groups().first().map(|group| group.anchor).unwrap_or_default()).take(source_path.len()), + source_path.closed, + )) + } + for target_path in target.subpaths.iter_mut().skip(source.subpaths.len()) { + target_path.apply_transform(target.transform); + source.subpaths.push(Subpath::from_anchors( + std::iter::repeat(target_path.manipulator_groups().first().map(|group| group.anchor).unwrap_or_default()).take(target_path.len()), + target_path.closed, + )) + } + + // Lerp points + for (subpath, target) in source.subpaths.iter_mut().zip(target.subpaths.iter()) { + for (manipulator, target) in subpath.manipulator_groups_mut().iter_mut().zip(target.manipulator_groups()) { + manipulator.in_handle = Some(manipulator.in_handle.unwrap_or(manipulator.anchor).lerp(target.in_handle.unwrap_or(target.anchor), time)); + manipulator.out_handle = Some(manipulator.out_handle.unwrap_or(manipulator.anchor).lerp(target.out_handle.unwrap_or(target.anchor), time)); + manipulator.anchor = manipulator.anchor.lerp(target.anchor, time); + } + } + + // Create result + let subpaths = std::mem::take(&mut source.subpaths); + let mut current = if time < 0.5 { source } else { target }; + current.style = style; + current.subpaths = subpaths; + current.transform = DAffine2::IDENTITY; + + current +} diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 8b12001c..badb9cad 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -56,7 +56,7 @@ pub enum TaggedValue { LineJoin(graphene_core::vector::style::LineJoin), FillType(graphene_core::vector::style::FillType), GradientType(graphene_core::vector::style::GradientType), - GradientPositions(Vec<(f64, Option)>), + GradientPositions(Vec<(f64, graphene_core::Color)>), Quantization(graphene_core::quantization::QuantizationChannels), OptionalColor(Option), ManipulatorGroupIds(Vec), @@ -273,7 +273,7 @@ impl<'a> TaggedValue { TaggedValue::LineJoin(_) => concrete!(graphene_core::vector::style::LineJoin), TaggedValue::FillType(_) => concrete!(graphene_core::vector::style::FillType), TaggedValue::GradientType(_) => concrete!(graphene_core::vector::style::GradientType), - TaggedValue::GradientPositions(_) => concrete!(Vec<(f64, Option)>), + TaggedValue::GradientPositions(_) => concrete!(Vec<(f64, graphene_core::Color)>), TaggedValue::Quantization(_) => concrete!(graphene_core::quantization::QuantizationChannels), TaggedValue::OptionalColor(_) => concrete!(Option), TaggedValue::ManipulatorGroupIds(_) => concrete!(Vec), @@ -337,7 +337,7 @@ impl<'a> TaggedValue { x if x == TypeId::of::() => Ok(TaggedValue::LineJoin(*downcast(input).unwrap())), x if x == TypeId::of::() => Ok(TaggedValue::FillType(*downcast(input).unwrap())), x if x == TypeId::of::() => Ok(TaggedValue::GradientType(*downcast(input).unwrap())), - x if x == TypeId::of::)>>() => Ok(TaggedValue::GradientPositions(*downcast(input).unwrap())), + x if x == TypeId::of::>() => Ok(TaggedValue::GradientPositions(*downcast(input).unwrap())), x if x == TypeId::of::() => Ok(TaggedValue::Quantization(*downcast(input).unwrap())), x if x == TypeId::of::>() => Ok(TaggedValue::OptionalColor(*downcast(input).unwrap())), x if x == TypeId::of::>() => Ok(TaggedValue::ManipulatorGroupIds(*downcast(input).unwrap())), diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index 7efbe7dc..12e36dfa 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -685,7 +685,7 @@ fn node_registry() -> HashMap, input: ImageFrame, params: [ImageFrame]), register_node!(graphene_core::transform::SetTransformNode<_>, input: VectorData, params: [DAffine2]), register_node!(graphene_core::transform::SetTransformNode<_>, input: ImageFrame, params: [DAffine2]), - register_node!(graphene_core::vector::SetFillNode<_, _, _, _, _, _, _>, input: VectorData, params: [graphene_core::vector::style::FillType, Option, graphene_core::vector::style::GradientType, DVec2, DVec2, DAffine2, Vec<(f64, Option)>]), + register_node!(graphene_core::vector::SetFillNode<_, _, _, _, _, _, _>, input: VectorData, params: [graphene_core::vector::style::FillType, Option, graphene_core::vector::style::GradientType, DVec2, DVec2, DAffine2, Vec<(f64, graphene_core::Color)>]), register_node!(graphene_core::vector::SetStrokeNode<_, _, _, _, _, _, _>, input: VectorData, params: [Option, f32, Vec, f32, graphene_core::vector::style::LineCap, graphene_core::vector::style::LineJoin, f32]), register_node!(graphene_core::vector::RepeatNode<_, _>, input: VectorData, params: [DVec2, u32]), register_node!(graphene_core::vector::BoundingBoxNode, input: VectorData, params: []), @@ -737,6 +737,7 @@ fn node_registry() -> HashMap, input: Footprint, output: VectorData, fn_params: [Footprint => VectorData, () => f32, () => f32, () => f32, () => bool, Footprint => Vec>]), register_node!(graphene_core::vector::LengthsOfSegmentsOfSubpaths, input: VectorData, params: []), register_node!(graphene_core::vector::SplinesFromPointsNode, input: VectorData, params: []), + async_node!(graphene_core::vector::MorphNode<_, _, _, _>, input: Footprint, output: VectorData, fn_params: [Footprint => VectorData, Footprint => VectorData, () => u32, () => f64]), 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]),