Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions fixtures/arc_enumeration.usda
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#usda 1.0
(
defaultPrim = "Root"
)

def Xform "Root"
{
def "WithReference" (
references = @./ref_target.usda@</World>
)
{
}

def "WithPayload" (
payload = @./ref_target.usda@</World>
Comment on lines +9 to +15
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixture references/payloads </World> in ref_target.usda, but fixtures/ref_target.usda defines defaultPrim = "Source" and the root prim is /Source (no /World). If this mismatch is intentional (to model a broken arc), it would help to add a brief comment in the fixture; otherwise consider changing the prim path to </Source> so the fixture is self-consistent.

Suggested change
references = @./ref_target.usda@</World>
)
{
}
def "WithPayload" (
payload = @./ref_target.usda@</World>
references = @./ref_target.usda@</Source>
)
{
}
def "WithPayload" (
payload = @./ref_target.usda@</Source>

Copilot uses AI. Check for mistakes.
)
{
}

def "NoArcs"
{
}
}
148 changes: 148 additions & 0 deletions src/stage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,90 @@ impl Stage {
self.composed_children(&path.into(), ChildrenKey::PropertyChildren)
}

/// Returns the references declared on a prim, collected from every layer
/// that has a spec at the given path.
///
/// Unlike the composition engine's internal arc resolution (which follows
/// references across files), this method reads only the **as-authored**
/// `references` field of each layer spec. It is intended for asset-health
/// inspection tasks such as detecting broken or missing references before
/// attempting full composition.
///
/// Duplicate entries from multiple layers are included as-is; callers can
/// de-duplicate if needed. The ordering is strongest-layer-first.
Comment on lines +111 to +117
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docs say this returns the as-authored references field, but the implementation uses ListOp::iter(), which explicitly excludes deleted_items and ordered_items (see sdf::ListOp::iter). If the intent is to enumerate all authored list-edit entries, the docs should be tightened (e.g., “opinion-contributing entries”), or the implementation should include deleted/ordered items as well.

Suggested change
/// references across files), this method reads only the **as-authored**
/// `references` field of each layer spec. It is intended for asset-health
/// inspection tasks such as detecting broken or missing references before
/// attempting full composition.
///
/// Duplicate entries from multiple layers are included as-is; callers can
/// de-duplicate if needed. The ordering is strongest-layer-first.
/// references across files), this method reads only the authored
/// `references` entries surfaced by the underlying list-op iteration for
/// each layer spec. It is intended for asset-health inspection tasks such
/// as detecting broken or missing references before attempting full
/// composition.
///
/// This does not expose every authored list-edit bucket: deleted and
/// ordered entries are not included. Duplicate entries from multiple
/// layers are included as-is; callers can de-duplicate if needed. The
/// ordering is strongest-layer-first.

Copilot uses AI. Check for mistakes.
///
/// # Example
///
/// ```no_run
/// use openusd::{ar::DefaultResolver, Stage, sdf::Path};
///
/// let resolver = DefaultResolver::new();
/// let stage = Stage::open(&resolver, "scene.usda").unwrap();
/// let refs = stage.references_in(Path::new("/World/Prop").unwrap());
/// for r in &refs {
/// println!("asset: {}, prim: {}", r.asset_path, r.prim_path);
/// }
/// ```
pub fn references_in(&self, path: impl Into<Path>) -> Vec<Reference> {
let path = path.into();
let mut result = Vec::new();

for layer in &self.layers {
if !layer.has_field(&path, FieldKey::References.as_str()) {
continue;
}
let Ok(value) = layer.get(&path, FieldKey::References.as_str()) else {
continue;
};
if let Value::ReferenceListOp(list_op) = value.into_owned() {
result.extend(list_op.iter().cloned());
}
Comment on lines +135 to +144
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

references_in silently skips layer.get(...) errors (even after has_field), which can hide real decode/IO issues (e.g. USDC value decoding failures) and return incomplete results without any signal. Consider changing the API to Result<Vec<Reference>> and propagating get errors (or at least distinguishing missing-field from other errors and surfacing the latter).

Copilot uses AI. Check for mistakes.
}

result
}

