use openusd::Result;
use crate::SchemaError;
use openusd::sdf;
use super::connectable::{AttributeType, ConnectionSource, ShadingAttribute};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ProducerFilter {
#[default]
Any,
ShaderOutputsOnly,
}
const MAX_CONNECTION_DEPTH: usize = 256;
pub(super) fn value_producing_attributes(
attribute: ShadingAttribute,
filter: ProducerFilter,
) -> Result<Vec<ShadingAttribute>, SchemaError> {
let mut producing = Vec::new();
resolve_recursive(attribute, &mut Vec::new(), &mut producing, filter)?;
Ok(producing)
}
fn resolve_recursive(
attribute: ShadingAttribute,
chain: &mut Vec<sdf::Path>,
producing: &mut Vec<ShadingAttribute>,
filter: ProducerFilter,
) -> Result<bool, SchemaError> {
if !attribute.attribute().is_defined()? || chain.contains(attribute.path()) {
return Ok(false);
}
let sources = attribute.connected_sources()?;
let sources = distinct_sources(&sources);
let connected = !sources.is_empty();
if connected {
if chain.len() >= MAX_CONNECTION_DEPTH {
return Err(SchemaError::ConnectionDepthExceeded {
attribute: attribute.path().clone(),
max: MAX_CONNECTION_DEPTH,
});
}
chain.push(attribute.path().clone());
}
let mut found = false;
for source in sources {
found |= follow_source(source, chain, producing, filter)?;
}
if connected {
chain.pop();
}
if filter == ProducerFilter::Any && !found && attribute.attribute().resolve_info()?.has_authored_value() {
producing.push(attribute);
found = true;
}
Ok(found)
}
fn distinct_sources(connected: &super::ConnectedSources) -> Vec<&ConnectionSource> {
let mut distinct: Vec<&ConnectionSource> = Vec::new();
for source in connected.sources() {
if !distinct.iter().any(|seen| seen.source_path() == source.source_path()) {
distinct.push(source);
}
}
distinct
}
fn follow_source(
source: &ConnectionSource,
chain: &mut Vec<sdf::Path>,
producing: &mut Vec<ShadingAttribute>,
filter: ProducerFilter,
) -> Result<bool, SchemaError> {
let attribute = source.attribute();
match source.source_type() {
AttributeType::Output if !source.source_is_container() => {
producing.push(attribute);
Ok(true)
}
AttributeType::Input if !source.source_is_container() => Ok(false),
AttributeType::Input | AttributeType::Output => resolve_recursive(attribute, chain, producing, filter),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::shade::{AttributeType, Connectable, Material, NodeGraph, Shader};
use openusd::Result;
use openusd::usd;
#[test]
fn untyped_source_terminal() -> Result<(), SchemaError> {
let stage = usd::Stage::builder().in_memory("anon.usda")?;
let source = stage.override_prim("/Mat/Source")?;
let source_output = source.create_attribute("outputs:result", "float")?;
let sink = Shader::define(&stage, "/Mat/Sink")?;
sink.create_input("value", "float")?
.set_connections([source_output.path().clone()])?;
let producing = sink.input("value").value_producing_attributes(ProducerFilter::Any)?;
assert_eq!(producing.len(), 1);
assert_eq!(producing[0].path(), source_output.path());
assert_eq!(producing[0].attribute_type(), AttributeType::Output);
Ok(())
}
#[test]
fn nested_graph_resolution() -> Result<(), SchemaError> {
let stage = usd::Stage::builder().in_memory("anon.usda")?;
let source = Shader::define(&stage, "/Mat/Source")?;
let source_output = source.create_output("result", "float")?;
let inner = NodeGraph::define(&stage, "/Mat/Inner")?;
inner.create_output("result", "float")?.connect_to(&source_output)?;
let outer = NodeGraph::define(&stage, "/Mat/Outer")?;
outer
.create_output("result", "float")?
.connect_to(&inner.output("result"))?;
let sink = Shader::define(&stage, "/Mat/Sink")?;
sink.create_input("value", "float")?
.connect_to(&outer.output("result"))?;
let producing = sink.input("value").value_producing_attributes(ProducerFilter::Any)?;
assert_eq!(producing.len(), 1);
assert_eq!(producing[0].path().as_str(), "/Mat/Source.outputs:result");
assert_eq!(producing[0].attribute_type(), AttributeType::Output);
Ok(())
}
#[test]
fn interface_value_resolution() -> Result<(), SchemaError> {
let stage = usd::Stage::builder().in_memory("anon.usda")?;
let material = Material::define(&stage, "/Mat")?;
material.create_input("gain", "float")?.set(2.0_f32)?;
let graph = NodeGraph::define(&stage, "/Mat/Graph")?;
graph
.create_output("result", "float")?
.connect_to(&material.input("gain"))?;
let producing = graph.output("result").value_producing_attributes(ProducerFilter::Any)?;
assert_eq!(producing.len(), 1);
assert_eq!(producing[0].path().as_str(), "/Mat.inputs:gain");
assert_eq!(producing[0].attribute_type(), AttributeType::Input);
assert!(
graph
.output("result")
.value_producing_attributes(ProducerFilter::ShaderOutputsOnly)?
.is_empty()
);
Ok(())
}
#[test]
fn multiple_source_order() -> Result<(), SchemaError> {
let stage = usd::Stage::builder().in_memory("anon.usda")?;
let first = Shader::define(&stage, "/Mat/First")?;
let first_output = first.create_output("result", "float")?;
let second = Shader::define(&stage, "/Mat/Second")?;
let second_output = second.create_output("result", "float")?;
let first_graph = NodeGraph::define(&stage, "/Mat/FirstGraph")?;
first_graph
.create_output("result", "float")?
.connect_to(&first_output)?;
let second_graph = NodeGraph::define(&stage, "/Mat/SecondGraph")?;
second_graph
.create_output("result", "float")?
.connect_to(&second_output)?;
let root = NodeGraph::define(&stage, "/Mat/Root")?;
root.create_output("result", "float")?.set_connections([
second_graph.output("result").path().clone(),
first_graph.output("result").path().clone(),
])?;
let producing = root.output("result").value_producing_attributes(ProducerFilter::Any)?;
let paths: Vec<&str> = producing.iter().map(|attribute| attribute.path().as_str()).collect();
assert_eq!(paths, vec!["/Mat/Second.outputs:result", "/Mat/First.outputs:result"]);
Ok(())
}
#[test]
fn diamond_resolves_both() -> Result<(), SchemaError> {
let stage = usd::Stage::builder().in_memory("anon.usda")?;
let shader = Shader::define(&stage, "/Mat/Source")?;
let shader_output = shader.create_output("result", "float")?;
let shared = NodeGraph::define(&stage, "/Mat/Shared")?;
shared.create_output("result", "float")?.connect_to(&shader_output)?;
let left = NodeGraph::define(&stage, "/Mat/Left")?;
left.create_output("result", "float")?
.connect_to(&shared.output("result"))?;
let right = NodeGraph::define(&stage, "/Mat/Right")?;
right
.create_output("result", "float")?
.connect_to(&shared.output("result"))?;
let root = NodeGraph::define(&stage, "/Mat/Root")?;
root.create_output("result", "float")?.set_connections([
left.output("result").path().clone(),
right.output("result").path().clone(),
])?;
let producing = root.output("result").value_producing_attributes(ProducerFilter::Any)?;
let paths: Vec<&str> = producing.iter().map(|attribute| attribute.path().as_str()).collect();
assert_eq!(paths, vec!["/Mat/Source.outputs:result", "/Mat/Source.outputs:result"]);
Ok(())
}
#[test]
fn repeated_source_followed_once() -> Result<(), SchemaError> {
let stage = usd::Stage::builder().in_memory("anon.usda")?;
let shader = Shader::define(&stage, "/Mat/Source")?;
let shader_output = shader.create_output("result", "float")?;
let root = NodeGraph::define(&stage, "/Mat/Root")?;
root.create_output("result", "float")?
.set_connections([shader_output.path().clone(), shader_output.path().clone()])?;
let producing = root.output("result").value_producing_attributes(ProducerFilter::Any)?;
assert_eq!(producing.len(), 1);
assert_eq!(producing[0].path().as_str(), "/Mat/Source.outputs:result");
Ok(())
}
#[test]
fn deep_chain_errors() -> Result<()> {
let stage = usd::Stage::builder().in_memory("anon.usda")?;
let depth = MAX_CONNECTION_DEPTH + 2;
for hop in 0..depth {
NodeGraph::define(&stage, format!("/Mat/N{hop}"))?.create_output("result", "float")?;
}
for hop in 0..depth - 1 {
let next = stage.attribute(format!("/Mat/N{}.outputs:result", hop + 1));
stage
.attribute(format!("/Mat/N{hop}.outputs:result"))?
.set_connections([next?.path().clone()])?;
}
let head = NodeGraph::get(&stage, "/Mat/N0")?.expect("NodeGraph");
let Err(error) = head.output("result").value_producing_attributes(ProducerFilter::Any) else {
panic!("a chain past the depth budget should be refused");
};
assert!(error.to_string().contains("deeper than"), "{error}");
Ok(())
}
#[test]
fn cycle_stops() -> Result<(), SchemaError> {
let stage = usd::Stage::builder().in_memory("anon.usda")?;
let first = NodeGraph::define(&stage, "/Mat/First")?;
let second = NodeGraph::define(&stage, "/Mat/Second")?;
first.create_output("result", "float")?;
second.create_output("result", "float")?;
first.output("result").connect_to(&second.output("result"))?;
second.output("result").connect_to(&first.output("result"))?;
assert!(
first
.output("result")
.value_producing_attributes(ProducerFilter::Any)?
.is_empty()
);
Ok(())
}
}