Make the Gradient tool work with gradient node chains feeding a Fill node (#4177)

* Add Gradient node

* Add support of new Gradient node for Gradient tool

* Adapt gemini reviews

* Add tests

* Bring back visibility check

* Remove the 'Gradient' node since it's unwanted after all

* Formatting

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
YohYamasaki
2026-06-06 03:23:14 +00:00
committed by GitHub
co-authored by Keavon Chambers
parent 9d2071ce6a
commit a3b62dac00
5 changed files with 172 additions and 39 deletions
@@ -271,6 +271,35 @@ pub fn get_viewport_center(layer: LayerNodeIdentifier, network_interface: &NodeN
network_interface.document_metadata().transform_to_viewport(layer).transform_point2(min + (max - min) * center)
}
/// Determine the input connector where the gradient chain enters the layer.
/// Returns Fill's fill input if the layer has a "Fill" node, otherwise returns the layer's content input.
pub fn gradient_chain_target_input(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> InputConnector {
if let Some(fill_node_id) = NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER)) {
InputConnector::node(fill_node_id, graphene_std::vector::fill::FillInput::<Fill>::INDEX)
} else {
InputConnector::node(layer.to_node(), 1)
}
}
/// Try to find a "Gradient Value" node that is connected to a "Fill" node, or to a layer directly.
pub fn get_upstream_gradient_value_node_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<NodeId> {
network_interface
.upstream_flow_back_from_nodes(vec![layer.to_node()], &[], FlowType::UpstreamFlow)
.skip(1)
.take_while(|node_id| !network_interface.is_layer(node_id, &[]))
.find(|node_id| network_interface.reference(node_id, &[]).as_ref() == Some(&DefinitionIdentifier::ProtoNode(graphene_std::math_nodes::gradient_value::IDENTIFIER)))
}
/// Get the node connected to Fill's fill input, if any.
pub fn get_fill_input_node_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<NodeId> {
let fill_node_id = NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER))?;
let fill_node = network_interface.document_network().nodes.get(&fill_node_id)?;
let NodeInput::Node { node_id, .. } = fill_node.inputs.get(graphene_std::vector::fill::FillInput::<Fill>::INDEX)? else {
return None;
};
Some(*node_id)
}
/// Get the current gradient of a layer from the closest "Fill" node.
pub fn get_gradient(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<Gradient> {
let fill_index = 1;
@@ -284,8 +313,8 @@ pub fn get_gradient(layer: LayerNodeIdentifier, network_interface: &NodeNetworkI
/// Get the gradient stops of a layer, if any.
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())
@@ -8,7 +8,7 @@ use crate::messages::portfolio::document::overlays::utility_types::{GizmoEmphasi
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, NodeNetworkInterface};
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
use crate::messages::tool::common_functionality::graph_modification_utils::{self, NodeGraphLayer, get_gradient_stops};
use crate::messages::tool::common_functionality::graph_modification_utils::{self, NodeGraphLayer, get_gradient_stops, gradient_chain_target_input};
use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration};
use graph_craft::document::value::TaggedValue;
use graphene_std::color::SRGBA8;
@@ -347,20 +347,19 @@ fn gradient_space_transform(layer: LayerNodeIdentifier, document: &DocumentMessa
// TODO: Remove this whole function once all gradients are stored via the modern `Gradient(GradientStops)` slot
fn get_gradient(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<Gradient> {
if let Some(stops) = get_gradient_stops(layer, network_interface) {
let GradientChainState {
transform,
gradient_type,
spread_method,
} = read_gradient_chain_state(layer, network_interface);
return Some(Gradient {
// Try to construct a gradient out of a chain, which is directly connected to a layer
let chain_state = read_gradient_chain_state(layer, network_interface);
Some(Gradient {
stops,
gradient_type,
spread_method,
start: transform.transform_point2(DVec2::ZERO),
end: transform.transform_point2(DVec2::X),
});
gradient_type: chain_state.gradient_type,
spread_method: chain_state.spread_method,
start: chain_state.transform.transform_point2(DVec2::ZERO),
end: chain_state.transform.transform_point2(DVec2::X),
})
} else {
// Try to find a legacy Fill::Gradient that is selected in a Fill node
graph_modification_utils::get_gradient(layer, network_interface)
}
graph_modification_utils::get_gradient(layer, network_interface)
}
#[derive(Clone, Copy, Debug)]
@@ -373,6 +372,9 @@ struct GradientChainState {
/// Resolve the gradient transform, type, and spread method by walking the chain feeding the layer. Transform composes all
/// 'Transform' nodes. Type and spread method come from the closest-to-layer node of each kind, or the type default.
fn read_gradient_chain_state(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> GradientChainState {
let target_input = gradient_chain_target_input(layer, network_interface);
let walk_from = network_interface.upstream_output_connector(&target_input, &[]).and_then(|out| out.node_id()).unwrap_or(layer.to_node());
let transform_reference = DefinitionIdentifier::ProtoNode(graphene_std::transform_nodes::transform::IDENTIFIER);
let gradient_type_reference = DefinitionIdentifier::ProtoNode(graphene_std::math_nodes::gradient_type::IDENTIFIER);
let spread_method_reference = DefinitionIdentifier::ProtoNode(graphene_std::math_nodes::spread_method::IDENTIFIER);
@@ -382,8 +384,8 @@ fn read_gradient_chain_state(layer: LayerNodeIdentifier, network_interface: &Nod
let mut spread_method: Option<GradientSpreadMethod> = None;
for node_id in network_interface
.upstream_flow_back_from_nodes(vec![layer.to_node()], &[], FlowType::HorizontalFlow)
.skip(1)
.upstream_flow_back_from_nodes(vec![walk_from], &[], FlowType::HorizontalFlow)
.skip_while(|node_id| network_interface.is_layer(node_id, &[]))
.take_while(|node_id| !network_interface.is_layer(node_id, &[]))
{
let Some(reference) = network_interface.reference(&node_id, &[]) else { continue };
@@ -512,14 +514,13 @@ fn calculate_insertion(start: DVec2, end: DVec2, stops: &GradientStops, mouse: D
impl SelectedGradient {
pub fn new(gradient: Gradient, layer: LayerNodeIdentifier, document: &DocumentMessageHandler) -> Self {
let transform = gradient_space_transform(layer, document);
let is_gradient_list = get_gradient_stops(layer, &document.network_interface).is_some();
Self {
layer: Some(layer),
transform,
gradient: gradient.clone(),
dragging: GradientDragTarget::End,
initial_gradient: gradient,
is_gradient_list,
is_gradient_list: get_gradient_stops(layer, &document.network_interface).is_some(),
}
}