Skip to content

Commit 2c29d2c

Browse files
authored
Fix Gradient tool inserting new nodes in the wrong position when updating a Fill node's gradient chain (#4203)
* Fix Gradient tool inserting new nodes in the wrong position when updating a Fill node's gradient chain * Improve robustness
1 parent a3b62da commit 2c29d2c

3 files changed

Lines changed: 136 additions & 5 deletions

File tree

editor/src/messages/portfolio/document/graph_operation/utility_types.rs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -362,14 +362,11 @@ impl<'a> ModifyInputsContext<'a> {
362362
return None;
363363
}
364364

365-
// Splice new node between target_input and its current upstream
365+
// Splice a new node onto the wire feeding `target_input`, positioning it sensibly within the chain.
366366
let node_definition = resolve_proto_node_type(reference)?;
367-
let current_input = self.network_interface.input_from_connector(target_input, &[])?.clone();
368-
369367
let node_id = NodeId::new();
370368
self.network_interface.insert_node(node_id, node_definition.default_node_template(), &[]);
371-
self.network_interface.set_input(&InputConnector::node(node_id, 0), current_input, &[]);
372-
self.network_interface.set_input(target_input, NodeInput::node(node_id, 0), &[]);
369+
self.network_interface.insert_node_before_input(&node_id, target_input, &[]);
373370

374371
Some(node_id)
375372
}

editor/src/messages/portfolio/document/utility_types/network_interface.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5911,6 +5911,46 @@ impl NodeNetworkInterface {
59115911
self.create_wire(&upstream_output, &InputConnector::node(*node_id, insert_node_input_index), network_path);
59125912
}
59135913

