diff --git a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs index 89aa49174a..2af07a433f 100644 --- a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs +++ b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs @@ -604,8 +604,15 @@ impl<'a> ModifyInputsContext<'a> { } }; + // Only the stops are being replaced, so the ramp's other settings stay as the value node already holds them + let gradient_spread = self.gradient_value_ramp(gradient_value_id).unwrap_or_default().gradient_spread; + let ramp = GradientRamp { + gradient_spread, + ..GradientRamp::from(stops) + }; + let input_connector = InputConnector::node(gradient_value_id, graphene_std::math_nodes::gradient_value::GradientInput); - self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::GradientRamp(GradientRamp::from(stops)), false), false); + self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::GradientRamp(ramp), false), false); } /// Update the last 'Gradient Positions' node in the chain when one exists, so on-canvas stop drags stay live even @@ -737,20 +744,26 @@ impl<'a> ModifyInputsContext<'a> { self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::GradientForm(gradient_form), false), false); } - /// Write the gradient spread to the last 'Gradient Spread' node in the chain, inserting one only when the value differs - /// from the default (`Pad`). + /// Set the spread on the chain's gradient value, which is where the ramp carries it. pub fn gradient_spread_set(&mut self, gradient_spread: GradientSpread) { let Some(output_layer) = self.get_output_layer() else { return }; - - let target_input = gradient_chain_target_input(output_layer, self.network_interface); - let identifier = graphene_std::math_nodes::gradient_spread::IDENTIFIER; - let create_if_nonexistent = gradient_spread != GradientSpread::default(); - let Some(node_id) = self.existing_proto_node_id_at(&target_input, identifier, create_if_nonexistent) else { + let Some(gradient_value_id) = get_upstream_gradient_value_node_id(output_layer, self.network_interface) else { return; }; + let Some(ramp) = self.gradient_value_ramp(gradient_value_id) else { return }; - let input_connector = InputConnector::node(node_id, graphene_std::math_nodes::gradient_spread::GradientSpreadInput); - self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::GradientSpread(gradient_spread), false), false); + let ramp = GradientRamp { gradient_spread, ..ramp }; + let input_connector = InputConnector::node(gradient_value_id, graphene_std::math_nodes::gradient_value::GradientInput); + self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::GradientRamp(ramp), false), false); + } + + /// The ramp currently held by a 'Gradient Value' node. + fn gradient_value_ramp(&self, gradient_value_id: NodeId) -> Option { + let node = self.network_interface.document_network().nodes.get(&gradient_value_id)?; + let TaggedValue::GradientRamp(ramp) = node.input(graphene_std::math_nodes::gradient_value::GradientInput)?.as_value()? else { + return None; + }; + Some(ramp.clone()) } pub fn clip_mode_toggle(&mut self, clip_mode: Option) { diff --git a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs index 7e169fe9dc..ef16387f91 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -388,6 +388,15 @@ pub fn get_fill_input_node_id(layer: LayerNodeIdentifier, network_interface: &No Some(*node_id) } +/// The spread baked into the 'Gradient Value' node feeding a layer's chain. +pub fn get_chain_source_gradient_spread(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option { + let gradient_value_node = network_interface.document_network().nodes.get(&get_upstream_gradient_value_node_id(layer, network_interface)?)?; + let TaggedValue::GradientRamp(ramp) = gradient_value_node.input(graphene_std::math_nodes::gradient_value::GradientInput)?.as_value()? else { + return None; + }; + Some(ramp.gradient_spread) +} + /// Get the gradient stops of a layer, if any. pub fn get_gradient_stops(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option { // Try to find the gradient stops value that is created by a Fill node first diff --git a/editor/src/messages/tool/tool_messages/gradient_tool.rs b/editor/src/messages/tool/tool_messages/gradient_tool.rs index 34703ddd61..91216451b1 100644 --- a/editor/src/messages/tool/tool_messages/gradient_tool.rs +++ b/editor/src/messages/tool/tool_messages/gradient_tool.rs @@ -9,8 +9,8 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye 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_fill_node_id_with_direct_fill_input, get_gradient_stops, get_upstream_gradient_value_node_id, gradient_chain_target_input, replaceable_paint_chain, - reverse_direction_tooltip_description, + self, NodeGraphLayer, get_chain_source_gradient_spread, get_fill_node_id_with_direct_fill_input, get_gradient_stops, get_upstream_gradient_value_node_id, gradient_chain_target_input, + replaceable_paint_chain, reverse_direction_tooltip_description, }; use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration}; use glam::DMat2; @@ -377,19 +377,16 @@ struct GradientAppearance { gradient_spread: GradientSpread, } -/// Resolve the gradient transform, type, and gradient spread by walking the chain feeding the layer. Transform composes all -/// 'Transform' nodes. Type and gradient spread come from the closest-to-layer node of each kind, or the type default. +/// Resolve the gradient transform, form, and spread by walking the chain feeding the layer. fn read_gradient_chain_state(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> GradientAppearance { 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_form_reference = DefinitionIdentifier::ProtoNode(graphene_std::math_nodes::gradient_form::IDENTIFIER); - let gradient_spread_reference = DefinitionIdentifier::ProtoNode(graphene_std::math_nodes::gradient_spread::IDENTIFIER); let mut transforms_downstream_to_upstream: Vec = Vec::new(); let mut gradient_form: Option = None; - let mut gradient_spread: Option = None; for node_id in network_interface .upstream_flow_back_from_nodes(vec![walk_from], &[], FlowType::HorizontalFlow) @@ -408,11 +405,6 @@ fn read_gradient_chain_state(layer: LayerNodeIdentifier, network_interface: &Nod && let Some(TaggedValue::GradientForm(value)) = document_node.inputs.get(1).and_then(|input| input.as_value()) { gradient_form = Some(*value); - } else if reference == gradient_spread_reference - && gradient_spread.is_none() - && let Some(TaggedValue::GradientSpread(value)) = document_node.inputs.get(1).and_then(|input| input.as_value()) - { - gradient_spread = Some(*value); } } @@ -422,7 +414,7 @@ fn read_gradient_chain_state(layer: LayerNodeIdentifier, network_interface: &Nod GradientAppearance { transform: composed_transform, gradient_form: gradient_form.unwrap_or_default(), - gradient_spread: gradient_spread.unwrap_or_default(), + gradient_spread: get_chain_source_gradient_spread(layer, network_interface).unwrap_or_default(), } } @@ -1979,14 +1971,14 @@ mod test_gradient { use crate::messages::portfolio::document::utility_types::misc::GroupFolderType; use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, OutputConnector}; use crate::messages::tool::common_functionality::graph_modification_utils::get_fill_node_id_with_direct_fill_input; - use crate::messages::tool::common_functionality::graph_modification_utils::get_gradient_stops; use crate::messages::tool::common_functionality::graph_modification_utils::get_upstream_gradient_value_node_id; pub use crate::test_utils::test_prelude::*; use glam::DAffine2; use graph_craft::document::NodeInput; use graph_craft::document::value::TaggedValue; + use graphene_std::NodeParameter; use graphene_std::color::SRGBA8; - use graphene_std::vector::style::{GradientSpread, build_transform_with_y_preservation}; + use graphene_std::vector::style::{GradientForm, GradientSpread, build_transform_with_y_preservation}; use graphene_std::vector::{Gradient, GradientRamp, GradientStop, fill}; use super::gradient_space_transform; @@ -2643,6 +2635,82 @@ mod test_gradient { assert_eq!(gradient.gradient_spread, GradientSpread::Reflect); } + #[tokio::test] + async fn spread_set_on_the_gradient_value_node_reaches_the_tool() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + let layer = create_fill_gradient_chain_layer(&mut editor).await; + editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] }).await; + editor.select_tool(ToolType::Gradient).await; + + let (gradient, _) = get_gradient_from_chain(&mut editor).await; + assert_eq!(gradient.gradient_spread, GradientSpread::Pad); + + // Stamp the spread into the value node's ramp, the way the Properties panel's color picker writes it + let gradient_value_id = { + let network_interface = &editor.active_document().network_interface; + get_upstream_gradient_value_node_id(layer, network_interface).expect("the chain should have a gradient value node") + }; + editor + .handle_message(NodeGraphMessage::SetInputValue { + node_id: gradient_value_id, + input_index: graphene_std::math_nodes::gradient_value::GradientInput::INDEX, + value: TaggedValue::GradientRamp(GradientRamp { + gradient_spread: GradientSpread::Repeat, + ..GradientRamp::from(&gradient.stops) + }) + .into(), + }) + .await; + + let (gradient, _) = get_gradient_from_chain(&mut editor).await; + assert_eq!(gradient.gradient_spread, GradientSpread::Repeat, "the tool should read the spread baked into the value node"); + + // Editing the stops must carry that spread through rather than stamping the default back over it + editor + .handle_message(GradientToolMessage::UpdateRamp { + ramp: ramp_with_spread(&gradient.stops, GradientSpread::Repeat), + }) + .await; + + let (gradient, _) = get_gradient_from_chain(&mut editor).await; + assert_eq!(gradient.gradient_spread, GradientSpread::Repeat, "editing the stops should preserve the spread"); + } + + #[tokio::test] + async fn spread_set_from_the_tool_lands_on_the_gradient_value_node() { + use crate::messages::tool::common_functionality::graph_modification_utils::get_chain_source_gradient_spread; + + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + let layer = create_fill_gradient_chain_layer(&mut editor).await; + editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] }).await; + editor.select_tool(ToolType::Gradient).await; + + let (gradient, _) = get_gradient_from_chain(&mut editor).await; + editor + .handle_message(GradientToolMessage::UpdateRamp { + ramp: ramp_with_spread(&gradient.stops, GradientSpread::Reflect), + }) + .await; + + // The Properties panel reads the value node's own ramp, so the spread has to be stored there + let network_interface = &editor.active_document().network_interface; + assert_eq!( + get_chain_source_gradient_spread(layer, network_interface), + Some(GradientSpread::Reflect), + "the spread should be written into the gradient value's ramp" + ); + + let spread_reference = DefinitionIdentifier::ProtoNode(graphene_std::math_nodes::gradient_spread::IDENTIFIER); + let inserted_spread_node = network_interface + .document_network() + .nodes + .keys() + .any(|node_id| network_interface.reference(node_id, &[]).as_ref() == Some(&spread_reference)); + assert!(!inserted_spread_node, "the tool should leave graph topology alone rather than authoring a 'Gradient Spread' node"); + } + #[tokio::test] async fn gradient_list_layer_drag_endpoint() { let mut editor = EditorTestUtils::create(); @@ -2792,8 +2860,6 @@ mod test_gradient { // graph space rather than stranded at the origin. #[tokio::test] async fn gradient_chain_node_on_fill_secondary_input_takes_feeder_slot() { - use graphene_std::vector::style::GradientSpread; - let mut editor = EditorTestUtils::create(); editor.new_document().await; editor.drag_tool(ToolType::Ellipse, 0., 0., 100., 100., ModifierKeys::empty()).await; @@ -2849,32 +2915,31 @@ mod test_gradient { .await; let feeder_position = editor.active_document_mut().network_interface.position(&gradient_value_id, &[]).expect("Gradient Value position"); - // Set the gradient spread through the tool, which splices a 'Gradient Spread' node onto the Fill's fill input wire. + // Set the gradient form through the tool, which splices a 'Gradient Form' node onto the Fill's fill input wire. editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] }).await; editor.select_tool(ToolType::Gradient).await; - let stops = get_gradient_stops(layer, &editor.active_document().network_interface).expect("the chain layer should resolve its gradient stops"); editor - .handle_message(GradientToolMessage::UpdateRamp { - ramp: ramp_with_spread(&stops, GradientSpread::Reflect), + .handle_message(GradientToolMessage::UpdateOptions { + options: super::GradientOptionsUpdate::Form(GradientForm::Radial), }) .await; - let spread_reference = DefinitionIdentifier::ProtoNode(graphene_std::math_nodes::gradient_spread::IDENTIFIER); - let spread_node_id = { + let form_reference = DefinitionIdentifier::ProtoNode(graphene_std::math_nodes::gradient_form::IDENTIFIER); + let form_node_id = { let network_interface = &editor.active_document().network_interface; network_interface .document_network() .nodes .keys() .copied() - .find(|node_id| network_interface.reference(node_id, &[]).as_ref() == Some(&spread_reference)) - .expect("Gradient Spread node should have been inserted") + .find(|node_id| network_interface.reference(node_id, &[]).as_ref() == Some(&form_reference)) + .expect("Gradient Form node should have been inserted") }; - let spread_position = editor.active_document_mut().network_interface.position(&spread_node_id, &[]).expect("Gradient Spread position"); + let form_position = editor.active_document_mut().network_interface.position(&form_node_id, &[]).expect("Gradient Form position"); let feeder_position_after = editor.active_document_mut().network_interface.position(&gradient_value_id, &[]).expect("Gradient Value position after"); - assert_eq!(spread_position, feeder_position, "the inserted node should occupy the feeder's former slot, not the graph origin"); + assert_eq!(form_position, feeder_position, "the inserted node should occupy the feeder's former slot, not the graph origin"); assert_eq!( feeder_position_after, feeder_position - glam::IVec2::new(crate::consts::NODE_CHAIN_WIDTH, 0),