use std::collections::{HashMap, HashSet};
use std::io::Cursor;
use anyhow::{bail, Context, Result};
use crate::sdf::expr;
use crate::{ar, sdf, usda, usdc, usdz};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DependencyKind {
SubLayer,
Reference,
Payload,
}
impl std::fmt::Display for DependencyKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::SubLayer => write!(f, "sublayer"),
Self::Reference => write!(f, "reference"),
Self::Payload => write!(f, "payload"),
}
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error(
"failed to resolve {kind} asset: {asset_path} (referenced by {referencing_layer}{})",
"prim_path.as_ref().map(|p| format!(\" at {p}\")).unwrap_or_default()"
)]
UnresolvedAsset {
asset_path: String,
referencing_layer: String,
kind: DependencyKind,
prim_path: Option<sdf::Path>,
},
}
struct Dependency {
asset_path: String,
kind: DependencyKind,
prim_path: Option<sdf::Path>,
}
struct CollectionContext<'a> {
load_payloads: bool,
include_prim_dependency: Option<&'a dyn Fn(&sdf::Path) -> bool>,
on_error: &'a dyn Fn(Error) -> Result<()>,
}
impl<'a> CollectionContext<'a> {
fn with_filter(&self, include_prim_dependency: Option<&'a dyn Fn(&sdf::Path) -> bool>) -> Self {
Self {
load_payloads: self.load_payloads,
include_prim_dependency,
on_error: self.on_error,
}
}
}
type StrictErrorHandler = fn(Error) -> Result<()>;
fn strict_error(e: Error) -> Result<()> {
bail!("{e}")
}
pub struct Collector<'a, R: ar::Resolver, E: Fn(Error) -> Result<()> = StrictErrorHandler> {
resolver: &'a R,
load_payloads: bool,
include_prim_dependency: Option<&'a dyn Fn(&sdf::Path) -> bool>,
on_error: E,
}
impl<'a, R: ar::Resolver> Collector<'a, R, StrictErrorHandler> {
pub fn new(resolver: &'a R) -> Self {
Self {
resolver,
load_payloads: true,
include_prim_dependency: None,
on_error: strict_error,
}
}
}
impl<'a, R: ar::Resolver, E: Fn(Error) -> Result<()>> Collector<'a, R, E> {
pub fn load_payloads(mut self, load_payloads: bool) -> Self {
self.load_payloads = load_payloads;
self
}
pub fn on_error<E2: Fn(Error) -> Result<()>>(self, on_error: E2) -> Collector<'a, R, E2> {
Collector {
resolver: self.resolver,
load_payloads: self.load_payloads,
include_prim_dependency: self.include_prim_dependency,
on_error,
}
}
pub(crate) fn prim_dependency_filter(mut self, include: &'a dyn Fn(&sdf::Path) -> bool) -> Self {
self.include_prim_dependency = Some(include);
self
}
pub fn collect(&self, root_path: &str) -> Result<Vec<sdf::Layer>> {
let mut layers = Vec::new();
let mut visited = HashSet::new();
let context = CollectionContext {
load_payloads: self.load_payloads,
include_prim_dependency: self.include_prim_dependency,
on_error: &self.on_error,
};
collect_recursive(
self.resolver,
root_path,
None,
&context,
&HashMap::new(),
&mut layers,
&mut visited,
)?;
layers.reverse();
Ok(layers)
}
}
fn collect_recursive(
resolver: &impl ar::Resolver,
asset_path: &str,
anchor: Option<&ar::ResolvedPath>,
context: &CollectionContext<'_>,
ancestor_expr_vars: &HashMap<String, sdf::Value>,
layers: &mut Vec<sdf::Layer>,
visited: &mut HashSet<String>,
) -> Result<()> {
let identifier = resolver.create_identifier(asset_path, anchor);
if visited.contains(&identifier) {
return Ok(());
}
let resolved = resolver
.resolve(&identifier)
.with_context(|| format!("failed to resolve asset path: {asset_path}"))?;
visited.insert(identifier.clone());
let data = open_layer(resolver, &resolved)?;
let mut expr_vars = expr::read_expression_variables(data.as_ref())?;
expr_vars.extend(ancestor_expr_vars.iter().map(|(k, v)| (k.clone(), v.clone())));
let deps = collect_dependencies(data.as_ref(), context.load_payloads)?;
let is_usdz = resolved.extension().and_then(|e| e.to_str()) == Some("usdz");
for dep in deps {
if let (Some(include), Some(prim_path)) = (context.include_prim_dependency, dep.prim_path.as_ref()) {
if !include(prim_path) {
continue;
}
}
let dep_asset = expr::evaluate_asset_path(&dep.asset_path, &expr_vars)?;
if is_usdz {
bail!(
"cross-file references within USDZ archives are not yet supported: {}",
resolved
);
}
let dep_id = resolver.create_identifier(&dep_asset, Some(&resolved));
if !visited.contains(&dep_id) && resolver.resolve(&dep_id).is_none() {
(context.on_error)(Error::UnresolvedAsset {
asset_path: dep_asset,
referencing_layer: identifier.clone(),
kind: dep.kind,
prim_path: dep.prim_path,
})?;
visited.insert(dep_id);
continue;
}
let child_filter = match dep.kind {
DependencyKind::SubLayer => context.include_prim_dependency,
DependencyKind::Reference | DependencyKind::Payload => None,
};
collect_recursive(
resolver,
&dep_asset,
Some(&resolved),
&context.with_filter(child_filter),
&expr_vars,
layers,
visited,
)?;
}
layers.push(sdf::Layer::new(identifier, data));
Ok(())
}
fn collect_dependencies(data: &dyn sdf::AbstractData, load_payloads: bool) -> Result<Vec<Dependency>> {
let mut deps = Vec::new();
let root = sdf::Path::abs_root();
if let Some(value) = data.try_get(&root, sdf::FieldKey::SubLayers.as_str())? {
if let sdf::Value::StringVec(sub_paths) = value.into_owned() {
for asset_path in sub_paths {
deps.push(Dependency {
asset_path,
kind: DependencyKind::SubLayer,
prim_path: None,
});
}
}
}
let prim_paths = collect_prim_paths(data)?;
for prim_path in &prim_paths {
if let Some(value) = data.try_get(prim_path, sdf::FieldKey::References.as_str())? {
if let sdf::Value::ReferenceListOp(list_op) = value.as_ref() {
for r in list_op.iter().filter(|r| !r.asset_path.is_empty()) {
deps.push(Dependency {
asset_path: r.asset_path.clone(),
kind: DependencyKind::Reference,
prim_path: Some(prim_path.clone()),
});
}
}
}
if load_payloads {
if let Some(value) = data.try_get(prim_path, sdf::FieldKey::Payload.as_str())? {
match value.as_ref() {
sdf::Value::Payload(p) if !p.asset_path.is_empty() => {
deps.push(Dependency {
asset_path: p.asset_path.clone(),
kind: DependencyKind::Payload,
prim_path: Some(prim_path.clone()),
});
}
sdf::Value::PayloadListOp(list_op) => {
for p in list_op.iter().filter(|p| !p.asset_path.is_empty()) {
deps.push(Dependency {
asset_path: p.asset_path.clone(),
kind: DependencyKind::Payload,
prim_path: Some(prim_path.clone()),
});
}
}
_ => {}
}
}
}
}
Ok(deps)
}
fn collect_prim_paths(data: &dyn sdf::AbstractData) -> Result<Vec<sdf::Path>> {
let mut result = Vec::new();
let mut queue = vec![sdf::Path::abs_root()];
while let Some(path) = queue.pop() {
if !data.has_spec(&path) {
continue;
}
if path != sdf::Path::abs_root() {
result.push(path.clone());
}
if let Some(value) = data.try_get(&path, sdf::ChildrenKey::PrimChildren.as_str())? {
if let sdf::Value::TokenVec(children) = value.into_owned() {
for name in children.iter().rev() {
if let Ok(child) = path.append_path(name.as_str()) {
queue.push(child);
}
}
}
}
if let Some(value) = data.try_get(&path, sdf::ChildrenKey::VariantSetChildren.as_str())? {
if let sdf::Value::TokenVec(set_names) = value.into_owned() {
for set_name in &set_names {
let set_path = path.append_variant_selection(set_name, "");
if let Some(value) = data.try_get(&set_path, sdf::ChildrenKey::VariantChildren.as_str())? {
if let sdf::Value::TokenVec(variant_names) = value.into_owned() {
for variant_name in &variant_names {
let variant_path = path.append_variant_selection(set_name, variant_name);
queue.push(variant_path);
}
}
}
}
}
}
}
Ok(result)
}
pub fn open_layer(resolver: &dyn ar::Resolver, resolved: &ar::ResolvedPath) -> Result<sdf::LayerData> {
let ext = resolved.extension().and_then(|e| e.to_str()).unwrap_or_default();
let mut asset = resolver.open_asset(resolved)?;
let bytes = asset.read_all()?;
if ext == "usdz" {
let mut archive = usdz::Archive::from_reader(Cursor::new(bytes)).context("failed to open USDZ archive")?;
return archive
.read_first_layer()
.context("failed to read first layer from USDZ archive");
}
let is_binary = ext == "usdc" || (ext == "usd" && bytes.starts_with(usdc::MAGIC));
if is_binary {
let data = usdc::CrateData::open(Cursor::new(bytes), true).context("failed to parse USDC layer")?;
Ok(Box::new(data))
} else {
let content = String::from_utf8(bytes).context("layer is not valid UTF-8")?;
let mut parser = usda::parser::Parser::new(&content);
let data = parser.parse().context("failed to parse USDA layer")?;
Ok(Box::new(usda::TextReader::from_data(data)))
}
}
#[cfg(test)]
mod tests {
use std::cell::RefCell;
use super::*;
use crate::ar::{DefaultResolver, Resolver};
use crate::sdf::AbstractData;
const VENDOR_COMPOSITION: &str = "vendor/usd-wg-assets/test_assets/foundation/stage_composition";
fn manifest_dir() -> String {
std::env::var("CARGO_MANIFEST_DIR").unwrap()
}
fn composition_path(relative: &str) -> String {
format!("{}/{}/{}", manifest_dir(), VENDOR_COMPOSITION, relative)
}
fn fixture_path(relative: &str) -> String {
format!("{}/fixtures/{}", manifest_dir(), relative)
}
#[test]
fn expression_sublayer() -> Result<()> {
let path = fixture_path("expr_sublayer.usda");
let resolver = DefaultResolver::new();
let layers = Collector::new(&resolver).collect(&path)?;
assert_eq!(layers.len(), 2, "root + 1 expression-resolved sublayer");
assert!(layers[0].identifier.contains("expr_sublayer.usda"));
assert!(layers[1].identifier.contains("expr_sublayer_target.usda"));
Ok(())
}
#[test]
fn expression_reference() -> Result<()> {
let path = fixture_path("expr_reference.usda");
let resolver = DefaultResolver::new();
let layers = Collector::new(&resolver).collect(&path)?;
assert_eq!(layers.len(), 2, "root + 1 expression-resolved reference");
assert!(layers[1].identifier.contains("expr_sublayer_target.usda"));
Ok(())
}
#[test]
fn expression_asset_path() -> Result<()> {
let path = fixture_path("expr_asset_path.usda");
let resolver = DefaultResolver::new();
let layers = Collector::new(&resolver).collect(&path)?;
assert_eq!(layers.len(), 2, "root + 1 expression-resolved reference");
assert!(layers[0].identifier.contains("expr_asset_path.usda"));
assert!(layers[1]
.identifier
.replace('\\', "/")
.contains("expr_assets/extraAssets.usda"));
Ok(())
}
#[test]
fn expression_payload() -> Result<()> {
let path = fixture_path("expr_payload.usda");
let resolver = DefaultResolver::new();
let layers = Collector::new(&resolver).collect(&path)?;
assert_eq!(layers.len(), 2, "root + 1 expression-resolved payload");
assert!(layers[1].identifier.contains("expr_sublayer_target.usda"));
Ok(())
}
#[test]
fn sublayer_same_folder() -> Result<()> {
let path = composition_path("subLayer/sublayer_same_folder.usda");
let resolver = DefaultResolver::new();
let layers = Collector::new(&resolver).collect(&path)?;
assert_eq!(layers.len(), 2, "root + 1 sublayer");
assert!(layers[0].identifier.contains("sublayer_same_folder.usda"));
assert!(layers[1].identifier.contains("_stage.usda"));
Ok(())
}
#[test]
fn sublayer_child_folder() -> Result<()> {
let path = composition_path("subLayer/sublayer_child_folder.usda");
let resolver = DefaultResolver::new();
let layers = Collector::new(&resolver).collect(&path)?;
assert_eq!(layers.len(), 2);
assert!(layers[1].identifier.contains("_child_stage.usda"));
Ok(())
}
#[test]
fn sublayer_parent_folder() -> Result<()> {
let path = composition_path("subLayer/sublayer_parent_folder.usda");
let resolver = DefaultResolver::new();
let layers = Collector::new(&resolver).collect(&path)?;
assert_eq!(layers.len(), 2);
assert!(layers[1].identifier.contains("_parent_stage.usda"));
Ok(())
}
#[test]
fn reference_same_folder() -> Result<()> {
let path = composition_path("references/reference_same_folder.usda");
let resolver = DefaultResolver::new();
let layers = Collector::new(&resolver).collect(&path)?;
assert_eq!(layers.len(), 2, "root + 1 referenced layer");
assert!(layers[1].identifier.contains("_stage.usda"));
Ok(())
}
#[test]
fn reference_child_folder() -> Result<()> {
let path = composition_path("references/reference_child_folder.usda");
let resolver = DefaultResolver::new();
let layers = Collector::new(&resolver).collect(&path)?;
assert_eq!(layers.len(), 2);
assert!(layers[1].identifier.contains("_child_stage.usda"));
Ok(())
}
#[test]
fn reference_parent_folder() -> Result<()> {
let path = composition_path("references/reference_parent_folder.usda");
let resolver = DefaultResolver::new();
let layers = Collector::new(&resolver).collect(&path)?;
assert_eq!(layers.len(), 2);
assert!(layers[1].identifier.contains("_parent_stage.usda"));
Ok(())
}
#[test]
fn payload_same_folder() -> Result<()> {
let path = composition_path("payload/payload_same_folder.usda");
let resolver = DefaultResolver::new();
let layers = Collector::new(&resolver).collect(&path)?;
assert_eq!(layers.len(), 2, "root + 1 payload layer");
assert!(layers[1].identifier.contains("_stage.usda"));
Ok(())
}
#[test]
fn skip_payloads() -> Result<()> {
let resolver = DefaultResolver::new();
let path = composition_path("payload/payload_same_folder.usda");
let layers = Collector::new(&resolver)
.load_payloads(false)
.on_error(|_| Ok(()))
.collect(&path)?;
assert_eq!(layers.len(), 1);
assert!(layers[0].identifier.contains("payload_same_folder.usda"));
let errors = RefCell::new(0);
let path = composition_path("payload/payload_invalid.usda");
let layers = Collector::new(&resolver)
.load_payloads(false)
.on_error(|_| {
*errors.borrow_mut() += 1;
Ok(())
})
.collect(&path)?;
assert_eq!(layers.len(), 1);
assert_eq!(*errors.borrow(), 0);
Ok(())
}
#[test]
fn payload_child_folder() -> Result<()> {
let path = composition_path("payload/payload_child_folder.usda");
let resolver = DefaultResolver::new();
let layers = Collector::new(&resolver).collect(&path)?;
assert_eq!(layers.len(), 2);
assert!(layers[1].identifier.contains("_child_stage.usda"));
Ok(())
}
#[test]
fn payload_parent_folder() -> Result<()> {
let path = composition_path("payload/payload_parent_folder.usda");
let resolver = DefaultResolver::new();
let layers = Collector::new(&resolver).collect(&path)?;
assert_eq!(layers.len(), 2);
assert!(layers[1].identifier.contains("_parent_stage.usda"));
Ok(())
}
#[test]
fn teapot_multi_level() -> Result<()> {
let path = format!("{}/vendor/usd-wg-assets/full_assets/Teapot/Teapot.usd", manifest_dir());
let resolver = DefaultResolver::new();
let layers = Collector::new(&resolver).collect(&path)?;
assert!(layers.len() >= 3, "expected at least 3 layers, got {}", layers.len());
assert!(layers[0].identifier.contains("Teapot.usd"));
let ids = layers.iter().map(|l| l.identifier.as_str()).collect::<Vec<_>>();
assert!(ids.iter().any(|id| id.contains("Teapot_Payload")));
assert!(ids.iter().any(|id| id.contains("Teapot_Materials")));
Ok(())
}
#[test]
fn strict_errors_on_missing_reference() {
let path = composition_path("references/reference_invalid.usda");
let resolver = DefaultResolver::new();
assert!(Collector::new(&resolver).collect(&path).is_err());
}
#[test]
fn handler_receives_error() -> Result<()> {
let resolver = DefaultResolver::new();
let errors = RefCell::new(Vec::new());
let path = composition_path("references/reference_invalid.usda");
Collector::new(&resolver)
.on_error(|e| {
errors.borrow_mut().push(e);
Ok(())
})
.collect(&path)?;
let path = composition_path("payload/payload_invalid.usda");
Collector::new(&resolver)
.on_error(|e| {
errors.borrow_mut().push(e);
Ok(())
})
.collect(&path)?;
let path = composition_path("subLayer/sublayer_invalid.usda");
Collector::new(&resolver)
.on_error(|e| {
errors.borrow_mut().push(e);
Ok(())
})
.collect(&path)?;
let errors = errors.into_inner();
assert_eq!(errors.len(), 3);
let Error::UnresolvedAsset {
kind, ref prim_path, ..
} = errors[0];
assert_eq!(kind, DependencyKind::Reference);
assert_eq!(prim_path.as_ref().unwrap().as_str(), "/World/invalid_reference");
let Error::UnresolvedAsset {
kind, ref prim_path, ..
} = errors[1];
assert_eq!(kind, DependencyKind::Payload);
assert_eq!(prim_path.as_ref().unwrap().as_str(), "/World/invalid_payload");
let Error::UnresolvedAsset {
kind, ref prim_path, ..
} = errors[2];
assert_eq!(kind, DependencyKind::SubLayer);
assert!(prim_path.is_none());
Ok(())
}
#[test]
fn handler_can_ignore_errors() -> Result<()> {
let path = composition_path("references/reference_invalid.usda");
let resolver = DefaultResolver::new();
let layers = Collector::new(&resolver).on_error(|_| Ok(())).collect(&path)?;
assert_eq!(layers.len(), 1);
assert!(layers[0].identifier.contains("reference_invalid.usda"));
Ok(())
}
#[test]
fn filter_skips_dependency() -> Result<()> {
let path = composition_path("references/reference_invalid.usda");
let resolver = DefaultResolver::new();
let errors = RefCell::new(0);
let include = |path: &sdf::Path| path.as_str() == "/World/cube";
let layers = Collector::new(&resolver)
.prim_dependency_filter(&include)
.on_error(|_| {
*errors.borrow_mut() += 1;
Ok(())
})
.collect(&path)?;
assert_eq!(layers.len(), 1);
assert_eq!(*errors.borrow(), 0);
assert!(layers[0].identifier.contains("reference_invalid.usda"));
Ok(())
}
#[test]
fn save_dispatches_on_extension() -> Result<()> {
use crate::sdf::{self, SpecType};
let mut data = sdf::Data::new();
let root = sdf::Path::abs_root();
let ps = data.create_spec(root, SpecType::PseudoRoot);
ps.add("primChildren", sdf::Value::TokenVec(vec!["Foo".into()]));
let foo = sdf::path("/Foo")?;
let sp = data.create_spec(foo, SpecType::Prim);
sp.add("specifier", sdf::Value::Specifier(sdf::Specifier::Def));
sp.add("typeName", sdf::Value::Token("Xform".into()));
let layer = sdf::Layer::new("test://layer", Box::new(data));
let dir = tempfile::tempdir()?;
let usda_path = dir.path().join("layer-save.usda");
let usdc_path = dir.path().join("layer-save.usdc");
let usdz_path = dir.path().join("layer-save.usdz");
layer.save(&usda_path)?;
layer.save(&usdc_path)?;
layer.save(&usdz_path)?;
assert!(std::fs::metadata(&usda_path)?.len() > 0);
assert!(std::fs::metadata(&usdc_path)?.len() > 0);
assert!(std::fs::metadata(&usdz_path)?.len() > 0);
let archive = crate::usdz::Archive::open(&usdz_path)?;
let name = archive.first_layer_name().expect("usdz has a layer");
assert!(name.ends_with(".usdc"));
Ok(())
}
#[test]
fn save_as_usd_writes_binary_and_roundtrips() -> Result<()> {
use crate::sdf::{self, SpecType};
let mut data = sdf::Data::new();
let root = sdf::Path::abs_root();
let ps = data.create_spec(root, SpecType::PseudoRoot);
ps.add("primChildren", sdf::Value::TokenVec(vec!["Bar".into()]));
let bar = sdf::path("/Bar")?;
let sp = data.create_spec(bar.clone(), SpecType::Prim);
sp.add("specifier", sdf::Value::Specifier(sdf::Specifier::Def));
sp.add("typeName", sdf::Value::Token("Cube".into()));
let layer = sdf::Layer::new("test://layer-usd", Box::new(data));
let dir = tempfile::tempdir()?;
let path = dir.path().join("layer-save.usd");
layer.save(&path)?;
let bytes = std::fs::read(&path)?;
assert!(
bytes.starts_with(crate::usdc::MAGIC),
"writer should emit binary for .usd, got magic {:?}",
&bytes[..crate::usdc::MAGIC.len().min(bytes.len())],
);
let resolver = DefaultResolver::new();
let resolved = resolver.resolve(path.to_str().unwrap()).unwrap();
let round = open_layer(&resolver, &resolved)?;
assert_eq!(round.spec_type(&bar), Some(SpecType::Prim));
assert_eq!(
round.get(&bar, "typeName").unwrap().into_owned(),
sdf::Value::Token("Cube".into())
);
Ok(())
}
#[test]
fn save_rejects_unknown_extension() {
use crate::sdf::{self, SpecType};
let mut data = sdf::Data::new();
data.create_spec(sdf::Path::abs_root(), SpecType::PseudoRoot);
let layer = sdf::Layer::new("test://layer", Box::new(data));
let err = layer.save("/tmp/openusd-bad.xyz").unwrap_err();
assert!(err.to_string().contains("unsupported"));
}
#[test]
fn save_as_forces_text_to_usd_extension() -> Result<()> {
use crate::sdf::{self, SpecType};
let mut data = sdf::Data::new();
let root = sdf::Path::abs_root();
let ps = data.create_spec(root, SpecType::PseudoRoot);
ps.add("primChildren", sdf::Value::TokenVec(vec!["Text".into()]));
let prim = sdf::path("/Text")?;
let sp = data.create_spec(prim.clone(), SpecType::Prim);
sp.add("specifier", sdf::Value::Specifier(sdf::Specifier::Def));
sp.add("typeName", sdf::Value::Token("Xform".into()));
let layer = sdf::Layer::new("test://text-as-usd", Box::new(data));
let dir = tempfile::tempdir()?;
let path = dir.path().join("text-as-usd.usd");
layer.save_as(&path, sdf::LayerFormat::Usda)?;
let bytes = std::fs::read(&path)?;
assert!(
!bytes.starts_with(crate::usdc::MAGIC),
"save_as(Usda) should produce text, but output begins with USDC magic",
);
assert!(bytes.starts_with(b"#usda"), "text output must start with #usda header");
let resolver = DefaultResolver::new();
let resolved = resolver.resolve(path.to_str().unwrap()).unwrap();
let round = open_layer(&resolver, &resolved)?;
assert_eq!(round.spec_type(&prim), Some(SpecType::Prim));
assert_eq!(
round.get(&prim, "typeName").unwrap().into_owned(),
sdf::Value::Token("Xform".into())
);
Ok(())
}
#[test]
fn layer_format_from_extension_matches_spec() {
assert_eq!(sdf::LayerFormat::from_extension("usda"), Some(sdf::LayerFormat::Usda));
assert_eq!(sdf::LayerFormat::from_extension("usdc"), Some(sdf::LayerFormat::Usdc));
assert_eq!(sdf::LayerFormat::from_extension("usd"), Some(sdf::LayerFormat::Usdc));
assert_eq!(sdf::LayerFormat::from_extension("USDA"), Some(sdf::LayerFormat::Usda));
assert_eq!(sdf::LayerFormat::from_extension("usdz"), Some(sdf::LayerFormat::Usdz));
assert_eq!(sdf::LayerFormat::from_extension("xyz"), None);
assert_eq!(sdf::LayerFormat::from_extension(""), None);
}
#[test]
fn create_prim_basic() -> Result<()> {
let mut layer = sdf::Layer::new_anonymous("anon.usda");
let mut prim = layer.create_prim("/World", sdf::Specifier::Def, "Xform")?;
prim.set_kind("group");
let world = layer.prim("/World").expect("prim authored");
assert_eq!(world.type_name(), Some("Xform"));
assert_eq!(world.specifier(), Some(sdf::Specifier::Def));
assert_eq!(world.kind(), Some("group"));
Ok(())
}
#[test]
fn auto_ancestor_chain() -> Result<()> {
let mut layer = sdf::Layer::new_anonymous("anon.usda");
layer.create_prim("/A/B/C", sdf::Specifier::Def, "Mesh")?;
assert_eq!(
layer.prim("/A/B/C").and_then(|p| p.specifier()),
Some(sdf::Specifier::Def)
);
assert_eq!(
layer.prim("/A/B").and_then(|p| p.specifier()),
Some(sdf::Specifier::Over)
);
assert_eq!(layer.prim("/A").and_then(|p| p.specifier()), Some(sdf::Specifier::Over));
Ok(())
}
#[test]
fn prim_children() -> Result<()> {
let mut layer = sdf::Layer::new_anonymous("anon.usda");
layer.create_prim("/World", sdf::Specifier::Def, "Xform")?;
layer.create_prim("/World/Mesh", sdf::Specifier::Def, "Mesh")?;
layer.create_prim("/World/Cube", sdf::Specifier::Def, "Cube")?;
let root = layer.pseudo_root().expect("pseudo-root present");
assert_eq!(root.prim_children(), Some(["World".to_string()].as_slice()));
let world = layer.prim("/World").expect("prim");
assert_eq!(
world.prim_children(),
Some(["Mesh".to_string(), "Cube".to_string()].as_slice())
);
Ok(())
}
#[test]
fn property_children() -> Result<()> {
let mut layer = sdf::Layer::new_anonymous("anon.usda");
layer.create_prim("/Mesh", sdf::Specifier::Def, "Mesh")?;
layer.create_attribute("/Mesh.points", "point3f[]", sdf::Variability::Varying, false)?;
layer.create_attribute("/Mesh.normals", "normal3f[]", sdf::Variability::Varying, false)?;
layer.create_relationship("/Mesh.material:binding", sdf::Variability::Varying, false)?;
let mesh = layer.prim("/Mesh").expect("prim");
assert_eq!(
mesh.property_children(),
Some(
[
"points".to_string(),
"normals".to_string(),
"material:binding".to_string()
]
.as_slice()
)
);
Ok(())
}
#[test]
fn relationship_variability() -> Result<()> {
let mut layer = sdf::Layer::new_anonymous("anon.usda");
layer.create_relationship("/Mesh.material:binding", sdf::Variability::Uniform, false)?;
let rel = layer.relationship("/Mesh.material:binding").expect("relationship");
assert_eq!(rel.variability(), sdf::Variability::Uniform);
layer.create_relationship("/Mesh.material:binding", sdf::Variability::Varying, false)?;
let rel = layer.relationship("/Mesh.material:binding").expect("relationship");
assert_eq!(rel.variability(), sdf::Variability::Varying);
assert!(rel.get(sdf::FieldKey::Variability.as_str()).is_none());
Ok(())
}
#[test]
fn bad_prim_children_errors() {
let mut layer = sdf::Layer::new_anonymous("anon.usda");
layer
.data
.as_data_mut()
.unwrap()
.spec_mut(&sdf::Path::abs_root())
.unwrap()
.add(sdf::ChildrenKey::PrimChildren, sdf::Value::String("bad".into()));
let err = layer.create_prim("/A", sdf::Specifier::Def, "Xform").unwrap_err();
assert!(matches!(err, sdf::AuthoringError::InvalidPath { .. }));
let root = layer
.data
.as_data()
.unwrap()
.spec(&sdf::Path::abs_root())
.expect("pseudo-root present");
assert!(matches!(
root.get(sdf::ChildrenKey::PrimChildren.as_str()),
Some(sdf::Value::String(value)) if value == "bad"
));
}
#[test]
fn bad_property_children_errors() -> Result<()> {
let mut layer = sdf::Layer::new_anonymous("anon.usda");
layer.create_prim("/Mesh", sdf::Specifier::Def, "Mesh")?;
layer
.data
.as_data_mut()
.unwrap()
.spec_mut(&sdf::path("/Mesh").unwrap())
.unwrap()
.add(sdf::ChildrenKey::PropertyChildren, sdf::Value::String("bad".into()));
let err = layer
.create_relationship("/Mesh.material:binding", sdf::Variability::Varying, false)
.unwrap_err();
assert!(matches!(err, sdf::AuthoringError::InvalidPath { .. }));
let data = layer.data.as_data().unwrap();
assert!(data.spec(&sdf::path("/Mesh.material:binding").unwrap()).is_none());
let mesh = data.spec(&sdf::path("/Mesh").unwrap()).expect("prim present");
assert!(matches!(
mesh.get(sdf::ChildrenKey::PropertyChildren.as_str()),
Some(sdf::Value::String(value)) if value == "bad"
));
Ok(())
}
#[test]
fn attr_samples() -> Result<()> {
let mut layer = sdf::Layer::new_anonymous("anon.usda");
layer.create_prim("/Sphere", sdf::Specifier::Def, "Sphere")?;
let mut radius = layer.create_attribute("/Sphere.radius", "double", sdf::Variability::Varying, false)?;
radius.set_default(sdf::Value::Double(2.5));
radius.set_time_sample(0.0, sdf::Value::Double(1.0));
radius.set_time_sample(10.0, sdf::Value::Double(3.0));
radius.set_time_sample(5.0, sdf::Value::Double(2.0));
let read = layer.attribute("/Sphere.radius").expect("attr");
assert_eq!(read.type_name(), Some("double"));
assert_eq!(read.default(), Some(&sdf::Value::Double(2.5)));
let samples = read.time_samples().expect("samples authored");
let times: Vec<f64> = samples.iter().map(|(t, _)| *t).collect();
assert_eq!(times, vec![0.0, 5.0, 10.0]);
Ok(())
}
#[test]
fn wrong_ancestor_type() {
let mut layer = sdf::Layer::new_anonymous("anon.usda");
layer
.data
.as_data_mut()
.unwrap()
.create_spec(sdf::path("/A").unwrap(), sdf::SpecType::Attribute);
let err = layer.create_prim("/A/B", sdf::Specifier::Def, "Xform").unwrap_err();
assert!(matches!(err, sdf::AuthoringError::InvalidPath { .. }));
let err = layer
.create_attribute("/A.x", "double", sdf::Variability::Varying, false)
.unwrap_err();
assert!(matches!(err, sdf::AuthoringError::InvalidPath { .. }));
}
#[test]
fn failed_prim_chain_creation_is_atomic() {
let mut layer = sdf::Layer::new_anonymous("anon.usda");
let bad_child = sdf::path("/A/B").unwrap();
layer
.data
.as_data_mut()
.unwrap()
.create_spec(bad_child, sdf::SpecType::Attribute);
let err = layer.create_prim("/A/B", sdf::Specifier::Def, "Xform").unwrap_err();
assert!(matches!(err, sdf::AuthoringError::InvalidPath { .. }));
let data = layer.data.as_data().unwrap();
assert!(data.spec(&sdf::path("/A").unwrap()).is_none());
let root = data.spec(&sdf::Path::abs_root()).expect("pseudo-root present");
assert!(root.get(sdf::ChildrenKey::PrimChildren.as_str()).is_none());
}
#[test]
fn nan_time_sample() -> Result<()> {
let mut layer = sdf::Layer::new_anonymous("anon.usda");
layer.create_prim("/Sphere", sdf::Specifier::Def, "Sphere")?;
let mut r = layer.create_attribute("/Sphere.radius", "double", sdf::Variability::Varying, false)?;
r.set_time_sample(1.0, sdf::Value::Double(1.0));
r.set_time_sample(f64::NAN, sdf::Value::Double(99.0));
r.set_time_sample(2.0, sdf::Value::Double(2.0));
let samples = layer
.attribute("/Sphere.radius")
.unwrap()
.time_samples()
.unwrap()
.to_vec();
let finite: Vec<f64> = samples.iter().map(|(t, _)| *t).filter(|t| t.is_finite()).collect();
assert_eq!(finite, vec![1.0, 2.0]);
assert!(layer
.attribute_mut("/Sphere.radius")
.unwrap()
.erase_time_sample(f64::NAN));
let times: Vec<f64> = layer
.attribute("/Sphere.radius")
.unwrap()
.time_samples()
.unwrap()
.iter()
.map(|(t, _)| *t)
.collect();
assert_eq!(times, vec![1.0, 2.0]);
Ok(())
}
#[test]
fn override_prim() -> Result<()> {
let mut layer = sdf::Layer::new_anonymous("anon.usda");
layer.override_prim("/A/B")?;
assert_eq!(layer.prim("/A").and_then(|p| p.specifier()), Some(sdf::Specifier::Over));
assert_eq!(
layer.prim("/A/B").and_then(|p| p.specifier()),
Some(sdf::Specifier::Over)
);
layer.create_prim("/Defined", sdf::Specifier::Def, "Xform")?;
layer.override_prim("/Defined")?;
assert_eq!(
layer.prim("/Defined").and_then(|p| p.specifier()),
Some(sdf::Specifier::Def)
);
Ok(())
}
#[test]
fn pseudo_root_metadata() -> Result<()> {
let mut layer = sdf::Layer::new_anonymous("anon.usda");
{
let mut root = layer.pseudo_root_mut().expect("writable");
root.set_default_prim("World");
root.set_documentation("auto-generated");
root.set_start_time_code(1.0);
root.set_end_time_code(24.0);
root.add_sublayer("./over.usda");
root.add_sublayer("./over.usda");
}
let root = layer.pseudo_root().expect("present");
assert_eq!(root.default_prim(), Some("World"));
assert_eq!(root.documentation(), Some("auto-generated"));
assert_eq!(root.start_time_code(), Some(1.0));
assert_eq!(root.end_time_code(), Some(24.0));
assert_eq!(
root.sublayers(),
Some(["./over.usda".to_string(), "./over.usda".to_string()].as_slice())
);
Ok(())
}
#[test]
fn read_only_layer() {
struct ReadOnly;
impl sdf::AbstractData for ReadOnly {
fn has_spec(&self, _: &sdf::Path) -> bool {
false
}
fn has_field(&self, _: &sdf::Path, _: &str) -> bool {
false
}
fn spec_type(&self, _: &sdf::Path) -> Option<sdf::SpecType> {
None
}
fn try_get(&self, _: &sdf::Path, _: &str) -> Result<Option<std::borrow::Cow<'_, sdf::Value>>> {
Ok(None)
}
fn list(&self, _: &sdf::Path) -> Option<Vec<String>> {
None
}
fn paths(&self) -> Vec<sdf::Path> {
Vec::new()
}
}
let mut layer = sdf::Layer::new("file.usda", Box::new(ReadOnly));
let err = layer.create_prim("/World", sdf::Specifier::Def, "Xform").unwrap_err();
assert!(matches!(err, sdf::AuthoringError::ReadOnly { .. }));
}
#[test]
fn invalid_paths() {
let mut layer = sdf::Layer::new_anonymous("anon.usda");
assert!(matches!(
layer.create_prim("/", sdf::Specifier::Def, "Xform").unwrap_err(),
sdf::AuthoringError::InvalidPath { .. }
));
assert!(matches!(
layer.create_prim("/A.foo", sdf::Specifier::Def, "Xform").unwrap_err(),
sdf::AuthoringError::InvalidPath { .. }
));
assert!(matches!(
layer
.create_attribute("/A", "double", sdf::Variability::Varying, false)
.unwrap_err(),
sdf::AuthoringError::InvalidPath { .. }
));
assert!(matches!(
layer
.create_attribute("A.foo", "double", sdf::Variability::Varying, false)
.unwrap_err(),
sdf::AuthoringError::InvalidPath { .. }
));
assert!(matches!(
layer
.create_relationship("A.foo", sdf::Variability::Varying, false)
.unwrap_err(),
sdf::AuthoringError::InvalidPath { .. }
));
assert!(matches!(
layer
.create_attribute("/.foo", "double", sdf::Variability::Varying, false)
.unwrap_err(),
sdf::AuthoringError::InvalidPath { .. }
));
assert!(matches!(
layer
.create_attribute("/A.rel[/Target].attr", "double", sdf::Variability::Varying, false)
.unwrap_err(),
sdf::AuthoringError::InvalidPath { .. }
));
assert!(matches!(
layer
.create_prim("/A{x=}child", sdf::Specifier::Def, "Xform")
.unwrap_err(),
sdf::AuthoringError::InvalidPath { .. }
));
}
#[test]
fn variant_authoring() -> Result<()> {
let mut layer = sdf::Layer::new_anonymous("variants.usda");
layer.create_prim("/Prim", sdf::Specifier::Def, "Xform")?;
layer.create_prim("/Prim{set=sel}child", sdf::Specifier::Def, "Scope")?;
assert_eq!(layer.spec_type(&sdf::path("/Prim")?), Some(sdf::SpecType::Prim));
assert_eq!(
layer.spec_type(&sdf::path("/Prim{set=}")?),
Some(sdf::SpecType::VariantSet)
);
assert_eq!(
layer.spec_type(&sdf::path("/Prim{set=sel}")?),
Some(sdf::SpecType::Variant)
);
assert_eq!(
layer.spec_type(&sdf::path("/Prim{set=sel}child")?),
Some(sdf::SpecType::Prim)
);
let token_vec = |path: &str, key: sdf::ChildrenKey| -> Result<Vec<String>> {
match layer.get(&sdf::path(path)?, key.as_str())?.into_owned() {
sdf::Value::TokenVec(v) => Ok(v),
other => panic!("expected TokenVec at {path}, got {other:?}"),
}
};
assert_eq!(token_vec("/Prim", sdf::ChildrenKey::VariantSetChildren)?, vec!["set"]);
assert_eq!(
token_vec("/Prim{set=}", sdf::ChildrenKey::VariantChildren)?,
vec!["sel"]
);
assert_eq!(
token_vec("/Prim{set=sel}", sdf::ChildrenKey::PrimChildren)?,
vec!["child"]
);
Ok(())
}
#[test]
fn prim_authoring_rejects_variant_leaf() -> Result<()> {
let mut layer = sdf::Layer::new_anonymous("variants.usda");
layer.create_prim("/Prim", sdf::Specifier::Def, "Xform")?;
assert!(matches!(
layer
.create_prim("/Prim{set=sel}", sdf::Specifier::Def, "Xform")
.unwrap_err(),
sdf::AuthoringError::InvalidPath { .. }
));
assert!(matches!(
layer.override_prim("/Prim{set=sel}").unwrap_err(),
sdf::AuthoringError::InvalidPath { .. }
));
assert_eq!(layer.spec_type(&sdf::path("/Prim{set=sel}")?), None);
Ok(())
}
#[test]
fn usda_roundtrip() -> Result<()> {
let mut layer = sdf::Layer::new_anonymous("scene.usda");
layer.pseudo_root_mut().unwrap().set_default_prim("World");
layer.create_prim("/World", sdf::Specifier::Def, "Xform")?;
let mut sphere = layer.create_prim("/World/Sphere", sdf::Specifier::Def, "Sphere")?;
sphere.set_kind("component");
let mut radius = layer.create_attribute("/World/Sphere.radius", "double", sdf::Variability::Varying, false)?;
radius.set_default(sdf::Value::Double(1.5));
let material = sdf::path("/World/Material")?;
layer.create_prim(&material, sdf::Specifier::Def, "Material")?;
let mut binding =
layer.create_relationship("/World/Sphere.material:binding", sdf::Variability::Varying, false)?;
binding.add_target(material.clone());
let mut surface = layer.create_attribute(
"/World/Sphere.inputs:surface",
"token",
sdf::Variability::Varying,
false,
)?;
surface.set_connection_paths([sdf::path("/World/Material.outputs:surface")?]);
let tmp = std::env::temp_dir().join("openusd_authoring_roundtrip.usda");
layer.save_as(&tmp, sdf::LayerFormat::Usda)?;
let parsed = usda::TextReader::read(&tmp)?;
assert_eq!(parsed.spec_type(&sdf::path("/World")?), Some(sdf::SpecType::Prim));
assert_eq!(
parsed.spec_type(&sdf::path("/World/Sphere")?),
Some(sdf::SpecType::Prim)
);
assert_eq!(
parsed.spec_type(&sdf::path("/World/Sphere.radius")?),
Some(sdf::SpecType::Attribute)
);
assert_eq!(
parsed
.get(&sdf::Path::abs_root(), sdf::FieldKey::DefaultPrim.as_str())?
.into_owned(),
sdf::Value::Token("World".into())
);
match parsed
.get(
&sdf::path("/World/Sphere.material:binding")?,
sdf::FieldKey::TargetPaths.as_str(),
)?
.into_owned()
{
sdf::Value::PathListOp(op) => {
assert!(op.explicit);
assert_eq!(op.explicit_items, vec![material]);
}
other => panic!("expected relationship targets as PathListOp, got {other:?}"),
}
match parsed
.get(
&sdf::path("/World/Sphere.inputs:surface")?,
sdf::FieldKey::ConnectionPaths.as_str(),
)?
.into_owned()
{
sdf::Value::PathListOp(op) => {
assert!(op.explicit);
assert_eq!(op.explicit_items, vec![sdf::path("/World/Material.outputs:surface")?]);
}
other => panic!("expected connection paths as PathListOp, got {other:?}"),
}
let _ = std::fs::remove_file(&tmp);
Ok(())
}
}