From 8555e6fb797665466282b5d669ce7b2b8d2460d4 Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Tue, 8 Sep 2026 15:09:41 +0000 Subject: [PATCH] Drop the superseded rank promotion executor tests and cover brush stroke lists --- .../src/dynamic_executor/test.rs | 816 ------------------ node-graph/nodes/path-bool/src/lib.rs | 2 +- 2 files changed, 1 insertion(+), 817 deletions(-) delete mode 100644 node-graph/interpreted-executor/src/dynamic_executor/test.rs diff --git a/node-graph/interpreted-executor/src/dynamic_executor/test.rs b/node-graph/interpreted-executor/src/dynamic_executor/test.rs deleted file mode 100644 index cd5b96fa60..0000000000 --- a/node-graph/interpreted-executor/src/dynamic_executor/test.rs +++ /dev/null @@ -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> = 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 Into Group's `List` connector by singleton raise, and the -// grouped `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 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 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"); -} - -// '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 wire should raise into Into Group's List 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> = futures::executor::block_on(tree.eval(NodeId(1), context)); - let grouped = result.expect("The position should arrive as an Item"); - - 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` 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"); - - let mut graphic_adapter = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(1)]), vec![NodeId(2)]); - graphic_adapter.identifier = ProtoNodeIdentifier::new("input_adapter"); - - 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 wire should embed into the node's List 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> = 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`, 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"); - - 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"); - - 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 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> = 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"); - - 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"); -} diff --git a/node-graph/nodes/path-bool/src/lib.rs b/node-graph/nodes/path-bool/src/lib.rs index 622f45ddca..5eb02ee189 100644 --- a/node-graph/nodes/path-bool/src/lib.rs +++ b/node-graph/nodes/path-bool/src/lib.rs @@ -421,7 +421,7 @@ fn flatten_vector_run_into<'a>(out: &mut List, level: GraphicLevel<'a>, push_rows(out, text_rows(&one, composed)); } // Brush strokes have no vector outline; a brush node renders them to rasters - Graphic::Stroke(_) => {} + Graphic::Stroke(_) | Graphic::StrokeList(_) => {} } } }