use std::collections::HashMap;
use anyhow::Result;
use crate::schemas::common::{get_typed, get_with_api};
use crate::sdf::{self, FieldKey, Value};
use crate::usd::{Attribute, Prim, Stage};
use super::impl_media_schema;
use super::tokens as tok;
#[derive(Clone, derive_more::Deref)]
pub struct SpatialAudio(Prim);
impl SpatialAudio {
pub fn define(stage: &Stage, path: impl Into<sdf::Path>) -> Result<Self> {
Ok(Self(stage.define_prim(path)?.set_type_name(tok::T_SPATIAL_AUDIO)?))
}
pub fn get(stage: &Stage, path: impl Into<sdf::Path>) -> Result<Option<Self>> {
get_typed(stage, path, tok::T_SPATIAL_AUDIO).map(|o| o.map(Self))
}
pub fn file_path_attr(&self) -> Attribute {
self.attribute(tok::A_FILE_PATH)
}
pub fn create_file_path_attr(&self) -> Result<Attribute> {
Ok(self
.create_attribute(tok::A_FILE_PATH, "asset")?
.set_custom(false)?
.set_variability(sdf::Variability::Uniform)?)
}
pub fn aural_mode_attr(&self) -> Attribute {
self.attribute(tok::A_AURAL_MODE)
}
pub fn create_aural_mode_attr(&self) -> Result<Attribute> {
Ok(self
.create_attribute(tok::A_AURAL_MODE, "token")?
.set_custom(false)?
.set_variability(sdf::Variability::Uniform)?)
}
pub fn playback_mode_attr(&self) -> Attribute {
self.attribute(tok::A_PLAYBACK_MODE)
}
pub fn create_playback_mode_attr(&self) -> Result<Attribute> {
Ok(self
.create_attribute(tok::A_PLAYBACK_MODE, "token")?
.set_custom(false)?
.set_variability(sdf::Variability::Uniform)?)
}
pub fn start_time_attr(&self) -> Attribute {
self.attribute(tok::A_START_TIME)
}
pub fn create_start_time_attr(&self) -> Result<Attribute> {
Ok(self
.create_attribute(tok::A_START_TIME, "timecode")?
.set_custom(false)?
.set_variability(sdf::Variability::Uniform)?)
}
pub fn end_time_attr(&self) -> Attribute {
self.attribute(tok::A_END_TIME)
}
pub fn create_end_time_attr(&self) -> Result<Attribute> {
Ok(self
.create_attribute(tok::A_END_TIME, "timecode")?
.set_custom(false)?
.set_variability(sdf::Variability::Uniform)?)
}
pub fn media_offset_attr(&self) -> Attribute {
self.attribute(tok::A_MEDIA_OFFSET)
}
pub fn create_media_offset_attr(&self) -> Result<Attribute> {
Ok(self
.create_attribute(tok::A_MEDIA_OFFSET, "double")?
.set_custom(false)?
.set_variability(sdf::Variability::Uniform)?)
}
pub fn gain_attr(&self) -> Attribute {
self.attribute(tok::A_GAIN)
}
pub fn create_gain_attr(&self) -> Result<Attribute> {
Ok(self.create_attribute(tok::A_GAIN, "double")?.set_custom(false)?)
}
}
impl_media_schema!(xformable SpatialAudio);
#[derive(Clone, derive_more::Deref)]
pub struct AssetPreviewsAPI(Prim);
impl AssetPreviewsAPI {
pub fn apply(stage: &Stage, path: impl Into<sdf::Path>) -> Result<Self> {
Ok(Self(
stage.override_prim(path)?.add_applied_schema(tok::API_ASSET_PREVIEWS)?,
))
}
pub fn get(stage: &Stage, path: impl Into<sdf::Path>) -> Result<Option<Self>> {
get_with_api(stage, path, &[tok::API_ASSET_PREVIEWS]).map(|o| o.map(Self))
}
pub fn default_thumbnail(&self) -> Result<Option<String>> {
let Some(Value::Dictionary(asset_info)) =
self.stage().field::<Value>(self.path().clone(), FieldKey::AssetInfo)?
else {
return Ok(None);
};
let leaf = nested_dict(&asset_info, tok::PREVIEWS)
.and_then(|d| nested_dict(d, tok::THUMBNAILS))
.and_then(|d| nested_dict(d, tok::PREVIEW_DEFAULT))
.and_then(|d| d.get(tok::DEFAULT_IMAGE));
Ok(leaf.and_then(Value::as_str).map(str::to_owned))
}
pub fn set_default_thumbnail(self, image: impl Into<String>) -> Result<Self> {
let image = image.into();
let prim = self.stage().override_prim(self.path().clone())?.update_metadata(
FieldKey::AssetInfo.as_str(),
|local| {
let mut asset_info = match local {
Some(Value::Dictionary(d)) => d,
_ => HashMap::new(),
};
let previews = nested_dict_mut(&mut asset_info, tok::PREVIEWS);
let thumbnails = nested_dict_mut(previews, tok::THUMBNAILS);
let default = nested_dict_mut(thumbnails, tok::PREVIEW_DEFAULT);
default.insert(tok::DEFAULT_IMAGE.to_string(), Value::AssetPath(image.into()));
Value::Dictionary(asset_info)
},
)?;
Ok(Self(prim))
}
}
impl_media_schema!(applied_api AssetPreviewsAPI);
fn nested_dict<'a>(d: &'a HashMap<String, Value>, key: &str) -> Option<&'a HashMap<String, Value>> {
match d.get(key) {
Some(Value::Dictionary(inner)) => Some(inner),
_ => None,
}
}
fn nested_dict_mut<'a>(d: &'a mut HashMap<String, Value>, key: &str) -> &'a mut HashMap<String, Value> {
let entry = d
.entry(key.to_string())
.or_insert_with(|| Value::Dictionary(HashMap::new()));
if !matches!(entry, Value::Dictionary(_)) {
*entry = Value::Dictionary(HashMap::new());
}
let Value::Dictionary(inner) = entry else {
unreachable!("entry was just ensured to be a dictionary")
};
inner
}
#[cfg(test)]
mod tests {
use super::*;
use crate::schemas::media::{AuralMode, PlaybackMode};
#[test]
fn spatial_audio_roundtrip() -> Result<()> {
let stage = Stage::builder().in_memory("anon.usda")?;
let a = SpatialAudio::define(&stage, "/World/Audio")?;
a.create_file_path_attr()?
.set(sdf::Value::AssetPath("./ambient.wav".into()))?;
a.create_aural_mode_attr()?
.set(sdf::Value::Token(AuralMode::NonSpatial.as_token().into()))?;
a.create_playback_mode_attr()?
.set(sdf::Value::Token(PlaybackMode::LoopFromStartToEnd.as_token().into()))?;
a.create_start_time_attr()?.set(sdf::TimeCode(24.0))?;
a.create_end_time_attr()?.set(sdf::TimeCode(48.0))?;
a.create_media_offset_attr()?.set(2.5_f64)?;
a.create_gain_attr()?.set(0.5_f64)?;
let a = SpatialAudio::get(&stage, "/World/Audio")?.expect("SpatialAudio");
assert_eq!(
a.file_path_attr().get::<sdf::Value>()?,
Some(sdf::Value::AssetPath("./ambient.wav".into()))
);
assert_eq!(a.start_time_attr().get::<sdf::TimeCode>()?, Some(sdf::TimeCode(24.0)));
assert_eq!(a.media_offset_attr().get::<f64>()?, Some(2.5));
assert_eq!(a.gain_attr().get::<f64>()?, Some(0.5));
assert!(SpatialAudio::get(&stage, "/Missing")?.is_none());
Ok(())
}
#[test]
fn get_rejects_non_spatial_audio() -> Result<()> {
let stage = Stage::builder().in_memory("anon.usda")?;
stage.define_prim("/NotAudio")?.set_type_name("Scope")?;
assert!(SpatialAudio::get(&stage, "/NotAudio")?.is_none());
Ok(())
}
#[test]
fn asset_previews_roundtrip() -> Result<()> {
let stage = Stage::builder().in_memory("anon.usda")?;
stage.define_prim("/Chair")?.set_type_name("Xform")?;
AssetPreviewsAPI::apply(&stage, "/Chair")?.set_default_thumbnail("./chair_thumb.jpg")?;
assert!(stage
.prim_at(sdf::path("/Chair")?)
.has_api_schema(tok::API_ASSET_PREVIEWS)?);
let previews = AssetPreviewsAPI::get(&stage, "/Chair")?.expect("AssetPreviewsAPI");
assert_eq!(previews.default_thumbnail()?.as_deref(), Some("./chair_thumb.jpg"));
Ok(())
}
#[test]
fn get_rejects_unapplied() -> Result<()> {
let stage = Stage::builder().in_memory("anon.usda")?;
stage.define_prim("/Bare")?.set_type_name("Xform")?;
assert!(AssetPreviewsAPI::get(&stage, "/Bare")?.is_none());
Ok(())
}
#[test]
fn set_thumbnail_preserves_other_asset_info() -> Result<()> {
let stage = Stage::builder().in_memory("anon.usda")?;
let mut info = HashMap::new();
info.insert("name".to_string(), Value::String("Chair".to_string()));
stage
.define_prim("/Chair")?
.set_type_name("Xform")?
.set_metadata("assetInfo", Value::Dictionary(info))?;
AssetPreviewsAPI::apply(&stage, "/Chair")?.set_default_thumbnail("./t.jpg")?;
let Some(Value::Dictionary(info)) = stage.field::<Value>(sdf::path("/Chair")?, FieldKey::AssetInfo)? else {
panic!("assetInfo");
};
assert_eq!(info.get("name"), Some(&Value::String("Chair".to_string())));
assert!(info.contains_key(tok::PREVIEWS));
Ok(())
}
}