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 <keavon@keavon.com>
This commit is contained in:
Adam Gerhant 2024-11-12 14:27:42 -08:00 committed by GitHub
parent 4c4d559d97
commit 4250f291ab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 735 additions and 238 deletions

View file

@ -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")]

View file

@ -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 }),

View file

@ -326,7 +326,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> 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<DocumentMessage, DocumentMessageData<'_>> 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<DocumentMessage, DocumentMessageData<'_>> 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,

View file

@ -64,6 +64,23 @@ static DOCUMENT_NODE_TYPES: once_cell::sync::Lazy<Vec<DocumentNodeDefinition>> =
/// The [`DocumentNode`] is the instance while these [`DocumentNodeDefinition`]s are the "classes" or "blueprints" from which the instances are built.
fn static_nodes() -> Vec<DocumentNodeDefinition> {
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<DocumentNodeDefinition> {
..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<DocumentNodeDefinition> {
..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<DocumentNodeDefinition> {
..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<DocumentNodeDefinition> {
..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<DocumentNodeDefinition> {
..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<DocumentNodeDefinition> {
..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.

View file

@ -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<NodeId, NodeId>,
},
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,
},

View file

@ -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<NodeGraphMessage, NodeGraphHandlerData<'a>> 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<NodeGraphMessage, NodeGraphHandlerData<'a>> 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::<HashMap<NodeId, NodeId>>();
let copied_nodes = network_interface.copy_nodes(&new_ids, breadcrumb_network_path).collect::<Vec<_>>();
let selected_node_ids = copied_nodes.iter().map(|(node_id, _)| *node_id).collect::<HashSet<_>>();
let selected_node_ids_vec = copied_nodes.iter().map(|(node_id, _)| *node_id).collect::<Vec<_>>();
// 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(&current_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(&current_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<NodeGraphMessage, NodeGraphHandlerData<'a>> 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<NodeGraphMessage, NodeGraphHandlerData<'a>> 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<NodeGraphMessage, NodeGraphHandlerData<'a>> 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<NodeGraphMessage, NodeGraphHandlerData<'a>> 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<NodeGraphMessage, NodeGraphHandlerData<'a>> 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<NodeGraphMessage, NodeGraphHandlerData<'a>> 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);

View file

@ -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::<Vec<_>>()
{
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<NodeId, NodeId>, network_path: &'a [NodeId]) -> impl Iterator<Item = (NodeId, NodeTemplate)> + '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::<Vec<_>>()
.into_iter()
.map(move |(new, node_id, node)| (new, self.map_ids(node, node_id, new_ids, network_path)))
.collect::<Vec<_>>();
for old_id in new_nodes.iter().map(|(_, old_id, _)| *old_id).collect::<Vec<_>>() {
// 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<u32> {
let mut node_height: Option<u32> = 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<String> {
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,73 +4244,83 @@ 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<NodeId> {
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::<Vec<_>>()
{
if self.is_layer(&upstream_node, network_path) {
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::<Vec<_>>()
{
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;
}
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 {
} else {
break;
}
downstream_id = upstream_node;
}
}
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 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 {
break;
}
downstream_id = upstream_node;
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;
}
}
// 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 {
break;
}
}
if let Some(downstream_layer) = downstream_layer {
self.unload_node_click_targets(&downstream_layer, network_path);
}
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<Item = &'a NodeId>, network_path: &[NodeId]) -> Option<Vec<NodeId>> {
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::<Vec<(NodeId, i32)>>();
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::<Vec<_>>())
}
/// 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::<Vec<(NodeId, i32)>>();
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::<Vec<_>>();
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<WirePath>
/// All export connector click targets
pub import_export_ports: TransientMetadata<Ports>,
/// Click targets for adding, removing, and moving import/export ports
pub modify_import_export: TransientMetadata<ModifyImportExportClickTarget>,
// 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<NetworkEdgeDistance>,
}
#[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<ClickTarget>,
pub remove_exports: Vec<ClickTarget>,
// Grip drag icon that appears when hovering over an import/export
pub move_import: Vec<ClickTarget>,
pub move_export: Vec<ClickTarget>,
}
#[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<String>,
pub output_names: Vec<String>,
/// Indicates to the UI if a primary output should be drawn for this node.

View file

@ -465,17 +465,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> 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<PortfolioMessage, PortfolioMessageData<'_>> 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]);

View file

@ -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;
}
</script>
<div
@ -357,6 +368,10 @@
disabled={!canBeToggledBetweenNodeAndLayer(contextMenuData.nodeId)}
/>
</LayoutRow>
<Separator type="Section" direction="Vertical" />
<LayoutRow class="merge-selected-nodes">
<TextButton label="Merge Selected Nodes" action={() => editor.handle.mergeSelectedNodes()} />
</LayoutRow>
{/if}
</LayoutCol>
{/if}
@ -424,6 +439,19 @@
</svg>
<p class="import-text" style:--offset-left={position.x / 24} style:--offset-top={position.y / 24}>{outputMetadata.name}</p>
{/each}
{#if $nodeGraph.addImport !== undefined}
<div class="plus" style:--offset-left={$nodeGraph.addImport.x / 24} style:--offset-top={$nodeGraph.addImport.y / 24}>
<IconButton
class={"visibility"}
data-visibility-button
size={24}
icon={"Add"}
action={() => {
/* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */
}}
/>
</div>
{/if}
{#each $nodeGraph.exports as { inputMetadata, position }, index}
<svg
xmlns="http://www.w3.org/2000/svg"
@ -446,6 +474,19 @@
</svg>
<p class="export-text" style:--offset-left={position.x / 24} style:--offset-top={position.y / 24}>{inputMetadata.name}</p>
{/each}
{#if $nodeGraph.addExport !== undefined}
<div class="plus" style:--offset-left={$nodeGraph.addExport.x / 24} style:--offset-top={$nodeGraph.addExport.y / 24}>
<IconButton
class={"visibility"}
data-visibility-button
size={24}
icon={"Add"}
action={() => {
/* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */
}}
/>
</div>
{/if}
</div>
<!-- Layers and nodes -->
@ -603,7 +644,7 @@
<!-- Nodes -->
{#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}
<div
@ -633,9 +674,11 @@
<!-- Secondary rows -->
{#if exposedInputsOutputs.length > 0}
<div class="secondary" class:in-selected-network={$nodeGraph.inSelectedNetwork}>
{#each exposedInputsOutputs as secondary, index}
<div class={`secondary-row expanded ${index < node.exposedInputs.length ? "input" : "output"}`}>
<TextLabel tooltip={secondary.name}>{secondary.name}</TextLabel>
{#each exposedInputsOutputs as [input, output]}
<div class={`secondary-row expanded ${input !== undefined ? "input" : "output"}`}>
<TextLabel tooltip={input !== undefined ? input.name : output.name}>
{input !== undefined ? input.name : output.name}
</TextLabel>
</div>
{/each}
</div>
@ -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;

View file

@ -174,7 +174,7 @@
{/if}
{@const breadcrumbTrailButtons = narrowWidgetProps(component.props, "BreadcrumbTrailButtons")}
{#if breadcrumbTrailButtons}
<BreadcrumbTrailButtons {...exclude(breadcrumbTrailButtons)} action={(index) => widgetValueCommitAndUpdate(index, index)} />
<BreadcrumbTrailButtons {...exclude(breadcrumbTrailButtons)} action={(breadcrumbIndex) => widgetValueCommitAndUpdate(index, breadcrumbIndex)} />
{/if}
{@const textInput = narrowWidgetProps(component.props, "TextInput")}
{#if textInput}

View file

@ -36,6 +36,8 @@ export function createNodeGraphState(editor: Editor) {
hasLeftInputWire: new Map<bigint, boolean>(),
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<bigint, FrontendNode>(),
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;
});
});

View file

@ -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 {

View file

@ -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]);
}
}
}