From b1151b3c47345dd11384cc252454cb929ea8a103 Mon Sep 17 00:00:00 2001 From: Adam Date: Sat, 6 Sep 2025 15:58:08 -0700 Subject: [PATCH 1/8] Complete nodes and wires --- node-graph/gcore/src/node_graph_overlay/nodes_and_wires.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/node-graph/gcore/src/node_graph_overlay/nodes_and_wires.rs b/node-graph/gcore/src/node_graph_overlay/nodes_and_wires.rs index 42d3d09c65..e32aba48c6 100644 --- a/node-graph/gcore/src/node_graph_overlay/nodes_and_wires.rs +++ b/node-graph/gcore/src/node_graph_overlay/nodes_and_wires.rs @@ -16,6 +16,7 @@ use crate::{ vector::{ Vector, style::{Fill, Stroke, StrokeAlign}, + style::{Fill, Stroke, StrokeAlign}, }, }; From a27a7f4506aed04b485190129762af49b00cb96a Mon Sep 17 00:00:00 2001 From: Adam Date: Wed, 3 Sep 2025 11:55:42 -0700 Subject: [PATCH 2/8] Native node graph runtime --- Cargo.lock | 2 + editor/src/dispatcher.rs | 5 +- frontend/wasm/Cargo.toml | 2 + frontend/wasm/src/editor_api.rs | 58 +++++++- frontend/wasm/src/lib.rs | 5 + .../wasm/src/wasm_node_graph_ui_executor.rs | 134 ++++++++++++++++++ node-graph/interpreted-executor/src/lib.rs | 1 + .../interpreted-executor/src/ui_runtime.rs | 58 ++++++++ 8 files changed, 262 insertions(+), 3 deletions(-) create mode 100644 frontend/wasm/src/wasm_node_graph_ui_executor.rs create mode 100644 node-graph/interpreted-executor/src/ui_runtime.rs diff --git a/Cargo.lock b/Cargo.lock index 3105ea07e4..ae468b75ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2207,9 +2207,11 @@ dependencies = [ "graph-craft", "graphene-std", "graphite-editor", + "interpreted-executor", "js-sys", "log", "math-parser", + "once_cell", "ron", "serde", "serde-wasm-bindgen", diff --git a/editor/src/dispatcher.rs b/editor/src/dispatcher.rs index c8f8cd4b48..5012a3dac4 100644 --- a/editor/src/dispatcher.rs +++ b/editor/src/dispatcher.rs @@ -157,7 +157,10 @@ impl Dispatcher { return; } else { // `FrontendMessage`s are saved and will be sent to the frontend after the message queue is done being processed - self.responses.push(message); + // Deduplicate the render native node graph messages. TODO: Replace responses with hashset + if !(message == FrontendMessage::RequestNativeNodeGraphRender && self.responses.contains(&FrontendMessage::RequestNativeNodeGraphRender)) { + self.responses.push(message); + } } } Message::Globals(message) => { diff --git a/frontend/wasm/Cargo.toml b/frontend/wasm/Cargo.toml index 90b8ebe6ba..65bcf67b00 100644 --- a/frontend/wasm/Cargo.toml +++ b/frontend/wasm/Cargo.toml @@ -25,7 +25,9 @@ editor = { path = "../../editor", package = "graphite-editor", features = [ "resvg", "vello", ] } +interpreted-executor = { workspace = true } graphene-std = { workspace = true } +once_cell = { workspace = true } # Workspace dependencies graph-craft = { workspace = true } diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index 23d8879001..5f7a3e54a5 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -5,7 +5,9 @@ // on the dispatcher messaging system and more complex Rust data types. // use crate::helpers::translate_key; -use crate::{EDITOR_HANDLE, EDITOR_HAS_CRASHED, Error, MESSAGE_BUFFER}; +#[cfg(not(feature = "native"))] +use crate::wasm_node_graph_ui_executor::WasmNodeGraphUIExecutor; +use crate::{EDITOR_HANDLE, EDITOR_HAS_CRASHED, Error, MESSAGE_BUFFER, WASM_NODE_GRAPH_EXECUTOR}; use editor::consts::FILE_EXTENSION; use editor::messages::input_mapper::utility_types::input_keyboard::ModifierKeys; use editor::messages::input_mapper::utility_types::input_mouse::{EditorMouseState, ScrollDelta, ViewportBounds}; @@ -17,6 +19,7 @@ use editor::messages::tool::tool_messages::tool_prelude::WidgetId; use graph_craft::document::NodeId; use graphene_std::raster::Image; use graphene_std::raster::color::Color; +use interpreted_executor::ui_runtime::CompilationRequest; use js_sys::{Object, Reflect}; use serde::Serialize; use serde_wasm_bindgen::{self, from_value}; @@ -149,9 +152,13 @@ impl EditorHandle { pub fn new(frontend_message_handler_callback: js_sys::Function) -> Self { let editor = Editor::new(); let editor_handle = EditorHandle { frontend_message_handler_callback }; + let node_graph_executor = WasmNodeGraphUIExecutor::new(); if EDITOR.with(|handle| handle.lock().ok().map(|mut guard| *guard = Some(editor))).is_none() { log::error!("Attempted to initialize the editor more than once"); } + if WASM_NODE_GRAPH_EXECUTOR.with(|handle| handle.lock().ok().map(|mut guard| *guard = Some(node_graph_executor))).is_none() { + log::error!("Attempted to initialize the editor more than once"); + } if EDITOR_HANDLE.with(|handle| handle.lock().ok().map(|mut guard| *guard = Some(editor_handle.clone()))).is_none() { log::error!("Attempted to initialize the editor handle more than once"); } @@ -208,6 +215,17 @@ impl EditorHandle { // Sends a FrontendMessage to JavaScript fn send_frontend_message_to_js(&self, mut message: FrontendMessage) { + // Intercept any requests to render the node graph overlay + if message == FrontendMessage::RequestNativeNodeGraphRender { + executor_editor_and_handle(|executor, editor, _handle| { + if let Some(node_graph_overlay_network) = editor.generate_node_graph_overlay_network() { + let compilation_request = CompilationRequest { network: node_graph_overlay_network }; + executor.compilation_request(compilation_request); + } + }); + return; + } + if let FrontendMessage::UpdateImageData { ref image_data } = message { let new_hash = calculate_hash(image_data); let prev_hash = IMAGE_DATA_HASH.load(Ordering::Relaxed); @@ -264,6 +282,18 @@ impl EditorHandle { #[cfg(not(feature = "native"))] wasm_bindgen_futures::spawn_local(poll_node_graph_evaluation()); + // Poll the UI node graph + #[cfg(not(feature = "native"))] + executor_editor_and_handle(|executor, editor, handle| { + for frontend_message in executor + .poll_node_graph_ui_evaluation(editor) + .into_iter() + .flat_map(|runtime_response| editor.handle_message(runtime_response)) + { + handle.send_frontend_message_to_js(frontend_message); + } + }); + if !EDITOR_HAS_CRASHED.load(Ordering::SeqCst) { handle(|handle| { // Process all messages that have been queued up @@ -982,6 +1012,31 @@ fn editor(callback: impl FnOnce(&mut editor::application::Editor) -> }) } +#[cfg(not(feature = "native"))] +fn executor(callback: impl FnOnce(&mut WasmNodeGraphUIExecutor) -> T) -> T { + WASM_NODE_GRAPH_EXECUTOR.with(|executor| { + let mut guard = executor.try_lock(); + let Ok(Some(executor)) = guard.as_deref_mut() else { + log::error!("Failed to borrow editor"); + return T::default(); + }; + + callback(executor) + }) +} + +#[cfg(not(feature = "native"))] +pub(crate) fn executor_editor_and_handle(callback: impl FnOnce(&mut WasmNodeGraphUIExecutor, &mut Editor, &mut EditorHandle)) { + executor(|executor| { + handle(|editor_handle| { + editor(|editor| { + // Call the closure with the editor and its handle + callback(executor, editor, editor_handle); + }) + }); + }) +} + /// Provides access to the `Editor` and its `EditorHandle` by calling the given closure with them as arguments. #[cfg(not(feature = "native"))] pub(crate) fn editor_and_handle(callback: impl FnOnce(&mut Editor, &mut EditorHandle)) { @@ -1039,7 +1094,6 @@ async fn poll_node_graph_evaluation() { // If the editor cannot be borrowed then it has encountered a panic - we should just ignore new dispatches }); } - fn auto_save_all_documents() { // Process no further messages after a crash to avoid spamming the console if EDITOR_HAS_CRASHED.load(Ordering::SeqCst) { diff --git a/frontend/wasm/src/lib.rs b/frontend/wasm/src/lib.rs index d3141288b3..7116222001 100644 --- a/frontend/wasm/src/lib.rs +++ b/frontend/wasm/src/lib.rs @@ -7,6 +7,7 @@ extern crate log; pub mod editor_api; pub mod helpers; pub mod native_communcation; +pub mod wasm_node_graph_ui_executor; use editor::messages::prelude::*; use std::panic; @@ -14,6 +15,8 @@ use std::sync::Mutex; use std::sync::atomic::{AtomicBool, Ordering}; use wasm_bindgen::prelude::*; +use crate::wasm_node_graph_ui_executor::WasmNodeGraphUIExecutor; + // Set up the persistent editor backend state pub static EDITOR_HAS_CRASHED: AtomicBool = AtomicBool::new(false); pub static NODE_GRAPH_ERROR_DISPLAYED: AtomicBool = AtomicBool::new(false); @@ -22,6 +25,8 @@ pub static LOGGER: WasmLog = WasmLog; thread_local! { #[cfg(not(feature = "native"))] pub static EDITOR: Mutex> = const { Mutex::new(None) }; + #[cfg(not(feature = "native"))] + pub static WASM_NODE_GRAPH_EXECUTOR: Mutex> = const { Mutex::new(None) }; pub static MESSAGE_BUFFER: std::cell::RefCell> = const { std::cell::RefCell::new(Vec::new()) }; pub static EDITOR_HANDLE: Mutex> = const { Mutex::new(None) }; } diff --git a/frontend/wasm/src/wasm_node_graph_ui_executor.rs b/frontend/wasm/src/wasm_node_graph_ui_executor.rs new file mode 100644 index 0000000000..7173cad103 --- /dev/null +++ b/frontend/wasm/src/wasm_node_graph_ui_executor.rs @@ -0,0 +1,134 @@ +use std::sync::{Mutex, mpsc::Receiver}; + +use editor::{ + application::Editor, + messages::prelude::{FrontendMessage, Message}, +}; +use graph_craft::graphene_compiler::Compiler; +use graphene_std::node_graph_overlay::{types::NodeGraphTransform, ui_context::UIRuntimeResponse}; +use interpreted_executor::{ + dynamic_executor::DynamicExecutor, + ui_runtime::{CompilationRequest, EvaluationRequest, NodeGraphUIRuntime}, +}; +use once_cell::sync::Lazy; + +pub static NODE_UI_RUNTIME: Lazy>> = Lazy::new(|| Mutex::new(None)); + +// Since the runtime is not locked, it is possible to spawn multiple futures concurrently. +// This is why the runtime_busy flag exists +// This struct should never be locked in a future +pub struct WasmNodeGraphUIExecutor { + response_receiver: Receiver, + runtime_busy: bool, + queued_compilation: Option, +} + +impl Default for WasmNodeGraphUIExecutor { + fn default() -> Self { + Self::new() + } +} + +impl WasmNodeGraphUIExecutor { + pub fn new() -> Self { + let (response_sender, response_receiver) = std::sync::mpsc::channel(); + let runtime = NodeGraphUIRuntime { + executor: DynamicExecutor::default(), + compiler: Compiler {}, + response_sender, + }; + if let Ok(mut node_runtime) = NODE_UI_RUNTIME.lock() { + node_runtime.replace(runtime); + } else { + log::error!("Could not lock runtime when creating new executor"); + }; + + WasmNodeGraphUIExecutor { + response_receiver, + runtime_busy: false, + queued_compilation: None, + } + } + + pub fn compilation_request(&mut self, compilation_request: CompilationRequest) { + if !self.runtime_busy { + self.runtime_busy = true; + wasm_bindgen_futures::spawn_local(async move { + let Ok(mut runtime) = NODE_UI_RUNTIME.try_lock() else { + log::error!("Could not get runtime when evaluating"); + return; + }; + let Some(runtime) = runtime.as_mut() else { + log::error!("Could not lock runtime when evaluating"); + return; + }; + runtime.compile(compilation_request).await; + }) + } else { + self.queued_compilation = Some(compilation_request); + } + } + + // Evaluates the node graph in a spawned future, and returns responses with the response_sender + fn evaluation_request(&mut self, editor: &Editor) { + if let Some(active_document) = editor.dispatcher.message_handlers.portfolio_message_handler.active_document() { + let Some(network_metadata) = active_document.network_interface.network_metadata(&active_document.breadcrumb_network_path) else { + return; + }; + + let transform = active_document.navigation_handler.calculate_offset_transform( + editor.dispatcher.message_handlers.input_preprocessor_message_handler.viewport_bounds.center(), + &network_metadata.persistent_metadata.navigation_metadata.node_graph_ptz, + ); + + let transform = NodeGraphTransform { + scale: transform.matrix2.x_axis.x, + x: transform.translation.x, + y: transform.translation.y, + }; + let resolution = editor.dispatcher.message_handlers.input_preprocessor_message_handler.viewport_bounds.size().as_uvec2(); + let evaluation_request = EvaluationRequest { transform, resolution }; + self.runtime_busy = true; + + wasm_bindgen_futures::spawn_local(async move { + let Ok(mut runtime) = NODE_UI_RUNTIME.try_lock() else { + log::error!("Could not get runtime when evaluating"); + return; + }; + let Some(runtime) = runtime.as_mut() else { + log::error!("Could not lock runtime when evaluating"); + return; + }; + runtime.evaluate(evaluation_request).await + }) + } + } + + // This is run every time a frame is requested to be rendered. + // It returns back Messages for how to update the frontend/editor click targets + // It also checks for any queued evaluation/compilation requests and runs them + pub fn poll_node_graph_ui_evaluation(&mut self, editor: &Editor) -> Vec { + let mut responses = Vec::new(); + for runtime_response in self.response_receiver.try_iter() { + match runtime_response { + UIRuntimeResponse::RuntimeReady => { + self.runtime_busy = false; + } + UIRuntimeResponse::OverlaySVG(svg_string) => { + responses.push(FrontendMessage::UpdateNativeNodeGraphSVG { svg_string }.into()); + } + UIRuntimeResponse::OverlayTexture(_texture) => todo!(), + } + } + + if !self.runtime_busy { + if let Some(compilation_request) = self.queued_compilation.take() { + self.compilation_request(compilation_request); + } else { + self.evaluation_request(editor); + } + } + + responses + } +} diff --git a/node-graph/interpreted-executor/src/lib.rs b/node-graph/interpreted-executor/src/lib.rs index 19d2015773..02ff6f2ccb 100644 --- a/node-graph/interpreted-executor/src/lib.rs +++ b/node-graph/interpreted-executor/src/lib.rs @@ -1,5 +1,6 @@ pub mod dynamic_executor; pub mod node_registry; +pub mod ui_runtime; pub mod util; #[cfg(test)] diff --git a/node-graph/interpreted-executor/src/ui_runtime.rs b/node-graph/interpreted-executor/src/ui_runtime.rs new file mode 100644 index 0000000000..c5ed664866 --- /dev/null +++ b/node-graph/interpreted-executor/src/ui_runtime.rs @@ -0,0 +1,58 @@ +use std::sync::{Arc, mpsc::Sender}; + +use glam::UVec2; +use graph_craft::{document::NodeNetwork, graphene_compiler::Compiler}; +use graphene_std::node_graph_overlay::{ + types::NodeGraphTransform, + ui_context::{UIContextImpl, UIRuntimeResponse}, +}; + +use crate::dynamic_executor::DynamicExecutor; + +pub struct NodeGraphUIRuntime { + pub executor: DynamicExecutor, + pub compiler: Compiler, + // Used within the node graph to return responses during evaluation + // Also used to return compilation responses, but not for the UI overlay since the types are not needed + pub response_sender: Sender, +} + +impl NodeGraphUIRuntime { + pub async fn compile(&mut self, compilation_request: CompilationRequest) { + match self.compiler.compile_single(compilation_request.network) { + Ok(proto_network) => { + if let Err(e) = self.executor.update(proto_network).await { + log::error!("update error: {e:?}") + } + } + Err(e) => { + log::error!("Error compiling node graph ui network: {e:?}"); + } + }; + let _ = self.response_sender.send(UIRuntimeResponse::RuntimeReady); + } + + pub async fn evaluate(&mut self, evaluation_request: EvaluationRequest) { + use graph_craft::graphene_compiler::Executor; + + let ui_context = Arc::new(UIContextImpl { + transform: evaluation_request.transform, + resolution: evaluation_request.resolution, + response_sender: self.response_sender.clone(), + }); + let _ = (&self.executor).execute(ui_context).await; + let _ = self.response_sender.send(UIRuntimeResponse::RuntimeReady); + } +} + +/// Represents an update to the render state +/// TODO: Incremental compilation +pub struct CompilationRequest { + pub network: NodeNetwork, +} + +// Requests an evaluation. The responses are added to the sender and can be processed when the next frame is requested +pub struct EvaluationRequest { + pub transform: NodeGraphTransform, + pub resolution: UVec2, +} From 377770c175a4459a1caa89c1502d863a485925d2 Mon Sep 17 00:00:00 2001 From: Adam Date: Fri, 5 Sep 2025 13:23:15 -0700 Subject: [PATCH 3/8] improve orphaned nodes --- .../src/dynamic_executor.rs | 117 +++++++++++------- 1 file changed, 75 insertions(+), 42 deletions(-) diff --git a/node-graph/interpreted-executor/src/dynamic_executor.rs b/node-graph/interpreted-executor/src/dynamic_executor.rs index 53f23fa50f..b1bbc35fc4 100644 --- a/node-graph/interpreted-executor/src/dynamic_executor.rs +++ b/node-graph/interpreted-executor/src/dynamic_executor.rs @@ -1,11 +1,14 @@ use crate::node_registry; use dyn_any::StaticType; -use graph_craft::Type; use graph_craft::document::NodeId; use graph_craft::document::value::{TaggedValue, UpcastAsRefNode, UpcastNode}; use graph_craft::graphene_compiler::Executor; use graph_craft::proto::{ConstructionArgs, GraphError, LocalFuture, NodeContainer, ProtoNetwork, ProtoNode, SharedNodeContainer, TypeErasedBox, TypingContext}; use graph_craft::proto::{GraphErrorType, GraphErrors}; +use graph_craft::{Type, concrete}; +use graphene_std::memo; +use graphene_std::node_graph_overlay::ui_context::UIContext; +use std::collections::hash_map::Entry; use std::collections::{HashMap, HashSet}; use std::error::Error; use std::sync::Arc; @@ -18,8 +21,6 @@ pub struct DynamicExecutor { tree: BorrowTree, /// Stores the types of the proto nodes. typing_context: TypingContext, - // This allows us to keep the nodes around for one more frame which is used for introspection - orphaned_nodes: HashSet, } impl Default for DynamicExecutor { @@ -28,7 +29,6 @@ impl Default for DynamicExecutor { output: Default::default(), tree: Default::default(), typing_context: TypingContext::new(&node_registry::NODE_REGISTRY), - orphaned_nodes: HashSet::new(), } } } @@ -55,12 +55,7 @@ impl DynamicExecutor { let output = proto_network.output; let tree = BorrowTree::new(proto_network, &typing_context).await?; - Ok(Self { - tree, - output, - typing_context, - orphaned_nodes: HashSet::new(), - }) + Ok(Self { tree, output, typing_context }) } /// Updates the existing [`BorrowTree`] to reflect the new [`ProtoNetwork`], reusing nodes where possible. @@ -69,11 +64,10 @@ impl DynamicExecutor { self.output = proto_network.output; self.typing_context.update(&proto_network)?; let (add, orphaned) = self.tree.update(proto_network, &self.typing_context).await?; - let old_to_remove = core::mem::replace(&mut self.orphaned_nodes, orphaned); - let mut remove = Vec::with_capacity(old_to_remove.len() - self.orphaned_nodes.len().min(old_to_remove.len())); - for node_id in old_to_remove { - if self.orphaned_nodes.contains(&node_id) { - let path = self.tree.free_node(node_id); + let mut remove = Vec::with_capacity(orphaned.len()); + for node_id in orphaned { + let (removed, path) = self.tree.free_node(node_id); + if removed { self.typing_context.remove_inference(node_id); if let Some(path) = path { remove.push(path); @@ -152,6 +146,24 @@ impl std::fmt::Display for IntrospectError { } } +// Stores the node when can be evaluated, as well as corresponding metadata +#[derive(Clone)] +struct CompiledProtonode { + // A type erased struct which implements Node, allowing .eval to be called + node_container: SharedNodeContainer, + path: Path, + compilations_without_node: u32, + // Non user visible cache nodes (those in the graph overlay and network wrapping artwork) + // Get immediately removed, since they output of those networks is side effect based, not return value based + // Normal nodes cache stick around for a few compilations, since there is a high likelyhood of reaching the same SNI again + // All other nodes get immediately removed since they do not store any data + remove_after: u32, + // Since the container is type erased, the types must be annotated + // types: NodeIOTypes, + // Used in incremental compilation + // callers: NodeId, +} + /// A store of dynamically typed nodes and their associated source map. /// /// [`BorrowTree`] maintains two main data structures: @@ -173,7 +185,7 @@ impl std::fmt::Display for IntrospectError { #[derive(Default, Clone)] pub struct BorrowTree { /// A hashmap of node IDs and dynamically typed nodes. - nodes: HashMap, + nodes: HashMap, /// A hashmap from the document path to the proto node ID. source_map: HashMap, } @@ -205,22 +217,26 @@ impl BorrowTree { } fn node_deps(&self, nodes: &[NodeId]) -> Vec { - nodes.iter().map(|node| self.nodes.get(node).unwrap().0.clone()).collect() + nodes.iter().map(|node| self.nodes.get(node).unwrap().node_container.clone()).collect() } - fn store_node(&mut self, node: SharedNodeContainer, id: NodeId, path: Path) { - self.nodes.insert(id, (node, path)); + fn store_node(&mut self, id: NodeId, node: SharedNodeContainer, path: Path, remove_after: u32) { + self.nodes.insert( + id, + CompiledProtonode { + node_container: node, + path, + compilations_without_node: 0, + remove_after, + }, + ); } /// Calls the `Node::serialize` for that specific node, returning for example the cached value for a monitor node. The node path must match the document node path. pub fn introspect(&self, node_path: &[NodeId]) -> Result, IntrospectError> { let (id, _) = self.source_map.get(node_path).ok_or_else(|| IntrospectError::PathNotFound(node_path.to_vec()))?; - let (node, _path) = self.nodes.get(id).ok_or(IntrospectError::ProtoNodeNotFound(*id))?; - node.serialize().ok_or(IntrospectError::NoData) - } - - pub fn get(&self, id: NodeId) -> Option { - self.nodes.get(&id).map(|(node, _)| node.clone()) + let compiled_protonode = self.nodes.get(id).ok_or(IntrospectError::ProtoNodeNotFound(*id))?; + compiled_protonode.node_container.serialize().ok_or(IntrospectError::NoData) } /// Evaluate the output node of the [`BorrowTree`]. @@ -229,8 +245,8 @@ impl BorrowTree { I: StaticType + 'i + Send + Sync, O: StaticType + 'i, { - let (node, _path) = self.nodes.get(&id).cloned()?; - let output = node.eval(Box::new(input)); + let compiled_protonode = self.nodes.get(&id).cloned()?; + let output = compiled_protonode.node_container.eval(Box::new(input)); dyn_any::downcast::(output.await).ok().map(|o| *o) } /// Evaluate the output node of the [`BorrowTree`] and cast it to a tagged value. @@ -239,23 +255,25 @@ impl BorrowTree { where I: StaticType + 'static + Send + Sync, { - let (node, _path) = self.nodes.get(&id).cloned().ok_or("Output node not found in executor")?; - let output = node.eval(Box::new(input)); + let compiled_protonode = self.nodes.get(&id).cloned().ok_or("Output node not found in executor")?; + let output = compiled_protonode.node_container.eval(Box::new(input)); TaggedValue::try_from_any(output.await) } - /// Removes a node from the [`BorrowTree`] and returns its associated path. - /// + /// Tries to removes a node from the [`BorrowTree`] and return its associated path. + // /// This method removes the specified node from both the `nodes` HashMap and, - /// if applicable, the `source_map` HashMap. + /// if applicable, the `source_map` HashMap if it has been not compiled for more + /// iterations that specified. /// /// # Arguments /// /// * `self` - Mutable reference to the [`BorrowTree`]. - /// * `id` - The `NodeId` of the node to be removed. + /// * `id` - The `NodeId` of the node that is not present in the network /// /// # Returns /// + /// bool - Whether the node has been removed /// [`Option`] - The path associated with the removed node, or `None` if the node wasn't found. /// /// # Example @@ -296,16 +314,26 @@ impl BorrowTree { /// /// # Notes /// - /// - Removes the node from `nodes` HashMap. + /// - Tries to Removes the node from `nodes` HashMap. /// - If the node is the primary node for its path in the `source_map`, it's also removed from there. /// - Returns `None` if the node is not found in the `nodes` HashMap. - pub fn free_node(&mut self, id: NodeId) -> Option { - let (_, path) = self.nodes.remove(&id)?; - if self.source_map.get(&path)?.0 == id { - self.source_map.remove(&path); - return Some(path); + pub fn free_node(&mut self, id: NodeId) -> (bool, Option) { + match self.nodes.entry(id) { + Entry::Occupied(mut occupied_entry) => { + occupied_entry.get_mut().compilations_without_node += 1; + if occupied_entry.get().compilations_without_node >= occupied_entry.get().remove_after { + let (_, compiled_protonode) = occupied_entry.remove_entry(); + if self.source_map.get(&compiled_protonode.path).is_some_and(|path| path.0 == id) { + self.source_map.remove(&compiled_protonode.path); + return (true, Some(compiled_protonode.path)); + } + return (true, None); + } + } + Entry::Vacant(_) => panic!("Compiled protonode must exist in borrow tree when removing"), } - None + + (false, None) } /// Updates the source map for a given node in the [`BorrowTree`]. @@ -383,7 +411,7 @@ impl BorrowTree { let node = Box::new(upcasted) as TypeErasedBox<'_>; NodeContainer::new(node) }; - self.store_node(node, id, path.into()); + self.store_node(id, node, path.into(), 1); } ConstructionArgs::Inline(_) => unimplemented!("Inline nodes are not supported yet"), ConstructionArgs::Nodes(ids) => { @@ -392,7 +420,12 @@ impl BorrowTree { let constructor = typing_context.constructor(id).ok_or_else(|| vec![GraphError::new(&proto_node, GraphErrorType::NoConstructor)])?; let node = constructor(construction_nodes).await; let node = NodeContainer::new(node); - self.store_node(node, id, path.into()); + let remove_after = if proto_node.identifier == memo::memo::IDENTIFIER && proto_node.call_argument != concrete!(UIContext) { + 3 + } else { + 1 + }; + self.store_node(id, node, path.into(), remove_after); } }; Ok(()) From 911b7f50234445d80bcaa00127c3137a22e40355 Mon Sep 17 00:00:00 2001 From: Adam Date: Fri, 5 Sep 2025 15:18:58 -0700 Subject: [PATCH 4/8] Improve editor api --- frontend/wasm/src/editor_api.rs | 170 ++++++++++------------- frontend/wasm/src/lib.rs | 9 +- frontend/wasm/src/native_communcation.rs | 2 +- 3 files changed, 74 insertions(+), 107 deletions(-) diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index 5f7a3e54a5..b9a9b81234 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -137,11 +137,34 @@ pub struct EditorHandle { frontend_message_handler_callback: js_sys::Function, } -// Defined separately from the `impl` block below since this `impl` block lacks the `#[wasm_bindgen]` attribute. -// Quirks in wasm-bindgen prevent functions in `#[wasm_bindgen]` `impl` blocks from being made publicly accessible from Rust. impl EditorHandle { - pub fn send_frontend_message_to_js_rust_proxy(&self, message: FrontendMessage) { - self.send_frontend_message_to_js(message); + // Sends a FrontendMessage to JavaScript + pub fn send_frontend_message_to_js(&self, mut message: FrontendMessage) { + if let FrontendMessage::UpdateImageData { ref image_data } = message { + let new_hash = calculate_hash(image_data); + let prev_hash = IMAGE_DATA_HASH.load(Ordering::Relaxed); + + if new_hash != prev_hash { + render_image_data_to_canvases(image_data.as_slice()); + IMAGE_DATA_HASH.store(new_hash, Ordering::Relaxed); + } + return; + } + + if let FrontendMessage::UpdateDocumentLayerStructure { data_buffer } = message { + message = FrontendMessage::UpdateDocumentLayerStructureJs { data_buffer: data_buffer.into() }; + } + + let message_type = message.to_discriminant().local_name(); + + let serializer = serde_wasm_bindgen::Serializer::new().serialize_large_number_types_as_bigints(true); + let message_data = message.serialize(&serializer).expect("Failed to serialize FrontendMessage"); + + let js_return_value = self.frontend_message_handler_callback.call2(&JsValue::null(), &JsValue::from(message_type), &message_data); + + if let Err(error) = js_return_value { + error!("While handling FrontendMessage {:?}, JavaScript threw an error:\n{:?}", message.to_discriminant().local_name(), error,) + } } } @@ -179,28 +202,12 @@ impl EditorHandle { #[cfg(not(feature = "native"))] fn dispatch>(&self, message: T) { // Process no further messages after a crash to avoid spamming the console - - use crate::MESSAGE_BUFFER; if EDITOR_HAS_CRASHED.load(Ordering::SeqCst) { return; } - - // Get the editor, dispatch the message, and store the `FrontendMessage` queue response - let frontend_messages = EDITOR.with(|editor| { - let mut guard = editor.try_lock(); - let Ok(Some(editor)) = guard.as_deref_mut() else { - // Enqueue messages which can't be procssed currently - MESSAGE_BUFFER.with_borrow_mut(|buffer| buffer.push(message.into())); - return vec![]; - }; - - editor.handle_message(message) + let _ = editor(|editor| { + self.process_messages(std::iter::once(message.into()), editor); }); - - // Send each `FrontendMessage` to the JavaScript frontend - for message in frontend_messages.into_iter() { - self.send_frontend_message_to_js(message); - } } #[cfg(feature = "native")] @@ -213,43 +220,22 @@ impl EditorHandle { crate::native_communcation::send_message_to_cef(serialized_message) } - // Sends a FrontendMessage to JavaScript - fn send_frontend_message_to_js(&self, mut message: FrontendMessage) { - // Intercept any requests to render the node graph overlay - if message == FrontendMessage::RequestNativeNodeGraphRender { - executor_editor_and_handle(|executor, editor, _handle| { + // Messages can come from the runtime, browser, or a timed callback. This processes them in the editor and does all the side effects + // Like updating the frontend and node graph ui network. + fn process_messages(&self, messages: impl IntoIterator, editor: &mut Editor) { + // Get the editor, dispatch the message, and store the `FrontendMessage` queue response + for side_effect in messages.into_iter().flat_map(|message| editor.handle_message(message)).collect::>() { + if side_effect == FrontendMessage::RequestNativeNodeGraphRender { if let Some(node_graph_overlay_network) = editor.generate_node_graph_overlay_network() { let compilation_request = CompilationRequest { network: node_graph_overlay_network }; - executor.compilation_request(compilation_request); + let res = executor(|executor| executor.compilation_request(compilation_request)); + if let Err(_) = res { + log::error!("Could not borrow executor in process_messages_in_editor"); + } } - }); - return; - } - - if let FrontendMessage::UpdateImageData { ref image_data } = message { - let new_hash = calculate_hash(image_data); - let prev_hash = IMAGE_DATA_HASH.load(Ordering::Relaxed); - - if new_hash != prev_hash { - render_image_data_to_canvases(image_data.as_slice()); - IMAGE_DATA_HASH.store(new_hash, Ordering::Relaxed); + } else { + self.send_frontend_message_to_js(side_effect); } - return; - } - - if let FrontendMessage::UpdateDocumentLayerStructure { data_buffer } = message { - message = FrontendMessage::UpdateDocumentLayerStructureJs { data_buffer: data_buffer.into() }; - } - - let message_type = message.to_discriminant().local_name(); - - let serializer = serde_wasm_bindgen::Serializer::new().serialize_large_number_types_as_bigints(true); - let message_data = message.serialize(&serializer).expect("Failed to serialize FrontendMessage"); - - let js_return_value = self.frontend_message_handler_callback.call2(&JsValue::null(), &JsValue::from(message_type), &message_data); - - if let Err(error) = js_return_value { - error!("While handling FrontendMessage {:?}, JavaScript threw an error:\n{:?}", message.to_discriminant().local_name(), error,) } } @@ -284,16 +270,18 @@ impl EditorHandle { // Poll the UI node graph #[cfg(not(feature = "native"))] - executor_editor_and_handle(|executor, editor, handle| { - for frontend_message in executor - .poll_node_graph_ui_evaluation(editor) - .into_iter() - .flat_map(|runtime_response| editor.handle_message(runtime_response)) - { - handle.send_frontend_message_to_js(frontend_message); + let result = editor(|editor| { + let node_graph_response = executor(|executor| executor.poll_node_graph_ui_evaluation(editor)); + + match node_graph_response { + Ok(node_graph_ui_messages) => handle(|handle| handle.process_messages(node_graph_ui_messages, editor)), + Err(_) => log::error!("Could not get executor in frame loop"), } }); + if let Err(_) = result { + log::error!("Could not get editor in frame loop"); + } if !EDITOR_HAS_CRASHED.load(Ordering::SeqCst) { handle(|handle| { // Process all messages that have been queued up @@ -1000,64 +988,49 @@ fn set_timeout(f: &Closure, delay: Duration) { /// Provides access to the `Editor` by calling the given closure with it as an argument. #[cfg(not(feature = "native"))] -fn editor(callback: impl FnOnce(&mut editor::application::Editor) -> T) -> T { +fn editor(callback: impl FnOnce(&mut editor::application::Editor) -> T) -> Result { EDITOR.with(|editor| { let mut guard = editor.try_lock(); let Ok(Some(editor)) = guard.as_deref_mut() else { - log::error!("Failed to borrow editor"); - return T::default(); + return Err(()); }; - - callback(editor) + Ok(callback(editor)) }) } #[cfg(not(feature = "native"))] -fn executor(callback: impl FnOnce(&mut WasmNodeGraphUIExecutor) -> T) -> T { +fn executor(callback: impl FnOnce(&mut WasmNodeGraphUIExecutor) -> T) -> Result { WASM_NODE_GRAPH_EXECUTOR.with(|executor| { let mut guard = executor.try_lock(); let Ok(Some(executor)) = guard.as_deref_mut() else { - log::error!("Failed to borrow editor"); - return T::default(); + return Err(()); }; - callback(executor) + Ok(callback(executor)) }) } -#[cfg(not(feature = "native"))] -pub(crate) fn executor_editor_and_handle(callback: impl FnOnce(&mut WasmNodeGraphUIExecutor, &mut Editor, &mut EditorHandle)) { - executor(|executor| { - handle(|editor_handle| { - editor(|editor| { - // Call the closure with the editor and its handle - callback(executor, editor, editor_handle); - }) - }); - }) -} - -/// Provides access to the `Editor` and its `EditorHandle` by calling the given closure with them as arguments. -#[cfg(not(feature = "native"))] -pub(crate) fn editor_and_handle(callback: impl FnOnce(&mut Editor, &mut EditorHandle)) { - handle(|editor_handle| { - editor(|editor| { - // Call the closure with the editor and its handle - callback(editor, editor_handle); - }) - }); -} /// Provides access to the `EditorHandle` by calling the given closure with them as arguments. pub(crate) fn handle(callback: impl FnOnce(&mut EditorHandle)) { EDITOR_HANDLE.with(|editor_handle| { let mut guard = editor_handle.try_lock(); let Ok(Some(editor_handle)) = guard.as_deref_mut() else { - log::error!("Failed to borrow editor handle"); - return; + return log::error!("Failed to borrow handle"); }; // Call the closure with the editor and its handle - callback(editor_handle); + callback(editor_handle) + }) +} + +/// Provides access to the `Editor` and its `EditorHandle` by calling the given closure with them as arguments. +#[cfg(not(feature = "native"))] +pub(crate) fn editor_and_handle(callback: impl FnOnce(&mut Editor, &mut EditorHandle)) { + let _ = handle(|editor_handle| { + let _ = editor(|editor| { + // Call the closure with the editor and its handle + callback(editor, editor_handle); + }); }); } @@ -1086,10 +1059,7 @@ async fn poll_node_graph_evaluation() { crate::NODE_GRAPH_ERROR_DISPLAYED.store(false, Ordering::SeqCst); } - // Send each `FrontendMessage` to the JavaScript frontend - for response in messages.into_iter().flat_map(|message| editor.handle_message(message)) { - handle.send_frontend_message_to_js(response); - } + handle.process_messages(messages, editor); // If the editor cannot be borrowed then it has encountered a panic - we should just ignore new dispatches }); diff --git a/frontend/wasm/src/lib.rs b/frontend/wasm/src/lib.rs index 7116222001..f0ecd0e9de 100644 --- a/frontend/wasm/src/lib.rs +++ b/frontend/wasm/src/lib.rs @@ -66,7 +66,7 @@ pub fn panic_hook(info: &panic::PanicHookInfo) { /text>"# // It's a mystery why the `/text>` tag above needs to be missing its `<`, but when it exists it prints the `<` character in the text. However this works with it removed. .to_string(); - handle.send_frontend_message_to_js_rust_proxy(FrontendMessage::UpdateDocumentArtwork { svg: error }); + handle.send_frontend_message_to_js(FrontendMessage::UpdateDocumentArtwork { svg: error }); }); } @@ -77,11 +77,8 @@ pub fn panic_hook(info: &panic::PanicHookInfo) { log::error!("{info}"); - EDITOR_HANDLE.with(|editor_handle| { - let mut guard = editor_handle.lock(); - if let Ok(Some(handle)) = guard.as_deref_mut() { - handle.send_frontend_message_to_js_rust_proxy(FrontendMessage::DisplayDialogPanic { panic_info: info.to_string() }); - } + editor_api::handle(|editor_handle| { + editor_handle.send_frontend_message_to_js(FrontendMessage::DisplayDialogPanic { panic_info: info.to_string() }); }); } diff --git a/frontend/wasm/src/native_communcation.rs b/frontend/wasm/src/native_communcation.rs index 97cfd4863d..5803de77c7 100644 --- a/frontend/wasm/src/native_communcation.rs +++ b/frontend/wasm/src/native_communcation.rs @@ -11,7 +11,7 @@ pub fn receive_native_message(buffer: ArrayBuffer) { Ok(messages) => { let callback = move |handle: &mut EditorHandle| { for message in messages { - handle.send_frontend_message_to_js_rust_proxy(message); + handle.send_frontend_message_to_js(message); } }; editor_api::handle(callback); From 1e1b6cf7cfedc64721940a777131386586acbd98 Mon Sep 17 00:00:00 2001 From: Adam Date: Fri, 5 Sep 2025 20:48:39 -0700 Subject: [PATCH 5/8] Refactor editor api to allow for future timeouts and side effect messages --- desktop/wrapper/src/message_dispatcher.rs | 6 +- editor/src/application.rs | 36 +--- editor/src/dispatcher.rs | 62 ++++--- .../src/messages/frontend/frontend_message.rs | 5 +- editor/src/messages/message.rs | 4 +- editor/src/messages/mod.rs | 1 + .../document/document_message_handler.rs | 3 +- .../document/node_graph/node_graph_message.rs | 1 + .../node_graph/node_graph_message_handler.rs | 65 ++++++- .../document/node_graph/node_properties.rs | 7 - .../network_interface/node_graph.rs | 165 +++++++++--------- editor/src/messages/side_effects/mod.rs | 5 + .../side_effects/side_effect_message.rs | 21 +++ .../side_effect_message_handler.rs | 53 ++++++ editor/src/node_graph_executor.rs | 3 +- editor/src/node_graph_executor/runtime.rs | 14 +- frontend/src/components/views/Graph.svelte | 38 +--- frontend/src/io-managers/input.ts | 3 +- frontend/src/messages.ts | 6 + frontend/src/state-providers/node-graph.ts | 17 +- frontend/wasm/src/editor_api.rs | 30 +++- node-graph/gapplication-io/src/lib.rs | 25 --- .../src/node_graph_overlay/nodes_and_wires.rs | 2 - .../gcore/src/node_graph_overlay/types.rs | 13 -- node-graph/graphene-cli/src/main.rs | 1 - 25 files changed, 336 insertions(+), 250 deletions(-) create mode 100644 editor/src/messages/side_effects/mod.rs create mode 100644 editor/src/messages/side_effects/side_effect_message.rs create mode 100644 editor/src/messages/side_effects/side_effect_message_handler.rs diff --git a/desktop/wrapper/src/message_dispatcher.rs b/desktop/wrapper/src/message_dispatcher.rs index 3c3852cf12..6e925ea169 100644 --- a/desktop/wrapper/src/message_dispatcher.rs +++ b/desktop/wrapper/src/message_dispatcher.rs @@ -65,9 +65,9 @@ impl<'a> DesktopWrapperMessageDispatcher<'a> { .editor .handle_message(EditorMessage::Batched { messages: std::mem::take(&mut self.editor_message_queue).into_boxed_slice(), - }) - .into_iter() - .filter_map(|m| intercept_frontend_message(self, m)); + }); + // .into_iter() + // .filter_map(|m| intercept_frontend_message(self, m)); frontend_messages.extend(current_frontend_messages); } diff --git a/editor/src/application.rs b/editor/src/application.rs index 08fba1f471..8af6b81697 100644 --- a/editor/src/application.rs +++ b/editor/src/application.rs @@ -1,8 +1,6 @@ use crate::dispatcher::Dispatcher; -use crate::messages::portfolio::document::node_graph::generate_node_graph_overlay::generate_node_graph_overlay; +use crate::dispatcher::EditorOutput; use crate::messages::prelude::*; -use graph_craft::document::{NodeInput, NodeNetwork}; -use graphene_std::node_graph_overlay::types::NodeGraphOverlayData; pub use graphene_std::uuid::*; // TODO: serialize with serde to save the current editor state @@ -24,41 +22,13 @@ impl Editor { (Self { dispatcher }, runtime) } - pub fn handle_message>(&mut self, message: T) -> Vec { - self.dispatcher.handle_message(message, true); - - std::mem::take(&mut self.dispatcher.responses) + pub fn handle_message>(&mut self, message: T) -> Vec { + self.dispatcher.handle_message(message, true) } pub fn poll_node_graph_evaluation(&mut self, responses: &mut VecDeque) -> Result<(), String> { self.dispatcher.poll_node_graph_evaluation(responses) } - - pub fn generate_node_graph_overlay_network(&mut self) -> Option { - let Some(active_document) = self.dispatcher.message_handlers.portfolio_message_handler.active_document_mut() else { - return None; - }; - let breadcrumb_network_path = &active_document.breadcrumb_network_path; - let nodes_to_render = active_document.network_interface.collect_nodes( - &active_document.node_graph_handler.node_graph_errors, - self.dispatcher.message_handlers.preferences_message_handler.graph_wire_style, - breadcrumb_network_path, - ); - let previewed_node = active_document.network_interface.previewed_node(breadcrumb_network_path); - let node_graph_render_data = NodeGraphOverlayData { - nodes_to_render, - open: active_document.graph_view_overlay_open, - in_selected_network: &active_document.selection_network_path == breadcrumb_network_path, - previewed_node, - }; - let opacity = active_document.graph_fade_artwork_percentage; - let node_graph_overlay_node = generate_node_graph_overlay(node_graph_render_data, opacity); - Some(NodeNetwork { - exports: vec![NodeInput::node(NodeId(0), 0)], - nodes: vec![(NodeId(0), node_graph_overlay_node)].into_iter().collect(), - ..Default::default() - }) - } } impl Default for Editor { diff --git a/editor/src/dispatcher.rs b/editor/src/dispatcher.rs index 5012a3dac4..24b9d90243 100644 --- a/editor/src/dispatcher.rs +++ b/editor/src/dispatcher.rs @@ -1,3 +1,7 @@ +use std::time::Duration; + +use interpreted_executor::ui_runtime::CompilationRequest; + use crate::messages::debug::utility_types::MessageLoggingVerbosity; use crate::messages::defer::DeferMessageContext; use crate::messages::dialog::DialogMessageContext; @@ -7,7 +11,6 @@ use crate::messages::prelude::*; #[derive(Debug, Default)] pub struct Dispatcher { message_queues: Vec>, - pub responses: Vec, pub message_handlers: DispatcherMessageHandlers, } @@ -28,6 +31,15 @@ pub struct DispatcherMessageHandlers { tool_message_handler: ToolMessageHandler, } +// Output messages are what the editor returns after processing Messages. It is handled by the scope outside the editor, +// which has access to the node graph executor, frontend, etc +pub enum EditorOutput { + // These messages perform some side effect other than updating the frontend, but outside the scope of the editor + RequestNativeNodeGraphRender { compilation_request: CompilationRequest }, + RequestDeferredMessage { message: Box, timeout: Duration }, + FrontendMessage { frontend_message: FrontendMessage }, +} + impl DispatcherMessageHandlers { pub fn with_executor(executor: crate::node_graph_executor::NodeGraphExecutor) -> Self { Self { @@ -41,7 +53,6 @@ impl DispatcherMessageHandlers { /// The last occurrence of the message in the message queue is sufficient to ensure correct behavior. /// In addition, these messages do not change any state in the backend (aside from caches). const SIDE_EFFECT_FREE_MESSAGES: &[MessageDiscriminant] = &[ - MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::NodeGraph(NodeGraphMessageDiscriminant::SendGraph))), MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::PropertiesPanel( PropertiesPanelMessageDiscriminant::Refresh, ))), @@ -93,12 +104,14 @@ impl Dispatcher { } } - pub fn handle_message>(&mut self, message: T, process_after_all_current: bool) { + pub fn handle_message>(&mut self, message: T, process_after_all_current: bool) -> Vec { let message = message.into(); - // If we are not maintaining the buffer, simply add to the current queue Self::schedule_execution(&mut self.message_queues, process_after_all_current, [message]); + let mut side_effects = Vec::new(); + let mut output_messages = Vec::new(); + while let Some(message) = self.message_queues.last_mut().and_then(VecDeque::pop_front) { // Skip processing of this message if it will be processed later (at the end of the shallowest level queue) if SIDE_EFFECT_FREE_MESSAGES.contains(&message.to_discriminant()) { @@ -148,20 +161,8 @@ impl Dispatcher { self.message_handlers.dialog_message_handler.process_message(message, &mut queue, context); } Message::Frontend(message) => { - // Handle these messages immediately by returning early - if let FrontendMessage::TriggerFontLoad { .. } = message { - self.responses.push(message); - self.cleanup_queues(false); - - // Return early to avoid running the code after the match block - return; - } else { - // `FrontendMessage`s are saved and will be sent to the frontend after the message queue is done being processed - // Deduplicate the render native node graph messages. TODO: Replace responses with hashset - if !(message == FrontendMessage::RequestNativeNodeGraphRender && self.responses.contains(&FrontendMessage::RequestNativeNodeGraphRender)) { - self.responses.push(message); - } - } + // `FrontendMessage`s are saved and will be sent to the frontend after the message queue is done being processed + output_messages.push(EditorOutput::FrontendMessage { frontend_message: message }); } Message::Globals(message) => { self.message_handlers.globals_message_handler.process_message(message, &mut queue, ()); @@ -213,14 +214,19 @@ impl Dispatcher { Message::Preferences(message) => { self.message_handlers.preferences_message_handler.process_message(message, &mut queue, ()); } + Message::SideEffect(message) => { + if !side_effects.contains(&message) { + side_effects.push(message); + } + } Message::Tool(message) => { let Some(document_id) = self.message_handlers.portfolio_message_handler.active_document_id() else { warn!("Called ToolMessage without an active document.\nGot {message:?}"); - return; + return Vec::new(); }; let Some(document) = self.message_handlers.portfolio_message_handler.documents.get_mut(&document_id) else { warn!("Called ToolMessage with an invalid active document.\nGot {message:?}"); - return; + return Vec::new(); }; let context = ToolMessageContext { @@ -236,7 +242,9 @@ impl Dispatcher { } Message::NoOp => {} Message::Batched { messages } => { - messages.iter().for_each(|message| self.handle_message(message.to_owned(), false)); + for nested_outputs in messages.into_iter().map(|message| self.handle_message(message, false)) { + output_messages.extend(nested_outputs); + } } } @@ -247,6 +255,12 @@ impl Dispatcher { self.cleanup_queues(false); } + + // The full message tree has been processed, so the side effects can be processed + for message in side_effects { + output_messages.extend(self.handle_side_effect(message)); + } + output_messages } pub fn collect_actions(&self) -> ActionList { @@ -314,6 +328,7 @@ impl Dispatcher { #[cfg(test)] mod test { + use crate::messages::side_effects::EditorOutputMessage; pub use crate::test_utils::test_prelude::*; /// Create an editor with three layers @@ -513,7 +528,10 @@ mod test { for response in responses { // Check for the existence of the file format incompatibility warning dialog after opening the test file - if let FrontendMessage::UpdateDialogColumn1 { layout_target: _, diff } = response { + if let EditorOutputMessage::FrontendMessage { + frontend_message: FrontendMessage::UpdateDialogColumn1 { layout_target: _, diff }, + } = response + { if let DiffUpdate::SubLayout(sub_layout) = &diff[0].new_value { if let LayoutGroup::Row { widgets } = &sub_layout[0] { if let Widget::TextLabel(TextLabel { value, .. }) = &widgets[0].widget { diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index 180f5b1cf3..3ec9fe88ea 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -267,7 +267,6 @@ pub enum FrontendMessage { UpdateMouseCursor { cursor: MouseCursorIcon, }, - RequestNativeNodeGraphRender, UpdateNativeNodeGraphSVG { #[serde(rename = "svgString")] svg_string: String, @@ -293,6 +292,10 @@ pub enum FrontendMessage { layout_target: LayoutTarget, diff: Vec, }, + UpdateTooltip { + position: Option, + text: String, + }, UpdateToolOptionsLayout { #[serde(rename = "layoutTarget")] layout_target: LayoutTarget, diff --git a/editor/src/messages/message.rs b/editor/src/messages/message.rs index b23d530e29..d92cfd44b0 100644 --- a/editor/src/messages/message.rs +++ b/editor/src/messages/message.rs @@ -1,4 +1,4 @@ -use crate::messages::prelude::*; +use crate::messages::{prelude::*, side_effects::{SideEffectMessage}}; use graphite_proc_macros::*; #[impl_message] @@ -18,6 +18,8 @@ pub enum Message { #[child] Dialog(DialogMessage), #[child] + SideEffect(SideEffectMessage), + #[child] Frontend(FrontendMessage), #[child] Globals(GlobalsMessage), diff --git a/editor/src/messages/mod.rs b/editor/src/messages/mod.rs index 6b2656df84..1e3adc6fc0 100644 --- a/editor/src/messages/mod.rs +++ b/editor/src/messages/mod.rs @@ -15,4 +15,5 @@ pub mod message; pub mod portfolio; pub mod preferences; pub mod prelude; +pub mod side_effects; pub mod tool; diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 46a16a1c3a..2bfd382447 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -1481,7 +1481,8 @@ impl MessageHandler> for DocumentMes x: transform.translation.x, y: transform.translation.y, }, - }) + }); + responses.add(NodeGraphMessage::PointerMove { shift: Key::Shift }); } } DocumentMessage::SelectionStepBack => { diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs index 6f0b606ed0..b79ffc9068 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs @@ -214,6 +214,7 @@ pub enum NodeGraphMessage { SetLockedOrVisibilitySideEffects { node_ids: Vec, }, + TryDisplayTooltip, UpdateBoxSelection, UpdateImportsExports, UpdateLayerPanel, 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 a83056207e..8bfd806a9e 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 @@ -15,6 +15,7 @@ use crate::messages::portfolio::document::utility_types::network_interface::{ use crate::messages::portfolio::document::utility_types::nodes::{CollapsedLayers, LayerPanelEntry}; use crate::messages::portfolio::document::utility_types::wires::{GraphWireStyle, WirePathInProgress, build_vector_wire}; use crate::messages::prelude::*; +use crate::messages::side_effects::SideEffectMessage; use crate::messages::tool::common_functionality::auto_panning::AutoPanning; use crate::messages::tool::common_functionality::graph_modification_utils::get_clip_mode; use crate::messages::tool::common_functionality::utility_functions::make_path_editable_is_allowed; @@ -30,6 +31,7 @@ use graphene_std::*; use kurbo::{DEFAULT_ACCURACY, Shape}; use renderer::Quad; use std::cmp::Ordering; +use std::time::Duration; #[derive(Debug, ExtractField)] pub struct NodeGraphMessageContext<'a> { @@ -92,6 +94,10 @@ pub struct NodeGraphMessageHandler { reordering_export: Option, /// The end index of the moved connector end_index: Option, + // If an input is being hovered. Used for tooltip + hovering_input: bool, + // If an output is being hovered. Used for tooltip + hovering_output: bool, } /// NodeGraphMessageHandler always modifies the network which the selected nodes are in. No GraphOperationMessages should be added here, since those messages will always affect the document network. @@ -1141,6 +1147,31 @@ impl<'a> MessageHandler> for NodeG .unwrap_or_else(|| modify_import_export.reorder_imports_exports.input_ports().count() + 1), ); responses.add(FrontendMessage::UpdateExportReorderIndex { index: self.end_index }); + } else if !self.hovering_input && !self.hovering_output { + if network_interface.input_connector_from_click(ipp.mouse.position, breadcrumb_network_path).is_some() { + self.hovering_input = true; + responses.add(SideEffectMessage::RequestDeferredMessage { + message: Box::new(NodeGraphMessage::TryDisplayTooltip.into()), + timeout: Duration::from_millis(800), + }); + } + if network_interface.output_connector_from_click(ipp.mouse.position, breadcrumb_network_path).is_some() { + self.hovering_output = true; + responses.add(SideEffectMessage::RequestDeferredMessage { + message: Box::new(NodeGraphMessage::TryDisplayTooltip.into()), + timeout: Duration::from_millis(800), + }) + } + } else if self.hovering_input { + if !network_interface.input_connector_from_click(ipp.mouse.position, breadcrumb_network_path).is_some() { + self.hovering_input = false; + responses.add(FrontendMessage::UpdateTooltip { position: None, text: String::new() }); + } + } else if self.hovering_output { + if !network_interface.output_connector_from_click(ipp.mouse.position, breadcrumb_network_path).is_some() { + self.hovering_output = false; + responses.add(FrontendMessage::UpdateTooltip { position: None, text: String::new() }); + } } } NodeGraphMessage::PointerUp => { @@ -1594,7 +1625,7 @@ impl<'a> MessageHandler> for NodeG responses.add(PropertiesPanelMessage::Refresh); responses.add(NodeGraphMessage::UpdateActionButtons); - responses.add(FrontendMessage::RequestNativeNodeGraphRender); + responses.add(SideEffectMessage::RenderNodeGraph); responses.add(NodeGraphMessage::UpdateImportsExports); self.update_node_graph_hints(responses); } @@ -1820,6 +1851,36 @@ impl<'a> MessageHandler> for NodeG responses.add(PropertiesPanelMessage::Refresh); } + NodeGraphMessage::TryDisplayTooltip => { + if let Some(input) = network_interface.input_connector_from_click(ipp.mouse.position, breadcrumb_network_path) { + let text = network_interface.input_tooltip_text(&input, breadcrumb_network_path); + if let Some(position) = network_interface.input_position(&input, breadcrumb_network_path) { + let Some(network_metadata) = network_interface.network_metadata(breadcrumb_network_path) else { + return; + }; + let position = network_metadata.persistent_metadata.navigation_metadata.node_graph_to_viewport.transform_point2(position); + let xy = FrontendXY { + x: position.x as i32, + y: position.y as i32, + }; + responses.add(FrontendMessage::UpdateTooltip { position: Some(xy), text }); + } + } else if let Some(output) = network_interface.output_connector_from_click(ipp.mouse.position, breadcrumb_network_path) { + let text = network_interface.output_tooltip_text(&output, breadcrumb_network_path); + if let Some(position) = network_interface.output_position(&output, breadcrumb_network_path) { + let Some(network_metadata) = network_interface.network_metadata(breadcrumb_network_path) else { + return; + }; + let position = network_metadata.persistent_metadata.navigation_metadata.node_graph_to_viewport.transform_point2(position); + + let xy = FrontendXY { + x: position.x as i32, + y: position.y as i32, + }; + responses.add(FrontendMessage::UpdateTooltip { position: Some(xy), text }); + } + } + } NodeGraphMessage::UpdateBoxSelection => { if let Some((box_selection_start, _)) = self.box_selection_start { // The mouse button was released but we missed the pointer up event @@ -2559,6 +2620,8 @@ impl Default for NodeGraphMessageHandler { reordering_export: None, reordering_import: None, end_index: None, + hovering_input: false, + hovering_output: false, } } } diff --git a/editor/src/messages/portfolio/document/node_graph/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_properties.rs index f29d8148b2..2481f492ca 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -1024,13 +1024,6 @@ pub fn get_document_node<'a>(node_id: NodeId, context: &'a NodePropertiesContext network.nodes.get(&node_id).ok_or(format!("node {node_id} not found in get_document_node")) } -pub fn query_node_and_input_info<'a>(node_id: NodeId, input_index: usize, context: &'a mut NodePropertiesContext<'a>) -> Result<(&'a DocumentNode, String, String), String> { - let (name, description) = context.network_interface.displayed_input_name_and_description(&node_id, input_index, context.selection_network_path); - let document_node = get_document_node(node_id, context)?; - - Ok((document_node, name, description)) -} - pub fn query_noise_pattern_state(node_id: NodeId, context: &NodePropertiesContext) -> Result<(bool, bool, bool, bool, bool, bool), String> { let document_node = get_document_node(node_id, context)?; let current_noise_type = document_node.inputs.iter().find_map(|input| match input.as_value() { diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface/node_graph.rs b/editor/src/messages/portfolio/document/utility_types/network_interface/node_graph.rs index 00b313b301..17c3713523 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface/node_graph.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface/node_graph.rs @@ -144,24 +144,9 @@ impl NodeNetworkInterface { } let input_type = self.input_type(input_connector, network_path); let data_type = input_type.displayed_type(); - let resolved_type = input_type.resolved_type_name(); - let connected_to = self - .upstream_output_connector(input_connector, network_path) - .map(|output_connector| match output_connector { - OutputConnector::Node { node_id, output_index } => { - let mut name = self.display_name(&node_id, network_path); - if cfg!(debug_assertions) { - name.push_str(&format!(" (id: {node_id})")); - } - format!("{name} output {output_index}") - } - OutputConnector::Import(import_index) => format!("Import index {import_index}"), - }) - .unwrap_or("nothing".to_string()); - - let (name, description) = match input_connector { - InputConnector::Node { node_id, input_index } => self.displayed_input_name_and_description(node_id, *input_index, network_path), + let name = match input_connector { + InputConnector::Node { node_id, input_index } => self.displayed_input_name_and_description(node_id, *input_index, network_path).0, InputConnector::Export(export_index) => { // Get export name from parent node metadata input, which must match the number of exports. // Empty string means to use type, or "Export + index" if type is empty determined @@ -173,44 +158,26 @@ impl NodeNetworkInterface { .unwrap_or_default() }; - let export_name = if !export_name.is_empty() { + if !export_name.is_empty() { export_name } else if let Some(export_type_name) = input_type.compiled_nested_type_name() { export_type_name } else { format!("Export index {}", export_index) - }; - - (export_name, String::new()) + } } }; - // TODO: Move in separate Tooltip overlay - // let valid_types = match self.valid_input_types(&input_connector, network_path) { - // Ok(input_types) => input_types.iter().map(|ty| ty.to_string()).collect(), - // Err(e) => { - // log::error!("Error getting valid types for input {input_connector:?}: {e}"); - // Vec::new() - // } - // }; - let connected_to_node = self.upstream_output_connector(input_connector, network_path).and_then(|output_connector| output_connector.node_id()); - Some(FrontendGraphInput { - data_type, - resolved_type, - name, - description, - connected_to, - connected_to_node, - }) + Some(FrontendGraphInput { data_type, name, connected_to_node }) } /// Returns None if there is an error, it is the document network, a hidden primary output or import pub fn frontend_output_from_connector(&mut self, output_connector: &OutputConnector, network_path: &[NodeId]) -> Option { let output_type = self.output_type(output_connector, network_path); - let (name, description) = match output_connector { + let name = match output_connector { OutputConnector::Node { node_id, output_index } => { // Do not display the primary output port for a node if it is a network node with a hidden primary export if *output_index == 0 && self.hidden_primary_output(node_id, network_path) { @@ -220,8 +187,7 @@ impl NodeNetworkInterface { let node_metadata = self.node_metadata(node_id, network_path)?; let output_name = node_metadata.persistent_metadata.output_names.get(*output_index).cloned().unwrap_or_default(); - let output_name = if !output_name.is_empty() { output_name } else { output_type.resolved_type_name() }; - (output_name, String::new()) + if !output_name.is_empty() { output_name } else { output_type.resolved_type_name() } } OutputConnector::Import(import_index) => { // Get the import name from the encapsulating node input metadata @@ -233,53 +199,19 @@ impl NodeNetworkInterface { if *import_index == 0 && self.hidden_primary_import(network_path) { return None; }; - let (import_name, description) = self.displayed_input_name_and_description(encapsulating_node_id, *import_index, encapsulating_path); + let import_name = self.displayed_input_name_and_description(encapsulating_node_id, *import_index, encapsulating_path).0; - let import_name = if !import_name.is_empty() { + if !import_name.is_empty() { import_name } else if let Some(import_type_name) = output_type.compiled_nested_type_name() { import_type_name } else { format!("Import index {}", *import_index) - }; - - (import_name, description) + } } }; let data_type = output_type.displayed_type(); - let resolved_type = output_type.resolved_type_name(); - let mut connected_to = self - .outward_wires(network_path) - .and_then(|outward_wires| outward_wires.get(output_connector)) - .cloned() - .unwrap_or_else(|| { - log::error!("Could not get {output_connector:?} in outward wires"); - Vec::new() - }) - .iter() - .map(|input| match input { - InputConnector::Node { node_id, input_index } => { - let mut name = self.display_name(node_id, network_path); - if cfg!(debug_assertions) { - name.push_str(&format!(" (id: {node_id})")); - } - format!("{name} input {input_index}") - } - InputConnector::Export(export_index) => format!("Export index {export_index}"), - }) - .collect::>(); - - if connected_to.is_empty() { - connected_to.push("nothing".to_string()); - } - - Some(FrontendGraphOutput { - data_type, - resolved_type, - name, - description, - connected_to, - }) + Some(FrontendGraphOutput { data_type, name }) } pub fn chain_width(&self, node_id: &NodeId, network_path: &[NodeId]) -> u32 { @@ -545,4 +477,79 @@ impl NodeNetworkInterface { Some(vector_wire) } + + pub fn input_tooltip_text(&mut self, input_connector: &InputConnector, network_path: &[NodeId]) -> String { + let input_type = self.input_type(input_connector, network_path); + let data_type_str = format!("Data Type: {input_type:?}"); + + let connected_to = self + .upstream_output_connector(input_connector, network_path) + .map(|output_connector| match output_connector { + OutputConnector::Node { node_id, output_index } => { + let mut name = self.display_name(&node_id, network_path); + if cfg!(debug_assertions) { + name.push_str(&format!(" (id: {node_id})")); + } + format!("{name} output {output_index}") + } + OutputConnector::Import(import_index) => format!("Import index {import_index}"), + }) + .unwrap_or("nothing".to_string()); + let connected_to_str = format!("Connected to: {connected_to}"); + + let valid_types = match self.valid_input_types(input_connector, network_path) { + Ok(valid) => valid, + Err(e) => { + log::error!("Could not get valid types in input tooltip text: {e}"); + return String::new(); + } + }; + + let valid_types_str = if !valid_types.is_empty() { + let mut strings = valid_types.iter().map(|x| format!("• {x}")).collect::>(); + strings.sort(); + strings.join("\n") + } else { + "None".to_string() + }; + + let valid_types_str = format!("Valid Types:\n{}", valid_types_str); + + format!("{data_type_str}\n\n{connected_to_str}\n\n{valid_types_str}") + } + + pub fn output_tooltip_text(&mut self, output_connector: &OutputConnector, network_path: &[NodeId]) -> String { + let output_type = self.output_type(output_connector, network_path); + let data_type_str = format!("Data Type: {output_type:?}"); + + let mut connected_to = self + .outward_wires(network_path) + .and_then(|outward_wires| outward_wires.get(output_connector)) + .cloned() + .unwrap_or_else(|| { + log::error!("Could not get {output_connector:?} in outward wires"); + Vec::new() + }) + .iter() + .map(|input| match input { + InputConnector::Node { node_id, input_index } => { + let mut name = self.display_name(node_id, network_path); + if cfg!(debug_assertions) { + name.push_str(&format!(" (id: {node_id})")); + } + format!("{name} input {input_index}") + } + InputConnector::Export(export_index) => format!("Export index {export_index}"), + }) + .collect::>(); + + connected_to.sort(); + if connected_to.is_empty() { + connected_to.push("nothing".to_string()); + } + let connected_to = connected_to.join("\n"); + let connected_to_str = format!("Connected to:\n{connected_to}"); + + format!("{data_type_str}\n\n{connected_to_str}") + } } diff --git a/editor/src/messages/side_effects/mod.rs b/editor/src/messages/side_effects/mod.rs new file mode 100644 index 0000000000..1cd7c93a77 --- /dev/null +++ b/editor/src/messages/side_effects/mod.rs @@ -0,0 +1,5 @@ +mod side_effect_message; +mod side_effect_message_handler; + +#[doc(inline)] +pub use side_effect_message::{SideEffectMessage, SideEffectMessageDiscriminant}; diff --git a/editor/src/messages/side_effects/side_effect_message.rs b/editor/src/messages/side_effects/side_effect_message.rs new file mode 100644 index 0000000000..70ae6e82e1 --- /dev/null +++ b/editor/src/messages/side_effects/side_effect_message.rs @@ -0,0 +1,21 @@ +use std::time::Duration; + +use crate::messages::prelude::*; + +// Output messages are what the editor returns after processing Messages. It is handled by the scope outside the editor, +// which has access to the node graph executor, frontend, etc +#[impl_message(Message, SideEffect)] +#[derive(derivative::Derivative, Clone, serde::Serialize, serde::Deserialize)] +#[derivative(Debug, PartialEq)] +pub enum SideEffectMessage { + // These messaged are automatically deduplicated and used to produce EditorOutputMessages + // They are run at the end of the messages queue, and use the final editor state + RenderNodeGraph, + RefreshPropertiesPanel, + DrawOverlays, + RenderRulers, + RenderScrollbars, + UpdateLayerStructure, + TriggerFontLoad, + RequestDeferredMessage { message: Box, timeout: Duration }, +} diff --git a/editor/src/messages/side_effects/side_effect_message_handler.rs b/editor/src/messages/side_effects/side_effect_message_handler.rs new file mode 100644 index 0000000000..3dd7464331 --- /dev/null +++ b/editor/src/messages/side_effects/side_effect_message_handler.rs @@ -0,0 +1,53 @@ +use graph_craft::document::{NodeInput, NodeNetwork}; +use graphene_std::{node_graph_overlay::types::NodeGraphOverlayData, uuid::NodeId}; +use interpreted_executor::ui_runtime::CompilationRequest; + +use crate::{ + dispatcher::{Dispatcher, EditorOutput}, + messages::side_effects::SideEffectMessage, +}; + +impl Dispatcher { + pub fn handle_side_effect(&mut self, message: SideEffectMessage) -> Vec { + let mut responses = Vec::new(); + match message { + SideEffectMessage::RenderNodeGraph => { + if let Some(node_graph_overlay_network) = self.generate_node_graph_overlay_network() { + let compilation_request = CompilationRequest { network: node_graph_overlay_network }; + responses.push(EditorOutput::RequestNativeNodeGraphRender { compilation_request }); + } + } + SideEffectMessage::RequestDeferredMessage { message, timeout } => { + responses.push(EditorOutput::RequestDeferredMessage { message, timeout }); + } + _ => todo!(), + }; + responses + } + + pub fn generate_node_graph_overlay_network(&mut self) -> Option { + let Some(active_document) = self.message_handlers.portfolio_message_handler.active_document_mut() else { + return None; + }; + let breadcrumb_network_path = &active_document.breadcrumb_network_path; + let nodes_to_render = active_document.network_interface.collect_nodes( + &active_document.node_graph_handler.node_graph_errors, + self.message_handlers.preferences_message_handler.graph_wire_style, + breadcrumb_network_path, + ); + let previewed_node = active_document.network_interface.previewed_node(breadcrumb_network_path); + let node_graph_render_data = NodeGraphOverlayData { + nodes_to_render, + open: active_document.graph_view_overlay_open, + in_selected_network: &active_document.selection_network_path == breadcrumb_network_path, + previewed_node, + }; + let opacity = active_document.graph_fade_artwork_percentage; + let node_graph_overlay_node = crate::messages::portfolio::document::node_graph::generate_node_graph_overlay::generate_node_graph_overlay(node_graph_render_data, opacity); + Some(NodeNetwork { + exports: vec![NodeInput::node(NodeId(0), 0)], + nodes: vec![(NodeId(0), node_graph_overlay_node)].into_iter().collect(), + ..Default::default() + }) + } +} diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index 84b0975ac4..b655046161 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -5,8 +5,8 @@ use graph_craft::document::value::{RenderOutput, TaggedValue}; use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput}; use graph_craft::proto::GraphErrors; use graph_craft::wasm_application_io::EditorPreferences; +use graphene_std::application_io::RenderConfig; use graphene_std::application_io::TimingInformation; -use graphene_std::application_io::{NodeGraphUpdateMessage, RenderConfig}; use graphene_std::renderer::{RenderMetadata, format_transform_matrix}; use graphene_std::text::FontCache; use graphene_std::transform::Footprint; @@ -44,7 +44,6 @@ pub struct CompilationResponse { pub enum NodeGraphUpdate { ExecutionResponse(ExecutionResponse), CompilationResponse(CompilationResponse), - NodeGraphUpdateMessage(NodeGraphUpdateMessage), } #[derive(Debug, Default)] diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index 217e5ad900..6eea0586dd 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -7,7 +7,7 @@ use graph_craft::graphene_compiler::Compiler; use graph_craft::proto::GraphErrors; use graph_craft::wasm_application_io::EditorPreferences; use graph_craft::{ProtoNodeIdentifier, concrete}; -use graphene_std::application_io::{ImageTexture, NodeGraphUpdateMessage, NodeGraphUpdateSender, RenderConfig}; +use graphene_std::application_io::{ImageTexture, RenderConfig}; use graphene_std::bounds::RenderBoundingBox; use graphene_std::memo::IORecord; use graphene_std::renderer::{Render, RenderParams, SvgRender}; @@ -94,12 +94,6 @@ impl InternalNodeGraphUpdateSender { } } -impl NodeGraphUpdateSender for InternalNodeGraphUpdateSender { - fn send(&self, message: NodeGraphUpdateMessage) { - self.0.send(NodeGraphUpdate::NodeGraphUpdateMessage(message)).expect("Failed to send response") - } -} - pub static NODE_RUNTIME: Lazy>> = Lazy::new(|| Mutex::new(None)); impl NodeRuntime { @@ -115,8 +109,6 @@ impl NodeRuntime { editor_api: WasmEditorApi { font_cache: FontCache::default(), editor_preferences: Box::new(EditorPreferences::default()), - node_graph_message_sender: Box::new(InternalNodeGraphUpdateSender(sender)), - application_io: None, } .into(), @@ -140,7 +132,6 @@ impl NodeRuntime { #[cfg(any(test, not(target_family = "wasm")))] application_io: Some(WasmApplicationIo::new_offscreen().await.into()), font_cache: self.editor_api.font_cache.clone(), - node_graph_message_sender: Box::new(self.sender.clone()), editor_preferences: Box::new(self.editor_preferences.clone()), } .into(); @@ -166,7 +157,6 @@ impl NodeRuntime { self.editor_api = WasmEditorApi { font_cache, application_io: self.editor_api.application_io.clone(), - node_graph_message_sender: Box::new(self.sender.clone()), editor_preferences: Box::new(self.editor_preferences.clone()), } .into(); @@ -180,7 +170,6 @@ impl NodeRuntime { self.editor_api = WasmEditorApi { font_cache: self.editor_api.font_cache.clone(), application_io: self.editor_api.application_io.clone(), - node_graph_message_sender: Box::new(self.sender.clone()), editor_preferences: Box::new(preferences), } .into(); @@ -410,7 +399,6 @@ pub async fn replace_application_io(application_io: WasmApplicationIo) { node_runtime.editor_api = WasmEditorApi { font_cache: node_runtime.editor_api.font_cache.clone(), application_io: Some(application_io.into()), - node_graph_message_sender: Box::new(node_runtime.sender.clone()), editor_preferences: Box::new(node_runtime.editor_preferences.clone()), } .into(); diff --git a/frontend/src/components/views/Graph.svelte b/frontend/src/components/views/Graph.svelte index 4fefc3f0bb..985f317d6c 100644 --- a/frontend/src/components/views/Graph.svelte +++ b/frontend/src/components/views/Graph.svelte @@ -180,33 +180,6 @@ return `M-2,-2 L${nodeWidth + 2},-2 L${nodeWidth + 2},${nodeHeight + 2} L-2,${nodeHeight + 2}z ${rectangles.join(" ")}`; } - function inputTooltip(value: FrontendGraphInput): string { - return dataTypeTooltip(value) + "\n\n" + inputConnectedToText(value) + "\n\n"; - } - - function outputTooltip(value: FrontendGraphOutput): string { - return dataTypeTooltip(value) + "\n\n" + outputConnectedToText(value); - } - - function dataTypeTooltip(value: FrontendGraphInput | FrontendGraphOutput): string { - return `Data Type: ${value.resolvedType}`; - } - - // function validTypesText(value: FrontendGraphInput): string { - // const validTypes = value.validTypes.length > 0 ? value.validTypes.map((x) => `• ${x}`).join("\n") : "None"; - // return `Valid Types:\n${validTypes}`; - // } - - function outputConnectedToText(output: FrontendGraphOutput): string { - if (output.connectedTo.length === 0) return "Connected to nothing"; - - return `Connected to:\n${output.connectedTo.join("\n")}`; - } - - function inputConnectedToText(input: FrontendGraphInput): string { - return `Connected to:\n${input.connectedToString}`; - } - function collectExposedInputsOutputs( inputs: (FrontendGraphInput | undefined)[], outputs: (FrontendGraphOutput | undefined)[], @@ -326,7 +299,6 @@ style:--offset-left={($nodeGraph.updateImportsExports.importPosition.x - 8) / 24} style:--offset-top={($nodeGraph.updateImportsExports.importPosition.y - 8) / 24 + index} > - {outputTooltip(frontendOutput)} {#if frontendOutput.connectedTo.length > 0} {:else} @@ -407,7 +379,6 @@ style:--offset-left={($nodeGraph.updateImportsExports.exportPosition.x - 8) / 24} style:--offset-top={($nodeGraph.updateImportsExports.exportPosition.y - 8) / 24 + index} > - {inputTooltip(frontendInput)} {#if frontendInput.connectedTo !== "nothing"} {:else} @@ -511,6 +482,15 @@ + +{#if $nodeGraph.tooltipPosition} +
+
+ {$nodeGraph.tooltipText} +
+
+{/if} + {#if $nodeGraph.selectionBox}
FrontendDocumentDetails) readonly openDocuments!: FrontendDocumentDetails[]; @@ -1736,6 +1741,7 @@ export const messageMakers: Record = { UpdateLayersPanelState, UpdateToolOptionsLayout, UpdateToolShelfLayout, + UpdateTooltip, UpdateViewportHolePunch, UpdateWirePathInProgress, UpdateWorkingColorsLayout, diff --git a/frontend/src/state-providers/node-graph.ts b/frontend/src/state-providers/node-graph.ts index 016ec52f67..723686a686 100644 --- a/frontend/src/state-providers/node-graph.ts +++ b/frontend/src/state-providers/node-graph.ts @@ -14,12 +14,13 @@ import { UpdateImportReorderIndex, UpdateExportReorderIndex, UpdateImportsExports, - UpdateLayerWidths, UpdateNativeNodeGraphSVG, UpdateNodeThumbnail, UpdateWirePathInProgress, UpdateNodeGraphSelectionBox, UpdateNodeGraphTransform, + UpdateTooltip, + type XY, } from "@graphite/messages"; // eslint-disable-next-line @typescript-eslint/explicit-function-return-type @@ -38,11 +39,8 @@ export function createNodeGraphState(editor: Editor) { nodeTypes: [] as FrontendNodeType[], nodeDescriptions: new Map(), - // Data that will be moved into the node graph to be rendered natively - nodesToRender: new Map(), - opacity: 0.8, - inSelectedNetwork: true, - previewedNode: undefined as bigint | undefined, + tooltipPosition: undefined as XY | undefined, + tooltipText: "test", // Data that will be passed in the context thumbnails: new Map(), @@ -118,6 +116,13 @@ export function createNodeGraphState(editor: Editor) { return state; }); }); + editor.subscriptions.subscribeJsMessage(UpdateTooltip, (updateTooltip) => { + update((state) => { + state.tooltipPosition = updateTooltip.position; + state.tooltipText = updateTooltip.text; + return state; + }); + }); return { subscribe, diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index b9a9b81234..dc05eb93e7 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -9,6 +9,7 @@ use crate::helpers::translate_key; use crate::wasm_node_graph_ui_executor::WasmNodeGraphUIExecutor; use crate::{EDITOR_HANDLE, EDITOR_HAS_CRASHED, Error, MESSAGE_BUFFER, WASM_NODE_GRAPH_EXECUTOR}; use editor::consts::FILE_EXTENSION; +use editor::dispatcher::EditorOutput; use editor::messages::input_mapper::utility_types::input_keyboard::ModifierKeys; use editor::messages::input_mapper::utility_types::input_mouse::{EditorMouseState, ScrollDelta, ViewportBounds}; use editor::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; @@ -19,7 +20,6 @@ use editor::messages::tool::tool_messages::tool_prelude::WidgetId; use graph_craft::document::NodeId; use graphene_std::raster::Image; use graphene_std::raster::color::Color; -use interpreted_executor::ui_runtime::CompilationRequest; use js_sys::{Object, Reflect}; use serde::Serialize; use serde_wasm_bindgen::{self, from_value}; @@ -221,20 +221,32 @@ impl EditorHandle { } // Messages can come from the runtime, browser, or a timed callback. This processes them in the editor and does all the side effects - // Like updating the frontend and node graph ui network. - fn process_messages(&self, messages: impl IntoIterator, editor: &mut Editor) { + // Like updating the frontend and node graph ui network. Some side effects are deduplicated and produce other side effects. + fn process_messages(&self, messages: impl IntoIterator, editor_param: &mut Editor) { // Get the editor, dispatch the message, and store the `FrontendMessage` queue response - for side_effect in messages.into_iter().flat_map(|message| editor.handle_message(message)).collect::>() { - if side_effect == FrontendMessage::RequestNativeNodeGraphRender { - if let Some(node_graph_overlay_network) = editor.generate_node_graph_overlay_network() { - let compilation_request = CompilationRequest { network: node_graph_overlay_network }; + for output in messages.into_iter().flat_map(|message| editor_param.handle_message(message)).collect::>() { + match output { + EditorOutput::RequestNativeNodeGraphRender { compilation_request } => { let res = executor(|executor| executor.compilation_request(compilation_request)); if let Err(_) = res { log::error!("Could not borrow executor in process_messages_in_editor"); } } - } else { - self.send_frontend_message_to_js(side_effect); + EditorOutput::RequestDeferredMessage { message, timeout } => { + let callback = Closure::once_into_js(move || { + editor_and_handle(|editor, handle| { + handle.process_messages(std::iter::once(*message), editor); + }); + }); + + window() + .unwrap() + .set_timeout_with_callback_and_timeout_and_arguments_0(callback.as_ref().unchecked_ref(), timeout.as_millis() as i32) + .unwrap(); + } + EditorOutput::FrontendMessage { frontend_message } => { + self.send_frontend_message_to_js(frontend_message); + } } } } diff --git a/node-graph/gapplication-io/src/lib.rs b/node-graph/gapplication-io/src/lib.rs index b9d072d2c0..5d6dac78d7 100644 --- a/node-graph/gapplication-io/src/lib.rs +++ b/node-graph/gapplication-io/src/lib.rs @@ -201,19 +201,6 @@ pub enum ApplicationError { InvalidUrl, } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub enum NodeGraphUpdateMessage {} - -pub trait NodeGraphUpdateSender { - fn send(&self, message: NodeGraphUpdateMessage); -} - -impl NodeGraphUpdateSender for std::sync::Mutex { - fn send(&self, message: NodeGraphUpdateMessage) { - self.lock().as_mut().unwrap().send(message) - } -} - pub trait GetEditorPreferences { fn use_vello(&self) -> bool; } @@ -245,14 +232,6 @@ pub struct RenderConfig { pub for_export: bool, } -struct Logger; - -impl NodeGraphUpdateSender for Logger { - fn send(&self, message: NodeGraphUpdateMessage) { - log::warn!("dispatching message with fallback node graph update sender {message:?}"); - } -} - struct DummyPreferences; impl GetEditorPreferences for DummyPreferences { @@ -266,7 +245,6 @@ pub struct EditorApi { pub font_cache: FontCache, /// Gives access to APIs like a rendering surface (native window handle or HTML5 canvas) and WGPU (which becomes WebGPU on web). pub application_io: Option>, - pub node_graph_message_sender: Box, /// Editor preferences made available to the graph through the [`WasmEditorApi`]. pub editor_preferences: Box, } @@ -278,7 +256,6 @@ impl Default for EditorApi { Self { font_cache: FontCache::default(), application_io: None, - node_graph_message_sender: Box::new(Logger), editor_preferences: Box::new(DummyPreferences), } } @@ -288,7 +265,6 @@ impl Hash for EditorApi { fn hash(&self, state: &mut H) { self.font_cache.hash(state); self.application_io.as_ref().map_or(0, |io| io.as_ref() as *const _ as usize).hash(state); - (self.node_graph_message_sender.as_ref() as *const dyn NodeGraphUpdateSender).hash(state); (self.editor_preferences.as_ref() as *const dyn GetEditorPreferences).hash(state); } } @@ -297,7 +273,6 @@ impl PartialEq for EditorApi { fn eq(&self, other: &Self) -> bool { self.font_cache == other.font_cache && self.application_io.as_ref().map_or(0, |io| addr_of!(io) as usize) == other.application_io.as_ref().map_or(0, |io| addr_of!(io) as usize) - && std::ptr::eq(self.node_graph_message_sender.as_ref() as *const _, other.node_graph_message_sender.as_ref() as *const _) && std::ptr::eq(self.editor_preferences.as_ref() as *const _, other.editor_preferences.as_ref() as *const _) } } diff --git a/node-graph/gcore/src/node_graph_overlay/nodes_and_wires.rs b/node-graph/gcore/src/node_graph_overlay/nodes_and_wires.rs index e32aba48c6..6c7069f6a5 100644 --- a/node-graph/gcore/src/node_graph_overlay/nodes_and_wires.rs +++ b/node-graph/gcore/src/node_graph_overlay/nodes_and_wires.rs @@ -168,8 +168,6 @@ pub fn draw_nodes(nodes: &Vec) -> Table { // } let node_text_row = TableRow::new_from_element(Graphic::Vector(node_text)); - // node_text_row.transform.left_apply_transform(&DAffine2::from_translation(DVec2::new(x + 8., y + 8.))); - // log::debug!("node_text_row {:?}", node_text_row.transform); node_table.push(node_text_row); // Add black clipping path to view text in node diff --git a/node-graph/gcore/src/node_graph_overlay/types.rs b/node-graph/gcore/src/node_graph_overlay/types.rs index abf537b38b..cffd435e8e 100644 --- a/node-graph/gcore/src/node_graph_overlay/types.rs +++ b/node-graph/gcore/src/node_graph_overlay/types.rs @@ -153,13 +153,7 @@ pub enum NodeOrLayer { pub struct FrontendGraphInput { #[serde(rename = "dataType")] pub data_type: FrontendGraphDataType, - #[serde(rename = "resolvedType")] - pub resolved_type: String, pub name: String, - pub description: String, - /// Either "nothing", "import index {index}", or "{node name} output {output_index}". - #[serde(rename = "connectedToString")] - pub connected_to: String, /// Used to render the upstream node once this node is rendered #[serde(rename = "connectedToNode")] pub connected_to_node: Option, @@ -170,13 +164,6 @@ pub struct FrontendGraphOutput { #[serde(rename = "dataType")] pub data_type: FrontendGraphDataType, pub name: String, - #[serde(rename = "resolvedType")] - pub resolved_type: String, - pub description: String, - /// If connected to an export, it is "export index {index}". - /// If connected to a node, it is "{node name} input {input_index}". - #[serde(rename = "connectedTo")] - pub connected_to: Vec, } #[derive(Clone, Debug, Default, PartialEq, Hash, dyn_any::DynAny, serde::Serialize, serde::Deserialize, specta::Type)] diff --git a/node-graph/graphene-cli/src/main.rs b/node-graph/graphene-cli/src/main.rs index 39de026cd9..9f4244789c 100644 --- a/node-graph/graphene-cli/src/main.rs +++ b/node-graph/graphene-cli/src/main.rs @@ -92,7 +92,6 @@ async fn main() -> Result<(), Box> { let editor_api = Arc::new(WasmEditorApi { font_cache: FontCache::default(), application_io: Some(application_io.into()), - node_graph_message_sender: Box::new(UpdateLogger {}), editor_preferences: Box::new(preferences), }); From 4ff9f91ad61500ed2e2f033745d92f701b0847e8 Mon Sep 17 00:00:00 2001 From: Adam Date: Sat, 6 Sep 2025 21:27:24 -0700 Subject: [PATCH 6/8] thumbnails --- editor/src/dispatcher.rs | 1 + .../document/node_graph/node_graph_message.rs | 5 ++ .../node_graph/node_graph_message_handler.rs | 6 ++ .../side_effect_message_handler.rs | 1 + editor/src/node_graph_executor.rs | 38 ++++++---- editor/src/node_graph_executor/runtime.rs | 39 +++++----- node-graph/gcore/src/node_graph_overlay.rs | 2 +- .../src/node_graph_overlay/nodes_and_wires.rs | 43 ++++++++--- .../gcore/src/node_graph_overlay/types.rs | 24 ++++++- node-graph/gsvg-renderer/src/renderer.rs | 71 +++++++++++++++++++ .../interpreted-executor/src/ui_runtime.rs | 1 + 11 files changed, 186 insertions(+), 45 deletions(-) diff --git a/editor/src/dispatcher.rs b/editor/src/dispatcher.rs index 24b9d90243..4f75879276 100644 --- a/editor/src/dispatcher.rs +++ b/editor/src/dispatcher.rs @@ -33,6 +33,7 @@ pub struct DispatcherMessageHandlers { // Output messages are what the editor returns after processing Messages. It is handled by the scope outside the editor, // which has access to the node graph executor, frontend, etc +#[derive(Debug)] pub enum EditorOutput { // These messages perform some side effect other than updating the frontend, but outside the scope of the editor RequestNativeNodeGraphRender { compilation_request: CompilationRequest }, diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs index b79ffc9068..9fa473a569 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs @@ -7,6 +7,7 @@ use glam::IVec2; use graph_craft::document::value::TaggedValue; use graph_craft::document::{NodeId, NodeInput}; use graph_craft::proto::GraphErrors; +use graphene_std::Graphic; use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypesDelta; #[impl_message(Message, DocumentMessage, NodeGraph)] @@ -219,6 +220,10 @@ pub enum NodeGraphMessage { UpdateImportsExports, UpdateLayerPanel, UpdateNewNodeGraph, + UpdateThumbnail { + node_id: NodeId, + graphic: Graphic, + }, UpdateTypes { #[serde(skip)] resolved_types: ResolvedDocumentNodeTypesDelta, 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 8bfd806a9e..7e8accf353 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 @@ -98,6 +98,8 @@ pub struct NodeGraphMessageHandler { hovering_input: bool, // If an output is being hovered. Used for tooltip hovering_output: bool, + // The rendered string for each thumbnail + pub thumbnails: HashMap, } /// NodeGraphMessageHandler always modifies the network which the selected nodes are in. No GraphOperationMessages should be added here, since those messages will always affect the document network. @@ -1985,6 +1987,9 @@ impl<'a> MessageHandler> for NodeG responses.add(NodeGraphMessage::SendGraph); } + NodeGraphMessage::UpdateThumbnail { node_id, graphic } => { + self.thumbnails.insert(node_id, graphic); + } NodeGraphMessage::UpdateTypes { resolved_types, node_graph_errors } => { network_interface.resolved_types.update(resolved_types); self.node_graph_errors = node_graph_errors; @@ -2622,6 +2627,7 @@ impl Default for NodeGraphMessageHandler { end_index: None, hovering_input: false, hovering_output: false, + thumbnails: HashMap::new(), } } } diff --git a/editor/src/messages/side_effects/side_effect_message_handler.rs b/editor/src/messages/side_effects/side_effect_message_handler.rs index 3dd7464331..fde4676c0b 100644 --- a/editor/src/messages/side_effects/side_effect_message_handler.rs +++ b/editor/src/messages/side_effects/side_effect_message_handler.rs @@ -41,6 +41,7 @@ impl Dispatcher { open: active_document.graph_view_overlay_open, in_selected_network: &active_document.selection_network_path == breadcrumb_network_path, previewed_node, + thumbnails: active_document.node_graph_handler.thumbnails.clone(), }; let opacity = active_document.graph_fade_artwork_percentage; let node_graph_overlay_node = crate::messages::portfolio::document::node_graph::generate_node_graph_overlay::generate_node_graph_overlay(node_graph_render_data, opacity); diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index b655046161..f66d1e8493 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -5,6 +5,7 @@ use graph_craft::document::value::{RenderOutput, TaggedValue}; use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput}; use graph_craft::proto::GraphErrors; use graph_craft::wasm_application_io::EditorPreferences; +use graphene_std::Graphic; use graphene_std::application_io::RenderConfig; use graphene_std::application_io::TimingInformation; use graphene_std::renderer::{RenderMetadata, format_transform_matrix}; @@ -29,10 +30,16 @@ pub struct ExecutionRequest { pub struct ExecutionResponse { execution_id: u64, result: Result, - responses: VecDeque, + execution_responses: Vec, vector_modify: HashMap, +} + +pub enum ExecutionResponseMessage { /// The resulting value from the temporary inspected during execution - inspect_result: Option, + InspectResult(Option), + UpdateNodeGraphThumbnail(NodeId, Graphic), + UpdateFrontendThumbnail(NodeId, String), + SendGraph, } #[derive(serde::Serialize, serde::Deserialize)] @@ -252,11 +259,24 @@ impl NodeGraphExecutor { let ExecutionResponse { execution_id, result, - responses: existing_responses, + execution_responses, vector_modify, - inspect_result, } = execution_response; - + for execution_response in execution_responses { + match execution_response { + ExecutionResponseMessage::InspectResult(inspect_result) => { + // 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() { + responses.add(DataPanelMessage::UpdateLayout { inspect_result }); + } else { + responses.add(DataPanelMessage::ClearLayout); + } + } + ExecutionResponseMessage::UpdateNodeGraphThumbnail(node_id, graphic) => responses.add(NodeGraphMessage::UpdateThumbnail { node_id, graphic }), + ExecutionResponseMessage::UpdateFrontendThumbnail(node_id, string) => responses.add(FrontendMessage::UpdateNodeThumbnail { id: node_id, value: string }), + ExecutionResponseMessage::SendGraph => responses.add(NodeGraphMessage::SendGraph), + } + } responses.add(OverlaysMessage::Draw); let node_graph_output = match result { @@ -269,7 +289,6 @@ impl NodeGraphExecutor { } }; - responses.extend(existing_responses.into_iter().map(Into::into)); document.network_interface.update_vector_modify(vector_modify); let execution_context = self.futures.remove(&execution_id).ok_or_else(|| "Invalid generation ID".to_string())?; @@ -283,13 +302,6 @@ impl NodeGraphExecutor { execution_id, document_id: execution_context.document_id, }); - - // 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() { - responses.add(DataPanelMessage::UpdateLayout { inspect_result }); - } else { - responses.add(DataPanelMessage::ClearLayout); - } } NodeGraphUpdate::CompilationResponse(execution_response) => { let CompilationResponse { node_graph_errors, result } = execution_response; diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index 6eea0586dd..84dd3fb74f 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -194,14 +194,14 @@ impl NodeRuntime { } GraphRuntimeRequest::ExecutionRequest(ExecutionRequest { execution_id, render_config, .. }) => { let result = self.execute_network(render_config).await; - let mut responses = VecDeque::new(); + let mut execution_responses = Vec::new(); // TODO: Only process monitor nodes if the graph has changed, not when only the Footprint changes - self.process_monitor_nodes(&mut responses, self.update_thumbnails); + self.process_monitor_nodes(&mut execution_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.and_then(|state| state.access(&self.executor)); - + execution_responses.push(ExecutionResponseMessage::InspectResult(inspect_result)); let texture = if let Ok(TaggedValue::RenderOutput(RenderOutput { data: RenderOutputType::Texture(texture), .. @@ -215,9 +215,8 @@ impl NodeRuntime { self.sender.send_execution_response(ExecutionResponse { execution_id, result, - responses, + execution_responses, vector_modify: self.vector_modify.clone(), - inspect_result, }); return texture; } @@ -271,10 +270,10 @@ impl NodeRuntime { } /// Updates state data - pub fn process_monitor_nodes(&mut self, responses: &mut VecDeque, update_thumbnails: bool) { + pub fn process_monitor_nodes(&mut self, responses: &mut Vec, 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))); - + let mut updated_thumbnails = false; 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)) { @@ -298,13 +297,17 @@ impl NodeRuntime { // Graphic table: thumbnail if let Some(io) = introspected_data.downcast_ref::>>() { if update_thumbnails { - Self::render_thumbnail(&mut self.thumbnail_renders, parent_network_node_id, &io.output, responses) + Self::render_thumbnail(&mut self.thumbnail_renders, parent_network_node_id, &io.output, responses); + responses.push(ExecutionResponseMessage::UpdateNodeGraphThumbnail(parent_network_node_id, io.output.clone().to_graphic())); + updated_thumbnails = true; } } // Artboard table: thumbnail else if let Some(io) = introspected_data.downcast_ref::>>() { if update_thumbnails { - Self::render_thumbnail(&mut self.thumbnail_renders, parent_network_node_id, &io.output, responses) + Self::render_thumbnail(&mut self.thumbnail_renders, parent_network_node_id, &io.output, responses); + responses.push(ExecutionResponseMessage::UpdateNodeGraphThumbnail(parent_network_node_id, io.output.clone().to_graphic())); + updated_thumbnails = true; } } // Vector table: vector modifications @@ -319,18 +322,21 @@ impl NodeRuntime { log::warn!("Failed to downcast monitor node output {parent_network_node_id:?}"); } } + if updated_thumbnails { + responses.push(ExecutionResponseMessage::SendGraph); + } } /// 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, responses: &mut VecDeque) { + fn render_thumbnail(thumbnail_renders: &mut HashMap>, parent_network_node_id: NodeId, graphic: &impl Render, responses: &mut Vec) { // 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: "Dense thumbnail omitted for performance".to_string(), - }); + responses.push(ExecutionResponseMessage::UpdateFrontendThumbnail( + parent_network_node_id, + "Dense thumbnail omitted for performance".to_string(), + )); } return; } @@ -364,10 +370,7 @@ impl NodeRuntime { 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(), - }); + responses.push(ExecutionResponseMessage::UpdateFrontendThumbnail(parent_network_node_id, new_thumbnail_svg.to_svg_string())); *old_thumbnail_svg = new_thumbnail_svg; } } diff --git a/node-graph/gcore/src/node_graph_overlay.rs b/node-graph/gcore/src/node_graph_overlay.rs index 874332d638..ac234a7664 100644 --- a/node-graph/gcore/src/node_graph_overlay.rs +++ b/node-graph/gcore/src/node_graph_overlay.rs @@ -21,7 +21,7 @@ pub mod ui_context; #[node_macro::node(skip_impl)] pub fn generate_nodes(_: impl Ctx, mut node_graph_overlay_data: NodeGraphOverlayData) -> Table { let mut nodes_and_wires = Table::new(); - let (layers, side_ports) = draw_layers(&node_graph_overlay_data.nodes_to_render); + let (layers, side_ports) = draw_layers(&mut node_graph_overlay_data); nodes_and_wires.extend(layers); let wires = draw_wires(&mut node_graph_overlay_data.nodes_to_render); diff --git a/node-graph/gcore/src/node_graph_overlay/nodes_and_wires.rs b/node-graph/gcore/src/node_graph_overlay/nodes_and_wires.rs index 6c7069f6a5..fa73ceccd4 100644 --- a/node-graph/gcore/src/node_graph_overlay/nodes_and_wires.rs +++ b/node-graph/gcore/src/node_graph_overlay/nodes_and_wires.rs @@ -8,15 +8,14 @@ use crate::{ consts::SOURCE_SANS_FONT_DATA, node_graph_overlay::{ consts::*, - types::{FrontendGraphDataType, FrontendNodeToRender}, + types::{FrontendGraphDataType, FrontendNodeToRender, NodeGraphOverlayData}, }, table::{Table, TableRow}, text::{self, TextAlign, TypesettingConfig}, transform::ApplyTransform, vector::{ Vector, - style::{Fill, Stroke, StrokeAlign}, - style::{Fill, Stroke, StrokeAlign}, + style::{Fill, Stroke}, }, }; @@ -210,10 +209,10 @@ pub fn draw_nodes(nodes: &Vec) -> Table { node_table } -pub fn draw_layers(nodes: &Vec) -> (Table, Table) { +pub fn draw_layers(nodes: &mut NodeGraphOverlayData) -> (Table, Table) { let mut layer_table = Table::new(); let mut side_ports_table = Table::new(); - for node_to_render in nodes { + for node_to_render in &nodes.nodes_to_render { if let Some(frontend_layer) = node_to_render.node_or_layer.layer.as_ref() { // The layer position is the top left of the thumbnail let layer_position = DVec2::new(frontend_layer.position.x as f64 * GRID_SIZE + 12., frontend_layer.position.y as f64 * GRID_SIZE); @@ -359,7 +358,7 @@ pub fn draw_layers(nodes: &Vec) -> (Table, Table< } let bottom_port = BezPath::from_svg("M0,0H8V8L5.479,6.319a2.666,2.666,0,0,0-2.959,0L0,8Z").unwrap(); let mut vector = Vector::from_bezpath(bottom_port); - let mut bottom_port_fill = if frontend_layer.bottom_input.connected_to_node.is_some() { + let bottom_port_fill = if frontend_layer.bottom_input.connected_to_node.is_some() { frontend_layer.bottom_input.data_type.data_color() } else { frontend_layer.bottom_input.data_type.data_color_dim() @@ -456,10 +455,34 @@ pub fn draw_layers(nodes: &Vec) -> (Table, Table< inner_thumbnail_table.push(TableRow::new_from_element(vector)); } } - let mut thumbnail_row = TableRow::new_from_element(Graphic::Vector(inner_thumbnail_table)); - thumbnail_row.alpha_blending.clip = true; - let graphic_table = Table::new_from_row(thumbnail_row); - layer_table.push(TableRow::new_from_element(Graphic::Graphic(graphic_table))); + let mut thumbnail_grid_row = TableRow::new_from_element(Graphic::Vector(inner_thumbnail_table)); + thumbnail_grid_row.alpha_blending.clip = true; + let mut clipped_thumbnail_table = Table::new(); + clipped_thumbnail_table.push(thumbnail_grid_row); + if let Some(thumbnail_graphic) = nodes.thumbnails.get_mut(&node_to_render.metadata.node_id) { + let thumbnail_graphic = std::mem::take(thumbnail_graphic); + let bbox = thumbnail_graphic.bounding_box(DAffine2::default(), false); + if let RenderBoundingBox::Rectangle(rect) = bbox { + let rect_size = rect[1] - rect[0]; + let target_size = DVec2::new(68., 44.); + // uniform scale that fits in target box + let scale_x = target_size.x / rect_size.x; + let scale_y = target_size.y / rect_size.y; + let scale = scale_x.min(scale_y); + + let translation = rect[0] * -scale; + let scaled_size = rect_size * scale; + let offset_to_center = (target_size - scaled_size) / 2.; + + let mut thumbnail_graphic_row = TableRow::new_from_element(thumbnail_graphic); + thumbnail_graphic_row.transform = DAffine2::from_translation(layer_position + offset_to_center) * DAffine2::from_scale_angle_translation(DVec2::splat(scale), 0., translation); + thumbnail_graphic_row.alpha_blending.clip = true; + + clipped_thumbnail_table.push(thumbnail_graphic_row); + } + } + + layer_table.push(TableRow::new_from_element(Graphic::Graphic(clipped_thumbnail_table))); } } diff --git a/node-graph/gcore/src/node_graph_overlay/types.rs b/node-graph/gcore/src/node_graph_overlay/types.rs index cffd435e8e..12bbb2a179 100644 --- a/node-graph/gcore/src/node_graph_overlay/types.rs +++ b/node-graph/gcore/src/node_graph_overlay/types.rs @@ -2,8 +2,11 @@ use glam::{DAffine2, DVec2}; use graphene_core_shaders::color::Color; use kurbo::BezPath; -use crate::{node_graph_overlay::consts::*, uuid::NodeId}; -use std::hash::{Hash, Hasher}; +use crate::{Graphic, node_graph_overlay::consts::*, uuid::NodeId}; +use std::{ + collections::HashMap, + hash::{Hash, Hasher}, +}; #[derive(Clone, Debug, Default, PartialEq, dyn_any::DynAny, serde::Serialize, serde::Deserialize, specta::Type)] pub struct NodeGraphTransform { @@ -27,13 +30,28 @@ impl NodeGraphTransform { } } -#[derive(Clone, Debug, Default, PartialEq, Hash, dyn_any::DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, Default, PartialEq, dyn_any::DynAny, serde::Serialize, serde::Deserialize)] pub struct NodeGraphOverlayData { pub nodes_to_render: Vec, pub open: bool, pub in_selected_network: bool, // Displays a dashed border around the node pub previewed_node: Option, + pub thumbnails: HashMap, +} + +impl Hash for NodeGraphOverlayData { + fn hash(&self, state: &mut H) { + self.nodes_to_render.hash(state); + self.open.hash(state); + self.in_selected_network.hash(state); + self.previewed_node.hash(state); + let mut entries: Vec<_> = self.thumbnails.iter().collect(); + entries.sort_by(|a, b| a.0.cmp(b.0)); + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + entries.hash(&mut hasher); + hasher.finish(); + } } #[derive(Clone, Debug, Default, PartialEq, dyn_any::DynAny, serde::Serialize, serde::Deserialize)] diff --git a/node-graph/gsvg-renderer/src/renderer.rs b/node-graph/gsvg-renderer/src/renderer.rs index 3917a14c3c..0b663563ad 100644 --- a/node-graph/gsvg-renderer/src/renderer.rs +++ b/node-graph/gsvg-renderer/src/renderer.rs @@ -9,6 +9,7 @@ use graphene_core::color::Color; use graphene_core::gradient::GradientStops; use graphene_core::gradient::GradientType; use graphene_core::math::quad::Quad; +use graphene_core::node_graph_overlay::consts::BEZ_PATH_TOLERANCE; use graphene_core::raster::BitmapMut; use graphene_core::raster::Image; use graphene_core::raster_types::{CPU, GPU, Raster}; @@ -22,6 +23,8 @@ use graphene_core::vector::click_target::{ClickTarget, FreePoint}; use graphene_core::vector::style::{Fill, PaintOrder, Stroke, StrokeAlign, ViewMode}; use graphene_core::{Artboard, Graphic}; use kurbo::Affine; +use kurbo::Rect; +use kurbo::Shape; use num_traits::Zero; use std::collections::{HashMap, HashSet}; use std::fmt::Write; @@ -231,6 +234,8 @@ pub trait Render: BoundingBox + RenderComplexity { #[cfg(feature = "vello")] fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, _render_params: &RenderParams); + fn to_graphic(self) -> Graphic; + /// The upstream click targets for each layer are collected during the render so that they do not have to be calculated for each click detection. fn add_upstream_click_targets(&self, _click_targets: &mut Vec) {} @@ -271,6 +276,17 @@ impl Render for Graphic { } } + fn to_graphic(self) -> Graphic { + match self { + Graphic::Graphic(table) => table.to_graphic(), + Graphic::Vector(table) => table.to_graphic(), + Graphic::RasterCPU(table) => table.to_graphic(), + Graphic::RasterGPU(table) => table.to_graphic(), + Graphic::Color(table) => table.to_graphic(), + Graphic::Gradient(table) => table.to_graphic(), + } + } + fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option) { if let Some(element_id) = element_id { match self { @@ -437,6 +453,23 @@ impl Render for Artboard { } } + fn to_graphic(self) -> Graphic { + let bg = Rect::new( + self.location.x as f64, + self.location.y as f64, + self.location.x as f64 + self.dimensions.x as f64, + self.location.y as f64 + self.dimensions.y as f64, + ); + let mut bg_vector = Vector::from_bezpath(bg.to_path(BEZ_PATH_TOLERANCE)); + bg_vector.style.fill = Fill::Solid(self.background); + let mut graphic_table = Table::new(); + graphic_table.push(TableRow::new_from_element(Graphic::Graphic(Table::new_from_element(Graphic::Vector(Table::new_from_element( + bg_vector, + )))))); + graphic_table.push(TableRow::new_from_element(Graphic::Graphic(self.content))); + Graphic::Graphic(graphic_table) + } + fn collect_metadata(&self, metadata: &mut RenderMetadata, mut footprint: Footprint, element_id: Option) { if let Some(element_id) = element_id { let subpath = Subpath::new_rect(DVec2::ZERO, self.dimensions.as_dvec2()); @@ -475,6 +508,20 @@ impl Render for Table { } } + fn to_graphic(self) -> Graphic { + let mut graphic_table = Table::new(); + for item in self.into_iter() { + let graphic = item.element.to_graphic(); + let graphic_row = TableRow { + element: graphic, + transform: item.transform, + alpha_blending: item.alpha_blending, + source_node_id: item.source_node_id, + }; + graphic_table.push(graphic_row); + } + Graphic::Graphic(graphic_table) + } fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, _element_id: Option) { for row in self.iter() { row.element.collect_metadata(metadata, footprint, *row.source_node_id); @@ -613,6 +660,10 @@ impl Render for Table { } } + fn to_graphic(self) -> Graphic { + Graphic::Graphic(self) + } + fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option) { for row in self.iter() { if let Some(element_id) = row.source_node_id { @@ -1111,6 +1162,10 @@ impl Render for Table { } } + fn to_graphic(self) -> Graphic { + Graphic::Vector(self) + } + fn add_upstream_click_targets(&self, click_targets: &mut Vec) { for row in self.iter() { let stroke_width = row.element.style.stroke().as_ref().map_or(0., Stroke::effective_width); @@ -1288,6 +1343,10 @@ impl Render for Table> { } } + fn to_graphic(self) -> Graphic { + Graphic::RasterCPU(self) + } + fn add_upstream_click_targets(&self, click_targets: &mut Vec) { let subpath = Subpath::new_rect(DVec2::ZERO, DVec2::ONE); click_targets.push(ClickTarget::new_with_subpath(subpath, 0.)); @@ -1334,6 +1393,10 @@ impl Render for Table> { } } + fn to_graphic(self) -> Graphic { + Graphic::RasterGPU(self) + } + fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option) { let Some(element_id) = element_id else { return }; let subpath = Subpath::new_rect(DVec2::ZERO, DVec2::ONE); @@ -1414,6 +1477,10 @@ impl Render for Table { } } } + + fn to_graphic(self) -> Graphic { + Graphic::Color(self) + } } impl Render for Table { @@ -1514,6 +1581,10 @@ impl Render for Table { } } } + + fn to_graphic(self) -> Graphic { + Graphic::Gradient(self) + } } #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/node-graph/interpreted-executor/src/ui_runtime.rs b/node-graph/interpreted-executor/src/ui_runtime.rs index c5ed664866..a04e0283a1 100644 --- a/node-graph/interpreted-executor/src/ui_runtime.rs +++ b/node-graph/interpreted-executor/src/ui_runtime.rs @@ -47,6 +47,7 @@ impl NodeGraphUIRuntime { /// Represents an update to the render state /// TODO: Incremental compilation +#[derive(Debug)] pub struct CompilationRequest { pub network: NodeNetwork, } From 5fff42f12dd2a9aa2fe338d8a70d626e99576286 Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 7 Sep 2025 16:56:29 -0700 Subject: [PATCH 7/8] Add SVG typography data type --- Cargo.lock | 2 + .../data_panel/data_panel_message_handler.rs | 18 +++ .../wasm/src/wasm_node_graph_ui_executor.rs | 7 +- node-graph/gcore/src/bounds.rs | 8 +- node-graph/gcore/src/consts.rs | 2 + node-graph/gcore/src/graphic.rs | 25 +++- .../gcore/src/node_graph_overlay/types.rs | 1 - node-graph/gcore/src/render_complexity.rs | 8 ++ node-graph/gcore/src/text.rs | 117 ++++++++++++++++++ node-graph/gcore/src/text/font_cache.rs | 2 +- node-graph/gpath-bool/src/lib.rs | 1 + node-graph/gsvg-renderer/Cargo.toml | 2 + node-graph/gsvg-renderer/src/renderer.rs | 87 +++++++++++++ .../interpreted-executor/src/ui_runtime.rs | 12 +- 14 files changed, 283 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ae468b75ef..c74194ad06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2095,7 +2095,9 @@ dependencies = [ "kurbo", "log", "num-traits", + "parley", "serde", + "skrifa 0.36.0", "usvg 0.45.1", "vello", ] 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 b9d01a5e0c..0e2229880e 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 @@ -12,6 +12,7 @@ use graphene_std::gradient::GradientStops; use graphene_std::memo::IORecord; use graphene_std::raster_types::{CPU, GPU, Raster}; use graphene_std::table::Table; +use graphene_std::text::Typography; use graphene_std::vector::Vector; use graphene_std::vector::style::{Fill, FillChoice}; use graphene_std::{Artboard, Graphic}; @@ -266,6 +267,7 @@ impl TableRowLayout for Graphic { Self::RasterGPU(table) => table.identifier(), Self::Color(table) => table.identifier(), Self::Gradient(table) => table.identifier(), + Self::Typography(table) => table.identifier(), } } // Don't put a breadcrumb for Graphic @@ -280,6 +282,7 @@ impl TableRowLayout for Graphic { Self::RasterGPU(table) => table.layout_with_breadcrumb(data), Self::Color(table) => table.layout_with_breadcrumb(data), Self::Gradient(table) => table.layout_with_breadcrumb(data), + Self::Typography(table) => table.layout_with_breadcrumb(data), } } } @@ -504,6 +507,21 @@ impl TableRowLayout for GradientStops { } } +impl TableRowLayout for Typography { + fn type_name() -> &'static str { + "Typography" + } + fn identifier(&self) -> String { + "Typography".to_string() + } + fn element_widget(&self, _index: usize) -> WidgetHolder { + TextLabel::new("Not supported").widget_holder() + } + fn element_page(&self, _data: &mut LayoutData) -> Vec { + vec![LayoutGroup::Row { widgets: Vec::new() }] + } +} + impl TableRowLayout for f64 { fn type_name() -> &'static str { "Number (f64)" diff --git a/frontend/wasm/src/wasm_node_graph_ui_executor.rs b/frontend/wasm/src/wasm_node_graph_ui_executor.rs index 7173cad103..c0e9d353a2 100644 --- a/frontend/wasm/src/wasm_node_graph_ui_executor.rs +++ b/frontend/wasm/src/wasm_node_graph_ui_executor.rs @@ -1,4 +1,8 @@ -use std::sync::{Mutex, mpsc::Receiver}; +use std::{ + cell::RefCell, + rc::Rc, + sync::{Arc, Mutex, mpsc::Receiver}, +}; use editor::{ application::Editor, @@ -36,6 +40,7 @@ impl WasmNodeGraphUIExecutor { executor: DynamicExecutor::default(), compiler: Compiler {}, response_sender, + font_collection: Arc::new(Mutex::new(FontCollection::new())), }; if let Ok(mut node_runtime) = NODE_UI_RUNTIME.lock() { node_runtime.replace(runtime); diff --git a/node-graph/gcore/src/bounds.rs b/node-graph/gcore/src/bounds.rs index fb4b19cd42..084500fb1c 100644 --- a/node-graph/gcore/src/bounds.rs +++ b/node-graph/gcore/src/bounds.rs @@ -1,4 +1,4 @@ -use crate::{Color, gradient::GradientStops}; +use crate::{Color, gradient::GradientStops, text::Typography}; use glam::{DAffine2, DVec2}; #[derive(Clone, Copy, Default, Debug, PartialEq)] @@ -38,3 +38,9 @@ impl BoundingBox for GradientStops { RenderBoundingBox::Infinite } } +impl BoundingBox for Typography { + fn bounding_box(&self, transform: DAffine2, _include_stroke: bool) -> RenderBoundingBox { + let bbox = DVec2::new(self.layout.full_width() as f64, self.layout.height() as f64); + RenderBoundingBox::Rectangle([transform.transform_point2(DVec2::ZERO), transform.transform_point2(bbox)]) + } +} diff --git a/node-graph/gcore/src/consts.rs b/node-graph/gcore/src/consts.rs index a506abbcb7..cec018c9e5 100644 --- a/node-graph/gcore/src/consts.rs +++ b/node-graph/gcore/src/consts.rs @@ -12,3 +12,5 @@ pub const DEFAULT_FONT_STYLE: &str = "Regular (400)"; // TODO: Grab this from the node_modules folder (either with `include_bytes!` or ideally at runtime) instead of checking the font file into the repo. // TODO: And maybe use the WOFF2 version (if it's supported) for its smaller, compressed file size. pub const SOURCE_SANS_FONT_DATA: &[u8] = include_bytes!("text/source-sans-pro-regular.ttf"); +pub const SOURCE_SANS_FONT_FAMILY: &str = "Source Sans Pro"; +pub const SOURCE_SANS_FONT_STYLE: &str = "Regular (400)"; diff --git a/node-graph/gcore/src/graphic.rs b/node-graph/gcore/src/graphic.rs index f902826c99..316a097c43 100644 --- a/node-graph/gcore/src/graphic.rs +++ b/node-graph/gcore/src/graphic.rs @@ -3,6 +3,7 @@ use crate::bounds::{BoundingBox, RenderBoundingBox}; use crate::gradient::GradientStops; use crate::raster_types::{CPU, GPU, Raster}; use crate::table::{Table, TableRow}; +use crate::text::Typography; use crate::uuid::NodeId; use crate::vector::Vector; use crate::{Artboard, Color, Ctx}; @@ -11,7 +12,7 @@ use glam::{DAffine2, DVec2}; use std::hash::Hash; /// The possible forms of graphical content that can be rendered by the Render node into either an image or SVG syntax. -#[derive(Clone, Debug, Hash, PartialEq, DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, DynAny, PartialEq, Hash)] pub enum Graphic { Graphic(Table), Vector(Table), @@ -19,6 +20,26 @@ pub enum Graphic { RasterGPU(Table>), Color(Table), Gradient(Table), + Typography(Table), +} + +impl serde::Serialize for Graphic { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let default: Table = Table::new(); + default.serialize(serializer) + } +} + +impl<'de> serde::Deserialize<'de> for Graphic { + fn deserialize(_deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + Ok(Graphic::Graphic(Table::new())) + } } impl Default for Graphic { @@ -232,6 +253,7 @@ impl Graphic { Graphic::RasterGPU(raster) => raster.iter().all(|row| row.alpha_blending.clip), Graphic::Color(color) => color.iter().all(|row| row.alpha_blending.clip), Graphic::Gradient(gradient) => gradient.iter().all(|row| row.alpha_blending.clip), + Graphic::Typography(typography) => typography.iter().all(|row| row.alpha_blending.clip), } } @@ -256,6 +278,7 @@ impl BoundingBox for Graphic { Graphic::Graphic(graphic) => graphic.bounding_box(transform, include_stroke), Graphic::Color(color) => color.bounding_box(transform, include_stroke), Graphic::Gradient(gradient) => gradient.bounding_box(transform, include_stroke), + Graphic::Typography(typography) => typography.bounding_box(transform, include_stroke), } } } diff --git a/node-graph/gcore/src/node_graph_overlay/types.rs b/node-graph/gcore/src/node_graph_overlay/types.rs index 12bbb2a179..e2cfb91e64 100644 --- a/node-graph/gcore/src/node_graph_overlay/types.rs +++ b/node-graph/gcore/src/node_graph_overlay/types.rs @@ -50,7 +50,6 @@ impl Hash for NodeGraphOverlayData { entries.sort_by(|a, b| a.0.cmp(b.0)); let mut hasher = std::collections::hash_map::DefaultHasher::new(); entries.hash(&mut hasher); - hasher.finish(); } } diff --git a/node-graph/gcore/src/render_complexity.rs b/node-graph/gcore/src/render_complexity.rs index 7920479377..3f8e1e8f5a 100644 --- a/node-graph/gcore/src/render_complexity.rs +++ b/node-graph/gcore/src/render_complexity.rs @@ -1,6 +1,7 @@ use crate::gradient::GradientStops; use crate::raster_types::{CPU, GPU, Raster}; use crate::table::Table; +use crate::text::Typography; use crate::vector::Vector; use crate::{Artboard, Color, Graphic}; @@ -31,6 +32,7 @@ impl RenderComplexity for Graphic { Self::RasterGPU(table) => table.render_complexity(), Self::Color(table) => table.render_complexity(), Self::Gradient(table) => table.render_complexity(), + Self::Typography(table) => table.render_complexity(), } } } @@ -65,3 +67,9 @@ impl RenderComplexity for GradientStops { 1 } } + +impl RenderComplexity for Typography { + fn render_complexity(&self) -> usize { + 1 + } +} diff --git a/node-graph/gcore/src/text.rs b/node-graph/gcore/src/text.rs index 3337a1488c..10adc8c10f 100644 --- a/node-graph/gcore/src/text.rs +++ b/node-graph/gcore/src/text.rs @@ -1,10 +1,21 @@ mod font_cache; mod to_path; +use core::fmt; +use std::{ + borrow::Cow, + collections::{HashMap, hash_map::Entry}, +}; + use dyn_any::DynAny; pub use font_cache::*; +use parley::{Layout, StyleProperty}; +use rustc_hash::FxHasher; +use std::hash::{Hash, Hasher}; pub use to_path::*; +use crate::{consts::*, table::Table, vector::Vector}; + /// Alignment of lines of type within a text block. #[repr(C)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)] @@ -29,3 +40,109 @@ impl From for parley::Alignment { } } } + +#[derive(Clone, DynAny)] +pub struct Typography { + pub layout: Layout<()>, + pub font_family: String, +} + +impl fmt::Debug for Typography { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Typography") + .field("layout", &"") // skip Layout<()> because it has no Debug + .finish() + } +} + +impl PartialEq for Typography { + fn eq(&self, _other: &Self) -> bool { + true + } +} + +impl Hash for Typography { + fn hash(&self, state: &mut H) { + self.layout.len().hash(state); + } +} + +impl Typography { + pub fn to_vector(&self) -> Table { + Table::new() + } +} + +pub struct NewFontCache { + font_context: parley::FontContext, + layout_context: parley::LayoutContext<()>, + font_mapping: HashMap, + hash: u64, +} + +impl NewFontCache { + pub fn new() -> Self { + let mut new = NewFontCache { + font_context: parley::FontContext::new(), + layout_context: parley::LayoutContext::new(), + font_mapping: HashMap::new(), + hash: 0, + }; + + let source_sans_font = Font::new(SOURCE_SANS_FONT_FAMILY.to_string(), SOURCE_SANS_FONT_STYLE.to_string()); + new.register_font(source_sans_font, SOURCE_SANS_FONT_DATA.to_vec()); + new + } + + pub fn register_font(&mut self, font: Font, data: Vec) { + match self.font_mapping.entry(font) { + Entry::Occupied(occupied_entry) => { + log::error!("Trying to register font that already is added: {:?}", occupied_entry.key()); + } + Entry::Vacant(vacant_entry) => { + let registered_font = self.font_context.collection.register_fonts(parley::fontique::Blob::from(data), None); + if registered_font.len() > 1 { + log::error!("Registered multiple fonts for {:?}. Only the first is accessible", vacant_entry.key()); + }; + match registered_font.into_iter().next() { + Some((family_id, font_info)) => { + let Some(family_name) = self.font_context.collection.family_name(family_id) else { + log::error!("Could not get family name for font: {:?}", vacant_entry.key()); + return; + }; + log::debug!("font info: {:?}", font_info); + let Some(font_info) = font_info.into_iter().next() else { + log::error!("Could not get font info for font: {:?}", vacant_entry.key()); + return; + }; + let mut hasher = FxHasher::default(); // or FxHasher::new() + // Hash the Font for a unique id and add it to the cached hash + vacant_entry.key().hash(&mut hasher); + let hash_value = hasher.finish(); + self.hash = self.hash.wrapping_add(hash_value); + + vacant_entry.insert((family_name.to_string(), font_info)); + } + None => log::error!("Could not register font for {:?}", vacant_entry.key()), + } + } + } + } + + pub fn generate_typography(&mut self, font: Font, text: &str) -> Option { + let Some((font_family, font_info)) = self.font_mapping.get(&font) else { + log::error!("Font not loaded: {:?}", font); + return None; + }; + let font_family = font_family.to_string(); + let mut builder = self.layout_context.ranged_builder(&mut self.font_context, text, 1., false); + + builder.push_default(StyleProperty::FontStack(parley::FontStack::Single(parley::FontFamily::Named(Cow::Owned(font_family.clone()))))); + builder.push_default(StyleProperty::FontWeight(font_info.weight())); + builder.push_default(StyleProperty::FontStyle(font_info.style())); + builder.push_default(StyleProperty::FontWidth(font_info.width())); + + let layout = builder.build(text); + Some(Typography { layout, font_family }) + } +} diff --git a/node-graph/gcore/src/text/font_cache.rs b/node-graph/gcore/src/text/font_cache.rs index 37a8bfc505..ba13f7a78b 100644 --- a/node-graph/gcore/src/text/font_cache.rs +++ b/node-graph/gcore/src/text/font_cache.rs @@ -2,7 +2,7 @@ use dyn_any::DynAny; use std::collections::HashMap; /// A font type (storing font family and font style and an optional preview URL) -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Hash, PartialEq, Eq, DynAny, specta::Type)] +#[derive(Debug, Clone, Hash, serde::Serialize, serde::Deserialize, PartialEq, Eq, DynAny, specta::Type)] pub struct Font { #[serde(rename = "fontFamily")] pub font_family: String, diff --git a/node-graph/gpath-bool/src/lib.rs b/node-graph/gpath-bool/src/lib.rs index df3a089414..7634476564 100644 --- a/node-graph/gpath-bool/src/lib.rs +++ b/node-graph/gpath-bool/src/lib.rs @@ -318,6 +318,7 @@ fn flatten_vector(graphic_table: &Table) -> Table { } }) .collect::>(), + Graphic::Typography(_) => Vec::new(), } }) .collect() diff --git a/node-graph/gsvg-renderer/Cargo.toml b/node-graph/gsvg-renderer/Cargo.toml index a6d0e395c5..d606dee915 100644 --- a/node-graph/gsvg-renderer/Cargo.toml +++ b/node-graph/gsvg-renderer/Cargo.toml @@ -19,6 +19,8 @@ log = { workspace = true } num-traits = { workspace = true } usvg = { workspace = true } kurbo = { workspace = true } +parley = { workspace = true } +skrifa = { workspace = true } # Optional workspace dependencies vello = { workspace = true, optional = true } diff --git a/node-graph/gsvg-renderer/src/renderer.rs b/node-graph/gsvg-renderer/src/renderer.rs index 0b663563ad..76dda5f63a 100644 --- a/node-graph/gsvg-renderer/src/renderer.rs +++ b/node-graph/gsvg-renderer/src/renderer.rs @@ -16,6 +16,7 @@ use graphene_core::raster_types::{CPU, GPU, Raster}; use graphene_core::render_complexity::RenderComplexity; use graphene_core::subpath::Subpath; use graphene_core::table::{Table, TableRow}; +use graphene_core::text::Typography; use graphene_core::transform::{Footprint, Transform}; use graphene_core::uuid::{NodeId, generate_uuid}; use graphene_core::vector::Vector; @@ -26,6 +27,8 @@ use kurbo::Affine; use kurbo::Rect; use kurbo::Shape; use num_traits::Zero; +use skrifa::MetadataProvider; +use skrifa::attribute::Style; use std::collections::{HashMap, HashSet}; use std::fmt::Write; use std::hash::{DefaultHasher, Hash, Hasher}; @@ -118,6 +121,18 @@ impl SvgRender { self.svg.push("/>".into()); } + pub fn leaf_text(&mut self, text: impl Into, attributes: impl FnOnce(&mut SvgRenderAttrs)) { + self.indent(); + + self.svg.push("".into()); + self.svg.push(text.into()); + self.svg.push("".into()); + } + pub fn leaf_node(&mut self, content: impl Into) { self.indent(); self.svg.push(content.into()); @@ -261,6 +276,7 @@ impl Render for Graphic { Graphic::RasterGPU(_) => (), Graphic::Color(table) => table.render_svg(render, render_params), Graphic::Gradient(table) => table.render_svg(render, render_params), + Graphic::Typography(table) => table.render_svg(render, render_params), } } @@ -273,6 +289,7 @@ impl Render for Graphic { Graphic::RasterGPU(table) => table.render_to_vello(scene, transform, context, render_params), Graphic::Color(table) => table.render_to_vello(scene, transform, context, render_params), Graphic::Gradient(table) => table.render_to_vello(scene, transform, context, render_params), + Graphic::Typography(table) => table.render_to_vello(scene, transform, context, render_params), } } @@ -284,6 +301,7 @@ impl Render for Graphic { Graphic::RasterGPU(table) => table.to_graphic(), Graphic::Color(table) => table.to_graphic(), Graphic::Gradient(table) => table.to_graphic(), + Graphic::Typography(table) => table.to_graphic(), } } @@ -328,6 +346,14 @@ impl Render for Graphic { Graphic::Gradient(table) => { metadata.upstream_footprints.insert(element_id, footprint); + // TODO: Find a way to handle more than the first row + if let Some(row) = table.iter().next() { + metadata.local_transforms.insert(element_id, *row.transform); + } + } + Graphic::Typography(table) => { + metadata.upstream_footprints.insert(element_id, footprint); + // TODO: Find a way to handle more than the first row if let Some(row) = table.iter().next() { metadata.local_transforms.insert(element_id, *row.transform); @@ -343,6 +369,7 @@ impl Render for Graphic { Graphic::RasterGPU(table) => table.collect_metadata(metadata, footprint, element_id), Graphic::Color(table) => table.collect_metadata(metadata, footprint, element_id), Graphic::Gradient(table) => table.collect_metadata(metadata, footprint, element_id), + Graphic::Typography(table) => table.collect_metadata(metadata, footprint, element_id), } } @@ -354,6 +381,7 @@ impl Render for Graphic { Graphic::RasterGPU(table) => table.add_upstream_click_targets(click_targets), Graphic::Color(table) => table.add_upstream_click_targets(click_targets), Graphic::Gradient(table) => table.add_upstream_click_targets(click_targets), + Graphic::Typography(table) => table.add_upstream_click_targets(click_targets), } } @@ -365,6 +393,7 @@ impl Render for Graphic { Graphic::RasterGPU(table) => table.contains_artboard(), Graphic::Color(table) => table.contains_artboard(), Graphic::Gradient(table) => table.contains_artboard(), + Graphic::Typography(table) => table.contains_artboard(), } } @@ -376,6 +405,7 @@ impl Render for Graphic { Graphic::RasterGPU(_) => (), Graphic::Color(_) => (), Graphic::Gradient(_) => (), + Graphic::Typography(_) => (), } } } @@ -1587,6 +1617,63 @@ impl Render for Table { } } +impl Render for Table { + fn render_svg(&self, render: &mut SvgRender, _render_params: &RenderParams) { + for table_row in self.iter() { + for line in table_row.element.layout.lines() { + for item in line.items() { + match item { + parley::PositionedLayoutItem::GlyphRun(glyph_run) => { + let font = glyph_run.run().font(); + let font_ref = skrifa::FontRef::from_index(font.data.as_ref(), font.index).unwrap(); + let font_attributes = font_ref.attributes(); + let font_style = match font_attributes.style { + Style::Normal => "normal".to_string(), + Style::Italic => "italic".to_string(), + Style::Oblique(Some(angle)) => format!("oblique {}deg", angle), + Style::Oblique(None) => "oblique".to_string(), + }; + render.parent_tag( + "g", + |attributes| { + let matrix = format_transform_matrix(*table_row.transform); + if !matrix.is_empty() { + attributes.push("transform", matrix); + attributes.push("font-family", table_row.element.font_family.clone()); + attributes.push("font-size", glyph_run.run().font_size().to_string()); + attributes.push("font-weight", font_attributes.weight.value().to_string()); + attributes.push("font-style", font_style); + } + }, + |render| { + for glyph in glyph_run.positioned_glyphs() { + let character = font_ref.glyph_names().get(skrifa::GlyphId::new(glyph.id as u32)).unwrap(); + render.leaf_text(character.as_str().to_string(), |attributes| { + attributes.push("x", glyph.x.to_string()); + attributes.push("y", glyph.y.to_string()); + }); + } + }, + ); + } + parley::PositionedLayoutItem::InlineBox(_positioned_inline_box) => { + log::error!("Inline box text rendering not supported"); + } + } + } + } + } + } + + fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, _render_params: &RenderParams) { + todo!() + } + + fn to_graphic(self) -> Graphic { + todo!() + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub enum SvgSegment { Slice(&'static str), diff --git a/node-graph/interpreted-executor/src/ui_runtime.rs b/node-graph/interpreted-executor/src/ui_runtime.rs index a04e0283a1..fe23e33cdf 100644 --- a/node-graph/interpreted-executor/src/ui_runtime.rs +++ b/node-graph/interpreted-executor/src/ui_runtime.rs @@ -1,10 +1,13 @@ -use std::sync::{Arc, mpsc::Sender}; +use std::sync::{Arc, Mutex, mpsc::Sender}; use glam::UVec2; use graph_craft::{document::NodeNetwork, graphene_compiler::Compiler}; -use graphene_std::node_graph_overlay::{ - types::NodeGraphTransform, - ui_context::{UIContextImpl, UIRuntimeResponse}, +use graphene_std::{ + node_graph_overlay::{ + types::NodeGraphTransform, + ui_context::{UIContextImpl, UIRuntimeResponse}, + }, + text::NewFontCache, }; use crate::dynamic_executor::DynamicExecutor; @@ -15,6 +18,7 @@ pub struct NodeGraphUIRuntime { // Used within the node graph to return responses during evaluation // Also used to return compilation responses, but not for the UI overlay since the types are not needed pub response_sender: Sender, + pub font_collection: Arc>, } impl NodeGraphUIRuntime { From 62da9724089b308dbe1e322b8d66bb52343fa6b5 Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 7 Sep 2025 19:49:02 -0700 Subject: [PATCH 8/8] New font cache --- .../node_graph/generate_node_graph_overlay.rs | 2 +- .../wasm/src/wasm_node_graph_ui_executor.rs | 13 +- node-graph/gcore/src/node_graph_overlay.rs | 7 +- .../src/node_graph_overlay/nodes_and_wires.rs | 165 ++++++++++-------- node-graph/gcore/src/text.rs | 59 +++++-- node-graph/gcore/src/text/to_path.rs | 1 - node-graph/graph-craft/src/document/value.rs | 22 ++- node-graph/gsvg-renderer/src/renderer.rs | 25 ++- .../interpreted-executor/src/node_registry.rs | 4 +- .../interpreted-executor/src/ui_runtime.rs | 24 ++- 10 files changed, 209 insertions(+), 113 deletions(-) diff --git a/editor/src/messages/portfolio/document/node_graph/generate_node_graph_overlay.rs b/editor/src/messages/portfolio/document/node_graph/generate_node_graph_overlay.rs index 2d69ae1dba..bb89c152d5 100644 --- a/editor/src/messages/portfolio/document/node_graph/generate_node_graph_overlay.rs +++ b/editor/src/messages/portfolio/document/node_graph/generate_node_graph_overlay.rs @@ -40,7 +40,7 @@ pub fn generate_node_graph_overlay(node_graph_overlay_data: NodeGraphOverlayData generate_nodes_id, DocumentNode { call_argument: concrete!(UIContext), - inputs: vec![NodeInput::network(concrete!(UIContext), 1)], + inputs: vec![NodeInput::network(concrete!(UIContext), 1), NodeInput::scope("font-cache")], implementation: DocumentNodeImplementation::ProtoNode("graphene_core::node_graph_overlay::GenerateNodesNode".into()), ..Default::default() }, diff --git a/frontend/wasm/src/wasm_node_graph_ui_executor.rs b/frontend/wasm/src/wasm_node_graph_ui_executor.rs index c0e9d353a2..5e0cd3317d 100644 --- a/frontend/wasm/src/wasm_node_graph_ui_executor.rs +++ b/frontend/wasm/src/wasm_node_graph_ui_executor.rs @@ -1,15 +1,14 @@ -use std::{ - cell::RefCell, - rc::Rc, - sync::{Arc, Mutex, mpsc::Receiver}, -}; +use std::sync::{Arc, Mutex, mpsc::Receiver}; use editor::{ application::Editor, messages::prelude::{FrontendMessage, Message}, }; use graph_craft::graphene_compiler::Compiler; -use graphene_std::node_graph_overlay::{types::NodeGraphTransform, ui_context::UIRuntimeResponse}; +use graphene_std::{ + node_graph_overlay::{types::NodeGraphTransform, ui_context::UIRuntimeResponse}, + text::{NewFontCache, NewFontCacheWrapper}, +}; use interpreted_executor::{ dynamic_executor::DynamicExecutor, ui_runtime::{CompilationRequest, EvaluationRequest, NodeGraphUIRuntime}, @@ -40,7 +39,7 @@ impl WasmNodeGraphUIExecutor { executor: DynamicExecutor::default(), compiler: Compiler {}, response_sender, - font_collection: Arc::new(Mutex::new(FontCollection::new())), + font_cache: NewFontCacheWrapper(Arc::new(Mutex::new(NewFontCache::new()))), }; if let Ok(mut node_runtime) = NODE_UI_RUNTIME.lock() { node_runtime.replace(runtime); diff --git a/node-graph/gcore/src/node_graph_overlay.rs b/node-graph/gcore/src/node_graph_overlay.rs index ac234a7664..6f1caf464c 100644 --- a/node-graph/gcore/src/node_graph_overlay.rs +++ b/node-graph/gcore/src/node_graph_overlay.rs @@ -9,6 +9,7 @@ use crate::{ ui_context::{UIContext, UIRuntimeResponse}, }, table::Table, + text::NewFontCacheWrapper, transform::ApplyTransform, }; @@ -19,9 +20,9 @@ pub mod types; pub mod ui_context; #[node_macro::node(skip_impl)] -pub fn generate_nodes(_: impl Ctx, mut node_graph_overlay_data: NodeGraphOverlayData) -> Table { +pub fn generate_nodes(_: impl Ctx, mut node_graph_overlay_data: NodeGraphOverlayData, font_cache: NewFontCacheWrapper) -> Table { let mut nodes_and_wires = Table::new(); - let (layers, side_ports) = draw_layers(&mut node_graph_overlay_data); + let (layers, side_ports) = draw_layers(&mut node_graph_overlay_data, font_cache.0.as_ref()); nodes_and_wires.extend(layers); let wires = draw_wires(&mut node_graph_overlay_data.nodes_to_render); @@ -29,7 +30,7 @@ pub fn generate_nodes(_: impl Ctx, mut node_graph_overlay_data: NodeGraphOverlay nodes_and_wires.extend(side_ports); - let nodes = draw_nodes(&node_graph_overlay_data.nodes_to_render); + let nodes = draw_nodes(&node_graph_overlay_data.nodes_to_render, font_cache.0.as_ref()); nodes_and_wires.extend(nodes); nodes_and_wires diff --git a/node-graph/gcore/src/node_graph_overlay/nodes_and_wires.rs b/node-graph/gcore/src/node_graph_overlay/nodes_and_wires.rs index fa73ceccd4..5898180bec 100644 --- a/node-graph/gcore/src/node_graph_overlay/nodes_and_wires.rs +++ b/node-graph/gcore/src/node_graph_overlay/nodes_and_wires.rs @@ -1,3 +1,5 @@ +use std::sync::Mutex; + use glam::{DAffine2, DVec2}; use graphene_core_shaders::color::{AlphaMut, Color}; use kurbo::{BezPath, Circle, Rect, RoundedRect, Shape}; @@ -5,13 +7,13 @@ use kurbo::{BezPath, Circle, Rect, RoundedRect, Shape}; use crate::{ Graphic, bounds::{BoundingBox, RenderBoundingBox}, - consts::SOURCE_SANS_FONT_DATA, + consts::{SOURCE_SANS_FONT_DATA, SOURCE_SANS_FONT_FAMILY, SOURCE_SANS_FONT_STYLE}, node_graph_overlay::{ consts::*, types::{FrontendGraphDataType, FrontendNodeToRender, NodeGraphOverlayData}, }, table::{Table, TableRow}, - text::{self, TextAlign, TypesettingConfig}, + text::{self, Font, NewFontCache, TextAlign, TypesettingConfig}, transform::ApplyTransform, vector::{ Vector, @@ -19,7 +21,7 @@ use crate::{ }, }; -pub fn draw_nodes(nodes: &Vec) -> Table { +pub fn draw_nodes(nodes: &Vec, font_cache: &Mutex) -> Table { let mut node_table = Table::new(); for node_to_render in nodes { if let Some(frontend_node) = node_to_render.node_or_layer.node.as_ref() { @@ -118,56 +120,74 @@ pub fn draw_nodes(nodes: &Vec) -> Table { border_table.push(border_vector_row); node_table.push(TableRow::new_from_element(Graphic::Vector(border_table))); - let typesetting = TypesettingConfig { - font_size: 14., - line_height_ratio: 1.2, - character_spacing: 0.0, - max_width: None, - max_height: None, - tilt: 0.0, - align: TextAlign::Left, - }; - - // Names for each row - let font_blob = Some(text::load_font(SOURCE_SANS_FONT_DATA)); - let mut node_text = crate::text::to_path(&node_to_render.metadata.display_name, font_blob, typesetting, false); - for text_row in node_text.iter_mut() { - *text_row.transform = DAffine2::from_translation(DVec2::new(x + 8., y + 3.)); - } - - for row in 1..=number_of_rows { - if let Some(input) = frontend_node.secondary_inputs.get(row - 1) { - let font_blob = Some(text::load_font(SOURCE_SANS_FONT_DATA)); - let mut input_row_text = crate::text::to_path(&input.name, font_blob, typesetting, false); - for text_row in input_row_text.iter_mut() { - *text_row.transform = DAffine2::from_translation(DVec2::new(x + 8., y + 24. * row as f64 + 3.)); - } - node_text.extend(input_row_text); - } else if let Some(output) = frontend_node.secondary_outputs.get(row - 1) { - let font_blob = Some(text::load_font(SOURCE_SANS_FONT_DATA)); - let mut output_row_text = crate::text::to_path(&output.name, font_blob, typesetting, false); - // Find width to right align text - let full_text_width = if let RenderBoundingBox::Rectangle(bbox) = output_row_text.bounding_box(DAffine2::default(), true) { - bbox[1].x - bbox[0].x - } else { - 0. + let text_table = match font_cache.lock() { + Ok(mut font_cache) => { + let font = Font::new(SOURCE_SANS_FONT_FAMILY.to_string(), SOURCE_SANS_FONT_STYLE.to_string()); + let first_row_graphic = match font_cache.generate_typography(&font, 14., &node_to_render.metadata.display_name) { + Some(mut typography) => { + typography.color = Color::WHITE; + Graphic::Typography(Table::new_from_element(typography)) + } + None => { + log::error!("Could not generate typography in draw node"); + Graphic::default() + } }; - // Account for clipping - let text_width = full_text_width.min(5. * GRID_SIZE - 16.); - let left_offset = 5. * GRID_SIZE - 8. - text_width; - for text_row in output_row_text.iter_mut() { - *text_row.transform = DAffine2::from_translation(DVec2::new(x + 8. + left_offset, y + 24. * row as f64 + 3.)); + + let mut node_text_row = TableRow::new_from_element(first_row_graphic); + node_text_row.transform = DAffine2::from_translation(DVec2::new(x + 8., y + 3.)); + let mut text_table = Table::new_from_row(node_text_row); + + for row in 1..=number_of_rows { + if let Some(input) = frontend_node.secondary_inputs.get(row - 1) { + let secondary_input_graphic = match font_cache.generate_typography(&font, 14., &input.name) { + Some(mut typography) => { + typography.color = Color::WHITE; + Graphic::Typography(Table::new_from_element(typography)) + } + None => { + log::error!("Could not generate typography in draw node"); + Graphic::default() + } + }; + let mut node_text_row = TableRow::new_from_element(secondary_input_graphic); + node_text_row.transform = DAffine2::from_translation(DVec2::new(x + 8., y + 24. * row as f64 + 3.)); + text_table.push(node_text_row); + } else if let Some(output) = frontend_node.secondary_outputs.get(row - 1) { + let secondary_output_graphic = match font_cache.generate_typography(&font, 14., &output.name) { + Some(mut typography) => { + typography.color = Color::WHITE; + Graphic::Typography(Table::new_from_element(typography)) + } + None => { + log::error!("Could not generate typography in draw node"); + Graphic::default() + } + }; + + // Find width to right align text + let full_text_width = if let RenderBoundingBox::Rectangle(bbox) = secondary_output_graphic.bounding_box(DAffine2::default(), true) { + bbox[1].x - bbox[0].x + } else { + 0. + }; + // Account for clipping + let text_width = full_text_width.min(5. * GRID_SIZE - 16.); + let left_offset = 5. * GRID_SIZE - 8. - text_width; + let mut node_text_row = TableRow::new_from_element(secondary_output_graphic); + node_text_row.transform = DAffine2::from_translation(DVec2::new(x + 8. + left_offset, y + 24. * row as f64 + 3.)); + text_table.push(node_text_row); + } } - node_text.extend(output_row_text); + text_table } - } - - // for text_row in node_text.iter_mut() { - // text_row.element.style.fill = Fill::Solid(Color::WHITE); - // } + Err(_) => { + log::error!("Could not lock font cache in draw node"); + Table::new() + } + }; - let node_text_row = TableRow::new_from_element(Graphic::Vector(node_text)); - node_table.push(node_text_row); + node_table.extend(text_table); // Add black clipping path to view text in node let text_area = Rect::new(x + 8., y, x + node_width - 8., y + node_height); @@ -209,7 +229,7 @@ pub fn draw_nodes(nodes: &Vec) -> Table { node_table } -pub fn draw_layers(nodes: &mut NodeGraphOverlayData) -> (Table, Table) { +pub fn draw_layers(nodes: &mut NodeGraphOverlayData, font_cache: &Mutex) -> (Table, Table) { let mut layer_table = Table::new(); let mut side_ports_table = Table::new(); for node_to_render in &nodes.nodes_to_render { @@ -224,22 +244,26 @@ pub fn draw_layers(nodes: &mut NodeGraphOverlayData) -> (Table, Table { + let font = Font::new(SOURCE_SANS_FONT_FAMILY.to_string(), SOURCE_SANS_FONT_STYLE.to_string()); + match font_cache.generate_typography(&font, 14., &node_to_render.metadata.display_name) { + Some(mut typography) => { + typography.color = Color::WHITE; + Graphic::Typography(Table::new_from_element(typography)) + } + None => { + log::error!("Could not generate typography in draw layers"); + Graphic::default() + } + } + } + Err(_) => { + log::error!("Could not lock font cache in draw layers"); + Graphic::default() + } }; - - let font_blob = Some(text::load_font(SOURCE_SANS_FONT_DATA)); - let mut text_table = crate::text::to_path(&node_to_render.metadata.display_name, font_blob, typesetting, false); - - let text_width = if let RenderBoundingBox::Rectangle(bbox) = text_table.bounding_box(DAffine2::default(), true) { + let text_width = if let RenderBoundingBox::Rectangle(bbox) = text_graphic.bounding_box(DAffine2::default(), true) { bbox[1].x - bbox[0].x } else { 0. @@ -326,13 +350,10 @@ pub fn draw_layers(nodes: &mut NodeGraphOverlayData) -> (Table, Table for parley::Alignment { pub struct Typography { pub layout: Layout<()>, pub font_family: String, + pub color: Color, + pub stroke: Option<(Color, f64)>, } impl fmt::Debug for Typography { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Typography") - .field("layout", &"") // skip Layout<()> because it has no Debug - .finish() + .field("font_family", &self.font_family) + .field("color", &self.color) + .field("stroke", &self.stroke) + .finish() } } @@ -73,11 +79,31 @@ impl Typography { } } +#[derive(Clone)] +pub struct NewFontCacheWrapper(pub Arc>); + +impl fmt::Debug for NewFontCacheWrapper { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("font cache").finish() + } +} + +impl PartialEq for NewFontCacheWrapper { + fn eq(&self, _other: &Self) -> bool { + log::error!("Font cache should not be compared"); + false + } +} + +unsafe impl dyn_any::StaticType for NewFontCacheWrapper { + type Static = NewFontCacheWrapper; +} + pub struct NewFontCache { - font_context: parley::FontContext, - layout_context: parley::LayoutContext<()>, - font_mapping: HashMap, - hash: u64, + pub font_context: parley::FontContext, + pub layout_context: parley::LayoutContext<()>, + pub font_mapping: HashMap, + pub hash: u64, } impl NewFontCache { @@ -110,7 +136,6 @@ impl NewFontCache { log::error!("Could not get family name for font: {:?}", vacant_entry.key()); return; }; - log::debug!("font info: {:?}", font_info); let Some(font_info) = font_info.into_iter().next() else { log::error!("Could not get font info for font: {:?}", vacant_entry.key()); return; @@ -129,20 +154,30 @@ impl NewFontCache { } } - pub fn generate_typography(&mut self, font: Font, text: &str) -> Option { - let Some((font_family, font_info)) = self.font_mapping.get(&font) else { + pub fn generate_typography(&mut self, font: &Font, font_size: f32, text: &str) -> Option { + let Some((font_family, font_info)) = self.font_mapping.get(font) else { log::error!("Font not loaded: {:?}", font); return None; }; let font_family = font_family.to_string(); + let mut builder = self.layout_context.ranged_builder(&mut self.font_context, text, 1., false); builder.push_default(StyleProperty::FontStack(parley::FontStack::Single(parley::FontFamily::Named(Cow::Owned(font_family.clone()))))); + builder.push_default(StyleProperty::FontSize(font_size)); builder.push_default(StyleProperty::FontWeight(font_info.weight())); builder.push_default(StyleProperty::FontStyle(font_info.style())); builder.push_default(StyleProperty::FontWidth(font_info.width())); - let layout = builder.build(text); - Some(Typography { layout, font_family }) + let mut layout: Layout<()> = builder.build(text); + layout.break_all_lines(None); + // layout.align(None, parley::Alignment::Start, AlignmentOptions::); + + Some(Typography { + layout, + font_family, + color: Color::BLACK, + stroke: None, + }) } } diff --git a/node-graph/gcore/src/text/to_path.rs b/node-graph/gcore/src/text/to_path.rs index d1b9b6f188..927caaf817 100644 --- a/node-graph/gcore/src/text/to_path.rs +++ b/node-graph/gcore/src/text/to_path.rs @@ -198,7 +198,6 @@ fn layout_text(str: &str, font_data: Option>, typesetting: TypesettingC builder.push_default(LineHeight::FontSizeRelative(typesetting.line_height_ratio as f32)); let mut layout: Layout<()> = builder.build(str); - layout.break_all_lines(typesetting.max_width.map(|mw| mw as f32)); layout.align(typesetting.max_width.map(|max_w| max_w as f32), typesetting.align.into(), AlignmentOptions::default()); diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 0eb72f4bc0..dfb79c868a 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -11,6 +11,7 @@ use graphene_brush::brush_stroke::BrushStroke; use graphene_core::raster::Image; use graphene_core::raster_types::{CPU, Raster}; use graphene_core::table::Table; +use graphene_core::text::NewFontCacheWrapper; use graphene_core::transform::ReferencePoint; use graphene_core::uuid::NodeId; use graphene_core::vector::Vector; @@ -23,6 +24,7 @@ use std::hash::Hash; use std::marker::PhantomData; use std::str::FromStr; pub use std::sync::Arc; +use std::sync::Mutex; pub struct TaggedValueTypeError; @@ -38,7 +40,9 @@ macro_rules! tagged_value { RenderOutput(RenderOutput), SurfaceFrame(SurfaceFrame), #[serde(skip)] - EditorApi(Arc) + EditorApi(Arc), + #[serde(skip)] + NewFontCache(NewFontCacheWrapper), } // We must manually implement hashing because some values are floats and so do not reproducibly hash (see FakeHash below) @@ -52,6 +56,7 @@ macro_rules! tagged_value { Self::RenderOutput(x) => x.hash(state), Self::SurfaceFrame(x) => x.hash(state), Self::EditorApi(x) => x.hash(state), + Self::NewFontCache(x) => x.hash(state), } } } @@ -64,6 +69,7 @@ macro_rules! tagged_value { Self::RenderOutput(x) => Box::new(x), Self::SurfaceFrame(x) => Box::new(x), Self::EditorApi(x) => Box::new(x), + Self::NewFontCache(x) => Box::new(x), } } /// Converts to a Arc @@ -74,6 +80,7 @@ macro_rules! tagged_value { Self::RenderOutput(x) => Arc::new(x), Self::SurfaceFrame(x) => Arc::new(x), Self::EditorApi(x) => Arc::new(x), + Self::NewFontCache(x) => Arc::new(x), } } /// Creates a graphene_core::Type::Concrete(TypeDescriptor { .. }) with the type of the value inside the tagged value @@ -83,7 +90,8 @@ macro_rules! tagged_value { $( Self::$identifier(_) => concrete!($ty), )* Self::RenderOutput(_) => concrete!(RenderOutput), Self::SurfaceFrame(_) => concrete!(SurfaceFrame), - Self::EditorApi(_) => concrete!(&WasmEditorApi) + Self::EditorApi(_) => concrete!(&WasmEditorApi), + Self::NewFontCache(_) => concrete!(NewFontCacheWrapper), } } /// Attempts to downcast the dynamic type to a tagged value @@ -458,6 +466,8 @@ trait FakeHash { fn hash(&self, state: &mut H); } mod fake_hash { + use graphene_core::text::NewFontCacheWrapper; + use super::*; impl FakeHash for f64 { fn hash(&self, state: &mut H) { @@ -516,6 +526,14 @@ mod fake_hash { self.1.hash(state) } } + impl FakeHash for NewFontCacheWrapper { + fn hash(&self, state: &mut H) { + match self.0.lock() { + Ok(inner) => inner.hash.hash(state), + Err(_) => log::error!("Could not lock font cache when hashing"), + } + } + } } #[test] diff --git a/node-graph/gsvg-renderer/src/renderer.rs b/node-graph/gsvg-renderer/src/renderer.rs index 76dda5f63a..789d880b47 100644 --- a/node-graph/gsvg-renderer/src/renderer.rs +++ b/node-graph/gsvg-renderer/src/renderer.rs @@ -124,13 +124,13 @@ impl SvgRender { pub fn leaf_text(&mut self, text: impl Into, attributes: impl FnOnce(&mut SvgRenderAttrs)) { self.indent(); - self.svg.push("".into()); self.svg.push(text.into()); - self.svg.push("".into()); + self.svg.push("".into()); } pub fn leaf_node(&mut self, content: impl Into) { @@ -1634,21 +1634,28 @@ impl Render for Table { Style::Oblique(None) => "oblique".to_string(), }; render.parent_tag( - "g", + "text", |attributes| { let matrix = format_transform_matrix(*table_row.transform); if !matrix.is_empty() { attributes.push("transform", matrix); - attributes.push("font-family", table_row.element.font_family.clone()); - attributes.push("font-size", glyph_run.run().font_size().to_string()); - attributes.push("font-weight", font_attributes.weight.value().to_string()); - attributes.push("font-style", font_style); + } + + attributes.push("font-family", table_row.element.font_family.clone()); + attributes.push("font-size", glyph_run.run().font_size().to_string()); + attributes.push("font-weight", font_attributes.weight.value().to_string()); + attributes.push("font-style", font_style); + attributes.push("fill", format!("#{}", table_row.element.color.to_rgb_hex_srgb_from_gamma())); + if let Some((stroke_color, stroke_width)) = table_row.element.stroke.as_ref().cloned() { + attributes.push("stroke-color", format!("#{}", stroke_color.to_rgb_hex_srgb_from_gamma())); + attributes.push("stroke-width", format!("{stroke_width}")); } }, |render| { for glyph in glyph_run.positioned_glyphs() { - let character = font_ref.glyph_names().get(skrifa::GlyphId::new(glyph.id as u32)).unwrap(); - render.leaf_text(character.as_str().to_string(), |attributes| { + let mut character = font_ref.glyph_names().get(skrifa::GlyphId::new(glyph.id as u32)).unwrap().as_str().to_string(); + let character = character.replace("space", " "); + render.leaf_text(character, |attributes| { attributes.push("x", glyph.x.to_string()); attributes.push("y", glyph.y.to_string()); }); diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index b2e5811b57..a4fd15134d 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -24,6 +24,7 @@ use graphene_std::gradient::GradientStops; use graphene_std::node_graph_overlay::types::NodeGraphOverlayData; use graphene_std::node_graph_overlay::ui_context::UIContext; use graphene_std::table::Table; +use graphene_std::text::NewFontCacheWrapper; use graphene_std::uuid::NodeId; use graphene_std::vector::Vector; #[cfg(feature = "gpu")] @@ -186,13 +187,14 @@ fn node_registry() -> HashMap, input: UIContext, fn_params: [UIContext => ()]), - async_node!(graphene_core::node_graph_overlay::GenerateNodesNode<_>, input: UIContext, fn_params: [UIContext => NodeGraphOverlayData]), + async_node!(graphene_core::node_graph_overlay::GenerateNodesNode<_, _>, input: UIContext, fn_params: [UIContext => NodeGraphOverlayData, UIContext => NewFontCacheWrapper]), async_node!(graphene_core::node_graph_overlay::TransformNodesNode<_>, input: UIContext, fn_params: [UIContext =>Table]), async_node!(graphene_core::node_graph_overlay::DotGridBackgroundNode<_>, input: UIContext, fn_params: [UIContext =>f64]), async_node!(graphene_core::node_graph_overlay::NodeGraphUiExtendNode<_, _>, input: UIContext, fn_params: [UIContext =>Table, UIContext =>Table]), async_node!(graphene_std::wasm_application_io::RenderNodeGraphUiNode<_>, input: UIContext, fn_params: [UIContext =>Table]), async_node!(graphene_core::node_graph_overlay::SendRenderNode<_>, input: UIContext, fn_params: [UIContext => String]), ]; + // ============= // CONVERT NODES // ============= diff --git a/node-graph/interpreted-executor/src/ui_runtime.rs b/node-graph/interpreted-executor/src/ui_runtime.rs index fe23e33cdf..6da1a3fc35 100644 --- a/node-graph/interpreted-executor/src/ui_runtime.rs +++ b/node-graph/interpreted-executor/src/ui_runtime.rs @@ -1,13 +1,19 @@ -use std::sync::{Arc, Mutex, mpsc::Sender}; +use std::sync::{Arc, mpsc::Sender}; use glam::UVec2; -use graph_craft::{document::NodeNetwork, graphene_compiler::Compiler}; +use graph_craft::{ + concrete, + document::{DocumentNode, DocumentNodeImplementation, NodeInput, NodeNetwork, value::TaggedValue}, + graphene_compiler::Compiler, +}; + use graphene_std::{ node_graph_overlay::{ types::NodeGraphTransform, ui_context::{UIContextImpl, UIRuntimeResponse}, }, - text::NewFontCache, + text::NewFontCacheWrapper, + uuid::NodeId, }; use crate::dynamic_executor::DynamicExecutor; @@ -18,11 +24,19 @@ pub struct NodeGraphUIRuntime { // Used within the node graph to return responses during evaluation // Also used to return compilation responses, but not for the UI overlay since the types are not needed pub response_sender: Sender, - pub font_collection: Arc>, + pub font_cache: NewFontCacheWrapper, } impl NodeGraphUIRuntime { - pub async fn compile(&mut self, compilation_request: CompilationRequest) { + pub async fn compile(&mut self, mut compilation_request: CompilationRequest) { + let font_cache_id = NodeId::new(); + let font_cache_node = DocumentNode { + inputs: vec![NodeInput::value(TaggedValue::NewFontCache(self.font_cache.clone()), false)], + implementation: DocumentNodeImplementation::ProtoNode(graphene_core::ops::identity::IDENTIFIER), + ..Default::default() + }; + compilation_request.network.nodes.insert(font_cache_id, font_cache_node); + compilation_request.network.scope_injections.insert("font-cache".to_string(), (font_cache_id, concrete!(()))); match self.compiler.compile_single(compilation_request.network) { Ok(proto_network) => { if let Err(e) = self.executor.update(proto_network).await {