mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-12-23 10:11:54 +00:00
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:
parent
4c4d559d97
commit
4250f291ab
13 changed files with 735 additions and 238 deletions
|
|
@ -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")]
|
||||
|
|
|
|||
|
|
@ -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 }),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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(¤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<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);
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue