use std::collections::{HashMap, HashSet};
use std::rc::Rc;
use crate::Result;
use crate::sdf::{self, Path, Value};
use crate::tf;
use crate::usd::{Attribute, Prim, PrimPredicate, SchemaBase, Stage};
use super::collection_expr::{CollectionEvaluator, CollectionSearcher, resolve_complete_membership_expression};
use super::{CollectionAPI, CollectionMode, ExpansionRule, tokens};
impl CollectionAPI {
pub fn collection_path(&self) -> Result<Path> {
Ok(self
.path()
.append_property(format!("{}:{}", tokens::COLLECTION, self.name()))?)
}
pub fn includes(&self) -> Result<Vec<Path>> {
self.includes_rel().targets()
}
pub fn excludes(&self) -> Result<Vec<Path>> {
self.excludes_rel().targets()
}
pub fn compute_membership_query(&self) -> Result<MembershipQuery> {
let mode: CollectionMode = composed_or_default(&self.mode_attr())?;
let mut query = MembershipQuery {
rule_map: PathExpansionRuleMap::new(),
top_expansion_rule: composed_or_default(&self.expansion_rule_attr())?,
evaluator: None,
};
if mode != CollectionMode::Expression {
let mut visited = HashSet::new();
visited.insert(self.collection_path()?);
self.build_into(&mut query.rule_map, &mut visited)?;
}
if mode != CollectionMode::Relationship {
let expression = resolve_complete_membership_expression(self)?;
if !expression.is_empty() {
if let Ok(evaluator) = CollectionEvaluator::build(self.stage(), expression) {
query.evaluator = Some(Rc::new(evaluator));
}
}
}
Ok(query)
}
pub fn set_expansion_rule(&self, rule: ExpansionRule) -> Result<()> {
self.expansion_rule_attr_builder().set(rule).build()?;
Ok(())
}
pub fn set_include_root(&self, value: bool) -> Result<()> {
self.include_root_attr_builder().set(value).build()?;
Ok(())
}
pub fn set_membership_expression(&self, expression: sdf::PathExpression) -> Result<()> {
self.membership_expression_attr_builder().set(expression).build()?;
Ok(())
}
pub fn set_mode(&self, mode: CollectionMode) -> Result<()> {
self.mode_attr_builder().set(mode).build()?;
Ok(())
}
pub fn include_path(&self, path: impl sdf::IntoPath) -> Result<()> {
let path = sdf::try_into_path(path)?;
if self.compute_membership_query()?.is_path_included(&path) {
return Ok(()); }
if path.is_abs_root() {
return self.set_include_root(true);
}
if self.excludes()?.contains(&path) {
self.create_excludes_rel()?.remove_target(&path)?;
if self.compute_membership_query()?.is_path_included(&path) {
return Ok(()); }
}
self.create_includes_rel()?.add_target(path)?;
Ok(())
}
pub fn exclude_path(&self, path: impl sdf::IntoPath) -> Result<()> {
let path = sdf::try_into_path(path)?;
let query = self.compute_membership_query()?;
if !query.is_empty() && !query.is_path_included(&path) {
return Ok(()); }
if path.is_abs_root() {
return self.set_include_root(false);
}
if !query.is_empty() && self.includes()?.contains(&path) {
self.create_includes_rel()?.remove_target(&path)?;
let query = self.compute_membership_query()?;
if !query.is_empty() && !query.is_path_included(&path) {
return Ok(()); }
}
self.create_excludes_rel()?.add_target(path)?;
Ok(())
}
pub fn has_no_included_paths(&self) -> Result<bool> {
Ok(self.includes()?.is_empty()
&& !composed_or_default::<bool>(&self.include_root_attr())?
&& (!self.excludes()?.is_empty()
|| membership_expression(self)?.is_none_or(|expression| expression.is_empty())))
}
fn build_into(&self, map: &mut PathExpansionRuleMap, visited: &mut HashSet<Path>) -> Result<()> {
let rule: ExpansionRule = composed_or_default(&self.expansion_rule_attr())?;
let path_rule = PathRule::from_expansion(rule);
if composed_or_default::<bool>(&self.include_root_attr())? && rule != ExpansionRule::ExplicitOnly {
map.insert(Path::abs_root(), path_rule);
}
for included in self.includes()? {
if let Some((prim, name)) = Self::instance_at_path(&included) {
let nested = CollectionAPI::from_prim_unchecked(Prim::new(self.stage(), prim), name);
if visited.insert(nested.collection_path()?) {
nested.build_into(map, visited)?;
}
continue;
}
map.insert(included, path_rule);
}
for excluded in self.excludes()? {
map.insert(excluded, PathRule::Exclude);
}
Ok(())
}
}
pub fn apply_collection(stage: &Stage, prim: impl sdf::IntoPath, name: impl Into<String>) -> Result<CollectionAPI> {
let prim = sdf::try_into_path(prim)?;
let name = tf::Token::from(name.into());
let schema = tf::Token::new(tokens::COLLECTION_API);
let registry = stage.schema_registry();
if registry.schema_info(&schema).is_some() && !registry.is_allowed_instance_name(&schema, &name) {
return Err(super::ApplyApiError::InstanceNameNotAllowed { schema, instance: name }.into());
}
CollectionAPI::apply(&stage.override_prim(prim)?, name)
}
pub fn compute_included_paths(stage: &Stage, query: &MembershipQuery, predicate: PrimPredicate) -> Result<Vec<Path>> {
let mut out = Vec::new();
if query.is_empty() {
return Ok(out);
}
if !query.uses_path_expansion_rule_map() {
return compute_included_by_expression(stage, query, predicate);
}
let mut seen = HashSet::new();
let mut err: Result<()> = Ok(());
let collect_props = query
.rule_map
.values()
.any(|r| *r == PathRule::ExpandPrimsAndProperties);
let mut effective: HashMap<Path, PathRule> = HashMap::new();
stage.traverse(predicate, |prim| {
if err.is_err() {
return;
}
let parent_rule = match prim.parent() {
Some(parent) => effective
.get(&parent)
.copied()
.unwrap_or_else(|| query.effective_rule(&parent)),
None => PathRule::Exclude,
};
let (included, rule) = query.is_path_included_below(prim, parent_rule);
effective.insert(prim.clone(), rule);
if !included {
return;
}
if seen.insert(prim.clone()) {
out.push(prim.clone());
}
if collect_props && let Err(e) = push_member_properties(stage, prim, rule, query, &mut seen, &mut out) {
err = Err(e);
}
})?;
err?;
let mut props: Vec<&Path> = query.rule_map.keys().filter(|p| p.is_property_path()).collect();
props.sort();
for path in props {
if query.is_path_included(path) && stage.has_spec(path)? && seen.insert(path.clone()) {
out.push(path.clone());
}
}
Ok(out)
}
fn compute_included_by_expression(
stage: &Stage,
query: &MembershipQuery,
predicate: PrimPredicate,
) -> Result<Vec<Path>> {
let Some(evaluator) = query.expression_evaluator() else {
return Ok(Vec::new());
};
let search_properties = query.top_expansion_rule() == ExpansionRule::ExpandPrimsAndProperties;
let walk = predicate.inherited_projection();
let clean = walk == predicate;
let mut searcher = evaluator.incremental_searcher();
let mut out = Vec::new();
let mut err: Result<()> = Ok(());
stage.traverse(walk, |prim| {
if err.is_err() {
return;
}
let result = match searcher.next(prim) {
Ok(result) => result,
Err(e) => {
err = Err(e);
return;
}
};
if !result.value && (!search_properties || result.constant) {
return;
}
let admitted = clean
|| match stage.prim_matches(prim, predicate) {
Ok(admitted) => admitted,
Err(e) => {
err = Err(e);
return;
}
};
if !admitted {
return;
}
if result.value {
out.push(prim.clone());
}
if search_properties {
let push = if result.value && result.constant {
Some(push_all_properties(stage, prim, &mut out))
} else if !result.value {
Some(push_matching_properties(stage, &mut searcher, prim, &mut out))
} else {
None
};
if let Some(Err(e)) = push {
err = Err(e);
}
}
})?;
err?;
Ok(out)
}
fn push_all_properties(stage: &Stage, prim: &Path, out: &mut Vec<Path>) -> Result<()> {
for name in stage.prim(prim.clone())?.property_names()? {
let Ok(prop) = prim.append_property(&name) else {
continue;
};
out.push(prop);
}
Ok(())
}
fn push_matching_properties(
stage: &Stage,
searcher: &mut CollectionSearcher<'_>,
prim: &Path,
out: &mut Vec<Path>,
) -> Result<()> {
for name in stage.prim(prim.clone())?.property_names()? {
let Ok(prop) = prim.append_property(&name) else {
continue;
};
if searcher.next(&prop)?.value {
out.push(prop);
}
}
Ok(())
}
fn push_member_properties(
stage: &Stage,
prim: &Path,
prim_rule: PathRule,
query: &MembershipQuery,
seen: &mut HashSet<Path>,
out: &mut Vec<Path>,
) -> Result<()> {
for name in stage.prim(prim.clone())?.property_names()? {
let Ok(prop) = prim.append_property(&name) else {
continue;
};
let (included, _) = query.is_path_included_below(&prop, prim_rule);
if included && seen.insert(prop.clone()) {
out.push(prop);
}
}
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PathRule {
ExplicitOnly,
ExpandPrims,
ExpandPrimsAndProperties,
Exclude,
}
impl PathRule {
fn from_expansion(rule: ExpansionRule) -> Self {
match rule {
ExpansionRule::ExplicitOnly => PathRule::ExplicitOnly,
ExpansionRule::ExpandPrims => PathRule::ExpandPrims,
ExpansionRule::ExpandPrimsAndProperties => PathRule::ExpandPrimsAndProperties,
}
}
}
pub type PathExpansionRuleMap = HashMap<Path, PathRule>;
#[derive(Debug, Clone, Default)]
pub struct MembershipQuery {
rule_map: PathExpansionRuleMap,
top_expansion_rule: ExpansionRule,
evaluator: Option<Rc<CollectionEvaluator>>,
}
impl PartialEq for MembershipQuery {
fn eq(&self, other: &Self) -> bool {
self.rule_map == other.rule_map
&& self.top_expansion_rule == other.top_expansion_rule
&& self.evaluator.is_none()
&& other.evaluator.is_none()
}
}
impl MembershipQuery {
pub fn new(rule_map: PathExpansionRuleMap) -> Self {
MembershipQuery {
rule_map,
top_expansion_rule: ExpansionRule::default(),
evaluator: None,
}
}
pub fn rule_map(&self) -> &PathExpansionRuleMap {
&self.rule_map
}
pub fn top_expansion_rule(&self) -> ExpansionRule {
self.top_expansion_rule
}
pub fn expression_evaluator(&self) -> Option<&CollectionEvaluator> {
self.evaluator.as_deref()
}
pub fn uses_path_expansion_rule_map(&self) -> bool {
!self.rule_map.is_empty()
}
pub fn is_empty(&self) -> bool {
self.rule_map.is_empty() && self.evaluator.is_none()
}
pub fn is_path_included(&self, path: &Path) -> bool {
if self.uses_path_expansion_rule_map() {
let (rule, on_self) = self.closest_rule(path);
return rule_includes(rule, on_self, path.is_property_path());
}
match &self.evaluator {
Some(evaluator) => evaluator.match_path(path).value,
None => false,
}
}
pub fn is_path_included_below(&self, path: &Path, parent_rule: PathRule) -> (bool, PathRule) {
if !self.uses_path_expansion_rule_map() {
return (self.is_path_included(path), parent_rule);
}
let on_self = self.rule_map.contains_key(path);
let rule = self.rule_map.get(path).copied().unwrap_or(parent_rule);
(rule_includes(rule, on_self, path.is_property_path()), rule)
}
fn effective_rule(&self, path: &Path) -> PathRule {
self.closest_rule(path).0
}
fn closest_rule(&self, path: &Path) -> (PathRule, bool) {
let mut current = path.clone();
loop {
if let Some(rule) = self.rule_map.get(¤t) {
return (*rule, ¤t == path);
}
match current.parent() {
Some(parent) if !parent.is_empty() => current = parent,
_ => return (PathRule::Exclude, false),
}
}
}
}
fn composed(attr: &Attribute) -> Result<Option<Value>> {
attr.get::<Value>()
}
pub(super) fn membership_expression(collection: &CollectionAPI) -> Result<Option<sdf::PathExpression>> {
Ok(composed(&collection.membership_expression_attr())?.and_then(Value::into_path_expression))
}
fn composed_or_default<T: TryFrom<Value> + Default>(attr: &Attribute) -> Result<T> {
Ok(composed(attr)?
.and_then(|value| T::try_from(value).ok())
.unwrap_or_default())
}
fn rule_includes(rule: PathRule, on_self: bool, is_property: bool) -> bool {
match rule {
PathRule::Exclude => false,
PathRule::ExplicitOnly => on_self,
PathRule::ExpandPrims => !is_property || on_self,
PathRule::ExpandPrimsAndProperties => true,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Result;
use crate::sdf;
use crate::sdf::Variability;
use crate::usd::SchemaRegistry;
fn query(entries: &[(&str, PathRule)]) -> MembershipQuery {
let map = entries.iter().map(|(p, r)| (sdf::path(p).unwrap(), *r)).collect();
MembershipQuery::new(map)
}
fn author_collection(stage: &Stage, prim: &str, name: &str) -> Result<CollectionAPI> {
CollectionAPI::apply(&stage.define_prim(prim)?.set_type_name("Scope")?, name)
}
#[test]
fn expansion_rule_round_trips() {
for r in [
ExpansionRule::ExplicitOnly,
ExpansionRule::ExpandPrims,
ExpansionRule::ExpandPrimsAndProperties,
] {
assert_eq!(ExpansionRule::from_token(r.as_token()), Some(r));
}
assert_eq!(ExpansionRule::from_token("nope"), None);
assert_eq!(ExpansionRule::default(), ExpansionRule::ExpandPrims);
}
#[test]
fn decodes_collection_paths() -> Result<()> {
let decode = |path: &str| -> Result<Option<(Path, String)>> {
Ok(CollectionAPI::instance_at_path(&sdf::path(path)?).map(|(prim, name)| (prim, name.to_string())))
};
assert_eq!(
decode("/W.collection:render")?,
Some((sdf::path("/W")?, "render".to_string()))
);
assert_eq!(
decode("/W.collection:lighting:key")?,
Some((sdf::path("/W")?, "lighting:key".to_string()))
);
assert_eq!(decode("/W.collection:render:includes")?, None);
assert_eq!(decode("/W.collection:includes")?, None);
assert_eq!(decode("/W.foo")?, None);
assert_eq!(decode("/W")?, None);
assert_eq!(decode("/W.collection")?, None);
Ok(())
}
#[test]
fn enumerates_collections_on_prim() -> Result<()> {
let stage = Stage::builder().in_memory("anon.usda")?;
stage
.define_prim("/W")?
.set_type_name("Scope")?
.add_applied_schema("CollectionAPI:render")?
.add_applied_schema("CollectionAPI:proxy")?
.add_applied_schema("MaterialBindingAPI")?;
let names: Vec<String> = CollectionAPI::get_all(&stage.prim("/W")?)?
.into_iter()
.map(|c| c.name().to_string())
.collect();
assert_eq!(names, vec!["render".to_string(), "proxy".to_string()]);
Ok(())
}
#[test]
fn reads_authored_opinions() -> Result<()> {
let stage = Stage::builder().in_memory("anon.usda")?;
let coll = author_collection(&stage, "/W", "render")?;
stage
.attribute_builder(coll.expansion_rule_attr().path().clone(), "token")
.variability(Variability::Uniform)
.set(Value::Token(ExpansionRule::ExplicitOnly.as_token().into()))
.build()?;
stage
.attribute_builder(coll.include_root_attr().path().clone(), "bool")
.variability(Variability::Uniform)
.set(Value::Bool(true))
.build()?;
coll.prim()
.author_relationship_targets("collection:render:includes", [sdf::path("/W/A")?])?;
assert_eq!(coll.expansion_rule()?, Some(ExpansionRule::ExplicitOnly));
assert_eq!(coll.include_root()?, Some(true));
assert_eq!(coll.includes()?, vec![sdf::path("/W/A")?]);
assert!(coll.excludes()?.is_empty());
let bare = author_collection(&stage, "/X", "c")?;
assert_eq!(bare.expansion_rule()?, Some(ExpansionRule::ExpandPrims));
assert_eq!(bare.include_root()?, None, "includeRoot declares no fallback");
Ok(())
}
#[test]
fn include_root_from_definition() -> Result<()> {
let stage = Stage::builder()
.schema_registry(SchemaRegistry::test_registry())
.in_memory("anon.usda")?;
let prim = stage.define_prim("/Light")?.apply_api("LightAPI")?;
let coll = CollectionAPI::from_prim_unchecked(prim, "lightLink");
assert_eq!(coll.include_root()?, Some(true), "the definition declares includeRoot");
assert!(!coll.has_no_included_paths()?);
assert!(
coll.compute_membership_query()?
.is_path_included(&sdf::path("/World/Geo")?),
"including the pseudo-root includes every path"
);
coll.set_include_root(false)?;
assert_eq!(coll.include_root()?, Some(false));
assert!(coll.has_no_included_paths()?);
Ok(())
}
#[test]
fn lenient_reads_fall_back() -> Result<()> {
let stage = Stage::builder().in_memory("anon.usda")?;
let coll = author_collection(&stage, "/W", "c")?;
coll.set_expansion_rule(ExpansionRule::ExplicitOnly)?;
coll.set_mode(CollectionMode::Relationship)?;
coll.set_include_root(true)?;
coll.set_membership_expression(sdf::PathExpression::parse("/W//"))?;
let root = stage.root_layer().identifier().to_string();
let overwrite = |attr: Attribute, value: Value| -> Result<()> {
stage.layer_mut(&root).expect("root layer is live").edit(|e| {
e.attribute_mut(attr.path())?
.expect("authored above")
.set("default", value);
Ok(())
})?;
Ok(())
};
overwrite(coll.expansion_rule_attr(), Value::token("bogus"))?;
overwrite(coll.mode_attr(), Value::token("bogus"))?;
let query = coll.compute_membership_query()?;
assert_eq!(query.top_expansion_rule, ExpansionRule::ExpandPrims);
assert!(
query.evaluator.is_some(),
"an automatic collection reads its expression"
);
overwrite(coll.expansion_rule_attr(), Value::Int(3))?;
overwrite(coll.mode_attr(), Value::Int(3))?;
overwrite(coll.include_root_attr(), Value::Int(1))?;
overwrite(coll.membership_expression_attr(), Value::Int(3))?;
let query = coll.compute_membership_query()?;
assert_eq!(query.top_expansion_rule, ExpansionRule::ExpandPrims);
assert_eq!(membership_expression(&coll)?, None);
assert!(coll.has_no_included_paths()?, "includeRoot reads as off");
Ok(())
}
#[test]
fn expand_prims_includes_descendant_prims_not_properties() -> Result<()> {
let q = query(&[("/W/A", PathRule::ExpandPrims)]);
assert!(q.is_path_included(&sdf::path("/W/A")?)); assert!(q.is_path_included(&sdf::path("/W/A/B")?)); assert!(!q.is_path_included(&sdf::path("/W")?)); assert!(!q.is_path_included(&sdf::path("/W/A.x")?)); assert!(!q.is_path_included(&sdf::path("/W/Other")?)); Ok(())
}
#[test]
fn explicit_only_matches_exact_paths() -> Result<()> {
let q = query(&[("/W/A", PathRule::ExplicitOnly)]);
assert!(q.is_path_included(&sdf::path("/W/A")?));
assert!(!q.is_path_included(&sdf::path("/W/A/B")?)); Ok(())
}
#[test]
fn expand_prims_and_properties_includes_properties() -> Result<()> {
let q = query(&[("/W/A", PathRule::ExpandPrimsAndProperties)]);
assert!(q.is_path_included(&sdf::path("/W/A")?));
assert!(q.is_path_included(&sdf::path("/W/A/B")?));
assert!(q.is_path_included(&sdf::path("/W/A.x")?)); Ok(())
}
#[test]
fn closest_ancestor_excludes_win() -> Result<()> {
let q = query(&[("/W", PathRule::ExpandPrims), ("/W/A", PathRule::Exclude)]);
assert!(q.is_path_included(&sdf::path("/W/B")?)); assert!(!q.is_path_included(&sdf::path("/W/A")?)); assert!(!q.is_path_included(&sdf::path("/W/A/C")?)); Ok(())
}
#[test]
fn below_propagates_parent_rule() -> Result<()> {
let q = query(&[("/W", PathRule::ExpandPrims)]);
let (inc, rule) = q.is_path_included_below(&sdf::path("/W/A")?, PathRule::ExpandPrims);
assert!(inc);
assert_eq!(rule, PathRule::ExpandPrims);
let (inc, _) = q.is_path_included_below(&sdf::path("/W/A/B")?, PathRule::Exclude);
assert!(!inc);
Ok(())
}
fn included_top_down(q: &MembershipQuery, path: &Path) -> bool {
let mut chain = vec![path.clone()];
while let Some(parent) = chain.last().unwrap().parent() {
if parent.is_empty() {
break;
}
chain.push(parent);
}
chain.reverse();
let mut parent_rule = match chain[0].parent() {
Some(p) if !p.is_empty() => q.effective_rule(&p),
_ => PathRule::Exclude,
};
let mut included = false;
for elem in &chain {
let (inc, rule) = q.is_path_included_below(elem, parent_rule);
included = inc;
parent_rule = rule;
}
included
}
#[test]
fn membership_methods_agree() -> Result<()> {
let q = query(&[
("/W", PathRule::ExpandPrims),
("/W/A", PathRule::Exclude),
("/W/B", PathRule::ExpandPrimsAndProperties),
("/W/C", PathRule::ExplicitOnly),
("/W/B.size", PathRule::ExpandPrimsAndProperties),
]);
for p in [
"/W",
"/W/A",
"/W/A/C",
"/W/B",
"/W/B/D",
"/W/B.size",
"/W/C",
"/W/C/D",
"/W/D",
"/W.x",
"/Other",
] {
let path = sdf::path(p)?;
assert_eq!(
q.is_path_included(&path),
included_top_down(&q, &path),
"point query and top-down fold disagree on {p}"
);
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn build_collection(
stage: &Stage,
prim: &str,
name: &str,
rule: ExpansionRule,
include_root: bool,
includes: &[&str],
excludes: &[&str],
) -> Result<CollectionAPI> {
let coll = author_collection(stage, prim, name)?;
coll.set_expansion_rule(rule)?;
if include_root {
coll.set_include_root(true)?;
}
if !includes.is_empty() {
coll.create_includes_rel()?.set_targets(includes.iter().copied())?;
}
if !excludes.is_empty() {
coll.create_excludes_rel()?.set_targets(excludes.iter().copied())?;
}
Ok(coll)
}
#[test]
fn compute_basic_includes() -> Result<()> {
let stage = Stage::builder().in_memory("anon.usda")?;
let coll = build_collection(&stage, "/W", "c", ExpansionRule::ExpandPrims, false, &["/W/A"], &[])?;
let q = coll.compute_membership_query()?;
assert!(q.is_path_included(&sdf::path("/W/A/B")?));
assert!(!q.is_path_included(&sdf::path("/W/Other")?));
Ok(())
}
#[test]
fn compute_include_root_with_excludes() -> Result<()> {
let stage = Stage::builder().in_memory("anon.usda")?;
let coll = build_collection(&stage, "/W", "c", ExpansionRule::ExpandPrims, true, &[], &["/W/A"])?;
let q = coll.compute_membership_query()?;
assert!(q.is_path_included(&sdf::path("/W/B")?));
assert!(!q.is_path_included(&sdf::path("/W/A")?));
assert!(!q.is_path_included(&sdf::path("/W/A/C")?));
Ok(())
}
#[test]
fn compute_merges_nested_collection() -> Result<()> {
let stage = Stage::builder().in_memory("anon.usda")?;
build_collection(&stage, "/R", "inner", ExpansionRule::ExpandPrims, false, &["/W/X"], &[])?;
let outer = build_collection(
&stage,
"/R",
"outer",
ExpansionRule::ExpandPrims,
false,
&["/R.collection:inner"],
&[],
)?;
let q = outer.compute_membership_query()?;
assert!(q.is_path_included(&sdf::path("/W/X/Leaf")?));
Ok(())
}
#[test]
fn compute_breaks_cycle() -> Result<()> {
let stage = Stage::builder().in_memory("anon.usda")?;
let a = build_collection(
&stage,
"/R",
"a",
ExpansionRule::ExpandPrims,
false,
&["/R.collection:b"],
&[],
)?;
build_collection(
&stage,
"/R",
"b",
ExpansionRule::ExpandPrims,
false,
&["/R.collection:a"],
&[],
)?;
let q = a.compute_membership_query()?;
assert!(q.is_empty());
Ok(())
}
fn scene() -> Result<Stage> {
let stage = Stage::builder().in_memory("anon.usda")?;
for p in ["/W", "/W/A", "/W/A/C", "/W/B"] {
stage.define_prim(sdf::path(p)?)?.set_type_name("Scope")?;
}
Ok(stage)
}
#[test]
fn included_paths_expand_prims() -> Result<()> {
let stage = scene()?;
let coll = build_collection(&stage, "/Col", "c", ExpansionRule::ExpandPrims, false, &["/W/A"], &[])?;
let q = coll.compute_membership_query()?;
let mut paths = compute_included_paths(&stage, &q, PrimPredicate::DEFAULT)?;
paths.sort();
assert_eq!(paths, vec![sdf::path("/W/A")?, sdf::path("/W/A/C")?]);
Ok(())
}
#[test]
fn included_paths_explicit_only() -> Result<()> {
let stage = scene()?;
let coll = build_collection(&stage, "/Col", "c", ExpansionRule::ExplicitOnly, false, &["/W/A"], &[])?;
let q = coll.compute_membership_query()?;
let paths = compute_included_paths(&stage, &q, PrimPredicate::DEFAULT)?;
assert_eq!(paths, vec![sdf::path("/W/A")?]); Ok(())
}
#[test]
fn included_paths_include_root_minus_excludes() -> Result<()> {
let stage = scene()?;
let coll = build_collection(
&stage,
"/Col",
"c",
ExpansionRule::ExpandPrims,
true,
&["/W"],
&["/W/A"],
)?;
let q = coll.compute_membership_query()?;
let paths = compute_included_paths(&stage, &q, PrimPredicate::DEFAULT)?;
assert!(paths.contains(&sdf::path("/W/B")?));
assert!(!paths.contains(&sdf::path("/W/A")?));
assert!(!paths.contains(&sdf::path("/W/A/C")?));
Ok(())
}
#[test]
fn authoring_include_exclude_roundtrip() -> Result<()> {
let stage = scene()?;
let coll = apply_collection(&stage, sdf::path("/W")?, "c")?;
coll.set_expansion_rule(ExpansionRule::ExpandPrims)?;
coll.include_path(sdf::path("/W/A")?)?;
coll.exclude_path(sdf::path("/W/A/C")?)?;
let q = coll.compute_membership_query()?;
assert!(q.is_path_included(&sdf::path("/W/A")?));
assert!(!q.is_path_included(&sdf::path("/W/A/C")?));
assert_eq!(CollectionAPI::get_all(&stage.prim("/W")?)?.len(), 1);
Ok(())
}
#[test]
fn include_path_drops_stale_exclude() -> Result<()> {
let stage = scene()?;
let coll = apply_collection(&stage, sdf::path("/W")?, "c")?;
coll.set_include_root(true)?;
coll.exclude_path(sdf::path("/W/A")?)?;
assert!(!coll.compute_membership_query()?.is_path_included(&sdf::path("/W/A")?));
coll.include_path(sdf::path("/W/A")?)?;
assert!(coll.excludes()?.is_empty());
assert!(coll.compute_membership_query()?.is_path_included(&sdf::path("/W/A")?));
Ok(())
}
#[test]
fn apply_authors_missing_prim() -> Result<()> {
let stage = Stage::builder().in_memory("anon.usda")?;
let coll = apply_collection(&stage, sdf::path("/W")?, "c")?;
coll.include_path(sdf::path("/W/A")?)?;
assert_eq!(CollectionAPI::get_all(&stage.prim("/W")?)?.len(), 1);
assert!(coll.compute_membership_query()?.is_path_included(&sdf::path("/W/A")?));
Ok(())
}
#[test]
fn apply_rejects_bad_name() -> Result<()> {
let stage = scene()?;
assert!(apply_collection(&stage, sdf::path("/W")?, "").is_err());
assert!(apply_collection(&stage, sdf::path("/W")?, "no space").is_err());
assert!(apply_collection(&stage, sdf::path("/W")?, "includes").is_err());
assert!(apply_collection(&stage, sdf::path("/W")?, "lighting:key").is_ok());
assert!(apply_collection(&stage, sdf::path("/W")?, "render").is_ok());
Ok(())
}
#[test]
fn enumerates_namespaced_names() -> Result<()> {
let stage = Stage::builder().in_memory("anon.usda")?;
stage
.define_prim("/W")?
.set_type_name("Scope")?
.add_applied_schema("CollectionAPI:render")?
.add_applied_schema("CollectionAPI:")? .add_applied_schema("CollectionAPI:lighting:key")?;
let names: Vec<String> = CollectionAPI::get_all(&stage.prim("/W")?)?
.into_iter()
.map(|c| c.name().to_string())
.collect();
assert_eq!(names, vec!["render".to_string(), "lighting:key".to_string()]);
Ok(())
}
#[test]
fn explicit_props_sorted() -> Result<()> {
let stage = scene()?;
for p in ["/W/B.a", "/W/B.c", "/W/B.b"] {
stage.create_attribute(sdf::path(p)?, "float")?.set(Value::Float(0.0))?;
}
let coll = build_collection(
&stage,
"/Col",
"c",
ExpansionRule::ExplicitOnly,
false,
&["/W/B.c", "/W/B.a", "/W/B.b"],
&[],
)?;
let q = coll.compute_membership_query()?;
let paths = compute_included_paths(&stage, &q, PrimPredicate::DEFAULT)?;
assert_eq!(
paths,
vec![sdf::path("/W/B.a")?, sdf::path("/W/B.b")?, sdf::path("/W/B.c")?]
);
Ok(())
}
#[test]
fn authored_relationships_not_custom() -> Result<()> {
let stage = scene()?;
let coll = apply_collection(&stage, sdf::path("/W")?, "c")?;
coll.include_path(sdf::path("/W")?)?;
coll.exclude_path(sdf::path("/W/A")?)?;
for relationship in [coll.includes_rel(), coll.excludes_rel()] {
let path = relationship.path();
assert!(relationship.has_authored_targets()?, "{path} should be authored");
assert!(!relationship.is_custom()?, "{path} should not be custom");
}
Ok(())
}
#[test]
fn has_no_included_paths_tracks_state() -> Result<()> {
let stage = scene()?;
let coll = apply_collection(&stage, sdf::path("/W")?, "c")?;
assert!(coll.has_no_included_paths()?);
coll.include_path(sdf::path("/W/A")?)?;
assert!(!coll.has_no_included_paths()?);
Ok(())
}
#[test]
fn exclude_path_on_empty_collection_records_target() -> Result<()> {
let stage = scene()?;
let coll = apply_collection(&stage, sdf::path("/W")?, "c")?;
coll.exclude_path(sdf::path("/W/A")?)?;
assert_eq!(coll.excludes()?, vec![sdf::path("/W/A")?]);
Ok(())
}
#[test]
fn exclude_root_clears_include_root() -> Result<()> {
let stage = scene()?;
let coll = apply_collection(&stage, sdf::path("/W")?, "c")?;
coll.set_include_root(true)?;
coll.exclude_path(Path::abs_root())?;
assert_ne!(coll.include_root()?, Some(true));
Ok(())
}
#[test]
fn include_path_already_included_is_noop() -> Result<()> {
let stage = scene()?;
let coll = apply_collection(&stage, sdf::path("/W")?, "c")?;
coll.include_path(sdf::path("/W/A")?)?; coll.include_path(sdf::path("/W/A/C")?)?; assert_eq!(coll.includes()?, vec![sdf::path("/W/A")?]); Ok(())
}
#[test]
fn included_paths_expand_properties() -> Result<()> {
let stage = scene()?;
stage.create_attribute("/W/B.size", "float")?.set(Value::Float(1.0))?;
let coll = build_collection(
&stage,
"/Col",
"c",
ExpansionRule::ExpandPrimsAndProperties,
false,
&["/W/B"],
&[],
)?;
let q = coll.compute_membership_query()?;
let paths = compute_included_paths(&stage, &q, PrimPredicate::DEFAULT)?;
assert!(paths.contains(&sdf::path("/W/B")?));
assert!(paths.contains(&sdf::path("/W/B.size")?)); Ok(())
}
#[test]
fn included_paths_skip_missing_property() -> Result<()> {
let stage = scene()?;
stage.create_attribute("/W/B.size", "float")?.set(Value::Float(1.0))?;
let coll = build_collection(
&stage,
"/Col",
"c",
ExpansionRule::ExpandPrims,
false,
&["/W/B.size", "/W/B.ghost"],
&[],
)?;
let q = coll.compute_membership_query()?;
let paths = compute_included_paths(&stage, &q, PrimPredicate::DEFAULT)?;
assert!(paths.contains(&sdf::path("/W/B.size")?)); assert!(!paths.contains(&sdf::path("/W/B.ghost")?)); Ok(())
}
fn build_expression(stage: &Stage, name: &str, expression: &str) -> Result<CollectionAPI> {
let coll = author_collection(stage, "/Col", name)?;
coll.set_membership_expression(sdf::PathExpression::parse(expression))?;
Ok(coll)
}
#[test]
fn mode_round_trips() {
for mode in [
CollectionMode::Automatic,
CollectionMode::Relationship,
CollectionMode::Expression,
] {
assert_eq!(CollectionMode::from_token(mode.as_token()), Some(mode));
}
assert_eq!(CollectionMode::from_token("bogus"), None);
}
#[test]
fn expression_membership() -> Result<()> {
let stage = scene()?;
let coll = build_expression(&stage, "e", "/W/A//")?;
let q = coll.compute_membership_query()?;
assert!(!q.uses_path_expansion_rule_map());
assert!(!q.is_empty());
assert!(q.is_path_included(&sdf::path("/W/A")?));
assert!(q.is_path_included(&sdf::path("/W/A/C")?));
assert!(!q.is_path_included(&sdf::path("/W/B")?));
assert!(!q.is_path_included(&sdf::path("/W/A/ghost")?));
Ok(())
}
#[test]
fn automatic_rule_map_wins() -> Result<()> {
let stage = scene()?;
let coll = build_collection(&stage, "/Col", "c", ExpansionRule::ExpandPrims, false, &["/W/B"], &[])?;
coll.set_membership_expression(sdf::PathExpression::parse("/W/A//"))?;
let q = coll.compute_membership_query()?;
assert!(q.uses_path_expansion_rule_map());
assert!(q.is_path_included(&sdf::path("/W/B")?));
assert!(!q.is_path_included(&sdf::path("/W/A")?));
coll.set_mode(CollectionMode::Expression)?;
let q = coll.compute_membership_query()?;
assert!(!q.uses_path_expansion_rule_map());
assert!(q.is_path_included(&sdf::path("/W/A")?));
assert!(!q.is_path_included(&sdf::path("/W/B")?));
Ok(())
}
#[test]
fn mode_relationship_ignores_expr() -> Result<()> {
let stage = scene()?;
let coll = build_expression(&stage, "e", "/W/A//")?;
coll.set_mode(CollectionMode::Relationship)?;
let q = coll.compute_membership_query()?;
assert!(q.is_empty());
assert!(!q.is_path_included(&sdf::path("/W/A")?));
Ok(())
}
#[test]
fn expression_included_paths() -> Result<()> {
let stage = scene()?;
let coll = build_expression(&stage, "e", "/W/A//")?;
let q = coll.compute_membership_query()?;
let mut paths = compute_included_paths(&stage, &q, PrimPredicate::DEFAULT)?;
paths.sort();
assert_eq!(paths, vec![sdf::path("/W/A")?, sdf::path("/W/A/C")?]);
Ok(())
}
#[test]
fn expression_included_properties() -> Result<()> {
let stage = scene()?;
stage.create_attribute("/W/A.size", "float")?.set(Value::Float(1.0))?;
stage.create_attribute("/W/B.size", "float")?.set(Value::Float(2.0))?;
let coll = build_expression(&stage, "e", "//A.size")?;
coll.set_expansion_rule(ExpansionRule::ExpandPrimsAndProperties)?;
let q = coll.compute_membership_query()?;
let paths = compute_included_paths(&stage, &q, PrimPredicate::DEFAULT)?;
assert!(paths.contains(&sdf::path("/W/A.size")?));
assert!(!paths.contains(&sdf::path("/W/B.size")?));
let bulk = build_expression(&stage, "b", "/W/B//")?;
bulk.set_expansion_rule(ExpansionRule::ExpandPrimsAndProperties)?;
let q = bulk.compute_membership_query()?;
let paths = compute_included_paths(&stage, &q, PrimPredicate::DEFAULT)?;
assert!(paths.contains(&sdf::path("/W/B")?));
assert!(paths.contains(&sdf::path("/W/B.size")?));
Ok(())
}
#[test]
fn expression_resolves_references() -> Result<()> {
let stage = scene()?;
let big = build_expression(&stage, "big", "/W/A// %:small")?;
build_expression(&stage, "small", "/W/B//")?;
let resolved = resolve_complete_membership_expression(&big)?;
assert_eq!(resolved.to_string(), "/W/A// /W/B//");
let q = big.compute_membership_query()?;
assert!(q.is_path_included(&sdf::path("/W/A")?));
assert!(q.is_path_included(&sdf::path("/W/B")?));
Ok(())
}
#[test]
fn expression_reference_cycle() -> Result<()> {
let stage = scene()?;
let a = build_expression(&stage, "a", "/W/A// %:b")?;
build_expression(&stage, "b", "/W/B// %/Col:a")?;
let resolved = resolve_complete_membership_expression(&a)?;
assert_eq!(resolved.to_string(), "/W/A// /W/B//");
Ok(())
}
#[test]
fn expression_unknown_reference() -> Result<()> {
let stage = scene()?;
let coll = build_expression(&stage, "e", "/W/A// %:missing")?;
let resolved = resolve_complete_membership_expression(&coll)?;
assert_eq!(resolved.to_string(), "/W/A//");
Ok(())
}
#[test]
fn expression_reference_diamond() -> Result<()> {
let stage = scene()?;
let top = build_expression(&stage, "top", "%:x %:y")?;
build_expression(&stage, "x", "%:shared")?;
build_expression(&stage, "y", "%:shared /W/A//")?;
build_expression(&stage, "shared", "/W/B//")?;
let resolved = resolve_complete_membership_expression(&top)?;
assert_eq!(resolved.to_string(), "/W/B// (/W/B// /W/A//)");
Ok(())
}
#[test]
fn expression_cycle_not_memoized() -> Result<()> {
let stage = scene()?;
let top = build_expression(&stage, "top", "%:a %:b")?;
build_expression(&stage, "a", "%:b /W/A//")?;
build_expression(&stage, "b", "%:a /W/B//")?;
let resolved = resolve_complete_membership_expression(&top)?;
assert_eq!(resolved.to_string(), "/W/B// /W/A// (/W/A// /W/B//)");
Ok(())
}
#[test]
fn predicate_unknown_keyword() -> Result<()> {
let stage = scene()?;
stage
.define_prim("/W/M")?
.set_type_name("Scope")?
.set_kind("component")?;
let coll = build_expression(&stage, "k", "/W//{kind(component, bogus=true)}")?;
let q = coll.compute_membership_query()?;
assert!(!q.is_path_included(&sdf::path("/W/M")?));
let ok = build_expression(&stage, "k2", "/W//{kind(component, strict=true)}")?;
let q = ok.compute_membership_query()?;
assert!(q.is_path_included(&sdf::path("/W/M")?));
Ok(())
}
#[test]
fn expression_predicates() -> Result<()> {
let stage = scene()?;
stage.override_prim("/W/O")?;
let defined = build_expression(&stage, "d", "/W//{specifier:def}")?;
let q = defined.compute_membership_query()?;
assert!(q.is_path_included(&sdf::path("/W/A")?));
assert!(!q.is_path_included(&sdf::path("/W/O")?));
let overs = build_expression(&stage, "o", "/W//{specifier:over}")?;
let q = overs.compute_membership_query()?;
assert!(q.is_path_included(&sdf::path("/W/O")?));
assert!(!q.is_path_included(&sdf::path("/W/A")?));
Ok(())
}
#[test]
fn expression_query_equality() -> Result<()> {
let stage = scene()?;
let coll = build_expression(&stage, "e", "/W/A//")?;
let a = coll.compute_membership_query()?;
let b = coll.compute_membership_query()?;
assert_ne!(a, b);
assert_ne!(a.clone(), a.clone());
Ok(())
}
}