General purpose value node

This commit is contained in:
Adam
2025-07-26 13:56:56 -07:00
parent 8803cb4079
commit 007812077b
9 changed files with 339 additions and 92 deletions
@@ -102,6 +102,25 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
description: Cow::Borrowed("Passes-through the input value without changing it. This is useful for rerouting wires for organization purposes."),
properties: Some("identity_properties"),
},
DocumentNodeDefinition {
identifier: "Value",
category: "General",
node_template: NodeTemplate {
document_node: DocumentNode {
implementation: DocumentNodeImplementation::proto("graphene_core::any::ValueNode"),
manual_composition: Some(generic!(T)),
inputs: vec![NodeInput::value(TaggedValue::None, false)],
..Default::default()
},
persistent_node_metadata: DocumentNodePersistentMetadata {
input_metadata: vec![("", "Value").into()],
output_names: vec!["Out".to_string()],
..Default::default()
},
},
description: Cow::Borrowed("Returns the value stored in its input"),
properties: Some("value_properties"),
},
// TODO: Auto-generate this from its proto node macro
DocumentNodeDefinition {
identifier: "Monitor",
@@ -1906,6 +1925,7 @@ fn static_node_properties() -> NodeProperties {
"monitor_properties".to_string(),
Box::new(|_node_id, _context| node_properties::string_properties("The Monitor node is used by the editor to access the data flowing through it.")),
);
map.insert("value_properties".to_string(), Box::new(node_properties::value_properties));
map
}
@@ -152,6 +152,10 @@ pub enum NodeGraphMessage {
node_id: NodeId,
alias: String,
},
SetReference {
node_id: NodeId,
reference: Option<String>,
},
SetToNodeOrLayer {
node_id: NodeId,
is_layer: bool,
@@ -537,7 +537,10 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
log::error!("Could not get center of selected_nodes");
return;
};
let center_of_selected_nodes_grid_space = IVec2::new((center_of_selected_nodes.x / 24. + 0.5).floor() as i32, (center_of_selected_nodes.y / 24. + 0.5).floor() as i32);
let center_of_selected_nodes_grid_space = IVec2::new(
(center_of_selected_nodes.x / GRID_SIZE as f64 + 0.5).floor() as i32,
(center_of_selected_nodes.y / GRID_SIZE as f64 + 0.5).floor() as i32,
);
default_node_template.persistent_node_metadata.node_type_metadata = NodeTypePersistentMetadata::node(center_of_selected_nodes_grid_space - IVec2::new(3, 1));
responses.add(DocumentMessage::AddTransaction);
responses.add(NodeGraphMessage::InsertNode {
@@ -645,7 +648,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
log::error!("Could not get network metadata in PointerDown");
return;
};
self.disconnecting = None;
let click = ipp.mouse.position;
let node_graph_point = network_metadata.persistent_metadata.navigation_metadata.node_graph_to_viewport.inverse().transform_point2(click);
@@ -994,8 +997,9 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
};
responses.add(FrontendMessage::UpdateWirePathInProgress { wire_path: Some(wire_path) });
}
} else if self.disconnecting.is_some() {
// Disconnecting with no upstream node, create new value node.
}
// Dragging from an exposed value input
else if self.disconnecting.is_some() {
let to_connector = network_interface.input_connector_from_click(ipp.mouse.position, selection_network_path);
if let Some(to_connector) = &to_connector {
let Some(input_position) = network_interface.input_position(to_connector, selection_network_path) else {
@@ -1004,58 +1008,12 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
};
self.wire_in_progress_to_connector = Some(input_position);
}
// Not hovering over a node input or node output, insert the node
else {
// Disconnect if the wire was previously connected to an input
if let Some(disconnecting) = self.disconnecting.take() {
let mut position = if let Some(to_connector) = self.wire_in_progress_to_connector { to_connector } else { point };
// Offset to drag from center of node
position = position - DVec2::new(24. * 3., 24.);
// Offset to account for division rounding error
if position.x < 0. {
position.x = position.x - 1.;
}
if position.y < 0. {
position.y = position.y - 1.;
}
let Some(input) = network_interface.take_input(&disconnecting, breadcrumb_network_path) else {
return;
};
let drag_start = DragStart {
start_x: point.x,
start_y: point.y,
round_x: 0,
round_y: 0,
};
self.drag_start = Some((drag_start, false));
self.node_has_moved_in_drag = false;
self.update_node_graph_hints(responses);
let node_id = NodeId::new();
responses.add(NodeGraphMessage::CreateNodeFromContextMenu {
node_id: Some(node_id),
node_type: "Identity".to_string(),
xy: Some(((position.x / 24.) as i32, (position.y / 24.) as i32)),
});
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, 0),
input,
});
responses.add(NodeGraphMessage::CreateWire {
output_connector: OutputConnector::Node { node_id, output_index: 0 },
input_connector: disconnecting,
});
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![node_id] });
// Update the frontend that the node is disconnected
responses.add(NodeGraphMessage::RunDocumentGraph);
responses.add(NodeGraphMessage::SendGraph);
}
// Not hovering over a node input or node output, create the value node if alt is pressed
else if ipp.keyboard.get(Key::Alt as usize) {
self.preview_on_mouse_up = None;
self.create_value_node(network_interface, point, breadcrumb_network_path, responses);
} else {
//TODO: Start creating wire
}
} else if let Some((drag_start, dragged)) = &mut self.drag_start {
if drag_start.start_x != point.x || drag_start.start_y != point.y {
@@ -1076,7 +1034,10 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
}
}
let mut graph_delta = IVec2::new(((point.x - drag_start.start_x) / 24.).round() as i32, ((point.y - drag_start.start_y) / 24.).round() as i32);
let mut graph_delta = IVec2::new(
((point.x - drag_start.start_x) / GRID_SIZE as f64).round() as i32,
((point.y - drag_start.start_y) / GRID_SIZE as f64).round() as i32,
);
let previous_round_x = drag_start.round_x;
let previous_round_y = drag_start.round_y;
@@ -1621,6 +1582,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
let document_bbox: [DVec2; 2] = viewport_bbox.map(|p| network_metadata.persistent_metadata.navigation_metadata.node_graph_to_viewport.inverse().transform_point2(p));
let mut nodes = Vec::new();
for node_id in &self.frontend_nodes {
let Some(node_bbox) = network_interface.node_bounding_box(node_id, breadcrumb_network_path) else {
log::error!("Could not get bbox for node: {:?}", node_id);
@@ -1637,6 +1599,17 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
}
}
// Always send nodes with errors
for error in &self.node_graph_errors {
let Some((id, path)) = error.node_path.split_last() else {
log::error!("Could not get node path in error: {:?}", error);
continue;
};
if breadcrumb_network_path == path {
nodes.push(*id);
}
}
responses.add(FrontendMessage::UpdateVisibleNodes { nodes });
}
NodeGraphMessage::SendGraph => {
@@ -1675,7 +1648,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
input,
});
responses.add(PropertiesPanelMessage::Refresh);
if !(network_interface.reference(&node_id, selection_network_path).is_none() || input_index == 0) && network_interface.connected_to_output(&node_id, selection_network_path) {
if network_interface.connected_to_output(&node_id, selection_network_path) {
responses.add(NodeGraphMessage::RunDocumentGraph);
}
}
@@ -1757,6 +1730,9 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
responses.add(NodeGraphMessage::SendWires);
}
NodeGraphMessage::SetReference { node_id, reference } => {
network_interface.set_reference(&node_id, breadcrumb_network_path, reference);
}
NodeGraphMessage::SetToNodeOrLayer { node_id, is_layer } => {
if is_layer && !network_interface.is_eligible_to_be_layer(&node_id, selection_network_path) {
return;
@@ -2496,6 +2472,69 @@ impl NodeGraphMessageHandler {
}
}
fn create_value_node(&mut self, network_interface: &mut NodeNetworkInterface, point: DVec2, breadcrumb_network_path: &[NodeId], responses: &mut VecDeque<Message>) {
let Some(disconnecting) = self.disconnecting.take() else {
log::error!("To connector must be initialized to create a value node");
return;
};
let Some(mut position) = self.wire_in_progress_to_connector.take() else {
log::error!("To connector must be initialized to create a value node");
return;
};
// Offset node insertion 3 grid spaces left and 1 grid space up so the center of the node is dragged
position = position - DVec2::new(GRID_SIZE as f64 * 3., GRID_SIZE as f64);
// Offset to account for division rounding error and place the selected node to the top left of the input
if position.x < 0. {
position.x = position.x - 1.;
}
if position.y < 0. {
position.y = position.y - 1.;
}
let Some(mut input) = network_interface.take_input(&disconnecting, breadcrumb_network_path) else {
return;
};
match &mut input {
NodeInput::Value { exposed, .. } => *exposed = false,
_ => return,
}
let drag_start = DragStart {
start_x: point.x,
start_y: point.y,
round_x: 0,
round_y: 0,
};
self.drag_start = Some((drag_start, false));
self.node_has_moved_in_drag = false;
self.update_node_graph_hints(responses);
let node_id = NodeId::new();
responses.add(NodeGraphMessage::CreateNodeFromContextMenu {
node_id: Some(node_id),
node_type: "Value".to_string(),
xy: Some(((position.x / GRID_SIZE as f64) as i32, (position.y / GRID_SIZE as f64) as i32)),
add_transaction: false,
});
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, 0),
input,
});
responses.add(NodeGraphMessage::CreateWire {
output_connector: OutputConnector::Node { node_id, output_index: 0 },
input_connector: disconnecting,
});
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![node_id] });
// Update the frontend that the node is disconnected
responses.add(NodeGraphMessage::RunDocumentGraph);
responses.add(NodeGraphMessage::SendGraph);
}
fn collect_wires(&mut self, network_interface: &mut NodeNetworkInterface, graph_wire_style: GraphWireStyle, breadcrumb_network_path: &[NodeId]) -> Vec<WirePathUpdate> {
let mut added_wires = network_interface
.node_graph_input_connectors(breadcrumb_network_path)
@@ -9,7 +9,7 @@ use choice::enum_choice;
use dyn_any::DynAny;
use glam::{DAffine2, DVec2};
use graph_craft::Type;
use graph_craft::document::value::TaggedValue;
use graph_craft::document::value::{TaggedValue, TaggedValueChoice};
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput};
use graphene_std::animation::RealTimeMode;
use graphene_std::extract_xy::XY;
@@ -89,7 +89,7 @@ pub fn start_widgets(parameter_widgets_info: ParameterWidgetsInfo) -> Vec<Widget
description,
input_type,
blank_assist,
exposeable,
exposable: exposeable,
} = parameter_widgets_info;
let Some(document_node) = document_node else {
@@ -122,6 +122,7 @@ pub(crate) fn property_from_type(
unit: Option<&str>,
display_decimal_places: Option<u32>,
step: Option<f64>,
exposable: bool,
context: &mut NodePropertiesContext,
) -> Result<Vec<LayoutGroup>, Vec<LayoutGroup>> {
let (mut number_min, mut number_max, range) = number_options;
@@ -144,7 +145,8 @@ pub(crate) fn property_from_type(
let min = |x: f64| number_min.unwrap_or(x);
let max = |x: f64| number_max.unwrap_or(x);
let default_info = ParameterWidgetsInfo::new(node_id, index, true, context);
let mut default_info = ParameterWidgetsInfo::new(node_id, index, true, context);
default_info.exposable = exposable;
let mut extra_widgets = vec![];
let widgets = match ty {
@@ -251,8 +253,8 @@ pub(crate) fn property_from_type(
}
}
Type::Generic(_) => vec![TextLabel::new("Generic type (not supported)").widget_holder()].into(),
Type::Fn(_, out) => return property_from_type(node_id, index, out, number_options, unit, display_decimal_places, step, context),
Type::Future(out) => return property_from_type(node_id, index, out, number_options, unit, display_decimal_places, step, context),
Type::Fn(_, out) => return property_from_type(node_id, index, out, number_options, unit, display_decimal_places, step, exposable, context),
Type::Future(out) => return property_from_type(node_id, index, out, number_options, unit, display_decimal_places, step, exposable, context),
};
extra_widgets.push(widgets);
@@ -1115,7 +1117,7 @@ pub(crate) fn channel_mixer_properties(node_id: NodeId, context: &mut NodeProper
let is_monochrome = bool_widget(ParameterWidgetsInfo::new(node_id, MonochromeInput::INDEX, true, context), CheckboxInput::default());
let mut parameter_info = ParameterWidgetsInfo::new(node_id, OutputChannelInput::INDEX, true, context);
parameter_info.exposeable = false;
parameter_info.exposable = false;
let output_channel = enum_choice::<RedGreenBlue>().for_socket(parameter_info).property_row();
let document_node = match get_document_node(node_id, context) {
@@ -1172,7 +1174,7 @@ pub(crate) fn selective_color_properties(node_id: NodeId, context: &mut NodeProp
use graphene_std::raster::selective_color::*;
let mut default_info = ParameterWidgetsInfo::new(node_id, ColorsInput::INDEX, true, context);
default_info.exposeable = false;
default_info.exposable = false;
let colors = enum_choice::<SelectiveColorChoice>().for_socket(default_info).property_row();
let document_node = match get_document_node(node_id, context) {
@@ -1526,7 +1528,7 @@ pub(crate) fn generate_node_properties(node_id: NodeId, context: &mut NodeProper
let mut input_types = implementations
.keys()
.filter_map(|item| item.inputs.get(input_index))
.filter(|ty| property_from_type(node_id, input_index, ty, number_options, unit_suffix, display_decimal_places, step, context).is_ok())
.filter(|ty| property_from_type(node_id, input_index, ty, number_options, unit_suffix, display_decimal_places, step, true, context).is_ok())
.collect::<Vec<_>>();
input_types.sort_by_key(|ty| ty.type_name());
let input_type = input_types.first().cloned();
@@ -1540,7 +1542,7 @@ pub(crate) fn generate_node_properties(node_id: NodeId, context: &mut NodeProper
_ => context.network_interface.input_type(&InputConnector::node(node_id, input_index), context.selection_network_path).0,
};
property_from_type(node_id, input_index, &input_type, number_options, unit_suffix, display_decimal_places, step, context).unwrap_or_else(|value| value)
property_from_type(node_id, input_index, &input_type, number_options, unit_suffix, display_decimal_places, step, true, context).unwrap_or_else(|value| value)
});
layout.extend(row);
@@ -1905,6 +1907,77 @@ pub fn math_properties(node_id: NodeId, context: &mut NodePropertiesContext) ->
]
}
pub fn value_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
let Some(document_node) = context.network_interface.document_node(&node_id, context.selection_network_path) else {
log::warn!("Value properties failed to be built because its document node is invalid.");
return vec![];
};
let Some(input) = document_node.inputs.get(0) else {
log::warn!("Secondary value input could not be found on value properties");
return vec![];
};
let mut select_value_widgets = Vec::new();
select_value_widgets.push(TextLabel::new("Value type: ").tooltip("Select the type the value node should output").widget_holder());
let Some(input_value) = input.as_non_exposed_value() else {
log::error!("Primary value node input should be a hidden value input");
return Vec::new();
};
let input_type = input_value.ty();
let Some(choice) = TaggedValueChoice::from_tagged_value(input_value) else {
log::error!("Tagged value in value node should always have a choice. input: {:?}", input);
return Vec::new();
};
// let committer = || {|_| {
// let messages = vec![
// DocumentMessage::AddTransaction.into(),
// NodeGraphMessage::RunDocumentGraph.into(),
// ];
// Message::Batched(messages.into_boxed_slice()).into()
// };
let updater = || {
move |v: &TaggedValueChoice| {
let value = v.to_tagged_value();
let messages = vec![NodeGraphMessage::SetInputValue { node_id, input_index: 0, value }.into(), NodeGraphMessage::SendGraph.into()];
Message::Batched(messages.into_boxed_slice()).into()
}
};
let value_dropdown = enum_choice::<TaggedValueChoice>().dropdown_menu(choice, updater, || commit_value);
select_value_widgets.extend_from_slice(&[Separator::new(SeparatorType::Unrelated).widget_holder(), value_dropdown]);
let mut type_widgets = match property_from_type(node_id, 0, &input_type, (None, None, None), None, None, None, false, context) {
Ok(type_widgets) => type_widgets,
Err(type_widgets) => type_widgets,
};
if type_widgets.len() <= 0 {
log::error!("Could not generate type widgets for value node");
return Vec::new();
}
let LayoutGroup::Row { widgets: mut type_widgets } = type_widgets.remove(0) else {
log::error!("Could not get autogenerated widgets for value node");
return Vec::new();
};
if type_widgets.len() <= 2 {
log::error!("Could not generate type widgets for value node");
return Vec::new();
}
//Remove the name and blank assist
type_widgets.remove(0);
type_widgets.remove(0);
vec![LayoutGroup::Row { widgets: select_value_widgets }, LayoutGroup::Row { widgets: type_widgets }]
}
pub struct ParameterWidgetsInfo<'a> {
document_node: Option<&'a DocumentNode>,
node_id: NodeId,
@@ -1913,7 +1986,7 @@ pub struct ParameterWidgetsInfo<'a> {
description: String,
input_type: FrontendGraphDataType,
blank_assist: bool,
exposeable: bool,
exposable: bool,
}
impl<'a> ParameterWidgetsInfo<'a> {
@@ -1930,7 +2003,7 @@ impl<'a> ParameterWidgetsInfo<'a> {
description,
input_type,
blank_assist,
exposeable: true,
exposable: true,
}
}
}
@@ -1986,7 +2059,7 @@ pub mod choice {
todo!()
}
fn dropdown_menu<U, C>(&self, current: E, updater_factory: impl Fn() -> U, committer_factory: impl Fn() -> C) -> WidgetHolder
pub fn dropdown_menu<U, C>(&self, current: E, updater_factory: impl Fn() -> U, committer_factory: impl Fn() -> C) -> WidgetHolder
where
U: Fn(&E) -> Message + 'static + Send + Sync,
C: Fn(&()) -> Message + 'static + Send + Sync,
@@ -4273,13 +4273,23 @@ impl NodeNetworkInterface {
// Side effects
match (&old_input, &new_input) {
// If a node input is exposed or hidden reload the click targets and update the bounding box for all nodes
(NodeInput::Value { exposed: new_exposed, .. }, NodeInput::Value { exposed: old_exposed, .. }) => {
(NodeInput::Value { exposed: new_exposed, .. }, NodeInput::Value { exposed: old_exposed, tagged_value }) => {
if let InputConnector::Node { node_id, .. } = input_connector {
if new_exposed != old_exposed {
self.unload_upstream_node_click_targets(vec![*node_id], network_path);
self.unload_all_nodes_bounding_box(network_path);
}
}
// Update the name of the value node
if let InputConnector::Node { node_id, .. } = input_connector {
let Some(reference) = self.reference(node_id, network_path) else {
log::error!("Could not get reference for {:?}", node_id);
return;
};
if reference.as_deref() == Some("Value") {
self.set_display_name(node_id, format!("{:?} Value", tagged_value.ty().nested_type()), network_path);
}
}
}
(_, NodeInput::Node { node_id: upstream_node_id, .. }) => {
// Load structure if the change is to the document network and to the first or second