use std::collections::HashMap;
use std::io::Cursor;
use std::sync::atomic::{AtomicU64, Ordering};
use anyhow::{Context, Result};
use crate::ar;
use super::schema::FieldKey;
use super::{
sink, AbstractData, AttributeSpecMut, AttributeSpecRef, ChangeList, CowData, Data, DataError, LayerData, Patch,
Path, PrimSpecMut, PrimSpecRef, PseudoRootSpecMut, PseudoRootSpecRef, RelationshipSpecMut, RelationshipSpecRef,
RelocateList, SpecError, SpecType, Value,
};
pub type LayerSinkId = sink::Id<dyn LayerSink>;
const ANONYMOUS_PREFIX: &str = "anon:";
static ANONYMOUS_COUNTER: AtomicU64 = AtomicU64::new(0);
static NEXT_GENERATION: AtomicU64 = AtomicU64::new(0);
pub struct Layer {
pub identifier: String,
real_path: Option<String>,
data: CowData<LayerData>,
changes: ChangeList,
sinks: sink::Set<dyn LayerSink>,
}
impl Layer {
pub(crate) fn new(identifier: impl Into<String>, data: LayerData) -> Self {
Self::build(identifier.into(), None, data)
}
pub(crate) fn new_resolved(identifier: impl Into<String>, real_path: &ar::ResolvedPath, data: LayerData) -> Self {
let identifier = identifier.into();
let real_path = real_path.to_string();
let real_path = (real_path != identifier).then_some(real_path);
Self::build(identifier, real_path, data)
}
fn build(identifier: String, real_path: Option<String>, data: LayerData) -> Self {
Self {
identifier,
real_path,
data: CowData::new(data),
changes: ChangeList::new(),
sinks: sink::Set::default(),
}
}
pub fn add_sink<S: LayerSink + 'static>(&mut self, sink: S) -> LayerSinkId {
self.sinks.add(Box::new(sink))
}
pub fn remove_sink(&mut self, id: LayerSinkId) {
self.sinks.remove(id);
}
pub fn data(&self) -> &dyn AbstractData {
&self.data
}
fn rollback(&mut self) {
self.data.rollback();
}
pub fn edit(
&mut self,
f: impl FnOnce(&mut LayerEdit<'_>) -> Result<(), AuthoringError>,
) -> Result<bool, EditError> {
edit_layers(&mut [self], |edits| f(&mut edits[0]).map_err(EditError::from))
}
fn prepare_commit(&mut self, generation: u64) -> Result<(), sink::Error> {
self.changes.update(&self.data);
if !self.changes.is_empty() && !self.sinks.is_empty() {
let change = PendingLayerChange {
layer_identifier: &self.identifier,
base: &**self.data.base(),
overlay: self.data.overlay(),
change_list: &self.changes,
generation,
};
self.sinks.iter().try_for_each(|sink| sink.before_commit(&change))?;
}
Ok(())
}
fn commit_data(&mut self) -> bool {
self.data.commit();
!self.changes.is_empty()
}
fn notify_after_commit(&self) {
if !self.changes.is_empty() {
for sink in self.sinks.iter() {
sink.after_commit(&self.identifier, &self.changes);
}
}
}
pub fn is_dirty(&self) -> bool {
!self.data.is_empty()
}
pub(crate) fn discard_changes(&mut self) {
self.data.commit();
}
pub fn identifier(&self) -> &str {
&self.identifier
}
pub(crate) fn real_path(&self) -> &str {
self.real_path.as_deref().unwrap_or(&self.identifier)
}
}
impl Layer {
pub fn export(&self, filename: impl AsRef<str>) -> Result<()> {
let filename = filename.as_ref();
let ext = std::path::Path::new(filename)
.extension()
.and_then(|e| e.to_str())
.unwrap_or_default();
let format = super::LayerRegistry::find_by_extension(ext).ok_or_else(|| match ext {
"" => anyhow::anyhow!("layer path {filename} has no extension; cannot choose format"),
other => anyhow::anyhow!("unsupported layer extension {other:?} (expected usda/usdc/usd/usdz)"),
})?;
if !format.caps().can_write() {
anyhow::bail!("file format {} does not support writing", format.format_id());
}
let mut file = std::fs::File::create(filename).with_context(|| format!("failed to create {filename}"))?;
format.write(self.data(), &mut file)
}
pub fn export_to_string(&self) -> Result<String> {
let format = super::LayerRegistry::find_by_id("usda").expect("usda is a built-in format");
let mut buf = Cursor::new(Vec::new());
format.write(self.data(), &mut buf)?;
String::from_utf8(buf.into_inner()).context("usda writer produced invalid UTF-8")
}
pub fn save(&self) -> Result<()> {
anyhow::ensure!(!self.is_anonymous(), "cannot save anonymous layer {}", self.identifier);
let path = std::path::Path::new(&self.identifier);
anyhow::ensure!(
path.is_absolute(),
"cannot save layer {}: identifier is not an absolute file path; use Layer::export(path) to choose a destination",
self.identifier
);
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or_default();
if let Some(format) = super::LayerRegistry::find_by_extension(ext) {
anyhow::ensure!(
format.caps().can_edit(),
"cannot save layer {} in place: the {} format is not editable; use Layer::export(path) to write a new copy",
self.identifier,
format.format_id()
);
}
self.export(&self.identifier)
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum AuthoringError {
#[error(transparent)]
Spec(#[from] SpecError),
#[error(transparent)]
Data(#[from] DataError),
#[error("path {path} is not valid here: {reason}")]
InvalidPath {
path: Path,
reason: &'static str,
},
}
#[derive(Debug, thiserror::Error)]
pub enum EditError {
#[error(transparent)]
Author(#[from] AuthoringError),
#[error(transparent)]
Rejected(#[from] sink::Error),
}
pub trait LayerSink {
fn before_commit(&self, change: &PendingLayerChange<'_>) -> Result<(), sink::Error> {
let _ = change;
Ok(())
}
fn after_commit(&self, layer: &str, changes: &ChangeList) {
let _ = (layer, changes);
}
}
impl<F: Fn(&str, &ChangeList)> LayerSink for F {
fn after_commit(&self, layer: &str, changes: &ChangeList) {
self(layer, changes);
}
}
pub struct PendingLayerChange<'a> {
pub layer_identifier: &'a str,
pub base: &'a dyn AbstractData,
pub overlay: &'a HashMap<Path, Patch>,
pub change_list: &'a ChangeList,
pub generation: u64,
}
impl Layer {
pub fn new_anonymous(tag: impl std::fmt::Display) -> Self {
let n = ANONYMOUS_COUNTER.fetch_add(1, Ordering::Relaxed);
Self::new_in_memory(format!("{ANONYMOUS_PREFIX}{n}:{tag}"))
}
pub(crate) fn new_in_memory(identifier: impl Into<String>) -> Self {
let mut data = Data::new();
data.create_spec(Path::abs_root(), SpecType::PseudoRoot);
Self::new(identifier, Box::new(data))
}
pub fn is_anonymous(&self) -> bool {
Self::is_anonymous_identifier(&self.identifier)
}
pub fn is_anonymous_identifier(identifier: &str) -> bool {
identifier.starts_with(ANONYMOUS_PREFIX)
}
pub fn prim(&self, path: impl Into<Path>) -> Option<PrimSpecRef<'_>> {
PrimSpecRef::get(self.data(), path.into())
}
pub fn attribute(&self, path: impl Into<Path>) -> Option<AttributeSpecRef<'_>> {
AttributeSpecRef::get(self.data(), path.into())
}
pub fn relationship(&self, path: impl Into<Path>) -> Option<RelationshipSpecRef<'_>> {
RelationshipSpecRef::get(self.data(), path.into())
}
pub fn pseudo_root(&self) -> Option<PseudoRootSpecRef<'_>> {
PseudoRootSpecRef::get(self.data())
}
pub fn relocates(&self) -> RelocateList {
self.pseudo_root().and_then(|root| root.relocates()).unwrap_or_default()
}
pub fn has_relocates(&self) -> bool {
self.has_root_field(FieldKey::LayerRelocates)
}
pub fn start_time_code(&self) -> f64 {
self.pseudo_root()
.and_then(|root| root.start_time_code())
.unwrap_or(0.0)
}
pub fn has_start_time_code(&self) -> bool {
self.has_root_field(FieldKey::StartTimeCode)
}
pub fn end_time_code(&self) -> f64 {
self.pseudo_root().and_then(|root| root.end_time_code()).unwrap_or(0.0)
}
pub fn has_end_time_code(&self) -> bool {
self.has_root_field(FieldKey::EndTimeCode)
}
pub fn time_codes_per_second(&self) -> f64 {
let root = self.pseudo_root();
root.as_ref()
.and_then(|root| root.time_codes_per_second())
.or_else(|| root.as_ref().and_then(|root| root.frames_per_second()))
.unwrap_or(24.0)
}
pub fn has_time_codes_per_second(&self) -> bool {
self.has_root_field(FieldKey::TimeCodesPerSecond)
}
pub fn frames_per_second(&self) -> f64 {
self.pseudo_root()
.and_then(|root| root.frames_per_second())
.unwrap_or(24.0)
}
pub fn has_frames_per_second(&self) -> bool {
self.has_root_field(FieldKey::FramesPerSecond)
}
pub fn frame_precision(&self) -> i32 {
self.pseudo_root().and_then(|root| root.frame_precision()).unwrap_or(3)
}
pub fn has_frame_precision(&self) -> bool {
self.has_root_field(FieldKey::FramePrecision)
}
fn has_root_field(&self, key: FieldKey) -> bool {
self.data.has_field(&Path::abs_root(), key.as_str())
}
}
pub struct LayerEdit<'a> {
layer: &'a mut Layer,
}
impl std::ops::Deref for LayerEdit<'_> {
type Target = Layer;
fn deref(&self) -> &Layer {
&*self.layer
}
}
impl LayerEdit<'_> {
pub fn data_mut(&mut self) -> &mut dyn AbstractData {
&mut self.layer.data
}
pub fn remove_spec(&mut self, path: &Path) -> Result<bool, AuthoringError> {
super::spec::remove_spec(self.data_mut(), path)
}
pub fn prim_mut(&mut self, path: impl Into<Path>) -> Option<PrimSpecMut<'_>> {
PrimSpecMut::get(self.data_mut(), path.into())
}
pub fn attribute_mut(&mut self, path: impl Into<Path>) -> Option<AttributeSpecMut<'_>> {
AttributeSpecMut::get(self.data_mut(), path.into())
}
pub fn relationship_mut(&mut self, path: impl Into<Path>) -> Option<RelationshipSpecMut<'_>> {
RelationshipSpecMut::get(self.data_mut(), path.into())
}
pub fn pseudo_root_mut(&mut self) -> Result<PseudoRootSpecMut<'_>, AuthoringError> {
let root = Path::abs_root();
match self.layer.data().spec_type(&root) {
Some(SpecType::PseudoRoot) => {}
Some(_) => {
return Err(AuthoringError::InvalidPath {
path: root,
reason: "root spec exists with non-PseudoRoot SpecType",
})
}
None => self.data_mut().create_spec(root, SpecType::PseudoRoot),
}
Ok(PseudoRootSpecMut::get(self.data_mut()).expect("just ensured a pseudo-root spec"))
}
pub fn set_default_prim(&mut self, name: impl Into<String>) -> Result<(), AuthoringError> {
let name = name.into();
if name.is_empty() || name.starts_with('/') || Path::new(&format!("/{name}")).is_err() {
return Err(AuthoringError::InvalidPath {
path: Path::abs_root(),
reason: "defaultPrim must be a relative prim identifier or nested prim path",
});
}
self.pseudo_root_mut()?.set_default_prim(name);
Ok(())
}
pub fn clear_default_prim(&mut self) -> Result<(), AuthoringError> {
self.clear_root_field(FieldKey::DefaultPrim)
}
pub fn set_relocates(&mut self, relocates: RelocateList) -> Result<(), AuthoringError> {
self.pseudo_root_mut()?.set_relocates(relocates);
Ok(())
}
pub fn clear_relocates(&mut self) -> Result<(), AuthoringError> {
self.clear_root_field(FieldKey::LayerRelocates)
}
pub fn set_expression_variables(&mut self, vars: HashMap<String, Value>) -> Result<(), AuthoringError> {
self.pseudo_root_mut()?.set_expression_variables(vars);
Ok(())
}
pub fn clear_expression_variables(&mut self) -> Result<(), AuthoringError> {
self.clear_root_field(FieldKey::ExpressionVariables)
}
pub fn set_start_time_code(&mut self, time: f64) -> Result<(), AuthoringError> {
self.pseudo_root_mut()?.set_start_time_code(time);
Ok(())
}
pub fn clear_start_time_code(&mut self) -> Result<(), AuthoringError> {
self.clear_root_field(FieldKey::StartTimeCode)
}
pub fn set_end_time_code(&mut self, time: f64) -> Result<(), AuthoringError> {
self.pseudo_root_mut()?.set_end_time_code(time);
Ok(())
}
pub fn clear_end_time_code(&mut self) -> Result<(), AuthoringError> {
self.clear_root_field(FieldKey::EndTimeCode)
}
pub fn set_time_codes_per_second(&mut self, rate: f64) -> Result<(), AuthoringError> {
self.pseudo_root_mut()?.set_time_codes_per_second(rate);
Ok(())
}
pub fn clear_time_codes_per_second(&mut self) -> Result<(), AuthoringError> {
self.clear_root_field(FieldKey::TimeCodesPerSecond)
}
pub fn set_frames_per_second(&mut self, rate: f64) -> Result<(), AuthoringError> {
self.pseudo_root_mut()?.set_frames_per_second(rate);
Ok(())
}
pub fn clear_frames_per_second(&mut self) -> Result<(), AuthoringError> {
self.clear_root_field(FieldKey::FramesPerSecond)
}
pub fn set_frame_precision(&mut self, precision: i32) -> Result<(), AuthoringError> {
self.pseudo_root_mut()?.set_frame_precision(precision);
Ok(())
}
pub fn clear_frame_precision(&mut self) -> Result<(), AuthoringError> {
self.clear_root_field(FieldKey::FramePrecision)
}
fn clear_root_field(&mut self, key: FieldKey) -> Result<(), AuthoringError> {
self.data_mut().erase_field(&Path::abs_root(), key.as_str());
Ok(())
}
}
impl std::fmt::Debug for Layer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Layer")
.field("identifier", &self.identifier)
.finish_non_exhaustive()
}
}
pub(crate) fn edit_layers<E: From<sink::Error>>(
layers: &mut [&mut Layer],
f: impl FnOnce(&mut [LayerEdit<'_>]) -> Result<(), E>,
) -> Result<bool, E> {
let mut guard = GroupEditGuard {
layers,
committed: false,
};
{
let mut edits: Vec<LayerEdit<'_>> = guard.layers.iter_mut().map(|layer| LayerEdit { layer }).collect();
f(&mut edits)?;
}
let generation = NEXT_GENERATION.fetch_add(1, Ordering::Relaxed);
for layer in guard.layers.iter_mut() {
layer.prepare_commit(generation)?;
}
let mut changed = false;
for layer in guard.layers.iter_mut() {
changed |= layer.commit_data();
}
guard.committed = true;
for layer in guard.layers.iter() {
layer.notify_after_commit();
}
Ok(changed)
}
pub(crate) fn dry_run_layers<E>(
layers: &mut [&mut Layer],
f: impl FnOnce(&mut [LayerEdit<'_>]) -> Result<(), E>,
) -> Result<(), E> {
let guard = GroupEditGuard {
layers,
committed: false,
};
let mut edits: Vec<LayerEdit<'_>> = guard.layers.iter_mut().map(|layer| LayerEdit { layer }).collect();
f(&mut edits)
}
struct GroupEditGuard<'a, 'b> {
layers: &'b mut [&'a mut Layer],
committed: bool,
}
impl Drop for GroupEditGuard<'_, '_> {
fn drop(&mut self) {
if self.committed {
return;
}
for layer in self.layers.iter_mut() {
layer.rollback();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sdf;
use crate::usda;
fn edit_layer(layer: &mut Layer, f: impl FnOnce(&mut LayerEdit<'_>)) {
layer
.edit(|e| {
f(e);
Ok(())
})
.expect("authored");
}
#[test]
fn anonymous_identifiers_unique() {
let a = Layer::new_anonymous("scene.usda");
let b = Layer::new_anonymous("scene.usda");
assert_ne!(a.identifier(), b.identifier());
assert!(a.identifier().ends_with(":scene.usda"));
assert!(a.is_anonymous() && b.is_anonymous());
assert!(!Layer::new_in_memory("scene.usda").is_anonymous());
}
#[test]
fn relocates_round_trip() {
let mut layer = Layer::new_anonymous("test.usda");
assert!(!layer.has_relocates());
assert!(layer.relocates().is_empty());
let pairs = vec![
(Path::from("/Rig/Model"), Path::from("/Group/Model")),
(Path::from("/Rig/Dead"), Path::from("")),
];
edit_layer(&mut layer, |e| {
e.set_relocates(pairs.clone()).unwrap();
});
assert!(layer.has_relocates());
assert_eq!(layer.relocates(), pairs);
}
#[test]
fn empty_opinion_vs_cleared() {
let mut layer = Layer::new_anonymous("test.usda");
edit_layer(&mut layer, |e| {
e.set_relocates(RelocateList::new()).unwrap();
});
assert!(layer.has_relocates());
assert!(layer.relocates().is_empty());
edit_layer(&mut layer, |e| {
e.clear_relocates().unwrap();
});
assert!(!layer.has_relocates());
assert!(layer.relocates().is_empty());
}
#[test]
fn time_code_round_trip() {
let mut layer = Layer::new_anonymous("test.usda");
assert!(!layer.has_start_time_code());
assert_eq!(layer.start_time_code(), 0.0);
assert_eq!(layer.end_time_code(), 0.0);
assert_eq!(layer.frame_precision(), 3);
edit_layer(&mut layer, |e| {
e.set_start_time_code(1.0).unwrap();
e.set_end_time_code(48.0).unwrap();
e.set_frame_precision(6).unwrap();
});
assert!(layer.has_start_time_code());
assert_eq!(layer.start_time_code(), 1.0);
assert_eq!(layer.end_time_code(), 48.0);
assert_eq!(layer.frame_precision(), 6);
}
#[test]
fn tcps_fps_fallback() {
let mut layer = Layer::new_anonymous("test.usda");
assert_eq!(layer.time_codes_per_second(), 24.0);
assert_eq!(layer.frames_per_second(), 24.0);
edit_layer(&mut layer, |e| {
e.set_frames_per_second(30.0).unwrap();
});
assert_eq!(layer.time_codes_per_second(), 30.0);
edit_layer(&mut layer, |e| {
e.set_time_codes_per_second(48.0).unwrap();
});
assert_eq!(layer.time_codes_per_second(), 48.0);
}
#[test]
fn clear_time_code() {
let mut layer = Layer::new_anonymous("test.usda");
edit_layer(&mut layer, |e| {
e.set_start_time_code(5.0).unwrap();
});
assert!(layer.has_start_time_code());
edit_layer(&mut layer, |e| {
e.clear_start_time_code().unwrap();
});
assert!(!layer.has_start_time_code());
assert_eq!(layer.start_time_code(), 0.0);
}
#[test]
fn edit_commits() {
use crate::sdf::{PrimSpec, Specifier};
let mut layer = Layer::new_anonymous("test.usda");
let changed = layer
.edit(|l| {
PrimSpec::new(l.data_mut(), "/World", Specifier::Def, "Xform")?;
Ok(())
})
.expect("no error or veto");
assert!(changed);
assert!(layer.prim(Path::from("/World")).is_some());
}
#[test]
fn edit_rolls_back_on_error() {
use crate::sdf::{PrimSpec, Specifier};
let mut layer = Layer::new_anonymous("test.usda");
let result: Result<bool, EditError> = layer.edit(|l| {
PrimSpec::new(l.data_mut(), "/A", Specifier::Def, "Xform")?;
Err(AuthoringError::InvalidPath {
path: Path::abs_root(),
reason: "test",
})
});
assert!(result.is_err());
assert!(layer.prim(Path::from("/A")).is_none(), "the staged edit rolled back");
assert!(!layer.is_dirty());
}
#[test]
fn edit_recovers_on_panic() {
use crate::sdf::{PrimSpec, Specifier};
use std::panic::{catch_unwind, AssertUnwindSafe};
let mut layer = Layer::new_anonymous("test.usda");
let panicked = catch_unwind(AssertUnwindSafe(|| {
let _: Result<bool, EditError> = layer.edit(|l| {
PrimSpec::new(l.data_mut(), "/A", Specifier::Def, "Xform").expect("authored");
panic!("boom");
});
}));
assert!(panicked.is_err(), "the panic propagates");
assert!(layer.prim(Path::from("/A")).is_none(), "the staged edit rolled back");
assert!(!layer.is_dirty(), "no staged edits or recorded changes remain");
let changed = layer
.edit(|l| {
PrimSpec::new(l.data_mut(), "/B", Specifier::Def, "Xform")?;
Ok(())
})
.expect("no error");
assert!(changed);
assert!(layer.prim(Path::from("/B")).is_some());
}
#[test]
fn group_commit_atomic_on_panic() {
use crate::sdf::{PrimSpec, Specifier};
use std::panic::{catch_unwind, AssertUnwindSafe};
let mut a = Layer::new_anonymous("a.usda");
let mut b = Layer::new_anonymous("b.usda");
a.add_sink(|_: &str, _: &ChangeList| panic!("boom"));
let panicked = catch_unwind(AssertUnwindSafe(|| {
let _ = edit_layers(&mut [&mut a, &mut b], |edits| {
PrimSpec::new(edits[0].data_mut(), "/A", Specifier::Def, "Xform").expect("authored a");
PrimSpec::new(edits[1].data_mut(), "/B", Specifier::Def, "Xform").expect("authored b");
Ok::<(), EditError>(())
});
}));
assert!(panicked.is_err(), "the after_commit panic propagates");
assert!(
a.prim(Path::from("/A")).is_some(),
"layer a committed despite the panic"
);
assert!(
b.prim(Path::from("/B")).is_some(),
"layer b committed despite the panic"
);
}
#[test]
fn discard_changes_keeps_content() {
use crate::sdf::{PrimSpec, Specifier};
let mut layer = Layer::new_anonymous("test.usda");
edit_layer(&mut layer, |e| {
PrimSpec::new(e.data_mut(), "/World", Specifier::Def, "Xform").unwrap();
});
layer.discard_changes();
assert!(!layer.is_dirty());
assert!(
layer.prim(Path::from("/World")).is_some(),
"the authored content survives"
);
}
#[test]
fn metadata_on_rootless_backend() {
let root = Path::abs_root();
let default_prim = FieldKey::DefaultPrim.as_str();
let mut layer = Layer::new("rootless.usda", Box::new(Data::new()));
edit_layer(&mut layer, |e| {
e.set_default_prim("World").unwrap();
});
assert!(layer.data().has_field(&root, default_prim));
edit_layer(&mut layer, |e| {
e.clear_default_prim().unwrap();
});
assert!(!layer.data().has_field(&root, default_prim));
}
#[derive(Default)]
struct Recorder {
before: std::cell::Cell<u32>,
after: std::cell::Cell<u32>,
veto: bool,
}
impl LayerSink for std::rc::Rc<Recorder> {
fn before_commit(&self, _change: &PendingLayerChange<'_>) -> Result<(), sink::Error> {
self.before.set(self.before.get() + 1);
if self.veto {
return Err(sink::Error::new("vetoed"));
}
Ok(())
}
fn after_commit(&self, _layer: &str, _changes: &ChangeList) {
self.after.set(self.after.get() + 1);
}
}
#[test]
fn layer_sink_fires() {
use crate::sdf::{PrimSpec, Specifier};
let mut layer = Layer::new_anonymous("test.usda");
let rec = std::rc::Rc::new(Recorder::default());
layer.add_sink(rec.clone());
layer
.edit(|l| {
PrimSpec::new(l.data_mut(), "/World", Specifier::Def, "Xform")?;
Ok(())
})
.expect("sink accepts");
assert_eq!((rec.before.get(), rec.after.get()), (1, 1));
assert!(layer.prim(Path::from("/World")).is_some());
}
#[test]
fn layer_sink_veto_rolls_back() {
use crate::sdf::{PrimSpec, Specifier};
let mut layer = Layer::new_anonymous("test.usda");
let rec = std::rc::Rc::new(Recorder {
veto: true,
..Default::default()
});
layer.add_sink(rec.clone());
let result = layer.edit(|l| {
PrimSpec::new(l.data_mut(), "/World", Specifier::Def, "Xform")?;
Ok(())
});
assert!(matches!(result, Err(EditError::Rejected(_))), "the sink vetoes");
assert_eq!((rec.before.get(), rec.after.get()), (1, 0), "after_commit never fired");
assert!(layer.prim(Path::from("/World")).is_none(), "the edit rolled back");
assert!(!layer.is_dirty());
}
#[test]
fn layer_sink_removed() {
use crate::sdf::{PrimSpec, Specifier};
let mut layer = Layer::new_anonymous("test.usda");
let rec = std::rc::Rc::new(Recorder::default());
let id = layer.add_sink(rec.clone());
layer.remove_sink(id);
layer
.edit(|l| {
PrimSpec::new(l.data_mut(), "/World", Specifier::Def, "Xform")?;
Ok(())
})
.expect("no sink");
assert_eq!((rec.before.get(), rec.after.get()), (0, 0));
}
#[test]
fn export_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.export(usda_path.to_str().unwrap())?;
layer.export(usdc_path.to_str().unwrap())?;
layer.export(usdz_path.to_str().unwrap())?;
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 export_usd_writes_binary() -> 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.export(path.to_str().unwrap())?;
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 round = sdf::LayerRegistry::default()
.open(path.to_str().unwrap())?
.expect("the layer opens");
assert_eq!(round.spec_type(&bar), Some(SpecType::Prim));
assert_eq!(
round.get_field(&bar, "typeName").unwrap().into_owned(),
sdf::Value::Token("Cube".into())
);
Ok(())
}
#[test]
fn export_rejects_unknown_ext() {
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.export("/tmp/openusd-bad.xyz").unwrap_err();
assert!(err.to_string().contains("unsupported"));
}
#[test]
fn save_writes_identifier() -> Result<()> {
use crate::sdf::{self, SpecType};
let mut data = sdf::Data::new();
data.create_spec(sdf::Path::abs_root(), SpecType::PseudoRoot);
let dir = tempfile::tempdir()?;
let path = dir.path().join("by-identifier.usda");
let layer = sdf::Layer::new(path.to_str().unwrap(), Box::new(data));
layer.save()?;
let bytes = std::fs::read(&path)?;
assert!(
bytes.starts_with(b"#usda"),
"save() should write text to a .usda identifier"
);
Ok(())
}
#[test]
fn save_rejects_anonymous() {
let layer = sdf::Layer::new_anonymous("scratch");
let err = layer.save().unwrap_err();
assert!(err.to_string().contains("anonymous"));
}
#[test]
fn save_rejects_non_file_identifier() {
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("scheme://host/x.usda", Box::new(data));
let err = layer.save().unwrap_err();
assert!(err.to_string().contains("absolute file path"));
}
#[test]
fn save_rejects_usdz_in_place() {
use crate::sdf::{self, SpecType};
let mut data = sdf::Data::new();
data.create_spec(sdf::Path::abs_root(), SpecType::PseudoRoot);
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("pkg.usdz");
let layer = sdf::Layer::new(path.to_str().unwrap(), Box::new(data));
let err = layer.save().unwrap_err();
assert!(err.to_string().contains("not editable"));
}
#[test]
fn export_to_string_is_usda() -> Result<()> {
use crate::sdf::{self, SpecType};
let mut data = sdf::Data::new();
let ps = data.create_spec(sdf::Path::abs_root(), SpecType::PseudoRoot);
ps.add("primChildren", sdf::Value::TokenVec(vec!["Foo".into()]));
let sp = data.create_spec(sdf::path("/Foo")?, SpecType::Prim);
sp.add("specifier", sdf::Value::Specifier(sdf::Specifier::Def));
let layer = sdf::Layer::new("scratch.usdc", Box::new(data));
let text = layer.export_to_string()?;
assert!(text.starts_with("#usda"), "export_to_string must emit usda text");
assert!(text.contains("\"Foo\""), "export_to_string must include the prim");
Ok(())
}
#[test]
fn create_prim_basic() -> Result<()> {
let mut layer = sdf::Layer::new_anonymous("anon.usda");
edit_layer(&mut layer, |e| {
sdf::PrimSpec::new(e.data_mut(), "/World", sdf::Specifier::Def, "Xform")
.unwrap()
.set_kind("group");
});
let world = layer.prim("/World").expect("prim authored");
assert_eq!(world.type_name().as_deref(), Some("Xform"));
assert_eq!(world.specifier(), Some(sdf::Specifier::Def));
assert_eq!(world.kind().as_deref(), Some("group"));
Ok(())
}
#[test]
fn auto_ancestor_chain() -> Result<()> {
let mut layer = sdf::Layer::new_anonymous("anon.usda");
edit_layer(&mut layer, |e| {
sdf::PrimSpec::new(e.data_mut(), "/A/B/C", sdf::Specifier::Def, "Mesh").unwrap();
});
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");
edit_layer(&mut layer, |e| {
sdf::PrimSpec::new(e.data_mut(), "/World", sdf::Specifier::Def, "Xform").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/World/Mesh", sdf::Specifier::Def, "Mesh").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/World/Cube", sdf::Specifier::Def, "Cube").unwrap();
});
let root = layer.pseudo_root().expect("pseudo-root present");
let root_children = root.prim_children().unwrap();
let root_children: Vec<&str> = root_children.iter().map(|t| t.as_str()).collect();
assert_eq!(root_children, ["World"]);
let world = layer.prim("/World").expect("prim");
let world_children = world.prim_children().unwrap();
let world_children: Vec<&str> = world_children.iter().map(|t| t.as_str()).collect();
assert_eq!(world_children, ["Mesh", "Cube"]);
Ok(())
}
#[test]
fn property_children() -> Result<()> {
let mut layer = sdf::Layer::new_anonymous("anon.usda");
edit_layer(&mut layer, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Mesh", sdf::Specifier::Def, "Mesh").unwrap();
sdf::AttributeSpec::new(
e.data_mut(),
"/Mesh.points",
"point3f[]",
sdf::Variability::Varying,
false,
)
.unwrap();
sdf::AttributeSpec::new(
e.data_mut(),
"/Mesh.normals",
"normal3f[]",
sdf::Variability::Varying,
false,
)
.unwrap();
sdf::RelationshipSpec::new(e.data_mut(), "/Mesh.material:binding", sdf::Variability::Varying, false)
.unwrap();
});
let mesh = layer.prim("/Mesh").expect("prim");
let props = mesh.property_children().unwrap();
let props: Vec<&str> = props.iter().map(|t| t.as_str()).collect();
assert_eq!(props, ["points", "normals", "material:binding"]);
Ok(())
}
#[test]
fn relationship_variability() -> Result<()> {
let mut layer = sdf::Layer::new_anonymous("anon.usda");
edit_layer(&mut layer, |e| {
sdf::RelationshipSpec::new(e.data_mut(), "/Mesh.material:binding", sdf::Variability::Uniform, false)
.unwrap();
});
let rel = layer.relationship("/Mesh.material:binding").expect("relationship");
assert_eq!(rel.variability(), sdf::Variability::Uniform);
edit_layer(&mut layer, |e| {
sdf::RelationshipSpec::new(e.data_mut(), "/Mesh.material:binding", sdf::Variability::Varying, false)
.unwrap();
});
let rel = layer.relationship("/Mesh.material:binding").expect("relationship");
assert_eq!(rel.variability(), sdf::Variability::Varying);
assert!(rel.field(sdf::FieldKey::Variability.as_str()).ok().flatten().is_none());
Ok(())
}
#[test]
fn bad_prim_children_errors() {
let mut layer = sdf::Layer::new_anonymous("anon.usda");
edit_layer(&mut layer, |e| {
e.data_mut().set_field(
&sdf::Path::abs_root(),
sdf::ChildrenKey::PrimChildren.as_str(),
sdf::Value::String("bad".into()),
);
});
let err = layer
.edit(|e| {
sdf::PrimSpec::new(e.data_mut(), "/A", sdf::Specifier::Def, "Xform")?;
Ok(())
})
.unwrap_err();
assert!(matches!(
err,
sdf::EditError::Author(sdf::AuthoringError::InvalidPath { .. })
));
let value = layer
.data()
.get_field(&sdf::Path::abs_root(), sdf::ChildrenKey::PrimChildren.as_str())
.unwrap()
.into_owned();
assert!(matches!(value, sdf::Value::String(value) if value == "bad"));
}
#[test]
fn bad_property_children_errors() -> Result<()> {
let mut layer = sdf::Layer::new_anonymous("anon.usda");
edit_layer(&mut layer, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Mesh", sdf::Specifier::Def, "Mesh").unwrap();
e.data_mut().set_field(
&sdf::path("/Mesh").unwrap(),
sdf::ChildrenKey::PropertyChildren.as_str(),
sdf::Value::String("bad".into()),
);
});
let err = layer
.edit(|e| {
sdf::RelationshipSpec::new(e.data_mut(), "/Mesh.material:binding", sdf::Variability::Varying, false)?;
Ok(())
})
.unwrap_err();
assert!(matches!(
err,
sdf::EditError::Author(sdf::AuthoringError::InvalidPath { .. })
));
assert!(!layer.data().has_spec(&sdf::path("/Mesh.material:binding").unwrap()));
let value = layer
.data()
.get_field(
&sdf::path("/Mesh").unwrap(),
sdf::ChildrenKey::PropertyChildren.as_str(),
)
.unwrap()
.into_owned();
assert!(matches!(value, sdf::Value::String(value) if value == "bad"));
Ok(())
}
#[test]
fn attr_samples() -> Result<()> {
let mut layer = sdf::Layer::new_anonymous("anon.usda");
edit_layer(&mut layer, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Sphere", sdf::Specifier::Def, "Sphere").unwrap();
let mut radius = sdf::AttributeSpec::new(
e.data_mut(),
"/Sphere.radius",
"double",
sdf::Variability::Varying,
false,
)
.unwrap();
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().as_deref(), 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");
edit_layer(&mut layer, |e| {
e.data_mut()
.create_spec(sdf::path("/A").unwrap(), sdf::SpecType::Attribute);
});
let err = layer
.edit(|e| {
sdf::PrimSpec::new(e.data_mut(), "/A/B", sdf::Specifier::Def, "Xform")?;
Ok(())
})
.unwrap_err();
assert!(matches!(
err,
sdf::EditError::Author(sdf::AuthoringError::InvalidPath { .. })
));
let err = layer
.edit(|e| {
sdf::AttributeSpec::new(e.data_mut(), "/A.x", "double", sdf::Variability::Varying, false)?;
Ok(())
})
.unwrap_err();
assert!(matches!(
err,
sdf::EditError::Author(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();
edit_layer(&mut layer, |e| {
e.data_mut().create_spec(bad_child, sdf::SpecType::Attribute);
});
let err = layer
.edit(|e| {
sdf::PrimSpec::new(e.data_mut(), "/A/B", sdf::Specifier::Def, "Xform")?;
Ok(())
})
.unwrap_err();
assert!(matches!(
err,
sdf::EditError::Author(sdf::AuthoringError::InvalidPath { .. })
));
assert!(!layer.data().has_spec(&sdf::path("/A").unwrap()));
assert!(layer
.data()
.try_field(&sdf::Path::abs_root(), sdf::ChildrenKey::PrimChildren.as_str())
.unwrap()
.is_none());
}
#[test]
fn nan_time_sample() -> Result<()> {
let mut layer = sdf::Layer::new_anonymous("anon.usda");
edit_layer(&mut layer, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Sphere", sdf::Specifier::Def, "Sphere").unwrap();
let mut r = sdf::AttributeSpec::new(
e.data_mut(),
"/Sphere.radius",
"double",
sdf::Variability::Varying,
false,
)
.unwrap();
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]);
let mut erased = false;
edit_layer(&mut layer, |e| {
erased = e.attribute_mut("/Sphere.radius").unwrap().erase_time_sample(f64::NAN);
});
assert!(erased);
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");
edit_layer(&mut layer, |e| {
sdf::PrimSpec::over(e.data_mut(), "/A/B").unwrap();
});
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)
);
edit_layer(&mut layer, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Defined", sdf::Specifier::Def, "Xform").unwrap();
sdf::PrimSpec::over(e.data_mut(), "/Defined").unwrap();
});
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");
edit_layer(&mut layer, |e| {
let mut root = e.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().as_deref(), Some("World"));
assert_eq!(root.documentation().as_deref(), 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(vec!["./over.usda".to_string(), "./over.usda".to_string()])
);
Ok(())
}
#[test]
fn invalid_paths() {
let mut layer = sdf::Layer::new_anonymous("anon.usda");
fn invalid(
layer: &mut sdf::Layer,
f: impl FnOnce(&mut sdf::LayerEdit<'_>) -> Result<(), sdf::AuthoringError>,
) -> bool {
matches!(
layer.edit(f).unwrap_err(),
sdf::EditError::Author(sdf::AuthoringError::InvalidPath { .. })
)
}
assert!(invalid(&mut layer, |e| {
sdf::PrimSpec::new(e.data_mut(), "/", sdf::Specifier::Def, "Xform")?;
Ok(())
}));
assert!(invalid(&mut layer, |e| {
sdf::PrimSpec::new(e.data_mut(), "/A.foo", sdf::Specifier::Def, "Xform")?;
Ok(())
}));
assert!(invalid(&mut layer, |e| {
sdf::AttributeSpec::new(e.data_mut(), "/A", "double", sdf::Variability::Varying, false)?;
Ok(())
}));
assert!(invalid(&mut layer, |e| {
sdf::AttributeSpec::new(e.data_mut(), "A.foo", "double", sdf::Variability::Varying, false)?;
Ok(())
}));
assert!(invalid(&mut layer, |e| {
sdf::RelationshipSpec::new(e.data_mut(), "A.foo", sdf::Variability::Varying, false)?;
Ok(())
}));
assert!(invalid(&mut layer, |e| {
sdf::AttributeSpec::new(e.data_mut(), "/.foo", "double", sdf::Variability::Varying, false)?;
Ok(())
}));
assert!(invalid(&mut layer, |e| {
sdf::AttributeSpec::new(
e.data_mut(),
"/A.rel[/Target].attr",
"double",
sdf::Variability::Varying,
false,
)?;
Ok(())
}));
assert!(invalid(&mut layer, |e| {
sdf::PrimSpec::new(e.data_mut(), "/A{x=}child", sdf::Specifier::Def, "Xform")?;
Ok(())
}));
}
#[test]
fn variant_authoring() -> Result<()> {
let mut layer = sdf::Layer::new_anonymous("variants.usda");
edit_layer(&mut layer, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Prim", sdf::Specifier::Def, "Xform").unwrap();
sdf::PrimSpec::new(e.data_mut(), "/Prim{set=sel}child", sdf::Specifier::Def, "Scope").unwrap();
});
assert_eq!(layer.data().spec_type(&sdf::path("/Prim")?), Some(sdf::SpecType::Prim));
assert_eq!(
layer.data().spec_type(&sdf::path("/Prim{set=}")?),
Some(sdf::SpecType::VariantSet)
);
assert_eq!(
layer.data().spec_type(&sdf::path("/Prim{set=sel}")?),
Some(sdf::SpecType::Variant)
);
assert_eq!(
layer.data().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.data().get_field(&sdf::path(path)?, key.as_str())?.into_owned() {
sdf::Value::TokenVec(v) => Ok(v.into_iter().map(Into::into).collect()),
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");
edit_layer(&mut layer, |e| {
sdf::PrimSpec::new(e.data_mut(), "/Prim", sdf::Specifier::Def, "Xform").unwrap();
});
assert!(matches!(
layer
.edit(|e| {
sdf::PrimSpec::new(e.data_mut(), "/Prim{set=sel}", sdf::Specifier::Def, "Xform")?;
Ok(())
})
.unwrap_err(),
sdf::EditError::Author(sdf::AuthoringError::InvalidPath { .. })
));
assert!(matches!(
layer
.edit(|e| {
sdf::PrimSpec::over(e.data_mut(), "/Prim{set=sel}")?;
Ok(())
})
.unwrap_err(),
sdf::EditError::Author(sdf::AuthoringError::InvalidPath { .. })
));
assert_eq!(layer.data().spec_type(&sdf::path("/Prim{set=sel}")?), None);
Ok(())
}
#[test]
fn usda_roundtrip() -> Result<()> {
let mut layer = sdf::Layer::new_anonymous("scene.usda");
let material = sdf::path("/World/Material")?;
edit_layer(&mut layer, |e| {
e.pseudo_root_mut().unwrap().set_default_prim("World");
sdf::PrimSpec::new(e.data_mut(), "/World", sdf::Specifier::Def, "Xform").unwrap();
let mut sphere = sdf::PrimSpec::new(e.data_mut(), "/World/Sphere", sdf::Specifier::Def, "Sphere").unwrap();
sphere.set_kind("component");
let mut radius = sdf::AttributeSpec::new(
e.data_mut(),
"/World/Sphere.radius",
"double",
sdf::Variability::Varying,
false,
)
.unwrap();
radius.set_default(sdf::Value::Double(1.5));
sdf::PrimSpec::new(e.data_mut(), &material, sdf::Specifier::Def, "Material").unwrap();
let mut binding = sdf::RelationshipSpec::new(
e.data_mut(),
"/World/Sphere.material:binding",
sdf::Variability::Varying,
false,
)
.unwrap();
binding.add_target(material.clone());
let mut surface = sdf::AttributeSpec::new(
e.data_mut(),
"/World/Sphere.inputs:surface",
"token",
sdf::Variability::Varying,
false,
)
.unwrap();
surface.set_connection_paths([sdf::path("/World/Material.outputs:surface").unwrap()]);
});
let tmp = std::env::temp_dir().join("openusd_authoring_roundtrip.usda");
layer.export(tmp.to_str().unwrap())?;
let parsed = usda::read_file(&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_field(&sdf::Path::abs_root(), sdf::FieldKey::DefaultPrim.as_str())?
.into_owned(),
sdf::Value::Token("World".into())
);
let targets = parsed
.get_field(
&sdf::path("/World/Sphere.material:binding")?,
sdf::FieldKey::TargetPaths.as_str(),
)?
.into_owned()
.try_as_path_list_op()
.expect("relationship targets as PathListOp");
assert!(targets.explicit);
assert_eq!(targets.explicit_items, vec![material]);
let connections = parsed
.get_field(
&sdf::path("/World/Sphere.inputs:surface")?,
sdf::FieldKey::ConnectionPaths.as_str(),
)?
.into_owned()
.try_as_path_list_op()
.expect("connection paths as PathListOp");
assert!(connections.explicit);
assert_eq!(
connections.explicit_items,
vec![sdf::path("/World/Material.outputs:surface")?]
);
let _ = std::fs::remove_file(&tmp);
Ok(())
}
}