Support embedding whole items into Graphic variants so their attributes survive the conversion (#4440)

Add embedding conversion adapters that move a whole ranked value inside a Graphic variant
This commit is contained in:
Keavon Chambers
2026-08-16 19:51:58 -07:00
committed by GitHub
parent f635c56eab
commit 104b8b71e6
5 changed files with 132 additions and 11 deletions

View File

@@ -372,6 +372,44 @@ fn color_list_wraps_through_the_colors_to_gradient_node() {
assert_eq!(gradient.element().len(), 1, "The single color should become the gradient's one stop");
}
// A paint wire feeding a `Graphic` connector embeds whole, so the gradient's own attributes stay on the item inside the variant
#[test]
fn gradient_value_embeds_through_the_graphic_input_adapter() {
use core_types::ATTR_GRADIENT_SPREAD;
use graphene_std::Graphic;
use graphene_std::vector::{Gradient, GradientRamp, GradientSpread};
let ramp = GradientRamp {
gradient_spread: GradientSpread::Reflect,
..GradientRamp::from(Gradient::default())
};
let gradient_node = ProtoNode::value(ConstructionArgs::Value(TaggedValue::GradientRamp(ramp).into()), vec![NodeId(0)]);
let mut input_adapter_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(0)]), vec![NodeId(1)]);
input_adapter_node.identifier = ProtoNodeIdentifier::new("input_adapter<Graphic>");
let network = ProtoNetwork {
inputs: vec![],
output: NodeId(1),
nodes: vec![(NodeId(0), gradient_node), (NodeId(1), input_adapter_node)],
};
let mut typing_context = TypingContext::new(&crate::node_registry::NODE_REGISTRY);
typing_context.update(&network).expect("An Item<Gradient> wire should resolve the adapter's embedding row");
let tree = futures::executor::block_on(BorrowTree::new(network, &typing_context)).expect("The embedding constructor should instantiate");
let context: Context = None;
let result: Option<Item<Graphic>> = futures::executor::block_on(tree.eval(NodeId(1), context));
let embedded = result.expect("The gradient should arrive as an Item<Graphic>");
assert!(embedded.attributes().iter_any().next().is_none(), "The fresh outer envelope describes the graphic, so it starts empty");
let Graphic::Gradient(inner) = embedded.element() else { panic!("expected a gradient graphic") };
assert_eq!(
inner.attribute::<GradientSpread>(ATTR_GRADIENT_SPREAD),
Some(&GradientSpread::Reflect),
"The gradient's placement attributes should ride the item inside the variant, where the renderer reads them"
);
}
// A scalar wire feeding a `DVec2` connector splats into both axes through the input adapter's `Convert` row
#[test]
fn number_value_splats_through_the_vec2_input_adapter() {

View File

@@ -499,6 +499,26 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
node_types.extend(input_adapter_row!(from_element: String, element: BoxCorners));
// A number wire may feed the ranked `Item<BoxCorners>` connector, each number becoming a uniform radius for all four corners
node_types.extend(input_adapter_row!(from_element: f64, element: BoxCorners));
// The embedding counterpart of `input_adapter_row!`, for element types like `Graphic` whose variants embed/wrap whole ranked values.
// Each item moves inside its matching variant, attributes and all, rather than mapping only its element.
macro_rules! embed_adapter_row {
(from_element: $from:ty, element: $element:ty) => {{
let entries: Vec<(ProtoNodeIdentifier, NodeConstructor, NodeIOTypes)> = vec![
input_adapter_row!(node: EmbedItemNode, from: Item<$from>, to: Item<$element>, element: $element),
input_adapter_row!(node: EmbedListNode, from: List<$from>, to: List<$element>, element: $element),
];
entries
}};
}
// Any paintable wire may feed a ranked `Item<Graphic>` connector, each item embedding as its matching `Graphic` variant.
// This is what lets a paint list zip element-wise against the content it paints.
node_types.extend(embed_adapter_row!(from_element: Color, element: Graphic));
node_types.extend(embed_adapter_row!(from_element: Gradient, element: Graphic));
node_types.extend(embed_adapter_row!(from_element: String, element: Graphic));
node_types.extend(embed_adapter_row!(from_element: Vector, element: Graphic));
node_types.extend(embed_adapter_row!(from_element: Raster<CPU>, element: Graphic));
#[cfg(feature = "gpu")]
node_types.extend(embed_adapter_row!(from_element: Raster<GPU>, element: Graphic));
// The `Convert`-based counterpart of `input_adapter_row!`, for casts the std `Into` trait cannot express
macro_rules! convert_adapter_node {
(from_element: $from:ty, element: $element:ty) => {{