use std::collections::hash_map::Entry;
use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
use anyhow::Result;
use crate::ar::Resolver;
use crate::sdf::expr;
use crate::sdf::schema::{ChildrenKey, FieldKey};
use crate::sdf::{self, AbstractData, LayerOffset, ListOp, Path, Payload, PayloadListOp, Reference, Value};
use super::mapping::MapFunction;
use super::prim_graph::{ArcType, Node, NodeFlags, NodeId, PrimIndexGraph};
use super::prim_indexer::BuildResult;
use super::{Error, LayerGraph, LayerId, VariantFallbackMap};
#[derive(Debug, Clone, Default)]
pub struct PrimIndex {
graph: PrimIndexGraph,
}
impl PrimIndex {
pub fn is_empty(&self) -> bool {
!self
.graph
.iter()
.any(|node| !node.is_inert() && !node.is_culled() && node.has_specs())
}
pub(crate) fn has_composition_arc(&self) -> bool {
self.arena()
.iter()
.any(|node| node.arc != ArcType::Root && !node.is_culled() && node.has_specs())
}
pub fn nodes(&self) -> impl DoubleEndedIterator<Item = &Node> + Clone + '_ {
self.all_nodes().filter(|node| !node.is_culled())
}
pub fn all_nodes(&self) -> impl DoubleEndedIterator<Item = &Node> + Clone + '_ {
self.ordered_nodes().filter(|node| !node.is_inert())
}
pub(crate) fn dependency_nodes(&self) -> impl Iterator<Item = &Node> + '_ {
self.ordered_nodes()
.filter(|node| !node.is_inert() || node.is_relocate_source())
}
fn ordered_nodes(&self) -> impl DoubleEndedIterator<Item = &Node> + Clone + '_ {
let nodes = &self.graph.nodes;
self.graph.strength_order.iter().map(move |id| &nodes[id.idx()])
}
pub(crate) fn nodes_with_ids(&self) -> impl DoubleEndedIterator<Item = (NodeId, &Node)> + '_ {
let nodes = &self.graph.nodes;
self.graph
.strength_order
.iter()
.map(move |&id| (id, &nodes[id.idx()]))
.filter(|(_, node)| !node.is_inert())
}
pub(crate) fn arena(&self) -> &[Node] {
&self.graph.nodes
}
pub(crate) fn graph(&self) -> &PrimIndexGraph {
&self.graph
}
pub fn root(&self) -> Option<NodeId> {
self.graph.root.is_valid().then_some(self.graph.root)
}
pub(crate) fn mark_permission_denied_under(&mut self, prefixes: &[Path]) {
if prefixes.is_empty() {
return;
}
for node in &mut self.graph.nodes {
if prefixes.iter().any(|prefix| node.path.has_prefix(prefix)) {
node.flags |= NodeFlags::PERMISSION_DENIED;
}
}
}
pub(crate) fn instance_local_nodes(&self, instance_depth: u16) -> Vec<bool> {
let nodes = &self.graph.nodes;
let mut local = vec![false; nodes.len()];
for (i, node) in nodes.iter().enumerate() {
debug_assert!(
node.parent.is_none_or(|p| p.idx() < i),
"instance partition requires every node's parent to precede it in the arena"
);
local[i] = match node.arc {
ArcType::Root => true,
ArcType::Reference | ArcType::Payload => {
node.namespace_depth < instance_depth && node.parent.is_some_and(|p| local[p.idx()])
}
_ => false,
};
}
local
}
pub(crate) fn mark_instance_local_inert(&mut self, instance_depth: u16) {
let local = self.instance_local_nodes(instance_depth);
for (node, &is_local) in self.graph.nodes.iter_mut().zip(local.iter()) {
if is_local {
node.flags |= NodeFlags::INERT;
}
}
}
pub(crate) fn rebase_root(&mut self, from: &Path, to: &Path) {
for node in &mut self.graph.nodes {
node.map_to_root = node.map_to_root.rebase_target(from, to);
node.map_to_parent = node.map_to_parent.rebase_target(from, to);
}
}
pub fn node(&self, id: NodeId) -> &Node {
&self.graph.nodes[id.idx()]
}
pub fn parent(&self, id: NodeId) -> Option<NodeId> {
self.node(id).parent
}
pub fn children(&self, id: NodeId) -> &[NodeId] {
&self.node(id).children
}
pub fn dump_to_string(&self) -> String {
use std::fmt::Write as _;
let mut rank = vec![0usize; self.graph.nodes.len()];
for (r, id) in self.graph.strength_order.iter().enumerate() {
rank[id.idx()] = r;
}
let mut out = String::new();
let mut stack: Vec<(NodeId, usize)> = self
.graph
.strength_order
.iter()
.rev()
.filter(|id| self.node(**id).parent.is_none())
.map(|&id| (id, 0))
.collect();
while let Some((id, depth)) = stack.pop() {
let node = self.node(id);
let _ = write!(
out,
"{:indent$}{:?} [{:?}] {} #{}",
"",
node.arc,
node.layer_id(),
node.path,
rank[id.idx()],
indent = depth * 4
);
let offset = node.map_to_root.time_offset();
if !offset.is_identity() {
let _ = write!(out, " offset=({},{})", offset.offset, offset.scale);
}
if let Some(origin) = node.origin {
if Some(origin) != node.parent {
let _ = write!(out, " origin={}", origin.0);
}
}
if !node.flags.is_empty() {
let _ = write!(out, " {:?}", node.flags);
}
out.push('\n');
for &child in node.children.iter().rev() {
stack.push((child, depth + 1));
}
}
out
}
#[cfg(test)]
pub(crate) fn push_node(&mut self, node: Node) {
let id = NodeId(self.graph.nodes.len() as u32);
self.graph.nodes.push(node);
self.graph.strength_order.push(id);
}
#[cfg(test)]
pub(crate) fn build_with_context(path: &Path, stack: &LayerGraph, ctx: &CompositionContext) -> BuildResult<Self> {
Self::build_with_cache(path, stack, ctx, &HashMap::new()).map(|(index, _errors)| index)
}
pub(crate) fn build_with_cache(
path: &Path,
stack: &LayerGraph,
ctx: &CompositionContext,
cached_indices: &HashMap<Path, PrimIndex>,
) -> BuildResult<(Self, Vec<Error>)> {
Self::build_with_cache_in(path, stack, ctx, cached_indices, stack.root_layer_stack(), true)
}
pub(crate) fn build_with_cache_in(
path: &Path,
stack: &LayerGraph,
ctx: &CompositionContext,
cached_indices: &HashMap<Path, PrimIndex>,
ambient: &[(LayerId, LayerOffset)],
ambient_is_root: bool,
) -> BuildResult<(Self, Vec<Error>)> {
if ambient_is_root {
if let Some(cached) = cached_indices.get(path) {
return Ok((cached.clone(), Vec::new()));
}
}
let indexer = super::prim_indexer::Indexer::new(stack, ctx, cached_indices, ambient, ambient_is_root);
let super::prim_indexer::BuildOutput { graph, errors } = indexer.build(path)?;
Ok((
PrimIndex {
graph: graph.unwrap_or_default(),
},
errors,
))
}
pub(crate) fn context_for_children(
&self,
stack: &LayerGraph,
parent_ctx: &CompositionContext,
) -> CompositionContext {
let selections = resolve_variant_selections_in(
self.nodes(),
stack,
&parent_ctx.variant_fallbacks,
&parent_ctx.selections,
);
let mut ancestor_arcs = parent_ctx.ancestor_arcs.clone();
for (_, node) in self.nodes_with_ids() {
if node.arc != ArcType::Root {
ancestor_arcs.push(AncestorArc {
map: node.map_to_root.clone(),
});
}
}
let mut merged_selections = parent_ctx.selections.clone();
for (k, v) in selections {
merged_selections.entry(k).or_insert(v);
}
CompositionContext {
selections: merged_selections,
ancestor_arcs,
variant_fallbacks: parent_ctx.variant_fallbacks.clone(),
instance_depth: parent_ctx.instance_depth,
denied_prefixes: parent_ctx.denied_prefixes.clone(),
}
}
pub(crate) fn variant_selections(&self) -> Vec<(String, String)> {
let mut selections: HashMap<String, String> = HashMap::new();
for node in self.all_nodes() {
if !node.path.is_prim_variant_selection_path() {
continue;
}
if let Some(sdf::PathElement::Variant { set, selection }) = node.path.last_element() {
selections
.entry(set.to_string())
.or_insert_with(|| selection.to_string());
}
}
let mut out: Vec<(String, String)> = selections.into_iter().collect();
out.sort();
out
}
}
#[derive(Debug, Clone, Default)]
pub(crate) struct CompositionContext {
pub selections: HashMap<String, String>,
pub ancestor_arcs: Vec<AncestorArc>,
pub variant_fallbacks: VariantFallbackMap,
pub instance_depth: Option<u16>,
pub denied_prefixes: Vec<Path>,
}
impl CompositionContext {
pub fn within_instance(&self) -> bool {
self.instance_depth.is_some()
}
}
#[derive(Debug, Clone)]
pub(crate) struct AncestorArc {
pub map: MapFunction,
}
fn resolve_variant_selections_in<'a>(
nodes: impl Iterator<Item = &'a Node> + Clone,
graph: &LayerGraph,
variant_fallbacks: &VariantFallbackMap,
seed: &HashMap<String, String>,
) -> HashMap<String, String> {
let mut selections: HashMap<String, String> = HashMap::new();
for (set_name, selection) in seed {
selections.entry(set_name.clone()).or_insert_with(|| selection.clone());
}
let mut ordered: Vec<&Node> = nodes.collect();
ordered.sort_by_key(|n| n.arc);
for node in &ordered {
for &(layer, _) in node.layer_stack() {
if let Ok(value) = graph.layer(layer).get(&node.path, FieldKey::VariantSelection.as_str()) {
if let Value::VariantSelectionMap(map) = value.into_owned() {
for (set_name, selection) in map {
selections.entry(set_name).or_insert(selection);
}
}
}
}
}
for node in &ordered {
for &(layer, _) in node.layer_stack() {
let data = graph.layer(layer);
let Ok(value) = data.get(&node.path, ChildrenKey::VariantSetChildren.as_str()) else {
continue;
};
let Value::TokenVec(set_names) = value.into_owned() else {
continue;
};
for set_name in set_names {
let Entry::Vacant(entry) = selections.entry(set_name) else {
continue;
};
let set_path = node.path.append_variant_selection(entry.key(), "");
let Ok(val) = data.get(&set_path, ChildrenKey::VariantChildren.as_str()) else {
continue;
};
let Value::TokenVec(variants) = val.into_owned() else {
continue;
};
let fallbacks = variant_fallbacks.get(entry.key());
if let Some(fb) = fallbacks.iter().find(|fb| variants.contains(fb)) {
entry.insert(fb.clone());
}
}
}
}
selections
}
fn compose_list_op_in<T, D, R, A>(
nodes: &[Node],
field: &str,
graph: &LayerGraph,
decode: D,
mut retime: R,
mut anchor: A,
) -> Result<Vec<T>>
where
T: Default + Clone + PartialEq,
D: Fn(Value) -> Option<ListOp<T>>,
R: FnMut(&mut T, LayerOffset, f64),
A: FnMut(&mut T, LayerId) -> f64,
{
let mut combined: Option<ListOp<T>> = None;
let mut folds: Vec<(T, LayerOffset, f64)> = Vec::new();
for node in nodes {
let mut seen_layers: HashSet<LayerId> = HashSet::new();
for &(layer, sub) in node.layer_stack() {
if !seen_layers.insert(layer) {
continue;
}
let Some(value) = graph.layer(layer).try_get(&node.path, field)? else {
continue;
};
let Some(mut list_op) = decode(value.into_owned()) else {
continue;
};
for item in list_op.iter_mut() {
let scale = anchor(item, layer);
if (!sub.is_identity() || scale != 1.0) && !folds.iter().any(|(i, _, _)| i == item) {
folds.push((item.clone(), sub, scale));
}
}
combined = Some(match combined {
Some(stronger) => stronger.combined_with(&list_op),
None => list_op,
});
}
}
let mut result = combined.map(|op| op.reduced().flatten()).unwrap_or_default();
for item in &mut result {
if let Some((_, sub, scale)) = folds.iter().find(|(i, _, _)| i == item) {
retime(item, *sub, *scale);
}
}
Ok(result)
}
pub(super) fn compose_arc_list_in<T: Default + Clone + PartialEq>(
nodes: &[Node],
field: FieldKey,
graph: &LayerGraph,
) -> Result<Vec<T>>
where
Value: TryInto<ListOp<T>>,
{
compose_list_op_in(
nodes,
field.as_str(),
graph,
|v| v.try_into().ok(),
|_, _, _| {},
|_, _| 1.0,
)
}
fn resolve_against_layer(asset_path: &str, layer: &sdf::Layer, resolver: &dyn Resolver) -> String {
let anchor = crate::ar::ResolvedPath::new(PathBuf::from(&layer.identifier));
resolver.create_identifier(asset_path, Some(&anchor))
}
fn arc_tcps_scale(introducing: &sdf::Layer, asset_path: &str, graph: &LayerGraph) -> f64 {
if asset_path.is_empty() {
return 1.0;
}
graph.find(asset_path).map_or(1.0, |target| {
super::effective_time_codes_per_second(introducing)
/ super::effective_time_codes_per_second(graph.layer(target))
})
}
fn anchor_asset_path(asset_path: &mut String, authoring_layer: &sdf::Layer, resolver: &dyn Resolver) {
if asset_path.is_empty() {
return;
}
*asset_path = resolve_against_layer(asset_path, authoring_layer, resolver);
}
#[allow(clippy::too_many_arguments)]
fn resolve_arc_asset_path(
asset_path: &mut String,
authoring_layer: LayerId,
graph: &LayerGraph,
resolver: &dyn Resolver,
expr_vars: &HashMap<String, Value>,
arc: ArcType,
site: &Path,
errors: &mut Vec<Error>,
) -> Option<f64> {
match expr::evaluate_asset_path(asset_path, expr_vars) {
Ok(resolved) => *asset_path = resolved,
Err(source) => {
errors.push(Error::InvalidExpression {
expression: asset_path.clone(),
arc,
site_path: site.clone(),
message: source.to_string(),
});
return None;
}
}
anchor_asset_path(asset_path, graph.layer(authoring_layer), resolver);
Some(arc_tcps_scale(graph.layer(authoring_layer), asset_path, graph))
}
pub(super) fn compose_references_in(
nodes: &[Node],
graph: &LayerGraph,
resolver: &dyn Resolver,
expr_vars: &HashMap<String, Value>,
site: &Path,
errors: &mut Vec<Error>,
) -> Result<Vec<Reference>> {
let mut refs = compose_list_op_in(
nodes,
FieldKey::References.as_str(),
graph,
|v| v.try_into().ok(),
|r: &mut Reference, sub, scale| {
if scale != 1.0 {
r.layer_offset = r.layer_offset.concatenate(&sdf::LayerOffset::scale_only(scale));
}
if !sub.is_identity() {
r.layer_offset = sub.concatenate(&r.layer_offset);
}
},
|r: &mut Reference, layer| {
resolve_arc_asset_path(
&mut r.asset_path,
layer,
graph,
resolver,
expr_vars,
ArcType::Reference,
site,
errors,
)
.unwrap_or(1.0)
},
)?;
refs.retain(|r| !expr::is_expression(&r.asset_path));
Ok(refs)
}
pub(super) fn collect_payloads_in(
nodes: &[Node],
graph: &LayerGraph,
resolver: &dyn Resolver,
expr_vars: &HashMap<String, Value>,
site: &Path,
errors: &mut Vec<Error>,
) -> Result<Vec<Payload>> {
let mut payloads = compose_list_op_in(
nodes,
FieldKey::Payload.as_str(),
graph,
|v| match v {
Value::Payload(p) => Some(PayloadListOp {
explicit: true,
explicit_items: vec![p],
..Default::default()
}),
Value::PayloadListOp(op) => Some(op),
_ => None,
},
|p: &mut Payload, sub, scale| {
if scale != 1.0 {
let offset = p.layer_offset.unwrap_or_default();
p.layer_offset = Some(offset.concatenate(&sdf::LayerOffset::scale_only(scale)));
}
if !sub.is_identity() {
p.layer_offset = Some(sub.concatenate(&p.layer_offset.unwrap_or_default()));
}
},
|p: &mut Payload, layer| {
resolve_arc_asset_path(
&mut p.asset_path,
layer,
graph,
resolver,
expr_vars,
ArcType::Payload,
site,
errors,
)
.unwrap_or(1.0)
},
)?;
payloads.retain(|p| !expr::is_expression(&p.asset_path));
Ok(payloads)
}
pub(super) fn find_layer_id<'a>(
asset_path: &str,
order: &[LayerId],
layer: impl Fn(LayerId) -> &'a sdf::Layer,
resolver: &dyn Resolver,
) -> Option<LayerId> {
let asset_path_ref = std::path::Path::new(asset_path);
let needle = asset_path_ref.strip_prefix(".").unwrap_or(asset_path_ref);
for &id in order {
let identifier = std::path::Path::new(layer(id).identifier());
if identifier == needle || identifier.ends_with(needle) {
return Some(id);
}
}
if needle.starts_with("..") {
for &anchor_id in order {
let resolved = resolve_against_layer(asset_path, layer(anchor_id), resolver);
if let Some(&id) = order.iter().find(|&&id| layer(id).identifier() == resolved) {
return Some(id);
}
}
}
None
}
#[cfg(test)]
mod tests {
use std::cmp::Ordering;
use super::*;
use crate::ar::DefaultResolver;
use crate::layer::Collector;
use anyhow::Result;
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())
}
fn load_layers(path: &str) -> Result<Vec<sdf::Layer>> {
let resolver = DefaultResolver::new();
Collector::new(&resolver).collect(path)
}
fn find(asset_path: &str, layers: &[sdf::Layer], resolver: &dyn Resolver) -> Option<usize> {
let ids: Vec<LayerId> = (0..layers.len() as u32).map(LayerId::from_raw).collect();
let position = |id: LayerId| ids.iter().position(|&x| x == id).unwrap();
find_layer_id(asset_path, &ids, |id| &layers[position(id)], resolver).map(position)
}
fn build(stack: &LayerGraph, prim: &str) -> PrimIndex {
build_with_fallbacks(stack, prim, VariantFallbackMap::new())
}
fn build_with_fallbacks(stack: &LayerGraph, prim: &str, fallbacks: VariantFallbackMap) -> PrimIndex {
let path = Path::from(prim);
let mut chain: Vec<Path> = Vec::new();
let mut p = Some(path.clone());
while let Some(pp) = p {
if pp == Path::abs_root() {
break;
}
chain.push(pp.clone());
p = pp.parent();
}
chain.reverse();
let mut cache: HashMap<Path, PrimIndex> = HashMap::new();
let mut parent_ctx = CompositionContext {
variant_fallbacks: fallbacks,
..CompositionContext::default()
};
let mut last = None;
for ancestor in &chain {
let (index, _errors) =
PrimIndex::build_with_cache(ancestor, stack, &parent_ctx, &cache).expect("index build failed");
parent_ctx = index.context_for_children(stack, &parent_ctx);
cache.insert(ancestor.clone(), index.clone());
last = Some(index);
}
last.expect("a non-empty namespace chain")
}
fn load_stack(path: &str) -> anyhow::Result<LayerGraph> {
let layers = load_layers(path)?;
Ok(LayerGraph::from_layers(
layers,
0,
Box::new(DefaultResolver::new()),
true,
))
}
fn one_layer_stack(root: Box<dyn sdf::AbstractData>) -> LayerGraph {
let layers = vec![sdf::Layer::new("root.usd", root)];
LayerGraph::from_layers(layers, 0, Box::new(DefaultResolver::new()), true)
}
fn two_layer_stack(root: Box<dyn sdf::AbstractData>, refl: Box<dyn sdf::AbstractData>) -> LayerGraph {
let layers = vec![sdf::Layer::new("root.usd", root), sdf::Layer::new("ref.usd", refl)];
LayerGraph::from_layers(layers, 0, Box::new(DefaultResolver::new()), true)
}
#[test]
fn single_layer_root_node() -> Result<()> {
let stack = load_stack(&composition_path("active.usda"))?;
let index = build(&stack, "/World");
assert_eq!(index.nodes().count(), 1);
assert_eq!(index.nodes().next().unwrap().layer_id(), stack.all_ids()[0]);
assert_eq!(index.nodes().next().unwrap().arc, ArcType::Root);
Ok(())
}
#[test]
fn sublayer_site_layer_order() -> Result<()> {
let stack = load_stack(&fixture_path("sublayer_override.usda"))?;
let index = build(&stack, "/World");
let ns: Vec<_> = index.nodes().collect();
assert_eq!(ns.len(), 1, "one per-site node spans both sublayers");
assert_eq!(ns[0].arc, ArcType::Root);
let layers: Vec<LayerId> = ns[0].layers().map(|(li, _)| li).collect();
let expected: Vec<LayerId> = stack.root_layer_stack().iter().map(|&(id, _)| id).collect();
assert_eq!(layers, expected, "stronger sublayer first");
Ok(())
}
#[test]
fn prim_only_in_stronger_layer() -> Result<()> {
let stack = load_stack(&fixture_path("sublayer_override.usda"))?;
let index = build(&stack, "/World/Sphere");
assert_eq!(index.nodes().count(), 1);
assert_eq!(index.nodes().next().unwrap().layer_id(), stack.all_ids()[0]);
Ok(())
}
#[test]
fn nonexistent_prim_empty_index() -> Result<()> {
let stack = load_stack(&composition_path("active.usda"))?;
let index = build(&stack, "/DoesNotExist");
assert!(index.is_empty());
Ok(())
}
#[test]
fn reference_arc_present() -> Result<()> {
let stack = load_stack(&fixture_path("ref_external.usda"))?;
let index = build(&stack, "/World/MyPrim");
assert!(index.nodes().any(|n| n.arc == ArcType::Reference));
Ok(())
}
#[test]
fn empty_reference_target_culled() -> Result<()> {
let root = parse_usda("#usda 1.0\ndef \"A\" ( references = @base.usd@</Empty> ) {\n custom double x = 1\n}\n");
let base = parse_usda("#usda 1.0\ndef \"Other\" {}\n");
let layers = vec![sdf::Layer::new("root.usd", root), sdf::Layer::new("base.usd", base)];
let stack = LayerGraph::from_layers(layers, 0, Box::new(DefaultResolver::new()), true);
let index = build(&stack, "/A");
assert!(
index.all_nodes().any(|n| n.arc == ArcType::Reference && n.is_culled()),
"empty reference target kept as a culled node"
);
assert!(
index.nodes().all(|n| n.arc != ArcType::Reference),
"culled reference contributes no opinion to resolution"
);
assert!(
!index.has_composition_arc(),
"an empty reference does not compose the prim"
);
assert!(!index.is_empty(), "the prim's own opinions remain");
Ok(())
}
#[test]
fn implied_class_flagged() -> Result<()> {
let root = parse_usda(
"#usda 1.0\ndef \"Model\" ( references = @ref.usd@</Ref> ) {\n over \"Class\" { custom string x = \"rootimplied\" }\n}\n",
);
let refl = parse_usda(
"#usda 1.0\ndef \"Ref\" {\n def \"Sub\" ( inherits = </Ref/Class> ) {}\n class \"Class\" { custom string x = \"ref\" }\n}\n",
);
let layers = vec![sdf::Layer::new("root.usd", root), sdf::Layer::new("ref.usd", refl)];
let stack = LayerGraph::from_layers(layers, 0, Box::new(DefaultResolver::new()), true);
let index = build(&stack, "/Model/Sub");
let implied = index
.arena()
.iter()
.find(|n| n.path.as_str() == "/Model/Class")
.expect("implied class node in the referencing namespace");
assert!(
implied.flags().contains(NodeFlags::IMPLIED_CLASS),
"implied class node is flagged"
);
assert!(implied.origin().is_some(), "implied class records its origin");
Ok(())
}
#[test]
fn prim_index_is_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<PrimIndex>();
assert_send_sync::<Node>();
assert_send_sync::<NodeId>();
}
#[test]
fn variant_from_external_reference() -> Result<()> {
let root = parse_usda(
"#usda 1.0\ndef \"Model\" (\n references = @ref.usd@</Ref>\n variants = { string v = \"b\" }\n) {}\n",
);
let refl = parse_usda(
"#usda 1.0\ndef \"Ref\" (\n add variantSets = \"v\"\n) {\n variantSet \"v\" = {\n \"a\" { custom string x = \"a\" }\n \"b\" { custom string x = \"b\" }\n }\n}\n",
);
let stack = two_layer_stack(root, refl);
let index = build(&stack, "/Model");
assert!(
index
.nodes()
.any(|n| n.arc == ArcType::Variant && n.path.as_str().contains("{v=b}")),
"selected variant from the referenced layer must be composed"
);
Ok(())
}
#[test]
fn variant_from_internal_reference() -> Result<()> {
let root = parse_usda(
"#usda 1.0\ndef \"Base\" (\n add variantSets = \"v\"\n) {\n variantSet \"v\" = {\n \"a\" { custom string x = \"a\" }\n \"b\" { custom string x = \"b\" }\n }\n}\ndef \"Model\" (\n references = </Base>\n variants = { string v = \"b\" }\n) {}\n",
);
let stack = one_layer_stack(root);
let index = build(&stack, "/Model");
assert!(
index.nodes().any(|n| n.path.as_str().contains("{v=b}")),
"selected variant from the internal-reference target must be composed"
);
assert!(
!index.nodes().any(|n| n.path.as_str().contains("{v=a}")),
"fallback variant must not be composed when v=b is selected"
);
Ok(())
}
#[test]
fn variant_contains_reference() -> Result<()> {
let root = parse_usda(
"#usda 1.0\ndef \"Model\" (\n references = @ref.usd@</Ref>\n variants = { string v = \"b\" }\n) {}\n",
);
let refl = parse_usda(
"#usda 1.0\ndef \"Inner\" { custom string y = \"inner\" }\ndef \"Ref\" (\n add variantSets = \"v\"\n) {\n variantSet \"v\" = {\n \"a\" {}\n \"b\" ( references = </Inner> ) {}\n }\n}\n",
);
let stack = two_layer_stack(root, refl);
let index = build(&stack, "/Model");
assert!(
index
.nodes()
.any(|n| n.arc == ArcType::Variant && n.path.as_str().contains("{v=b}")),
"selected variant node present"
);
assert!(
index.nodes().any(|n| n.path.as_str() == "/Inner"),
"reference inside the selected variant must be followed"
);
Ok(())
}
#[test]
fn structural_links_consistent() -> Result<()> {
let stack = load_stack(&fixture_path("ref_external.usda"))?;
let index = build(&stack, "/World/MyPrim");
let root = index.root().expect("non-empty index has a root");
assert_eq!(index.node(root).arc, ArcType::Root);
let reference = index
.arena()
.iter()
.position(|n| n.arc == ArcType::Reference)
.map(|i| NodeId(i as u32))
.expect("reference fixture has a reference node");
let parent = index.parent(reference).expect("reference has a parent");
assert!(index.children(parent).contains(&reference));
for (i, _) in index.arena().iter().enumerate() {
let id = NodeId(i as u32);
if let Some(parent) = index.parent(id) {
assert!(
index.children(parent).contains(&id),
"node {i} parent {parent:?} missing it as a child"
);
}
}
Ok(())
}
#[test]
fn graft_preserves_subtree() -> Result<()> {
let root = parse_usda(
"#usda 1.0\ndef \"Model\" ( inherits = </Class> ) {}\ndef \"Class\" ( references = @base.usd@</Base> ) {}\n",
);
let base = parse_usda("#usda 1.0\ndef \"Base\" {}\n");
let layers = vec![sdf::Layer::new("root.usd", root), sdf::Layer::new("base.usd", base)];
let stack = LayerGraph::from_layers(layers, 0, Box::new(DefaultResolver::new()), true);
let index = build(&stack, "/Model");
let find = |p: &str| {
index
.arena()
.iter()
.position(|n| n.path.as_str() == p)
.map(|i| NodeId(i as u32))
};
let class = find("/Class").expect("inherited /Class node");
let base = find("/Base").expect("grafted /Base node from /Class's reference");
assert_eq!(
index.parent(base),
Some(class),
"reference subtree preserved under its inherit root"
);
assert!(index.node(base).origin().is_some(), "grafted node carries an origin");
Ok(())
}
#[test]
fn dump_renders_tree() -> Result<()> {
let root = parse_usda(
"#usda 1.0\ndef \"Model\" ( inherits = </Class> ) {}\ndef \"Class\" ( references = @base.usd@</Base> ) {}\n",
);
let base = parse_usda("#usda 1.0\ndef \"Base\" {}\n");
let layers = vec![sdf::Layer::new("root.usd", root), sdf::Layer::new("base.usd", base)];
let stack = LayerGraph::from_layers(layers, 0, Box::new(DefaultResolver::new()), true);
let index = build(&stack, "/Model");
let dump = index.dump_to_string();
let line = |needle: &str| {
dump.lines()
.find(|l| l.contains(needle))
.unwrap_or_else(|| panic!("dump missing {needle}: {dump}"))
};
let indent = |l: &str| l.len() - l.trim_start().len();
assert!(dump.lines().all(|l| l.contains('#')), "each line has a strength rank");
assert!(line("/Model").starts_with("Root"), "root prim is the tree root");
assert!(
indent(line("/Class")) > indent(line("/Model")),
"inherit nests under the root"
);
assert!(
indent(line("/Base")) > indent(line("/Class")),
"grafted reference nests under the inherit"
);
Ok(())
}
#[test]
fn inherit_arc_present() -> Result<()> {
let stack = load_stack(&composition_path("class_inherit.usda"))?;
let index = build(&stack, "/World/cubeWithoutSetColor");
assert!(index.nodes().any(|n| n.arc == ArcType::Inherit));
Ok(())
}
#[test]
fn inherit_root_is_strongest() -> Result<()> {
let stack = load_stack(&composition_path("class_inherit.usda"))?;
let index = build(&stack, "/World/cubeWithSetColor");
let arcs: Vec<_> = index.nodes().map(|n| n.arc).collect();
assert_eq!(arcs[0], ArcType::Root);
assert!(arcs.contains(&ArcType::Inherit));
Ok(())
}
#[test]
fn variant_arc_with_selection() -> Result<()> {
let path = format!(
"{}/vendor/usd-wg-assets/docs/CompositionPuzzles/VariantSetAndLocal1/puzzle_1.usda",
manifest_dir()
);
let stack = load_stack(&path)?;
let index = build(&stack, "/World/Sphere");
assert!(index.nodes().any(|n| n.arc == ArcType::Variant));
let variant_node = index.nodes().find(|n| n.arc == ArcType::Variant).unwrap();
assert_eq!(variant_node.path.as_str(), "/World/Sphere{size=small}");
Ok(())
}
#[test]
fn specialize_arc_present() -> Result<()> {
let stack = load_stack(&composition_path("inherit_and_specialize.usda"))?;
let index = build(&stack, "/World/cubeScene/specializes");
assert!(index.nodes().any(|n| n.arc == ArcType::Specialize));
Ok(())
}
#[test]
fn find_layer_exact_match() -> Result<()> {
let resolver = DefaultResolver::new();
let layers = load_layers(&fixture_path("ref_external.usda"))?;
assert!(find(&layers[0].identifier, &layers, &resolver).is_some());
Ok(())
}
#[test]
fn find_layer_suffix_match() -> Result<()> {
let resolver = DefaultResolver::new();
let layers = load_layers(&fixture_path("ref_external.usda"))?;
assert!(find("ref_target.usda", &layers, &resolver).is_some());
Ok(())
}
#[test]
fn find_layer_relative_child() -> Result<()> {
let tmp = tempfile::tempdir()?;
let asset_dir = tmp.path().join("asset");
std::fs::create_dir_all(&asset_dir)?;
let model = asset_dir.join("model.usda");
std::fs::write(&model, b"placeholder")?;
let resolver = DefaultResolver::new();
let layers = vec![sdf::Layer::new(
model.canonicalize()?.to_string_lossy(),
Box::new(sdf::Data::new()),
)];
assert_eq!(find("./asset/model.usda", &layers, &resolver), Some(0));
Ok(())
}
#[test]
fn find_layer_no_partial_name_match() -> Result<()> {
let resolver = DefaultResolver::new();
let layers = load_layers(&fixture_path("ref_external.usda"))?;
assert!(find("target.usda", &layers, &resolver).is_none());
Ok(())
}
#[test]
fn find_layer_not_found() -> Result<()> {
let resolver = DefaultResolver::new();
let layers = load_layers(&fixture_path("ref_external.usda"))?;
assert!(find("nonexistent.usda", &layers, &resolver).is_none());
Ok(())
}
#[test]
fn find_layer_relative_parent_anchored() -> Result<()> {
let tmp = tempfile::tempdir()?;
let a_dir = tmp.path().join("Props");
let b_dir = tmp.path().join("Materials");
std::fs::create_dir_all(&a_dir)?;
std::fs::create_dir_all(&b_dir)?;
let a = a_dir.join("link.usd");
let b = b_dir.join("Materials.usd");
std::fs::write(&a, b"placeholder")?;
std::fs::write(&b, b"placeholder")?;
let layers = vec![
sdf::Layer::new(a.canonicalize()?.to_string_lossy(), Box::new(sdf::Data::new())),
sdf::Layer::new(b.canonicalize()?.to_string_lossy(), Box::new(sdf::Data::new())),
];
let resolver = DefaultResolver::new();
assert_eq!(find("../Materials/Materials.usd", &layers, &resolver), Some(1));
Ok(())
}
#[test]
fn reference_diamond_recursive() -> Result<()> {
let path = format!(
"{}/vendor/core-spec-supplemental-release_dec2025/composition/tests/assets/BasicReferenceDiamond_root/usda/root.usd",
manifest_dir()
);
let stack = load_stack(&path)?;
let index = build(&stack, "/Root");
assert!(
index
.nodes()
.any(|n| n.arc == ArcType::Reference && n.path.as_str() == "/A"),
"should have node from A.usd"
);
assert!(
index
.nodes()
.any(|n| n.arc == ArcType::Reference && n.path.as_str() == "/B"),
"should have node from B.usd"
);
assert!(
index
.nodes()
.any(|n| n.arc == ArcType::Reference && n.path.as_str() == "/C"),
"should have node from C.usd via nested reference"
);
let a_idx = stack.find("A.usd").unwrap();
let a_attr_path = Path::new("/A.A_attr").unwrap();
assert!(
stack.layer(a_idx).has_spec(&a_attr_path),
"A.usd should have spec at /A.A_attr"
);
Ok(())
}
#[test]
fn specializes_from_variant() -> Result<()> {
let path = format!(
"{}/vendor/core-spec-supplemental-release_dec2025/composition/tests/assets/SpecializesAndVariants_root/usda/root.usd",
manifest_dir()
);
let stack = load_stack(&path)?;
let index = build(&stack, "/B");
assert!(
index.nodes().any(|n| n.arc == ArcType::Specialize),
"should have specialize node from variant"
);
assert!(
index
.nodes()
.any(|n| n.path.as_str().contains("{nestedVariantSet=nestedVariant}")),
"should have /A's variant node"
);
Ok(())
}
#[test]
fn variant_reference_and_inherit_propagation() -> Result<()> {
let path = format!(
"{}/vendor/core-spec-supplemental-release_dec2025/composition/tests/assets/BasicVariantWithConnections_root/usda/root.usd",
manifest_dir()
);
let stack = load_stack(&path)?;
assert!(
stack.find("camera_perspective.usd").is_some(),
"camera_perspective.usd should be collected from variant reference"
);
let index = build(&stack, "/main_cam/Lens");
assert!(
index.nodes().any(|n| n.path.as_str() == "/camera/_localclass_Lens"),
"should have inherit node for _localclass_Lens"
);
Ok(())
}
#[test]
fn inherited_variant_selection_propagation() -> Result<()> {
let path = format!(
"{}/vendor/core-spec-supplemental-release_dec2025/composition/tests/assets/TrickyVariantWeakerSelection2_root/usda/root.usd",
manifest_dir()
);
let stack = load_stack(&path)?;
let index = build(&stack, "/bob");
assert!(
index.nodes().any(|n| n.path.as_str().contains("{geotype=cube}")),
"should have geotype=cube variant node from inherited selection"
);
Ok(())
}
fn parse_usda(text: &str) -> Box<dyn sdf::AbstractData> {
let data = crate::usda::parser::Parser::new(text).parse().expect("parse usda");
Box::new(crate::usda::TextReader::from_data(data))
}
#[test]
fn arc_cycle_recorded() -> Result<()> {
let a = parse_usda(
r#"#usda 1.0
(
defaultPrim = "Root"
)
def "Root" (
references = @b.usd@
)
{
}
"#,
);
let b = parse_usda(
r#"#usda 1.0
(
defaultPrim = "Root"
)
def "Root" (
references = @a.usd@
)
{
}
"#,
);
let layers = vec![sdf::Layer::new("a.usd", a), sdf::Layer::new("b.usd", b)];
let stack = LayerGraph::from_layers(layers, 0, Box::new(DefaultResolver::new()), true);
let (_index, errors) = PrimIndex::build_with_cache(
&Path::from("/Root"),
&stack,
&CompositionContext::default(),
&HashMap::new(),
)?;
assert!(
errors.iter().any(|e| matches!(e, Error::ArcCycle(_))),
"expected a recorded ArcCycle error, got {errors:?}"
);
Ok(())
}
#[test]
fn subroot_arc_cycle_recorded() -> Result<()> {
let a = parse_usda(
r#"#usda 1.0
(
defaultPrim = "Root"
)
def "Root" (
references = @b.usd@</Outer/Inner>
)
{
}
"#,
);
let b = parse_usda(
r#"#usda 1.0
def "Outer"
{
def "Inner" (
references = @a.usd@
)
{
}
}
"#,
);
let layers = vec![sdf::Layer::new("a.usd", a), sdf::Layer::new("b.usd", b)];
let stack = LayerGraph::from_layers(layers, 0, Box::new(DefaultResolver::new()), true);
let (_index, errors) = PrimIndex::build_with_cache(
&Path::from("/Root"),
&stack,
&CompositionContext::default(),
&HashMap::new(),
)?;
assert!(
errors.iter().any(|e| matches!(e, Error::ArcCycle(_))),
"expected a recorded ArcCycle error for a cross-frame cycle, got {errors:?}"
);
Ok(())
}
#[test]
fn unresolved_layer_recorded() -> Result<()> {
let layer = parse_usda(
r#"#usda 1.0
def "Prim" (
references = @nonexistent.usd@
)
{
custom string marker = "ok"
}
"#,
);
let layers = vec![sdf::Layer::new("test.usda", layer)];
let stack = LayerGraph::from_layers(layers, 0, Box::new(DefaultResolver::new()), true);
let (index, errors) = PrimIndex::build_with_cache(
&Path::from("/Prim"),
&stack,
&CompositionContext::default(),
&HashMap::new(),
)?;
assert!(
errors.iter().any(|e| matches!(e, Error::UnresolvedLayer { .. })),
"expected a recorded UnresolvedLayer error, got {errors:?}"
);
assert!(
!index.is_empty(),
"the prim's local opinion must survive the skipped arc"
);
Ok(())
}
#[test]
fn missing_default_prim_recorded() -> Result<()> {
let root = parse_usda(
r#"#usda 1.0
def "Prim" (
references = @target.usda@
)
{
custom string marker = "ok"
}
"#,
);
let target = parse_usda("#usda 1.0\ndef \"Foo\" {}\n");
let layers = vec![
sdf::Layer::new("root.usda", root),
sdf::Layer::new("target.usda", target),
];
let stack = LayerGraph::from_layers(layers, 0, Box::new(DefaultResolver::new()), true);
let (index, errors) = PrimIndex::build_with_cache(
&Path::from("/Prim"),
&stack,
&CompositionContext::default(),
&HashMap::new(),
)?;
assert!(
errors.iter().any(|e| matches!(e, Error::MissingDefaultPrim { .. })),
"expected a recorded MissingDefaultPrim error, got {errors:?}"
);
assert!(
!index.is_empty(),
"the prim's local opinion must survive the skipped arc"
);
Ok(())
}
fn variant_paths(index: &PrimIndex) -> Vec<String> {
index
.nodes()
.filter(|n| n.arc == ArcType::Variant)
.map(|n| n.path.as_str().to_string())
.collect()
}
#[test]
fn variant_no_selection_unselected() -> Result<()> {
let stack = load_stack(&fixture_path("variant_fallback.usda"))?;
let index = build(&stack, "/NoSelection");
let paths = variant_paths(&index);
assert!(
paths.is_empty(),
"no variant should be selected without an authored selection or fallback: got {paths:?}"
);
Ok(())
}
#[test]
fn variant_fallback_overrides_default() -> Result<()> {
let stack = load_stack(&fixture_path("variant_fallback.usda"))?;
let fb = VariantFallbackMap::new().add("shadingComplexity", ["simple"]);
let index = build_with_fallbacks(&stack, "/NoSelection", fb);
let paths = variant_paths(&index);
assert!(
paths.iter().any(|p| p.contains("{shadingComplexity=simple}")),
"fallback should select 'simple': got {paths:?}"
);
assert!(
!paths.iter().any(|p| p.contains("{shadingComplexity=full}")),
"'full' should NOT be selected when fallback says 'simple'"
);
Ok(())
}
#[test]
fn variant_authored_selection_beats_fallback() -> Result<()> {
let stack = load_stack(&fixture_path("variant_fallback.usda"))?;
let fb = VariantFallbackMap::new().add("shadingComplexity", ["none"]);
let index = build_with_fallbacks(&stack, "/Root", fb);
let paths = variant_paths(&index);
assert!(
paths.iter().any(|p| p.contains("{shadingComplexity=full}")),
"authored selection 'full' should win over fallback 'none': got {paths:?}"
);
Ok(())
}
#[test]
fn variant_fallback_skips_nonexistent() -> Result<()> {
let stack = load_stack(&fixture_path("variant_fallback.usda"))?;
let fb = VariantFallbackMap::new().add("shadingComplexity", ["ultra", "simple"]);
let index = build_with_fallbacks(&stack, "/NoSelection", fb);
let paths = variant_paths(&index);
assert!(
paths.iter().any(|p| p.contains("{shadingComplexity=simple}")),
"should skip 'ultra' and use 'simple': got {paths:?}"
);
Ok(())
}
#[test]
fn variant_fallback_all_invalid_unselected() -> Result<()> {
let stack = load_stack(&fixture_path("variant_fallback.usda"))?;
let fb = VariantFallbackMap::new().add("shadingComplexity", ["ultra", "mega"]);
let index = build_with_fallbacks(&stack, "/NoSelection", fb);
let paths = variant_paths(&index);
assert!(
paths.is_empty(),
"no variant should be selected when every fallback is invalid: got {paths:?}"
);
Ok(())
}
#[test]
fn node_strength_comparator() {
let p = |s: &str| Path::from(s.to_string());
let id = MapFunction::identity();
let lid = LayerId::from_raw(0);
let mut g = PrimIndexGraph::default();
let root = g.add_child(NodeId::INVALID, lid, p("/A"), ArcType::Root, id.clone(), false);
let inh = g.add_child(root, lid, p("/Class"), ArcType::Inherit, id.clone(), false);
let r1 = g.add_child(root, lid, p("/R1"), ArcType::Reference, id.clone(), false);
let r2 = g.add_child(root, lid, p("/R2"), ArcType::Reference, id.clone(), false);
assert_eq!(g.compare_sibling_node_strength(inh, r1), Ordering::Less);
assert_eq!(g.compare_sibling_node_strength(r1, r2), Ordering::Less);
assert_eq!(g.compare_sibling_node_strength(r2, r1), Ordering::Greater);
assert_eq!(g.compare_node_strength(root, inh), Ordering::Less);
assert_eq!(g.compare_node_strength(inh, root), Ordering::Greater);
assert_eq!(g.compare_node_strength(inh, r2), Ordering::Less);
assert_eq!(g.compare_node_strength(r2, r2), Ordering::Equal);
let deep = g.add_child(root, lid, p("/D"), ArcType::Reference, id.clone(), false);
g.nodes[deep.idx()].namespace_depth = 5;
assert_eq!(g.compare_sibling_node_strength(deep, r1), Ordering::Less);
}
#[test]
fn arc_type_liverps_ordering() {
assert!(ArcType::Root < ArcType::Inherit);
assert!(ArcType::Inherit < ArcType::Variant);
assert!(ArcType::Variant < ArcType::Relocate);
assert!(ArcType::Relocate < ArcType::Reference);
assert!(ArcType::Reference < ArcType::Payload);
assert!(ArcType::Payload < ArcType::Specialize);
}
fn spec_composition_path(relative: &str) -> String {
format!(
"{}/vendor/core-spec-supplemental-release_dec2025/composition/tests/assets/{relative}",
manifest_dir()
)
}
fn layer_name(identifier: &str) -> &str {
std::path::Path::new(identifier)
.file_name()
.and_then(|s| s.to_str())
.unwrap_or(identifier)
}
fn prim_stack(index: &PrimIndex, stack: &LayerGraph) -> Vec<(String, String)> {
index
.nodes()
.map(|n| {
(
layer_name(stack.identifier(n.layer_id())).to_owned(),
n.path.to_string(),
)
})
.collect()
}
#[test]
fn specialize_global_weakness_basic() -> Result<()> {
let stack = load_stack(&spec_composition_path("BasicSpecializes_root/usda/root.usd"))?;
let index = build(&stack, "/Root");
let ps = prim_stack(&index, &stack);
assert_eq!(
ps,
vec![
("root.usd".into(), "/Root".into()),
("ref.usd".into(), "/Ref".into()),
("ref2.usd".into(), "/Ref".into()),
("root.usd".into(), "/Specializes".into()),
("ref.usd".into(), "/Specializes".into()),
("ref2.usd".into(), "/Specializes".into()),
("root.usd".into(), "/Base".into()),
("ref.usd".into(), "/Base".into()),
("ref2.usd".into(), "/Base".into()),
]
);
let ns: Vec<_> = index.nodes().collect();
for node in &ns[..3] {
assert!(
!node.introduced_by_specialize(),
"node {:?} should not be specialize",
node.path
);
}
for node in &ns[3..] {
assert!(
node.introduced_by_specialize(),
"node {:?} should be specialize",
node.path
);
}
Ok(())
}
#[test]
fn specialize_global_weakness_multiple() -> Result<()> {
let stack = load_stack(&spec_composition_path("BasicSpecializes_root/usda/root.usd"))?;
let index = build(&stack, "/MultipleSpecializes");
let first_spec = index
.nodes()
.position(|n| n.introduced_by_specialize())
.expect("should have specialize nodes");
assert!(first_spec >= 2, "at least Root + Reference before specializes");
for node in index.nodes().skip(first_spec) {
assert!(
node.introduced_by_specialize(),
"node {:?} should be globally weak",
node.path
);
}
Ok(())
}
#[test]
fn specialize_chain_ordering() -> Result<()> {
let stack = load_stack(&spec_composition_path("BasicSpecializes_root/usda/root.usd"))?;
let index = build(&stack, "/Basic");
let ps = prim_stack(&index, &stack);
assert_eq!(
ps,
vec![
("root.usd".into(), "/Basic".into()),
("root.usd".into(), "/BasicSpecializes1".into()),
("root.usd".into(), "/BasicSpecializes2".into()),
]
);
Ok(())
}
fn offset_stack(index: &PrimIndex, stack: &LayerGraph) -> Vec<(String, String, ArcType, f64, f64)> {
index
.nodes()
.flat_map(|n| {
let path = n.path.to_string();
let arc = n.arc;
n.layers().map(move |(li, off)| {
(
layer_name(stack.identifier(li)).to_owned(),
path.clone(),
arc,
off.offset,
off.scale,
)
})
})
.collect()
}
fn basic_time_offset_stack() -> anyhow::Result<LayerGraph> {
load_stack(&spec_composition_path("BasicTimeOffset_root/usda/root.usd"))
}
#[test]
fn time_offset_reference_then_sublayer() -> anyhow::Result<()> {
let stack = basic_time_offset_stack()?;
let index = build(&stack, "/Root");
assert_eq!(
offset_stack(&index, &stack),
vec![
("root.usd".into(), "/Root".into(), ArcType::Root, 0.0, 1.0),
("A.usd".into(), "/Model".into(), ArcType::Reference, 10.0, 1.0),
("B.usd".into(), "/Model".into(), ArcType::Reference, 30.0, 1.0),
]
);
Ok(())
}
#[test]
fn time_offset_payload_with_scale_and_sublayer() -> anyhow::Result<()> {
let stack = basic_time_offset_stack()?;
let index = build(&stack, "/PayloadRefPayload");
let got = offset_stack(&index, &stack);
assert!(
got.contains(&("root.usd".into(), "/PayloadRefPayload".into(), ArcType::Root, 0.0, 1.0,)),
"missing root opinion: got {got:?}"
);
assert!(
got.contains(&("ref_sub.usd".into(), "/Ref".into(), ArcType::Payload, 50.0, 2.0)),
"missing ref_sub /Ref payload opinion at (50,2): got {got:?}"
);
assert!(
got.contains(&("B.usd".into(), "/Model".into(), ArcType::Payload, 50.0, 2.0)),
"missing B.usd /Model payload opinion at (50,2): got {got:?}"
);
Ok(())
}
#[test]
fn time_offset_payload_with_nested_reference() -> anyhow::Result<()> {
let stack = basic_time_offset_stack()?;
let index = build(&stack, "/PayloadMultiRef");
let got = offset_stack(&index, &stack);
assert!(
got.contains(&("root.usd".into(), "/PayloadMultiRef".into(), ArcType::Root, 0.0, 1.0,)),
"missing root opinion: got {got:?}"
);
assert!(
got.contains(&("ref_sub.usd".into(), "/Ref2".into(), ArcType::Payload, 50.0, 2.0)),
"missing ref_sub /Ref2 payload opinion: got {got:?}"
);
assert!(
got.contains(&("B.usd".into(), "/Model".into(), ArcType::Reference, 50.0, 2.0)),
"missing B.usd /Model ref opinion at (50,2): got {got:?}"
);
Ok(())
}
#[test]
fn time_offset_descendant_inherits_parents_offset() -> anyhow::Result<()> {
let stack = basic_time_offset_stack()?;
let index = build(&stack, "/Root/Anim");
let got = offset_stack(&index, &stack);
assert!(
got.contains(&("B.usd".into(), "/Model/Anim".into(), ArcType::Reference, 30.0, 1.0)),
"missing /Model/Anim at effective (30, 1): got {got:?}"
);
Ok(())
}
#[test]
fn time_samples_retimed_across_reference() -> Result<()> {
let root = parse_usda(
r#"#usda 1.0
def "Root" (
references = @model.usd@</Model> (offset = 10; scale = 2)
)
{
}
"#,
);
let model = parse_usda(
r#"#usda 1.0
def "Model"
{
double radius.timeSamples = {
1: 0.0,
5: 1.0,
}
}
"#,
);
let layers = vec![sdf::Layer::new("root.usda", root), sdf::Layer::new("model.usd", model)];
let stack = LayerGraph::from_layers(layers, 0, Box::new(DefaultResolver::new()), true);
let index = PrimIndex::build_with_context(&Path::from("/Root"), &stack, &CompositionContext::default())?;
let samples = index
.resolve_time_samples(&stack, Some(".radius"))?
.expect("retimed samples");
let times: Vec<f64> = samples.iter().map(|(t, _)| *t).collect();
assert_eq!(times, vec![12.0, 20.0]);
Ok(())
}
#[test]
fn reference_offset_zero_scale_falls_back_to_identity() -> Result<()> {
let root = parse_usda(
r#"#usda 1.0
def "Root" (
references = @model.usd@</Model> (offset = 10; scale = 0)
)
{
}
"#,
);
let model = parse_usda(
r#"#usda 1.0
def "Model" {}
"#,
);
let layers = vec![sdf::Layer::new("root.usda", root), sdf::Layer::new("model.usd", model)];
let stack = LayerGraph::from_layers(layers, 0, Box::new(DefaultResolver::new()), true);
let index = build(&stack, "/Root");
let got = offset_stack(&index, &stack);
assert!(
got.contains(&("model.usd".into(), "/Model".into(), ArcType::Reference, 0.0, 1.0)),
"expected reference offset to fall back to identity for scale=0: got {got:?}"
);
Ok(())
}
#[test]
fn payload_offset_negative_scale_falls_back_to_identity() -> Result<()> {
let root = parse_usda(
r#"#usda 1.0
def "Root" (
payload = @model.usd@</Model> (offset = 5; scale = -2)
)
{
}
"#,
);
let model = parse_usda(
r#"#usda 1.0
def "Model" {}
"#,
);
let layers = vec![sdf::Layer::new("root.usda", root), sdf::Layer::new("model.usd", model)];
let stack = LayerGraph::from_layers(layers, 0, Box::new(DefaultResolver::new()), true);
let index = build(&stack, "/Root");
let got = offset_stack(&index, &stack);
assert!(
got.contains(&("model.usd".into(), "/Model".into(), ArcType::Payload, 0.0, 1.0)),
"expected payload offset to fall back to identity for scale=-2: got {got:?}"
);
Ok(())
}
#[test]
fn sublayer_offset_zero_scale_falls_back_to_identity() -> Result<()> {
let root = parse_usda(
r#"#usda 1.0
(
subLayers = [
@sub.usda@ (offset = 10; scale = 0)
]
)
def "Root" {}
"#,
);
let sub = parse_usda(
r#"#usda 1.0
def "Root" {}
"#,
);
let layers = vec![sdf::Layer::new("root.usda", root), sdf::Layer::new("sub.usda", sub)];
let stack = LayerGraph::from_layers(layers, 0, Box::new(DefaultResolver::new()), true);
let index = build(&stack, "/Root");
let got = offset_stack(&index, &stack);
assert!(
got.iter().any(|(name, path, _, off, scale)| name == "sub.usda"
&& path == "/Root"
&& *off == 0.0
&& *scale == 1.0),
"expected sublayer offset to fall back to identity for scale=0: got {got:?}"
);
Ok(())
}
#[test]
fn clip_sets_order_folds_layers() -> Result<()> {
let root = parse_usda(
r#"#usda 1.0
(
subLayers = [
@sub.usda@
]
)
def "P" (
prepend clipSets = ["a"]
)
{
}
"#,
);
let sub = parse_usda(
r#"#usda 1.0
over "P" (
append clipSets = ["b"]
)
{
}
"#,
);
let layers = vec![sdf::Layer::new("root.usda", root), sdf::Layer::new("sub.usda", sub)];
let stack = LayerGraph::from_layers(layers, 0, Box::new(DefaultResolver::new()), true);
let index = build(&stack, "/P");
assert_eq!(
index.clip_sets_order(&stack)?,
Some(vec!["a".to_string(), "b".to_string()])
);
Ok(())
}
#[test]
fn clip_sets_order_unauthored() -> Result<()> {
let root = parse_usda(
r#"#usda 1.0
def "P" {}
"#,
);
let layers = vec![sdf::Layer::new("root.usda", root)];
let stack = LayerGraph::from_layers(layers, 0, Box::new(DefaultResolver::new()), true);
let index = build(&stack, "/P");
assert_eq!(index.clip_sets_order(&stack)?, None);
Ok(())
}
#[test]
fn clip_sets_order_authored_empty() -> Result<()> {
let root = parse_usda(
r#"#usda 1.0
def "P" (
clipSets = []
)
{
}
"#,
);
let layers = vec![sdf::Layer::new("root.usda", root)];
let stack = LayerGraph::from_layers(layers, 0, Box::new(DefaultResolver::new()), true);
let index = build(&stack, "/P");
assert_eq!(index.clip_sets_order(&stack)?, Some(Vec::new()));
Ok(())
}
}