diff --git a/editor/src/messages/layout/layout_message_handler.rs b/editor/src/messages/layout/layout_message_handler.rs index 830a659521..3aba33dbaa 100644 --- a/editor/src/messages/layout/layout_message_handler.rs +++ b/editor/src/messages/layout/layout_message_handler.rs @@ -154,6 +154,10 @@ impl Vec> MessageHandler { + let callback_message = (parameter_expose_button.on_update.callback)(parameter_expose_button); + responses.push_back(callback_message); + } Widget::PivotAssist(pivot_assist) => { let update_value = value.as_str().expect("RadioInput update was not of type: u64"); pivot_assist.position = update_value.into(); diff --git a/editor/src/messages/layout/utility_types/layout_widget.rs b/editor/src/messages/layout/utility_types/layout_widget.rs index 2442a94009..22dbfe808e 100644 --- a/editor/src/messages/layout/utility_types/layout_widget.rs +++ b/editor/src/messages/layout/utility_types/layout_widget.rs @@ -67,6 +67,7 @@ impl Layout { Widget::LayerReferenceInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)), Widget::NumberInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)), Widget::OptionalInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)), + Widget::ParameterExposeButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)), Widget::PopoverButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)), Widget::TextButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)), Widget::IconLabel(_) @@ -294,6 +295,7 @@ pub enum Widget { LayerReferenceInput(LayerReferenceInput), NumberInput(NumberInput), OptionalInput(OptionalInput), + ParameterExposeButton(ParameterExposeButton), PivotAssist(PivotAssist), PopoverButton(PopoverButton), RadioInput(RadioInput), diff --git a/editor/src/messages/layout/utility_types/widgets/button_widgets.rs b/editor/src/messages/layout/utility_types/widgets/button_widgets.rs index 86933a9e0a..de4bd25b92 100644 --- a/editor/src/messages/layout/utility_types/widgets/button_widgets.rs +++ b/editor/src/messages/layout/utility_types/widgets/button_widgets.rs @@ -1,5 +1,5 @@ -use crate::messages::input_mapper::utility_types::misc::ActionKeys; use crate::messages::layout::utility_types::layout_widget::WidgetCallback; +use crate::messages::{input_mapper::utility_types::misc::ActionKeys, portfolio::document::node_graph::FrontendGraphDataType}; use derivative::*; use serde::{Deserialize, Serialize}; @@ -45,6 +45,26 @@ pub struct PopoverButton { pub tooltip_shortcut: Option, } +#[derive(Clone, Serialize, Deserialize, Derivative, Default)] +#[derivative(Debug, PartialEq)] +#[serde(rename_all(serialize = "camelCase", deserialize = "camelCase"))] +pub struct ParameterExposeButton { + pub exposed: bool, + + #[serde(rename = "dataType")] + pub data_type: FrontendGraphDataType, + + pub tooltip: String, + + #[serde(skip)] + pub tooltip_shortcut: Option, + + // Callbacks + #[serde(skip)] + #[derivative(Debug = "ignore", PartialEq = "ignore")] + pub on_update: WidgetCallback, +} + #[derive(Clone, Serialize, Deserialize, Derivative, Default)] #[derivative(Debug, PartialEq)] #[serde(rename_all(serialize = "camelCase", deserialize = "camelCase"))] diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs index 800d28b4b6..81a1aba2c9 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs @@ -8,9 +8,26 @@ use graphene::layers::nodegraph_layer::NodeGraphFrameLayer; mod document_node_types; mod node_properties; -#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] -pub enum DataType { +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +pub enum FrontendGraphDataType { + #[default] + #[serde(rename = "general")] + General, + #[serde(rename = "raster")] Raster, + #[serde(rename = "color")] + Color, + #[serde(rename = "vector")] + Vector, + #[serde(rename = "number")] + Number, +} + +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct NodeGraphInput { + #[serde(rename = "dataType")] + data_type: FrontendGraphDataType, + name: String, } #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] @@ -19,8 +36,8 @@ pub struct FrontendNode { #[serde(rename = "displayName")] pub display_name: String, #[serde(rename = "exposedInputs")] - pub exposed_inputs: Vec, - pub outputs: Vec, + pub exposed_inputs: Vec, + pub outputs: Vec, pub position: (i32, i32), } @@ -98,11 +115,24 @@ impl NodeGraphMessageHandler { let mut nodes = Vec::new(); for (id, node) in &network.nodes { + let Some(node_type) = document_node_types::resolve_document_node_type(&node.name) else{ + warn!("Node '{}' does not exist in library", node.name); + continue + }; nodes.push(FrontendNode { id: *id, display_name: node.name.clone(), - exposed_inputs: node.inputs.iter().filter(|input| input.is_exposed()).map(|_| DataType::Raster).collect(), - outputs: vec![DataType::Raster], + exposed_inputs: node + .inputs + .iter() + .zip(node_type.inputs) + .filter(|(input, _)| input.is_exposed()) + .map(|(_, input_type)| NodeGraphInput { + data_type: input_type.data_type, + name: input_type.name.to_string(), + }) + .collect(), + outputs: node_type.outputs.to_vec(), position: node.metadata.position, }) } @@ -160,7 +190,7 @@ impl MessageHandler { if let Some(_old_layer_path) = self.layer_path.replace(layer_path) { diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs index 02e7b93394..d9ae7010db 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs @@ -1,86 +1,175 @@ -use std::borrow::Cow; +use super::{FrontendGraphDataType, FrontendNodeType}; +use crate::messages::layout::utility_types::layout_widget::{LayoutGroup, Widget, WidgetHolder}; +use crate::messages::layout::utility_types::widgets::label_widgets::TextLabel; use graph_craft::document::value::TaggedValue; -use graph_craft::document::NodeInput; +use graph_craft::document::{DocumentNode, NodeId, NodeInput}; use graph_craft::proto::{NodeIdentifier, Type}; use graphene_std::raster::Image; -use super::FrontendNodeType; +use std::borrow::Cow; + +pub struct DocumentInputType { + pub name: &'static str, + pub data_type: FrontendGraphDataType, + pub default: NodeInput, +} pub struct DocumentNodeType { pub name: &'static str, pub identifier: NodeIdentifier, - pub default_inputs: &'static [NodeInput], + pub inputs: &'static [DocumentInputType], + pub outputs: &'static [FrontendGraphDataType], + pub properties: fn(&DocumentNode, NodeId) -> Vec, } // TODO: Dynamic node library -static DOCUMENT_NODE_TYPES: [DocumentNodeType; 5] = [ +static DOCUMENT_NODE_TYPES: [DocumentNodeType; 7] = [ DocumentNodeType { name: "Identity", identifier: NodeIdentifier::new("graphene_core::ops::IdNode", &[Type::Concrete(Cow::Borrowed("Any<'_>"))]), - default_inputs: &[NodeInput::Node(0)], + inputs: &[DocumentInputType { + name: "In", + data_type: FrontendGraphDataType::General, + default: NodeInput::Node(0), + }], + outputs: &[FrontendGraphDataType::General], + properties: |_document_node, _node_id| { + vec![LayoutGroup::Row { + widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel { + value: format!("The identity node simply returns the input"), + ..Default::default() + }))], + }] + }, + }, + DocumentNodeType { + name: "Input", + identifier: NodeIdentifier::new("graphene_core::ops::IdNode", &[Type::Concrete(Cow::Borrowed("Any<'_>"))]), + inputs: &[], + outputs: &[FrontendGraphDataType::Raster], + properties: |_document_node, _node_id| { + vec![LayoutGroup::Row { + widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel { + value: format!("The input to the graph is the bitmap under the frame"), + ..Default::default() + }))], + }] + }, + }, + DocumentNodeType { + name: "Output", + identifier: NodeIdentifier::new("graphene_core::ops::IdNode", &[Type::Concrete(Cow::Borrowed("Any<'_>"))]), + inputs: &[DocumentInputType { + name: "In", + data_type: FrontendGraphDataType::Raster, + default: NodeInput::Value { + tagged_value: TaggedValue::Image(Image::empty()), + exposed: true, + }, + }], + outputs: &[], + properties: |_document_node, _node_id| { + vec![LayoutGroup::Row { + widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel { + value: format!("The output to the graph is rendered in the frame"), + ..Default::default() + }))], + }] + }, }, DocumentNodeType { name: "Grayscale Image", identifier: NodeIdentifier::new("graphene_std::raster::GrayscaleImageNode", &[]), - default_inputs: &[NodeInput::Value { - tagged_value: TaggedValue::Image(Image { - width: 0, - height: 0, - data: Vec::new(), - }), - exposed: true, + inputs: &[DocumentInputType { + name: "Image", + data_type: FrontendGraphDataType::Raster, + default: NodeInput::Value { + tagged_value: TaggedValue::Image(Image::empty()), + exposed: true, + }, }], + outputs: &[FrontendGraphDataType::Raster], + properties: |_document_node, _node_id| { + vec![LayoutGroup::Row { + widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel { + value: format!("The output to the graph is rendered in the frame"), + ..Default::default() + }))], + }] + }, }, DocumentNodeType { name: "Brighten Image", identifier: NodeIdentifier::new("graphene_std::raster::BrightenImageNode", &[Type::Concrete(Cow::Borrowed("&TypeErasedNode"))]), - default_inputs: &[ - NodeInput::Value { - tagged_value: TaggedValue::Image(Image { - width: 0, - height: 0, - data: Vec::new(), - }), - exposed: true, + inputs: &[ + DocumentInputType { + name: "Image", + data_type: FrontendGraphDataType::Raster, + default: NodeInput::Value { + tagged_value: TaggedValue::Image(Image::empty()), + exposed: true, + }, }, - NodeInput::Value { - tagged_value: TaggedValue::F32(10.), - exposed: false, + DocumentInputType { + name: "Amount", + data_type: FrontendGraphDataType::Number, + default: NodeInput::Value { + tagged_value: TaggedValue::F32(10.), + exposed: false, + }, }, ], + outputs: &[FrontendGraphDataType::Raster], + properties: super::node_properties::brighten_image_properties, }, DocumentNodeType { name: "Hue Shift Image", identifier: NodeIdentifier::new("graphene_std::raster::HueShiftImage", &[Type::Concrete(Cow::Borrowed("&TypeErasedNode"))]), - default_inputs: &[ - NodeInput::Value { - tagged_value: TaggedValue::Image(Image { - width: 0, - height: 0, - data: Vec::new(), - }), - exposed: true, + inputs: &[ + DocumentInputType { + name: "Image", + data_type: FrontendGraphDataType::Raster, + default: NodeInput::Value { + tagged_value: TaggedValue::Image(Image::empty()), + exposed: true, + }, }, - NodeInput::Value { - tagged_value: TaggedValue::F32(50.), - exposed: false, + DocumentInputType { + name: "Amount", + data_type: FrontendGraphDataType::Number, + default: NodeInput::Value { + tagged_value: TaggedValue::F32(10.), + exposed: false, + }, }, ], + outputs: &[FrontendGraphDataType::Raster], + properties: super::node_properties::hue_shift_image_properties, }, DocumentNodeType { name: "Add", - identifier: NodeIdentifier::new("graphene_core::ops::AddNode", &[Type::Concrete(Cow::Borrowed("u32")), Type::Concrete(Cow::Borrowed("u32"))]), - default_inputs: &[ - NodeInput::Value { - tagged_value: TaggedValue::U32(0), - exposed: false, + identifier: NodeIdentifier::new("graphene_core::ops::AddNode", &[Type::Concrete(Cow::Borrowed("&TypeErasedNode"))]), + inputs: &[ + DocumentInputType { + name: "Left", + data_type: FrontendGraphDataType::Number, + default: NodeInput::Value { + tagged_value: TaggedValue::F32(0.), + exposed: true, + }, }, - NodeInput::Value { - tagged_value: TaggedValue::U32(0), - exposed: false, + DocumentInputType { + name: "Right", + data_type: FrontendGraphDataType::Number, + default: NodeInput::Value { + tagged_value: TaggedValue::F32(0.), + exposed: true, + }, }, ], + outputs: &[FrontendGraphDataType::Number], + properties: super::node_properties::add_properties, }, ]; diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs index e4fc2971b2..2532b663af 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs @@ -1,111 +1,164 @@ use crate::messages::layout::utility_types::layout_widget::{LayoutGroup, Widget, WidgetCallback, WidgetHolder}; +use crate::messages::layout::utility_types::widgets::button_widgets::ParameterExposeButton; use crate::messages::layout::utility_types::widgets::input_widgets::{NumberInput, NumberInputMode}; use crate::messages::layout::utility_types::widgets::label_widgets::{Separator, SeparatorDirection, SeparatorType, TextLabel}; use crate::messages::prelude::NodeGraphMessage; use graph_craft::document::value::TaggedValue; -use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput}; +use graph_craft::document::{DocumentNode, NodeId, NodeInput}; + +use super::FrontendGraphDataType; + +pub fn hue_shift_image_properties(document_node: &DocumentNode, node_id: NodeId) -> Vec { + vec![LayoutGroup::Row { + widgets: vec![ + WidgetHolder::new(Widget::ParameterExposeButton(ParameterExposeButton { + exposed: true, + data_type: FrontendGraphDataType::Number, + tooltip: "Expose input parameter in node graph".into(), + ..Default::default() + })), + WidgetHolder::new(Widget::Separator(Separator { + separator_type: SeparatorType::Unrelated, + direction: SeparatorDirection::Horizontal, + })), + WidgetHolder::new(Widget::TextLabel(TextLabel { + value: "Shift Degrees".into(), + ..Default::default() + })), + WidgetHolder::new(Widget::Separator(Separator { + separator_type: SeparatorType::Unrelated, + direction: SeparatorDirection::Horizontal, + })), + WidgetHolder::new(Widget::NumberInput(NumberInput { + value: Some({ + let NodeInput::Value {tagged_value: TaggedValue::F32(x), ..} = document_node.inputs[1] else { + panic!("Hue rotate should be f32") + }; + x as f64 + }), + unit: "°".into(), + mode: NumberInputMode::Range, + range_min: Some(-180.), + range_max: Some(180.), + on_update: WidgetCallback::new(move |number_input: &NumberInput| { + NodeGraphMessage::SetInputValue { + node: node_id, + input_index: 1, + value: TaggedValue::F32(number_input.value.unwrap() as f32), + } + .into() + }), + ..NumberInput::default() + })), + ], + }] +} + +pub fn brighten_image_properties(document_node: &DocumentNode, node_id: NodeId) -> Vec { + vec![LayoutGroup::Row { + widgets: vec![ + WidgetHolder::new(Widget::ParameterExposeButton(ParameterExposeButton { + exposed: true, + data_type: FrontendGraphDataType::Number, + tooltip: "Expose input parameter in node graph".into(), + ..Default::default() + })), + WidgetHolder::new(Widget::Separator(Separator { + separator_type: SeparatorType::Unrelated, + direction: SeparatorDirection::Horizontal, + })), + WidgetHolder::new(Widget::TextLabel(TextLabel { + value: "Brighten Amount".into(), + ..Default::default() + })), + WidgetHolder::new(Widget::Separator(Separator { + separator_type: SeparatorType::Unrelated, + direction: SeparatorDirection::Horizontal, + })), + WidgetHolder::new(Widget::NumberInput(NumberInput { + value: Some({ + let NodeInput::Value {tagged_value: TaggedValue::F32(x), ..} = document_node.inputs[1] else { + panic!("Brighten amount should be f32") + }; + x as f64 + }), + mode: NumberInputMode::Range, + range_min: Some(-255.), + range_max: Some(255.), + on_update: WidgetCallback::new(move |number_input: &NumberInput| { + NodeGraphMessage::SetInputValue { + node: node_id, + input_index: 1, + value: TaggedValue::F32(number_input.value.unwrap() as f32), + } + .into() + }), + ..NumberInput::default() + })), + ], + }] +} + +pub fn add_properties(document_node: &DocumentNode, node_id: NodeId) -> Vec { + let operand = |name: &str, index| LayoutGroup::Row { + widgets: vec![ + WidgetHolder::new(Widget::ParameterExposeButton(ParameterExposeButton { + exposed: true, + data_type: FrontendGraphDataType::Number, + tooltip: "Expose input parameter in node graph".into(), + ..Default::default() + })), + WidgetHolder::new(Widget::Separator(Separator { + separator_type: SeparatorType::Unrelated, + direction: SeparatorDirection::Horizontal, + })), + WidgetHolder::new(Widget::TextLabel(TextLabel { + value: name.into(), + ..Default::default() + })), + WidgetHolder::new(Widget::Separator(Separator { + separator_type: SeparatorType::Unrelated, + direction: SeparatorDirection::Horizontal, + })), + WidgetHolder::new(Widget::NumberInput(NumberInput { + value: Some({ + let NodeInput::Value {tagged_value: TaggedValue::F32(x), ..} = document_node.inputs[index] else { + panic!("Add input should be f32") + }; + + x as f64 + }), + mode: NumberInputMode::Increment, + on_update: WidgetCallback::new(move |number_input: &NumberInput| { + NodeGraphMessage::SetInputValue { + node: node_id, + input_index: index, + value: TaggedValue::F32(number_input.value.unwrap() as f32), + } + .into() + }), + ..NumberInput::default() + })), + ], + }; + vec![operand("Left", 0), operand("Right", 1)] +} + +fn unknown_node_properties(document_node: &DocumentNode) -> Vec { + vec![LayoutGroup::Row { + widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel { + value: format!("Node '{}' cannot be found in library", document_node.name), + ..Default::default() + }))], + }] +} pub fn generate_node_properties(document_node: &DocumentNode, node_id: NodeId) -> LayoutGroup { let name = document_node.name.clone(); - let layout = match &document_node.implementation { - DocumentNodeImplementation::Network(_) => match document_node.name.as_str() { - "Hue Shift Image" => vec![LayoutGroup::Row { - widgets: vec![ - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "Shift Degrees".into(), - ..Default::default() - })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Unrelated, - direction: SeparatorDirection::Horizontal, - })), - WidgetHolder::new(Widget::NumberInput(NumberInput { - value: Some({ - let NodeInput::Value {tagged_value: TaggedValue::F32(x), ..} = document_node.inputs[1] else { - panic!("Hue rotate should be f32") - }; - x as f64 - }), - unit: "°".into(), - mode: NumberInputMode::Range, - range_min: Some(-180.), - range_max: Some(180.), - on_update: WidgetCallback::new(move |number_input: &NumberInput| { - NodeGraphMessage::SetInputValue { - node: node_id, - input_index: 1, - value: TaggedValue::F32(number_input.value.unwrap() as f32), - } - .into() - }), - ..NumberInput::default() - })), - ], - }], - "Brighten Image" => vec![LayoutGroup::Row { - widgets: vec![ - WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "Brighten Amount".into(), - ..Default::default() - })), - WidgetHolder::new(Widget::Separator(Separator { - separator_type: SeparatorType::Unrelated, - direction: SeparatorDirection::Horizontal, - })), - WidgetHolder::new(Widget::NumberInput(NumberInput { - value: Some({ - let NodeInput::Value {tagged_value: TaggedValue::F32(x), ..} = document_node.inputs[1] else { - panic!("Brighten amount should be f32") - }; - x as f64 - }), - mode: NumberInputMode::Range, - range_min: Some(-255.), - range_max: Some(255.), - on_update: WidgetCallback::new(move |number_input: &NumberInput| { - NodeGraphMessage::SetInputValue { - node: node_id, - input_index: 1, - value: TaggedValue::F32(number_input.value.unwrap() as f32), - } - .into() - }), - ..NumberInput::default() - })), - ], - }], - _ => vec![LayoutGroup::Row { - widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel { - value: format!("Cannot currently display parameters for network {}", document_node.name), - ..Default::default() - }))], - }], - }, - DocumentNodeImplementation::Unresolved(identifier) => match identifier.name.as_ref() { - "graphene_std::raster::MapImageNode" | "graphene_core::ops::IdNode" => vec![LayoutGroup::Row { - widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel { - value: format!("{} exposes no parameters", document_node.name), - ..Default::default() - }))], - }], - unknown => { - vec![ - LayoutGroup::Row { - widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel { - value: format!("TODO: {} parameters", unknown), - ..Default::default() - }))], - }, - LayoutGroup::Row { - widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel { - value: "Add in editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs".to_string(), - ..Default::default() - }))], - }, - ] - } - }, + let layout = match super::document_node_types::resolve_document_node_type(&name) { + Some(document_node_type) => (document_node_type.properties)(document_node, node_id), + None => unknown_node_properties(document_node), }; LayoutGroup::Section { name, layout } } diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 1ea4005fcd..e630be2c63 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -41,6 +41,8 @@ --color-data-general: #c5c5c5; --color-data-general-rgb: 197, 197, 197; + --color-data-general-dim: #767676; + --color-data-general-dim-rgb: 118, 118, 118; --color-data-vector: #65bbe5; --color-data-vector-rgb: 101, 187, 229; --color-data-vector-dim: #4b778c; @@ -51,10 +53,14 @@ --color-data-raster-dim-rgb: 139, 119, 82; --color-data-mask: #8d85c7; --color-data-mask-rgb: 141, 133, 199; - --color-data-unused1: #d6536e; - --color-data-unused1-rgb: 214, 83, 110; - --color-data-unused2: #70a898; - --color-data-unused2-rgb: 112, 168, 152; + --color-data-number: #d6536e; + --color-data-number-rgb: 214, 83, 110; + --color-data-number-dim: #803242; + --color-data-number-dim-rgb: 128, 50, 66; + --color-data-color: #70a898; + --color-data-color-rgb: 112, 168, 152; + --color-data-color-dim: #43645b; + --color-data-color-dim-rgb: 67, 100, 91; --color-none: white; --color-none-repeat: no-repeat; diff --git a/frontend/src/components/panels/NodeGraph.vue b/frontend/src/components/panels/NodeGraph.vue index f521aeef86..fb063eaa70 100644 --- a/frontend/src/components/panels/NodeGraph.vue +++ b/frontend/src/components/panels/NodeGraph.vue @@ -35,8 +35,8 @@ class="node" :class="{ selected: selected.includes(node.id) }" :style="{ - '--offset-left': node.position?.x || 0, - '--offset-top': node.position?.y || 0, + '--offset-left': (node.position?.x || 0) + (selected.includes(node.id) ? draggingNodes?.roundX || 0 : 0), + '--offset-top': (node.position?.y || 0) + (selected.includes(node.id) ? draggingNodes?.roundY || 0 : 0), '--data-color': 'var(--color-data-raster)', '--data-color-dim': 'var(--color-data-raster-dim)', }" @@ -44,16 +44,43 @@ >
-
+
-
+
{{ node.displayName }}
+
+
+
+
+
+
+
+ {{ argument.name }} +
+
{ + await nextTick(); - const containerBounds = this.$refs.nodesContainer as HTMLDivElement | undefined; - if (!containerBounds) return; + const containerBounds = this.$refs.nodesContainer as HTMLDivElement | undefined; + if (!containerBounds) return; - const links = this.nodeGraph.state.links; - this.nodeLinkPaths = links.flatMap((link) => { - const connectorIndex = 0; + const links = this.nodeGraph.state.links; + this.nodeLinkPaths = links.flatMap((link) => { + const connectorIndex = 0; - const nodePrimaryOutput = (containerBounds.querySelector(`[data-node="${String(link.linkStart)}"] [data-port="output"]`) || undefined) as HTMLDivElement | undefined; + const nodePrimaryOutput = (containerBounds.querySelector(`[data-node="${String(link.linkStart)}"] [data-port="output"]`) || undefined) as HTMLDivElement | undefined; - const nodeInputConnectors = containerBounds.querySelectorAll(`[data-node="${String(link.linkEnd)}"] [data-port="input"]`) || undefined; - const nodePrimaryInput = nodeInputConnectors?.[connectorIndex] as HTMLDivElement | undefined; + const nodeInputConnectors = containerBounds.querySelectorAll(`[data-node="${String(link.linkEnd)}"] [data-port="input"]`) || undefined; + const nodePrimaryInput = nodeInputConnectors?.[connectorIndex] as HTMLDivElement | undefined; - if (!nodePrimaryInput || !nodePrimaryOutput) return []; - return [this.createWirePath(nodePrimaryOutput, nodePrimaryInput.getBoundingClientRect(), false, false)]; - }); - }, + if (!nodePrimaryInput || !nodePrimaryOutput) return []; + return [this.createWirePath(nodePrimaryOutput, nodePrimaryInput.getBoundingClientRect(), false, false)]; + }); }, - }, - methods: { nodeIcon(nodeName: string): IconName { const iconMap: Record = { - Grayscale: "NodeColorCorrection", - "Map Image": "NodeOutput", + Output: "NodeOutput", + "Hue Shift Image": "NodeColorCorrection", + "Brighten Image": "NodeColorCorrection", + "Grayscale Image": "NodeColorCorrection", }; return iconMap[nodeName] || "NodeNodes"; }, @@ -441,8 +475,16 @@ export default defineComponent({ if (e.shiftKey || e.ctrlKey) { if (this.selected.includes(id)) this.selected.splice(this.selected.lastIndexOf(id), 1); else this.selected.push(id); - } else { + } else if (!this.selected.includes(id)) { this.selected = [id]; + } else { + this.selectIfNotDragged = id; + } + + if (this.selected.includes(id)) { + this.draggingNodes = { startX: e.x, startY: e.y, roundX: 0, roundY: 0 }; + const graphDiv: HTMLDivElement | undefined = (this.$refs.graph as typeof LayoutCol | undefined)?.$el; + graphDiv?.setPointerCapture(e.pointerId); } this.editor.instance.selectNodes(new BigUint64Array(this.selected)); @@ -468,6 +510,14 @@ export default defineComponent({ } else { this.linkInProgressToConnector = new DOMRect(e.x, e.y); } + } else if (this.draggingNodes) { + const deltaX = Math.round((e.x - this.draggingNodes.startX) / this.transform.scale / this.gridSpacing); + const deltaY = Math.round((e.y - this.draggingNodes.startY) / this.transform.scale / this.gridSpacing); + if (this.draggingNodes.roundX !== deltaX || this.draggingNodes.roundY !== deltaY) { + this.draggingNodes.roundX = deltaX; + this.draggingNodes.roundY = deltaY; + this.refreshLinks(); + } } }, pointerUp(e: PointerEvent) { @@ -494,6 +544,16 @@ export default defineComponent({ this.editor.instance.connectNodesByLink(BigInt(outputConnectedNodeID), BigInt(inputConnectedNodeID), inputNodeConnectionIndex); } } + } else if (this.draggingNodes) { + if (this.draggingNodes.startX === e.x || this.draggingNodes.startY === e.y) { + if (this.selectIfNotDragged) { + this.selected = [this.selectIfNotDragged]; + this.editor.instance.selectNodes(new BigUint64Array(this.selected)); + } + } + this.editor.instance.moveSelectedNodes(this.draggingNodes.roundX, this.draggingNodes.roundY); + this.draggingNodes = undefined; + this.selectIfNotDragged = undefined; } this.linkInProgressFromConnector = undefined; diff --git a/frontend/src/components/widgets/WidgetRow.vue b/frontend/src/components/widgets/WidgetRow.vue index c8381f684d..5738d43683 100644 --- a/frontend/src/components/widgets/WidgetRow.vue +++ b/frontend/src/components/widgets/WidgetRow.vue @@ -26,6 +26,7 @@ @changeFont="(value: unknown) => updateLayout(component.widgetId, value)" :sharpRightCorners="nextIsSuffix" /> + @@ -104,6 +105,7 @@ import { isWidgetColumn, isWidgetRow, type WidgetColumn, type WidgetRow } from " import PivotAssist from "@/components/widgets/assists/PivotAssist.vue"; import IconButton from "@/components/widgets/buttons/IconButton.vue"; +import ParameterExposeButton from "@/components/widgets/buttons/ParameterExposeButton.vue"; import PopoverButton from "@/components/widgets/buttons/PopoverButton.vue"; import TextButton from "@/components/widgets/buttons/TextButton.vue"; import CheckboxInput from "@/components/widgets/inputs/CheckboxInput.vue"; @@ -175,6 +177,7 @@ export default defineComponent({ LayerReferenceInput, NumberInput, OptionalInput, + ParameterExposeButton, PivotAssist, PopoverButton, RadioInput, diff --git a/frontend/src/components/widgets/buttons/ParameterExposeButton.vue b/frontend/src/components/widgets/buttons/ParameterExposeButton.vue new file mode 100644 index 0000000000..6176f4a860 --- /dev/null +++ b/frontend/src/components/widgets/buttons/ParameterExposeButton.vue @@ -0,0 +1,73 @@ + + + + + diff --git a/frontend/src/components/widgets/groups/WidgetSection.vue b/frontend/src/components/widgets/groups/WidgetSection.vue index 69453a0f96..ada94fe20d 100644 --- a/frontend/src/components/widgets/groups/WidgetSection.vue +++ b/frontend/src/components/widgets/groups/WidgetSection.vue @@ -100,6 +100,10 @@ text-align: right; } + > .parameter-expose-button ~ .text-label:first-of-type { + text-align: left; + } + > .text-button { flex-grow: 1; } diff --git a/frontend/src/components/widgets/inputs/LayerReferenceInput.vue b/frontend/src/components/widgets/inputs/LayerReferenceInput.vue index fc056ac0a7..24e5c67d23 100644 --- a/frontend/src/components/widgets/inputs/LayerReferenceInput.vue +++ b/frontend/src/components/widgets/inputs/LayerReferenceInput.vue @@ -58,7 +58,8 @@ text-align: center; &.missing { - color: var(--color-data-unused1); + // TODO: Define this as a permanent color palette choice + color: #d6536e; } &.layer-name { diff --git a/frontend/src/wasm-communication/messages.ts b/frontend/src/wasm-communication/messages.ts index e550ec3008..a9a5ca0821 100644 --- a/frontend/src/wasm-communication/messages.ts +++ b/frontend/src/wasm-communication/messages.ts @@ -69,16 +69,22 @@ export class FrontendDocumentDetails extends DocumentDetails { readonly id!: bigint; } -export type DataType = "Raster" | "Color" | "Image" | "F32"; +export type FrontendGraphDataType = "general" | "raster" | "color" | "vector" | "number"; + +export class NodeGraphInput { + readonly dataType!: FrontendGraphDataType; + + readonly name!: string; +} export class FrontendNode { readonly id!: bigint; readonly displayName!: string; - readonly exposedInputs!: DataType[]; + readonly exposedInputs!: NodeGraphInput[]; - readonly outputs!: DataType[]; + readonly outputs!: FrontendGraphDataType[]; @TupleToVec2 readonly position!: XY | undefined; @@ -1006,6 +1012,15 @@ export class TextAreaInput extends WidgetProps { tooltip!: string | undefined; } +export class ParameterExposeButton extends WidgetProps { + exposed!: boolean; + + dataType!: string; + + @Transform(({ value }: { value: string }) => value || undefined) + tooltip!: string | undefined; +} + export class TextButton extends WidgetProps { label!: string; @@ -1099,6 +1114,7 @@ const widgetSubTypes = [ { value: SwatchPairInput, name: "SwatchPairInput" }, { value: TextAreaInput, name: "TextAreaInput" }, { value: TextButton, name: "TextButton" }, + { value: ParameterExposeButton, name: "ParameterExposeButton" }, { value: TextInput, name: "TextInput" }, { value: TextLabel, name: "TextLabel" }, { value: PivotAssist, name: "PivotAssist" }, diff --git a/node-graph/graph-craft/src/node_registry.rs b/node-graph/graph-craft/src/node_registry.rs index c947cafc29..56766b2747 100644 --- a/node-graph/graph-craft/src/node_registry.rs +++ b/node-graph/graph-craft/src/node_registry.rs @@ -42,6 +42,24 @@ static NODE_REGISTRY: &[(NodeIdentifier, NodeConstructor)] = &[ } }) }), + ( + NodeIdentifier::new("graphene_core::ops::AddNode", &[Type::Concrete(Cow::Borrowed("&TypeErasedNode"))]), + |proto_node, stack| { + stack.push_fn(move |nodes| { + let ConstructionArgs::Nodes(construction_nodes) = proto_node.construction_args else { unreachable!("Add Node constructed with out rhs input node") }; + let value_node = nodes.get(construction_nodes[0] as usize).unwrap(); + let input_node: DowncastBothNode<_, (), f32> = DowncastBothNode::new(value_node); + let node: DynAnyNode<_, f32, _, _> = DynAnyNode::new(ConsNode::new(input_node).then(graphene_core::ops::AddNode)); + + if let ProtoNodeInput::Node(node_id) = proto_node.input { + let pre_node = nodes.get(node_id as usize).unwrap(); + (pre_node).then(node).into_type_erased() + } else { + node.into_type_erased() + } + }) + }, + ), ( NodeIdentifier::new( "graphene_core::ops::AddNode", diff --git a/node-graph/gstd/src/raster.rs b/node-graph/gstd/src/raster.rs index 11c839d816..7ab3896dee 100644 --- a/node-graph/gstd/src/raster.rs +++ b/node-graph/gstd/src/raster.rs @@ -98,7 +98,7 @@ impl Node for BufferNode { } } -#[derive(Clone, Debug, PartialEq, DynAny)] +#[derive(Clone, Debug, PartialEq, DynAny, Default)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Image { pub width: u32, @@ -106,6 +106,16 @@ pub struct Image { pub data: Vec, } +impl Image { + pub const fn empty() -> Self { + Self { + width: 0, + height: 0, + data: Vec::new(), + } + } +} + impl IntoIterator for Image { type Item = Color; type IntoIter = std::vec::IntoIter;