From 1ab681da8cca41899bf8727e36769f94ef7e2ae6 Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 1 Sep 2025 17:39:54 -0700 Subject: [PATCH 01/14] Complete separating node rendering from imports/exports --- .../src/messages/frontend/frontend_message.rs | 19 +- .../document/document_message_handler.rs | 11 +- .../document/node_graph/node_graph_message.rs | 2 - .../node_graph/node_graph_message_handler.rs | 94 +- .../document/node_graph/utility_types.rs | 26 +- .../utility_types/network_interface.rs | 287 +--- .../network_interface/node_graph.rs | 144 +- .../portfolio/document/utility_types/wires.rs | 18 +- .../portfolio/portfolio_message_handler.rs | 2 - .../preferences_message_handler.rs | 3 +- .../src/components/panels/Document.svelte | 17 +- frontend/src/components/views/Graph.svelte | 1401 ++++++++--------- frontend/src/messages.ts | 77 +- frontend/src/state-providers/node-graph.ts | 79 +- 14 files changed, 994 insertions(+), 1186 deletions(-) diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index 59a713b544..963a85cd9d 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -2,10 +2,10 @@ use super::utility_types::{DocumentDetails, MouseCursorIcon, OpenDocument}; use crate::messages::app_window::app_window_message_handler::AppWindowPlatform; use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::node_graph::utility_types::{ - BoxSelection, ContextMenuInformation, FrontendClickTargets, FrontendGraphInput, FrontendGraphOutput, FrontendNodeToRender, FrontendNodeType, FrontendXY, Transform, + BoxSelection, ContextMenuInformation, FrontendClickTargets, FrontendExports, FrontendImport, FrontendNodeToRender, FrontendNodeType, FrontendXY, Transform, }; use crate::messages::portfolio::document::utility_types::nodes::{JsRawBuffer, LayerPanelEntry, RawBuffer}; -use crate::messages::portfolio::document::utility_types::wires::{WirePath, WirePathUpdate}; +use crate::messages::portfolio::document::utility_types::wires::WirePathInProgress; use crate::messages::prelude::*; use crate::messages::tool::utility_types::HintData; use graph_craft::document::NodeId; @@ -127,9 +127,8 @@ pub enum FrontendMessage { }, UpdateImportsExports { /// If the primary import is not visible, then it is None. - imports: Vec>, - /// If the primary export is not visible, then it is None. - exports: Vec>, + imports: Vec>, + exports: FrontendExports, /// The primary import location. #[serde(rename = "importPosition")] import_position: FrontendXY, @@ -140,7 +139,7 @@ pub enum FrontendMessage { #[serde(rename = "addImportExport")] add_import_export: bool, }, - UpdateBox { + UpdateNodeGraphSelectionBox { #[serde(rename = "box")] box_selection: Option, }, @@ -285,10 +284,6 @@ pub enum FrontendMessage { UpdateVisibleNodes { nodes: Vec, }, - UpdateNodeGraphWires { - wires: Vec, - }, - ClearAllNodeGraphWires, UpdateNodeGraphControlBarLayout { #[serde(rename = "layoutTarget")] layout_target: LayoutTarget, @@ -321,8 +316,8 @@ pub enum FrontendMessage { diff: Vec, }, UpdateWirePathInProgress { - #[serde(rename = "wirePath")] - wire_path: Option, + #[serde(rename = "wirePathInProgress")] + wire_path_in_progress: Option, }, UpdateWorkingColorsLayout { #[serde(rename = "layoutTarget")] diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index ccf12a467a..332efc463a 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -474,7 +474,6 @@ impl MessageHandler> for DocumentMes DocumentMessage::EnterNestedNetwork { node_id } => { self.breadcrumb_network_path.push(node_id); self.selection_network_path.clone_from(&self.breadcrumb_network_path); - responses.add(NodeGraphMessage::UnloadWires); responses.add(NodeGraphMessage::SendGraph); responses.add(DocumentMessage::ZoomCanvasToFitAll); responses.add(NodeGraphMessage::SetGridAlignedEdges); @@ -494,7 +493,7 @@ impl MessageHandler> for DocumentMes responses.add(FrontendMessage::UpdateContextMenuInformation { context_menu_information: None }); self.node_graph_handler.wire_in_progress_from_connector = None; self.node_graph_handler.wire_in_progress_to_connector = None; - responses.add(FrontendMessage::UpdateWirePathInProgress { wire_path: None }); + responses.add(FrontendMessage::UpdateWirePathInProgress { wire_path_in_progress: None }); } else { responses.add(DocumentMessage::GraphViewOverlay { open: false }); } @@ -504,7 +503,6 @@ impl MessageHandler> for DocumentMes self.breadcrumb_network_path.pop(); self.selection_network_path.clone_from(&self.breadcrumb_network_path); } - responses.add(NodeGraphMessage::UnloadWires); responses.add(NodeGraphMessage::SendGraph); responses.add(DocumentMessage::PTZUpdate); responses.add(NodeGraphMessage::SetGridAlignedEdges); @@ -557,7 +555,6 @@ impl MessageHandler> for DocumentMes } } DocumentMessage::GraphViewOverlay { open } => { - let opened = !self.graph_view_overlay_open && open; self.graph_view_overlay_open = open; responses.add(FrontendMessage::UpdateGraphViewOverlay { open }); @@ -567,9 +564,6 @@ impl MessageHandler> for DocumentMes responses.add(DocumentMessage::RenderRulers); responses.add(DocumentMessage::RenderScrollbars); - if opened { - responses.add(NodeGraphMessage::UnloadWires); - } if open { responses.add(ToolMessage::DeactivateTools); responses.add(OverlaysMessage::Draw); // Clear the overlays @@ -1949,7 +1943,6 @@ impl DocumentMessageHandler { responses.add(NodeGraphMessage::ForceRunDocumentGraph); // TODO: Remove once the footprint is used to load the imports/export distances from the edge - responses.add(NodeGraphMessage::UnloadWires); responses.add(NodeGraphMessage::SetGridAlignedEdges); Some(previous_network) @@ -1981,8 +1974,6 @@ impl DocumentMessageHandler { responses.add(PortfolioMessage::UpdateOpenDocumentsList); responses.add(NodeGraphMessage::SelectedNodesUpdated); responses.add(NodeGraphMessage::ForceRunDocumentGraph); - responses.add(NodeGraphMessage::UnloadWires); - responses.add(NodeGraphMessage::SendWires); Some(previous_network) } diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs index 37ac29df44..7e9fa5a16b 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs @@ -139,8 +139,6 @@ pub enum NodeGraphMessage { }, SendClickTargets, EndSendClickTargets, - UnloadWires, - SendWires, UpdateVisibleNodes, SendGraph, SetGridAlignedEdges, diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs index 084f2f72cb..4f897806b7 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs @@ -13,7 +13,7 @@ use crate::messages::portfolio::document::utility_types::network_interface::{ self, FlowType, InputConnector, NodeNetworkInterface, NodeTemplate, NodeTypePersistentMetadata, OutputConnector, Previewing, }; use crate::messages::portfolio::document::utility_types::nodes::{CollapsedLayers, LayerPanelEntry}; -use crate::messages::portfolio::document::utility_types::wires::{GraphWireStyle, WirePath, WirePathUpdate, build_vector_wire}; +use crate::messages::portfolio::document::utility_types::wires::{GraphWireStyle, WirePathInProgress, build_vector_wire}; use crate::messages::prelude::*; use crate::messages::tool::common_functionality::auto_panning::AutoPanning; use crate::messages::tool::common_functionality::graph_modification_utils::get_clip_mode; @@ -93,8 +93,6 @@ pub struct NodeGraphMessageHandler { end_index: Option, /// Used to keep track of what nodes are sent to the front end so that only visible ones are sent to the frontend frontend_nodes: Vec, - /// Used to keep track of what wires are sent to the front end so the old ones can be removed - frontend_wires: HashSet<(NodeId, usize)>, } /// NodeGraphMessageHandler always modifies the network which the selected nodes are in. No GraphOperationMessages should be added here, since those messages will always affect the document network. @@ -297,7 +295,7 @@ impl<'a> MessageHandler> for NodeG self.wire_in_progress_type = FrontendGraphDataType::General; self.wire_in_progress_to_connector = None; } - responses.add(FrontendMessage::UpdateWirePathInProgress { wire_path: None }); + responses.add(FrontendMessage::UpdateWirePathInProgress { wire_path_in_progress: None }); responses.add(FrontendMessage::UpdateContextMenuInformation { context_menu_information: self.context_menu.clone(), }); @@ -470,7 +468,6 @@ impl<'a> MessageHandler> for NodeG } responses.add(NodeGraphMessage::UpdateImportsExports); - responses.add(NodeGraphMessage::SendWires); } NodeGraphMessage::ExposePrimaryExport { exposed } => { let export_connector: InputConnector = InputConnector::Export(0); @@ -761,7 +758,7 @@ impl<'a> MessageHandler> for NodeG responses.add(NodeGraphMessage::SelectedNodesSet { nodes: self.selection_before_pointer_down.clone(), }); - responses.add(FrontendMessage::UpdateBox { box_selection: None }); + responses.add(FrontendMessage::UpdateNodeGraphSelectionBox { box_selection: None }); return; } // Abort dragging a wire @@ -770,7 +767,7 @@ impl<'a> MessageHandler> for NodeG self.wire_in_progress_type = FrontendGraphDataType::General; self.wire_in_progress_to_connector = None; responses.add(DocumentMessage::AbortTransaction); - responses.add(FrontendMessage::UpdateWirePathInProgress { wire_path: None }); + responses.add(FrontendMessage::UpdateWirePathInProgress { wire_path_in_progress: None }); return; } @@ -851,7 +848,7 @@ impl<'a> MessageHandler> for NodeG responses.add(FrontendMessage::UpdateContextMenuInformation { context_menu_information: self.context_menu.clone(), }); - responses.add(FrontendMessage::UpdateWirePathInProgress { wire_path: None }); + responses.add(FrontendMessage::UpdateWirePathInProgress { wire_path_in_progress: None }); } // Toggle visibility of clicked node and return @@ -1058,14 +1055,14 @@ impl<'a> MessageHandler> for NodeG to_connector_is_layer, GraphWireStyle::Direct, ); - let path_string = vector_wire.to_svg(); - let wire_path = WirePath { - path_string, + let wire_path = WirePathInProgress { + wire: vector_wire.to_svg(), data_type: self.wire_in_progress_type, thick: false, - dashed: false, }; - responses.add(FrontendMessage::UpdateWirePathInProgress { wire_path: Some(wire_path) }); + responses.add(FrontendMessage::UpdateWirePathInProgress { + wire_path_in_progress: Some(wire_path), + }); } } else if let Some((drag_start, dragged)) = &mut self.drag_start { if drag_start.start_x != point.x || drag_start.start_y != point.y { @@ -1315,7 +1312,8 @@ impl<'a> MessageHandler> for NodeG return None; } - let (wire, is_stack) = network_interface.vector_wire_from_input(&input, preferences.graph_wire_style, selection_network_path)?; + let wire = network_interface.wire_from_input(&input, preferences.graph_wire_style, selection_network_path)?; + let thick = network_interface.wire_is_thick(&input, selection_network_path); let node_bbox = kurbo::Rect::new(node_bbox[0].x, node_bbox[0].y, node_bbox[1].x, node_bbox[1].y).to_path(DEFAULT_ACCURACY); let inside = bezpath_is_inside_bezpath(&wire, &node_bbox, None, None); @@ -1324,7 +1322,7 @@ impl<'a> MessageHandler> for NodeG .segments() .any(|segment| node_bbox.segments().filter_map(|segment| segment.as_line()).any(|line| !segment.intersect_line(line).is_empty())); - (intersect || inside).then_some((input, is_stack)) + (intersect || inside).then_some((input, thick)) }) .collect::>(); @@ -1391,8 +1389,8 @@ impl<'a> MessageHandler> for NodeG self.reordering_export = None; self.reordering_import = None; responses.add(DocumentMessage::EndTransaction); - responses.add(FrontendMessage::UpdateWirePathInProgress { wire_path: None }); - responses.add(FrontendMessage::UpdateBox { box_selection: None }); + responses.add(FrontendMessage::UpdateWirePathInProgress { wire_path_in_progress: None }); + responses.add(FrontendMessage::UpdateNodeGraphSelectionBox { box_selection: None }); responses.add(FrontendMessage::UpdateImportReorderIndex { index: None }); responses.add(FrontendMessage::UpdateExportReorderIndex { index: None }); self.update_node_graph_hints(responses); @@ -1591,17 +1589,6 @@ impl<'a> MessageHandler> for NodeG click_targets: Some(network_interface.collect_frontend_click_targets(breadcrumb_network_path)), }), NodeGraphMessage::EndSendClickTargets => responses.add(FrontendMessage::UpdateClickTargets { click_targets: None }), - NodeGraphMessage::UnloadWires => { - for input in network_interface.node_graph_input_connectors(breadcrumb_network_path) { - network_interface.unload_wire(&input, breadcrumb_network_path); - } - - responses.add(FrontendMessage::ClearAllNodeGraphWires); - } - NodeGraphMessage::SendWires => { - let wires = self.collect_wires(network_interface, preferences.graph_wire_style, breadcrumb_network_path); - responses.add(FrontendMessage::UpdateNodeGraphWires { wires }); - } NodeGraphMessage::UpdateVisibleNodes => { let Some(network_metadata) = network_interface.network_metadata(breadcrumb_network_path) else { return; @@ -1634,7 +1621,7 @@ impl<'a> MessageHandler> for NodeG responses.add(DocumentMessage::DocumentStructureChanged); responses.add(PropertiesPanelMessage::Refresh); responses.add(NodeGraphMessage::UpdateActionButtons); - let nodes_to_render = network_interface.collect_nodes(&self.node_graph_errors, breadcrumb_network_path); + let nodes_to_render = network_interface.collect_nodes(&self.node_graph_errors, preferences.graph_wire_style, breadcrumb_network_path); self.frontend_nodes = nodes_to_render.iter().map(|node| node.metadata.node_id).collect(); let previewed_node = network_interface.previewed_node(breadcrumb_network_path); responses.add(FrontendMessage::UpdateNodeGraphRender { @@ -1650,7 +1637,6 @@ impl<'a> MessageHandler> for NodeG responses.add(NodeGraphMessage::UpdateImportsExports); responses.add(FrontendMessage::UpdateLayerWidths { layer_widths }); - responses.add(NodeGraphMessage::SendWires); self.update_node_graph_hints(responses); } NodeGraphMessage::SetGridAlignedEdges => { @@ -1725,8 +1711,6 @@ impl<'a> MessageHandler> for NodeG Ordering::Equal => {} } } - - responses.add(NodeGraphMessage::SendWires); } NodeGraphMessage::ToggleSelectedAsLayersOrNodes => { let Some(selected_nodes) = network_interface.selected_nodes_in_nested_network(selection_network_path) else { @@ -1746,8 +1730,6 @@ impl<'a> MessageHandler> for NodeG } NodeGraphMessage::ShiftNodePosition { node_id, x, y } => { network_interface.shift_absolute_node_position(&node_id, IVec2::new(x, y), selection_network_path); - - responses.add(NodeGraphMessage::SendWires); } NodeGraphMessage::SetToNodeOrLayer { node_id, is_layer } => { if is_layer && !network_interface.is_eligible_to_be_layer(&node_id, selection_network_path) { @@ -1761,7 +1743,6 @@ impl<'a> MessageHandler> for NodeG }); responses.add(NodeGraphMessage::RunDocumentGraph); responses.add(NodeGraphMessage::SendGraph); - responses.add(NodeGraphMessage::SendWires); } NodeGraphMessage::SetDisplayName { node_id, @@ -1945,12 +1926,12 @@ impl<'a> MessageHandler> for NodeG nodes: nodes.into_iter().collect::>(), }); } - responses.add(FrontendMessage::UpdateBox { box_selection }) + responses.add(FrontendMessage::UpdateNodeGraphSelectionBox { box_selection }) } } NodeGraphMessage::UpdateImportsExports => { - let imports = network_interface.frontend_imports(breadcrumb_network_path); - let exports = network_interface.frontend_exports(breadcrumb_network_path); + let imports = network_interface.frontend_imports(preferences.graph_wire_style, breadcrumb_network_path); + let exports = network_interface.frontend_exports(preferences.graph_wire_style, breadcrumb_network_path); let Some((import_position, export_position)) = network_interface.import_export_position(breadcrumb_network_path) else { log::error!("Could not get import export positions"); @@ -1970,7 +1951,6 @@ impl<'a> MessageHandler> for NodeG let add_import_export = !breadcrumb_network_path.is_empty(); responses.add(NodeGraphMessage::UpdateVisibleNodes); - responses.add(NodeGraphMessage::SendWires); responses.add(FrontendMessage::UpdateImportsExports { imports, exports, @@ -2461,41 +2441,6 @@ impl NodeGraphMessageHandler { } } - fn collect_wires(&mut self, network_interface: &mut NodeNetworkInterface, graph_wire_style: GraphWireStyle, breadcrumb_network_path: &[NodeId]) -> Vec { - let mut added_wires = network_interface - .node_graph_input_connectors(breadcrumb_network_path) - .iter() - .filter_map(|connector| network_interface.newly_loaded_input_wire(connector, graph_wire_style, breadcrumb_network_path)) - .collect::>(); - - let changed_wire_inputs = added_wires.iter().map(|update| (update.id, update.input_index)).collect::>(); - self.frontend_wires.extend(changed_wire_inputs); - - let mut orphaned_wire_inputs = self.frontend_wires.clone(); - self.frontend_wires = network_interface - .node_graph_wire_inputs(breadcrumb_network_path) - .iter() - .filter_map(|visible_wire_input| orphaned_wire_inputs.take(visible_wire_input)) - .collect::>(); - added_wires.extend(orphaned_wire_inputs.into_iter().map(|(id, input_index)| WirePathUpdate { - id, - input_index, - wire_path_update: None, - })); - - if let Some(wire_to_root) = network_interface.wire_to_root(graph_wire_style, breadcrumb_network_path) { - added_wires.push(wire_to_root); - } else { - added_wires.push(WirePathUpdate { - id: NodeId(u64::MAX), - input_index: u32::MAX as usize, - wire_path_update: None, - }) - } - - added_wires - } - fn collect_subgraph_names(network_interface: &mut NodeNetworkInterface, breadcrumb_network_path: &[NodeId]) -> Option> { let mut current_network_path = vec![]; let mut current_network = network_interface.nested_network(¤t_network_path).unwrap(); @@ -2667,7 +2612,6 @@ impl Default for NodeGraphMessageHandler { reordering_import: None, end_index: None, frontend_nodes: Vec::new(), - frontend_wires: HashSet::new(), } } } diff --git a/editor/src/messages/portfolio/document/node_graph/utility_types.rs b/editor/src/messages/portfolio/document/node_graph/utility_types.rs index f004921ef8..7551ad0734 100644 --- a/editor/src/messages/portfolio/document/node_graph/utility_types.rs +++ b/editor/src/messages/portfolio/document/node_graph/utility_types.rs @@ -60,10 +60,10 @@ pub struct FrontendXY { pub struct FrontendGraphInput { #[serde(rename = "dataType")] pub data_type: FrontendGraphDataType, - pub name: String, - pub description: String, #[serde(rename = "resolvedType")] pub resolved_type: String, + pub name: String, + pub description: String, /// Either "nothing", "import index {index}", or "{node name} output {output_index}". #[serde(rename = "connectedToString")] pub connected_to: String, @@ -86,6 +86,26 @@ pub struct FrontendGraphOutput { pub connected_to: Vec, } +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] +pub struct FrontendExport { + pub port: FrontendGraphInput, + pub wire: Option, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] +pub struct FrontendExports { + /// If the primary export is not visible, then it is None. + pub exports: Vec>, + #[serde(rename = "previewWire")] + pub preview_wire: Option, +} + +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] +pub struct FrontendImport { + pub port: FrontendGraphOutput, + pub wires: Vec, +} + // Metadata that is common to nodes and layers #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] pub struct FrontendNodeMetadata { @@ -159,6 +179,8 @@ pub struct FrontendNodeToRender { pub metadata: FrontendNodeMetadata, #[serde(rename = "nodeOrLayer")] pub node_or_layer: FrontendNodeOrLayer, + //TODO: Remove + pub wires: Vec<(String, bool, FrontendGraphDataType)>, } #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface.rs b/editor/src/messages/portfolio/document/utility_types/network_interface.rs index fecba7ed14..2c2482948b 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface.rs @@ -4,9 +4,8 @@ use super::nodes::SelectedNodes; use crate::consts::{EXPORTS_TO_RIGHT_EDGE_PIXEL_GAP, EXPORTS_TO_TOP_EDGE_PIXEL_GAP, GRID_SIZE, IMPORTS_TO_LEFT_EDGE_PIXEL_GAP, IMPORTS_TO_TOP_EDGE_PIXEL_GAP}; use crate::messages::portfolio::document::graph_operation::utility_types::ModifyInputsContext; use crate::messages::portfolio::document::node_graph::document_node_definitions::{DocumentNodeDefinition, resolve_document_node_type}; -use crate::messages::portfolio::document::node_graph::utility_types::{Direction, FrontendClickTargets, FrontendGraphDataType}; +use crate::messages::portfolio::document::node_graph::utility_types::{Direction, FrontendClickTargets}; use crate::messages::portfolio::document::utility_types::network_interface::resolved_types::ResolvedDocumentNodeTypes; -use crate::messages::portfolio::document::utility_types::wires::{GraphWireStyle, WirePath, WirePathUpdate, build_vector_wire}; use crate::messages::tool::common_functionality::graph_modification_utils; use crate::messages::tool::tool_messages::tool_prelude::NumberInputMode; use deserialization::deserialize_node_persistent_metadata; @@ -20,7 +19,6 @@ use graphene_std::subpath::Subpath; use graphene_std::transform::Footprint; use graphene_std::vector::click_target::{ClickTarget, ClickTargetType}; use graphene_std::vector::{PointId, Vector, VectorModificationType}; -use kurbo::BezPath; use serde_json::{Value, json}; use std::collections::{HashMap, HashSet, VecDeque}; use std::hash::Hash; @@ -684,13 +682,6 @@ impl NodeNetworkInterface { Some(&metadata.persistent_metadata) } - fn transient_input_metadata(&self, node_id: &NodeId, index: usize, network_path: &[NodeId]) -> Option<&InputTransientMetadata> { - let metadata = self - .node_metadata(node_id, network_path) - .and_then(|node_metadata| node_metadata.persistent_metadata.input_metadata.get(index))?; - Some(&metadata.transient_metadata) - } - /// Returns the input name to display in the properties panel. If the name is empty then the type is used. pub fn displayed_input_name_and_description(&mut self, node_id: &NodeId, input_index: usize, network_path: &[NodeId]) -> (String, String) { let Some(input_metadata) = self.persistent_input_metadata(node_id, input_index, network_path) else { @@ -1538,30 +1529,6 @@ impl NodeNetworkInterface { return; }; network_metadata.transient_metadata.import_export_ports.unload(); - - // Always unload all wires connected to them as well - let number_of_imports = self.number_of_imports(network_path); - let Some(outward_wires) = self.outward_wires(network_path) else { - log::error!("Could not get outward wires in remove_import"); - return; - }; - let mut input_connectors = Vec::new(); - for import_index in 0..number_of_imports { - let Some(outward_wires_for_import) = outward_wires.get(&OutputConnector::Import(import_index)).cloned() else { - log::error!("Could not get outward wires for import in remove_import"); - return; - }; - input_connectors.extend(outward_wires_for_import); - } - let Some(network) = self.nested_network(network_path) else { - return; - }; - for export_index in 0..network.exports.len() { - input_connectors.push(InputConnector::Export(export_index)); - } - for input in &input_connectors { - self.unload_wire(input, network_path); - } } pub fn modify_import_export(&mut self, network_path: &[NodeId]) -> Option<&ModifyImportExportClickTarget> { @@ -1980,99 +1947,6 @@ impl NodeNetworkInterface { .find_map(|(input_index, click_target)| if index == input_index { click_target.bounding_box_center() } else { None }) } - pub fn newly_loaded_input_wire(&mut self, input: &InputConnector, graph_wire_style: GraphWireStyle, network_path: &[NodeId]) -> Option { - if !self.wire_is_loaded(input, network_path) { - self.load_wire(input, graph_wire_style, network_path); - } else { - return None; - } - - let wire = match input { - InputConnector::Node { node_id, input_index } => { - let input_metadata = self.transient_input_metadata(node_id, *input_index, network_path)?; - let TransientMetadata::Loaded(wire) = &input_metadata.wire else { - log::error!("Could not load wire for input: {input:?}"); - return None; - }; - wire.clone() - } - InputConnector::Export(export_index) => { - let network_metadata = self.network_metadata(network_path)?; - let Some(TransientMetadata::Loaded(wire)) = network_metadata.transient_metadata.wires.get(*export_index) else { - log::error!("Could not load wire for input: {input:?}"); - return None; - }; - wire.clone() - } - }; - Some(wire) - } - - pub fn wire_is_loaded(&mut self, input: &InputConnector, network_path: &[NodeId]) -> bool { - match input { - InputConnector::Node { node_id, input_index } => { - let Some(input_metadata) = self.transient_input_metadata(node_id, *input_index, network_path) else { - log::error!("Input metadata should always exist for input"); - return false; - }; - input_metadata.wire.is_loaded() - } - InputConnector::Export(export_index) => { - let Some(network_metadata) = self.network_metadata(network_path) else { - return false; - }; - match network_metadata.transient_metadata.wires.get(*export_index) { - Some(wire) => wire.is_loaded(), - None => false, - } - } - } - } - - fn load_wire(&mut self, input: &InputConnector, graph_wire_style: GraphWireStyle, network_path: &[NodeId]) { - let dashed = match self.previewing(network_path) { - Previewing::Yes { .. } => match input { - InputConnector::Node { .. } => false, - InputConnector::Export(export_index) => *export_index == 0, - }, - Previewing::No => false, - }; - let Some(wire) = self.wire_path_from_input(input, graph_wire_style, dashed, network_path) else { - log::error!("Could not load wire path from input"); - return; - }; - match input { - InputConnector::Node { node_id, input_index } => { - let Some(node_metadata) = self.node_metadata_mut(node_id, network_path) else { return }; - let Some(input_metadata) = node_metadata.persistent_metadata.input_metadata.get_mut(*input_index) else { - // log::warn!("Node metadata must exist on node: {input:?}"); - return; - }; - let wire_update = WirePathUpdate { - id: *node_id, - input_index: *input_index, - wire_path_update: Some(wire), - }; - input_metadata.transient_metadata.wire = TransientMetadata::Loaded(wire_update); - } - InputConnector::Export(export_index) => { - let Some(network_metadata) = self.network_metadata_mut(network_path) else { return }; - if *export_index >= network_metadata.transient_metadata.wires.len() { - network_metadata.transient_metadata.wires.resize(export_index + 1, TransientMetadata::Unloaded); - } - let Some(input_metadata) = network_metadata.transient_metadata.wires.get_mut(*export_index) else { - return; - }; - let wire_update = WirePathUpdate { - id: NodeId(u64::MAX), - input_index: *export_index, - wire_path_update: Some(wire), - }; - *input_metadata = TransientMetadata::Loaded(wire_update); - } - } - } - pub fn all_input_connectors(&self, network_path: &[NodeId]) -> Vec { let mut input_connectors = Vec::new(); let Some(network) = self.nested_network(network_path) else { @@ -2097,141 +1971,6 @@ impl NodeNetworkInterface { .collect() } - /// Maps to the frontend representation of a wire start. Includes disconnected value wire inputs. - pub fn node_graph_wire_inputs(&self, network_path: &[NodeId]) -> Vec<(NodeId, usize)> { - self.node_graph_input_connectors(network_path) - .iter() - .map(|input| match input { - InputConnector::Node { node_id, input_index } => (*node_id, *input_index), - InputConnector::Export(export_index) => (NodeId(u64::MAX), *export_index), - }) - .chain(std::iter::once((NodeId(u64::MAX), u32::MAX as usize))) - .collect() - } - - fn unload_wires_for_node(&mut self, node_id: &NodeId, network_path: &[NodeId]) { - let number_of_outputs = self.number_of_outputs(node_id, network_path); - let Some(outward_wires) = self.outward_wires(network_path) else { - log::error!("Could not get outward wires in reorder_export"); - return; - }; - let mut input_connectors = Vec::new(); - for output_index in 0..number_of_outputs { - let Some(inputs) = outward_wires.get(&OutputConnector::node(*node_id, output_index)) else { - continue; - }; - input_connectors.extend(inputs.clone()) - } - for input_index in 0..self.number_of_inputs(node_id, network_path) { - input_connectors.push(InputConnector::node(*node_id, input_index)); - } - for input in input_connectors { - self.unload_wire(&input, network_path); - } - } - - pub fn unload_wire(&mut self, input: &InputConnector, network_path: &[NodeId]) { - match input { - InputConnector::Node { node_id, input_index } => { - let Some(node_metadata) = self.node_metadata_mut(node_id, network_path) else { - return; - }; - let Some(input_metadata) = node_metadata.persistent_metadata.input_metadata.get_mut(*input_index) else { - // log::warn!("Node metadata must exist on node: {input:?}"); - return; - }; - input_metadata.transient_metadata.wire = TransientMetadata::Unloaded; - } - InputConnector::Export(export_index) => { - let Some(network_metadata) = self.network_metadata_mut(network_path) else { - return; - }; - if *export_index >= network_metadata.transient_metadata.wires.len() { - network_metadata.transient_metadata.wires.resize(export_index + 1, TransientMetadata::Unloaded); - } - let Some(input_metadata) = network_metadata.transient_metadata.wires.get_mut(*export_index) else { - return; - }; - *input_metadata = TransientMetadata::Unloaded; - } - } - } - - /// When previewing, there may be a second path to the root node. - pub fn wire_to_root(&mut self, graph_wire_style: GraphWireStyle, network_path: &[NodeId]) -> Option { - let input = InputConnector::Export(0); - let current_export = self.upstream_output_connector(&input, network_path)?; - - let root_node = match self.previewing(network_path) { - Previewing::Yes { root_node_to_restore } => root_node_to_restore, - Previewing::No => None, - }?; - - if Some(root_node.node_id) == current_export.node_id() { - return None; - } - let Some(input_position) = self.get_input_center(&input, network_path) else { - log::error!("Could not get input position for wire end in root node: {input:?}"); - return None; - }; - let upstream_output = OutputConnector::node(root_node.node_id, root_node.output_index); - let Some(output_position) = self.get_output_center(&upstream_output, network_path) else { - log::error!("Could not get output position for wire start in root node: {upstream_output:?}"); - return None; - }; - let vertical_end = input.node_id().is_some_and(|node_id| self.is_layer(&node_id, network_path) && input.input_index() == 0); - let vertical_start: bool = upstream_output.node_id().is_some_and(|node_id| self.is_layer(&node_id, network_path)); - let thick = vertical_end && vertical_start; - let vector_wire = build_vector_wire(output_position, input_position, vertical_start, vertical_end, graph_wire_style); - - let path_string = vector_wire.to_svg(); - let data_type = FrontendGraphDataType::displayed_type(&self.input_type(&input, network_path)); - let wire_path_update = Some(WirePath { - path_string, - data_type, - thick, - dashed: false, - }); - - Some(WirePathUpdate { - id: NodeId(u64::MAX), - input_index: u32::MAX as usize, - wire_path_update, - }) - } - - /// Returns the vector subpath and a boolean of whether the wire should be thick. - pub fn vector_wire_from_input(&mut self, input: &InputConnector, wire_style: GraphWireStyle, network_path: &[NodeId]) -> Option<(BezPath, bool)> { - let Some(input_position) = self.get_input_center(input, network_path) else { - log::error!("Could not get dom rect for wire end: {input:?}"); - return None; - }; - // An upstream output could not be found, so the wire does not exist, but it should still be loaded as as empty vector - let Some(upstream_output) = self.upstream_output_connector(input, network_path) else { - return Some((BezPath::new(), false)); - }; - let Some(output_position) = self.get_output_center(&upstream_output, network_path) else { - log::error!("Could not get output port for wire start: {:?}", upstream_output); - return None; - }; - let vertical_end = input.node_id().is_some_and(|node_id| self.is_layer(&node_id, network_path) && input.input_index() == 0); - let vertical_start = upstream_output.node_id().is_some_and(|node_id| self.is_layer(&node_id, network_path)); - let thick = vertical_end && vertical_start; - Some((build_vector_wire(output_position, input_position, vertical_start, vertical_end, wire_style), thick)) - } - - pub fn wire_path_from_input(&mut self, input: &InputConnector, graph_wire_style: GraphWireStyle, dashed: bool, network_path: &[NodeId]) -> Option { - let (vector_wire, thick) = self.vector_wire_from_input(input, graph_wire_style, network_path)?; - let path_string = vector_wire.to_svg(); - let data_type = FrontendGraphDataType::displayed_type(&self.input_type(input, network_path)); - Some(WirePath { - path_string, - data_type, - thick, - dashed, - }) - } - pub fn node_click_targets(&mut self, node_id: &NodeId, network_path: &[NodeId]) -> Option<&DocumentNodeClickTargets> { self.try_load_node_click_targets(node_id, network_path); self.try_get_node_click_targets(node_id, network_path) @@ -2469,7 +2208,6 @@ impl NodeNetworkInterface { return; }; node_metadata.transient_metadata.click_targets.unload(); - self.unload_wires_for_node(node_id, network_path); } pub fn unload_upstream_node_click_targets(&mut self, node_ids: Vec, network_path: &[NodeId]) { @@ -3792,17 +3530,14 @@ impl NodeNetworkInterface { // If a connection is made to the imports (NodeInput::Value { .. } | NodeInput::Scope { .. } | NodeInput::Inline { .. }, NodeInput::Network { .. }) => { self.unload_outward_wires(network_path); - self.unload_wire(input_connector, network_path); } // If a connection to the imports is disconnected (NodeInput::Network { .. }, NodeInput::Value { .. } | NodeInput::Scope { .. } | NodeInput::Inline { .. }) => { self.unload_outward_wires(network_path); - self.unload_wire(input_connector, network_path); } // If a node is disconnected. (NodeInput::Node { .. }, NodeInput::Value { .. } | NodeInput::Scope { .. } | NodeInput::Inline { .. }) => { self.unload_outward_wires(network_path); - self.unload_wire(input_connector, network_path); if let Some((old_upstream_node_id, previous_position)) = previous_metadata { let old_upstream_node_is_layer = self.is_layer(&old_upstream_node_id, network_path); @@ -5848,9 +5583,6 @@ pub struct NodeNetworkTransientMetadata { pub modify_import_export: TransientMetadata, // Distance to the edges of the network, where the import/export ports are displayed. Rounded to nearest grid space when the panning ends. pub rounded_network_edge_distance: TransientMetadata, - - // Wires from the exports - pub wires: Vec>, } #[derive(Debug, Clone)] @@ -6029,12 +5761,11 @@ impl InputPersistentMetadata { } } -#[derive(Debug, Clone, Default)] -struct InputTransientMetadata { - wire: TransientMetadata, - // downstream_protonode: populated for all inputs after each compile - // types: populated for each protonode after each -} +// #[derive(Debug, Clone, Default)] +// struct InputTransientMetadata { +// // downstream_protonode: populated for all inputs after each compile +// // types: populated for each protonode after each +// } /// Persistent metadata for each node in the network, which must be included when creating, serializing, and deserializing saving a node. #[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] @@ -6073,15 +5804,15 @@ impl DocumentNodePersistentMetadata { #[derive(Debug, Default, serde::Serialize, serde::Deserialize)] pub struct InputMetadata { pub persistent_metadata: InputPersistentMetadata, - #[serde(skip)] - transient_metadata: InputTransientMetadata, + // #[serde(skip)] + // transient_metadata: InputTransientMetadata, } impl Clone for InputMetadata { fn clone(&self) -> Self { InputMetadata { persistent_metadata: self.persistent_metadata.clone(), - transient_metadata: Default::default(), + // transient_metadata: Default::default(), } } } diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface/node_graph.rs b/editor/src/messages/portfolio/document/utility_types/network_interface/node_graph.rs index 60a3357725..c06e65d40a 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface/node_graph.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface/node_graph.rs @@ -1,20 +1,25 @@ use glam::{DVec2, IVec2}; use graph_craft::proto::GraphErrors; use graphene_std::uuid::NodeId; +use kurbo::BezPath; use crate::{ consts::{EXPORTS_TO_RIGHT_EDGE_PIXEL_GAP, EXPORTS_TO_TOP_EDGE_PIXEL_GAP, GRID_SIZE, IMPORTS_TO_LEFT_EDGE_PIXEL_GAP, IMPORTS_TO_TOP_EDGE_PIXEL_GAP}, messages::portfolio::document::{ node_graph::utility_types::{ - FrontendGraphDataType, FrontendGraphInput, FrontendGraphOutput, FrontendLayer, FrontendNode, FrontendNodeMetadata, FrontendNodeOrLayer, FrontendNodeToRender, FrontendXY, + FrontendExport, FrontendExports, FrontendGraphDataType, FrontendGraphInput, FrontendGraphOutput, FrontendImport, FrontendLayer, FrontendNode, FrontendNodeMetadata, FrontendNodeOrLayer, + FrontendNodeToRender, FrontendXY, + }, + utility_types::{ + network_interface::{FlowType, InputConnector, NodeNetworkInterface, OutputConnector, Previewing}, + wires::{GraphWireStyle, build_vector_wire}, }, - utility_types::network_interface::{FlowType, InputConnector, NodeNetworkInterface, OutputConnector}, }, }; // Functions used to collect data from the network interface for use in rendering the node graph impl NodeNetworkInterface { - pub fn collect_nodes(&mut self, node_graph_errors: &GraphErrors, network_path: &[NodeId]) -> Vec { + pub fn collect_nodes(&mut self, node_graph_errors: &GraphErrors, wire_style: GraphWireStyle, network_path: &[NodeId]) -> Vec { let Some(network) = self.nested_network(network_path) else { log::error!("Could not get nested network when collecting nodes"); return Vec::new(); @@ -99,7 +104,25 @@ impl NodeNetworkInterface { } }; - let frontend_node_to_render = FrontendNodeToRender { metadata, node_or_layer }; + let wires = (0..self.number_of_displayed_inputs(&node_id, network_path)) + .filter_map(|input_index| { + self.wire_from_input(&InputConnector::node(node_id, input_index), wire_style, network_path) + .filter(|_| { + self.upstream_output_connector(&InputConnector::node(node_id, input_index), network_path) + .is_some_and(|output| !matches!(output, OutputConnector::Import(_))) + }) + .map(|path| path.to_svg()) + .map(|wire| { + ( + wire, + self.wire_is_thick(&InputConnector::node(node_id, input_index), network_path), + FrontendGraphDataType::displayed_type(&self.input_type(&InputConnector::node(node_id, input_index), network_path)), + ) + }) + }) + .collect(); + + let frontend_node_to_render = FrontendNodeToRender { metadata, node_or_layer, wires }; nodes.push(frontend_node_to_render); } @@ -318,16 +341,35 @@ impl NodeNetworkInterface { .is_some_and(|node_id| self.is_layer(&node_id, network_path)) } - pub fn frontend_imports(&mut self, network_path: &[NodeId]) -> Vec> { + /// The imports contain both the output port and the outward wires + pub fn frontend_imports(&mut self, graph_wire_style: GraphWireStyle, network_path: &[NodeId]) -> Vec> { match network_path.split_last() { - Some((node_id, encapsulatingnetwork_path)) => { - let Some(node) = self.document_node(node_id, encapsulatingnetwork_path) else { - log::error!("Could not get node {node_id} in network {encapsulatingnetwork_path:?}"); + Some((node_id, encapsulating_network_path)) => { + let Some(node) = self.document_node(node_id, encapsulating_network_path) else { + log::error!("Could not get node {node_id} in network {encapsulating_network_path:?}"); return Vec::new(); }; let mut frontend_imports = (0..node.inputs.len()) - .map(|import_index| self.frontend_output_from_connector(&OutputConnector::Import(import_index), network_path)) + .map(|import_index| { + let port = self.frontend_output_from_connector(&OutputConnector::Import(import_index), network_path); + port.and_then(|port| { + let outward_wires = self.outward_wires(network_path)?; + let downstream_inputs = outward_wires.get(&OutputConnector::Import(import_index)).cloned()?; + let wires = downstream_inputs + .iter() + .filter_map(|input_connector| { + let Some(wire) = self.wire_from_input(&input_connector, graph_wire_style, network_path) else { + log::error!("Could not get wire path for import input: {input_connector:?}"); + return None; + }; + Some(wire.to_svg()) + }) + .collect::>(); + Some(FrontendImport { port, wires }) + }) + }) .collect::>(); + if frontend_imports.is_empty() { frontend_imports.push(None); } @@ -338,13 +380,29 @@ impl NodeNetworkInterface { } } - pub fn frontend_exports(&mut self, network_path: &[NodeId]) -> Vec> { - let Some(network) = self.nested_network(network_path) else { return Vec::new() }; - let mut frontend_exports = ((0..network.exports.len()).map(|export_index| self.frontend_input_from_connector(&InputConnector::Export(export_index), network_path))).collect::>(); - if frontend_exports.is_empty() { - frontend_exports.push(None); + /// The imports contain the export port, the outward wires, and the preview wire if it exists + pub fn frontend_exports(&mut self, graph_wire_style: GraphWireStyle, network_path: &[NodeId]) -> FrontendExports { + let Some(network) = self.nested_network(network_path) else { + log::error!("Could not get nested network in frontend exports"); + return FrontendExports::default(); + }; + let mut exports = (0..network.exports.len()) + .map(|export_index| { + let export_connector = InputConnector::Export(export_index); + let frontend_export = self.frontend_input_from_connector(&export_connector, network_path); + + frontend_export.and_then(|export| { + let wire = self.wire_from_input(&export_connector, graph_wire_style, network_path).map(|path| path.to_svg()); + Some(FrontendExport { port: export, wire }) + }) + }) + .collect::>(); + + if exports.is_empty() { + exports.push(None); } - frontend_exports + let preview_wire = self.wire_to_root(graph_wire_style, network_path); + FrontendExports { exports, preview_wire } } pub fn import_export_position(&mut self, network_path: &[NodeId]) -> Option<(IVec2, IVec2)> { @@ -424,4 +482,60 @@ impl NodeNetworkInterface { Some((rounded_import_top_left.as_ivec2(), rounded_export_top_right.as_ivec2())) } + + pub fn wire_is_thick(&self, input: &InputConnector, network_path: &[NodeId]) -> bool { + let Some(upstream_output) = self.upstream_output_connector(input, network_path) else { + return false; + }; + let vertical_end = input.node_id().is_some_and(|node_id| self.is_layer(&node_id, network_path) && input.input_index() == 0); + let vertical_start = upstream_output.node_id().is_some_and(|node_id| self.is_layer(&node_id, network_path)); + vertical_end && vertical_start + } + + /// Returns the vector subpath and a boolean of whether the wire should be thick. + pub fn wire_from_input(&mut self, input: &InputConnector, wire_style: GraphWireStyle, network_path: &[NodeId]) -> Option { + let Some(input_position) = self.get_input_center(input, network_path) else { + log::error!("Could not get dom rect for wire end: {input:?}"); + return None; + }; + // An upstream output could not be found + let Some(upstream_output) = self.upstream_output_connector(input, network_path) else { + return None; + }; + let Some(output_position) = self.get_output_center(&upstream_output, network_path) else { + log::error!("Could not get output port for wire start: {:?}", upstream_output); + return None; + }; + let vertical_end = input.node_id().is_some_and(|node_id| self.is_layer(&node_id, network_path) && input.input_index() == 0); + let vertical_start = upstream_output.node_id().is_some_and(|node_id| self.is_layer(&node_id, network_path)); + Some(build_vector_wire(output_position, input_position, vertical_start, vertical_end, wire_style)) + } + + /// When previewing, there may be a second path to the root node. + pub fn wire_to_root(&mut self, graph_wire_style: GraphWireStyle, network_path: &[NodeId]) -> Option { + let input = InputConnector::Export(0); + let current_export = self.upstream_output_connector(&input, network_path)?; + + let root_node = match self.previewing(network_path) { + Previewing::Yes { root_node_to_restore } => root_node_to_restore, + Previewing::No => None, + }?; + + if Some(root_node.node_id) == current_export.node_id() { + return None; + } + let Some(input_position) = self.get_input_center(&input, network_path) else { + log::error!("Could not get input position for wire end in root node: {input:?}"); + return None; + }; + let upstream_output = OutputConnector::node(root_node.node_id, root_node.output_index); + let Some(output_position) = self.get_output_center(&upstream_output, network_path) else { + log::error!("Could not get output position for wire start in root node: {upstream_output:?}"); + return None; + }; + let vertical_start = upstream_output.node_id().is_some_and(|node_id| self.is_layer(&node_id, network_path)); + let vector_wire = build_vector_wire(output_position, input_position, vertical_start, false, graph_wire_style); + + Some(vector_wire.to_svg()) + } } diff --git a/editor/src/messages/portfolio/document/utility_types/wires.rs b/editor/src/messages/portfolio/document/utility_types/wires.rs index ad6ab32843..869bcb8890 100644 --- a/editor/src/messages/portfolio/document/utility_types/wires.rs +++ b/editor/src/messages/portfolio/document/utility_types/wires.rs @@ -1,26 +1,14 @@ use crate::messages::portfolio::document::node_graph::utility_types::FrontendGraphDataType; use glam::{DVec2, IVec2}; -use graphene_std::{uuid::NodeId, vector::misc::dvec2_to_point}; +use graphene_std::vector::misc::dvec2_to_point; use kurbo::{BezPath, DEFAULT_ACCURACY, Line, Point, Shape}; #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] -pub struct WirePath { - #[serde(rename = "pathString")] - pub path_string: String, +pub struct WirePathInProgress { + pub wire: String, #[serde(rename = "dataType")] pub data_type: FrontendGraphDataType, pub thick: bool, - pub dashed: bool, -} - -#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] -pub struct WirePathUpdate { - pub id: NodeId, - #[serde(rename = "inputIndex")] - pub input_index: usize, - // If none, then remove the wire from the map - #[serde(rename = "wirePathUpdate")] - pub wire_path_update: Option, } #[derive(Copy, Clone, Debug, PartialEq, Default, serde::Serialize, serde::Deserialize, specta::Type)] diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index c83346c885..39dec1e08a 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -909,8 +909,6 @@ impl MessageHandler> for Portfolio responses.add(DocumentMessage::GraphViewOverlay { open: node_graph_open }); if node_graph_open { responses.add(NodeGraphMessage::UpdateGraphBarRight); - responses.add(NodeGraphMessage::UnloadWires); - responses.add(NodeGraphMessage::SendWires) } else { responses.add(PortfolioMessage::UpdateDocumentWidgets); } diff --git a/editor/src/messages/preferences/preferences_message_handler.rs b/editor/src/messages/preferences/preferences_message_handler.rs index 214903b9b9..9081376669 100644 --- a/editor/src/messages/preferences/preferences_message_handler.rs +++ b/editor/src/messages/preferences/preferences_message_handler.rs @@ -87,8 +87,7 @@ impl MessageHandler for PreferencesMessageHandler { } PreferencesMessage::GraphWireStyle { style } => { self.graph_wire_style = style; - responses.add(NodeGraphMessage::UnloadWires); - responses.add(NodeGraphMessage::SendWires); + responses.add(NodeGraphMessage::SendGraph); } PreferencesMessage::ViewportZoomWheelRate { rate } => { self.viewport_zoom_wheel_rate = rate; diff --git a/frontend/src/components/panels/Document.svelte b/frontend/src/components/panels/Document.svelte index 0a58272291..dd10a5f7b3 100644 --- a/frontend/src/components/panels/Document.svelte +++ b/frontend/src/components/panels/Document.svelte @@ -564,8 +564,9 @@ {/if} - - +
+ +
-
-
- - {#if $nodeGraph.contextMenuInformation} - + +
+ {#each Array.from($nodeGraph.nodesToRender) as [nodeId, nodeToRender]} + {#if nodeToRender.nodeOrLayer.layer !== undefined} + {@const nodeMetadata = nodeToRender.metadata} + {@const layer = nodeToRender.nodeOrLayer.layer} + {@const clipPathId = String(Math.random()).substring(2)} + {@const layerAreaWidth = $nodeGraph.layerWidths.get(nodeToRender.metadata.nodeId) || 8} + {@const layerChainWidth = layer.chainWidth !== 0 ? layer.chainWidth + 0.5 : 0} + {@const description = (nodeMetadata.reference && $nodeGraph.nodeDescriptions.get(nodeMetadata.reference)) || undefined} +
- {#if typeof $nodeGraph.contextMenuInformation.contextMenuData === "string" && $nodeGraph.contextMenuInformation.contextMenuData === "CreateNode"} - createNode(e.detail)} /> - {:else if $nodeGraph.contextMenuInformation.contextMenuData && "compatibleType" in $nodeGraph.contextMenuInformation.contextMenuData} - createNode(e.detail)} /> - {:else} - {@const contextMenuData = $nodeGraph.contextMenuInformation.contextMenuData} - - Display as - toggleLayerDisplay(false, contextMenuData.nodeId), - }, - { - value: "layer", - label: "Layer", - action: () => toggleLayerDisplay(true, contextMenuData.nodeId), - }, - ]} - disabled={!canBeToggledBetweenNodeAndLayer(contextMenuData.nodeId)} - /> - - - - editor.handle.mergeSelectedNodes()} /> - + {#if nodeMetadata.errors} + {layer.errors} + {layer.errors} {/if} - - {/if} - - - {#if $nodeGraph.clickTargets} -
- - {#each $nodeGraph.clickTargets.nodeClickTargets as pathString} - - {/each} - {#each $nodeGraph.clickTargets.layerClickTargets as pathString} - - {/each} - {#each $nodeGraph.clickTargets.connectorClickTargets as pathString} - - {/each} - {#each $nodeGraph.clickTargets.iconClickTargets as pathString} - - {/each} - - - {#each $nodeGraph.clickTargets.modifyImportExport as pathString} - - {/each} - -
- {/if} +
+ {#if $nodeGraph.thumbnails.has(nodeId)} + {@html $nodeGraph.thumbnails.get(nodeId)} + {/if} + + + {outputTooltip(layer.output)} + 0 ? "var(--data-color)" : "var(--data-color-dim)"} /> - -
- - {#each $nodeGraph.wires.values() as map} - {#each map.values() as { pathString, dataType, thick, dashed }} - {#if thick} - + {#if layer.output.connectedTo.length > 0 && layer.primaryOutputConnectedToLayer} + {/if} - {/each} - {/each} - -
- - -
- {#if $nodeGraph.updateImportsExports} - {#each $nodeGraph.updateImportsExports.imports as frontendOutput, index} - {#if frontendOutput} - - {outputTooltip(frontendOutput)} - {#if frontendOutput.connectedTo.length > 0} - - {:else} - - {/if} - - -
(hoveringImportIndex = index)} - on:pointerleave={() => (hoveringImportIndex = undefined)} - class="edit-import-export import" - class:separator-bottom={index === 0 && $nodeGraph.updateImportsExports.addImportExport} - class:separator-top={index === 1 && $nodeGraph.updateImportsExports.addImportExport} - style:--offset-left={($nodeGraph.updateImportsExports.importPosition.x - 8) / 24} - style:--offset-top={($nodeGraph.updateImportsExports.importPosition.y - 8) / 24 + index} - > - {#if editingNameImportIndex == index} - e.key === "Enter" && setEditingImportName(e)} - /> - {:else} -

setEditingImportNameIndex(index, frontendOutput.name)}> - {frontendOutput.name} -

- {/if} - {#if (hoveringImportIndex === index || editingNameImportIndex === index) && $nodeGraph.updateImportsExports.addImportExport} - { - /* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */ - }} - /> - {#if index > 0} -
- {/if} + + + + {#if layer.bottomInput} + {inputTooltip(layer.bottomInput)} + {/if} + {#if layer.bottomInput?.connectedToNode !== undefined} + + {#if layer.primaryInputConnectedToLayer} + {/if} -
- {:else} -
- editor.handle.addPrimaryImport()} /> -
- {/if} - {/each} - - {#each $nodeGraph.updateImportsExports.exports as frontendInput, index} - {#if frontendInput} + {:else} + + {/if} + +
+ + {#if layer.sideInput} +
- {inputTooltip(frontendInput)} - {#if frontendInput.connectedTo !== "nothing"} - - {:else} - - {/if} + {inputTooltip(layer.sideInput)} + -
(hoveringExportIndex = index)} - on:pointerleave={() => (hoveringExportIndex = undefined)} - class="edit-import-export export" - class:separator-bottom={index === 0 && $nodeGraph.updateImportsExports.addImportExport} - class:separator-top={index === 1 && $nodeGraph.updateImportsExports.addImportExport} - style:--offset-left={($nodeGraph.updateImportsExports.exportPosition.x - 8) / 24} - style:--offset-top={($nodeGraph.updateImportsExports.exportPosition.y - 8) / 24 + index} - > - {#if (hoveringExportIndex === index || editingNameExportIndex === index) && $nodeGraph.updateImportsExports.addImportExport} - {#if index > 0} -
- {/if} - { - /* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */ - }} - /> - {/if} - {#if editingNameExportIndex === index} - e.key === "Enter" && setEditingExportName(e)} - /> - {:else} -

setEditingExportNameIndex(index, frontendInput.name)}> - {frontendInput.name} -

- {/if} -
- {:else} -
- editor.handle.addPrimaryExport()} /> -
- {/if} - {/each} - - {#if $nodeGraph.updateImportsExports.addImportExport == true} -
- editor.handle.addSecondaryImport()} /> -
-
- editor.handle.addSecondaryExport()} />
{/if} - - {#if $nodeGraph.reorderImportIndex !== undefined} - {@const position = { - x: Number($nodeGraph.updateImportsExports.importPosition.x), - y: Number($nodeGraph.updateImportsExports.importPosition.y) + Number($nodeGraph.reorderImportIndex) * 24, +
+ + {nodeMetadata.displayName} +
+
+ { + /* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */ }} -
- {/if} + tooltip={nodeMetadata.visible ? "Visible" : "Hidden"} + /> + + + + + + + + + +
+ {/if} + {/each} - {#if $nodeGraph.reorderExportIndex !== undefined} - {@const position = { - x: Number($nodeGraph.updateImportsExports.exportPosition.x), - y: Number($nodeGraph.updateImportsExports.exportPosition.y) + Number($nodeGraph.reorderExportIndex) * 24, - }} -
+ {#each Array.from($nodeGraph.nodesToRender) as [_, nodeToRender]} + {#each nodeToRender.wires as [wire, thick, dataType]} + + + + {/each} + {/each} + {#each Array.from($nodeGraph.nodesToRender) as [nodeId, nodeToRender]} + {#if nodeToRender.nodeOrLayer.node !== undefined && $nodeGraph.visibleNodes.has(nodeId)} + {@const nodeMetadata = nodeToRender.metadata} + {@const node = nodeToRender.nodeOrLayer.node} + {@const exposedInputsOutputs = collectExposedInputsOutputs(node.inputs, node.outputs)} + {@const clipPathId = String(Math.random()).substring(2)} + {@const description = (nodeMetadata.reference && $nodeGraph.nodeDescriptions.get(nodeMetadata.reference)) || undefined} +
+ {#if nodeMetadata.errors} + {node.errors} + {node.errors} {/if} - {/if} -
- -
- {#each Array.from($nodeGraph.nodesToRender) as [nodeId, nodeToRender]} - {#if nodeToRender.nodeOrLayer.layer !== undefined} - {@const nodeMetadata = nodeToRender.metadata} - {@const layer = nodeToRender.nodeOrLayer.layer} - {@const clipPathId = String(Math.random()).substring(2)} - {@const layerAreaWidth = $nodeGraph.layerWidths.get(nodeToRender.metadata.nodeId) || 8} - {@const layerChainWidth = layer.chainWidth !== 0 ? layer.chainWidth + 0.5 : 0} - {@const description = (nodeMetadata.reference && $nodeGraph.nodeDescriptions.get(nodeMetadata.reference)) || undefined} -
- {#if nodeMetadata.errors} - {layer.errors} - {layer.errors} - {/if} -
- {#if $nodeGraph.thumbnails.has(nodeId)} - {@html $nodeGraph.thumbnails.get(nodeId)} - {/if} - + +
+ + + {nodeMetadata.displayName} +
+ + {#if exposedInputsOutputs.length > 0} +
+ {#each exposedInputsOutputs as [input, output]} +
+ + {input?.name ?? output?.name ?? ""} + +
+ {/each} +
+ {/if} + +
+ {#each node.inputs as input} + {#if input !== undefined} - {outputTooltip(layer.output)} + {inputTooltip(input)} 0 ? "var(--data-color)" : "var(--data-color-dim)"} + 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.connectedToString === "nothing" ? "-dim" : ""})`} /> - - {#if layer.output.connectedTo.length > 0 && layer.primaryOutputConnectedToLayer} - - {/if} - + {/if} + {/each} +
+ +
+ {#each node.outputs as output} + {#if output !== undefined} - {#if layer.bottomInput} - {inputTooltip(layer.bottomInput)} - {/if} - {#if layer.bottomInput?.connectedToNode !== undefined} - - {#if layer.primaryInputConnectedToLayer} - - {/if} - {:else} - - {/if} - -
- - {#if layer.sideInput} -
- - {inputTooltip(layer.sideInput)} - - -
- {/if} -
- - {nodeMetadata.displayName} -
-
- { - /* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */ - }} - tooltip={nodeMetadata.visible ? "Visible" : "Hidden"} - /> - - - - - - - - - -
- {/if} - {/each} - -
- - {#each $nodeGraph.wires.values() as map} - {#each map.values() as { pathString, dataType, thick, dashed }} - {#if !thick} + {outputTooltip(output)} - {/if} - {/each} + + {/if} {/each} - {#if $nodeGraph.wirePathInProgress} - - {/if} +
+ + + + + +
- {#each Array.from($nodeGraph.nodesToRender) as [nodeId, nodeToRender]} - {#if nodeToRender.nodeOrLayer.node !== undefined && $nodeGraph.visibleNodes.has(nodeId)} - {@const nodeMetadata = nodeToRender.metadata} - {@const node = nodeToRender.nodeOrLayer.node} - {@const exposedInputsOutputs = collectExposedInputsOutputs(node.inputs, node.outputs)} - {@const clipPathId = String(Math.random()).substring(2)} - {@const description = (nodeMetadata.reference && $nodeGraph.nodeDescriptions.get(nodeMetadata.reference)) || undefined} + {/if} + {/each} +
+ +
+ + {#if $nodeGraph.contextMenuInformation} + + {#if typeof $nodeGraph.contextMenuInformation.contextMenuData === "string" && $nodeGraph.contextMenuInformation.contextMenuData === "CreateNode"} + createNode(e.detail)} /> + {:else if $nodeGraph.contextMenuInformation.contextMenuData && "compatibleType" in $nodeGraph.contextMenuInformation.contextMenuData} + createNode(e.detail)} /> + {:else} + {@const contextMenuData = $nodeGraph.contextMenuInformation.contextMenuData} + + Display as + toggleLayerDisplay(false, contextMenuData.nodeId), + }, + { + value: "layer", + label: "Layer", + action: () => toggleLayerDisplay(true, contextMenuData.nodeId), + }, + ]} + disabled={!canBeToggledBetweenNodeAndLayer(contextMenuData.nodeId)} + /> + + + + editor.handle.mergeSelectedNodes()} /> + + {/if} + + {/if} + + + {#if $nodeGraph.clickTargets} +
+ + {#each $nodeGraph.clickTargets.nodeClickTargets as pathString} + + {/each} + {#each $nodeGraph.clickTargets.layerClickTargets as pathString} + + {/each} + {#each $nodeGraph.clickTargets.connectorClickTargets as pathString} + + {/each} + {#each $nodeGraph.clickTargets.iconClickTargets as pathString} + + {/each} + + + {#each $nodeGraph.clickTargets.modifyImportExport as pathString} + + {/each} + +
+ {/if} + + + + {#if $nodeGraph.wirePathInProgress} + + {/if} + + + +
+ {#if $nodeGraph.updateImportsExports} + {#each $nodeGraph.updateImportsExports.imports as frontendImport, index} + {#if frontendImport} + {@const frontendOutput = frontendImport.port} + + {#each frontendImport.wires as wire} + + + + {/each} + + {outputTooltip(frontendOutput)} + {#if frontendOutput.connectedTo.length > 0} + + {:else} + + {/if} + +
(hoveringImportIndex = index)} + on:pointerleave={() => (hoveringImportIndex = undefined)} + class="edit-import-export import" + class:separator-bottom={index === 0 && $nodeGraph.updateImportsExports.addImportExport} + class:separator-top={index === 1 && $nodeGraph.updateImportsExports.addImportExport} + style:--offset-left={($nodeGraph.updateImportsExports.importPosition.x - 8) / 24} + style:--offset-top={($nodeGraph.updateImportsExports.importPosition.y - 8) / 24 + index} > - {#if nodeMetadata.errors} - {node.errors} - {node.errors} + {#if editingNameImportIndex == index} + e.key === "Enter" && setEditingImportName(e)} + /> + {:else} +

setEditingImportNameIndex(index, frontendOutput.name)}> + {frontendOutput.name} +

{/if} - -
- - - {nodeMetadata.displayName} -
- - {#if exposedInputsOutputs.length > 0} -
- {#each exposedInputsOutputs as [input, output]} -
- - {input?.name ?? output?.name ?? ""} - -
- {/each} -
+ {#if (hoveringImportIndex === index || editingNameImportIndex === index) && $nodeGraph.updateImportsExports.addImportExport} + { + /* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */ + }} + /> + {#if index > 0} +
+ {/if} {/if} - -
- {#each node.inputs as input} - {#if input !== undefined} - - {inputTooltip(input)} - - - {/if} - {/each} -
- -
- {#each node.outputs as output} - {#if output !== undefined} - - {outputTooltip(output)} - - - {/if} - {/each} -
- - - - - - - +
+ {:else} +
+ editor.handle.addPrimaryImport()} />
{/if} {/each} -
-
- - - {#if $nodeGraph.box} -
- {/if} -
- - diff --git a/node-graph/gcore/src/node_graph_overlay/nodes_and_wires.rs b/node-graph/gcore/src/node_graph_overlay/nodes_and_wires.rs index 571173790c..42d3d09c65 100644 --- a/node-graph/gcore/src/node_graph_overlay/nodes_and_wires.rs +++ b/node-graph/gcore/src/node_graph_overlay/nodes_and_wires.rs @@ -15,7 +15,7 @@ use crate::{ transform::ApplyTransform, vector::{ Vector, - style::{Fill, Stroke}, + style::{Fill, Stroke, StrokeAlign}, }, }; @@ -102,16 +102,17 @@ pub fn draw_nodes(nodes: &Vec) -> Table { let border_mask_table = Table::new_from_row(border_mask_row); node_table.push(TableRow::new_from_element(Graphic::Vector(border_mask_table))); - // Border table is implemented as a clip mask + // Border is implemented as a clip mask let mut border_table = Table::new(); let mut border_vector = Vector::from_bezpath(node_bez_path); - let primary_output_color = frontend_node + let border_color = frontend_node .primary_output .as_ref() .map(|primary_output| primary_output.data_type.data_color_dim()) .unwrap_or(FrontendGraphDataType::General.data_color_dim()); - let border_color = Color::from_rgba8_no_srgb(primary_output_color).unwrap(); - border_vector.style.stroke = Some(crate::vector::style::Stroke::new(Some(border_color), 1.)); + let stroke = Stroke::new(Some(border_color), 1.); + // stroke.align = StrokeAlign::Inside; + border_vector.style.stroke = Some(stroke); let mut border_vector_row = TableRow::new_from_element(border_vector); border_vector_row.alpha_blending.clip = true; border_table.push(border_vector_row); @@ -182,22 +183,22 @@ pub fn draw_nodes(nodes: &Vec) -> Table { // Input and output ports let mut ports_table = Table::new(); if let Some(primary_input) = &frontend_node.primary_input { - let mut row = port_row(&primary_input.data_type); + let mut row = port_row(&primary_input.data_type, primary_input.connected_to_node.is_some()); row.transform = DAffine2::from_translation(DVec2::new(0., 12.)); ports_table.push(row); } for (index, secondary_input) in frontend_node.secondary_inputs.iter().enumerate() { - let mut row = port_row(&secondary_input.data_type); + let mut row = port_row(&secondary_input.data_type, secondary_input.connected_to_node.is_some()); row.transform = DAffine2::from_translation(DVec2::new(0., 12. + GRID_SIZE * (index + 1) as f64)); ports_table.push(row); } if let Some(primary_output) = &frontend_node.primary_output { - let mut row = port_row(&primary_output.data_type); + let mut row = port_row(&primary_output.data_type, true); row.transform = DAffine2::from_translation(DVec2::new(5. * GRID_SIZE, 12.)); ports_table.push(row); } for (index, secondary_output) in frontend_node.secondary_outputs.iter().enumerate() { - let mut row = port_row(&secondary_output.data_type); + let mut row = port_row(&secondary_output.data_type, true); row.transform = DAffine2::from_translation(DVec2::new(5. * GRID_SIZE, 12. + GRID_SIZE * (index + 1) as f64)); ports_table.push(row); } @@ -212,7 +213,7 @@ pub fn draw_nodes(nodes: &Vec) -> Table { pub fn draw_layers(nodes: &Vec) -> (Table, Table) { let mut layer_table = Table::new(); - let mut side_ports = Table::new(); + let mut side_ports_table = Table::new(); for node_to_render in nodes { if let Some(frontend_layer) = node_to_render.node_or_layer.layer.as_ref() { // The layer position is the top left of the thumbnail @@ -246,16 +247,14 @@ pub fn draw_layers(nodes: &Vec) -> (Table, Table< 0. }; - // Text starts at thumbnail + left padding - let text_start = 12. + 8.; + let text_left_padding = 8.; let right_text_edge = 8. + text_width; - let rounded_text_edge = (right_text_edge as f64 / 24.).ceil() * 24.; + // Text starts at thumbnail + left padding + let rounded_text_edge = ((12. + right_text_edge as f64) / 24.).ceil() * 24.; - let rounded_layer_width_pixels = rounded_text_edge + 24.; - // add the left thumbnail gap - let layer_right_edge_width = rounded_layer_width_pixels + 12.; + let rounded_layer_width_pixels = rounded_text_edge + 12.; - let right_layer_width = layer_right_edge_width.max(4.5 * GRID_SIZE); + let right_layer_width = rounded_layer_width_pixels.max(4.5 * GRID_SIZE); let thumbnail_width = 3. * GRID_SIZE; let full_layer_width = chain_width + thumbnail_width + right_layer_width; @@ -321,7 +320,9 @@ pub fn draw_layers(nodes: &Vec) -> (Table, Table< let bez_path = border_rect.to_path(BEZ_PATH_TOLERANCE); let mut border_vector = Vector::from_bezpath(bez_path); let border_color = Color::from_rgba8_no_srgb(COLOR_5_DULLGRAY).unwrap(); - border_vector.style.stroke = Some(crate::vector::style::Stroke::new(Some(border_color), 1.)); + let stroke = Stroke::new(Some(border_color), 1.); + // stroke.align = StrokeAlign::Inside; + border_vector.style.stroke = Some(stroke); let mut layer_border_clip = TableRow::new_from_element(border_vector); layer_border_clip.alpha_blending.clip = true; border_table.push(layer_border_clip); @@ -330,7 +331,7 @@ pub fn draw_layers(nodes: &Vec) -> (Table, Table< // The top layer contains the ports,thumbnail,text, etc for text_row in text_table.iter_mut() { text_row.element.style.fill = Fill::Solid(Color::WHITE); - *text_row.transform = DAffine2::from_translation(layer_position + DVec2::new(thumbnail_width + text_start, 16.)); + *text_row.transform = DAffine2::from_translation(layer_position + DVec2::new(thumbnail_width + text_left_padding, 16.)); } let top_layer = text_table; layer_table.push(TableRow::new_from_element(Graphic::Vector(top_layer))); @@ -338,34 +339,40 @@ pub fn draw_layers(nodes: &Vec) -> (Table, Table< // Ports let mut ports_table = Table::new(); if let Some(side_input) = &frontend_layer.side_input { - let mut port = port_row(&side_input.data_type); + let mut port: TableRow = port_row(&side_input.data_type, side_input.connected_to_node.is_some()); port.transform = DAffine2::from_translation(DVec2::new(layer_position.x - 15., layer_position.y + GRID_SIZE - 4.)); - ports_table.push(port); + side_ports_table.push(port); } let top_port = BezPath::from_svg("M0,6.953l2.521,-1.694a2.649,2.649,0,0,1,2.959,0l2.52,1.694v5.047h-8z").unwrap(); let mut vector = Vector::from_bezpath(top_port); - vector.style.fill = Fill::Solid(Color::from_rgba8_no_srgb(frontend_layer.output.data_type.data_color()).unwrap()); - let mut side_port = TableRow::new_from_element(vector); - side_port.transform = DAffine2::from_translation(DVec2::new(frontend_layer.position.x as f64 * 24. + GRID_SIZE * 2. - 4., layer_position.y - 12.)); - side_ports.push(side_port); + vector.style.fill = Fill::Solid(frontend_layer.output.data_type.data_color()); + let mut top_port = TableRow::new_from_element(vector); + top_port.transform = DAffine2::from_translation(DVec2::new(frontend_layer.position.x as f64 * 24. + GRID_SIZE * 2. - 4., layer_position.y - 12.)); + ports_table.push(top_port); + if frontend_layer.primary_output_connected_to_layer { let top_wire_cap = BezPath::from_svg("M0,-3.5h8v8l-2.521,-1.681a2.666,2.666,0,0,0,-2.959,0l-2.52,1.681z").unwrap(); let mut vector = Vector::from_bezpath(top_wire_cap); - vector.style.fill = Fill::Solid(Color::from_rgba8_no_srgb(frontend_layer.output.data_type.data_color_dim()).unwrap()); + vector.style.fill = Fill::Solid(frontend_layer.output.data_type.data_color_dim()); let mut vector_row = TableRow::new_from_element(vector); vector_row.transform = DAffine2::from_translation(DVec2::new(frontend_layer.position.x as f64 * 24. + GRID_SIZE * 2. - 4., layer_position.y - 12.)); ports_table.push(vector_row); } let bottom_port = BezPath::from_svg("M0,0H8V8L5.479,6.319a2.666,2.666,0,0,0-2.959,0L0,8Z").unwrap(); let mut vector = Vector::from_bezpath(bottom_port); - vector.style.fill = Fill::Solid(Color::from_rgba8_no_srgb(frontend_layer.bottom_input.data_type.data_color()).unwrap()); + let mut bottom_port_fill = if frontend_layer.bottom_input.connected_to_node.is_some() { + frontend_layer.bottom_input.data_type.data_color() + } else { + frontend_layer.bottom_input.data_type.data_color_dim() + }; + vector.style.fill = Fill::Solid(bottom_port_fill); let mut vector_row = TableRow::new_from_element(vector); vector_row.transform = DAffine2::from_translation(DVec2::new(frontend_layer.position.x as f64 * 24. + GRID_SIZE * 2. - 4., layer_position.y + 2. * GRID_SIZE)); ports_table.push(vector_row); if frontend_layer.primary_input_connected_to_layer { let bottom_port_cap = BezPath::from_svg("M0,10.95l2.52,-1.69c0.89,-0.6,2.06,-0.6,2.96,0l2.52,1.69v5.05h-8v-5.05z").unwrap(); let mut vector = Vector::from_bezpath(bottom_port_cap); - vector.style.fill = Fill::Solid(Color::from_rgba8_no_srgb(frontend_layer.bottom_input.data_type.data_color_dim()).unwrap()); + vector.style.fill = Fill::Solid(frontend_layer.bottom_input.data_type.data_color_dim()); let mut vector_row = TableRow::new_from_element(vector); vector_row.transform = DAffine2::from_translation(DVec2::new(frontend_layer.position.x as f64 * 24. + GRID_SIZE * 2. - 4., layer_position.y + 2. * GRID_SIZE)); ports_table.push(vector_row); @@ -406,11 +413,59 @@ pub fn draw_layers(nodes: &Vec) -> (Table, Table< icons_table.push(grip_row); } layer_table.push(TableRow::new_from_element(Graphic::Vector(icons_table))); + + // Thumbnail border/bg + let border = RoundedRect::new(layer_position.x, layer_position.y, layer_position.x + thumbnail_width, layer_position.y + 2. * GRID_SIZE, 2.); + let mut border_vec = Vector::from_bezpath(border.to_path(BEZ_PATH_TOLERANCE)); + let stroke = Stroke::new(Some(frontend_layer.output.data_type.data_color_dim()), 1.); + // stroke.align = StrokeAlign::Inside; + border_vec.style.stroke = Some(stroke); + border_vec.style.fill = Fill::Solid(Color::from_rgba8_no_srgb(COLOR_2_MILDBLACK).unwrap()); + layer_table.push(TableRow::new_from_element(Graphic::Vector(Table::new_from_element(border_vec)))); + + // Region to display thumbnail + let clip_vector = Vector::from_bezpath( + Rect::new( + layer_position.x + 2., + layer_position.y + 2., + layer_position.x + thumbnail_width - 2., + layer_position.y + GRID_SIZE * 2. - 2., + ) + .to_path(BEZ_PATH_TOLERANCE), + ); + layer_table.push(TableRow::new_from_element(Graphic::Vector(Table::new_from_row(TableRow::new_from_element(clip_vector))))); + + // Inner thumbnail + let mut inner_thumbnail_table = Table::new(); + for col in 0..9 { + for row in 0..6 { + let fill = if (col + row) % 2 == 0 { + Color::from_rgba8_no_srgb(COLOR_C_BRIGHTGRAY).unwrap() + } else { + Color::from_rgba8_no_srgb(COLOR_F_WHITE).unwrap() + }; + let mut vector = Vector::from_bezpath( + Rect::new( + 2. + 8. * col as f64 + layer_position.x, + 2. + 8. * row as f64 + layer_position.y, + 2. + 8. * col as f64 + layer_position.x + 9., + 2. + 8. * row as f64 + layer_position.y + 9., + ) + .to_path(BEZ_PATH_TOLERANCE), + ); + vector.style.fill = Fill::Solid(fill); + inner_thumbnail_table.push(TableRow::new_from_element(vector)); + } + } + let mut thumbnail_row = TableRow::new_from_element(Graphic::Vector(inner_thumbnail_table)); + thumbnail_row.alpha_blending.clip = true; + let graphic_table = Table::new_from_row(thumbnail_row); + layer_table.push(TableRow::new_from_element(Graphic::Graphic(graphic_table))); } } let mut ports_table = Table::new(); - ports_table.push(TableRow::new_from_element(Graphic::Vector(side_ports))); + ports_table.push(TableRow::new_from_element(Graphic::Vector(side_ports_table))); (layer_table, ports_table) } @@ -455,12 +510,17 @@ fn node_first_row(x0: f64, y0: f64, rounded_bottom: bool) -> Vector { vector } -fn port_row(data_type: &FrontendGraphDataType) -> TableRow { +fn port_row(data_type: &FrontendGraphDataType, full_brightness: bool) -> TableRow { let path = BezPath::from_svg("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").unwrap_or_else(|e| { panic!("Could not parse port svg from string: {}", e); }); let mut vector = Vector::from_bezpath(path); - vector.style.fill = Fill::Solid(Color::from_rgba8_no_srgb(data_type.data_color()).unwrap()); + let fill = if full_brightness { + Fill::Solid(data_type.data_color()) + } else { + Fill::Solid(data_type.data_color_dim()) + }; + vector.style.fill = fill; TableRow::new_from_element(vector) } @@ -470,7 +530,7 @@ pub fn draw_wires(nodes: &mut Vec) -> Table { for (wire_string, thick, data_type) in &mut node.wires { let mut wire_vector = Vector::from_bezpath(std::mem::take(wire_string)); let weight = if *thick { 8. } else { 2. }; - wire_vector.style.set_stroke(Stroke::new(Some(Color::from_rgba8_no_srgb(data_type.data_color_dim()).unwrap()), weight)); + wire_vector.style.set_stroke(Stroke::new(Some(data_type.data_color_dim()), weight)); wire_table.push(TableRow::new_from_element(wire_vector)); } } diff --git a/node-graph/gcore/src/node_graph_overlay/types.rs b/node-graph/gcore/src/node_graph_overlay/types.rs index 1acfcc3a0b..abf537b38b 100644 --- a/node-graph/gcore/src/node_graph_overlay/types.rs +++ b/node-graph/gcore/src/node_graph_overlay/types.rs @@ -1,4 +1,5 @@ use glam::{DAffine2, DVec2}; +use graphene_core_shaders::color::Color; use kurbo::BezPath; use crate::{node_graph_overlay::consts::*, uuid::NodeId}; @@ -214,8 +215,8 @@ pub enum FrontendGraphDataType { } impl FrontendGraphDataType { - pub fn data_color(&self) -> &'static str { - match self { + pub fn data_color(&self) -> Color { + let color_str = match self { FrontendGraphDataType::General => COLOR_DATA_GENERAL, FrontendGraphDataType::Number => COLOR_DATA_NUMBER, FrontendGraphDataType::Artboard => COLOR_DATA_ARTBOARD, @@ -225,10 +226,11 @@ impl FrontendGraphDataType { FrontendGraphDataType::Color => COLOR_DATA_COLOR, FrontendGraphDataType::Gradient => COLOR_DATA_GRADIENT, FrontendGraphDataType::Typography => COLOR_DATA_TYPOGRAPHY, - } + }; + Color::from_rgba8_no_srgb(color_str).unwrap() } - pub fn data_color_dim(&self) -> &'static str { - match self { + pub fn data_color_dim(&self) -> Color { + let color_str = match self { FrontendGraphDataType::General => COLOR_DATA_GENERAL_DIM, FrontendGraphDataType::Number => COLOR_DATA_NUMBER_DIM, FrontendGraphDataType::Artboard => COLOR_DATA_ARTBOARD_DIM, @@ -238,6 +240,7 @@ impl FrontendGraphDataType { FrontendGraphDataType::Color => COLOR_DATA_COLOR_DIM, FrontendGraphDataType::Gradient => COLOR_DATA_GRADIENT_DIM, FrontendGraphDataType::Typography => COLOR_DATA_TYPOGRAPHY_DIM, - } + }; + Color::from_rgba8_no_srgb(color_str).unwrap() } } From 8caf9317a55ca9ecfc728dce2cf9add96cf5b3b7 Mon Sep 17 00:00:00 2001 From: Adam Date: Sat, 6 Sep 2025 21:27:24 -0700 Subject: [PATCH 14/14] thumbnails --- .../document/node_graph/node_graph_message.rs | 5 ++ .../node_graph/node_graph_message_handler.rs | 6 ++ editor/src/node_graph_executor.rs | 37 ++++++---- editor/src/node_graph_executor/runtime.rs | 39 +++++----- node-graph/gcore/src/node_graph_overlay.rs | 2 +- .../src/node_graph_overlay/nodes_and_wires.rs | 41 ++++++++--- .../gcore/src/node_graph_overlay/types.rs | 24 ++++++- node-graph/gsvg-renderer/src/renderer.rs | 71 +++++++++++++++++++ 8 files changed, 182 insertions(+), 43 deletions(-) diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs index 6f0b606ed0..06c2e8d2bd 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs @@ -7,6 +7,7 @@ use glam::IVec2; use graph_craft::document::value::TaggedValue; use graph_craft::document::{NodeId, NodeInput}; use graph_craft::proto::GraphErrors; +use graphene_std::Graphic; use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypesDelta; #[impl_message(Message, DocumentMessage, NodeGraph)] @@ -218,6 +219,10 @@ pub enum NodeGraphMessage { UpdateImportsExports, UpdateLayerPanel, UpdateNewNodeGraph, + UpdateThumbnail { + node_id: NodeId, + graphic: Graphic, + }, UpdateTypes { #[serde(skip)] resolved_types: ResolvedDocumentNodeTypesDelta, diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs index a83056207e..5007646aa7 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs @@ -92,6 +92,8 @@ pub struct NodeGraphMessageHandler { reordering_export: Option, /// The end index of the moved connector end_index: Option, + // The rendered string for each thumbnail + pub thumbnails: HashMap, } /// NodeGraphMessageHandler always modifies the network which the selected nodes are in. No GraphOperationMessages should be added here, since those messages will always affect the document network. @@ -1924,6 +1926,9 @@ impl<'a> MessageHandler> for NodeG responses.add(NodeGraphMessage::SendGraph); } + NodeGraphMessage::UpdateThumbnail { node_id, graphic } => { + self.thumbnails.insert(node_id, graphic); + } NodeGraphMessage::UpdateTypes { resolved_types, node_graph_errors } => { network_interface.resolved_types.update(resolved_types); self.node_graph_errors = node_graph_errors; @@ -2559,6 +2564,7 @@ impl Default for NodeGraphMessageHandler { reordering_export: None, reordering_import: None, end_index: None, + thumbnails: HashMap::new(), } } } diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index 8d609d34d2..19ffb07c59 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -29,10 +29,16 @@ pub struct ExecutionRequest { pub struct ExecutionResponse { execution_id: u64, result: Result, - responses: VecDeque, + execution_responses: Vec, vector_modify: HashMap, +} + +pub enum ExecutionResponseMessage { /// The resulting value from the temporary inspected during execution - inspect_result: Option, + InspectResult(Option), + UpdateNodeGraphThumbnail(NodeId, Graphic), + UpdateFrontendThumbnail(NodeId, String), + SendGraph, } #[derive(serde::Serialize, serde::Deserialize)] @@ -260,11 +266,24 @@ impl NodeGraphExecutor { let ExecutionResponse { execution_id, result, - responses: existing_responses, + execution_responses, vector_modify, - inspect_result, } = execution_response; - + for execution_response in execution_responses { + match execution_response { + ExecutionResponseMessage::InspectResult(inspect_result) => { + // Update the Data panel on the frontend using the value of the inspect result. + if let Some(inspect_result) = (self.previous_node_to_inspect.is_some()).then_some(inspect_result).flatten() { + responses.add(DataPanelMessage::UpdateLayout { inspect_result }); + } else { + responses.add(DataPanelMessage::ClearLayout); + } + } + ExecutionResponseMessage::UpdateNodeGraphThumbnail(node_id, graphic) => responses.add(NodeGraphMessage::UpdateThumbnail { node_id, graphic }), + ExecutionResponseMessage::UpdateFrontendThumbnail(node_id, string) => responses.add(FrontendMessage::UpdateNodeThumbnail { id: node_id, value: string }), + ExecutionResponseMessage::SendGraph => responses.add(NodeGraphMessage::SendGraph), + } + } responses.add(OverlaysMessage::Draw); let node_graph_output = match result { @@ -277,7 +296,6 @@ impl NodeGraphExecutor { } }; - responses.extend(existing_responses.into_iter().map(Into::into)); document.network_interface.update_vector_modify(vector_modify); let execution_context = self.futures.remove(&execution_id).ok_or_else(|| "Invalid generation ID".to_string())?; @@ -291,13 +309,6 @@ impl NodeGraphExecutor { execution_id, document_id: execution_context.document_id, }); - - // Update the Data panel on the frontend using the value of the inspect result. - if let Some(inspect_result) = (self.previous_node_to_inspect.is_some()).then_some(inspect_result).flatten() { - responses.add(DataPanelMessage::UpdateLayout { inspect_result }); - } else { - responses.add(DataPanelMessage::ClearLayout); - } } NodeGraphUpdate::CompilationResponse(execution_response) => { let CompilationResponse { node_graph_errors, result } = execution_response; diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index b89c5b2f25..ae72db8732 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -212,14 +212,14 @@ impl NodeRuntime { } GraphRuntimeRequest::ExecutionRequest(ExecutionRequest { execution_id, render_config, .. }) => { let result = self.execute_network(render_config).await; - let mut responses = VecDeque::new(); + let mut execution_responses = Vec::new(); // TODO: Only process monitor nodes if the graph has changed, not when only the Footprint changes - self.process_monitor_nodes(&mut responses, self.update_thumbnails); + self.process_monitor_nodes(&mut execution_responses, self.update_thumbnails); self.update_thumbnails = false; // Resolve the result from the inspection by accessing the monitor node let inspect_result = self.inspect_state.and_then(|state| state.access(&self.executor)); - + execution_responses.push(ExecutionResponseMessage::InspectResult(inspect_result)); let texture = if let Ok(TaggedValue::RenderOutput(RenderOutput { data: RenderOutputType::Texture(texture), .. @@ -233,9 +233,8 @@ impl NodeRuntime { self.sender.send_execution_response(ExecutionResponse { execution_id, result, - responses, + execution_responses, vector_modify: self.vector_modify.clone(), - inspect_result, }); return texture; } @@ -289,10 +288,10 @@ impl NodeRuntime { } /// Updates state data - pub fn process_monitor_nodes(&mut self, responses: &mut VecDeque, update_thumbnails: bool) { + pub fn process_monitor_nodes(&mut self, responses: &mut Vec, update_thumbnails: bool) { // TODO: Consider optimizing this since it's currently O(m*n^2), with a sort it could be made O(m * n*log(n)) self.thumbnail_renders.retain(|id, _| self.monitor_nodes.iter().any(|monitor_node_path| monitor_node_path.contains(id))); - + let mut updated_thumbnails = false; for monitor_node_path in &self.monitor_nodes { // Skip the inspect monitor node if self.inspect_state.is_some_and(|inspect_state| monitor_node_path.last().copied() == Some(inspect_state.monitor_node)) { @@ -316,13 +315,17 @@ impl NodeRuntime { // Graphic table: thumbnail if let Some(io) = introspected_data.downcast_ref::>>() { if update_thumbnails { - Self::render_thumbnail(&mut self.thumbnail_renders, parent_network_node_id, &io.output, responses) + Self::render_thumbnail(&mut self.thumbnail_renders, parent_network_node_id, &io.output, responses); + responses.push(ExecutionResponseMessage::UpdateNodeGraphThumbnail(parent_network_node_id, io.output.clone().to_graphic())); + updated_thumbnails = true; } } // Artboard table: thumbnail else if let Some(io) = introspected_data.downcast_ref::>>() { if update_thumbnails { - Self::render_thumbnail(&mut self.thumbnail_renders, parent_network_node_id, &io.output, responses) + Self::render_thumbnail(&mut self.thumbnail_renders, parent_network_node_id, &io.output, responses); + responses.push(ExecutionResponseMessage::UpdateNodeGraphThumbnail(parent_network_node_id, io.output.clone().to_graphic())); + updated_thumbnails = true; } } // Vector table: vector modifications @@ -337,18 +340,21 @@ impl NodeRuntime { log::warn!("Failed to downcast monitor node output {parent_network_node_id:?}"); } } + if updated_thumbnails { + responses.push(ExecutionResponseMessage::SendGraph); + } } /// If this is `Graphic` data, regenerate click targets and thumbnails for the layers in the graph, modifying the state and updating the UI. - fn render_thumbnail(thumbnail_renders: &mut HashMap>, parent_network_node_id: NodeId, graphic: &impl Render, responses: &mut VecDeque) { + fn render_thumbnail(thumbnail_renders: &mut HashMap>, parent_network_node_id: NodeId, graphic: &impl Render, responses: &mut Vec) { // Skip thumbnails if the layer is too complex (for performance) if graphic.render_complexity() > 1000 { let old = thumbnail_renders.insert(parent_network_node_id, Vec::new()); if old.is_none_or(|v| !v.is_empty()) { - responses.push_back(FrontendMessage::UpdateNodeThumbnail { - id: parent_network_node_id, - value: "Dense thumbnail omitted for performance".to_string(), - }); + responses.push(ExecutionResponseMessage::UpdateFrontendThumbnail( + parent_network_node_id, + "Dense thumbnail omitted for performance".to_string(), + )); } return; } @@ -382,10 +388,7 @@ impl NodeRuntime { let old_thumbnail_svg = thumbnail_renders.entry(parent_network_node_id).or_default(); if old_thumbnail_svg != &new_thumbnail_svg { - responses.push_back(FrontendMessage::UpdateNodeThumbnail { - id: parent_network_node_id, - value: new_thumbnail_svg.to_svg_string(), - }); + responses.push(ExecutionResponseMessage::UpdateFrontendThumbnail(parent_network_node_id, new_thumbnail_svg.to_svg_string())); *old_thumbnail_svg = new_thumbnail_svg; } } diff --git a/node-graph/gcore/src/node_graph_overlay.rs b/node-graph/gcore/src/node_graph_overlay.rs index 874332d638..ac234a7664 100644 --- a/node-graph/gcore/src/node_graph_overlay.rs +++ b/node-graph/gcore/src/node_graph_overlay.rs @@ -21,7 +21,7 @@ pub mod ui_context; #[node_macro::node(skip_impl)] pub fn generate_nodes(_: impl Ctx, mut node_graph_overlay_data: NodeGraphOverlayData) -> Table { let mut nodes_and_wires = Table::new(); - let (layers, side_ports) = draw_layers(&node_graph_overlay_data.nodes_to_render); + let (layers, side_ports) = draw_layers(&mut node_graph_overlay_data); nodes_and_wires.extend(layers); let wires = draw_wires(&mut node_graph_overlay_data.nodes_to_render); diff --git a/node-graph/gcore/src/node_graph_overlay/nodes_and_wires.rs b/node-graph/gcore/src/node_graph_overlay/nodes_and_wires.rs index 42d3d09c65..1e25b8a5cb 100644 --- a/node-graph/gcore/src/node_graph_overlay/nodes_and_wires.rs +++ b/node-graph/gcore/src/node_graph_overlay/nodes_and_wires.rs @@ -8,13 +8,14 @@ use crate::{ consts::SOURCE_SANS_FONT_DATA, node_graph_overlay::{ consts::*, - types::{FrontendGraphDataType, FrontendNodeToRender}, + types::{FrontendGraphDataType, FrontendNodeToRender, NodeGraphOverlayData}, }, table::{Table, TableRow}, text::{self, TextAlign, TypesettingConfig}, transform::ApplyTransform, vector::{ Vector, + style::{Fill, Stroke}, style::{Fill, Stroke, StrokeAlign}, }, }; @@ -211,10 +212,10 @@ pub fn draw_nodes(nodes: &Vec) -> Table { node_table } -pub fn draw_layers(nodes: &Vec) -> (Table, Table) { +pub fn draw_layers(nodes: &mut NodeGraphOverlayData) -> (Table, Table) { let mut layer_table = Table::new(); let mut side_ports_table = Table::new(); - for node_to_render in nodes { + for node_to_render in &nodes.nodes_to_render { if let Some(frontend_layer) = node_to_render.node_or_layer.layer.as_ref() { // The layer position is the top left of the thumbnail let layer_position = DVec2::new(frontend_layer.position.x as f64 * GRID_SIZE + 12., frontend_layer.position.y as f64 * GRID_SIZE); @@ -360,7 +361,7 @@ pub fn draw_layers(nodes: &Vec) -> (Table, Table< } let bottom_port = BezPath::from_svg("M0,0H8V8L5.479,6.319a2.666,2.666,0,0,0-2.959,0L0,8Z").unwrap(); let mut vector = Vector::from_bezpath(bottom_port); - let mut bottom_port_fill = if frontend_layer.bottom_input.connected_to_node.is_some() { + let bottom_port_fill = if frontend_layer.bottom_input.connected_to_node.is_some() { frontend_layer.bottom_input.data_type.data_color() } else { frontend_layer.bottom_input.data_type.data_color_dim() @@ -457,10 +458,34 @@ pub fn draw_layers(nodes: &Vec) -> (Table, Table< inner_thumbnail_table.push(TableRow::new_from_element(vector)); } } - let mut thumbnail_row = TableRow::new_from_element(Graphic::Vector(inner_thumbnail_table)); - thumbnail_row.alpha_blending.clip = true; - let graphic_table = Table::new_from_row(thumbnail_row); - layer_table.push(TableRow::new_from_element(Graphic::Graphic(graphic_table))); + let mut thumbnail_grid_row = TableRow::new_from_element(Graphic::Vector(inner_thumbnail_table)); + thumbnail_grid_row.alpha_blending.clip = true; + let mut clipped_thumbnail_table = Table::new(); + clipped_thumbnail_table.push(thumbnail_grid_row); + if let Some(thumbnail_graphic) = nodes.thumbnails.get_mut(&node_to_render.metadata.node_id) { + let thumbnail_graphic = std::mem::take(thumbnail_graphic); + let bbox = thumbnail_graphic.bounding_box(DAffine2::default(), false); + if let RenderBoundingBox::Rectangle(rect) = bbox { + let rect_size = rect[1] - rect[0]; + let target_size = DVec2::new(68., 44.); + // uniform scale that fits in target box + let scale_x = target_size.x / rect_size.x; + let scale_y = target_size.y / rect_size.y; + let scale = scale_x.min(scale_y); + + let translation = rect[0] * -scale; + let scaled_size = rect_size * scale; + let offset_to_center = (target_size - scaled_size) / 2.; + + let mut thumbnail_graphic_row = TableRow::new_from_element(thumbnail_graphic); + thumbnail_graphic_row.transform = DAffine2::from_translation(layer_position + offset_to_center) * DAffine2::from_scale_angle_translation(DVec2::splat(scale), 0., translation); + thumbnail_graphic_row.alpha_blending.clip = true; + + clipped_thumbnail_table.push(thumbnail_graphic_row); + } + } + + layer_table.push(TableRow::new_from_element(Graphic::Graphic(clipped_thumbnail_table))); } } diff --git a/node-graph/gcore/src/node_graph_overlay/types.rs b/node-graph/gcore/src/node_graph_overlay/types.rs index abf537b38b..8df7c0ace6 100644 --- a/node-graph/gcore/src/node_graph_overlay/types.rs +++ b/node-graph/gcore/src/node_graph_overlay/types.rs @@ -2,8 +2,11 @@ use glam::{DAffine2, DVec2}; use graphene_core_shaders::color::Color; use kurbo::BezPath; -use crate::{node_graph_overlay::consts::*, uuid::NodeId}; -use std::hash::{Hash, Hasher}; +use crate::{Graphic, node_graph_overlay::consts::*, uuid::NodeId}; +use std::{ + collections::HashMap, + hash::{Hash, Hasher}, +}; #[derive(Clone, Debug, Default, PartialEq, dyn_any::DynAny, serde::Serialize, serde::Deserialize, specta::Type)] pub struct NodeGraphTransform { @@ -27,13 +30,28 @@ impl NodeGraphTransform { } } -#[derive(Clone, Debug, Default, PartialEq, Hash, dyn_any::DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, Default, PartialEq, dyn_any::DynAny, serde::Serialize, serde::Deserialize)] pub struct NodeGraphOverlayData { pub nodes_to_render: Vec, pub open: bool, pub in_selected_network: bool, // Displays a dashed border around the node pub previewed_node: Option, + pub thumbnails: HashMap, +} + +impl Hash for NodeGraphOverlayData { + fn hash(&self, state: &mut H) { + self.nodes_to_render.hash(state); + self.open.hash(state); + self.in_selected_network.hash(state); + self.previewed_node.hash(state); + let mut entries: Vec<_> = self.thumbnails.iter().collect(); + entries.sort_by(|a, b| a.0.cmp(b.0)); + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + entries.hash(&mut hasher); + hasher.finish(); + } } #[derive(Clone, Debug, Default, PartialEq, dyn_any::DynAny, serde::Serialize, serde::Deserialize)] diff --git a/node-graph/gsvg-renderer/src/renderer.rs b/node-graph/gsvg-renderer/src/renderer.rs index cb5bfd02a6..efeb98f151 100644 --- a/node-graph/gsvg-renderer/src/renderer.rs +++ b/node-graph/gsvg-renderer/src/renderer.rs @@ -9,6 +9,7 @@ use graphene_core::color::Color; use graphene_core::gradient::GradientStops; use graphene_core::gradient::GradientType; use graphene_core::math::quad::Quad; +use graphene_core::node_graph_overlay::consts::BEZ_PATH_TOLERANCE; use graphene_core::raster::BitmapMut; use graphene_core::raster::Image; use graphene_core::raster_types::{CPU, GPU, Raster}; @@ -23,6 +24,8 @@ use graphene_core::vector::click_target::{ClickTarget, FreePoint}; use graphene_core::vector::style::{Fill, PaintOrder, Stroke, StrokeAlign, ViewMode}; use graphene_core::{Artboard, Graphic}; use kurbo::Affine; +use kurbo::Rect; +use kurbo::Shape; use num_traits::Zero; use skrifa::MetadataProvider; use skrifa::attribute::Style; @@ -247,6 +250,8 @@ pub trait Render: BoundingBox + RenderComplexity { #[cfg(feature = "vello")] fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, _render_params: &RenderParams); + fn to_graphic(self) -> Graphic; + /// The upstream click targets for each layer are collected during the render so that they do not have to be calculated for each click detection. fn add_upstream_click_targets(&self, _click_targets: &mut Vec) {} @@ -289,6 +294,17 @@ impl Render for Graphic { } } + fn to_graphic(self) -> Graphic { + match self { + Graphic::Graphic(table) => table.to_graphic(), + Graphic::Vector(table) => table.to_graphic(), + Graphic::RasterCPU(table) => table.to_graphic(), + Graphic::RasterGPU(table) => table.to_graphic(), + Graphic::Color(table) => table.to_graphic(), + Graphic::Gradient(table) => table.to_graphic(), + } + } + fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option) { if let Some(element_id) = element_id { match self { @@ -467,6 +483,23 @@ impl Render for Artboard { } } + fn to_graphic(self) -> Graphic { + let bg = Rect::new( + self.location.x as f64, + self.location.y as f64, + self.location.x as f64 + self.dimensions.x as f64, + self.location.y as f64 + self.dimensions.y as f64, + ); + let mut bg_vector = Vector::from_bezpath(bg.to_path(BEZ_PATH_TOLERANCE)); + bg_vector.style.fill = Fill::Solid(self.background); + let mut graphic_table = Table::new(); + graphic_table.push(TableRow::new_from_element(Graphic::Graphic(Table::new_from_element(Graphic::Vector(Table::new_from_element( + bg_vector, + )))))); + graphic_table.push(TableRow::new_from_element(Graphic::Graphic(self.content))); + Graphic::Graphic(graphic_table) + } + fn collect_metadata(&self, metadata: &mut RenderMetadata, mut footprint: Footprint, element_id: Option) { if let Some(element_id) = element_id { let subpath = Subpath::new_rect(DVec2::ZERO, self.dimensions.as_dvec2()); @@ -505,6 +538,20 @@ impl Render for Table { } } + fn to_graphic(self) -> Graphic { + let mut graphic_table = Table::new(); + for item in self.into_iter() { + let graphic = item.element.to_graphic(); + let graphic_row = TableRow { + element: graphic, + transform: item.transform, + alpha_blending: item.alpha_blending, + source_node_id: item.source_node_id, + }; + graphic_table.push(graphic_row); + } + Graphic::Graphic(graphic_table) + } fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, _element_id: Option) { for row in self.iter() { row.element.collect_metadata(metadata, footprint, *row.source_node_id); @@ -643,6 +690,10 @@ impl Render for Table { } } + fn to_graphic(self) -> Graphic { + Graphic::Graphic(self) + } + fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option) { for row in self.iter() { if let Some(element_id) = row.source_node_id { @@ -1141,6 +1192,10 @@ impl Render for Table { } } + fn to_graphic(self) -> Graphic { + Graphic::Vector(self) + } + fn add_upstream_click_targets(&self, click_targets: &mut Vec) { for row in self.iter() { let stroke_width = row.element.style.stroke().as_ref().map_or(0., Stroke::effective_width); @@ -1318,6 +1373,10 @@ impl Render for Table> { } } + fn to_graphic(self) -> Graphic { + Graphic::RasterCPU(self) + } + fn add_upstream_click_targets(&self, click_targets: &mut Vec) { let subpath = Subpath::new_rect(DVec2::ZERO, DVec2::ONE); click_targets.push(ClickTarget::new_with_subpath(subpath, 0.)); @@ -1364,6 +1423,10 @@ impl Render for Table> { } } + fn to_graphic(self) -> Graphic { + Graphic::RasterGPU(self) + } + fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option) { let Some(element_id) = element_id else { return }; let subpath = Subpath::new_rect(DVec2::ZERO, DVec2::ONE); @@ -1444,6 +1507,10 @@ impl Render for Table { } } } + + fn to_graphic(self) -> Graphic { + Graphic::Color(self) + } } impl Render for Table { @@ -1544,6 +1611,10 @@ impl Render for Table { } } } + + fn to_graphic(self) -> Graphic { + Graphic::Gradient(self) + } } impl Render for Table {