From 4bceae26f24b2ca699f366a3d4d5b47cf8dae0e3 Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Sat, 22 Aug 2026 16:13:01 +0000 Subject: [PATCH] Add the leveled fill rows and the graphic coercion entries --- .../interpreted-executor/src/node_registry.rs | 15 +++ node-graph/nodes/graphic/src/graphic.rs | 1 + node-graph/nodes/vector/src/vector_nodes.rs | 119 +++++++++++++++--- 3 files changed, 116 insertions(+), 19 deletions(-) diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index 4746e8b0c4..634622971e 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -116,6 +116,21 @@ fn node_registry() -> HashMap> { .into_iter() .map(|entry| (graphene_std::transform_nodes::transform_nodes::transform::IDENTIFIER.clone(), entry)), ); + // The leveled fill rows, served under the legacy fill's identifier beside + // its legacy list rows. + node_types.extend( + graphene_std::vector::fill_vector_leveled_entries() + .into_iter() + .chain(graphene_std::vector::fill_graphic_leveled_entries()) + .map(|entry| (graphene_std::vector::fill::IDENTIFIER.clone(), entry)), + ); + // Element-wise coercion into `Graphic` for single-typed leveled inputs, + // served by the to_graphic rows. + node_types.extend( + graphene_std::graphic::to_graphic_entries() + .into_iter() + .map(|entry| (ProtoNodeIdentifier::new("graphene_core::ops::IntoNode"), entry)), + ); // The transitional level bridge: a leveled wire materializes into the legacy // list an unconverted consumer expects. The rows are keyed under the legacy // convert identifiers and die with the last legacy consumer. diff --git a/node-graph/nodes/graphic/src/graphic.rs b/node-graph/nodes/graphic/src/graphic.rs index 041ca693c1..dc233174d8 100644 --- a/node-graph/nodes/graphic/src/graphic.rs +++ b/node-graph/nodes/graphic/src/graphic.rs @@ -623,6 +623,7 @@ pub fn level_to_list( } pub use _level_to_list_mod::level_to_list_entries; +pub use _to_graphic_mod::to_graphic_entries; /// Removes a level of nesting from a `Graphic[]`, or all nesting if "Fully Flatten" is enabled. #[node_macro::node(category("General"), extent(flatten_graphic_extent))] diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index 37524f260c..b1eda5aa2e 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -8,7 +8,10 @@ use core_types::list::{Item, ItemAttributeValues, List, ListDyn}; use core_types::registry::types::{Angle, Length, Multiplier, Percentage, PixelLength, Progression, SeedValue}; use core_types::transform::{Footprint, Transform}; use core_types::uuid::NodeId; +use core_types::attribute::Attr; +use core_types::gpoll::GraphError; use core_types::{ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, CacheHash, Color, Ctx, DeriveCtx, ExtractIndex, InjectIndex}; +use graphic_types::markers::Fill; use glam::{DAffine2, DMat2, DVec2}; use graphic_types::Vector; use graphic_types::graphic::{bake_paint_transforms, graphic_list_at, has_paint_at, is_paint_present, set_paint_attribute_at}; @@ -233,25 +236,6 @@ where content } -trait IntoF64Vec { - fn into_vec(self) -> Vec; -} -impl IntoF64Vec for f64 { - fn into_vec(self) -> Vec { - vec![self] - } -} -impl IntoF64Vec for List { - fn into_vec(self) -> Vec { - self.into_iter().map(|row| row.into_element()).collect() - } -} -impl IntoF64Vec for String { - fn into_vec(self) -> Vec { - self.split(&[',', ' ']).filter(|s| !s.is_empty()).filter_map(|s| s.parse::().ok()).collect() - } -} - /// 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( @@ -333,6 +317,103 @@ fn legacy_list_of(level: core_types::node::Lis graphic_types::graphic::run_to_render_list::(&item).expect("the run holds the row's element type") } +fn park_paint<'e>(arena: &'e core_types::arena::Arena, paint: List) -> Result<&'e List, Interrupt> { + let (parked, _) = arena.alloc(paint).ok_or(GraphError { + kind: core_types::gpoll::ErrorKind::ArenaExhausted, + trace: Vec::new(), + })?; + Ok(parked) +} + +/// The gradient defaulting the legacy fill performed, applied to the nested +/// stops list the paint table wraps. +fn default_gradient_paint(paint: &mut List, bounds: Option<[DVec2; 2]>, gradient_type: GradientType, spread_method: GradientSpreadMethod, transform: Option) { + for index in 0..paint.len() { + let Some(Graphic::Gradient(gradient)) = paint.element_mut(index) else { continue }; + if gradient.iter_attribute_values::(ATTR_GRADIENT_TYPE).is_none() { + for value in gradient.iter_attribute_values_mut_or_default::(ATTR_GRADIENT_TYPE) { + *value = gradient_type; + } + } + + if gradient.iter_attribute_values::(ATTR_SPREAD_METHOD).is_none() { + for value in gradient.iter_attribute_values_mut_or_default::(ATTR_SPREAD_METHOD) { + *value = spread_method; + } + } + + if gradient.iter_attribute_values::(ATTR_TRANSFORM).is_none() { + let transform = transform.unwrap_or_else(|| { + // Nudge a degenerate axis so the gradient transform stays invertible, matching the editor's `nonzero_bounding_box` + let [min, mut max] = bounds.unwrap_or([DVec2::ZERO, DVec2::ONE]); + if max.x - min.x < 1e-10 { + max.x = min.x + 1.; + } + if max.y - min.y < 1e-10 { + max.y = min.y + 1.; + } + initial_gradient_transform_for_bounding_box([min, max]) + }); + + for value in gradient.iter_attribute_values_mut_or_default::(ATTR_TRANSFORM) { + *value = transform; + } + } + } +} + +/// The leveled fill over vector lanes: builds the paint table and writes each +/// lane's fill attribute. Registered under the legacy fill's identifier; the +/// gradient transform fallback spans the lane's own bounds rather than the +/// whole content's. +#[node_macro::node(category(""))] +fn fill_vector_leveled<'e>( + ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy, + (element, _content_fill): (Vector, Attr<'e, Fill>), + #[default(Color::BLACK)] + fill: IList, + _backup_color: IList, + _backup_gradient: IList, + _gradient_type: GradientType, + _spread_method: GradientSpreadMethod, + _transform: Option, +) -> Result<(Vector, Attr<'e, Fill>), Interrupt> { + // SAFETY: a materialized input's frames are arena-resident. + let item = unsafe { core_types::record::GroupItem::from_resident(fill.batch()) }; + let mut paint = graphic_types::graphic::group_to_legacy_list(&core_types::record::Group { row: None, content: core_types::record::GroupContent::Run(item) }); + default_gradient_paint(&mut paint, element.bounding_box(), _gradient_type, _spread_method, _transform); + let parked = park_paint(ctx.arena(), paint)?; + Ok((element, Attr(Some(parked)))) +} + +/// The leveled fill over graphic lanes, as [`fill_vector_leveled`]. +#[node_macro::node(category(""))] +fn fill_graphic_leveled<'e>( + ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy, + (element, _content_fill): (Graphic, Attr<'e, Fill>), + #[default(Color::BLACK)] + fill: IList, + _backup_color: IList, + _backup_gradient: IList, + _gradient_type: GradientType, + _spread_method: GradientSpreadMethod, + _transform: Option, +) -> Result<(Graphic, Attr<'e, Fill>), Interrupt> { + let bounds = match BoundingBox::bounding_box(&element, DAffine2::IDENTITY, false) { + RenderBoundingBox::Rectangle(bounds) => Some(bounds), + _ => None, + }; + // SAFETY: a materialized input's frames are arena-resident. + let item = unsafe { core_types::record::GroupItem::from_resident(fill.batch()) }; + let mut paint = graphic_types::graphic::group_to_legacy_list(&core_types::record::Group { row: None, content: core_types::record::GroupContent::Run(item) }); + default_gradient_paint(&mut paint, bounds, _gradient_type, _spread_method, _transform); + let parked = park_paint(ctx.arena(), paint)?; + Ok((element, Attr(Some(parked)))) +} + +pub use _fill_graphic_leveled_mod::fill_graphic_leveled_entries; +pub use _fill_vector_leveled_mod::fill_vector_leveled_entries; + #[node_macro::node(name("Copy to Points"), category("Repeat"), path(core_types::vector))] fn copy_to_points( _: impl Ctx,