Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ pub(super) fn post_process_nodes(custom: Vec<DocumentNodeDefinition>) -> HashMap
inputs,
call_argument: input_type.clone(),
implementation: DocumentNodeImplementation::ProtoNode(id.clone()),
visible: true,
visible: Visible::Passthrough,
skip_deduplication: false,
context_features: ContextDependencies::from(context_features.as_slice()),
..Default::default()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2571,7 +2571,7 @@ impl NodeGraphMessageHandler {
return Vec::new();
};
let mut nodes = Vec::new();
for (node_id, visible) in network.nodes.iter().map(|(node_id, node)| (*node_id, node.visible)).collect::<Vec<_>>() {
for (node_id, visible) in network.nodes.iter().map(|(node_id, node)| (*node_id, node.visible.is_visible())).collect::<Vec<_>>() {
let primary_input_connector = InputConnector::node(node_id, 0);

let primary_input = if network_interface
Expand Down Expand Up @@ -2721,7 +2721,7 @@ impl NodeGraphMessageHandler {

let parents_visible = layer.ancestors(network_interface.document_metadata()).filter(|&ancestor| ancestor != layer).all(|layer| {
if layer != LayerNodeIdentifier::ROOT_PARENT {
network_interface.document_node(&layer.to_node(), &[]).map(|node| node.visible).unwrap_or_default()
network_interface.document_node(&layer.to_node(), &[]).map(|node| node.visible.is_visible()).unwrap_or_default()
} else {
true
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,17 @@ use crate::messages::portfolio::document::node_graph::document_node_definitions:
use crate::messages::portfolio::document::node_graph::utility_types::{Direction, FrontendClickTargets, FrontendGraphDataType, FrontendGraphInput, FrontendGraphOutput};
use crate::messages::portfolio::document::overlays::utility_functions::text_width;
use crate::messages::portfolio::document::utility_types::network_interface::resolved_types::ResolvedDocumentNodeTypes;
use crate::messages::portfolio::document::utility_types::network_interface::resolved_types::TypeSource;
use crate::messages::portfolio::document::utility_types::wires::{GraphWireStyle, WirePath, WirePathUpdate, build_vector_wire};
use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::tool_messages::tool_prelude::NumberInputMode;
use deserialization::deserialize_node_persistent_metadata;
use glam::{DAffine2, DVec2, IVec2};
use graph_craft::Type;
use graph_craft::concrete;
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput, NodeNetwork, OldDocumentNodeImplementation, OldNodeNetwork};
use graph_craft::document::{HiddenNodeInput, Visible};
use graphene_std::ContextDependencies;
use graphene_std::math::quad::Quad;
use graphene_std::subpath::Subpath;
Expand Down Expand Up @@ -1049,7 +1052,7 @@ impl NodeNetworkInterface {
log::error!("Could not get node in is_visible");
return false;
};
node.visible
node.visible.is_visible()
}

pub fn is_layer(&self, node_id: &NodeId, network_path: &[NodeId]) -> bool {
Expand Down Expand Up @@ -1358,7 +1361,7 @@ impl NodeNetworkInterface {

node.inputs = old_node.inputs;
node.call_argument = old_node.manual_composition.unwrap();
node.visible = old_node.visible;
node.visible = if old_node.visible { Visible::Passthrough } else { Visible::TaggedValues(Vec::new()) };
node.skip_deduplication = old_node.skip_deduplication;
node.original_location = old_node.original_location;
node_metadata.persistent_metadata.display_name = old_node.alias;
Expand Down Expand Up @@ -4481,15 +4484,49 @@ impl NodeNetworkInterface {
}

pub fn set_visibility(&mut self, node_id: &NodeId, network_path: &[NodeId], is_visible: bool) {
let Some(network) = self.network_mut(network_path) else {
return;
};
let Some(node) = network.nodes.get_mut(node_id) else {
log::error!("Could not get node {node_id} in set_visibility");
if is_visible {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic is generally wrong. For nodes with no inputs, we want to use the downstream traversal and type lookup solution. For nodes with any number of inputs they just become pass through nodes as is the current behavior.

if let Some(network) = self.network_mut(network_path) {
if let Some(node) = network.nodes.get_mut(node_id) {
node.visible = Visible::Passthrough;
}
}
self.transaction_modified();
return;
}

let input_count = self.document_node(node_id, network_path).map_or(0, |node| node.inputs.len());
let tagged_inputs: Vec<HiddenNodeInput> = (0..input_count)
.map(|input_index| HiddenNodeInput {
input_index,
tagged_value: self.tagged_value_from_input(&InputConnector::node(*node_id, input_index), network_path),
})
.collect();
let has_non_unit_input = tagged_inputs.iter().any(|hidden_input: &HiddenNodeInput| hidden_input.tagged_value.ty() != concrete!(()));
let visibility = if has_non_unit_input {
Visible::TaggedValues(tagged_inputs)
} else {
let output_connector = OutputConnector::Node { node_id: *node_id, output_index: 0 };
let output_type = match self.output_type(&output_connector, network_path) {
TypeSource::Compiled(ty) => ty,
TypeSource::TaggedValue(ty) => ty,
TypeSource::Unknown | TypeSource::Invalid => {
log::error!("Output type unknown when hiding node");
concrete!(())
}
TypeSource::Error(e) => {
log::error!("Error retrieving output type in set_visibility: {e}");
concrete!(())
}
};
Visible::Value(output_type)
};

node.visible = is_visible;
if let Some(network) = self.network_mut(network_path) {
if let Some(node) = network.nodes.get_mut(node_id) {
node.visible = visibility;
}
}

self.transaction_modified();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ impl NodeNetworkInterface {
concrete!(())
}
};
TaggedValue::from_type_or_none(&guaranteed_type)
TaggedValue::from_type(&guaranteed_type).expect("Failed to construct TaggedValue for identity type")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can crash when used on types from old documents which are no longer handled by the macro. A log::error! with the from_type_or_none would be safer

}

/// A list of all valid input types for this specific node.
Expand Down
2 changes: 1 addition & 1 deletion node-graph/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ pub struct DocumentNode {
pub call_argument: Type,
pub implementation: DocumentNodeImplementation,
pub skip_deduplication: bool,
pub visible: bool,
pub visible: Visible,
pub original_location: OriginalLocation,
}
```
Expand Down
152 changes: 130 additions & 22 deletions node-graph/graph-craft/src/document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,32 @@ fn return_true() -> bool {
true
}

#[derive(Clone, Debug, PartialEq, Hash, serde::Serialize, serde::Deserialize)]
pub struct HiddenNodeInput {
pub input_index: usize,
pub tagged_value: TaggedValue,
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would expect this to include the NodeId so it can be matched with the downstream (toward the export) node


#[derive(Clone, Debug, PartialEq, Hash, serde::Serialize, serde::Deserialize)]
pub enum Visible {
Passthrough,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Passthrough means to use the current hiding logic, where it is replaced with a Passthrough node

Value(Type), //kept for backward compatibility
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

backward compatibility with what? This is a new enum.

TaggedValues(Vec<HiddenNodeInput>),
Copy link
Collaborator

@adamgerhant adamgerhant Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tagged values only occurs for nodes with no inputs, where valid input values must be chosen for the nodes it is connected to

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also it might be more clear to rename this to Hidden, since it is just behavior for Hidden nodes

}
impl Visible {
pub fn is_visible(&self) -> bool {
matches!(self, Visible::Passthrough)
}

pub fn is_hidden(&self) -> bool {
!self.is_visible()
}
}

fn return_visible_true() -> Visible {
Visible::Passthrough
}

/// An instance of a [`DocumentNodeDefinition`] that has been instantiated in a [`NodeNetwork`].
/// Currently, when an instance is made, it lives all on its own without any lasting connection to the definition.
/// But we will want to change it in the future so it merely references its definition.
Expand All @@ -50,8 +76,8 @@ pub struct DocumentNode {
// A nested document network or a proto-node identifier.
pub implementation: DocumentNodeImplementation,
/// Represents the eye icon for hiding/showing the node in the graph UI. When hidden, a node gets replaced with an identity node during the graph flattening step.
#[serde(default = "return_true")]
pub visible: bool,
#[serde(default = "return_visible_true")]
pub visible: Visible,
Copy link
Collaborator

@adamgerhant adamgerhant Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be an option. None means visible, Some means hidden with the appropriate handling.

/// When two different proto nodes hash to the same value (e.g. two value nodes each containing `2_u32` or two multiply nodes that have the same node IDs as input), the duplicates are removed.
/// See [`ProtoNetwork::generate_stable_node_ids`] for details.
/// However sometimes this is not desirable, for example in the case of a [`graphene_core::memo::MonitorNode`] that needs to be accessed outside of the graph.
Expand Down Expand Up @@ -94,7 +120,7 @@ impl Default for DocumentNode {
inputs: Default::default(),
call_argument: concrete!(Context),
implementation: Default::default(),
visible: true,
visible: Visible::Passthrough,
skip_deduplication: Default::default(),
original_location: OriginalLocation::default(),
context_features: Default::default(),
Expand Down Expand Up @@ -785,16 +811,84 @@ impl NodeNetwork {
return;
};

// If the node is hidden, replace it with an identity node
let identity_node = DocumentNodeImplementation::ProtoNode(graphene_core::ops::identity::IDENTIFIER);
if !node.visible && node.implementation != identity_node {
node.implementation = identity_node;
match node.visible.clone() {
Visible::Passthrough => {}

// Connect layer node to the group below
node.inputs.drain(1..);
node.call_argument = concrete!(());
self.nodes.insert(id, node);
return;
Visible::Value(output_type) => {
let matching_input = node
.inputs
.iter()
.find_map(|input| match input {
NodeInput::Import { import_type, .. } if *import_type == output_type => Some(input.clone()),
NodeInput::Value { tagged_value, .. } if tagged_value.ty() == output_type => Some(input.clone()),
_ => None,
})
.or_else(|| node.inputs.iter().find(|input| matches!(input, NodeInput::Node { .. })).cloned());

if let Some(primary) = matching_input {
node.implementation = DocumentNodeImplementation::ProtoNode(graphene_core::ops::identity::IDENTIFIER);
node.call_argument = concrete!(());
node.inputs.clear();
node.inputs.push(primary);
} else {
let tagged = TaggedValue::from_type(&output_type).unwrap_or_else(|| TaggedValue::None);

node.implementation = DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("core_types::value::ClonedNode"));
node.call_argument = concrete!(());
node.inputs.clear();
node.inputs.push(NodeInput::value(tagged, false));
}

self.nodes.insert(id, node);
return;
}

Visible::TaggedValues(tagged_values) => {
let passthrough_input = tagged_values
.iter()
.filter(|hidden_input| hidden_input.tagged_value.ty() != concrete!(()))
.find_map(|hidden_input| {
let input = node.inputs.get(hidden_input.input_index)?;
match input {
NodeInput::Node { .. } => Some(input.clone()),
NodeInput::Import { import_type, .. } if *import_type == hidden_input.tagged_value.ty() => Some(input.clone()),
NodeInput::Value { tagged_value, .. } if tagged_value.ty() == hidden_input.tagged_value.ty() => Some(input.clone()),
NodeInput::Import { .. } | NodeInput::Value { .. } | NodeInput::Scope(_) | NodeInput::Reflection(_) | NodeInput::Inline(_) => None,
}
})
.or_else(|| {
tagged_values.iter().find_map(|hidden_input| {
let input = node.inputs.get(hidden_input.input_index)?;
match input {
NodeInput::Node { .. } | NodeInput::Import { .. } | NodeInput::Value { .. } => Some(input.clone()),
NodeInput::Scope(_) | NodeInput::Reflection(_) | NodeInput::Inline(_) => None,
}
})
});

if let Some(primary) = passthrough_input {
node.implementation = DocumentNodeImplementation::ProtoNode(graphene_core::ops::identity::IDENTIFIER);
node.call_argument = concrete!(());
node.inputs.clear();
node.inputs.push(primary);
} else {
let tagged = tagged_values
.iter()
.find(|hidden_input| hidden_input.tagged_value.ty() != concrete!(()))
.or_else(|| tagged_values.first())
.map(|hidden_input| hidden_input.tagged_value.clone())
.or_else(|| TaggedValue::from_type(&node.call_argument))
.unwrap_or(TaggedValue::None);

node.implementation = DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("core_types::value::ClonedNode"));
node.call_argument = concrete!(());
node.inputs.clear();
node.inputs.push(NodeInput::value(tagged, false));
}

self.nodes.insert(id, node);
return;
}
}

let path = node.original_location.path.clone().unwrap_or_default();
Expand Down Expand Up @@ -878,21 +972,35 @@ impl NodeNetwork {

// Connect all nodes that were previously connected to this node to the nodes of the inner network
for (i, export) in inner_network.exports.into_iter().enumerate() {
if let NodeInput::Node { node_id, output_index, .. } = &export {
for deps in &node.original_location.dependants {
for dep in deps {
self.replace_node_inputs(*dep, (id, i), (*node_id, *output_index));
match export {
NodeInput::Node { node_id, output_index } => {
for deps in &node.original_location.dependants {
for dep in deps {
self.replace_node_inputs(*dep, (id, i), (node_id, output_index));
}
}
}

if let Some(new_output_node) = self.nodes.get_mut(node_id) {
for dep in &node.original_location.dependants[i] {
new_output_node.original_location.dependants[*output_index].push(*dep);
if let Some(new_output_node) = self.nodes.get_mut(&node_id) {
for dep in &node.original_location.dependants[i] {
new_output_node.original_location.dependants[output_index].push(*dep);
}
}

self.replace_network_outputs(NodeInput::node(id, i), NodeInput::node(node_id, output_index));
}
}

self.replace_network_outputs(NodeInput::node(id, i), export);
NodeInput::Import { import_index, .. } => {
let parent_input = node.inputs.get(import_index).expect("Import index must exist on parent");

self.replace_network_outputs(NodeInput::node(id, i), parent_input.clone());
}

NodeInput::Value { .. } => {
self.replace_network_outputs(NodeInput::node(id, i), export);
}

_ => {}
}
}

for node_id in new_nodes {
Expand Down
8 changes: 4 additions & 4 deletions node-graph/preprocessor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,15 +87,15 @@ pub fn generate_node_substitutions() -> HashMap<ProtoNodeIdentifier, DocumentNod
DocumentNode {
inputs,
implementation: DocumentNodeImplementation::ProtoNode(proto_node),
visible: true,
visible: Visible::Passthrough,
original_location,
..Default::default()
}
}
_ => DocumentNode {
inputs: vec![NodeInput::import(generic!(X), i)],
implementation: DocumentNodeImplementation::ProtoNode(identity_node.clone()),
visible: false,
visible: Visible::Passthrough,
..Default::default()
},
},
Expand All @@ -111,7 +111,7 @@ pub fn generate_node_substitutions() -> HashMap<ProtoNodeIdentifier, DocumentNod
inputs: network_inputs,
call_argument: input_type.clone(),
implementation: DocumentNodeImplementation::ProtoNode(id.clone()),
visible: true,
visible: Visible::Passthrough,
skip_deduplication: false,
context_features: ContextDependencies::from(metadata.context_features.as_slice()),
..Default::default()
Expand All @@ -131,7 +131,7 @@ pub fn generate_node_substitutions() -> HashMap<ProtoNodeIdentifier, DocumentNod
scope_injections: Default::default(),
generated: true,
}),
visible: true,
visible: Visible::Passthrough,
skip_deduplication: false,
..Default::default()
};
Expand Down