use std::collections::{HashMap, HashSet};
use anyhow::Result;
use crate::sdf::{FieldKey, Path, Value, Variability};
use crate::usd::{Prim, PrimPredicate, Relationship, Stage};
const API_COLLECTION: &str = "CollectionAPI";
const NS_COLLECTION: &str = "collection:";
const EXPANSION_RULE: &str = "expansionRule";
const INCLUDE_ROOT: &str = "includeRoot";
const INCLUDES: &str = "includes";
const EXCLUDES: &str = "excludes";
const MEMBERSHIP_EXPRESSION: &str = "membershipExpression";
const TOK_EXPLICIT_ONLY: &str = "explicitOnly";
const TOK_EXPAND_PRIMS: &str = "expandPrims";
const TOK_EXPAND_PRIMS_AND_PROPERTIES: &str = "expandPrimsAndProperties";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ExpansionRule {
ExplicitOnly,
#[default]
ExpandPrims,
ExpandPrimsAndProperties,
}
impl ExpansionRule {
pub fn as_token(self) -> &'static str {
match self {
ExpansionRule::ExplicitOnly => TOK_EXPLICIT_ONLY,
ExpansionRule::ExpandPrims => TOK_EXPAND_PRIMS,
ExpansionRule::ExpandPrimsAndProperties => TOK_EXPAND_PRIMS_AND_PROPERTIES,
}
}
pub fn from_token(s: &str) -> Option<Self> {
Some(match s {
TOK_EXPLICIT_ONLY => ExpansionRule::ExplicitOnly,
TOK_EXPAND_PRIMS => ExpansionRule::ExpandPrims,
TOK_EXPAND_PRIMS_AND_PROPERTIES => ExpansionRule::ExpandPrimsAndProperties,
_ => return None,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Collection {
prim: Path,
name: String,
}
impl Collection {
pub fn new(prim: impl Into<Path>, name: impl Into<String>) -> Self {
Collection {
prim: prim.into(),
name: name.into(),
}
}
pub fn prim(&self) -> &Path {
&self.prim
}
pub fn name(&self) -> &str {
&self.name
}
pub fn collection_path(&self) -> Result<Path> {
self.prim.append_property(&format!("{NS_COLLECTION}{}", self.name))
}
fn prop(&self, suffix: &str) -> Result<Path> {
self.prim.append_property(&self.rel_name(suffix))
}
pub fn expansion_rule(&self, stage: &Stage) -> Result<ExpansionRule> {
Ok(
match stage.field::<Value>(self.prop(EXPANSION_RULE)?, FieldKey::Default)? {
Some(Value::Token(t) | Value::String(t)) => ExpansionRule::from_token(&t).unwrap_or_default(),
_ => ExpansionRule::default(),
},
)
}
pub fn include_root(&self, stage: &Stage) -> Result<bool> {
Ok(matches!(
stage.field::<Value>(self.prop(INCLUDE_ROOT)?, FieldKey::Default)?,
Some(Value::Bool(true))
))
}
pub fn includes(&self, stage: &Stage) -> Result<Vec<Path>> {
stage.relationship_at(self.prop(INCLUDES)?).targets()
}
pub fn excludes(&self, stage: &Stage) -> Result<Vec<Path>> {
stage.relationship_at(self.prop(EXCLUDES)?).targets()
}
pub fn membership_expression(&self, stage: &Stage) -> Result<Option<String>> {
Ok(
match stage.field::<Value>(self.prop(MEMBERSHIP_EXPRESSION)?, FieldKey::Default)? {
Some(Value::PathExpression(s) | Value::String(s) | Value::Token(s)) => Some(s),
_ => None,
},
)
}
pub fn has_expression(&self, stage: &Stage) -> Result<bool> {
Ok(self.membership_expression(stage)?.is_some()
&& self.includes(stage)?.is_empty()
&& self.excludes(stage)?.is_empty()
&& !self.include_root(stage)?)
}
pub fn compute_membership_query(&self, stage: &Stage) -> Result<MembershipQuery> {
let mut map = PathExpansionRuleMap::new();
let mut visited = HashSet::new();
visited.insert(self.collection_path()?);
self.build_into(stage, &mut map, &mut visited)?;
Ok(MembershipQuery::new(map))
}
fn rel_name(&self, suffix: &str) -> String {
format!("{NS_COLLECTION}{}:{suffix}", self.name)
}
fn schema_rel(&self, prim: &Prim, suffix: &str) -> Result<Relationship> {
Ok(prim.create_relationship(&self.rel_name(suffix))?.set_custom(false)?)
}
pub fn set_expansion_rule(&self, stage: &Stage, rule: ExpansionRule) -> Result<()> {
stage
.create_attribute(self.prop(EXPANSION_RULE)?, "token")?
.set_variability(Variability::Uniform)?
.set_custom(false)?
.set(Value::Token(rule.as_token().to_string()))?;
Ok(())
}
pub fn set_include_root(&self, stage: &Stage, value: bool) -> Result<()> {
stage
.create_attribute(self.prop(INCLUDE_ROOT)?, "bool")?
.set_variability(Variability::Uniform)?
.set_custom(false)?
.set(Value::Bool(value))?;
Ok(())
}
pub fn include_path(&self, stage: &Stage, path: impl Into<Path>) -> Result<()> {
let path = path.into();
if self.compute_membership_query(stage)?.is_path_included(&path) {
return Ok(()); }
if path.is_abs_root() {
return self.set_include_root(stage, true);
}
let prim = Prim::new(stage, self.prim.clone());
if self.excludes(stage)?.contains(&path) {
self.schema_rel(&prim, EXCLUDES)?.remove_target(&path)?;
if self.compute_membership_query(stage)?.is_path_included(&path) {
return Ok(()); }
}
self.schema_rel(&prim, INCLUDES)?.add_target(path)?;
Ok(())
}
pub fn exclude_path(&self, stage: &Stage, path: impl Into<Path>) -> Result<()> {
let path = path.into();
let query = self.compute_membership_query(stage)?;
if !query.is_empty() && !query.is_path_included(&path) {
return Ok(()); }
if path.is_abs_root() {
return self.set_include_root(stage, false);
}
let prim = Prim::new(stage, self.prim.clone());
if !query.is_empty() && self.includes(stage)?.contains(&path) {
self.schema_rel(&prim, INCLUDES)?.remove_target(&path)?;
let query = self.compute_membership_query(stage)?;
if !query.is_empty() && !query.is_path_included(&path) {
return Ok(()); }
}
self.schema_rel(&prim, EXCLUDES)?.add_target(path)?;
Ok(())
}
pub fn has_no_included_paths(&self, stage: &Stage) -> Result<bool> {
Ok(self.includes(stage)?.is_empty()
&& !self.include_root(stage)?
&& (!self.excludes(stage)?.is_empty() || self.membership_expression(stage)?.is_none()))
}
fn build_into(&self, stage: &Stage, map: &mut PathExpansionRuleMap, visited: &mut HashSet<Path>) -> Result<()> {
let rule = self.expansion_rule(stage)?;
let path_rule = PathRule::from_expansion(rule);
if self.include_root(stage)? && rule != ExpansionRule::ExplicitOnly {
map.insert(Path::abs_root(), path_rule);
}
for included in self.includes(stage)? {
if let Some((prim, name)) = is_collection_api_path(&included) {
let nested = Collection::new(prim, name);
if visited.insert(nested.collection_path()?) {
nested.build_into(stage, map, visited)?;
}
continue;
}
map.insert(included, path_rule);
}
for excluded in self.excludes(stage)? {
map.insert(excluded, PathRule::Exclude);
}
Ok(())
}
}
pub fn apply_collection(stage: &Stage, prim: impl Into<Path>, name: impl Into<String>) -> Result<Collection> {
let prim = prim.into();
let name = name.into();
if !Path::is_valid_identifier(&name) {
anyhow::bail!("invalid collection name {name:?}: must be a valid identifier");
}
stage
.override_prim(prim.clone())?
.add_applied_schema(format!("{API_COLLECTION}:{name}"))?;
Ok(Collection::new(prim, name))
}
pub fn collections_on(stage: &Stage, prim: &Path) -> Result<Vec<Collection>> {
let mut out = Vec::new();
for schema in stage.prim_at(prim.clone()).api_schemas()? {
if let Some(name) = instance_name(&schema) {
out.push(Collection::new(prim.clone(), name));
}
}
Ok(out)
}
fn instance_name(api_schema: &str) -> Option<String> {
let rest = api_schema.strip_prefix(API_COLLECTION)?.strip_prefix(':')?;
Path::is_valid_identifier(rest).then(|| rest.to_string())
}
pub fn is_collection_api_path(path: &Path) -> Option<(Path, String)> {
let (prim, property) = path.split_property()?;
let rest = property.strip_prefix(NS_COLLECTION)?;
Path::is_valid_identifier(rest).then(|| (prim, rest.to_string()))
}
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);
}
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 {
if 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.clone())? && seen.insert(path.clone()) {
out.push(path.clone());
}
}
Ok(out)
}
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_at(prim.clone()).property_names()? {
let prop = prim.append_property(&name)?;
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, PartialEq)]
pub struct MembershipQuery {
rule_map: PathExpansionRuleMap,
}
impl MembershipQuery {
pub fn new(rule_map: PathExpansionRuleMap) -> Self {
MembershipQuery { rule_map }
}
pub fn rule_map(&self) -> &PathExpansionRuleMap {
&self.rule_map
}
pub fn is_empty(&self) -> bool {
self.rule_map.is_empty()
}
pub fn is_path_included(&self, path: &Path) -> bool {
let (rule, on_self) = self.closest_rule(path);
rule_includes(rule, on_self, path.is_property_path())
}
pub fn is_path_included_below(&self, path: &Path, parent_rule: PathRule) -> (bool, PathRule) {
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 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::sdf;
use crate::sdf::Variability;
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<()> {
stage
.define_prim(sdf::path(prim)?)?
.set_type_name("Scope")?
.add_applied_schema(format!("{API_COLLECTION}:{name}"))?;
Ok(())
}
#[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<()> {
assert_eq!(
is_collection_api_path(&sdf::path("/W.collection:render")?),
Some((sdf::path("/W")?, "render".to_string()))
);
assert_eq!(
is_collection_api_path(&sdf::path("/W.collection:render:includes")?),
None
);
assert_eq!(is_collection_api_path(&sdf::path("/W.foo")?), None);
assert_eq!(is_collection_api_path(&sdf::path("/W")?), None);
Ok(())
}
#[test]
fn enumerates_collections_on_prim() -> Result<()> {
let stage = Stage::builder().in_memory("anon.usda")?;
stage
.define_prim(sdf::path("/W")?)?
.set_type_name("Scope")?
.add_applied_schema("CollectionAPI:render")?
.add_applied_schema("CollectionAPI:proxy")?
.add_applied_schema("MaterialBindingAPI")?;
let names: Vec<String> = collections_on(&stage, &sdf::path("/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")?;
author_collection(&stage, "/W", "render")?;
let w = sdf::path("/W")?;
let coll = Collection::new(w.clone(), "render");
stage
.create_attribute(coll.prop(EXPANSION_RULE)?, "token")?
.set_variability(Variability::Uniform)?
.set(Value::Token(ExpansionRule::ExplicitOnly.as_token().to_string()))?;
stage
.create_attribute(coll.prop(INCLUDE_ROOT)?, "bool")?
.set_variability(Variability::Uniform)?
.set(Value::Bool(true))?;
crate::usd::Prim::new(&stage, w.clone())
.author_relationship_targets(&format!("collection:render:{INCLUDES}"), [sdf::path("/W/A")?])?;
assert_eq!(coll.expansion_rule(&stage)?, ExpansionRule::ExplicitOnly);
assert!(coll.include_root(&stage)?);
assert_eq!(coll.includes(&stage)?, vec![sdf::path("/W/A")?]);
assert!(coll.excludes(&stage)?.is_empty());
author_collection(&stage, "/X", "c")?;
let bare = Collection::new(sdf::path("/X")?, "c");
assert_eq!(bare.expansion_rule(&stage)?, ExpansionRule::ExpandPrims);
assert!(!bare.include_root(&stage)?);
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<()> {
let prim_path = sdf::path(prim)?;
stage
.define_prim(prim_path.clone())?
.set_type_name("Scope")?
.add_applied_schema(format!("{API_COLLECTION}:{name}"))?;
let coll = Collection::new(prim_path.clone(), name);
stage
.create_attribute(coll.prop(EXPANSION_RULE)?, "token")?
.set_variability(Variability::Uniform)?
.set(Value::Token(rule.as_token().to_string()))?;
if include_root {
stage
.create_attribute(coll.prop(INCLUDE_ROOT)?, "bool")?
.set_variability(Variability::Uniform)?
.set(Value::Bool(true))?;
}
let prim_handle = crate::usd::Prim::new(stage, prim_path);
if !includes.is_empty() {
let targets: Vec<Path> = includes.iter().map(|p| sdf::path(p).unwrap()).collect();
prim_handle.author_relationship_targets(&format!("collection:{name}:{INCLUDES}"), targets)?;
}
if !excludes.is_empty() {
let targets: Vec<Path> = excludes.iter().map(|p| sdf::path(p).unwrap()).collect();
prim_handle.author_relationship_targets(&format!("collection:{name}:{EXCLUDES}"), targets)?;
}
Ok(())
}
#[test]
fn compute_basic_includes() -> Result<()> {
let stage = Stage::builder().in_memory("anon.usda")?;
build_collection(&stage, "/W", "c", ExpansionRule::ExpandPrims, false, &["/W/A"], &[])?;
let q = Collection::new(sdf::path("/W")?, "c").compute_membership_query(&stage)?;
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")?;
build_collection(&stage, "/W", "c", ExpansionRule::ExpandPrims, true, &[], &["/W/A"])?;
let q = Collection::new(sdf::path("/W")?, "c").compute_membership_query(&stage)?;
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"], &[])?;
build_collection(
&stage,
"/R",
"outer",
ExpansionRule::ExpandPrims,
false,
&["/R.collection:inner"],
&[],
)?;
let q = Collection::new(sdf::path("/R")?, "outer").compute_membership_query(&stage)?;
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")?;
build_collection(
&stage,
"/R",
"a",
ExpansionRule::ExpandPrims,
false,
&["/R.collection:b"],
&[],
)?;
build_collection(
&stage,
"/R",
"b",
ExpansionRule::ExpandPrims,
false,
&["/R.collection:a"],
&[],
)?;
let q = Collection::new(sdf::path("/R")?, "a").compute_membership_query(&stage)?;
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()?;
build_collection(&stage, "/Col", "c", ExpansionRule::ExpandPrims, false, &["/W/A"], &[])?;
let q = Collection::new(sdf::path("/Col")?, "c").compute_membership_query(&stage)?;
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()?;
build_collection(&stage, "/Col", "c", ExpansionRule::ExplicitOnly, false, &["/W/A"], &[])?;
let q = Collection::new(sdf::path("/Col")?, "c").compute_membership_query(&stage)?;
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()?;
build_collection(
&stage,
"/Col",
"c",
ExpansionRule::ExpandPrims,
true,
&["/W"],
&["/W/A"],
)?;
let q = Collection::new(sdf::path("/Col")?, "c").compute_membership_query(&stage)?;
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(&stage, ExpansionRule::ExpandPrims)?;
coll.include_path(&stage, sdf::path("/W/A")?)?;
coll.exclude_path(&stage, sdf::path("/W/A/C")?)?;
let q = coll.compute_membership_query(&stage)?;
assert!(q.is_path_included(&sdf::path("/W/A")?));
assert!(!q.is_path_included(&sdf::path("/W/A/C")?));
assert_eq!(collections_on(&stage, &sdf::path("/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(&stage, true)?;
coll.exclude_path(&stage, sdf::path("/W/A")?)?;
assert!(!coll
.compute_membership_query(&stage)?
.is_path_included(&sdf::path("/W/A")?));
coll.include_path(&stage, sdf::path("/W/A")?)?;
assert!(coll.excludes(&stage)?.is_empty());
assert!(coll
.compute_membership_query(&stage)?
.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(&stage, sdf::path("/W/A")?)?;
assert_eq!(collections_on(&stage, &sdf::path("/W")?)?.len(), 1);
assert!(coll
.compute_membership_query(&stage)?
.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")?, "a:b").is_err()); assert!(apply_collection(&stage, sdf::path("/W")?, "render").is_ok());
Ok(())
}
#[test]
fn skips_malformed_schemas() -> Result<()> {
let stage = Stage::builder().in_memory("anon.usda")?;
stage
.define_prim(sdf::path("/W")?)?
.set_type_name("Scope")?
.add_applied_schema("CollectionAPI:render")?
.add_applied_schema("CollectionAPI:")? .add_applied_schema("CollectionAPI:a:b")?; let names: Vec<String> = collections_on(&stage, &sdf::path("/W")?)?
.into_iter()
.map(|c| c.name().to_string())
.collect();
assert_eq!(names, vec!["render".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))?;
}
build_collection(
&stage,
"/Col",
"c",
ExpansionRule::ExplicitOnly,
false,
&["/W/B.c", "/W/B.a", "/W/B.b"],
&[],
)?;
let q = Collection::new(sdf::path("/Col")?, "c").compute_membership_query(&stage)?;
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(&stage, sdf::path("/W")?)?;
coll.exclude_path(&stage, sdf::path("/W/A")?)?;
for suffix in [INCLUDES, EXCLUDES] {
assert_eq!(
stage.field::<Value>(coll.prop(suffix)?, FieldKey::Custom)?,
Some(Value::Bool(false)),
"collection:c:{suffix} should be authored non-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(&stage)?);
coll.include_path(&stage, sdf::path("/W/A")?)?;
assert!(!coll.has_no_included_paths(&stage)?);
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(&stage, sdf::path("/W/A")?)?;
assert_eq!(coll.excludes(&stage)?, 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(&stage, true)?;
coll.exclude_path(&stage, Path::abs_root())?;
assert!(!coll.include_root(&stage)?);
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(&stage, sdf::path("/W/A")?)?; coll.include_path(&stage, sdf::path("/W/A/C")?)?; assert_eq!(coll.includes(&stage)?, vec![sdf::path("/W/A")?]); Ok(())
}
#[test]
fn included_paths_expand_properties() -> Result<()> {
let stage = scene()?;
stage
.create_attribute(sdf::path("/W/B.size")?, "float")?
.set(Value::Float(1.0))?;
build_collection(
&stage,
"/Col",
"c",
ExpansionRule::ExpandPrimsAndProperties,
false,
&["/W/B"],
&[],
)?;
let q = Collection::new(sdf::path("/Col")?, "c").compute_membership_query(&stage)?;
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(sdf::path("/W/B.size")?, "float")?
.set(Value::Float(1.0))?;
build_collection(
&stage,
"/Col",
"c",
ExpansionRule::ExpandPrims,
false,
&["/W/B.size", "/W/B.ghost"],
&[],
)?;
let q = Collection::new(sdf::path("/Col")?, "c").compute_membership_query(&stage)?;
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(())
}
}