Add support for adding/removing a graph's primary imports/exports separately from the secondary (#3089)

* Primary Import/Export

* Primary Import/Export

* Complete import exports

* Final changes

* Improve line width

* Code review

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Adam Gerhant
2025-08-29 20:57:15 +00:00
committed by GitHub
co-authored by Keavon Chambers
parent ce1cd1706b
commit f1a1275601
26 changed files with 1075 additions and 1064 deletions
+4
View File
@@ -33,6 +33,10 @@
}, },
// Rust Analyzer config // Rust Analyzer config
"rust-analyzer.cargo.allTargets": false, "rust-analyzer.cargo.allTargets": false,
"rust-analyzer.procMacro.ignored": {
"serde_derive": ["Serialize", "Deserialize"],
"specta_macros": ["Type"] // Disabled because of: https://github.com/specta-rs/specta/issues/387
},
// ESLint config // ESLint config
"eslint.format.enable": true, "eslint.format.enable": true,
"eslint.workingDirectories": ["./frontend", "./website"], "eslint.workingDirectories": ["./frontend", "./website"],
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -8,6 +8,7 @@ use crate::messages::portfolio::document::utility_types::nodes::{JsRawBuffer, La
use crate::messages::portfolio::document::utility_types::wires::{WirePath, WirePathUpdate}; use crate::messages::portfolio::document::utility_types::wires::{WirePath, WirePathUpdate};
use crate::messages::prelude::*; use crate::messages::prelude::*;
use crate::messages::tool::utility_types::HintData; use crate::messages::tool::utility_types::HintData;
use glam::IVec2;
use graph_craft::document::NodeId; use graph_craft::document::NodeId;
use graphene_std::raster::Image; use graphene_std::raster::Image;
use graphene_std::raster::color::Color; use graphene_std::raster::color::Color;
@@ -124,12 +125,19 @@ pub enum FrontendMessage {
document_id: DocumentId, document_id: DocumentId,
}, },
UpdateImportsExports { UpdateImportsExports {
imports: Vec<(FrontendGraphOutput, i32, i32)>, /// If the primary import is not visible, then it is None.
exports: Vec<(FrontendGraphInput, i32, i32)>, imports: Vec<Option<FrontendGraphOutput>>,
#[serde(rename = "addImport")] /// If the primary export is not visible, then it is None.
add_import: Option<(i32, i32)>, exports: Vec<Option<FrontendGraphInput>>,
#[serde(rename = "addExport")] /// The primary import location.
add_export: Option<(i32, i32)>, #[serde(rename = "importPosition")]
import_position: IVec2,
/// The primary export location.
#[serde(rename = "exportPosition")]
export_position: IVec2,
/// The document network does not have an add import or export button.
#[serde(rename = "addImportExport")]
add_import_export: bool,
}, },
UpdateInSelectedNetwork { UpdateInSelectedNetwork {
#[serde(rename = "inSelectedNetwork")] #[serde(rename = "inSelectedNetwork")]
@@ -17,7 +17,7 @@ use crate::messages::portfolio::document::overlays::utility_types::{OverlaysType
use crate::messages::portfolio::document::properties_panel::properties_panel_message_handler::PropertiesPanelMessageContext; use crate::messages::portfolio::document::properties_panel::properties_panel_message_handler::PropertiesPanelMessageContext;
use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier}; use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, DocumentMode, FlipAxis, PTZ}; use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, DocumentMode, FlipAxis, PTZ};
use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, InputConnector, NodeTemplate}; use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, InputConnector, NodeTemplate, OutputConnector};
use crate::messages::portfolio::document::utility_types::nodes::RawBuffer; use crate::messages::portfolio::document::utility_types::nodes::RawBuffer;
use crate::messages::portfolio::utility_types::PanelType; use crate::messages::portfolio::utility_types::PanelType;
use crate::messages::portfolio::utility_types::PersistentData; use crate::messages::portfolio::utility_types::PersistentData;
@@ -2766,7 +2766,7 @@ impl DocumentMessageHandler {
}); });
} }
pub fn update_layers_panel_bottom_bar_widgets(&self, layers_panel_open: bool, responses: &mut VecDeque<Message>) { pub fn update_layers_panel_bottom_bar_widgets(&mut self, layers_panel_open: bool, responses: &mut VecDeque<Message>) {
if !layers_panel_open { if !layers_panel_open {
return; return;
} }
@@ -2776,6 +2776,7 @@ impl DocumentMessageHandler {
let selected_layer = selected_layers.next(); let selected_layer = selected_layers.next();
let has_selection = selected_layer.is_some(); let has_selection = selected_layer.is_some();
let has_multiple_selection = selected_layers.next().is_some(); let has_multiple_selection = selected_layers.next().is_some();
for _ in selected_layers {}
let widgets = vec![ let widgets = vec![
PopoverButton::new() PopoverButton::new()
@@ -2789,7 +2790,7 @@ impl DocumentMessageHandler {
let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, &self.network_interface); let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, &self.network_interface);
let node_type = graph_layer.horizontal_layer_flow().nth(1); let node_type = graph_layer.horizontal_layer_flow().nth(1);
if let Some(node_id) = node_type { if let Some(node_id) = node_type {
let (output_type, _) = self.network_interface.output_type(&node_id, 0, &self.selection_network_path); let (output_type, _) = self.network_interface.output_type(&OutputConnector::node(node_id, 0), &self.selection_network_path);
Some(format!("type:{}", output_type.nested_type())) Some(format!("type:{}", output_type.nested_type()))
} else { } else {
None None
@@ -382,7 +382,6 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
inputs: vec![ inputs: vec![
NodeInput::network(graphene_std::Type::Fn(Box::new(concrete!(Context)), Box::new(concrete!(Table<Artboard>))), 0), NodeInput::network(graphene_std::Type::Fn(Box::new(concrete!(Context)), Box::new(concrete!(Table<Artboard>))), 0),
NodeInput::node(NodeId(2), 0), NodeInput::node(NodeId(2), 0),
NodeInput::Reflection(graph_craft::document::DocumentNodeMetadata::DocumentNodePath),
], ],
implementation: DocumentNodeImplementation::ProtoNode(graphic::extend::IDENTIFIER), implementation: DocumentNodeImplementation::ProtoNode(graphic::extend::IDENTIFIER),
..Default::default() ..Default::default()
@@ -771,6 +770,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
document_node: DocumentNode { document_node: DocumentNode {
implementation: DocumentNodeImplementation::Network(NodeNetwork { implementation: DocumentNodeImplementation::Network(NodeNetwork {
exports: vec![ exports: vec![
NodeInput::value(TaggedValue::None, false),
NodeInput::node(NodeId(0), 0), NodeInput::node(NodeId(0), 0),
NodeInput::node(NodeId(1), 0), NodeInput::node(NodeId(1), 0),
NodeInput::node(NodeId(2), 0), NodeInput::node(NodeId(2), 0),
@@ -825,8 +825,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
}, },
persistent_node_metadata: DocumentNodePersistentMetadata { persistent_node_metadata: DocumentNodePersistentMetadata {
input_metadata: vec![("Image", "TODO").into()], input_metadata: vec![("Image", "TODO").into()],
output_names: vec!["Red".to_string(), "Green".to_string(), "Blue".to_string(), "Alpha".to_string()], output_names: vec!["".to_string(), "Red".to_string(), "Green".to_string(), "Blue".to_string(), "Alpha".to_string()],
has_primary_output: false,
network_metadata: Some(NodeNetworkMetadata { network_metadata: Some(NodeNetworkMetadata {
persistent_metadata: NodeNetworkPersistentMetadata { persistent_metadata: NodeNetworkPersistentMetadata {
node_metadata: [ node_metadata: [
@@ -883,7 +882,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
node_template: NodeTemplate { node_template: NodeTemplate {
document_node: DocumentNode { document_node: DocumentNode {
implementation: DocumentNodeImplementation::Network(NodeNetwork { implementation: DocumentNodeImplementation::Network(NodeNetwork {
exports: vec![NodeInput::node(NodeId(0), 0), NodeInput::node(NodeId(1), 0)], exports: vec![NodeInput::value(TaggedValue::None, false), NodeInput::node(NodeId(0), 0), NodeInput::node(NodeId(1), 0)],
nodes: [ nodes: [
DocumentNode { DocumentNode {
inputs: vec![NodeInput::network(concrete!(Table<Raster<CPU>>), 0), NodeInput::value(TaggedValue::XY(XY::X), false)], inputs: vec![NodeInput::network(concrete!(Table<Raster<CPU>>), 0), NodeInput::value(TaggedValue::XY(XY::X), false)],
@@ -905,13 +904,12 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default() ..Default::default()
}), }),
inputs: vec![NodeInput::value(TaggedValue::Raster(Default::default()), true)], inputs: vec![NodeInput::value(TaggedValue::DVec2(DVec2::ZERO), true)],
..Default::default() ..Default::default()
}, },
persistent_node_metadata: DocumentNodePersistentMetadata { persistent_node_metadata: DocumentNodePersistentMetadata {
input_metadata: vec![("Vec2", "TODO").into()], input_metadata: vec![("Vec2", "TODO").into()],
output_names: vec!["X".to_string(), "Y".to_string()], output_names: vec!["".to_string(), "X".to_string(), "Y".to_string()],
has_primary_output: false,
network_metadata: Some(NodeNetworkMetadata { network_metadata: Some(NodeNetworkMetadata {
persistent_metadata: NodeNetworkPersistentMetadata { persistent_metadata: NodeNetworkPersistentMetadata {
node_metadata: [ node_metadata: [
@@ -77,7 +77,6 @@ pub(super) fn post_process_nodes(mut custom: Vec<DocumentNodeDefinition>) -> Vec
}) })
.collect(), .collect(),
output_names: vec![output_type.to_string()], output_names: vec![output_type.to_string()],
has_primary_output: true,
locked: false, locked: false,
..Default::default() ..Default::default()
}, },
@@ -18,7 +18,11 @@ pub enum NodeGraphMessage {
}, },
AddPathNode, AddPathNode,
AddImport, AddImport,
AddPrimaryImport,
AddSecondaryImport,
AddExport, AddExport,
AddPrimaryExport,
AddSecondaryExport,
Init, Init,
SelectedNodesUpdated, SelectedNodesUpdated,
Copy, Copy,
@@ -63,6 +67,12 @@ pub enum NodeGraphMessage {
set_to_exposed: bool, set_to_exposed: bool,
start_transaction: bool, start_transaction: bool,
}, },
ExposeEncapsulatingPrimaryInput {
exposed: bool,
},
ExposePrimaryExport {
exposed: bool,
},
InsertNode { InsertNode {
node_id: NodeId, node_id: NodeId,
node_template: NodeTemplate, node_template: NodeTemplate,
@@ -1,4 +1,4 @@
use super::utility_types::{BoxSelection, ContextMenuInformation, DragStart, FrontendGraphInput, FrontendGraphOutput, FrontendNode}; use super::utility_types::{BoxSelection, ContextMenuInformation, DragStart, FrontendNode};
use super::{document_node_definitions, node_properties}; use super::{document_node_definitions, node_properties};
use crate::consts::GRID_SIZE; use crate::consts::GRID_SIZE;
use crate::messages::input_mapper::utility_types::macros::action_keys; use crate::messages::input_mapper::utility_types::macros::action_keys;
@@ -127,7 +127,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![new_layer_id] }); responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![new_layer_id] });
} }
NodeGraphMessage::AddPathNode => { NodeGraphMessage::AddPathNode => {
if let Some(layer) = make_path_editable_is_allowed(network_interface, network_interface.document_metadata()) { if let Some(layer) = make_path_editable_is_allowed(network_interface) {
responses.add(NodeGraphMessage::CreateNodeInLayerWithTransaction { node_type: "Path".to_string(), layer }); responses.add(NodeGraphMessage::CreateNodeInLayerWithTransaction { node_type: "Path".to_string(), layer });
responses.add(EventMessage::SelectionChanged); responses.add(EventMessage::SelectionChanged);
} }
@@ -136,10 +136,44 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
network_interface.add_import(graph_craft::document::value::TaggedValue::None, true, -1, "", "", breadcrumb_network_path); network_interface.add_import(graph_craft::document::value::TaggedValue::None, true, -1, "", "", breadcrumb_network_path);
responses.add(NodeGraphMessage::SendGraph); responses.add(NodeGraphMessage::SendGraph);
} }
NodeGraphMessage::AddPrimaryImport => {
if network_interface.number_of_imports(breadcrumb_network_path) == 0 {
responses.add(NodeGraphMessage::AddImport);
} else {
responses.add(NodeGraphMessage::ExposeEncapsulatingPrimaryInput { exposed: true });
}
}
NodeGraphMessage::AddSecondaryImport => {
// If necessary, add a hidden primary import before the secondary import
if network_interface.number_of_imports(breadcrumb_network_path) == 0 {
responses.add(NodeGraphMessage::AddImport);
responses.add(NodeGraphMessage::ExposeEncapsulatingPrimaryInput { exposed: false });
}
// Add the secondary import
responses.add(NodeGraphMessage::AddImport);
}
NodeGraphMessage::AddExport => { NodeGraphMessage::AddExport => {
network_interface.add_export(graph_craft::document::value::TaggedValue::None, -1, "", breadcrumb_network_path); network_interface.add_export(graph_craft::document::value::TaggedValue::None, -1, "", breadcrumb_network_path);
responses.add(NodeGraphMessage::SendGraph); responses.add(NodeGraphMessage::SendGraph);
} }
NodeGraphMessage::AddPrimaryExport => {
if network_interface.number_of_exports(breadcrumb_network_path) == 0 {
responses.add(NodeGraphMessage::AddExport);
} else {
responses.add(NodeGraphMessage::ExposePrimaryExport { exposed: true });
}
}
NodeGraphMessage::AddSecondaryExport => {
// If necessary, add a hidden primary import before the secondary import
if network_interface.number_of_exports(breadcrumb_network_path) == 0 {
responses.add(NodeGraphMessage::AddExport);
responses.add(NodeGraphMessage::ExposePrimaryExport { exposed: false });
}
// Add the secondary export
responses.add(NodeGraphMessage::AddExport);
}
NodeGraphMessage::Init => { NodeGraphMessage::Init => {
responses.add(BroadcastMessage::SubscribeEvent { responses.add(BroadcastMessage::SubscribeEvent {
on: EventMessage::SelectionChanged, on: EventMessage::SelectionChanged,
@@ -401,6 +435,77 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
responses.add(DocumentMessage::ZoomCanvasTo100Percent); responses.add(DocumentMessage::ZoomCanvasTo100Percent);
} }
} }
NodeGraphMessage::ExposeEncapsulatingPrimaryInput { exposed } => {
let Some((node_id, network_path)) = breadcrumb_network_path.split_last() else {
return;
};
let encapsulating_connector = InputConnector::node(*node_id, 0);
if !exposed {
network_interface.disconnect_input(&encapsulating_connector, network_path);
}
let Some(mut input) = network_interface.input_from_connector(&encapsulating_connector, network_path).cloned() else {
return;
};
if let NodeInput::Value { exposed: old_exposed, .. } = &mut input {
*old_exposed = exposed;
}
network_interface.set_input(&encapsulating_connector, input, network_path);
let Some(outward_wires) = network_interface.outward_wires(breadcrumb_network_path) else {
log::error!("Could not get outward wires in remove_import");
return;
};
let Some(downstream_connections) = outward_wires.get(&OutputConnector::Import(0)).cloned() else {
log::error!("Could not get outward wires for import in remove_import");
return;
};
// Disconnect all connections in the encapsulating network
for downstream_connection in &downstream_connections {
network_interface.disconnect_input(downstream_connection, breadcrumb_network_path);
}
responses.add(NodeGraphMessage::UpdateImportsExports);
responses.add(NodeGraphMessage::SendWires);
}
NodeGraphMessage::ExposePrimaryExport { exposed } => {
let export_connector: InputConnector = InputConnector::Export(0);
if !exposed {
network_interface.disconnect_input(&export_connector, breadcrumb_network_path);
}
let Some(mut input) = network_interface.input_from_connector(&export_connector, breadcrumb_network_path).cloned() else {
return;
};
if let NodeInput::Value { exposed: old_exposed, .. } = &mut input {
*old_exposed = exposed;
}
network_interface.set_input(&export_connector, input, breadcrumb_network_path);
// Disconnect all connections in the encapsulating network
if let Some((encapsulating_node, encapsulating_path)) = breadcrumb_network_path.split_last() {
let Some(outward_wires) = network_interface.outward_wires(encapsulating_path) else {
log::error!("Could not get outward wires in remove_import");
return;
};
let Some(downstream_connections) = outward_wires.get(&OutputConnector::node(*encapsulating_node, 0)).cloned() else {
log::error!("Could not get outward wires for import in remove_import");
return;
};
for downstream_connection in &downstream_connections {
network_interface.disconnect_input(downstream_connection, encapsulating_path);
}
}
responses.add(NodeGraphMessage::UpdateImportsExports);
}
NodeGraphMessage::InsertNode { node_id, node_template } => { NodeGraphMessage::InsertNode { node_id, node_template } => {
network_interface.insert_node(node_id, node_template, selection_network_path); network_interface.insert_node(node_id, node_template, selection_network_path);
} }
@@ -706,21 +811,21 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
return; return;
}; };
if modify_import_export.add_import_export.clicked_input_port_from_point(node_graph_point).is_some() { if let Some(remove_import_index) = modify_import_export.remove_imports_exports.clicked_output_port_from_point(node_graph_point) {
responses.add(DocumentMessage::AddTransaction); responses.add(DocumentMessage::AddTransaction);
responses.add(NodeGraphMessage::AddExport); if remove_import_index == 0 {
return; responses.add(NodeGraphMessage::ExposeEncapsulatingPrimaryInput { exposed: false })
} else if modify_import_export.add_import_export.clicked_output_port_from_point(node_graph_point).is_some() { } else {
responses.add(DocumentMessage::AddTransaction); responses.add(NodeGraphMessage::RemoveImport { import_index: remove_import_index });
responses.add(NodeGraphMessage::AddImport); }
return;
} else if let Some(remove_import_index) = modify_import_export.remove_imports_exports.clicked_output_port_from_point(node_graph_point) {
responses.add(DocumentMessage::AddTransaction);
responses.add(NodeGraphMessage::RemoveImport { import_index: remove_import_index });
return; return;
} else if let Some(remove_export_index) = modify_import_export.remove_imports_exports.clicked_input_port_from_point(node_graph_point) { } else if let Some(remove_export_index) = modify_import_export.remove_imports_exports.clicked_input_port_from_point(node_graph_point) {
responses.add(DocumentMessage::AddTransaction); responses.add(DocumentMessage::AddTransaction);
responses.add(NodeGraphMessage::RemoveExport { export_index: remove_export_index }); if remove_export_index == 0 {
responses.add(NodeGraphMessage::ExposePrimaryExport { exposed: false })
} else {
responses.add(NodeGraphMessage::RemoveExport { export_index: remove_export_index });
}
return; return;
} else if let Some(move_import_index) = modify_import_export.reorder_imports_exports.clicked_output_port_from_point(node_graph_point) { } else if let Some(move_import_index) = modify_import_export.reorder_imports_exports.clicked_output_port_from_point(node_graph_point) {
responses.add(DocumentMessage::StartTransaction); responses.add(DocumentMessage::StartTransaction);
@@ -756,10 +861,8 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
} }
// Alt-click sets the clicked node as previewed // Alt-click sets the clicked node as previewed
if alt_click { if alt_click && let Some(clicked_node) = clicked_id {
if let Some(clicked_node) = clicked_id { self.preview_on_mouse_up = Some(clicked_node);
self.preview_on_mouse_up = Some(clicked_node);
}
} }
// Begin moving an existing wire // Begin moving an existing wire
@@ -785,14 +888,8 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
self.initial_disconnecting = false; self.initial_disconnecting = false;
self.wire_in_progress_from_connector = network_interface.output_position(&clicked_output, selection_network_path); self.wire_in_progress_from_connector = network_interface.output_position(&clicked_output, selection_network_path);
if let Some((output_type, source)) = clicked_output let (output_type, source) = &network_interface.output_type(&clicked_output, breadcrumb_network_path);
.node_id() self.wire_in_progress_type = FrontendGraphDataType::displayed_type(output_type, source);
.map(|node_id| network_interface.output_type(&node_id, clicked_output.index(), breadcrumb_network_path))
{
self.wire_in_progress_type = FrontendGraphDataType::displayed_type(&output_type, &source);
} else {
self.wire_in_progress_type = FrontendGraphDataType::General;
}
self.update_node_graph_hints(responses); self.update_node_graph_hints(responses);
return; return;
@@ -1023,7 +1120,8 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
}; };
(position > point.y).then_some(*index) (position > point.y).then_some(*index)
}) })
.unwrap_or(modify_import_export.reorder_imports_exports.output_ports().count()), .filter(|end_index| *end_index > 0) // An import cannot be reordered to be the primary
.unwrap_or_else(|| modify_import_export.reorder_imports_exports.output_ports().count() + 1),
); );
responses.add(FrontendMessage::UpdateImportReorderIndex { index: self.end_index }); responses.add(FrontendMessage::UpdateImportReorderIndex { index: self.end_index });
} else if self.reordering_export.is_some() { } else if self.reordering_export.is_some() {
@@ -1043,7 +1141,8 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
}; };
(position > point.y).then_some(*index) (position > point.y).then_some(*index)
}) })
.unwrap_or(modify_import_export.reorder_imports_exports.input_ports().count()), .filter(|end_index| *end_index > 0) // An export cannot be reordered to be the primary
.unwrap_or_else(|| modify_import_export.reorder_imports_exports.input_ports().count() + 1),
); );
responses.add(FrontendMessage::UpdateExportReorderIndex { index: self.end_index }); responses.add(FrontendMessage::UpdateExportReorderIndex { index: self.end_index });
} }
@@ -1098,27 +1197,27 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
responses.add(NodeGraphMessage::RunDocumentGraph); responses.add(NodeGraphMessage::RunDocumentGraph);
responses.add(NodeGraphMessage::SendGraph); responses.add(NodeGraphMessage::SendGraph);
} else if output_connector.is_some() && input_connector.is_none() && !self.initial_disconnecting { } else if !self.initial_disconnecting
&& input_connector.is_none()
&& let Some(output_connector) = output_connector
{
// If the add node menu is already open, we don't want to open it again // If the add node menu is already open, we don't want to open it again
if self.context_menu.is_some() { if self.context_menu.is_some() {
return; return;
} }
// Get the output types from the network interface
let (output_type, type_source) = network_interface.output_type(&output_connector, selection_network_path);
let Some(network_metadata) = network_interface.network_metadata(selection_network_path) else { let Some(network_metadata) = network_interface.network_metadata(selection_network_path) else {
warn!("No network_metadata"); warn!("No network_metadata");
return; return;
}; };
// Get the compatible type from the output connector
let compatible_type = output_connector.and_then(|output_connector| {
output_connector.node_id().and_then(|node_id| {
// Get the output types from the network interface
let (output_type, type_source) = network_interface.output_type(&node_id, output_connector.index(), selection_network_path);
match type_source { let compatible_type = match type_source {
TypeSource::RandomProtonodeImplementation | TypeSource::Error(_) => None, TypeSource::RandomProtonodeImplementation | TypeSource::Error(_) => None,
_ => Some(format!("type:{}", output_type.nested_type())), _ => Some(format!("type:{}", output_type.nested_type())),
} };
})
});
let appear_right_of_mouse = if ipp.mouse.position.x > ipp.viewport_bounds.size().x - 173. { -173. } else { 0. }; let appear_right_of_mouse = if ipp.mouse.position.x > ipp.viewport_bounds.size().x - 173. { -173. } else { 0. };
let appear_above_mouse = if ipp.mouse.position.y > ipp.viewport_bounds.size().y - 34. { -34. } else { 0. }; let appear_above_mouse = if ipp.mouse.position.y > ipp.viewport_bounds.size().y - 34. { -34. } else { 0. };
let node_graph_shift = DVec2::new(appear_right_of_mouse, appear_above_mouse) / network_metadata.persistent_metadata.navigation_metadata.node_graph_to_viewport.matrix2.x_axis.x; let node_graph_shift = DVec2::new(appear_right_of_mouse, appear_above_mouse) / network_metadata.persistent_metadata.navigation_metadata.node_graph_to_viewport.matrix2.x_axis.x;
@@ -1442,11 +1541,13 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
} }
NodeGraphMessage::RemoveImport { import_index: usize } => { NodeGraphMessage::RemoveImport { import_index: usize } => {
network_interface.remove_import(usize, selection_network_path); network_interface.remove_import(usize, selection_network_path);
responses.add(NodeGraphMessage::UpdateImportsExports);
responses.add(NodeGraphMessage::SendGraph); responses.add(NodeGraphMessage::SendGraph);
responses.add(NodeGraphMessage::RunDocumentGraph); responses.add(NodeGraphMessage::RunDocumentGraph);
} }
NodeGraphMessage::RemoveExport { export_index: usize } => { NodeGraphMessage::RemoveExport { export_index: usize } => {
network_interface.remove_export(usize, selection_network_path); network_interface.remove_export(usize, selection_network_path);
responses.add(NodeGraphMessage::UpdateImportsExports);
responses.add(NodeGraphMessage::SendGraph); responses.add(NodeGraphMessage::SendGraph);
responses.add(NodeGraphMessage::RunDocumentGraph); responses.add(NodeGraphMessage::RunDocumentGraph);
} }
@@ -1821,7 +1922,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
let shift = ipp.keyboard.get(Key::Shift as usize); let shift = ipp.keyboard.get(Key::Shift as usize);
let Some(selected_nodes) = network_interface.selected_nodes_in_nested_network(selection_network_path) else { let Some(selected_nodes) = network_interface.selected_nodes_in_nested_network(selection_network_path) else {
log::error!("Could not get selected nodes in PointerMove"); log::error!("Could not get selected nodes in UpdateBoxSelection");
return; return;
}; };
let previous_selection = selected_nodes.selected_nodes_ref().iter().cloned().collect::<HashSet<_>>(); let previous_selection = selected_nodes.selected_nodes_ref().iter().cloned().collect::<HashSet<_>>();
@@ -1850,29 +1951,25 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
} }
} }
NodeGraphMessage::UpdateImportsExports => { NodeGraphMessage::UpdateImportsExports => {
let imports = network_interface.frontend_imports(breadcrumb_network_path).unwrap_or_default(); let imports = network_interface.frontend_imports(breadcrumb_network_path);
let exports = network_interface.frontend_exports(breadcrumb_network_path).unwrap_or_default(); let exports = network_interface.frontend_exports(breadcrumb_network_path);
let add_import = network_interface
.frontend_import_export_modify( let Some((import_position, export_position)) = network_interface.import_export_position(breadcrumb_network_path) else {
|modify_import_export_click_target| modify_import_export_click_target.add_import_export.output_ports().collect::<Vec<_>>(), log::error!("Could not get import export positions");
breadcrumb_network_path, return;
) };
.into_iter()
.next(); // Do not show the add import or add export button in the document network;
let add_export = network_interface let add_import_export = !breadcrumb_network_path.is_empty();
.frontend_import_export_modify(
|modify_import_export_click_target| modify_import_export_click_target.add_import_export.input_ports().collect::<Vec<_>>(),
breadcrumb_network_path,
)
.into_iter()
.next();
responses.add(NodeGraphMessage::UpdateVisibleNodes); responses.add(NodeGraphMessage::UpdateVisibleNodes);
responses.add(NodeGraphMessage::SendWires); responses.add(NodeGraphMessage::SendWires);
responses.add(FrontendMessage::UpdateImportsExports { responses.add(FrontendMessage::UpdateImportsExports {
imports, imports,
exports, exports,
add_import, import_position,
add_export, export_position,
add_import_export,
}); });
} }
@@ -1990,11 +2087,6 @@ impl NodeGraphMessageHandler {
return; return;
}; };
let Some(network) = network_interface.nested_network(breadcrumb_network_path) else {
warn!("No network in update_selection_action_buttons");
return;
};
let Some(selected_nodes) = network_interface.selected_nodes_in_nested_network(breadcrumb_network_path) else { let Some(selected_nodes) = network_interface.selected_nodes_in_nested_network(breadcrumb_network_path) else {
warn!("No selected nodes in update_selection_action_buttons"); warn!("No selected nodes in update_selection_action_buttons");
return; return;
@@ -2008,6 +2100,7 @@ impl NodeGraphMessageHandler {
let mut selected_layers = selected_nodes.selected_layers(network_interface.document_metadata()); let mut selected_layers = selected_nodes.selected_layers(network_interface.document_metadata());
let selected_layer = selected_layers.next(); let selected_layer = selected_layers.next();
let has_multiple_selection = selected_layers.next().is_some(); let has_multiple_selection = selected_layers.next().is_some();
for _ in selected_layers {}
let mut widgets = vec![ let mut widgets = vec![
PopoverButton::new() PopoverButton::new()
@@ -2020,7 +2113,7 @@ impl NodeGraphMessageHandler {
let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, network_interface); let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, network_interface);
let node_type = graph_layer.horizontal_layer_flow().nth(1); let node_type = graph_layer.horizontal_layer_flow().nth(1);
if let Some(node_id) = node_type { if let Some(node_id) = node_type {
let (output_type, _) = network_interface.output_type(&node_id, 0, &[]); let (output_type, _) = network_interface.output_type(&OutputConnector::node(node_id, 0), &[]);
Some(format!("type:{}", output_type.nested_type())) Some(format!("type:{}", output_type.nested_type()))
} else { } else {
None None
@@ -2106,6 +2199,11 @@ impl NodeGraphMessageHandler {
let mut selection = selected_nodes.selected_nodes(); let mut selection = selected_nodes.selected_nodes();
let (selection, no_other_selections) = (selection.next(), selection.count() == 0); let (selection, no_other_selections) = (selection.next(), selection.count() == 0);
let Some(network) = network_interface.nested_network(breadcrumb_network_path) else {
warn!("No network in update_selection_action_buttons");
return;
};
let previewing = if matches!(network_interface.previewing(breadcrumb_network_path), Previewing::Yes { .. }) { let previewing = if matches!(network_interface.previewing(breadcrumb_network_path), Previewing::Yes { .. }) {
network.exports.iter().find_map(|export| { network.exports.iter().find_map(|export| {
let NodeInput::Node { node_id, .. } = export else { return None }; let NodeInput::Node { node_id, .. } = export else { return None };
@@ -2338,7 +2436,7 @@ impl NodeGraphMessageHandler {
let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer_identifier, context.network_interface); let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer_identifier, context.network_interface);
let node_type = graph_layer.horizontal_layer_flow().nth(1); let node_type = graph_layer.horizontal_layer_flow().nth(1);
if let Some(node_id) = node_type { if let Some(node_id) = node_type {
let (output_type, _) = context.network_interface.output_type(&node_id, 0, &[]); let (output_type, _) = context.network_interface.output_type(&OutputConnector::node(node_id, 0), &[]);
Some(format!("type:{}", output_type.nested_type())) Some(format!("type:{}", output_type.nested_type()))
} else { } else {
None None
@@ -2426,110 +2524,48 @@ impl NodeGraphMessageHandler {
} }
fn collect_nodes(&self, network_interface: &mut NodeNetworkInterface, breadcrumb_network_path: &[NodeId]) -> Vec<FrontendNode> { fn collect_nodes(&self, network_interface: &mut NodeNetworkInterface, breadcrumb_network_path: &[NodeId]) -> Vec<FrontendNode> {
let Some(outward_wires) = network_interface.outward_wires(breadcrumb_network_path).cloned() else {
return Vec::new();
};
let mut can_be_layer_lookup = HashSet::new();
let mut position_lookup = HashMap::new();
let Some(network) = network_interface.nested_network(breadcrumb_network_path) else { let Some(network) = network_interface.nested_network(breadcrumb_network_path) else {
log::error!("Could not get nested network when collecting nodes"); log::error!("Could not get nested network when collecting nodes");
return Vec::new(); return Vec::new();
}; };
for node_id in network.nodes.keys().cloned().collect::<Vec<_>>() {
if network_interface.is_eligible_to_be_layer(&node_id, breadcrumb_network_path) {
can_be_layer_lookup.insert(node_id);
}
if let Some(position) = network_interface.position(&node_id, breadcrumb_network_path) {
position_lookup.insert(node_id, position);
} else {
log::error!("Could not get position for node {node_id}");
}
}
let mut frontend_inputs_lookup = frontend_inputs_lookup(breadcrumb_network_path, network_interface);
let Some(network) = network_interface.nested_network(breadcrumb_network_path) else {
log::error!("Could not get nested network when collecting nodes");
return Vec::new();
};
let Some(network_metadata) = network_interface.network_metadata(breadcrumb_network_path) else {
log::error!("Could not get network_metadata when collecting nodes");
return Vec::new();
};
let mut nodes = Vec::new(); let mut nodes = Vec::new();
for (&node_id, node) in &network.nodes { for (node_id, visible) in network.nodes.iter().map(|(node_id, node)| (*node_id, node.visible)).collect::<Vec<_>>() {
let node_id_path = [breadcrumb_network_path, (&[node_id])].concat(); let node_id_path = [breadcrumb_network_path, &[node_id]].concat();
let inputs = frontend_inputs_lookup.remove(&node_id).unwrap_or_default(); let primary_input_connector = InputConnector::node(node_id, 0);
let mut inputs = inputs.into_iter().map(|input| { let primary_input = if network_interface
input.map(|input| FrontendGraphInput { .input_from_connector(&primary_input_connector, breadcrumb_network_path)
data_type: FrontendGraphDataType::displayed_type(&input.ty, &input.type_source), .is_some_and(|input| input.is_exposed())
resolved_type: format!("{:?}", &input.ty), {
valid_types: input.valid_types.iter().map(|ty| ty.to_string()).collect(), network_interface.frontend_input_from_connector(&primary_input_connector, breadcrumb_network_path)
name: input.input_name,
description: input.input_description,
connected_to: input.output_connector,
})
});
let primary_input = inputs.next().flatten();
let exposed_inputs = inputs.flatten().collect();
let (output_type, type_source) = network_interface.output_type(&node_id, 0, breadcrumb_network_path);
let frontend_data_type = FrontendGraphDataType::displayed_type(&output_type, &type_source);
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) {
Some(FrontendGraphOutput {
data_type: frontend_data_type,
name: "Output 1".to_string(),
description: String::new(),
resolved_type: format!("{output_type:?}"),
connected_to,
})
} else { } else {
None None
}; };
let exposed_inputs = (1..network_interface.number_of_inputs(&node_id, breadcrumb_network_path))
.filter_map(|input_index| network_interface.frontend_input_from_connector(&InputConnector::node(node_id, input_index), breadcrumb_network_path))
.collect();
let mut exposed_outputs = Vec::new(); let primary_output = network_interface.frontend_output_from_connector(&OutputConnector::node(node_id, 0), breadcrumb_network_path);
for output_index in 0..network_interface.number_of_outputs(&node_id, breadcrumb_network_path) {
if output_index == 0 && network_interface.has_primary_output(&node_id, breadcrumb_network_path) {
continue;
}
let (output_type, type_source) = network_interface.output_type(&node_id, 0, breadcrumb_network_path);
let data_type = FrontendGraphDataType::displayed_type(&output_type, &type_source);
let Some(node_metadata) = network_metadata.persistent_metadata.node_metadata.get(&node_id) else { let exposed_outputs = (1..network_interface.number_of_outputs(&node_id, breadcrumb_network_path))
log::error!("Could not get node_metadata when getting output for {node_id}"); .filter_map(|output_index| network_interface.frontend_output_from_connector(&OutputConnector::node(node_id, output_index), breadcrumb_network_path))
continue; .collect();
}; let (primary_output_connected_to_layer, primary_input_connected_to_layer) = if network_interface.is_layer(&node_id, breadcrumb_network_path) {
let output_name = node_metadata (
.persistent_metadata network_interface.primary_output_connected_to_layer(&node_id, breadcrumb_network_path),
.output_names network_interface.primary_input_connected_to_layer(&node_id, breadcrumb_network_path),
.get(output_index) )
.cloned() } else {
.filter(|output_name| !output_name.is_empty()) (false, false)
.unwrap_or_else(|| output_type.nested_type().to_string());
let connected_to = outward_wires.get(&OutputConnector::node(node_id, output_index)).cloned().unwrap_or_default();
exposed_outputs.push(FrontendGraphOutput {
data_type,
name: output_name,
description: String::new(),
resolved_type: format!("{output_type:?}"),
connected_to,
});
}
let Some(network) = network_interface.nested_network(breadcrumb_network_path) else {
log::error!("Could not get nested network when collecting nodes");
return Vec::new();
}; };
let is_export = network.exports.first().is_some_and(|export| export.as_node().is_some_and(|export_node_id| node_id == export_node_id));
let is_export = network_interface
.input_from_connector(&InputConnector::Export(0), breadcrumb_network_path)
.is_some_and(|export| export.as_node().is_some_and(|export_node_id| node_id == export_node_id));
let is_root_node = network_interface.root_node(breadcrumb_network_path).is_some_and(|root_node| root_node.node_id == node_id); let is_root_node = network_interface.root_node(breadcrumb_network_path).is_some_and(|root_node| root_node.node_id == node_id);
let Some(position) = position_lookup.get(&node_id).map(|pos| (pos.x, pos.y)) else { let Some(position) = network_interface.position(&node_id, breadcrumb_network_path) else {
log::error!("Could not get position for node: {node_id}"); log::error!("Could not get position for node: {node_id}");
continue; continue;
}; };
@@ -2555,19 +2591,20 @@ impl NodeGraphMessageHandler {
is_layer: network_interface is_layer: network_interface
.node_metadata(&node_id, breadcrumb_network_path) .node_metadata(&node_id, breadcrumb_network_path)
.is_some_and(|node_metadata| node_metadata.persistent_metadata.is_layer()), .is_some_and(|node_metadata| node_metadata.persistent_metadata.is_layer()),
can_be_layer: can_be_layer_lookup.contains(&node_id), can_be_layer: network_interface.is_eligible_to_be_layer(&node_id, breadcrumb_network_path),
reference: network_interface.reference(&node_id, breadcrumb_network_path).cloned().unwrap_or_default(), reference: network_interface.reference(&node_id, breadcrumb_network_path).cloned().unwrap_or_default(),
display_name: network_interface.display_name(&node_id, breadcrumb_network_path), display_name: network_interface.display_name(&node_id, breadcrumb_network_path),
primary_input, primary_input,
exposed_inputs, exposed_inputs,
primary_output, primary_output,
exposed_outputs, exposed_outputs,
primary_output_connected_to_layer,
primary_input_connected_to_layer,
position, position,
previewed, previewed,
visible: node.visible, visible,
locked, locked,
errors, errors,
ui_only: false,
}); });
} }
@@ -2717,73 +2754,6 @@ impl NodeGraphMessageHandler {
} }
} }
#[derive(Default)]
struct InputLookup {
input_name: String,
input_description: String,
ty: Type,
type_source: TypeSource,
valid_types: Vec<Type>,
output_connector: Option<OutputConnector>,
}
type FrontendInputsLookup = HashMap<NodeId, Vec<Option<InputLookup>>>;
/// Create a lookup hashmap that can be used to create the frontend inputs. This is needed because `input_type` requires a mutable `network_interface`.
fn frontend_inputs_lookup(breadcrumb_network_path: &[NodeId], network_interface: &mut NodeNetworkInterface) -> FrontendInputsLookup {
let Some(network) = network_interface.nested_network(breadcrumb_network_path) else {
return Default::default();
};
let mut frontend_inputs_lookup = HashMap::new();
for (node_id, index, output_connector, is_exposed) in network
.nodes
.iter()
.flat_map(|(node_id, node)| {
node.inputs
.iter()
.enumerate()
.map(|(index, input)| (*node_id, index, OutputConnector::from_input(input), input.is_exposed()))
})
.collect::<Vec<_>>()
{
// Skip not exposed inputs (they still get an entry to help with finding the primary input)
let lookup = if !is_exposed {
None
} else {
// Get the name from the metadata here (since it also requires a reference to the `network_interface`)
let (input_name, input_description) = network_interface.displayed_input_name_and_description(&node_id, index, breadcrumb_network_path);
Some(InputLookup {
input_name,
input_description,
output_connector,
..Default::default()
})
};
frontend_inputs_lookup.entry(node_id).or_insert_with(Vec::new).push(lookup);
}
for (&node_id, value) in frontend_inputs_lookup.iter_mut() {
for (index, value) in value.iter_mut().enumerate() {
// Skip not exposed inputs for efficiency
let Some(value) = value else { continue };
// Resolve the type (done in a separate loop because it requires a mutable reference to the `network_interface`)
let (ty, type_source) = network_interface.input_type(&InputConnector::node(node_id, index), breadcrumb_network_path);
value.ty = ty;
value.type_source = type_source;
}
}
for (&node_id, value) in frontend_inputs_lookup.iter_mut() {
for (index, value) in value.iter_mut().enumerate() {
// Skip not exposed inputs for efficiency
let Some(value) = value else { continue };
// Resolve the type (done in a separate loop because it requires a mutable reference to the `network_interface`)
value.valid_types = network_interface.valid_input_types(&InputConnector::node(node_id, index), breadcrumb_network_path);
}
}
frontend_inputs_lookup
}
impl Default for NodeGraphMessageHandler { impl Default for NodeGraphMessageHandler {
fn default() -> Self { fn default() -> Self {
Self { Self {
@@ -1,4 +1,5 @@
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, OutputConnector, TypeSource}; use crate::messages::portfolio::document::utility_types::network_interface::TypeSource;
use glam::IVec2;
use graph_craft::document::NodeId; use graph_craft::document::NodeId;
use graph_craft::document::value::TaggedValue; use graph_craft::document::value::TaggedValue;
use graphene_std::Type; use graphene_std::Type;
@@ -60,7 +61,8 @@ pub struct FrontendGraphInput {
#[serde(rename = "validTypes")] #[serde(rename = "validTypes")]
pub valid_types: Vec<String>, pub valid_types: Vec<String>,
#[serde(rename = "connectedTo")] #[serde(rename = "connectedTo")]
pub connected_to: Option<OutputConnector>, /// Either "nothing", "import index {index}", or "{node name} output {output_index}".
pub connected_to: String,
} }
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
@@ -68,11 +70,13 @@ pub struct FrontendGraphOutput {
#[serde(rename = "dataType")] #[serde(rename = "dataType")]
pub data_type: FrontendGraphDataType, pub data_type: FrontendGraphDataType,
pub name: String, pub name: String,
pub description: String,
#[serde(rename = "resolvedType")] #[serde(rename = "resolvedType")]
pub resolved_type: String, pub resolved_type: String,
pub description: String,
/// If connected to an export, it is "export index {index}".
/// If connected to a node, it is "{node name} input {input_index}".
#[serde(rename = "connectedTo")] #[serde(rename = "connectedTo")]
pub connected_to: Vec<InputConnector>, pub connected_to: Vec<String>,
} }
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
@@ -93,13 +97,15 @@ pub struct FrontendNode {
pub primary_output: Option<FrontendGraphOutput>, pub primary_output: Option<FrontendGraphOutput>,
#[serde(rename = "exposedOutputs")] #[serde(rename = "exposedOutputs")]
pub exposed_outputs: Vec<FrontendGraphOutput>, pub exposed_outputs: Vec<FrontendGraphOutput>,
pub position: (i32, i32), #[serde(rename = "primaryOutputConnectedToLayer")]
pub primary_output_connected_to_layer: bool,
#[serde(rename = "primaryInputConnectedToLayer")]
pub primary_input_connected_to_layer: bool,
pub position: IVec2,
pub visible: bool, pub visible: bool,
pub locked: bool, pub locked: bool,
pub previewed: bool, pub previewed: bool,
pub errors: Option<String>, pub errors: Option<String>,
#[serde(rename = "uiOnly")]
pub ui_only: bool,
} }
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,136 @@
use crate::messages::portfolio::document::utility_types::network_interface::{DocumentNodePersistentMetadata, InputMetadata, InputPersistentMetadata, NodeNetworkMetadata, NodeTypePersistentMetadata};
use serde_json::Value;
use std::collections::HashMap;
/// Persistent metadata for each node in the network, which must be included when creating, serializing, and deserializing saving a node.
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct DocumentNodePersistentMetadataInputNames {
pub reference: Option<String>,
#[serde(default)]
pub display_name: String,
pub input_names: Vec<String>,
pub output_names: Vec<String>,
pub has_primary_output: bool,
#[serde(default)]
pub locked: bool,
#[serde(default)]
pub pinned: bool,
pub node_type_metadata: NodeTypePersistentMetadata,
pub network_metadata: Option<NodeNetworkMetadata>,
}
impl From<DocumentNodePersistentMetadataInputNames> for DocumentNodePersistentMetadata {
fn from(old: DocumentNodePersistentMetadataInputNames) -> Self {
DocumentNodePersistentMetadata {
input_metadata: Vec::new(),
..old.into()
}
}
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct DocumentNodePersistentMetadataPropertiesRow {
pub reference: Option<String>,
#[serde(default)]
pub display_name: String,
pub input_properties: Vec<PropertiesRow>,
pub output_names: Vec<String>,
pub has_primary_output: bool,
#[serde(default)]
pub locked: bool,
#[serde(default)]
pub pinned: bool,
pub node_type_metadata: NodeTypePersistentMetadata,
pub network_metadata: Option<NodeNetworkMetadata>,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct PropertiesRow {
pub input_data: HashMap<String, Value>,
pub widget_override: Option<String>,
#[serde(skip)]
pub input_name: String,
#[serde(skip)]
pub input_description: String,
}
impl From<DocumentNodePersistentMetadataPropertiesRow> for DocumentNodePersistentMetadata {
fn from(old: DocumentNodePersistentMetadataPropertiesRow) -> Self {
let mut input_metadata = Vec::new();
for properties_row in old.input_properties {
input_metadata.push(InputMetadata {
persistent_metadata: InputPersistentMetadata {
input_data: properties_row.input_data,
widget_override: properties_row.widget_override,
input_name: properties_row.input_name,
input_description: properties_row.input_description,
},
..Default::default()
})
}
DocumentNodePersistentMetadata {
reference: old.reference,
display_name: old.display_name,
input_metadata: Vec::new(),
output_names: old.output_names,
locked: old.locked,
pinned: old.pinned,
node_type_metadata: old.node_type_metadata,
network_metadata: old.network_metadata,
}
}
}
/// Persistent metadata for each node in the network, which must be included when creating, serializing, and deserializing saving a node.
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct DocumentNodePersistentMetadataHasPrimaryOutput {
pub reference: Option<String>,
#[serde(default)]
pub display_name: String,
pub input_metadata: Vec<InputMetadata>,
pub output_names: Vec<String>,
pub has_primary_output: bool,
#[serde(default)]
pub locked: bool,
#[serde(default)]
pub pinned: bool,
pub node_type_metadata: NodeTypePersistentMetadata,
pub network_metadata: Option<NodeNetworkMetadata>,
}
impl From<DocumentNodePersistentMetadataHasPrimaryOutput> for DocumentNodePersistentMetadata {
fn from(old: DocumentNodePersistentMetadataHasPrimaryOutput) -> Self {
DocumentNodePersistentMetadata {
reference: old.reference,
display_name: old.display_name,
input_metadata: old.input_metadata,
output_names: old.output_names,
locked: old.locked,
pinned: old.pinned,
node_type_metadata: old.node_type_metadata,
network_metadata: old.network_metadata,
}
}
}
pub fn deserialize_node_persistent_metadata<'de, D>(deserializer: D) -> Result<DocumentNodePersistentMetadata, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::Deserialize;
let value = Value::deserialize(deserializer)?;
if let Ok(document) = serde_json::from_value::<DocumentNodePersistentMetadataHasPrimaryOutput>(value.clone()) {
return Ok(document.into());
};
if let Ok(document) = serde_json::from_value::<DocumentNodePersistentMetadata>(value.clone()) {
return Ok(document);
};
if let Ok(document) = serde_json::from_value::<DocumentNodePersistentMetadataPropertiesRow>(value.clone()) {
return Ok(document.into());
};
match serde_json::from_value::<DocumentNodePersistentMetadataInputNames>(value.clone()) {
Ok(document) => Ok(document.into()),
Err(e) => Err(serde::de::Error::custom(e)),
}
}
@@ -109,7 +109,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
let metadata = &document.network_interface.document_network_metadata().persistent_metadata; let metadata = &document.network_interface.document_network_metadata().persistent_metadata;
(!metadata.selection_undo_history.is_empty(), !metadata.selection_redo_history.is_empty()) (!metadata.selection_undo_history.is_empty(), !metadata.selection_redo_history.is_empty())
}; };
self.menu_bar_message_handler.make_path_editable_is_allowed = make_path_editable_is_allowed(&document.network_interface, document.metadata()).is_some(); self.menu_bar_message_handler.make_path_editable_is_allowed = make_path_editable_is_allowed(&mut document.network_interface).is_some();
} }
self.menu_bar_message_handler.process_message(message, responses, ()); self.menu_bar_message_handler.process_message(message, responses, ());
@@ -1144,7 +1144,7 @@ impl PortfolioMessageHandler {
} }
} }
fn load_document(&mut self, new_document: DocumentMessageHandler, document_id: DocumentId, layers_panel_open: bool, responses: &mut VecDeque<Message>, to_front: bool) { fn load_document(&mut self, mut new_document: DocumentMessageHandler, document_id: DocumentId, layers_panel_open: bool, responses: &mut VecDeque<Message>, to_front: bool) {
if to_front { if to_front {
self.document_ids.push_front(document_id); self.document_ids.push_front(document_id);
} else { } else {
@@ -1,8 +1,8 @@
use super::snapping::{SnapCandidatePoint, SnapData, SnapManager}; use super::snapping::{SnapCandidatePoint, SnapData, SnapManager};
use super::transformation_cage::{BoundingBoxManager, SizeSnapData}; use super::transformation_cage::{BoundingBoxManager, SizeSnapData};
use crate::consts::ROTATE_INCREMENT; use crate::consts::ROTATE_INCREMENT;
use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier}; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface; use crate::messages::portfolio::document::utility_types::network_interface::{NodeNetworkInterface, OutputConnector};
use crate::messages::portfolio::document::utility_types::transformation::Selected; use crate::messages::portfolio::document::utility_types::transformation::Selected;
use crate::messages::prelude::*; use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::graph_modification_utils::{NodeGraphLayer, get_text}; use crate::messages::tool::common_functionality::graph_modification_utils::{NodeGraphLayer, get_text};
@@ -568,25 +568,21 @@ pub fn find_two_param_best_approximate(p1: DVec2, p3: DVec2, d1: DVec2, d2: DVec
(d1 * len1, d2 * len2) (d1 * len1, d2 * len2)
} }
pub fn make_path_editable_is_allowed(network_interface: &NodeNetworkInterface, metadata: &DocumentMetadata) -> Option<LayerNodeIdentifier> { pub fn make_path_editable_is_allowed(network_interface: &mut NodeNetworkInterface) -> Option<LayerNodeIdentifier> {
// Must have exactly one layer selected // Must have exactly one layer selected
let selected_nodes = network_interface.selected_nodes(); let selected_nodes = network_interface.selected_nodes();
let mut selected_layers = selected_nodes.selected_layers(metadata); let mut selected_layers = selected_nodes.selected_layers(network_interface.document_metadata());
let first_layer = selected_layers.next()?; let first_layer = selected_layers.next()?;
if selected_layers.next().is_some() { if selected_layers.next().is_some() {
return None; return None;
} }
for _ in selected_layers {}
// Must be a layer of type Table<Vector> // Must be a layer of type Table<Vector>
let compatible_type = NodeGraphLayer::new(first_layer, network_interface) let node_id = NodeGraphLayer::new(first_layer, network_interface).horizontal_layer_flow().nth(1)?;
.horizontal_layer_flow()
.nth(1) let (output_type, _) = network_interface.output_type(&OutputConnector::node(node_id, 0), &[]);
.map(|node_id| { if output_type.nested_type() != concrete!(Table<Vector>).nested_type() {
let (output_type, _) = network_interface.output_type(&node_id, 0, &[]);
output_type.nested_type() == concrete!(Table<Vector>).nested_type()
})
.unwrap_or_default();
if !compatible_type {
return None; return None;
} }
@@ -3101,7 +3101,7 @@ impl Fsm for PathToolFsmState {
colinear, colinear,
}; };
tool_data.make_path_editable_is_allowed = make_path_editable_is_allowed(&document.network_interface, document.metadata()).is_some(); tool_data.make_path_editable_is_allowed = make_path_editable_is_allowed(&mut document.network_interface).is_some();
tool_data.update_selection_status(shape_editor, document); tool_data.update_selection_status(shape_editor, document);
self self
} }
+197 -178
View File
@@ -4,8 +4,7 @@
import { fade } from "svelte/transition"; import { fade } from "svelte/transition";
import type { Editor } from "@graphite/editor"; import type { Editor } from "@graphite/editor";
import type { Node } from "@graphite/messages"; import type { FrontendGraphInput, FrontendGraphOutput } from "@graphite/messages";
import type { FrontendNode, FrontendGraphInput, FrontendGraphOutput } from "@graphite/messages";
import type { NodeGraphState } from "@graphite/state-providers/node-graph"; import type { NodeGraphState } from "@graphite/state-providers/node-graph";
import type { IconName } from "@graphite/utility-functions/icons"; import type { IconName } from "@graphite/utility-functions/icons";
@@ -178,7 +177,7 @@
} }
function dataTypeTooltip(value: FrontendGraphInput | FrontendGraphOutput): string { function dataTypeTooltip(value: FrontendGraphInput | FrontendGraphOutput): string {
return value.resolvedType ? `Data Type:\n${value.resolvedType}` : `Data Type (Unresolved):\n${value.dataType}`; return `Data Type: ${value.resolvedType}`;
} }
function validTypesText(value: FrontendGraphInput): string { function validTypesText(value: FrontendGraphInput): string {
@@ -189,37 +188,11 @@
function outputConnectedToText(output: FrontendGraphOutput): string { function outputConnectedToText(output: FrontendGraphOutput): string {
if (output.connectedTo.length === 0) return "Connected to nothing"; if (output.connectedTo.length === 0) return "Connected to nothing";
return output.connectedTo return `Connected to:\n${output.connectedTo.join("\n")}`;
.map((inputConnector) => {
if ((inputConnector as Node).nodeId === undefined) return `Connected to export index ${inputConnector.index}`;
return `Connected to ${(inputConnector as Node).nodeId}, connector index ${inputConnector.index}`;
})
.join("\n");
} }
function inputConnectedToText(input: FrontendGraphInput): string { function inputConnectedToText(input: FrontendGraphInput): string {
if (input.connectedTo === undefined) return "Connected to nothing"; return `Connected to:\n${input.connectedTo}`;
if ((input.connectedTo as Node).nodeId === undefined) return `Connected to import index ${input.connectedTo.index}`;
return `Connected to ${(input.connectedTo as Node).nodeId}, connector index ${input.connectedTo.index}`;
}
function primaryOutputConnectedToLayer(node: FrontendNode): boolean {
let firstConnectedNode = Array.from($nodeGraph.nodes.values()).find((n) =>
node.primaryOutput?.connectedTo.some((connector) => {
if ((connector as Node).nodeId === undefined) return false;
if (connector.index !== 0n) return false;
return n.id === (connector as Node).nodeId || false;
}),
);
return firstConnectedNode?.isLayer || false;
}
function primaryInputConnectedToLayer(node: FrontendNode): boolean {
const connectedNode = Array.from($nodeGraph.nodes.values()).find((n) => {
if ((node.primaryInput?.connectedTo as Node) === undefined) return false;
return n.id === (node.primaryInput?.connectedTo as Node).nodeId;
});
return connectedNode?.isLayer || false;
} }
function zipWithUndefined(arr1: FrontendGraphInput[], arr2: FrontendGraphOutput[]) { function zipWithUndefined(arr1: FrontendGraphInput[], arr2: FrontendGraphOutput[]) {
@@ -265,16 +238,12 @@
{ {
value: "node", value: "node",
label: "Node", label: "Node",
action: () => { action: () => toggleLayerDisplay(false, contextMenuData.nodeId),
toggleLayerDisplay(false, contextMenuData.nodeId);
},
}, },
{ {
value: "layer", value: "layer",
label: "Layer", label: "Layer",
action: () => { action: () => toggleLayerDisplay(true, contextMenuData.nodeId),
toggleLayerDisplay(true, contextMenuData.nodeId);
},
}, },
]} ]}
disabled={!canBeToggledBetweenNodeAndLayer(contextMenuData.nodeId)} disabled={!canBeToggledBetweenNodeAndLayer(contextMenuData.nodeId)}
@@ -334,149 +303,181 @@
<!-- Import and Export connectors --> <!-- Import and Export connectors -->
<div class="imports-and-exports" style:transform-origin={`0 0`} style:transform={`translate(${$nodeGraph.transform.x}px, ${$nodeGraph.transform.y}px) scale(${$nodeGraph.transform.scale})`}> <div class="imports-and-exports" style:transform-origin={`0 0`} style:transform={`translate(${$nodeGraph.transform.x}px, ${$nodeGraph.transform.y}px) scale(${$nodeGraph.transform.scale})`}>
{#each $nodeGraph.imports as { outputMetadata, position }, index} {#if $nodeGraph.updateImportsExports}
<svg {#each $nodeGraph.updateImportsExports.imports as frontendOutput, index}
xmlns="http://www.w3.org/2000/svg" {#if frontendOutput}
viewBox="0 0 8 8" <svg
class="connector" xmlns="http://www.w3.org/2000/svg"
data-connector="output" viewBox="0 0 8 8"
data-datatype={outputMetadata.dataType} class="connector"
style:--data-color={`var(--color-data-${outputMetadata.dataType.toLowerCase()})`} data-connector="output"
style:--data-color-dim={`var(--color-data-${outputMetadata.dataType.toLowerCase()}-dim)`} data-datatype={frontendOutput.dataType}
style:--offset-left={position.x / 24} style:--data-color={`var(--color-data-${frontendOutput.dataType.toLowerCase()})`}
style:--offset-top={position.y / 24} style:--data-color-dim={`var(--color-data-${frontendOutput.dataType.toLowerCase()}-dim)`}
> style:--offset-left={($nodeGraph.updateImportsExports.importPosition.x - 8) / 24}
<title>{`${dataTypeTooltip(outputMetadata)}\n\n${outputConnectedToText(outputMetadata)}`}</title> style:--offset-top={($nodeGraph.updateImportsExports.importPosition.y - 8) / 24 + index}
{#if outputMetadata.connectedTo !== undefined} >
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color)" /> <title>{`${dataTypeTooltip(frontendOutput)}\n\n${outputConnectedToText(frontendOutput)}`}</title>
{:else} {#if frontendOutput.connectedTo.length > 0}
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color-dim)" /> <path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color)" />
{/if} {:else}
</svg> <path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color-dim)" />
{/if}
</svg>
<div <div
class="edit-import-export import" on:pointerenter={() => (hoveringImportIndex = index)}
on:pointerenter={() => (hoveringImportIndex = index)} on:pointerleave={() => (hoveringImportIndex = undefined)}
on:pointerleave={() => (hoveringImportIndex = undefined)} class="edit-import-export import"
style:--offset-left={position.x / 24} class:separator-bottom={index === 0 && $nodeGraph.updateImportsExports.addImportExport}
style:--offset-top={position.y / 24} class:separator-top={index === 1 && $nodeGraph.updateImportsExports.addImportExport}
> style:--offset-left={($nodeGraph.updateImportsExports.importPosition.x - 8) / 24}
{#if editingNameImportIndex == index} style:--offset-top={($nodeGraph.updateImportsExports.importPosition.y - 8) / 24 + index}
<input >
class="import-text-input" {#if editingNameImportIndex == index}
type="text" <input
style:width={importsToEdgeTextInputWidth()} class="import-text-input"
bind:this={inputElement} type="text"
bind:value={editingNameText} style:width={importsToEdgeTextInputWidth()}
on:blur={setEditingImportName} bind:this={inputElement}
on:keydown={(e) => e.key === "Enter" && setEditingImportName(e)} bind:value={editingNameText}
/> on:blur={setEditingImportName}
on:keydown={(e) => e.key === "Enter" && setEditingImportName(e)}
/>
{:else}
<p class="import-text" on:dblclick={() => setEditingImportNameIndex(index, frontendOutput.name)}>
{frontendOutput.name}
</p>
{/if}
{#if (hoveringImportIndex === index || editingNameImportIndex === index) && $nodeGraph.updateImportsExports.addImportExport}
<IconButton
size={16}
icon={"Remove"}
class="remove-button-import"
data-index={index}
data-import-text-edge
action={() => {
/* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */
}}
/>
{#if index > 0}
<div class="reorder-drag-grip" title="Reorder this export" />
{/if}
{/if}
</div>
{:else} {:else}
<p class="import-text" on:dblclick={() => setEditingImportNameIndex(index, outputMetadata.name)}>{outputMetadata.name}</p> <div
class="plus"
style:--offset-top={($nodeGraph.updateImportsExports.importPosition.y - 12) / 24}
style:--offset-left={($nodeGraph.updateImportsExports.importPosition.x - 12) / 24}
>
<IconButton size={24} icon="Add" action={() => editor.handle.addPrimaryImport()} />
</div>
{/if} {/if}
{#if hoveringImportIndex === index || editingNameImportIndex === index} {/each}
<IconButton
size={16} {#each $nodeGraph.updateImportsExports.exports as frontendInput, index}
icon={"Remove"} {#if frontendInput}
class="remove-button-import" <svg
data-index={index} xmlns="http://www.w3.org/2000/svg"
data-import-text-edge viewBox="0 0 8 8"
action={() => { class="connector"
/* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */ data-connector="input"
}} data-datatype={frontendInput.dataType}
/> style:--data-color={`var(--color-data-${frontendInput.dataType.toLowerCase()})`}
<div class="reorder-drag-grip" title="Reorder this import"></div> style:--data-color-dim={`var(--color-data-${frontendInput.dataType.toLowerCase()}-dim)`}
{/if} style:--offset-left={($nodeGraph.updateImportsExports.exportPosition.x - 8) / 24}
</div> style:--offset-top={($nodeGraph.updateImportsExports.exportPosition.y - 8) / 24 + index}
{/each} >
{#if $nodeGraph.reorderImportIndex !== undefined} <title>{`${dataTypeTooltip(frontendInput)}\n\n${inputConnectedToText(frontendInput)}`}</title>
{@const position = { {#if frontendInput.connectedTo !== "nothing"}
x: Number($nodeGraph.imports[0].position.x), <path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color)" />
y: Number($nodeGraph.imports[0].position.y) + Number($nodeGraph.reorderImportIndex) * 24, {:else}
}} <path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color-dim)" />
<div class="reorder-bar" style:--offset-left={(position.x - 48) / 24} style:--offset-top={(position.y - 4) / 24} /> {/if}
{/if} </svg>
{#if $nodeGraph.addImport !== undefined} <div
<div class="plus" style:--offset-left={$nodeGraph.addImport.x / 24} style:--offset-top={$nodeGraph.addImport.y / 24}> on:pointerenter={() => (hoveringExportIndex = index)}
<IconButton on:pointerleave={() => (hoveringExportIndex = undefined)}
size={24} class="edit-import-export export"
icon="Add" class:separator-bottom={index === 0 && $nodeGraph.updateImportsExports.addImportExport}
action={() => { class:separator-top={index === 1 && $nodeGraph.updateImportsExports.addImportExport}
/* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */ style:--offset-left={($nodeGraph.updateImportsExports.exportPosition.x - 8) / 24}
}} style:--offset-top={($nodeGraph.updateImportsExports.exportPosition.y - 8) / 24 + index}
/> >
</div> {#if (hoveringExportIndex === index || editingNameExportIndex === index) && $nodeGraph.updateImportsExports.addImportExport}
{/if} {#if index > 0}
{#each $nodeGraph.exports as { inputMetadata, position }, index} <div class="reorder-drag-grip" title="Reorder this export" />
<svg {/if}
xmlns="http://www.w3.org/2000/svg" <IconButton
viewBox="0 0 8 8" size={16}
class="connector" icon={"Remove"}
data-connector="input" class="remove-button-export"
data-datatype={inputMetadata.dataType} data-index={index}
style:--data-color={`var(--color-data-${inputMetadata.dataType.toLowerCase()})`} data-export-text-edge
style:--data-color-dim={`var(--color-data-${inputMetadata.dataType.toLowerCase()}-dim)`} action={() => {
style:--offset-left={position.x / 24} /* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */
style:--offset-top={position.y / 24} }}
> />
<title>{`${dataTypeTooltip(inputMetadata)}\n\n${inputConnectedToText(inputMetadata)}`}</title> {/if}
{#if inputMetadata.connectedTo !== undefined} {#if editingNameExportIndex === index}
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color)" /> <input
type="text"
style:width={exportsToEdgeTextInputWidth()}
bind:this={inputElement}
bind:value={editingNameText}
on:blur={setEditingExportName}
on:keydown={(e) => e.key === "Enter" && setEditingExportName(e)}
/>
{:else}
<p class="export-text" on:dblclick={() => setEditingExportNameIndex(index, frontendInput.name)}>
{frontendInput.name}
</p>
{/if}
</div>
{:else} {:else}
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color-dim)" /> <div
class="plus"
style:--offset-left={($nodeGraph.updateImportsExports.exportPosition.x - 12) / 24}
style:--offset-top={($nodeGraph.updateImportsExports.exportPosition.y - 12) / 24}
>
<IconButton size={24} icon="Add" action={() => editor.handle.addPrimaryExport()} />
</div>
{/if} {/if}
</svg> {/each}
<div
class="edit-import-export export" {#if $nodeGraph.updateImportsExports.addImportExport == true}
on:pointerenter={() => (hoveringExportIndex = index)} <div
on:pointerleave={() => (hoveringExportIndex = undefined)} class="plus"
style:--offset-left={position.x / 24} style:--offset-left={($nodeGraph.updateImportsExports.importPosition.x - 12) / 24}
style:--offset-top={position.y / 24} style:--offset-top={($nodeGraph.updateImportsExports.importPosition.y - 12) / 24 + $nodeGraph.updateImportsExports.imports.length}
> >
{#if hoveringExportIndex === index || editingNameExportIndex === index} <IconButton size={24} icon="Add" action={() => editor.handle.addSecondaryImport()} />
<div class="reorder-drag-grip" title="Reorder this export"></div> </div>
<IconButton <div
size={16} class="plus"
icon={"Remove"} style:--offset-left={($nodeGraph.updateImportsExports.exportPosition.x - 12) / 24}
class="remove-button-export" style:--offset-top={($nodeGraph.updateImportsExports.exportPosition.y - 12) / 24 + $nodeGraph.updateImportsExports.exports.length}
data-index={index} >
data-export-text-edge <IconButton size={24} icon={"Add"} action={() => editor.handle.addSecondaryExport()} />
action={() => { </div>
/* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */ {/if}
}}
/> {#if $nodeGraph.reorderImportIndex !== undefined}
{/if} {@const position = {
{#if editingNameExportIndex === index} x: Number($nodeGraph.updateImportsExports.importPosition.x),
<input y: Number($nodeGraph.updateImportsExports.importPosition.y) + Number($nodeGraph.reorderImportIndex) * 24,
type="text" }}
style:width={exportsToEdgeTextInputWidth()} <div class="reorder-bar" style:--offset-left={(position.x - 48) / 24} style:--offset-top={(position.y - 12) / 24} />
bind:this={inputElement} {/if}
bind:value={editingNameText}
on:blur={setEditingExportName} {#if $nodeGraph.reorderExportIndex !== undefined}
on:keydown={(e) => e.key === "Enter" && setEditingExportName(e)} {@const position = {
/> x: Number($nodeGraph.updateImportsExports.exportPosition.x),
{:else} y: Number($nodeGraph.updateImportsExports.exportPosition.y) + Number($nodeGraph.reorderExportIndex) * 24,
<p class="export-text" on:dblclick={() => setEditingExportNameIndex(index, inputMetadata.name)}>{inputMetadata.name}</p> }}
{/if} <div class="reorder-bar" style:--offset-left={position.x / 24} style:--offset-top={(position.y - 12) / 24} />
</div> {/if}
{/each}
{#if $nodeGraph.reorderExportIndex !== undefined}
{@const position = {
x: Number($nodeGraph.exports[0].position.x),
y: Number($nodeGraph.exports[0].position.y) + Number($nodeGraph.reorderExportIndex) * 24,
}}
<div class="reorder-bar" style:--offset-left={position.x / 24} style:--offset-top={(position.y - 4) / 24} />
{/if}
{#if $nodeGraph.addExport !== undefined}
<div class="plus" style:--offset-left={$nodeGraph.addExport.x / 24} style:--offset-top={$nodeGraph.addExport.y / 24}>
<IconButton
size={24}
icon={"Add"}
action={() => {
/* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */
}}
/>
</div>
{/if} {/if}
</div> </div>
@@ -530,7 +531,7 @@
<title>{`${dataTypeTooltip(node.primaryOutput)}\n\n${outputConnectedToText(node.primaryOutput)}`}</title> <title>{`${dataTypeTooltip(node.primaryOutput)}\n\n${outputConnectedToText(node.primaryOutput)}`}</title>
{#if node.primaryOutput.connectedTo.length > 0} {#if node.primaryOutput.connectedTo.length > 0}
<path d="M0,6.953l2.521,-1.694a2.649,2.649,0,0,1,2.959,0l2.52,1.694v5.047h-8z" fill="var(--data-color)" /> <path d="M0,6.953l2.521,-1.694a2.649,2.649,0,0,1,2.959,0l2.52,1.694v5.047h-8z" fill="var(--data-color)" />
{#if primaryOutputConnectedToLayer(node)} {#if node.primaryOutputConnectedToLayer}
<path d="M0,-3.5h8v8l-2.521,-1.681a2.666,2.666,0,0,0,-2.959,0l-2.52,1.681z" fill="var(--data-color-dim)" /> <path d="M0,-3.5h8v8l-2.521,-1.681a2.666,2.666,0,0,0,-2.959,0l-2.52,1.681z" fill="var(--data-color-dim)" />
{/if} {/if}
{:else} {:else}
@@ -551,9 +552,9 @@
{#if node.primaryInput} {#if node.primaryInput}
<title>{`${dataTypeTooltip(node.primaryInput)}\n\n${validTypesText(node.primaryInput)}\n\n${inputConnectedToText(node.primaryInput)}`}</title> <title>{`${dataTypeTooltip(node.primaryInput)}\n\n${validTypesText(node.primaryInput)}\n\n${inputConnectedToText(node.primaryInput)}`}</title>
{/if} {/if}
{#if node.primaryInput?.connectedTo !== undefined} {#if node.primaryInput?.connectedTo !== "nothing"}
<path d="M0,0H8V8L5.479,6.319a2.666,2.666,0,0,0-2.959,0L0,8Z" fill="var(--data-color)" /> <path d="M0,0H8V8L5.479,6.319a2.666,2.666,0,0,0-2.959,0L0,8Z" fill="var(--data-color)" />
{#if primaryInputConnectedToLayer(node)} {#if node.primaryInputConnectedToLayer}
<path d="M0,10.95l2.52,-1.69c0.89,-0.6,2.06,-0.6,2.96,0l2.52,1.69v5.05h-8v-5.05z" fill="var(--data-color-dim)" /> <path d="M0,10.95l2.52,-1.69c0.89,-0.6,2.06,-0.6,2.96,0l2.52,1.69v5.05h-8v-5.05z" fill="var(--data-color-dim)" />
{/if} {/if}
{:else} {:else}
@@ -937,6 +938,24 @@
margin-top: -5px; margin-top: -5px;
height: 24px; height: 24px;
&.separator-bottom::after,
&.separator-top::before {
content: "";
position: absolute;
background: var(--color-8-uppergray);
height: 1px;
left: -4px;
right: -4px;
}
&.separator-bottom::after {
bottom: -1px;
}
&.separator-top::before {
top: 0;
}
&.import { &.import {
right: calc(100% - var(--offset-left) * 24px); right: calc(100% - var(--offset-left) * 24px);
} }
+13 -64
View File
@@ -12,12 +12,6 @@ export class JsMessage {
} }
const TupleToVec2 = Transform(({ value }: { value: [number, number] | undefined }) => (value === undefined ? undefined : { x: value[0], y: value[1] })); const TupleToVec2 = Transform(({ value }: { value: [number, number] | undefined }) => (value === undefined ? undefined : { x: value[0], y: value[1] }));
const ImportsToVec2Array = Transform(({ obj: { imports } }: { obj: { imports: [FrontendGraphOutput, number, number][] } }) =>
imports.map(([outputMetadata, x, y]) => ({ outputMetadata, position: { x, y } })),
);
const ExportsToVec2Array = Transform(({ obj: { exports } }: { obj: { exports: [FrontendGraphInput, number, number][] } }) =>
exports.map(([inputMetadata, x, y]) => ({ inputMetadata, position: { x, y } })),
);
// const BigIntTupleToVec2 = Transform(({ value }: { value: [bigint, bigint] | undefined }) => (value === undefined ? undefined : { x: Number(value[0]), y: Number(value[1]) })); // const BigIntTupleToVec2 = Transform(({ value }: { value: [bigint, bigint] | undefined }) => (value === undefined ? undefined : { x: Number(value[0]), y: Number(value[1]) }));
@@ -58,17 +52,17 @@ export class UpdateContextMenuInformation extends JsMessage {
} }
export class UpdateImportsExports extends JsMessage { export class UpdateImportsExports extends JsMessage {
@ImportsToVec2Array readonly imports!: (FrontendGraphOutput | undefined)[];
readonly imports!: { outputMetadata: FrontendGraphOutput; position: XY }[];
@ExportsToVec2Array readonly exports!: (FrontendGraphInput | undefined)[];
readonly exports!: { inputMetadata: FrontendGraphInput; position: XY }[];
@TupleToVec2 @TupleToVec2
readonly addImport!: XY | undefined; readonly importPosition!: XY;
@TupleToVec2 @TupleToVec2
readonly addExport!: XY | undefined; readonly exportPosition!: XY;
readonly addImportExport!: boolean;
} }
export class UpdateInSelectedNetwork extends JsMessage { export class UpdateInSelectedNetwork extends JsMessage {
@@ -194,29 +188,6 @@ export type ContextMenuInformation = {
export type FrontendGraphDataType = "General" | "Number" | "Artboard" | "Graphic" | "Raster" | "Vector" | "Color"; export type FrontendGraphDataType = "General" | "Number" | "Artboard" | "Graphic" | "Raster" | "Vector" | "Color";
export class Node {
readonly index!: bigint;
// Omitted if this Node is an Import or Export to/from the node network
readonly nodeId?: bigint;
}
const CreateOutputConnectorOptional = Transform(({ obj }) => {
if (obj.connectedTo == undefined) {
return undefined;
}
if (obj.connectedTo?.export !== undefined) {
return { index: obj.connectedTo?.export };
} else if (obj.connectedTo?.import !== undefined) {
return { index: obj.connectedTo?.import };
} else {
if (obj.connectedTo?.node.inputIndex !== undefined) {
return { nodeId: obj.connectedTo?.node.nodeId, index: obj.connectedTo?.node.inputIndex };
} else {
return { nodeId: obj.connectedTo?.node.nodeId, index: obj.connectedTo?.node.outputIndex };
}
}
});
export class FrontendGraphInput { export class FrontendGraphInput {
readonly dataType!: FrontendGraphDataType; readonly dataType!: FrontendGraphDataType;
@@ -228,28 +199,9 @@ export class FrontendGraphInput {
readonly validTypes!: string[]; readonly validTypes!: string[];
@CreateOutputConnectorOptional readonly connectedTo!: string;
connectedTo!: Node | undefined;
} }
const CreateInputConnectorArray = Transform(({ obj }) => {
const newInputConnectors: Node[] = [];
obj.connectedTo.forEach((connector: any) => {
if (connector.export !== undefined) {
newInputConnectors.push({ index: connector.export });
} else if (connector.import !== undefined) {
newInputConnectors.push({ index: connector.import });
} else {
if (connector.node.inputIndex !== undefined) {
newInputConnectors.push({ nodeId: connector.node.nodeId, index: connector.node.inputIndex });
} else {
newInputConnectors.push({ nodeId: connector.node.nodeId, index: connector.node.outputIndex });
}
}
});
return newInputConnectors;
});
export class FrontendGraphOutput { export class FrontendGraphOutput {
readonly dataType!: FrontendGraphDataType; readonly dataType!: FrontendGraphDataType;
@@ -259,8 +211,7 @@ export class FrontendGraphOutput {
readonly resolvedType!: string; readonly resolvedType!: string;
@CreateInputConnectorArray readonly connectedTo!: string[];
connectedTo!: Node[];
} }
export class FrontendNode { export class FrontendNode {
@@ -274,20 +225,20 @@ export class FrontendNode {
readonly displayName!: string; readonly displayName!: string;
@Type(() => FrontendGraphInput)
readonly primaryInput!: FrontendGraphInput | undefined; readonly primaryInput!: FrontendGraphInput | undefined;
@Type(() => FrontendGraphInput)
readonly exposedInputs!: FrontendGraphInput[]; readonly exposedInputs!: FrontendGraphInput[];
@Type(() => FrontendGraphOutput)
readonly primaryOutput!: FrontendGraphOutput | undefined; readonly primaryOutput!: FrontendGraphOutput | undefined;
@Type(() => FrontendGraphOutput)
readonly exposedOutputs!: FrontendGraphOutput[]; readonly exposedOutputs!: FrontendGraphOutput[];
readonly primaryInputConnectedToLayer!: boolean;
readonly primaryOutputConnectedToLayer!: boolean;
@TupleToVec2 @TupleToVec2
readonly position!: XY | undefined; readonly position!: XY;
// TODO: Store field for the width of the left node chain // TODO: Store field for the width of the left node chain
@@ -298,8 +249,6 @@ export class FrontendNode {
readonly unlocked!: boolean; readonly unlocked!: boolean;
readonly errors!: string | undefined; readonly errors!: string | undefined;
readonly uiOnly!: boolean;
} }
export class FrontendNodeType { export class FrontendNodeType {
+2 -9
View File
@@ -1,7 +1,6 @@
import { writable } from "svelte/store"; import { writable } from "svelte/store";
import { type Editor } from "@graphite/editor"; import { type Editor } from "@graphite/editor";
import type { FrontendGraphOutput, FrontendGraphInput } from "@graphite/messages";
import { import {
type Box, type Box,
type FrontendClickTargets, type FrontendClickTargets,
@@ -37,10 +36,7 @@ export function createNodeGraphState(editor: Editor) {
layerWidths: new Map<bigint, number>(), layerWidths: new Map<bigint, number>(),
chainWidths: new Map<bigint, number>(), chainWidths: new Map<bigint, number>(),
hasLeftInputWire: new Map<bigint, boolean>(), hasLeftInputWire: new Map<bigint, boolean>(),
imports: [] as { outputMetadata: FrontendGraphOutput; position: { x: number; y: number } }[], updateImportsExports: undefined as UpdateImportsExports | undefined,
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>(), nodes: new Map<bigint, FrontendNode>(),
visibleNodes: new Set<bigint>(), visibleNodes: new Set<bigint>(),
/// The index is the exposed input index. The exports have a first key value of u32::MAX. /// The index is the exposed input index. The exports have a first key value of u32::MAX.
@@ -96,10 +92,7 @@ export function createNodeGraphState(editor: Editor) {
}); });
editor.subscriptions.subscribeJsMessage(UpdateImportsExports, (updateImportsExports) => { editor.subscriptions.subscribeJsMessage(UpdateImportsExports, (updateImportsExports) => {
update((state) => { update((state) => {
state.imports = updateImportsExports.imports; state.updateImportsExports = updateImportsExports;
state.exports = updateImportsExports.exports;
state.addImport = updateImportsExports.addImport;
state.addExport = updateImportsExports.addExport;
return state; return state;
}); });
}); });
+24
View File
@@ -314,6 +314,30 @@ impl EditorHandle {
self.dispatch(message); self.dispatch(message);
} }
#[wasm_bindgen(js_name = addPrimaryImport)]
pub fn add_primary_import(&self) {
self.dispatch(DocumentMessage::AddTransaction);
self.dispatch(NodeGraphMessage::AddPrimaryImport);
}
#[wasm_bindgen(js_name = addSecondaryImport)]
pub fn add_secondary_import(&self) {
self.dispatch(DocumentMessage::AddTransaction);
self.dispatch(NodeGraphMessage::AddSecondaryImport);
}
#[wasm_bindgen(js_name = addPrimaryExport)]
pub fn add_primary_export(&self) {
self.dispatch(DocumentMessage::AddTransaction);
self.dispatch(NodeGraphMessage::AddPrimaryExport);
}
#[wasm_bindgen(js_name = addSecondaryExport)]
pub fn add_secondary_export(&self) {
self.dispatch(DocumentMessage::AddTransaction);
self.dispatch(NodeGraphMessage::AddSecondaryExport);
}
/// Toggles minimizing or restoring down the application window /// Toggles minimizing or restoring down the application window
#[wasm_bindgen(js_name = appWindowMaximize)] #[wasm_bindgen(js_name = appWindowMaximize)]
pub fn app_window_maximize(&self) { pub fn app_window_maximize(&self) {
+1 -1
View File
@@ -15,7 +15,7 @@ fn extract_xy<T: Into<DVec2>>(_: impl Ctx, #[implementations(DVec2, IVec2, UVec2
/// The X or Y component of a vec2. /// The X or Y component of a vec2.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType, specta::Type, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType, specta::Type, serde::Serialize, serde::Deserialize)]
#[widget(Dropdown)] #[widget(Radio)]
pub enum XY { pub enum XY {
#[default] #[default]
X, X,