use super::*; use crate::messages::frontend::utility_types::{ExportBounds, FileType}; use glam::{DAffine2, DVec2, UVec2}; use graph_craft::application_io::resource::ResourceRegistry; use graph_craft::application_io::{PlatformApplicationIo, PlatformEditorApi}; use graph_craft::document::value::{RenderOutput, RenderOutputType, TaggedValue}; use graph_craft::document::{NodeId, NodeNetwork}; use graph_craft::graphene_compiler::Compiler; use graph_craft::proto::GraphErrors; use graphene_std::application_io::{ApplicationIo, ExportFormat, NodeGraphUpdateMessage, NodeGraphUpdateSender, RenderConfig, Texture}; use graphene_std::bounds::RenderBoundingBox; use graphene_std::core_types::gpoll::GPoll; use graphene_std::ops::ConvertAsync; #[cfg(all(target_family = "wasm", feature = "gpu", feature = "wasm"))] use graphene_std::platform_application_io::canvas_utils::{Canvas, CanvasSurface, CanvasSurfaceHandle}; use graphene_std::raster_types::Raster; use graphene_std::renderer::{Render, RenderParams, RenderSvgSegmentList, SvgRender, SvgSegment}; use graphene_std::runtime::{DynGraphRuntime, DynNotifier, DynSpawner, GraphRuntime, RuntimeHandle, SourceFuture, Spawner, poll_once}; use graphene_std::transform::RenderQuality; use graphene_std::vector::Vector; use graphene_std::vector::style::RenderMode; use graphene_std::{Artboard, Graphic}; use interpreted_executor::dynamic_executor::{DynamicExecutor, ResolvedDocumentNodeTypesDelta}; use interpreted_executor::util::wrap_network_in_scope; use spin::Mutex; use std::sync::Arc; use std::sync::mpsc::{Receiver, Sender}; /// Persistent data between graph executions. It's updated via message passing from the editor thread with [`GraphRuntimeRequest`]`. /// Some of these fields are put into a [`PlatformEditorApi`] which is passed to the final compiled graph network upon each execution. /// Once the implementation is finished, this will live in a separate thread. Right now it's part of the main JS thread, but its own separate JS stack frame independent from the editor. pub struct NodeRuntime { #[cfg(test)] pub(super) executor: DynamicExecutor, #[cfg(not(test))] executor: DynamicExecutor, receiver: Receiver, sender: InternalNodeGraphUpdateSender, editor_preferences: EditorPreferences, old_graph: Option, update_thumbnails: bool, graph_runtime: Arc, /// The last plain render request, replayed when an async source completion marks the graph dirty. last_render: Option, editor_api: Arc, resources: ResourceRegistry, node_graph_errors: GraphErrors, monitor_nodes: Vec>, /// Which node is inspected and which monitor node is used (if any) for the current execution. inspect_state: Option, preprocessor: preprocessor::Preprocessor, // TODO: Remove, it doesn't need to be persisted anymore /// The current renders of the thumbnails for layer nodes. thumbnail_renders: HashMap>, vector_modify: HashMap, /// Cached surface for Wasm viewport rendering (reused across frames) #[cfg(all(target_family = "wasm", feature = "gpu", feature = "wasm"))] wasm_canvas_cache: CanvasSurfaceHandle, /// Currently displayed texture, the runtime keeps a reference to it to avoid the texture getting destroyed while it is still in use. #[cfg(all(target_family = "wasm", feature = "gpu", feature = "wasm"))] current_viewport_texture: Option, } /// Messages passed from the editor thread to the node runtime thread. #[derive(Debug, serde::Serialize, serde::Deserialize)] pub enum GraphRuntimeRequest { GraphUpdate(GraphUpdate), ExecutionRequest(ExecutionRequest), EditorPreferencesUpdate(EditorPreferences), } #[derive(Debug, serde::Serialize, serde::Deserialize)] pub struct GraphUpdate { pub(super) network: NodeNetwork, pub(super) resources: ResourceRegistry, /// 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)] pub struct ExportConfig { pub name: String, pub file_type: FileType, pub scale_factor: f64, pub bounds: ExportBounds, pub size: UVec2, pub artboard_name: Option, pub artboard_count: usize, } #[derive(Clone)] struct InternalNodeGraphUpdateSender(Sender); impl InternalNodeGraphUpdateSender { fn send_compilation_response(&self, response: CompilationResponse) { self.0.send(NodeGraphUpdate::CompilationResponse(response)).expect("Failed to send response") } fn send_execution_response(&self, response: ExecutionResponse) { self.0.send(NodeGraphUpdate::ExecutionResponse(response)).expect("Failed to send response") } fn send_eyedropper_preview(&self, raster: Raster) { self.0.send(NodeGraphUpdate::EyedropperPreview(raster)).expect("Failed to send response") } } impl NodeGraphUpdateSender for InternalNodeGraphUpdateSender { fn send(&self, message: NodeGraphUpdateMessage) { self.0.send(NodeGraphUpdate::NodeGraphUpdateMessage(message)).expect("Failed to send response") } } // TODO: Replace with `core::cell::LazyCell` () or similar pub static NODE_RUNTIME: once_cell::sync::Lazy>> = once_cell::sync::Lazy::new(|| Mutex::new(None)); #[cfg(not(target_family = "wasm"))] pub struct TokioSpawner(Option); #[cfg(not(target_family = "wasm"))] impl TokioSpawner { pub fn new() -> Self { Self(Some(tokio::runtime::Runtime::new().expect("Failed to start the async source runtime"))) } } #[cfg(not(target_family = "wasm"))] impl Default for TokioSpawner { fn default() -> Self { Self::new() } } #[cfg(not(target_family = "wasm"))] impl Spawner for TokioSpawner { fn spawn(&self, mut task: SourceFuture) -> bool { let runtime = self.0.as_ref().expect("runtime lives until drop"); let _guard = runtime.enter(); if poll_once(&mut task) { return true; } runtime.spawn(task); false } } /// Dropping a tokio runtime blocks on its tasks, which panics inside an async context; the tests drop /// [`NodeRuntime`] from one, so shut down in the background instead. #[cfg(not(target_family = "wasm"))] impl Drop for TokioSpawner { fn drop(&mut self) { if let Some(runtime) = self.0.take() { runtime.shutdown_background(); } } } #[cfg(target_family = "wasm")] pub struct WasmSpawner; #[cfg(target_family = "wasm")] impl Spawner for WasmSpawner { fn spawn(&self, mut task: SourceFuture) -> bool { if poll_once(&mut task) { return true; } wasm_bindgen_futures::spawn_local(task); false } } impl NodeRuntime { pub fn new(receiver: Receiver, sender: Sender) -> Self { #[cfg(not(target_family = "wasm"))] let spawner: Box = Box::new(TokioSpawner::new()); #[cfg(target_family = "wasm")] let spawner: Box = Box::new(WasmSpawner); let graph_runtime: Arc = Arc::new(GraphRuntime::new(spawner)); let mut executor = DynamicExecutor::default(); executor.set_runtime(Arc::clone(&graph_runtime)); Self { executor, receiver, sender: InternalNodeGraphUpdateSender(sender.clone()), editor_preferences: EditorPreferences::default(), old_graph: None, resources: ResourceRegistry::default(), update_thumbnails: true, graph_runtime: Arc::clone(&graph_runtime), last_render: None, editor_api: PlatformEditorApi { editor_preferences: Box::new(EditorPreferences::default()), node_graph_message_sender: Box::new(InternalNodeGraphUpdateSender(sender)), runtime: RuntimeHandle(graph_runtime), #[cfg(not(test))] application_io: None, #[cfg(test)] application_io: Some(PlatformApplicationIo::default().into()), } .into(), node_graph_errors: Vec::new(), monitor_nodes: Vec::new(), preprocessor: preprocessor::Preprocessor::new(), thumbnail_renders: Default::default(), vector_modify: Default::default(), inspect_state: None, #[cfg(all(target_family = "wasm", feature = "gpu"))] wasm_canvas_cache: CanvasSurfaceHandle::new(), #[cfg(all(target_family = "wasm", feature = "gpu"))] current_viewport_texture: None, } } #[cfg(test)] pub fn take_dirty(&self) -> bool { self.executor.take_dirty() } pub async fn run(&mut self) -> Option { let mut preferences = None; let mut graph = None; let mut eyedropper = None; let mut execution = None; for request in self.receiver.try_iter() { match request { GraphRuntimeRequest::GraphUpdate(_) => graph = Some(request), GraphRuntimeRequest::ExecutionRequest(ref execution_request) => { if execution_request.render_config.for_eyedropper { eyedropper = Some(request); continue; } let for_export = execution_request.render_config.for_export; if !for_export { self.last_render = Some(execution_request.clone()); } execution = Some(request); // If we get an export request we always execute it immedeatly otherwise it could get deduplicated if for_export { break; } } GraphRuntimeRequest::EditorPreferencesUpdate(_) => preferences = Some(request), } } // Eydropper should use the same time and pointer to not invalidate the cache if let Some(GraphRuntimeRequest::ExecutionRequest(eyedropper)) = &mut eyedropper && let Some(GraphRuntimeRequest::ExecutionRequest(execution)) = &execution { eyedropper.render_config.time = execution.render_config.time; eyedropper.render_config.pointer = execution.render_config.pointer; } if self.executor.take_dirty() && execution.is_none() { execution = self.last_render.clone().map(GraphRuntimeRequest::ExecutionRequest); } let requests = [preferences, graph, eyedropper, execution].into_iter().flatten(); for request in requests { match request { GraphRuntimeRequest::EditorPreferencesUpdate(preferences) => { self.editor_preferences = preferences.clone(); self.editor_api = PlatformEditorApi { application_io: self.editor_api.application_io.clone(), node_graph_message_sender: Box::new(self.sender.clone()), editor_preferences: Box::new(preferences), runtime: self.editor_api.runtime.clone(), } .into(); if let Some(graph) = self.old_graph.clone() { // We ignore this result as compilation errors should have been reported in an earlier iteration let _ = self.update_network(graph); } } GraphRuntimeRequest::GraphUpdate(GraphUpdate { mut network, resources, node_to_inspect, }) => { // Insert the monitor node to manage the inspection self.inspect_state = InspectState::monitor_inspect_node(&mut network, &node_to_inspect); self.old_graph = Some(network.clone()); self.resources = resources; self.node_graph_errors.clear(); let result = self.update_network(network); let node_graph_errors = self.node_graph_errors.clone(); self.update_thumbnails = true; self.sender.send_compilation_response(CompilationResponse { result, node_graph_errors }); } GraphRuntimeRequest::ExecutionRequest(ExecutionRequest { execution_id, mut render_config, .. }) => { // We may want to render via the SVG pipeline even though raster was requested, if SVG Preview render mode is active or WebGPU/Vello is unavailable if render_config.export_format == ExportFormat::Raster && (render_config.render_mode == RenderMode::SvgPreview || self.editor_api.application_io.as_ref().unwrap().gpu_executor().is_none()) { render_config.export_format = ExportFormat::Svg; } let result = self.execute_network(render_config); let mut responses = VecDeque::new(); // TODO: Only process monitor nodes if the graph has changed, not when only the Footprint changes if !render_config.for_eyedropper { self.process_monitor_nodes(&mut responses, self.update_thumbnails); } self.update_thumbnails = false; // Resolve the result from the inspection by accessing the monitor node let inspect_result = self.inspect_state.as_ref().and_then(|state| state.access(&self.executor)); let (result, texture) = match result { Ok(TaggedValue::RenderOutput(RenderOutput { data: RenderOutputType::Texture(texture), metadata, })) if render_config.for_export => { let executor = self .editor_api .application_io .as_ref() .unwrap() .gpu_executor_arc() .expect("GPU executor should be available when we receive a texture"); let raster_cpu = Raster::new_gpu(texture).convert(Footprint::BOUNDLESS, wgpu_executor::WgpuExecutorHandle(executor)).await; let (data, width, height) = raster_cpu.to_flat_u8(); ( Ok(TaggedValue::RenderOutput(RenderOutput { data: RenderOutputType::Buffer { data, width, height }, metadata, })), None, ) } Ok(TaggedValue::RenderOutput(RenderOutput { data: RenderOutputType::Texture(texture), metadata: _, })) if render_config.for_eyedropper => { let executor = self .editor_api .application_io .as_ref() .unwrap() .gpu_executor_arc() .expect("GPU executor should be available when we receive a texture"); let raster_cpu = Raster::new_gpu(texture).convert(Footprint::BOUNDLESS, wgpu_executor::WgpuExecutorHandle(executor)).await; self.sender.send_eyedropper_preview(raster_cpu); continue; } // Eyedropper render that didn't produce a texture (e.g., SVG fallback when GPU is unavailable); discard it _ if render_config.for_eyedropper => { continue; } #[cfg(all(target_family = "wasm", feature = "gpu"))] Ok(TaggedValue::RenderOutput(RenderOutput { data: RenderOutputType::Texture(texture), metadata, })) if !render_config.for_export => { self.current_viewport_texture = Some(texture.clone()); let app_io = self.editor_api.application_io.as_ref().unwrap(); let executor = app_io.gpu_executor().expect("GPU executor should be available when we receive a texture"); self.wasm_canvas_cache.present(&texture, executor); let logical_resolution = render_config.viewport.resolution.as_dvec2() / render_config.scale; ( Ok(TaggedValue::RenderOutput(RenderOutput { data: RenderOutputType::CanvasFrame { canvas_id: self.wasm_canvas_cache.id(), resolution: logical_resolution, }, metadata, })), None, ) } Ok(TaggedValue::RenderOutput(RenderOutput { data: RenderOutputType::Texture(texture), metadata, })) => ( Ok(TaggedValue::RenderOutput(RenderOutput { data: RenderOutputType::Texture(texture.clone()), metadata, })), Some(texture), ), r => (r, None), }; self.sender.send_execution_response(ExecutionResponse { execution_id, result, responses, vector_modify: self.vector_modify.clone(), inspect_result, }); return texture; } } } None } fn update_network(&mut self, graph: NodeNetwork) -> Result { let mut scoped_network = wrap_network_in_scope(graph, self.editor_api.clone()); if let Err(e) = self.preprocessor.preprocess(&mut scoped_network, &|resource_id| self.resources.hash(&resource_id)) { return Err((ResolvedDocumentNodeTypesDelta::default(), e.to_string())); } // We assume only one output assert_eq!(scoped_network.exports.len(), 1, "Graph with multiple outputs not yet handled"); let c = Compiler {}; let proto_network = match c.compile_single(scoped_network, &interpreted_executor::node_registry::NODE_REGISTRY) { Ok(network) => network, Err(e) => return Err((ResolvedDocumentNodeTypesDelta::default(), e)), }; self.monitor_nodes = proto_network .nodes .iter() .filter(|(_, node)| node.identifier == graphene_std::memo::monitor::IDENTIFIER) .map(|(_, node)| node.original_location.path.clone().unwrap_or_default()) .collect::>(); assert_ne!(proto_network.nodes.len(), 0, "No proto nodes exist?"); self.executor.update(proto_network).map_err(|(types, e)| { self.node_graph_errors.clone_from(&e); (types, format!("{e:?}")) }) } fn execute_network(&mut self, render_config: RenderConfig) -> Result { use graph_craft::graphene_compiler::Executor; match (&self.executor).execute(render_config).map_err(|e| e.to_string())? { GPoll::Final(value) | GPoll::Partial(value) => Ok(value), GPoll::Fallback(boxed) => { let (value, error) = *boxed; error!("Node graph evaluation reported an error alongside its fallback output: {error:?}"); Ok(value) } GPoll::Pending => Err("Node graph evaluation is pending".to_string()), GPoll::Error(error) => Err(format!("Node graph evaluation failed: {error:?}")), } } /// Updates state data pub fn process_monitor_nodes(&mut self, responses: &mut VecDeque, update_thumbnails: bool) { // TODO: Consider optimizing this since it's currently O(m*n^2), with a sort it could be made O(m * n*log(n)) self.thumbnail_renders.retain(|id, _| self.monitor_nodes.iter().any(|monitor_node_path| monitor_node_path.contains(id))); for monitor_node_path in &self.monitor_nodes { // Skip the inspect monitor node if self .inspect_state .as_ref() .is_some_and(|inspect_state| monitor_node_path.last().copied() == Some(inspect_state.monitor_node)) { continue; } // The monitor nodes are located within a document node, and are thus children in that network, so this gets the parent document node's ID let Some(parent_network_node_id) = monitor_node_path.len().checked_sub(2).and_then(|index| monitor_node_path.get(index)).copied() else { warn!("Monitor node has invalid node id"); continue; }; // Read the monitored run directly, inside the introspection window let thumbnail_renders = &mut self.thumbnail_renders; let vector_modify = &mut self.vector_modify; let result = self.executor.introspect_with(monitor_node_path, |layout, batch, _arena| { use graphene_std::core_types::record::{Group, GroupItem, RunView}; let type_id = layout.element.type_id; // Graphic run: thumbnail (text-aware bounds, since the `BoundingBox` trait can't lay out `Graphic::Text` content) if type_id == std::any::TypeId::of::() { if update_thumbnails { // SAFETY: the batch is resident for the read. let item = unsafe { GroupItem::from_resident(batch) }; let bounds = graphene_std::renderer::graphic_list_bounding_box(&RunView::::new(&item)?, DAffine2::IDENTITY); let group = Graphic::Group(Group { row: None, content: item }); Self::render_thumbnail(thumbnail_renders, parent_network_node_id, &group, bounds, responses) } Some(()) } // Artboard thumbnail bounds come from the clipping rectangles, not the content union, since the renderer // clips content to those rectangles so anything outside isn't visible else if type_id == std::any::TypeId::of::() { if update_thumbnails { // SAFETY: the batch is resident for the read. let item = unsafe { GroupItem::from_resident(batch) }; let run = RunView::::new(&item)?; let bounds = artboard_clip_bounds(&run); Self::render_thumbnail(thumbnail_renders, parent_network_node_id, &run, bounds, responses) } Some(()) } // Vector run: vector modifications else if type_id == std::any::TypeId::of::() { // SAFETY: the batch is resident for the read. let item = unsafe { GroupItem::from_resident(batch) }; let run = RunView::::new(&item)?; use graphene_std::core_types::lane::LaneSource; vector_modify.insert(parent_network_node_id, run.element(0).cloned().unwrap_or_default()); Some(()) } // String run: thumbnail (bounds need text layout, which the `BoundingBox` trait can't do for a bare `String`) else if type_id == std::any::TypeId::of::() { if update_thumbnails { // SAFETY: the batch is resident for the read. let item = unsafe { GroupItem::from_resident(batch) }; let run = RunView::::new(&item)?; let bounds = graphene_std::renderer::text_list_bounding_box(&run, DAffine2::IDENTITY); Self::render_thumbnail(thumbnail_renders, parent_network_node_id, &run, bounds, responses) } Some(()) } else { log::warn!("Failed to read monitor node output {parent_network_node_id:?}"); Some(()) } }); if result.is_err() { // TODO: Fix the root of the issue causing the spam of this warning (this at least temporarily disables it in release builds) #[cfg(debug_assertions)] warn!("Failed to introspect monitor node {}", result.unwrap_err()); } } } /// If this is `Graphic` data, regenerate click targets and thumbnails for the layers in the graph, modifying the state and updating the UI. fn render_thumbnail( thumbnail_renders: &mut HashMap>, parent_network_node_id: NodeId, graphic: &impl Render, bounds: RenderBoundingBox, responses: &mut VecDeque, ) { // Skip thumbnails if the layer is too complex (for performance) if graphic.render_complexity() > 1000 { let old = thumbnail_renders.insert(parent_network_node_id, Vec::new()); if old.is_none_or(|v| !v.is_empty()) { responses.push_back(FrontendMessage::UpdateNodeThumbnail { id: parent_network_node_id, value: "" .to_string(), }); } return; } // Fall back to a 1×1 rectangle if no caller offered finite bounds, then aspect-correct to the panel's 3:2 ratio let raw_bounds = match bounds { RenderBoundingBox::Rectangle(bounds) if (bounds[1] - bounds[0]) != DVec2::ZERO => bounds, _ => [DVec2::ZERO, DVec2::ONE], }; let bounds = expand_to_thumbnail_aspect(raw_bounds); let new_thumbnail_svg = { let footprint = Footprint { transform: DAffine2::from_translation(DVec2::new(bounds[0].x, bounds[0].y)), resolution: UVec2::new((bounds[1].x - bounds[0].x).abs() as u32, (bounds[1].y - bounds[0].y).abs() as u32), quality: RenderQuality::Full, }; // Render the thumbnail from a `Graphic` into an SVG string let render_params = RenderParams { footprint, thumbnail: true, ..Default::default() }; let mut render = SvgRender::new(); graphic.render_svg(&mut render, &render_params); // And give the SVG a viewbox and outer ... wrapper tag render.format_svg(bounds[0], bounds[1]); render.svg }; // Update frontend thumbnail let old_thumbnail_svg = thumbnail_renders.entry(parent_network_node_id).or_default(); if old_thumbnail_svg != &new_thumbnail_svg { responses.push_back(FrontendMessage::UpdateNodeThumbnail { id: parent_network_node_id, value: new_thumbnail_svg.to_svg_string(), }); *old_thumbnail_svg = new_thumbnail_svg; } } } /// Returns the union of the artboards' clipping rectangles, used as the thumbnail bounds for an artboard layer so the /// framing matches what's actually visible after clipping rather than the unclipped content extents. fn artboard_clip_bounds<'a, S: graphene_std::core_types::lane::LaneSource>>(artboards: &S) -> RenderBoundingBox { use graphene_std::core_types::attribute::{Dimensions, Location}; let mut combined: Option<[DVec2; 2]> = None; for index in 0..artboards.lane_count() { let location: DVec2 = artboards.attr::(index); let dimensions: DVec2 = artboards.attr::(index); let bounds = [location, location + dimensions]; combined = Some(match combined { Some(existing) => [existing[0].min(bounds[0]), existing[1].max(bounds[1])], None => bounds, }); } match combined { Some(bounds) => RenderBoundingBox::Rectangle(bounds), None => RenderBoundingBox::None, } } /// Expands an AABB outward (centered) to match the Layers panel thumbnail's 3:2 aspect ratio, padding the smaller axis /// so the input's extent is always preserved. fn expand_to_thumbnail_aspect(bounds: [DVec2; 2]) -> [DVec2; 2] { const THUMBNAIL_ASPECT_RATIO: f64 = 1.5; let size = bounds[1] - bounds[0]; let center = (bounds[0] + bounds[1]) / 2.; let (width, height) = if size.x >= size.y * THUMBNAIL_ASPECT_RATIO { (size.x, size.x / THUMBNAIL_ASPECT_RATIO) } else { (size.y * THUMBNAIL_ASPECT_RATIO, size.y) }; let half = DVec2::new(width, height) / 2.; [center - half, center + half] } pub async fn run_node_graph() -> (bool, Option) { let Some(mut runtime) = NODE_RUNTIME.try_lock() else { return (false, None) }; if let Some(ref mut runtime) = runtime.as_mut() { return (true, runtime.run().await); } (false, None) } pub fn replace_node_runtime(runtime: NodeRuntime) -> Option { let mut node_runtime = NODE_RUNTIME.lock(); node_runtime.replace(runtime) } pub(crate) fn replace_application_io(application_io: PlatformApplicationIo) { let mut node_runtime = NODE_RUNTIME.lock(); if let Some(node_runtime) = &mut *node_runtime { node_runtime.replace_application_io(application_io); } } pub fn set_completion_notifier(notifier: Arc) { let node_runtime = NODE_RUNTIME.lock(); if let Some(node_runtime) = &*node_runtime { node_runtime.graph_runtime.set_notifier(notifier); } } impl NodeRuntime { pub(crate) fn replace_application_io(&mut self, application_io: PlatformApplicationIo) { self.editor_api = PlatformEditorApi { application_io: Some(application_io.into()), node_graph_message_sender: Box::new(self.sender.clone()), editor_preferences: Box::new(self.editor_preferences.clone()), runtime: self.editor_api.runtime.clone(), } .into(); } } /// Which node is inspected and which monitor node is used (if any) for the current execution #[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>, /// 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 { pub fn take_data(&mut self) -> Option> { self.introspected_data.clone() } } // 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_path == other.inspect_node_path } } impl InspectState { /// 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, doesn't resolve to a node inside a reachable subnetwork, or the target has no /// flatten-safe primary output to monitor (e.g. an empty merged subnetwork), which would otherwise leave the monitor's /// input dangling once the subnetwork is flattened away. 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)?; // A subnetwork's primary output only survives flattening if its first export is a node let monitorable = match &target_network.nodes.get(&inspect_node)?.implementation { DocumentNodeImplementation::Network(inner) => matches!(inner.exports.first(), Some(NodeInput::Node { .. })), _ => true, }; if !monitorable { return None; } 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 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 { continue; } *node_id = monitor_id; } let monitor_node = DocumentNode { inputs: vec![NodeInput::node(inspect_node, 0)], // Connect to the primary output of the inspect node implementation: DocumentNodeImplementation::ProtoNode(graphene_std::memo::monitor::IDENTIFIER), call_argument: graph_craft::generic!(T), skip_deduplication: true, ..Default::default() }; target_network.nodes.insert(monitor_id, monitor_node); 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 { // 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 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) }