mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 14:18:04 +08:00
Split 'To Graphic' and 'Wrap Graphic' into the 'As Graphic' type assertion and 'Into Group' reducer nodes (#4441)
* Split 'To Graphic' and 'Wrap Graphic' into the 'As Graphic' type assertion and 'Into Group' reducer nodes * Make 'Into Group' reduce List<DVec2> not Item<DVec2> * Reset the Merge definition for every legacy alias of its coercion nodes * Improve test
This commit is contained in:
2
demo-artwork/changing-seasons.graphite
generated
2
demo-artwork/changing-seasons.graphite
generated
File diff suppressed because one or more lines are too long
2
demo-artwork/isometric-fountain.graphite
generated
2
demo-artwork/isometric-fountain.graphite
generated
File diff suppressed because one or more lines are too long
2
demo-artwork/marbled-mandelbrot.graphite
generated
2
demo-artwork/marbled-mandelbrot.graphite
generated
File diff suppressed because one or more lines are too long
2
demo-artwork/painted-dreams.graphite
generated
2
demo-artwork/painted-dreams.graphite
generated
File diff suppressed because one or more lines are too long
2
demo-artwork/parametric-dunescape.graphite
generated
2
demo-artwork/parametric-dunescape.graphite
generated
File diff suppressed because one or more lines are too long
2
demo-artwork/procedural-string-lights.graphite
generated
2
demo-artwork/procedural-string-lights.graphite
generated
File diff suppressed because one or more lines are too long
2
demo-artwork/red-dress.graphite
generated
2
demo-artwork/red-dress.graphite
generated
File diff suppressed because one or more lines are too long
2
demo-artwork/valley-of-spires.graphite
generated
2
demo-artwork/valley-of-spires.graphite
generated
File diff suppressed because one or more lines are too long
@@ -154,14 +154,14 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
|
||||
// Primary (bottom) input type coercion
|
||||
NodeTemplate {
|
||||
inputs: vec![NodeInput::import(generic!(T), 0)],
|
||||
implementation: NodeTemplateImplementation::ProtoNode(graphic::to_graphic::IDENTIFIER),
|
||||
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-21, -3)),
|
||||
implementation: NodeTemplateImplementation::ProtoNode(graphic::as_graphic::IDENTIFIER),
|
||||
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-21, -2)),
|
||||
..Default::default()
|
||||
},
|
||||
// Secondary (left) input type coercion
|
||||
NodeTemplate {
|
||||
inputs: vec![NodeInput::import(generic!(T), 1)],
|
||||
implementation: NodeTemplateImplementation::ProtoNode(graphic::wrap_graphic::IDENTIFIER),
|
||||
implementation: NodeTemplateImplementation::ProtoNode(graphic::into_group::IDENTIFIER),
|
||||
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-21, -1)),
|
||||
..Default::default()
|
||||
},
|
||||
@@ -169,7 +169,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
|
||||
NodeTemplate {
|
||||
inputs: vec![NodeInput::Reflection(graph_craft::document::DocumentNodeMetadata::DocumentNodePath)],
|
||||
implementation: NodeTemplateImplementation::ProtoNode(graphic::path_of_subgraph::IDENTIFIER),
|
||||
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-21, 1)),
|
||||
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-21, 0)),
|
||||
..Default::default()
|
||||
},
|
||||
// Stamp each item of the content with the parent layer's NodeId via the `editor:layer_path` attribute,
|
||||
@@ -196,7 +196,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
|
||||
call_argument: generic!(T),
|
||||
inputs: vec![NodeInput::node(NodeId(0), 0), NodeInput::node(NodeId(4), 0)],
|
||||
implementation: NodeTemplateImplementation::ProtoNode(graphic::extend::IDENTIFIER),
|
||||
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, -3)),
|
||||
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, -2)),
|
||||
..Default::default()
|
||||
},
|
||||
]
|
||||
|
||||
@@ -45,6 +45,14 @@ pub struct NodeReplacement<'a> {
|
||||
aliases: &'a [&'a str],
|
||||
}
|
||||
|
||||
/// Every name the Merge layer network's two type-coercion nodes have gone by, which is every alias of the node they both converged on.
|
||||
fn into_group_aliases() -> impl Iterator<Item = &'static &'static str> {
|
||||
NODE_REPLACEMENTS
|
||||
.iter()
|
||||
.filter(|replacement| replacement.node == graphene_std::graphic::into_group::IDENTIFIER)
|
||||
.flat_map(|replacement| replacement.aliases)
|
||||
}
|
||||
|
||||
const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
|
||||
// ================================
|
||||
// blending
|
||||
@@ -194,22 +202,20 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
|
||||
],
|
||||
},
|
||||
NodeReplacement {
|
||||
node: graphene_std::graphic::to_graphic::IDENTIFIER,
|
||||
node: graphene_std::graphic::into_group::IDENTIFIER,
|
||||
aliases: &[
|
||||
"graphene_core::ToGraphicGroupNode",
|
||||
"graphene_core::graphic_element::ToGroupNode",
|
||||
"graphene_core::graphic_types::ToGroupNode",
|
||||
"graphene_core::graphic::ToGraphicNode",
|
||||
],
|
||||
},
|
||||
NodeReplacement {
|
||||
node: graphene_std::graphic::wrap_graphic::IDENTIFIER,
|
||||
aliases: &[
|
||||
// Converted from "To Element"
|
||||
// Converted from "To Element", then "Wrap Graphic"
|
||||
"graphene_core::ToGraphicElementNode",
|
||||
"graphene_core::graphic_element::ToElementNode",
|
||||
"graphene_core::graphic_types::ToElementNode",
|
||||
"graphene_core::graphic::WrapGraphicNode",
|
||||
"graphic_nodes::graphic::WrapGraphicNode",
|
||||
// Converted from "To Graphic", whose grouping of non-graphical content this node now carries alone
|
||||
"graphene_core::ToGraphicGroupNode",
|
||||
"graphene_core::graphic_element::ToGroupNode",
|
||||
"graphene_core::graphic_types::ToGroupNode",
|
||||
"graphene_core::graphic::ToGraphicNode",
|
||||
"graphic_nodes::graphic::ToGraphicNode",
|
||||
],
|
||||
},
|
||||
// ================================
|
||||
@@ -1071,6 +1077,12 @@ pub fn document_migration_reset_node_definition(document_serialized_content: &st
|
||||
return true;
|
||||
}
|
||||
|
||||
// Every Merge layer network is built from the two nodes that became "As Graphic" and "Into Group", so their definitions
|
||||
// are reset to pick up the current plumbing instead of the alias migration meant for standalone copies of those nodes.
|
||||
if into_group_aliases().any(|alias| document_serialized_content.contains(alias)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
@@ -2958,6 +2970,20 @@ mod tests {
|
||||
assert!(resolve_proto_node_type(graphene_std::platform_application_io::upload_texture::IDENTIFIER).is_some());
|
||||
}
|
||||
|
||||
// Migrating a Merge network's coercion nodes by alias would leave a reducer in the primary slot, so every alias must reset instead
|
||||
#[test]
|
||||
fn every_into_group_alias_resets_the_merge_definition() {
|
||||
let aliases = into_group_aliases().collect::<Vec<_>>();
|
||||
assert!(!aliases.is_empty(), "the reset is driven by these aliases, so losing them all would disable it unnoticed");
|
||||
|
||||
for alias in aliases {
|
||||
assert!(
|
||||
document_migration_reset_node_definition(&format!(r#""implementation":{{"ProtoNode":"{alias}"}}"#)),
|
||||
"a document referencing `{alias}` should reset its layer definitions"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_duplicate_node_replacements() {
|
||||
let mut hashmap = HashMap::<ProtoNodeIdentifier, u32>::new();
|
||||
|
||||
@@ -74,14 +74,14 @@ fn item_wire_promotes_to_list_connector() {
|
||||
assert!(result.is_some(), "The promoted wire should execute end-to-end");
|
||||
}
|
||||
|
||||
// The layer content path: a rank-0 content wire enters Wrap Graphic's `List` connector by singleton raise, and the
|
||||
// wrapped `Item<Graphic>` raises again at Extend's `List` connector, so layers accept rank-0 chains without new machinery
|
||||
// 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 wrap_graphic_node = ProtoNode::value(ConstructionArgs::Nodes(vec![NodeId(0)]), vec![NodeId(1)]);
|
||||
wrap_graphic_node.identifier = ProtoNodeIdentifier::new("graphic_nodes::graphic::WrapGraphicNode");
|
||||
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)]);
|
||||
|
||||
@@ -91,12 +91,12 @@ fn rank_0_content_promotes_through_the_layer_coercion_path() {
|
||||
let network = ProtoNetwork {
|
||||
inputs: vec![],
|
||||
output: NodeId(3),
|
||||
nodes: vec![(NodeId(0), content_node), (NodeId(1), wrap_graphic_node), (NodeId(2), base_node), (NodeId(3), extend_node)],
|
||||
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 Wrap Graphic's List connector");
|
||||
assert!(typing_context.promotions(NodeId(3)).is_some(), "The wrapped Item<Graphic> should be raised at Extend's List connector");
|
||||
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;
|
||||
@@ -346,6 +346,36 @@ fn position_value_converts_through_the_vector_input_adapter() {
|
||||
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 `List<Color>` wire into one gradient with those colors as its stops
|
||||
#[test]
|
||||
fn color_list_wraps_through_the_colors_to_gradient_node() {
|
||||
@@ -372,6 +402,56 @@ 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");
|
||||
}
|
||||
|
||||
// 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() {
|
||||
|
||||
@@ -437,6 +437,7 @@ impl IntoGraphicList for List<String> {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Remove this
|
||||
impl IntoGraphicList for Item<DAffine2> {
|
||||
fn into_graphic_list(self) -> List<Graphic> {
|
||||
List::new_from_element(Graphic::default())
|
||||
@@ -444,6 +445,7 @@ impl IntoGraphicList for Item<DAffine2> {
|
||||
}
|
||||
|
||||
// DAffine2
|
||||
// TODO: Remove this
|
||||
impl From<Item<DAffine2>> for Graphic {
|
||||
fn from(_: Item<DAffine2>) -> Self {
|
||||
Graphic::default()
|
||||
@@ -453,7 +455,20 @@ impl From<Item<DAffine2>> for Graphic {
|
||||
// DVec2
|
||||
impl From<Item<DVec2>> for Graphic {
|
||||
fn from(position: Item<DVec2>) -> Self {
|
||||
Graphic::VectorList(List::new_from_element(Vector::from_anchor_position(position.into_element())))
|
||||
let (position, attributes) = position.into_parts();
|
||||
|
||||
Graphic::Vector(Box::new(Item::from_parts(Vector::from_anchor_position(position), attributes)))
|
||||
}
|
||||
}
|
||||
impl From<List<DVec2>> for Graphic {
|
||||
fn from(positions: List<DVec2>) -> Self {
|
||||
let anchors = positions.into_iter().map(|position| {
|
||||
let (position, attributes) = position.into_parts();
|
||||
|
||||
Item::from_parts(Vector::from_anchor_position(position), attributes)
|
||||
});
|
||||
|
||||
Graphic::VectorList(anchors.collect())
|
||||
}
|
||||
}
|
||||
// Note: List conversions handled by blanket impl in gcore
|
||||
|
||||
@@ -20,7 +20,7 @@ pub async fn create_artboard<T: IntoGraphicList>(
|
||||
Context -> List<Raster<GPU>>,
|
||||
Context -> List<Color>,
|
||||
Context -> List<Gradient>,
|
||||
Context -> Item<DAffine2>,
|
||||
Context -> Item<DAffine2>, // TODO: Remove this
|
||||
)]
|
||||
content: impl Node<Context<'static>, Output = T>,
|
||||
/// Coordinate of the top-left corner of the artboard within the document.
|
||||
|
||||
@@ -927,10 +927,10 @@ pub async fn legacy_layer_extend<T: 'n + Send + Clone>(
|
||||
base
|
||||
}
|
||||
|
||||
/// Nests the input graphical content in a wrapper graphic. This essentially "groups" the input.
|
||||
/// Nests the input graphical content in a wrapper graphic, collecting it all into a single group.
|
||||
/// The inverse of this node is 'Flatten Graphic'.
|
||||
#[node_macro::node(category("General"))]
|
||||
pub async fn wrap_graphic<T: Into<Graphic> + 'n>(
|
||||
pub async fn into_group<T: Into<Graphic> + 'n>(
|
||||
_: impl Ctx,
|
||||
#[implementations(
|
||||
List<Graphic>,
|
||||
@@ -940,31 +940,19 @@ pub async fn wrap_graphic<T: Into<Graphic> + 'n>(
|
||||
List<Color>,
|
||||
List<Gradient>,
|
||||
List<String>,
|
||||
Item<DAffine2>,
|
||||
Item<DVec2>,
|
||||
List<DVec2>,
|
||||
Item<DAffine2>, // TODO: Remove this
|
||||
)]
|
||||
content: T,
|
||||
) -> Item<Graphic> {
|
||||
Item::new_from_element(content.into())
|
||||
}
|
||||
|
||||
/// Converts a list of graphical content into a `Graphic[]` by placing it into an element of a new wrapper `Graphic[]`.
|
||||
/// If it is already a `Graphic[]`, it is not wrapped again. Use the 'Wrap Graphic' node if wrapping is always desired.
|
||||
/// Type-asserts a value to be graphical content, converting each item of other content types into its matching form.
|
||||
/// Use the 'Into Group' node instead to collect the content into a single group.
|
||||
#[node_macro::node(category("General"))]
|
||||
pub async fn to_graphic<T: IntoGraphicList>(
|
||||
_: impl Ctx,
|
||||
#[implementations(
|
||||
List<Graphic>,
|
||||
List<Vector>,
|
||||
List<Raster<CPU>>,
|
||||
List<Raster<GPU>>,
|
||||
List<Color>,
|
||||
List<Gradient>,
|
||||
List<String>,
|
||||
)]
|
||||
content: T,
|
||||
) -> List<Graphic> {
|
||||
content.into_graphic_list()
|
||||
pub async fn as_graphic(_: impl Ctx, value: Item<Graphic>) -> Item<Graphic> {
|
||||
value
|
||||
}
|
||||
|
||||
/// Removes a level of nesting from a `Graphic[]`, or all nesting if "Fully Flatten" is enabled.
|
||||
|
||||
Reference in New Issue
Block a user