Skip to content

Commit 5b00893

Browse files
authored
Make the node graph's "Merge Selected Nodes" target the right-clicked node, and fix it crashing (#4252)
1 parent 9a9436c commit 5b00893

3 files changed

Lines changed: 52 additions & 3 deletions

File tree

editor/src/messages/portfolio/document/document_message_handler.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3908,6 +3908,37 @@ mod document_message_handler_tests {
39083908
editor.handle_message(DocumentMessage::CreateEmptyFolder).await;
39093909
assert!(true, "Application didn't crash after folder move operation");
39103910
}
3911+
3912+
// Merging nodes whose output isn't wired downstream produces an encapsulating subnetwork with no exports.
3913+
// Inspecting it via the Data panel (which splices in a monitor node) must not leave a dangling reference that crashes compilation.
3914+
#[tokio::test]
3915+
async fn merge_selected_nodes_while_inspecting_does_not_crash() {
3916+
let mut editor = EditorTestUtils::create();
3917+
editor.new_document().await;
3918+
editor.draw_rect(0., 0., 100., 100.).await;
3919+
3920+
let node_a = editor.create_node_by_name(DefinitionIdentifier::ProtoNode(graphene_std::transform_nodes::transform::IDENTIFIER)).await;
3921+
let node_b = editor.create_node_by_name(DefinitionIdentifier::ProtoNode(graphene_std::transform_nodes::transform::IDENTIFIER)).await;
3922+
editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![node_a, node_b] }).await;
3923+
editor.handle_message(NodeGraphMessage::MergeSelectedNodes).await;
3924+
3925+
let merged = editor.active_document().network_interface.selected_nodes_in_nested_network(&[]).unwrap().0.clone();
3926+
assert_eq!(merged.len(), 1, "merge should leave one encapsulating node selected");
3927+
3928+
// Simulate the Data panel inspecting the merged node, which compiles the graph with a monitor node spliced in
3929+
let portfolio = &mut editor.editor.dispatcher.message_handlers.portfolio_message_handler;
3930+
let document_id = portfolio.active_document_id.unwrap();
3931+
let document = portfolio.documents.get_mut(&document_id).unwrap();
3932+
portfolio
3933+
.executor
3934+
.submit_node_graph_evaluation(document, document_id, glam::UVec2::ONE, 1., Default::default(), merged, true, DVec2::ZERO)
3935+
.unwrap();
3936+
editor.runtime.run().await;
3937+
3938+
let mut messages = VecDeque::new();
3939+
editor.editor.poll_node_graph_evaluation(&mut messages).unwrap();
3940+
}
3941+
39113942
#[tokio::test]
39123943
async fn test_moving_folder_with_children() {
39133944
let mut editor = EditorTestUtils::create();

editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -838,11 +838,17 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
838838
let currently_is_node = !network_interface.is_layer(&node_id, breadcrumb_network_path);
839839
let can_be_layer = network_interface.is_eligible_to_be_layer(&node_id, breadcrumb_network_path);
840840

841+
let selected_nodes = network_interface.selected_nodes_in_nested_network(selection_network_path);
842+
let is_clicked_selected = selected_nodes.as_ref().is_some_and(|selected| selected.selected_nodes().any(|id| *id == node_id));
843+
844+
// Right-clicking an unselected node selects just it, so selection-based actions like Merge Selected Nodes target it
845+
if !is_clicked_selected {
846+
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![node_id] });
847+
}
848+
841849
// Determine which layers the Lock/Unlock action would affect:
842850
// - If the right-clicked node is in the selection, it affects all selected layers
843851
// - If the right-clicked node is not in the selection, it affects just the right-clicked node
844-
let selected_nodes = network_interface.selected_nodes_in_nested_network(selection_network_path);
845-
let is_clicked_selected = selected_nodes.as_ref().is_some_and(|selected| selected.selected_nodes().any(|id| *id == node_id));
846852
let affected_layer_ids = if is_clicked_selected {
847853
selected_nodes.map(|selected| selected.selected_nodes().copied().filter(|id| network_interface.is_layer(id, selection_network_path)).collect())
848854
} else {

editor/src/node_graph_executor/runtime.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -609,11 +609,23 @@ impl PartialEq for InspectResult {
609609

610610
impl InspectState {
611611
/// Insert the monitor node alongside the inspect node identified by `inspect_path` (full path from root, last element is the target).
612-
/// Returns `None` if the path is empty or doesn't resolve to a node inside a reachable subnetwork.
612+
/// Returns `None` if the path is empty, doesn't resolve to a node inside a reachable subnetwork, or the target has no
613+
/// flatten-safe primary output to monitor (e.g. an empty merged subnetwork), which would otherwise leave the monitor's
614+
/// input dangling once the subnetwork is flattened away.
613615
pub fn monitor_inspect_node(network: &mut NodeNetwork, inspect_path: &[NodeId]) -> Option<Self> {
614616
let (inspect_node, parent_path) = inspect_path.split_last()?;
615617
let inspect_node = *inspect_node;
616618
let target_network = navigate_to_network_mut(network, parent_path)?;
619+
620+
// A subnetwork's primary output only survives flattening if its first export is a node
621+
let monitorable = match &target_network.nodes.get(&inspect_node)?.implementation {
622+
DocumentNodeImplementation::Network(inner) => matches!(inner.exports.first(), Some(NodeInput::Node { .. })),
623+
_ => true,
624+
};
625+
if !monitorable {
626+
return None;
627+
}
628+
617629
let monitor_id = NodeId::new();
618630

619631
// It is necessary to replace the inputs before inserting the monitor node to avoid changing the input of the new monitor node

0 commit comments

Comments
 (0)