From 4250f291abec78245707eea1dbb06cfbb3b71ca7 Mon Sep 17 00:00:00 2001 From: Adam Gerhant <116332429+adamgerhant@users.noreply.github.com> Date: Tue, 12 Nov 2024 14:27:42 -0800 Subject: [PATCH] Add merging nodes into a subgraph with Ctrl+M and basic subgraph signature customization (#2097) * Merge nodes * Fix bugs/crashes * WIP: Debugging * Fix bugs, add button * Add imports/exports * Improve button * Fix breadcrumbs * Fix lints and change shortcut key --------- Co-authored-by: Keavon Chambers --- .../src/messages/frontend/frontend_message.rs | 4 + .../messages/input_mapper/input_mappings.rs | 1 + .../document/document_message_handler.rs | 15 +- .../node_graph/document_node_definitions.rs | 29 +- .../document/node_graph/node_graph_message.rs | 16 +- .../node_graph/node_graph_message_handler.rs | 314 ++++++++--- .../utility_types/network_interface.rs | 494 +++++++++++++----- .../portfolio/portfolio_message_handler.rs | 14 +- frontend/src/components/views/Graph.svelte | 63 ++- .../src/components/widgets/WidgetSpan.svelte | 2 +- frontend/src/state-providers/node-graph.ts | 4 + frontend/src/wasm-communication/messages.ts | 6 + frontend/wasm/src/editor_api.rs | 11 +- 13 files changed, 735 insertions(+), 238 deletions(-) diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index 8f95e55be0..f5482cdeb1 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -131,6 +131,10 @@ pub enum FrontendMessage { UpdateImportsExports { imports: Vec<(FrontendGraphOutput, i32, i32)>, exports: Vec<(FrontendGraphInput, i32, i32)>, + #[serde(rename = "addImport")] + add_import: Option<(i32, i32)>, + #[serde(rename = "addExport")] + add_export: Option<(i32, i32)>, }, UpdateInSelectedNetwork { #[serde(rename = "inSelectedNetwork")] diff --git a/editor/src/messages/input_mapper/input_mappings.rs b/editor/src/messages/input_mapper/input_mappings.rs index ffb856ce52..b008ae123d 100644 --- a/editor/src/messages/input_mapper/input_mappings.rs +++ b/editor/src/messages/input_mapper/input_mappings.rs @@ -75,6 +75,7 @@ pub fn input_mappings() -> Mapping { entry!(KeyDown(KeyL); modifiers=[Alt], action_dispatch=NodeGraphMessage::ToggleSelectedAsLayersOrNodes), entry!(KeyDown(KeyC); modifiers=[Shift], action_dispatch=NodeGraphMessage::PrintSelectedNodeCoordinates), entry!(KeyDown(KeyC); modifiers=[Alt], action_dispatch=NodeGraphMessage::SendClickTargets), + entry!(KeyDown(KeyM); modifiers=[Accel], action_dispatch=NodeGraphMessage::MergeSelectedNodes), entry!(KeyUp(KeyC); action_dispatch=NodeGraphMessage::EndSendClickTargets), entry!(KeyDown(ArrowUp); action_dispatch=NodeGraphMessage::ShiftSelectedNodes { direction: Direction::Up, rubber_band: false }), entry!(KeyDown(ArrowRight); action_dispatch=NodeGraphMessage::ShiftSelectedNodes { direction: Direction::Right, rubber_band: false }), diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 5aa2b11c94..303ff572f6 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -326,7 +326,7 @@ impl MessageHandler> for DocumentMessag responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![id] }); } DocumentMessage::DebugPrintDocument => { - info!("{:#?}", self.network_interface); + info!("{:?}", self.network_interface); } DocumentMessage::DeleteNode { node_id } => { responses.add(DocumentMessage::StartTransaction); @@ -1128,6 +1128,9 @@ impl MessageHandler> for DocumentMessag // TODO: Allow non layer nodes to have click targets let layer_click_targets = click_targets .into_iter() + .filter(|(node_id, _)| + // Ensure that the layer is in the document network to prevent logging an error + self.network_interface.network(&[]).unwrap().nodes.contains_key(node_id)) .filter_map(|(node_id, click_targets)| { self.network_interface.is_layer(&node_id, &[]).then(|| { let layer = LayerNodeIdentifier::new(node_id, &self.network_interface, &[]); @@ -1223,11 +1226,19 @@ impl MessageHandler> for DocumentMessag self.network_interface.set_transform(transform, &self.breadcrumb_network_path); let imports = self.network_interface.frontend_imports(&self.breadcrumb_network_path).unwrap_or_default(); let exports = self.network_interface.frontend_exports(&self.breadcrumb_network_path).unwrap_or_default(); + let add_import = self.network_interface.frontend_import_modify(&self.breadcrumb_network_path); + let add_export = self.network_interface.frontend_export_modify(&self.breadcrumb_network_path); + responses.add(DocumentMessage::RenderRulers); responses.add(DocumentMessage::RenderScrollbars); responses.add(NodeGraphMessage::UpdateEdges); responses.add(NodeGraphMessage::UpdateBoxSelection); - responses.add(FrontendMessage::UpdateImportsExports { imports, exports }); + responses.add(FrontendMessage::UpdateImportsExports { + imports, + exports, + add_import, + add_export, + }); responses.add(FrontendMessage::UpdateNodeGraphTransform { transform: Transform { scale: transform.matrix2.x_axis.x, diff --git a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs index afb6f8f3cb..9c1e61dcaa 100644 --- a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs +++ b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs @@ -64,6 +64,23 @@ static DOCUMENT_NODE_TYPES: once_cell::sync::Lazy> = /// The [`DocumentNode`] is the instance while these [`DocumentNodeDefinition`]s are the "classes" or "blueprints" from which the instances are built. fn static_nodes() -> Vec { let mut custom = vec![ + // TODO: Auto-generate this from its proto node macro + DocumentNodeDefinition { + identifier: "Default Network", + category: "General", + node_template: NodeTemplate { + document_node: DocumentNode { + implementation: DocumentNodeImplementation::Network(NodeNetwork::default()), + ..Default::default() + }, + persistent_node_metadata: DocumentNodePersistentMetadata { + network_metadata: Some(NodeNetworkMetadata::default()), + ..Default::default() + }, + }, + description: Cow::Borrowed("A default node network you can use to create your own custom nodes."), + properties: &node_properties::node_no_properties, + }, // TODO: Auto-generate this from its proto node macro DocumentNodeDefinition { identifier: "Identity", @@ -80,7 +97,7 @@ fn static_nodes() -> Vec { ..Default::default() }, }, - description: Cow::Borrowed("The identity node simply passes its data through. You can use this to organize your node graph if you want."), + description: Cow::Borrowed("The identity node passes its data through. You can use this to organize your node graph."), properties: &|_document_node, _node_id, _context| node_properties::string_properties("The identity node simply passes its data through"), }, // TODO: Auto-generate this from its proto node macro @@ -101,7 +118,7 @@ fn static_nodes() -> Vec { ..Default::default() }, }, - description: Cow::Borrowed("The Monitor node is used by the editor to access the data flowing through it"), + description: Cow::Borrowed("The Monitor node is used by the editor to access the data flowing through it."), properties: &|_document_node, _node_id, _context| node_properties::string_properties("The Monitor node is used by the editor to access the data flowing through it"), }, DocumentNodeDefinition { @@ -208,7 +225,7 @@ fn static_nodes() -> Vec { ..Default::default() }, }, - description: Cow::Borrowed("The Merge node combines graphical data through composition"), + description: Cow::Borrowed("The Merge node combines graphical data through composition."), properties: &node_properties::node_no_properties, }, DocumentNodeDefinition { @@ -319,7 +336,7 @@ fn static_nodes() -> Vec { ..Default::default() }, }, - description: Cow::Borrowed("Creates a new Artboard which can be used as a working surface"), + description: Cow::Borrowed("Creates a new Artboard which can be used as a working surface."), properties: &node_properties::artboard_properties, }, DocumentNodeDefinition { @@ -719,7 +736,7 @@ fn static_nodes() -> Vec { ..Default::default() }, }, - description: Cow::Borrowed("Creates an embedded image with the given transform"), + description: Cow::Borrowed("Creates an embedded image with the given transform."), properties: &|_document_node, _node_id, _context| node_properties::string_properties("Creates an embedded image with the given transform"), }, DocumentNodeDefinition { @@ -798,7 +815,7 @@ fn static_nodes() -> Vec { ..Default::default() }, }, - description: Cow::Borrowed("Generates different noise patters"), + description: Cow::Borrowed("Generates different noise patterns."), properties: &node_properties::noise_pattern_properties, }, // TODO: This needs to work with resolution-aware (raster with footprint, post-Cull node) data. 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 30f6d730ab..80553e730e 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 @@ -4,6 +4,7 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate, OutputConnector}; use crate::messages::prelude::*; +use glam::IVec2; use graph_craft::document::value::TaggedValue; use graph_craft::document::{NodeId, NodeInput}; use graph_craft::proto::GraphErrors; @@ -16,6 +17,8 @@ pub enum NodeGraphMessage { nodes: Vec<(NodeId, NodeTemplate)>, new_ids: HashMap, }, + AddImport, + AddExport, Init, SelectedNodesUpdated, Copy, @@ -68,6 +71,7 @@ pub enum NodeGraphMessage { input_connector: InputConnector, insert_node_input_index: usize, }, + MergeSelectedNodes, MoveLayerToStack { layer: LayerNodeIdentifier, parent: LayerNodeIdentifier, @@ -126,19 +130,23 @@ pub enum NodeGraphMessage { node_id: NodeId, alias: String, }, + SetToNodeOrLayer { + node_id: NodeId, + is_layer: bool, + }, ShiftNodePosition { node_id: NodeId, x: i32, y: i32, }, - SetToNodeOrLayer { - node_id: NodeId, - is_layer: bool, - }, ShiftSelectedNodes { direction: Direction, rubber_band: bool, }, + ShiftSelectedNodesByAmount { + graph_delta: IVec2, + rubber_band: bool, + }, TogglePreview { node_id: NodeId, }, 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 1d4a996fe0..2700c93835 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 @@ -8,7 +8,9 @@ use crate::messages::portfolio::document::graph_operation::utility_types::Modify 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::utility_types::document_metadata::LayerNodeIdentifier; -use crate::messages::portfolio::document::utility_types::network_interface::{self, InputConnector, NodeNetworkInterface, NodeTemplate, OutputConnector, Previewing, TypeSource}; +use crate::messages::portfolio::document::utility_types::network_interface::{ + self, InputConnector, NodeNetworkInterface, NodeTemplate, NodeTypePersistentMetadata, OutputConnector, Previewing, TypeSource, +}; use crate::messages::portfolio::document::utility_types::nodes::{CollapsedLayers, LayerPanelEntry}; use crate::messages::prelude::*; use crate::messages::tool::common_functionality::auto_panning::AutoPanning; @@ -97,6 +99,8 @@ impl<'a> MessageHandler> for NodeGrap responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![new_layer_id] }); } + NodeGraphMessage::AddImport => network_interface.add_import(graph_craft::document::value::TaggedValue::None, true, -1, String::new(), breadcrumb_network_path), + NodeGraphMessage::AddExport => network_interface.add_export(graph_craft::document::value::TaggedValue::None, -1, String::new(), breadcrumb_network_path), NodeGraphMessage::Init => { responses.add(BroadcastMessage::SubscribeEvent { on: BroadcastEvent::SelectionChanged, @@ -345,6 +349,171 @@ impl<'a> MessageHandler> for NodeGrap } => { network_interface.insert_node_between(&node_id, &input_connector, insert_node_input_index, selection_network_path); } + NodeGraphMessage::MergeSelectedNodes => { + let new_ids = network_interface + .selected_nodes(breadcrumb_network_path) + .unwrap() + .selected_nodes() + .map(|id| (*id, *id)) + .collect::>(); + + let copied_nodes = network_interface.copy_nodes(&new_ids, breadcrumb_network_path).collect::>(); + let selected_node_ids = copied_nodes.iter().map(|(node_id, _)| *node_id).collect::>(); + let selected_node_ids_vec = copied_nodes.iter().map(|(node_id, _)| *node_id).collect::>(); + // Mapping of the encapsulating node inputs/outputs to where it needs to be connected + let mut input_connections = Vec::new(); + let mut output_connections = Vec::new(); + // Mapping of the inner nodes that need to be connected to the imports/exports + let mut import_connections = Vec::new(); + let mut export_connections = Vec::new(); + // Scan current nodes top to bottom and find all inputs/outputs connected to nodes that are not in the copied nodes. These will represent the new imports and exports. + let Some(nodes_sorted_top_to_bottom) = + network_interface.nodes_sorted_top_to_bottom(network_interface.selected_nodes(breadcrumb_network_path).unwrap().selected_nodes(), breadcrumb_network_path) + else { + return; + }; + //Ensure that nodes can be grouped by checking if there is an unselected node between selected nodes + for selected_node_id in &selected_node_ids { + for input_index in 0..network_interface.number_of_inputs(selected_node_id, breadcrumb_network_path) { + let input_connector = InputConnector::node(*selected_node_id, input_index); + if let Some(upstream_deselected_node_id) = network_interface + .upstream_output_connector(&input_connector, breadcrumb_network_path) + .and_then(|output_connector| output_connector.node_id()) + .filter(|node_id| !selected_node_ids.contains(node_id)) + { + for upstream_node_id in + network_interface.upstream_flow_back_from_nodes(vec![upstream_deselected_node_id], breadcrumb_network_path, network_interface::FlowType::UpstreamFlow) + { + if selected_node_ids.contains(&upstream_node_id) { + responses.add(DialogMessage::DisplayDialogError { + title: "Error Grouping Nodes".to_string(), + description: "A discontinuous selection of nodes cannot be grouped.\nEnsure no deselected nodes are between selected nodes".to_string(), + }); + return; + } + } + } + } + } + for node_id in nodes_sorted_top_to_bottom { + for input_index in 0..network_interface.number_of_inputs(&node_id, breadcrumb_network_path) { + let current_input_connector = InputConnector::node(node_id, input_index); + let Some(upstream_connector) = network_interface.upstream_output_connector(¤t_input_connector, breadcrumb_network_path) else { + continue; + }; + if upstream_connector + .node_id() + .is_some_and(|upstream_node_id| selected_node_ids.iter().any(|copied_id| *copied_id == upstream_node_id)) + { + continue; + } + + // If the upstream connection is not part of the copied nodes, then connect it to the new imports, or add it if it has not already been added. + let import_index = input_connections.iter().position(|old_connection| old_connection == &upstream_connector).unwrap_or_else(|| { + input_connections.push(upstream_connector); + input_connections.len() - 1 + }); + import_connections.push((current_input_connector, import_index)); + } + for output_index in 0..network_interface.number_of_outputs(&node_id, breadcrumb_network_path) { + let current_output_connector = OutputConnector::node(node_id, output_index); + let Some(outward_wires) = network_interface.outward_wires(breadcrumb_network_path) else { + log::error!("Could not get outward wires in upstream_nodes_below_layer"); + continue; + }; + let Some(downstream_connections) = outward_wires.get(¤t_output_connector).cloned() else { + log::error!("Could not get downstream connections for {current_output_connector:?}"); + continue; + }; + + // The output gets connected to all the previous inputs the node was connected to + let mut connect_output_to = Vec::new(); + for downstream_connection in downstream_connections { + if downstream_connection.node_id().is_some_and(|downstream_node_id| selected_node_ids.contains(&downstream_node_id)) { + continue; + } + connect_output_to.push(downstream_connection); + } + if !connect_output_to.is_empty() { + // Every output connected to some non selected node forms a new export + export_connections.push(current_output_connector); + output_connections.push(connect_output_to); + } + } + } + + // Use the network interface to add a default node, then set the imports, exports, paste the nodes inside, and connect them to the imports/exports + let encapsulating_node_id = NodeId::new(); + let mut default_node_template = document_node_definitions::resolve_document_node_type("Default Network") + .expect("Default Network node should exist") + .default_node_template(); + let Some(center_of_selected_nodes) = network_interface.selected_nodes_bounding_box(breadcrumb_network_path).map(|[a, b]| (a + b) / 2.) else { + log::error!("Could not get center of selected_nodes"); + return; + }; + let center_of_selected_nodes_grid_space = IVec2::new((center_of_selected_nodes.x / 24. + 0.5).floor() as i32, (center_of_selected_nodes.y / 24. + 0.5).floor() as i32); + default_node_template.persistent_node_metadata.node_type_metadata = NodeTypePersistentMetadata::node(center_of_selected_nodes_grid_space - IVec2::new(3, 1)); + responses.add(DocumentMessage::AddTransaction); + responses.add(NodeGraphMessage::InsertNode { + node_id: encapsulating_node_id, + node_template: default_node_template, + }); + responses.add(NodeGraphMessage::SetDisplayNameImpl { + node_id: encapsulating_node_id, + alias: "Untitled Node".to_string(), + }); + + responses.add(DocumentMessage::EnterNestedNetwork { node_id: encapsulating_node_id }); + for _ in 0..input_connections.len() { + responses.add(NodeGraphMessage::AddImport); + } + for _ in 0..output_connections.len() { + responses.add(NodeGraphMessage::AddExport); + } + responses.add(NodeGraphMessage::AddNodes { nodes: copied_nodes, new_ids }); + responses.add(NodeGraphMessage::SelectedNodesSet { nodes: selected_node_ids_vec.clone() }); + + // Shift the nodes back to the origin + responses.add(NodeGraphMessage::ShiftSelectedNodesByAmount { + graph_delta: -center_of_selected_nodes_grid_space - IVec2::new(2, 2), + rubber_band: false, + }); + + for (input_connector, import_index) in import_connections { + responses.add(NodeGraphMessage::CreateWire { + output_connector: OutputConnector::Import(import_index), + input_connector, + }); + } + for (export_index, output_connector) in export_connections.into_iter().enumerate() { + responses.add(NodeGraphMessage::CreateWire { + output_connector, + input_connector: InputConnector::Export(export_index), + }); + } + responses.add(DocumentMessage::ExitNestedNetwork { steps_back: 1 }); + for (input_index, output_connector) in input_connections.into_iter().enumerate() { + responses.add(NodeGraphMessage::CreateWire { + output_connector, + input_connector: InputConnector::node(encapsulating_node_id, input_index), + }); + } + for (output_index, input_connectors) in output_connections.into_iter().enumerate() { + for input_connector in input_connectors { + responses.add(NodeGraphMessage::CreateWire { + output_connector: OutputConnector::node(encapsulating_node_id, output_index), + input_connector, + }); + } + } + responses.add(NodeGraphMessage::DeleteNodes { + node_ids: selected_node_ids_vec, + delete_children: false, + }); + responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![encapsulating_node_id] }); + responses.add(NodeGraphMessage::SendGraph); + responses.add(NodeGraphMessage::RunDocumentGraph); + } NodeGraphMessage::MoveLayerToStack { layer, parent, insert_index } => { network_interface.move_layer_to_stack(layer, parent, insert_index, selection_network_path); } @@ -390,6 +559,23 @@ impl<'a> MessageHandler> for NodeGrap let node_graph_point = network_metadata.persistent_metadata.navigation_metadata.node_graph_to_viewport.inverse().transform_point2(click); + let Some(modify_import_export) = network_interface.modify_import_export(selection_network_path) else { + log::error!("Could not get modify import export in PointerDown"); + return; + }; + + if modify_import_export.add_export.intersect_point_no_stroke(node_graph_point) { + responses.add(DocumentMessage::AddTransaction); + responses.add(NodeGraphMessage::AddExport); + responses.add(NodeGraphMessage::SendGraph); + return; + } else if modify_import_export.add_import.intersect_point_no_stroke(node_graph_point) { + responses.add(DocumentMessage::AddTransaction); + responses.add(NodeGraphMessage::AddImport); + responses.add(NodeGraphMessage::SendGraph); + return; + } + if network_interface .layer_click_target_from_click(click, network_interface::LayerClickTargetTypes::Grip, selection_network_path) .is_some() @@ -674,20 +860,8 @@ impl<'a> MessageHandler> for NodeGrap if ipp.keyboard.get(crate::messages::tool::tool_messages::tool_prelude::Key::Alt as usize) { responses.add(NodeGraphMessage::DuplicateSelectedNodes); // Duplicating sets a 2x2 offset, so shift the nodes back to the original position - responses.add(NodeGraphMessage::ShiftSelectedNodes { - direction: Direction::Up, - rubber_band: false, - }); - responses.add(NodeGraphMessage::ShiftSelectedNodes { - direction: Direction::Up, - rubber_band: false, - }); - responses.add(NodeGraphMessage::ShiftSelectedNodes { - direction: Direction::Left, - rubber_band: false, - }); - responses.add(NodeGraphMessage::ShiftSelectedNodes { - direction: Direction::Left, + responses.add(NodeGraphMessage::ShiftSelectedNodesByAmount { + graph_delta: IVec2::new(-2, -2), rubber_band: false, }); self.preview_on_mouse_up = None; @@ -704,43 +878,7 @@ impl<'a> MessageHandler> for NodeGrap graph_delta.x -= previous_round_x; graph_delta.y -= previous_round_y; - while graph_delta != IVec2::ZERO { - match graph_delta.x.cmp(&0) { - Ordering::Greater => { - responses.add(NodeGraphMessage::ShiftSelectedNodes { - direction: Direction::Right, - rubber_band: true, - }); - graph_delta.x -= 1; - } - Ordering::Less => { - responses.add(NodeGraphMessage::ShiftSelectedNodes { - direction: Direction::Left, - rubber_band: true, - }); - graph_delta.x += 1; - } - Ordering::Equal => {} - } - - match graph_delta.y.cmp(&0) { - Ordering::Greater => { - responses.add(NodeGraphMessage::ShiftSelectedNodes { - direction: Direction::Down, - rubber_band: true, - }); - graph_delta.y -= 1; - } - Ordering::Less => { - responses.add(NodeGraphMessage::ShiftSelectedNodes { - direction: Direction::Up, - rubber_band: true, - }); - graph_delta.y += 1; - } - Ordering::Equal => {} - } - } + responses.add(NodeGraphMessage::ShiftSelectedNodesByAmount { graph_delta, rubber_band: true }); } else if self.box_selection_start.is_some() { responses.add(NodeGraphMessage::UpdateBoxSelection); } @@ -1094,7 +1232,14 @@ impl<'a> MessageHandler> for NodeGrap let (layer_widths, chain_widths, has_left_input_wire) = network_interface.collect_layer_widths(breadcrumb_network_path); let imports = network_interface.frontend_imports(breadcrumb_network_path).unwrap_or_default(); let exports = network_interface.frontend_exports(breadcrumb_network_path).unwrap_or_default(); - responses.add(FrontendMessage::UpdateImportsExports { imports, exports }); + let add_import = network_interface.frontend_import_modify(breadcrumb_network_path); + let add_export = network_interface.frontend_export_modify(breadcrumb_network_path); + responses.add(FrontendMessage::UpdateImportsExports { + imports, + exports, + add_import, + add_export, + }); responses.add(FrontendMessage::UpdateNodeGraph { nodes, wires }); responses.add(FrontendMessage::UpdateLayerWidths { layer_widths, @@ -1110,7 +1255,14 @@ impl<'a> MessageHandler> for NodeGrap // Send the new edges to the frontend let imports = network_interface.frontend_imports(breadcrumb_network_path).unwrap_or_default(); let exports = network_interface.frontend_exports(breadcrumb_network_path).unwrap_or_default(); - responses.add(FrontendMessage::UpdateImportsExports { imports, exports }); + let add_import = network_interface.frontend_import_modify(breadcrumb_network_path); + let add_export = network_interface.frontend_export_modify(breadcrumb_network_path); + responses.add(FrontendMessage::UpdateImportsExports { + imports, + exports, + add_import, + add_export, + }); } } NodeGraphMessage::SetInputValue { node_id, input_index, value } => { @@ -1142,7 +1294,45 @@ impl<'a> MessageHandler> for NodeGrap responses.add(DocumentMessage::RenderScrollbars); } } + NodeGraphMessage::ShiftSelectedNodesByAmount { mut graph_delta, rubber_band } => { + while graph_delta != IVec2::ZERO { + match graph_delta.x.cmp(&0) { + Ordering::Greater => { + responses.add(NodeGraphMessage::ShiftSelectedNodes { + direction: Direction::Right, + rubber_band, + }); + graph_delta.x -= 1; + } + Ordering::Less => { + responses.add(NodeGraphMessage::ShiftSelectedNodes { + direction: Direction::Left, + rubber_band, + }); + graph_delta.x += 1; + } + Ordering::Equal => {} + } + match graph_delta.y.cmp(&0) { + Ordering::Greater => { + responses.add(NodeGraphMessage::ShiftSelectedNodes { + direction: Direction::Down, + rubber_band, + }); + graph_delta.y -= 1; + } + Ordering::Less => { + responses.add(NodeGraphMessage::ShiftSelectedNodes { + direction: Direction::Up, + rubber_band, + }); + graph_delta.y += 1; + } + Ordering::Equal => {} + } + } + } NodeGraphMessage::ToggleSelectedAsLayersOrNodes => { let Some(selected_nodes) = network_interface.selected_nodes(selection_network_path) else { log::error!("Could not get selected nodes in NodeGraphMessage::ToggleSelectedAsLayersOrNodes"); @@ -1435,6 +1625,7 @@ impl NodeGraphMessageHandler { Cut, DeleteSelectedNodes, DuplicateSelectedNodes, + MergeSelectedNodes, ToggleSelectedAsLayersOrNodes, ToggleSelectedLocked, ToggleSelectedVisibility, @@ -1930,18 +2121,18 @@ impl NodeGraphMessageHandler { let exposed_inputs = inputs.flatten().collect(); let output_types = network_interface.output_types(&node_id, breadcrumb_network_path); - let primary_output_type = output_types.first().expect("Primary output should always exist"); - let frontend_data_type = if let Some((output_type, _)) = primary_output_type { + let primary_output_type = output_types.first().cloned().flatten(); + let frontend_data_type = if let Some((output_type, _)) = &primary_output_type { FrontendGraphDataType::with_type(output_type) } else { FrontendGraphDataType::General }; let connected_to = outward_wires.get(&OutputConnector::node(node_id, 0)).cloned().unwrap_or_default(); - let primary_output = if network_interface.has_primary_output(&node_id, breadcrumb_network_path) { + let primary_output = if network_interface.has_primary_output(&node_id, breadcrumb_network_path) && !output_types.is_empty() { Some(FrontendGraphOutput { data_type: frontend_data_type, name: "Output 1".to_string(), - resolved_type: primary_output_type.clone().map(|(input, type_source)| format!("{input:?} from {type_source:?}")), + resolved_type: primary_output_type.map(|(input, type_source)| format!("{input:?} from {type_source:?}")), connected_to, }) } else { @@ -1967,7 +2158,8 @@ impl NodeGraphMessageHandler { .output_names .get(index) .map(|output_name| output_name.to_string()) - .unwrap_or(format!("Output {}", index + 1)); + .filter(|output_name| !output_name.is_empty()) + .unwrap_or_else(|| exposed_output.clone().map(|(output_type, _)| output_type.nested_type().to_string()).unwrap_or_default()); let connected_to = outward_wires.get(&OutputConnector::node(node_id, index)).cloned().unwrap_or_default(); exposed_outputs.push(FrontendGraphOutput { @@ -2204,10 +2396,8 @@ fn frontend_inputs_lookup(breadcrumb_network_path: &[NodeId], network_interface: let Some(network) = network_interface.network(breadcrumb_network_path) else { return Default::default(); }; - let network_metadata = network_interface.network_metadata(breadcrumb_network_path); let mut frontend_inputs_lookup = HashMap::new(); for (&node_id, node) in network.nodes.iter() { - let node_metadata = network_metadata.and_then(|network_metadata| network_metadata.persistent_metadata.node_metadata.get(&node_id)); let mut inputs = Vec::with_capacity(node.inputs.len()); for (index, input) in node.inputs.iter().enumerate() { let is_exposed = input.is_exposed_to_frontend(breadcrumb_network_path.is_empty()); @@ -2219,7 +2409,7 @@ fn frontend_inputs_lookup(breadcrumb_network_path: &[NodeId], network_interface: } // Get the name from the metadata here (since it also requires a reference to the `network_interface`) - let name = node_metadata.and_then(|node_metadata| node_metadata.persistent_metadata.input_names.get(index)).cloned(); + let name = network_interface.input_name(&node_id, index, breadcrumb_network_path); // Get the output connector that feeds into this input (done here as well for simplicity) let connector = OutputConnector::from_input(input); 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 a6d62053f3..95dca57036 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface.rs @@ -173,30 +173,17 @@ impl NodeNetworkInterface { } pub fn chain_width(&self, node_id: &NodeId, network_path: &[NodeId]) -> u32 { - if self.number_of_inputs(node_id, network_path) > 1 { + 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::HorizontalFlow) + .upstream_flow_back_from_nodes(vec![*node_id], network_path, FlowType::HorizontalPrimaryOutputFlow) .skip(1) .enumerate() .collect::>() { - let Some(network_metadata) = self.network_metadata(network_path) else { - log::error!("Could not get nested network_metadata in chain_width"); - return 0; - }; // Check if the node is positioned as a chain - let is_chain = network_metadata - .persistent_metadata - .node_metadata - .get(&node_id) - .map(|node_metadata| &node_metadata.persistent_metadata.node_type_metadata) - .is_some_and(|node_type_metadata| match node_type_metadata { - NodeTypePersistentMetadata::Node(node_persistent_metadata) => matches!(node_persistent_metadata.position, NodePosition::Chain), - _ => false, - }); - if is_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; @@ -269,6 +256,7 @@ impl NodeNetworkInterface { encapsulating_node.inputs.len() } else { // There is one(?) import to the document network, but the imports are not displayed + // I think this is zero now that the scope system has been added 1 } } @@ -283,19 +271,31 @@ impl NodeNetworkInterface { } } - fn number_of_inputs(&self, node_id: &NodeId, network_path: &[NodeId]) -> usize { + fn number_of_displayed_inputs(&self, node_id: &NodeId, network_path: &[NodeId]) -> usize { let Some(network) = self.network(network_path) else { - log::error!("Could not get network in number_of_input"); + log::error!("Could not get network in number_of_displayed_inputs"); return 0; }; let Some(node) = network.nodes.get(node_id) else { - log::error!("Could not get node {node_id} in number_of_input"); + log::error!("Could not get node {node_id} in number_of_displayed_inputs"); return 0; }; node.inputs.iter().filter(|input| input.is_exposed_to_frontend(network_path.is_empty())).count() } - fn number_of_outputs(&self, node_id: &NodeId, network_path: &[NodeId]) -> usize { + pub fn number_of_inputs(&self, node_id: &NodeId, network_path: &[NodeId]) -> usize { + let Some(network) = self.network(network_path) else { + log::error!("Could not get network in number_of_inputs"); + return 0; + }; + let Some(node) = network.nodes.get(node_id) else { + log::error!("Could not get node {node_id} in number_of_inputs"); + return 0; + }; + node.inputs.len() + } + + pub fn number_of_outputs(&self, node_id: &NodeId, network_path: &[NodeId]) -> usize { let Some(network) = self.network(network_path) else { log::error!("Could not get network in number_of_outputs"); return 0; @@ -314,7 +314,7 @@ impl NodeNetworkInterface { /// Creates a copy for each node by disconnecting nodes which are not connected to other copied nodes. /// Returns an iterator of all persistent metadata for a node and their ids pub fn copy_nodes<'a>(&'a mut self, new_ids: &'a HashMap, network_path: &'a [NodeId]) -> impl Iterator + 'a { - new_ids + let mut new_nodes = new_ids .iter() .filter_map(|(node_id, &new)| { self.create_node_template(node_id, network_path).and_then(|mut node_template| { @@ -342,7 +342,7 @@ impl NodeNetworkInterface { }; } - // Ensure a chain node has a selected downstream layer, and set absolute nodes to a chain if there is a downstream layer + // If a chain node does not have a selected downstream layer, then set the position to absolute let downstream_layer = self.downstream_layer(node_id, network_path); if downstream_layer.map_or(true, |downstream_layer| new_ids.keys().all(|key| *key != downstream_layer.to_node())) { let Some(position) = self.position(node_id, network_path) else { @@ -352,20 +352,6 @@ impl NodeNetworkInterface { node_template.persistent_node_metadata.node_type_metadata = NodeTypePersistentMetadata::Node(NodePersistentMetadata { position: NodePosition::Absolute(position), }); - } else if !self.is_layer(node_id, network_path) { - if let Some(downstream_layer) = downstream_layer { - if self - .upstream_flow_back_from_nodes(vec![downstream_layer.to_node()], network_path, FlowType::HorizontalFlow) - .skip(1) - .take_while(|node_id| !self.is_layer(node_id, network_path)) - .any(|upstream_node| upstream_node == *node_id) - { - match &mut node_template.persistent_node_metadata.node_type_metadata { - NodeTypePersistentMetadata::Node(node_metadata) => node_metadata.position = NodePosition::Chain, - NodeTypePersistentMetadata::Layer(_) => log::error!("Node is not be a layer"), - }; - } - } } // Shift all absolute nodes 2 to the right and 2 down @@ -383,12 +369,25 @@ impl NodeNetworkInterface { } } - Some((new, node_id, node_template)) + Some((new, *node_id, node_template)) }) }) - .collect::>() - .into_iter() - .map(move |(new, node_id, node)| (new, self.map_ids(node, node_id, new_ids, network_path))) + .collect::>(); + + for old_id in new_nodes.iter().map(|(_, old_id, _)| *old_id).collect::>() { + // Try set all selected nodes upstream of a layer to be chain nodes + if self.is_layer(&old_id, network_path) { + for valid_upstream_chain_node in self.valid_upstream_chain_nodes(&InputConnector::node(old_id, 1), network_path) { + if let Some(node_template) = new_nodes.iter_mut().find_map(|(_, old_id, template)| (*old_id == valid_upstream_chain_node).then_some(template)) { + match &mut node_template.persistent_node_metadata.node_type_metadata { + NodeTypePersistentMetadata::Node(node_metadata) => node_metadata.position = NodePosition::Chain, + NodeTypePersistentMetadata::Layer(_) => log::error!("Node cannot be a layer"), + }; + } + } + } + } + new_nodes.into_iter().map(move |(new, node_id, node)| (new, self.map_ids(node, &node_id, new_ids, network_path))) } /// Create a node template from an existing node. @@ -838,6 +837,32 @@ impl NodeNetworkInterface { }) } + pub fn frontend_import_modify(&mut self, network_path: &[NodeId]) -> Option<(i32, i32)> { + (!network_path.is_empty()) + .then(|| { + self.modify_import_export(network_path).and_then(|modify_import_export_click_target| { + modify_import_export_click_target + .add_export + .bounding_box() + .map(|bounding_box| (bounding_box[0].x as i32, bounding_box[0].y as i32)) + }) + }) + .flatten() + } + + pub fn frontend_export_modify(&mut self, network_path: &[NodeId]) -> Option<(i32, i32)> { + (!network_path.is_empty()) + .then(|| { + self.modify_import_export(network_path).and_then(|modify_import_export_click_target| { + modify_import_export_click_target + .add_import + .bounding_box() + .map(|bounding_box| (bounding_box[0].x as i32, bounding_box[0].y as i32)) + }) + }) + .flatten() + } + 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) @@ -1023,7 +1048,16 @@ impl NodeNetworkInterface { .and_then(|node_metadata| node_metadata.persistent_metadata.reference.as_ref().map(|reference| reference.to_string())) } - pub fn display_name(&self, node_id: &NodeId, network_path: &[NodeId]) -> String { + // None means that the type will be used + pub fn input_name(&self, node_id: &NodeId, index: usize, network_path: &[NodeId]) -> Option { + self.node_metadata(node_id, network_path) + .and_then(|node_metadata| node_metadata.persistent_metadata.input_names.get(index)) + .cloned() + .filter(|s| !s.is_empty()) + } + + // Use frontend display name instead + fn display_name(&self, node_id: &NodeId, network_path: &[NodeId]) -> String { let Some(node_metadata) = self.node_metadata(node_id, network_path) else { log::error!("Could not get node_metadata in display_name"); return "".to_string(); @@ -1889,6 +1923,109 @@ impl NodeNetworkInterface { network_metadata.transient_metadata.import_export_ports.unload(); } + pub fn modify_import_export(&mut self, network_path: &[NodeId]) -> Option<&ModifyImportExportClickTarget> { + let Some(network_metadata) = self.network_metadata(network_path) else { + log::error!("Could not get nested network_metadata in modify_import_export"); + return None; + }; + if !network_metadata.transient_metadata.modify_import_export.is_loaded() { + self.load_modify_import_export(network_path); + } + let Some(network_metadata) = self.network_metadata(network_path) else { + log::error!("Could not get nested network_metadata in modify_import_export"); + return None; + }; + let TransientMetadata::Loaded(click_targets) = &network_metadata.transient_metadata.modify_import_export else { + log::error!("could not load modify import export ports"); + return None; + }; + Some(click_targets) + } + + pub fn load_modify_import_export(&mut self, network_path: &[NodeId]) { + 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; + }; + let Some(rounded_network_edge_distance) = self.rounded_network_edge_distance(network_path).cloned() else { + log::error!("Could not get rounded_network_edge_distance in load_export_ports"); + return; + }; + let Some(network_metadata) = self.network_metadata(network_path) else { + log::error!("Could not get nested network_metadata in load_export_ports"); + return; + }; + let Some(network) = self.network(network_path) else { + log::error!("Could not get current network in load_export_ports"); + return; + }; + + let viewport_top_right = network_metadata + .persistent_metadata + .navigation_metadata + .node_graph_to_viewport + .inverse() + .transform_point2(rounded_network_edge_distance.exports_to_edge_distance); + 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 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.) + offset_from_top_right; + let export_top_right = DVec2::new(viewport_top_right.x.max(bounding_box_top_right.x), viewport_top_right.y.min(bounding_box_top_right.y)); + let add_export_center = export_top_right + DVec2::new(0., network.exports.len() as f64 * 24.); + let add_export = ClickTarget::new(Subpath::new_ellipse(add_export_center - DVec2::new(8., 8.), add_export_center + DVec2::new(8., 8.)), 0.); + + let viewport_top_left = network_metadata + .persistent_metadata + .navigation_metadata + .node_graph_to_viewport + .inverse() + .transform_point2(rounded_network_edge_distance.imports_to_edge_distance); + + 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(viewport_top_left.x.min(bounding_box_top_left.x), viewport_top_left.y.min(bounding_box_top_left.y)); + let add_import_center = import_top_left + DVec2::new(0., self.number_of_displayed_imports(network_path) as f64 * 24.); + let add_import = ClickTarget::new(Subpath::new_ellipse(add_import_center - DVec2::new(8., 8.), add_import_center + DVec2::new(8., 8.)), 0.); + + let Some(network_metadata) = self.network_metadata_mut(network_path) else { + log::error!("Could not get current network in load_modify_import_export"); + return; + }; + + network_metadata.transient_metadata.modify_import_export = TransientMetadata::Loaded(ModifyImportExportClickTarget { + add_export, + add_import, + remove_imports: Vec::new(), + remove_exports: Vec::new(), + move_import: Vec::new(), + move_export: Vec::new(), + }); + } + + fn unload_modify_import_export(&mut self, network_path: &[NodeId]) { + let Some(network_metadata) = self.network_metadata_mut(network_path) else { + log::error!("Could not get nested network_metadata in unload_export_ports"); + return; + }; + network_metadata.transient_metadata.modify_import_export.unload(); + } + pub fn rounded_network_edge_distance(&mut self, network_path: &[NodeId]) -> Option<&NetworkEdgeDistance> { let Some(network_metadata) = self.network_metadata(network_path) else { log::error!("Could not get nested network_metadata in rounded_network_edge_distance"); @@ -2054,24 +2191,35 @@ impl NodeNetworkInterface { for (current_node_id, node) in network.nodes.iter() { for (input_index, input) in node.inputs.iter().enumerate() { if let NodeInput::Node { node_id, output_index, .. } = input { - let outward_wires_entry = outward_wires - .get_mut(&OutputConnector::node(*node_id, *output_index)) - .expect("All output connectors should be initialized"); + // If this errors then there is an input to a node that does not exist + let outward_wires_entry = outward_wires.get_mut(&OutputConnector::node(*node_id, *output_index)).unwrap_or_else(|| { + panic!( + "Output connector {:?} should be initialized for each node output from a node", + OutputConnector::node(*node_id, *output_index) + ) + }); outward_wires_entry.push(InputConnector::node(*current_node_id, input_index)); } else if let NodeInput::Network { import_index, .. } = input { - let outward_wires_entry = outward_wires.get_mut(&OutputConnector::Import(*import_index)).expect("All output connectors should be initialized"); + let outward_wires_entry = outward_wires + .get_mut(&OutputConnector::Import(*import_index)) + .unwrap_or_else(|| panic!("Output connector {:?} should be initialized for each import from a node", OutputConnector::Import(*import_index))); outward_wires_entry.push(InputConnector::node(*current_node_id, input_index)); } } } for (export_index, export) in network.exports.iter().enumerate() { if let NodeInput::Node { node_id, output_index, .. } = export { - let outward_wires_entry = outward_wires - .get_mut(&OutputConnector::node(*node_id, *output_index)) - .expect("All output connectors should be initialized"); + let outward_wires_entry = outward_wires.get_mut(&OutputConnector::node(*node_id, *output_index)).unwrap_or_else(|| { + panic!( + "Output connector {:?} should be initialized for each node input from exports", + OutputConnector::node(*node_id, *output_index) + ) + }); outward_wires_entry.push(InputConnector::Export(export_index)); } else if let NodeInput::Network { import_index, .. } = export { - let outward_wires_entry = outward_wires.get_mut(&OutputConnector::Import(*import_index)).expect("All output connectors should be initialized"); + let outward_wires_entry = outward_wires + .get_mut(&OutputConnector::Import(*import_index)) + .unwrap_or_else(|| panic!("Output connector {:?} should be initialized between imports and exports", OutputConnector::Import(*import_index))); outward_wires_entry.push(InputConnector::Export(export_index)); } } @@ -2242,7 +2390,7 @@ impl NodeNetworkInterface { output_row_count += 1; } - let height = std::cmp::max(input_row_count, output_row_count) as u32 * crate::consts::GRID_SIZE; + let height = input_row_count.max(output_row_count).max(1) as u32 * crate::consts::GRID_SIZE; let width = 5 * crate::consts::GRID_SIZE; let node_click_target_top_left = node_top_left + DVec2::new(0., 12.); let node_click_target_bottom_right = node_click_target_top_left + DVec2::new(width as f64, height as f64); @@ -2560,7 +2708,7 @@ impl NodeNetworkInterface { } pub fn is_eligible_to_be_layer(&mut self, node_id: &NodeId, network_path: &[NodeId]) -> bool { - let input_count = self.number_of_inputs(node_id, network_path); + let input_count = self.number_of_displayed_inputs(node_id, network_path); let output_count = self.number_of_outputs(node_id, network_path); self.node_metadata(node_id, network_path) @@ -2762,11 +2910,17 @@ impl NodeNetworkInterface { log::error!("Could not get nested network_metadata in selected_nodes_bounding_box_viewport"); return None; }; + let node_graph_to_viewport = network_metadata.persistent_metadata.navigation_metadata.node_graph_to_viewport; + self.selected_nodes_bounding_box(network_path) + .map(|[a, b]| [node_graph_to_viewport.transform_point2(a), node_graph_to_viewport.transform_point2(b)]) + } + + /// Get the combined bounding box of the click targets of the selected nodes in the node graph in layer space + pub fn selected_nodes_bounding_box(&mut self, network_path: &[NodeId]) -> Option<[DVec2; 2]> { let Some(selected_nodes) = self.selected_nodes(network_path) else { log::error!("Could not get selected nodes in selected_nodes_bounding_box_viewport"); return None; }; - let node_graph_to_viewport = network_metadata.persistent_metadata.navigation_metadata.node_graph_to_viewport; selected_nodes .selected_nodes() .cloned() @@ -2774,7 +2928,7 @@ impl NodeNetworkInterface { .iter() .filter_map(|node_id| { self.node_click_targets(node_id, network_path) - .and_then(|transient_node_metadata| transient_node_metadata.node_click_target.bounding_box_with_transform(node_graph_to_viewport)) + .and_then(|transient_node_metadata| transient_node_metadata.node_click_target.bounding_box()) }) .reduce(graphene_core::renderer::Quad::combine_bounds) } @@ -2979,6 +3133,7 @@ impl NodeNetworkInterface { }; network_metadata.persistent_metadata.navigation_metadata.node_graph_to_viewport = transform; self.unload_import_export_ports(network_path); + self.unload_modify_import_export(network_path); } // This should be run whenever the pan ends, a zoom occurs, or the network is opened @@ -2990,6 +3145,7 @@ impl NodeNetworkInterface { network_metadata.persistent_metadata.navigation_metadata.node_graph_top_right = node_graph_top_right; self.unload_rounded_network_edge_distance(network_path); self.unload_import_export_ports(network_path); + self.unload_modify_import_export(network_path); } pub fn vector_modify(&mut self, node_id: &NodeId, modification_type: VectorModificationType) { @@ -3010,13 +3166,6 @@ impl NodeNetworkInterface { /// Inserts a new export at insert index. If the insert index is -1 it is inserted at the end. The output_name is used by the encapsulating node. pub fn add_export(&mut self, default_value: TaggedValue, insert_index: isize, output_name: String, network_path: &[NodeId]) { - // Set the parent node (if it exists) to be a non layer if it is no longer eligible to be a layer - if let Some(parent_id) = network_path.last().cloned() { - if !self.is_eligible_to_be_layer(&parent_id, network_path) && self.is_layer(&parent_id, network_path) { - self.set_to_node_or_layer(&parent_id, network_path, false); - } - }; - let Some(network) = self.network_mut(network_path) else { log::error!("Could not get nested network in add_export"); return; @@ -3031,6 +3180,14 @@ impl NodeNetworkInterface { self.transaction_modified(); + let mut encapsulating_path = network_path.to_vec(); + // Set the parent node (if it exists) to be a non layer if it is no longer eligible to be a layer + if let Some(parent_id) = encapsulating_path.pop() { + if !self.is_eligible_to_be_layer(&parent_id, &encapsulating_path) && self.is_layer(&parent_id, &encapsulating_path) { + self.set_to_node_or_layer(&parent_id, &encapsulating_path, false); + } + }; + // There will not be an encapsulating node if the network is the document network if let Some(encapsulating_node_metadata) = self.encapsulating_node_metadata_mut(network_path) { if insert_index == -1 { @@ -3042,6 +3199,7 @@ impl NodeNetworkInterface { // Update the export ports and outward wires for the current network self.unload_import_export_ports(network_path); + self.unload_modify_import_export(network_path); self.unload_outward_wires(network_path); // Update the outward wires and bounding box for all nodes in the encapsulating network @@ -3062,17 +3220,22 @@ impl NodeNetworkInterface { } /// Inserts a new input at insert index. If the insert index is -1 it is inserted at the end. The output_name is used by the encapsulating node. - pub fn add_input(&mut self, node_id: &NodeId, network_path: &[NodeId], default_value: TaggedValue, exposed: bool, insert_index: isize, input_name: String) { + pub fn add_import(&mut self, default_value: TaggedValue, exposed: bool, insert_index: isize, input_name: String, network_path: &[NodeId]) { + let mut encapsulating_network_path = network_path.to_vec(); + let Some(node_id) = encapsulating_network_path.pop() else { + log::error!("Cannot add import for document network"); + return; + }; // Set the node to be a non layer if it is no longer eligible to be a layer - if !self.is_eligible_to_be_layer(node_id, network_path) && self.is_layer(node_id, network_path) { - self.set_to_node_or_layer(node_id, network_path, false); + if !self.is_eligible_to_be_layer(&node_id, &encapsulating_network_path) && self.is_layer(&node_id, &encapsulating_network_path) { + self.set_to_node_or_layer(&node_id, &encapsulating_network_path, false); } - let Some(network) = self.network_mut(network_path) else { + let Some(network) = self.network_mut(&encapsulating_network_path) else { log::error!("Could not get nested network in insert_input"); return; }; - let Some(node) = network.nodes.get_mut(node_id) else { + let Some(node) = network.nodes.get_mut(&node_id) else { log::error!("Could not get node in insert_input"); return; }; @@ -3086,7 +3249,7 @@ impl NodeNetworkInterface { self.transaction_modified(); - let Some(node_metadata) = self.node_metadata_mut(node_id, network_path) else { + let Some(node_metadata) = self.node_metadata_mut(&node_id, &encapsulating_network_path) else { log::error!("Could not get node_metadata in insert_input"); return; }; @@ -3103,14 +3266,18 @@ impl NodeNetworkInterface { } // Update the click targets for the node - self.unload_node_click_targets(node_id, network_path); + self.unload_node_click_targets(&node_id, &encapsulating_network_path); // Update the transient network metadata bounding box for all nodes and outward wires - self.unload_all_nodes_bounding_box(network_path); + self.unload_all_nodes_bounding_box(&encapsulating_network_path); + + // Unload the metadata for the nested network self.unload_outward_wires(network_path); + self.unload_import_export_ports(network_path); + self.unload_modify_import_export(network_path); // If the input is inserted as the first input, then it may have affected the document metadata structure - if network_path.is_empty() && (insert_index == 0 || insert_index == 1) { + if encapsulating_network_path.is_empty() && (insert_index == 0 || insert_index == 1) { self.load_structure(); } } @@ -3582,7 +3749,7 @@ impl NodeNetworkInterface { continue; } - for input_index in 0..self.number_of_inputs(delete_node_id, network_path) { + for input_index in 0..self.number_of_displayed_inputs(delete_node_id, network_path) { self.disconnect_input(&InputConnector::node(*delete_node_id, input_index), network_path); } @@ -3907,6 +4074,7 @@ impl NodeNetworkInterface { self.unload_upstream_node_click_targets(vec![*node_id], network_path); self.unload_all_nodes_bounding_box(network_path); self.unload_import_export_ports(network_path); + self.unload_modify_import_export(network_path); self.load_structure(); } @@ -4076,74 +4244,84 @@ impl NodeNetworkInterface { self.unload_all_nodes_bounding_box(network_path); } - /// Input connector is the input to the layer - pub fn try_set_upstream_to_chain(&mut self, input_connector: &InputConnector, network_path: &[NodeId]) { - // If the new input is to a non layer node on the same y position as the input connector, or the input connector is the side input of a layer, then set it to chain position - if let InputConnector::Node { + fn valid_upstream_chain_nodes(&mut self, input_connector: &InputConnector, network_path: &[NodeId]) -> Vec { + let InputConnector::Node { node_id: input_connector_node_id, input_index, } = input_connector - { - let mut set_position_to_chain = false; - if self.is_layer(input_connector_node_id, network_path) && *input_index == 1 || self.is_chain(input_connector_node_id, network_path) && *input_index == 0 { - let mut downstream_id = *input_connector_node_id; - for upstream_node in self - .upstream_flow_back_from_nodes(vec![*input_connector_node_id], network_path, FlowType::HorizontalFlow) - .skip(1) - .collect::>() - { - if self.is_layer(&upstream_node, network_path) { - break; - } - if !self.has_primary_output(&upstream_node, network_path) { - break; - } - let Some(outward_wires) = self.outward_wires(network_path).and_then(|outward_wires| outward_wires.get(&OutputConnector::node(upstream_node, 0))) else { - log::error!("Could not get outward wires in try_set_upstream_to_chain"); - break; - }; - if outward_wires.len() != 1 { - break; - } - let downstream_position = self.position(&downstream_id, network_path); - let upstream_node_position = self.position(&upstream_node, network_path); - if let (Some(input_connector_position), Some(new_upstream_node_position)) = (downstream_position, upstream_node_position) { - if input_connector_position.y == new_upstream_node_position.y - && new_upstream_node_position.x >= input_connector_position.x - 9 - && new_upstream_node_position.x <= input_connector_position.x - { - set_position_to_chain = true; - self.set_chain_position(&upstream_node, network_path); - } else { - break; - } + else { + return Vec::new(); + }; + let mut set_position_to_chain = Vec::new(); + if self.is_layer(input_connector_node_id, network_path) && *input_index == 1 || self.is_chain(input_connector_node_id, network_path) && *input_index == 0 { + let mut downstream_id = *input_connector_node_id; + for upstream_node in self + .upstream_flow_back_from_nodes(vec![*input_connector_node_id], network_path, FlowType::HorizontalFlow) + .skip(1) + .collect::>() + { + if self.is_layer(&upstream_node, network_path) { + break; + } + if !self.has_primary_output(&upstream_node, network_path) { + break; + } + let Some(outward_wires) = self.outward_wires(network_path).and_then(|outward_wires| outward_wires.get(&OutputConnector::node(upstream_node, 0))) else { + log::error!("Could not get outward wires in try_set_upstream_to_chain"); + break; + }; + if outward_wires.len() != 1 { + break; + } + let downstream_position = self.position(&downstream_id, network_path); + let upstream_node_position = self.position(&upstream_node, network_path); + if let (Some(input_connector_position), Some(new_upstream_node_position)) = (downstream_position, upstream_node_position) { + if input_connector_position.y == new_upstream_node_position.y + && new_upstream_node_position.x >= input_connector_position.x - 9 + && new_upstream_node_position.x <= input_connector_position.x + { + set_position_to_chain.push(upstream_node); } else { break; } - downstream_id = upstream_node; + } else { + break; } + downstream_id = upstream_node; } - // Reload click target of the layer which used to encapsulate the node - if set_position_to_chain { - let mut downstream_layer = Some(*input_connector_node_id); - while let Some(downstream_layer_id) = downstream_layer { - if downstream_layer_id == *input_connector_node_id || !self.is_layer(&downstream_layer_id, network_path) { - let Some(outward_wires) = self.outward_wires(network_path) else { - log::error!("Could not get outward wires in try_set_upstream_to_chain"); - downstream_layer = None; - break; - }; - downstream_layer = outward_wires - .get(&OutputConnector::node(downstream_layer_id, 0)) - .and_then(|outward_wires| if outward_wires.len() == 1 { outward_wires[0].node_id() } else { None }); - } else { + } + set_position_to_chain + } + + /// Input connector is the input to the layer + pub fn try_set_upstream_to_chain(&mut self, input_connector: &InputConnector, network_path: &[NodeId]) { + // If the new input is to a non layer node on the same y position as the input connector, or the input connector is the side input of a layer, then set it to chain position + + let valid_upstream_chain_nodes = self.valid_upstream_chain_nodes(input_connector, network_path); + + for node_id in &valid_upstream_chain_nodes { + self.set_chain_position(node_id, network_path); + } + // Reload click target of the layer which used to encapsulate the node + if !valid_upstream_chain_nodes.is_empty() { + let mut downstream_layer = Some(input_connector.node_id().unwrap()); + while let Some(downstream_layer_id) = downstream_layer { + if downstream_layer_id == input_connector.node_id().unwrap() || !self.is_layer(&downstream_layer_id, network_path) { + let Some(outward_wires) = self.outward_wires(network_path) else { + log::error!("Could not get outward wires in try_set_upstream_to_chain"); + downstream_layer = None; break; - } - } - if let Some(downstream_layer) = downstream_layer { - self.unload_node_click_targets(&downstream_layer, network_path); + }; + downstream_layer = outward_wires + .get(&OutputConnector::node(downstream_layer_id, 0)) + .and_then(|outward_wires| if outward_wires.len() == 1 { outward_wires[0].node_id() } else { None }); + } else { + break; } } + if let Some(downstream_layer) = downstream_layer { + self.unload_node_click_targets(&downstream_layer, network_path); + } } } @@ -4200,6 +4378,21 @@ impl NodeNetworkInterface { } } + pub fn nodes_sorted_top_to_bottom<'a>(&mut self, node_ids: impl Iterator, network_path: &[NodeId]) -> Option> { + let mut node_ids_with_position = node_ids + .filter_map(|&node_id| { + let Some(position) = self.position(&node_id, network_path) else { + log::error!("Could not get position for node {node_id} in shift_selected_nodes"); + return None; + }; + Some((node_id, position.y)) + }) + .collect::>(); + + node_ids_with_position.sort_unstable_by(|a, b| a.1.cmp(&b.1)); + Some(node_ids_with_position.into_iter().map(|(node_id, _)| node_id).collect::>()) + } + /// Used when moving layer by the layer panel, does not run any pushing logic. Moves all sole dependents of the layer as well. /// Ensure that the layer is absolute position. pub fn shift_absolute_node_position(&mut self, layer: &NodeId, shift: IVec2, network_path: &[NodeId]) { @@ -4287,25 +4480,16 @@ impl NodeNetworkInterface { } } - let mut node_ids_with_position = node_ids - .iter() - .filter_map(|&node_id| { - let Some(position) = self.position(&node_id, network_path) else { - log::error!("Could not get position for node {node_id} in shift_selected_nodes"); - return None; - }; - Some((node_id, position.y)) - }) - .collect::>(); + let Some(mut sorted_node_ids) = self.nodes_sorted_top_to_bottom(node_ids.iter(), network_path) else { + return; + }; - if node_ids_with_position.len() != node_ids.len() { + if sorted_node_ids.len() != node_ids.len() { log::error!("Could not get position for all nodes in shift_selected_nodes"); return; } - node_ids_with_position.sort_unstable_by(|a, b| a.1.cmp(&b.1)); // If shifting down, then the lowest node (greatest y value) should be shifted first - let mut sorted_node_ids = node_ids_with_position.into_iter().map(|(node_id, _)| node_id).collect::>(); if direction == Direction::Down { sorted_node_ids.reverse(); } @@ -4884,7 +5068,7 @@ impl NodeNetworkInterface { // Insert a node onto a wire. Ensure insert_node_input_index is an exposed input pub fn insert_node_between(&mut self, node_id: &NodeId, input_connector: &InputConnector, insert_node_input_index: usize, network_path: &[NodeId]) { - if self.number_of_inputs(node_id, network_path) == 0 { + if self.number_of_displayed_inputs(node_id, network_path) == 0 { log::error!("Cannot insert a node onto a wire with no exposed inputs"); return; } @@ -4954,6 +5138,8 @@ pub enum FlowType { PrimaryFlow, /// Iterate over the secondary input (inclusive) for layer nodes and primary input for non layer nodes. HorizontalFlow, + /// Same as horizontal flow, but only iterates over connections to primary outputs + HorizontalPrimaryOutputFlow, /// Upstream flow starting from the either the node (inclusive) or secondary input of the layer (not inclusive). LayerChildrenUpstreamFlow, } @@ -4976,7 +5162,7 @@ impl<'a> Iterator for FlowIter<'a> { let node_id = self.stack.pop()?; if let (Some(document_node), Some(node_metadata)) = (self.network.nodes.get(&node_id), self.network_metadata.persistent_metadata.node_metadata.get(&node_id)) { - let skip = if self.flow_type == FlowType::HorizontalFlow && node_metadata.persistent_metadata.is_layer() { + let skip = if matches!(self.flow_type, FlowType::HorizontalFlow | FlowType::HorizontalPrimaryOutputFlow) && node_metadata.persistent_metadata.is_layer() { 1 } else { 0 @@ -4984,7 +5170,11 @@ impl<'a> Iterator for FlowIter<'a> { let take = if self.flow_type == FlowType::UpstreamFlow { usize::MAX } else { 1 }; let inputs = document_node.inputs.iter().skip(skip).take(take); - let node_ids = inputs.filter_map(|input| if let NodeInput::Node { node_id, .. } = input { Some(node_id) } else { None }); + let node_ids = inputs.filter_map(|input| match input { + NodeInput::Node { output_index, .. } if self.flow_type == FlowType::HorizontalPrimaryOutputFlow && *output_index != 0 => None, + NodeInput::Node { node_id, .. } => Some(node_id), + _ => None, + }); self.stack.extend(node_ids); @@ -5311,10 +5501,25 @@ pub struct NodeNetworkTransientMetadata { // pub wire_paths: Vec /// All export connector click targets pub import_export_ports: TransientMetadata, + /// Click targets for adding, removing, and moving import/export ports + pub modify_import_export: TransientMetadata, // Distance to the edges of the network, where the import/export ports are displayed. Rounded to nearest grid space when the panning ends. pub rounded_network_edge_distance: TransientMetadata, } +#[derive(Debug, Clone)] +pub struct ModifyImportExportClickTarget { + // Plus icon that appears below all imports/exports + pub add_import: ClickTarget, + pub add_export: ClickTarget, + // Subtract icon that appears when hovering over an import/export + pub remove_imports: Vec, + pub remove_exports: Vec, + // Grip drag icon that appears when hovering over an import/export + pub move_import: Vec, + pub move_export: Vec, +} + #[derive(Debug, Clone)] pub struct NetworkEdgeDistance { /// The viewport pixel distance between the left edge of the node graph and the exports. @@ -5368,7 +5573,8 @@ pub struct DocumentNodePersistentMetadata { /// A name chosen by the user for this instance of the node. Empty indicates no given name, in which case the reference name is displayed to the user in italics. #[serde(default)] pub display_name: String, - /// TODO: Should input/output names always be the same length as the inputs/outputs of the DocumentNode? + /// Input/Output names may not be the same length as the number of inputs/outputs. They are the same as the nested networks Imports/Exports. + /// If the string is empty/DNE, then it uses the type. pub input_names: Vec, pub output_names: Vec, /// Indicates to the UI if a primary output should be drawn for this node. diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 484036e2b0..c658fb1de4 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -465,17 +465,7 @@ impl MessageHandler> for PortfolioMes let Some(ref reference) = node_metadata.persistent_metadata.reference.clone() else { continue; }; - if let Some(node_definition) = resolve_document_node_type(reference) { - let document_node = node_definition.default_node_template().document_node; - document.network_interface.set_manual_compostion(node_id, &[], document_node.manual_composition); - // if ["Fill", "Stroke", "Splines from Points", "Sample Subpaths", "Sample Points", "Copy to Points", "Path", "Scatter Points"].contains(&reference.as_str()) { - // document.network_interface.set_implementation(node_id, &[], document_node.implementation); - // } - document.network_interface.replace_implementation(node_id, &[], document_node.implementation); - document - .network_interface - .replace_implementation_metadata(node_id, &[], node_definition.default_node_template().persistent_node_metadata); - } + let Some(node) = document.network_interface.network(&[]).unwrap().nodes.get(node_id) else { log::error!("could not get node in deserialize_document"); continue; @@ -597,7 +587,7 @@ impl MessageHandler> for PortfolioMes // Upgrade artboard name being passed as hidden value input to "To Artboard" if reference == "Artboard" { - let label = document.network_interface.display_name(node_id, &[]); + let label = document.network_interface.frontend_display_name(node_id, &[]); document .network_interface .set_input(&InputConnector::node(NodeId(0), 1), NodeInput::value(TaggedValue::String(label), false), &[*node_id]); diff --git a/frontend/src/components/views/Graph.svelte b/frontend/src/components/views/Graph.svelte index fec5097af8..bfa6592113 100644 --- a/frontend/src/components/views/Graph.svelte +++ b/frontend/src/components/views/Graph.svelte @@ -13,8 +13,10 @@ import LayoutCol from "@graphite/components/layout/LayoutCol.svelte"; import LayoutRow from "@graphite/components/layout/LayoutRow.svelte"; import IconButton from "@graphite/components/widgets/buttons/IconButton.svelte"; + import TextButton from "@graphite/components/widgets/buttons/TextButton.svelte"; import RadioInput from "@graphite/components/widgets/inputs/RadioInput.svelte"; import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte"; + import Separator from "@graphite/components/widgets/labels/Separator.svelte"; import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte"; const GRID_COLLAPSE_SPACING = 10; const GRID_SIZE = 24; @@ -309,6 +311,15 @@ }); return connectedNode?.isLayer || false; } + + function zipWithUndefined(arr1: FrontendGraphInput[], arr2: FrontendGraphOutput[]) { + const maxLength = Math.max(arr1.length, arr2.length); + const result = []; + for (let i = 0; i < maxLength; i++) { + result.push([arr1[i], arr2[i]]); + } + return result; + }
+ + + editor.handle.mergeSelectedNodes()} /> + {/if} {/if} @@ -424,6 +439,19 @@

