diff --git a/node-graph/graph-craft/src/document.rs b/node-graph/graph-craft/src/document.rs index b2b5f8277b..8535527982 100644 --- a/node-graph/graph-craft/src/document.rs +++ b/node-graph/graph-craft/src/document.rs @@ -1060,7 +1060,7 @@ impl NodeNetwork { let (tagged_value, exposed) = match previous_export { NodeInput::Value { tagged_value, exposed } => (tagged_value, exposed), NodeInput::Reflection(reflect) => match reflect { - DocumentNodeMetadata::DocumentNodePath => (TaggedValue::NodeIdPath(core_types::list::NodeIdPath::from(path.to_vec())).into(), false), + DocumentNodeMetadata::DocumentNodePath => (TaggedValue::NodeIdPath(path.to_vec()).into(), false), DocumentNodeMetadata::SourceId => { let source_id = Self::source_id_for_path(path); if let Some(context_features) = context_features.as_deref_mut() { diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 5d46e756ef..30207a9bf7 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -6,7 +6,7 @@ use brush_nodes::{BrushCache, Stroke}; use core_types::color::SRGBA8; use core_types::context::Context; use core_types::gpoll::GPoll; -use core_types::list::{Item, List, NodeIdPath}; +use core_types::list::{Item, List}; use core_types::registry::SourceHandle; use core_types::transform::Footprint; use core_types::uuid::NodeId; @@ -108,9 +108,9 @@ macro_rules! tagged_value { // ======================= #[serde(skip)] RenderOutput(RenderOutput), - /// Path to the consumer of a `NodeInput::Reflection(DocumentNodePath)`. Materializes an `Item` at runtime via `to_dynany`/`to_any` during graph flattening, matching the ranked connectors it feeds. + /// Path to the consumer of a `NodeInput::Reflection(DocumentNodePath)`, in the `Vec` form our node catalog consumes. #[serde(skip)] - NodeIdPath(NodeIdPath), + NodeIdPath(Vec), /// The `DocumentNode` value carried by an `Extract` proto node, populated at flatten time by `resolve_extract_nodes`. The on-disk placeholder uses `TypeDefault(concrete!(DocumentNode))`. #[serde(skip)] DocumentNode(DocumentNode), @@ -174,6 +174,14 @@ macro_rules! tagged_value { if name == core_types::normalize_type_name(std::any::type_name::<$type_default>()) { return Box::new(<$type_default>::default()); } }; } + macro_rules! check_list { + ($element:ty) => { + if name == core_types::normalize_type_name(std::any::type_name::>()) { return Box::new(List::<$element>::default()); } + }; + } + for_each_list_type_default!(check_list); + for_each_item_type_default!(check); + for_each_bare_type_default!(check); Self::from_type_or_none(&Type::Concrete(td.clone())).to_dynany() } Self::F64Array(values) => { @@ -219,6 +227,14 @@ macro_rules! tagged_value { if name == core_types::normalize_type_name(std::any::type_name::<$type_default>()) { return Arc::new(<$type_default>::default()); } }; } + macro_rules! check_list { + ($element:ty) => { + if name == core_types::normalize_type_name(std::any::type_name::>()) { return Arc::new(List::<$element>::default()); } + }; + } + for_each_list_type_default!(check_list); + for_each_item_type_default!(check); + for_each_bare_type_default!(check); Self::from_type_or_none(&Type::Concrete(td.clone())).to_any() } Self::F64Array(values) => { @@ -282,7 +298,7 @@ macro_rules! tagged_value { // NON-SERIALIZED VARIANTS // ======================= Self::RenderOutput(_) => concrete!(RenderOutput), - Self::NodeIdPath(_) => concrete!(core_types::list::NodeIdPath), + Self::NodeIdPath(_) => concrete!(Vec), Self::DocumentNode(_) => concrete!(DocumentNode), Self::ContextModification(_) => concrete!(ContextModification), Self::EditorApi(_) => concrete!(Arc), @@ -328,7 +344,7 @@ macro_rules! tagged_value { Self::BrushCache(_) => scalar::(), $( Self::$identifier(_) => scalar::<$ty>(), )* Self::RenderOutput(_) => scalar::(), - Self::NodeIdPath(_) => scalar::(), + Self::NodeIdPath(_) => scalar::>(), Self::DocumentNode(_) => scalar::(), Self::ContextModification(_) => scalar::(), Self::EditorApi(_) => scalar::>(), @@ -366,8 +382,8 @@ macro_rules! tagged_value { Self::from_type_or_none(&Type::Concrete(td)).to_edge() } Self::F64Array(values) => Ok(leveled_record_value_source(values)), - Self::Color(color) => Ok(record_value_source(color)), - Self::GradientRamp(stops) => Ok(leveled_record_value_source(vec![stops])), + Self::Color(color) => Ok(leveled_record_value_source(vec![color])), + Self::GradientRamp(ramp) => Ok(leveled_record_value_source(vec![Gradient::from(ramp)])), Self::Strokes(strokes) => Ok(leveled_record_value_source(strokes)), Self::DashPattern(lengths) => Ok(record_value_source(DashPattern(lengths.into_iter().map(core_types::list::Item::new_from_element).collect()))), Self::BoxCorners(values) => Ok(record_value_source(BoxCorners(values.into_iter().map(core_types::list::Item::new_from_element).collect()))), @@ -489,7 +505,6 @@ macro_rules! tagged_value { pub fn from_type(input: &Type) -> Option { match input { Type::Generic(_) => None, - Type::Record(inner) => Self::from_type(inner), Type::Concrete(concrete_type) => { let name = concrete_type.name.as_ref(); // Tries using the default for the tagged value type. If it not implemented, then uses the default used in document_node_types. If it is not used there, then TaggedValue::None is returned. @@ -500,6 +515,13 @@ macro_rules! tagged_value { $( if name == core_types::normalize_type_name(std::any::type_name::<$ty>()) { return Some(TaggedValue::$identifier(Default::default())) } )* if name == core_types::normalize_type_name(std::any::type_name::>()) { return Some(TaggedValue::F64Array(Vec::new())) } if name == core_types::normalize_type_name(std::any::type_name::>()) { return Some(TaggedValue::Strokes(Vec::new())) } + // The manual variants are not in the generated `$ty` list, so their defaults are named here + if name == core_types::normalize_type_name(std::any::type_name::()) { return Some(TaggedValue::BoxCorners(Vec::new())) } + if name == core_types::normalize_type_name(std::any::type_name::()) { return Some(TaggedValue::DashPattern(Vec::new())) } + if name == core_types::normalize_type_name(std::any::type_name::>()) { return Some(TaggedValue::BoxCorners(Vec::new())) } + if name == core_types::normalize_type_name(std::any::type_name::>()) { return Some(TaggedValue::DashPattern(Vec::new())) } + if name == core_types::normalize_type_name(std::any::type_name::()) { return Some(TaggedValue::BrushCache(Default::default())) } + if name == core_types::normalize_type_name(std::any::type_name::()) { return Some(TaggedValue::GradientRamp(GradientRamp::default())) } // Leveled inputs type by their element; each element name maps to the // same tagged default as its legacy list form. if name == core_types::normalize_type_name(std::any::type_name::()) { return Some(TaggedValue::Color(Color::default())) } @@ -516,6 +538,12 @@ macro_rules! tagged_value { }; } for_each_bare_type_default!(check); + macro_rules! check_list { + ($element:ty) => { + if name == core_types::normalize_type_name(std::any::type_name::>()) { return Some(TaggedValue::TypeDefault(core_types::descriptor!(List<$element>))); } + }; + } + for_each_list_type_default!(check_list); None } Type::Fn(_, output) => TaggedValue::from_type(output), @@ -793,6 +821,10 @@ impl TaggedValue { () if ty == TypeId::of::() => to_color(string).map(TaggedValue::Color)?, // The Fill/Stroke paint wires carry `Graphic` or `Gradient` elements, so a paint default parses through the element recursion as a color or gradient literal () if ty == TypeId::of::() => to_color(string).map(TaggedValue::Color)?, + // A rank-1 paint wire names its list type, so it parses the same literal as its element does + () if ty == TypeId::of::>() => to_color(string).map(TaggedValue::Color)?, + () if ty == TypeId::of::>() => to_color(string).map(TaggedValue::Color)?, + () if ty == TypeId::of::>() => to_gradient(string).map(|gradient| TaggedValue::GradientRamp(gradient.into()))?, () if ty == TypeId::of::() => to_gradient(string).map(|gradient| TaggedValue::GradientRamp(gradient.into()))?, () if ty == TypeId::of::() => to_reference_point(string).map(TaggedValue::ReferencePoint)?, () if ty == TypeId::of::() => TaggedValue::DashPattern(core_types::misc::parse_f64_list(string)), @@ -803,7 +835,6 @@ impl TaggedValue { } Type::Fn(_, output) => TaggedValue::from_primitive_string(string, output), Type::Future(fut) => TaggedValue::from_primitive_string(string, fut), - Type::Record(element) => TaggedValue::from_primitive_string(string, element), } } @@ -875,6 +906,35 @@ pub fn deserialize_tagged_value_with_legacy_migration<'de, D: serde::Deserialize } return Ok(MemoHash::new(TaggedValue::TypeDefault(core_types::descriptor!(List)))); } + // Documents written against the structural rank model store the payload as a `Type` + // (`{"List": {"Concrete": {..}}}`). Our one wire kind reduces that to the element it names, + // keeping the `List<..>` spelling for a rank-1 wire so the existing name lookups still match. + "TypeDefault" if content.as_object().is_some_and(|c| c.contains_key("Concrete") || c.contains_key("Item") || c.contains_key("List")) => { + fn structural_type_name(value: &serde_json::Value) -> Option { + let object = value.as_object()?; + if let Some(concrete) = object.get("Concrete") { + return Some(concrete.as_object()?.get("name")?.as_str()?.to_string()); + } + if let Some(item) = object.get("Item") { + return structural_type_name(item); + } + if let Some(list) = object.get("List") { + return Some(format!("core_types::list::List<{}>", structural_type_name(list)?)); + } + object.get("Generic")?.as_str().map(str::to_string) + } + let Some(name) = structural_type_name(&content) else { + return Err(serde::de::Error::custom("a structural TypeDefault payload named no type")); + }; + let descriptor = TypeDescriptor { + id: None, + name: std::borrow::Cow::Owned(name), + alias: None, + size: 0, + align: 0, + }; + return Ok(MemoHash::new(TaggedValue::TypeDefault(descriptor))); + } // The `TypeDefault` payload is a bare `TypeDescriptor`: our one wire kind needs no structural rank in it "TypeDefault" if content.as_object().is_some_and(|c| c.contains_key("name")) => { let descriptor: TypeDescriptor = serde_json::from_value(content.clone()).map_err(serde::de::Error::custom)?; diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index 8fe328381a..00cc7c535c 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -12,7 +12,9 @@ use graphene_std::registry::{ConstructionError, SourceHandle, NodeIOTypes, Regis use graphene_std::runtime::RuntimeHandle; use graphene_std::vector::Vector; -use graphene_std::{Context, Graphic, ProtoNodeIdentifier, concrete}; +use graphene_std::gradient::Gradient; +use graphene_std::brush::Stroke; +use graphene_std::{Color, Context, Graphic, ProtoNodeIdentifier, concrete}; use node_registry_macros::{convert_node, into_node}; use std::collections::HashMap; #[cfg(feature = "gpu")] @@ -28,6 +30,16 @@ fn node_registry() -> HashMap> { #[cfg(feature = "gpu")] into_node!(from: List>, to: List>), convert_node!(from: List, to: List), + convert_node!(from: List, to: List), + convert_node!(from: List, to: List), + convert_node!(from: List, to: List), + convert_node!(from: List, to: List), + convert_node!(from: Vector, to: Graphic), + convert_node!(from: Color, to: Graphic), + convert_node!(from: Gradient, to: Graphic), + convert_node!(from: String, to: Graphic), + convert_node!(from: Stroke, to: Graphic), + convert_node!(from: Raster, to: Graphic), convert_node!(from: List>, to: List), #[cfg(feature = "gpu")] convert_node!(from: List>, to: List), diff --git a/node-graph/libraries/core-types/src/types.rs b/node-graph/libraries/core-types/src/types.rs index 0a5b2b5889..e2d0b1d278 100644 --- a/node-graph/libraries/core-types/src/types.rs +++ b/node-graph/libraries/core-types/src/types.rs @@ -280,7 +280,8 @@ impl PartialEq for TypeDescriptor { /// Graph runtime type information used for type inference. #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] #[derive(Clone, PartialEq, Eq, Hash, graphene_hash::CacheHash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "PascalCase"))] pub enum Type { /// A wrapper for some type variable used within the inference system. Resolved at inference time and replaced with a concrete type. Generic(Cow<'static, str>), @@ -294,6 +295,41 @@ pub enum Type { Record(Box), } +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for Type { + /// Documents written against the structural rank model store `Item` and `List` wire types. + /// Our one wire kind has neither, so an `Item` reduces to the element it wraps and a `List` + /// becomes the concrete `List<..>` type it names. + fn deserialize>(deserializer: D) -> Result { + #[derive(serde::Deserialize)] + enum Stored { + Generic(Cow<'static, str>), + Concrete(TypeDescriptor), + Fn(Box, Box), + Future(Box), + Record(Box), + Item(Box), + List(Box), + } + + Ok(match Stored::deserialize(deserializer)? { + Stored::Generic(name) => Type::Generic(name), + Stored::Concrete(descriptor) => Type::Concrete(descriptor), + Stored::Fn(input, output) => Type::Fn(input, output), + Stored::Future(inner) => Type::Future(inner), + Stored::Record(inner) => Type::Record(inner), + Stored::Item(element) => *element, + Stored::List(element) => Type::Concrete(TypeDescriptor { + id: None, + name: Cow::Owned(format!("core_types::list::List<{}>", element.identifier_name())), + alias: None, + size: 0, + align: 0, + }), + }) + } +} + impl Default for Type { fn default() -> Self { concrete!(()) diff --git a/node-graph/libraries/graphic-types/src/graphic/mod.rs b/node-graph/libraries/graphic-types/src/graphic/mod.rs index c429ea5c77..063c497818 100644 --- a/node-graph/libraries/graphic-types/src/graphic/mod.rs +++ b/node-graph/libraries/graphic-types/src/graphic/mod.rs @@ -516,6 +516,46 @@ impl<'e> ListConvert> for Raster { Graphic::RasterGPU(self) } } +// A leveled paint input types by its element, so the same embedding is needed one rank down. +macro_rules! convert_leaf_to_graphic { + ($($element:ty),* $(,)?) => { + $( + impl<'e> core_types::ops::Convert, ()> for $element { + fn convert(self, _: core_types::transform::Footprint, _: ()) -> Graphic<'e> { + core_types::ops::ListConvert::convert_item(self) + } + } + )* + }; +} +convert_leaf_to_graphic!(Vector, Raster, Raster, Color, Gradient, String, Stroke); + +// The paint wires accept any leaf element, the role master's `From for Graphic` embedding adapters play. +impl<'e> ListConvert> for Graphic<'e> { + fn convert_item(self) -> Graphic<'e> { + self + } +} +impl<'e> ListConvert> for Color { + fn convert_item(self) -> Graphic<'e> { + Graphic::Color(self) + } +} +impl<'e> ListConvert> for Gradient { + fn convert_item(self) -> Graphic<'e> { + Graphic::Gradient(self) + } +} +impl<'e> ListConvert> for String { + fn convert_item(self) -> Graphic<'e> { + Graphic::Text(self) + } +} +impl<'e> ListConvert> for Stroke { + fn convert_item(self) -> Graphic<'e> { + Graphic::Stroke(self) + } +} impl RenderComplexity for Graphic<'_> { fn render_complexity(&self) -> usize { diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index 8a886c307a..7e1fcb3f76 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -375,29 +375,40 @@ fn default_gradient_paint(paint: &mut List, bounds: Option<[DVec2; 2]>, /// The materialized paint level as the canonical owned paint list, content /// kept in its native form. -fn paint_table(paint: core_types::node::List<'_, Graphic<'_>>) -> List> { +/// A paint level as its legacy list, embedding each leaf element into `Graphic` and keeping its lane attributes. +/// This is the role master's `From for Graphic` embedding adapters play for its monomorphic paint input. +fn paint_table

(paint: core_types::node::List<'_, P>) -> List> +where + P: Clone + Send + Sync + dyn_any::StaticTypeSized + core_types::ops::ListConvert>, +{ let item = paint.as_group_item(); - graphic_types::graphic::run_to_list::(&item).expect("a paint level holds graphic lanes") + let typed = graphic_types::graphic::run_to_list::

(&item).expect("a paint level holds its declared lanes"); + let mut out = List::new(); + for row in typed.into_iter() { + let (element, attributes) = row.into_parts(); + out.push(Item::from_parts(core_types::ops::ListConvert::convert_item(element), attributes)); + } + out } /// Applies a fill style to the vector content, giving an appearance to the area within the interior of the geometry. #[node_macro::node(category("Vector: Style"), path(graphene_core::vector), properties("fill_properties"))] -fn fill<'e>( +fn fill<'e, P: Clone + Send + Sync + dyn_any::StaticTypeSized + core_types::ops::ListConvert>>( ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy, /// The content with vector paths to apply the fill style to. (element, _content_fill): (Vector, Attr), /// The fill to paint the path with. #[default(Color::BLACK)] - paint: IList>, + #[implementations(Graphic<'static>, Color, Gradient)] + paint: IList

, _backup_color: IList, _backup_gradient: IList, _gradient_form: GradientForm, - _gradient_spread: GradientSpread, _has_transform: bool, _transform: DAffine2, ) -> Result<(Vector, Attr<'e, Fill>), Interrupt> { let mut paint = paint_table(paint); - default_gradient_paint(&mut paint, element.bounding_box(), _gradient_form, _gradient_spread, _has_transform.then_some(_transform)); + default_gradient_paint(&mut paint, element.bounding_box(), _gradient_form, GradientSpread::default(), _has_transform.then_some(_transform)); let parked = park_paint(ctx.arena(), paint)?; Ok((element, Attr(Some(parked)))) } @@ -413,7 +424,6 @@ fn fill_graphic_leveled<'e>( _backup_color: IList, _backup_gradient: IList, _gradient_form: GradientForm, - _gradient_spread: GradientSpread, _has_transform: bool, _transform: DAffine2, ) -> Result<(Graphic<'static>, Attr<'e, Fill>), Interrupt> { @@ -422,20 +432,21 @@ fn fill_graphic_leveled<'e>( _ => None, }; let mut paint = paint_table(paint); - default_gradient_paint(&mut paint, bounds, _gradient_form, _gradient_spread, _has_transform.then_some(_transform)); + default_gradient_paint(&mut paint, bounds, _gradient_form, GradientSpread::default(), _has_transform.then_some(_transform)); let parked = park_paint(ctx.arena(), paint)?; Ok((element, Attr(Some(parked)))) } /// Applies a stroke style to the vector content, giving an appearance to the area within the outline of the geometry. #[node_macro::node(category("Vector: Style"), path(graphene_core::vector), properties("stroke_properties"))] -fn stroke<'e>( +fn stroke<'e, P: Clone + Send + Sync + dyn_any::StaticTypeSized + core_types::ops::ListConvert>>( ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy, /// The content with vector paths to apply the stroke style to. (element, content_transform): (Vector, Attr), /// The stroke paint. #[default(Color::BLACK)] - paint: IList>, + #[implementations(Graphic<'static>, Color, Gradient)] + paint: IList

, /// The stroke thickness. #[unit(" px")] #[default(2.)]