5914+
/// Inserts the freshly-created `node_id` onto the wire feeding `input_connector`: the previous upstream becomes the
5915+
/// new node's primary (index 0) input, and the new node feeds `input_connector`.
5916+
///
5917+
/// When the wire is part of a layer's encapsulated primary chain, `set_input` chain-positions the new node
5918+
/// automatically. On an unencapsulated secondary-input branch (e.g. a 'Fill' node's fill input) chain positioning
5919+
/// doesn't apply, so the node would otherwise land at the graph origin; instead it's placed on the displaced
5920+
/// upstream node's spot and that whole branch is shifted left (in absolute graph space) to make room.
5921+
pub fn insert_node_before_input(&mut self, node_id: &NodeId, input_connector: &InputConnector, network_path: &[NodeId]) {
5922+
let feeder = self.upstream_output_connector(input_connector, network_path).and_then(|output| output.node_id());
5923+
5924+
let Some(current_input) = self.input_from_connector(input_connector, network_path).cloned() else {
5925+
log::error!("Could not get input in insert_node_before_input");
5926+
return;
5927+
};
5928+
5929+
if self.input_from_connector(&InputConnector::node(*node_id, 0), network_path).is_none() {
5930+
return;
5931+
}
5932+
5933+
self.set_input(&InputConnector::node(*node_id, 0), current_input, network_path);
5934+
self.set_input(input_connector, NodeInput::node(*node_id, 0), network_path);
5935+
5936+
// If `set_input` chain-positioned the node (it joined a layer chain), there's nothing more to do.
5937+
if !self.is_absolute(node_id, network_path) {
5938+
return;
5939+
}
5940+
5941+
// Otherwise place the node where the displaced feeder was, then shift the feeder's branch left to make room.
5942+
let Some(feeder) = feeder else { return };
5943+
let Some(node_position) = self.position(node_id, network_path) else { return };
5944+
let Some(feeder_position) = self.position(&feeder, network_path) else { return };
5945+
5946+
self.shift_node(node_id, feeder_position - node_position, network_path);
5947+
// Deduplicate, since `UpstreamFlow` can yield a shared node more than once and we must shift each node only once.
5948+
let upstream_nodes: HashSet<NodeId> = self.upstream_flow_back_from_nodes(vec![feeder], network_path, FlowType::UpstreamFlow).collect();
5949+
for upstream_node in &upstream_nodes {
5950+
self.shift_node(upstream_node, IVec2::new(-NODE_CHAIN_WIDTH, 0), network_path);
5951+
}
5952+
}
5953+
59145954
/// Moves a node to the start of a layer chain (feeding into the secondary input of the layer).
59155955
/// When `import` is true, uses lightweight wiring that skips `is_acyclic` checks and per-node cache invalidation.
59165956
pub fn move_node_to_chain_start(&mut self, node_id: &NodeId, parent: LayerNodeIdentifier, network_path: &[NodeId], import: bool) {

editor/src/messages/tool/tool_messages/gradient_tool.rs

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2502,4 +2502,98 @@ mod test_gradient {
25022502
assert_eq!(SRGBA8::from(updated.stops.color[1]), SRGBA8::from(Color::GREEN), "Middle stop color should be preserved");
25032503
assert_eq!(SRGBA8::from(updated.stops.color[2]), SRGBA8::from(Color::BLUE), "Last stop color should be preserved");
25042504
}
2505+
2506+
// When the gradient chain feeds a 'Fill' node's secondary input it's an unencapsulated side-branch (no layer
2507+
// background to lay it out), so a node inserted there must be placed onto the displaced feeder's spot in absolute
2508+
// graph space rather than stranded at the origin.
2509+
#[tokio::test]
2510+
async fn gradient_chain_node_on_fill_secondary_input_takes_feeder_slot() {
2511+
use graphene_std::vector::style::GradientSpreadMethod;
2512+
2513+
let mut editor = EditorTestUtils::create();
2514+
editor.new_document().await;
2515+
editor.drag_tool(ToolType::Ellipse, 0., 0., 100., 100., ModifierKeys::empty()).await;
2516+
let layer = editor.active_document().metadata().all_layers().next().unwrap();
2517+
2518+
// Find the 'Fill' node in the layer's primary chain.
2519+
let fill_reference = DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER);
2520+
let fill_node_id = {
2521+
let network_interface = &editor.active_document().network_interface;
2522+
network_interface
2523+
.document_network()
2524+
.nodes
2525+
.keys()
2526+
.copied()
2527+
.find(|node_id| network_interface.reference(node_id, &[]).as_ref() == Some(&fill_reference))
2528+
.expect("Fill node should exist")
2529+
};
2530+
2531+
// Feed a 'Gradient Value' node into the Fill node's secondary (fill) input.
2532+
let gradient_value_id = editor.create_node_by_name(DefinitionIdentifier::ProtoNode(graphene_std::math_nodes::gradient_value::IDENTIFIER)).await;
2533+
editor
2534+
.handle_message(NodeGraphMessage::CreateWire {
2535+
output_connector: OutputConnector::node(gradient_value_id, 0),
2536+
input_connector: InputConnector::node(fill_node_id, 1),
2537+
})
2538+
.await;
2539+
editor
2540+
.handle_message(NodeGraphMessage::SetInputValue {
2541+
node_id: gradient_value_id,
2542+
input_index: 1,
2543+
value: TaggedValue::Gradient(GradientStops::new([
2544+
GradientStop {
2545+
position: 0.,
2546+
midpoint: 0.5,
2547+
color: Color::RED,
2548+
},
2549+
GradientStop {
2550+
position: 1.,
2551+
midpoint: 0.5,
2552+
color: Color::BLUE,
2553+
},
2554+
])),
2555+
})
2556+
.await;
2557+
2558+
// Move the feeder off the origin so its slot is unambiguous, then record where it sits.
2559+
editor
2560+
.handle_message(NodeGraphMessage::ShiftNodePosition {
2561+
node_id: gradient_value_id,
2562+
x: 4,
2563+
y: 6,
2564+
})
2565+
.await;
2566+
let feeder_position = editor.active_document_mut().network_interface.position(&gradient_value_id, &[]).expect("Gradient Value position");
2567+
2568+
// Set the spread method through the tool, which splices a 'Spread Method' node onto the Fill's fill input wire.
2569+
editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] }).await;
2570+
editor.select_tool(ToolType::Gradient).await;
2571+
editor
2572+
.handle_message(GradientToolMessage::UpdateOptions {
2573+
options: GradientOptionsUpdate::SetSpreadMethod(GradientSpreadMethod::Reflect),
2574+
})
2575+
.await;
2576+
2577+
let spread_reference = DefinitionIdentifier::ProtoNode(graphene_std::math_nodes::spread_method::IDENTIFIER);
2578+
let spread_node_id = {
2579+
let network_interface = &editor.active_document().network_interface;
2580+
network_interface
2581+
.document_network()
2582+
.nodes
2583+
.keys()
2584+
.copied()
2585+
.find(|node_id| network_interface.reference(node_id, &[]).as_ref() == Some(&spread_reference))
2586+
.expect("Spread Method node should have been inserted")
2587+
};
2588+
2589+
let spread_position = editor.active_document_mut().network_interface.position(&spread_node_id, &[]).expect("Spread Method position");
2590+
let feeder_position_after = editor.active_document_mut().network_interface.position(&gradient_value_id, &[]).expect("Gradient Value position after");
2591+
2592+
assert_eq!(spread_position, feeder_position, "the inserted node should occupy the feeder's former slot, not the graph origin");
2593+
assert_eq!(
2594+
feeder_position_after,
2595+
feeder_position - glam::IVec2::new(crate::consts::NODE_CHAIN_WIDTH, 0),
2596+
"the feeder's branch should shift one chain-width left to make room"
2597+
);
2598+
}
25052599
}

0 commit comments

Comments
 (0)