use std::collections::{HashMap, HashSet};
use std::rc::Rc;
use crate::Result;
use crate::sdf::{self, FieldKey, Path, Value, Variability};
use crate::usd::{Prim, PrimPredicate, Relationship, Stage};
use super::collection_expr::{CollectionEvaluator, CollectionSearcher, resolve_complete_membership_expression};
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 MODE: &str = "mode";
const TOK_EXPLICIT_ONLY: &str = "explicitOnly";
const TOK_EXPAND_PRIMS: &str = "expandPrims";
const TOK_EXPAND_PRIMS_AND_PROPERTIES: &str = "expandPrimsAndProperties";
const TOK_AUTOMATIC: &str = "automatic";
const TOK_RELATIONSHIP: &str = "relationship";
const TOK_EXPRESSION: &str = "expression";
#[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, Copy, PartialEq, Eq, Default)]
pub enum CollectionMode {
#[default]
Automatic,
Relationship,
Expression,
}
impl CollectionMode {
pub fn as_token(self) -> &'static str {
match self {
CollectionMode::Automatic => TOK_AUTOMATIC,
CollectionMode::Relationship => TOK_RELATIONSHIP,
CollectionMode::Expression => TOK_EXPRESSION,
}
}
pub fn from_token(s: &str) -> Option<Self> {
Some(match s {
TOK_AUTOMATIC => CollectionMode::Automatic,
TOK_RELATIONSHIP => CollectionMode::Relationship,
TOK_EXPRESSION => CollectionMode::Expression,
_ => return None,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Collection {
prim: Path,
name: String,
}
impl Collection {
pub fn new(prim: impl sdf::IntoPath, name: impl Into<String>) -> Result<Self, sdf::PathParseError> {
Ok(Collection {
prim: sdf::try_into_path(prim)?,
name: name.into(),
})
}
pub(crate) fn from_parts(prim: Path, name: String) -> Self {
Collection { prim, name }
}
pub fn prim(&self) -> &Path {
&self.prim
}
pub fn name(&self) -> &str {
&self.name
}
pub fn collection_path(&self) -> Result<Path> {
Ok(self.prim.append_property(format!("{NS_COLLECTION}{}", self.name))?)
}
fn prop(&self, suffix: &str) -> Result<Path> {
Ok(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)) => ExpansionRule::from_token(t.as_str()).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(self.prop(INCLUDES)?)?.targets()
}
pub fn excludes(&self, stage: &Stage) -> Result<Vec<Path>> {
stage.relationship(self.prop(EXCLUDES)?)?.targets()
}
pub fn membership_expression(&self, stage: &Stage) -> Result<Option<sdf::PathExpression>> {
Ok(
match stage.field::<Value>(self.prop(MEMBERSHIP_EXPRESSION)?, FieldKey::Default)? {
Some(Value::PathExpression(expr)) => Some(expr),
Some(Value::String(s)) => Some(sdf::PathExpression::parse(&s)),
Some(Value::Token(s)) => Some(sdf::PathExpression::parse(s.as_str())),
_ => None,
},
)
}
pub fn mode(&self, stage: &Stage) -> Result<CollectionMode> {
Ok(match stage.field::<Value>(self.prop(MODE)?, FieldKey::Default)? {
Some(Value::Token(t)) => CollectionMode::from_token(t.as_str()).unwrap_or_default(),
_ => CollectionMode::default(),
})
}
pub fn compute_membership_query(&self, stage: &Stage) -> Result<MembershipQuery> {
let mode = self.mode(stage)?;
let mut query = MembershipQuery {
rule_map: PathExpansionRuleMap::new(),
top_expansion_rule: self.expansion_rule(stage)?,
evaluator: None,
};
if mode != CollectionMode::Expression {
let mut visited = HashSet::new();
visited.insert(self.collection_path()?);
self.build_into(stage, &mut query.rule_map, &mut visited)?;
}
if mode != CollectionMode::Relationship {
let expression = resolve_complete_membership_expression(stage, self)?;
if !expression.is_empty() {
if let Ok(evaluator) = CollectionEvaluator::build(stage, expression) {
query.evaluator = Some(Rc::new(evaluator));
}
}
}
Ok(query)
}
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()))?;
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 set_membership_expression(&self, stage: &Stage, expression: sdf::PathExpression) -> Result<()> {
stage
.create_attribute(self.prop(MEMBERSHIP_EXPRESSION)?, "pathExpression")?
.set_variability(Variability::Uniform)?
.set_custom(false)?
.set(Value::PathExpression(expression))?;
Ok(())
}
pub fn set_mode(&self, stage: &Stage, mode: CollectionMode) -> Result<()> {
stage
.create_attribute(self.prop(MODE)?, "token")?
.set_variability(Variability::Uniform)?
.set_custom(false)?
.set(Value::token(mode.as_token()))?;
Ok(())
}
pub fn include_path(&self, stage: &Stage, path: impl sdf::IntoPath) -> Result<()> {
let path = sdf::try_into_path(path)?;
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 sdf::IntoPath) -> Result<()> {
let path = sdf::try_into_path(path)?;
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::from_parts(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 sdf::IntoPath, name: impl Into<String>) -> Result<Collection> {
let prim = sdf::try_into_path(prim)?;
let name = name.into();
if !Path::is_valid_identifier(&name) {
return Err(super::StageAuthoringError::InvalidIdentifier {
name,
what: "collection name",
}
.into());
}
stage
.override_prim(prim.clone())?
.add_applied_schema(format!("{API_COLLECTION}:{name}"))?;
Ok(Collection::from_parts(prim, name))
}
pub fn collections_on(stage: &Stage, prim: impl sdf::IntoPath) -> Result<Vec<Collection>> {
let prim = sdf::try_into_path(prim)?;
let mut out = Vec::new();
for schema in Prim::new(stage, prim.clone()).api_schemas()? {
if let Some(name) = instance_name(&schema) {
out.push(Collection::from_parts(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);
}
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 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;
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("/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().into()))?;
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()))?;
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("/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("/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("/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(())
}
fn build_expression(stage: &Stage, name: &str, expression: &str) -> Result<Collection> {
author_collection(stage, "/Col", name)?;
let coll = Collection::new(sdf::path("/Col")?, name)?;
coll.set_membership_expression(stage, 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(&stage)?;
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()?;
build_collection(&stage, "/Col", "c", ExpansionRule::ExpandPrims, false, &["/W/B"], &[])?;
let coll = Collection::new(sdf::path("/Col")?, "c")?;
coll.set_membership_expression(&stage, sdf::PathExpression::parse("/W/A//"))?;
let q = coll.compute_membership_query(&stage)?;
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(&stage, CollectionMode::Expression)?;
let q = coll.compute_membership_query(&stage)?;
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(&stage, CollectionMode::Relationship)?;
let q = coll.compute_membership_query(&stage)?;
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(&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 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(&stage, ExpansionRule::ExpandPrimsAndProperties)?;
let q = coll.compute_membership_query(&stage)?;
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(&stage, ExpansionRule::ExpandPrimsAndProperties)?;
let q = bulk.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 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(&stage, &big)?;
assert_eq!(resolved.to_string(), "/W/A// /W/B//");
let q = big.compute_membership_query(&stage)?;
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(&stage, &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(&stage, &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(&stage, &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(&stage, &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(&stage)?;
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(&stage)?;
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(&stage)?;
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(&stage)?;
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(&stage)?;
let b = coll.compute_membership_query(&stage)?;
assert_ne!(a, b);
assert_ne!(a.clone(), a.clone());
Ok(())
}
}