mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
Drop the superseded rank promotion executor tests and cover brush stroke lists
This commit is contained in:
@@ -1,816 +0,0 @@
|
||||
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 Into Group's `List` connector by singleton raise, and the
|
||||
// grouped `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 into_group_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(0)]), vec![NodeId(1)]);
|
||||
into_group_node.identifier = ProtoNodeIdentifier::new("graphic_nodes::graphic::IntoGroupNode");
|
||||
|
||||
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), into_group_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 Into Group's List connector");
|
||||
assert!(typing_context.promotions(NodeId(3)).is_some(), "The grouped 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");
|
||||
}
|
||||
|
||||
// 'Into Group' reduces whole lists, so a rank-0 position reaches it by singleton raise rather than a row of its own
|
||||
#[test]
|
||||
fn position_value_raises_into_the_into_group_reducer() {
|
||||
use graphene_std::Graphic;
|
||||
|
||||
let position_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::DVec2(glam::DVec2::new(3., 4.)).into()), vec![NodeId(0)]);
|
||||
|
||||
let mut into_group_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(0)]), vec![NodeId(1)]);
|
||||
into_group_node.identifier = ProtoNodeIdentifier::new("graphic_nodes::graphic::IntoGroupNode");
|
||||
|
||||
let network = ProtoNetwork {
|
||||
inputs: vec![],
|
||||
output: NodeId(1),
|
||||
nodes: vec![(NodeId(0), position_node), (NodeId(1), into_group_node)],
|
||||
};
|
||||
let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY);
|
||||
typing_context.update(&network).expect("An Item<DVec2> wire should raise into Into Group's List<DVec2> row");
|
||||
assert!(typing_context.promotions(NodeId(1)).is_some(), "The rank-0 position should be raised at Into Group's List connector");
|
||||
let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The reducer constructor should instantiate");
|
||||
|
||||
let context: Context = None;
|
||||
let result: Option<Item<Graphic>> = futures::executor::block_on(tree.eval(NodeId(1), context));
|
||||
let grouped = result.expect("The position should arrive as an Item<Graphic>");
|
||||
|
||||
let Graphic::VectorList(anchors) = grouped.element() else {
|
||||
panic!("expected a vector list graphic")
|
||||
};
|
||||
assert_eq!(anchors.len(), 1, "The single position should group as one anchor point");
|
||||
}
|
||||
|
||||
// The 'Colors to Gradient' node turns an entire color wire into one gradient with those colors as its stops,
|
||||
// reaching its `List<Graphic>` connector through the embedding adapter
|
||||
#[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 graphic_adapter = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(1)]), vec![NodeId(2)]);
|
||||
graphic_adapter.identifier = ProtoNodeIdentifier::new("input_adapter<Graphic>");
|
||||
|
||||
let mut colors_to_gradient_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(2)]), vec![NodeId(3)]);
|
||||
colors_to_gradient_node.identifier = ProtoNodeIdentifier::new("graphic_nodes::graphic::ColorsToGradientNode");
|
||||
|
||||
let network = ProtoNetwork {
|
||||
inputs: vec![],
|
||||
output: NodeId(3),
|
||||
nodes: vec![(NodeId(0), color_node), (NodeId(1), raise_node), (NodeId(2), graphic_adapter), (NodeId(3), 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 embed into the node's List<Graphic> connector");
|
||||
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(3), 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");
|
||||
}
|
||||
|
||||
// As Graphic asserts the type without changing rank, so a vector list arrives as one graphic per item rather than one group
|
||||
#[test]
|
||||
fn as_graphic_converts_a_vector_list_element_wise() {
|
||||
use graphene_std::Graphic;
|
||||
|
||||
// Two radii frame the Circle generator into a two-element `List<Vector>`, the wire As Graphic then converts
|
||||
let primary = ProtoNode::value(ConstructionArgs::Value(TaggedValue::None.into()), vec![NodeId(0)]);
|
||||
let radii = ProtoNode::value(ConstructionArgs::Value(TaggedValue::F64Array(vec![10., 20.]).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 mut graphic_adapter = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(3)]), vec![NodeId(4)]);
|
||||
graphic_adapter.identifier = ProtoNodeIdentifier::new("input_adapter<Graphic>");
|
||||
|
||||
let mut as_graphic_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(4)]), vec![NodeId(5)]);
|
||||
as_graphic_node.identifier = ProtoNodeIdentifier::new("graphic_nodes::graphic::AsGraphicNode");
|
||||
|
||||
let network = ProtoNetwork {
|
||||
inputs: vec![],
|
||||
output: NodeId(5),
|
||||
nodes: vec![
|
||||
(NodeId(0), primary),
|
||||
(NodeId(1), radii),
|
||||
(NodeId(2), radius_adapter),
|
||||
(NodeId(3), circle_node),
|
||||
(NodeId(4), graphic_adapter),
|
||||
(NodeId(5), as_graphic_node),
|
||||
],
|
||||
};
|
||||
let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY);
|
||||
typing_context
|
||||
.update(&network)
|
||||
.expect("A List<Vector> wire should resolve As Graphic's mapped variant through the embedding adapter");
|
||||
let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The mapped constructor should instantiate");
|
||||
|
||||
let context: Context = None;
|
||||
let result: Option<List<Graphic>> = futures::executor::block_on(tree.eval(NodeId(5), context));
|
||||
let graphics = result.expect("The vector list should arrive as a graphic list");
|
||||
|
||||
assert_eq!(graphics.len(), 2, "Each vector should become its own graphic rather than collapsing into one group");
|
||||
assert!(
|
||||
graphics.iter_element_values().all(|graphic| matches!(graphic, Graphic::Vector(_))),
|
||||
"Each element should embed as the rank-0 vector variant that mirrors its wire"
|
||||
);
|
||||
}
|
||||
|
||||
// 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");
|
||||
}
|
||||
Reference in New Issue
Block a user