use openusd::Result;
use crate::common::is_any_typed;
use openusd::{sdf, tf, usd};
use super::tokens::{NS_INPUTS, NS_OUTPUTS, T_MATERIAL, T_NODE_GRAPH};
use super::{Input, Output};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AttributeType {
Input,
Output,
}
impl AttributeType {
pub const fn prefix(self) -> &'static str {
match self {
AttributeType::Input => NS_INPUTS,
AttributeType::Output => NS_OUTPUTS,
}
}
}
pub trait ConnectionTarget {
fn target_path(&self) -> &sdf::Path;
}
#[derive(Clone)]
pub enum ShadingAttribute {
Input(Input),
Output(Output),
}
impl ShadingAttribute {
pub fn attribute(&self) -> &usd::Attribute {
match self {
ShadingAttribute::Input(input) => input.attribute(),
ShadingAttribute::Output(output) => output.attribute(),
}
}
pub fn into_attribute(self) -> usd::Attribute {
match self {
ShadingAttribute::Input(input) => input.into_attribute(),
ShadingAttribute::Output(output) => output.into_attribute(),
}
}
pub const fn attribute_type(&self) -> AttributeType {
match self {
ShadingAttribute::Input(_) => AttributeType::Input,
ShadingAttribute::Output(_) => AttributeType::Output,
}
}
pub fn path(&self) -> &sdf::Path {
self.attribute().path()
}
pub fn connected_sources(&self) -> Result<ConnectedSources> {
connected_sources(self.attribute())
}
}
impl ConnectionTarget for ShadingAttribute {
fn target_path(&self) -> &sdf::Path {
self.path()
}
}
#[derive(Clone)]
pub struct ConnectionSource {
source_prim: usd::Prim,
source_path: sdf::Path,
source_name: tf::Token,
source_type: AttributeType,
type_name: Option<sdf::ValueTypeName>,
source_is_container: bool,
}
impl ConnectionSource {
pub fn source_prim(&self) -> &usd::Prim {
&self.source_prim
}
pub fn source_path(&self) -> &sdf::Path {
&self.source_path
}
pub fn source_name(&self) -> &tf::Token {
&self.source_name
}
pub fn full_name(&self) -> &str {
self.source_path
.split_property()
.expect("a ConnectionSource always holds a property path")
.1
}
pub const fn source_type(&self) -> AttributeType {
self.source_type
}
pub fn type_name(&self) -> Option<&sdf::ValueTypeName> {
self.type_name.as_ref()
}
pub fn attribute(&self) -> ShadingAttribute {
let attribute = self.source_prim.attribute(self.full_name());
match self.source_type {
AttributeType::Input => ShadingAttribute::Input(Input::new(attribute)),
AttributeType::Output => ShadingAttribute::Output(Output::new(attribute)),
}
}
pub(super) const fn source_is_container(&self) -> bool {
self.source_is_container
}
}
#[derive(Clone, Default)]
pub struct ConnectedSources {
sources: Vec<ConnectionSource>,
invalid_source_paths: Vec<sdf::Path>,
}
impl ConnectedSources {
pub fn sources(&self) -> &[ConnectionSource] {
&self.sources
}
pub fn invalid_source_paths(&self) -> &[sdf::Path] {
&self.invalid_source_paths
}
pub fn into_sources(self) -> Vec<ConnectionSource> {
self.sources
}
}
pub(super) fn connected_sources(attribute: &usd::Attribute) -> Result<ConnectedSources> {
let mut result = ConnectedSources::default();
for source_path in attribute.connections()? {
let Some((source_prim_path, full_name)) = source_path.split_property() else {
result.invalid_source_paths.push(source_path);
continue;
};
let Some((source_name, source_type)) = base_name_and_type(full_name) else {
result.invalid_source_paths.push(source_path);
continue;
};
let Some((source_prim, source_is_container)) = source_prim(attribute.stage(), &source_prim_path)? else {
result.invalid_source_paths.push(source_path);
continue;
};
let source_attribute = attribute.stage().attribute(source_path.clone())?;
if !source_attribute.is_defined()? {
result.invalid_source_paths.push(source_path);
continue;
}
let source_name = tf::Token::from(source_name);
result.sources.push(ConnectionSource {
source_prim,
source_path,
source_name,
source_type,
type_name: source_attribute.type_name()?,
source_is_container,
});
}
Ok(result)
}
pub(super) fn input_name(base: &str) -> String {
format!("{NS_INPUTS}{base}")
}
pub(super) fn output_name(base: &str) -> String {
format!("{NS_OUTPUTS}{base}")
}
pub fn base_name(full_name: &str) -> &str {
base_name_and_type(full_name).map_or(full_name, |(name, _)| name)
}
pub fn base_name_and_type(full_name: &str) -> Option<(&str, AttributeType)> {
full_name
.strip_prefix(NS_INPUTS)
.map(|name| (name, AttributeType::Input))
.or_else(|| {
full_name
.strip_prefix(NS_OUTPUTS)
.map(|name| (name, AttributeType::Output))
})
}
fn source_prim(stage: &usd::Stage, path: &sdf::Path) -> Result<Option<(usd::Prim, bool)>> {
let prim = stage.prim(path.clone())?;
if !prim.is_valid()? {
return Ok(None);
}
let source_is_container = is_container(&prim)?;
Ok(Some((prim, source_is_container)))
}
pub(super) fn is_container(prim: &usd::Prim) -> Result<bool> {
is_any_typed(prim, &[T_NODE_GRAPH, T_MATERIAL])
}
pub(super) fn authored_inputs(prim: &usd::Prim) -> Result<Vec<Input>> {
Ok(prim
.authored_attributes()?
.into_iter()
.filter_map(Input::from_attribute)
.collect())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::shade::{Connectable, Shader};
use openusd::Result;
#[test]
fn structured_sources() -> Result<()> {
let stage = usd::Stage::builder().in_memory("anon.usda")?;
let source = Shader::define(&stage, "/Mat/Source")?;
let output = source.create_output("rgb", "float3")?;
let sink = Shader::define(&stage, "/Mat/Sink")?;
sink.create_input("color", "color3f")?.connect_to(&output)?;
let connected = sink.input("color").connected_sources()?;
assert!(connected.invalid_source_paths().is_empty());
let source = connected.sources().first().expect("connected source");
assert_eq!(source.source_prim().path().as_str(), "/Mat/Source");
assert_eq!(source.source_path().as_str(), "/Mat/Source.outputs:rgb");
assert_eq!(source.source_name().as_str(), "rgb");
assert_eq!(source.full_name(), "outputs:rgb");
assert_eq!(source.source_type(), AttributeType::Output);
assert_eq!(source.type_name(), Some(&sdf::ValueTypeName::FLOAT3));
Ok(())
}
#[test]
fn untyped_source_valid() -> Result<()> {
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")?;
let source_output = Output::from_attribute(source_output).expect("an outputs: attribute");
sink.create_input("value", "float")?.connect_to(&source_output)?;
let connected = sink.input("value").connected_sources()?;
assert!(connected.invalid_source_paths().is_empty());
assert_eq!(connected.sources().len(), 1);
assert_eq!(connected.sources()[0].source_prim().path(), source.path());
Ok(())
}
#[test]
fn invalid_sources_reported() -> Result<()> {
let stage = usd::Stage::builder().in_memory("anon.usda")?;
let valid = Shader::define(&stage, "/Mat/Valid")?;
let valid_output = valid.create_output("result", "float")?;
let missing_output = Shader::define(&stage, "/Mat/MissingOutput")?;
let plain = stage.define_prim("/Mat/Plain")?.set_type_name("Scope")?;
plain.create_attribute("result", "float")?;
let sink = Shader::define(&stage, "/Mat/Sink")?;
sink.create_input("value", "float")?.set_connections([
sdf::path("/Missing.outputs:result")?,
sdf::path("/Mat/Plain.result")?,
missing_output.path().append_property("outputs:result")?,
valid_output.path().clone(),
])?;
let connected = sink.input("value").connected_sources()?;
assert_eq!(connected.sources().len(), 1);
assert_eq!(connected.sources()[0].source_path(), valid_output.path());
let invalid: Vec<&str> = connected.invalid_source_paths().iter().map(sdf::Path::as_str).collect();
assert_eq!(
invalid,
vec![
"/Missing.outputs:result",
"/Mat/Plain.result",
"/Mat/MissingOutput.outputs:result"
]
);
Ok(())
}
#[test]
fn material_is_container() -> Result<()> {
let stage = usd::Stage::builder().in_memory("anon.usda")?;
let material = crate::shade::Material::define(&stage, "/Mat")?;
material.create_input("gain", "float")?;
let shader = Shader::define(&stage, "/Mat/Shader")?;
shader
.create_input("value", "float")?
.connect_to(&material.input("gain"))?;
let connected = shader.input("value").connected_sources()?;
let source = connected.sources().first().expect("connected source");
assert_eq!(source.source_type(), AttributeType::Input);
assert!(source.source_is_container(), "a Material is a NodeGraph container");
Ok(())
}
}