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..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 @@ -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::*; @@ -248,6 +249,52 @@ impl<'a> MessageHandler> for NodeG return; } network_interface.create_wire(&output_connector, &input_connector, selection_network_path); + + // 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 + { + // 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)) + .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); 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..8fa42473a0 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 { @@ -408,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 @@ -836,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 { @@ -1119,6 +1159,7 @@ impl Fsm for GradientToolFsmState { tool_data.color_picker_editing_color_stop = None; } tool_data.selected_gradient = None; + GradientToolFsmState::Ready { hovering: GradientHoverTarget::None, selected: GradientSelectedTarget::None, @@ -1323,13 +1364,19 @@ 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 { 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 + }; 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 @@ -1361,6 +1408,7 @@ impl Fsm for GradientToolFsmState { initial_gradient: gradient.clone(), is_gradient_chain, }); + pending_transform_materialization = transform_to_materialize.map(|transform| (layer, transform)); break; } @@ -1404,6 +1452,7 @@ impl Fsm for GradientToolFsmState { initial_gradient_transform: appearance.transform, is_gradient_chain, }); + pending_transform_materialization = transform_to_materialize.map(|transform| (layer, transform)); } } @@ -1421,7 +1470,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)); } } } @@ -1490,7 +1540,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 +1566,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)); + + 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; + } let mut selected_gradient = SelectedGradient::new(gradient, appearance, source, layer, document); selected_gradient.dragging = GradientDragTarget::New; @@ -1532,8 +1592,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); @@ -1854,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,