use super::{Attribute, Relationship, Stage, StageAuthoringError};
use crate::{pcp, sdf};
#[derive(Clone)]
pub struct Prim {
stage: Stage,
path: sdf::Path,
}
impl Prim {
pub(crate) fn new(stage: &Stage, path: sdf::Path) -> Self {
Self {
stage: stage.clone(),
path,
}
}
pub fn path(&self) -> &sdf::Path {
&self.path
}
pub fn stage(&self) -> &Stage {
&self.stage
}
pub fn set_type_name(self, name: impl Into<String>) -> Result<Self, StageAuthoringError> {
let name = name.into();
self.edit(&[sdf::FieldKey::TypeName], |spec| spec.set_type_name(name))
}
pub fn set_active(self, active: bool) -> Result<Self, StageAuthoringError> {
self.edit(&[sdf::FieldKey::Active], |spec| spec.set_active(active))
}
pub fn set_kind(self, kind: impl Into<String>) -> Result<Self, StageAuthoringError> {
let kind = kind.into();
self.edit(&[sdf::FieldKey::Kind], |spec| spec.set_kind(kind))
}
pub fn set_hidden(self, hidden: bool) -> Result<Self, StageAuthoringError> {
self.edit(&[sdf::FieldKey::Hidden], |spec| spec.set_hidden(hidden))
}
pub fn set_instanceable(self, instanceable: bool) -> Result<Self, StageAuthoringError> {
self.edit(&[sdf::FieldKey::Instanceable], |spec| {
spec.set_instanceable(instanceable)
})
}
pub fn add_applied_schema(self, name: impl Into<String>) -> Result<Self, StageAuthoringError> {
let name = name.into();
self.stage.with_target_layer_at(&self.path, |layer, path| {
let data = layer.writable_data_mut()?;
match data.spec_mut(&path).and_then(|s| s.as_prim_mut()) {
Some(mut spec) => {
spec.add_applied_schema(name)?;
let mut cl = sdf::ChangeList::new();
cl.entry_mut(&path)
.info_changed
.insert(sdf::FieldKey::ApiSchemas.as_str());
Ok(cl)
}
None => Err(sdf::AuthoringError::InvalidPath {
path: path.clone(),
reason: "no prim spec at path on the edit target layer",
}),
}
})?;
Ok(self)
}
pub fn set_metadata(self, key: &'static str, value: impl Into<sdf::Value>) -> Result<Self, StageAuthoringError> {
let value = value.into();
self.update_metadata(key, |_| value)
}
pub fn update_metadata<F>(self, key: &'static str, f: F) -> Result<Self, StageAuthoringError>
where
F: FnOnce(Option<sdf::Value>) -> sdf::Value,
{
self.stage.with_target_layer_at(&self.path, |layer, path| {
let data = layer.writable_data_mut()?;
match data.spec_mut(&path).and_then(|s| s.as_prim_mut()) {
Some(mut spec) => {
let value = f(spec.get(key).cloned());
spec.add(key, value);
let mut cl = sdf::ChangeList::new();
cl.entry_mut(&path).info_changed.insert(key);
Ok(cl)
}
None => Err(sdf::AuthoringError::InvalidPath {
path: path.clone(),
reason: "no prim spec at path on the edit target layer",
}),
}
})?;
Ok(self)
}
pub fn create_attribute(&self, name: &str, type_name: impl Into<String>) -> Result<Attribute, StageAuthoringError> {
let attr_path = self.path.append_property(name).map_err(|_| {
StageAuthoringError::Layer(sdf::AuthoringError::InvalidPath {
path: sdf::Path::from(format!("{}.{}", self.path, name).as_str()),
reason: "attribute name is not a valid property name",
})
})?;
self.stage.create_attribute(attr_path, type_name)
}
pub fn create_relationship(&self, name: &str) -> Result<Relationship, StageAuthoringError> {
let rel_path = self.path.append_property(name).map_err(|_| {
StageAuthoringError::Layer(sdf::AuthoringError::InvalidPath {
path: sdf::Path::from(format!("{}.{}", self.path, name).as_str()),
reason: "relationship name is not a valid property name",
})
})?;
self.stage.create_relationship(rel_path)
}
pub fn author_relationship_targets<I, P>(&self, name: &str, targets: I) -> Result<Relationship, StageAuthoringError>
where
I: IntoIterator<Item = P>,
P: Into<sdf::Path>,
{
self.create_relationship(name)?
.set_custom(false)?
.set_targets(targets.into_iter().map(Into::into))
}
pub fn append_to_uniform_token_array(&self, name: &str, value: impl Into<String>) -> anyhow::Result<bool> {
let value = value.into();
let attr_path = self.path.append_property(name)?;
let existing: Vec<String> = match self.stage.field::<sdf::Value>(&attr_path, sdf::FieldKey::Default)? {
Some(sdf::Value::TokenVec(v) | sdf::Value::StringVec(v)) => v,
Some(sdf::Value::TokenListOp(op)) => op.flatten(),
Some(sdf::Value::StringListOp(op)) => op.flatten(),
_ => Vec::new(),
};
if existing.iter().any(|t| t == &value) {
return Ok(false);
}
let mut updated = existing;
updated.push(value);
self.stage
.create_attribute(attr_path, "token[]")?
.set_variability(sdf::Variability::Uniform)?
.set_custom(false)?
.set(sdf::Value::TokenVec(updated))?;
Ok(true)
}
pub fn clip_sets(&self) -> anyhow::Result<Vec<String>> {
let Some(sdf::Value::Dictionary(sets)) = self.stage.field::<sdf::Value>(&self.path, sdf::FieldKey::Clips)?
else {
return Ok(Vec::new());
};
let mut names: Vec<String> = sets.into_keys().collect();
names.sort();
Ok(names)
}
pub fn has_clips(&self) -> anyhow::Result<bool> {
Ok(!self.clip_sets()?.is_empty())
}
pub fn type_name(&self) -> anyhow::Result<Option<String>> {
self.stage.field::<String>(&self.path, sdf::FieldKey::TypeName)
}
pub fn specifier(&self) -> anyhow::Result<Option<sdf::Specifier>> {
self.stage.field::<sdf::Specifier>(&self.path, sdf::FieldKey::Specifier)
}
pub fn kind(&self) -> anyhow::Result<Option<String>> {
self.stage.field::<String>(&self.path, sdf::FieldKey::Kind)
}
pub fn custom_data(&self) -> anyhow::Result<Option<sdf::Value>> {
self.stage.field::<sdf::Value>(&self.path, sdf::FieldKey::CustomData)
}
pub fn api_schemas(&self) -> anyhow::Result<Vec<String>> {
self.stage
.masked(&self.path, |g, cache| cache.api_schemas(g, &self.path))
}
pub fn has_api_schema(&self, name: &str) -> anyhow::Result<bool> {
Ok(self.api_schemas()?.iter().any(|s| s == name))
}
pub fn is_active(&self) -> anyhow::Result<bool> {
self.all_ancestors(|stage, path| Ok(stage.field::<bool>(path, sdf::FieldKey::Active)?.unwrap_or(true)))
}
pub fn is_loaded(&self) -> anyhow::Result<bool> {
if !self.is_active()? {
return Ok(false);
}
if self.stage.load().load_payloads() {
return Ok(true);
}
for path in Stage::prim_ancestors_inclusive(self.path.clone()) {
if has_payload(&self.stage, &path)? {
return Ok(false);
}
}
Ok(true)
}
pub fn is_defined(&self) -> anyhow::Result<bool> {
self.all_ancestors(|stage, path| {
let specifier = stage.field::<sdf::Specifier>(path, sdf::FieldKey::Specifier)?;
Ok(matches!(specifier, Some(sdf::Specifier::Def | sdf::Specifier::Class)))
})
}
pub fn is_abstract(&self) -> anyhow::Result<bool> {
if self.path == sdf::Path::abs_root() || !self.stage.has_spec(&self.path)? {
return Ok(false);
}
for path in Stage::prim_ancestors_inclusive(self.path.clone()) {
if self.stage.field::<sdf::Specifier>(&path, sdf::FieldKey::Specifier)? == Some(sdf::Specifier::Class) {
return Ok(true);
}
}
Ok(false)
}
pub fn has_composition_arc(&self) -> anyhow::Result<bool> {
self.stage
.masked(&self.path, |g, cache| cache.has_composition_arc(g, &self.path))
}
pub fn is_instance(&self) -> anyhow::Result<bool> {
if self.path == sdf::Path::abs_root()
|| !self.stage.mask().includes(&self.path)
|| !self.stage.has_spec(&self.path)?
{
return Ok(false);
}
if !self
.stage
.field::<bool>(&self.path, sdf::FieldKey::Instanceable)?
.unwrap_or(false)
{
return Ok(false);
}
self.has_composition_arc()
}
pub fn is_model(&self) -> anyhow::Result<bool> {
Ok(self.model_kind()?.is_some())
}
pub fn is_group(&self) -> anyhow::Result<bool> {
Ok(matches!(self.model_kind()?, Some("group" | "assembly")))
}
pub fn is_component(&self) -> anyhow::Result<bool> {
Ok(self.model_kind()? == Some("component"))
}
pub fn is_subcomponent(&self) -> anyhow::Result<bool> {
Ok(self.kind()?.as_deref() == Some("subcomponent"))
}
pub fn prototype(&self) -> anyhow::Result<Option<sdf::Path>> {
self.stage
.masked(&self.path, |g, cache| cache.prototype_of(g, &self.path))
}
pub fn instances(&self) -> Vec<sdf::Path> {
let mask = self.stage.mask();
let instances = self.stage.cache().instances_of(&self.path);
instances
.into_iter()
.filter(|instance| mask.includes(instance))
.collect()
}
pub fn is_prototype(&self) -> bool {
self.stage.cache().is_prototype(&self.path)
}
pub fn is_in_prototype(&self) -> bool {
self.stage.cache().is_in_prototype(&self.path)
}
pub fn is_instance_proxy(&self) -> anyhow::Result<bool> {
self.stage
.masked(&self.path, |g, cache| cache.is_instance_proxy(g, &self.path))
}
pub fn prim_in_prototype(&self) -> anyhow::Result<Option<Prim>> {
let path = self
.stage
.masked(&self.path, |g, cache| cache.prim_in_prototype(g, &self.path))?;
Ok(path.map(|p| Prim::new(&self.stage, p)))
}
fn model_kind(&self) -> anyhow::Result<Option<&'static str>> {
if self.path == sdf::Path::abs_root() || !self.stage.has_spec(&self.path)? {
return Ok(None);
}
let leaf = match self.kind()?.as_deref() {
Some("group") => "group",
Some("assembly") => "assembly",
Some("component") => "component",
_ => return Ok(None),
};
let Some(parent) = self.path.parent() else {
return Ok(Some(leaf));
};
for ancestor in Stage::prim_ancestors_inclusive(parent) {
let kind = self.stage.field::<String>(&ancestor, sdf::FieldKey::Kind)?;
if !matches!(kind.as_deref(), Some("group" | "assembly")) {
return Ok(None);
}
}
Ok(Some(leaf))
}
fn all_ancestors<F>(&self, keep: F) -> anyhow::Result<bool>
where
F: Fn(&Stage, &sdf::Path) -> anyhow::Result<bool>,
{
if self.path == sdf::Path::abs_root() {
return Ok(true);
}
if !self.stage.has_spec(&self.path)? {
return Ok(false);
}
for path in Stage::prim_ancestors_inclusive(self.path.clone()) {
if !keep(&self.stage, &path)? {
return Ok(false);
}
}
Ok(true)
}
pub fn prim_stack(&self) -> anyhow::Result<Vec<(String, sdf::Path)>> {
self.stage.with_cache(|g, c| c.prim_stack(g, &self.path))
}
pub fn prim_index(&self) -> PrimIndexRef {
PrimIndexRef::new(&self.stage, self.path.clone())
}
pub fn attribute(&self, name: &str) -> Attribute {
Attribute::new(&self.stage, self.property_path(name))
}
pub fn relationship(&self, name: &str) -> Relationship {
Relationship::new(&self.stage, self.property_path(name))
}
pub fn child_names(&self) -> anyhow::Result<Vec<String>> {
let names = self
.stage
.masked(&self.path, |g, cache| cache.prim_children(g, &self.path))?;
Ok(self.stage.filter_child_names(&self.path, names))
}
pub fn children(&self) -> anyhow::Result<Vec<Prim>> {
Ok(self
.child_names()?
.into_iter()
.filter_map(|name| self.path.append_path(name.as_str()).ok())
.map(|path| Prim::new(&self.stage, path))
.collect())
}
pub fn property_names(&self) -> anyhow::Result<Vec<String>> {
self.stage
.masked(&self.path, |g, cache| cache.prim_properties(g, &self.path))
}
pub fn attributes(&self) -> anyhow::Result<Vec<Attribute>> {
Ok(self
.properties_of_type(sdf::SpecType::Attribute)?
.into_iter()
.map(|path| Attribute::new(&self.stage, path))
.collect())
}
pub fn relationships(&self) -> anyhow::Result<Vec<Relationship>> {
Ok(self
.properties_of_type(sdf::SpecType::Relationship)?
.into_iter()
.map(|path| Relationship::new(&self.stage, path))
.collect())
}
pub fn is_valid(&self) -> anyhow::Result<bool> {
self.stage.has_spec(&self.path)
}
fn properties_of_type(&self, ty: sdf::SpecType) -> anyhow::Result<Vec<sdf::Path>> {
let mut paths = Vec::new();
for name in self.property_names()? {
let path = self.property_path(&name);
if self.stage.spec_type(&path)? == Some(ty) {
paths.push(path);
}
}
Ok(paths)
}
fn property_path(&self, name: &str) -> sdf::Path {
self.path.append_property(name).unwrap_or_else(|_| self.path.clone())
}
pub fn variant_sets(&self) -> VariantSets {
VariantSets::new(&self.stage, self.path.clone())
}
fn edit<F>(self, fields: &[sdf::FieldKey], f: F) -> Result<Self, StageAuthoringError>
where
F: FnOnce(&mut sdf::PrimSpecMut<'_>),
{
let info_changed: Vec<&'static str> = fields.iter().map(sdf::FieldKey::as_str).collect();
self.stage.with_target_layer_at(&self.path, |layer, path| {
let data = layer.writable_data_mut()?;
match data.spec_mut(&path).and_then(|s| s.as_prim_mut()) {
Some(mut spec) => {
f(&mut spec);
let mut cl = sdf::ChangeList::new();
let entry = cl.entry_mut(&path);
for name in &info_changed {
entry.info_changed.insert(name);
}
Ok(cl)
}
None => Err(sdf::AuthoringError::InvalidPath {
path: path.clone(),
reason: "no prim spec at path on the edit target layer",
}),
}
})?;
Ok(self)
}
}
fn has_payload(stage: &Stage, prim: &sdf::Path) -> anyhow::Result<bool> {
let payload = stage.field::<sdf::Value>(prim, sdf::FieldKey::Payload)?;
Ok(match payload {
Some(sdf::Value::Payload(payload)) => payload_has_target(&payload),
Some(sdf::Value::PayloadListOp(op)) => op.reduced().flatten().iter().any(payload_has_target),
_ => false,
})
}
fn payload_has_target(payload: &sdf::Payload) -> bool {
!payload.asset_path.is_empty() || !payload.prim_path.is_empty()
}
#[derive(Clone)]
pub struct PrimIndexRef {
stage: Stage,
path: sdf::Path,
}
impl PrimIndexRef {
pub(super) fn new(stage: &Stage, path: sdf::Path) -> Self {
Self {
stage: stage.clone(),
path,
}
}
pub fn graph(&self) -> anyhow::Result<pcp::PrimIndex> {
self.stage.with_cache(|g, c| Ok(c.index(g, &self.path)?.clone()))
}
pub fn child_names(&self) -> anyhow::Result<(Vec<String>, Vec<String>)> {
self.stage.with_cache(|g, c| c.compute_prim_child_names(g, &self.path))
}
}
#[derive(Clone)]
pub struct VariantSets {
stage: Stage,
prim: sdf::Path,
}
impl VariantSets {
pub(super) fn new(stage: &Stage, prim: sdf::Path) -> Self {
Self {
stage: stage.clone(),
prim,
}
}
pub fn get_all_variant_selections(&self) -> anyhow::Result<Vec<(String, String)>> {
self.stage.with_cache(|g, c| c.variant_selections(g, &self.prim))
}
}
#[cfg(test)]
mod tests {
use crate::sdf;
use crate::usd::Stage;
fn stage() -> anyhow::Result<Stage> {
Stage::builder().in_memory("anon.usda")
}
#[test]
fn handles_outlive_stage() -> anyhow::Result<()> {
let prims: Vec<super::Prim> = {
let stage = stage()?;
stage.define_prim("/A")?.set_type_name("Xform")?;
stage.define_prim("/B")?.set_type_name("Scope")?;
vec![stage.prim_at("/A"), stage.prim_at("/B")]
};
assert_eq!(prims[0].path().as_str(), "/A");
let type_name = prims[1].stage().prim_at(prims[1].path()).type_name()?;
assert_eq!(type_name.as_deref(), Some("Scope"));
Ok(())
}
#[test]
fn clip_introspection() -> anyhow::Result<()> {
let path = format!(
"{}/vendor/core-spec-supplemental-release_dec2025/value_resolution/tests/assets/clip_basic/entry.usd",
env!("CARGO_MANIFEST_DIR")
);
let stage = Stage::open(&path)?;
let model = super::Prim::new(&stage, sdf::path("/Model")?);
assert!(model.has_clips()?);
assert_eq!(model.clip_sets()?, vec!["default".to_string()]);
let size = super::Attribute::new(&stage, sdf::path("/Model.size")?);
assert_eq!(size.get_at(10.0)?, Some(sdf::Value::Float(10.0)));
let other = super::Prim::new(&stage, sdf::path("/Model2")?);
assert!(!other.has_clips()?);
Ok(())
}
#[test]
fn prim_prototype_handle() -> anyhow::Result<()> {
let path = format!("{}/fixtures/instancing_shared.usda", env!("CARGO_MANIFEST_DIR"));
let stage = Stage::open(&path)?;
let a = super::Prim::new(&stage, sdf::path("/A")?);
assert!(a.is_instance()?);
assert!(a.prototype()?.is_some());
assert!(!a.is_in_prototype());
let proto = super::Prim::new(&stage, sdf::path("/Proto")?);
assert!(!proto.is_instance()?);
assert!(proto.prototype()?.is_none());
Ok(())
}
#[test]
fn prim_specifier() -> anyhow::Result<()> {
let stage = stage()?;
stage.define_prim("/Def")?;
stage.override_prim("/Over")?;
assert_eq!(stage.prim_at("/Def").specifier()?, Some(sdf::Specifier::Def));
assert_eq!(stage.prim_at("/Over").specifier()?, Some(sdf::Specifier::Over));
Ok(())
}
#[test]
fn prim_custom_data() -> anyhow::Result<()> {
let stage = stage()?;
let dict = sdf::Value::Dictionary([("note".to_string(), sdf::Value::String("hi".into()))].into());
stage
.define_prim("/A")?
.set_metadata(sdf::FieldKey::CustomData.as_str(), dict)?;
let Some(sdf::Value::Dictionary(read)) = stage.prim_at("/A").custom_data()? else {
panic!("customData should resolve to a dictionary");
};
assert_eq!(read.get("note"), Some(&sdf::Value::String("hi".into())));
assert!(stage.prim_at("/B").custom_data()?.is_none());
Ok(())
}
#[test]
fn prim_chain() -> anyhow::Result<()> {
let stage = stage()?;
stage
.define_prim("/World")?
.set_type_name("Xform")?
.set_kind("group")?
.set_active(true)?;
assert_eq!(
stage.field::<sdf::Value>("/World", sdf::FieldKey::TypeName)?,
Some(sdf::Value::Token("Xform".into())),
);
assert_eq!(stage.prim_at("/World").kind()?.as_deref(), Some("group"));
Ok(())
}
#[test]
fn add_api_schema() -> anyhow::Result<()> {
let stage = stage()?;
let prim = stage.define_prim("/World")?.add_applied_schema("MaterialBindingAPI")?;
assert_eq!(
stage.prim_at(prim.path()).api_schemas()?,
vec!["MaterialBindingAPI".to_string()]
);
assert!(stage.prim_at(prim.path()).has_api_schema("MaterialBindingAPI")?);
Ok(())
}
#[test]
fn add_api_schema_merges() -> anyhow::Result<()> {
let stage = stage()?;
stage.define_prim("/World")?;
stage.with_target_layer_at(&sdf::Path::new("/World").expect("valid path"), |layer, _path| {
let data = layer.writable_data_mut()?;
let spec = data
.spec_mut(&sdf::Path::new("/World").expect("valid path"))
.expect("prim spec");
spec.add(
sdf::FieldKey::ApiSchemas,
sdf::Value::TokenListOp(sdf::TokenListOp {
appended_items: vec!["ExistingAPI".to_string()],
..Default::default()
}),
);
let mut cl = sdf::ChangeList::new();
cl.entry_mut(&sdf::Path::new("/World").expect("valid path"))
.info_changed
.insert(sdf::FieldKey::ApiSchemas.as_str());
Ok(cl)
})?;
stage
.override_prim("/World")?
.add_applied_schema("ExistingAPI")?
.add_applied_schema("NewAPI")?;
let local = stage.field::<sdf::Value>("/World", sdf::FieldKey::ApiSchemas)?;
let Some(sdf::Value::TokenListOp(op)) = local else {
panic!("expected apiSchemas TokenListOp");
};
assert_eq!(op.appended_items, vec!["ExistingAPI".to_string()]);
assert_eq!(op.prepended_items, vec!["NewAPI".to_string()]);
Ok(())
}
#[test]
fn set_prim_metadata() -> anyhow::Result<()> {
let stage = stage()?;
let mut dict = std::collections::HashMap::new();
dict.insert("hint".to_string(), sdf::Value::String("v".to_string()));
stage
.define_prim("/World")?
.set_metadata("customData", sdf::Value::Dictionary(dict))?;
let Some(sdf::Value::Dictionary(read)) = stage.field::<sdf::Value>("/World", "customData")? else {
panic!("expected customData dictionary");
};
assert_eq!(read.get("hint"), Some(&sdf::Value::String("v".to_string())));
Ok(())
}
#[test]
fn update_metadata_reads_local() -> anyhow::Result<()> {
let stage = stage()?;
let mut dict = std::collections::HashMap::new();
dict.insert("a".to_string(), sdf::Value::Int(1));
stage
.define_prim("/World")?
.set_metadata("customData", sdf::Value::Dictionary(dict))?;
stage.define_prim("/World")?.update_metadata("customData", |local| {
let Some(sdf::Value::Dictionary(mut d)) = local else {
panic!("expected local customData dictionary");
};
d.insert("b".to_string(), sdf::Value::Int(2));
sdf::Value::Dictionary(d)
})?;
let Some(sdf::Value::Dictionary(read)) = stage.field::<sdf::Value>("/World", "customData")? else {
panic!("expected customData dictionary");
};
assert_eq!(read.get("a"), Some(&sdf::Value::Int(1)));
assert_eq!(read.get("b"), Some(&sdf::Value::Int(2)));
Ok(())
}
}