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
8 changes: 8 additions & 0 deletions fixtures/up_axis_absent.usda
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#usda 1.0
(
defaultPrim = "Root"
)

def Xform "Root"
{
}
9 changes: 9 additions & 0 deletions fixtures/up_axis_y.usda
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#usda 1.0
(
upAxis = "Y"
defaultPrim = "Root"
)

def Xform "Root"
{
}
9 changes: 9 additions & 0 deletions fixtures/up_axis_z.usda
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#usda 1.0
(
upAxis = "Z"
defaultPrim = "Root"
)

def Xform "Root"
{
}
2 changes: 2 additions & 0 deletions src/sdf/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ pub enum FieldKey {
VariantSetNames,
EndFrame,
StartFrame,
UpAxis,
}

impl From<FieldKey> for &'static str {
Expand Down Expand Up @@ -138,6 +139,7 @@ impl FieldKey {
FieldKey::VariantSetNames => "variantSetNames",
FieldKey::EndFrame => "endFrame",
FieldKey::StartFrame => "startFrame",
FieldKey::UpAxis => "upAxis",
}
}
}
Expand Down
81 changes: 81 additions & 0 deletions src/stage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,20 @@ use crate::compose::prim_index::{ArcType, Node, PrimIndex};
use crate::sdf::schema::{ChildrenKey, FieldKey};
use crate::sdf::{AbstractData, ListOp, Path, Payload, Reference, SpecType, Value};

/// The scene's up-axis, as stored in the root layer's `upAxis` metadata.
///
/// USD defines two valid values: `"Y"` (Y-up, the default for many DCC tools)
/// and `"Z"` (Z-up, used by e.g. OpenUSD's own reference scenes).
///
/// See <https://openusd.org/dev/api/group___usd_geom_up_axis__group.html>
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UpAxis {
/// Y axis points up.
Y,
/// Z axis points up.
Z,
}

/// A composed USD stage.
///
/// Owns the loaded layer stack and provides composed access to prims,
Expand Down Expand Up @@ -85,6 +99,35 @@ impl Stage {
self.field::<String>(&Path::abs_root(), FieldKey::DefaultPrim).ok()?
}

/// Returns the `upAxis` metadata from the root layer, if set.
///
/// USD defines two valid values: `"Y"` and `"Z"`. Any other authored
/// value (malformed data) is treated as `None` rather than an error so
/// that callers can continue inspecting the rest of the stage.
Comment on lines +102 to +106
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 docstring says this returns upAxis metadata "from the root layer", but the implementation uses self.field(...) which resolves the strongest opinion across the entire layer stack (it may come from a sublayer if the strongest/root layer doesn’t author it). Consider clarifying the wording (e.g., “from the pseudo-root (composed across layers)” or “from the root layer stack”) to match actual behavior.

Copilot uses AI. Check for mistakes.
///
/// # Example
///
/// ```no_run
/// use openusd::{ar::DefaultResolver, Stage};
/// use openusd::stage::UpAxis;
///
/// let resolver = DefaultResolver::new();
/// let stage = Stage::open(&resolver, "scene.usda").unwrap();
/// match stage.up_axis() {
/// Some(UpAxis::Y) => println!("Y-up"),
/// Some(UpAxis::Z) => println!("Z-up"),
/// None => println!("upAxis not set"),
/// }
/// ```
pub fn up_axis(&self) -> Option<UpAxis> {
let raw = self.field::<String>(&Path::abs_root(), FieldKey::UpAxis).ok()??;
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.

ok()?? works here but is a bit hard to read at a glance (it’s relying on Option-? twice to drop both Err and None). Consider rewriting this in a more explicit/idiomatic way (e.g. using .ok().flatten()? or a small match) to improve maintainability.

Suggested change
let raw = self.field::<String>(&Path::abs_root(), FieldKey::UpAxis).ok()??;
let raw = self
.field::<String>(&Path::abs_root(), FieldKey::UpAxis)
.ok()
.flatten()?;

Copilot uses AI. Check for mistakes.
match raw.as_str() {
"Y" => Some(UpAxis::Y),
"Z" => Some(UpAxis::Z),
_ => None,
}
Comment on lines +124 to +128
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 function’s behavior for malformed authored values (anything other than "Y"/"Z" => None) is part of the stated contract, but there isn’t a test covering that case. Adding a small fixture (e.g. upAxis = "X") and a corresponding test would help prevent regressions.

Copilot uses AI. Check for mistakes.
}

/// Returns the composed list of root prim names (children of the pseudo-root).
pub fn root_prims(&self) -> Result<Vec<String>> {
self.prim_children(Path::abs_root())
Expand Down Expand Up @@ -1035,4 +1078,42 @@ mod tests {

Ok(())
}

// --- Stage::up_axis() ---

/// A stage with `upAxis = "Y"` should return `UpAxis::Y`.
#[test]
fn up_axis_y() -> Result<()> {
let path = fixture_path("up_axis_y.usda");
let resolver = DefaultResolver::new();
let stage = Stage::open(&resolver, &path)?;

assert_eq!(stage.up_axis(), Some(UpAxis::Y));

Ok(())
}

/// A stage with `upAxis = "Z"` should return `UpAxis::Z`.
#[test]
fn up_axis_z() -> Result<()> {
let path = fixture_path("up_axis_z.usda");
let resolver = DefaultResolver::new();
let stage = Stage::open(&resolver, &path)?;

assert_eq!(stage.up_axis(), Some(UpAxis::Z));

Ok(())
}

/// A stage without an `upAxis` metadata entry should return `None`.
#[test]
fn up_axis_absent_returns_none() -> Result<()> {
let path = fixture_path("up_axis_absent.usda");
let resolver = DefaultResolver::new();
let stage = Stage::open(&resolver, &path)?;

assert_eq!(stage.up_axis(), None);

Ok(())
}
}
Loading