Files
Graphite/node-graph/interpreted-executor/src/dynamic_executor/test.rs
Keavon Chambers 104b8b71e6 Support embedding whole items into Graphic variants so their attributes survive the conversion (#4440)
Add embedding conversion adapters that move a whole ranked value inside a Graphic variant
2026-08-16 19:51:58 -07:00

733 lines
39 KiB
Rust

use super::*;
use core_types::Context;
use core_types::list::{Item, List};
use core_types::{item, list};
use graph_craft::ProtoNodeIdentifier;
use graph_craft::document::value::TaggedValue;
use graphene_std::vector::Vector;
#[test]
fn push_node_sync() {
let mut tree = BorrowTree::default();
let val_1_protonode = ProtoNode::value(ConstructionArgs::Value(TaggedValue::U32(2u32).into()), vec![]);
let context = TypingContext::default();
let future = tree.push_node(NodeId(0), val_1_protonode, &context);
futures::executor::block_on(future).unwrap();
let _node = tree.get(NodeId(0)).unwrap();
let result: Option<Item<u32>> = futures::executor::block_on(tree.eval(NodeId(0), ()));
assert_eq!(result.map(|item| *item.element()), Some(2_u32));
}
/// Builds a two-node network feeding the given value into Bounding Box, whose primary input registers both `Item<Vector>` and `List<Vector>` wire variants.
fn bounding_box_network(content: TaggedValue) -> ProtoNetwork {
let value_node = ProtoNode::value(ConstructionArgs::Value(content.into()), vec![NodeId(0)]);
let mut bounding_box_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(0)]), vec![NodeId(1)]);
bounding_box_node.identifier = ProtoNodeIdentifier::new("core_types::vector::BoundingBoxNode");
ProtoNetwork {
inputs: vec![],
output: NodeId(1),
nodes: vec![(NodeId(0), value_node), (NodeId(1), bounding_box_node)],
}
}
fn compile_bounding_box_network(content: TaggedValue) -> BorrowTree {
let network = bounding_box_network(content);
let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY);
typing_context.update(&network).expect("The network should resolve against exactly one registered wire variant");
futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The resolved variant's constructor should instantiate")
}
#[test]
fn item_wire_variant_resolves_and_executes() {
let tree = compile_bounding_box_network(TaggedValue::TypeDefault(item!(Vector)));
let context: Context = None;
let result: Option<Item<Vector>> = futures::executor::block_on(tree.eval(NodeId(1), context.clone()));
assert!(result.is_some(), "The Item wire variant should downcast and execute end-to-end");
let wrong_type: Option<List<Vector>> = futures::executor::block_on(tree.eval(NodeId(1), context));
assert!(wrong_type.is_none(), "An Item wire should not downcast as a List");
}
#[test]
fn item_wire_promotes_to_list_connector() {
let value_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::TypeDefault(item!(f64)).into()), vec![NodeId(0)]);
// Box Corners takes a `List<f64>` primary, so feeding it an `Item<f64>` wire exercises the singleton raise
let mut box_corners_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(0)]), vec![NodeId(1)]);
box_corners_node.identifier = graphene_std::vector::generator_nodes::box_corners::IDENTIFIER;
let network = ProtoNetwork {
inputs: vec![],
output: NodeId(1),
nodes: vec![(NodeId(0), value_node), (NodeId(1), box_corners_node)],
};
let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY);
typing_context.update(&network).expect("An Item wire should resolve a List connector via promotion");
assert!(typing_context.promotions(NodeId(1)).is_some(), "The typing pass should record the promotion");
let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The promotion adapter should instantiate");
let context: Context = None;
let result: Option<Item<graphene_std::vector::misc::BoxCorners>> = futures::executor::block_on(tree.eval(NodeId(1), context));
assert!(result.is_some(), "The promoted wire should execute end-to-end");
}
// The layer content path: a rank-0 content wire enters Wrap Graphic's `List` connector by singleton raise, and the
// wrapped `Item<Graphic>` raises again at Extend's `List` connector, so layers accept rank-0 chains without new machinery
#[test]
fn rank_0_content_promotes_through_the_layer_coercion_path() {
let content_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::TypeDefault(item!(Vector)).into()), vec![NodeId(0)]);
let mut wrap_graphic_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(0)]), vec![NodeId(1)]);
wrap_graphic_node.identifier = ProtoNodeIdentifier::new("graphic_nodes::graphic::WrapGraphicNode");
let base_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::TypeDefault(list!(graphene_std::Graphic)).into()), vec![NodeId(2)]);
let mut extend_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(2), NodeId(1)]), vec![NodeId(3)]);
extend_node.identifier = ProtoNodeIdentifier::new("graphic_nodes::graphic::ExtendNode");
let network = ProtoNetwork {
inputs: vec![],
output: NodeId(3),
nodes: vec![(NodeId(0), content_node), (NodeId(1), wrap_graphic_node), (NodeId(2), base_node), (NodeId(3), extend_node)],
};
let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY);
typing_context.update(&network).expect("A rank-0 content wire should resolve the layer coercion path via promotion");
assert!(typing_context.promotions(NodeId(1)).is_some(), "The rank-0 content should be raised at Wrap Graphic's List connector");
assert!(typing_context.promotions(NodeId(3)).is_some(), "The wrapped Item<Graphic> should be raised at Extend's List connector");
let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The promotion adapters should instantiate");
let context: Context = None;
let result: Option<List<graphene_std::Graphic>> = futures::executor::block_on(tree.eval(NodeId(3), context));
let stack = result.expect("The layer coercion path should execute end-to-end");
assert_eq!(stack.len(), 1, "The rank-0 content should contribute exactly one graphic to the stack");
}
/// Builds a network feeding the given content plus an f64 distance value into Offset Points, whose distance input is ranked `Item<f64>`.
fn offset_points_network(content: TaggedValue) -> ProtoNetwork {
let content_node = ProtoNode::value(ConstructionArgs::Value(content.into()), vec![NodeId(0)]);
let distance_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::F64(10.).into()), vec![NodeId(1)]);
let mut input_adapter_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(1)]), vec![NodeId(2)]);
input_adapter_node.identifier = ProtoNodeIdentifier::new("input_adapter<f64>");
let mut offset_points_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(0), NodeId(2)]), vec![NodeId(3)]);
offset_points_node.identifier = ProtoNodeIdentifier::new("core_types::vector::OffsetPointsNode");
ProtoNetwork {
inputs: vec![],
output: NodeId(3),
nodes: vec![(NodeId(0), content_node), (NodeId(1), distance_node), (NodeId(2), input_adapter_node), (NodeId(3), offset_points_node)],
}
}
#[test]
fn mixed_rank_connectors_resolve_via_promotion() {
let network = offset_points_network(TaggedValue::TypeDefault(list!(Vector)));
let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY);
typing_context
.update(&network)
.expect("A List primary with an Item parameter should resolve the mapped variant via promotion");
assert!(typing_context.promotions(NodeId(3)).is_some(), "The Item distance should be marked for promotion");
let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("Construction should wrap the promoted argument");
let context: Context = None;
let result: Option<List<Vector>> = futures::executor::block_on(tree.eval(NodeId(3), context));
assert!(result.is_some(), "The zipped mapped variant should execute end-to-end");
}
#[test]
fn all_item_connectors_resolve_without_promotion() {
let network = offset_points_network(TaggedValue::TypeDefault(item!(Vector)));
let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY);
typing_context.update(&network).expect("All-Item connectors should resolve the rank-0 variant exactly");
assert!(typing_context.promotions(NodeId(3)).is_none(), "No promotion should be needed at rank 0");
let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The rank-0 variant should instantiate");
let context: Context = None;
let result: Option<Item<Vector>> = futures::executor::block_on(tree.eval(NodeId(3), context));
assert!(result.is_some(), "The rank-0 variant should execute and stay rank 0");
}
/// Builds a Transform network: content (node 0) plus four parameter values, each promoted onto Item wires as the preprocessor would.
fn transform_network(content: TaggedValue, rotation: TaggedValue) -> ProtoNetwork {
let mut nodes = vec![(NodeId(0), ProtoNode::value(ConstructionArgs::Value(content.into()), vec![NodeId(0)]))];
let parameters = [
(TaggedValue::DVec2(glam::DVec2::new(5., 0.)), "DVec2"),
(rotation, "f64"),
(TaggedValue::DVec2(glam::DVec2::ONE), "DVec2"),
(TaggedValue::DVec2(glam::DVec2::ZERO), "DVec2"),
];
let mut transform_inputs = vec![NodeId(0)];
let mut next_id = 1;
for (value, element) in parameters {
let value_id = NodeId(next_id);
let input_adapter_id = NodeId(next_id + 1);
next_id += 2;
nodes.push((value_id, ProtoNode::value(ConstructionArgs::Value(value.into()), vec![value_id])));
let mut input_adapter_node = ProtoNode::value(ConstructionArgs::Nodes(vec![value_id]), vec![input_adapter_id]);
input_adapter_node.identifier = ProtoNodeIdentifier::with_owned_string(format!("input_adapter<{element}>"));
nodes.push((input_adapter_id, input_adapter_node));
transform_inputs.push(input_adapter_id);
}
let output = NodeId(next_id);
let mut transform_node = ProtoNode::value(ConstructionArgs::Nodes(transform_inputs), vec![output]);
transform_node.identifier = graphene_std::transform_nodes::transform::IDENTIFIER;
nodes.push((output, transform_node));
ProtoNetwork { inputs: vec![], output, nodes }
}
#[test]
fn transform_composes_onto_item_wire() {
use glam::{DAffine2, DVec2};
let network = transform_network(TaggedValue::TypeDefault(item!(Vector)), TaggedValue::F64(0.));
let output = network.output;
let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY);
typing_context.update(&network).expect("Transform should resolve its rank-0 variant");
assert!(typing_context.promotions(output).is_none(), "All-Item connectors should need no promotion");
let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("Transform's rank-0 variant should instantiate");
let context: Context = None;
let result: Option<Item<Vector>> = futures::executor::block_on(tree.eval(output, context));
let item = result.expect("A rank-0 chain through Transform should stay rank 0");
let transform = item.attribute_cloned_or_default::<DAffine2>(core_types::ATTR_TRANSFORM);
assert_eq!(transform.translation, DVec2::new(5., 0.), "The translation should compose onto the item's transform attribute");
}
#[test]
fn transform_broadcasts_item_content_across_a_framed_parameter() {
use glam::DAffine2;
let network = transform_network(TaggedValue::TypeDefault(item!(Vector)), TaggedValue::F64Array(vec![0., 90.]));
let output = network.output;
let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY);
typing_context
.update(&network)
.expect("A framed rotation should resolve the mapped variant via promotion of the other connectors");
assert!(typing_context.promotions(output).is_some(), "The Item-typed connectors should be raised into the frame");
let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The mapped variant should instantiate");
let context: Context = None;
let result: Option<List<Vector>> = futures::executor::block_on(tree.eval(output, context));
let list = result.expect("The broadcast should produce a List");
assert_eq!(list.len(), 2, "One output item per frame slot");
let first: DAffine2 = list.attribute_cloned_or_default(core_types::ATTR_TRANSFORM, 0);
let second: DAffine2 = list.attribute_cloned_or_default(core_types::ATTR_TRANSFORM, 1);
assert!((first.matrix2.col(0).y - 0.).abs() < 1e-10, "Slot 0 should be unrotated");
assert!((second.matrix2.col(0).y - 1.).abs() < 1e-10, "Slot 1 should be rotated 90 degrees");
}
#[test]
fn generator_frames_over_a_list_parameter() {
// A `()` generator (Circle) fed a `List<f64>` radius should frame into one circle per slot
let primary = ProtoNode::value(ConstructionArgs::Value(TaggedValue::None.into()), vec![NodeId(0)]);
let radii = ProtoNode::value(ConstructionArgs::Value(TaggedValue::F64Array(vec![10., 20., 30.]).into()), vec![NodeId(1)]);
let mut radius_adapter = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(1)]), vec![NodeId(2)]);
radius_adapter.identifier = ProtoNodeIdentifier::new("input_adapter<f64>");
let mut circle_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(0), NodeId(2)]), vec![NodeId(3)]);
circle_node.identifier = graphene_std::vector_nodes::circle::IDENTIFIER;
let network = ProtoNetwork {
inputs: vec![],
output: NodeId(3),
nodes: vec![(NodeId(0), primary), (NodeId(1), radii), (NodeId(2), radius_adapter), (NodeId(3), circle_node)],
};
let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY);
typing_context.update(&network).expect("A List<f64> radius should resolve Circle's mapped generator variant");
let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The mapped generator variant should instantiate");
let context: Context = None;
let result: Option<List<Vector>> = futures::executor::block_on(tree.eval(NodeId(3), context));
let list = result.expect("The generator frame should produce a List<Vector>");
assert_eq!(list.len(), 3, "One circle per radius slot");
}
/// Builds the compiler's cache chain (child, then Memoize, then Context Modification) around a value, as `insert_context_nullification_node` does.
fn nullification_chain_network(value: TaggedValue) -> ProtoNetwork {
let value_node = ProtoNode::value(ConstructionArgs::Value(value.into()), vec![NodeId(0)]);
let mut memoize_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(0)]), vec![NodeId(1)]);
memoize_node.identifier = graphene_core::memo::memoize::IDENTIFIER;
let features_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::ContextFeatures(Default::default()).into()), vec![NodeId(2)]);
let mut nullification_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(1), NodeId(2)]), vec![NodeId(3)]);
nullification_node.identifier = graphene_core::context_modification::context_modification::IDENTIFIER;
ProtoNetwork {
inputs: vec![],
output: NodeId(3),
nodes: vec![(NodeId(0), value_node), (NodeId(1), memoize_node), (NodeId(2), features_node), (NodeId(3), nullification_node)],
}
}
#[test]
fn the_nullification_chain_resolves_for_ranked_enum_wires() {
use graphene_std::vector::style::StrokeAlign;
// The Item form, as a wrapped input adapter's output presents to the chain
let network = nullification_chain_network(TaggedValue::TypeDefault(item!(StrokeAlign)));
let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY);
typing_context.update(&network).expect("An Item<StrokeAlign> wire should resolve through the compiler's cache chain");
// The List form, as a whole-list enum wire presents to the chain
let network = nullification_chain_network(TaggedValue::TypeDefault(list!(StrokeAlign)));
let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY);
typing_context.update(&network).expect("A List<StrokeAlign> wire should resolve through the compiler's cache chain");
}
#[test]
fn value_wires_materialize_as_items_at_resolution() {
use glam::{DAffine2, DVec2};
let values = [
TaggedValue::DAffine2(DAffine2::IDENTITY),
TaggedValue::DVec2(DVec2::new(7., 0.)),
TaggedValue::F64(0.),
TaggedValue::DVec2(DVec2::ONE),
TaggedValue::DVec2(DVec2::ZERO),
];
let mut nodes: Vec<_> = values
.into_iter()
.enumerate()
.map(|(index, value)| (NodeId(index as u64), ProtoNode::value(ConstructionArgs::Value(value.into()), vec![NodeId(index as u64)])))
.collect();
let mut transform_node = ProtoNode::value(ConstructionArgs::Nodes((0..5).map(NodeId).collect()), vec![NodeId(5)]);
transform_node.identifier = graphene_std::transform_nodes::transform::IDENTIFIER;
nodes.push((NodeId(5), transform_node));
let network = ProtoNetwork {
inputs: vec![],
output: NodeId(5),
nodes,
};
let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY);
typing_context.update(&network).expect("Value wires should materialize as Items and resolve the all-Item variant");
assert!(typing_context.promotions(NodeId(5)).is_none(), "Already-Item value wires should need no promotion");
let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The all-Item variant should instantiate");
let context: Context = None;
let result: Option<Item<DAffine2>> = futures::executor::block_on(tree.eval(NodeId(5), context));
let item = result.expect("A value matrix should flow through Transform as an Item");
let transform = item.attribute_cloned_or_default::<DAffine2>(core_types::ATTR_TRANSFORM);
assert_eq!(transform.translation, DVec2::new(7., 0.), "The translation should compose onto the gained transform attribute");
}
// A position's Item wire converts through the vector input adapter into a single-anchor path, which the ItemToList promotion can then raise at a List connector
#[test]
fn position_value_converts_through_the_vector_input_adapter() {
let position_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::DVec2(glam::DVec2::new(3., 4.)).into()), vec![NodeId(0)]);
let mut input_adapter_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(0)]), vec![NodeId(1)]);
input_adapter_node.identifier = ProtoNodeIdentifier::new("input_adapter<Vector>");
let network = ProtoNetwork {
inputs: vec![],
output: NodeId(1),
nodes: vec![(NodeId(0), position_node), (NodeId(1), input_adapter_node)],
};
let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY);
typing_context.update(&network).expect("An Item<DVec2> wire should resolve the adapter's element conversion row");
let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The conversion constructor should instantiate");
let context: Context = None;
let result: Option<Item<Vector>> = futures::executor::block_on(tree.eval(NodeId(1), context));
assert!(result.is_some(), "The position should arrive as an Item<Vector> single-anchor path");
}
// The 'Colors to Gradient' node turns an entire `List<Color>` wire into one gradient with those colors as its stops
#[test]
fn color_list_wraps_through_the_colors_to_gradient_node() {
let color_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::Color(graphene_std::Color::WHITE).into()), vec![NodeId(0)]);
let mut raise_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(0)]), vec![NodeId(1)]);
raise_node.identifier = ProtoNodeIdentifier::new("graphene_core::ops::ItemToListNode<Color>");
let mut colors_to_gradient_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(1)]), vec![NodeId(2)]);
colors_to_gradient_node.identifier = ProtoNodeIdentifier::new("graphic_nodes::graphic::ColorsToGradientNode");
let network = ProtoNetwork {
inputs: vec![],
output: NodeId(2),
nodes: vec![(NodeId(0), color_node), (NodeId(1), raise_node), (NodeId(2), colors_to_gradient_node)],
};
let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY);
typing_context.update(&network).expect("A List<Color> wire should resolve the node's List<Color> implementation");
let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The node constructor should instantiate");
let context: Context = None;
let result: Option<Item<graphene_std::vector::Gradient>> = futures::executor::block_on(tree.eval(NodeId(2), context));
let gradient = result.expect("The color list should arrive wrapped as a gradient");
assert_eq!(gradient.element().len(), 1, "The single color should become the gradient's one stop");
}
// A paint wire feeding a `Graphic` connector embeds whole, so the gradient's own attributes stay on the item inside the variant
#[test]
fn gradient_value_embeds_through_the_graphic_input_adapter() {
use core_types::ATTR_GRADIENT_SPREAD;
use graphene_std::Graphic;
use graphene_std::vector::{Gradient, GradientRamp, GradientSpread};
let ramp = GradientRamp {
gradient_spread: GradientSpread::Reflect,
..GradientRamp::from(Gradient::default())
};
let gradient_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::GradientRamp(ramp).into()), vec![NodeId(0)]);
let mut input_adapter_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(0)]), vec![NodeId(1)]);
input_adapter_node.identifier = ProtoNodeIdentifier::new("input_adapter<Graphic>");
let network = ProtoNetwork {
inputs: vec![],
output: NodeId(1),
nodes: vec![(NodeId(0), gradient_node), (NodeId(1), input_adapter_node)],
};
let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY);
typing_context.update(&network).expect("An Item<Gradient> wire should resolve the adapter's embedding row");
let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The embedding constructor should instantiate");
let context: Context = None;
let result: Option<Item<Graphic>> = futures::executor::block_on(tree.eval(NodeId(1), context));
let embedded = result.expect("The gradient should arrive as an Item<Graphic>");
assert!(embedded.attributes().iter_any().next().is_none(), "The fresh outer envelope describes the graphic, so it starts empty");
let Graphic::Gradient(inner) = embedded.element() else { panic!("expected a gradient graphic") };
assert_eq!(
inner.attribute::<GradientSpread>(ATTR_GRADIENT_SPREAD),
Some(&GradientSpread::Reflect),
"The gradient's placement attributes should ride the item inside the variant, where the renderer reads them"
);
}
// A scalar wire feeding a `DVec2` connector splats into both axes through the input adapter's `Convert` row
#[test]
fn number_value_splats_through_the_vec2_input_adapter() {
let number_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::F64(-60.).into()), vec![NodeId(0)]);
let mut input_adapter_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(0)]), vec![NodeId(1)]);
input_adapter_node.identifier = ProtoNodeIdentifier::new("input_adapter<DVec2>");
let network = ProtoNetwork {
inputs: vec![],
output: NodeId(1),
nodes: vec![(NodeId(0), number_node), (NodeId(1), input_adapter_node)],
};
let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY);
typing_context.update(&network).expect("An f64 wire should resolve the adapter's splat conversion row");
let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The splat constructor should instantiate");
let context: Context = None;
let result: Option<Item<glam::DVec2>> = futures::executor::block_on(tree.eval(NodeId(1), context));
assert_eq!(result.map(|item| *item.element()), Some(glam::DVec2::splat(-60.)), "The scalar should splat into both axes");
}
// A scalar wire feeding a `String` connector formats as text through the input adapter's `Convert` row
#[test]
fn number_value_formats_through_the_string_input_adapter() {
let number_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::F64(42.).into()), vec![NodeId(0)]);
let mut input_adapter_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(0)]), vec![NodeId(1)]);
input_adapter_node.identifier = ProtoNodeIdentifier::new("input_adapter<String>");
let network = ProtoNetwork {
inputs: vec![],
output: NodeId(1),
nodes: vec![(NodeId(0), number_node), (NodeId(1), input_adapter_node)],
};
let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY);
typing_context.update(&network).expect("An f64 wire should resolve the adapter's formatting conversion row");
let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The formatting constructor should instantiate");
let context: Context = None;
let result: Option<Item<String>> = futures::executor::block_on(tree.eval(NodeId(1), context));
assert_eq!(result.map(|item| item.element().clone()), Some("42".to_string()), "The number should format as its text representation");
}
// A `List` wire feeding a `ListDyn` connector erases its element type through the input adapter's `Into` row
#[test]
fn list_wire_erases_through_the_list_dyn_input_adapter() {
use core_types::list::ListDyn;
let list_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::F64Array(vec![1., 2., 3.]).into()), vec![NodeId(0)]);
let mut input_adapter_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(0)]), vec![NodeId(1)]);
input_adapter_node.identifier = ProtoNodeIdentifier::new("input_adapter<ListDyn>");
let network = ProtoNetwork {
inputs: vec![],
output: NodeId(1),
nodes: vec![(NodeId(0), list_node), (NodeId(1), input_adapter_node)],
};
let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY);
typing_context.update(&network).expect("A List<f64> wire should resolve the ListDyn erasure row");
let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The erasure constructor should instantiate");
let context: Context = None;
let result: Option<ListDyn> = futures::executor::block_on(tree.eval(NodeId(1), context));
let erased = result.expect("The erased list should arrive as a ListDyn");
assert_eq!(erased.len(), 3, "The erased list should keep its row count");
}
#[test]
fn value_wire_passes_through_the_input_adapter_as_item() {
let value_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::F64(3.).into()), vec![NodeId(0)]);
let mut input_adapter_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(0)]), vec![NodeId(1)]);
input_adapter_node.identifier = ProtoNodeIdentifier::new("input_adapter<f64>");
let network = ProtoNetwork {
inputs: vec![],
output: NodeId(1),
nodes: vec![(NodeId(0), value_node), (NodeId(1), input_adapter_node)],
};
let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY);
typing_context.update(&network).expect("An f64 value's Item wire should resolve the adapter's passthrough row");
let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The passthrough constructor should instantiate");
let context: Context = None;
let result: Option<Item<f64>> = futures::executor::block_on(tree.eval(NodeId(1), context));
assert_eq!(result.map(|item| *item.element()), Some(3.), "The value should arrive as an Item");
}
// Path Modify's ranked modification parameter: a `Box<VectorModification>` value rides the `Item` wire through its input adapter,
// exercising the nested-generic identifier round-trip between the registered `stringify!` name and the preprocessor's simplified name
#[test]
fn modification_value_rides_the_item_wire_through_its_input_adapter() {
use graphene_std::vector::VectorModification;
let modification = TaggedValue::VectorModification(Default::default());
let value_node = ProtoNode::value(ConstructionArgs::Value(modification.into()), vec![NodeId(0)]);
let mut input_adapter_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(0)]), vec![NodeId(1)]);
input_adapter_node.identifier = ProtoNodeIdentifier::new("input_adapter<Box<VectorModification>>");
let network = ProtoNetwork {
inputs: vec![],
output: NodeId(1),
nodes: vec![(NodeId(0), value_node), (NodeId(1), input_adapter_node)],
};
let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY);
typing_context.update(&network).expect("A modification value's Item wire should resolve the adapter's passthrough row");
let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The passthrough constructor should instantiate");
let context: Context = None;
let result: Option<Item<Box<VectorModification>>> = futures::executor::block_on(tree.eval(NodeId(1), context));
assert!(result.is_some(), "The modification should arrive as an Item");
}
// The Write Attribute value input: a value's Item wire boxes its element into a type-erased attribute value through the input adapter
#[test]
fn item_wire_boxes_into_the_attribute_value_connector() {
use graphene_std::list::AttributeValueDyn;
let value_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::F64(3.).into()), vec![NodeId(0)]);
let mut attribute_adapter_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(0)]), vec![NodeId(1)]);
attribute_adapter_node.identifier = ProtoNodeIdentifier::new("input_adapter<AttributeValueDyn>");
let network = ProtoNetwork {
inputs: vec![],
output: NodeId(1),
nodes: vec![(NodeId(0), value_node), (NodeId(1), attribute_adapter_node)],
};
let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY);
typing_context.update(&network).expect("An Item<f64> wire should resolve the attribute value boxing row");
assert!(typing_context.promotions(NodeId(1)).is_none(), "The already-Item value wire should need no promotion");
let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The boxing constructor should instantiate");
let context: Context = None;
let result: Option<Item<AttributeValueDyn>> = futures::executor::block_on(tree.eval(NodeId(1), context));
let boxed = result.expect("The boxed attribute value should arrive as an Item");
assert_eq!(
boxed.element().0.as_any().downcast_ref::<f64>(),
Some(&3.),
"The stored value should be the bare element, not the whole Item"
);
}
#[test]
fn list_wire_variant_resolves_and_executes() {
let tree = compile_bounding_box_network(TaggedValue::TypeDefault(list!(Vector)));
let context: Context = None;
let result: Option<List<Vector>> = futures::executor::block_on(tree.eval(NodeId(1), context.clone()));
assert!(result.is_some(), "The mapped List wire variant should downcast and execute end-to-end");
let wrong_type: Option<Item<Vector>> = futures::executor::block_on(tree.eval(NodeId(1), context));
assert!(wrong_type.is_none(), "A List wire should not downcast as an Item");
}
#[test]
fn expander_flattens_under_the_frame() {
// A string value's Item wire feeds String Split's expander primary; its parameters ride Item wires through their input adapters
let string_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::String("a,b".into()).into()), vec![NodeId(0)]);
let delimiter_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::String(",".into()).into()), vec![NodeId(1)]);
let mut delimiter_input_adapter_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(1)]), vec![NodeId(2)]);
delimiter_input_adapter_node.identifier = ProtoNodeIdentifier::new("input_adapter<String>");
let escaping_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::Bool(false).into()), vec![NodeId(3)]);
let mut escaping_input_adapter_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(3)]), vec![NodeId(4)]);
escaping_input_adapter_node.identifier = ProtoNodeIdentifier::new("input_adapter<bool>");
let output = NodeId(5);
let mut string_split_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(0), NodeId(2), NodeId(4)]), vec![output]);
string_split_node.identifier = graphene_std::text_nodes::string_split::IDENTIFIER;
let network = ProtoNetwork {
inputs: vec![],
output,
nodes: vec![
(NodeId(0), string_node),
(NodeId(1), delimiter_node),
(NodeId(2), delimiter_input_adapter_node),
(NodeId(3), escaping_node),
(NodeId(4), escaping_input_adapter_node),
(output, string_split_node),
],
};
let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY);
typing_context
.update(&network)
.expect("All-Item connectors should resolve the expander's direct `Item -> List` variant");
assert!(typing_context.promotions(output).is_none(), "No promotion should be needed when every connector is already an Item");
let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The expander variant should instantiate");
let context: Context = None;
let result: Option<List<String>> = futures::executor::block_on(tree.eval(output, context));
let list = result.expect("An Item-wired expander should produce a List");
assert_eq!(list.len(), 2, "Splitting \"a,b\" on the comma should expand into two rows");
let substrings: Vec<_> = list.iter_element_values().map(|s| s.as_str()).collect();
assert_eq!(substrings, ["a", "b"], "The rows should hold the split substrings");
}
#[test]
fn whole_list_switches_as_one_bundle() {
// One bool selecting between two whole `List<f64>` stacks: each branch bundles into a rank-0 cell, and the result unbundles back to the flat stack
let condition_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::Bool(true).into()), vec![NodeId(0)]);
let if_true_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::F64Array(vec![1., 2., 3.]).into()), vec![NodeId(1)]);
let if_false_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::F64Array(vec![4., 5.]).into()), vec![NodeId(2)]);
let mut switch_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(0), NodeId(1), NodeId(2)]), vec![NodeId(3)]);
switch_node.identifier = ProtoNodeIdentifier::new("math_nodes::SwitchNode");
let mut unbundle_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(3)]), vec![NodeId(4)]);
unbundle_node.identifier = ProtoNodeIdentifier::new("graphene_core::ops::UnbundleNode<f64>");
let network = ProtoNetwork {
inputs: vec![],
output: NodeId(4),
nodes: vec![
(NodeId(0), condition_node),
(NodeId(1), if_true_node),
(NodeId(2), if_false_node),
(NodeId(3), switch_node),
(NodeId(4), unbundle_node),
],
};
let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY);
typing_context
.update(&network)
.expect("A List<f64> branch should resolve the Item<Bundle<f64>> row via the bundle wrap");
let promotions = typing_context.promotions(NodeId(3)).expect("The condition wrap and both branch bundles should be recorded");
let branch_bundles = promotions
.iter()
.filter(|(index, adapter)| *index != 0 && matches!(adapter, graph_craft::proto::Promotion::Bundle(_)))
.count();
assert_eq!(branch_bundles, 2, "Both branches should bundle their whole list into one opaque cell");
let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The bundle, wrap, and unbundle adapters should instantiate");
let context: Context = None;
let result: Option<List<f64>> = futures::executor::block_on(tree.eval(NodeId(4), context));
let list = result.expect("The whole stack should round-trip through the bundle switch back to a flat List<f64>");
let values: Vec<f64> = list.iter_element_values().copied().collect();
assert_eq!(values, [1., 2., 3.], "The taken branch's whole list should come through unchanged");
}
#[test]
fn a_bundle_unbundles_into_a_list_connector() {
// A bundled wire (sourced here from a BundleNode, as a Switch branch produces one) feeding Extend's whole-`List` base connector
let stack_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::TypeDefault(list!(graphene_std::Graphic)).into()), vec![NodeId(0)]);
let mut bundle_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(0)]), vec![NodeId(1)]);
bundle_node.identifier = ProtoNodeIdentifier::new("graphene_core::ops::BundleNode<Graphic>");
let new_layers_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::TypeDefault(list!(graphene_std::Graphic)).into()), vec![NodeId(2)]);
let mut extend_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(1), NodeId(2)]), vec![NodeId(3)]);
extend_node.identifier = ProtoNodeIdentifier::new("graphic_nodes::graphic::ExtendNode");
let network = ProtoNetwork {
inputs: vec![],
output: NodeId(3),
nodes: vec![(NodeId(0), stack_node), (NodeId(1), bundle_node), (NodeId(2), new_layers_node), (NodeId(3), extend_node)],
};
let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY);
typing_context.update(&network).expect("A bundled wire should feed Extend's List<Graphic> connector via the unbundle");
let promotions = typing_context.promotions(NodeId(3)).expect("Extend's bundled base should be marked for unbundling");
assert!(
promotions.iter().any(|(index, adapter)| *index == 0 && matches!(adapter, graph_craft::proto::Promotion::Unbundle(_))),
"The base connector should unbundle the whole list"
);
let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The unbundle adapter should instantiate");
let context: Context = None;
let result: Option<List<graphene_std::Graphic>> = futures::executor::block_on(tree.eval(NodeId(3), context));
assert!(result.is_some(), "The unbundled stack should flow into Extend as a List<Graphic>");
}
#[test]
fn a_whole_list_of_scalars_switches_as_one_bundle() {
// A single bool selecting between two whole `List<f64>` values, covering a primitive element type and confirming the selected list survives intact
let condition_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::Bool(true).into()), vec![NodeId(0)]);
let if_true_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::F64Array(vec![1., 2.]).into()), vec![NodeId(1)]);
let if_false_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::F64Array(vec![3., 4., 5.]).into()), vec![NodeId(2)]);
let mut switch_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(0), NodeId(1), NodeId(2)]), vec![NodeId(3)]);
switch_node.identifier = ProtoNodeIdentifier::new("math_nodes::SwitchNode");
let mut unbundle_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(3)]), vec![NodeId(4)]);
unbundle_node.identifier = ProtoNodeIdentifier::new("graphene_core::ops::UnbundleNode<f64>");
let network = ProtoNetwork {
inputs: vec![],
output: NodeId(4),
nodes: vec![
(NodeId(0), condition_node),
(NodeId(1), if_true_node),
(NodeId(2), if_false_node),
(NodeId(3), switch_node),
(NodeId(4), unbundle_node),
],
};
let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY);
typing_context
.update(&network)
.expect("A List<f64> branch should resolve the Item<Bundle<f64>> row via the bundle wrap");
let promotions = typing_context.promotions(NodeId(3)).expect("The condition wrap and both branch bundles should be recorded");
let branch_bundles = promotions
.iter()
.filter(|(index, adapter)| *index != 0 && matches!(adapter, graph_craft::proto::Promotion::Bundle(_)))
.count();
assert_eq!(branch_bundles, 2, "Both scalar-list branches should bundle into one opaque cell");
let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The bundle, wrap, and unbundle adapters should instantiate");
let context: Context = None;
let result: Option<List<f64>> = futures::executor::block_on(tree.eval(NodeId(4), context));
let list = result.expect("The whole scalar list should round-trip through the bundle switch");
assert_eq!(list.len(), 2, "The true branch's whole list should be selected and preserved intact");
}