use std::borrow::Cow;
use crate::sdf::{self, Path};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct PopulationMask {
paths: Vec<Path>,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum PopulationMaskError {
#[error(transparent)]
Parse(#[from] sdf::PathParseError),
#[error("a population mask path must be an absolute prim path, got `{0}`")]
NotAbsolutePrimPath(Path),
}
impl PopulationMask {
pub fn all() -> Self {
Self {
paths: vec![Path::abs_root()],
}
}
pub fn empty() -> Self {
Self { paths: Vec::new() }
}
pub fn new(paths: impl IntoIterator<Item: sdf::IntoPath>) -> Result<Self, PopulationMaskError> {
let mut paths: Vec<Path> = paths.into_iter().map(validate).collect::<Result<_, _>>()?;
paths.sort();
paths.dedup_by(|candidate, kept| candidate.has_prefix(kept));
Ok(Self { paths })
}
pub fn with_path(mut self, path: impl sdf::IntoPath) -> Result<Self, PopulationMaskError> {
self.add_path(path)?;
Ok(self)
}
pub fn add_path(&mut self, path: impl sdf::IntoPath) -> Result<&mut Self, PopulationMaskError> {
let path = validate(path)?;
if self.includes_subtree(&path) {
return Ok(self);
}
let at = self.paths.partition_point(|p| *p < path);
let end = at + self.paths[at..].partition_point(|p| p.has_prefix(&path));
self.paths.splice(at..end, [path]);
Ok(self)
}
pub fn paths(&self) -> &[Path] {
&self.paths
}
pub fn is_empty(&self) -> bool {
self.paths.is_empty()
}
pub fn is_all(&self) -> bool {
self.paths.first().is_some_and(Path::is_abs_root)
}
pub fn includes(&self, path: &Path) -> bool {
self.matches(path, false)
}
pub fn includes_subtree(&self, path: &Path) -> bool {
self.matches(path, true)
}
pub fn make_relative_to(&self, instance: &Path) -> Self {
if self.includes_subtree(instance) {
return Self::all();
}
let root = Path::abs_root();
Self {
paths: self
.paths
.iter()
.filter_map(|p| p.replace_prefix(instance, &root))
.collect(),
}
}
fn matches(&self, path: &Path, subtree_only: bool) -> bool {
if self.is_all() {
return true;
}
let path = if path.is_property_path() || path.contains_prim_variant_selection() {
Cow::Owned(path.prim_path().strip_all_variant_selections())
} else {
Cow::Borrowed(path)
};
let path = path.as_ref();
let at = self.paths.partition_point(|p| p < path);
if at > 0 && path.has_prefix(&self.paths[at - 1]) {
return true;
}
match self.paths.get(at) {
Some(entry) if entry == path => true,
Some(entry) => !subtree_only && entry.has_prefix(path),
None => false,
}
}
}
impl Default for PopulationMask {
fn default() -> Self {
Self::all()
}
}
fn validate(path: impl sdf::IntoPath) -> Result<Path, PopulationMaskError> {
let path = sdf::try_into_path(path)?;
if path.is_abs() && (path.is_abs_root() || (path.is_prim_path() && !path.contains_prim_variant_selection())) {
Ok(path)
} else {
Err(PopulationMaskError::NotAbsolutePrimPath(path))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn p(s: &str) -> Path {
Path::new(s).expect("valid test path")
}
fn mask(paths: &[&str]) -> PopulationMask {
PopulationMask::new(paths.iter().copied()).expect("valid mask paths")
}
#[test]
fn new_normalizes() {
let m = mask(&["/B/Child", "/A", "/B", "/A/Deep/Er"]);
assert_eq!(m.paths(), &[p("/A"), p("/B")]);
}
#[test]
fn root_subsumes_all() {
let mut m = mask(&["/A", "/B"]);
m.add_path("/").expect("root is a valid mask path");
assert_eq!(m.paths(), &[Path::abs_root()]);
assert!(m.is_all());
}
#[test]
fn add_path_reduces() {
let mut m = mask(&["/A/B"]);
m.add_path("/A/B/C").expect("valid");
assert_eq!(m.paths(), &[p("/A/B")], "a covered path is dropped");
m.add_path("/A").expect("valid");
assert_eq!(m.paths(), &[p("/A")], "a covering path replaces what it subsumes");
}
#[test]
fn includes_vs_subtree() {
let m = mask(&["/World/Hero"]);
for (path, includes, subtree) in [
("/", true, false),
("/World", true, false),
("/World/Hero", true, true),
("/World/Hero/Geom", true, true),
("/World/Other", false, false),
] {
assert_eq!(m.includes(&p(path)), includes, "includes {path}");
assert_eq!(m.includes_subtree(&p(path)), subtree, "includes_subtree {path}");
}
}
#[test]
fn includes_strips_variants() {
assert!(mask(&["/Prim/Child"]).includes(&p("/Prim{set=sel}Child")));
}
#[test]
fn rejects_non_prim_paths() {
for bad in ["A/B", "/A.attr", "/A{set=sel}", "/A{set=sel}B"] {
assert!(
PopulationMask::new([bad]).is_err(),
"{bad} must be rejected from a mask"
);
}
}
#[test]
fn make_relative_to_cases() {
let m = mask(&["/World/A/geom", "/World/B"]);
assert_eq!(m.make_relative_to(&p("/World/A")).paths(), &[p("/geom")]);
assert!(
m.make_relative_to(&p("/World/B")).is_all(),
"fully included keys as all"
);
assert!(
m.make_relative_to(&p("/Other")).is_empty(),
"an excluded instance keys as the empty mask"
);
}
#[test]
fn equal_masks_hash_equal() {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let hash = |m: &PopulationMask| {
let mut hasher = DefaultHasher::new();
m.hash(&mut hasher);
hasher.finish()
};
let a = mask(&["/A", "/B/C"]);
let b = mask(&["/B/C", "/A", "/A/Redundant"]);
assert_eq!(a, b);
assert_eq!(hash(&a), hash(&b));
}
}