Add transform-related nodes to improve transformation abilities (#2893)

* Improve transformation abilities with transform-related nodes

* Fix Transform -> Merge and Transform -> Artboard connections
This commit is contained in:
Keavon Chambers 2025-07-17 11:58:57 -07:00 committed by GitHub
parent 8f26c5c2ad
commit 561b671f8d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 257 additions and 29 deletions

View File

@ -1303,11 +1303,11 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
}, },
DocumentNodeDefinition { DocumentNodeDefinition {
identifier: "Transform", identifier: "Transform",
category: "General", category: "Math: Transform",
node_template: NodeTemplate { node_template: NodeTemplate {
document_node: DocumentNode { document_node: DocumentNode {
inputs: vec![ inputs: vec![
NodeInput::value(TaggedValue::VectorData(VectorDataTable::default()), true), NodeInput::value(TaggedValue::DAffine2(DAffine2::default()), true),
NodeInput::value(TaggedValue::DVec2(DVec2::ZERO), false), NodeInput::value(TaggedValue::DVec2(DVec2::ZERO), false),
NodeInput::value(TaggedValue::F64(0.), false), NodeInput::value(TaggedValue::F64(0.), false),
NodeInput::value(TaggedValue::DVec2(DVec2::ONE), false), NodeInput::value(TaggedValue::DVec2(DVec2::ONE), false),
@ -1317,7 +1317,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
exports: vec![NodeInput::node(NodeId(1), 0)], exports: vec![NodeInput::node(NodeId(1), 0)],
nodes: [ nodes: [
DocumentNode { DocumentNode {
inputs: vec![NodeInput::network(concrete!(VectorDataTable), 0)], inputs: vec![NodeInput::network(generic!(T), 0)],
implementation: DocumentNodeImplementation::ProtoNode(memo::monitor::IDENTIFIER), implementation: DocumentNodeImplementation::ProtoNode(memo::monitor::IDENTIFIER),
manual_composition: Some(generic!(T)), manual_composition: Some(generic!(T)),
skip_deduplication: true, skip_deduplication: true,
@ -1374,7 +1374,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default() ..Default::default()
}), }),
input_metadata: vec![ input_metadata: vec![
("Vector Data", "TODO").into(), ("Value", "TODO").into(),
InputMetadata::with_name_description_override( InputMetadata::with_name_description_override(
"Translation", "Translation",
"TODO", "TODO",

View File

@ -21,7 +21,7 @@ use graphene_std::raster::{
}; };
use graphene_std::raster_types::{CPU, GPU, RasterDataTable}; use graphene_std::raster_types::{CPU, GPU, RasterDataTable};
use graphene_std::text::Font; use graphene_std::text::Font;
use graphene_std::transform::{Footprint, ReferencePoint}; use graphene_std::transform::{Footprint, ReferencePoint, Transform};
use graphene_std::vector::VectorDataTable; use graphene_std::vector::VectorDataTable;
use graphene_std::vector::misc::GridType; use graphene_std::vector::misc::GridType;
use graphene_std::vector::misc::{ArcType, MergeByDistanceAlgorithm}; use graphene_std::vector::misc::{ArcType, MergeByDistanceAlgorithm};
@ -176,6 +176,7 @@ pub(crate) fn property_from_type(
Some(x) if x == TypeId::of::<bool>() => bool_widget(default_info, CheckboxInput::default()).into(), Some(x) if x == TypeId::of::<bool>() => bool_widget(default_info, CheckboxInput::default()).into(),
Some(x) if x == TypeId::of::<String>() => text_widget(default_info).into(), Some(x) if x == TypeId::of::<String>() => text_widget(default_info).into(),
Some(x) if x == TypeId::of::<DVec2>() => coordinate_widget(default_info, "X", "Y", "", None, false), Some(x) if x == TypeId::of::<DVec2>() => coordinate_widget(default_info, "X", "Y", "", None, false),
Some(x) if x == TypeId::of::<DAffine2>() => transform_widget(default_info, &mut extra_widgets),
// ========================== // ==========================
// PRIMITIVE COLLECTION TYPES // PRIMITIVE COLLECTION TYPES
// ========================== // ==========================
@ -504,6 +505,126 @@ pub fn footprint_widget(parameter_widgets_info: ParameterWidgetsInfo, extra_widg
last.clone() last.clone()
} }
pub fn transform_widget(parameter_widgets_info: ParameterWidgetsInfo, extra_widgets: &mut Vec<LayoutGroup>) -> LayoutGroup {
let ParameterWidgetsInfo { document_node, node_id, index, .. } = parameter_widgets_info;
let mut location_widgets = start_widgets(parameter_widgets_info);
location_widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
let mut rotation_widgets = vec![TextLabel::new("").widget_holder()];
add_blank_assist(&mut rotation_widgets);
rotation_widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
let mut scale_widgets = vec![TextLabel::new("").widget_holder()];
add_blank_assist(&mut scale_widgets);
scale_widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
let Some(document_node) = document_node else { return LayoutGroup::default() };
let Some(input) = document_node.inputs.get(index) else {
log::warn!("A widget failed to be built because its node's input index is invalid.");
return Vec::new().into();
};
let widgets = if let Some(&TaggedValue::DAffine2(transform)) = input.as_non_exposed_value() {
let translation = transform.translation;
let rotation = transform.decompose_rotation();
let scale = transform.decompose_scale();
location_widgets.extend_from_slice(&[
NumberInput::new(Some(translation.x))
.label("X")
.unit(" px")
.on_update(update_value(
move |x: &NumberInput| {
let mut transform = transform;
transform.translation.x = x.value.unwrap_or(transform.translation.x);
TaggedValue::DAffine2(transform)
},
node_id,
index,
))
.on_commit(commit_value)
.widget_holder(),
Separator::new(SeparatorType::Related).widget_holder(),
NumberInput::new(Some(translation.y))
.label("Y")
.unit(" px")
.on_update(update_value(
move |y: &NumberInput| {
let mut transform = transform;
transform.translation.y = y.value.unwrap_or(transform.translation.y);
TaggedValue::DAffine2(transform)
},
node_id,
index,
))
.on_commit(commit_value)
.widget_holder(),
]);
rotation_widgets.extend_from_slice(&[NumberInput::new(Some(rotation.to_degrees()))
.unit("°")
.mode(NumberInputMode::Range)
.range_min(Some(-180.))
.range_max(Some(180.))
.on_update(update_value(
move |r: &NumberInput| {
let transform = DAffine2::from_scale_angle_translation(scale, r.value.map(|r| r.to_radians()).unwrap_or(rotation), translation);
TaggedValue::DAffine2(transform)
},
node_id,
index,
))
.on_commit(commit_value)
.widget_holder()]);
scale_widgets.extend_from_slice(&[
NumberInput::new(Some(scale.x))
.label("W")
.unit("x")
.on_update(update_value(
move |w: &NumberInput| {
let transform = DAffine2::from_scale_angle_translation(DVec2::new(w.value.unwrap_or(scale.x), scale.y), rotation, translation);
TaggedValue::DAffine2(transform)
},
node_id,
index,
))
.on_commit(commit_value)
.widget_holder(),
Separator::new(SeparatorType::Related).widget_holder(),
NumberInput::new(Some(scale.y))
.label("H")
.unit("x")
.on_update(update_value(
move |h: &NumberInput| {
let transform = DAffine2::from_scale_angle_translation(DVec2::new(scale.x, h.value.unwrap_or(scale.y)), rotation, translation);
TaggedValue::DAffine2(transform)
},
node_id,
index,
))
.on_commit(commit_value)
.widget_holder(),
]);
vec![
LayoutGroup::Row { widgets: location_widgets },
LayoutGroup::Row { widgets: rotation_widgets },
LayoutGroup::Row { widgets: scale_widgets },
]
} else {
vec![LayoutGroup::Row { widgets: location_widgets }]
};
if let Some((last, rest)) = widgets.split_last() {
*extra_widgets = rest.to_vec();
last.clone()
} else {
LayoutGroup::default()
}
}
pub fn coordinate_widget(parameter_widgets_info: ParameterWidgetsInfo, x: &str, y: &str, unit: &str, min: Option<f64>, is_integer: bool) -> LayoutGroup { pub fn coordinate_widget(parameter_widgets_info: ParameterWidgetsInfo, x: &str, y: &str, unit: &str, min: Option<f64>, is_integer: bool) -> LayoutGroup {
let ParameterWidgetsInfo { document_node, node_id, index, .. } = parameter_widgets_info; let ParameterWidgetsInfo { document_node, node_id, index, .. } = parameter_widgets_info;
@ -1345,9 +1466,9 @@ pub(crate) fn rectangle_properties(node_id: NodeId, context: &mut NodeProperties
pub(crate) fn node_no_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> { pub(crate) fn node_no_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
let text = if context.network_interface.is_layer(&node_id, context.selection_network_path) { let text = if context.network_interface.is_layer(&node_id, context.selection_network_path) {
"Layer has no properties" "Layer has no parameters"
} else { } else {
"Node has no properties" "Node has no parameters"
}; };
string_properties(text) string_properties(text)
} }

View File

@ -27,7 +27,8 @@ impl FrontendGraphDataType {
| TaggedValue::OptionalDVec2(_) | TaggedValue::OptionalDVec2(_)
| TaggedValue::F64Array4(_) | TaggedValue::F64Array4(_)
| TaggedValue::VecF64(_) | TaggedValue::VecF64(_)
| TaggedValue::VecDVec2(_) => Self::Number, | TaggedValue::VecDVec2(_)
| TaggedValue::DAffine2(_) => Self::Number,
TaggedValue::GraphicGroup(_) | TaggedValue::GraphicElement(_) => Self::Group, // TODO: Is GraphicElement supposed to be included here? TaggedValue::GraphicGroup(_) | TaggedValue::GraphicElement(_) => Self::Group, // TODO: Is GraphicElement supposed to be included here?
TaggedValue::ArtboardGroup(_) => Self::Artboard, TaggedValue::ArtboardGroup(_) => Self::Artboard,
_ => Self::General, _ => Self::General,

View File

@ -100,6 +100,11 @@ impl From<RasterDataTable<GPU>> for GraphicGroupTable {
Self::new(GraphicElement::RasterDataGPU(raster_data_table)) Self::new(GraphicElement::RasterDataGPU(raster_data_table))
} }
} }
impl From<DAffine2> for GraphicGroupTable {
fn from(_: DAffine2) -> Self {
GraphicGroupTable::default()
}
}
/// The possible forms of graphical content held in a Vec by the `elements` field of [`GraphicElement`]. /// The possible forms of graphical content held in a Vec by the `elements` field of [`GraphicElement`].
#[derive(Clone, Debug, Hash, PartialEq, DynAny, serde::Serialize, serde::Deserialize)] #[derive(Clone, Debug, Hash, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
@ -118,6 +123,12 @@ impl Default for GraphicElement {
} }
} }
impl From<DAffine2> for GraphicElement {
fn from(_: DAffine2) -> Self {
GraphicElement::default()
}
}
impl GraphicElement { impl GraphicElement {
pub fn as_group(&self) -> Option<&GraphicGroupTable> { pub fn as_group(&self) -> Option<&GraphicGroupTable> {
match self { match self {
@ -351,6 +362,7 @@ async fn to_element<Data: Into<GraphicElement> + 'n>(
VectorDataTable, VectorDataTable,
RasterDataTable<CPU>, RasterDataTable<CPU>,
RasterDataTable<GPU>, RasterDataTable<GPU>,
DAffine2,
)] )]
data: Data, data: Data,
) -> GraphicElement { ) -> GraphicElement {
@ -463,6 +475,7 @@ async fn to_artboard<Data: Into<GraphicGroupTable> + 'n>(
Context -> VectorDataTable, Context -> VectorDataTable,
Context -> RasterDataTable<CPU>, Context -> RasterDataTable<CPU>,
Context -> RasterDataTable<GPU>, Context -> RasterDataTable<GPU>,
Context -> DAffine2,
)] )]
contents: impl Node<Context<'static>, Output = Data>, contents: impl Node<Context<'static>, Output = Data>,
label: String, label: String,

View File

@ -1,4 +1,5 @@
use crate::AlphaBlending; use crate::AlphaBlending;
use crate::transform::ApplyTransform;
use crate::uuid::NodeId; use crate::uuid::NodeId;
use dyn_any::StaticType; use dyn_any::StaticType;
use glam::DAffine2; use glam::DAffine2;
@ -136,6 +137,20 @@ impl<T: Hash> Hash for Instances<T> {
} }
} }
impl<T> ApplyTransform for Instances<T> {
fn apply_transform(&mut self, modification: &DAffine2) {
for transform in &mut self.transform {
*transform *= *modification;
}
}
fn left_apply_transform(&mut self, modification: &DAffine2) {
for transform in &mut self.transform {
*transform = *modification * *transform;
}
}
}
impl<T: PartialEq> PartialEq for Instances<T> { impl<T: PartialEq> PartialEq for Instances<T> {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
self.instance.len() == other.instance.len() && { self.instance.iter().zip(other.instance.iter()).all(|(a, b)| a == b) } self.instance.len() == other.instance.len() && { self.instance.iter().zip(other.instance.iter()).all(|(a, b)| a == b) }

View File

@ -6,14 +6,20 @@ use glam::{DAffine2, DMat2, DVec2};
pub trait Transform { pub trait Transform {
fn transform(&self) -> DAffine2; fn transform(&self) -> DAffine2;
fn local_pivot(&self, pivot: DVec2) -> DVec2 { fn local_pivot(&self, pivot: DVec2) -> DVec2 {
pivot pivot
} }
fn decompose_scale(&self) -> DVec2 { fn decompose_scale(&self) -> DVec2 {
DVec2::new( DVec2::new(self.transform().transform_vector2(DVec2::X).length(), self.transform().transform_vector2(DVec2::Y).length())
self.transform().transform_vector2((1., 0.).into()).length(), }
self.transform().transform_vector2((0., 1.).into()).length(),
) /// Requires that the transform does not contain any skew.
fn decompose_rotation(&self) -> f64 {
let rotation_matrix = (self.transform() * DAffine2::from_scale(self.decompose_scale().recip())).matrix2;
let rotation = -rotation_matrix.mul_vec2(DVec2::X).angle_to(DVec2::X);
if rotation == -0. { 0. } else { rotation }
} }
} }
@ -141,12 +147,21 @@ impl std::hash::Hash for Footprint {
pub trait ApplyTransform { pub trait ApplyTransform {
fn apply_transform(&mut self, modification: &DAffine2); fn apply_transform(&mut self, modification: &DAffine2);
fn left_apply_transform(&mut self, modification: &DAffine2);
} }
impl<T: TransformMut> ApplyTransform for T { impl<T: TransformMut> ApplyTransform for T {
fn apply_transform(&mut self, &modification: &DAffine2) { fn apply_transform(&mut self, &modification: &DAffine2) {
*self.transform_mut() = self.transform() * modification *self.transform_mut() = self.transform() * modification
} }
fn left_apply_transform(&mut self, &modification: &DAffine2) {
*self.transform_mut() = modification * self.transform()
}
} }
impl ApplyTransform for () { impl ApplyTransform for DVec2 {
fn apply_transform(&mut self, &_modification: &DAffine2) {} fn apply_transform(&mut self, modification: &DAffine2) {
*self = modification.transform_point2(*self);
}
fn left_apply_transform(&mut self, modification: &DAffine2) {
*self = modification.inverse().transform_point2(*self);
}
} }

View File

@ -7,20 +7,22 @@ use core::f64;
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
#[node_macro::node(category(""))] #[node_macro::node(category(""))]
async fn transform<T: 'n + 'static>( async fn transform<T: ApplyTransform + 'n + 'static>(
ctx: impl Ctx + CloneVarArgs + ExtractAll, ctx: impl Ctx + CloneVarArgs + ExtractAll,
#[implementations( #[implementations(
Context -> DAffine2,
Context -> DVec2,
Context -> VectorDataTable, Context -> VectorDataTable,
Context -> GraphicGroupTable, Context -> GraphicGroupTable,
Context -> RasterDataTable<CPU>, Context -> RasterDataTable<CPU>,
Context -> RasterDataTable<GPU>, Context -> RasterDataTable<GPU>,
)] )]
transform_target: impl Node<Context<'static>, Output = Instances<T>>, value: impl Node<Context<'static>, Output = T>,
translate: DVec2, translate: DVec2,
rotate: f64, rotate: f64,
scale: DVec2, scale: DVec2,
skew: DVec2, skew: DVec2,
) -> Instances<T> { ) -> T {
let matrix = DAffine2::from_scale_angle_translation(scale, rotate, translate) * DAffine2::from_cols_array(&[1., skew.y, skew.x, 1., 0., 0.]); let matrix = DAffine2::from_scale_angle_translation(scale, rotate, translate) * DAffine2::from_cols_array(&[1., skew.y, skew.x, 1., 0., 0.]);
let footprint = ctx.try_footprint().copied(); let footprint = ctx.try_footprint().copied();
@ -31,11 +33,9 @@ async fn transform<T: 'n + 'static>(
ctx = ctx.with_footprint(footprint); ctx = ctx.with_footprint(footprint);
} }
let mut transform_target = transform_target.eval(ctx.into_context()).await; let mut transform_target = value.eval(ctx.into_context()).await;
for data_transform in transform_target.instance_mut_iter() { transform_target.left_apply_transform(&matrix);
*data_transform.transform = matrix * *data_transform.transform;
}
transform_target transform_target
} }
@ -52,6 +52,40 @@ fn replace_transform<Data, TransformInput: Transform>(
data data
} }
#[node_macro::node(category("Math: Transform"), path(graphene_core::vector))]
async fn extract_transform<T>(
_: impl Ctx,
#[implementations(
GraphicGroupTable,
VectorDataTable,
RasterDataTable<CPU>,
RasterDataTable<GPU>,
)]
vector_data: Instances<T>,
) -> DAffine2 {
vector_data.instance_ref_iter().next().map(|vector_data| *vector_data.transform).unwrap_or_default()
}
#[node_macro::node(category("Math: Transform"))]
fn invert_transform(_: impl Ctx, transform: DAffine2) -> DAffine2 {
transform.inverse()
}
#[node_macro::node(category("Math: Transform"))]
fn decompose_translation(_: impl Ctx, transform: DAffine2) -> DVec2 {
transform.translation
}
#[node_macro::node(category("Math: Transform"))]
fn decompose_rotation(_: impl Ctx, transform: DAffine2) -> f64 {
transform.decompose_rotation()
}
#[node_macro::node(category("Math: Transform"))]
fn decompose_scale(_: impl Ctx, transform: DAffine2) -> DVec2 {
transform.decompose_scale()
}
#[node_macro::node(category("Debug"))] #[node_macro::node(category("Debug"))]
async fn boundless_footprint<T: 'n + 'static>( async fn boundless_footprint<T: 'n + 'static>(
ctx: impl Ctx + CloneVarArgs + ExtractAll, ctx: impl Ctx + CloneVarArgs + ExtractAll,

View File

@ -418,7 +418,7 @@ impl Hash for VectorModification {
} }
} }
/// A node that applies a procedural modification to some [`VectorData`]. /// Applies a diff modification to a vector path.
#[node_macro::node(category(""))] #[node_macro::node(category(""))]
async fn path_modify(_ctx: impl Ctx, mut vector_data: VectorDataTable, modification: Box<VectorModification>, node_path: Vec<NodeId>) -> VectorDataTable { async fn path_modify(_ctx: impl Ctx, mut vector_data: VectorDataTable, modification: Box<VectorModification>, node_path: Vec<NodeId>) -> VectorDataTable {
if vector_data.is_empty() { if vector_data.is_empty() {
@ -437,6 +437,23 @@ async fn path_modify(_ctx: impl Ctx, mut vector_data: VectorDataTable, modificat
vector_data vector_data
} }
/// Applies the vector path's local transformation to its geometry and resets it to the identity.
#[node_macro::node(category("Vector"))]
async fn apply_transform(_ctx: impl Ctx, mut vector_data: VectorDataTable) -> VectorDataTable {
for vector_data_instance in vector_data.instance_mut_iter() {
let vector_data = vector_data_instance.instance;
let transform = *vector_data_instance.transform;
for (_, point) in vector_data.point_domain.positions_mut() {
*point = transform.transform_point2(*point);
}
*vector_data_instance.transform = DAffine2::IDENTITY;
}
vector_data
}
// Do we want to enforce that all serialized/deserialized hashmaps are a vec of tuples? // Do we want to enforce that all serialized/deserialized hashmaps are a vec of tuples?
// TODO: Eventually remove this document upgrade code // TODO: Eventually remove this document upgrade code
use serde::de::{SeqAccess, Visitor}; use serde::de::{SeqAccess, Visitor};

View File

@ -1874,7 +1874,7 @@ fn bevel_algorithm(mut vector_data: VectorData, vector_data_transform: DAffine2,
} }
if segment_domain_length != sorted_segments.len() { if segment_domain_length != sorted_segments.len() {
for i in 0..segment_domain_length as usize { for i in 0..segment_domain_length {
if !sorted_segments.contains(&i) { if !sorted_segments.contains(&i) {
sorted_segments.push(i); sorted_segments.push(i);
} }

View File

@ -1,6 +1,7 @@
use glam::DVec2; use glam::{DAffine2, DVec2};
use graphene_core::gradient::GradientStops; use graphene_core::gradient::GradientStops;
use graphene_core::registry::types::{Fraction, Percentage, TextArea}; use graphene_core::registry::types::{Fraction, Percentage, PixelSize, TextArea};
use graphene_core::transform::Footprint;
use graphene_core::{Color, Ctx, num_traits}; use graphene_core::{Color, Ctx, num_traits};
use log::warn; use log::warn;
use math_parser::ast; use math_parser::ast;
@ -107,11 +108,11 @@ fn subtract<U: Sub<T>, T>(
fn multiply<U: Mul<T>, T>( fn multiply<U: Mul<T>, T>(
_: impl Ctx, _: impl Ctx,
/// The left-hand side of the multiplication operation. /// The left-hand side of the multiplication operation.
#[implementations(f64, f32, u32, DVec2, f64, DVec2)] #[implementations(f64, f32, u32, f64, DVec2, DVec2, DAffine2)]
multiplier: U, multiplier: U,
/// The right-hand side of the multiplication operation. /// The right-hand side of the multiplication operation.
#[default(1.)] #[default(1.)]
#[implementations(f64, f32, u32, DVec2, DVec2, f64)] #[implementations(f64, f32, u32, DVec2, f64, DVec2, DAffine2)]
multiplicand: T, multiplicand: T,
) -> <U as Mul<T>>::Output { ) -> <U as Mul<T>>::Output {
multiplier * multiplicand multiplier * multiplicand
@ -681,6 +682,16 @@ fn string_value(_: impl Ctx, _primary: (), string: TextArea) -> String {
string string
} }
/// Constructs a footprint value which may be set to any transformation of a unit square describing a render area, and a render resolution at least 1x1 integer pixels.
#[node_macro::node(category("Value"))]
fn footprint_value(_: impl Ctx, _primary: (), transform: DAffine2, #[default(100., 100.)] resolution: PixelSize) -> Footprint {
Footprint {
transform,
resolution: resolution.max(DVec2::ONE).as_uvec2(),
..Default::default()
}
}
#[node_macro::node(category("Math: Vector"))] #[node_macro::node(category("Math: Vector"))]
fn dot_product(_: impl Ctx, vector_a: DVec2, vector_b: DVec2) -> f64 { fn dot_product(_: impl Ctx, vector_a: DVec2, vector_b: DVec2) -> f64 {
vector_a.dot(vector_b) vector_a.dot(vector_b)

View File

@ -1,5 +1,5 @@
use dyn_any::StaticType; use dyn_any::StaticType;
use glam::{DVec2, IVec2, UVec2}; use glam::{DAffine2, DVec2, IVec2, UVec2};
use graph_craft::document::value::RenderOutput; use graph_craft::document::value::RenderOutput;
use graph_craft::proto::{NodeConstructor, TypeErasedBox}; use graph_craft::proto::{NodeConstructor, TypeErasedBox};
use graphene_core::raster::color::Color; use graphene_core::raster::color::Color;
@ -52,6 +52,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => String]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => String]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => IVec2]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => IVec2]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => DVec2]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => DVec2]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => DAffine2]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => bool]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => bool]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => f64]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => f64]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => u32]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => u32]),
@ -166,7 +167,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
for (id, entry) in graphene_core::registry::NODE_REGISTRY.lock().unwrap().iter() { for (id, entry) in graphene_core::registry::NODE_REGISTRY.lock().unwrap().iter() {
for (constructor, types) in entry.iter() { for (constructor, types) in entry.iter() {
map.entry(id.clone().into()).or_default().insert(types.clone(), *constructor); map.entry(id.clone()).or_default().insert(types.clone(), *constructor);
} }
} }