Merge origin/master into the async record refactor

Scaffolding merge for the reconcile; the final series to master is
authored fresh. Rank plumbing resolves to our axis-IR model, the node
macro and the LaneSource render walk stay ours, master's vector
restructure and gradient vocabulary are adopted, and the paint and
appearance adoption is deliberately deferred behind our fill and stroke
markers.
This commit is contained in:
Dennis Kobert
2026-09-08 15:03:57 +00:00
385 changed files with 34669 additions and 20078 deletions

View File

@@ -0,0 +1,816 @@
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");
}

View File

@@ -140,7 +140,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, Vec<RegistryEntry>> {
"List<Raster<CPU>>",
"List<Raster<GPU>>",
"List<Color>",
"List<GradientStops>",
"List<Gradient>",
"List<String>",
])
.map(|(entry, target)| (ProtoNodeIdentifier::with_owned_string(format!("graphene_core::ops::ConvertNode<{target}>")), entry)),
@@ -191,10 +191,12 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, Vec<RegistryEntry>> {
// This might be caused by the stringify! macro
let mut new_name = id.as_str().replace('\n', " ");
// Remove struct generics for all nodes except for the IntoNode and ConvertNode
if !(new_name.contains("IntoNode") || new_name.contains("ConvertNode"))
&& let Some((path, _generics)) = new_name.split_once("<")
{
// Remove struct generics for all nodes except the adapter identifiers, whose element suffix distinguishes their rows
let element_suffixed_adapter = new_name.starts_with("input_adapter<")
|| new_name.starts_with("graphene_core::ops::ItemToListNode<")
|| new_name.starts_with("graphene_core::ops::BundleNode<")
|| new_name.starts_with("graphene_core::ops::UnbundleNode<");
if !element_suffixed_adapter && let Some((path, _generics)) = new_name.split_once("<") {
new_name = path.to_string();
}

View File

@@ -1,8 +1,8 @@
use graph_craft::application_io::PlatformEditorApi;
use graph_craft::concrete;
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeInput, NodeNetwork};
use graph_craft::generic;
use graph_craft::{concrete, item};
use graphene_std::Context;
use graphene_std::ContextFeatures;
use graphene_std::uuid::NodeId;