diff --git a/node-graph/nodes/gcore/src/context.rs b/node-graph/nodes/gcore/src/context.rs index e697f5c851..ee5ba9e424 100644 --- a/node-graph/nodes/gcore/src/context.rs +++ b/node-graph/nodes/gcore/src/context.rs @@ -1,3 +1,4 @@ +use core_types::gpoll::{Extent, GPoll, GraphError, Interrupt}; use core_types::list::List; use core_types::{Color, ExtractVarArgs}; use core_types::{Ctx, ExtractIndex, ExtractPosition}; @@ -46,6 +47,78 @@ fn read_gradient(ctx: impl Ctx + ExtractVarArgs) -> List { var_arg.downcast_ref().cloned().unwrap_or_default() } +/// The mapped row riding as vararg 0, in the production single-item shape. +fn vararg_list<'a, T: 'static>(ctx: &'a impl ExtractVarArgs) -> Option<&'a List> { + let arg = ctx.vararg(0).ok()?; + (arg as &dyn std::any::Any).downcast_ref::>() +} + +/// Lanes of a leveled vararg source: one per item, none without a row, +/// matching the legacy empty-list return. +fn vararg_lanes(ctx: &impl ExtractVarArgs, level: u8) -> GPoll { + match level { + 0 => GPoll::Final(Extent::Exactly(vararg_list::(ctx).map_or(0, List::len))), + _ => GPoll::Final(Extent::Exactly(1)), + } +} + +fn vararg_element(ctx: &(impl ExtractVarArgs + ExtractIndex)) -> Result { + vararg_list::(ctx) + .and_then(|list| list.element(ctx.innermost_index() as usize)) + .cloned() + .ok_or_else(|| GraphError::new("vararg row addressed past its items").into()) +} + +/// Rank-model vararg source: the mapped row's items as lanes, elements only. +#[node_macro::node(category("Test"), extent_raw(read_graphic_row_extent))] +pub fn read_graphic_row(ctx: impl Ctx + ExtractVarArgs + ExtractIndex) -> Result, Interrupt> { + vararg_element(ctx) +} + +fn read_graphic_row_extent(_: &ReadGraphicRowNode, ctx: &C, level: u8) -> GPoll { + vararg_lanes::(ctx, level) +} + +/// Rank-model vararg source: the mapped row's items as lanes, elements only. +#[node_macro::node(category("Test"), extent_raw(read_vector_row_extent))] +pub fn read_vector_row(ctx: impl Ctx + ExtractVarArgs + ExtractIndex) -> Result, Interrupt> { + vararg_element(ctx) +} + +fn read_vector_row_extent(_: &ReadVectorRowNode, ctx: &C, level: u8) -> GPoll { + vararg_lanes::(ctx, level) +} + +/// Rank-model vararg source: the mapped row's items as lanes, elements only. +#[node_macro::node(category("Test"), extent_raw(read_raster_row_extent))] +pub fn read_raster_row(ctx: impl Ctx + ExtractVarArgs + ExtractIndex) -> Result>, Interrupt> { + vararg_element(ctx) +} + +fn read_raster_row_extent(_: &ReadRasterRowNode, ctx: &C, level: u8) -> GPoll { + vararg_lanes::>(ctx, level) +} + +/// Rank-model vararg source: the mapped row's items as lanes, elements only. +#[node_macro::node(category("Test"), extent_raw(read_color_row_extent))] +pub fn read_color_row(ctx: impl Ctx + ExtractVarArgs + ExtractIndex) -> Result, Interrupt> { + vararg_element(ctx) +} + +fn read_color_row_extent(_: &ReadColorRowNode, ctx: &C, level: u8) -> GPoll { + vararg_lanes::(ctx, level) +} + +/// Rank-model vararg source: the mapped row's items as lanes, elements only. +#[node_macro::node(category("Test"), extent_raw(read_gradient_row_extent))] +pub fn read_gradient_row(ctx: impl Ctx + ExtractVarArgs + ExtractIndex) -> Result, Interrupt> { + vararg_element(ctx) +} + +fn read_gradient_row_extent(_: &ReadGradientRowNode, ctx: &C, level: u8) -> GPoll { + vararg_lanes::(ctx, level) +} + #[node_macro::node(category("Context"), path(core_types::vector))] fn read_position( ctx: impl Ctx + ExtractPosition, diff --git a/node-graph/nodes/gcore/src/record.rs b/node-graph/nodes/gcore/src/record.rs index e96b796594..d1a2e3ac95 100644 --- a/node-graph/nodes/gcore/src/record.rs +++ b/node-graph/nodes/gcore/src/record.rs @@ -1518,6 +1518,38 @@ mod tests { assert_eq!(unsafe { out.rec(&value).element::() }, 42.); } + #[test] + fn read_row_exposes_the_vararg_items_as_lanes() { + use core_types::Color; + + let arena = Arena::new(1024).unwrap(); + let generations = []; + let scope = scope_fixture(&generations, &arena); + let ctx = ContextImpl::root(&scope); + + let node = install(crate::context::ReadColorRowNode::new(), crate::context::read_color_row_layout_meta(), &[]); + let out = Node::::layout(&node).clone(); + assert_eq!(out.depth, 1); + // No row pushed: an empty level, matching the legacy empty-list return. + assert_eq!(node.extent_at(&ctx, 0), GPoll::Final(Extent::Exactly(0))); + + let mut item = core_types::list::List::new_from_element(Color::BLACK); + item.push(core_types::list::Item::new_from_element(Color::WHITE)); + let scoped = ctx.push_vararg(&item); + let base = scoped.ctx(); + assert_eq!(node.extent_at(&base, 0), GPoll::Final(Extent::Exactly(2))); + + let head = base.index_head(); + for (lane, expected) in [(0u64, Color::BLACK), (1, Color::WHITE)] { + let mark = stack::sp(); + let GPoll::Final(value) = node.eval(&base.promoted(&head, lane)) else { + panic!("expected a final record at lane {lane}"); + }; + assert_eq!(unsafe { out.rec(&value).element::() }, expected, "lane {lane}"); + unsafe { stack::rewind(mark) }; + } + } + #[test] fn reducer_drains_a_lower_bound_level() { let arena = Arena::new(1 << 16).unwrap();