Add new "Gradient" node#4177
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces support for a new "Gradient" node alongside the legacy "Gradient Value" node, updating the gradient tool, graph operations, and style conversions to handle both. Key changes include adding helper functions to traverse and resolve upstream gradient nodes, implementing a new existing_proto_node_id_at method to walk and insert nodes at specific inputs, and updating the gradient stop, type, and spread setters to support the new node structure. Feedback focuses on preventing state corruption in existing_proto_node_id_at by resolving inputs before node insertion, expanding get_gradient_stops to support the new node type, and avoiding redundant lookups in gradient_stops_set.
| // splice new node between target_input and its current upstream | ||
| let node_definition = resolve_proto_node_type(reference)?; | ||
| let node_id = NodeId::new(); | ||
| self.network_interface.insert_node(node_id, node_definition.default_node_template(), &[]); | ||
|
|
||
| let current_input = self.network_interface.input_from_connector(target_input, &[])?.clone(); | ||
| self.network_interface.set_input(&InputConnector::node(node_id, 0), current_input, &[]); | ||
| self.network_interface.set_input(target_input, NodeInput::node(node_id, 0), &[]); | ||
|
|
||
| Some(node_id) |
There was a problem hiding this comment.
In existing_proto_node_id_at, if input_from_connector returns None (line 363), the function returns early but has already inserted the new node via insert_node (line 361). This leaves an orphaned node in the document network. Resolving current_input before inserting the node prevents this state corruption.
| // splice new node between target_input and its current upstream | |
| let node_definition = resolve_proto_node_type(reference)?; | |
| let node_id = NodeId::new(); | |
| self.network_interface.insert_node(node_id, node_definition.default_node_template(), &[]); | |
| let current_input = self.network_interface.input_from_connector(target_input, &[])?.clone(); | |
| self.network_interface.set_input(&InputConnector::node(node_id, 0), current_input, &[]); | |
| self.network_interface.set_input(target_input, NodeInput::node(node_id, 0), &[]); | |
| Some(node_id) | |
| // splice new node between target_input and its current upstream | |
| let current_input = self.network_interface.input_from_connector(target_input, &[])?.clone(); | |
| let node_definition = resolve_proto_node_type(reference)?; | |
| let node_id = NodeId::new(); | |
| self.network_interface.insert_node(node_id, node_definition.default_node_template(), &[]); | |
| self.network_interface.set_input(&InputConnector::node(node_id, 0), current_input, &[]); | |
| self.network_interface.set_input(target_input, NodeInput::node(node_id, 0), &[]); | |
| Some(node_id) |
| pub fn get_gradient_stops(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<GradientStops> { | ||
| let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs(&DefinitionIdentifier::ProtoNode(graphene_std::math_nodes::gradient_value::IDENTIFIER))?; | ||
| let TaggedValue::Gradient(stops) = inputs.get(graphene_std::math_nodes::gradient_value::GradientInput::INDEX)?.as_value()? else { | ||
| let gradient_value_node = network_interface.document_network().nodes.get(&get_upstream_gradient_value_node_id(layer, network_interface)?)?; | ||
| let TaggedValue::Gradient(stops) = gradient_value_node.inputs.get(graphene_std::math_nodes::gradient_value::GradientInput::INDEX)?.as_value()? else { | ||
| return None; | ||
| }; | ||
| Some(stops.clone()) | ||
| } |
There was a problem hiding this comment.
get_gradient_stops currently only supports retrieving stops from the legacy Gradient Value node. If a layer is configured with the new Gradient node, this function will return None, which could break other parts of the editor relying on it. We should update it to check for both node types.
| pub fn get_gradient_stops(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<GradientStops> { | |
| let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs(&DefinitionIdentifier::ProtoNode(graphene_std::math_nodes::gradient_value::IDENTIFIER))?; | |
| let TaggedValue::Gradient(stops) = inputs.get(graphene_std::math_nodes::gradient_value::GradientInput::INDEX)?.as_value()? else { | |
| let gradient_value_node = network_interface.document_network().nodes.get(&get_upstream_gradient_value_node_id(layer, network_interface)?)?; | |
| let TaggedValue::Gradient(stops) = gradient_value_node.inputs.get(graphene_std::math_nodes::gradient_value::GradientInput::INDEX)?.as_value()? else { | |
| return None; | |
| }; | |
| Some(stops.clone()) | |
| } | |
| pub fn get_gradient_stops(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<GradientStops> { | |
| if let Some(gradient_node_id) = get_upstream_gradient_node_id(layer, network_interface) { | |
| let node = network_interface.document_network().nodes.get(&gradient_node_id)?; | |
| let TaggedValue::Gradient(stops) = node.inputs.get(graphene_std::graphic_nodes::gradient::GradientInput::<List<GradientStops>>::INDEX)?.as_value()? else { | |
| return None; | |
| }; | |
| return Some(stops.clone()); | |
| } | |
| let gradient_value_node = network_interface.document_network().nodes.get(&get_upstream_gradient_value_node_id(layer, network_interface)?)?; | |
| let TaggedValue::Gradient(stops) = gradient_value_node.inputs.get(graphene_std::math_nodes::gradient_value::GradientInput::INDEX)?.as_value()? else { | |
| return None; | |
| }; | |
| Some(stops.clone()) | |
| } |
| pub fn gradient_stops_set(&mut self, stops: GradientStops) { | ||
| let Some(gradient_node_id) = self.existing_proto_node_id(graphene_std::math_nodes::gradient_value::IDENTIFIER, true) else { | ||
| // Try to find a Gradient node first and update its gradient stops | ||
| if let Some(output_layer) = self.get_output_layer() | ||
| && let Some(gradient_node_id) = get_upstream_gradient_node_id(output_layer, self.network_interface) | ||
| { | ||
| let input_connector = InputConnector::node(gradient_node_id, graphene_std::graphic_nodes::gradient::GradientInput::<List<GradientStops>>::INDEX); | ||
| self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Gradient(stops), false), false); | ||
| return; | ||
| }; | ||
| let input_connector = InputConnector::node(gradient_node_id, graphene_std::math_nodes::gradient_value::GradientInput::INDEX); | ||
| self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Gradient(stops), false), false); | ||
| } | ||
| // Then try to find Gradient Value node that is connected to a Fill node | ||
| if let Some(output_layer) = self.get_output_layer() | ||
| && let Some(gradient_value_id) = get_upstream_gradient_value_node_id(output_layer, self.network_interface) | ||
| { | ||
| let input_connector = InputConnector::node(gradient_value_id, graphene_std::math_nodes::gradient_value::GradientInput::INDEX); | ||
| self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Gradient(stops), false), false); | ||
| } | ||
| } |
There was a problem hiding this comment.
The gradient_stops_set function calls self.get_output_layer() twice. Binding it once at the beginning of the function simplifies the control flow and avoids redundant lookups.
pub fn gradient_stops_set(&mut self, stops: GradientStops) {
let Some(output_layer) = self.get_output_layer() else {
return;
};
// Try to find a Gradient node first and update its gradient stops
if let Some(gradient_node_id) = get_upstream_gradient_node_id(output_layer, self.network_interface) {
let input_connector = InputConnector::node(gradient_node_id, graphene_std::graphic_nodes::gradient::GradientInput::<List<GradientStops>>::INDEX);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Gradient(stops), false), false);
return;
}
// Then try to find Gradient Value node that is connected to a Fill node
if let Some(gradient_value_id) = get_upstream_gradient_value_node_id(output_layer, self.network_interface) {
let input_connector = InputConnector::node(gradient_value_id, graphene_std::math_nodes::gradient_value::GradientInput::INDEX);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Gradient(stops), false), false);
}
}
WIP!
Add a new "Gradient" node that bundles stops + type + spread method + control geometry into a single node, outputting List.
Update
From<List<GradientStops>>for Fill to honor the attributes (gradient_type / spread_method / transform) on the list.Teach the Gradient tool to read from and write to the new Gradient node, supporting some connection patterns:
Fill::Gradientenum, existing)