use std::cell::{Cell, RefCell};
use anyhow::Result;
use bitflags::bitflags;
use crate::{ar, layer, pcp, sdf};
use super::interp::{self, InterpolationType};
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum CompositionError {
#[error(transparent)]
Layer(#[from] layer::Error),
#[error(transparent)]
Pcp(#[from] pcp::Error),
}
bitflags! {
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct PrimStatus: u32 {
const ACTIVE = 1 << 0;
const LOADED = 1 << 1;
const DEFINED = 1 << 2;
const ABSTRACT = 1 << 3;
const INSTANCE = 1 << 4;
const MODEL = 1 << 5;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PrimPredicate {
required: PrimStatus,
rejected: PrimStatus,
}
impl PrimPredicate {
const INHERITED_REQUIRED: PrimStatus = PrimStatus::ACTIVE.union(PrimStatus::LOADED).union(PrimStatus::DEFINED);
const INHERITED_REJECTED: PrimStatus = PrimStatus::ABSTRACT;
pub const ALL: Self = Self::new(PrimStatus::empty(), PrimStatus::empty());
pub const DEFAULT: Self = Self::new(Self::INHERITED_REQUIRED, Self::INHERITED_REJECTED);
pub const fn new(required: PrimStatus, rejected: PrimStatus) -> Self {
Self { required, rejected }
}
pub const fn matches(self, status: PrimStatus) -> bool {
status.contains(self.required) && !status.intersects(self.rejected)
}
fn consulted_bits(self) -> PrimStatus {
self.required.union(self.rejected)
}
fn prunes_descendants(self, status: PrimStatus) -> bool {
let required = self.required.intersection(Self::INHERITED_REQUIRED);
if !status.contains(required) {
return true;
}
status.intersects(self.rejected.intersection(Self::INHERITED_REJECTED))
}
}
impl Default for PrimPredicate {
fn default() -> Self {
Self::DEFAULT
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum InitialLoadSet {
#[default]
LoadAll,
LoadNone,
}
impl InitialLoadSet {
pub const fn load_payloads(self) -> bool {
matches!(self, Self::LoadAll)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StagePopulationMask {
paths: Vec<sdf::Path>,
}
impl StagePopulationMask {
pub fn all() -> Self {
Self {
paths: vec![sdf::Path::abs_root()],
}
}
pub fn empty() -> Self {
Self { paths: Vec::new() }
}
pub fn new(paths: impl IntoIterator<Item = impl Into<sdf::Path>>) -> Self {
let mut mask = Self::empty();
for path in paths {
mask.add_path(path);
}
mask
}
pub fn with_path(mut self, path: impl Into<sdf::Path>) -> Self {
self.add_path(path);
self
}
pub fn add_path(&mut self, path: impl Into<sdf::Path>) -> &mut Self {
let path = sdf::Path::abs_root().make_absolute(&path.into().prim_path());
if path == sdf::Path::abs_root() {
self.paths.clear();
self.paths.push(path);
} else if !self.is_all() && !self.paths.contains(&path) {
self.paths.push(path);
}
self
}
pub fn paths(&self) -> &[sdf::Path] {
&self.paths
}
pub fn is_empty(&self) -> bool {
self.paths.is_empty()
}
pub fn is_all(&self) -> bool {
self.paths.first() == Some(&sdf::Path::abs_root())
}
pub fn includes(&self, path: &sdf::Path) -> bool {
if self.is_all() {
return true;
}
let path = path.prim_path().strip_all_variant_selections();
self.paths
.iter()
.any(|mask_path| path.has_prefix(mask_path) || mask_path.has_prefix(&path))
}
}
impl Default for StagePopulationMask {
fn default() -> Self {
Self::all()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EditTarget {
layer_index: usize,
}
impl EditTarget {
pub const fn for_layer_index(layer_index: usize) -> Self {
Self { layer_index }
}
pub const fn layer_index(self) -> usize {
self.layer_index
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum StageAuthoringError {
#[error(transparent)]
Layer(#[from] sdf::AuthoringError),
#[error("edit target layer index {index} is out of range ({count} layers)")]
LayerOutOfRange {
index: usize,
count: usize,
},
}
pub struct Stage {
graph: RefCell<pcp::Cache>,
initial_load_set: InitialLoadSet,
population_mask: StagePopulationMask,
on_composition_error: Box<dyn Fn(pcp::Error) -> Result<()>>,
interpolation_type: Cell<InterpolationType>,
edit_target: Cell<EditTarget>,
}
impl Stage {
pub fn open(root_path: &str) -> Result<Self> {
Self::builder().open(root_path)
}
pub fn builder() -> StageBuilder<ar::DefaultResolver> {
StageBuilder::new()
}
pub fn edit_target(&self) -> EditTarget {
self.edit_target.get()
}
pub fn set_edit_target(&self, target: EditTarget) -> Result<(), StageAuthoringError> {
let count = self.graph.borrow().layer_count();
let index = target.layer_index;
if index >= count {
return Err(StageAuthoringError::LayerOutOfRange { index, count });
}
self.edit_target.set(target);
Ok(())
}
pub fn define_prim(&self, path: impl Into<sdf::Path>) -> Result<super::Prim<'_>, StageAuthoringError> {
let path = path.into();
let layer_path = path.clone();
self.with_target_layer(|layer| {
let data = layer.data();
let had_spec = data.has_spec(&layer_path);
let prior_specifier_matches = had_spec
&& matches!(
data.try_get(&layer_path, sdf::FieldKey::Specifier.as_str())
.ok()
.flatten()
.as_deref(),
Some(sdf::Value::Specifier(sdf::Specifier::Def))
);
let auto_ancestors = layer.missing_prim_ancestors(&layer_path);
layer.create_prim(layer_path.clone(), sdf::Specifier::Def, "")?;
let mut cl = sdf::ChangeList::new();
if !had_spec {
let entry = cl.entry_mut(&layer_path);
entry.flags |= sdf::ChangeFlags::ADD_NON_INERT_PRIM;
entry.info_changed.insert(sdf::FieldKey::Specifier.as_str());
} else if !prior_specifier_matches {
cl.entry_mut(&layer_path)
.info_changed
.insert(sdf::FieldKey::Specifier.as_str());
}
for anc in auto_ancestors {
cl.entry_mut(&anc).flags |= sdf::ChangeFlags::ADD_INERT_PRIM;
}
Ok(cl)
})?;
Ok(super::Prim::new(self, path))
}
pub fn override_prim(&self, path: impl Into<sdf::Path>) -> Result<super::Prim<'_>, StageAuthoringError> {
let path = path.into();
let layer_path = path.clone();
self.with_target_layer(|layer| {
let had_spec = layer.data().has_spec(&layer_path);
let auto_ancestors = layer.missing_prim_ancestors(&layer_path);
layer.override_prim(layer_path.clone())?;
let mut cl = sdf::ChangeList::new();
if !had_spec {
cl.entry_mut(&layer_path).flags |= sdf::ChangeFlags::ADD_INERT_PRIM;
}
for anc in auto_ancestors {
cl.entry_mut(&anc).flags |= sdf::ChangeFlags::ADD_INERT_PRIM;
}
Ok(cl)
})?;
Ok(super::Prim::new(self, path))
}
pub fn create_attribute(
&self,
path: impl Into<sdf::Path>,
type_name: impl Into<String>,
) -> Result<super::Attribute<'_>, StageAuthoringError> {
let path = path.into();
let type_name = type_name.into();
let layer_path = path.clone();
self.with_target_layer(|layer| {
let owning_prim = layer_path.prim_path();
let auto_ancestors = layer.missing_prim_chain_inclusive(&owning_prim);
layer.create_attribute(layer_path.clone(), type_name, sdf::Variability::Varying, true)?;
let mut cl = sdf::ChangeList::new();
cl.entry_mut(&layer_path).flags |= sdf::ChangeFlags::ADD_PROPERTY;
for anc in auto_ancestors {
cl.entry_mut(&anc).flags |= sdf::ChangeFlags::ADD_INERT_PRIM;
}
Ok(cl)
})?;
Ok(super::Attribute::new(self, path))
}
pub fn create_relationship(
&self,
path: impl Into<sdf::Path>,
) -> Result<super::Relationship<'_>, StageAuthoringError> {
let path = path.into();
let layer_path = path.clone();
self.with_target_layer(|layer| {
let owning_prim = layer_path.prim_path();
let auto_ancestors = layer.missing_prim_chain_inclusive(&owning_prim);
layer.create_relationship(layer_path.clone(), sdf::Variability::Varying, true)?;
let mut cl = sdf::ChangeList::new();
cl.entry_mut(&layer_path).flags |= sdf::ChangeFlags::ADD_PROPERTY;
for anc in auto_ancestors {
cl.entry_mut(&anc).flags |= sdf::ChangeFlags::ADD_INERT_PRIM;
}
Ok(cl)
})?;
Ok(super::Relationship::new(self, path))
}
pub fn set_default_prim(&self, name: impl Into<String>) -> Result<(), StageAuthoringError> {
let name = name.into();
self.with_root_layer(|layer| {
let prior = layer
.data()
.try_get(&sdf::Path::abs_root(), sdf::FieldKey::DefaultPrim.as_str())
.ok()
.flatten();
let unchanged = matches!(
prior.as_deref(),
Some(sdf::Value::Token(s) | sdf::Value::String(s)) if s == &name
);
layer.set_default_prim(name)?;
let mut cl = sdf::ChangeList::new();
if !unchanged {
cl.entry_mut(&sdf::Path::abs_root())
.info_changed
.insert(sdf::FieldKey::DefaultPrim.as_str());
}
Ok(cl)
})
}
pub(super) fn with_target_layer<F>(&self, f: F) -> Result<bool, StageAuthoringError>
where
F: FnOnce(&mut sdf::Layer) -> Result<sdf::ChangeList, sdf::AuthoringError>,
{
let target = self.edit_target.get();
let mut cache = self.graph.borrow_mut();
let count = cache.layer_count();
let index = target.layer_index;
let result = {
let layer = cache
.layer_mut(index)
.ok_or(StageAuthoringError::LayerOutOfRange { index, count })?;
f(layer)
};
Self::finalize_layer(&mut cache, index, result)
}
fn with_root_layer<F>(&self, f: F) -> Result<(), StageAuthoringError>
where
F: FnOnce(&mut sdf::Layer) -> Result<sdf::ChangeList, sdf::AuthoringError>,
{
let mut cache = self.graph.borrow_mut();
let index = cache.session_layer_count();
let count = cache.layer_count();
let result = {
let layer = cache
.layer_mut(index)
.ok_or(StageAuthoringError::LayerOutOfRange { index, count })?;
f(layer)
};
Self::finalize_layer(&mut cache, index, result).map(|_| ())
}
fn finalize_layer(
cache: &mut pcp::Cache,
layer_index: usize,
result: Result<sdf::ChangeList, sdf::AuthoringError>,
) -> Result<bool, StageAuthoringError> {
match result {
Ok(cl) if cl.is_empty() => Ok(false),
Ok(cl) => {
let mut changes = pcp::Changes::new();
changes.did_change(cache, &[(layer_index, cl)]);
changes.apply(cache);
Ok(true)
}
Err(e) => {
if !matches!(e, sdf::AuthoringError::ReadOnly { .. }) {
let mut changes = pcp::Changes::new();
changes.layer_stack |= pcp::LayerStackChanges::SIGNIFICANT;
changes.apply(cache);
}
Err(StageAuthoringError::Layer(e))
}
}
}
pub fn layer_count(&self) -> usize {
self.graph.borrow().layer_count()
}
pub fn is_indexed(&self, path: &sdf::Path) -> bool {
self.graph.borrow().is_indexed(path)
}
pub fn indexed_count(&self) -> usize {
self.graph.borrow().indexed_count()
}
pub fn layer_identifiers(&self) -> Vec<String> {
self.graph.borrow().layer_identifiers()
}
pub fn has_session_layer(&self) -> bool {
self.graph.borrow().session_layer_count() > 0
}
pub const fn initial_load_set(&self) -> InitialLoadSet {
self.initial_load_set
}
pub const fn population_mask(&self) -> &StagePopulationMask {
&self.population_mask
}
pub fn session_layer(&self) -> Option<String> {
let cache = self.graph.borrow();
if cache.session_layer_count() > 0 {
cache.layer_identifier(0).map(str::to_owned)
} else {
None
}
}
pub fn default_prim(&self) -> Option<String> {
self.graph.borrow().default_prim()
}
pub fn interpolation_type(&self) -> InterpolationType {
self.interpolation_type.get()
}
pub fn set_interpolation_type(&self, mode: InterpolationType) {
self.interpolation_type.set(mode);
}
pub fn time_samples(&self, attr_path: impl Into<sdf::Path>) -> Result<Option<sdf::TimeSampleMap>> {
Ok(match self.field::<sdf::Value>(attr_path, sdf::FieldKey::TimeSamples)? {
Some(sdf::Value::TimeSamples(samples)) => Some(samples),
_ => None,
})
}
pub fn value_at(&self, attr_path: impl Into<sdf::Path>, time: f64) -> Result<Option<sdf::Value>> {
let attr_path = attr_path.into();
if let Some(samples) = self.time_samples(&attr_path)? {
return Ok(interp::evaluate(&samples, time, self.interpolation_type.get()));
}
let default = self.field::<sdf::Value>(attr_path, sdf::FieldKey::Default)?;
Ok(default.and_then(|v| match v {
sdf::Value::ValueBlock | sdf::Value::None => None,
other => Some(other),
}))
}
pub fn root_prims(&self) -> Result<Vec<String>> {
let root = sdf::Path::abs_root();
let children = self.try_or_handle(|cache| cache.prim_children(&root))?;
Ok(self.filter_child_names(&root, children))
}
pub fn prim_children(&self, path: impl Into<sdf::Path>) -> Result<Vec<String>> {
let path = path.into().prim_path();
if !self.population_mask.includes(&path) {
return Ok(Vec::new());
}
let children = self.try_or_handle(|cache| cache.prim_children(&path))?;
Ok(self.filter_child_names(&path, children))
}
pub fn prim_properties(&self, path: impl Into<sdf::Path>) -> Result<Vec<String>> {
let path = path.into().prim_path();
if !self.population_mask.includes(&path) {
return Ok(Vec::new());
}
self.try_or_handle(|cache| cache.prim_properties(&path))
}
pub fn has_spec(&self, path: impl Into<sdf::Path>) -> Result<bool> {
let path = path.into();
if !self.population_mask.includes(&path.prim_path()) {
return Ok(false);
}
self.try_or_handle(|cache| cache.has_spec(&path))
}
pub fn spec_type(&self, path: impl Into<sdf::Path>) -> Result<Option<sdf::SpecType>> {
let path = path.into();
if !self.population_mask.includes(&path.prim_path()) {
return Ok(None);
}
self.try_or_handle(|cache| cache.spec_type(&path))
}
pub fn field<T>(&self, path: impl Into<sdf::Path>, field: impl AsRef<str>) -> Result<Option<T>>
where
T: TryFrom<sdf::Value>,
T::Error: std::error::Error + Send + Sync + 'static,
{
let path = path.into();
if !self.population_mask.includes(&path.prim_path()) {
return Ok(None);
}
let raw = self.try_or_handle(|cache| cache.resolve_field(&path, field.as_ref()))?;
match raw {
Some(value) => Ok(Some(T::try_from(value)?)),
None => Ok(None),
}
}
pub fn api_schemas(&self, prim: &sdf::Path) -> Result<Vec<String>> {
let prim = prim.prim_path();
if !self.population_mask.includes(&prim) {
return Ok(Vec::new());
}
self.try_or_handle(|cache| cache.api_schemas(&prim))
}
pub fn has_api_schema(&self, prim: &sdf::Path, name: &str) -> Result<bool> {
Ok(self.api_schemas(prim)?.iter().any(|s| s == name))
}
pub fn type_name(&self, prim: &sdf::Path) -> Result<Option<String>> {
self.field::<String>(prim, "typeName")
}
pub fn specifier(&self, prim: impl Into<sdf::Path>) -> Result<Option<sdf::Specifier>> {
self.field::<sdf::Specifier>(prim.into().prim_path(), sdf::FieldKey::Specifier)
}
pub fn kind(&self, prim: impl Into<sdf::Path>) -> Result<Option<String>> {
self.field::<String>(prim.into().prim_path(), sdf::FieldKey::Kind)
}
pub fn is_active(&self, prim: impl Into<sdf::Path>) -> Result<bool> {
let prim = prim.into().prim_path();
if prim == sdf::Path::abs_root() {
return Ok(true);
}
if !self.has_spec(&prim)? {
return Ok(false);
}
for path in Self::prim_ancestors_inclusive(prim) {
if self
.field::<bool>(&path, sdf::FieldKey::Active)?
.is_some_and(|active| !active)
{
return Ok(false);
}
}
Ok(true)
}
pub fn is_loaded(&self, prim: impl Into<sdf::Path>) -> Result<bool> {
let prim = prim.into().prim_path();
if !self.is_active(&prim)? {
return Ok(false);
}
if self.initial_load_set.load_payloads() {
return Ok(true);
}
for path in Self::prim_ancestors_inclusive(prim) {
if self.has_payload(&path)? {
return Ok(false);
}
}
Ok(true)
}
pub fn is_defined(&self, prim: impl Into<sdf::Path>) -> Result<bool> {
let prim = prim.into().prim_path();
if prim == sdf::Path::abs_root() {
return Ok(true);
}
if !self.has_spec(&prim)? {
return Ok(false);
}
for path in Self::prim_ancestors_inclusive(prim) {
if !matches!(self.specifier(path)?, Some(sdf::Specifier::Def | sdf::Specifier::Class)) {
return Ok(false);
}
}
Ok(true)
}
pub fn is_abstract(&self, prim: impl Into<sdf::Path>) -> Result<bool> {
let prim = prim.into().prim_path();
if prim == sdf::Path::abs_root() || !self.has_spec(&prim)? {
return Ok(false);
}
for path in Self::prim_ancestors_inclusive(prim) {
if self.specifier(path)? == Some(sdf::Specifier::Class) {
return Ok(true);
}
}
Ok(false)
}
pub fn has_composition_arc(&self, prim: impl Into<sdf::Path>) -> Result<bool> {
let prim = prim.into().prim_path();
if !self.population_mask.includes(&prim) {
return Ok(false);
}
self.try_or_handle(|cache| cache.has_composition_arc(&prim))
}
pub fn is_instance(&self, prim: impl Into<sdf::Path>) -> Result<bool> {
let prim = prim.into().prim_path();
if prim == sdf::Path::abs_root() || !self.has_spec(&prim)? {
return Ok(false);
}
if !self.field::<bool>(&prim, sdf::FieldKey::Instanceable)?.unwrap_or(false) {
return Ok(false);
}
self.has_composition_arc(&prim)
}
pub fn is_model(&self, prim: impl Into<sdf::Path>) -> Result<bool> {
Ok(self.model_kind(prim.into())?.is_some())
}
pub fn is_group(&self, prim: impl Into<sdf::Path>) -> Result<bool> {
Ok(matches!(self.model_kind(prim.into())?, Some("group" | "assembly")))
}
pub fn is_component(&self, prim: impl Into<sdf::Path>) -> Result<bool> {
Ok(self.model_kind(prim.into())? == Some("component"))
}
pub fn is_subcomponent(&self, prim: impl Into<sdf::Path>) -> Result<bool> {
Ok(self.kind(prim)?.as_deref() == Some("subcomponent"))
}
pub fn prim_status(&self, prim: impl Into<sdf::Path>) -> Result<PrimStatus> {
self.prim_status_masked(&prim.into().prim_path(), PrimStatus::all())
}
fn prim_status_masked(&self, prim: &sdf::Path, mask: PrimStatus) -> Result<PrimStatus> {
let mut status = PrimStatus::empty();
if mask.contains(PrimStatus::ACTIVE) {
status.set(PrimStatus::ACTIVE, self.is_active(prim)?);
}
if mask.contains(PrimStatus::LOADED) {
status.set(PrimStatus::LOADED, self.is_loaded(prim)?);
}
if mask.contains(PrimStatus::DEFINED) {
status.set(PrimStatus::DEFINED, self.is_defined(prim)?);
}
if mask.contains(PrimStatus::ABSTRACT) {
status.set(PrimStatus::ABSTRACT, self.is_abstract(prim)?);
}
if mask.contains(PrimStatus::INSTANCE) {
status.set(PrimStatus::INSTANCE, self.is_instance(prim)?);
}
if mask.contains(PrimStatus::MODEL) {
status.set(PrimStatus::MODEL, self.is_model(prim)?);
}
Ok(status)
}
fn model_kind(&self, prim: sdf::Path) -> Result<Option<&'static str>> {
let prim = prim.prim_path();
if prim == sdf::Path::abs_root() || !self.has_spec(&prim)? {
return Ok(None);
}
let leaf = match self.kind(&prim)?.as_deref() {
Some("group") => "group",
Some("assembly") => "assembly",
Some("component") => "component",
_ => return Ok(None),
};
let Some(parent) = prim.parent() else {
return Ok(Some(leaf));
};
for ancestor in Self::prim_ancestors_inclusive(parent) {
if !matches!(self.kind(ancestor)?.as_deref(), Some("group" | "assembly")) {
return Ok(None);
}
}
Ok(Some(leaf))
}
fn has_payload(&self, prim: &sdf::Path) -> Result<bool> {
let payload = self.field::<sdf::Value>(prim, sdf::FieldKey::Payload)?;
Ok(match payload {
Some(sdf::Value::Payload(payload)) => Self::payload_has_target(&payload),
Some(sdf::Value::PayloadListOp(op)) => op.reduced().flatten().iter().any(Self::payload_has_target),
_ => false,
})
}
fn payload_has_target(payload: &sdf::Payload) -> bool {
!payload.asset_path.is_empty() || !payload.prim_path.is_empty()
}
fn filter_child_names(&self, parent: &sdf::Path, children: Vec<String>) -> Vec<String> {
if self.population_mask.is_all() {
return children;
}
children
.into_iter()
.filter(|name| {
parent
.append_path(name.as_str())
.is_ok_and(|child| self.population_mask.includes(&child))
})
.collect()
}
fn prim_ancestors_inclusive(start: sdf::Path) -> impl Iterator<Item = sdf::Path> {
std::iter::successors(Some(start), sdf::Path::parent).take_while(|p| *p != sdf::Path::abs_root())
}
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, visitor: impl FnMut(&sdf::Path)) -> Result<()> {
self.traverse_with_predicate(PrimPredicate::DEFAULT, visitor)
}
pub fn traverse_all(&self, visitor: impl FnMut(&sdf::Path)) -> Result<()> {
self.traverse_with_predicate(PrimPredicate::ALL, visitor)
}
pub fn traverse_with_predicate(&self, predicate: PrimPredicate, mut visitor: impl FnMut(&sdf::Path)) -> Result<()> {
let needed = predicate.consulted_bits();
let mut stack = vec![sdf::Path::abs_root()];
while let Some(path) = stack.pop() {
if path != sdf::Path::abs_root() {
let status = self.prim_status_masked(&path, needed)?;
if predicate.matches(status) {
visitor(&path);
}
if predicate.prunes_descendants(status) {
continue;
}
}
let children = self.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: ar::Resolver = ar::DefaultResolver,
E: Fn(CompositionError) -> Result<()> = StrictErrorHandler,
> {
resolver: R,
on_error: E,
variant_fallbacks: pcp::VariantFallbackMap,
session_layer: Option<String>,
initial_load_set: InitialLoadSet,
population_mask: StagePopulationMask,
interpolation_type: InterpolationType,
}
impl StageBuilder {
fn new() -> Self {
Self {
resolver: ar::DefaultResolver::new(),
on_error: strict_composition_error,
variant_fallbacks: pcp::VariantFallbackMap::new(),
session_layer: None,
initial_load_set: InitialLoadSet::LoadAll,
population_mask: StagePopulationMask::all(),
interpolation_type: InterpolationType::default(),
}
}
}
impl<R: ar::Resolver, E: Fn(CompositionError) -> Result<()>> StageBuilder<R, E> {
pub fn resolver<R2: ar::Resolver>(self, resolver: R2) -> StageBuilder<R2, E> {
StageBuilder {
resolver,
on_error: self.on_error,
variant_fallbacks: self.variant_fallbacks,
session_layer: self.session_layer,
initial_load_set: self.initial_load_set,
population_mask: self.population_mask,
interpolation_type: self.interpolation_type,
}
}
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,
initial_load_set: self.initial_load_set,
population_mask: self.population_mask,
interpolation_type: self.interpolation_type,
}
}
pub fn interpolation_type(mut self, mode: InterpolationType) -> Self {
self.interpolation_type = mode;
self
}
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 initial_load_set(mut self, load_set: InitialLoadSet) -> Self {
self.initial_load_set = load_set;
self
}
pub fn population_mask(mut self, mask: StagePopulationMask) -> Self {
self.population_mask = mask;
self
}
pub fn open(self, root_path: &str) -> Result<Stage>
where
R: 'static,
E: 'static,
{
let session_layers = self.collect_optional_session_layers()?;
let root_layers = self.collect_layers(root_path)?;
let session_layer_count = session_layers.len();
let layers: Vec<sdf::Layer> = session_layers.into_iter().chain(root_layers).collect();
Ok(self.make_stage(layers, session_layer_count))
}
pub fn in_memory(self, identifier: impl Into<String>) -> Result<Stage>
where
R: 'static,
E: 'static,
{
let identifier = identifier.into();
let session_layers = self.collect_optional_session_layers()?;
let session_layer_count = session_layers.len();
let layers: Vec<sdf::Layer> = session_layers
.into_iter()
.chain(std::iter::once(sdf::Layer::new_anonymous(identifier)))
.collect();
Ok(self.make_stage(layers, session_layer_count))
}
fn collect_layers(&self, path: &str) -> Result<Vec<sdf::Layer>> {
let include_prim_dependency = |p: &sdf::Path| self.population_mask.includes(p);
let collector = layer::Collector::new(&self.resolver)
.load_payloads(self.initial_load_set.load_payloads())
.on_error(|e| (self.on_error)(CompositionError::Layer(e)));
if self.population_mask.is_all() {
collector.collect(path)
} else {
collector.prim_dependency_filter(&include_prim_dependency).collect(path)
}
}
fn collect_optional_session_layers(&self) -> Result<Vec<sdf::Layer>> {
match self.session_layer.as_deref() {
Some(p) => self.collect_layers(p),
None => Ok(Vec::new()),
}
}
fn make_stage(self, layers: Vec<sdf::Layer>, session_layer_count: usize) -> Stage
where
R: 'static,
E: 'static,
{
let on_error = self.on_error;
let load_payloads = self.initial_load_set.load_payloads();
let stack = pcp::LayerStack::new(layers, session_layer_count, Box::new(self.resolver), load_payloads);
let on_composition_error = Box::new(move |e: pcp::Error| on_error(CompositionError::Pcp(e)));
let edit_target = EditTarget::for_layer_index(session_layer_count);
Stage {
graph: RefCell::new(pcp::Cache::new(stack, self.variant_fallbacks)),
initial_load_set: self.initial_load_set,
population_mask: self.population_mask,
on_composition_error,
interpolation_type: Cell::new(self.interpolation_type),
edit_target: Cell::new(edit_target),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
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_uses_default_predicate() -> 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/CubeActive"]);
Ok(())
}
#[test]
fn traverse_all_visits_every_composed_prim() -> Result<()> {
let path = composition_path("active.usda");
let stage = Stage::open(&path)?;
let mut prims = Vec::new();
stage.traverse_all(|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", sdf::FieldKey::Active)?;
assert_eq!(active, Some(false));
let active = stage.field::<bool>("/World/CubeActive", sdf::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::<sdf::Value>("/World", sdf::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 = sdf::Path::new("/World/Cube")?.append_property("primvars:displayColor")?;
let value: Option<sdf::Value> = stage.field(&prop_path, sdf::FieldKey::Default)?;
assert!(value.is_some(), "displayColor should have a composed value");
let value = value.unwrap();
let base_red = sdf::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", sdf::FieldKey::Active)?;
assert_eq!(inactive, Some(false));
let active = stage.field::<bool>("/World/CubeActive", sdf::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 = sdf::Path::new("/World/cubeWithSetColor")?.append_property("primvars:displayColor")?;
let value: Option<sdf::Value> = stage.field(&prop, sdf::FieldKey::Default)?;
assert!(value.is_some());
let green = sdf::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 = sdf::Path::new("/World/Sphere")?.append_property("radius")?;
let value = stage.field::<f64>(&prop, sdf::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 = sdf::Path::new("/World/cubeScene/specializes")?.append_property("primvars:displayColor")?;
let value: Option<sdf::Value> = stage.field(&prop, sdf::FieldKey::Default)?;
assert!(value.is_some());
let red = sdf::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", sdf::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", sdf::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", sdf::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 = sdf::Path::new("/NoSelection")?.append_property("complexity")?;
let value = stage.field::<f64>(&prop, sdf::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 = sdf::Path::new("/Root")?.append_property("complexity")?;
let value = stage.field::<f64>(&prop, sdf::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(sdf::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(sdf::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(sdf::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 = sdf::Path::new("/World")?.append_property("radius")?;
let value = stage.field::<f64>(&prop, sdf::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 = sdf::Path::new("/World")?.append_property("visibility")?;
let value = stage.field::<String>(&prop, sdf::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 = sdf::Path::new("/World")?.append_property("name")?;
let value = stage.field::<String>(&prop, sdf::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(())
}
#[test]
fn api_schemas_returns_applied_schemas() -> Result<()> {
let stage = Stage::open("fixtures/api_schemas.usda")?;
let geo = sdf::Path::new("/World/Geo")?;
let schemas = stage.api_schemas(&geo)?;
assert!(schemas.contains(&"MaterialBindingAPI".to_string()));
assert!(schemas.contains(&"SkelBindingAPI".to_string()));
Ok(())
}
#[test]
fn api_schemas_compose_list_ops() -> Result<()> {
let dir = tempfile::tempdir()?;
std::fs::write(
dir.path().join("weak.usda"),
r#"#usda 1.0
def Xform "World"
{
def Mesh "Geo" (
append apiSchemas = ["WeakAPI", "RemovedAPI"]
)
{
}
}
"#,
)?;
std::fs::write(
dir.path().join("middle.usda"),
r#"#usda 1.0
(
subLayers = [
@weak.usda@
]
)
over "World"
{
over "Geo" (
prepend apiSchemas = ["StrongAPI"]
)
{
}
}
"#,
)?;
let root = dir.path().join("root.usda");
std::fs::write(
&root,
r#"#usda 1.0
(
subLayers = [
@middle.usda@
]
)
over "World"
{
over "Geo" (
delete apiSchemas = ["RemovedAPI"]
)
{
}
}
"#,
)?;
let stage = Stage::open(root.to_str().expect("utf-8 temp path"))?;
let schemas = stage.api_schemas(&sdf::Path::new("/World/Geo")?)?;
assert_eq!(schemas, vec!["StrongAPI".to_string(), "WeakAPI".to_string()]);
Ok(())
}
#[test]
fn api_schemas_compose_reorder_list_op() -> Result<()> {
let dir = tempfile::tempdir()?;
std::fs::write(
dir.path().join("weak.usda"),
r#"#usda 1.0
def Xform "World"
{
def Mesh "Geo" (
apiSchemas = ["A", "B", "C"]
)
{
}
}
"#,
)?;
let root = dir.path().join("root.usda");
std::fs::write(
&root,
r#"#usda 1.0
(
subLayers = [
@weak.usda@
]
)
over "World"
{
over "Geo" (
reorder apiSchemas = ["C", "A"]
)
{
}
}
"#,
)?;
let stage = Stage::open(root.to_str().expect("utf-8 temp path"))?;
let schemas = stage.api_schemas(&sdf::Path::new("/World/Geo")?)?;
assert_eq!(schemas, vec!["C".to_string(), "B".to_string(), "A".to_string()]);
Ok(())
}
#[test]
fn api_schemas_via_inherit() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
std::fs::write(
&root,
r#"#usda 1.0
class "_Base" (
prepend apiSchemas = ["BaseAPI"]
)
{
}
def Xform "World"
{
def Mesh "Geo" (
inherits = </_Base>
prepend apiSchemas = ["LocalAPI"]
)
{
}
}
"#,
)?;
let stage = Stage::open(root.to_str().expect("utf-8 temp path"))?;
let geo = sdf::Path::new("/World/Geo")?;
assert_eq!(
stage.api_schemas(&geo)?,
vec!["LocalAPI".to_string(), "BaseAPI".to_string()],
);
assert!(stage.has_api_schema(&geo, "BaseAPI")?);
assert!(stage.has_api_schema(&geo, "LocalAPI")?);
Ok(())
}
#[test]
fn api_schemas_via_reference() -> Result<()> {
let dir = tempfile::tempdir()?;
std::fs::write(
dir.path().join("asset.usda"),
r#"#usda 1.0
(
defaultPrim = "Source"
)
def Mesh "Source" (
prepend apiSchemas = ["AssetAPI"]
)
{
}
"#,
)?;
let root = dir.path().join("root.usda");
std::fs::write(
&root,
r#"#usda 1.0
def Xform "World"
{
def "Geo" (
references = @asset.usda@
prepend apiSchemas = ["LocalAPI"]
)
{
}
}
"#,
)?;
let stage = Stage::open(root.to_str().expect("utf-8 temp path"))?;
let geo = sdf::Path::new("/World/Geo")?;
assert_eq!(
stage.api_schemas(&geo)?,
vec!["LocalAPI".to_string(), "AssetAPI".to_string()],
);
Ok(())
}
#[test]
fn api_schemas_via_variant() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
std::fs::write(
&root,
r#"#usda 1.0
def Xform "World"
{
def Mesh "Geo" (
variants = {
string mode = "full"
}
prepend variantSets = "mode"
prepend apiSchemas = ["LocalAPI"]
)
{
variantSet "mode" = {
"full" (
prepend apiSchemas = ["VariantAPI"]
) {
}
"empty" {
}
}
}
}
"#,
)?;
let stage = Stage::open(root.to_str().expect("utf-8 temp path"))?;
let geo = sdf::Path::new("/World/Geo")?;
let schemas = stage.api_schemas(&geo)?;
assert!(
schemas.contains(&"VariantAPI".to_string()),
"variant contribution missing: {schemas:?}",
);
assert!(
schemas.contains(&"LocalAPI".to_string()),
"local contribution missing: {schemas:?}",
);
Ok(())
}
#[test]
fn api_schemas_property_path() -> Result<()> {
let stage = Stage::open("fixtures/api_schemas.usda")?;
let prim = sdf::Path::new("/World/Geo")?;
let prop = sdf::Path::new("/World/Geo.points")?;
assert_eq!(stage.api_schemas(&prop)?, stage.api_schemas(&prim)?);
Ok(())
}
#[test]
fn api_schemas_empty_for_prim_without_schemas() -> Result<()> {
let stage = Stage::open("fixtures/api_schemas.usda")?;
let props = sdf::Path::new("/World/Props")?;
assert!(stage.api_schemas(&props)?.is_empty());
Ok(())
}
#[test]
fn has_api_schema_matches_applied() -> Result<()> {
let stage = Stage::open("fixtures/api_schemas.usda")?;
let geo = sdf::Path::new("/World/Geo")?;
assert!(stage.has_api_schema(&geo, "MaterialBindingAPI")?);
assert!(!stage.has_api_schema(&geo, "SkelRootAPI")?);
Ok(())
}
#[test]
fn type_name_returns_prim_type() -> Result<()> {
let stage = Stage::open("fixtures/api_schemas.usda")?;
assert_eq!(
stage.type_name(&sdf::Path::new("/World/Geo")?)?,
Some("Mesh".to_string())
);
assert_eq!(stage.type_name(&sdf::Path::new("/World")?)?, Some("Xform".to_string()));
Ok(())
}
fn open_stage_queries_fixture() -> Result<Stage> {
Stage::open("fixtures/stage_queries.usda")
}
#[test]
fn active_loaded() -> Result<()> {
let stage = open_stage_queries_fixture()?;
assert!(stage.is_active("/World/ActiveParent/Child")?);
assert!(stage.is_loaded("/World/ActiveParent/Child")?);
assert!(!stage.is_active("/World/InactiveParent")?);
assert!(!stage.is_active("/World/InactiveParent/Child")?);
assert!(!stage.is_loaded("/World/InactiveParent/Child")?);
assert!(!stage.is_active("/World/Missing")?);
Ok(())
}
#[test]
fn load_none() -> Result<()> {
let path = composition_path("payload/payload_same_folder.usda");
let loaded = Stage::open(&path)?;
assert_eq!(loaded.layer_count(), 2);
assert!(loaded.is_loaded("/World")?);
assert_eq!(loaded.prim_children("/World")?, vec!["Cube"]);
let unloaded = Stage::builder()
.initial_load_set(InitialLoadSet::LoadNone)
.open(&path)?;
assert_eq!(unloaded.initial_load_set(), InitialLoadSet::LoadNone);
assert_eq!(unloaded.layer_count(), 1);
assert!(!unloaded.is_loaded("/World")?);
assert_eq!(unloaded.prim_children("/World")?, Vec::<String>::new());
let mut prims = Vec::new();
unloaded.traverse(|p| prims.push(p.as_str().to_string()))?;
assert!(prims.is_empty());
Ok(())
}
#[test]
fn mask_traverse() -> Result<()> {
let stage = Stage::builder()
.population_mask(StagePopulationMask::new(["/World/ActiveParent/Child"]))
.open("fixtures/stage_queries.usda")?;
assert_eq!(stage.root_prims()?, vec!["World"]);
assert_eq!(stage.prim_children("/World")?, vec!["ActiveParent"]);
assert_eq!(stage.prim_children("/World/ActiveParent")?, vec!["Child"]);
assert!(stage.has_spec("/World")?);
assert!(stage.has_spec("/World/ActiveParent/Child")?);
assert!(!stage.has_spec("/World/Group")?);
assert_eq!(stage.kind("/World/Group")?, None);
let mut prims = Vec::new();
stage.traverse_all(|p| prims.push(p.as_str().to_string()))?;
assert_eq!(
prims,
vec!["/World", "/World/ActiveParent", "/World/ActiveParent/Child"]
);
Ok(())
}
#[test]
fn mask_skips_dependency() -> Result<()> {
let path = composition_path("references/reference_invalid.usda");
let stage = Stage::builder()
.population_mask(StagePopulationMask::new(["/World/cube"]))
.open(&path)?;
assert_eq!(stage.root_prims()?, vec!["World"]);
assert_eq!(stage.prim_children("/World")?, vec!["cube"]);
assert!(!stage.has_spec("/World/invalid_reference")?);
Ok(())
}
#[test]
fn defined_abstract() -> Result<()> {
let stage = open_stage_queries_fixture()?;
assert_eq!(stage.specifier("/World/OverOnly")?, Some(sdf::Specifier::Over));
assert!(stage.is_defined("/World/ActiveParent/Child")?);
assert!(!stage.is_defined("/World/OverOnly")?);
assert!(!stage.is_defined("/World/OverParent/Child")?);
assert!(stage.is_defined("/World/ClassParent/Child")?);
assert!(stage.is_abstract("/World/ClassParent")?);
assert!(stage.is_abstract("/World/ClassParent/Child")?);
assert!(!stage.is_abstract("/World/ActiveParent/Child")?);
Ok(())
}
#[test]
fn instance_flag() -> Result<()> {
let stage = open_stage_queries_fixture()?;
assert!(stage.has_composition_arc("/World/Instance")?);
assert!(stage.is_instance("/World/Instance")?);
assert!(!stage.has_composition_arc("/World/InstanceableNoArc")?);
assert!(!stage.is_instance("/World/InstanceableNoArc")?);
Ok(())
}
#[test]
fn model_hierarchy() -> Result<()> {
let stage = open_stage_queries_fixture()?;
assert_eq!(stage.kind("/World")?, Some("assembly".to_string()));
assert!(stage.is_model("/World")?);
assert!(stage.is_group("/World")?);
assert!(stage.is_model("/World/Group")?);
assert!(stage.is_group("/World/Group")?);
assert!(stage.is_model("/World/Group/Component")?);
assert!(stage.is_component("/World/Group/Component")?);
assert!(!stage.is_model("/World/Group/Subcomponent")?);
assert!(stage.is_subcomponent("/World/Group/Subcomponent")?);
assert_eq!(
stage.kind("/World/InvalidComponentParent/Component")?,
Some("component".to_string())
);
assert!(!stage.is_model("/World/InvalidComponentParent/Component")?);
assert!(!stage.is_component("/World/InvalidComponentParent/Component")?);
Ok(())
}
#[test]
fn prim_status_bits() -> Result<()> {
let stage = open_stage_queries_fixture()?;
assert_eq!(
stage.prim_status("/World/ClassParent/Child")?,
PrimStatus::ACTIVE | PrimStatus::LOADED | PrimStatus::DEFINED | PrimStatus::ABSTRACT
);
assert_eq!(
stage.prim_status("/World/Instance")?,
PrimStatus::ACTIVE | PrimStatus::LOADED | PrimStatus::DEFINED | PrimStatus::INSTANCE
);
Ok(())
}
#[test]
fn traverse_default() -> Result<()> {
let stage = open_stage_queries_fixture()?;
let mut prims = Vec::new();
stage.traverse(|p| prims.push(p.as_str().to_string()))?;
assert!(prims.contains(&"/World".to_string()));
assert!(prims.contains(&"/World/ActiveParent".to_string()));
assert!(prims.contains(&"/World/ActiveParent/Child".to_string()));
assert!(prims.contains(&"/World/Instance".to_string()));
assert!(!prims.contains(&"/World/InactiveParent".to_string()));
assert!(!prims.contains(&"/World/InactiveParent/Child".to_string()));
assert!(!prims.contains(&"/World/OverOnly".to_string()));
assert!(!prims.contains(&"/World/OverParent".to_string()));
assert!(!prims.contains(&"/World/OverParent/Child".to_string()));
assert!(!prims.contains(&"/World/ClassParent".to_string()));
assert!(!prims.contains(&"/World/ClassParent/Child".to_string()));
Ok(())
}
#[test]
fn traverse_all_predicate() -> Result<()> {
let stage = open_stage_queries_fixture()?;
let mut prims = Vec::new();
stage.traverse_with_predicate(PrimPredicate::ALL, |p| prims.push(p.as_str().to_string()))?;
assert!(prims.contains(&"/World/InactiveParent".to_string()));
assert!(prims.contains(&"/World/InactiveParent/Child".to_string()));
assert!(prims.contains(&"/World/OverOnly".to_string()));
assert!(prims.contains(&"/World/OverParent/Child".to_string()));
assert!(prims.contains(&"/World/ClassParent".to_string()));
assert!(prims.contains(&"/World/ClassParent/Child".to_string()));
Ok(())
}
#[test]
fn custom_predicate() -> Result<()> {
let stage = open_stage_queries_fixture()?;
let predicate = PrimPredicate::new(PrimStatus::ACTIVE | PrimStatus::DEFINED, PrimStatus::empty());
let mut prims = Vec::new();
stage.traverse_with_predicate(predicate, |p| prims.push(p.as_str().to_string()))?;
assert!(prims.contains(&"/World/ClassParent".to_string()));
assert!(prims.contains(&"/World/ClassParent/Child".to_string()));
assert!(!prims.contains(&"/World/InactiveParent".to_string()));
assert!(!prims.contains(&"/World/OverOnly".to_string()));
Ok(())
}
fn in_memory_stage() -> Result<Stage> {
Stage::builder().in_memory("anon.usda")
}
#[test]
fn define_prim() -> Result<()> {
let stage = in_memory_stage()?;
stage.define_prim("/World")?.set_type_name("Xform")?;
stage.define_prim("/World/Mesh")?.set_type_name("Mesh")?;
assert_eq!(stage.spec_type("/World")?, Some(sdf::SpecType::Prim));
assert_eq!(stage.spec_type("/World/Mesh")?, Some(sdf::SpecType::Prim));
assert_eq!(
stage.field::<sdf::Value>("/World", sdf::FieldKey::TypeName)?,
Some(sdf::Value::Token("Xform".into())),
);
Ok(())
}
#[test]
fn authoring_invalidates_cached_miss() -> Result<()> {
let stage = in_memory_stage()?;
assert!(!stage.has_spec("/World")?);
stage.define_prim("/World")?.set_type_name("Xform")?;
assert!(stage.has_spec("/World")?);
assert_eq!(
stage.field::<sdf::Value>("/World", sdf::FieldKey::TypeName)?,
Some(sdf::Value::Token("Xform".into())),
);
Ok(())
}
#[test]
fn override_prim() -> Result<()> {
let stage = in_memory_stage()?;
stage.override_prim("/A/B")?;
assert_eq!(
stage.field::<sdf::Value>("/A", sdf::FieldKey::Specifier)?,
Some(sdf::Value::Specifier(sdf::Specifier::Over)),
);
assert_eq!(
stage.field::<sdf::Value>("/A/B", sdf::FieldKey::Specifier)?,
Some(sdf::Value::Specifier(sdf::Specifier::Over)),
);
Ok(())
}
#[test]
fn create_attribute() -> Result<()> {
let stage = in_memory_stage()?;
stage.define_prim("/Sphere")?.set_type_name("Sphere")?;
stage.create_attribute("/Sphere.radius", "double")?;
assert_eq!(stage.spec_type("/Sphere.radius")?, Some(sdf::SpecType::Attribute));
assert_eq!(
stage.field::<sdf::Value>("/Sphere.radius", sdf::FieldKey::TypeName)?,
Some(sdf::Value::Token("double".into())),
);
assert_eq!(
stage.field::<sdf::Value>("/Sphere.radius", sdf::FieldKey::Custom)?,
Some(sdf::Value::Bool(true)),
);
Ok(())
}
#[test]
fn create_relationship() -> Result<()> {
let stage = in_memory_stage()?;
stage.define_prim("/Mesh")?.set_type_name("Mesh")?;
stage
.create_relationship("/Mesh.material:binding")?
.set_variability(sdf::Variability::Uniform)?;
assert_eq!(
stage.spec_type("/Mesh.material:binding")?,
Some(sdf::SpecType::Relationship)
);
assert_eq!(
stage.field::<sdf::Value>("/Mesh.material:binding", sdf::FieldKey::Custom)?,
Some(sdf::Value::Bool(true)),
);
Ok(())
}
#[test]
fn author_default_prim() -> Result<()> {
let stage = in_memory_stage()?;
stage.set_default_prim("World")?;
stage.define_prim("/World")?.set_type_name("Xform")?;
assert_eq!(stage.default_prim().as_deref(), Some("World"));
Ok(())
}
#[test]
fn default_prim_targets_root() -> Result<()> {
let session = fixture_path("session_layer.usda");
let stage = Stage::builder().session_layer(&session).in_memory("anon.usda")?;
stage.set_edit_target(EditTarget::for_layer_index(0))?; stage.set_default_prim("World")?;
assert_eq!(stage.default_prim().as_deref(), Some("World"));
Ok(())
}
#[test]
fn default_prim_rejects_path() -> Result<()> {
let stage = in_memory_stage()?;
let err = stage.set_default_prim("/World").unwrap_err();
assert!(matches!(
err,
StageAuthoringError::Layer(sdf::AuthoringError::InvalidPath { .. })
));
Ok(())
}
#[test]
fn default_prim_accepts_nested() -> Result<()> {
let stage = in_memory_stage()?;
stage.set_default_prim("World/Mesh")?;
assert_eq!(stage.default_prim().as_deref(), Some("World/Mesh"));
Ok(())
}
#[test]
fn read_only_rejects_authoring() -> Result<()> {
let stage = Stage::open(&composition_path("subLayer/sublayer_same_folder.usda"))?;
let err = stage.define_prim("/X").err().expect("expected ReadOnly error");
assert!(matches!(
err,
StageAuthoringError::Layer(sdf::AuthoringError::ReadOnly { .. })
));
Ok(())
}
#[test]
fn read_only_default_prim() -> Result<()> {
let stage = Stage::open(&composition_path("subLayer/sublayer_same_folder.usda"))?;
let err = stage.set_default_prim("World").unwrap_err();
assert!(matches!(
err,
StageAuthoringError::Layer(sdf::AuthoringError::ReadOnly { .. })
));
Ok(())
}
#[test]
fn edit_target_out_of_range() -> Result<()> {
let stage = in_memory_stage()?;
let err = stage.set_edit_target(EditTarget::for_layer_index(99)).unwrap_err();
assert!(matches!(err, StageAuthoringError::LayerOutOfRange { .. }));
Ok(())
}
#[test]
fn in_memory_session_layer() -> Result<()> {
let session = fixture_path("session_layer.usda");
let stage = Stage::builder().session_layer(&session).in_memory("anon.usda")?;
assert!(stage.has_session_layer());
assert_eq!(stage.layer_count(), 2);
assert_eq!(stage.edit_target().layer_index(), 1);
stage.define_prim("/World")?.set_type_name("Xform")?;
assert_eq!(stage.spec_type("/World")?, Some(sdf::SpecType::Prim));
Ok(())
}
}