Add support for exposing node parameter inputs (#866)

* Add exposing inputs to graph

* Use uuids and better node positioning

* Fix accidentally refering to the wrong grid spacing

* Secondary input without primary input

* Cleanup document node types

* Rename to input and addend
This commit is contained in:
0HyperCube 2022-11-22 19:57:08 +00:00 committed by Keavon Chambers
parent 0a78ebda25
commit 3dd9e88655
8 changed files with 174 additions and 109 deletions

View File

@ -14,7 +14,7 @@ pub enum NodeGraphMessage {
}, },
CreateNode { CreateNode {
// Having the caller generate the id means that we don't have to return it. This can be a random u64. // Having the caller generate the id means that we don't have to return it. This can be a random u64.
node_id: NodeId, node_id: Option<NodeId>,
// I don't really know what this is for (perhaps a user identifiable name). // I don't really know what this is for (perhaps a user identifiable name).
node_type: String, node_type: String,
}, },

View File

@ -35,6 +35,8 @@ pub struct FrontendNode {
pub id: graph_craft::document::NodeId, pub id: graph_craft::document::NodeId,
#[serde(rename = "displayName")] #[serde(rename = "displayName")]
pub display_name: String, pub display_name: String,
#[serde(rename = "primaryInput")]
pub primary_input: Option<FrontendGraphDataType>,
#[serde(rename = "exposedInputs")] #[serde(rename = "exposedInputs")]
pub exposed_inputs: Vec<NodeGraphInput>, pub exposed_inputs: Vec<NodeGraphInput>,
pub outputs: Vec<FrontendGraphDataType>, pub outputs: Vec<FrontendGraphDataType>,
@ -122,10 +124,17 @@ impl NodeGraphMessageHandler {
nodes.push(FrontendNode { nodes.push(FrontendNode {
id: *id, id: *id,
display_name: node.name.clone(), display_name: node.name.clone(),
primary_input: node
.inputs
.first()
.filter(|input| input.is_exposed())
.and_then(|_| node_type.inputs.get(0))
.map(|input_type| input_type.data_type),
exposed_inputs: node exposed_inputs: node
.inputs .inputs
.iter() .iter()
.zip(node_type.inputs) .zip(node_type.inputs)
.skip(1)
.filter(|(input, _)| input.is_exposed()) .filter(|(input, _)| input.is_exposed())
.map(|(_, input_type)| NodeGraphInput { .map(|(_, input_type)| NodeGraphInput {
data_type: input_type.data_type, data_type: input_type.data_type,
@ -180,6 +189,7 @@ impl MessageHandler<NodeGraphMessage, (&mut Document, &InputPreprocessorMessageH
responses.push_back(DocumentMessage::NodeGraphFrameGenerate.into()); responses.push_back(DocumentMessage::NodeGraphFrameGenerate.into());
} }
NodeGraphMessage::CreateNode { node_id, node_type } => { NodeGraphMessage::CreateNode { node_id, node_type } => {
let node_id = node_id.unwrap_or_else(crate::application::generate_uuid);
let Some(network) = self.get_active_network_mut(document) else{ let Some(network) = self.get_active_network_mut(document) else{
warn!("No network"); warn!("No network");
return; return;
@ -208,6 +218,8 @@ impl MessageHandler<NodeGraphMessage, (&mut Document, &InputPreprocessorMessageH
.into_iter() .into_iter()
.collect(), .collect(),
}; };
let far_right_node = network.nodes.iter().map(|node| node.1.metadata.position).max_by_key(|pos| pos.0).unwrap_or_default();
network.nodes.insert( network.nodes.insert(
node_id, node_id,
DocumentNode { DocumentNode {
@ -217,7 +229,7 @@ impl MessageHandler<NodeGraphMessage, (&mut Document, &InputPreprocessorMessageH
implementation: DocumentNodeImplementation::Network(inner_network), implementation: DocumentNodeImplementation::Network(inner_network),
metadata: graph_craft::document::DocumentNodeMetadata { metadata: graph_craft::document::DocumentNodeMetadata {
// TODO: Better position default // TODO: Better position default
position: (node_id as i32 * 7 - 41, node_id as i32 * 2 - 10), position: (far_right_node.0 + 7, far_right_node.1 + 2),
}, },
}, },
); );
@ -243,8 +255,16 @@ impl MessageHandler<NodeGraphMessage, (&mut Document, &InputPreprocessorMessageH
if let NodeInput::Value { exposed, .. } = &mut node.inputs[input_index] { if let NodeInput::Value { exposed, .. } = &mut node.inputs[input_index] {
*exposed = new_exposed; *exposed = new_exposed;
} else if let Some(node_type) = document_node_types::resolve_document_node_type(&node.name) {
if let NodeInput::Value { tagged_value, .. } = &node_type.inputs[input_index].default {
node.inputs[input_index] = NodeInput::Value {
tagged_value: tagged_value.clone(),
exposed: new_exposed,
};
}
} }
Self::send_graph(network, responses); Self::send_graph(network, responses);
responses.push_back(PropertiesPanelMessage::ResendActiveProperties.into());
} }
NodeGraphMessage::MoveSelectedNodes { displacement_x, displacement_y } => { NodeGraphMessage::MoveSelectedNodes { displacement_x, displacement_y } => {
let Some(network) = self.get_active_network_mut(document) else{ let Some(network) = self.get_active_network_mut(document) else{

View File

@ -37,7 +37,7 @@ static DOCUMENT_NODE_TYPES: [DocumentNodeType; 7] = [
properties: |_document_node, _node_id| { properties: |_document_node, _node_id| {
vec![LayoutGroup::Row { vec![LayoutGroup::Row {
widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel { widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
value: format!("The identity node simply returns the input"), value: "The identity node simply returns the input".to_string(),
..Default::default() ..Default::default()
}))], }))],
}] }]
@ -46,12 +46,16 @@ static DOCUMENT_NODE_TYPES: [DocumentNodeType; 7] = [
DocumentNodeType { DocumentNodeType {
name: "Input", name: "Input",
identifier: NodeIdentifier::new("graphene_core::ops::IdNode", &[Type::Concrete(Cow::Borrowed("Any<'_>"))]), identifier: NodeIdentifier::new("graphene_core::ops::IdNode", &[Type::Concrete(Cow::Borrowed("Any<'_>"))]),
inputs: &[], inputs: &[DocumentInputType {
name: "In",
data_type: FrontendGraphDataType::Raster,
default: NodeInput::Network,
}],
outputs: &[FrontendGraphDataType::Raster], outputs: &[FrontendGraphDataType::Raster],
properties: |_document_node, _node_id| { properties: |_document_node, _node_id| {
vec![LayoutGroup::Row { vec![LayoutGroup::Row {
widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel { widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
value: format!("The input to the graph is the bitmap under the frame"), value: "The input to the graph is the bitmap under the frame".to_string(),
..Default::default() ..Default::default()
}))], }))],
}] }]
@ -72,7 +76,7 @@ static DOCUMENT_NODE_TYPES: [DocumentNodeType; 7] = [
properties: |_document_node, _node_id| { properties: |_document_node, _node_id| {
vec![LayoutGroup::Row { vec![LayoutGroup::Row {
widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel { widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
value: format!("The output to the graph is rendered in the frame"), value: "The output to the graph is rendered in the frame".to_string(),
..Default::default() ..Default::default()
}))], }))],
}] }]
@ -93,7 +97,7 @@ static DOCUMENT_NODE_TYPES: [DocumentNodeType; 7] = [
properties: |_document_node, _node_id| { properties: |_document_node, _node_id| {
vec![LayoutGroup::Row { vec![LayoutGroup::Row {
widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel { widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
value: format!("The output to the graph is rendered in the frame"), value: "The output to the graph is rendered in the frame".to_string(),
..Default::default() ..Default::default()
}))], }))],
}] }]
@ -152,7 +156,7 @@ static DOCUMENT_NODE_TYPES: [DocumentNodeType; 7] = [
identifier: NodeIdentifier::new("graphene_core::ops::AddNode", &[Type::Concrete(Cow::Borrowed("&TypeErasedNode"))]), identifier: NodeIdentifier::new("graphene_core::ops::AddNode", &[Type::Concrete(Cow::Borrowed("&TypeErasedNode"))]),
inputs: &[ inputs: &[
DocumentInputType { DocumentInputType {
name: "Left", name: "Input",
data_type: FrontendGraphDataType::Number, data_type: FrontendGraphDataType::Number,
default: NodeInput::Value { default: NodeInput::Value {
tagged_value: TaggedValue::F32(0.), tagged_value: TaggedValue::F32(0.),
@ -160,7 +164,7 @@ static DOCUMENT_NODE_TYPES: [DocumentNodeType; 7] = [
}, },
}, },
DocumentInputType { DocumentInputType {
name: "Right", name: "Addend",
data_type: FrontendGraphDataType::Number, data_type: FrontendGraphDataType::Number,
default: NodeInput::Value { default: NodeInput::Value {
tagged_value: TaggedValue::F32(0.), tagged_value: TaggedValue::F32(0.),
@ -178,5 +182,9 @@ pub fn resolve_document_node_type(name: &str) -> Option<&DocumentNodeType> {
} }
pub fn collect_node_types() -> Vec<FrontendNodeType> { pub fn collect_node_types() -> Vec<FrontendNodeType> {
DOCUMENT_NODE_TYPES.iter().map(|node_type| FrontendNodeType { name: node_type.name.to_string() }).collect() DOCUMENT_NODE_TYPES
.iter()
.filter(|node_type| !matches!(node_type.name, "Input" | "Output"))
.map(|node_type| FrontendNodeType { name: node_type.name.to_string() })
.collect()
} }

View File

@ -10,33 +10,46 @@ use graph_craft::document::{DocumentNode, NodeId, NodeInput};
use super::FrontendGraphDataType; use super::FrontendGraphDataType;
pub fn hue_shift_image_properties(document_node: &DocumentNode, node_id: NodeId) -> Vec<LayoutGroup> { pub fn hue_shift_image_properties(document_node: &DocumentNode, node_id: NodeId) -> Vec<LayoutGroup> {
vec![LayoutGroup::Row { let index = 1;
widgets: vec![ let input: &NodeInput = document_node.inputs.get(index).unwrap();
WidgetHolder::new(Widget::ParameterExposeButton(ParameterExposeButton { let exposed = input.is_exposed();
exposed: true,
data_type: FrontendGraphDataType::Number, let mut widgets = vec![
tooltip: "Expose input parameter in node graph".into(), WidgetHolder::new(Widget::ParameterExposeButton(ParameterExposeButton {
..Default::default() exposed,
})), data_type: FrontendGraphDataType::Number,
WidgetHolder::new(Widget::Separator(Separator { tooltip: "Expose input parameter in node graph".into(),
separator_type: SeparatorType::Unrelated, on_update: WidgetCallback::new(move |_parameter| {
direction: SeparatorDirection::Horizontal, NodeGraphMessage::ExposeInput {
})), node_id,
WidgetHolder::new(Widget::TextLabel(TextLabel { input_index: index,
value: "Shift Degrees".into(), new_exposed: !exposed,
..Default::default() }
})), .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()
})),
];
if let NodeInput::Value {
tagged_value: TaggedValue::F32(x),
exposed: false,
} = document_node.inputs[index]
{
widgets.extend_from_slice(&[
WidgetHolder::new(Widget::Separator(Separator { WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Unrelated, separator_type: SeparatorType::Unrelated,
direction: SeparatorDirection::Horizontal, direction: SeparatorDirection::Horizontal,
})), })),
WidgetHolder::new(Widget::NumberInput(NumberInput { WidgetHolder::new(Widget::NumberInput(NumberInput {
value: Some({ value: Some(x as f64),
let NodeInput::Value {tagged_value: TaggedValue::F32(x), ..} = document_node.inputs[1] else {
panic!("Hue rotate should be f32")
};
x as f64
}),
unit: "°".into(), unit: "°".into(),
mode: NumberInputMode::Range, mode: NumberInputMode::Range,
range_min: Some(-180.), range_min: Some(-180.),
@ -51,38 +64,54 @@ pub fn hue_shift_image_properties(document_node: &DocumentNode, node_id: NodeId)
}), }),
..NumberInput::default() ..NumberInput::default()
})), })),
], ])
}] }
vec![LayoutGroup::Row { widgets }]
} }
pub fn brighten_image_properties(document_node: &DocumentNode, node_id: NodeId) -> Vec<LayoutGroup> { pub fn brighten_image_properties(document_node: &DocumentNode, node_id: NodeId) -> Vec<LayoutGroup> {
vec![LayoutGroup::Row { let index = 1;
widgets: vec![ let input: &NodeInput = document_node.inputs.get(index).unwrap();
WidgetHolder::new(Widget::ParameterExposeButton(ParameterExposeButton { let exposed = input.is_exposed();
exposed: true,
data_type: FrontendGraphDataType::Number, let mut widgets = vec![
tooltip: "Expose input parameter in node graph".into(), WidgetHolder::new(Widget::ParameterExposeButton(ParameterExposeButton {
..Default::default() exposed,
})), data_type: FrontendGraphDataType::Number,
WidgetHolder::new(Widget::Separator(Separator { tooltip: "Expose input parameter in node graph".into(),
separator_type: SeparatorType::Unrelated, on_update: WidgetCallback::new(move |_parameter| {
direction: SeparatorDirection::Horizontal, NodeGraphMessage::ExposeInput {
})), node_id,
WidgetHolder::new(Widget::TextLabel(TextLabel { input_index: index,
value: "Brighten Amount".into(), new_exposed: !exposed,
..Default::default() }
})), .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()
})),
];
if let NodeInput::Value {
tagged_value: TaggedValue::F32(x),
exposed: false,
} = document_node.inputs[index]
{
widgets.extend_from_slice(&[
WidgetHolder::new(Widget::Separator(Separator { WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Unrelated, separator_type: SeparatorType::Unrelated,
direction: SeparatorDirection::Horizontal, direction: SeparatorDirection::Horizontal,
})), })),
WidgetHolder::new(Widget::NumberInput(NumberInput { WidgetHolder::new(Widget::NumberInput(NumberInput {
value: Some({ value: Some(x as f64),
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, mode: NumberInputMode::Range,
range_min: Some(-255.), range_min: Some(-255.),
range_max: Some(255.), range_max: Some(255.),
@ -96,17 +125,29 @@ pub fn brighten_image_properties(document_node: &DocumentNode, node_id: NodeId)
}), }),
..NumberInput::default() ..NumberInput::default()
})), })),
], ])
}] }
vec![LayoutGroup::Row { widgets }]
} }
pub fn add_properties(document_node: &DocumentNode, node_id: NodeId) -> Vec<LayoutGroup> { pub fn add_properties(document_node: &DocumentNode, node_id: NodeId) -> Vec<LayoutGroup> {
let operand = |name: &str, index| LayoutGroup::Row { let operand = |name: &str, index| {
widgets: vec![ let input: &NodeInput = document_node.inputs.get(index).unwrap();
let exposed = input.is_exposed();
let mut widgets = vec![
WidgetHolder::new(Widget::ParameterExposeButton(ParameterExposeButton { WidgetHolder::new(Widget::ParameterExposeButton(ParameterExposeButton {
exposed: true, exposed,
data_type: FrontendGraphDataType::Number, data_type: FrontendGraphDataType::Number,
tooltip: "Expose input parameter in node graph".into(), tooltip: "Expose input parameter in node graph".into(),
on_update: WidgetCallback::new(move |_parameter| {
NodeGraphMessage::ExposeInput {
node_id,
input_index: index,
new_exposed: !exposed,
}
.into()
}),
..Default::default() ..Default::default()
})), })),
WidgetHolder::new(Widget::Separator(Separator { WidgetHolder::new(Widget::Separator(Separator {
@ -117,32 +158,37 @@ pub fn add_properties(document_node: &DocumentNode, node_id: NodeId) -> Vec<Layo
value: name.into(), value: name.into(),
..Default::default() ..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 if let NodeInput::Value {
}), tagged_value: TaggedValue::F32(x),
mode: NumberInputMode::Increment, exposed: false,
on_update: WidgetCallback::new(move |number_input: &NumberInput| { } = document_node.inputs[index]
NodeGraphMessage::SetInputValue { {
node: node_id, widgets.extend_from_slice(&[
input_index: index, WidgetHolder::new(Widget::Separator(Separator {
value: TaggedValue::F32(number_input.value.unwrap() as f32), separator_type: SeparatorType::Unrelated,
} direction: SeparatorDirection::Horizontal,
.into() })),
}), WidgetHolder::new(Widget::NumberInput(NumberInput {
..NumberInput::default() value: Some(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()
})),
]);
}
LayoutGroup::Row { widgets }
}; };
vec![operand("Left", 0), operand("Right", 1)] vec![operand("Input", 0), operand("Addend", 1)]
} }
fn unknown_node_properties(document_node: &DocumentNode) -> Vec<LayoutGroup> { fn unknown_node_properties(document_node: &DocumentNode) -> Vec<LayoutGroup> {

View File

@ -45,11 +45,11 @@
<div class="primary"> <div class="primary">
<div class="ports"> <div class="ports">
<div <div
v-if="node.exposedInputs.length > 0" v-if="node.primaryInput"
class="input port" class="input port"
data-port="input" data-port="input"
:data-datatype="node.exposedInputs[0].dataType" :data-datatype="node.primaryInput"
:style="{ '--data-color': `var(--color-data-${node.exposedInputs[0].dataType})`, '--data-color-dim': `var(--color-data-${node.exposedInputs[0].dataType}-dim)` }" :style="{ '--data-color': `var(--color-data-${node.primaryInput})`, '--data-color-dim': `var(--color-data-${node.primaryInput}-dim)` }"
> >
<div></div> <div></div>
</div> </div>
@ -66,8 +66,8 @@
<IconLabel :icon="nodeIcon(node.displayName)" /> <IconLabel :icon="nodeIcon(node.displayName)" />
<TextLabel>{{ node.displayName }}</TextLabel> <TextLabel>{{ node.displayName }}</TextLabel>
</div> </div>
<div v-if="node.exposedInputs.length > 1" class="arguments"> <div v-if="node.exposedInputs.length > 0" class="arguments">
<div v-for="(argument, index) in node.exposedInputs.slice(1)" :key="index" class="argument"> <div v-for="(argument, index) in node.exposedInputs" :key="index" class="argument">
<div class="ports"> <div class="ports">
<div <div
class="input port" class="input port"
@ -366,7 +366,7 @@ export default defineComponent({
const links = this.nodeGraph.state.links; const links = this.nodeGraph.state.links;
this.nodeLinkPaths = links.flatMap((link) => { this.nodeLinkPaths = links.flatMap((link) => {
const connectorIndex = 0; const connectorIndex = Number(link.linkEndInputIndex);
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;
@ -511,8 +511,8 @@ export default defineComponent({
this.linkInProgressToConnector = new DOMRect(e.x, e.y); this.linkInProgressToConnector = new DOMRect(e.x, e.y);
} }
} else if (this.draggingNodes) { } else if (this.draggingNodes) {
const deltaX = Math.round((e.x - this.draggingNodes.startX) / this.transform.scale / this.gridSpacing); const deltaX = Math.round((e.x - this.draggingNodes.startX) / this.transform.scale / GRID_SIZE);
const deltaY = Math.round((e.y - this.draggingNodes.startY) / this.transform.scale / this.gridSpacing); const deltaY = Math.round((e.y - this.draggingNodes.startY) / this.transform.scale / GRID_SIZE);
if (this.draggingNodes.roundX !== deltaX || this.draggingNodes.roundY !== deltaY) { if (this.draggingNodes.roundX !== deltaX || this.draggingNodes.roundY !== deltaY) {
this.draggingNodes.roundX = deltaX; this.draggingNodes.roundX = deltaX;
this.draggingNodes.roundY = deltaY; this.draggingNodes.roundY = deltaY;

View File

@ -82,6 +82,8 @@ export class FrontendNode {
readonly displayName!: string; readonly displayName!: string;
readonly primaryInput!: FrontendGraphDataType | undefined;
readonly exposedInputs!: NodeGraphInput[]; readonly exposedInputs!: NodeGraphInput[];
readonly outputs!: FrontendGraphDataType[]; readonly outputs!: FrontendGraphDataType[];

View File

@ -553,18 +553,7 @@ impl JsEditorHandle {
/// Creates a new document node in the node graph /// Creates a new document node in the node graph
#[wasm_bindgen(js_name = createNode)] #[wasm_bindgen(js_name = createNode)]
pub fn create_node(&self, node_type: String) { pub fn create_node(&self, node_type: String) {
fn generate_node_id() -> u64 { let message = NodeGraphMessage::CreateNode { node_id: None, node_type };
static mut NODE_ID: u64 = 10;
unsafe {
NODE_ID += 1;
NODE_ID
}
}
let message = NodeGraphMessage::CreateNode {
node_id: generate_node_id(),
node_type,
};
self.dispatch(message); self.dispatch(message);
} }

View File

@ -108,10 +108,10 @@ impl NodeInput {
} }
} }
pub fn is_exposed(&self) -> bool { pub fn is_exposed(&self) -> bool {
if let NodeInput::Value { exposed, .. } = self { match self {
*exposed NodeInput::Node(_) => true,
} else { NodeInput::Value { exposed, .. } => *exposed,
true NodeInput::Network => false,
} }
} }
} }