{outputMetadata.name}

{/each} + {#if $nodeGraph.addImport !== undefined} +
+ { + /* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */ + }} + /> +
+ {/if} {#each $nodeGraph.exports as { inputMetadata, position }, index}

{inputMetadata.name}

{/each} + {#if $nodeGraph.addExport !== undefined} +
+ { + /* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */ + }} + /> +
+ {/if}
@@ -603,7 +644,7 @@ {#each Array.from($nodeGraph.nodes.values()).flatMap((node, nodeIndex) => (node.isLayer ? [] : [{ node, nodeIndex }])) as { node, nodeIndex } (nodeIndex)} - {@const exposedInputsOutputs = [...node.exposedInputs, ...node.exposedOutputs]} + {@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 exposedInputsOutputs.length > 0}
- {#each exposedInputsOutputs as secondary, index} -
- {secondary.name} + {#each exposedInputsOutputs as [input, output]} +
+ + {input !== undefined ? input.name : output.name} +
{/each}
@@ -796,6 +839,10 @@ line-height: 24px; margin-right: 8px; } + + .merge-selected-nodes { + justify-content: center; + } } .click-targets { @@ -869,6 +916,14 @@ left: calc(var(--offset-left) * 24px); } + .plus { + margin-top: -4px; + margin-left: -4px; + position: absolute; + top: calc(var(--offset-top) * 24px); + left: calc(var(--offset-left) * 24px); + } + .export-text { position: absolute; margin-top: 0; diff --git a/frontend/src/components/widgets/WidgetSpan.svelte b/frontend/src/components/widgets/WidgetSpan.svelte index 8e4c9b663f..baa3a94b20 100644 --- a/frontend/src/components/widgets/WidgetSpan.svelte +++ b/frontend/src/components/widgets/WidgetSpan.svelte @@ -174,7 +174,7 @@ {/if} {@const breadcrumbTrailButtons = narrowWidgetProps(component.props, "BreadcrumbTrailButtons")} {#if breadcrumbTrailButtons} - widgetValueCommitAndUpdate(index, index)} /> + widgetValueCommitAndUpdate(index, breadcrumbIndex)} /> {/if} {@const textInput = narrowWidgetProps(component.props, "TextInput")} {#if textInput} diff --git a/frontend/src/state-providers/node-graph.ts b/frontend/src/state-providers/node-graph.ts index 2366a30fee..faafc31662 100644 --- a/frontend/src/state-providers/node-graph.ts +++ b/frontend/src/state-providers/node-graph.ts @@ -36,6 +36,8 @@ export function createNodeGraphState(editor: Editor) { hasLeftInputWire: new Map(), imports: [] as { outputMetadata: FrontendGraphOutput; position: { x: number; y: number } }[], exports: [] as { inputMetadata: FrontendGraphInput; position: { x: number; y: number } }[], + addImport: undefined as { x: number; y: number } | undefined, + addExport: undefined as { x: number; y: number } | undefined, nodes: new Map(), wires: [] as FrontendNodeWire[], wirePathInProgress: undefined as WirePath | undefined, @@ -80,6 +82,8 @@ export function createNodeGraphState(editor: Editor) { update((state) => { state.imports = updateImportsExports.imports; state.exports = updateImportsExports.exports; + state.addImport = updateImportsExports.addImport; + state.addExport = updateImportsExports.addExport; return state; }); }); diff --git a/frontend/src/wasm-communication/messages.ts b/frontend/src/wasm-communication/messages.ts index 3b94dabb23..c6fdf9167d 100644 --- a/frontend/src/wasm-communication/messages.ts +++ b/frontend/src/wasm-communication/messages.ts @@ -61,6 +61,12 @@ export class UpdateImportsExports extends JsMessage { @ExportsToVec2Array readonly exports!: { inputMetadata: FrontendGraphInput; position: XY }[]; + + @TupleToVec2 + readonly addImport!: XY | undefined; + + @TupleToVec2 + readonly addExport!: XY | undefined; } export class UpdateInSelectedNetwork extends JsMessage { diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index 2742d22fab..bb7f976aab 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -569,6 +569,13 @@ impl EditorHandle { self.dispatch(message); } + /// Merge a group of nodes into a subnetwork + #[wasm_bindgen(js_name = mergeSelectedNodes)] + pub fn merge_nodes(&self) { + let message = NodeGraphMessage::MergeSelectedNodes; + self.dispatch(message); + } + /// Creates a new document node in the node graph #[wasm_bindgen(js_name = createNode)] pub fn create_node(&self, node_type: String, x: i32, y: i32) { @@ -763,9 +770,7 @@ impl EditorHandle { document .network_interface .replace_implementation(&node_id, &[], DocumentNodeImplementation::proto("graphene_core::ToArtboardNode")); - document - .network_interface - .add_input(&node_id, &[], TaggedValue::IVec2(glam::IVec2::default()), false, 2, "".to_string()); + document.network_interface.add_import(TaggedValue::IVec2(glam::IVec2::default()), false, 2, "".to_string(), &[node_id]); } } }