use anyhow::{bail, Result};
use crate::sdf;
use crate::usd::{Attribute, Prim, Relationship, Stage};
use super::impl_vol_schema;
use super::tokens as tok;
use crate::schemas::common::get_typed;
#[derive(Clone, derive_more::Deref)]
pub struct Volume(Prim);
impl Volume {
pub fn define(stage: &Stage, path: impl Into<sdf::Path>) -> Result<Self> {
Ok(Self(stage.define_prim(path)?.set_type_name(tok::T_VOLUME)?))
}
pub fn get(stage: &Stage, path: impl Into<sdf::Path>) -> Result<Option<Self>> {
get_typed(stage, path, tok::T_VOLUME).map(|o| o.map(Self))
}
pub fn field_rel(&self, name: &str) -> Relationship {
self.relationship(&format!("{}{name}", tok::NS_FIELD))
}
pub fn create_field_relationship(self, name: &str, target: impl Into<sdf::Path>) -> Result<Self> {
if name.is_empty() {
bail!("Volume field name must not be empty");
}
self.create_relationship(&format!("{}{name}", tok::NS_FIELD))?
.set_custom(false)?
.add_target(target.into())?;
Ok(self)
}
pub fn has_field_relationship(&self, name: &str) -> Result<bool> {
let rel = self.path().append_property(&format!("{}{name}", tok::NS_FIELD))?;
Ok(!self.stage().relationship_at(rel).targets()?.is_empty())
}
pub fn field_paths(&self) -> Result<Vec<(String, sdf::Path)>> {
let mut fields = Vec::new();
for name in self.stage().prim_at(self.path().clone()).property_names()? {
let Some(field_name) = name.strip_prefix(tok::NS_FIELD) else {
continue;
};
let rel = self.path().append_property(&name)?;
if let Some(target) = self.stage().relationship_at(rel).targets()?.into_iter().next() {
fields.push((field_name.to_string(), target));
}
}
fields.sort();
Ok(fields)
}
}
impl_vol_schema!(gprim Volume);
#[derive(Clone, derive_more::Deref)]
pub struct OpenVDBAsset(Prim);
impl OpenVDBAsset {
pub fn define(stage: &Stage, path: impl Into<sdf::Path>) -> Result<Self> {
Ok(Self(stage.define_prim(path)?.set_type_name(tok::T_OPENVDB_ASSET)?))
}
pub fn get(stage: &Stage, path: impl Into<sdf::Path>) -> Result<Option<Self>> {
get_typed(stage, path, tok::T_OPENVDB_ASSET).map(|o| o.map(Self))
}
pub fn field_class_attr(&self) -> Attribute {
self.attribute(tok::A_FIELD_CLASS)
}
pub fn create_field_class_attr(&self) -> Result<Attribute> {
Ok(self
.create_attribute(tok::A_FIELD_CLASS, "token")?
.set_custom(false)?
.set_variability(sdf::Variability::Uniform)?)
}
}
impl_vol_schema!(field_asset OpenVDBAsset);
#[derive(Clone, derive_more::Deref)]
pub struct Field3DAsset(Prim);
impl Field3DAsset {
pub fn define(stage: &Stage, path: impl Into<sdf::Path>) -> Result<Self> {
Ok(Self(stage.define_prim(path)?.set_type_name(tok::T_FIELD3D_ASSET)?))
}
pub fn get(stage: &Stage, path: impl Into<sdf::Path>) -> Result<Option<Self>> {
get_typed(stage, path, tok::T_FIELD3D_ASSET).map(|o| o.map(Self))
}
pub fn field_purpose_attr(&self) -> Attribute {
self.attribute(tok::A_FIELD_PURPOSE)
}
pub fn create_field_purpose_attr(&self) -> Result<Attribute> {
Ok(self
.create_attribute(tok::A_FIELD_PURPOSE, "token")?
.set_custom(false)?
.set_variability(sdf::Variability::Uniform)?)
}
}
impl_vol_schema!(field_asset Field3DAsset);
#[cfg(test)]
mod tests {
use super::*;
use crate::schemas::vol::{FieldAsset, VectorDataRoleHint};
#[test]
fn volume_fields_roundtrip() -> Result<()> {
let stage = Stage::builder().in_memory("anon.usda")?;
Volume::define(&stage, "/V")?
.create_field_relationship("density", sdf::path("/V/density")?)?
.create_field_relationship("temperature", sdf::path("/V/temperature")?)?;
let v = Volume::get(&stage, "/V")?.expect("Volume");
assert!(v.has_field_relationship("density")?);
assert_eq!(
v.field_paths()?,
vec![
("density".to_string(), sdf::path("/V/density")?),
("temperature".to_string(), sdf::path("/V/temperature")?),
],
);
Ok(())
}
#[test]
fn openvdb_asset_roundtrip() -> Result<()> {
let stage = Stage::builder().in_memory("anon.usda")?;
let a = OpenVDBAsset::define(&stage, "/V/density")?;
a.create_file_path_attr()?
.set(sdf::Value::AssetPath("./smoke.vdb".into()))?;
a.create_field_name_attr()?.set("density".to_string())?;
a.create_field_index_attr()?.set(0)?;
a.create_field_data_type_attr()?.set("float".to_string())?;
a.create_vector_data_role_hint_attr()?.set(VectorDataRoleHint::NoRole)?;
a.create_field_class_attr()?.set("fogVolume".to_string())?;
let a = OpenVDBAsset::get(&stage, "/V/density")?.expect("OpenVDBAsset");
assert_eq!(a.field_name_attr().get::<String>()?.as_deref(), Some("density"));
assert_eq!(a.field_index_attr().get::<i32>()?, Some(0));
assert_eq!(
a.vector_data_role_hint_attr().get::<VectorDataRoleHint>()?,
Some(VectorDataRoleHint::NoRole)
);
assert_eq!(a.field_class_attr().get::<String>()?.as_deref(), Some("fogVolume"));
Ok(())
}
#[test]
fn field3d_asset_and_type_gate() -> Result<()> {
let stage = Stage::builder().in_memory("anon.usda")?;
let a = Field3DAsset::define(&stage, "/V/vel")?;
a.create_field_data_type_attr()?.set("float3".to_string())?;
a.create_vector_data_role_hint_attr()?.set(VectorDataRoleHint::Vector)?;
a.create_field_purpose_attr()?.set("motion".to_string())?;
let a = Field3DAsset::get(&stage, "/V/vel")?.expect("Field3DAsset");
assert_eq!(
a.vector_data_role_hint_attr().get::<VectorDataRoleHint>()?,
Some(VectorDataRoleHint::Vector)
);
assert_eq!(a.field_purpose_attr().get::<String>()?.as_deref(), Some("motion"));
assert!(OpenVDBAsset::get(&stage, "/V/vel")?.is_none());
Ok(())
}
#[test]
fn create_field_rejects_empty() -> Result<()> {
let stage = Stage::builder().in_memory("anon.usda")?;
let result = Volume::define(&stage, "/V")?.create_field_relationship("", sdf::path("/V/density")?);
assert!(result.is_err());
Ok(())
}
}