Add graph type error diagnostics to the UI (#1535)

* Fontend input types

* Fix index of errors / types

* Bug fixes, styling improvements, and code review

* Improvements to the error box

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
0HyperCube
2023-12-29 08:38:45 +00:00
committed by GitHub
co-authored by Keavon Chambers
parent 96b5d7b520
commit 947a131a4b
20 changed files with 566 additions and 170 deletions
@@ -65,7 +65,7 @@ pub struct DocumentMessageHandler {
#[serde(default = "default_rulers_visible")] #[serde(default = "default_rulers_visible")]
pub rulers_visible: bool, pub rulers_visible: bool,
#[serde(default = "default_collapsed")] #[serde(default = "default_collapsed")]
pub collapsed: Vec<LayerNodeIdentifier>, // TODO: Is this actually used? Maybe or maybe not. Investigate and potentially remove. pub collapsed: Vec<LayerNodeIdentifier>,
// ============================================= // =============================================
// Fields omitted from the saved document format // Fields omitted from the saved document format
// ============================================= // =============================================
@@ -265,7 +265,7 @@ impl MessageHandler<DocumentMessage, DocumentInputs<'_>> for DocumentMessageHand
node_graph_message_handler: &self.node_graph_handler, node_graph_message_handler: &self.node_graph_handler,
executor, executor,
document_name: self.name.as_str(), document_name: self.name.as_str(),
document_network: &mut self.network, document_network: &self.network,
document_metadata: &mut self.metadata, document_metadata: &mut self.metadata,
}; };
self.properties_panel_message_handler self.properties_panel_message_handler
@@ -1,7 +1,8 @@
use crate::messages::prelude::*; use crate::messages::prelude::*;
use graph_craft::document::value::TaggedValue; use graph_craft::document::value::TaggedValue;
use graph_craft::document::{DocumentNode, NodeId, NodeInput}; use graph_craft::document::{DocumentNode, NodeId, NodeInput};
use graph_craft::proto::GraphErrors;
use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypes;
#[impl_message(Message, DocumentMessage, NodeGraph)] #[impl_message(Message, DocumentMessage, NodeGraph)]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
@@ -111,4 +112,10 @@ pub enum NodeGraphMessage {
node_id: NodeId, node_id: NodeId,
}, },
UpdateNewNodeGraph, UpdateNewNodeGraph,
UpdateTypes {
#[serde(skip)]
resolved_types: ResolvedDocumentNodeTypes,
#[serde(skip)]
node_graph_errors: GraphErrors,
},
} }
@@ -5,10 +5,11 @@ use crate::messages::input_mapper::utility_types::macros::action_keys;
use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier}; use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
use crate::messages::prelude::*; use crate::messages::prelude::*;
use graph_craft::document::value::TaggedValue; use graph_craft::document::value::TaggedValue;
use graph_craft::document::{DocumentNode, NodeId, NodeInput, NodeNetwork, NodeOutput}; use graph_craft::document::{DocumentNode, NodeId, NodeInput, NodeNetwork, NodeOutput, Source};
use graph_craft::proto::GraphErrors;
use graphene_core::*; use graphene_core::*;
use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypes;
mod document_node_types; mod document_node_types;
mod node_properties; mod node_properties;
@@ -23,22 +24,22 @@ pub enum FrontendGraphDataType {
Raster, Raster,
#[serde(rename = "color")] #[serde(rename = "color")]
Color, Color,
#[serde(rename = "number")] #[serde(rename = "general")]
Text, Text,
#[serde(rename = "vector")] #[serde(rename = "vector")]
Subpath, Subpath,
#[serde(rename = "number")] #[serde(rename = "number")]
Number, Number,
#[serde(rename = "number")] #[serde(rename = "general")]
Boolean, Boolean,
/// Refers to the mathematical vector, with direction and magnitude. /// Refers to the mathematical vector, with direction and magnitude.
#[serde(rename = "vec2")] #[serde(rename = "number")]
Vector, Vector,
#[serde(rename = "graphic")] #[serde(rename = "raster")]
GraphicGroup, GraphicGroup,
#[serde(rename = "artboard")] #[serde(rename = "artboard")]
Artboard, Artboard,
#[serde(rename = "palette")] #[serde(rename = "color")]
Palette, Palette,
} }
impl FrontendGraphDataType { impl FrontendGraphDataType {
@@ -65,6 +66,8 @@ pub struct FrontendGraphInput {
#[serde(rename = "dataType")] #[serde(rename = "dataType")]
data_type: FrontendGraphDataType, data_type: FrontendGraphDataType,
name: String, name: String,
#[serde(rename = "resolvedType")]
resolved_type: Option<String>,
} }
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
@@ -72,6 +75,8 @@ pub struct FrontendGraphOutput {
#[serde(rename = "dataType")] #[serde(rename = "dataType")]
data_type: FrontendGraphDataType, data_type: FrontendGraphDataType,
name: String, name: String,
#[serde(rename = "resolvedType")]
resolved_type: Option<String>,
} }
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
@@ -92,6 +97,7 @@ pub struct FrontendNode {
pub position: (i32, i32), pub position: (i32, i32),
pub disabled: bool, pub disabled: bool,
pub previewed: bool, pub previewed: bool,
pub errors: Option<String>,
} }
// (link_start, link_end, link_end_input_index) // (link_start, link_end, link_end_input_index)
@@ -124,6 +130,8 @@ impl FrontendNodeType {
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct NodeGraphMessageHandler { pub struct NodeGraphMessageHandler {
pub network: Vec<NodeId>, pub network: Vec<NodeId>,
pub resolved_types: ResolvedDocumentNodeTypes,
pub node_graph_errors: GraphErrors,
has_selection: bool, has_selection: bool,
widgets: [LayoutGroup; 2], widgets: [LayoutGroup; 2],
} }
@@ -144,6 +152,8 @@ impl Default for NodeGraphMessageHandler {
Self { Self {
network: Vec::new(), network: Vec::new(),
resolved_types: ResolvedDocumentNodeTypes::default(),
node_graph_errors: Vec::new(),
has_selection: false, has_selection: false,
widgets: [LayoutGroup::Row { widgets: Vec::new() }, LayoutGroup::Row { widgets: right_side_widgets }], widgets: [LayoutGroup::Row { widgets: Vec::new() }, LayoutGroup::Row { widgets: right_side_widgets }],
} }
@@ -264,7 +274,7 @@ impl NodeGraphMessageHandler {
} }
} }
fn send_graph(network: &NodeNetwork, graph_view_overlay_open: bool, responses: &mut VecDeque<Message>) { fn send_graph(&self, network: &NodeNetwork, graph_view_overlay_open: bool, responses: &mut VecDeque<Message>) {
responses.add(PropertiesPanelMessage::Refresh); responses.add(PropertiesPanelMessage::Refresh);
if !graph_view_overlay_open { if !graph_view_overlay_open {
@@ -298,6 +308,7 @@ impl NodeGraphMessageHandler {
let mut nodes = Vec::new(); let mut nodes = Vec::new();
for (id, node) in &network.nodes { for (id, node) in &network.nodes {
let node_path = vec![*id];
// TODO: This should be based on the graph runtime type inference system in order to change the colors of node connectors to match the data type in use // TODO: This should be based on the graph runtime type inference system in order to change the colors of node connectors to match the data type in use
let Some(node_type) = document_node_types::resolve_document_node_type(&node.name) else { let Some(node_type) = document_node_types::resolve_document_node_type(&node.name) else {
warn!("Node '{}' does not exist in library", node.name); warn!("Node '{}' does not exist in library", node.name);
@@ -305,20 +316,26 @@ impl NodeGraphMessageHandler {
}; };
// Inputs // Inputs
let mut inputs = node.inputs.iter().zip(node_type.inputs.iter().map(|input_type| FrontendGraphInput { let mut inputs = node.inputs.iter().zip(node_type.inputs.iter().enumerate().map(|(index, input_type)| {
data_type: input_type.data_type, let index = node.inputs.iter().take(index).filter(|input| input.is_exposed()).count();
name: input_type.name.to_string(), FrontendGraphInput {
data_type: input_type.data_type,
name: input_type.name.to_string(),
resolved_type: self.resolved_types.inputs.get(&Source { node: node_path.clone(), index }).map(|input| format!("{input:?}")),
}
})); }));
let primary_input = inputs.next().filter(|(input, _)| input.is_exposed()).map(|(_, input_type)| input_type); let primary_input = inputs.next().filter(|(input, _)| input.is_exposed()).map(|(_, input_type)| input_type);
let exposed_inputs = inputs.filter(|(input, _)| input.is_exposed()).map(|(_, input_type)| input_type).collect(); let exposed_inputs = inputs.filter(|(input, _)| input.is_exposed()).map(|(_, input_type)| input_type).collect();
// Outputs // Outputs
let mut outputs = node_type.outputs.iter().map(|output_type| FrontendGraphOutput { let mut outputs = node_type.outputs.iter().enumerate().map(|(index, output_type)| FrontendGraphOutput {
data_type: output_type.data_type, data_type: output_type.data_type,
name: output_type.name.to_string(), name: output_type.name.to_string(),
resolved_type: self.resolved_types.outputs.get(&Source { node: node_path.clone(), index }).map(|output| format!("{output:?}")),
}); });
let primary_output = if node.has_primary_output { outputs.next() } else { None }; let primary_output = if node.has_primary_output { outputs.next() } else { None };
let errors = self.node_graph_errors.iter().find(|error| error.node_path.starts_with(&node_path)).map(|error| error.error.clone());
nodes.push(FrontendNode { nodes.push(FrontendNode {
is_layer: node.is_layer(), is_layer: node.is_layer(),
id: *id, id: *id,
@@ -331,6 +348,7 @@ impl NodeGraphMessageHandler {
position: node.metadata.position.into(), position: node.metadata.position.into(),
previewed: network.outputs_contain(*id), previewed: network.outputs_contain(*id),
disabled: network.disabled.contains(id), disabled: network.disabled.contains(id),
errors: errors.map(|e| format!("{e:?}")),
}) })
} }
responses.add(FrontendMessage::UpdateNodeGraph { nodes, links }); responses.add(FrontendMessage::UpdateNodeGraph { nodes, links });
@@ -607,7 +625,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
} }
} }
if let Some(network) = document_network.nested_network(&self.network) { if let Some(network) = document_network.nested_network(&self.network) {
Self::send_graph(network, graph_view_overlay_open, responses); self.send_graph(network, graph_view_overlay_open, responses);
} }
self.update_selected(document_network, metadata, responses); self.update_selected(document_network, metadata, responses);
} }
@@ -635,7 +653,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
responses.add(NodeGraphMessage::InsertNode { node_id, document_node }); responses.add(NodeGraphMessage::InsertNode { node_id, document_node });
} }
Self::send_graph(network, graph_view_overlay_open, responses); self.send_graph(network, graph_view_overlay_open, responses);
self.update_selected(document_network, metadata, responses); self.update_selected(document_network, metadata, responses);
responses.add(NodeGraphMessage::SendGraph { should_rerender: false }); responses.add(NodeGraphMessage::SendGraph { should_rerender: false });
} }
@@ -648,7 +666,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
self.network.pop(); self.network.pop();
} }
if let Some(network) = document_network.nested_network(&self.network) { if let Some(network) = document_network.nested_network(&self.network) {
Self::send_graph(network, graph_view_overlay_open, responses); self.send_graph(network, graph_view_overlay_open, responses);
} }
self.update_selected(document_network, metadata, responses); self.update_selected(document_network, metadata, responses);
} }
@@ -698,7 +716,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
node.metadata.position += IVec2::new(displacement_x, displacement_y) node.metadata.position += IVec2::new(displacement_x, displacement_y)
} }
} }
Self::send_graph(network, graph_view_overlay_open, responses); self.send_graph(network, graph_view_overlay_open, responses);
} }
NodeGraphMessage::PasteNodes { serialized_nodes } => { NodeGraphMessage::PasteNodes { serialized_nodes } => {
let Some(network) = document_network.nested_network(&self.network) else { let Some(network) = document_network.nested_network(&self.network) else {
@@ -763,7 +781,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
} }
NodeGraphMessage::SendGraph { should_rerender } => { NodeGraphMessage::SendGraph { should_rerender } => {
if let Some(network) = document_network.nested_network(&self.network) { if let Some(network) = document_network.nested_network(&self.network) {
Self::send_graph(network, graph_view_overlay_open, responses); self.send_graph(network, graph_view_overlay_open, responses);
if should_rerender { if should_rerender {
responses.add(NodeGraphMessage::RunDocumentGraph); responses.add(NodeGraphMessage::RunDocumentGraph);
} }
@@ -890,7 +908,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
} else if !network.inputs.contains(&node_id) && !network.original_outputs().iter().any(|output| output.node_id == node_id) { } else if !network.inputs.contains(&node_id) && !network.original_outputs().iter().any(|output| output.node_id == node_id) {
network.disabled.push(node_id); network.disabled.push(node_id);
} }
Self::send_graph(network, graph_view_overlay_open, responses); self.send_graph(network, graph_view_overlay_open, responses);
// Only generate node graph if one of the selected nodes is connected to the output // Only generate node graph if one of the selected nodes is connected to the output
if network.connected_to_output(node_id) { if network.connected_to_output(node_id) {
@@ -926,7 +944,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
} else { } else {
return; return;
} }
Self::send_graph(network, graph_view_overlay_open, responses); self.send_graph(network, graph_view_overlay_open, responses);
} }
self.update_selection_action_buttons(document_network, metadata, responses); self.update_selection_action_buttons(document_network, metadata, responses);
responses.add(NodeGraphMessage::RunDocumentGraph); responses.add(NodeGraphMessage::RunDocumentGraph);
@@ -936,13 +954,25 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
metadata.clear_selected_nodes(); metadata.clear_selected_nodes();
responses.add(BroadcastEvent::SelectionChanged); responses.add(BroadcastEvent::SelectionChanged);
Self::send_graph(network, graph_view_overlay_open, responses); self.send_graph(network, graph_view_overlay_open, responses);
let node_types = document_node_types::collect_node_types(); let node_types = document_node_types::collect_node_types();
responses.add(FrontendMessage::UpdateNodeTypes { node_types }); responses.add(FrontendMessage::UpdateNodeTypes { node_types });
} }
self.update_selected(document_network, metadata, responses); self.update_selected(document_network, metadata, responses);
} }
NodeGraphMessage::UpdateTypes { resolved_types, node_graph_errors } => {
let changed = self.resolved_types != resolved_types || self.node_graph_errors != node_graph_errors;
self.resolved_types = resolved_types;
self.node_graph_errors = node_graph_errors;
if changed {
if let Some(network) = document_network.nested_network(&self.network) {
self.send_graph(network, graph_view_overlay_open, responses)
}
}
}
} }
self.has_selection = metadata.has_selected_nodes(); self.has_selection = metadata.has_selected_nodes();
} }
@@ -214,7 +214,7 @@ fn vec2_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name
} }
fn vec_f32_input(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, text_props: TextInput, blank_assist: bool) -> Vec<WidgetHolder> { fn vec_f32_input(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, text_props: TextInput, blank_assist: bool) -> Vec<WidgetHolder> {
let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::Color, blank_assist); let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::Vector, blank_assist);
let from_string = |string: &str| { let from_string = |string: &str| {
string string
@@ -243,7 +243,7 @@ fn vec_f32_input(document_node: &DocumentNode, node_id: NodeId, index: usize, na
} }
fn vec_dvec2_input(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, text_props: TextInput, blank_assist: bool) -> Vec<WidgetHolder> { fn vec_dvec2_input(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, text_props: TextInput, blank_assist: bool) -> Vec<WidgetHolder> {
let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::Color, blank_assist); let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::Vector, blank_assist);
let from_string = |string: &str| { let from_string = |string: &str| {
string string
@@ -739,7 +739,7 @@ fn gradient_positions(rows: &mut Vec<LayoutGroup>, document_node: &DocumentNode,
} }
fn color_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, color_props: ColorButton, blank_assist: bool) -> LayoutGroup { fn color_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, color_props: ColorButton, blank_assist: bool) -> LayoutGroup {
let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::Number, blank_assist); let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::Color, blank_assist);
if let NodeInput::Value { tagged_value, exposed: false } = &document_node.inputs[index] { if let NodeInput::Value { tagged_value, exposed: false } = &document_node.inputs[index] {
if let &TaggedValue::Color(x) = tagged_value { if let &TaggedValue::Color(x) = tagged_value {
@@ -1452,9 +1452,7 @@ pub fn transform_properties(document_node: &DocumentNode, node_id: NodeId, _cont
let scale = vec2_widget(document_node, node_id, 3, "Scale", "W", "H", "x", None, add_blank_assist); let scale = vec2_widget(document_node, node_id, 3, "Scale", "W", "H", "x", None, add_blank_assist);
let vector_data = start_widgets(document_node, node_id, 0, "Data", FrontendGraphDataType::Vector, false); vec![translation, rotation, scale]
let vector_data = LayoutGroup::Row { widgets: vector_data };
vec![vector_data, translation, rotation, scale]
} }
pub fn node_section_font(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> { pub fn node_section_font(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
@@ -6,7 +6,7 @@ use graph_craft::document::NodeNetwork;
pub struct PropertiesPanelMessageHandlerData<'a> { pub struct PropertiesPanelMessageHandlerData<'a> {
pub document_name: &'a str, pub document_name: &'a str,
pub document_network: &'a mut NodeNetwork, pub document_network: &'a NodeNetwork,
pub document_metadata: &'a mut DocumentMetadata, pub document_metadata: &'a mut DocumentMetadata,
pub node_graph_message_handler: &'a NodeGraphMessageHandler, pub node_graph_message_handler: &'a NodeGraphMessageHandler,
pub executor: &'a mut NodeGraphExecutor, pub executor: &'a mut NodeGraphExecutor,
+18 -6
View File
@@ -12,6 +12,7 @@ use graph_craft::document::value::TaggedValue;
use graph_craft::document::{generate_uuid, DocumentNodeImplementation, NodeId, NodeNetwork}; use graph_craft::document::{generate_uuid, DocumentNodeImplementation, NodeId, NodeNetwork};
use graph_craft::graphene_compiler::Compiler; use graph_craft::graphene_compiler::Compiler;
use graph_craft::imaginate_input::ImaginatePreferences; use graph_craft::imaginate_input::ImaginatePreferences;
use graph_craft::proto::GraphErrors;
use graphene_core::application_io::{NodeGraphUpdateMessage, NodeGraphUpdateSender, RenderConfig}; use graphene_core::application_io::{NodeGraphUpdateMessage, NodeGraphUpdateSender, RenderConfig};
use graphene_core::memo::IORecord; use graphene_core::memo::IORecord;
use graphene_core::raster::{Image, ImageFrame}; use graphene_core::raster::{Image, ImageFrame};
@@ -22,7 +23,7 @@ use graphene_core::vector::style::ViewMode;
use graphene_core::vector::VectorData; use graphene_core::vector::VectorData;
use graphene_core::{Color, GraphicElement, SurfaceFrame}; use graphene_core::{Color, GraphicElement, SurfaceFrame};
use graphene_std::wasm_application_io::{WasmApplicationIo, WasmEditorApi}; use graphene_std::wasm_application_io::{WasmApplicationIo, WasmEditorApi};
use interpreted_executor::dynamic_executor::DynamicExecutor; use interpreted_executor::dynamic_executor::{DynamicExecutor, ResolvedDocumentNodeTypes};
use glam::{DAffine2, DVec2, UVec2}; use glam::{DAffine2, DVec2, UVec2};
use std::cell::RefCell; use std::cell::RefCell;
@@ -40,6 +41,8 @@ pub struct NodeRuntime {
pub(crate) thumbnails: HashMap<NodeId, SvgSegmentList>, pub(crate) thumbnails: HashMap<NodeId, SvgSegmentList>,
pub(crate) click_targets: HashMap<NodeId, Vec<ClickTarget>>, pub(crate) click_targets: HashMap<NodeId, Vec<ClickTarget>>,
pub(crate) upstream_transforms: HashMap<NodeId, (Footprint, DAffine2)>, pub(crate) upstream_transforms: HashMap<NodeId, (Footprint, DAffine2)>,
pub(crate) resolved_types: ResolvedDocumentNodeTypes,
pub(crate) node_graph_errors: GraphErrors,
graph_hash: Option<u64>, graph_hash: Option<u64>,
monitor_nodes: Vec<Vec<NodeId>>, monitor_nodes: Vec<Vec<NodeId>>,
} }
@@ -73,6 +76,8 @@ pub(crate) struct GenerationResponse {
new_thumbnails: HashMap<NodeId, SvgSegmentList>, new_thumbnails: HashMap<NodeId, SvgSegmentList>,
new_click_targets: HashMap<LayerNodeIdentifier, Vec<ClickTarget>>, new_click_targets: HashMap<LayerNodeIdentifier, Vec<ClickTarget>>,
new_upstream_transforms: HashMap<NodeId, (Footprint, DAffine2)>, new_upstream_transforms: HashMap<NodeId, (Footprint, DAffine2)>,
resolved_types: ResolvedDocumentNodeTypes,
node_graph_errors: GraphErrors,
transform: DAffine2, transform: DAffine2,
} }
@@ -113,6 +118,8 @@ impl NodeRuntime {
click_targets: HashMap::new(), click_targets: HashMap::new(),
graph_hash: None, graph_hash: None,
upstream_transforms: HashMap::new(), upstream_transforms: HashMap::new(),
resolved_types: ResolvedDocumentNodeTypes::default(),
node_graph_errors: Vec::new(),
monitor_nodes: Vec::new(), monitor_nodes: Vec::new(),
} }
} }
@@ -147,6 +154,8 @@ impl NodeRuntime {
new_thumbnails: self.thumbnails.clone(), new_thumbnails: self.thumbnails.clone(),
new_click_targets: self.click_targets.clone().into_iter().map(|(id, targets)| (LayerNodeIdentifier::new_unchecked(id), targets)).collect(), new_click_targets: self.click_targets.clone().into_iter().map(|(id, targets)| (LayerNodeIdentifier::new_unchecked(id), targets)).collect(),
new_upstream_transforms: self.upstream_transforms.clone(), new_upstream_transforms: self.upstream_transforms.clone(),
resolved_types: self.resolved_types.clone(),
node_graph_errors: core::mem::take(&mut self.node_graph_errors),
transform, transform,
}; };
self.sender.send_generation_response(response); self.sender.send_generation_response(response);
@@ -188,7 +197,7 @@ impl NodeRuntime {
self.monitor_nodes = scoped_network self.monitor_nodes = scoped_network
.recursive_nodes() .recursive_nodes()
.filter(|(_, node)| node.implementation == DocumentNodeImplementation::proto("graphene_core::memo::MonitorNode<_, _, _>")) .filter(|(_, node)| node.implementation == DocumentNodeImplementation::proto("graphene_core::memo::MonitorNode<_, _, _>"))
.map(|(_, node)| node.path.clone().unwrap_or_default()) .map(|(_, node)| node.original_location.path.clone().unwrap_or_default())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
// We assume only one output // We assume only one output
@@ -201,11 +210,11 @@ impl NodeRuntime {
assert_ne!(proto_network.nodes.len(), 0, "No protonodes exist?"); assert_ne!(proto_network.nodes.len(), 0, "No protonodes exist?");
if let Err(e) = self.executor.update(proto_network).await { if let Err(e) = self.executor.update(proto_network).await {
error!("Failed to update executor:\n{e}"); self.node_graph_errors = e;
return Err(e); } else {
self.graph_hash = Some(hash_code);
} }
self.resolved_types = self.executor.document_node_types();
self.graph_hash = Some(hash_code);
} }
use graph_craft::graphene_compiler::Executor; use graph_craft::graphene_compiler::Executor;
@@ -560,8 +569,11 @@ impl NodeGraphExecutor {
new_thumbnails, new_thumbnails,
new_click_targets, new_click_targets,
new_upstream_transforms, new_upstream_transforms,
resolved_types,
node_graph_errors,
transform, transform,
}) => { }) => {
responses.add(NodeGraphMessage::UpdateTypes { resolved_types, node_graph_errors });
let node_graph_output = result.map_err(|e| format!("Node graph evaluation failed: {e:?}"))?; let node_graph_output = result.map_err(|e| format!("Node graph evaluation failed: {e:?}"))?;
let execution_context = self.futures.remove(&generation_id).ok_or_else(|| "Invalid generation ID".to_string())?; let execution_context = self.futures.remove(&generation_id).ok_or_else(|| "Invalid generation ID".to_string())?;
+8 -11
View File
@@ -101,22 +101,19 @@
--color-e-nearwhite-rgb: 238, 238, 238; --color-e-nearwhite-rgb: 238, 238, 238;
--color-f-white: #fff; --color-f-white: #fff;
--color-f-white-rgb: 255, 255, 255; --color-f-white-rgb: 255, 255, 255;
--color-error-red: #d6536e;
--color-error-red-rgb: 214, 83, 110;
--color-data-general: #c5c5c5; --color-data-general: #c5c5c5;
--color-data-general-dim: #767676; --color-data-general-dim: #767676;
--color-data-vector: #65bbe5; --color-data-number: #cbbab4;
--color-data-vector-dim: #4b778c; --color-data-number-dim: #87736b;
--color-data-raster: #e4bb72; --color-data-raster: #e4bb72;
--color-data-raster-dim: #8b7752; --color-data-raster-dim: #8b7752;
--color-data-mask: #8d85c7; --color-data-vector: #65bbe5;
--color-data-number: #d6536e; --color-data-vector-dim: #4b778c;
--color-data-number-dim: #803242; --color-data-color: #dce472;
--color-data-vec2: #cc00ff; --color-data-color-dim: #898d55;
--color-data-vec2-dim: #71008d;
--color-data-color: #70a898;
--color-data-color-dim: #43645b;
--color-data-graphic: #e4bb72;
--color-data-graphic-dim: #8b7752;
--color-data-artboard: #70a898; --color-data-artboard: #70a898;
--color-data-artboard-dim: #3a6156; --color-data-artboard-dim: #3a6156;
@@ -627,7 +627,6 @@
width: 24px; width: 24px;
font-size: 0; font-size: 0;
overflow: hidden; overflow: hidden;
transition: background-color 0.5s ease;
div { div {
display: inline-block; display: inline-block;
@@ -636,6 +635,7 @@
// For the least jarring luminance conversion, these colors are derived by placing a black layer with the "desaturate" blend mode over the colors. // For the least jarring luminance conversion, these colors are derived by placing a black layer with the "desaturate" blend mode over the colors.
// We don't use the CSS `filter: grayscale(1);` property because it produces overly dark tones for bright colors with a noticeable jump on hover. // We don't use the CSS `filter: grayscale(1);` property because it produces overly dark tones for bright colors with a noticeable jump on hover.
background: var(--pure-color-gray); background: var(--pure-color-gray);
transition: background-color 0.2s ease;
} }
&:hover div, &:hover div,
@@ -583,7 +583,7 @@
&[title^="Coming Soon"] { &[title^="Coming Soon"] {
opacity: 0.25; opacity: 0.25;
transition: opacity 0.25s; transition: opacity 0.2s;
&:hover { &:hover {
opacity: 1; opacity: 1;
@@ -703,7 +703,7 @@
.graph-view { .graph-view {
pointer-events: none; pointer-events: none;
transition: opacity 0.1s ease-in-out; transition: opacity 0.2s ease-in-out;
opacity: 0; opacity: 0;
&.open { &.open {
+91 -17
View File
@@ -1,11 +1,13 @@
<script lang="ts"> <script lang="ts">
import { getContext, onMount, tick } from "svelte"; import { getContext, onMount, tick } from "svelte";
import { fade } from "svelte/transition";
import { FADE_TRANSITION } from "@graphite/consts";
import type { NodeGraphState } from "@graphite/state-providers/node-graph"; import type { NodeGraphState } from "@graphite/state-providers/node-graph";
import type { IconName } from "@graphite/utility-functions/icons"; import type { IconName } from "@graphite/utility-functions/icons";
import type { Editor } from "@graphite/wasm-communication/editor"; import type { Editor } from "@graphite/wasm-communication/editor";
import { UpdateNodeGraphSelection } from "@graphite/wasm-communication/messages"; import { UpdateNodeGraphSelection } from "@graphite/wasm-communication/messages";
import type { FrontendNodeLink, FrontendNodeType, FrontendNode, FrontendGraphDataType } from "@graphite/wasm-communication/messages"; import type { FrontendNodeLink, FrontendNodeType, FrontendNode, FrontendGraphInput, FrontendGraphOutput } from "@graphite/wasm-communication/messages";
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte"; import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
import TextButton from "@graphite/components/widgets/buttons/TextButton.svelte"; import TextButton from "@graphite/components/widgets/buttons/TextButton.svelte";
@@ -28,7 +30,7 @@
let nodesContainer: HTMLDivElement | undefined; let nodesContainer: HTMLDivElement | undefined;
let nodeSearchInput: TextInput | undefined; let nodeSearchInput: TextInput | undefined;
let transform = { scale: 1, x: 0, y: 0 }; let transform = { scale: 1, x: 1200, y: 0 };
let panning = false; let panning = false;
let selected: bigint[] = []; let selected: bigint[] = [];
let draggingNodes: { startX: number; startY: number; roundX: number; roundY: number } | undefined = undefined; let draggingNodes: { startX: number; startY: number; roundX: number; roundY: number } | undefined = undefined;
@@ -292,6 +294,8 @@
function pointerDown(e: PointerEvent) { function pointerDown(e: PointerEvent) {
const [lmb, rmb] = [e.button === 0, e.button === 2]; const [lmb, rmb] = [e.button === 0, e.button === 2];
const nodeError = (e.target as SVGSVGElement).closest("[data-node-error]") as HTMLElement;
if (nodeError && lmb) return;
const port = (e.target as SVGSVGElement).closest("[data-port]") as SVGSVGElement; const port = (e.target as SVGSVGElement).closest("[data-port]") as SVGSVGElement;
const node = (e.target as HTMLElement).closest("[data-node]") as HTMLElement | undefined; const node = (e.target as HTMLElement).closest("[data-node]") as HTMLElement | undefined;
const nodeId = node?.getAttribute("data-node") || undefined; const nodeId = node?.getAttribute("data-node") || undefined;
@@ -606,9 +610,9 @@
return `M-2,-2 L${nodeWidth + 2},-2 L${nodeWidth + 2},${nodeHeight + 2} L-2,${nodeHeight + 2}z ${rectangles.join(" ")}`; return `M-2,-2 L${nodeWidth + 2},-2 L${nodeWidth + 2},${nodeHeight + 2} L-2,${nodeHeight + 2}z ${rectangles.join(" ")}`;
} }
function dataTypeTooltip(dataType: FrontendGraphDataType): string { function dataTypeTooltip(value: FrontendGraphInput | FrontendGraphOutput): string {
const capitalized = dataType[0].toUpperCase() + dataType.slice(1); const dataTypeCapitalized = `${value.dataType[0].toUpperCase()}${value.dataType.slice(1)}`;
return `${capitalized} Data`; return value.resolvedType ? `Resolved Data: ${value.resolvedType}` : `Unresolved Data: ${dataTypeCapitalized}`;
} }
onMount(() => { onMount(() => {
@@ -674,7 +678,7 @@
<!-- Layers --> <!-- Layers -->
{#each $nodeGraph.nodes.flatMap((node, nodeIndex) => (node.isLayer ? [{ node, nodeIndex }] : [])) as { node, nodeIndex } (nodeIndex)} {#each $nodeGraph.nodes.flatMap((node, nodeIndex) => (node.isLayer ? [{ node, nodeIndex }] : [])) as { node, nodeIndex } (nodeIndex)}
{@const clipPathId = String(Math.random()).substring(2)} {@const clipPathId = String(Math.random()).substring(2)}
{@const stackDatainput = node.exposedInputs[0]} {@const stackDataInput = node.exposedInputs[0]}
<div <div
class="layer" class="layer"
class:selected={selected.includes(node.id)} class:selected={selected.includes(node.id)}
@@ -687,6 +691,10 @@
style:--data-color-dim={`var(--color-data-${node.primaryOutput?.dataType || "general"}-dim)`} style:--data-color-dim={`var(--color-data-${node.primaryOutput?.dataType || "general"}-dim)`}
data-node={node.id} data-node={node.id}
> >
{#if node.errors}
<span class="node-error faded" transition:fade={FADE_TRANSITION} data-node-error>{node.errors}</span>
<span class="node-error hover" transition:fade={FADE_TRANSITION} data-node-error>{node.errors}</span>
{/if}
<div class="node-chain" /> <div class="node-chain" />
<!-- Layer input port (from left) --> <!-- Layer input port (from left) -->
<div class="input ports"> <div class="input ports">
@@ -701,7 +709,7 @@
bind:this={inputs[nodeIndex][0]} bind:this={inputs[nodeIndex][0]}
> >
{#if node.primaryInput} {#if node.primaryInput}
<title>{dataTypeTooltip(node.primaryInput.dataType)}</title> <title>{dataTypeTooltip(node.primaryInput)}</title>
{/if} {/if}
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" /> <path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" />
</svg> </svg>
@@ -721,7 +729,7 @@
style:--data-color-dim={`var(--color-data-${node.primaryOutput.dataType}-dim)`} style:--data-color-dim={`var(--color-data-${node.primaryOutput.dataType}-dim)`}
bind:this={outputs[nodeIndex][0]} bind:this={outputs[nodeIndex][0]}
> >
<title>{dataTypeTooltip(node.primaryOutput.dataType)}</title> <title>{dataTypeTooltip(node.primaryOutput)}</title>
<path d="M0,2.953,2.521,1.259a2.649,2.649,0,0,1,2.959,0L8,2.953V8H0Z" /> <path d="M0,2.953,2.521,1.259a2.649,2.649,0,0,1,2.959,0L8,2.953V8H0Z" />
</svg> </svg>
{/if} {/if}
@@ -730,12 +738,12 @@
viewBox="0 0 8 8" viewBox="0 0 8 8"
class="port bottom" class="port bottom"
data-port="input" data-port="input"
data-datatype={stackDatainput.dataType} data-datatype={stackDataInput.dataType}
style:--data-color={`var(--color-data-${stackDatainput.dataType})`} style:--data-color={`var(--color-data-${stackDataInput.dataType})`}
style:--data-color-dim={`var(--color-data-${stackDatainput.dataType}-dim)`} style:--data-color-dim={`var(--color-data-${stackDataInput.dataType}-dim)`}
bind:this={inputs[nodeIndex][1]} bind:this={inputs[nodeIndex][1]}
> >
<title>{dataTypeTooltip(stackDatainput.dataType)}</title> <title>{dataTypeTooltip(stackDataInput)}</title>
<path d="M0,0H8V8L5.479,6.319a2.666,2.666,0,0,0-2.959,0L0,8Z" /> <path d="M0,0H8V8L5.479,6.319a2.666,2.666,0,0,0-2.959,0L0,8Z" />
</svg> </svg>
</div> </div>
@@ -769,6 +777,10 @@
style:--data-color-dim={`var(--color-data-${node.primaryOutput?.dataType || "general"}-dim)`} style:--data-color-dim={`var(--color-data-${node.primaryOutput?.dataType || "general"}-dim)`}
data-node={node.id} data-node={node.id}
> >
{#if node.errors}
<span class="node-error faded" transition:fade={FADE_TRANSITION} data-node-error>{node.errors}</span>
<span class="node-error hover" transition:fade={FADE_TRANSITION} data-node-error>{node.errors}</span>
{/if}
<!-- Primary row --> <!-- Primary row -->
<div class="primary" class:no-parameter-section={exposedInputsOutputs.length === 0}> <div class="primary" class:no-parameter-section={exposedInputsOutputs.length === 0}>
<IconLabel icon={nodeIcon(node.name)} /> <IconLabel icon={nodeIcon(node.name)} />
@@ -798,7 +810,7 @@
style:--data-color-dim={`var(--color-data-${node.primaryInput?.dataType}-dim)`} style:--data-color-dim={`var(--color-data-${node.primaryInput?.dataType}-dim)`}
bind:this={inputs[nodeIndex][0]} bind:this={inputs[nodeIndex][0]}
> >
<title>{dataTypeTooltip(node.primaryInput.dataType)}</title> <title>{dataTypeTooltip(node.primaryInput)}</title>
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" /> <path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" />
</svg> </svg>
{/if} {/if}
@@ -814,7 +826,7 @@
style:--data-color-dim={`var(--color-data-${parameter.dataType}-dim)`} style:--data-color-dim={`var(--color-data-${parameter.dataType}-dim)`}
bind:this={inputs[nodeIndex][index + 1]} bind:this={inputs[nodeIndex][index + 1]}
> >
<title>{dataTypeTooltip(parameter.dataType)}</title> <title>{dataTypeTooltip(parameter)}</title>
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" /> <path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" />
</svg> </svg>
{/if} {/if}
@@ -833,7 +845,7 @@
style:--data-color-dim={`var(--color-data-${node.primaryOutput.dataType}-dim)`} style:--data-color-dim={`var(--color-data-${node.primaryOutput.dataType}-dim)`}
bind:this={outputs[nodeIndex][0]} bind:this={outputs[nodeIndex][0]}
> >
<title>{dataTypeTooltip(node.primaryOutput.dataType)}</title> <title>{dataTypeTooltip(node.primaryOutput)}</title>
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" /> <path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" />
</svg> </svg>
{/if} {/if}
@@ -846,9 +858,9 @@
data-datatype={parameter.dataType} data-datatype={parameter.dataType}
style:--data-color={`var(--color-data-${parameter.dataType})`} style:--data-color={`var(--color-data-${parameter.dataType})`}
style:--data-color-dim={`var(--color-data-${parameter.dataType}-dim)`} style:--data-color-dim={`var(--color-data-${parameter.dataType}-dim)`}
bind:this={outputs[nodeIndex][outputIndex + 1]} bind:this={outputs[nodeIndex][outputIndex + (node.primaryOutput ? 1 : 0)]}
> >
<title>{dataTypeTooltip(parameter.dataType)}</title> <title>{dataTypeTooltip(parameter)}</title>
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" /> <path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" />
</svg> </svg>
{/each} {/each}
@@ -980,6 +992,68 @@
// backdrop-filter: blur(4px); // backdrop-filter: blur(4px);
background: rgba(0, 0, 0, 0.33); background: rgba(0, 0, 0, 0.33);
.node-error {
position: absolute;
width: max-content;
white-space: pre-wrap;
max-width: 600px;
line-height: 18px;
color: var(--color-2-mildblack);
background: var(--color-error-red);
padding: 8px;
border-radius: 4px;
bottom: calc(100% + 12px);
z-index: -1;
transition: opacity 0.2s ease-in-out;
opacity: 0.5;
// Tail
&::after {
content: "";
position: absolute;
left: 6px;
bottom: -8px;
width: 0;
height: 0;
border-style: solid;
border-width: 8px 6px 0 6px;
border-color: var(--color-error-red) transparent transparent transparent;
}
&.hover {
opacity: 0;
z-index: 1;
pointer-events: none;
}
&.faded:hover + .hover {
opacity: 1;
}
&.faded:hover {
z-index: 2;
opacity: 1;
-webkit-user-select: text;
user-select: text;
transition:
opacity 0.2s ease-in-out,
z-index 0s 0.2s;
&::selection {
background-color: var(--color-e-nearwhite);
// Target only Safari
@supports (background: -webkit-named-image(i)) {
& {
// Setting an alpha value opts out of Safari's "fancy" (but not visible on dark backgrounds) selection highlight rendering
// https://stackoverflow.com/a/71753552/775283
background-color: rgba(var(--color-e-nearwhite-rgb), calc(254 / 255));
}
}
}
}
}
&::after { &::after {
content: ""; content: "";
position: absolute; position: absolute;
@@ -9,7 +9,7 @@
</script> </script>
<LayoutRow class="parameter-expose-button"> <LayoutRow class="parameter-expose-button">
<button class:exposed style:--data-type-color={`var(--color-data-${dataType})`} on:click={action} title={tooltip} tabindex="-1"> <button class:exposed style:--data-type-color={`var(--color-data-${dataType})`} style:--data-type-color-dim={`var(--color-data-${dataType}-dim)`} on:click={action} title={tooltip} tabindex="-1">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10">
<path class="interior" d="M0,7.882c0,1.832,1.325,2.63,2.945,1.772L8.785,6.56c1.62-.858,1.62-2.262,0-3.12L2.945.345C1.325-.512,0,.285,0,2.118Z" /> <path class="interior" d="M0,7.882c0,1.832,1.325,2.63,2.945,1.772L8.785,6.56c1.62-.858,1.62-2.262,0-3.12L2.945.345C1.325-.512,0,.285,0,2.118Z" />
<path <path
@@ -53,25 +53,33 @@
} }
&:not(.exposed) { &:not(.exposed) {
.outline { &:not(:hover) {
fill: var(--data-type-color); .outline {
fill: var(--data-type-color-dim);
}
} }
&:hover { &:hover {
.interior { .outline {
fill: var(--color-6-lowergray); fill: var(--data-type-color);
} }
} }
} }
&.exposed { &.exposed {
.interior { &:not(:hover) {
fill: var(--data-type-color); .interior {
fill: var(--data-type-color);
}
} }
&:hover { &:hover {
.outline { .outline {
fill: var(--color-f-white); fill: var(--data-type-color);
}
.interior {
fill: var(--data-type-color-dim);
} }
} }
} }
+2
View File
@@ -0,0 +1,2 @@
import { cubicInOut } from "svelte/easing";
export const FADE_TRANSITION = { duration: 200, easing: cubicInOut };
+7 -1
View File
@@ -82,18 +82,22 @@ export class FrontendDocumentDetails extends DocumentDetails {
readonly id!: bigint; readonly id!: bigint;
} }
export type FrontendGraphDataType = "general" | "raster" | "color" | "vector" | "vec2" | "graphic" | "artboard"; export type FrontendGraphDataType = "general" | "number" | "raster" | "vector" | "color" | "artboard";
export class FrontendGraphInput { export class FrontendGraphInput {
readonly dataType!: FrontendGraphDataType; readonly dataType!: FrontendGraphDataType;
readonly name!: string; readonly name!: string;
readonly resolvedType!: string | undefined;
} }
export class FrontendGraphOutput { export class FrontendGraphOutput {
readonly dataType!: FrontendGraphDataType; readonly dataType!: FrontendGraphDataType;
readonly name!: string; readonly name!: string;
readonly resolvedType!: string | undefined;
} }
export class FrontendNode { export class FrontendNode {
@@ -119,6 +123,8 @@ export class FrontendNode {
readonly previewed!: boolean; readonly previewed!: boolean;
readonly disabled!: boolean; readonly disabled!: boolean;
readonly errors!: string | undefined;
} }
export class FrontendNodeLink { export class FrontendNodeLink {
+20 -3
View File
@@ -6,7 +6,7 @@ use dyn_any::StaticType;
#[cfg(feature = "std")] #[cfg(feature = "std")]
pub use std::borrow::Cow; pub use std::borrow::Cow;
#[derive(Debug, Clone, PartialEq, Eq, Hash)] #[derive(Clone, PartialEq, Eq, Hash)]
pub struct NodeIOTypes { pub struct NodeIOTypes {
pub input: Type, pub input: Type,
pub output: Type, pub output: Type,
@@ -23,6 +23,16 @@ impl NodeIOTypes {
} }
} }
impl core::fmt::Debug for NodeIOTypes {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_fmt(format_args!(
"node({}) -> {}",
[&self.input].into_iter().chain(&self.parameters).map(|input| input.to_string()).collect::<Vec<_>>().join(", "),
self.output
))
}
}
#[macro_export] #[macro_export]
macro_rules! concrete { macro_rules! concrete {
($type:ty) => { ($type:ty) => {
@@ -193,6 +203,13 @@ impl Type {
} }
} }
fn format_type(ty: &str) -> String {
ty.split('<')
.map(|path| path.split(',').map(|path| path.split("::").last().unwrap_or(path)).collect::<Vec<_>>().join(","))
.collect::<Vec<_>>()
.join("<")
}
impl core::fmt::Debug for Type { impl core::fmt::Debug for Type {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self { match self {
@@ -200,7 +217,7 @@ impl core::fmt::Debug for Type {
#[cfg(feature = "type_id_logging")] #[cfg(feature = "type_id_logging")]
Self::Concrete(arg0) => write!(f, "Concrete({}, {:?})", arg0.name, arg0.id), Self::Concrete(arg0) => write!(f, "Concrete({}, {:?})", arg0.name, arg0.id),
#[cfg(not(feature = "type_id_logging"))] #[cfg(not(feature = "type_id_logging"))]
Self::Concrete(arg0) => write!(f, "Concrete({})", arg0.name), Self::Concrete(arg0) => write!(f, "Concrete({})", format_type(&arg0.name)),
Self::Fn(arg0, arg1) => write!(f, "({arg0:?} -> {arg1:?})"), Self::Fn(arg0, arg1) => write!(f, "({arg0:?} -> {arg1:?})"),
Self::Future(arg0) => write!(f, "Future({arg0:?})"), Self::Future(arg0) => write!(f, "Future({arg0:?})"),
} }
@@ -211,7 +228,7 @@ impl std::fmt::Display for Type {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
Type::Generic(name) => write!(f, "{name}"), Type::Generic(name) => write!(f, "{name}"),
Type::Concrete(ty) => write!(f, "{}", ty.name), Type::Concrete(ty) => write!(f, "{}", format_type(&ty.name)),
Type::Fn(input, output) => write!(f, "({input} -> {output})"), Type::Fn(input, output) => write!(f, "({input} -> {output})"),
Type::Future(ty) => write!(f, "Future<{ty}>"), Type::Future(ty) => write!(f, "Future<{ty}>"),
} }
+131 -27
View File
@@ -166,9 +166,32 @@ pub struct DocumentNode {
/// Used as a hash of the graph input where applicable. This ensures that protonodes that depend on the graph's input are always regenerated. /// Used as a hash of the graph input where applicable. This ensures that protonodes that depend on the graph's input are always regenerated.
#[serde(default)] #[serde(default)]
pub world_state_hash: u64, pub world_state_hash: u64,
/// The path to this node as of when [`NodeNetwork::generate_node_paths`] was called. /// The path to this node and its inputs and outputs as of when [`NodeNetwork::generate_node_paths`] was called.
/// For example if this node was ID 6 inside a node with ID 4 and with a [`DocumentNodeImplementation::Network`], the path would be [4, 6]. #[serde(skip)]
pub original_location: OriginalLocation,
}
/// Represents the original location of a node input/output when [`NodeNetwork::generate_node_paths`] was called, allowing the types and errors to be derived.
#[derive(Clone, Debug, PartialEq, Eq, Hash, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Source {
pub node: Vec<NodeId>,
pub index: usize,
}
/// The path to this node and its inputs and outputs as of when [`NodeNetwork::generate_node_paths`] was called.
#[derive(Clone, Debug, PartialEq, Eq, DynAny, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct OriginalLocation {
/// The original location to the document node - e.g. [grandparent_id, parent_id, node_id].
pub path: Option<Vec<NodeId>>, pub path: Option<Vec<NodeId>>,
/// Each document input source maps to one protonode input (however one protonode input may come from several sources)
pub inputs_source: HashMap<Source, usize>,
/// A list of document sources for the node's output
pub outputs_source: HashMap<Source, usize>,
pub inputs_exposed: Vec<bool>,
/// Skipping inputs is useful for the manual composition thing - whereby a hidden `Footprint` input is added as the first input.
pub skip_inputs: usize,
} }
impl Default for DocumentNode { impl Default for DocumentNode {
@@ -183,14 +206,42 @@ impl Default for DocumentNode {
metadata: Default::default(), metadata: Default::default(),
skip_deduplication: Default::default(), skip_deduplication: Default::default(),
world_state_hash: Default::default(), world_state_hash: Default::default(),
path: Default::default(), original_location: OriginalLocation::default(),
} }
} }
} }
impl Hash for OriginalLocation {
fn hash<H: Hasher>(&self, state: &mut H) {
self.path.hash(state);
self.inputs_source.iter().for_each(|val| val.hash(state));
self.outputs_source.iter().for_each(|val| val.hash(state));
self.inputs_exposed.hash(state);
self.skip_inputs.hash(state);
}
}
impl OriginalLocation {
pub fn inputs<'a>(&'a self, index: usize) -> impl Iterator<Item = Source> + 'a {
[(index >= self.skip_inputs).then(|| Source {
node: self.path.clone().unwrap_or_default(),
index: self.inputs_exposed.iter().take(index - self.skip_inputs).filter(|&&exposed| exposed).count(),
})]
.into_iter()
.flatten()
.chain(self.inputs_source.iter().filter(move |x| *x.1 == index).map(|(source, _)| source.clone()))
}
pub fn outputs<'a>(&'a self, index: usize) -> impl Iterator<Item = Source> + 'a {
[Source {
node: self.path.clone().unwrap_or_default(),
index,
}]
.into_iter()
.chain(self.outputs_source.iter().filter(move |x| *x.1 == index).map(|(source, _)| source.clone()))
}
}
impl DocumentNode { impl DocumentNode {
/// Locate the input that is a [`NodeInput::Network`] at index `offset` and replace it with a [`NodeInput::Node`]. /// Locate the input that is a [`NodeInput::Network`] at index `offset` and replace it with a [`NodeInput::Node`].
pub fn populate_first_network_input(&mut self, node_id: NodeId, output_index: usize, offset: usize, lambda: bool) { pub fn populate_first_network_input(&mut self, node_id: NodeId, output_index: usize, offset: usize, lambda: bool, source: impl Iterator<Item = Source>, skip: usize) {
let (index, _) = self let (index, _) = self
.inputs .inputs
.iter() .iter()
@@ -200,6 +251,10 @@ impl DocumentNode {
.unwrap_or_else(|| panic!("no network input found for {self:#?} and offset: {offset}")); .unwrap_or_else(|| panic!("no network input found for {self:#?} and offset: {offset}"));
self.inputs[index] = NodeInput::Node { node_id, output_index, lambda }; self.inputs[index] = NodeInput::Node { node_id, output_index, lambda };
let input_source = &mut self.original_location.inputs_source;
for source in source {
input_source.insert(source, index + self.original_location.skip_inputs - skip);
}
} }
fn resolve_proto_node(mut self) -> ProtoNode { fn resolve_proto_node(mut self) -> ProtoNode {
@@ -246,7 +301,7 @@ impl DocumentNode {
identifier: fqn, identifier: fqn,
input, input,
construction_args: args, construction_args: args,
document_node_path: self.path.unwrap_or_default(), original_location: self.original_location,
skip_deduplication: self.skip_deduplication, skip_deduplication: self.skip_deduplication,
world_state_hash: self.world_state_hash, world_state_hash: self.world_state_hash,
} }
@@ -762,10 +817,15 @@ impl NodeNetwork {
if let DocumentNodeImplementation::Network(network) = &mut node.implementation { if let DocumentNodeImplementation::Network(network) = &mut node.implementation {
network.generate_node_paths(new_path.as_slice()); network.generate_node_paths(new_path.as_slice());
} }
if node.path.is_some() { if node.original_location.path.is_some() {
log::warn!("Attempting to overwrite node path"); log::warn!("Attempting to overwrite node path");
} else { } else {
node.path = Some(new_path); node.original_location = OriginalLocation {
path: Some(new_path),
inputs_exposed: node.inputs.iter().map(|input| input.is_exposed()).collect(),
skip_inputs: if node.manual_composition.is_some() { 1 } else { 0 },
..Default::default()
}
} }
} }
} }
@@ -831,7 +891,6 @@ impl NodeNetwork {
/// Remove all nodes that contain [`DocumentNodeImplementation::Network`] by moving the nested nodes into the parent network. /// Remove all nodes that contain [`DocumentNodeImplementation::Network`] by moving the nested nodes into the parent network.
pub fn flatten_with_fns(&mut self, node: NodeId, map_ids: impl Fn(NodeId, NodeId) -> NodeId + Copy, gen_id: impl Fn() -> NodeId + Copy) { pub fn flatten_with_fns(&mut self, node: NodeId, map_ids: impl Fn(NodeId, NodeId) -> NodeId + Copy, gen_id: impl Fn() -> NodeId + Copy) {
self.resolve_extract_nodes();
let Some((id, mut node)) = self.nodes.remove_entry(&node) else { let Some((id, mut node)) = self.nodes.remove_entry(&node) else {
warn!("The node which was supposed to be flattened does not exist in the network, id {node} network {self:#?}"); warn!("The node which was supposed to be flattened does not exist in the network, id {node} network {self:#?}");
return; return;
@@ -850,7 +909,7 @@ impl NodeNetwork {
} }
// replace value inputs with value nodes // replace value inputs with value nodes
for input in &mut node.inputs { for input in node.inputs.iter_mut() {
// Skip inputs that are already value nodes // Skip inputs that are already value nodes
if node.implementation == DocumentNodeImplementation::Unresolved("graphene_core::value::ClonedNode".into()) { if node.implementation == DocumentNodeImplementation::Unresolved("graphene_core::value::ClonedNode".into()) {
break; break;
@@ -860,20 +919,17 @@ impl NodeNetwork {
if let NodeInput::Value { tagged_value, exposed } = previous_input { if let NodeInput::Value { tagged_value, exposed } = previous_input {
let value_node_id = gen_id(); let value_node_id = gen_id();
let merged_node_id = map_ids(id, value_node_id); let merged_node_id = map_ids(id, value_node_id);
let path = if let Some(mut new_path) = node.path.clone() { let mut original_location = node.original_location.clone();
new_path.push(value_node_id); if let Some(path) = &mut original_location.path {
Some(new_path) path.push(value_node_id);
} else { }
None
};
self.nodes.insert( self.nodes.insert(
merged_node_id, merged_node_id,
DocumentNode { DocumentNode {
name: "Value".into(), name: "Value".into(),
inputs: vec![NodeInput::Value { tagged_value, exposed }], inputs: vec![NodeInput::Value { tagged_value, exposed }],
implementation: DocumentNodeImplementation::Unresolved("graphene_core::value::ClonedNode".into()), implementation: DocumentNodeImplementation::Unresolved("graphene_core::value::ClonedNode".into()),
path, original_location,
..Default::default() ..Default::default()
}, },
); );
@@ -888,8 +944,6 @@ impl NodeNetwork {
} }
if let DocumentNodeImplementation::Network(mut inner_network) = node.implementation { if let DocumentNodeImplementation::Network(mut inner_network) = node.implementation {
// Resolve all extract nodes in the inner network
inner_network.resolve_extract_nodes();
// Connect all network inputs to either the parent network nodes, or newly created value nodes. // Connect all network inputs to either the parent network nodes, or newly created value nodes.
inner_network.map_ids(|inner_id| map_ids(id, inner_id)); inner_network.map_ids(|inner_id| map_ids(id, inner_id));
let new_nodes = inner_network.nodes.keys().cloned().collect::<Vec<_>>(); let new_nodes = inner_network.nodes.keys().cloned().collect::<Vec<_>>();
@@ -914,14 +968,15 @@ impl NodeNetwork {
"Document Nodes with a Network implementation should have the same number of inner network inputs as inputs declared on the Document Node" "Document Nodes with a Network implementation should have the same number of inner network inputs as inputs declared on the Document Node"
); );
// Match the document node input and the inputs of the inner network // Match the document node input and the inputs of the inner network
for (document_input, network_input) in node.inputs.into_iter().zip(inner_network.inputs.iter()) { for (input_index, (document_input, network_input)) in node.inputs.into_iter().zip(inner_network.inputs.iter()).enumerate() {
// Keep track of how many network inputs we have already connected for each node // Keep track of how many network inputs we have already connected for each node
let offset = network_offsets.entry(network_input).or_insert(0); let offset = network_offsets.entry(network_input).or_insert(0);
match document_input { match document_input {
// If the input to self is a node, connect the corresponding output of the inner network to it // If the input to self is a node, connect the corresponding output of the inner network to it
NodeInput::Node { node_id, output_index, lambda } => { NodeInput::Node { node_id, output_index, lambda } => {
let network_input = self.nodes.get_mut(network_input).unwrap(); let network_input = self.nodes.get_mut(network_input).unwrap();
network_input.populate_first_network_input(node_id, output_index, *offset, lambda); let skip = node.original_location.skip_inputs;
network_input.populate_first_network_input(node_id, output_index, *offset, lambda, node.original_location.inputs(input_index), skip);
} }
NodeInput::Network(_) => { NodeInput::Network(_) => {
*network_offsets.get_mut(network_input).unwrap() += 1; *network_offsets.get_mut(network_input).unwrap() += 1;
@@ -941,6 +996,13 @@ impl NodeNetwork {
self.replace_node_inputs(node_input(id, i, false), node_input(output.node_id, output.node_output_index, false)); self.replace_node_inputs(node_input(id, i, false), node_input(output.node_id, output.node_output_index, false));
self.replace_node_inputs(node_input(id, i, true), node_input(output.node_id, output.node_output_index, true)); self.replace_node_inputs(node_input(id, i, true), node_input(output.node_id, output.node_output_index, true));
if let Some(new_output_node) = self.nodes.get_mut(&output.node_id) {
for source in node.original_location.outputs(i) {
info!("{:?} {}", source, output.node_output_index);
new_output_node.original_location.outputs_source.insert(source, output.node_output_index);
}
}
self.replace_network_outputs(NodeOutput::new(id, i), output); self.replace_network_outputs(NodeOutput::new(id, i), output);
} }
@@ -960,9 +1022,15 @@ impl NodeNetwork {
if ident.name == "graphene_core::ops::IdentityNode" { if ident.name == "graphene_core::ops::IdentityNode" {
assert_eq!(node.inputs.len(), 1, "Id node has more than one input"); assert_eq!(node.inputs.len(), 1, "Id node has more than one input");
if let NodeInput::Node { node_id, output_index, .. } = node.inputs[0] { if let NodeInput::Node { node_id, output_index, .. } = node.inputs[0] {
if let Some(input_node) = self.nodes.get_mut(&node_id) {
for source in node.original_location.outputs(0) {
input_node.original_location.outputs_source.insert(source, output_index);
}
}
let input_node_id = node_id; let input_node_id = node_id;
for output in self.nodes.values_mut() { for output in self.nodes.values_mut() {
for input in &mut output.inputs { for (index, input) in output.inputs.iter_mut().enumerate() {
if let NodeInput::Node { if let NodeInput::Node {
node_id: output_node_id, node_id: output_node_id,
output_index: output_output_index, output_index: output_output_index,
@@ -972,6 +1040,11 @@ impl NodeNetwork {
if *output_node_id == id { if *output_node_id == id {
*output_node_id = input_node_id; *output_node_id = input_node_id;
*output_output_index = output_index; *output_output_index = output_index;
let input_source = &mut output.original_location.inputs_source;
for source in node.original_location.inputs(index) {
input_source.insert(source, index + output.original_location.skip_inputs - node.original_location.skip_inputs);
}
} }
} }
} }
@@ -1300,7 +1373,14 @@ mod test {
identifier: "graphene_core::structural::ConsNode".into(), identifier: "graphene_core::structural::ConsNode".into(),
input: ProtoNodeInput::ManualComposition(concrete!(u32)), input: ProtoNodeInput::ManualComposition(concrete!(u32)),
construction_args: ConstructionArgs::Nodes(vec![(NodeId(14), false)]), construction_args: ConstructionArgs::Nodes(vec![(NodeId(14), false)]),
document_node_path: vec![NodeId(1), NodeId(0)], original_location: OriginalLocation {
path: Some(vec![NodeId(1), NodeId(0)]),
inputs_source: [(Source { node: vec![NodeId(1)], index: 0 }, 1)].into(),
outputs_source: HashMap::new(),
inputs_exposed: vec![false, false],
skip_inputs: 0,
},
..Default::default() ..Default::default()
}, },
), ),
@@ -1310,7 +1390,13 @@ mod test {
identifier: "graphene_core::ops::AddPairNode".into(), identifier: "graphene_core::ops::AddPairNode".into(),
input: ProtoNodeInput::Node(NodeId(10), false), input: ProtoNodeInput::Node(NodeId(10), false),
construction_args: ConstructionArgs::Nodes(vec![]), construction_args: ConstructionArgs::Nodes(vec![]),
document_node_path: vec![NodeId(1), NodeId(1)], original_location: OriginalLocation {
path: Some(vec![NodeId(1), NodeId(1)]),
inputs_source: HashMap::new(),
outputs_source: [(Source { node: vec![NodeId(1)], index: 0 }, 0)].into(),
inputs_exposed: vec![true],
skip_inputs: 0,
},
..Default::default() ..Default::default()
}, },
), ),
@@ -1338,7 +1424,13 @@ mod test {
name: "Cons".into(), name: "Cons".into(),
inputs: vec![NodeInput::Network(concrete!(u32)), NodeInput::node(NodeId(14), 0)], inputs: vec![NodeInput::Network(concrete!(u32)), NodeInput::node(NodeId(14), 0)],
implementation: DocumentNodeImplementation::Unresolved("graphene_core::structural::ConsNode".into()), implementation: DocumentNodeImplementation::Unresolved("graphene_core::structural::ConsNode".into()),
path: Some(vec![NodeId(1), NodeId(0)]), original_location: OriginalLocation {
path: Some(vec![NodeId(1), NodeId(0)]),
inputs_source: [(Source { node: vec![NodeId(1)], index: 0 }, 1)].into(),
outputs_source: HashMap::new(),
inputs_exposed: vec![false, false],
skip_inputs: 0,
},
..Default::default() ..Default::default()
}, },
), ),
@@ -1351,7 +1443,13 @@ mod test {
exposed: false, exposed: false,
}], }],
implementation: DocumentNodeImplementation::Unresolved("graphene_core::value::ClonedNode".into()), implementation: DocumentNodeImplementation::Unresolved("graphene_core::value::ClonedNode".into()),
path: Some(vec![NodeId(1), NodeId(4)]), original_location: OriginalLocation {
path: Some(vec![NodeId(1), NodeId(4)]),
inputs_source: HashMap::new(),
outputs_source: HashMap::new(),
inputs_exposed: vec![false, false],
skip_inputs: 0,
},
..Default::default() ..Default::default()
}, },
), ),
@@ -1361,7 +1459,13 @@ mod test {
name: "Add".into(), name: "Add".into(),
inputs: vec![NodeInput::node(NodeId(10), 0)], inputs: vec![NodeInput::node(NodeId(10), 0)],
implementation: DocumentNodeImplementation::Unresolved("graphene_core::ops::AddPairNode".into()), implementation: DocumentNodeImplementation::Unresolved("graphene_core::ops::AddPairNode".into()),
path: Some(vec![NodeId(1), NodeId(1)]), original_location: OriginalLocation {
path: Some(vec![NodeId(1), NodeId(1)]),
inputs_source: HashMap::new(),
outputs_source: [(Source { node: vec![NodeId(1)], index: 0 }, 0)].into(),
inputs_exposed: vec![true],
skip_inputs: 0,
},
..Default::default() ..Default::default()
}, },
), ),
+137 -42
View File
@@ -1,16 +1,16 @@
use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::ops::Deref;
use std::hash::Hash;
use crate::document::NodeId;
use crate::document::{value, InlineRust}; use crate::document::{value, InlineRust};
use crate::document::{NodeId, OriginalLocation};
use dyn_any::DynAny; use dyn_any::DynAny;
use graphene_core::*; use graphene_core::*;
#[cfg(feature = "serde")] #[cfg(feature = "serde")]
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::fmt::Debug;
use std::hash::Hash;
use std::ops::Deref;
use std::pin::Pin; use std::pin::Pin;
pub type DynFuture<'n, T> = Pin<Box<dyn core::future::Future<Output = T> + 'n>>; pub type DynFuture<'n, T> = Pin<Box<dyn core::future::Future<Output = T> + 'n>>;
@@ -205,7 +205,7 @@ pub struct ProtoNode {
pub construction_args: ConstructionArgs, pub construction_args: ConstructionArgs,
pub input: ProtoNodeInput, pub input: ProtoNodeInput,
pub identifier: ProtoNodeIdentifier, pub identifier: ProtoNodeIdentifier,
pub document_node_path: Vec<NodeId>, pub original_location: OriginalLocation,
pub skip_deduplication: bool, pub skip_deduplication: bool,
// TODO: This is a hack, figure out a proper solution // TODO: This is a hack, figure out a proper solution
/// Represents a global state on which the node depends. /// Represents a global state on which the node depends.
@@ -218,7 +218,7 @@ impl Default for ProtoNode {
identifier: ProtoNodeIdentifier::new("graphene_core::ops::IdentityNode"), identifier: ProtoNodeIdentifier::new("graphene_core::ops::IdentityNode"),
construction_args: ConstructionArgs::Value(value::TaggedValue::U32(0)), construction_args: ConstructionArgs::Value(value::TaggedValue::U32(0)),
input: ProtoNodeInput::None, input: ProtoNodeInput::None,
document_node_path: vec![], original_location: OriginalLocation::default(),
skip_deduplication: false, skip_deduplication: false,
world_state_hash: 0, world_state_hash: 0,
} }
@@ -266,7 +266,7 @@ impl ProtoNode {
self.identifier.name.hash(&mut hasher); self.identifier.name.hash(&mut hasher);
self.construction_args.hash(&mut hasher); self.construction_args.hash(&mut hasher);
if self.skip_deduplication { if self.skip_deduplication {
self.document_node_path.hash(&mut hasher); self.original_location.path.hash(&mut hasher);
} }
self.world_state_hash.hash(&mut hasher); self.world_state_hash.hash(&mut hasher);
std::mem::discriminant(&self.input).hash(&mut hasher); std::mem::discriminant(&self.input).hash(&mut hasher);
@@ -282,11 +282,19 @@ impl ProtoNode {
/// Construct a new [`ProtoNode`] with the specified construction args and a `ClonedNode` implementation. /// Construct a new [`ProtoNode`] with the specified construction args and a `ClonedNode` implementation.
pub fn value(value: ConstructionArgs, path: Vec<NodeId>) -> Self { pub fn value(value: ConstructionArgs, path: Vec<NodeId>) -> Self {
let inputs_exposed = match &value {
ConstructionArgs::Nodes(nodes) => nodes.len() + 1,
_ => 2,
};
Self { Self {
identifier: ProtoNodeIdentifier::new("graphene_core::value::ClonedNode"), identifier: ProtoNodeIdentifier::new("graphene_core::value::ClonedNode"),
construction_args: value, construction_args: value,
input: ProtoNodeInput::None, input: ProtoNodeInput::None,
document_node_path: path, original_location: OriginalLocation {
path: Some(path),
inputs_exposed: vec![false; inputs_exposed],
..Default::default()
},
skip_deduplication: false, skip_deduplication: false,
world_state_hash: 0, world_state_hash: 0,
} }
@@ -396,8 +404,10 @@ impl ProtoNetwork {
let input = input_node_id_proto.input.clone(); let input = input_node_id_proto.input.clone();
let mut path = input_node_id_proto.document_node_path.clone(); let mut path = input_node_id_proto.original_location.path.clone();
path.push(node_id); if let Some(path) = &mut path {
path.push(node_id);
}
self.nodes.push(( self.nodes.push((
compose_node_id, compose_node_id,
@@ -405,7 +415,7 @@ impl ProtoNetwork {
identifier: ProtoNodeIdentifier::new("graphene_core::structural::ComposeNode<_, _, _>"), identifier: ProtoNodeIdentifier::new("graphene_core::structural::ComposeNode<_, _, _>"),
construction_args: ConstructionArgs::Nodes(vec![(input_node_id, false), (node_id, true)]), construction_args: ConstructionArgs::Nodes(vec![(input_node_id, false), (node_id, true)]),
input, input,
document_node_path: path, original_location: OriginalLocation { path, ..Default::default() },
skip_deduplication: false, skip_deduplication: false,
world_state_hash: 0, world_state_hash: 0,
}, },
@@ -544,6 +554,78 @@ impl ProtoNetwork {
Ok(()) Ok(())
} }
} }
#[derive(Clone, PartialEq)]
pub enum GraphErrorType {
NodeNotFound(NodeId),
InputNodeNotFound(NodeId),
UnexpectedGenerics { index: usize, parameters: Vec<Type> },
NoImplementations,
NoConstructor,
InvalidImplementations { parameters: String, error_inputs: Vec<Vec<(usize, (Type, Type))>> },
MultipleImplementations { parameters: String, valid: Vec<NodeIOTypes> },
}
impl core::fmt::Debug for GraphErrorType {
// TODO: format with the document graph context so the input index is the same as in the graph UI.
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
GraphErrorType::NodeNotFound(id) => write!(f, "Input node {id} is not present in the typing context"),
GraphErrorType::InputNodeNotFound(id) => write!(f, "Input node {id} is not present in the typing context"),
GraphErrorType::UnexpectedGenerics { index, parameters } => write!(f, "Generic parameters should not exist but found at {index}: {parameters:?}"),
GraphErrorType::NoImplementations => write!(f, "No implementations found"),
GraphErrorType::NoConstructor => write!(f, "No construct found for node"),
GraphErrorType::InvalidImplementations { parameters, error_inputs } => {
let ordinal = |x: usize| match x.to_string().as_str() {
x if x.ends_with('1') && !x.ends_with("11") => format!("{x}st"),
x if x.ends_with('2') && !x.ends_with("12") => format!("{x}nd"),
x if x.ends_with('3') && !x.ends_with("13") => format!("{x}rd"),
x => format!("{x}th parameter"),
};
let format_index = |index: usize| if index == 0 { "primary".to_string() } else { format!("{} parameter", ordinal(index - 1)) };
let format_error = |(index, (real, expected)): &(usize, (Type, Type))| format!("• The {} input expected {} but found {}", format_index(*index), expected, real);
let format_error_list = |errors: &Vec<(usize, (Type, Type))>| errors.iter().map(format_error).collect::<Vec<_>>().join("\n");
let errors = error_inputs.iter().map(format_error_list).collect::<Vec<_>>();
write!(
f,
"Node graph type error! If this just appeared while editing the graph,\n\
consider using undo to go back and trying another way to connect the nodes.\n\
\n\
No node implementation exists for type ({parameters}).\n\
\n\
Caused by{}:\n\
{}",
if errors.len() > 1 { " one of" } else { "" },
errors.join("\n")
)
}
GraphErrorType::MultipleImplementations { parameters, valid } => write!(f, "Multiple implementations found ({parameters}):\n{valid:#?}"),
}
}
}
#[derive(Clone, PartialEq)]
pub struct GraphError {
pub node_path: Vec<NodeId>,
pub identifier: Cow<'static, str>,
pub error: GraphErrorType,
}
impl GraphError {
pub fn new(node: &ProtoNode, text: impl Into<GraphErrorType>) -> Self {
Self {
node_path: node.original_location.path.clone().unwrap_or_default(),
identifier: node.identifier.name.clone(),
error: text.into(),
}
}
}
impl core::fmt::Debug for GraphError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("NodeGraphError")
.field("path", &self.node_path.iter().map(|id| id.0).collect::<Vec<_>>())
.field("identifier", &self.identifier.to_string())
.field("error", &self.error)
.finish()
}
}
pub type GraphErrors = Vec<GraphError>;
/// The `TypingContext` is used to store the types of the nodes indexed by their stable node id. /// The `TypingContext` is used to store the types of the nodes indexed by their stable node id.
#[derive(Default, Clone)] #[derive(Default, Clone)]
@@ -562,10 +644,10 @@ impl TypingContext {
} }
} }
/// Updates the `TypingContext` wtih a given proto network. This will infer the types of the nodes /// Updates the `TypingContext` with a given proto network. This will infer the types of the nodes
/// and store them in the `inferred` field. The proto network has to be topologically sorted /// and store them in the `inferred` field. The proto network has to be topologically sorted
/// and contain fully resolved stable node ids. /// and contain fully resolved stable node ids.
pub fn update(&mut self, network: &ProtoNetwork) -> Result<(), String> { pub fn update(&mut self, network: &ProtoNetwork) -> Result<(), GraphErrors> {
for (id, node) in network.nodes.iter() { for (id, node) in network.nodes.iter() {
self.infer(*id, node)?; self.infer(*id, node)?;
} }
@@ -583,12 +665,10 @@ impl TypingContext {
} }
/// Returns the inferred types for a given node id. /// Returns the inferred types for a given node id.
pub fn infer(&mut self, node_id: NodeId, node: &ProtoNode) -> Result<NodeIOTypes, String> { pub fn infer(&mut self, node_id: NodeId, node: &ProtoNode) -> Result<NodeIOTypes, GraphErrors> {
let identifier = node.identifier.name.clone();
// Return the inferred type if it is already known // Return the inferred type if it is already known
if let Some(infered) = self.inferred.get(&node_id) { if let Some(inferred) = self.inferred.get(&node_id) {
return Ok(infered.clone()); return Ok(inferred.clone());
} }
let parameters = match node.construction_args { let parameters = match node.construction_args {
@@ -606,10 +686,10 @@ impl TypingContext {
.map(|(id, _)| { .map(|(id, _)| {
self.inferred self.inferred
.get(id) .get(id)
.ok_or(format!("Inferring type of {node_id} depends on {id} which is not present in the typing context")) .ok_or_else(|| vec![GraphError::new(node, GraphErrorType::NodeNotFound(*id))])
.map(|node| node.ty()) .map(|node| node.ty())
}) })
.collect::<Result<Vec<Type>, String>>()?, .collect::<Result<Vec<Type>, GraphErrors>>()?,
ConstructionArgs::Inline(ref inline) => vec![inline.ty.clone()], ConstructionArgs::Inline(ref inline) => vec![inline.ty.clone()],
}; };
@@ -618,23 +698,17 @@ impl TypingContext {
ProtoNodeInput::None => concrete!(()), ProtoNodeInput::None => concrete!(()),
ProtoNodeInput::ManualComposition(ref ty) => ty.clone(), ProtoNodeInput::ManualComposition(ref ty) => ty.clone(),
ProtoNodeInput::Node(id, _) => { ProtoNodeInput::Node(id, _) => {
let input = self let input = self.inferred.get(&id).ok_or_else(|| vec![GraphError::new(node, GraphErrorType::InputNodeNotFound(id))])?;
.inferred
.get(&id)
.ok_or(format!("Inferring type of {node_id} depends on {id} which is not present in the typing context"))?;
input.output.clone() input.output.clone()
} }
}; };
let impls = self let impls = self.lookup.get(&node.identifier).ok_or_else(|| vec![GraphError::new(node, GraphErrorType::NoImplementations)])?;
.lookup
.get(&node.identifier)
.ok_or(format!("No implementations found for:\n\n{:?}\n\nOther implementations found:\n\n{:?}", node.identifier, self.lookup))?;
if parameters.iter().any(|p| { if let Some(index) = parameters.iter().position(|p| {
matches!(p, matches!(p,
Type::Fn(_, b) if matches!(b.as_ref(), Type::Generic(_))) Type::Fn(_, b) if matches!(b.as_ref(), Type::Generic(_)))
}) { }) {
return Err(format!("Generic types are not supported in parameters: {:?} occurred in {:?}", parameters, node.identifier)); return Err(vec![GraphError::new(node, GraphErrorType::UnexpectedGenerics { index, parameters })]);
} }
fn covariant(from: &Type, to: &Type) -> bool { fn covariant(from: &Type, to: &Type) -> bool {
match (from, to) { match (from, to) {
@@ -651,7 +725,7 @@ impl TypingContext {
// List of all implementations that match the input and parameter types // List of all implementations that match the input and parameter types
let valid_output_types = impls let valid_output_types = impls
.keys() .keys()
.filter(|node_io| covariant(&input, &node_io.input) && parameters.iter().zip(node_io.parameters.iter()).all(|(p1, p2)| covariant(p1, p2) && covariant(p1, p2))) .filter(|node_io| covariant(&input, &node_io.input) && parameters.iter().zip(node_io.parameters.iter()).all(|(p1, p2)| covariant(p1, p2)))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
// Attempt to substitute generic types with concrete types and save the list of results // Attempt to substitute generic types with concrete types and save the list of results
@@ -677,10 +751,28 @@ impl TypingContext {
match valid_impls.as_slice() { match valid_impls.as_slice() {
[] => { [] => {
dbg!(&self.inferred); dbg!(&self.inferred);
Err(format!( let mut best_errors = usize::MAX;
"No implementations found for:\n\n{identifier}\n\nwith input:\n\n{input:?}\n\nand parameters:\n\n{parameters:?}\n\nOther Implementations found:\n\n{:?}", let mut error_inputs = Vec::new();
impls.keys().collect::<Vec<_>>(), for node_io in impls.keys() {
)) let current_errors = [&input]
.into_iter()
.chain(&parameters)
.cloned()
.zip([&node_io.input].into_iter().chain(&node_io.parameters).cloned())
.enumerate()
.filter(|(_, (p1, p2))| !covariant(p1, p2))
.map(|(index, ty)| (node.original_location.inputs(index).min_by_key(|s| s.node.len()).map(|s| s.index).unwrap_or(index), ty))
.collect::<Vec<_>>();
if current_errors.len() < best_errors {
best_errors = current_errors.len();
error_inputs.clear();
}
if current_errors.len() <= best_errors {
error_inputs.push(current_errors);
}
}
let parameters = [&input].into_iter().chain(&parameters).map(|t| t.to_string()).collect::<Vec<_>>().join(", ");
Err(vec![GraphError::new(node, GraphErrorType::InvalidImplementations { parameters, error_inputs })])
} }
[(org_nio, output)] => { [(org_nio, output)] => {
let node_io = NodeIOTypes::new(input, (*output).clone(), parameters); let node_io = NodeIOTypes::new(input, (*output).clone(), parameters);
@@ -690,9 +782,12 @@ impl TypingContext {
self.constructor.insert(node_id, impls[org_nio]); self.constructor.insert(node_id, impls[org_nio]);
Ok(node_io) Ok(node_io)
} }
_ => Err(format!(
"Multiple implementations found for {identifier} with input {input:?} and parameters {parameters:?} (valid types: {valid_output_types:?}" _ => {
)), let parameters = [&input].into_iter().chain(&parameters).map(|t| t.to_string()).collect::<Vec<_>>().join(", ");
let valid = valid_output_types.into_iter().cloned().collect();
Err(vec![GraphError::new(node, GraphErrorType::MultipleImplementations { parameters, valid })])
}
} }
} }
} }
@@ -1,15 +1,16 @@
use std::collections::{HashMap, HashSet}; use crate::node_registry;
use std::error::Error;
use std::sync::Arc;
use dyn_any::StaticType; use dyn_any::StaticType;
use graph_craft::document::value::{TaggedValue, UpcastNode}; use graph_craft::document::value::{TaggedValue, UpcastNode};
use graph_craft::document::NodeId; use graph_craft::document::{NodeId, Source};
use graph_craft::graphene_compiler::Executor; use graph_craft::graphene_compiler::Executor;
use graph_craft::proto::{ConstructionArgs, LocalFuture, NodeContainer, ProtoNetwork, ProtoNode, SharedNodeContainer, TypeErasedBox, TypingContext}; use graph_craft::proto::{ConstructionArgs, GraphError, LocalFuture, NodeContainer, ProtoNetwork, ProtoNode, SharedNodeContainer, TypeErasedBox, TypingContext};
use graph_craft::proto::{GraphErrorType, GraphErrors};
use graph_craft::Type; use graph_craft::Type;
use crate::node_registry; use std::collections::{HashMap, HashSet};
use std::error::Error;
use std::sync::Arc;
/// An executor of a node graph that does not require an online compilation server, and instead uses `Box<dyn ...>`. /// An executor of a node graph that does not require an online compilation server, and instead uses `Box<dyn ...>`.
pub struct DynamicExecutor { pub struct DynamicExecutor {
@@ -33,8 +34,15 @@ impl Default for DynamicExecutor {
} }
} }
#[derive(PartialEq, Clone, Debug, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ResolvedDocumentNodeTypes {
pub inputs: HashMap<Source, Type>,
pub outputs: HashMap<Source, Type>,
}
impl DynamicExecutor { impl DynamicExecutor {
pub async fn new(proto_network: ProtoNetwork) -> Result<Self, String> { pub async fn new(proto_network: ProtoNetwork) -> Result<Self, GraphErrors> {
let mut typing_context = TypingContext::new(&node_registry::NODE_REGISTRY); let mut typing_context = TypingContext::new(&node_registry::NODE_REGISTRY);
typing_context.update(&proto_network)?; typing_context.update(&proto_network)?;
let output = proto_network.output; let output = proto_network.output;
@@ -49,7 +57,7 @@ impl DynamicExecutor {
} }
/// Updates the existing [`BorrowTree`] to reflect the new [`ProtoNetwork`], reusing nodes where possible. /// Updates the existing [`BorrowTree`] to reflect the new [`ProtoNetwork`], reusing nodes where possible.
pub async fn update(&mut self, proto_network: ProtoNetwork) -> Result<(), String> { pub async fn update(&mut self, proto_network: ProtoNetwork) -> Result<(), GraphErrors> {
self.output = proto_network.output; self.output = proto_network.output;
self.typing_context.update(&proto_network)?; self.typing_context.update(&proto_network)?;
let mut orphans = self.tree.update(proto_network, &self.typing_context).await?; let mut orphans = self.tree.update(proto_network, &self.typing_context).await?;
@@ -74,6 +82,22 @@ impl DynamicExecutor {
pub fn output_type(&self) -> Option<Type> { pub fn output_type(&self) -> Option<Type> {
self.typing_context.type_of(self.output).map(|node_io| node_io.output.clone()) self.typing_context.type_of(self.output).map(|node_io| node_io.output.clone())
} }
pub fn document_node_types(&self) -> ResolvedDocumentNodeTypes {
let mut resolved_document_node_types = ResolvedDocumentNodeTypes::default();
for (source, &(protonode_id, protonode_index)) in self.tree.inputs_source_map() {
let Some(node_io) = self.typing_context.type_of(protonode_id) else { continue };
let Some(ty) = [&node_io.input].into_iter().chain(&node_io.parameters).nth(protonode_index) else {
continue;
};
resolved_document_node_types.inputs.insert(source.clone(), ty.clone());
}
for (source, &protonode_id) in self.tree.outputs_source_map() {
let Some(node_io) = self.typing_context.type_of(protonode_id) else { continue };
resolved_document_node_types.outputs.insert(source.clone(), node_io.output.clone());
}
resolved_document_node_types
}
} }
impl<'a, I: StaticType + 'a> Executor<I, TaggedValue> for &'a DynamicExecutor { impl<'a, I: StaticType + 'a> Executor<I, TaggedValue> for &'a DynamicExecutor {
@@ -89,10 +113,14 @@ pub struct BorrowTree {
nodes: HashMap<NodeId, SharedNodeContainer>, nodes: HashMap<NodeId, SharedNodeContainer>,
/// A hashmap from the document path to the protonode ID. /// A hashmap from the document path to the protonode ID.
source_map: HashMap<Vec<NodeId>, NodeId>, source_map: HashMap<Vec<NodeId>, NodeId>,
/// Each document input source maps to one protonode input (however one protonode input may come from several sources)
inputs_source_map: HashMap<Source, (NodeId, usize)>,
/// A mapping of document input sources to the (single) protonode output
outputs_source_map: HashMap<Source, NodeId>,
} }
impl BorrowTree { impl BorrowTree {
pub async fn new(proto_network: ProtoNetwork, typing_context: &TypingContext) -> Result<BorrowTree, String> { pub async fn new(proto_network: ProtoNetwork, typing_context: &TypingContext) -> Result<BorrowTree, GraphErrors> {
let mut nodes = BorrowTree::default(); let mut nodes = BorrowTree::default();
for (id, node) in proto_network.nodes { for (id, node) in proto_network.nodes {
nodes.push_node(id, node, typing_context).await? nodes.push_node(id, node, typing_context).await?
@@ -101,7 +129,7 @@ impl BorrowTree {
} }
/// Pushes new nodes into the tree and return orphaned nodes /// Pushes new nodes into the tree and return orphaned nodes
pub async fn update(&mut self, proto_network: ProtoNetwork, typing_context: &TypingContext) -> Result<Vec<NodeId>, String> { pub async fn update(&mut self, proto_network: ProtoNetwork, typing_context: &TypingContext) -> Result<Vec<NodeId>, GraphErrors> {
let mut old_nodes: HashSet<_> = self.nodes.keys().copied().collect(); let mut old_nodes: HashSet<_> = self.nodes.keys().copied().collect();
for (id, node) in proto_network.nodes { for (id, node) in proto_network.nodes {
if !self.nodes.contains_key(&id) { if !self.nodes.contains_key(&id) {
@@ -110,6 +138,8 @@ impl BorrowTree {
old_nodes.remove(&id); old_nodes.remove(&id);
} }
self.source_map.retain(|_, nid| !old_nodes.contains(nid)); self.source_map.retain(|_, nid| !old_nodes.contains(nid));
self.inputs_source_map.retain(|_, (nid, _)| !old_nodes.contains(nid));
self.outputs_source_map.retain(|_, nid| !old_nodes.contains(nid));
self.nodes.retain(|nid, _| !old_nodes.contains(nid)); self.nodes.retain(|nid, _| !old_nodes.contains(nid));
Ok(old_nodes.into_iter().collect()) Ok(old_nodes.into_iter().collect())
} }
@@ -152,18 +182,23 @@ impl BorrowTree {
} }
/// Insert a new node into the borrow tree, calling the constructor function from `node_registry.rs`. /// Insert a new node into the borrow tree, calling the constructor function from `node_registry.rs`.
pub async fn push_node(&mut self, id: NodeId, proto_node: ProtoNode, typing_context: &TypingContext) -> Result<(), String> { pub async fn push_node(&mut self, id: NodeId, proto_node: ProtoNode, typing_context: &TypingContext) -> Result<(), GraphErrors> {
let ProtoNode { self.source_map.insert(proto_node.original_location.path.clone().unwrap_or_default(), id);
construction_args,
identifier,
document_node_path,
..
} = proto_node;
self.source_map.insert(document_node_path, id);
match construction_args { let params = match &proto_node.construction_args {
ConstructionArgs::Nodes(nodes) => nodes.len() + 1,
_ => 2,
};
self.inputs_source_map
.extend((0..params).flat_map(|i| proto_node.original_location.inputs(i).map(move |source| (source, (id, i)))));
self.outputs_source_map.extend(proto_node.original_location.outputs(0).map(|source| (source, id)));
for x in proto_node.original_location.outputs_source.values() {
assert_eq!(*x, 0, "protonodes should refer to output index 0");
}
match &proto_node.construction_args {
ConstructionArgs::Value(value) => { ConstructionArgs::Value(value) => {
let upcasted = UpcastNode::new(value); let upcasted = UpcastNode::new(value.to_owned());
let node = Box::new(upcasted) as TypeErasedBox<'_>; let node = Box::new(upcasted) as TypeErasedBox<'_>;
let node = NodeContainer::new(node); let node = NodeContainer::new(node);
self.store_node(node, id); self.store_node(node, id);
@@ -172,7 +207,7 @@ impl BorrowTree {
ConstructionArgs::Nodes(ids) => { ConstructionArgs::Nodes(ids) => {
let ids: Vec<_> = ids.iter().map(|(id, _)| *id).collect(); let ids: Vec<_> = ids.iter().map(|(id, _)| *id).collect();
let construction_nodes = self.node_deps(&ids); let construction_nodes = self.node_deps(&ids);
let constructor = typing_context.constructor(id).ok_or(format!("No constructor found for node {identifier:?}"))?; let constructor = typing_context.constructor(id).ok_or_else(|| vec![GraphError::new(&proto_node, GraphErrorType::NoConstructor)])?;
let node = constructor(construction_nodes).await; let node = constructor(construction_nodes).await;
let node = NodeContainer::new(node); let node = NodeContainer::new(node);
self.store_node(node, id); self.store_node(node, id);
@@ -180,6 +215,14 @@ impl BorrowTree {
}; };
Ok(()) Ok(())
} }
pub fn inputs_source_map(&self) -> impl Iterator<Item = (&Source, &(NodeId, usize))> {
self.inputs_source_map.iter()
}
pub fn outputs_source_map(&self) -> impl Iterator<Item = (&Source, &NodeId)> {
self.outputs_source_map.iter()
}
} }
#[cfg(test)] #[cfg(test)]
+1 -1
View File
@@ -73,7 +73,7 @@ mod tests {
let compiler = Compiler {}; let compiler = Compiler {};
let protograph = compiler.compile_single(network).expect("Graph should be generated"); let protograph = compiler.compile_single(network).expect("Graph should be generated");
let exec = block_on(DynamicExecutor::new(protograph)).unwrap_or_else(|e| panic!("Failed to create executor: {e}")); let exec = block_on(DynamicExecutor::new(protograph)).unwrap_or_else(|e| panic!("Failed to create executor: {e:?}"));
let result = block_on((&exec).execute(32_u32)).unwrap(); let result = block_on((&exec).execute(32_u32)).unwrap();
assert_eq!(result, TaggedValue::U32(33)); assert_eq!(result, TaggedValue::U32(33));
@@ -112,6 +112,7 @@ body > h2 {
svg text { svg text {
pointer-events: none; pointer-events: none;
-webkit-user-select: none;
user-select: none; user-select: none;
} }
+2
View File
@@ -280,6 +280,7 @@ pre {
font-size: 0.75em; font-size: 0.75em;
font-weight: bold; font-weight: bold;
font-style: italic; font-style: italic;
-webkit-user-select: none;
user-select: none; user-select: none;
pointer-events: none; pointer-events: none;
position: fixed; position: fixed;
@@ -326,6 +327,7 @@ pre {
&:first-child { &:first-child {
padding-left: 20px; padding-left: 20px;
padding-right: 10px; padding-right: 10px;
-webkit-user-select: none;
user-select: none; user-select: none;
vertical-align: top; vertical-align: top;
text-align: right; text-align: right;