use std::cell::RefCell;
use anyhow::Result;
use crate::ar::{DefaultResolver, Resolver};
use crate::sdf::{Path, SpecType, Value};
use crate::{layer, pcp, CompositionError};
pub struct Stage {
graph: RefCell<pcp::Cache>,
on_composition_error: Box<dyn Fn(pcp::Error) -> Result<()>>,
}
impl Stage {
pub fn open(root_path: &str) -> Result<Self> {
Self::builder().open(root_path)
}
pub fn builder() -> StageBuilder<DefaultResolver> {
StageBuilder::new()
}
fn from_layers(
session_layers: Vec<layer::Layer>,
root_layers: Vec<layer::Layer>,
on_composition_error: Box<dyn Fn(pcp::Error) -> Result<()>>,
variant_fallbacks: pcp::VariantFallbackMap,
) -> Self {
let session_layer_count = session_layers.len();
let total = session_layer_count + root_layers.len();
let mut identifiers = Vec::with_capacity(total);
let mut layers = Vec::with_capacity(total);
for layer in session_layers.into_iter().chain(root_layers) {
identifiers.push(layer.identifier);
layers.push(layer.data);
}
let stack = pcp::LayerStack::new(layers, identifiers, session_layer_count);
Self {
graph: RefCell::new(pcp::Cache::new(stack, variant_fallbacks)),
on_composition_error,
}
}
pub fn layer_count(&self) -> usize {
self.graph.borrow().layer_count()
}
pub fn layer_identifiers(&self) -> Vec<String> {
self.graph.borrow().layer_identifiers().to_vec()
}
pub fn has_session_layer(&self) -> bool {
self.graph.borrow().session_layer_count() > 0
}
pub fn session_layer(&self) -> Option<String> {
let cache = self.graph.borrow();
if cache.session_layer_count() > 0 {
Some(cache.layer_identifiers()[0].clone())
} else {
None
}
}
pub fn default_prim(&self) -> Option<String> {
self.graph.borrow().default_prim()
}
pub fn root_prims(&self) -> Result<Vec<String>> {
self.try_or_handle(|cache| cache.prim_children(&Path::abs_root()))
}
pub fn prim_children(&self, path: impl Into<Path>) -> Result<Vec<String>> {
self.try_or_handle(|cache| cache.prim_children(&path.into()))
}
pub fn prim_properties(&self, path: impl Into<Path>) -> Result<Vec<String>> {
self.try_or_handle(|cache| cache.prim_properties(&path.into()))
}
pub fn has_spec(&self, path: impl Into<Path>) -> Result<bool> {
self.try_or_handle(|cache| cache.has_spec(&path.into()))
}
pub fn spec_type(&self, path: impl Into<Path>) -> Result<Option<SpecType>> {
self.try_or_handle(|cache| cache.spec_type(&path.into()))
}
pub fn field<T>(&self, path: impl Into<Path>, field: impl AsRef<str>) -> Result<Option<T>>
where
T: TryFrom<Value>,
T::Error: std::error::Error + Send + Sync + 'static,
{
let raw = self.try_or_handle(|cache| cache.resolve_field(&path.into(), field.as_ref()))?;
match raw {
Some(value) => Ok(Some(T::try_from(value)?)),
None => Ok(None),
}
}
fn try_or_handle<T: Default>(&self, f: impl FnOnce(&mut pcp::Cache) -> Result<T>) -> Result<T> {
match f(&mut self.graph.borrow_mut()) {
Ok(val) => Ok(val),
Err(e) => match e.downcast::<pcp::Error>() {
Ok(pcp_err) => {
(self.on_composition_error)(pcp_err)?;
Ok(T::default())
}
Err(other) => Err(other),
},
}
}
pub fn traverse(&self, mut visitor: impl FnMut(&Path)) -> Result<()> {
let mut stack = vec![Path::abs_root()];
while let Some(path) = stack.pop() {
if path != Path::abs_root() {
visitor(&path);
}
let children = self.try_or_handle(|cache| cache.prim_children(&path))?;
for name in children.iter().rev() {
if let Ok(child) = path.append_path(name.as_str()) {
stack.push(child);
}
}
}
Ok(())
}
}
type StrictErrorHandler = fn(CompositionError) -> Result<()>;
fn strict_composition_error(e: CompositionError) -> Result<()> {
Err(anyhow::anyhow!("{e}"))
}
pub struct StageBuilder<R: Resolver = DefaultResolver, E: Fn(CompositionError) -> Result<()> = StrictErrorHandler> {
resolver: R,
on_error: E,
variant_fallbacks: pcp::VariantFallbackMap,
session_layer: Option<String>,
}
impl StageBuilder {
fn new() -> Self {
Self {
resolver: DefaultResolver::new(),
on_error: strict_composition_error,
variant_fallbacks: pcp::VariantFallbackMap::new(),
session_layer: None,
}
}
}
impl<R: Resolver, E: Fn(CompositionError) -> Result<()>> StageBuilder<R, E> {
pub fn resolver<R2: Resolver>(self, resolver: R2) -> StageBuilder<R2, E> {
StageBuilder {
resolver,
on_error: self.on_error,
variant_fallbacks: self.variant_fallbacks,
session_layer: self.session_layer,
}
}
pub fn on_error<E2: Fn(CompositionError) -> Result<()>>(self, handler: E2) -> StageBuilder<R, E2> {
StageBuilder {
resolver: self.resolver,
on_error: handler,
variant_fallbacks: self.variant_fallbacks,
session_layer: self.session_layer,
}
}
pub fn session_layer(mut self, path: impl Into<String>) -> Self {
self.session_layer = Some(path.into());
self
}
pub fn variant_fallbacks(mut self, fallbacks: pcp::VariantFallbackMap) -> Self {
self.variant_fallbacks = fallbacks;
self
}
pub fn open(self, root_path: &str) -> Result<Stage>
where
E: 'static,
{
let on_error = self.on_error;
let variant_fallbacks = self.variant_fallbacks;
let session_layers = if let Some(ref session_path) = self.session_layer {
layer::collect_layers_with_handler(&self.resolver, session_path, |e| on_error(CompositionError::Layer(e)))?
} else {
Vec::new()
};
let root_layers =
layer::collect_layers_with_handler(&self.resolver, root_path, |e| on_error(CompositionError::Layer(e)))?;
let pcp_handler = Box::new(move |e: pcp::Error| on_error(CompositionError::Pcp(e)));
Ok(Stage::from_layers(
session_layers,
root_layers,
pcp_handler,
variant_fallbacks,
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sdf::schema::FieldKey;
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!("{}/{VENDOR_COMPOSITION}/{relative}", manifest_dir())
}
fn fixture_path(relative: &str) -> String {
format!("{}/fixtures/{relative}", manifest_dir())
}
#[test]
fn open_single_layer() -> Result<()> {
let path = composition_path("active.usda");
let stage = Stage::open(&path)?;
assert_eq!(stage.layer_count(), 1);
assert_eq!(stage.default_prim(), Some("World".to_string()));
assert_eq!(stage.root_prims()?, vec!["World"]);
Ok(())
}
#[test]
fn traverse_single_layer() -> Result<()> {
let path = composition_path("active.usda");
let stage = Stage::open(&path)?;
let mut prims = Vec::new();
stage.traverse(|p| prims.push(p.as_str().to_string()))?;
assert_eq!(prims, vec!["/World", "/World/CubeInactive", "/World/CubeActive"]);
Ok(())
}
#[test]
fn field_single_layer() -> Result<()> {
let path = composition_path("active.usda");
let stage = Stage::open(&path)?;
let active = stage.field::<bool>("/World/CubeInactive", FieldKey::Active)?;
assert_eq!(active, Some(false));
let active = stage.field::<bool>("/World/CubeActive", FieldKey::Active)?;
assert_eq!(active, Some(true));
Ok(())
}
#[test]
fn field_not_authored() -> Result<()> {
let path = composition_path("active.usda");
let stage = Stage::open(&path)?;
let active = stage.field::<Value>("/World", FieldKey::Active)?;
assert_eq!(active, None);
Ok(())
}
#[test]
fn sublayer_stronger_opinion_wins() -> Result<()> {
let path = fixture_path("sublayer_override.usda");
let stage = Stage::open(&path)?;
assert_eq!(stage.layer_count(), 2);
let prop_path = Path::new("/World/Cube")?.append_property("primvars:displayColor")?;
let value: Option<Value> = stage.field(&prop_path, FieldKey::Default)?;
assert!(value.is_some(), "displayColor should have a composed value");
let value = value.unwrap();
let base_red = Value::Vec3fVec(vec![[1.0, 0.0, 0.0]]);
assert_ne!(value, base_red, "stronger layer opinion should win over weaker");
Ok(())
}
#[test]
fn sublayer_children_union() -> Result<()> {
let path = fixture_path("sublayer_override.usda");
let stage = Stage::open(&path)?;
let children = stage.prim_children("/World")?;
assert!(children.contains(&"Cube".to_string()), "Cube from base layer");
assert!(children.contains(&"Sphere".to_string()), "Sphere from override layer");
Ok(())
}
#[test]
fn sublayer_prims_from_weaker_layer() -> Result<()> {
let path = composition_path("subLayer/sublayer_same_folder.usda");
let stage = Stage::open(&path)?;
assert_eq!(stage.layer_count(), 2);
assert_eq!(stage.default_prim(), Some("World".to_string()));
let mut prims = Vec::new();
stage.traverse(|p| prims.push(p.as_str().to_string()))?;
assert!(prims.contains(&"/World/Cube".to_string()));
Ok(())
}
#[test]
fn field_active_metadata() -> Result<()> {
let path = composition_path("active.usda");
let stage = Stage::open(&path)?;
let inactive: Option<bool> = stage.field("/World/CubeInactive", FieldKey::Active)?;
assert_eq!(inactive, Some(false));
let active = stage.field::<bool>("/World/CubeActive", FieldKey::Active)?;
assert_eq!(active, Some(true));
Ok(())
}
#[test]
fn reference_external_default_prim() -> Result<()> {
let path = fixture_path("ref_external.usda");
let stage = Stage::open(&path)?;
assert!(stage.has_spec("/World/MyPrim")?);
let children = stage.prim_children("/World/MyPrim")?;
assert!(
children.contains(&"Child".to_string()),
"referenced children should be visible"
);
Ok(())
}
#[test]
fn reference_default_prim_from_external_layer() -> Result<()> {
let path = composition_path("references/reference_same_folder.usda");
let stage = Stage::open(&path)?;
let children = stage.prim_children("/World")?;
assert!(
children.contains(&"Cube".to_string()),
"Cube from referenced layer should appear under /World"
);
Ok(())
}
#[test]
fn reference_explicit_prim_path() -> Result<()> {
let path = fixture_path("ref_prim.usda");
let stage = Stage::open(&path)?;
let children = stage.prim_children("/World/RefPrim")?;
assert!(
children.contains(&"Child".to_string()),
"referenced children should be namespace-remapped"
);
Ok(())
}
#[test]
fn inherit_from_class() -> Result<()> {
let path = composition_path("class_inherit.usda");
let stage = Stage::open(&path)?;
let props = stage.prim_properties("/World/cubeWithoutSetColor")?;
assert!(
props.contains(&"primvars:displayColor".to_string()),
"inherited property should be visible"
);
Ok(())
}
#[test]
fn inherit_local_opinion_wins() -> Result<()> {
let path = composition_path("class_inherit.usda");
let stage = Stage::open(&path)?;
let prop = Path::new("/World/cubeWithSetColor")?.append_property("primvars:displayColor")?;
let value: Option<Value> = stage.field(&prop, FieldKey::Default)?;
assert!(value.is_some());
let green = Value::Vec3fVec(vec![[0.0, 0.8, 0.0]]);
assert_ne!(value.unwrap(), green, "local opinion should win over inherited");
Ok(())
}
#[test]
fn variant_local_opinion_wins() -> Result<()> {
let path = format!(
"{}/vendor/usd-wg-assets/docs/CompositionPuzzles/VariantSetAndLocal1/puzzle_1.usda",
manifest_dir()
);
let stage = Stage::open(&path)?;
let prop = Path::new("/World/Sphere")?.append_property("radius")?;
let value = stage.field::<f64>(&prop, FieldKey::Default)?;
assert_eq!(value, Some(1.0), "local opinion (1) should win over variant (2)");
Ok(())
}
#[test]
fn payload_pulls_children() -> Result<()> {
let path = composition_path("payload/payload_same_folder.usda");
let stage = Stage::open(&path)?;
let children = stage.prim_children("/World")?;
assert!(
children.contains(&"Cube".to_string()),
"Cube from payload layer should appear under /World"
);
Ok(())
}
#[test]
fn specialize_local_opinion_wins() -> Result<()> {
let path = composition_path("inherit_and_specialize.usda");
let stage = Stage::open(&path)?;
let prop = Path::new("/World/cubeScene/specializes")?.append_property("primvars:displayColor")?;
let value: Option<Value> = stage.field(&prop, FieldKey::Default)?;
assert!(value.is_some());
let red = Value::Vec3fVec(vec![[0.8, 0.0, 0.0]]);
assert_ne!(value.unwrap(), red, "local opinion should win over specialized");
Ok(())
}
#[test]
fn instanceable_true_parses_and_is_readable() -> Result<()> {
let path = fixture_path("instanceable_metadata.usda");
let stage = Stage::open(&path)?;
let value = stage.field::<bool>("/Root/InstancePrototype", FieldKey::Instanceable)?;
assert_eq!(value, Some(true), "instanceable = true should be stored");
Ok(())
}
#[test]
fn instanceable_false_parses_and_is_readable() -> Result<()> {
let path = fixture_path("instanceable_metadata.usda");
let stage = Stage::open(&path)?;
let value = stage.field::<bool>("/Root/NotInstanceable", FieldKey::Instanceable)?;
assert_eq!(value, Some(false), "instanceable = false should be stored");
Ok(())
}
#[test]
fn instanceable_absent_returns_none() -> Result<()> {
let path = fixture_path("instanceable_metadata.usda");
let stage = Stage::open(&path)?;
let value = stage.field::<bool>("/Root", FieldKey::Instanceable)?;
assert_eq!(value, None, "instanceable should be None when not authored");
Ok(())
}
#[test]
fn variant_fallback_selects_preferred() -> Result<()> {
let path = fixture_path("variant_fallback.usda");
let fallbacks = crate::pcp::VariantFallbackMap::new().add("shadingComplexity", ["simple"]);
let stage = Stage::builder().variant_fallbacks(fallbacks).open(&path)?;
let prop = Path::new("/NoSelection")?.append_property("complexity")?;
let value = stage.field::<f64>(&prop, FieldKey::Default)?;
assert_eq!(value, Some(0.5), "fallback 'simple' should give complexity=0.5");
Ok(())
}
#[test]
fn variant_fallback_does_not_override_authored() -> Result<()> {
let path = fixture_path("variant_fallback.usda");
let fallbacks = crate::pcp::VariantFallbackMap::new().add("shadingComplexity", ["none"]);
let stage = Stage::builder().variant_fallbacks(fallbacks).open(&path)?;
let prop = Path::new("/Root")?.append_property("complexity")?;
let value = stage.field::<f64>(&prop, FieldKey::Default)?;
assert_eq!(value, Some(1.0), "authored 'full' should win over fallback 'none'");
Ok(())
}
#[test]
fn inherit_child_exists_without_local_override() -> Result<()> {
let path = fixture_path("inherit_child_propagation.usda");
let stage = Stage::open(&path)?;
let children = stage.prim_children("/Instance")?;
assert!(
children.contains(&"Child".to_string()),
"inherited child should appear: got {children:?}"
);
assert!(
stage.has_spec(Path::new("/Instance/Child.name")?)?,
"property from inherited child should be visible"
);
Ok(())
}
#[test]
fn inherit_nested_child_propagation() -> Result<()> {
let path = fixture_path("inherit_nested_child.usda");
let stage = Stage::open(&path)?;
let a_children = stage.prim_children("/Prim")?;
assert!(
a_children.contains(&"A".to_string()),
"first-level child: got {a_children:?}"
);
let b_children = stage.prim_children("/Prim/A")?;
assert!(
b_children.contains(&"B".to_string()),
"second-level child: got {b_children:?}"
);
assert!(
stage.has_spec(Path::new("/Prim/A/B.val")?)?,
"deeply nested inherited property should be visible"
);
Ok(())
}
#[test]
fn inherit_chain_child_propagation() -> Result<()> {
let path = fixture_path("inherit_chain_child.usda");
let stage = Stage::open(&path)?;
let children = stage.prim_children("/Leaf")?;
assert!(
children.contains(&"Deep".to_string()),
"chain-inherited child: got {children:?}"
);
assert!(
stage.has_spec(Path::new("/Leaf/Deep.x")?)?,
"property from chain-inherited child should be visible"
);
Ok(())
}
fn open_with_session() -> Result<Stage> {
let root = fixture_path("session_root.usda");
let session = fixture_path("session_layer.usda");
Stage::builder().session_layer(&session).open(&root)
}
#[test]
fn no_session_layer_by_default() -> Result<()> {
let stage = Stage::open(&fixture_path("session_root.usda"))?;
assert!(!stage.has_session_layer());
assert_eq!(stage.session_layer(), None);
assert_eq!(stage.layer_count(), 1);
Ok(())
}
#[test]
fn session_layer_opinion_wins() -> Result<()> {
let stage = open_with_session()?;
assert!(stage.has_session_layer());
assert_eq!(stage.layer_count(), 2);
let prop = Path::new("/World")?.append_property("radius")?;
let value = stage.field::<f64>(&prop, FieldKey::Default)?;
assert_eq!(value, Some(99.0), "session layer opinion should win");
Ok(())
}
#[test]
fn session_layer_adds_properties() -> Result<()> {
let stage = open_with_session()?;
let prop = Path::new("/World")?.append_property("visibility")?;
let value = stage.field::<String>(&prop, FieldKey::Default)?;
assert_eq!(value, Some("hidden".to_string()));
Ok(())
}
#[test]
fn session_layer_preserves_root_opinions() -> Result<()> {
let stage = open_with_session()?;
let prop = Path::new("/World")?.append_property("name")?;
let value = stage.field::<String>(&prop, FieldKey::Default)?;
assert_eq!(value, Some("root".to_string()));
Ok(())
}
#[test]
fn session_layer_does_not_affect_default_prim() -> Result<()> {
let stage = open_with_session()?;
assert_eq!(stage.default_prim(), Some("World".to_string()));
Ok(())
}
#[test]
fn session_layer_preserves_children() -> Result<()> {
let stage = open_with_session()?;
let children = stage.prim_children("/World")?;
assert!(
children.contains(&"Child".to_string()),
"root layer's children should be visible: got {children:?}"
);
Ok(())
}
}