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> = 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` and `List` 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> = 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> = 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` primary, so feeding it an `Item` 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> = 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` 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 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> = 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`. 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"); 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> = 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> = 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> = 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::(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> = 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` 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"); 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 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> = futures::executor::block_on(tree.eval(NodeId(3), context)); let list = result.expect("The generator frame should produce a List"); 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 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 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> = 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::(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"); 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 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> = futures::executor::block_on(tree.eval(NodeId(1), context)); assert!(result.is_some(), "The position should arrive as an Item single-anchor path"); } // The 'Colors to Gradient' node turns an entire `List` 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"); 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 wire should resolve the node's List 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> = 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"); 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 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> = futures::executor::block_on(tree.eval(NodeId(1), context)); let embedded = result.expect("The gradient should arrive as an Item"); 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::(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"); 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> = 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"); 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> = 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"); 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 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 = 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"); 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> = 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` 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>"); 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>> = 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"); 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 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> = 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::(), 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> = 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> = 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"); 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"); 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> = 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` 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"); 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 branch should resolve the Item> 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> = 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"); let values: Vec = 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"); 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 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> = futures::executor::block_on(tree.eval(NodeId(3), context)); assert!(result.is_some(), "The unbundled stack should flow into Extend as a List"); } #[test] fn a_whole_list_of_scalars_switches_as_one_bundle() { // A single bool selecting between two whole `List` 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"); 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 branch should resolve the Item> 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> = 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"); }