diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 98f445a8a8..6b6b59de8a 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -587,6 +587,38 @@ tagged_value! { } impl TaggedValue { + /// The flip form of [`Self::to_edge`]: list-carrying variants serve their + /// payload as a level, one lane per item, instead of materializing a + /// legacy list value. Every other variant takes the [`Self::to_edge`] + /// path unchanged. Dormant until the flip retypes the list-typed inputs. + pub fn to_leveled_edge(self) -> Result { + use core_types::value::leveled_record_value_edge; + match self { + Self::TypeDefault(td) => { + let name = td.name.as_ref(); + macro_rules! check_level { + ($list:ty, $element:ty) => { + if name == std::any::type_name::<$list>() { + return Ok(leveled_record_value_edge(Vec::<$element>::new())); + } + }; + } + check_level!(List, Graphic); + check_level!(List, Artboard); + check_level!(List>, Raster); + check_level!(List, Vector); + check_level!(List, String); + Self::TypeDefault(td).to_edge() + } + Self::F64Array(values) => Ok(leveled_record_value_edge(values)), + Self::Color(color) => Ok(leveled_record_value_edge(color.into_iter().collect::>())), + Self::Gradient(stops) => Ok(leveled_record_value_edge(vec![stops])), + Self::BrushStrokes(strokes) => Ok(leveled_record_value_edge(strokes)), + Self::NodeIdPath(path) => Ok(leveled_record_value_edge(path)), + other => other.to_edge(), + } + } + pub fn to_primitive_string(&self) -> String { match self { TaggedValue::None => "()".to_string(), @@ -910,3 +942,32 @@ mod typedefault_dispatch { for_each_type_default!(check); } } + +#[cfg(test)] +mod leveled_edges { + use super::*; + use core_types::descriptor; + use core_types::registry::record_edge_type; + + #[test] + fn list_variants_produce_leveled_edges_typed_by_element() { + let edge = TaggedValue::F64Array(vec![1., 2., 3.]).to_leveled_edge().unwrap(); + assert_eq!(edge.ty(), &record_edge_type::()); + assert_eq!(edge.layout().depth, 1); + + let edge = TaggedValue::Color(Some(Color::default())).to_leveled_edge().unwrap(); + assert_eq!(edge.ty(), &record_edge_type::()); + assert_eq!(edge.layout().depth, 1); + + let edge = TaggedValue::TypeDefault(descriptor!(List)).to_leveled_edge().unwrap(); + assert_eq!(edge.ty(), &record_edge_type::()); + assert_eq!(edge.layout().depth, 1); + } + + #[test] + fn scalar_variants_keep_their_rank_zero_edges() { + let edge = TaggedValue::Bool(true).to_leveled_edge().unwrap(); + assert_eq!(edge.ty(), &record_edge_type::()); + assert_eq!(edge.layout().depth, 0); + } +} diff --git a/node-graph/libraries/core-types/src/value.rs b/node-graph/libraries/core-types/src/value.rs index c06a33bc4b..4fcd0f96cb 100644 --- a/node-graph/libraries/core-types/src/value.rs +++ b/node-graph/libraries/core-types/src/value.rs @@ -50,6 +50,53 @@ pub fn record_value_edge(value: T) -> crate::r crate::registry::EdgeHandle::new_record::(std::sync::Arc::new(ValueSource::new(value)) as std::sync::Arc) } +/// The node behind a leveled value edge: a constant list served as one level, +/// one lane per item, with the list's length as the exact extent. +pub struct LeveledValueSource { + values: Vec, + layout: crate::record::Layout, +} + +impl LeveledValueSource { + pub fn new(values: Vec) -> Self { + Self { + values, + layout: crate::record::Layout::default().with_writes(1, crate::record::element_write::(), &[]), + } + } +} + +impl<'e, C, T> crate::node::Node for LeveledValueSource +where + C: crate::context::ExtractArena + crate::context::ExtractIndex, + T: Clone + Send + Sync + 'static, +{ + type Output = crate::record::RecordValue<'e>; + + fn eval(&self, input: &C) -> crate::gpoll::GPoll> { + let Some(value) = self.values.get(input.innermost_index() as usize) else { + return crate::gpoll::GPoll::error("value level addressed past its items"); + }; + crate::record::lift_poll(crate::gpoll::GPoll::Final(value.clone()), &self.layout, input.arena()) + } + + fn extent_at(&self, _input: &C, level: u8) -> crate::gpoll::GPoll { + match level { + 0 => crate::gpoll::GPoll::Final(crate::gpoll::Extent::Exactly(self.values.len())), + _ => crate::gpoll::GPoll::Final(crate::gpoll::Extent::Exactly(1)), + } + } + + fn layout(&self) -> &crate::record::Layout { + &self.layout + } +} + +/// The native record edge of a constant level: the edge type is the element's. +pub fn leveled_record_value_edge(values: Vec) -> crate::registry::EdgeHandle { + crate::registry::EdgeHandle::new_record::(std::sync::Arc::new(LeveledValueSource::new(values)) as std::sync::Arc) +} + impl ClonedNode { pub const fn new(value: T) -> ClonedNode { ClonedNode(value) diff --git a/node-graph/nodes/gcore/src/record.rs b/node-graph/nodes/gcore/src/record.rs index 97486d3e13..54c1ac9800 100644 --- a/node-graph/nodes/gcore/src/record.rs +++ b/node-graph/nodes/gcore/src/record.rs @@ -1661,6 +1661,25 @@ mod tests { assert_eq!(unsafe { out.rec(&value).element::() }, 20.); } + #[test] + fn a_leveled_value_source_serves_its_items_as_lanes() { + let arena = Arena::new(1 << 16).unwrap(); + let generations = []; + let scope = scope_fixture(&generations, &arena); + let ctx = ContextImpl::root(&scope); + + let source = core_types::value::LeveledValueSource::new(vec![1.5, 2.25, 3.75]); + let leveled = Node::::layout(&source).clone(); + let out = f64_layout(&[]); + reserve_for(&[&leveled, &out]); + + let node = install_flip(SumNode::new(source, &leveled), &out); + let GPoll::Final(value) = node.eval(&ctx) else { + panic!("expected a final record"); + }; + assert_eq!(unsafe { out.rec(&value).element::() }, 7.5); + } + #[test] fn reducer_drains_a_lower_bound_level() { let arena = Arena::new(1 << 16).unwrap();