From 0398eda9d9e95b933fd5c47d17efbef71a8738a8 Mon Sep 17 00:00:00 2001 From: YohYamasaki Date: Tue, 7 Jul 2026 17:53:18 +0900 Subject: [PATCH 1/5] Fix gradient chain metadata sync for exposed Fill nodes --- .../graph_operation_message.rs | 9 ++++- .../graph_operation_message_handler.rs | 36 +++++++++++++++++-- .../document/graph_operation/utility_types.rs | 27 ++++++++++++++ .../node_graph/node_graph_message_handler.rs | 2 ++ .../graph_modification_utils.rs | 27 ++++++++++++++ .../tool/tool_messages/gradient_tool.rs | 28 +++++++++++++-- 6 files changed, 123 insertions(+), 6 deletions(-) diff --git a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs index 7d986bce1b..ce358851dd 100644 --- a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs +++ b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs @@ -1,6 +1,6 @@ use super::utility_types::TransformIn; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; -use crate::messages::portfolio::document::utility_types::network_interface::NodeTemplate; +use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate, OutputConnector}; use crate::messages::prelude::*; use glam::{DAffine2, DVec2}; use graph_craft::document::NodeId; @@ -27,6 +27,9 @@ pub enum GraphOperationMessage { spread_method: GradientSpreadMethod, transform: DAffine2, }, + InitializeFillGradientMetadata { + fill_node_id: NodeId, + }, BlendingFillSet { layer: LayerNodeIdentifier, fill: f64, @@ -157,4 +160,8 @@ pub enum GraphOperationMessage { /// When true, centers the SVG at the transform origin (clipboard paste / drag-drop). When false, keeps natural SVG coordinates (file-open flow). center: bool, }, + NormalizeAfterInputConnection { + output_connector: OutputConnector, + input_connector: InputConnector, + }, } diff --git a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs index 4e64599e79..fd959fdd68 100644 --- a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs +++ b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs @@ -2,19 +2,21 @@ use super::transform_utils; use super::utility_types::ModifyInputsContext; use crate::consts::{LAYER_INDENT_OFFSET, STACK_VERTICAL_GAP}; use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; +use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; -use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeNetworkInterface, OutputConnector}; +use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, InputConnector, NodeNetworkInterface, OutputConnector}; use crate::messages::portfolio::document::utility_types::nodes::CollapsedLayers; use crate::messages::prelude::*; use crate::messages::tool::common_functionality::graph_modification_utils::get_clip_mode; use glam::{DAffine2, DVec2, IVec2}; use graph_craft::descriptor; use graph_craft::document::{NodeId, NodeInput}; +use graphene_std::NodeInputDecleration; use graphene_std::list::List; use graphene_std::renderer::convert_usvg_path::convert_usvg_path; use graphene_std::text::{Font, TypesettingConfig}; use graphene_std::vector::style::{GradientSpreadMethod, GradientStop, GradientStops, GradientType, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin}; -use graphene_std::{Artboard, Color}; +use graphene_std::{Artboard, Color, Graphic}; #[derive(ExtractField)] pub struct GraphOperationMessageContext<'a> { @@ -50,6 +52,10 @@ impl MessageHandler> for modify_inputs.fill_gradient_set(gradient, gradient_type, spread_method, transform); } } + GraphOperationMessage::InitializeFillGradientMetadata { fill_node_id } => { + let mut modify_inputs = ModifyInputsContext::new(network_interface, responses); + modify_inputs.initialize_fill_gradient_metadata(fill_node_id); + } GraphOperationMessage::BlendingFillSet { layer, fill } => { if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) { modify_inputs.opacity_fill_set(fill); @@ -479,6 +485,32 @@ impl MessageHandler> for // (skipped automatically when identity, so file-open with content at origin creates no Transform node). modify_inputs.transform_set(placement_transform, TransformIn::Local, false); } + GraphOperationMessage::NormalizeAfterInputConnection { output_connector, input_connector } => { + let InputConnector::Node { node_id, input_index } = input_connector else { return }; + + // Fill node: When a Gradient Value chain is connected to a Fill node's paint input, reset the hidden gradient metadata. + // This prevents the rendered gradient and Gradient tool state from getting out of sync. + if let Some(def_id) = network_interface.reference(&node_id, &[]).as_ref() + && def_id == &DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER) + && input_index == graphene_std::vector::fill::FillInput::>::INDEX + { + let Some(walk_from) = output_connector.node_id() else { return }; + + let is_gradient_value_connected = network_interface + .upstream_flow_back_from_nodes(vec![walk_from], &[], FlowType::PrimaryFlow) + .take_while(|node_id| !network_interface.is_layer(node_id, &[])) + .any(|node_id| { + let reference = network_interface.reference(&node_id, &[]); + reference.as_ref() == Some(&DefinitionIdentifier::ProtoNode(graphene_std::math_nodes::gradient_value::IDENTIFIER)) + }); + + if !is_gradient_value_connected { + return; + } + + responses.add(GraphOperationMessage::InitializeFillGradientMetadata { fill_node_id: node_id }); + }; + } } } 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 4920f0aeb5..b2983b5eed 100644 --- a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs +++ b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs @@ -508,6 +508,33 @@ impl<'a> ModifyInputsContext<'a> { ); } + // Reset all gradient-related metadata on the target Fill node. + pub fn initialize_fill_gradient_metadata(&mut self, fill_node_id: NodeId) { + self.set_input_with_refresh( + InputConnector::node(fill_node_id, graphene_std::vector::fill::BackupGradientInput::INDEX), + NodeInput::value(TaggedValue::Gradient(Default::default()), false), + true, + ); + + self.set_input_with_refresh( + InputConnector::node(fill_node_id, graphene_std::vector::fill::GradientTypeInput::INDEX), + NodeInput::value(TaggedValue::GradientType(Default::default()), false), + true, + ); + + self.set_input_with_refresh( + InputConnector::node(fill_node_id, graphene_std::vector::fill::SpreadMethodInput::INDEX), + NodeInput::value(TaggedValue::GradientSpreadMethod(Default::default()), false), + true, + ); + + self.set_input_with_refresh( + InputConnector::node(fill_node_id, graphene_std::vector::fill::TransformInput::INDEX), + NodeInput::value(TaggedValue::OptionalDAffine2(None), false), + false, + ); + } + pub fn blend_mode_set(&mut self, blend_mode: BlendMode) { let Some(blend_node_id) = self.existing_proto_node_id(graphene_std::blending_nodes::blend_mode::IDENTIFIER, true) else { return; diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs index 628effc89b..bdf39ee834 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs @@ -248,6 +248,8 @@ impl<'a> MessageHandler> for NodeG return; } network_interface.create_wire(&output_connector, &input_connector, selection_network_path); + + responses.add(GraphOperationMessage::NormalizeAfterInputConnection { output_connector, input_connector }); } NodeGraphMessage::Copy => { let all_selected_nodes = network_interface.upstream_chain_nodes(selection_network_path); 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 c68bc5ac56..b5518ddbbb 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -818,6 +818,33 @@ pub fn set_fill_for_selected_layers(fill_choice: FillChoice, document: &Document } } +/// Check if a gradient chain feeding a Fill node needs a Transform node. +pub fn gradient_fill_chain_needs_transform_node(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> bool { + let target_input = gradient_chain_target_input(layer, network_interface); + let InputConnector::Node { node_id: fill_id, .. } = target_input else { + return false; + }; + + // Only handle the case where a Gradient Value node is connected to a Fill node + if network_interface.reference(&fill_id, &[]).as_ref() != Some(&DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER)) + || get_upstream_gradient_value_node_id(layer, network_interface).is_none() + { + return false; + } + + // Skip chains that already have a Transform node + let Some(walk_from) = network_interface.upstream_output_connector(&target_input, &[]).and_then(|output| output.node_id()) else { + return false; + }; + + let has_transform_node = network_interface + .upstream_flow_back_from_nodes(vec![walk_from], &[], FlowType::HorizontalFlow) + .take_while(|node_id| !network_interface.is_layer(node_id, &[])) + .any(|node_id| network_interface.reference(&node_id, &[]).as_ref() == Some(&DefinitionIdentifier::ProtoNode(graphene_std::transform_nodes::transform::IDENTIFIER))); + + !has_transform_node +} + /// Sets the stroke color on all selected non-artboard layers. Layers without an existing Stroke node get one created using /// the provided `weight`, so picking any color (including `None`) from an unticked stroke control bar entry both attaches /// the Stroke node and applies the chosen color. diff --git a/editor/src/messages/tool/tool_messages/gradient_tool.rs b/editor/src/messages/tool/tool_messages/gradient_tool.rs index cd3d6672f0..d104961b35 100644 --- a/editor/src/messages/tool/tool_messages/gradient_tool.rs +++ b/editor/src/messages/tool/tool_messages/gradient_tool.rs @@ -9,14 +9,16 @@ 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, + self, NodeGraphLayer, get_fill_node_id_with_direct_fill_input, get_gradient_stops, get_upstream_gradient_value_node_id, gradient_chain_target_input, gradient_fill_chain_needs_transform_node, }; use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration}; use glam::DMat2; use graph_craft::document::value::TaggedValue; use graphene_std::color::SRGBA8; use graphene_std::raster::color::Color; -use graphene_std::vector::style::{FillChoice, FillChoiceUI, GradientSpreadMethod, GradientStop, GradientStops, GradientStopsUI, GradientType, build_transform_with_y_preservation}; +use graphene_std::vector::style::{ + FillChoice, FillChoiceUI, GradientSpreadMethod, GradientStop, GradientStops, GradientStopsUI, GradientType, build_transform_with_y_preservation, initial_gradient_transform_for_bounding_box, +}; #[derive(Default, ExtractField)] pub struct GradientTool { @@ -1119,6 +1121,16 @@ impl Fsm for GradientToolFsmState { tool_data.color_picker_editing_color_stop = None; } tool_data.selected_gradient = None; + + // Insert a Transform node if a chain-backed gradient does not have one yet. This prevents the rendered gradient and Gradient tool state from getting out of sync. + let network_interface = &document.network_interface; + for layer in network_interface.selected_nodes().selected_visible_layers(network_interface) { + if gradient_fill_chain_needs_transform_node(layer, network_interface) { + let transform = initial_gradient_transform_for_bounding_box(network_interface.document_metadata().nonzero_bounding_box(layer)); + responses.add(GraphOperationMessage::GradientTransformSet { layer, transform }); + } + } + GradientToolFsmState::Ready { hovering: GradientHoverTarget::None, selected: GradientSelectedTarget::None, @@ -1490,7 +1502,7 @@ impl Fsm for GradientToolFsmState { responses.add(NodeGraphMessage::SelectedNodesSet { nodes }); } - let (gradient, appearance, source) = match resolve_gradient(layer, &document.network_interface) { + let (gradient, mut appearance, source) = match resolve_gradient(layer, &document.network_interface) { // Use the already existing gradient if it exists Some(gradient) => gradient, // Generate a new gradient running primary → secondary so the default working colors @@ -1516,6 +1528,16 @@ impl Fsm for GradientToolFsmState { GradientSource::Direct, ), }; + + // Insert a Transform node if a chain-backed gradient does not have one yet. This prevents the rendered gradient and Gradient tool state from getting out of sync. + if matches!(source, GradientSource::Chain) && gradient_fill_chain_needs_transform_node(layer, &document.network_interface) { + let transform = initial_gradient_transform_for_bounding_box(document.network_interface.document_metadata().nonzero_bounding_box(layer)); + + responses.add(GraphOperationMessage::GradientTransformSet { layer, transform }); + + // GraphOperation is queued, so it won't affect this PointerDown immediately. Patch the local tool state too. + appearance.transform = transform; + } let mut selected_gradient = SelectedGradient::new(gradient, appearance, source, layer, document); selected_gradient.dragging = GradientDragTarget::New; From c4d27b8022a3aa4d846e4267a4f171c0fabfb5c6 Mon Sep 17 00:00:00 2001 From: YohYamasaki Date: Tue, 7 Jul 2026 20:22:22 +0900 Subject: [PATCH 2/5] Avoid using GraphOperationMessage from NodeGraphMessageHandler --- .../graph_operation_message.rs | 9 +--- .../graph_operation_message_handler.rs | 36 +--------------- .../document/graph_operation/utility_types.rs | 27 ------------ .../node_graph/node_graph_message_handler.rs | 43 ++++++++++++++++++- 4 files changed, 45 insertions(+), 70 deletions(-) diff --git a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs index ce358851dd..7d986bce1b 100644 --- a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs +++ b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs @@ -1,6 +1,6 @@ use super::utility_types::TransformIn; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; -use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate, OutputConnector}; +use crate::messages::portfolio::document::utility_types::network_interface::NodeTemplate; use crate::messages::prelude::*; use glam::{DAffine2, DVec2}; use graph_craft::document::NodeId; @@ -27,9 +27,6 @@ pub enum GraphOperationMessage { spread_method: GradientSpreadMethod, transform: DAffine2, }, - InitializeFillGradientMetadata { - fill_node_id: NodeId, - }, BlendingFillSet { layer: LayerNodeIdentifier, fill: f64, @@ -160,8 +157,4 @@ pub enum GraphOperationMessage { /// When true, centers the SVG at the transform origin (clipboard paste / drag-drop). When false, keeps natural SVG coordinates (file-open flow). center: bool, }, - NormalizeAfterInputConnection { - output_connector: OutputConnector, - input_connector: InputConnector, - }, } diff --git a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs index fd959fdd68..4e64599e79 100644 --- a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs +++ b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs @@ -2,21 +2,19 @@ use super::transform_utils; use super::utility_types::ModifyInputsContext; use crate::consts::{LAYER_INDENT_OFFSET, STACK_VERTICAL_GAP}; use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; -use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; -use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, InputConnector, NodeNetworkInterface, OutputConnector}; +use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeNetworkInterface, OutputConnector}; use crate::messages::portfolio::document::utility_types::nodes::CollapsedLayers; use crate::messages::prelude::*; use crate::messages::tool::common_functionality::graph_modification_utils::get_clip_mode; use glam::{DAffine2, DVec2, IVec2}; use graph_craft::descriptor; use graph_craft::document::{NodeId, NodeInput}; -use graphene_std::NodeInputDecleration; use graphene_std::list::List; use graphene_std::renderer::convert_usvg_path::convert_usvg_path; use graphene_std::text::{Font, TypesettingConfig}; use graphene_std::vector::style::{GradientSpreadMethod, GradientStop, GradientStops, GradientType, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin}; -use graphene_std::{Artboard, Color, Graphic}; +use graphene_std::{Artboard, Color}; #[derive(ExtractField)] pub struct GraphOperationMessageContext<'a> { @@ -52,10 +50,6 @@ impl MessageHandler> for modify_inputs.fill_gradient_set(gradient, gradient_type, spread_method, transform); } } - GraphOperationMessage::InitializeFillGradientMetadata { fill_node_id } => { - let mut modify_inputs = ModifyInputsContext::new(network_interface, responses); - modify_inputs.initialize_fill_gradient_metadata(fill_node_id); - } GraphOperationMessage::BlendingFillSet { layer, fill } => { if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) { modify_inputs.opacity_fill_set(fill); @@ -485,32 +479,6 @@ impl MessageHandler> for // (skipped automatically when identity, so file-open with content at origin creates no Transform node). modify_inputs.transform_set(placement_transform, TransformIn::Local, false); } - GraphOperationMessage::NormalizeAfterInputConnection { output_connector, input_connector } => { - let InputConnector::Node { node_id, input_index } = input_connector else { return }; - - // Fill node: When a Gradient Value chain is connected to a Fill node's paint input, reset the hidden gradient metadata. - // This prevents the rendered gradient and Gradient tool state from getting out of sync. - if let Some(def_id) = network_interface.reference(&node_id, &[]).as_ref() - && def_id == &DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER) - && input_index == graphene_std::vector::fill::FillInput::>::INDEX - { - let Some(walk_from) = output_connector.node_id() else { return }; - - let is_gradient_value_connected = network_interface - .upstream_flow_back_from_nodes(vec![walk_from], &[], FlowType::PrimaryFlow) - .take_while(|node_id| !network_interface.is_layer(node_id, &[])) - .any(|node_id| { - let reference = network_interface.reference(&node_id, &[]); - reference.as_ref() == Some(&DefinitionIdentifier::ProtoNode(graphene_std::math_nodes::gradient_value::IDENTIFIER)) - }); - - if !is_gradient_value_connected { - return; - } - - responses.add(GraphOperationMessage::InitializeFillGradientMetadata { fill_node_id: node_id }); - }; - } } } 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 b2983b5eed..4920f0aeb5 100644 --- a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs +++ b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs @@ -508,33 +508,6 @@ impl<'a> ModifyInputsContext<'a> { ); } - // Reset all gradient-related metadata on the target Fill node. - pub fn initialize_fill_gradient_metadata(&mut self, fill_node_id: NodeId) { - self.set_input_with_refresh( - InputConnector::node(fill_node_id, graphene_std::vector::fill::BackupGradientInput::INDEX), - NodeInput::value(TaggedValue::Gradient(Default::default()), false), - true, - ); - - self.set_input_with_refresh( - InputConnector::node(fill_node_id, graphene_std::vector::fill::GradientTypeInput::INDEX), - NodeInput::value(TaggedValue::GradientType(Default::default()), false), - true, - ); - - self.set_input_with_refresh( - InputConnector::node(fill_node_id, graphene_std::vector::fill::SpreadMethodInput::INDEX), - NodeInput::value(TaggedValue::GradientSpreadMethod(Default::default()), false), - true, - ); - - self.set_input_with_refresh( - InputConnector::node(fill_node_id, graphene_std::vector::fill::TransformInput::INDEX), - NodeInput::value(TaggedValue::OptionalDAffine2(None), false), - false, - ); - } - pub fn blend_mode_set(&mut self, blend_mode: BlendMode) { let Some(blend_node_id) = self.existing_proto_node_id(graphene_std::blending_nodes::blend_mode::IDENTIFIER, true) else { return; diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs index bdf39ee834..629bb22185 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs @@ -25,6 +25,7 @@ use crate::messages::viewport::Position; use glam::{DAffine2, DVec2, IVec2}; use graph_craft::document::value::TaggedValue; use graph_craft::document::{DocumentNodeImplementation, NodeId, NodeInput}; +use graphene_std::list::List; use graphene_std::math::math_ext::QuadExt; use graphene_std::vector::algorithms::bezpath_algorithms::bezpath_is_inside_bezpath; use graphene_std::*; @@ -249,7 +250,47 @@ impl<'a> MessageHandler> for NodeG } network_interface.create_wire(&output_connector, &input_connector, selection_network_path); - responses.add(GraphOperationMessage::NormalizeAfterInputConnection { output_connector, input_connector }); + // Post wire creation process + let InputConnector::Node { node_id, input_index } = input_connector else { return }; + // Fill node: When a Gradient Value chain is connected to a Fill node's paint input, reset the hidden gradient metadata. + // This prevents the rendered gradient and Gradient tool state from getting out of sync. + if network_interface.reference(&node_id, selection_network_path).as_ref() == Some(&DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER)) + && input_index == graphene_std::vector::fill::FillInput::>::INDEX + { + let Some(walk_from) = output_connector.node_id() else { return }; + let is_gradient_value_connected = network_interface + .upstream_flow_back_from_nodes(vec![walk_from], selection_network_path, FlowType::PrimaryFlow) + .take_while(|node_id| !network_interface.is_layer(node_id, selection_network_path)) + .any(|node_id| { + network_interface.reference(&node_id, selection_network_path).as_ref() == Some(&DefinitionIdentifier::ProtoNode(graphene_std::math_nodes::gradient_value::IDENTIFIER)) + }); + + if is_gradient_value_connected { + network_interface.set_input( + &InputConnector::node(node_id, graphene_std::vector::fill::BackupGradientInput::INDEX), + NodeInput::value(TaggedValue::Gradient(Default::default()), false), + selection_network_path, + ); + network_interface.set_input( + &InputConnector::node(node_id, graphene_std::vector::fill::GradientTypeInput::INDEX), + NodeInput::value(TaggedValue::GradientType(Default::default()), false), + selection_network_path, + ); + network_interface.set_input( + &InputConnector::node(node_id, graphene_std::vector::fill::SpreadMethodInput::INDEX), + NodeInput::value(TaggedValue::GradientSpreadMethod(Default::default()), false), + selection_network_path, + ); + network_interface.set_input( + &InputConnector::node(node_id, graphene_std::vector::fill::TransformInput::INDEX), + NodeInput::value(TaggedValue::OptionalDAffine2(None), false), + selection_network_path, + ); + + responses.add(PropertiesPanelMessage::Refresh); + responses.add(NodeGraphMessage::RunDocumentGraph); + } + } } NodeGraphMessage::Copy => { let all_selected_nodes = network_interface.upstream_chain_nodes(selection_network_path); From 2b2f6032bf4c08aef43053968f97d09794483023 Mon Sep 17 00:00:00 2001 From: YohYamasaki Date: Wed, 8 Jul 2026 09:24:23 +0900 Subject: [PATCH 3/5] Avoid transform materialization without gradient edits --- .../tool/tool_messages/gradient_tool.rs | 46 ++++++++++++------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/editor/src/messages/tool/tool_messages/gradient_tool.rs b/editor/src/messages/tool/tool_messages/gradient_tool.rs index d104961b35..104dd23e75 100644 --- a/editor/src/messages/tool/tool_messages/gradient_tool.rs +++ b/editor/src/messages/tool/tool_messages/gradient_tool.rs @@ -410,7 +410,12 @@ fn resolve_gradient(layer: LayerNodeIdentifier, network_interface: &NodeNetworkI } // Then, try to construct a gradient out of a chain, which is directly connected to a Fill node or a layer - let appearance = read_gradient_chain_state(layer, network_interface); + let mut appearance = read_gradient_chain_state(layer, network_interface); + + // Match the Fill node's fallback transform until the chain materializes its own Transform node. + if gradient_fill_chain_needs_transform_node(layer, network_interface) { + appearance.transform = initial_gradient_transform_for_bounding_box(network_interface.document_metadata().nonzero_bounding_box(layer)); + } Some((stops, appearance, GradientSource::Chain)) } else { None @@ -1122,15 +1127,6 @@ impl Fsm for GradientToolFsmState { } tool_data.selected_gradient = None; - // Insert a Transform node if a chain-backed gradient does not have one yet. This prevents the rendered gradient and Gradient tool state from getting out of sync. - let network_interface = &document.network_interface; - for layer in network_interface.selected_nodes().selected_visible_layers(network_interface) { - if gradient_fill_chain_needs_transform_node(layer, network_interface) { - let transform = initial_gradient_transform_for_bounding_box(network_interface.document_metadata().nonzero_bounding_box(layer)); - responses.add(GraphOperationMessage::GradientTransformSet { layer, transform }); - } - } - GradientToolFsmState::Ready { hovering: GradientHoverTarget::None, selected: GradientSelectedTarget::None, @@ -1335,13 +1331,23 @@ impl Fsm for GradientToolFsmState { let mut drag_hint: Option = None; let mut transaction_started = false; + let mut pending_transform_materialization = None; for layer in document.network_interface.selected_nodes().selected_visible_layers(&document.network_interface) { - let Some((gradient, appearance, source)) = resolve_gradient(layer, &document.network_interface) else { + let Some((gradient, mut appearance, source)) = resolve_gradient(layer, &document.network_interface) else { continue; }; + let is_gradient_chain = source == GradientSource::Chain; + let transform_to_materialize = if is_gradient_chain && gradient_fill_chain_needs_transform_node(layer, &document.network_interface) { + Some(initial_gradient_transform_for_bounding_box(document.network_interface.document_metadata().nonzero_bounding_box(layer))) + } else { + None + }; + // if let Some(transform) = transform_to_materialize { + // // GraphOperation is queued later, so it won't affect this PointerDown immediately. Patch the local tool state too. + // appearance.transform = transform; + // } let gradient_space_transform = gradient_space_transform(layer, document); let unit_to_viewport = gradient_space_transform * appearance.transform; - let is_gradient_chain = source == GradientSource::Chain; let (start, end) = gradient_handle_positions(unit_to_viewport); // Check for dragging a midpoint diamond @@ -1373,6 +1379,7 @@ impl Fsm for GradientToolFsmState { initial_gradient: gradient.clone(), is_gradient_chain, }); + pending_transform_materialization = transform_to_materialize.map(|transform| (layer, transform)); break; } @@ -1416,6 +1423,7 @@ impl Fsm for GradientToolFsmState { initial_gradient_transform: appearance.transform, is_gradient_chain, }); + pending_transform_materialization = transform_to_materialize.map(|transform| (layer, transform)); } } @@ -1433,7 +1441,8 @@ impl Fsm for GradientToolFsmState { initial_gradient: gradient.clone(), initial_gradient_transform: appearance.transform, is_gradient_chain, - }) + }); + pending_transform_materialization = transform_to_materialize.map(|transform| (layer, transform)); } } } @@ -1533,7 +1542,7 @@ impl Fsm for GradientToolFsmState { if matches!(source, GradientSource::Chain) && gradient_fill_chain_needs_transform_node(layer, &document.network_interface) { let transform = initial_gradient_transform_for_bounding_box(document.network_interface.document_metadata().nonzero_bounding_box(layer)); - responses.add(GraphOperationMessage::GradientTransformSet { layer, transform }); + pending_transform_materialization = Some((layer, transform)); // GraphOperation is queued, so it won't affect this PointerDown immediately. Patch the local tool state too. appearance.transform = transform; @@ -1554,8 +1563,13 @@ impl Fsm for GradientToolFsmState { } }; - if matches!(gradient_state, GradientToolFsmState::Drawing { .. }) && !transaction_started { - responses.add(DocumentMessage::StartTransaction); + if matches!(gradient_state, GradientToolFsmState::Drawing { .. }) { + if !transaction_started { + responses.add(DocumentMessage::StartTransaction); + } + if let Some((layer, transform)) = pending_transform_materialization { + responses.add(GraphOperationMessage::GradientTransformSet { layer, transform }); + } } responses.add(OverlaysMessage::Draw); From 6893b2ec8c85720633ffe98164ac1dcaa37d2b1e Mon Sep 17 00:00:00 2001 From: YohYamasaki Date: Wed, 8 Jul 2026 10:17:20 +0900 Subject: [PATCH 4/5] Add blocker for multi-output custom node --- .../document/node_graph/node_graph_message_handler.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs index 629bb22185..24375a264a 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs @@ -257,7 +257,11 @@ impl<'a> MessageHandler> for NodeG if network_interface.reference(&node_id, selection_network_path).as_ref() == Some(&DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER)) && input_index == graphene_std::vector::fill::FillInput::>::INDEX { - let Some(walk_from) = output_connector.node_id() else { return }; + // Only inspect primary outputs to avoid treating another output of a multi-output custom node as a gradient chain. + let OutputConnector::Node { node_id: walk_from, output_index: 0 } = output_connector else { + return; + }; + let is_gradient_value_connected = network_interface .upstream_flow_back_from_nodes(vec![walk_from], selection_network_path, FlowType::PrimaryFlow) .take_while(|node_id| !network_interface.is_layer(node_id, selection_network_path)) From 6acf3b61b8bd6821f57de13f6b425cc6299eaa62 Mon Sep 17 00:00:00 2001 From: YohYamasaki Date: Wed, 8 Jul 2026 12:16:45 +0900 Subject: [PATCH 5/5] Avoid materializing transform nodes for non-transform gradient updates --- .../tool/tool_messages/gradient_tool.rs | 44 ++++++++++++++++--- 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/editor/src/messages/tool/tool_messages/gradient_tool.rs b/editor/src/messages/tool/tool_messages/gradient_tool.rs index 104dd23e75..8fa42473a0 100644 --- a/editor/src/messages/tool/tool_messages/gradient_tool.rs +++ b/editor/src/messages/tool/tool_messages/gradient_tool.rs @@ -843,6 +843,39 @@ fn dispatch_gradient_chain_writes(layer: LayerNodeIdentifier, gradient: &Gradien }); } +/// Send only the chain write operations for fields that changed from the caller-provided snapshot. +fn dispatch_changed_gradient_chain_writes( + layer: LayerNodeIdentifier, + old_gradient: &GradientStops, + old_appearance: GradientAppearance, + gradient: &GradientStops, + appearance: GradientAppearance, + responses: &mut VecDeque, +) { + if old_gradient != gradient { + responses.add(GraphOperationMessage::GradientStopsSet { layer, stops: gradient.clone() }); + } + + if old_appearance.transform != appearance.transform { + responses.add(GraphOperationMessage::GradientTransformSet { + layer, + transform: appearance.transform, + }); + } + if old_appearance.gradient_type != appearance.gradient_type { + responses.add(GraphOperationMessage::GradientTypeSet { + layer, + gradient_type: appearance.gradient_type, + }); + } + if old_appearance.spread_method != appearance.spread_method { + responses.add(GraphOperationMessage::GradientSpreadMethodSet { + layer, + spread_method: appearance.spread_method, + }); + } +} + impl GradientTool { /// Get the gradient type of the selected gradient (if it exists) pub fn selected_gradient(&self) -> Option { @@ -1333,7 +1366,7 @@ impl Fsm for GradientToolFsmState { let mut transaction_started = false; let mut pending_transform_materialization = None; for layer in document.network_interface.selected_nodes().selected_visible_layers(&document.network_interface) { - let Some((gradient, mut appearance, source)) = resolve_gradient(layer, &document.network_interface) else { + let Some((gradient, appearance, source)) = resolve_gradient(layer, &document.network_interface) else { continue; }; let is_gradient_chain = source == GradientSource::Chain; @@ -1342,10 +1375,6 @@ impl Fsm for GradientToolFsmState { } else { None }; - // if let Some(transform) = transform_to_materialize { - // // GraphOperation is queued later, so it won't affect this PointerDown immediately. Patch the local tool state too. - // appearance.transform = transform; - // } let gradient_space_transform = gradient_space_transform(layer, document); let unit_to_viewport = gradient_space_transform * appearance.transform; let (start, end) = gradient_handle_positions(unit_to_viewport); @@ -1890,11 +1919,14 @@ fn apply_gradient_update( responses.add(DocumentMessage::StartTransaction); transaction_started = true; } + + let old_gradient = gradient.clone(); + let old_appearance = appearance.clone(); update((&mut gradient, &mut appearance)); // Only check for the gradient list once we know we'll write back, since this is a graph traversal per layer if get_upstream_gradient_value_node_id(layer, &context.document.network_interface).is_some() { - dispatch_gradient_chain_writes(layer, &gradient, appearance, responses); + dispatch_changed_gradient_chain_writes(layer, &old_gradient, old_appearance, &gradient, appearance, responses); } else { responses.add(GraphOperationMessage::FillGradientSet { layer,