use std::collections::HashMap;
use crate::gf;
use crate::pcp::clip::keys;
use crate::sdf::{self, AssetPath, Value};
use super::{Prim, StageAuthoringError};
pub struct ClipsAPI {
prim: Prim,
}
impl ClipsAPI {
pub fn new(prim: &Prim) -> Self {
Self { prim: prim.clone() }
}
pub fn clip_set_names(&self) -> anyhow::Result<Vec<String>> {
self.prim.clip_sets()
}
pub fn clip_sets(&self) -> anyhow::Result<Option<sdf::StringListOp>> {
let path = self.prim.path().clone();
self.prim
.stage()
.masked(&path, |g, cache| cache.clip_sets_list_op(g, &path))
}
pub fn set_clip_sets(&self, clip_sets: sdf::StringListOp) -> Result<(), StageAuthoringError> {
self.prim
.clone()
.set_metadata(sdf::FieldKey::ClipSets.as_str(), Value::StringListOp(clip_sets))?;
Ok(())
}
pub fn clips(&self) -> anyhow::Result<Option<sdf::Dictionary>> {
Ok(self
.prim
.stage()
.field::<Value>(self.prim.path(), sdf::FieldKey::Clips)?
.and_then(Value::try_as_dictionary))
}
pub fn set_clips(&self, clips: sdf::Dictionary) -> Result<(), StageAuthoringError> {
self.prim
.clone()
.set_metadata(sdf::FieldKey::Clips.as_str(), Value::Dictionary(clips))?;
Ok(())
}
pub fn clip_asset_paths(&self, clip_set: &str) -> anyhow::Result<Vec<AssetPath>> {
Ok(self
.field(clip_set, keys::ASSET_PATHS)?
.and_then(Value::try_as_asset_path_vec)
.unwrap_or_default())
}
pub fn clip_active(&self, clip_set: &str) -> anyhow::Result<Vec<gf::Vec2d>> {
Ok(self
.field(clip_set, keys::ACTIVE)?
.and_then(Value::try_as_vec_2d_vec)
.unwrap_or_default())
}
pub fn clip_times(&self, clip_set: &str) -> anyhow::Result<Vec<gf::Vec2d>> {
Ok(self
.field(clip_set, keys::TIMES)?
.and_then(Value::try_as_vec_2d_vec)
.unwrap_or_default())
}
pub fn clip_prim_path(&self, clip_set: &str) -> anyhow::Result<Option<String>> {
self.cast_field(clip_set, keys::PRIM_PATH)
}
pub fn clip_manifest_asset_path(&self, clip_set: &str) -> anyhow::Result<Option<AssetPath>> {
Ok(self
.field(clip_set, keys::MANIFEST_ASSET_PATH)?
.and_then(Value::try_as_asset_path))
}
pub fn interpolate_missing_clip_values(&self, clip_set: &str) -> anyhow::Result<Option<bool>> {
self.cast_field(clip_set, keys::INTERPOLATE_MISSING)
}
pub fn clip_template_asset_path(&self, clip_set: &str) -> anyhow::Result<Option<String>> {
self.cast_field(clip_set, keys::TEMPLATE_ASSET_PATH)
}
pub fn clip_template_stride(&self, clip_set: &str) -> anyhow::Result<Option<f64>> {
self.cast_field(clip_set, keys::TEMPLATE_STRIDE)
}
pub fn clip_template_start_time(&self, clip_set: &str) -> anyhow::Result<Option<f64>> {
self.cast_field(clip_set, keys::TEMPLATE_START_TIME)
}
pub fn clip_template_end_time(&self, clip_set: &str) -> anyhow::Result<Option<f64>> {
self.cast_field(clip_set, keys::TEMPLATE_END_TIME)
}
pub fn clip_template_active_offset(&self, clip_set: &str) -> anyhow::Result<Option<f64>> {
self.cast_field(clip_set, keys::TEMPLATE_ACTIVE_OFFSET)
}
pub fn set_clip_asset_paths(
&self,
clip_set: &str,
paths: impl IntoIterator<Item = impl Into<AssetPath>>,
) -> Result<(), StageAuthoringError> {
let paths = paths.into_iter().map(Into::into).collect();
self.set_field(clip_set, keys::ASSET_PATHS, Value::AssetPathVec(paths))
}
pub fn set_clip_active(&self, clip_set: &str, active: Vec<gf::Vec2d>) -> Result<(), StageAuthoringError> {
self.set_field(clip_set, keys::ACTIVE, Value::Vec2dVec(active))
}
pub fn set_clip_times(&self, clip_set: &str, times: Vec<gf::Vec2d>) -> Result<(), StageAuthoringError> {
self.set_field(clip_set, keys::TIMES, Value::Vec2dVec(times))
}
pub fn set_clip_prim_path(&self, clip_set: &str, prim_path: impl Into<String>) -> Result<(), StageAuthoringError> {
self.set_field(clip_set, keys::PRIM_PATH, Value::String(prim_path.into()))
}
pub fn set_clip_manifest_asset_path(
&self,
clip_set: &str,
asset_path: impl Into<AssetPath>,
) -> Result<(), StageAuthoringError> {
self.set_field(clip_set, keys::MANIFEST_ASSET_PATH, Value::AssetPath(asset_path.into()))
}
pub fn set_interpolate_missing_clip_values(
&self,
clip_set: &str,
interpolate: bool,
) -> Result<(), StageAuthoringError> {
self.set_field(clip_set, keys::INTERPOLATE_MISSING, Value::Bool(interpolate))
}
pub fn set_clip_template_asset_path(
&self,
clip_set: &str,
asset_path: impl Into<String>,
) -> Result<(), StageAuthoringError> {
self.set_field(
clip_set,
keys::TEMPLATE_ASSET_PATH,
Value::AssetPath(AssetPath::new(asset_path)),
)
}
pub fn set_clip_template_stride(&self, clip_set: &str, stride: f64) -> Result<(), StageAuthoringError> {
self.set_field(clip_set, keys::TEMPLATE_STRIDE, Value::Double(stride))
}
pub fn set_clip_template_start_time(&self, clip_set: &str, start: f64) -> Result<(), StageAuthoringError> {
self.set_field(clip_set, keys::TEMPLATE_START_TIME, Value::Double(start))
}
pub fn set_clip_template_end_time(&self, clip_set: &str, end: f64) -> Result<(), StageAuthoringError> {
self.set_field(clip_set, keys::TEMPLATE_END_TIME, Value::Double(end))
}
pub fn set_clip_template_active_offset(&self, clip_set: &str, offset: f64) -> Result<(), StageAuthoringError> {
self.set_field(clip_set, keys::TEMPLATE_ACTIVE_OFFSET, Value::Double(offset))
}
fn cast_field<T: sdf::FromValueCast>(&self, clip_set: &str, key: &str) -> anyhow::Result<Option<T>> {
Ok(self.field(clip_set, key)?.and_then(|v| v.cast().ok()))
}
fn field(&self, clip_set: &str, key: &str) -> anyhow::Result<Option<Value>> {
let Some(mut sets) = self.clips()? else {
return Ok(None);
};
Ok(match sets.get_mut(clip_set) {
Some(Value::Dictionary(set)) => set.remove(key),
_ => None,
})
}
fn set_field(&self, clip_set: &str, key: &str, value: Value) -> Result<(), StageAuthoringError> {
let clip_set = clip_set.to_string();
let key = key.to_string();
self.prim
.clone()
.update_metadata(sdf::FieldKey::Clips.as_str(), move |current| {
let mut sets = match current {
Some(Value::Dictionary(d)) => d,
_ => HashMap::new(),
};
match sets
.entry(clip_set)
.or_insert_with(|| Value::Dictionary(HashMap::new()))
{
Value::Dictionary(set) => {
set.insert(key, value);
}
other => *other = Value::Dictionary(HashMap::from([(key, value)])),
}
Value::Dictionary(sets)
})?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::gf;
use crate::usd::Stage;
use std::collections::HashMap;
fn fixture(name: &str) -> String {
format!(
"{}/fixtures/{name}/root.usda",
std::env::var("CARGO_MANIFEST_DIR").unwrap()
)
}
#[test]
fn reads_explicit_clip_set_from_fixture() -> anyhow::Result<()> {
let stage = Stage::open(&fixture("clip_asset_anchor"))?;
let clips = ClipsAPI::new(&stage.prim(sdf::path("/Model")?));
assert_eq!(clips.clip_set_names()?, vec!["default".to_string()]);
assert_eq!(clips.clip_asset_paths("default")?, vec![AssetPath::new("./clip.usda")]);
assert_eq!(clips.clip_active("default")?, vec![gf::vec2d(0.0, 0.0)]);
assert_eq!(clips.clip_prim_path("default")?.as_deref(), Some("/Model"));
Ok(())
}
#[test]
fn reads_template_clip_set_from_fixture() -> anyhow::Result<()> {
let stage = Stage::open(&fixture("clip_template"))?;
let clips = ClipsAPI::new(&stage.prim(sdf::path("/Model")?));
assert_eq!(
clips.clip_template_asset_path("default")?.as_deref(),
Some("./clip.#.usda")
);
assert_eq!(clips.clip_template_start_time("default")?, Some(1.0));
assert_eq!(clips.clip_template_end_time("default")?, Some(2.0));
assert_eq!(clips.clip_template_stride("default")?, Some(1.0));
assert!(clips.clip_asset_paths("default")?.is_empty());
Ok(())
}
#[test]
fn reads_remaining_fields_and_missing_edges() -> anyhow::Result<()> {
let stage = Stage::builder().in_memory("anon.usda")?;
let set = Value::Dictionary(
[
(
keys::TIMES.to_string(),
Value::Vec2dVec(vec![gf::vec2d(0.0, 0.0), gf::vec2d(10.0, 5.0)]),
),
(
keys::MANIFEST_ASSET_PATH.to_string(),
Value::AssetPath("manifest.usda".into()),
),
(keys::INTERPOLATE_MISSING.to_string(), Value::Bool(true)),
(keys::TEMPLATE_ACTIVE_OFFSET.to_string(), Value::Double(0.5)),
]
.into_iter()
.collect::<HashMap<_, _>>(),
);
stage.define_prim(sdf::path("/Anim")?)?.set_metadata(
"clips",
Value::Dictionary([("default".to_string(), set)].into_iter().collect()),
)?;
let clips = ClipsAPI::new(&stage.prim(sdf::path("/Anim")?));
assert_eq!(
clips.clip_times("default")?,
vec![gf::vec2d(0.0, 0.0), gf::vec2d(10.0, 5.0)]
);
assert_eq!(
clips.clip_manifest_asset_path("default")?,
Some(AssetPath::new("manifest.usda"))
);
assert_eq!(clips.interpolate_missing_clip_values("default")?, Some(true));
assert_eq!(clips.clip_template_active_offset("default")?, Some(0.5));
assert!(clips.clip_asset_paths("nope")?.is_empty());
assert!(clips.clip_prim_path("default")?.is_none());
assert!(ClipsAPI::new(&stage.prim(sdf::path("/Absent")?))
.clip_set_names()?
.is_empty());
Ok(())
}
#[test]
fn set_get_round_trip() -> anyhow::Result<()> {
let stage = Stage::builder().in_memory("anon.usda")?;
stage.define_prim(sdf::path("/Anim")?)?;
let clips = ClipsAPI::new(&stage.prim(sdf::path("/Anim")?));
clips.set_clip_asset_paths("default", ["a.usda", "b.usda"])?;
clips.set_clip_active("default", vec![gf::vec2d(0.0, 0.0), gf::vec2d(10.0, 1.0)])?;
clips.set_clip_times("default", vec![gf::vec2d(0.0, 0.0), gf::vec2d(10.0, 5.0)])?;
clips.set_clip_prim_path("default", "/Anim")?;
clips.set_clip_manifest_asset_path("default", "manifest.usda")?;
clips.set_interpolate_missing_clip_values("default", true)?;
clips.set_clip_template_asset_path("tmpl", "clip.#.usda")?;
clips.set_clip_template_stride("tmpl", 1.0)?;
clips.set_clip_template_start_time("tmpl", 0.0)?;
clips.set_clip_template_end_time("tmpl", 10.0)?;
clips.set_clip_template_active_offset("tmpl", 0.5)?;
assert_eq!(clips.clip_set_names()?, vec!["default".to_string(), "tmpl".to_string()]);
assert_eq!(
clips.clip_asset_paths("default")?,
vec![AssetPath::new("a.usda"), AssetPath::new("b.usda")]
);
assert_eq!(
clips.clip_active("default")?,
vec![gf::vec2d(0.0, 0.0), gf::vec2d(10.0, 1.0)]
);
assert_eq!(
clips.clip_times("default")?,
vec![gf::vec2d(0.0, 0.0), gf::vec2d(10.0, 5.0)]
);
assert_eq!(clips.clip_prim_path("default")?.as_deref(), Some("/Anim"));
assert_eq!(
clips.clip_manifest_asset_path("default")?,
Some(AssetPath::new("manifest.usda"))
);
assert_eq!(clips.interpolate_missing_clip_values("default")?, Some(true));
assert_eq!(clips.clip_template_asset_path("tmpl")?.as_deref(), Some("clip.#.usda"));
assert_eq!(clips.clip_template_stride("tmpl")?, Some(1.0));
assert_eq!(clips.clip_template_start_time("tmpl")?, Some(0.0));
assert_eq!(clips.clip_template_end_time("tmpl")?, Some(10.0));
assert_eq!(clips.clip_template_active_offset("tmpl")?, Some(0.5));
Ok(())
}
#[test]
fn clip_sets_list_op_round_trip() -> anyhow::Result<()> {
let stage = Stage::builder().in_memory("anon.usda")?;
stage.define_prim(sdf::path("/Anim")?)?;
let clips = ClipsAPI::new(&stage.prim(sdf::path("/Anim")?));
assert!(clips.clip_sets()?.is_none());
clips.set_clip_sets(sdf::StringListOp::explicit(vec!["b".into(), "a".into()]))?;
let order = clips.clip_sets()?.expect("authored");
assert_eq!(order.flatten(), vec!["b".to_string(), "a".to_string()]);
Ok(())
}
#[test]
fn clips_dict_round_trip() -> anyhow::Result<()> {
let stage = Stage::builder().in_memory("anon.usda")?;
stage.define_prim(sdf::path("/Anim")?)?;
let clips = ClipsAPI::new(&stage.prim(sdf::path("/Anim")?));
assert!(clips.clips()?.is_none());
let set = Value::Dictionary([(keys::PRIM_PATH.to_string(), Value::String("/Geo".into()))].into());
clips.set_clips(sdf::Dictionary::from([("default".to_string(), set)]))?;
assert_eq!(clips.clip_set_names()?, vec!["default".to_string()]);
assert_eq!(clips.clip_prim_path("default")?.as_deref(), Some("/Geo"));
assert!(clips.clips()?.is_some());
Ok(())
}
#[test]
fn set_creates_over_without_local_spec() -> anyhow::Result<()> {
let stage = Stage::builder().in_memory("anon.usda")?;
let clips = ClipsAPI::new(&stage.prim(sdf::path("/NoSpec")?));
clips.set_clip_prim_path("default", "/Geo")?;
assert_eq!(clips.clip_prim_path("default")?.as_deref(), Some("/Geo"));
Ok(())
}
}