use std::collections::HashMap;
use anyhow::Result;
use crate::gf;
use crate::sdf::schema::FieldKey;
use crate::sdf::{self, AssetPath, LayerOffset, Path, Value};
use super::index_cache::block_to_none;
use super::layer_graph::LayerGraph;
use super::LayerId;
pub(crate) mod keys {
pub const ASSET_PATHS: &str = "assetPaths";
pub const MANIFEST_ASSET_PATH: &str = "manifestAssetPath";
pub const PRIM_PATH: &str = "primPath";
pub const ACTIVE: &str = "active";
pub const TIMES: &str = "times";
pub const INTERPOLATE_MISSING: &str = "interpolateMissingClipValues";
pub const TEMPLATE_ASSET_PATH: &str = "templateAssetPath";
pub const TEMPLATE_START_TIME: &str = "templateStartTime";
pub const TEMPLATE_END_TIME: &str = "templateEndTime";
pub const TEMPLATE_STRIDE: &str = "templateStride";
pub const TEMPLATE_ACTIVE_OFFSET: &str = "templateActiveOffset";
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct ClipSet {
pub name: String,
pub prim_path: Option<Path>,
pub manifest_asset: Option<String>,
pub asset_paths: Vec<AssetPath>,
pub active: Vec<(f64, usize)>,
pub times: Vec<gf::Vec2d>,
pub interpolate_missing: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct ResolvedClipSet {
pub set: ClipSet,
pub asset_layer: LayerId,
pub manifest_layer: Option<LayerId>,
}
impl ClipSet {
pub(crate) fn parse(clips: &Value, clip_sets_order: Option<&[String]>) -> Vec<ClipSet> {
let Value::Dictionary(sets) = clips else {
return Vec::new();
};
let ordered_names: Vec<&String> = match clip_sets_order {
Some(order) => order.iter().filter(|name| sets.contains_key(*name)).collect(),
None => {
let mut names: Vec<&String> = sets.keys().collect();
names.sort();
names
}
};
ordered_names
.into_iter()
.filter_map(|name| match sets.get(name) {
Some(Value::Dictionary(set)) => Self::parse_set(name, set),
_ => None,
})
.collect()
}
fn parse_set(name: &str, set: &HashMap<String, Value>) -> Option<ClipSet> {
let prim_path = set
.get(keys::PRIM_PATH)
.and_then(Value::as_str)
.and_then(|s| Path::new(s).ok());
let manifest_asset = set
.get(keys::MANIFEST_ASSET_PATH)
.and_then(Value::as_str)
.map(str::to_owned);
let (asset_paths, active, times) = match get::<Vec<AssetPath>>(set, keys::ASSET_PATHS) {
Some(asset_paths) => {
let mut active: Vec<(f64, usize)> = get::<Vec<gf::Vec2d>>(set, keys::ACTIVE)
.unwrap_or_default()
.into_iter()
.map(|p| (p.x, p.y as usize))
.collect();
active.sort_by(|a, b| a.0.total_cmp(&b.0));
let mut times = get::<Vec<gf::Vec2d>>(set, keys::TIMES).unwrap_or_default();
times.sort_by(|a, b| a.x.total_cmp(&b.x));
(asset_paths, active, times)
}
None => expand_template(set)?,
};
let interpolate_missing = get::<bool>(set, keys::INTERPOLATE_MISSING).unwrap_or(false);
Some(ClipSet {
name: name.to_string(),
prim_path,
manifest_asset,
asset_paths,
active,
times,
interpolate_missing,
})
}
pub(crate) fn active_clip(&self, stage_time: f64) -> Option<usize> {
let mut chosen = self.active.first()?.1;
for &(stage, index) in &self.active {
if stage <= stage_time {
chosen = index;
} else {
break;
}
}
Some(chosen)
}
pub(crate) fn map_stage_to_clip(&self, stage_time: f64) -> f64 {
map_stage_to_clip(&self.times, stage_time)
}
pub(crate) fn retime_stage_times(&mut self, offset: LayerOffset) {
if offset.is_identity() {
return;
}
for (stage, _) in &mut self.active {
*stage = offset.apply(*stage);
}
for knot in &mut self.times {
knot.x = offset.apply(knot.x);
}
if offset.scale < 0.0 {
self.active.sort_by(|a, b| a.0.total_cmp(&b.0));
self.times.sort_by(|a, b| a.x.total_cmp(&b.x));
}
}
pub(crate) fn stage_sample_times(&self, per_clip: &[Vec<f64>]) -> Vec<f64> {
let mut out: Vec<f64> = Vec::new();
for (k, &(start, clip_index)) in self.active.iter().enumerate() {
let samples = per_clip.get(clip_index).map_or(&[][..], Vec::as_slice);
let end = self.active.get(k + 1).map(|next| next.0);
let lower = (k > 0).then_some(start);
let in_interval = |t: f64| lower.is_none_or(|l| t >= l) && end.is_none_or(|e| t < e);
if let Some(boundary) = lower {
out.push(boundary);
}
out.extend(self.times.iter().map(|knot| knot.x).filter(|&x| in_interval(x)));
for &clip_time in samples {
out.extend(
self.stage_times_for_clip_time(clip_time)
.into_iter()
.filter(|&t| in_interval(t)),
);
}
}
out.retain(|t| t.is_finite());
out.sort_by(f64::total_cmp);
out.dedup();
out
}
pub(crate) fn may_be_time_varying(&self) -> bool {
self.active.len() > 1
}
fn stage_times_for_clip_time(&self, clip_time: f64) -> Vec<f64> {
if self.times.is_empty() {
return vec![clip_time];
}
self.times
.windows(2)
.filter_map(|seg| {
let (sa, ca) = (seg[0].x, seg[0].y);
let (sb, cb) = (seg[1].x, seg[1].y);
let (lo, hi) = if ca <= cb { (ca, cb) } else { (cb, ca) };
if ca == cb || clip_time < lo || clip_time > hi {
return None;
}
Some(sa + (clip_time - ca) / (cb - ca) * (sb - sa))
})
.collect()
}
}
fn map_stage_to_clip(times: &[gf::Vec2d], stage_time: f64) -> f64 {
let (Some(first), Some(last)) = (times.first(), times.last()) else {
return stage_time;
};
if stage_time < first.x {
return first.y;
}
if stage_time >= last.x {
return last.y;
}
let lo = times.iter().rposition(|knot| knot.x <= stage_time).unwrap_or(0);
let (lo_knot, hi_knot) = (times[lo], times[lo + 1]);
let (stage0, clip0) = (lo_knot.x, lo_knot.y);
let (stage1, clip1) = (hi_knot.x, hi_knot.y);
if stage0 == stage1 {
return clip1;
}
if stage_time == stage0 {
return clip0;
}
let ratio = (stage_time - stage0) / (stage1 - stage0);
gf::lerp(clip0, clip1, ratio)
}
type TemplateExpansion = (Vec<AssetPath>, Vec<(f64, usize)>, Vec<gf::Vec2d>);
fn expand_template(set: &HashMap<String, Value>) -> Option<TemplateExpansion> {
let template = set
.get(keys::TEMPLATE_ASSET_PATH)
.and_then(Value::as_str)
.map(str::to_owned)?;
let start = get::<f64>(set, keys::TEMPLATE_START_TIME)?;
let end = get::<f64>(set, keys::TEMPLATE_END_TIME)?;
let stride = get::<f64>(set, keys::TEMPLATE_STRIDE)?;
let active_offset = get::<f64>(set, keys::TEMPLATE_ACTIVE_OFFSET);
if stride.is_nan() || stride <= 0.0 || end < start {
return None;
}
if active_offset.is_some_and(|off| off.abs() > stride) {
return None;
}
let pattern = HashPattern::parse(&template)?;
const PROMOTION: f64 = 10000.0;
let end_p = end * PROMOTION;
let stride_p = stride * PROMOTION;
let mut asset_paths = Vec::new();
let mut active = Vec::new();
let mut times = Vec::new();
if let Some(off) = active_offset {
let front = start - off.abs();
times.push(gf::vec2d(front, front));
}
let mut t = start * PROMOTION;
let mut index = 0usize;
while t <= end_p + 0.5 {
let clip_time = t / PROMOTION;
asset_paths.push(pattern.format(clip_time).into());
times.push(gf::vec2d(clip_time, clip_time));
let stage_time = match active_offset {
Some(off) => (t + off * PROMOTION) / PROMOTION,
None => clip_time,
};
active.push((stage_time, index));
index += 1;
t += stride_p;
}
if let Some(off) = active_offset {
let back = end + off.abs();
times.push(gf::vec2d(back, back));
}
if asset_paths.is_empty() {
return None;
}
active.sort_by(|a, b| a.0.total_cmp(&b.0));
times.sort_by(|a, b| a.x.total_cmp(&b.x));
Some((asset_paths, active, times))
}
struct HashPattern {
prefix: String,
int_width: usize,
frac_width: Option<usize>,
suffix: String,
}
impl HashPattern {
fn parse(template: &str) -> Option<HashPattern> {
let first = template.find('#')?;
let prefix = template[..first].to_string();
let rest = &template[first..];
let int_width = rest.chars().take_while(|&c| c == '#').count();
let after_int = &rest[int_width..];
let (frac_width, suffix) = if let Some(dot_rest) = after_int.strip_prefix('.') {
if dot_rest.starts_with('#') {
let frac_width = dot_rest.chars().take_while(|&c| c == '#').count();
(Some(frac_width), dot_rest[frac_width..].to_string())
} else {
(None, after_int.to_string())
}
} else {
(None, after_int.to_string())
};
if suffix.contains('#') {
return None;
}
Some(HashPattern {
prefix,
int_width,
frac_width,
suffix,
})
}
fn format(&self, time: f64) -> String {
let body = match self.frac_width {
Some(frac_width) => {
let rendered = format!("{:.*}", frac_width, time);
let (int_part, frac_part) = rendered.split_once('.').unwrap_or((rendered.as_str(), ""));
let neg = int_part.starts_with('-');
let digits = int_part.trim_start_matches('-');
let padded = format!("{:0>width$}", digits, width = self.int_width);
let sign = if neg { "-" } else { "" };
format!("{sign}{padded}.{frac_part}")
}
None => format!("{:0width$}", time as i64, width = self.int_width),
};
format!("{}{}{}", self.prefix, body, self.suffix)
}
}
fn get<T: TryFrom<Value>>(set: &HashMap<String, Value>, key: &str) -> Option<T> {
set.get(key).cloned().and_then(|v| T::try_from(v).ok())
}
#[derive(Default)]
pub(crate) struct ClipCache {
clip_layers: HashMap<String, sdf::Layer>,
}
pub(super) struct ClipQuery<'a> {
pub anchor: &'a Path,
pub attr_prim: &'a Path,
pub suffix: &'a str,
}
impl ClipCache {
pub(super) fn value_in_sets(
&mut self,
graph: &LayerGraph,
sets: &[ResolvedClipSet],
query: &ClipQuery,
time: f64,
interp: &dyn Fn(&sdf::TimeSampleMap, f64) -> Option<Value>,
) -> Result<Option<Value>> {
for resolved in sets {
let set = &resolved.set;
let base = set.prim_path.clone().unwrap_or_else(|| query.anchor.clone());
let clip_path = clip_attr_path(query, &base)?;
let manifest = set
.manifest_asset
.as_deref()
.map(|asset| (asset, resolved.manifest_layer.unwrap_or(resolved.asset_layer)));
let manifest_declared = self.clip_set_declares(graph, resolved, &clip_path)?;
if manifest.is_some() && !manifest_declared {
continue;
}
let Some(active) = set.active_clip(time) else {
continue;
};
let Some(asset) = set.asset_paths.get(active) else {
continue;
};
let clip_time = set.map_stage_to_clip(time);
if let Some(value) = self.clip_sample_at(
graph,
asset.as_str(),
resolved.asset_layer,
&clip_path,
clip_time,
interp,
)? {
return Ok(Some(value));
}
if !manifest_declared {
continue;
}
if let Some((asset, layer)) = manifest {
if let Some(value) = self.manifest_default(graph, asset, layer, &clip_path)? {
return Ok(Some(value));
}
}
if set.interpolate_missing {
if let Some(value) = self.interpolate_missing_value(graph, resolved, &clip_path, time, interp)? {
return Ok(Some(value));
}
}
return Ok(Some(Value::ValueBlock));
}
Ok(None)
}
pub(super) fn clip_introspection_in_sets(
&mut self,
graph: &LayerGraph,
sets: &[ResolvedClipSet],
query: &ClipQuery,
) -> Result<Option<(Vec<f64>, bool)>> {
for resolved in sets {
let base = resolved.set.prim_path.clone().unwrap_or_else(|| query.anchor.clone());
let clip_path = clip_attr_path(query, &base)?;
if let Some(per_clip) = self.clip_set_participates(graph, resolved, &clip_path)? {
let set = &resolved.set;
return Ok(Some((set.stage_sample_times(&per_clip), set.may_be_time_varying())));
}
}
Ok(None)
}
fn clip_layer(
&mut self,
graph: &LayerGraph,
asset_path: &str,
anchor_layer: LayerId,
) -> Result<Option<&sdf::Layer>> {
let anchor = graph.anchor_location(Some(anchor_layer));
let clip_id = graph.layer_registry().create_identifier(asset_path, anchor.as_ref());
if !self.clip_layers.contains_key(&clip_id) {
let Some(data) = graph.layer_registry().open(&clip_id)? else {
return Ok(None);
};
self.clip_layers
.insert(clip_id.clone(), sdf::Layer::new(clip_id.clone(), data));
}
Ok(self.clip_layers.get(&clip_id))
}
fn clip_set_declares(&mut self, graph: &LayerGraph, resolved: &ResolvedClipSet, clip_path: &Path) -> Result<bool> {
match resolved.set.manifest_asset.as_deref() {
Some(asset) => {
let layer = resolved.manifest_layer.unwrap_or(resolved.asset_layer);
Ok(matches!(self.clip_layer(graph, asset, layer)?,
Some(opened) if opened.data().has_spec(clip_path)))
}
None => Ok(false),
}
}
fn clip_set_participates(
&mut self,
graph: &LayerGraph,
resolved: &ResolvedClipSet,
clip_path: &Path,
) -> Result<Option<Vec<Vec<f64>>>> {
let set = &resolved.set;
let declared = self.clip_set_declares(graph, resolved, clip_path)?;
if set.manifest_asset.is_some() && !declared {
return Ok(None);
}
if set.active.is_empty() {
return Ok(None);
}
let mut per_clip: Vec<Vec<f64>> = Vec::with_capacity(set.asset_paths.len());
for asset in &set.asset_paths {
per_clip.push(self.clip_in_clip_times(graph, asset.as_str(), resolved.asset_layer, clip_path)?);
}
let scheduled_sample = set
.active
.iter()
.any(|&(_, index)| per_clip.get(index).is_some_and(|s| !s.is_empty()));
if !declared && !scheduled_sample {
return Ok(None);
}
Ok(Some(per_clip))
}
fn clip_in_clip_times(
&mut self,
graph: &LayerGraph,
asset: &str,
anchor_layer: LayerId,
clip_path: &Path,
) -> Result<Vec<f64>> {
Ok(self
.clip_time_samples(graph, asset, anchor_layer, clip_path)?
.map(|samples| samples.iter().map(|(t, _)| *t).collect())
.unwrap_or_default())
}
fn clip_time_samples(
&mut self,
graph: &LayerGraph,
asset: &str,
anchor_layer: LayerId,
clip_path: &Path,
) -> Result<Option<sdf::TimeSampleMap>> {
Ok(match self.clip_layer(graph, asset, anchor_layer)? {
Some(layer) => match layer.data().try_field(clip_path, FieldKey::TimeSamples.as_str())? {
Some(value) => match value.into_owned() {
Value::TimeSamples(samples) => Some(samples),
_ => None,
},
None => None,
},
None => None,
})
}
fn clip_sample_at(
&mut self,
graph: &LayerGraph,
asset: &str,
anchor_layer: LayerId,
clip_path: &Path,
clip_time: f64,
interp: &dyn Fn(&sdf::TimeSampleMap, f64) -> Option<Value>,
) -> Result<Option<Value>> {
Ok(self
.clip_time_samples(graph, asset, anchor_layer, clip_path)?
.and_then(|samples| interp(&samples, clip_time)))
}
fn manifest_default(
&mut self,
graph: &LayerGraph,
manifest: &str,
manifest_layer: LayerId,
clip_path: &Path,
) -> Result<Option<Value>> {
let value = match self.clip_layer(graph, manifest, manifest_layer)? {
Some(layer) => layer
.data()
.try_field(clip_path, FieldKey::Default.as_str())?
.map(|value| value.into_owned()),
None => None,
};
Ok(value.and_then(block_to_none))
}
fn interpolate_missing_value(
&mut self,
graph: &LayerGraph,
resolved: &ResolvedClipSet,
clip_path: &Path,
time: f64,
interp: &dyn Fn(&sdf::TimeSampleMap, f64) -> Option<Value>,
) -> Result<Option<Value>> {
let set = &resolved.set;
let anchor = resolved.asset_layer;
let active_pos = set.active.iter().rposition(|&(stage, _)| stage <= time).unwrap_or(0);
let mut upper = None;
for &(stage, idx) in set.active.iter().skip(active_pos + 1) {
if let Some(asset) = set.asset_paths.get(idx) {
let clip_time = set.map_stage_to_clip(stage);
if let Some(value) = self.clip_sample_at(graph, asset.as_str(), anchor, clip_path, clip_time, interp)? {
upper = Some((stage, value));
break;
}
}
}
let mut lower = None;
for &(stage, idx) in set.active[..active_pos].iter().rev() {
if let Some(asset) = set.asset_paths.get(idx) {
let clip_time = set.map_stage_to_clip(stage);
if let Some(value) = self.clip_sample_at(graph, asset.as_str(), anchor, clip_path, clip_time, interp)? {
lower = Some((stage, value));
break;
}
}
}
Ok(match (lower, upper) {
(Some((lt, lv)), Some((ut, uv))) => interp(&vec![(lt, lv), (ut, uv)], time),
(Some((_, value)), None) | (None, Some((_, value))) => Some(value),
(None, None) => None,
})
}
}
fn clip_attr_path(query: &ClipQuery, base: &Path) -> Result<Path> {
let attr = Path::new(&format!("{}{}", query.attr_prim, query.suffix))?;
Ok(attr.replace_prefix(query.anchor, base).unwrap_or(attr))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sdf;
#[test]
fn loads_and_caches_clip_layer() -> anyhow::Result<()> {
let root = format!(
"{}/vendor/core-spec-supplemental-release_dec2025/value_resolution/tests/assets/clip_basic/usda/root.usda",
std::env::var("CARGO_MANIFEST_DIR").unwrap()
);
let registry = sdf::LayerRegistry::default();
let id = registry.create_identifier(&root, None);
let data = registry.open(&root).expect("open root").expect("root resolves");
let graph = LayerGraph::from_layers(vec![sdf::Layer::new(id, data)], 0, registry);
let root_id = graph.root_id().expect("root layer");
let mut clips = ClipCache::default();
{
let clip = clips
.clip_layer(&graph, "./clip.usda", root_id)?
.expect("clip resolves");
assert!(clip.identifier.contains("clip.usda"));
assert!(clip.data().has_spec(&sdf::path("/Model.size")?));
}
assert!(clips.clip_layer(&graph, "./clip.usda", root_id)?.is_some());
assert!(clips.clip_layer(&graph, "./does_not_exist.usda", root_id)?.is_none());
Ok(())
}
fn knots(pairs: &[(f64, f64)]) -> Vec<gf::Vec2d> {
pairs.iter().map(|&(x, y)| gf::vec2d(x, y)).collect()
}
#[test]
fn hash_substitution_integer() {
assert_eq!(HashPattern::parse("foo.##.usd").unwrap().format(12.0), "foo.12.usd");
assert_eq!(HashPattern::parse("foo.###.usd").unwrap().format(12.0), "foo.012.usd");
assert_eq!(HashPattern::parse("foo.#.usd").unwrap().format(333.0), "foo.333.usd");
assert_eq!(HashPattern::parse("foo.#.usd").unwrap().format(1.6), "foo.1.usd");
}
#[test]
fn hash_substitution_subinteger() {
assert_eq!(
HashPattern::parse("foo.#.###.usd").unwrap().format(1.15),
"foo.1.150.usd"
);
assert_eq!(HashPattern::parse("foo.#.##.usd").unwrap().format(1.1), "foo.1.10.usd");
}
#[test]
fn hash_pattern_rejects_three_groups() {
assert!(HashPattern::parse("foo.#.#.#.usd").is_none());
assert!(HashPattern::parse("foo.usd").is_none());
}
#[test]
fn template_expands_to_explicit_clip_set() {
use std::collections::HashMap;
let mut set = HashMap::new();
set.insert(
keys::TEMPLATE_ASSET_PATH.to_string(),
Value::AssetPath("clip.##.usd".into()),
);
set.insert(keys::TEMPLATE_START_TIME.to_string(), Value::Double(101.0));
set.insert(keys::TEMPLATE_END_TIME.to_string(), Value::Double(103.0));
set.insert(keys::TEMPLATE_STRIDE.to_string(), Value::Double(1.0));
let parsed = ClipSet::parse_set("default", &set).expect("template set");
assert_eq!(
parsed.asset_paths,
vec![
"clip.101.usd".to_string(),
"clip.102.usd".to_string(),
"clip.103.usd".to_string()
],
);
assert_eq!(parsed.active, vec![(101.0, 0), (102.0, 1), (103.0, 2)]);
assert_eq!(parsed.times, knots(&[(101.0, 101.0), (102.0, 102.0), (103.0, 103.0)]));
}
#[test]
fn template_active_offset_shifts_active_times() {
use std::collections::HashMap;
let mut set = HashMap::new();
set.insert(
keys::TEMPLATE_ASSET_PATH.to_string(),
Value::AssetPath("c.#.usd".into()),
);
set.insert(keys::TEMPLATE_START_TIME.to_string(), Value::Double(0.0));
set.insert(keys::TEMPLATE_END_TIME.to_string(), Value::Double(2.0));
set.insert(keys::TEMPLATE_STRIDE.to_string(), Value::Double(1.0));
set.insert(keys::TEMPLATE_ACTIVE_OFFSET.to_string(), Value::Double(-0.5));
let parsed = ClipSet::parse_set("default", &set).expect("template set");
assert_eq!(parsed.active, vec![(-0.5, 0), (0.5, 1), (1.5, 2)]);
assert_eq!(
parsed.times,
knots(&[(-0.5, -0.5), (0.0, 0.0), (1.0, 1.0), (2.0, 2.0), (2.5, 2.5)])
);
}
#[test]
fn template_rejects_invalid_metadata() {
use std::collections::HashMap;
let base = |off: f64, stride: f64| {
let mut set = HashMap::new();
set.insert(
keys::TEMPLATE_ASSET_PATH.to_string(),
Value::AssetPath("c.#.usd".into()),
);
set.insert(keys::TEMPLATE_START_TIME.to_string(), Value::Double(0.0));
set.insert(keys::TEMPLATE_END_TIME.to_string(), Value::Double(2.0));
set.insert(keys::TEMPLATE_STRIDE.to_string(), Value::Double(stride));
set.insert(keys::TEMPLATE_ACTIVE_OFFSET.to_string(), Value::Double(off));
set
};
assert!(ClipSet::parse_set("default", &base(2.0, 1.0)).is_none());
assert!(ClipSet::parse_set("default", &base(0.0, 0.0)).is_none());
}
#[test]
fn explicit_asset_paths_win_over_template() {
use std::collections::HashMap;
let mut set = HashMap::new();
set.insert(
keys::ASSET_PATHS.to_string(),
Value::AssetPathVec(vec!["explicit.usd".into()]),
);
set.insert(
keys::TEMPLATE_ASSET_PATH.to_string(),
Value::AssetPath("c.#.usd".into()),
);
set.insert(keys::TEMPLATE_START_TIME.to_string(), Value::Double(0.0));
set.insert(keys::TEMPLATE_END_TIME.to_string(), Value::Double(2.0));
set.insert(keys::TEMPLATE_STRIDE.to_string(), Value::Double(1.0));
let parsed = ClipSet::parse_set("default", &set).expect("explicit set");
assert_eq!(parsed.asset_paths, vec!["explicit.usd".to_string()]);
}
fn clip_set(active: Vec<(f64, usize)>, times: Vec<gf::Vec2d>) -> ClipSet {
ClipSet {
name: "default".into(),
prim_path: None,
manifest_asset: None,
asset_paths: Vec::new(),
active,
times,
interpolate_missing: false,
}
}
#[test]
fn active_clip_ranges() {
let cs = clip_set(vec![(0.0, 0), (1.0, 1), (2.0, 2)], vec![]);
assert_eq!(cs.active_clip(-5.0), Some(0)); assert_eq!(cs.active_clip(0.0), Some(0));
assert_eq!(cs.active_clip(1.5), Some(1));
assert_eq!(cs.active_clip(2.0), Some(2));
assert_eq!(cs.active_clip(100.0), Some(2)); }
#[test]
fn active_clip_empty() {
assert_eq!(clip_set(vec![], vec![]).active_clip(0.0), None);
}
#[test]
fn map_times_linear() {
let cs = clip_set(vec![], knots(&[(0.0, 1.0), (1.0, 2.0), (2.0, 3.0)]));
assert_eq!(cs.map_stage_to_clip(0.0), 1.0);
assert_eq!(cs.map_stage_to_clip(1.0), 2.0);
assert_eq!(cs.map_stage_to_clip(1.5), 2.5); assert_eq!(cs.map_stage_to_clip(-3.0), 1.0); assert_eq!(cs.map_stage_to_clip(9.0), 3.0); }
#[test]
fn map_times_identity() {
assert_eq!(clip_set(vec![], vec![]).map_stage_to_clip(7.5), 7.5);
}
#[test]
fn stage_times_identity() {
let cs = clip_set(vec![(0.0, 0), (10.0, 1)], vec![]);
let times = cs.stage_sample_times(&[vec![0.0, 5.0, 12.0], vec![10.0, 15.0]]);
assert_eq!(times, vec![0.0, 5.0, 10.0, 15.0]);
}
#[test]
fn stage_times_empty_window_boundary() {
let cs = clip_set(vec![(0.0, 0), (10.0, 1)], vec![]);
let times = cs.stage_sample_times(&[vec![0.0, 5.0], vec![]]);
assert_eq!(times, vec![0.0, 5.0, 10.0]);
}
#[test]
fn stage_times_linear_timing() {
let cs = clip_set(vec![(0.0, 0)], knots(&[(0.0, 0.0), (10.0, 5.0)]));
let times = cs.stage_sample_times(&[vec![0.0, 2.5, 5.0]]);
assert_eq!(times, vec![0.0, 5.0, 10.0]);
}
#[test]
fn stage_times_before_first_active() {
let cs = clip_set(vec![(10.0, 0)], vec![]);
let times = cs.stage_sample_times(&[vec![5.0, 15.0]]);
assert_eq!(times, vec![5.0, 15.0]);
}
#[test]
fn stage_times_no_active() {
assert!(clip_set(vec![], vec![])
.stage_sample_times(&[vec![0.0, 1.0]])
.is_empty());
}
#[test]
fn map_times_jump_discontinuity() {
let cs = clip_set(vec![], knots(&[(0.0, 0.0), (10.0, 10.0), (10.0, 25.0), (20.0, 35.0)]));
assert_eq!(cs.map_stage_to_clip(5.0), 5.0);
assert!((cs.map_stage_to_clip(9.999) - 9.999).abs() < 1e-6); assert_eq!(cs.map_stage_to_clip(10.0), 25.0); assert_eq!(cs.map_stage_to_clip(15.0), 30.0); assert_eq!(cs.map_stage_to_clip(20.0), 35.0);
}
#[test]
fn map_times_initial_jump() {
let cs = clip_set(vec![], knots(&[(0.0, 0.0), (0.0, 25.0), (10.0, 35.0)]));
assert_eq!(cs.map_stage_to_clip(-1.0), 0.0);
assert_eq!(cs.map_stage_to_clip(0.0), 25.0);
assert_eq!(cs.map_stage_to_clip(5.0), 30.0);
}
#[test]
fn map_times_looping() {
let cs = clip_set(vec![], knots(&[(0.0, 0.0), (25.0, 25.0), (25.0, 0.0), (50.0, 25.0)]));
assert_eq!(cs.map_stage_to_clip(20.0), 20.0);
assert_eq!(cs.map_stage_to_clip(45.0), 20.0); }
#[test]
fn parse_explicit_from_usda() {
use crate::sdf::AbstractData;
let parsed = crate::usda::parser::Parser::new(
r#"#usda 1.0
def Xform "Geo" (
clips = {
dictionary default = {
double2[] active = [(0, 0), (1, 1), (2, 2)]
asset[] assetPaths = [@./quad_1.usda@, @./quad_2.usda@, @./quad_3.usda@]
asset manifestAssetPath = @./manifest.usda@
string primPath = "/Geo"
double2[] times = [(0, 1), (1, 2), (2, 3)]
}
}
)
{
}
"#,
)
.parse()
.expect("parse usda");
let data = sdf::Data::from_specs(parsed);
let clips = data
.try_field(&Path::new("/Geo").unwrap(), "clips")
.expect("try_field")
.expect("clips authored")
.into_owned();
let sets = ClipSet::parse(&clips, None);
assert_eq!(sets.len(), 1);
let cs = &sets[0];
assert_eq!(cs.name, "default");
assert_eq!(cs.prim_path, Some(Path::new("/Geo").unwrap()));
assert_eq!(cs.manifest_asset.as_deref(), Some("./manifest.usda"));
assert_eq!(cs.asset_paths, vec!["./quad_1.usda", "./quad_2.usda", "./quad_3.usda"]);
assert_eq!(cs.active, vec![(0.0, 0), (1.0, 1), (2.0, 2)]);
assert_eq!(cs.times, knots(&[(0.0, 1.0), (1.0, 2.0), (2.0, 3.0)]));
}
}