use std::collections::hash_map::Entry;
use std::collections::HashMap;
use anyhow::Result;
use crate::sdf::{Path, Value};
use crate::usd::{is_collection_api_path, Collection, MembershipQuery, Prim, Relationship, Stage};
use super::impl_shade_schema;
use super::tokens::{
API_MATERIAL_BINDING, META_BIND_MATERIAL_AS, PURPOSE_ALL, REL_MATERIAL_BINDING, REL_MATERIAL_BINDING_COLLECTION,
};
use super::BindingStrength;
use crate::schemas::common::get_with_api;
#[derive(Clone, derive_more::Deref)]
pub struct MaterialBindingAPI(Prim);
impl MaterialBindingAPI {
pub fn apply(stage: &Stage, path: impl Into<Path>) -> Result<Self> {
Ok(Self(
stage.override_prim(path)?.add_applied_schema(API_MATERIAL_BINDING)?,
))
}
pub fn get(stage: &Stage, path: impl Into<Path>) -> Result<Option<Self>> {
get_with_api(stage, path, &[API_MATERIAL_BINDING]).map(|o| o.map(Self))
}
pub fn bind(&self, material: impl Into<Path>) -> Result<&Self> {
self.bind_for_purpose(PURPOSE_ALL, material, BindingStrength::WeakerThanDescendants)
}
pub fn bind_for_purpose(
&self,
purpose: &str,
material: impl Into<Path>,
strength: BindingStrength,
) -> Result<&Self> {
let rel = self
.0
.author_relationship_targets(&direct_binding_rel(purpose), [material.into()])?;
apply_binding_strength(self.stage(), rel, strength)?;
Ok(self)
}
pub fn bind_collection(
&self,
binding_name: &str,
collection: impl Into<Path>,
material: impl Into<Path>,
purpose: &str,
strength: BindingStrength,
) -> Result<&Self> {
let rel = self.0.author_relationship_targets(
&collection_binding_rel(purpose, binding_name),
[collection.into(), material.into()],
)?;
apply_binding_strength(self.stage(), rel, strength)?;
Ok(self)
}
pub fn direct_binding(&self, purpose: &str) -> Result<Option<Path>> {
direct_binding(self.stage(), self.path(), purpose)
}
pub fn collection_binding(&self, binding_name: &str, purpose: &str) -> Result<Option<(Path, Path)>> {
let rel = self
.path()
.append_property(&collection_binding_rel(purpose, binding_name))?;
let targets = self.stage().relationship_at(rel).targets()?;
Ok(match targets.as_slice() {
[collection, material] => Some((collection.clone(), material.clone())),
_ => None,
})
}
pub fn binding_strength(&self, purpose: &str) -> Result<BindingStrength> {
binding_strength(self.stage(), self.path(), purpose)
}
pub fn compute_bound_material(&self, purpose: &str) -> Result<Option<Path>> {
let prim = self.path();
let mut cache: HashMap<Path, Option<MembershipQuery>> = HashMap::new();
for pur in purpose_fallbacks(purpose) {
if let Some(material) = bound_material_for_single_purpose(self.stage(), prim, pur, &mut cache)? {
return Ok(Some(material));
}
}
Ok(None)
}
}
impl_shade_schema!(single_api MaterialBindingAPI);
fn direct_binding_rel(purpose: &str) -> String {
if purpose == PURPOSE_ALL {
REL_MATERIAL_BINDING.to_string()
} else {
format!("{REL_MATERIAL_BINDING}:{purpose}")
}
}
fn collection_binding_rel(purpose: &str, name: &str) -> String {
if purpose == PURPOSE_ALL {
format!("{REL_MATERIAL_BINDING_COLLECTION}:{name}")
} else {
format!("{REL_MATERIAL_BINDING_COLLECTION}:{purpose}:{name}")
}
}
fn apply_binding_strength(stage: &Stage, rel: Relationship, strength: BindingStrength) -> Result<()> {
let needs_write = strength != BindingStrength::WeakerThanDescendants
|| composed_strength(stage, rel.path())? != BindingStrength::WeakerThanDescendants;
if needs_write {
rel.set_metadata(META_BIND_MATERIAL_AS, strength)?;
}
Ok(())
}
fn composed_strength(stage: &Stage, rel: &Path) -> Result<BindingStrength> {
Ok(match stage.field::<Value>(rel.clone(), META_BIND_MATERIAL_AS)? {
Some(Value::Token(t)) => BindingStrength::from_token(&t).unwrap_or_default(),
_ => BindingStrength::default(),
})
}
fn direct_binding(stage: &Stage, prim: &Path, purpose: &str) -> Result<Option<Path>> {
let rel = prim.append_property(&direct_binding_rel(purpose))?;
Ok(stage.relationship_at(rel).targets()?.into_iter().next())
}
fn binding_strength(stage: &Stage, prim: &Path, purpose: &str) -> Result<BindingStrength> {
let rel = prim.append_property(&direct_binding_rel(purpose))?;
composed_strength(stage, &rel)
}
fn bound_material_for_single_purpose(
stage: &Stage,
prim: &Path,
purpose: &str,
cache: &mut HashMap<Path, Option<MembershipQuery>>,
) -> Result<Option<Path>> {
let mut winner: Option<Path> = None;
let mut current = Some(prim.clone());
while let Some(p) = current {
if !p.is_abs_root() {
if let Some((material, strength)) = winning_binding_at(stage, &p, prim, purpose, cache)? {
if winner.is_none() || strength == BindingStrength::StrongerThanDescendants {
winner = Some(material);
}
}
}
current = p.parent();
}
Ok(winner)
}
fn winning_binding_at(
stage: &Stage,
p: &Path,
queried: &Path,
purpose: &str,
cache: &mut HashMap<Path, Option<MembershipQuery>>,
) -> Result<Option<(Path, BindingStrength)>> {
for (collection, material, strength) in collection_bindings_on(stage, p, purpose)? {
if is_collection_member(stage, &collection, queried, cache)? {
return Ok(Some((material, strength)));
}
}
if let Some(material) = direct_binding(stage, p, purpose)? {
return Ok(Some((material, binding_strength(stage, p, purpose)?)));
}
Ok(None)
}
fn collection_bindings_on(stage: &Stage, p: &Path, purpose: &str) -> Result<Vec<(Path, Path, BindingStrength)>> {
let prefix = format!("{REL_MATERIAL_BINDING_COLLECTION}:");
let mut out = Vec::new();
for name in stage.prim_at(p.clone()).property_names()? {
let Some(rest) = name.strip_prefix(&prefix) else {
continue;
};
let binding_purpose = match rest.split(':').collect::<Vec<_>>().as_slice() {
[pur, _name] => *pur,
_ => PURPOSE_ALL,
};
if binding_purpose != purpose {
continue;
}
let rel = p.append_property(&name)?;
if let [collection, material] = stage.relationship_at(rel.clone()).targets()?.as_slice() {
out.push((collection.clone(), material.clone(), composed_strength(stage, &rel)?));
}
}
Ok(out)
}
fn is_collection_member(
stage: &Stage,
collection_path: &Path,
queried: &Path,
cache: &mut HashMap<Path, Option<MembershipQuery>>,
) -> Result<bool> {
let query = match cache.entry(collection_path.clone()) {
Entry::Occupied(e) => e.into_mut(),
Entry::Vacant(e) => {
let query = match is_collection_api_path(collection_path) {
Some((prim, name)) => Some(Collection::new(prim, name).compute_membership_query(stage)?),
None => None,
};
e.insert(query)
}
};
Ok(query.as_ref().is_some_and(|q| q.is_path_included(queried)))
}
fn purpose_fallbacks(purpose: &str) -> Vec<&str> {
if purpose == PURPOSE_ALL {
vec![PURPOSE_ALL]
} else {
vec![purpose, PURPOSE_ALL]
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sdf;
#[test]
fn direct_all_purpose_roundtrip() -> Result<()> {
let stage = Stage::builder().in_memory("anon.usda")?;
stage.define_prim("/World/Mesh")?.set_type_name("Mesh")?;
stage.define_prim("/World/Mat")?.set_type_name("Material")?;
MaterialBindingAPI::apply(&stage, sdf::path("/World/Mesh")?)?.bind(sdf::path("/World/Mat")?)?;
let binding = MaterialBindingAPI::get(&stage, "/World/Mesh")?.expect("MaterialBindingAPI");
assert_eq!(
binding.direct_binding("")?.map(|p| p.as_str().to_string()),
Some("/World/Mat".to_string())
);
assert_eq!(binding.binding_strength("")?, BindingStrength::WeakerThanDescendants);
Ok(())
}
#[test]
fn purpose_binding_with_strength() -> Result<()> {
let stage = Stage::builder().in_memory("anon.usda")?;
stage.define_prim("/Mesh")?.set_type_name("Mesh")?;
stage.define_prim("/Mat")?.set_type_name("Material")?;
MaterialBindingAPI::apply(&stage, sdf::path("/Mesh")?)?.bind_for_purpose(
"preview",
sdf::path("/Mat")?,
BindingStrength::StrongerThanDescendants,
)?;
let binding = MaterialBindingAPI::get(&stage, "/Mesh")?.expect("MaterialBindingAPI");
assert_eq!(
binding.direct_binding("preview")?.map(|p| p.as_str().to_string()),
Some("/Mat".to_string())
);
assert_eq!(
binding.binding_strength("preview")?,
BindingStrength::StrongerThanDescendants
);
assert!(binding.direct_binding("")?.is_none());
Ok(())
}
#[test]
fn rebind_overrides_strength() -> Result<()> {
let stage = Stage::builder().in_memory("anon.usda")?;
stage.define_prim("/Mesh")?.set_type_name("Mesh")?;
let binding = MaterialBindingAPI::apply(&stage, sdf::path("/Mesh")?)?;
binding.bind_for_purpose("", sdf::path("/MatA")?, BindingStrength::StrongerThanDescendants)?;
binding.bind_for_purpose("", sdf::path("/MatB")?, BindingStrength::WeakerThanDescendants)?;
assert_eq!(binding.binding_strength("")?, BindingStrength::WeakerThanDescendants);
Ok(())
}
#[test]
fn collection_binding_roundtrip() -> Result<()> {
let stage = Stage::builder().in_memory("anon.usda")?;
stage.define_prim("/Set")?.set_type_name("Xform")?;
stage.define_prim("/Set/Mat")?.set_type_name("Material")?;
MaterialBindingAPI::apply(&stage, sdf::path("/Set")?)?.bind_collection(
"metalBits",
sdf::path("/Set.collection:metal")?,
sdf::path("/Set/Mat")?,
"",
BindingStrength::WeakerThanDescendants,
)?;
let binding = MaterialBindingAPI::get(&stage, "/Set")?.expect("MaterialBindingAPI");
let (collection, material) = binding
.collection_binding("metalBits", "")?
.expect("collection binding");
assert_eq!(collection.as_str(), "/Set.collection:metal");
assert_eq!(material.as_str(), "/Set/Mat");
Ok(())
}
fn bound(stage: &Stage, prim: &str, purpose: &str) -> Option<String> {
MaterialBindingAPI(stage.prim_at(sdf::path(prim).unwrap()))
.compute_bound_material(purpose)
.unwrap()
.map(|p| p.as_str().to_string())
}
#[test]
fn closer_binding_wins() -> Result<()> {
let stage = Stage::builder().in_memory("anon.usda")?;
stage.define_prim("/Set")?.set_type_name("Xform")?;
stage.define_prim("/Set/Mesh")?.set_type_name("Mesh")?;
MaterialBindingAPI::apply(&stage, sdf::path("/Set")?)?.bind(sdf::path("/MatA")?)?;
assert_eq!(bound(&stage, "/Set/Mesh", ""), Some("/MatA".to_string()));
MaterialBindingAPI::apply(&stage, sdf::path("/Set/Mesh")?)?.bind(sdf::path("/MatB")?)?;
assert_eq!(bound(&stage, "/Set/Mesh", ""), Some("/MatB".to_string()));
assert_eq!(bound(&stage, "/Set", ""), Some("/MatA".to_string()));
assert_eq!(bound(&stage, "/Other", ""), None);
Ok(())
}
#[test]
fn stronger_ancestor_wins() -> Result<()> {
let stage = Stage::builder().in_memory("anon.usda")?;
stage.define_prim("/Set")?.set_type_name("Xform")?;
stage.define_prim("/Set/Mesh")?.set_type_name("Mesh")?;
MaterialBindingAPI::apply(&stage, sdf::path("/Set")?)?.bind_for_purpose(
"",
sdf::path("/MatStrong")?,
BindingStrength::StrongerThanDescendants,
)?;
MaterialBindingAPI::apply(&stage, sdf::path("/Set/Mesh")?)?.bind(sdf::path("/MatWeak")?)?;
assert_eq!(bound(&stage, "/Set/Mesh", ""), Some("/MatStrong".to_string()));
Ok(())
}
#[test]
fn restricted_purpose_preferred() -> Result<()> {
let stage = Stage::builder().in_memory("anon.usda")?;
stage.define_prim("/Mesh")?.set_type_name("Mesh")?;
let binding = MaterialBindingAPI::apply(&stage, sdf::path("/Mesh")?)?;
binding.bind(sdf::path("/MatAll")?)?; binding.bind_for_purpose(
"preview",
sdf::path("/MatPreview")?,
BindingStrength::WeakerThanDescendants,
)?;
assert_eq!(bound(&stage, "/Mesh", "preview"), Some("/MatPreview".to_string()));
assert_eq!(bound(&stage, "/Mesh", "full"), Some("/MatAll".to_string()));
Ok(())
}
#[test]
fn restricted_ancestor_wins() -> Result<()> {
let stage = Stage::builder().in_memory("anon.usda")?;
stage.define_prim("/Set")?.set_type_name("Xform")?;
stage.define_prim("/Set/Mesh")?.set_type_name("Mesh")?;
MaterialBindingAPI::apply(&stage, sdf::path("/Set")?)?.bind_for_purpose(
"preview",
sdf::path("/MatPreview")?,
BindingStrength::WeakerThanDescendants,
)?;
MaterialBindingAPI::apply(&stage, sdf::path("/Set/Mesh")?)?.bind(sdf::path("/MatAll")?)?;
assert_eq!(bound(&stage, "/Set/Mesh", "preview"), Some("/MatPreview".to_string()));
assert_eq!(bound(&stage, "/Set/Mesh", ""), Some("/MatAll".to_string()));
Ok(())
}
fn collection_scene() -> Result<Stage> {
let stage = Stage::builder().in_memory("anon.usda")?;
stage.define_prim("/Set")?.set_type_name("Xform")?;
stage.define_prim("/Set/A")?.set_type_name("Mesh")?;
stage.define_prim("/Set/B")?.set_type_name("Mesh")?;
let coll = crate::usd::apply_collection(&stage, sdf::path("/Set")?, "metal")?;
coll.include_path(&stage, sdf::path("/Set/A")?)?;
let binding = MaterialBindingAPI::apply(&stage, sdf::path("/Set")?)?;
binding.bind(sdf::path("/MatDir")?)?;
binding.bind_collection(
"metalBits",
sdf::path("/Set.collection:metal")?,
sdf::path("/MatMetal")?,
"",
BindingStrength::WeakerThanDescendants,
)?;
Ok(stage)
}
#[test]
fn collection_beats_direct() -> Result<()> {
let stage = collection_scene()?;
assert_eq!(bound(&stage, "/Set/A", ""), Some("/MatMetal".to_string()));
assert_eq!(bound(&stage, "/Set/B", ""), Some("/MatDir".to_string()));
Ok(())
}
#[test]
fn collection_native_order() -> Result<()> {
let stage = Stage::builder().in_memory("anon.usda")?;
stage.define_prim("/Set")?.set_type_name("Xform")?;
stage.define_prim("/Set/A")?.set_type_name("Mesh")?;
for c in ["first", "second"] {
let coll = crate::usd::apply_collection(&stage, sdf::path("/Set")?, c)?;
coll.include_path(&stage, sdf::path("/Set/A")?)?;
}
let binding = MaterialBindingAPI::apply(&stage, sdf::path("/Set")?)?;
binding.bind_collection(
"aaa",
sdf::path("/Set.collection:second")?,
sdf::path("/MatSecond")?,
"",
BindingStrength::WeakerThanDescendants,
)?;
binding.bind_collection(
"zzz",
sdf::path("/Set.collection:first")?,
sdf::path("/MatFirst")?,
"",
BindingStrength::WeakerThanDescendants,
)?;
assert_eq!(bound(&stage, "/Set/A", ""), Some("/MatSecond".to_string()));
Ok(())
}
}