/// Returns the payloads declared on a prim, collected from every layer
/// that has a spec at the given path.
///
/// Mirrors [`references_in`](Self::references_in): reads the as-authored
/// `payload` field from each layer spec without following composition arcs.
/// Both `Payload` (single) and `PayloadListOp` (list-edit) field forms are
/// handled.
///
/// # Example
///
/// ```no_run
/// use openusd::{ar::DefaultResolver, Stage, sdf::Path};
///
/// let resolver = DefaultResolver::new();
/// let stage = Stage::open(&resolver, "scene.usda").unwrap();
/// let payloads = stage.payloads_in(Path::new("/World/Prop").unwrap());
/// for p in &payloads {
/// println!("asset: {}", p.asset_path);
/// }
/// ```
pub fn payloads_in(&self, path: impl Into<Path>) -> Vec<Payload> {
let path = path.into();
let mut result = Vec::new();

for layer in &self.layers {
if !layer.has_field(&path, FieldKey::Payload.as_str()) {
continue;
}
let Ok(value) = layer.get(&path, FieldKey::Payload.as_str()) else {
continue;
};
match value.into_owned() {
Value::Payload(p) => result.push(p),
Value::PayloadListOp(list_op) => result.extend(list_op.iter().cloned()),
_ => {}
}
Comment on lines +174 to +185
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

payloads_in also swallows layer.get(...) errors and will silently return partial results if decoding fails. For consistency with other Stage queries (and to avoid hiding layer corruption), consider returning Result<Vec<Payload>> and propagating non-missing errors after the has_field check.

Copilot uses AI. Check for mistakes.
}

result
}

/// Returns `true` if any layer has a spec at the given composed path.
pub fn has_spec(&self, path: impl Into<Path>) -> bool {
!self.prim_index(&path.into()).is_empty()
Expand Down Expand Up @@ -1035,4 +1119,68 @@ mod tests {

Ok(())
}

// --- Stage::references_in() / Stage::payloads_in() ---

/// references_in() should return the declared reference for a prim that has one.
#[test]
fn references_in_returns_declared_reference() -> Result<()> {
let path = fixture_path("arc_enumeration.usda");
let resolver = DefaultResolver::new();
let stage = Stage::open(&resolver, &path)?;

let refs = stage.references_in(Path::new("/Root/WithReference")?);
assert_eq!(refs.len(), 1, "expected exactly one reference");
assert!(
refs[0].asset_path.ends_with("ref_target.usda"),
"asset_path should point to ref_target.usda, got: {}",
refs[0].asset_path
);

Ok(())
}

/// references_in() should return an empty vec for a prim with no references.
#[test]
fn references_in_empty_for_prim_without_references() -> Result<()> {
let path = fixture_path("arc_enumeration.usda");
let resolver = DefaultResolver::new();
let stage = Stage::open(&resolver, &path)?;

let refs = stage.references_in(Path::new("/Root/NoArcs")?);
assert!(refs.is_empty(), "prim with no references should return empty vec");

Ok(())
}

/// payloads_in() should return the declared payload for a prim that has one.
#[test]
fn payloads_in_returns_declared_payload() -> Result<()> {
let path = fixture_path("arc_enumeration.usda");
let resolver = DefaultResolver::new();
let stage = Stage::open(&resolver, &path)?;

let payloads = stage.payloads_in(Path::new("/Root/WithPayload")?);
assert_eq!(payloads.len(), 1, "expected exactly one payload");
assert!(
payloads[0].asset_path.ends_with("ref_target.usda"),
"asset_path should point to ref_target.usda, got: {}",
payloads[0].asset_path
);

Ok(())
}

/// payloads_in() should return an empty vec for a prim with no payloads.
#[test]
fn payloads_in_empty_for_prim_without_payloads() -> Result<()> {
let path = fixture_path("arc_enumeration.usda");
let resolver = DefaultResolver::new();
let stage = Stage::open(&resolver, &path)?;

let payloads = stage.payloads_in(Path::new("/Root/NoArcs")?);
assert!(payloads.is_empty(), "prim with no payloads should return empty vec");

Ok(())
}
}
Loading