mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 14:18:04 +08:00
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:
@@ -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() {
|
||||
|
||||
@@ -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) => {{
|
||||
|
||||
@@ -52,7 +52,12 @@ impl From<List<Graphic>> for Graphic {
|
||||
// Vector
|
||||
impl From<Vector> for Graphic {
|
||||
fn from(vector: Vector) -> Self {
|
||||
Graphic::VectorList(List::new_from_element(vector))
|
||||
Graphic::Vector(Box::new(Item::new_from_element(vector)))
|
||||
}
|
||||
}
|
||||
impl From<Item<Vector>> for Graphic {
|
||||
fn from(vector: Item<Vector>) -> Self {
|
||||
Graphic::Vector(Box::new(vector))
|
||||
}
|
||||
}
|
||||
impl From<List<Vector>> for Graphic {
|
||||
@@ -66,7 +71,12 @@ impl From<List<Vector>> for Graphic {
|
||||
// Raster<CPU>
|
||||
impl From<Raster<CPU>> for Graphic {
|
||||
fn from(raster: Raster<CPU>) -> Self {
|
||||
Graphic::RasterCPUList(List::new_from_element(raster))
|
||||
Graphic::RasterCPU(Box::new(Item::new_from_element(raster)))
|
||||
}
|
||||
}
|
||||
impl From<Item<Raster<CPU>>> for Graphic {
|
||||
fn from(raster: Item<Raster<CPU>>) -> Self {
|
||||
Graphic::RasterCPU(Box::new(raster))
|
||||
}
|
||||
}
|
||||
impl From<List<Raster<CPU>>> for Graphic {
|
||||
@@ -79,7 +89,12 @@ impl From<List<Raster<CPU>>> for Graphic {
|
||||
// Raster<GPU>
|
||||
impl From<Raster<GPU>> for Graphic {
|
||||
fn from(raster: Raster<GPU>) -> Self {
|
||||
Graphic::RasterGPUList(List::new_from_element(raster))
|
||||
Graphic::RasterGPU(Item::new_from_element(raster))
|
||||
}
|
||||
}
|
||||
impl From<Item<Raster<GPU>>> for Graphic {
|
||||
fn from(raster: Item<Raster<GPU>>) -> Self {
|
||||
Graphic::RasterGPU(raster)
|
||||
}
|
||||
}
|
||||
impl From<List<Raster<GPU>>> for Graphic {
|
||||
@@ -92,7 +107,12 @@ impl From<List<Raster<GPU>>> for Graphic {
|
||||
// Color
|
||||
impl From<Color> for Graphic {
|
||||
fn from(color: Color) -> Self {
|
||||
Graphic::ColorList(List::new_from_element(color))
|
||||
Graphic::Color(Item::new_from_element(color))
|
||||
}
|
||||
}
|
||||
impl From<Item<Color>> for Graphic {
|
||||
fn from(color: Item<Color>) -> Self {
|
||||
Graphic::Color(color)
|
||||
}
|
||||
}
|
||||
impl From<List<Color>> for Graphic {
|
||||
@@ -106,7 +126,12 @@ impl From<List<Color>> for Graphic {
|
||||
// Gradient
|
||||
impl From<Gradient> for Graphic {
|
||||
fn from(gradient: Gradient) -> Self {
|
||||
Graphic::GradientList(List::new_from_element(gradient))
|
||||
Graphic::Gradient(Item::new_from_element(gradient))
|
||||
}
|
||||
}
|
||||
impl From<Item<Gradient>> for Graphic {
|
||||
fn from(gradient: Item<Gradient>) -> Self {
|
||||
Graphic::Gradient(gradient)
|
||||
}
|
||||
}
|
||||
impl From<List<Gradient>> for Graphic {
|
||||
@@ -118,7 +143,12 @@ impl From<List<Gradient>> for Graphic {
|
||||
// String
|
||||
impl From<String> for Graphic {
|
||||
fn from(text: String) -> Self {
|
||||
Graphic::TextList(List::new_from_element(text))
|
||||
Graphic::Text(Item::new_from_element(text))
|
||||
}
|
||||
}
|
||||
impl From<Item<String>> for Graphic {
|
||||
fn from(text: Item<String>) -> Self {
|
||||
Graphic::Text(text)
|
||||
}
|
||||
}
|
||||
impl From<List<String>> for Graphic {
|
||||
@@ -877,7 +907,7 @@ impl<T: Clone> OmitIndex for List<T> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use core_types::list::List;
|
||||
use core_types::list::{ATTR_POSITION, List};
|
||||
use core_types::uuid::NodeId;
|
||||
|
||||
fn vector_graphic() -> Graphic {
|
||||
@@ -974,6 +1004,26 @@ mod tests {
|
||||
assert_eq!(color_of(0), Some(Color::BLACK), "a declared item should keep its own appearance");
|
||||
assert_eq!(color_of(1), Some(Color::WHITE), "a padded item should inherit the parent appearance");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn embedded_item_keeps_its_attributes_inside_the_variant() {
|
||||
let color = Item::new_from_element(Color::RED).with_attribute(ATTR_POSITION, 0.25_f64);
|
||||
|
||||
let Graphic::Color(inner) = Graphic::from(color) else { panic!("expected a color graphic") };
|
||||
assert_eq!(inner.element(), &Color::RED);
|
||||
assert_eq!(inner.attribute::<f64>(ATTR_POSITION), Some(&0.25));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn embedded_list_becomes_one_graphic_holding_every_element() {
|
||||
let mut colors = List::new_from_element(Color::RED);
|
||||
colors.push(Item::new_from_element(Color::BLUE));
|
||||
|
||||
let Graphic::ColorList(inner) = Graphic::from(colors) else {
|
||||
panic!("expected a color list graphic")
|
||||
};
|
||||
assert_eq!(inner.len(), 2, "a whole list embeds as one graphic holding all its elements");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -77,6 +77,19 @@ fn into_list<'i, T: 'i + Send + Into<E>, E: 'i + Send>(_: impl Ctx, value: List<
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Moves a whole `Item` wire inside an element type like `Graphic` that embeds ranked values as its variants.
|
||||
/// The attributes describe the element they arrived with, so they travel inside it and the fresh envelope starts empty.
|
||||
#[node_macro::node(category(""), skip_impl)]
|
||||
fn embed_item<'i, T: 'i + Send, E: 'i + Send + From<Item<T>>>(_: impl Ctx, value: Item<T>, _element_ty: PhantomData<E>) -> Item<E> {
|
||||
Item::new_from_element(value.into())
|
||||
}
|
||||
|
||||
/// The `List` counterpart of `embed_item`, each item moving whole inside its own embedding element.
|
||||
#[node_macro::node(category(""), skip_impl)]
|
||||
fn embed_list<'i, T: 'i + Send, E: 'i + Send + From<Item<T>>>(_: impl Ctx, value: List<T>, _element_ty: PhantomData<E>) -> List<E> {
|
||||
value.into_iter().map(|item| Item::new_from_element(E::from(item))).collect()
|
||||
}
|
||||
|
||||
/// The [`Convert`]-based counterpart of `into_item`, casting an `Item` wire's element to a connector's numeric element type.
|
||||
#[node_macro::node(category(""), skip_impl)]
|
||||
async fn convert_item<'i, T: 'i + Send + Convert<E, ()>, E: 'i + Send>(ctx: impl Ctx + ExtractFootprint, value: Item<T>, _element_ty: PhantomData<E>) -> Item<E> {
|
||||
|
||||
@@ -3975,10 +3975,10 @@ mod test {
|
||||
let fill = appearance.first_paint_of(Cover::Fill).expect("Morph should keep the fill paint at the midpoint");
|
||||
|
||||
// Interpolated color between red and blue should have >0 value on both R and B
|
||||
let Graphic::ColorList(colors) = fill else {
|
||||
let Graphic::Color(color) = fill else {
|
||||
panic!("Expected a solid color fill, got {fill:?}");
|
||||
};
|
||||
let color = *colors.element(0).expect("Color present");
|
||||
let color = *color.element();
|
||||
assert!(color.r() > 0. && color.b() > 0., "Fill should be a red-to-blue blend, got {color:?}");
|
||||
}
|
||||
|
||||
@@ -3992,10 +3992,10 @@ mod test {
|
||||
|
||||
let paint_color = |appearance: &Appearance, cover| {
|
||||
let paint = appearance.first_paint_of(cover).expect("Morph should keep both paints at the midpoint");
|
||||
let Graphic::ColorList(colors) = paint else {
|
||||
let Graphic::Color(color) = paint else {
|
||||
panic!("Expected a solid color paint, got {paint:?}");
|
||||
};
|
||||
*colors.element(0).expect("Color present")
|
||||
*color.element()
|
||||
};
|
||||
|
||||
// The two endpoints list their covers in opposite paint orders, which pairing by position would cross
|
||||
|
||||
Reference in New Issue
Block a user