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..e2bc8c97dc 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, 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}; 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")] @@ -253,9 +244,6 @@ pub enum FrontendMessage { #[serde(rename = "setColorChoice")] set_color_choice: Option, }, - UpdateGraphFadeArtwork { - percentage: f64, - }, UpdateInputHints { #[serde(rename = "hintData")] hint_data: HintData, @@ -283,8 +271,18 @@ pub enum FrontendMessage { UpdateMouseCursor { cursor: MouseCursorIcon, }, - UpdateNodeGraphNodes { - nodes: Vec, + 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 + #[serde(rename = "previewedNode")] + previewed_node: Option, + #[serde(rename = "nativeNodeGraphRender")] + native_node_graph_render: bool, }, UpdateVisibleNodes { nodes: Vec, @@ -298,9 +296,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..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 }); @@ -1191,7 +1189,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) { @@ -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.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs index 1432046a25..93705d3706 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 @@ -185,6 +185,7 @@ pub enum NodeGraphMessage { TogglePreviewImpl { node_id: NodeId, }, + ToggleNativeNodeGraphRender, SetImportExportName { name: String, index: ImportOrExport, @@ -231,7 +232,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 e86870f736..911b4f456e 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; @@ -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::{ @@ -93,8 +93,8 @@ pub struct NodeGraphMessageHandler { end_index: Option, /// Used to keep track of what nodes are sent to the front end so that only visible ones are sent to the frontend frontend_nodes: Vec, - /// Used to keep track of what wires are sent to the front end so the old ones can be removed - frontend_wires: HashSet<(NodeId, usize)>, + /// Disables rendering nodes in Svelte + native_node_graph_render: bool, } /// NodeGraphMessageHandler always modifies the network which the selected nodes are in. No GraphOperationMessages should be added here, since those messages will always affect the document network. @@ -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,24 +1633,26 @@ 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 = self.collect_nodes(network_interface, breadcrumb_network_path); - self.frontend_nodes = nodes.iter().map(|node| node.id).collect(); - responses.add(FrontendMessage::UpdateNodeGraphNodes { nodes }); - responses.add(NodeGraphMessage::UpdateVisibleNodes); + responses.add(NodeGraphMessage::UpdateActionButtons); + 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, + native_node_graph_render: self.native_node_graph_render, + }); + 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(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 { @@ -1799,6 +1801,10 @@ impl<'a> MessageHandler> for NodeG NodeGraphMessage::TogglePreviewImpl { node_id } => { network_interface.toggle_preview(node_id, selection_network_path); } + NodeGraphMessage::ToggleNativeNodeGraphRender => { + self.native_node_graph_render = !self.native_node_graph_render; + responses.add(NodeGraphMessage::SendGraph); + } NodeGraphMessage::ToggleSelectedLocked => { let Some(selected_nodes) = network_interface.selected_nodes_in_nested_network(selection_network_path) else { log::error!("Could not get selected nodes in NodeGraphMessage::ToggleSelectedLocked"); @@ -1956,6 +1962,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 +2015,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"); @@ -2499,94 +2501,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(); @@ -2758,7 +2672,7 @@ impl Default for NodeGraphMessageHandler { reordering_import: None, end_index: None, frontend_nodes: Vec::new(), - frontend_wires: HashSet::new(), + native_node_graph_render: false, } } } 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..f004921ef8 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")] @@ -59,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)] @@ -78,35 +86,81 @@ 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 { + #[serde(rename = "nodeId")] + pub node_id: NodeId, + // TODO: Remove and replace with popup manager system #[serde(rename = "canBeLayer")] pub can_be_layer: 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, - #[serde(rename = "primaryOutputConnectedToLayer")] - pub primary_output_connected_to_layer: bool, - #[serde(rename = "primaryInputConnectedToLayer")] - pub primary_input_connected_to_layer: bool, - pub position: IVec2, + 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, - pub locked: bool, - pub previewed: 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 = "primaryInputConnectedToLayer")] + pub primary_input_connected_to_layer: bool, + #[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 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.rs b/editor/src/messages/portfolio/document/utility_types/network_interface.rs index 27f93d5ecb..fecba7ed14 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 @@ -3149,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 @@ -3164,21 +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 - .upstream_flow_back_from_nodes(vec![*node_id], network_path, FlowType::HorizontalFlow) - .skip(1) - .all(|node_id| self.is_chain(&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 new file mode 100644 index 0000000000..60a3357725 --- /dev/null +++ b/editor/src/messages/portfolio/document/utility_types/network_interface/node_graph.rs @@ -0,0 +1,427 @@ +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, 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 { + let Some(network) = self.nested_network(network_path) else { + 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(); + + 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 + } + }); + + 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(), + visible, + errors, + }; + + 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_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 } + } + 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 + } + + /// 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() + // } + // }; + + 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, + }) + } + + /// 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) + || 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 + 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())) + } +} diff --git a/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs b/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs index 9b86761d87..f66fdd4783 100644 --- a/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs +++ b/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs @@ -18,6 +18,7 @@ pub struct MenuBarMessageHandler { pub has_selection_history: (bool, bool), pub message_logging_verbosity: MessageLoggingVerbosity, pub reset_node_definitions_on_open: bool, + pub native_node_graph_render: bool, pub make_path_editable_is_allowed: bool, pub data_panel_open: bool, pub layers_panel_open: bool, @@ -48,6 +49,7 @@ impl LayoutHolder for MenuBarMessageHandler { let message_logging_verbosity_names = self.message_logging_verbosity == MessageLoggingVerbosity::Names; let message_logging_verbosity_contents = self.message_logging_verbosity == MessageLoggingVerbosity::Contents; let reset_node_definitions_on_open = self.reset_node_definitions_on_open; + let native_node_graph_render = self.native_node_graph_render; let make_path_editable_is_allowed = self.make_path_editable_is_allowed; let menu_bar_entries = vec![ @@ -696,6 +698,12 @@ impl LayoutHolder for MenuBarMessageHandler { action: MenuBarEntry::create_action(|_| PortfolioMessage::ToggleResetNodesToDefinitionsOnOpen.into()), ..MenuBarEntry::default() }], + vec![MenuBarEntry { + label: "Native Node Graph UI Render".into(), + icon: Some(if native_node_graph_render { "CheckboxChecked" } else { "CheckboxUnchecked" }.into()), + action: MenuBarEntry::create_action(|_| NodeGraphMessage::ToggleNativeNodeGraphRender.into()), + ..MenuBarEntry::default() + }], vec![ MenuBarEntry { label: "Print Trace Logs".into(), 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} -
- -
+ 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); } - 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 +162,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 }); } @@ -200,28 +204,250 @@ } function inputConnectedToText(input: FrontendGraphInput): string { - return `Connected to:\n${input.connectedTo}`; + 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; } -
+{#if !$nodeGraph.nativeNodeGraphRender} +
+ +
+ {#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)} + {/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} + +
+ + {#if layer.sideInput} +
+ + {inputTooltip(layer.sideInput)} + + +
+ {/if} +
+ + {nodeMetadata.displayName} +
+
+ { + /* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */ + }} + tooltip={nodeMetadata.visible ? "Visible" : "Hidden"} + /> + + + + + + + + + +
+ {/if} + {/each} + + {#each Array.from($nodeGraph.nodesToRender) as [_, nodeToRender]} + {#each nodeToRender.wires as [wire, thick, dataType]} + + + + {/each} + {/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 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} + +
{#if $nodeGraph.contextMenuInformation} {/if} - - {#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.wires.values() as map} - {#each map.values() as { pathString, dataType, thick, dashed }} - {#if thick} - - {/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} -
- {/if} - {/if} -
- {:else} -
- editor.handle.addPrimaryImport()} /> -
- {/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()} /> -
- {/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.reorderExportIndex !== undefined} - {@const position = { - x: Number($nodeGraph.updateImportsExports.exportPosition.x), - y: Number($nodeGraph.updateImportsExports.exportPosition.y) + Number($nodeGraph.reorderExportIndex) * 24, - }} -
- {/if} - {/if} -
- - -
- - {#each Array.from($nodeGraph.nodes) - .filter(([nodeId, node]) => node.isLayer && $nodeGraph.visibleNodes.has(nodeId)) - .map(([_, node], nodeIndex) => ({ node, nodeIndex })) as { node, nodeIndex } (nodeIndex)} - {@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} -
- {#if node.errors} - {node.errors} - {node.errors} - {/if} -
- {#if $nodeGraph.thumbnails.has(node.id)} - {@html $nodeGraph.thumbnails.get(node.id)} - {/if} - - {#if node.primaryOutput} - - {outputTooltip(node.primaryOutput)} - {#if node.primaryOutput.connectedTo.length > 0} - - {#if node.primaryOutputConnectedToLayer} - - {/if} - {:else} - - {/if} - - {/if} - - - {#if node.primaryInput} - {inputTooltip(node.primaryInput)} - {/if} - {#if node.primaryInput?.connectedTo !== "nothing"} - - {#if node.primaryInputConnectedToLayer} - - {/if} - {:else} - - {/if} - -
- - {#if node.exposedInputs.length > 0} -
- - {inputTooltip(stackDataInput)} - {#if stackDataInput.connectedTo !== undefined} - - {:else} - - {/if} - -
- {/if} -
- - {node.displayName} -
-
- { - /* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */ - }} - tooltip={node.visible ? "Visible" : "Hidden"} - /> - - - - - - - - + + {#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}
- {/each} + {/if} - -
+ +
{#each $nodeGraph.wires.values() as map} {#each map.values() as { pathString, dataType, thick, dashed }} - {#if !thick} + {#if thick} - {/if}
- - {#each Array.from($nodeGraph.nodes) - .filter(([nodeId, node]) => !node.isLayer && $nodeGraph.visibleNodes.has(nodeId)) - .map(([_, node], nodeIndex) => ({ node, nodeIndex })) as { node, nodeIndex } (nodeIndex)} - {@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} -
- {/if} - -
- {#if node.primaryInput?.dataType} + +
+ {#if $nodeGraph.updateImportsExports} + {#each $nodeGraph.updateImportsExports.imports as frontendOutput, index} + {#if frontendOutput} - {inputTooltip(node.primaryInput)} - {#if node.primaryInput.connectedTo !== undefined} + {outputTooltip(frontendOutput)} + {#if frontendOutput.connectedTo.length > 0} {:else} {/if} - {/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} - (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} > - {outputTooltip(node.primaryOutput)} - {#if node.primaryOutput.connectedTo !== undefined} - + {#if editingNameImportIndex == index} +

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 node.exposedOutputs as secondary} + {/each} + + {#each $nodeGraph.updateImportsExports.exports as frontendInput, index} + {#if frontendInput} - {outputTooltip(secondary)} - {#if secondary.connectedTo !== undefined} + {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()} /> +
+ {/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.reorderExportIndex !== undefined} + {@const position = { + x: Number($nodeGraph.updateImportsExports.exportPosition.x), + y: Number($nodeGraph.updateImportsExports.exportPosition.y) + Number($nodeGraph.reorderExportIndex) * 24, + }} +
+ {/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)} + {/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} + +
+ + {#if layer.sideInput} +
+ + {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} + {#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 $nodeGraph.box} -
-{/if} + + {#if $nodeGraph.box} +
+ {/if} +