From 76a2cbdb2529cab176fa4f58a19813f674bb71ef Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 31 Aug 2025 15:07:24 -0700 Subject: [PATCH 1/7] Move frontend node graph functions into separate file --- .../node_graph/node_graph_message_handler.rs | 92 +--- .../utility_types/network_interface.rs | 310 +------------- .../network_interface/node_graph.rs | 404 ++++++++++++++++++ 3 files changed, 409 insertions(+), 397 deletions(-) create mode 100644 editor/src/messages/portfolio/document/utility_types/network_interface/node_graph.rs 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 e86870f736..688f455f5b 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 @@ -1,4 +1,4 @@ -use super::utility_types::{BoxSelection, ContextMenuInformation, DragStart, FrontendNode}; +use super::utility_types::{BoxSelection, ContextMenuInformation, DragStart}; use super::{document_node_definitions, node_properties}; use crate::consts::GRID_SIZE; use crate::messages::input_mapper::utility_types::macros::action_keys; @@ -1634,7 +1634,7 @@ impl<'a> MessageHandler> for NodeG responses.add(DocumentMessage::DocumentStructureChanged); responses.add(PropertiesPanelMessage::Refresh); if breadcrumb_network_path == selection_network_path && graph_view_overlay_open { - let nodes = self.collect_nodes(network_interface, breadcrumb_network_path); + let nodes = network_interface.collect_nodes(&self.node_graph_errors, breadcrumb_network_path); self.frontend_nodes = nodes.iter().map(|node| node.id).collect(); responses.add(FrontendMessage::UpdateNodeGraphNodes { nodes }); responses.add(NodeGraphMessage::UpdateVisibleNodes); @@ -2499,94 +2499,6 @@ impl NodeGraphMessageHandler { added_wires } - fn collect_nodes(&self, network_interface: &mut NodeNetworkInterface, breadcrumb_network_path: &[NodeId]) -> Vec { - let Some(network) = network_interface.nested_network(breadcrumb_network_path) else { - log::error!("Could not get nested network when collecting nodes"); - return Vec::new(); - }; - let mut nodes = Vec::new(); - for (node_id, visible) in network.nodes.iter().map(|(node_id, node)| (*node_id, node.visible)).collect::>() { - let node_id_path = [breadcrumb_network_path, &[node_id]].concat(); - - let primary_input_connector = InputConnector::node(node_id, 0); - - let primary_input = if network_interface - .input_from_connector(&primary_input_connector, breadcrumb_network_path) - .is_some_and(|input| input.is_exposed()) - { - network_interface.frontend_input_from_connector(&primary_input_connector, breadcrumb_network_path) - } else { - None - }; - let exposed_inputs = (1..network_interface.number_of_inputs(&node_id, breadcrumb_network_path)) - .filter_map(|input_index| network_interface.frontend_input_from_connector(&InputConnector::node(node_id, input_index), breadcrumb_network_path)) - .collect(); - - let primary_output = network_interface.frontend_output_from_connector(&OutputConnector::node(node_id, 0), breadcrumb_network_path); - - let exposed_outputs = (1..network_interface.number_of_outputs(&node_id, breadcrumb_network_path)) - .filter_map(|output_index| network_interface.frontend_output_from_connector(&OutputConnector::node(node_id, output_index), breadcrumb_network_path)) - .collect(); - let (primary_output_connected_to_layer, primary_input_connected_to_layer) = if network_interface.is_layer(&node_id, breadcrumb_network_path) { - ( - network_interface.primary_output_connected_to_layer(&node_id, breadcrumb_network_path), - network_interface.primary_input_connected_to_layer(&node_id, breadcrumb_network_path), - ) - } else { - (false, false) - }; - - let is_export = network_interface - .input_from_connector(&InputConnector::Export(0), breadcrumb_network_path) - .is_some_and(|export| export.as_node().is_some_and(|export_node_id| node_id == export_node_id)); - let is_root_node = network_interface.root_node(breadcrumb_network_path).is_some_and(|root_node| root_node.node_id == node_id); - - let Some(position) = network_interface.position(&node_id, breadcrumb_network_path) else { - log::error!("Could not get position for node: {node_id}"); - continue; - }; - let previewed = is_export && !is_root_node; - - let locked = network_interface.is_locked(&node_id, breadcrumb_network_path); - - let errors = self - .node_graph_errors - .iter() - .find(|error| error.node_path == node_id_path) - .map(|error| format!("{:?}", error.error.clone())) - .or_else(|| { - if self.node_graph_errors.iter().any(|error| error.node_path.starts_with(&node_id_path)) { - Some("Node graph type error within this node".to_string()) - } else { - None - } - }); - - nodes.push(FrontendNode { - id: node_id, - is_layer: network_interface - .node_metadata(&node_id, breadcrumb_network_path) - .is_some_and(|node_metadata| node_metadata.persistent_metadata.is_layer()), - can_be_layer: network_interface.is_eligible_to_be_layer(&node_id, breadcrumb_network_path), - reference: network_interface.reference(&node_id, breadcrumb_network_path).cloned().unwrap_or_default(), - display_name: network_interface.display_name(&node_id, breadcrumb_network_path), - primary_input, - exposed_inputs, - primary_output, - exposed_outputs, - primary_output_connected_to_layer, - primary_input_connected_to_layer, - position, - previewed, - visible, - locked, - errors, - }); - } - - nodes - } - fn collect_subgraph_names(network_interface: &mut NodeNetworkInterface, breadcrumb_network_path: &[NodeId]) -> Option> { let mut current_network_path = vec![]; let mut current_network = network_interface.nested_network(¤t_network_path).unwrap(); diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface.rs b/editor/src/messages/portfolio/document/utility_types/network_interface.rs index 27f93d5ecb..97fcde3ac0 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface.rs @@ -4,7 +4,7 @@ use super::nodes::SelectedNodes; use crate::consts::{EXPORTS_TO_RIGHT_EDGE_PIXEL_GAP, EXPORTS_TO_TOP_EDGE_PIXEL_GAP, GRID_SIZE, IMPORTS_TO_LEFT_EDGE_PIXEL_GAP, IMPORTS_TO_TOP_EDGE_PIXEL_GAP}; use crate::messages::portfolio::document::graph_operation::utility_types::ModifyInputsContext; use crate::messages::portfolio::document::node_graph::document_node_definitions::{DocumentNodeDefinition, resolve_document_node_type}; -use crate::messages::portfolio::document::node_graph::utility_types::{Direction, FrontendClickTargets, FrontendGraphDataType, FrontendGraphInput, FrontendGraphOutput}; +use crate::messages::portfolio::document::node_graph::utility_types::{Direction, FrontendClickTargets, FrontendGraphDataType}; use crate::messages::portfolio::document::utility_types::network_interface::resolved_types::ResolvedDocumentNodeTypes; use crate::messages::portfolio::document::utility_types::wires::{GraphWireStyle, WirePath, WirePathUpdate, build_vector_wire}; use crate::messages::tool::common_functionality::graph_modification_utils; @@ -27,6 +27,7 @@ use std::hash::Hash; use std::ops::Deref; mod deserialization; +pub mod node_graph; pub mod resolved_types; /// All network modifications should be done through this API, so the fields cannot be public. However, all fields within this struct can be public since it it not possible to have a public mutable reference. @@ -227,31 +228,6 @@ impl NodeNetworkInterface { layers } - pub fn chain_width(&self, node_id: &NodeId, network_path: &[NodeId]) -> u32 { - if self.number_of_displayed_inputs(node_id, network_path) > 1 { - let mut last_chain_node_distance = 0u32; - // Iterate upstream from the layer, and get the number of nodes distance to the last node with Position::Chain - for (index, node_id) in self - .upstream_flow_back_from_nodes(vec![*node_id], network_path, FlowType::HorizontalPrimaryOutputFlow) - .skip(1) - .enumerate() - .collect::>() - { - // Check if the node is positioned as a chain - if self.is_chain(&node_id, network_path) { - last_chain_node_distance = (index as u32) + 1; - } else { - return last_chain_node_distance * 7 + 1; - } - } - - last_chain_node_distance * 7 + 1 - } else { - // Layer with no inputs has no chain - 0 - } - } - /// Check if the specified node id is connected to the output pub fn connected_to_output(&self, target_node_id: &NodeId, network_path: &[NodeId]) -> bool { let Some(network) = self.nested_network(network_path) else { @@ -506,256 +482,6 @@ impl NodeNetworkInterface { }) } - pub fn frontend_imports(&mut self, network_path: &[NodeId]) -> Vec> { - match network_path.split_last() { - Some((node_id, encapsulating_network_path)) => { - let Some(node) = self.document_node(node_id, encapsulating_network_path) else { - log::error!("Could not get node {node_id} in network {encapsulating_network_path:?}"); - return Vec::new(); - }; - let mut frontend_imports = (0..node.inputs.len()) - .map(|import_index| self.frontend_output_from_connector(&OutputConnector::Import(import_index), network_path)) - .collect::>(); - if frontend_imports.is_empty() { - frontend_imports.push(None); - } - frontend_imports - } - // In the document network display no imports - None => Vec::new(), - } - } - - pub fn frontend_exports(&mut self, network_path: &[NodeId]) -> Vec> { - let Some(network) = self.nested_network(network_path) else { return Vec::new() }; - let mut frontend_exports = ((0..network.exports.len()).map(|export_index| self.frontend_input_from_connector(&InputConnector::Export(export_index), network_path))).collect::>(); - if frontend_exports.is_empty() { - frontend_exports.push(None); - } - frontend_exports - } - - pub fn import_export_position(&mut self, network_path: &[NodeId]) -> Option<(IVec2, IVec2)> { - let Some(all_nodes_bounding_box) = self.all_nodes_bounding_box(network_path).cloned() else { - log::error!("Could not get all nodes bounding box in load_export_ports"); - return None; - }; - let Some(network) = self.nested_network(network_path) else { - log::error!("Could not get current network in load_export_ports"); - return None; - }; - - let Some(network_metadata) = self.network_metadata(network_path) else { - log::error!("Could not get nested network_metadata in load_export_ports"); - return None; - }; - let node_graph_to_viewport = network_metadata.persistent_metadata.navigation_metadata.node_graph_to_viewport; - let target_viewport_top_left = DVec2::new(IMPORTS_TO_LEFT_EDGE_PIXEL_GAP as f64, IMPORTS_TO_TOP_EDGE_PIXEL_GAP as f64); - - let node_graph_pixel_offset_top_left = node_graph_to_viewport.inverse().transform_point2(target_viewport_top_left); - - // A 5x5 grid offset from the top left corner - let node_graph_grid_space_offset_top_left = node_graph_to_viewport.inverse().transform_point2(DVec2::ZERO) + DVec2::new(5. * GRID_SIZE as f64, 4. * GRID_SIZE as f64); - - // The inner bound of the import is the highest/furthest left of the two offsets - let top_left_inner_bound = DVec2::new( - node_graph_pixel_offset_top_left.x.min(node_graph_grid_space_offset_top_left.x), - node_graph_pixel_offset_top_left.y.min(node_graph_grid_space_offset_top_left.y), - ); - - let offset_from_top_left = if network - .exports - .first() - .is_some_and(|export| export.as_node().is_some_and(|export_node| self.is_layer(&export_node, network_path))) - { - DVec2::new(-4. * GRID_SIZE as f64, -2. * GRID_SIZE as f64) - } else { - DVec2::new(-4. * GRID_SIZE as f64, 0.) - }; - - let bounding_box_top_left = DVec2::new((all_nodes_bounding_box[0].x / 24. + 0.5).floor() * 24., (all_nodes_bounding_box[0].y / 24. + 0.5).floor() * 24.) + offset_from_top_left; - let import_top_left = DVec2::new(top_left_inner_bound.x.min(bounding_box_top_left.x), top_left_inner_bound.y.min(bounding_box_top_left.y)); - let rounded_import_top_left = DVec2::new((import_top_left.x / 24.).round() * 24., (import_top_left.y / 24.).round() * 24.); - - let viewport_top_right = network_metadata.persistent_metadata.navigation_metadata.node_graph_top_right; - let target_viewport_top_right = DVec2::new( - viewport_top_right.x - EXPORTS_TO_RIGHT_EDGE_PIXEL_GAP as f64, - viewport_top_right.y + EXPORTS_TO_TOP_EDGE_PIXEL_GAP as f64, - ); - - // An offset from the right edge in viewport pixels - let node_graph_pixel_offset_top_right = node_graph_to_viewport.inverse().transform_point2(target_viewport_top_right); - - // A 5x5 grid offset from the right corner - let node_graph_grid_space_offset_top_right = node_graph_to_viewport.inverse().transform_point2(viewport_top_right) + DVec2::new(-5. * GRID_SIZE as f64, 4. * GRID_SIZE as f64); - - // The inner bound of the export is the highest/furthest right of the two offsets - let top_right_inner_bound = DVec2::new( - node_graph_pixel_offset_top_right.x.max(node_graph_grid_space_offset_top_right.x), - node_graph_pixel_offset_top_right.y.min(node_graph_grid_space_offset_top_right.y), - ); - - let offset_from_top_right = if network - .exports - .first() - .is_some_and(|export| export.as_node().is_some_and(|export_node| self.is_layer(&export_node, network_path))) - { - DVec2::new(2. * GRID_SIZE as f64, -2. * GRID_SIZE as f64) - } else { - DVec2::new(4. * GRID_SIZE as f64, 0.) - }; - - let mut bounding_box_top_right = DVec2::new((all_nodes_bounding_box[1].x / 24. + 0.5).floor() * 24., (all_nodes_bounding_box[0].y / 24. + 0.5).floor() * 24.); - bounding_box_top_right += offset_from_top_right; - let export_top_right = DVec2::new(top_right_inner_bound.x.max(bounding_box_top_right.x), top_right_inner_bound.y.min(bounding_box_top_right.y)); - let rounded_export_top_right = DVec2::new((export_top_right.x / 24.).round() * 24., (export_top_right.y / 24.).round() * 24.); - - Some((rounded_import_top_left.as_ivec2(), rounded_export_top_right.as_ivec2())) - } - - /// Returns None if there is an error, it is a hidden primary export, or a hidden input - pub fn frontend_input_from_connector(&mut self, input_connector: &InputConnector, network_path: &[NodeId]) -> Option { - // Return None if it is a hidden input - if self.input_from_connector(input_connector, network_path).is_some_and(|input| !input.is_exposed()) { - return None; - } - let input_type = self.input_type(input_connector, network_path); - let data_type = FrontendGraphDataType::displayed_type(&input_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), - 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 - let export_name = if network_path.is_empty() { - "Canvas".to_string() - } else { - self.encapsulating_node_metadata(network_path) - .and_then(|encapsulating_metadata| encapsulating_metadata.persistent_metadata.output_names.get(*export_index).cloned()) - .unwrap_or_default() - }; - - let export_name = 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() - // } - // }; - - Some(FrontendGraphInput { - data_type, - resolved_type, - name, - description, - connected_to, - }) - } - - /// 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 { - 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) { - return None; - }; - // Get the output name from the interior network export name - 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()) - } - OutputConnector::Import(import_index) => { - // Get the import name from the encapsulating node input metadata - let Some((encapsulating_node_id, encapsulating_path)) = network_path.split_last() else { - // Return None if it is an import in the document network - return None; - }; - // Return None if the primary input is hidden and this is the primary import - 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 = 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 = FrontendGraphDataType::displayed_type(&output_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, - }) - } - pub fn height_from_click_target(&mut self, node_id: &NodeId, network_path: &[NodeId]) -> Option { let mut node_height: Option = self .node_click_targets(node_id, network_path) @@ -1047,28 +773,6 @@ impl NodeNetworkInterface { node_metadata.persistent_metadata.is_layer() } - pub fn primary_output_connected_to_layer(&mut self, node_id: &NodeId, network_path: &[NodeId]) -> bool { - let Some(outward_wires) = self.outward_wires(network_path) else { - log::error!("Could not get outward_wires in primary_output_connected_to_layer"); - return false; - }; - let Some(downstream_connectors) = outward_wires.get(&OutputConnector::node(*node_id, 0)) else { - log::error!("Could not get downstream_connectors in primary_output_connected_to_layer"); - return false; - }; - let downstream_nodes = downstream_connectors - .iter() - .filter_map(|connector| if connector.input_index() == 0 { connector.node_id() } else { None }) - .collect::>(); - downstream_nodes.iter().any(|node_id| self.is_layer(node_id, network_path)) - } - - pub fn primary_input_connected_to_layer(&mut self, node_id: &NodeId, network_path: &[NodeId]) -> bool { - self.input_from_connector(&InputConnector::node(*node_id, 0), network_path) - .and_then(|input| input.as_node()) - .is_some_and(|node_id| self.is_layer(&node_id, network_path)) - } - pub fn hidden_primary_export(&self, network_path: &[NodeId]) -> bool { let Some((node_id, network_path)) = network_path.split_last() else { // The document network does not have a hidden primary export @@ -3167,15 +2871,7 @@ impl NodeNetworkInterface { let chain_widths = nodes.iter().map(|node_id| (*node_id, self.chain_width(node_id, network_path))).collect::>(); let has_left_input_wire = nodes .iter() - .map(|node_id| { - ( - *node_id, - !self - .upstream_flow_back_from_nodes(vec![*node_id], network_path, FlowType::HorizontalFlow) - .skip(1) - .all(|node_id| self.is_chain(&node_id, network_path)), - ) - }) + .map(|node_id| (*node_id, self.layer_has_left_border_gap(node_id, network_path))) .collect::>(); (layer_widths, chain_widths, has_left_input_wire) 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 new file mode 100644 index 0000000000..b1d94d9ab8 --- /dev/null +++ b/editor/src/messages/portfolio/document/utility_types/network_interface/node_graph.rs @@ -0,0 +1,404 @@ +use glam::{DVec2, IVec2}; +use graph_craft::proto::GraphErrors; +use graphene_std::uuid::NodeId; + +use crate::{ + consts::{EXPORTS_TO_RIGHT_EDGE_PIXEL_GAP, EXPORTS_TO_TOP_EDGE_PIXEL_GAP, GRID_SIZE, IMPORTS_TO_LEFT_EDGE_PIXEL_GAP, IMPORTS_TO_TOP_EDGE_PIXEL_GAP}, + messages::portfolio::document::{ + node_graph::utility_types::{FrontendGraphDataType, FrontendGraphInput, FrontendGraphOutput, FrontendNode}, + utility_types::network_interface::{FlowType, InputConnector, NodeNetworkInterface, OutputConnector}, + }, +}; + +// Functions used to collect data from the network interface for use in rendering the node graph +impl NodeNetworkInterface { + pub fn collect_nodes(&mut self, node_graph_errors: &GraphErrors, network_path: &[NodeId]) -> Vec { + let Some(network) = self.nested_network(network_path) else { + log::error!("Could not get nested network when collecting nodes"); + return Vec::new(); + }; + let mut nodes = Vec::new(); + for (node_id, visible) in network.nodes.iter().map(|(node_id, node)| (*node_id, node.visible)).collect::>() { + let node_id_path = [network_path, &[node_id]].concat(); + + let primary_input_connector = InputConnector::node(node_id, 0); + + let primary_input = if self.input_from_connector(&primary_input_connector, network_path).is_some_and(|input| input.is_exposed()) { + self.frontend_input_from_connector(&primary_input_connector, network_path) + } else { + None + }; + let exposed_inputs = (1..self.number_of_inputs(&node_id, network_path)) + .filter_map(|input_index| self.frontend_input_from_connector(&InputConnector::node(node_id, input_index), network_path)) + .collect(); + + let primary_output = self.frontend_output_from_connector(&OutputConnector::node(node_id, 0), network_path); + + let exposed_outputs = (1..self.number_of_outputs(&node_id, network_path)) + .filter_map(|output_index| self.frontend_output_from_connector(&OutputConnector::node(node_id, output_index), network_path)) + .collect(); + let (primary_output_connected_to_layer, primary_input_connected_to_layer) = if self.is_layer(&node_id, network_path) { + ( + self.primary_output_connected_to_layer(&node_id, network_path), + self.primary_input_connected_to_layer(&node_id, network_path), + ) + } else { + (false, false) + }; + + let Some(position) = self.position(&node_id, network_path) else { + log::error!("Could not get position for node: {node_id}"); + continue; + }; + let previewed = self.previewed_node(network_path) == Some(node_id); + + let locked = self.is_locked(&node_id, network_path); + + let errors = node_graph_errors + .iter() + .find(|error| error.node_path == node_id_path) + .map(|error| format!("{:?}", error.error.clone())) + .or_else(|| { + if node_graph_errors.iter().any(|error| error.node_path.starts_with(&node_id_path)) { + Some("Node graph type error within this node".to_string()) + } else { + None + } + }); + + nodes.push(FrontendNode { + id: node_id, + is_layer: self.node_metadata(&node_id, network_path).is_some_and(|node_metadata| node_metadata.persistent_metadata.is_layer()), + can_be_layer: self.is_eligible_to_be_layer(&node_id, network_path), + reference: self.reference(&node_id, network_path).cloned().unwrap_or_default(), + display_name: self.display_name(&node_id, network_path), + primary_input, + exposed_inputs, + primary_output, + exposed_outputs, + primary_output_connected_to_layer, + primary_input_connected_to_layer, + position, + previewed, + visible, + locked, + errors, + }); + } + + nodes + } + + /// Returns None if there is an error, it is a hidden primary export, or a hidden input + pub fn frontend_input_from_connector(&mut self, input_connector: &InputConnector, network_path: &[NodeId]) -> Option { + // Return None if it is a hidden input or doesn't exist + if self.input_from_connector(input_connector, network_path).is_some_and(|input| !input.is_exposed()) { + return None; + } + let input_type = self.input_type(input_connector, network_path); + let data_type = FrontendGraphDataType::displayed_type(&input_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), + 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 + let export_name = if network_path.is_empty() { + "Canvas".to_string() + } else { + self.encapsulating_node_metadata(network_path) + .and_then(|encapsulating_metadata| encapsulating_metadata.persistent_metadata.output_names.get(*export_index).cloned()) + .unwrap_or_default() + }; + + let export_name = 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() + // } + // }; + + Some(FrontendGraphInput { + data_type, + resolved_type, + name, + description, + connected_to, + }) + } + + /// 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 { + 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) { + return None; + }; + // Get the output name from the interior network export name + 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()) + } + OutputConnector::Import(import_index) => { + // Get the import name from the encapsulating node input metadata + let Some((encapsulating_node_id, encapsulating_path)) = network_path.split_last() else { + // Return None if it is an import in the document network + return None; + }; + // Return None if the primary input is hidden and this is the primary import + 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 = 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 = FrontendGraphDataType::displayed_type(&output_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, + }) + } + + pub fn chain_width(&self, node_id: &NodeId, network_path: &[NodeId]) -> u32 { + if self.number_of_displayed_inputs(node_id, network_path) > 1 { + let mut last_chain_node_distance = 0u32; + // Iterate upstream from the layer, and get the number of nodes distance to the last node with Position::Chain + for (index, node_id) in self + .upstream_flow_back_from_nodes(vec![*node_id], network_path, FlowType::HorizontalPrimaryOutputFlow) + .skip(1) + .enumerate() + .collect::>() + { + // Check if the node is positioned as a chain + if self.is_chain(&node_id, network_path) { + last_chain_node_distance = (index as u32) + 1; + } else { + return last_chain_node_distance * 7 + 1; + } + } + + last_chain_node_distance * 7 + 1 + } else { + // Layer with no inputs has no chain + 0 + } + } + + /// Checks if a layer should display a gap in its left border + pub fn layer_has_left_border_gap(&self, node_id: &NodeId, network_path: &[NodeId]) -> bool { + self.upstream_flow_back_from_nodes(vec![*node_id], network_path, FlowType::HorizontalFlow) + .skip(1) + .any(|node_id| !self.is_chain(&node_id, network_path)) + } + + /// Returns the node which should have a dashed border drawn around it + pub fn previewed_node(&self, network_path: &[NodeId]) -> Option { + self.upstream_output_connector(&InputConnector::Export(0), network_path) + .and_then(|output_connector| output_connector.node_id()) + .filter(|output_node| self.root_node(network_path).is_some_and(|root_node| root_node.node_id != *output_node)) + } + + /// If any downstream input are bottom layer inputs, then the thick cap should be displayed above the output port + fn primary_output_connected_to_layer(&mut self, node_id: &NodeId, network_path: &[NodeId]) -> bool { + let Some(outward_wires) = self.outward_wires(network_path) else { + log::error!("Could not get outward_wires in primary_output_connected_to_layer"); + return false; + }; + let Some(downstream_connectors) = outward_wires.get(&OutputConnector::node(*node_id, 0)) else { + log::error!("Could not get downstream_connectors in primary_output_connected_to_layer"); + return false; + }; + let downstream_nodes = downstream_connectors + .iter() + .filter_map(|connector| if connector.input_index() == 0 { connector.node_id() } else { None }) + .collect::>(); + downstream_nodes.iter().any(|node_id| self.is_layer(node_id, network_path)) + } + + /// If any upstream nodes are layers, then the thick cap should be displayed below the primary input port + fn primary_input_connected_to_layer(&mut self, node_id: &NodeId, network_path: &[NodeId]) -> bool { + self.input_from_connector(&InputConnector::node(*node_id, 0), network_path) + .and_then(|input| input.as_node()) + .is_some_and(|node_id| self.is_layer(&node_id, network_path)) + } + + pub fn frontend_imports(&mut self, network_path: &[NodeId]) -> Vec> { + match network_path.split_last() { + Some((node_id, encapsulatingnetwork_path)) => { + let Some(node) = self.document_node(node_id, encapsulatingnetwork_path) else { + log::error!("Could not get node {node_id} in network {encapsulatingnetwork_path:?}"); + return Vec::new(); + }; + let mut frontend_imports = (0..node.inputs.len()) + .map(|import_index| self.frontend_output_from_connector(&OutputConnector::Import(import_index), network_path)) + .collect::>(); + if frontend_imports.is_empty() { + frontend_imports.push(None); + } + frontend_imports + } + // In the document network display no imports + None => Vec::new(), + } + } + + pub fn frontend_exports(&mut self, network_path: &[NodeId]) -> Vec> { + let Some(network) = self.nested_network(network_path) else { return Vec::new() }; + let mut frontend_exports = ((0..network.exports.len()).map(|export_index| self.frontend_input_from_connector(&InputConnector::Export(export_index), network_path))).collect::>(); + if frontend_exports.is_empty() { + frontend_exports.push(None); + } + frontend_exports + } + + pub fn import_export_position(&mut self, network_path: &[NodeId]) -> Option<(IVec2, IVec2)> { + let Some(all_nodes_bounding_box) = self.all_nodes_bounding_box(network_path).cloned() else { + log::error!("Could not get all nodes bounding box in load_export_ports"); + return None; + }; + let Some(network) = self.nested_network(network_path) else { + log::error!("Could not get current network in load_export_ports"); + return None; + }; + + let Some(network_metadata) = self.network_metadata(network_path) else { + log::error!("Could not get nested network_metadata in load_export_ports"); + return None; + }; + let node_graph_to_viewport = network_metadata.persistent_metadata.navigation_metadata.node_graph_to_viewport; + let target_viewport_top_left = DVec2::new(IMPORTS_TO_LEFT_EDGE_PIXEL_GAP as f64, IMPORTS_TO_TOP_EDGE_PIXEL_GAP as f64); + + let node_graph_pixel_offset_top_left = node_graph_to_viewport.inverse().transform_point2(target_viewport_top_left); + + // A 5x5 grid offset from the top left corner + let node_graph_grid_space_offset_top_left = node_graph_to_viewport.inverse().transform_point2(DVec2::ZERO) + DVec2::new(5. * GRID_SIZE as f64, 4. * GRID_SIZE as f64); + + // The inner bound of the import is the highest/furthest left of the two offsets + let top_left_inner_bound = DVec2::new( + node_graph_pixel_offset_top_left.x.min(node_graph_grid_space_offset_top_left.x), + node_graph_pixel_offset_top_left.y.min(node_graph_grid_space_offset_top_left.y), + ); + + let offset_from_top_left = if network + .exports + .first() + .is_some_and(|export| export.as_node().is_some_and(|export_node| self.is_layer(&export_node, network_path))) + { + DVec2::new(-4. * GRID_SIZE as f64, -2. * GRID_SIZE as f64) + } else { + DVec2::new(-4. * GRID_SIZE as f64, 0.) + }; + + let bounding_box_top_left = DVec2::new((all_nodes_bounding_box[0].x / 24. + 0.5).floor() * 24., (all_nodes_bounding_box[0].y / 24. + 0.5).floor() * 24.) + offset_from_top_left; + let import_top_left = DVec2::new(top_left_inner_bound.x.min(bounding_box_top_left.x), top_left_inner_bound.y.min(bounding_box_top_left.y)); + let rounded_import_top_left = DVec2::new((import_top_left.x / 24.).round() * 24., (import_top_left.y / 24.).round() * 24.); + + let viewport_top_right = network_metadata.persistent_metadata.navigation_metadata.node_graph_top_right; + let target_viewport_top_right = DVec2::new( + viewport_top_right.x - EXPORTS_TO_RIGHT_EDGE_PIXEL_GAP as f64, + viewport_top_right.y + EXPORTS_TO_TOP_EDGE_PIXEL_GAP as f64, + ); + + // An offset from the right edge in viewport pixels + let node_graph_pixel_offset_top_right = node_graph_to_viewport.inverse().transform_point2(target_viewport_top_right); + + // A 5x5 grid offset from the right corner + let node_graph_grid_space_offset_top_right = node_graph_to_viewport.inverse().transform_point2(viewport_top_right) + DVec2::new(-5. * GRID_SIZE as f64, 4. * GRID_SIZE as f64); + + // The inner bound of the export is the highest/furthest right of the two offsets + let top_right_inner_bound = DVec2::new( + node_graph_pixel_offset_top_right.x.max(node_graph_grid_space_offset_top_right.x), + node_graph_pixel_offset_top_right.y.min(node_graph_grid_space_offset_top_right.y), + ); + + let offset_from_top_right = if network + .exports + .first() + .is_some_and(|export| export.as_node().is_some_and(|export_node| self.is_layer(&export_node, network_path))) + { + DVec2::new(2. * GRID_SIZE as f64, -2. * GRID_SIZE as f64) + } else { + DVec2::new(4. * GRID_SIZE as f64, 0.) + }; + + let mut bounding_box_top_right = DVec2::new((all_nodes_bounding_box[1].x / 24. + 0.5).floor() * 24., (all_nodes_bounding_box[0].y / 24. + 0.5).floor() * 24.); + bounding_box_top_right += offset_from_top_right; + let export_top_right = DVec2::new(top_right_inner_bound.x.max(bounding_box_top_right.x), top_right_inner_bound.y.min(bounding_box_top_right.y)); + let rounded_export_top_right = DVec2::new((export_top_right.x / 24.).round() * 24., (export_top_right.y / 24.).round() * 24.); + + Some((rounded_import_top_left.as_ivec2(), rounded_export_top_right.as_ivec2())) + } +} From 1996dd205ad1b9f5877ac3ef2f5b87d491157dd1 Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 31 Aug 2025 17:42:52 -0700 Subject: [PATCH 2/7] Consolidate frontend node --- editor/src/dispatcher.rs | 1 + .../src/messages/frontend/frontend_message.rs | 26 ++-- .../document/document_message_handler.rs | 2 +- .../document/node_graph/node_graph_message.rs | 2 - .../node_graph/node_graph_message_handler.rs | 49 ++++--- .../document/node_graph/utility_types.rs | 14 +- .../utility_types/network_interface.rs | 12 +- .../network_interface/node_graph.rs | 36 +++--- frontend/src/components/views/Graph.svelte | 120 +++++++----------- frontend/src/messages.ts | 34 ++--- frontend/src/state-providers/node-graph.ts | 31 ++--- 11 files changed, 136 insertions(+), 191 deletions(-) diff --git a/editor/src/dispatcher.rs b/editor/src/dispatcher.rs index 7e73a54fcf..3942b3d3ab 100644 --- a/editor/src/dispatcher.rs +++ b/editor/src/dispatcher.rs @@ -41,6 +41,7 @@ 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, ))), diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index bbba55c57a..b8969308c7 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -2,13 +2,12 @@ use super::utility_types::{DocumentDetails, MouseCursorIcon, OpenDocument}; use crate::messages::app_window::app_window_message_handler::AppWindowPlatform; use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::node_graph::utility_types::{ - BoxSelection, ContextMenuInformation, FrontendClickTargets, FrontendGraphInput, FrontendGraphOutput, FrontendNode, FrontendNodeType, Transform, + BoxSelection, ContextMenuInformation, FrontendClickTargets, FrontendGraphInput, FrontendGraphOutput, FrontendNode, FrontendNodeType, FrontendXY, Transform, }; use crate::messages::portfolio::document::utility_types::nodes::{JsRawBuffer, LayerPanelEntry, RawBuffer}; use crate::messages::portfolio::document::utility_types::wires::{WirePath, WirePathUpdate}; use crate::messages::prelude::*; use crate::messages::tool::utility_types::HintData; -use glam::IVec2; use graph_craft::document::NodeId; use graphene_std::raster::Image; use graphene_std::raster::color::Color; @@ -133,18 +132,14 @@ pub enum FrontendMessage { exports: Vec>, /// The primary import location. #[serde(rename = "importPosition")] - import_position: IVec2, + import_position: FrontendXY, /// The primary export location. #[serde(rename = "exportPosition")] - export_position: IVec2, + export_position: FrontendXY, /// The document network does not have an add import or export button. #[serde(rename = "addImportExport")] add_import_export: bool, }, - UpdateInSelectedNetwork { - #[serde(rename = "inSelectedNetwork")] - in_selected_network: bool, - }, UpdateBox { #[serde(rename = "box")] box_selection: Option, @@ -185,10 +180,6 @@ pub enum FrontendMessage { UpdateLayerWidths { #[serde(rename = "layerWidths")] layer_widths: HashMap, - #[serde(rename = "chainWidths")] - chain_widths: HashMap, - #[serde(rename = "hasLeftInputWire")] - has_left_input_wire: HashMap, }, UpdateDialogButtons { #[serde(rename = "layoutTarget")] @@ -284,7 +275,13 @@ pub enum FrontendMessage { cursor: MouseCursorIcon, }, UpdateNodeGraphNodes { - nodes: Vec, + #[serde(rename = "nodesToRender")] + nodes_to_render: Vec, + #[serde(rename = "inSelectedNetwork")] + in_selected_network: bool, + // Displays a dashed border around the node + #[serde(rename = "previewedNode")] + previewed_node: Option, }, UpdateVisibleNodes { nodes: Vec, @@ -298,9 +295,6 @@ pub enum FrontendMessage { layout_target: LayoutTarget, diff: Vec, }, - UpdateNodeGraphSelection { - selected: Vec, - }, UpdateNodeGraphTransform { transform: Transform, }, diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index d7f2257b68..88aeacacf1 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -1191,7 +1191,7 @@ impl MessageHandler> for DocumentMes } responses.add(PropertiesPanelMessage::Refresh); responses.add(NodeGraphMessage::UpdateLayerPanel); - responses.add(NodeGraphMessage::UpdateInSelectedNetwork); + responses.add(NodeGraphMessage::SendGraph); } DocumentMessage::SetBlendModeForSelectedLayers { blend_mode } => { for layer in self.network_interface.selected_nodes().selected_layers_except_artboards(&self.network_interface) { 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 1432046a25..37ac29df44 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 @@ -231,7 +231,5 @@ pub enum NodeGraphMessage { }, UpdateActionButtons, UpdateGraphBarRight, - UpdateInSelectedNetwork, UpdateHints, - SendSelectedNodes, } 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 688f455f5b..7af7cd2ee5 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 @@ -6,7 +6,7 @@ use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::document_message_handler::navigation_controls; use crate::messages::portfolio::document::graph_operation::utility_types::ModifyInputsContext; use crate::messages::portfolio::document::node_graph::document_node_definitions::NodePropertiesContext; -use crate::messages::portfolio::document::node_graph::utility_types::{ContextMenuData, Direction, FrontendGraphDataType}; +use crate::messages::portfolio::document::node_graph::utility_types::{ContextMenuData, Direction, FrontendGraphDataType, FrontendXY}; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::misc::GroupFolderType; use crate::messages::portfolio::document::utility_types::network_interface::{ @@ -192,7 +192,7 @@ impl<'a> MessageHandler> for NodeG responses.add(MenuBarMessage::SendLayout); responses.add(NodeGraphMessage::UpdateLayerPanel); responses.add(PropertiesPanelMessage::Refresh); - responses.add(NodeGraphMessage::SendSelectedNodes); + responses.add(NodeGraphMessage::SendGraph); responses.add(ArtboardToolMessage::UpdateSelectedArtboard); responses.add(DocumentMessage::DocumentStructureChanged); responses.add(OverlaysMessage::Draw); @@ -1633,21 +1633,22 @@ impl<'a> MessageHandler> for NodeG responses.add(NodeGraphMessage::UpdateLayerPanel); responses.add(DocumentMessage::DocumentStructureChanged); responses.add(PropertiesPanelMessage::Refresh); - if breadcrumb_network_path == selection_network_path && graph_view_overlay_open { - let nodes = network_interface.collect_nodes(&self.node_graph_errors, breadcrumb_network_path); - self.frontend_nodes = nodes.iter().map(|node| node.id).collect(); - responses.add(FrontendMessage::UpdateNodeGraphNodes { nodes }); + responses.add(NodeGraphMessage::UpdateActionButtons); + if graph_view_overlay_open { + let nodes_to_render = network_interface.collect_nodes(&self.node_graph_errors, breadcrumb_network_path); + self.frontend_nodes = nodes_to_render.iter().map(|node| node.id).collect(); + let previewed_node = network_interface.previewed_node(breadcrumb_network_path); + responses.add(FrontendMessage::UpdateNodeGraphNodes { + nodes_to_render, + in_selected_network: selection_network_path == breadcrumb_network_path, + previewed_node, + }); responses.add(NodeGraphMessage::UpdateVisibleNodes); - let (layer_widths, chain_widths, has_left_input_wire) = network_interface.collect_layer_widths(breadcrumb_network_path); + let layer_widths = network_interface.collect_layer_widths(breadcrumb_network_path); responses.add(NodeGraphMessage::UpdateImportsExports); - responses.add(FrontendMessage::UpdateLayerWidths { - layer_widths, - chain_widths, - has_left_input_wire, - }); - responses.add(NodeGraphMessage::SendSelectedNodes); + responses.add(FrontendMessage::UpdateLayerWidths { layer_widths }); responses.add(NodeGraphMessage::SendWires); self.update_node_graph_hints(responses); } @@ -1956,6 +1957,15 @@ impl<'a> MessageHandler> for NodeG return; }; + let import_position = FrontendXY { + x: import_position.x, + y: import_position.y, + }; + let export_position = FrontendXY { + x: export_position.x, + y: export_position.y, + }; + // Do not show the add import or add export button in the document network; let add_import_export = !breadcrumb_network_path.is_empty(); @@ -2000,22 +2010,9 @@ impl<'a> MessageHandler> for NodeG self.update_graph_bar_right(graph_fade_artwork_percentage, network_interface, breadcrumb_network_path, navigation_handler); self.send_node_bar_layout(responses); } - NodeGraphMessage::UpdateInSelectedNetwork => responses.add(FrontendMessage::UpdateInSelectedNetwork { - in_selected_network: selection_network_path == breadcrumb_network_path, - }), NodeGraphMessage::UpdateHints => { self.update_node_graph_hints(responses); } - NodeGraphMessage::SendSelectedNodes => { - let Some(selected_nodes) = network_interface.selected_nodes_in_nested_network(breadcrumb_network_path) else { - log::error!("Could not get selected nodes in NodeGraphMessage::SendSelectedNodes"); - return; - }; - responses.add(NodeGraphMessage::UpdateActionButtons); - responses.add(FrontendMessage::UpdateNodeGraphSelection { - selected: selected_nodes.selected_nodes().cloned().collect::>(), - }); - } } let Some(selected_nodes) = network_interface.selected_nodes_in_nested_network(selection_network_path) else { log::error!("Could not get selected nodes in NodeGraphMessageHandler"); diff --git a/editor/src/messages/portfolio/document/node_graph/utility_types.rs b/editor/src/messages/portfolio/document/node_graph/utility_types.rs index 07741ee003..45dd0532f2 100644 --- a/editor/src/messages/portfolio/document/node_graph/utility_types.rs +++ b/editor/src/messages/portfolio/document/node_graph/utility_types.rs @@ -1,4 +1,3 @@ -use glam::IVec2; use graph_craft::document::NodeId; use graph_craft::document::value::TaggedValue; use graphene_std::Type; @@ -51,6 +50,12 @@ impl FrontendGraphDataType { } } +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] +pub struct FrontendXY { + pub x: i32, + pub y: i32, +} + #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] pub struct FrontendGraphInput { #[serde(rename = "dataType")] @@ -85,6 +90,7 @@ pub struct FrontendNode { pub is_layer: bool, #[serde(rename = "canBeLayer")] pub can_be_layer: bool, + pub selected: bool, pub reference: Option, #[serde(rename = "displayName")] pub display_name: String, @@ -96,11 +102,15 @@ pub struct FrontendNode { pub primary_output: Option, #[serde(rename = "exposedOutputs")] pub exposed_outputs: Vec, + #[serde(rename = "chainWidth")] + pub chain_width: u32, + #[serde(rename = "layerHasLeftBorderGap")] + pub layer_has_left_border_gap: bool, #[serde(rename = "primaryOutputConnectedToLayer")] pub primary_output_connected_to_layer: bool, #[serde(rename = "primaryInputConnectedToLayer")] pub primary_input_connected_to_layer: bool, - pub position: IVec2, + pub position: FrontendXY, pub visible: bool, pub locked: bool, pub previewed: bool, diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface.rs b/editor/src/messages/portfolio/document/utility_types/network_interface.rs index 97fcde3ac0..fecba7ed14 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface.rs @@ -2853,10 +2853,11 @@ impl NodeNetworkInterface { bounding_box_subpath.bounding_box_with_transform(network_metadata.persistent_metadata.navigation_metadata.node_graph_to_viewport) } - pub fn collect_layer_widths(&mut self, network_path: &[NodeId]) -> (HashMap, HashMap, HashMap) { + // TODO: Remove and get layer click targets from render output + pub fn collect_layer_widths(&mut self, network_path: &[NodeId]) -> HashMap { let Some(network_metadata) = self.network_metadata(network_path) else { log::error!("Could not get nested network_metadata in collect_layer_widths"); - return (HashMap::new(), HashMap::new(), HashMap::new()); + return HashMap::new(); }; let nodes = network_metadata .persistent_metadata @@ -2868,13 +2869,8 @@ impl NodeNetworkInterface { .iter() .filter_map(|node_id| self.layer_width(node_id, network_path).map(|layer_width| (*node_id, layer_width))) .collect::>(); - let chain_widths = nodes.iter().map(|node_id| (*node_id, self.chain_width(node_id, network_path))).collect::>(); - let has_left_input_wire = nodes - .iter() - .map(|node_id| (*node_id, self.layer_has_left_border_gap(node_id, network_path))) - .collect::>(); - (layer_widths, chain_widths, has_left_input_wire) + layer_widths } pub fn compute_modified_vector(&self, layer: LayerNodeIdentifier) -> Option { 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 b1d94d9ab8..055003117e 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 @@ -5,7 +5,7 @@ use graphene_std::uuid::NodeId; use crate::{ consts::{EXPORTS_TO_RIGHT_EDGE_PIXEL_GAP, EXPORTS_TO_TOP_EDGE_PIXEL_GAP, GRID_SIZE, IMPORTS_TO_LEFT_EDGE_PIXEL_GAP, IMPORTS_TO_TOP_EDGE_PIXEL_GAP}, messages::portfolio::document::{ - node_graph::utility_types::{FrontendGraphDataType, FrontendGraphInput, FrontendGraphOutput, FrontendNode}, + node_graph::utility_types::{FrontendGraphDataType, FrontendGraphInput, FrontendGraphOutput, FrontendNode, FrontendXY}, utility_types::network_interface::{FlowType, InputConnector, NodeNetworkInterface, OutputConnector}, }, }; @@ -17,6 +17,7 @@ impl NodeNetworkInterface { log::error!("Could not get nested network when collecting nodes"); return Vec::new(); }; + let selected_nodes = self.selected_nodes_in_nested_network(network_path).unwrap_or_default(); let mut nodes = Vec::new(); for (node_id, visible) in network.nodes.iter().map(|(node_id, node)| (*node_id, node.visible)).collect::>() { let node_id_path = [network_path, &[node_id]].concat(); @@ -37,19 +38,12 @@ impl NodeNetworkInterface { let exposed_outputs = (1..self.number_of_outputs(&node_id, network_path)) .filter_map(|output_index| self.frontend_output_from_connector(&OutputConnector::node(node_id, output_index), network_path)) .collect(); - let (primary_output_connected_to_layer, primary_input_connected_to_layer) = if self.is_layer(&node_id, network_path) { - ( - self.primary_output_connected_to_layer(&node_id, network_path), - self.primary_input_connected_to_layer(&node_id, network_path), - ) - } else { - (false, false) - }; let Some(position) = self.position(&node_id, network_path) else { log::error!("Could not get position for node: {node_id}"); continue; }; + let position = FrontendXY { x: position.x, y: position.y }; let previewed = self.previewed_node(network_path) == Some(node_id); let locked = self.is_locked(&node_id, network_path); @@ -70,19 +64,24 @@ impl NodeNetworkInterface { id: node_id, is_layer: self.node_metadata(&node_id, network_path).is_some_and(|node_metadata| node_metadata.persistent_metadata.is_layer()), can_be_layer: self.is_eligible_to_be_layer(&node_id, network_path), + selected: selected_nodes.0.contains(&node_id), reference: self.reference(&node_id, network_path).cloned().unwrap_or_default(), display_name: self.display_name(&node_id, network_path), + previewed, + visible, + errors, + primary_input, exposed_inputs, primary_output, exposed_outputs, - primary_output_connected_to_layer, - primary_input_connected_to_layer, position, - previewed, - visible, + locked, - errors, + chain_width: self.chain_width(&node_id, network_path), + layer_has_left_border_gap: self.layer_has_left_border_gap(&node_id, network_path), + primary_input_connected_to_layer: self.primary_output_connected_to_layer(&node_id, network_path), + primary_output_connected_to_layer: self.primary_input_connected_to_layer(&node_id, network_path), }); } @@ -259,9 +258,12 @@ impl NodeNetworkInterface { /// Checks if a layer should display a gap in its left border pub fn layer_has_left_border_gap(&self, node_id: &NodeId, network_path: &[NodeId]) -> bool { - self.upstream_flow_back_from_nodes(vec![*node_id], network_path, FlowType::HorizontalFlow) - .skip(1) - .any(|node_id| !self.is_chain(&node_id, network_path)) + self.upstream_flow_back_from_nodes(vec![*node_id], network_path, FlowType::HorizontalFlow).skip(1).any(|node_id| { + !self.is_chain(&node_id, network_path) + || self + .upstream_output_connector(&InputConnector::node(node_id, 0), network_path) + .is_some_and(|output_connector| matches!(output_connector, OutputConnector::Import(_))) + }) } /// Returns the node which should have a dashed border drawn around it diff --git a/frontend/src/components/views/Graph.svelte b/frontend/src/components/views/Graph.svelte index d6cef02fec..210ed69517 100644 --- a/frontend/src/components/views/Graph.svelte +++ b/frontend/src/components/views/Graph.svelte @@ -116,12 +116,11 @@ } function toggleLayerDisplay(displayAsLayer: boolean, toggleId: bigint) { - let node = $nodeGraph.nodes.get(toggleId); - if (node) editor.handle.setToNodeOrLayer(node.id, displayAsLayer); + editor.handle.setToNodeOrLayer(toggleId, displayAsLayer); } function canBeToggledBetweenNodeAndLayer(toggleDisplayAsLayerNodeId: bigint) { - return $nodeGraph.nodes.get(toggleDisplayAsLayerNodeId)?.canBeLayer || false; + return $nodeGraph.nodesToRender.get(toggleDisplayAsLayerNodeId)?.canBeLayer || false; } function createNode(nodeType: string) { @@ -148,7 +147,7 @@ return borderMask(boxes, nodeWidth, nodeHeight); } - function layerBorderMask(nodeWidthFromThumbnail: number, nodeChainAreaLeftExtension: number, hasLeftInputWire: boolean): string { + function layerBorderMask(nodeWidthFromThumbnail: number, nodeChainAreaLeftExtension: number, layerHasLeftBorderGap: boolean): string { const NODE_HEIGHT = 2 * 24; const THUMBNAIL_WIDTH = 72 + 8 * 2; const FUDGE_HEIGHT_BEYOND_LAYER_HEIGHT = 2; @@ -158,7 +157,7 @@ const boxes: { x: number; y: number; width: number; height: number }[] = []; // Left input - if (hasLeftInputWire && nodeChainAreaLeftExtension > 0) { + if (layerHasLeftBorderGap && nodeChainAreaLeftExtension > 0) { boxes.push({ x: -8, y: 16, width: 16, height: 16 }); } @@ -318,8 +317,6 @@ xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 8" class="connector" - data-connector="output" - data-datatype={frontendOutput.dataType} style:--data-color={`var(--color-data-${frontendOutput.dataType.toLowerCase()})`} style:--data-color-dim={`var(--color-data-${frontendOutput.dataType.toLowerCase()}-dim)`} style:--offset-left={($nodeGraph.updateImportsExports.importPosition.x - 8) / 24} @@ -390,8 +387,6 @@ xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 8" class="connector" - data-connector="input" - data-datatype={frontendInput.dataType} style:--data-color={`var(--color-data-${frontendInput.dataType.toLowerCase()})`} style:--data-color-dim={`var(--color-data-${frontendInput.dataType.toLowerCase()}-dim)`} style:--offset-left={($nodeGraph.updateImportsExports.exportPosition.x - 8) / 24} @@ -492,54 +487,47 @@
- {#each Array.from($nodeGraph.nodes) - .filter(([nodeId, node]) => node.isLayer && $nodeGraph.visibleNodes.has(nodeId)) - .map(([_, node], nodeIndex) => ({ node, nodeIndex })) as { node, nodeIndex } (nodeIndex)} + {#each Array.from($nodeGraph.nodesToRender).filter(([nodeId, node]) => node.isLayer && $nodeGraph.visibleNodes.has(nodeId)) as [nodeId, layer]} {@const clipPathId = String(Math.random()).substring(2)} - {@const stackDataInput = node.exposedInputs[0]} - {@const layerAreaWidth = $nodeGraph.layerWidths.get(node.id) || 8} - {@const layerChainWidth = $nodeGraph.chainWidths.get(node.id) || 0} - {@const hasLeftInputWire = $nodeGraph.hasLeftInputWire.get(node.id) || false} - {@const description = (node.reference && $nodeGraph.nodeDescriptions.get(node.reference)) || undefined} + {@const layerAreaWidth = $nodeGraph.layerWidths.get(layer.id) || 8} + {@const layerChainWidth = layer.chainWidth !== 0 ? layer.chainWidth + 0.5 : 0} + {@const description = (layer.reference && $nodeGraph.nodeDescriptions.get(layer.reference)) || undefined}
- {#if node.errors} - {node.errors} - {node.errors} + {#if layer.errors} + {layer.errors} + {layer.errors} {/if}
- {#if $nodeGraph.thumbnails.has(node.id)} - {@html $nodeGraph.thumbnails.get(node.id)} + {#if $nodeGraph.thumbnails.has(nodeId)} + {@html $nodeGraph.thumbnails.get(nodeId)} {/if} - {#if node.primaryOutput} + {#if layer.primaryOutput} - {outputTooltip(node.primaryOutput)} - {#if node.primaryOutput.connectedTo.length > 0} + {outputTooltip(layer.primaryOutput)} + {#if layer.primaryOutput.connectedTo.length > 0} - {#if node.primaryOutputConnectedToLayer} + {#if layer.primaryOutputConnectedToLayer} {/if} {:else} @@ -552,17 +540,15 @@ xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 12" class="connector bottom" - data-connector="input" - data-datatype={node.primaryInput?.dataType} - style:--data-color={`var(--color-data-${(node.primaryInput?.dataType || "General").toLowerCase()})`} - style:--data-color-dim={`var(--color-data-${(node.primaryInput?.dataType || "General").toLowerCase()}-dim)`} + style:--data-color={`var(--color-data-${(layer.primaryInput?.dataType || "General").toLowerCase()})`} + style:--data-color-dim={`var(--color-data-${(layer.primaryInput?.dataType || "General").toLowerCase()}-dim)`} > - {#if node.primaryInput} - {inputTooltip(node.primaryInput)} + {#if layer.primaryInput} + {inputTooltip(layer.primaryInput)} {/if} - {#if node.primaryInput?.connectedTo !== "nothing"} + {#if layer.primaryInput?.connectedTo !== "nothing"} - {#if node.primaryInputConnectedToLayer} + {#if layer.primaryInputConnectedToLayer} {/if} {:else} @@ -571,19 +557,17 @@
- {#if node.exposedInputs.length > 0} + {#if layer.exposedInputs.length > 0}
- {inputTooltip(stackDataInput)} - {#if stackDataInput.connectedTo !== undefined} + {inputTooltip(layer.exposedInputs[0])} + {#if layer.exposedInputs[0].connectedTo !== undefined} {:else} @@ -593,25 +577,25 @@ {/if}
- {node.displayName} + {layer.displayName}
{ /* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */ }} - tooltip={node.visible ? "Visible" : "Hidden"} + tooltip={layer.visible ? "Visible" : "Hidden"} /> - + @@ -647,15 +631,13 @@
- {#each Array.from($nodeGraph.nodes) - .filter(([nodeId, node]) => !node.isLayer && $nodeGraph.visibleNodes.has(nodeId)) - .map(([_, node], nodeIndex) => ({ node, nodeIndex })) as { node, nodeIndex } (nodeIndex)} + {#each Array.from($nodeGraph.nodesToRender).filter(([nodeId, node]) => !node.isLayer && $nodeGraph.visibleNodes.has(nodeId)) as [nodeId, node]} {@const exposedInputsOutputs = zipWithUndefined(node.exposedInputs, node.exposedOutputs)} {@const clipPathId = String(Math.random()).substring(2)} {@const description = (node.reference && $nodeGraph.nodeDescriptions.get(node.reference)) || undefined}
{#if node.errors} {node.errors} @@ -695,8 +676,6 @@ xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 8" class="connector primary-connector" - data-connector="input" - data-datatype={node.primaryInput?.dataType} style:--data-color={`var(--color-data-${node.primaryInput.dataType.toLowerCase()})`} style:--data-color-dim={`var(--color-data-${node.primaryInput.dataType.toLowerCase()}-dim)`} > @@ -714,8 +693,6 @@ xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 8" class="connector" - data-connector="input" - data-datatype={secondary.dataType} style:--data-color={`var(--color-data-${secondary.dataType.toLowerCase()})`} style:--data-color-dim={`var(--color-data-${secondary.dataType.toLowerCase()}-dim)`} > @@ -736,8 +713,6 @@ xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 8" class="connector primary-connector" - data-connector="output" - data-datatype={node.primaryOutput.dataType} style:--data-color={`var(--color-data-${node.primaryOutput.dataType.toLowerCase()})`} style:--data-color-dim={`var(--color-data-${node.primaryOutput.dataType.toLowerCase()}-dim)`} > @@ -754,8 +729,6 @@ xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 8" class="connector" - data-connector="output" - data-datatype={secondary.dataType} style:--data-color={`var(--color-data-${secondary.dataType.toLowerCase()})`} style:--data-color-dim={`var(--color-data-${secondary.dataType.toLowerCase()}-dim)`} > @@ -784,7 +757,6 @@
- {#if $nodeGraph.box}
obj.layerWidths); -const ChainWidths = Transform(({ obj }) => obj.chainWidths); -const HasLeftInputWire = Transform(({ obj }) => obj.hasLeftInputWire); export class UpdateLayerWidths extends JsMessage { @LayerWidths readonly layerWidths!: Map; - @ChainWidths - readonly chainWidths!: Map; - @HasLeftInputWire - readonly hasLeftInputWire!: Map; } export class UpdateNodeGraphNodes extends JsMessage { - @Type(() => FrontendNode) - readonly nodes!: FrontendNode[]; + readonly nodesToRender!: FrontendNode[]; + + readonly inSelectedNetwork!: boolean; + + readonly previewedNode!: bigint | undefined; } export class UpdateVisibleNodes extends JsMessage { @@ -123,11 +114,6 @@ export class UpdateNodeThumbnail extends JsMessage { readonly value!: string; } -export class UpdateNodeGraphSelection extends JsMessage { - @Type(() => BigInt) - readonly selected!: bigint[]; -} - export class UpdateOpenDocumentsList extends JsMessage { @Type(() => OpenDocument) readonly openDocuments!: OpenDocument[]; @@ -211,11 +197,13 @@ export class FrontendGraphOutput { } export class FrontendNode { + readonly id!: bigint; + readonly isLayer!: boolean; readonly canBeLayer!: boolean; - readonly id!: bigint; + readonly selected!: boolean; readonly reference!: string | undefined; @@ -229,6 +217,10 @@ export class FrontendNode { readonly exposedOutputs!: FrontendGraphOutput[]; + readonly chainWidth!: number; + + readonly layerHasLeftBorderGap!: boolean; + readonly primaryInputConnectedToLayer!: boolean; readonly primaryOutputConnectedToLayer!: boolean; @@ -1672,7 +1664,6 @@ export const messageMakers: Record = { UpdateImportReorderIndex, UpdateImportsExports, UpdateInputHints, - UpdateInSelectedNetwork, UpdateLayersPanelBottomBarLayout, UpdateLayersPanelControlBarLeftLayout, UpdateLayersPanelControlBarRightLayout, @@ -1682,7 +1673,6 @@ export const messageMakers: Record = { UpdateMouseCursor, UpdateNodeGraphControlBarLayout, UpdateNodeGraphNodes, - UpdateNodeGraphSelection, UpdateNodeGraphTransform, UpdateNodeGraphWires, UpdateNodeThumbnail, diff --git a/frontend/src/state-providers/node-graph.ts b/frontend/src/state-providers/node-graph.ts index fd6c7c5724..73e1d436a0 100644 --- a/frontend/src/state-providers/node-graph.ts +++ b/frontend/src/state-providers/node-graph.ts @@ -13,7 +13,6 @@ import { UpdateBox, UpdateClickTargets, UpdateContextMenuInformation, - UpdateInSelectedNetwork, UpdateImportReorderIndex, UpdateExportReorderIndex, UpdateImportsExports, @@ -21,7 +20,6 @@ import { UpdateNodeGraphNodes, UpdateVisibleNodes, UpdateNodeGraphWires, - UpdateNodeGraphSelection, UpdateNodeGraphTransform, UpdateNodeThumbnail, UpdateWirePathInProgress, @@ -33,10 +31,9 @@ export function createNodeGraphState(editor: Editor) { clickTargets: undefined as FrontendClickTargets | undefined, contextMenuInformation: undefined as ContextMenuInformation | undefined, layerWidths: new Map(), - chainWidths: new Map(), - hasLeftInputWire: new Map(), updateImportsExports: undefined as UpdateImportsExports | undefined, - nodes: new Map(), + nodesToRender: new Map(), + visibleNodes: new Set(), /// The index is the exposed input index. The exports have a first key value of u32::MAX. wires: new Map>(), @@ -44,9 +41,9 @@ export function createNodeGraphState(editor: Editor) { nodeDescriptions: new Map(), nodeTypes: [] as FrontendNodeType[], thumbnails: new Map(), - selected: [] as bigint[], transform: { scale: 1, x: 0, y: 0 }, inSelectedNetwork: true, + previewedNode: undefined as bigint | undefined, reorderImportIndex: undefined as number | undefined, reorderExportIndex: undefined as number | undefined, }); @@ -95,26 +92,20 @@ export function createNodeGraphState(editor: Editor) { return state; }); }); - editor.subscriptions.subscribeJsMessage(UpdateInSelectedNetwork, (updateInSelectedNetwork) => { - update((state) => { - state.inSelectedNetwork = updateInSelectedNetwork.inSelectedNetwork; - return state; - }); - }); editor.subscriptions.subscribeJsMessage(UpdateLayerWidths, (updateLayerWidths) => { update((state) => { state.layerWidths = updateLayerWidths.layerWidths; - state.chainWidths = updateLayerWidths.chainWidths; - state.hasLeftInputWire = updateLayerWidths.hasLeftInputWire; return state; }); }); editor.subscriptions.subscribeJsMessage(UpdateNodeGraphNodes, (updateNodeGraphNodes) => { update((state) => { - state.nodes.clear(); - updateNodeGraphNodes.nodes.forEach((node) => { - state.nodes.set(node.id, node); + state.nodesToRender.clear(); + updateNodeGraphNodes.nodesToRender.forEach((node) => { + state.nodesToRender.set(node.id, node); }); + state.inSelectedNetwork = updateNodeGraphNodes.inSelectedNetwork; + state.previewedNode = updateNodeGraphNodes.previewedNode; return state; }); }); @@ -148,12 +139,6 @@ export function createNodeGraphState(editor: Editor) { return state; }); }); - editor.subscriptions.subscribeJsMessage(UpdateNodeGraphSelection, (updateNodeGraphSelection) => { - update((state) => { - state.selected = updateNodeGraphSelection.selected; - return state; - }); - }); editor.subscriptions.subscribeJsMessage(UpdateNodeGraphTransform, (updateNodeGraphTransform) => { update((state) => { state.transform = updateNodeGraphTransform.transform; From 8a6aa46488052e84790f07c4b343091769756639 Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 31 Aug 2025 13:54:15 -0700 Subject: [PATCH 3/7] Rewrite FrontendNode --- .../src/messages/frontend/frontend_message.rs | 4 +- .../node_graph/node_graph_message_handler.rs | 6 +- .../document/node_graph/utility_types.rs | 82 +++++++++++++------ frontend/src/components/views/Graph.svelte | 5 +- frontend/src/messages.ts | 68 ++++++++++----- frontend/src/state-providers/node-graph.ts | 6 +- 6 files changed, 118 insertions(+), 53 deletions(-) diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index b8969308c7..d77a115587 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -2,7 +2,7 @@ use super::utility_types::{DocumentDetails, MouseCursorIcon, OpenDocument}; use crate::messages::app_window::app_window_message_handler::AppWindowPlatform; use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::node_graph::utility_types::{ - BoxSelection, ContextMenuInformation, FrontendClickTargets, FrontendGraphInput, FrontendGraphOutput, FrontendNode, FrontendNodeType, FrontendXY, Transform, + BoxSelection, ContextMenuInformation, FrontendClickTargets, FrontendGraphInput, FrontendGraphOutput, FrontendNode, FrontendNodeOrLayer, FrontendNodeType, FrontendXY, FrontendXY, Transform, }; use crate::messages::portfolio::document::utility_types::nodes::{JsRawBuffer, LayerPanelEntry, RawBuffer}; use crate::messages::portfolio::document::utility_types::wires::{WirePath, WirePathUpdate}; @@ -276,7 +276,7 @@ pub enum FrontendMessage { }, UpdateNodeGraphNodes { #[serde(rename = "nodesToRender")] - nodes_to_render: Vec, + nodes_to_render: Vec, #[serde(rename = "inSelectedNetwork")] in_selected_network: bool, // Displays a dashed border around the node 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 7af7cd2ee5..8864d63f0d 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 @@ -6,7 +6,9 @@ use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::document_message_handler::navigation_controls; use crate::messages::portfolio::document::graph_operation::utility_types::ModifyInputsContext; use crate::messages::portfolio::document::node_graph::document_node_definitions::NodePropertiesContext; -use crate::messages::portfolio::document::node_graph::utility_types::{ContextMenuData, Direction, FrontendGraphDataType, FrontendXY}; +use crate::messages::portfolio::document::node_graph::utility_types::{ + ContextMenuData, Direction, FrontendGraphDataType, FrontendLayer, FrontendNodeMetadata, FrontendNodeOrLayer, FrontendXY, FrontendXY, +}; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::misc::GroupFolderType; use crate::messages::portfolio::document::utility_types::network_interface::{ @@ -22,7 +24,7 @@ use crate::messages::tool::tool_messages::tool_prelude::{Key, MouseMotion}; use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo}; use glam::{DAffine2, DVec2, IVec2}; use graph_craft::document::{DocumentNodeImplementation, NodeId, NodeInput}; -use graph_craft::proto::GraphErrors; +use graph_craft::proto::{GraphErrors, NodeMetadata}; use graphene_std::math::math_ext::QuadExt; use graphene_std::vector::algorithms::bezpath_algorithms::bezpath_is_inside_bezpath; use graphene_std::*; diff --git a/editor/src/messages/portfolio/document/node_graph/utility_types.rs b/editor/src/messages/portfolio/document/node_graph/utility_types.rs index 45dd0532f2..1adf67902e 100644 --- a/editor/src/messages/portfolio/document/node_graph/utility_types.rs +++ b/editor/src/messages/portfolio/document/node_graph/utility_types.rs @@ -64,9 +64,12 @@ pub struct FrontendGraphInput { pub description: String, #[serde(rename = "resolvedType")] pub resolved_type: String, - #[serde(rename = "connectedTo")] /// 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, } #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] @@ -83,38 +86,71 @@ pub struct FrontendGraphOutput { pub connected_to: Vec, } +// Metadata that is common to nodes and layers #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] -pub struct FrontendNode { - pub id: graph_craft::document::NodeId, - #[serde(rename = "isLayer")] - pub is_layer: bool, +pub struct FrontendNodeMetadata { + // TODO: Remove and replace with popup manager system #[serde(rename = "canBeLayer")] pub can_be_layer: bool, - pub selected: bool, - pub reference: Option, #[serde(rename = "displayName")] pub display_name: String, - #[serde(rename = "primaryInput")] - pub primary_input: Option, - #[serde(rename = "exposedInputs")] - pub exposed_inputs: Vec, - #[serde(rename = "primaryOutput")] - pub primary_output: Option, - #[serde(rename = "exposedOutputs")] - pub exposed_outputs: Vec, + pub selected: bool, + // Used to get the description, which is stored in a global hashmap + pub reference: Option, + // Reduces opacity of node/hidden eye icon + pub visible: bool, + // The svg string for each input + // pub wires: Vec>, + pub errors: Option, +} + +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] +pub struct FrontendNode { + // pub position: FrontendNodePosition, + pub position: FrontendXY, + pub inputs: Vec>, + pub outputs: Vec>, +} + +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] +pub struct FrontendLayer { + #[serde(rename = "bottomInput")] + pub bottom_input: FrontendGraphInput, + #[serde(rename = "sideInput")] + pub side_input: Option, + pub output: FrontendGraphOutput, + // pub position: FrontendLayerPosition, + pub position: FrontendXY, + pub locked: bool, #[serde(rename = "chainWidth")] pub chain_width: u32, #[serde(rename = "layerHasLeftBorderGap")] - pub layer_has_left_border_gap: bool, - #[serde(rename = "primaryOutputConnectedToLayer")] - pub primary_output_connected_to_layer: bool, + layer_has_left_border_gap: bool, #[serde(rename = "primaryInputConnectedToLayer")] pub primary_input_connected_to_layer: bool, - pub position: FrontendXY, - pub visible: bool, - pub locked: bool, - pub previewed: bool, - pub errors: Option, + #[serde(rename = "primaryOutputConnectedToLayer")] + pub primary_output_connected_to_layer: bool, +} + +// // Should be an enum but those are hard to serialize/deserialize to TS +// #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] +// pub struct FrontendNodePosition { +// pub absolute: Option, +// pub chain: Option, +// } + +// // Should be an enum but those are hard to serialize/deserialize to TS +// #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] +// pub struct FrontendLayerPosition { +// pub absolute: Option, +// pub stack: Option, +// } + +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] +pub struct FrontendNodeOrLayer { + pub metadata: FrontendNodeMetadata, + pub node: Option, + pub layer: Option, } #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] diff --git a/frontend/src/components/views/Graph.svelte b/frontend/src/components/views/Graph.svelte index 210ed69517..0c48a672a2 100644 --- a/frontend/src/components/views/Graph.svelte +++ b/frontend/src/components/views/Graph.svelte @@ -117,10 +117,11 @@ function toggleLayerDisplay(displayAsLayer: boolean, toggleId: bigint) { editor.handle.setToNodeOrLayer(toggleId, displayAsLayer); + editor.handle.setToNodeOrLayer(toggleId, displayAsLayer); } function canBeToggledBetweenNodeAndLayer(toggleDisplayAsLayerNodeId: bigint) { - return $nodeGraph.nodesToRender.get(toggleDisplayAsLayerNodeId)?.canBeLayer || false; + return $nodeGraph.nodesToRender.get(toggleDisplayAsLayerNodeId)?.metadata.canBeLayer || false; } function createNode(nodeType: string) { @@ -199,7 +200,7 @@ } function inputConnectedToText(input: FrontendGraphInput): string { - return `Connected to:\n${input.connectedTo}`; + return `Connected to:\n${input.connectedToString}`; } function zipWithUndefined(arr1: FrontendGraphInput[], arr2: FrontendGraphOutput[]) { diff --git a/frontend/src/messages.ts b/frontend/src/messages.ts index df532396e0..7a76acb655 100644 --- a/frontend/src/messages.ts +++ b/frontend/src/messages.ts @@ -175,13 +175,15 @@ export type FrontendGraphDataType = "General" | "Number" | "Artboard" | "Graphic export class FrontendGraphInput { readonly dataType!: FrontendGraphDataType; + readonly resolvedType!: string; + readonly name!: string; readonly description!: string; - readonly resolvedType!: string; + readonly connectedToString!: string; - readonly connectedTo!: string; + readonly connectedToNode!: bigint | undefined; } export class FrontendGraphOutput { @@ -196,26 +198,42 @@ export class FrontendGraphOutput { readonly connectedTo!: string[]; } -export class FrontendNode { - readonly id!: bigint; - - readonly isLayer!: boolean; - +export class FrontendNodeMetadata { readonly canBeLayer!: boolean; + readonly displayName!: string; + readonly selected!: boolean; readonly reference!: string | undefined; - readonly displayName!: string; + readonly visible!: boolean; + + // readonly wires!: (string | undefined)[]; + + readonly errors!: string | undefined; +} + +export class FrontendNode { + // readonly position!: FrontendNodePosition; + readonly position!: XY; + + readonly inputs!: (FrontendGraphInput | undefined)[]; - readonly primaryInput!: FrontendGraphInput | undefined; + readonly outputs!: (FrontendGraphOutput | undefined)[]; +} + +export class FrontendLayer { + // readonly position!: FrontendLayerPosition; + readonly position!: XY; - readonly exposedInputs!: FrontendGraphInput[]; + readonly bottomInput!: FrontendGraphInput; - readonly primaryOutput!: FrontendGraphOutput | undefined; + readonly sideInput!: FrontendGraphInput | undefined; - readonly exposedOutputs!: FrontendGraphOutput[]; + readonly output!: FrontendGraphOutput; + + readonly locked!: boolean; readonly chainWidth!: number; @@ -224,19 +242,27 @@ export class FrontendNode { readonly primaryInputConnectedToLayer!: boolean; readonly primaryOutputConnectedToLayer!: boolean; +} - @TupleToVec2 - readonly position!: XY; - - // TODO: Store field for the width of the left node chain - - readonly previewed!: boolean; +export class FrontendNodePosition { + readonly absolute!: XY | undefined; + readonly chain!: boolean | undefined; +} - readonly visible!: boolean; +export class FrontendLayerPosition { + readonly absolute!: XY | undefined; + readonly stack!: number | undefined; +} - readonly unlocked!: boolean; +export class FrontendNodeOrLayer { + readonly metadata!: FrontendNodeMetadata; + readonly node!: FrontendNode | undefined; + readonly layer!: FrontendLayer | undefined; +} - readonly errors!: string | undefined; +export class UpdateCentralNodeGraph extends JsMessage { + readonly nodeOrLayer!: FrontendNodeOrLayer[]; + readonly inSelectedNetwork!: boolean; } export class FrontendNodeType { diff --git a/frontend/src/state-providers/node-graph.ts b/frontend/src/state-providers/node-graph.ts index 73e1d436a0..d2f4c693f2 100644 --- a/frontend/src/state-providers/node-graph.ts +++ b/frontend/src/state-providers/node-graph.ts @@ -1,11 +1,11 @@ import { writable } from "svelte/store"; import { type Editor } from "@graphite/editor"; +import type { FrontendNodeOrLayer } from "@graphite/messages"; import { type Box, type FrontendClickTargets, type ContextMenuInformation, - type FrontendNode, type FrontendNodeType, type WirePath, ClearAllNodeGraphWires, @@ -32,8 +32,7 @@ export function createNodeGraphState(editor: Editor) { contextMenuInformation: undefined as ContextMenuInformation | undefined, layerWidths: new Map(), updateImportsExports: undefined as UpdateImportsExports | undefined, - nodesToRender: new Map(), - + nodesToRender: new Map(), visibleNodes: new Set(), /// The index is the exposed input index. The exports have a first key value of u32::MAX. wires: new Map>(), @@ -92,6 +91,7 @@ export function createNodeGraphState(editor: Editor) { return state; }); }); + editor.subscriptions.subscribeJsMessage(UpdateLayerWidths, (updateLayerWidths) => { update((state) => { state.layerWidths = updateLayerWidths.layerWidths; From 8bab555357bd814e76184b76a8805640fceffaaa Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 31 Aug 2025 14:49:02 -0700 Subject: [PATCH 4/7] Improve collect_nodes --- .../src/messages/frontend/frontend_message.rs | 2 +- .../node_graph/node_graph_message_handler.rs | 8 +- .../document/node_graph/utility_types.rs | 12 +- .../network_interface/node_graph.rs | 113 +++++++++++------- frontend/src/messages.ts | 10 +- frontend/src/state-providers/node-graph.ts | 4 +- 6 files changed, 91 insertions(+), 58 deletions(-) diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index d77a115587..e6f84f5aa1 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -2,7 +2,7 @@ use super::utility_types::{DocumentDetails, MouseCursorIcon, OpenDocument}; use crate::messages::app_window::app_window_message_handler::AppWindowPlatform; use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::node_graph::utility_types::{ - BoxSelection, ContextMenuInformation, FrontendClickTargets, FrontendGraphInput, FrontendGraphOutput, FrontendNode, FrontendNodeOrLayer, FrontendNodeType, FrontendXY, FrontendXY, Transform, + BoxSelection, ContextMenuInformation, FrontendClickTargets, FrontendGraphInput, FrontendGraphOutput, FrontendNodeToRender, FrontendNodeType, FrontendXY, Transform, }; use crate::messages::portfolio::document::utility_types::nodes::{JsRawBuffer, LayerPanelEntry, RawBuffer}; use crate::messages::portfolio::document::utility_types::wires::{WirePath, WirePathUpdate}; 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 8864d63f0d..1aa6cdf6d6 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 @@ -6,9 +6,7 @@ use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::document_message_handler::navigation_controls; use crate::messages::portfolio::document::graph_operation::utility_types::ModifyInputsContext; use crate::messages::portfolio::document::node_graph::document_node_definitions::NodePropertiesContext; -use crate::messages::portfolio::document::node_graph::utility_types::{ - ContextMenuData, Direction, FrontendGraphDataType, FrontendLayer, FrontendNodeMetadata, FrontendNodeOrLayer, FrontendXY, FrontendXY, -}; +use crate::messages::portfolio::document::node_graph::utility_types::{ContextMenuData, Direction, FrontendGraphDataType, FrontendXY}; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::misc::GroupFolderType; use crate::messages::portfolio::document::utility_types::network_interface::{ @@ -24,7 +22,7 @@ use crate::messages::tool::tool_messages::tool_prelude::{Key, MouseMotion}; use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo}; use glam::{DAffine2, DVec2, IVec2}; use graph_craft::document::{DocumentNodeImplementation, NodeId, NodeInput}; -use graph_craft::proto::{GraphErrors, NodeMetadata}; +use graph_craft::proto::GraphErrors; use graphene_std::math::math_ext::QuadExt; use graphene_std::vector::algorithms::bezpath_algorithms::bezpath_is_inside_bezpath; use graphene_std::*; @@ -1638,7 +1636,7 @@ impl<'a> MessageHandler> for NodeG responses.add(NodeGraphMessage::UpdateActionButtons); if graph_view_overlay_open { let nodes_to_render = network_interface.collect_nodes(&self.node_graph_errors, breadcrumb_network_path); - self.frontend_nodes = nodes_to_render.iter().map(|node| node.id).collect(); + self.frontend_nodes = nodes_to_render.iter().map(|node| node.metadata.node_id).collect(); let previewed_node = network_interface.previewed_node(breadcrumb_network_path); responses.add(FrontendMessage::UpdateNodeGraphNodes { nodes_to_render, diff --git a/editor/src/messages/portfolio/document/node_graph/utility_types.rs b/editor/src/messages/portfolio/document/node_graph/utility_types.rs index 1adf67902e..14a9ea5107 100644 --- a/editor/src/messages/portfolio/document/node_graph/utility_types.rs +++ b/editor/src/messages/portfolio/document/node_graph/utility_types.rs @@ -89,6 +89,8 @@ pub struct FrontendGraphOutput { // Metadata that is common to nodes and layers #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] pub struct FrontendNodeMetadata { + #[serde(rename = "node_id")] + pub node_id: NodeId, // TODO: Remove and replace with popup manager system #[serde(rename = "canBeLayer")] pub can_be_layer: bool, @@ -125,7 +127,7 @@ pub struct FrontendLayer { #[serde(rename = "chainWidth")] pub chain_width: u32, #[serde(rename = "layerHasLeftBorderGap")] - layer_has_left_border_gap: bool, + pub layer_has_left_border_gap: bool, #[serde(rename = "primaryInputConnectedToLayer")] pub primary_input_connected_to_layer: bool, #[serde(rename = "primaryOutputConnectedToLayer")] @@ -148,11 +150,17 @@ pub struct FrontendLayer { #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] pub struct FrontendNodeOrLayer { - pub metadata: FrontendNodeMetadata, pub node: Option, pub layer: Option, } +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] +pub struct FrontendNodeToRender { + pub metadata: FrontendNodeMetadata, + #[serde(rename = "nodeOrLayer")] + pub node_or_layer: FrontendNodeOrLayer, +} + #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] pub struct FrontendNodeType { pub name: Cow<'static, str>, 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 055003117e..aae024f91c 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 @@ -5,14 +5,16 @@ use graphene_std::uuid::NodeId; use crate::{ consts::{EXPORTS_TO_RIGHT_EDGE_PIXEL_GAP, EXPORTS_TO_TOP_EDGE_PIXEL_GAP, GRID_SIZE, IMPORTS_TO_LEFT_EDGE_PIXEL_GAP, IMPORTS_TO_TOP_EDGE_PIXEL_GAP}, messages::portfolio::document::{ - node_graph::utility_types::{FrontendGraphDataType, FrontendGraphInput, FrontendGraphOutput, FrontendNode, FrontendXY}, + node_graph::utility_types::{ + FrontendGraphDataType, FrontendGraphInput, FrontendGraphOutput, FrontendLayer, FrontendNode, FrontendNodeMetadata, FrontendNodeOrLayer, FrontendNodeToRender, FrontendXY, + }, utility_types::network_interface::{FlowType, InputConnector, NodeNetworkInterface, OutputConnector}, }, }; // Functions used to collect data from the network interface for use in rendering the node graph impl NodeNetworkInterface { - pub fn collect_nodes(&mut self, node_graph_errors: &GraphErrors, network_path: &[NodeId]) -> Vec { + pub fn collect_nodes(&mut self, node_graph_errors: &GraphErrors, network_path: &[NodeId]) -> Vec { let Some(network) = self.nested_network(network_path) else { log::error!("Could not get nested network when collecting nodes"); return Vec::new(); @@ -22,32 +24,6 @@ impl NodeNetworkInterface { for (node_id, visible) in network.nodes.iter().map(|(node_id, node)| (*node_id, node.visible)).collect::>() { let node_id_path = [network_path, &[node_id]].concat(); - let primary_input_connector = InputConnector::node(node_id, 0); - - let primary_input = if self.input_from_connector(&primary_input_connector, network_path).is_some_and(|input| input.is_exposed()) { - self.frontend_input_from_connector(&primary_input_connector, network_path) - } else { - None - }; - let exposed_inputs = (1..self.number_of_inputs(&node_id, network_path)) - .filter_map(|input_index| self.frontend_input_from_connector(&InputConnector::node(node_id, input_index), network_path)) - .collect(); - - let primary_output = self.frontend_output_from_connector(&OutputConnector::node(node_id, 0), network_path); - - let exposed_outputs = (1..self.number_of_outputs(&node_id, network_path)) - .filter_map(|output_index| self.frontend_output_from_connector(&OutputConnector::node(node_id, output_index), network_path)) - .collect(); - - let Some(position) = self.position(&node_id, network_path) else { - log::error!("Could not get position for node: {node_id}"); - continue; - }; - let position = FrontendXY { x: position.x, y: position.y }; - let previewed = self.previewed_node(network_path) == Some(node_id); - - let locked = self.is_locked(&node_id, network_path); - let errors = node_graph_errors .iter() .find(|error| error.node_path == node_id_path) @@ -60,31 +36,73 @@ impl NodeNetworkInterface { } }); - nodes.push(FrontendNode { - id: node_id, - is_layer: self.node_metadata(&node_id, network_path).is_some_and(|node_metadata| node_metadata.persistent_metadata.is_layer()), + let metadata = FrontendNodeMetadata { + node_id, can_be_layer: self.is_eligible_to_be_layer(&node_id, network_path), + display_name: self.display_name(&node_id, network_path), selected: selected_nodes.0.contains(&node_id), reference: self.reference(&node_id, network_path).cloned().unwrap_or_default(), - display_name: self.display_name(&node_id, network_path), - previewed, visible, errors, + }; - primary_input, - exposed_inputs, - primary_output, - exposed_outputs, - position, - - locked, - chain_width: self.chain_width(&node_id, network_path), - layer_has_left_border_gap: self.layer_has_left_border_gap(&node_id, network_path), - primary_input_connected_to_layer: self.primary_output_connected_to_layer(&node_id, network_path), - primary_output_connected_to_layer: self.primary_input_connected_to_layer(&node_id, network_path), - }); - } + let node_or_layer = match self.is_layer(&node_id, network_path) { + true => { + let Some(position) = self.position(&node_id, network_path) else { + log::error!("Could not get position for node: {node_id}"); + continue; + }; + let position = FrontendXY { x: position.x, y: position.y }; + + let Some(bottom_input) = self.frontend_input_from_connector(&InputConnector::node(node_id, 0), network_path) else { + log::error!("Layer must have a visible primary input"); + continue; + }; + let side_input = self.frontend_input_from_connector(&InputConnector::node(node_id, 1), network_path); + let Some(output) = self.frontend_output_from_connector(&OutputConnector::node(node_id, 0), network_path) else { + log::error!("Layer must have a visible primary output"); + continue; + }; + + let layer = Some(FrontendLayer { + bottom_input, + side_input, + output, + position, + locked: self.is_locked(&node_id, network_path), + chain_width: self.chain_width(&node_id, network_path), + layer_has_left_border_gap: self.layer_has_left_border_gap(&node_id, network_path), + primary_input_connected_to_layer: self.primary_output_connected_to_layer(&node_id, network_path), + primary_output_connected_to_layer: self.primary_input_connected_to_layer(&node_id, network_path), + }); + FrontendNodeOrLayer { node: None, layer } + } + false => { + let Some(position) = self.position(&node_id, network_path) else { + log::error!("Could not get position for node: {node_id}"); + continue; + }; + + let position = FrontendXY { x: position.x, y: position.y }; + + let inputs = (0..self.number_of_inputs(&node_id, network_path)) + .map(|input_index| self.frontend_input_from_connector(&InputConnector::node(node_id, input_index), network_path)) + .collect(); + let outputs = (0..self.number_of_outputs(&node_id, network_path)) + .map(|output_index| self.frontend_output_from_connector(&OutputConnector::node(node_id, output_index), network_path)) + .collect(); + + let node = Some(FrontendNode { position, inputs, outputs }); + + FrontendNodeOrLayer { node, layer: None } + } + }; + + let frontend_node_to_render = FrontendNodeToRender { metadata, node_or_layer }; + + nodes.push(frontend_node_to_render); + } nodes } @@ -146,12 +164,15 @@ impl NodeNetworkInterface { // } // }; + 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, }) } diff --git a/frontend/src/messages.ts b/frontend/src/messages.ts index 7a76acb655..66092a2d3f 100644 --- a/frontend/src/messages.ts +++ b/frontend/src/messages.ts @@ -78,7 +78,7 @@ export class UpdateLayerWidths extends JsMessage { } export class UpdateNodeGraphNodes extends JsMessage { - readonly nodesToRender!: FrontendNode[]; + readonly nodesToRender!: FrontendNodeToRender[]; readonly inSelectedNetwork!: boolean; @@ -199,6 +199,8 @@ export class FrontendGraphOutput { } export class FrontendNodeMetadata { + readonly nodeId!: bigint; + readonly canBeLayer!: boolean; readonly displayName!: string; @@ -255,11 +257,15 @@ export class FrontendLayerPosition { } export class FrontendNodeOrLayer { - readonly metadata!: FrontendNodeMetadata; readonly node!: FrontendNode | undefined; readonly layer!: FrontendLayer | undefined; } +export class FrontendNodeToRender { + readonly metadata!: FrontendNodeMetadata; + readonly nodeOrLayer!: FrontendNodeOrLayer; +} + export class UpdateCentralNodeGraph extends JsMessage { readonly nodeOrLayer!: FrontendNodeOrLayer[]; readonly inSelectedNetwork!: boolean; diff --git a/frontend/src/state-providers/node-graph.ts b/frontend/src/state-providers/node-graph.ts index d2f4c693f2..9e12d4820f 100644 --- a/frontend/src/state-providers/node-graph.ts +++ b/frontend/src/state-providers/node-graph.ts @@ -1,11 +1,11 @@ import { writable } from "svelte/store"; import { type Editor } from "@graphite/editor"; -import type { FrontendNodeOrLayer } from "@graphite/messages"; import { type Box, type FrontendClickTargets, type ContextMenuInformation, + type FrontendNodeToRender, type FrontendNodeType, type WirePath, ClearAllNodeGraphWires, @@ -102,7 +102,7 @@ export function createNodeGraphState(editor: Editor) { update((state) => { state.nodesToRender.clear(); updateNodeGraphNodes.nodesToRender.forEach((node) => { - state.nodesToRender.set(node.id, node); + state.nodesToRender.set(node.metadata.nodeId, node); }); state.inSelectedNetwork = updateNodeGraphNodes.inSelectedNetwork; state.previewedNode = updateNodeGraphNodes.previewedNode; From a917c0c88d33c4c91ae8ca8aa4c13f2fb1de10b9 Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 31 Aug 2025 19:41:44 -0700 Subject: [PATCH 5/7] Fix graph.svelte --- .../document/node_graph/utility_types.rs | 2 +- .../network_interface/node_graph.rs | 4 +- frontend/src/components/views/Graph.svelte | 431 ++++++++---------- 3 files changed, 205 insertions(+), 232 deletions(-) diff --git a/editor/src/messages/portfolio/document/node_graph/utility_types.rs b/editor/src/messages/portfolio/document/node_graph/utility_types.rs index 14a9ea5107..f004921ef8 100644 --- a/editor/src/messages/portfolio/document/node_graph/utility_types.rs +++ b/editor/src/messages/portfolio/document/node_graph/utility_types.rs @@ -89,7 +89,7 @@ pub struct FrontendGraphOutput { // Metadata that is common to nodes and layers #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] pub struct FrontendNodeMetadata { - #[serde(rename = "node_id")] + #[serde(rename = "nodeId")] pub node_id: NodeId, // TODO: Remove and replace with popup manager system #[serde(rename = "canBeLayer")] 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 aae024f91c..60a3357725 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 @@ -72,8 +72,8 @@ impl NodeNetworkInterface { locked: self.is_locked(&node_id, network_path), chain_width: self.chain_width(&node_id, network_path), layer_has_left_border_gap: self.layer_has_left_border_gap(&node_id, network_path), - primary_input_connected_to_layer: self.primary_output_connected_to_layer(&node_id, network_path), - primary_output_connected_to_layer: self.primary_input_connected_to_layer(&node_id, network_path), + primary_input_connected_to_layer: self.primary_input_connected_to_layer(&node_id, network_path), + primary_output_connected_to_layer: self.primary_output_connected_to_layer(&node_id, network_path), }); FrontendNodeOrLayer { node: None, layer } } diff --git a/frontend/src/components/views/Graph.svelte b/frontend/src/components/views/Graph.svelte index 0c48a672a2..4e45e8e9f9 100644 --- a/frontend/src/components/views/Graph.svelte +++ b/frontend/src/components/views/Graph.svelte @@ -130,20 +130,24 @@ editor.handle.createNode(nodeType, $nodeGraph.contextMenuInformation.contextMenuCoordinates.x, $nodeGraph.contextMenuInformation.contextMenuCoordinates.y); } - function nodeBorderMask(nodeWidth: number, primaryInputExists: boolean, exposedSecondaryInputs: number, primaryOutputExists: boolean, exposedSecondaryOutputs: number): string { - const nodeHeight = Math.max(1 + exposedSecondaryInputs, 1 + exposedSecondaryOutputs) * 24; + function nodeBorderMask(nodeInputs: (FrontendGraphInput | undefined)[], nodeOutputs: (FrontendGraphOutput | undefined)[]): string { + const nodeWidth = 120; + const secondaryInputs = nodeInputs.slice(1).filter((x): x is FrontendGraphInput => x !== undefined); + const secondaryOutputs = nodeOutputs.slice(1); + + const nodeHeight = Math.max(1 + secondaryInputs.length, 1 + secondaryOutputs.length) * 24; const boxes: { x: number; y: number; width: number; height: number }[] = []; // Primary input - if (primaryInputExists) boxes.push({ x: -8, y: 4, width: 16, height: 16 }); + if (nodeInputs[0]) boxes.push({ x: -8, y: 4, width: 16, height: 16 }); // Secondary inputs - for (let i = 0; i < exposedSecondaryInputs; i++) boxes.push({ x: -8, y: 4 + (i + 1) * 24, width: 16, height: 16 }); + for (let i = 0; i < secondaryInputs.length; i++) boxes.push({ x: -8, y: 4 + (i + 1) * 24, width: 16, height: 16 }); // Primary output - if (primaryOutputExists) boxes.push({ x: nodeWidth - 8, y: 4, width: 16, height: 16 }); + if (nodeOutputs[0]) boxes.push({ x: nodeWidth - 8, y: 4, width: 16, height: 16 }); // Exposed outputs - for (let i = 0; i < exposedSecondaryOutputs; i++) boxes.push({ x: nodeWidth - 8, y: 4 + (i + 1) * 24, width: 16, height: 16 }); + for (let i = 0; i < secondaryOutputs.length; i++) boxes.push({ x: nodeWidth - 8, y: 4 + (i + 1) * 24, width: 16, height: 16 }); return borderMask(boxes, nodeWidth, nodeHeight); } @@ -203,11 +207,17 @@ return `Connected to:\n${input.connectedToString}`; } - function zipWithUndefined(arr1: FrontendGraphInput[], arr2: FrontendGraphOutput[]) { - const maxLength = Math.max(arr1.length, arr2.length); - const result = []; + function collectExposedInputsOutputs( + inputs: (FrontendGraphInput | undefined)[], + outputs: (FrontendGraphOutput | undefined)[], + ): [FrontendGraphInput | undefined, FrontendGraphOutput | undefined][] { + const secondaryInputs = inputs.slice(1).filter((x): x is FrontendGraphInput => x !== undefined); + const secondaryOutputs = outputs.slice(1); + const maxLength = Math.max(secondaryInputs.length, secondaryOutputs.length); + const result: [FrontendGraphInput | undefined, FrontendGraphOutput | undefined][] = []; + for (let i = 0; i < maxLength; i++) { - result.push([arr1[i], arr2[i]]); + result.push([secondaryInputs[i] || undefined, secondaryOutputs[i] || undefined]); } return result; } @@ -485,124 +495,118 @@ {/if}
- -
- - {#each Array.from($nodeGraph.nodesToRender).filter(([nodeId, node]) => node.isLayer && $nodeGraph.visibleNodes.has(nodeId)) as [nodeId, layer]} - {@const clipPathId = String(Math.random()).substring(2)} - {@const layerAreaWidth = $nodeGraph.layerWidths.get(layer.id) || 8} - {@const layerChainWidth = layer.chainWidth !== 0 ? layer.chainWidth + 0.5 : 0} - {@const description = (layer.reference && $nodeGraph.nodeDescriptions.get(layer.reference)) || undefined} -
- {#if layer.errors} - {layer.errors} - {layer.errors} - {/if} -
- {#if $nodeGraph.thumbnails.has(nodeId)} - {@html $nodeGraph.thumbnails.get(nodeId)} +
+ {#each Array.from($nodeGraph.nodesToRender) as [nodeId, nodeToRender]} + {#if nodeToRender.nodeOrLayer.layer !== undefined} + {@const nodeMetadata = nodeToRender.metadata} + {@const layer = nodeToRender.nodeOrLayer.layer} + {@const clipPathId = String(Math.random()).substring(2)} + {@const layerAreaWidth = $nodeGraph.layerWidths.get(nodeToRender.metadata.nodeId) || 8} + {@const layerChainWidth = layer.chainWidth !== 0 ? layer.chainWidth + 0.5 : 0} + {@const description = (nodeMetadata.reference && $nodeGraph.nodeDescriptions.get(nodeMetadata.reference)) || undefined} +
+ {#if nodeMetadata.errors} + {layer.errors} + {layer.errors} {/if} - - {#if layer.primaryOutput} +
+ {#if $nodeGraph.thumbnails.has(nodeId)} + {@html $nodeGraph.thumbnails.get(nodeId)} + {/if} + - {outputTooltip(layer.primaryOutput)} - {#if layer.primaryOutput.connectedTo.length > 0} - - {#if layer.primaryOutputConnectedToLayer} - - {/if} - {:else} - + {outputTooltip(layer.output)} + 0 ? "var(--data-color)" : "var(--data-color-dim)"} /> + + {#if layer.output.connectedTo.length > 0 && layer.primaryOutputConnectedToLayer} + {/if} - {/if} - - - {#if layer.primaryInput} - {inputTooltip(layer.primaryInput)} - {/if} - {#if layer.primaryInput?.connectedTo !== "nothing"} - - {#if layer.primaryInputConnectedToLayer} - - {/if} - {:else} - - {/if} - -
- - {#if layer.exposedInputs.length > 0} -
+ - {inputTooltip(layer.exposedInputs[0])} - {#if layer.exposedInputs[0].connectedTo !== undefined} - + {#if layer.bottomInput} + {inputTooltip(layer.bottomInput)} + {/if} + {#if layer.bottomInput?.connectedToNode !== undefined} + + {#if layer.primaryInputConnectedToLayer} + + {/if} {:else} - + {/if}
- {/if} -
- - {layer.displayName} + + {#if layer.sideInput} +
+ + {inputTooltip(layer.sideInput)} + + +
+ {/if} +
+ + {nodeMetadata.displayName} +
+
+ { + /* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */ + }} + tooltip={nodeMetadata.visible ? "Visible" : "Hidden"} + /> + + + + + + + + +
-
- { - /* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */ - }} - tooltip={layer.visible ? "Visible" : "Hidden"} - /> - - - - - - - - - -
+ {/if} {/each} -
@@ -630,129 +634,96 @@ {/if}
- - - {#each Array.from($nodeGraph.nodesToRender).filter(([nodeId, node]) => !node.isLayer && $nodeGraph.visibleNodes.has(nodeId)) as [nodeId, node]} - {@const exposedInputsOutputs = zipWithUndefined(node.exposedInputs, node.exposedOutputs)} - {@const clipPathId = String(Math.random()).substring(2)} - {@const description = (node.reference && $nodeGraph.nodeDescriptions.get(node.reference)) || undefined} -
- {#if node.errors} - {node.errors} - {node.errors} - {/if} - -
- - - {node.displayName} -
- - {#if exposedInputsOutputs.length > 0} -
- {#each exposedInputsOutputs as [input, output]} -
- - {input !== undefined ? input.name : output.name} - -
- {/each} + {#each Array.from($nodeGraph.nodesToRender) as [nodeId, nodeToRender]} + {#if nodeToRender.nodeOrLayer.node !== undefined && $nodeGraph.visibleNodes.has(nodeId)} + {@const nodeMetadata = nodeToRender.metadata} + {@const node = nodeToRender.nodeOrLayer.node} + {@const exposedInputsOutputs = collectExposedInputsOutputs(node.inputs, node.outputs)} + {@const clipPathId = String(Math.random()).substring(2)} + {@const description = (nodeMetadata.reference && $nodeGraph.nodeDescriptions.get(nodeMetadata.reference)) || undefined} +
+ {#if nodeMetadata.errors} + {node.errors} + {node.errors} + {/if} + +
+ + + {nodeMetadata.displayName}
- {/if} - -
- {#if node.primaryInput?.dataType} - - {inputTooltip(node.primaryInput)} - {#if node.primaryInput.connectedTo !== undefined} - - {:else} - - {/if} - + + {#if exposedInputsOutputs.length > 0} +
+ {#each exposedInputsOutputs as [input, output]} +
+ + {input?.name ?? output?.name ?? ""} + +
+ {/each} +
{/if} - {#each node.exposedInputs as secondary, index} - {#if index < node.exposedInputs.length} - - {inputTooltip(secondary)} - {#if secondary.connectedTo !== undefined} - - {:else} - - {/if} - - {/if} - {/each} -
- -
- {#if node.primaryOutput} - - {outputTooltip(node.primaryOutput)} - {#if node.primaryOutput.connectedTo !== undefined} - - {:else} - + +
+ {#each node.inputs as input} + {#if input !== undefined} + + {inputTooltip(input)} + + {/if} - - {/if} - {#each node.exposedOutputs as secondary} - - {outputTooltip(secondary)} - {#if secondary.connectedTo !== undefined} - - {:else} - + {/each} +
+ +
+ {#each node.outputs as output} + {#if output !== undefined} + + {outputTooltip(output)} + + {/if} - - {/each} + {/each} +
+ + + + + + +
- - - - - - - -
+ {/if} {/each}
@@ -1095,6 +1066,8 @@ // Keeps the connectors above the wires z-index: 1; + margin-top: -24px; + &.input { left: -3px; } From e2c45195ef10d7954fbd5ec4dfb03160cd1cded7 Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 1 Sep 2025 11:59:49 -0700 Subject: [PATCH 6/7] Add opacity and open to the render data --- .../src/messages/frontend/frontend_message.rs | 7 +- .../document/document_message_handler.rs | 8 +- .../node_graph/node_graph_message_handler.rs | 32 +- editor/src/test_utils.rs | 8 +- .../src/components/panels/Document.svelte | 28 +- frontend/src/components/views/Graph.svelte | 965 +++++++++--------- frontend/src/messages.ts | 13 +- frontend/src/state-providers/document.ts | 9 - frontend/src/state-providers/node-graph.ts | 15 +- 9 files changed, 540 insertions(+), 545 deletions(-) diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index e6f84f5aa1..59a713b544 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -244,9 +244,6 @@ pub enum FrontendMessage { #[serde(rename = "setColorChoice")] set_color_choice: Option, }, - UpdateGraphFadeArtwork { - percentage: f64, - }, UpdateInputHints { #[serde(rename = "hintData")] hint_data: HintData, @@ -274,9 +271,11 @@ pub enum FrontendMessage { UpdateMouseCursor { cursor: MouseCursorIcon, }, - UpdateNodeGraphNodes { + UpdateNodeGraphRender { #[serde(rename = "nodesToRender")] nodes_to_render: Vec, + open: bool, + opacity: f64, #[serde(rename = "inSelectedNetwork")] in_selected_network: bool, // Displays a dashed border around the node diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 88aeacacf1..ccf12a467a 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -561,9 +561,6 @@ impl MessageHandler> for DocumentMes self.graph_view_overlay_open = open; responses.add(FrontendMessage::UpdateGraphViewOverlay { open }); - responses.add(FrontendMessage::UpdateGraphFadeArtwork { - percentage: self.graph_fade_artwork_percentage, - }); // Update the tilt menu bar buttons to be disabled when the graph is open responses.add(MenuBarMessage::SendLayout); @@ -579,12 +576,13 @@ impl MessageHandler> for DocumentMes responses.add(NavigationMessage::CanvasTiltSet { angle_radians: 0. }); responses.add(NodeGraphMessage::SetGridAlignedEdges); responses.add(NodeGraphMessage::UpdateGraphBarRight); - responses.add(NodeGraphMessage::SendGraph); responses.add(NodeGraphMessage::UpdateHints); } else { responses.add(ToolMessage::ActivateTool { tool_type: *current_tool }); responses.add(OverlaysMessage::Draw); // Redraw overlays when graph is closed } + + responses.add(NodeGraphMessage::SendGraph); } DocumentMessage::GraphViewOverlayToggle => { responses.add(DocumentMessage::GraphViewOverlay { open: !self.graph_view_overlay_open }); @@ -1200,7 +1198,7 @@ impl MessageHandler> for DocumentMes } DocumentMessage::SetGraphFadeArtwork { percentage } => { self.graph_fade_artwork_percentage = percentage; - responses.add(FrontendMessage::UpdateGraphFadeArtwork { percentage }); + responses.add(NodeGraphMessage::SendGraph); } DocumentMessage::SetNodePinned { node_id, pinned } => { responses.add(DocumentMessage::AddTransaction); 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 1aa6cdf6d6..084f2f72cb 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 @@ -1634,24 +1634,24 @@ impl<'a> MessageHandler> for NodeG responses.add(DocumentMessage::DocumentStructureChanged); responses.add(PropertiesPanelMessage::Refresh); responses.add(NodeGraphMessage::UpdateActionButtons); - if graph_view_overlay_open { - let nodes_to_render = network_interface.collect_nodes(&self.node_graph_errors, breadcrumb_network_path); - self.frontend_nodes = nodes_to_render.iter().map(|node| node.metadata.node_id).collect(); - let previewed_node = network_interface.previewed_node(breadcrumb_network_path); - responses.add(FrontendMessage::UpdateNodeGraphNodes { - nodes_to_render, - in_selected_network: selection_network_path == breadcrumb_network_path, - previewed_node, - }); - responses.add(NodeGraphMessage::UpdateVisibleNodes); + let nodes_to_render = network_interface.collect_nodes(&self.node_graph_errors, breadcrumb_network_path); + self.frontend_nodes = nodes_to_render.iter().map(|node| node.metadata.node_id).collect(); + let previewed_node = network_interface.previewed_node(breadcrumb_network_path); + responses.add(FrontendMessage::UpdateNodeGraphRender { + nodes_to_render, + open: graph_view_overlay_open, + opacity: graph_fade_artwork_percentage, + in_selected_network: selection_network_path == breadcrumb_network_path, + previewed_node, + }); + responses.add(NodeGraphMessage::UpdateVisibleNodes); - let layer_widths = network_interface.collect_layer_widths(breadcrumb_network_path); + let layer_widths = network_interface.collect_layer_widths(breadcrumb_network_path); - responses.add(NodeGraphMessage::UpdateImportsExports); - responses.add(FrontendMessage::UpdateLayerWidths { layer_widths }); - responses.add(NodeGraphMessage::SendWires); - self.update_node_graph_hints(responses); - } + responses.add(NodeGraphMessage::UpdateImportsExports); + responses.add(FrontendMessage::UpdateLayerWidths { layer_widths }); + responses.add(NodeGraphMessage::SendWires); + self.update_node_graph_hints(responses); } NodeGraphMessage::SetGridAlignedEdges => { if graph_view_overlay_open { diff --git a/editor/src/test_utils.rs b/editor/src/test_utils.rs index addadae0c2..f917a8b003 100644 --- a/editor/src/test_utils.rs +++ b/editor/src/test_utils.rs @@ -301,11 +301,11 @@ pub trait FrontendMessageTestUtils { impl FrontendMessageTestUtils for FrontendMessage { fn check_node_graph_error(&self) { - let FrontendMessage::UpdateNodeGraphNodes { nodes, .. } = self else { return }; + let FrontendMessage::UpdateNodeGraphRender { nodes_to_render, .. } = self else { return }; - for node in nodes { - if let Some(error) = &node.errors { - panic!("error on {}: {}", node.display_name, error); + for node in nodes_to_render { + if let Some(error) = &node.metadata.errors { + panic!("error on {}: {}", node.metadata.display_name, error); } } } diff --git a/frontend/src/components/panels/Document.svelte b/frontend/src/components/panels/Document.svelte index c259c9ba74..0a58272291 100644 --- a/frontend/src/components/panels/Document.svelte +++ b/frontend/src/components/panels/Document.svelte @@ -565,9 +565,7 @@ {/if}
-
- -
+ -
- - {#if $nodeGraph.contextMenuInformation} - - {#if typeof $nodeGraph.contextMenuInformation.contextMenuData === "string" && $nodeGraph.contextMenuInformation.contextMenuData === "CreateNode"} - createNode(e.detail)} /> - {:else if $nodeGraph.contextMenuInformation.contextMenuData && "compatibleType" in $nodeGraph.contextMenuInformation.contextMenuData} - createNode(e.detail)} /> - {:else} - {@const contextMenuData = $nodeGraph.contextMenuInformation.contextMenuData} - - Display as - toggleLayerDisplay(false, contextMenuData.nodeId), - }, - { - value: "layer", - label: "Layer", - action: () => toggleLayerDisplay(true, contextMenuData.nodeId), - }, - ]} - disabled={!canBeToggledBetweenNodeAndLayer(contextMenuData.nodeId)} - /> - - - - editor.handle.mergeSelectedNodes()} /> - - {/if} - - {/if} +
+
+ + {#if $nodeGraph.contextMenuInformation} + + {#if typeof $nodeGraph.contextMenuInformation.contextMenuData === "string" && $nodeGraph.contextMenuInformation.contextMenuData === "CreateNode"} + createNode(e.detail)} /> + {:else if $nodeGraph.contextMenuInformation.contextMenuData && "compatibleType" in $nodeGraph.contextMenuInformation.contextMenuData} + createNode(e.detail)} /> + {:else} + {@const contextMenuData = $nodeGraph.contextMenuInformation.contextMenuData} + + Display as + toggleLayerDisplay(false, contextMenuData.nodeId), + }, + { + value: "layer", + label: "Layer", + action: () => toggleLayerDisplay(true, contextMenuData.nodeId), + }, + ]} + disabled={!canBeToggledBetweenNodeAndLayer(contextMenuData.nodeId)} + /> + + + + editor.handle.mergeSelectedNodes()} /> + + {/if} + + {/if} - - {#if $nodeGraph.clickTargets} -
+ + {#if $nodeGraph.clickTargets} +
+ + {#each $nodeGraph.clickTargets.nodeClickTargets as pathString} + + {/each} + {#each $nodeGraph.clickTargets.layerClickTargets as pathString} + + {/each} + {#each $nodeGraph.clickTargets.connectorClickTargets as pathString} + + {/each} + {#each $nodeGraph.clickTargets.iconClickTargets as pathString} + + {/each} + + + {#each $nodeGraph.clickTargets.modifyImportExport as pathString} + + {/each} + +
+ {/if} + + +
- {#each $nodeGraph.clickTargets.nodeClickTargets as pathString} - - {/each} - {#each $nodeGraph.clickTargets.layerClickTargets as pathString} - - {/each} - {#each $nodeGraph.clickTargets.connectorClickTargets as pathString} - - {/each} - {#each $nodeGraph.clickTargets.iconClickTargets as pathString} - - {/each} - - - {#each $nodeGraph.clickTargets.modifyImportExport as pathString} - + {#each $nodeGraph.wires.values() as map} + {#each map.values() as { pathString, dataType, thick, dashed }} + {#if thick} + + {/if} + {/each} {/each}
- {/if} - -
- - {#each $nodeGraph.wires.values() as map} - {#each map.values() as { pathString, dataType, thick, dashed }} - {#if thick} - + +
+ {#if $nodeGraph.updateImportsExports} + {#each $nodeGraph.updateImportsExports.imports as frontendOutput, index} + {#if frontendOutput} + + {outputTooltip(frontendOutput)} + {#if frontendOutput.connectedTo.length > 0} + + {:else} + + {/if} + + +
(hoveringImportIndex = index)} + on:pointerleave={() => (hoveringImportIndex = undefined)} + class="edit-import-export import" + class:separator-bottom={index === 0 && $nodeGraph.updateImportsExports.addImportExport} + class:separator-top={index === 1 && $nodeGraph.updateImportsExports.addImportExport} + style:--offset-left={($nodeGraph.updateImportsExports.importPosition.x - 8) / 24} + style:--offset-top={($nodeGraph.updateImportsExports.importPosition.y - 8) / 24 + index} + > + {#if editingNameImportIndex == index} + e.key === "Enter" && setEditingImportName(e)} + /> + {:else} +

setEditingImportNameIndex(index, frontendOutput.name)}> + {frontendOutput.name} +

+ {/if} + {#if (hoveringImportIndex === index || editingNameImportIndex === index) && $nodeGraph.updateImportsExports.addImportExport} + { + /* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */ + }} + /> + {#if index > 0} +
+ {/if} + {/if} +
+ {:else} +
+ editor.handle.addPrimaryImport()} /> +
{/if} {/each} - {/each} - -
- - -
- {#if $nodeGraph.updateImportsExports} - {#each $nodeGraph.updateImportsExports.imports as frontendOutput, index} - {#if frontendOutput} - - {outputTooltip(frontendOutput)} - {#if frontendOutput.connectedTo.length > 0} - - {:else} - - {/if} - -
(hoveringImportIndex = index)} - on:pointerleave={() => (hoveringImportIndex = undefined)} - class="edit-import-export import" - class:separator-bottom={index === 0 && $nodeGraph.updateImportsExports.addImportExport} - class:separator-top={index === 1 && $nodeGraph.updateImportsExports.addImportExport} - style:--offset-left={($nodeGraph.updateImportsExports.importPosition.x - 8) / 24} - style:--offset-top={($nodeGraph.updateImportsExports.importPosition.y - 8) / 24 + index} - > - {#if editingNameImportIndex == index} - e.key === "Enter" && setEditingImportName(e)} - /> - {:else} -

setEditingImportNameIndex(index, frontendOutput.name)}> - {frontendOutput.name} -

- {/if} - {#if (hoveringImportIndex === index || editingNameImportIndex === index) && $nodeGraph.updateImportsExports.addImportExport} - { - /* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */ - }} - /> - {#if index > 0} -
+ {#each $nodeGraph.updateImportsExports.exports as frontendInput, index} + {#if frontendInput} + + {inputTooltip(frontendInput)} + {#if frontendInput.connectedTo !== "nothing"} + + {:else} + {/if} - {/if} -
- {:else} + +
(hoveringExportIndex = index)} + on:pointerleave={() => (hoveringExportIndex = undefined)} + class="edit-import-export export" + class:separator-bottom={index === 0 && $nodeGraph.updateImportsExports.addImportExport} + class:separator-top={index === 1 && $nodeGraph.updateImportsExports.addImportExport} + style:--offset-left={($nodeGraph.updateImportsExports.exportPosition.x - 8) / 24} + style:--offset-top={($nodeGraph.updateImportsExports.exportPosition.y - 8) / 24 + index} + > + {#if (hoveringExportIndex === index || editingNameExportIndex === index) && $nodeGraph.updateImportsExports.addImportExport} + {#if index > 0} +
+ {/if} + { + /* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */ + }} + /> + {/if} + {#if editingNameExportIndex === index} + e.key === "Enter" && setEditingExportName(e)} + /> + {:else} +

setEditingExportNameIndex(index, frontendInput.name)}> + {frontendInput.name} +

+ {/if} +
+ {:else} +
+ editor.handle.addPrimaryExport()} /> +
+ {/if} + {/each} + + {#if $nodeGraph.updateImportsExports.addImportExport == true}
- editor.handle.addPrimaryImport()} /> + editor.handle.addSecondaryImport()} />
- {/if} - {/each} - - {#each $nodeGraph.updateImportsExports.exports as frontendInput, index} - {#if frontendInput} - - {inputTooltip(frontendInput)} - {#if frontendInput.connectedTo !== "nothing"} - - {:else} - - {/if} - -
(hoveringExportIndex = index)} - on:pointerleave={() => (hoveringExportIndex = undefined)} - class="edit-import-export export" - class:separator-bottom={index === 0 && $nodeGraph.updateImportsExports.addImportExport} - class:separator-top={index === 1 && $nodeGraph.updateImportsExports.addImportExport} - style:--offset-left={($nodeGraph.updateImportsExports.exportPosition.x - 8) / 24} - style:--offset-top={($nodeGraph.updateImportsExports.exportPosition.y - 8) / 24 + index} - > - {#if (hoveringExportIndex === index || editingNameExportIndex === index) && $nodeGraph.updateImportsExports.addImportExport} - {#if index > 0} -
- {/if} - { - /* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */ - }} - /> - {/if} - {#if editingNameExportIndex === index} - e.key === "Enter" && setEditingExportName(e)} - /> - {:else} -

setEditingExportNameIndex(index, frontendInput.name)}> - {frontendInput.name} -

- {/if} -
- {:else}
- editor.handle.addPrimaryExport()} /> + editor.handle.addSecondaryExport()} />
{/if} - {/each} - - {#if $nodeGraph.updateImportsExports.addImportExport == true} -
- editor.handle.addSecondaryImport()} /> -
-
- editor.handle.addSecondaryExport()} /> -
- {/if} - {#if $nodeGraph.reorderImportIndex !== undefined} - {@const position = { - x: Number($nodeGraph.updateImportsExports.importPosition.x), - y: Number($nodeGraph.updateImportsExports.importPosition.y) + Number($nodeGraph.reorderImportIndex) * 24, - }} -
- {/if} + {#if $nodeGraph.reorderImportIndex !== undefined} + {@const position = { + x: Number($nodeGraph.updateImportsExports.importPosition.x), + y: Number($nodeGraph.updateImportsExports.importPosition.y) + Number($nodeGraph.reorderImportIndex) * 24, + }} +
+ {/if} - {#if $nodeGraph.reorderExportIndex !== undefined} - {@const position = { - x: Number($nodeGraph.updateImportsExports.exportPosition.x), - y: Number($nodeGraph.updateImportsExports.exportPosition.y) + Number($nodeGraph.reorderExportIndex) * 24, - }} -
+ {#if $nodeGraph.reorderExportIndex !== undefined} + {@const position = { + x: Number($nodeGraph.updateImportsExports.exportPosition.x), + y: Number($nodeGraph.updateImportsExports.exportPosition.y) + Number($nodeGraph.reorderExportIndex) * 24, + }} +
+ {/if} {/if} - {/if} -
+
-
- {#each Array.from($nodeGraph.nodesToRender) as [nodeId, nodeToRender]} - {#if nodeToRender.nodeOrLayer.layer !== undefined} - {@const nodeMetadata = nodeToRender.metadata} - {@const layer = nodeToRender.nodeOrLayer.layer} - {@const clipPathId = String(Math.random()).substring(2)} - {@const layerAreaWidth = $nodeGraph.layerWidths.get(nodeToRender.metadata.nodeId) || 8} - {@const layerChainWidth = layer.chainWidth !== 0 ? layer.chainWidth + 0.5 : 0} - {@const description = (nodeMetadata.reference && $nodeGraph.nodeDescriptions.get(nodeMetadata.reference)) || undefined} -
- {#if nodeMetadata.errors} - {layer.errors} - {layer.errors} - {/if} -
- {#if $nodeGraph.thumbnails.has(nodeId)} - {@html $nodeGraph.thumbnails.get(nodeId)} +
+ {#each Array.from($nodeGraph.nodesToRender) as [nodeId, nodeToRender]} + {#if nodeToRender.nodeOrLayer.layer !== undefined} + {@const nodeMetadata = nodeToRender.metadata} + {@const layer = nodeToRender.nodeOrLayer.layer} + {@const clipPathId = String(Math.random()).substring(2)} + {@const layerAreaWidth = $nodeGraph.layerWidths.get(nodeToRender.metadata.nodeId) || 8} + {@const layerChainWidth = layer.chainWidth !== 0 ? layer.chainWidth + 0.5 : 0} + {@const description = (nodeMetadata.reference && $nodeGraph.nodeDescriptions.get(nodeMetadata.reference)) || undefined} +
+ {#if nodeMetadata.errors} + {layer.errors} + {layer.errors} {/if} - - - {outputTooltip(layer.output)} - 0 ? "var(--data-color)" : "var(--data-color-dim)"} /> - - {#if layer.output.connectedTo.length > 0 && layer.primaryOutputConnectedToLayer} - - {/if} - - - - {#if layer.bottomInput} - {inputTooltip(layer.bottomInput)} - {/if} - {#if layer.bottomInput?.connectedToNode !== undefined} - - {#if layer.primaryInputConnectedToLayer} - - {/if} - {:else} - +
+ {#if $nodeGraph.thumbnails.has(nodeId)} + {@html $nodeGraph.thumbnails.get(nodeId)} {/if} - -
- - {#if layer.sideInput} -
+ - {inputTooltip(layer.sideInput)} + {outputTooltip(layer.output)} 0 ? "var(--data-color)" : "var(--data-color-dim)"} /> + + {#if layer.output.connectedTo.length > 0 && layer.primaryOutputConnectedToLayer} + + {/if} + + + + {#if layer.bottomInput} + {inputTooltip(layer.bottomInput)} + {/if} + {#if layer.bottomInput?.connectedToNode !== undefined} + + {#if layer.primaryInputConnectedToLayer} + + {/if} + {:else} + + {/if}
- {/if} -
- - {nodeMetadata.displayName} -
-
- { - /* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */ - }} - tooltip={nodeMetadata.visible ? "Visible" : "Hidden"} - /> - - - - - - - - - -
- {/if} - {/each} - -
- - {#each $nodeGraph.wires.values() as map} - {#each map.values() as { pathString, dataType, thick, dashed }} - {#if !thick} - - {/if} - {/each} - {/each} - {#if $nodeGraph.wirePathInProgress} - - {/if} - -
- {#each Array.from($nodeGraph.nodesToRender) as [nodeId, nodeToRender]} - {#if nodeToRender.nodeOrLayer.node !== undefined && $nodeGraph.visibleNodes.has(nodeId)} - {@const nodeMetadata = nodeToRender.metadata} - {@const node = nodeToRender.nodeOrLayer.node} - {@const exposedInputsOutputs = collectExposedInputsOutputs(node.inputs, node.outputs)} - {@const clipPathId = String(Math.random()).substring(2)} - {@const description = (nodeMetadata.reference && $nodeGraph.nodeDescriptions.get(nodeMetadata.reference)) || undefined} -
- {#if nodeMetadata.errors} - {node.errors} - {node.errors} - {/if} - -
- - - {nodeMetadata.displayName} -
- - {#if exposedInputsOutputs.length > 0} -
- {#each exposedInputsOutputs as [input, output]} -
- - {input?.name ?? output?.name ?? ""} - -
- {/each} -
- {/if} - -
- {#each node.inputs as input} - {#if input !== undefined} - - {inputTooltip(input)} - - - {/if} - {/each} -
- -
- {#each node.outputs as output} - {#if output !== undefined} + + {#if layer.sideInput} +
- {outputTooltip(output)} + {inputTooltip(layer.sideInput)} +
+ {/if} +
+ + {nodeMetadata.displayName} +
+
+ { + /* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */ + }} + tooltip={nodeMetadata.visible ? "Visible" : "Hidden"} + /> + + + + + + + + + +
+ {/if} + {/each} + +
+ + {#each $nodeGraph.wires.values() as map} + {#each map.values() as { pathString, dataType, thick, dashed }} + {#if !thick} + {/if} {/each} + {/each} + {#if $nodeGraph.wirePathInProgress} + + {/if} + +
+ {#each Array.from($nodeGraph.nodesToRender) as [nodeId, nodeToRender]} + {#if nodeToRender.nodeOrLayer.node !== undefined && $nodeGraph.visibleNodes.has(nodeId)} + {@const nodeMetadata = nodeToRender.metadata} + {@const node = nodeToRender.nodeOrLayer.node} + {@const exposedInputsOutputs = collectExposedInputsOutputs(node.inputs, node.outputs)} + {@const clipPathId = String(Math.random()).substring(2)} + {@const description = (nodeMetadata.reference && $nodeGraph.nodeDescriptions.get(nodeMetadata.reference)) || undefined} +
+ {#if nodeMetadata.errors} + {node.errors} + {node.errors} + {/if} + +
+ + + {nodeMetadata.displayName} +
+ + {#if exposedInputsOutputs.length > 0} +
+ {#each exposedInputsOutputs as [input, output]} +
+ + {input?.name ?? output?.name ?? ""} + +
+ {/each} +
+ {/if} + +
+ {#each node.inputs as input} + {#if input !== undefined} + + {inputTooltip(input)} + + + {/if} + {/each} +
+ +
+ {#each node.outputs as output} + {#if output !== undefined} + + {outputTooltip(output)} + + + {/if} + {/each} +
+ + + + + + +
- - - - - - - -
- {/if} - {/each} + {/if} + {/each} +
-
- -{#if $nodeGraph.box} -
-{/if} + + {#if $nodeGraph.box} +
+ {/if} +