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
parent 96b5d7b520
commit 947a131a4b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 566 additions and 170 deletions

View file

@ -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

View file

@ -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,
},
}

View file

@ -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();
}

View file

@ -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> {

View file

@ -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,

View file

@ -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())?;

View file

@ -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;

View file

@ -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,

View file

@ -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 {

View file

@ -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;

View file

@ -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
View file

@ -0,0 +1,2 @@
import { cubicInOut } from "svelte/easing";
export const FADE_TRANSITION = { duration: 200, easing: cubicInOut };

View file

@ -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 {

View file

@ -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}>"),
}

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.
#[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()
},
),

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::{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(&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)] => {
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(&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 })])
}
}
}
}

View file

@ -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)]

View file

@ -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));

View file

@ -112,6 +112,7 @@ body > h2 {
svg text {
pointer-events: none;
-webkit-user-select: none;
user-select: none;
}

View file

@ -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;