mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-08-04 13:30:48 +00:00
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:
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")]
|
||||
pub rulers_visible: bool,
|
||||
#[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
|
||||
// =============================================
|
||||
|
@ -265,7 +265,7 @@ impl MessageHandler<DocumentMessage, DocumentInputs<'_>> for DocumentMessageHand
|
|||
node_graph_message_handler: &self.node_graph_handler,
|
||||
executor,
|
||||
document_name: self.name.as_str(),
|
||||
document_network: &mut self.network,
|
||||
document_network: &self.network,
|
||||
document_metadata: &mut self.metadata,
|
||||
};
|
||||
self.properties_panel_message_handler
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
use crate::messages::prelude::*;
|
||||
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graph_craft::document::{DocumentNode, NodeId, NodeInput};
|
||||
use graph_craft::proto::GraphErrors;
|
||||
use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypes;
|
||||
|
||||
#[impl_message(Message, DocumentMessage, NodeGraph)]
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
|
@ -111,4 +112,10 @@ pub enum NodeGraphMessage {
|
|||
node_id: NodeId,
|
||||
},
|
||||
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::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
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 interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypes;
|
||||
mod document_node_types;
|
||||
mod node_properties;
|
||||
|
||||
|
@ -23,22 +24,22 @@ pub enum FrontendGraphDataType {
|
|||
Raster,
|
||||
#[serde(rename = "color")]
|
||||
Color,
|
||||
#[serde(rename = "number")]
|
||||
#[serde(rename = "general")]
|
||||
Text,
|
||||
#[serde(rename = "vector")]
|
||||
Subpath,
|
||||
#[serde(rename = "number")]
|
||||
Number,
|
||||
#[serde(rename = "number")]
|
||||
#[serde(rename = "general")]
|
||||
Boolean,
|
||||
/// Refers to the mathematical vector, with direction and magnitude.
|
||||
#[serde(rename = "vec2")]
|
||||
#[serde(rename = "number")]
|
||||
Vector,
|
||||
#[serde(rename = "graphic")]
|
||||
#[serde(rename = "raster")]
|
||||
GraphicGroup,
|
||||
#[serde(rename = "artboard")]
|
||||
Artboard,
|
||||
#[serde(rename = "palette")]
|
||||
#[serde(rename = "color")]
|
||||
Palette,
|
||||
}
|
||||
impl FrontendGraphDataType {
|
||||
|
@ -65,6 +66,8 @@ pub struct FrontendGraphInput {
|
|||
#[serde(rename = "dataType")]
|
||||
data_type: FrontendGraphDataType,
|
||||
name: String,
|
||||
#[serde(rename = "resolvedType")]
|
||||
resolved_type: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
|
@ -72,6 +75,8 @@ pub struct FrontendGraphOutput {
|
|||
#[serde(rename = "dataType")]
|
||||
data_type: FrontendGraphDataType,
|
||||
name: String,
|
||||
#[serde(rename = "resolvedType")]
|
||||
resolved_type: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
|
@ -92,6 +97,7 @@ pub struct FrontendNode {
|
|||
pub position: (i32, i32),
|
||||
pub disabled: bool,
|
||||
pub previewed: bool,
|
||||
pub errors: Option<String>,
|
||||
}
|
||||
|
||||
// (link_start, link_end, link_end_input_index)
|
||||
|
@ -124,6 +130,8 @@ impl FrontendNodeType {
|
|||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct NodeGraphMessageHandler {
|
||||
pub network: Vec<NodeId>,
|
||||
pub resolved_types: ResolvedDocumentNodeTypes,
|
||||
pub node_graph_errors: GraphErrors,
|
||||
has_selection: bool,
|
||||
widgets: [LayoutGroup; 2],
|
||||
}
|
||||
|
@ -144,6 +152,8 @@ impl Default for NodeGraphMessageHandler {
|
|||
|
||||
Self {
|
||||
network: Vec::new(),
|
||||
resolved_types: ResolvedDocumentNodeTypes::default(),
|
||||
node_graph_errors: Vec::new(),
|
||||
has_selection: false,
|
||||
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);
|
||||
|
||||
if !graph_view_overlay_open {
|
||||
|
@ -298,6 +308,7 @@ impl NodeGraphMessageHandler {
|
|||
|
||||
let mut nodes = Vec::new();
|
||||
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
|
||||
let Some(node_type) = document_node_types::resolve_document_node_type(&node.name) else {
|
||||
warn!("Node '{}' does not exist in library", node.name);
|
||||
|
@ -305,20 +316,26 @@ impl NodeGraphMessageHandler {
|
|||
};
|
||||
|
||||
// Inputs
|
||||
let mut inputs = node.inputs.iter().zip(node_type.inputs.iter().map(|input_type| FrontendGraphInput {
|
||||
data_type: input_type.data_type,
|
||||
name: input_type.name.to_string(),
|
||||
let mut inputs = node.inputs.iter().zip(node_type.inputs.iter().enumerate().map(|(index, input_type)| {
|
||||
let index = node.inputs.iter().take(index).filter(|input| input.is_exposed()).count();
|
||||
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 exposed_inputs = inputs.filter(|(input, _)| input.is_exposed()).map(|(_, input_type)| input_type).collect();
|
||||
|
||||
// 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,
|
||||
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 errors = self.node_graph_errors.iter().find(|error| error.node_path.starts_with(&node_path)).map(|error| error.error.clone());
|
||||
nodes.push(FrontendNode {
|
||||
is_layer: node.is_layer(),
|
||||
id: *id,
|
||||
|
@ -331,6 +348,7 @@ impl NodeGraphMessageHandler {
|
|||
position: node.metadata.position.into(),
|
||||
previewed: network.outputs_contain(*id),
|
||||
disabled: network.disabled.contains(id),
|
||||
errors: errors.map(|e| format!("{e:?}")),
|
||||
})
|
||||
}
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
|
@ -635,7 +653,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
|
|||
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);
|
||||
responses.add(NodeGraphMessage::SendGraph { should_rerender: false });
|
||||
}
|
||||
|
@ -648,7 +666,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
|
|||
self.network.pop();
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
@ -698,7 +716,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
|
|||
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 } => {
|
||||
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 } => {
|
||||
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 {
|
||||
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) {
|
||||
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
|
||||
if network.connected_to_output(node_id) {
|
||||
|
@ -926,7 +944,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
|
|||
} else {
|
||||
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);
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
|
@ -936,13 +954,25 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
|
|||
metadata.clear_selected_nodes();
|
||||
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();
|
||||
responses.add(FrontendMessage::UpdateNodeTypes { node_types });
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
|
|
@ -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> {
|
||||
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| {
|
||||
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> {
|
||||
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| {
|
||||
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 {
|
||||
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 &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 vector_data = start_widgets(document_node, node_id, 0, "Data", FrontendGraphDataType::Vector, false);
|
||||
let vector_data = LayoutGroup::Row { widgets: vector_data };
|
||||
vec![vector_data, translation, rotation, scale]
|
||||
vec![translation, rotation, scale]
|
||||
}
|
||||
|
||||
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 document_name: &'a str,
|
||||
pub document_network: &'a mut NodeNetwork,
|
||||
pub document_network: &'a NodeNetwork,
|
||||
pub document_metadata: &'a mut DocumentMetadata,
|
||||
pub node_graph_message_handler: &'a NodeGraphMessageHandler,
|
||||
pub executor: &'a mut NodeGraphExecutor,
|
||||
|
|
|
@ -12,6 +12,7 @@ use graph_craft::document::value::TaggedValue;
|
|||
use graph_craft::document::{generate_uuid, DocumentNodeImplementation, NodeId, NodeNetwork};
|
||||
use graph_craft::graphene_compiler::Compiler;
|
||||
use graph_craft::imaginate_input::ImaginatePreferences;
|
||||
use graph_craft::proto::GraphErrors;
|
||||
use graphene_core::application_io::{NodeGraphUpdateMessage, NodeGraphUpdateSender, RenderConfig};
|
||||
use graphene_core::memo::IORecord;
|
||||
use graphene_core::raster::{Image, ImageFrame};
|
||||
|
@ -22,7 +23,7 @@ use graphene_core::vector::style::ViewMode;
|
|||
use graphene_core::vector::VectorData;
|
||||
use graphene_core::{Color, GraphicElement, SurfaceFrame};
|
||||
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 std::cell::RefCell;
|
||||
|
@ -40,6 +41,8 @@ pub struct NodeRuntime {
|
|||
pub(crate) thumbnails: HashMap<NodeId, SvgSegmentList>,
|
||||
pub(crate) click_targets: HashMap<NodeId, Vec<ClickTarget>>,
|
||||
pub(crate) upstream_transforms: HashMap<NodeId, (Footprint, DAffine2)>,
|
||||
pub(crate) resolved_types: ResolvedDocumentNodeTypes,
|
||||
pub(crate) node_graph_errors: GraphErrors,
|
||||
graph_hash: Option<u64>,
|
||||
monitor_nodes: Vec<Vec<NodeId>>,
|
||||
}
|
||||
|
@ -73,6 +76,8 @@ pub(crate) struct GenerationResponse {
|
|||
new_thumbnails: HashMap<NodeId, SvgSegmentList>,
|
||||
new_click_targets: HashMap<LayerNodeIdentifier, Vec<ClickTarget>>,
|
||||
new_upstream_transforms: HashMap<NodeId, (Footprint, DAffine2)>,
|
||||
resolved_types: ResolvedDocumentNodeTypes,
|
||||
node_graph_errors: GraphErrors,
|
||||
transform: DAffine2,
|
||||
}
|
||||
|
||||
|
@ -113,6 +118,8 @@ impl NodeRuntime {
|
|||
click_targets: HashMap::new(),
|
||||
graph_hash: None,
|
||||
upstream_transforms: HashMap::new(),
|
||||
resolved_types: ResolvedDocumentNodeTypes::default(),
|
||||
node_graph_errors: Vec::new(),
|
||||
monitor_nodes: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
@ -147,6 +154,8 @@ impl NodeRuntime {
|
|||
new_thumbnails: self.thumbnails.clone(),
|
||||
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(),
|
||||
resolved_types: self.resolved_types.clone(),
|
||||
node_graph_errors: core::mem::take(&mut self.node_graph_errors),
|
||||
transform,
|
||||
};
|
||||
self.sender.send_generation_response(response);
|
||||
|
@ -188,7 +197,7 @@ impl NodeRuntime {
|
|||
self.monitor_nodes = scoped_network
|
||||
.recursive_nodes()
|
||||
.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<_>>();
|
||||
|
||||
// We assume only one output
|
||||
|
@ -201,11 +210,11 @@ impl NodeRuntime {
|
|||
|
||||
assert_ne!(proto_network.nodes.len(), 0, "No protonodes exist?");
|
||||
if let Err(e) = self.executor.update(proto_network).await {
|
||||
error!("Failed to update executor:\n{e}");
|
||||
return Err(e);
|
||||
self.node_graph_errors = e;
|
||||
} else {
|
||||
self.graph_hash = Some(hash_code);
|
||||
}
|
||||
|
||||
self.graph_hash = Some(hash_code);
|
||||
self.resolved_types = self.executor.document_node_types();
|
||||
}
|
||||
|
||||
use graph_craft::graphene_compiler::Executor;
|
||||
|
@ -560,8 +569,11 @@ impl NodeGraphExecutor {
|
|||
new_thumbnails,
|
||||
new_click_targets,
|
||||
new_upstream_transforms,
|
||||
resolved_types,
|
||||
node_graph_errors,
|
||||
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 execution_context = self.futures.remove(&generation_id).ok_or_else(|| "Invalid generation ID".to_string())?;
|
||||
|
||||
|
|
|
@ -101,22 +101,19 @@
|
|||
--color-e-nearwhite-rgb: 238, 238, 238;
|
||||
--color-f-white: #fff;
|
||||
--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-dim: #767676;
|
||||
--color-data-vector: #65bbe5;
|
||||
--color-data-vector-dim: #4b778c;
|
||||
--color-data-number: #cbbab4;
|
||||
--color-data-number-dim: #87736b;
|
||||
--color-data-raster: #e4bb72;
|
||||
--color-data-raster-dim: #8b7752;
|
||||
--color-data-mask: #8d85c7;
|
||||
--color-data-number: #d6536e;
|
||||
--color-data-number-dim: #803242;
|
||||
--color-data-vec2: #cc00ff;
|
||||
--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-vector: #65bbe5;
|
||||
--color-data-vector-dim: #4b778c;
|
||||
--color-data-color: #dce472;
|
||||
--color-data-color-dim: #898d55;
|
||||
--color-data-artboard: #70a898;
|
||||
--color-data-artboard-dim: #3a6156;
|
||||
|
||||
|
|
|
@ -627,7 +627,6 @@
|
|||
width: 24px;
|
||||
font-size: 0;
|
||||
overflow: hidden;
|
||||
transition: background-color 0.5s ease;
|
||||
|
||||
div {
|
||||
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.
|
||||
// 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);
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover div,
|
||||
|
|
|
@ -583,7 +583,7 @@
|
|||
|
||||
&[title^="Coming Soon"] {
|
||||
opacity: 0.25;
|
||||
transition: opacity 0.25s;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
|
@ -703,7 +703,7 @@
|
|||
|
||||
.graph-view {
|
||||
pointer-events: none;
|
||||
transition: opacity 0.1s ease-in-out;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
opacity: 0;
|
||||
|
||||
&.open {
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
<script lang="ts">
|
||||
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 { IconName } from "@graphite/utility-functions/icons";
|
||||
import type { Editor } from "@graphite/wasm-communication/editor";
|
||||
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 TextButton from "@graphite/components/widgets/buttons/TextButton.svelte";
|
||||
|
@ -28,7 +30,7 @@
|
|||
let nodesContainer: HTMLDivElement | 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 selected: bigint[] = [];
|
||||
let draggingNodes: { startX: number; startY: number; roundX: number; roundY: number } | undefined = undefined;
|
||||
|
@ -292,6 +294,8 @@
|
|||
function pointerDown(e: PointerEvent) {
|
||||
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 node = (e.target as HTMLElement).closest("[data-node]") as HTMLElement | 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(" ")}`;
|
||||
}
|
||||
|
||||
function dataTypeTooltip(dataType: FrontendGraphDataType): string {
|
||||
const capitalized = dataType[0].toUpperCase() + dataType.slice(1);
|
||||
return `${capitalized} Data`;
|
||||
function dataTypeTooltip(value: FrontendGraphInput | FrontendGraphOutput): string {
|
||||
const dataTypeCapitalized = `${value.dataType[0].toUpperCase()}${value.dataType.slice(1)}`;
|
||||
return value.resolvedType ? `Resolved Data: ${value.resolvedType}` : `Unresolved Data: ${dataTypeCapitalized}`;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
|
@ -674,7 +678,7 @@
|
|||
<!-- Layers -->
|
||||
{#each $nodeGraph.nodes.flatMap((node, nodeIndex) => (node.isLayer ? [{ node, nodeIndex }] : [])) as { node, nodeIndex } (nodeIndex)}
|
||||
{@const clipPathId = String(Math.random()).substring(2)}
|
||||
{@const stackDatainput = node.exposedInputs[0]}
|
||||
{@const stackDataInput = node.exposedInputs[0]}
|
||||
<div
|
||||
class="layer"
|
||||
class:selected={selected.includes(node.id)}
|
||||
|
@ -687,6 +691,10 @@
|
|||
style:--data-color-dim={`var(--color-data-${node.primaryOutput?.dataType || "general"}-dim)`}
|
||||
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" />
|
||||
<!-- Layer input port (from left) -->
|
||||
<div class="input ports">
|
||||
|
@ -701,7 +709,7 @@
|
|||
bind:this={inputs[nodeIndex][0]}
|
||||
>
|
||||
{#if node.primaryInput}
|
||||
<title>{dataTypeTooltip(node.primaryInput.dataType)}</title>
|
||||
<title>{dataTypeTooltip(node.primaryInput)}</title>
|
||||
{/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" />
|
||||
</svg>
|
||||
|
@ -721,7 +729,7 @@
|
|||
style:--data-color-dim={`var(--color-data-${node.primaryOutput.dataType}-dim)`}
|
||||
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" />
|
||||
</svg>
|
||||
{/if}
|
||||
|
@ -730,12 +738,12 @@
|
|||
viewBox="0 0 8 8"
|
||||
class="port bottom"
|
||||
data-port="input"
|
||||
data-datatype={stackDatainput.dataType}
|
||||
style:--data-color={`var(--color-data-${stackDatainput.dataType})`}
|
||||
style:--data-color-dim={`var(--color-data-${stackDatainput.dataType}-dim)`}
|
||||
data-datatype={stackDataInput.dataType}
|
||||
style:--data-color={`var(--color-data-${stackDataInput.dataType})`}
|
||||
style:--data-color-dim={`var(--color-data-${stackDataInput.dataType}-dim)`}
|
||||
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" />
|
||||
</svg>
|
||||
</div>
|
||||
|
@ -769,6 +777,10 @@
|
|||
style:--data-color-dim={`var(--color-data-${node.primaryOutput?.dataType || "general"}-dim)`}
|
||||
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 -->
|
||||
<div class="primary" class:no-parameter-section={exposedInputsOutputs.length === 0}>
|
||||
<IconLabel icon={nodeIcon(node.name)} />
|
||||
|
@ -798,7 +810,7 @@
|
|||
style:--data-color-dim={`var(--color-data-${node.primaryInput?.dataType}-dim)`}
|
||||
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" />
|
||||
</svg>
|
||||
{/if}
|
||||
|
@ -814,7 +826,7 @@
|
|||
style:--data-color-dim={`var(--color-data-${parameter.dataType}-dim)`}
|
||||
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" />
|
||||
</svg>
|
||||
{/if}
|
||||
|
@ -833,7 +845,7 @@
|
|||
style:--data-color-dim={`var(--color-data-${node.primaryOutput.dataType}-dim)`}
|
||||
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" />
|
||||
</svg>
|
||||
{/if}
|
||||
|
@ -846,9 +858,9 @@
|
|||
data-datatype={parameter.dataType}
|
||||
style:--data-color={`var(--color-data-${parameter.dataType})`}
|
||||
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" />
|
||||
</svg>
|
||||
{/each}
|
||||
|
@ -980,6 +992,68 @@
|
|||
// backdrop-filter: blur(4px);
|
||||
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 {
|
||||
content: "";
|
||||
position: absolute;
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
</script>
|
||||
|
||||
<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">
|
||||
<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
|
||||
|
@ -53,25 +53,33 @@
|
|||
}
|
||||
|
||||
&:not(.exposed) {
|
||||
.outline {
|
||||
fill: var(--data-type-color);
|
||||
&:not(:hover) {
|
||||
.outline {
|
||||
fill: var(--data-type-color-dim);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.interior {
|
||||
fill: var(--color-6-lowergray);
|
||||
.outline {
|
||||
fill: var(--data-type-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.exposed {
|
||||
.interior {
|
||||
fill: var(--data-type-color);
|
||||
&:not(:hover) {
|
||||
.interior {
|
||||
fill: var(--data-type-color);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.outline {
|
||||
fill: var(--color-f-white);
|
||||
fill: var(--data-type-color);
|
||||
}
|
||||
|
||||
.interior {
|
||||
fill: var(--data-type-color-dim);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
2
frontend/src/consts.ts
Normal file
2
frontend/src/consts.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
import { cubicInOut } from "svelte/easing";
|
||||
export const FADE_TRANSITION = { duration: 200, easing: cubicInOut };
|
|
@ -82,18 +82,22 @@ export class FrontendDocumentDetails extends DocumentDetails {
|
|||
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 {
|
||||
readonly dataType!: FrontendGraphDataType;
|
||||
|
||||
readonly name!: string;
|
||||
|
||||
readonly resolvedType!: string | undefined;
|
||||
}
|
||||
|
||||
export class FrontendGraphOutput {
|
||||
readonly dataType!: FrontendGraphDataType;
|
||||
|
||||
readonly name!: string;
|
||||
|
||||
readonly resolvedType!: string | undefined;
|
||||
}
|
||||
|
||||
export class FrontendNode {
|
||||
|
@ -119,6 +123,8 @@ export class FrontendNode {
|
|||
readonly previewed!: boolean;
|
||||
|
||||
readonly disabled!: boolean;
|
||||
|
||||
readonly errors!: string | undefined;
|
||||
}
|
||||
|
||||
export class FrontendNodeLink {
|
||||
|
|
|
@ -6,7 +6,7 @@ use dyn_any::StaticType;
|
|||
#[cfg(feature = "std")]
|
||||
pub use std::borrow::Cow;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
#[derive(Clone, PartialEq, Eq, Hash)]
|
||||
pub struct NodeIOTypes {
|
||||
pub input: 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_rules! concrete {
|
||||
($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 {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
|
@ -200,7 +217,7 @@ impl core::fmt::Debug for Type {
|
|||
#[cfg(feature = "type_id_logging")]
|
||||
Self::Concrete(arg0) => write!(f, "Concrete({}, {:?})", arg0.name, arg0.id),
|
||||
#[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::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 {
|
||||
match self {
|
||||
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::Future(ty) => write!(f, "Future<{ty}>"),
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
#[serde(default)]
|
||||
pub world_state_hash: u64,
|
||||
/// The path to this node 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].
|
||||
/// The path to this node and its inputs and outputs as of when [`NodeNetwork::generate_node_paths`] was called.
|
||||
#[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>>,
|
||||
/// 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 {
|
||||
|
@ -183,14 +206,42 @@ impl Default for DocumentNode {
|
|||
metadata: Default::default(),
|
||||
skip_deduplication: 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 {
|
||||
/// 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
|
||||
.inputs
|
||||
.iter()
|
||||
|
@ -200,6 +251,10 @@ impl DocumentNode {
|
|||
.unwrap_or_else(|| panic!("no network input found for {self:#?} and offset: {offset}"));
|
||||
|
||||
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 {
|
||||
|
@ -246,7 +301,7 @@ impl DocumentNode {
|
|||
identifier: fqn,
|
||||
input,
|
||||
construction_args: args,
|
||||
document_node_path: self.path.unwrap_or_default(),
|
||||
original_location: self.original_location,
|
||||
skip_deduplication: self.skip_deduplication,
|
||||
world_state_hash: self.world_state_hash,
|
||||
}
|
||||
|
@ -762,10 +817,15 @@ impl NodeNetwork {
|
|||
if let DocumentNodeImplementation::Network(network) = &mut node.implementation {
|
||||
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");
|
||||
} 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.
|
||||
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 {
|
||||
warn!("The node which was supposed to be flattened does not exist in the network, id {node} network {self:#?}");
|
||||
return;
|
||||
|
@ -850,7 +909,7 @@ impl NodeNetwork {
|
|||
}
|
||||
|
||||
// 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
|
||||
if node.implementation == DocumentNodeImplementation::Unresolved("graphene_core::value::ClonedNode".into()) {
|
||||
break;
|
||||
|
@ -860,20 +919,17 @@ impl NodeNetwork {
|
|||
if let NodeInput::Value { tagged_value, exposed } = previous_input {
|
||||
let value_node_id = gen_id();
|
||||
let merged_node_id = map_ids(id, value_node_id);
|
||||
let path = if let Some(mut new_path) = node.path.clone() {
|
||||
new_path.push(value_node_id);
|
||||
Some(new_path)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut original_location = node.original_location.clone();
|
||||
if let Some(path) = &mut original_location.path {
|
||||
path.push(value_node_id);
|
||||
}
|
||||
self.nodes.insert(
|
||||
merged_node_id,
|
||||
DocumentNode {
|
||||
name: "Value".into(),
|
||||
inputs: vec![NodeInput::Value { tagged_value, exposed }],
|
||||
implementation: DocumentNodeImplementation::Unresolved("graphene_core::value::ClonedNode".into()),
|
||||
path,
|
||||
original_location,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
@ -888,8 +944,6 @@ impl NodeNetwork {
|
|||
}
|
||||
|
||||
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.
|
||||
inner_network.map_ids(|inner_id| map_ids(id, inner_id));
|
||||
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"
|
||||
);
|
||||
// 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
|
||||
let offset = network_offsets.entry(network_input).or_insert(0);
|
||||
match document_input {
|
||||
// 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 } => {
|
||||
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(_) => {
|
||||
*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, 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);
|
||||
}
|
||||
|
||||
|
@ -960,9 +1022,15 @@ impl NodeNetwork {
|
|||
if ident.name == "graphene_core::ops::IdentityNode" {
|
||||
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 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;
|
||||
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 {
|
||||
node_id: output_node_id,
|
||||
output_index: output_output_index,
|
||||
|
@ -972,6 +1040,11 @@ impl NodeNetwork {
|
|||
if *output_node_id == id {
|
||||
*output_node_id = input_node_id;
|
||||
*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(),
|
||||
input: ProtoNodeInput::ManualComposition(concrete!(u32)),
|
||||
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()
|
||||
},
|
||||
),
|
||||
|
@ -1310,7 +1390,13 @@ mod test {
|
|||
identifier: "graphene_core::ops::AddPairNode".into(),
|
||||
input: ProtoNodeInput::Node(NodeId(10), false),
|
||||
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()
|
||||
},
|
||||
),
|
||||
|
@ -1338,7 +1424,13 @@ mod test {
|
|||
name: "Cons".into(),
|
||||
inputs: vec![NodeInput::Network(concrete!(u32)), NodeInput::node(NodeId(14), 0)],
|
||||
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()
|
||||
},
|
||||
),
|
||||
|
@ -1351,7 +1443,13 @@ mod test {
|
|||
exposed: false,
|
||||
}],
|
||||
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()
|
||||
},
|
||||
),
|
||||
|
@ -1361,7 +1459,13 @@ mod test {
|
|||
name: "Add".into(),
|
||||
inputs: vec![NodeInput::node(NodeId(10), 0)],
|
||||
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()
|
||||
},
|
||||
),
|
||||
|
|
|
@ -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::{NodeId, OriginalLocation};
|
||||
|
||||
use dyn_any::DynAny;
|
||||
use graphene_core::*;
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
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;
|
||||
|
||||
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 input: ProtoNodeInput,
|
||||
pub identifier: ProtoNodeIdentifier,
|
||||
pub document_node_path: Vec<NodeId>,
|
||||
pub original_location: OriginalLocation,
|
||||
pub skip_deduplication: bool,
|
||||
// TODO: This is a hack, figure out a proper solution
|
||||
/// Represents a global state on which the node depends.
|
||||
|
@ -218,7 +218,7 @@ impl Default for ProtoNode {
|
|||
identifier: ProtoNodeIdentifier::new("graphene_core::ops::IdentityNode"),
|
||||
construction_args: ConstructionArgs::Value(value::TaggedValue::U32(0)),
|
||||
input: ProtoNodeInput::None,
|
||||
document_node_path: vec![],
|
||||
original_location: OriginalLocation::default(),
|
||||
skip_deduplication: false,
|
||||
world_state_hash: 0,
|
||||
}
|
||||
|
@ -266,7 +266,7 @@ impl ProtoNode {
|
|||
self.identifier.name.hash(&mut hasher);
|
||||
self.construction_args.hash(&mut hasher);
|
||||
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);
|
||||
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.
|
||||
pub fn value(value: ConstructionArgs, path: Vec<NodeId>) -> Self {
|
||||
let inputs_exposed = match &value {
|
||||
ConstructionArgs::Nodes(nodes) => nodes.len() + 1,
|
||||
_ => 2,
|
||||
};
|
||||
Self {
|
||||
identifier: ProtoNodeIdentifier::new("graphene_core::value::ClonedNode"),
|
||||
construction_args: value,
|
||||
input: ProtoNodeInput::None,
|
||||
document_node_path: path,
|
||||
original_location: OriginalLocation {
|
||||
path: Some(path),
|
||||
inputs_exposed: vec![false; inputs_exposed],
|
||||
..Default::default()
|
||||
},
|
||||
skip_deduplication: false,
|
||||
world_state_hash: 0,
|
||||
}
|
||||
|
@ -396,8 +404,10 @@ impl ProtoNetwork {
|
|||
|
||||
let input = input_node_id_proto.input.clone();
|
||||
|
||||
let mut path = input_node_id_proto.document_node_path.clone();
|
||||
path.push(node_id);
|
||||
let mut path = input_node_id_proto.original_location.path.clone();
|
||||
if let Some(path) = &mut path {
|
||||
path.push(node_id);
|
||||
}
|
||||
|
||||
self.nodes.push((
|
||||
compose_node_id,
|
||||
|
@ -405,7 +415,7 @@ impl ProtoNetwork {
|
|||
identifier: ProtoNodeIdentifier::new("graphene_core::structural::ComposeNode<_, _, _>"),
|
||||
construction_args: ConstructionArgs::Nodes(vec![(input_node_id, false), (node_id, true)]),
|
||||
input,
|
||||
document_node_path: path,
|
||||
original_location: OriginalLocation { path, ..Default::default() },
|
||||
skip_deduplication: false,
|
||||
world_state_hash: 0,
|
||||
},
|
||||
|
@ -544,6 +554,78 @@ impl ProtoNetwork {
|
|||
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.
|
||||
#[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 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() {
|
||||
self.infer(*id, node)?;
|
||||
}
|
||||
|
@ -583,12 +665,10 @@ impl TypingContext {
|
|||
}
|
||||
|
||||
/// Returns the inferred types for a given node id.
|
||||
pub fn infer(&mut self, node_id: NodeId, node: &ProtoNode) -> Result<NodeIOTypes, String> {
|
||||
let identifier = node.identifier.name.clone();
|
||||
|
||||
pub fn infer(&mut self, node_id: NodeId, node: &ProtoNode) -> Result<NodeIOTypes, GraphErrors> {
|
||||
// Return the inferred type if it is already known
|
||||
if let Some(infered) = self.inferred.get(&node_id) {
|
||||
return Ok(infered.clone());
|
||||
if let Some(inferred) = self.inferred.get(&node_id) {
|
||||
return Ok(inferred.clone());
|
||||
}
|
||||
|
||||
let parameters = match node.construction_args {
|
||||
|
@ -606,10 +686,10 @@ impl TypingContext {
|
|||
.map(|(id, _)| {
|
||||
self.inferred
|
||||
.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())
|
||||
})
|
||||
.collect::<Result<Vec<Type>, String>>()?,
|
||||
.collect::<Result<Vec<Type>, GraphErrors>>()?,
|
||||
ConstructionArgs::Inline(ref inline) => vec![inline.ty.clone()],
|
||||
};
|
||||
|
||||
|
@ -618,23 +698,17 @@ impl TypingContext {
|
|||
ProtoNodeInput::None => concrete!(()),
|
||||
ProtoNodeInput::ManualComposition(ref ty) => ty.clone(),
|
||||
ProtoNodeInput::Node(id, _) => {
|
||||
let input = self
|
||||
.inferred
|
||||
.get(&id)
|
||||
.ok_or(format!("Inferring type of {node_id} depends on {id} which is not present in the typing context"))?;
|
||||
let input = self.inferred.get(&id).ok_or_else(|| vec![GraphError::new(node, GraphErrorType::InputNodeNotFound(id))])?;
|
||||
input.output.clone()
|
||||
}
|
||||
};
|
||||
let impls = self
|
||||
.lookup
|
||||
.get(&node.identifier)
|
||||
.ok_or(format!("No implementations found for:\n\n{:?}\n\nOther implementations found:\n\n{:?}", node.identifier, self.lookup))?;
|
||||
let impls = self.lookup.get(&node.identifier).ok_or_else(|| vec![GraphError::new(node, GraphErrorType::NoImplementations)])?;
|
||||
|
||||
if parameters.iter().any(|p| {
|
||||
if let Some(index) = parameters.iter().position(|p| {
|
||||
matches!(p,
|
||||
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 {
|
||||
match (from, to) {
|
||||
|
@ -651,7 +725,7 @@ impl TypingContext {
|
|||
// List of all implementations that match the input and parameter types
|
||||
let valid_output_types = impls
|
||||
.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<_>>();
|
||||
|
||||
// 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() {
|
||||
[] => {
|
||||
dbg!(&self.inferred);
|
||||
Err(format!(
|
||||
"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{:?}",
|
||||
impls.keys().collect::<Vec<_>>(),
|
||||
))
|
||||
let mut best_errors = usize::MAX;
|
||||
let mut error_inputs = Vec::new();
|
||||
for node_io in impls.keys() {
|
||||
let current_errors = [&input]
|
||||
.into_iter()
|
||||
.chain(¶meters)
|
||||
.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(¶meters).map(|t| t.to_string()).collect::<Vec<_>>().join(", ");
|
||||
Err(vec![GraphError::new(node, GraphErrorType::InvalidImplementations { parameters, error_inputs })])
|
||||
}
|
||||
[(org_nio, output)] => {
|
||||
let node_io = NodeIOTypes::new(input, (*output).clone(), parameters);
|
||||
|
@ -690,9 +782,12 @@ impl TypingContext {
|
|||
self.constructor.insert(node_id, impls[org_nio]);
|
||||
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(¶meters).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 std::error::Error;
|
||||
use std::sync::Arc;
|
||||
use crate::node_registry;
|
||||
|
||||
use dyn_any::StaticType;
|
||||
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::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 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 ...>`.
|
||||
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 {
|
||||
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);
|
||||
typing_context.update(&proto_network)?;
|
||||
let output = proto_network.output;
|
||||
|
@ -49,7 +57,7 @@ impl DynamicExecutor {
|
|||
}
|
||||
|
||||
/// 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.typing_context.update(&proto_network)?;
|
||||
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> {
|
||||
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 {
|
||||
|
@ -89,10 +113,14 @@ pub struct BorrowTree {
|
|||
nodes: HashMap<NodeId, SharedNodeContainer>,
|
||||
/// A hashmap from the document path to the protonode ID.
|
||||
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 {
|
||||
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();
|
||||
for (id, node) in proto_network.nodes {
|
||||
nodes.push_node(id, node, typing_context).await?
|
||||
|
@ -101,7 +129,7 @@ impl BorrowTree {
|
|||
}
|
||||
|
||||
/// 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();
|
||||
for (id, node) in proto_network.nodes {
|
||||
if !self.nodes.contains_key(&id) {
|
||||
|
@ -110,6 +138,8 @@ impl BorrowTree {
|
|||
old_nodes.remove(&id);
|
||||
}
|
||||
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));
|
||||
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`.
|
||||
pub async fn push_node(&mut self, id: NodeId, proto_node: ProtoNode, typing_context: &TypingContext) -> Result<(), String> {
|
||||
let ProtoNode {
|
||||
construction_args,
|
||||
identifier,
|
||||
document_node_path,
|
||||
..
|
||||
} = proto_node;
|
||||
self.source_map.insert(document_node_path, id);
|
||||
pub async fn push_node(&mut self, id: NodeId, proto_node: ProtoNode, typing_context: &TypingContext) -> Result<(), GraphErrors> {
|
||||
self.source_map.insert(proto_node.original_location.path.clone().unwrap_or_default(), 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) => {
|
||||
let upcasted = UpcastNode::new(value);
|
||||
let upcasted = UpcastNode::new(value.to_owned());
|
||||
let node = Box::new(upcasted) as TypeErasedBox<'_>;
|
||||
let node = NodeContainer::new(node);
|
||||
self.store_node(node, id);
|
||||
|
@ -172,7 +207,7 @@ impl BorrowTree {
|
|||
ConstructionArgs::Nodes(ids) => {
|
||||
let ids: Vec<_> = ids.iter().map(|(id, _)| *id).collect();
|
||||
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 = NodeContainer::new(node);
|
||||
self.store_node(node, id);
|
||||
|
@ -180,6 +215,14 @@ impl BorrowTree {
|
|||
};
|
||||
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)]
|
||||
|
|
|
@ -73,7 +73,7 @@ mod tests {
|
|||
let compiler = Compiler {};
|
||||
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();
|
||||
assert_eq!(result, TaggedValue::U32(33));
|
||||
|
|
|
@ -112,6 +112,7 @@ body > h2 {
|
|||
|
||||
svg text {
|
||||
pointer-events: none;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
|
|
|
@ -280,6 +280,7 @@ pre {
|
|||
font-size: 0.75em;
|
||||
font-weight: bold;
|
||||
font-style: italic;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
position: fixed;
|
||||
|
@ -326,6 +327,7 @@ pre {
|
|||
&:first-child {
|
||||
padding-left: 20px;
|
||||
padding-right: 10px;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
vertical-align: top;
|
||||
text-align: right;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue