use std::borrow::Cow;
use std::collections::HashMap;
use openusd::Result;
use openusd::usd::SchemaBase;
use openusd::{sdf, tf, usd};
use super::tokens as tok;
use super::{ImplementationSource, Shader};
pub type SdrMetadata = HashMap<tf::Token, String>;
struct SourceAttr {
implementation: ImplementationSource,
universal_name: &'static str,
suffix: &'static str,
type_name: sdf::ValueTypeName,
}
const SOURCE_ASSET: SourceAttr = SourceAttr {
implementation: ImplementationSource::SourceAsset,
universal_name: tok::A_INFO_SOURCE_ASSET,
suffix: tok::IMPL_SOURCE_SOURCE_ASSET,
type_name: sdf::ValueTypeName::ASSET,
};
const SOURCE_ASSET_SUBIDENTIFIER: SourceAttr = SourceAttr {
implementation: ImplementationSource::SourceAsset,
universal_name: tok::A_INFO_SOURCE_ASSET_SUBIDENTIFIER,
suffix: tok::SOURCE_ASSET_SUBIDENTIFIER,
type_name: sdf::ValueTypeName::TOKEN,
};
const SOURCE_CODE: SourceAttr = SourceAttr {
implementation: ImplementationSource::SourceCode,
universal_name: tok::A_INFO_SOURCE_CODE,
suffix: tok::IMPL_SOURCE_SOURCE_CODE,
type_name: sdf::ValueTypeName::STRING,
};
impl Shader {
pub fn implementation_source(&self) -> Result<ImplementationSource> {
implementation_source(self)
}
pub fn source_asset(&self, source_type: impl AsRef<str>) -> Result<Option<sdf::AssetPath>> {
source_value(self, &SOURCE_ASSET, source_type.as_ref())
}
pub fn set_source_asset(
&self,
asset: impl Into<sdf::AssetPath>,
source_type: impl AsRef<str>,
) -> Result<usd::Attribute> {
set_source_value(
self,
&SOURCE_ASSET,
source_type.as_ref(),
sdf::Value::AssetPath(asset.into()),
)
}
pub fn source_asset_subidentifier(&self, source_type: impl AsRef<str>) -> Result<Option<tf::Token>> {
source_value(self, &SOURCE_ASSET_SUBIDENTIFIER, source_type.as_ref())
}
pub fn set_source_asset_subidentifier(
&self,
subidentifier: impl Into<tf::Token>,
source_type: impl AsRef<str>,
) -> Result<usd::Attribute> {
set_source_value(
self,
&SOURCE_ASSET_SUBIDENTIFIER,
source_type.as_ref(),
sdf::Value::Token(subidentifier.into()),
)
}
pub fn source_code(&self, source_type: impl AsRef<str>) -> Result<Option<String>> {
source_value(self, &SOURCE_CODE, source_type.as_ref())
}
pub fn set_source_code(&self, source: impl Into<String>, source_type: impl AsRef<str>) -> Result<usd::Attribute> {
set_source_value(
self,
&SOURCE_CODE,
source_type.as_ref(),
sdf::Value::String(source.into()),
)
}
pub fn set_shader_id(&self, id: impl Into<tf::Token>) -> Result<usd::Attribute> {
if self.implementation_source_attr().resolve_info()?.has_authored_value() {
self.create_implementation_source_attr()?
.set(ImplementationSource::Id)?;
}
Ok(self.create_id_attr()?.set(sdf::Value::Token(id.into()))?)
}
pub fn source_types(&self) -> Result<Vec<tf::Token>> {
let suffix = match implementation_source(self)? {
ImplementationSource::Id => return Ok(Vec::new()),
ImplementationSource::SourceAsset => tok::IMPL_SOURCE_SOURCE_ASSET,
ImplementationSource::SourceCode => tok::IMPL_SOURCE_SOURCE_CODE,
};
Ok(self
.property_names()?
.into_iter()
.filter_map(|name| source_type(name.as_str(), suffix).map(tf::Token::from))
.collect())
}
pub fn sdr_metadata(&self) -> Result<SdrMetadata> {
Ok(metadata_map(self.get_metadata(tok::META_SDR_METADATA)?))
}
pub fn sdr_metadata_by_key(&self, key: impl AsRef<str>) -> Result<Option<String>> {
Ok(metadata_value(self.get_metadata(tok::META_SDR_METADATA)?, key.as_ref()))
}
pub fn has_sdr_metadata(&self) -> Result<bool> {
Ok(self.get_metadata::<sdf::Value>(tok::META_SDR_METADATA)?.is_some())
}
pub fn has_sdr_metadata_by_key(&self, key: impl AsRef<str>) -> Result<bool> {
Ok(metadata_has_key(
self.get_metadata(tok::META_SDR_METADATA)?,
key.as_ref(),
))
}
pub fn set_sdr_metadata(&self, metadata: &SdrMetadata) -> Result<(), usd::StageAuthoringError> {
self.prim()
.clone()
.update_metadata(tok::META_SDR_METADATA, |current| merge_metadata_map(current, metadata))?;
Ok(())
}
pub fn set_sdr_metadata_by_key(
&self,
key: impl Into<String>,
value: impl Into<String>,
) -> Result<(), usd::StageAuthoringError> {
let entry = [(key.into(), value.into())];
self.prim()
.clone()
.update_metadata(tok::META_SDR_METADATA, |current| merge_metadata(current, entry))?;
Ok(())
}
pub fn clear_sdr_metadata(&self) -> Result<(), usd::StageAuthoringError> {
self.prim().clone().clear_metadata(tok::META_SDR_METADATA)?;
Ok(())
}
pub fn clear_sdr_metadata_by_key(&self, key: impl AsRef<str>) -> Result<(), usd::StageAuthoringError> {
self.prim().clone().update_metadata(tok::META_SDR_METADATA, |current| {
remove_metadata_key(current, key.as_ref())
})?;
Ok(())
}
}
fn source_value<T>(prim: &usd::Prim, attr: &SourceAttr, source_type: &str) -> Result<Option<T>>
where
T: TryFrom<sdf::Value>,
T::Error: Into<openusd::Error>,
{
if implementation_source(prim)? != attr.implementation {
return Ok(None);
}
let name = source_property_name(attr, source_type);
let attribute = prim.attribute(name.as_ref());
if source_type.is_empty() || attribute.is_defined()? {
return attribute.get();
}
prim.attribute(attr.universal_name).get()
}
fn set_source_value(
shader: &Shader,
attr: &SourceAttr,
source_type: &str,
value: sdf::Value,
) -> Result<usd::Attribute> {
shader.create_implementation_source_attr()?.set(attr.implementation)?;
let name = source_property_name(attr, source_type);
Ok(shader
.create_attribute(name.as_ref(), attr.type_name.clone())?
.set_custom(false)?
.set_variability(sdf::Variability::Uniform)?
.set(value)?)
}
fn implementation_source(prim: &usd::Prim) -> Result<ImplementationSource> {
let value = prim.attribute(tok::A_INFO_IMPLEMENTATION_SOURCE).get::<sdf::Value>()?;
Ok(value
.and_then(sdf::Value::try_as_token)
.and_then(ImplementationSource::from_token)
.unwrap_or_default())
}
fn source_property_name(attr: &SourceAttr, source_type: &str) -> Cow<'static, str> {
if source_type.is_empty() {
Cow::Borrowed(attr.universal_name)
} else {
Cow::Owned(format!("{}{source_type}:{}", tok::NS_INFO, attr.suffix))
}
}
fn source_type<'a>(name: &'a str, suffix: &str) -> Option<&'a str> {
let (source_type, found) = name.strip_prefix(tok::NS_INFO)?.split_once(':')?;
(!source_type.is_empty() && found == suffix).then_some(source_type)
}
pub(super) fn metadata_map(value: Option<sdf::Value>) -> SdrMetadata {
let Some(sdf::Value::Dictionary(dictionary)) = value else {
return SdrMetadata::new();
};
dictionary
.into_iter()
.filter_map(|(key, value)| stringify(value).map(|value| (tf::Token::from(key), value)))
.collect()
}
pub(super) fn metadata_value(value: Option<sdf::Value>, key: &str) -> Option<String> {
let sdf::Value::Dictionary(mut dictionary) = value? else {
return None;
};
stringify(dictionary.remove(key)?)
}
pub(super) fn metadata_has_key(value: Option<sdf::Value>, key: &str) -> bool {
matches!(value, Some(sdf::Value::Dictionary(dictionary)) if dictionary.contains_key(key))
}
pub(super) fn merge_metadata(
current: Option<sdf::Value>,
entries: impl IntoIterator<Item = (String, String)>,
) -> Option<sdf::Value> {
let mut dictionary = match current {
Some(sdf::Value::Dictionary(dictionary)) => dictionary,
_ => HashMap::new(),
};
dictionary.extend(entries.into_iter().map(|(key, value)| (key, sdf::Value::String(value))));
(!dictionary.is_empty()).then_some(sdf::Value::Dictionary(dictionary))
}
pub(super) fn merge_metadata_map(current: Option<sdf::Value>, metadata: &SdrMetadata) -> Option<sdf::Value> {
let entries = metadata
.iter()
.map(|(key, value)| (key.as_str().to_string(), value.clone()));
merge_metadata(current, entries)
}
pub(super) fn remove_metadata_key(current: Option<sdf::Value>, key: &str) -> Option<sdf::Value> {
match current {
Some(sdf::Value::Dictionary(mut dictionary)) => {
dictionary.remove(key);
(!dictionary.is_empty()).then_some(sdf::Value::Dictionary(dictionary))
}
other => other,
}
}
pub(super) fn attribute_sdr_metadata(attribute: &usd::Attribute) -> Result<SdrMetadata> {
Ok(metadata_map(attribute.get_metadata(tok::META_SDR_METADATA)?))
}
pub(super) fn attribute_sdr_metadata_by_key(attribute: &usd::Attribute, key: &str) -> Result<Option<String>> {
Ok(metadata_value(attribute.get_metadata(tok::META_SDR_METADATA)?, key))
}
pub(super) fn attribute_has_sdr_metadata(attribute: &usd::Attribute) -> Result<bool> {
Ok(attribute.get_metadata::<sdf::Value>(tok::META_SDR_METADATA)?.is_some())
}
pub(super) fn attribute_has_sdr_metadata_by_key(attribute: &usd::Attribute, key: &str) -> Result<bool> {
Ok(metadata_has_key(attribute.get_metadata(tok::META_SDR_METADATA)?, key))
}
pub(super) fn set_attribute_sdr_metadata(
attribute: usd::Attribute,
metadata: &SdrMetadata,
) -> Result<usd::Attribute, usd::StageAuthoringError> {
attribute.update_metadata(tok::META_SDR_METADATA, |current| merge_metadata_map(current, metadata))
}
pub(super) fn set_attribute_sdr_metadata_by_key(
attribute: usd::Attribute,
key: String,
value: String,
) -> Result<usd::Attribute, usd::StageAuthoringError> {
attribute.update_metadata(tok::META_SDR_METADATA, |current| {
merge_metadata(current, [(key, value)])
})
}
pub(super) fn clear_attribute_sdr_metadata_by_key(
attribute: usd::Attribute,
key: &str,
) -> Result<usd::Attribute, usd::StageAuthoringError> {
attribute.update_metadata(tok::META_SDR_METADATA, |current| remove_metadata_key(current, key))
}
fn stringify(value: sdf::Value) -> Option<String> {
Some(match value {
sdf::Value::Bool(value) => value.to_string(),
sdf::Value::Uchar(value) => value.to_string(),
sdf::Value::Int(value) => value.to_string(),
sdf::Value::Uint(value) => value.to_string(),
sdf::Value::Int64(value) => value.to_string(),
sdf::Value::Uint64(value) => value.to_string(),
sdf::Value::Half(value) => value.to_string(),
sdf::Value::Float(value) => value.to_string(),
sdf::Value::Double(value) => value.to_string(),
other => other.cast::<String>().ok()?,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::shade::Connectable;
use openusd::Result;
fn sorted_source_types(shader: &Shader) -> Result<Vec<String>> {
let mut types = shader.source_types()?;
types.sort();
Ok(types.into_iter().map(String::from).collect())
}
fn dictionary(entries: &[(&str, &str)]) -> sdf::Value {
sdf::Value::Dictionary(
entries
.iter()
.map(|&(key, value)| (key.to_string(), sdf::Value::String(value.to_string())))
.collect(),
)
}
#[test]
fn implementation_selects_id() -> Result<()> {
let stage = usd::Stage::builder().in_memory("anon.usda")?;
let shader = Shader::define(&stage, "/Shader")?;
shader.create_id_attr()?.set(sdf::Value::token("Example"))?;
assert_eq!(shader.implementation_source()?, ImplementationSource::Id);
assert_eq!(shader.id()?.as_deref(), Some("Example"));
shader
.create_implementation_source_attr()?
.set(sdf::Value::token("invalid"))?;
assert_eq!(shader.implementation_source()?, ImplementationSource::Id);
assert_eq!(shader.id()?.as_deref(), Some("Example"));
shader
.implementation_source_attr()
.set(ImplementationSource::SourceAsset)?;
assert_eq!(shader.id()?, None);
Ok(())
}
#[test]
fn source_asset_fallback() -> Result<()> {
let stage = usd::Stage::builder().in_memory("anon.usda")?;
let shader = Shader::define(&stage, "/Shader")?;
shader
.create_implementation_source_attr()?
.set(ImplementationSource::SourceAsset)?;
shader
.create_source_asset_attr()?
.set(sdf::Value::AssetPath("./universal.osl".into()))?;
shader
.create_attribute("info:osl:sourceAsset", "asset")?
.set(sdf::Value::AssetPath("./specific.osl".into()))?;
shader.create_attribute("info:mdl:sourceAsset", "asset")?;
shader
.create_attribute("info:osl:sourceAsset:subIdentifier", "token")?
.set(sdf::Value::token("Specific"))?;
shader
.create_source_asset_subidentifier_attr()?
.set(sdf::Value::token("Universal"))?;
shader
.create_attribute("info:ri:sourceCode", "string")?
.set("inactive")?;
assert_eq!(
shader.source_asset("osl")?.expect("OSL asset").authored_path,
"./specific.osl"
);
assert_eq!(
shader.source_asset("ri")?.expect("fallback asset").authored_path,
"./universal.osl"
);
assert_eq!(
shader
.source_asset("bad type")?
.expect("malformed source type falls back")
.authored_path,
"./universal.osl"
);
assert_eq!(shader.source_asset("mdl")?, None);
assert_eq!(
shader.source_asset("")?.expect("universal asset").authored_path,
"./universal.osl"
);
assert_eq!(shader.source_asset_subidentifier("osl")?.as_deref(), Some("Specific"));
assert_eq!(shader.source_asset_subidentifier("ri")?.as_deref(), Some("Universal"));
assert_eq!(shader.source_asset_subidentifier("")?.as_deref(), Some("Universal"));
assert_eq!(sorted_source_types(&shader)?, ["mdl", "osl"]);
Ok(())
}
#[test]
fn source_code_selection() -> Result<()> {
let stage = usd::Stage::builder().in_memory("anon.usda")?;
let shader = Shader::define(&stage, "/Shader")?;
shader
.create_implementation_source_attr()?
.set(ImplementationSource::SourceCode)?;
shader.create_source_code_attr()?.set("universal")?;
shader
.create_attribute("info:osl:sourceCode", "string")?
.set("specific")?;
shader.create_attribute("info:ri:sourceCode", "string")?;
shader
.create_attribute("info:mdl:sourceAsset", "asset")?
.set(sdf::Value::AssetPath("./inactive.mdl".into()))?;
assert_eq!(shader.source_code("osl")?.as_deref(), Some("specific"));
assert_eq!(shader.source_code("ri")?, None);
assert_eq!(shader.source_code("glsl")?.as_deref(), Some("universal"));
assert_eq!(shader.source_code("")?.as_deref(), Some("universal"));
assert_eq!(shader.source_asset("mdl")?, None);
assert_eq!(sorted_source_types(&shader)?, ["osl", "ri"]);
Ok(())
}
#[test]
fn authors_source_selection() -> Result<()> {
let stage = usd::Stage::builder().in_memory("anon.usda")?;
let shader = Shader::define(&stage, "/Shader")?;
shader.set_shader_id("Example")?;
assert_eq!(shader.id()?.as_deref(), Some("Example"));
assert_eq!(shader.implementation_source_attr().get::<sdf::Value>()?, None);
shader.set_source_asset("./shader.mdl", "mdl")?;
assert_eq!(shader.implementation_source()?, ImplementationSource::SourceAsset);
assert_eq!(
shader.source_asset("mdl")?.expect("MDL asset").authored_path,
"./shader.mdl"
);
assert_eq!(shader.id()?, None);
shader.set_source_asset_subidentifier("Main", "mdl")?;
assert_eq!(shader.source_asset_subidentifier("mdl")?.as_deref(), Some("Main"));
shader.set_shader_id("Example")?;
assert_eq!(shader.implementation_source()?, ImplementationSource::Id);
assert_eq!(shader.id()?.as_deref(), Some("Example"));
shader.set_source_code("shader Example() {}", "")?;
assert_eq!(shader.implementation_source()?, ImplementationSource::SourceCode);
assert_eq!(shader.source_code("")?.as_deref(), Some("shader Example() {}"));
assert_eq!(shader.source_types()?, Vec::<tf::Token>::new());
Ok(())
}
#[test]
fn shader_id_corrects_family() -> Result<()> {
let stage = usd::Stage::builder().in_memory("root.usda")?;
let root = stage.root_layer().identifier().to_string();
let weak = sdf::Layer::new_anonymous("weak");
let weak_id = weak.identifier().to_string();
stage.insert_layer(&root, 0, weak, sdf::LayerOffset::IDENTITY)?;
stage.set_edit_target(usd::EditTarget::for_layer(&weak_id))?;
let shader = Shader::define(&stage, "/Shader")?;
shader
.create_implementation_source_attr()?
.set(ImplementationSource::SourceCode)?;
stage.set_edit_target(stage.edit_target_root())?;
shader
.create_implementation_source_attr()?
.set(ImplementationSource::Id)?;
assert_eq!(shader.implementation_source()?, ImplementationSource::Id);
stage.set_edit_target(usd::EditTarget::for_layer(&weak_id))?;
shader.set_shader_id("Example")?;
let weak = stage.layer(&weak_id).expect("weak layer").export_to_string()?;
assert!(!weak.contains(tok::IMPL_SOURCE_SOURCE_CODE));
Ok(())
}
#[test]
fn metadata_composes() -> Result<()> {
let stage = usd::Stage::builder().in_memory("root.usda")?;
let root = stage.root_layer().identifier().to_string();
let mut weak = sdf::Layer::new_anonymous("weak");
weak.edit(|edit| {
let mut shader = sdf::PrimSpec::new(edit.data_mut(), "/Shader", sdf::Specifier::Def, tok::T_SHADER)?;
shader.set(
tok::META_SDR_METADATA,
dictionary(&[("label", "weak"), ("page", "weak")]),
);
let mut input = sdf::AttributeSpec::new(
edit.data_mut(),
"/Shader.inputs:value",
"float",
sdf::Variability::Varying,
false,
)?;
input.set(tok::META_SDR_METADATA, dictionary(&[("widget", "slider")]));
Ok(())
})?;
stage.insert_layer(&root, 0, weak, sdf::LayerOffset::IDENTITY)?;
let shader = Shader::define(&stage, "/Shader")?;
let mut strong_metadata = dictionary(&[("page", "strong")])
.try_as_dictionary()
.expect("dictionary helper result");
strong_metadata.insert("count".to_string(), sdf::Value::Int(7));
shader
.prim()
.clone()
.set_metadata(tok::META_SDR_METADATA, sdf::Value::Dictionary(strong_metadata))?;
let input = shader.create_input("value", "float")?;
let output = shader.create_output("result", "float")?;
output
.clone()
.into_attribute()
.set_metadata(tok::META_SDR_METADATA, dictionary(&[("role", "result")]))?;
let metadata = shader.sdr_metadata()?;
assert_eq!(
metadata.get(&tf::Token::from("label")).map(String::as_str),
Some("weak")
);
assert_eq!(
metadata.get(&tf::Token::from("page")).map(String::as_str),
Some("strong")
);
assert_eq!(metadata.get(&tf::Token::from("count")).map(String::as_str), Some("7"));
assert_eq!(shader.sdr_metadata_by_key("page")?.as_deref(), Some("strong"));
assert_eq!(shader.sdr_metadata_by_key("count")?.as_deref(), Some("7"));
assert!(shader.has_sdr_metadata()?);
assert!(shader.has_sdr_metadata_by_key("label")?);
assert!(shader.has_sdr_metadata_by_key("count")?);
assert!(!shader.has_sdr_metadata_by_key("absent")?);
assert_eq!(input.sdr_metadata_by_key("widget")?.as_deref(), Some("slider"));
assert!(input.has_sdr_metadata()?);
assert_eq!(output.sdr_metadata_by_key("role")?.as_deref(), Some("result"));
assert!(output.has_sdr_metadata_by_key("role")?);
Ok(())
}
#[test]
fn metadata_unauthored() -> Result<()> {
let stage = usd::Stage::builder().in_memory("anon.usda")?;
let shader = Shader::define(&stage, "/Shader")?;
let input = shader.create_input("value", "float")?;
assert!(!shader.has_sdr_metadata()?);
assert!(!shader.has_sdr_metadata_by_key("label")?);
assert!(shader.sdr_metadata()?.is_empty());
assert_eq!(shader.sdr_metadata_by_key("label")?, None);
assert!(!input.has_sdr_metadata()?);
assert!(input.sdr_metadata()?.is_empty());
Ok(())
}
#[test]
fn authors_metadata_entries() -> Result<()> {
let stage = usd::Stage::builder().in_memory("anon.usda")?;
let shader = Shader::define(&stage, "/Shader")?;
shader.set_sdr_metadata_by_key("label", "Diffuse")?;
shader.set_sdr_metadata_by_key("page", "Basic")?;
assert_eq!(shader.sdr_metadata_by_key("label")?.as_deref(), Some("Diffuse"));
assert_eq!(shader.sdr_metadata_by_key("page")?.as_deref(), Some("Basic"));
shader.set_sdr_metadata(&SdrMetadata::from([(tf::Token::from("role"), "surface".to_string())]))?;
assert_eq!(shader.sdr_metadata()?.len(), 3);
let empty = Shader::define(&stage, "/Empty")?;
empty.set_sdr_metadata(&SdrMetadata::new())?;
assert!(!empty.has_sdr_metadata()?);
shader.clear_sdr_metadata_by_key("page")?;
assert_eq!(shader.sdr_metadata_by_key("page")?, None);
assert!(shader.has_sdr_metadata_by_key("label")?);
shader.clear_sdr_metadata()?;
assert!(!shader.has_sdr_metadata()?);
let input = shader.create_input("value", "float")?;
let input = input.set_sdr_metadata_by_key("widget", "slider")?;
assert_eq!(input.sdr_metadata_by_key("widget")?.as_deref(), Some("slider"));
let input = input.clear_sdr_metadata_by_key("widget")?;
assert!(!input.has_sdr_metadata()?);
Ok(())
}
#[test]
fn clearing_key_uncovers_weaker() -> Result<()> {
let stage = usd::Stage::builder().in_memory("root.usda")?;
let root = stage.root_layer().identifier().to_string();
let mut weak = sdf::Layer::new_anonymous("weak");
weak.edit(|edit| {
let mut shader = sdf::PrimSpec::new(edit.data_mut(), "/Shader", sdf::Specifier::Def, tok::T_SHADER)?;
shader.set(tok::META_SDR_METADATA, dictionary(&[("label", "weak")]));
Ok(())
})?;
stage.insert_layer(&root, 0, weak, sdf::LayerOffset::IDENTITY)?;
let shader = Shader::define(&stage, "/Shader")?;
shader.set_sdr_metadata_by_key("label", "strong")?;
assert_eq!(shader.sdr_metadata_by_key("label")?.as_deref(), Some("strong"));
shader.clear_sdr_metadata_by_key("label")?;
assert_eq!(shader.sdr_metadata_by_key("label")?.as_deref(), Some("weak"));
Ok(())
}
}