From cf150b5cff34b1196a420f8f7171759eefa8c463 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Tue, 28 Apr 2026 12:55:13 -0700 Subject: [PATCH] Make the Data panel able to display data from selected nodes living in subgraphs (#4064) Enable the Data panel to display data from selected nodes living in subgraphs --- .../data_panel/data_panel_message_handler.rs | 15 ++-- .../document/document_message_handler.rs | 6 ++ .../portfolio/portfolio_message_handler.rs | 27 ++++--- editor/src/node_graph_executor.rs | 20 +++--- editor/src/node_graph_executor/runtime.rs | 72 ++++++++++++++----- 5 files changed, 98 insertions(+), 42 deletions(-) diff --git a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs index e00a87bb..9d0588ae 100644 --- a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs +++ b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs @@ -27,7 +27,9 @@ pub struct DataPanelMessageContext<'a> { /// The data panel allows for graph data to be previewed. #[derive(Default, Debug, Clone, ExtractField)] pub struct DataPanelMessageHandler { - introspected_node: Option, + /// Full path from the root network to the introspected node, with the node itself as the last element. + /// Empty when nothing is being introspected. + introspected_node_path: Vec, introspected_data: Option>, element_path: Vec, active_vector_table_tab: VectorTableTab, @@ -38,12 +40,12 @@ impl MessageHandler> for DataPanel fn process_message(&mut self, message: DataPanelMessage, responses: &mut VecDeque, context: DataPanelMessageContext) { match message { DataPanelMessage::UpdateLayout { mut inspect_result } => { - self.introspected_node = Some(inspect_result.inspect_node); self.introspected_data = inspect_result.take_data(); + self.introspected_node_path = inspect_result.inspect_node_path; self.update_layout(responses, context); } DataPanelMessage::ClearLayout => { - self.introspected_node = None; + self.introspected_node_path.clear(); self.introspected_data = None; self.element_path.clear(); self.active_vector_table_tab = VectorTableTab::default(); @@ -93,8 +95,9 @@ impl DataPanelMessageHandler { let mut widgets = Vec::new(); // Selected layer/node name - if let Some(node_id) = self.introspected_node { - let is_layer = network_interface.is_layer(&node_id, &[]); + if let Some((node_id, parent_path)) = self.introspected_node_path.split_last() { + let node_id = *node_id; + let is_layer = network_interface.is_layer(&node_id, parent_path); widgets.extend([ if is_layer { @@ -103,7 +106,7 @@ impl DataPanelMessageHandler { IconLabel::new("Node").tooltip_description("Name of the selected node.").widget_instance() }, Separator::new(SeparatorStyle::Related).widget_instance(), - TextInput::new(network_interface.display_name(&node_id, &[])) + TextInput::new(network_interface.display_name(&node_id, parent_path)) .tooltip_description(if is_layer { "Name of the selected layer." } else { "Name of the selected node." }) .on_update(move |text_input| { NodeGraphMessage::SetDisplayName { diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 101eecb7..7414c7a5 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -1686,6 +1686,12 @@ impl DocumentMessageHandler { self.network_interface.document_metadata() } + /// Path to the subnetwork that the user's selection is currently scoped to. + /// Empty when the selection lives in the root document network. + pub fn selection_network_path(&self) -> &[NodeId] { + &self.selection_network_path + } + pub fn serialize_document(&self) -> String { let val = serde_json::to_string(self); // We fully expect the serialization to succeed diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 02316404..d4e82c0e 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -1868,22 +1868,31 @@ impl PortfolioMessageHandler { } } - /// Get the ID of the selected node that should be used as the current source for the Data panel. - pub fn node_to_inspect(&self) -> Option { + /// Returns the full path from the root network to the selected node that should drive the Data panel. + /// The last element is the node itself; preceding elements identify the nested subnetwork it lives in + /// so the Data panel can introspect nodes inside subgraphs. An empty `Vec` signals "nothing to inspect". + pub fn node_to_inspect(&self) -> Vec { // Skip if the Data panel is not open if !self.workspace_panel_layout.is_panel_visible(PanelType::Data) || self.workspace_panel_layout.focus_document { - return None; + return Vec::new(); } - let document = self.document(self.active_document_id?)?; - let selected_nodes = document.network_interface.selected_nodes().0; + let Some(document) = self.active_document_id.and_then(|id| self.document(id)) else { + return Vec::new(); + }; + let network_path = document.selection_network_path(); + let Some(selected_nodes) = document.network_interface.selected_nodes_in_nested_network(network_path) else { + return Vec::new(); + }; // Skip if there is not exactly one selected node - if selected_nodes.len() != 1 { - return None; - } + let [node_id] = selected_nodes.0.as_slice() else { + return Vec::new(); + }; - selected_nodes.first().copied() + let mut path = network_path.to_vec(); + path.push(*node_id); + path } /// Remove a dockable panel type from whichever panel group currently contains it. Does not prune empty groups. diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index 619839ff..4252bd38 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -53,7 +53,11 @@ pub struct NodeGraphExecutor { current_execution_id: u64, futures: VecDeque<(u64, ExecutionContext)>, node_graph_hash: u64, - previous_node_to_inspect: Option, + /// Full path from the root document network to the node currently being inspected by the Data panel, or empty if nothing is selected. + /// The last element is the inspect target itself; preceding elements identify the nested subnetwork the node lives in, + /// so the runtime can splice its monitor node alongside the target rather than only at the top level. + /// Tracking the previously-sent value lets `update_node_graph` re-send the network when the inspection target changes. + previous_node_to_inspect: Vec, } #[derive(Debug, Clone)] @@ -75,7 +79,7 @@ impl NodeGraphExecutor { runtime_io: NodeRuntimeIO::with_channels(request_sender, response_receiver), node_graph_hash: 0, current_execution_id: 0, - previous_node_to_inspect: None, + previous_node_to_inspect: Vec::new(), }; (node_runtime, node_executor) } @@ -109,18 +113,18 @@ impl NodeGraphExecutor { let instrumented = Instrumented::new(&mut network); self.runtime_io - .send(GraphRuntimeRequest::GraphUpdate(GraphUpdate { network, node_to_inspect: None })) + .send(GraphRuntimeRequest::GraphUpdate(GraphUpdate { network, node_to_inspect: Vec::new() })) .map_err(|e| e.to_string())?; Ok(instrumented) } /// Update the cached network if necessary. - fn update_node_graph(&mut self, document: &mut DocumentMessageHandler, node_to_inspect: Option, ignore_hash: bool) -> Result<(), String> { + fn update_node_graph(&mut self, document: &mut DocumentMessageHandler, node_to_inspect: Vec, ignore_hash: bool) -> Result<(), String> { let network_hash = document.network_interface.network_hash(); // Refresh the graph when it changes or the inspect node changes if network_hash != self.node_graph_hash || self.previous_node_to_inspect != node_to_inspect || ignore_hash { let network = document.network_interface.document_network().clone(); - self.previous_node_to_inspect = node_to_inspect; + self.previous_node_to_inspect.clone_from(&node_to_inspect); self.node_graph_hash = network_hash; self.runtime_io @@ -174,7 +178,7 @@ impl NodeGraphExecutor { viewport_resolution: UVec2, viewport_scale: f64, time: TimingInformation, - node_to_inspect: Option, + node_to_inspect: Vec, ignore_hash: bool, pointer: DVec2, ) -> Result { @@ -271,7 +275,7 @@ impl NodeGraphExecutor { // Execute the node graph self.runtime_io - .send(GraphRuntimeRequest::GraphUpdate(GraphUpdate { network, node_to_inspect: None })) + .send(GraphRuntimeRequest::GraphUpdate(GraphUpdate { network, node_to_inspect: Vec::new() })) .map_err(|e| e.to_string())?; let execution_id = self.queue_execution(render_config); self.futures.push_back(( @@ -338,7 +342,7 @@ impl NodeGraphExecutor { }); // Update the Data panel on the frontend using the value of the inspect result. - if let Some(inspect_result) = (self.previous_node_to_inspect.is_some()).then_some(inspect_result).flatten() { + if let Some(inspect_result) = (!self.previous_node_to_inspect.is_empty()).then_some(inspect_result).flatten() { responses.add(DataPanelMessage::UpdateLayout { inspect_result }); } else { responses.add(DataPanelMessage::ClearLayout); diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index ecb155e6..5a08a22b 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -76,8 +76,10 @@ pub enum GraphRuntimeRequest { #[derive(Debug, serde::Serialize, serde::Deserialize)] pub struct GraphUpdate { pub(super) network: NodeNetwork, - /// The node that should be temporary inspected during execution - pub(super) node_to_inspect: Option, + /// Full path from the root network to the node that should be temporarily inspected during execution. + /// The last element is the inspect target; preceding elements identify the nested subnetwork it lives in, + /// so the runtime can splice its monitor node alongside the target instead of only at the top level. + pub(super) node_to_inspect: Vec, } #[derive(Default, Debug, Clone, serde::Serialize, serde::Deserialize)] @@ -235,7 +237,7 @@ impl NodeRuntime { } GraphRuntimeRequest::GraphUpdate(GraphUpdate { mut network, node_to_inspect }) => { // Insert the monitor node to manage the inspection - self.inspect_state = node_to_inspect.map(|inspect| InspectState::monitor_inspect_node(&mut network, inspect)); + self.inspect_state = InspectState::monitor_inspect_node(&mut network, &node_to_inspect); self.old_graph = Some(network.clone()); @@ -264,7 +266,7 @@ impl NodeRuntime { self.update_thumbnails = false; // Resolve the result from the inspection by accessing the monitor node - let inspect_result = self.inspect_state.and_then(|state| state.access(&self.executor)); + let inspect_result = self.inspect_state.as_ref().and_then(|state| state.access(&self.executor)); let (result, texture) = match result { Ok(TaggedValue::RenderOutput(RenderOutput { @@ -408,7 +410,11 @@ impl NodeRuntime { for monitor_node_path in &self.monitor_nodes { // Skip the inspect monitor node - if self.inspect_state.is_some_and(|inspect_state| monitor_node_path.last().copied() == Some(inspect_state.monitor_node)) { + if self + .inspect_state + .as_ref() + .is_some_and(|inspect_state| monitor_node_path.last().copied() == Some(inspect_state.monitor_node)) + { continue; } @@ -540,16 +546,22 @@ pub async fn replace_application_io(application_io: PlatformApplicationIo) { } /// Which node is inspected and which monitor node is used (if any) for the current execution -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone)] struct InspectState { inspect_node: NodeId, monitor_node: NodeId, + /// Path of the subnetwork the monitor was inserted into (i.e., the parent of `inspect_node`). + /// Used to construct the full node path when introspecting the monitor's value. + monitor_parent_path: Vec, } /// The resulting value from the temporary inspected during execution #[derive(Clone, Debug, Default)] pub struct InspectResult { introspected_data: Option>, - pub inspect_node: NodeId, + /// Full path from the root network to the inspected node, with the node itself as the last element. + /// The parent slice (`split_last().1`) is the network the node lives in, which downstream consumers + /// (e.g. the Data panel) need when looking the node up via `network_interface.is_layer(...)` etc. + pub inspect_node_path: Vec, } impl InspectResult { @@ -561,17 +573,21 @@ impl InspectResult { // This is very ugly but is required to be inside a message impl PartialEq for InspectResult { fn eq(&self, other: &Self) -> bool { - self.inspect_node == other.inspect_node + self.inspect_node_path == other.inspect_node_path } } impl InspectState { - /// Insert the monitor node to manage the inspection - pub fn monitor_inspect_node(network: &mut NodeNetwork, inspect_node: NodeId) -> Self { + /// Insert the monitor node alongside the inspect node identified by `inspect_path` (full path from root, last element is the target). + /// Returns `None` if the path is empty or doesn't resolve to a node inside a reachable subnetwork. + pub fn monitor_inspect_node(network: &mut NodeNetwork, inspect_path: &[NodeId]) -> Option { + let (inspect_node, parent_path) = inspect_path.split_last()?; + let inspect_node = *inspect_node; + let target_network = navigate_to_network_mut(network, parent_path)?; let monitor_id = NodeId::new(); // It is necessary to replace the inputs before inserting the monitor node to avoid changing the input of the new monitor node - for input in network.nodes.values_mut().flat_map(|node| node.inputs.iter_mut()).chain(&mut network.exports) { + for input in target_network.nodes.values_mut().flat_map(|node| node.inputs.iter_mut()).chain(&mut target_network.exports) { let NodeInput::Node { node_id, output_index, .. } = input else { continue }; // We only care about the primary output of our inspect node if *output_index != 0 || *node_id != inspect_node { @@ -588,21 +604,39 @@ impl InspectState { skip_deduplication: true, ..Default::default() }; - network.nodes.insert(monitor_id, monitor_node); + target_network.nodes.insert(monitor_id, monitor_node); - Self { + Some(Self { inspect_node, monitor_node: monitor_id, - } + monitor_parent_path: parent_path.to_vec(), + }) } /// Resolve the result from the inspection by accessing the monitor node fn access(&self, executor: &DynamicExecutor) -> Option { - let introspected_data = executor.introspect(&[self.monitor_node]).inspect_err(|e| warn!("Failed to introspect monitor node {e}")).ok(); + // The executor's source map indexes by full path from root, so prepend the subnetwork path to the monitor ID. + let mut monitor_path = self.monitor_parent_path.clone(); + monitor_path.push(self.monitor_node); + let introspected_data = executor.introspect(&monitor_path).inspect_err(|e| warn!("Failed to introspect monitor node {e}")).ok(); // TODO: Consider displaying the error instead of ignoring it - Some(InspectResult { - inspect_node: self.inspect_node, - introspected_data, - }) + let mut inspect_node_path = self.monitor_parent_path.clone(); + inspect_node_path.push(self.inspect_node); + Some(InspectResult { inspect_node_path, introspected_data }) } } + +/// Walks `network` down through `path`, returning a mutable reference to the nested `NodeNetwork` +/// at the end. Each path element must name a `DocumentNode` whose implementation is `Network(...)`. +/// Returns `None` if any step is missing or doesn't refer to a subnetwork. +fn navigate_to_network_mut<'a>(network: &'a mut NodeNetwork, path: &[NodeId]) -> Option<&'a mut NodeNetwork> { + let mut current = network; + for node_id in path { + let node = current.nodes.get_mut(node_id)?; + current = match &mut node.implementation { + DocumentNodeImplementation::Network(nested) => nested, + _ => return None, + }; + } + Some(current) +}