use openusd::Result;
use crate::SchemaError;
use openusd::gf;
use openusd::sdf;
use openusd::tf;
use openusd::usd::{Attribute, Prim, TimeCode};
use super::Imageable;
use super::tokens as tok;
const TOKEN_INVERT_PREFIX: &str = "!invert!";
const TOKEN_RESET_XFORM_STACK: &str = "!resetXformStack!";
const NS_XFORM_OP: &str = "xformOp:";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum XformOpPrecision {
Double,
Float,
Half,
}
pub trait Xformable: Imageable {
fn xform_op_order_attr(&self) -> Attribute {
self.prim().attribute(tok::A_XFORM_OP_ORDER)
}
fn create_xform_op_order_attr(&self) -> Result<Attribute> {
Ok(self
.prim()
.create_attribute(tok::A_XFORM_OP_ORDER, sdf::ValueTypeName::TOKEN_ARRAY)?
.set_custom(false)?
.set_variability(sdf::Variability::Uniform)?)
}
fn xform_op_order(&self) -> Result<Option<Vec<String>>> {
let attr = self.prim().path().append_property(tok::A_XFORM_OP_ORDER)?;
Ok(match self.prim().stage().field::<sdf::Value>(attr, "default")? {
Some(sdf::Value::TokenVec(v)) => Some(v.into_iter().map(Into::into).collect()),
Some(sdf::Value::StringVec(v)) => Some(v),
Some(sdf::Value::TokenListOp(op)) => Some(op.flatten().into_iter().map(Into::into).collect()),
Some(sdf::Value::StringListOp(op)) => Some(op.flatten()),
_ => None,
})
}
fn resets_xform_stack(&self) -> Result<bool> {
Ok(matches!(
self.xform_op_order()?.as_deref().and_then(|s| s.first()),
Some(s) if s == TOKEN_RESET_XFORM_STACK
))
}
fn local_to_parent_transform(&self, time: impl Into<TimeCode>) -> Result<gf::Matrix4d, SchemaError> {
let time = time.into();
let Some(order) = self.xform_op_order()? else {
return Ok(gf::Matrix4d::IDENTITY);
};
let mut m = gf::Matrix4d::IDENTITY;
for (i, op_name) in order.iter().enumerate() {
if op_name == TOKEN_RESET_XFORM_STACK {
if i == 0 {
continue;
}
return Err(SchemaError::InvalidOpOrder {
prim: self.prim().path().clone(),
index: i,
});
}
m = build_op_matrix(self.prim(), op_name, time)? * m;
}
Ok(m)
}
fn set_xform_op_order<I, S>(self, order: I) -> Result<Self>
where
Self: Sized,
I: IntoIterator<Item = S>,
S: Into<String>,
{
let tokens: Vec<String> = order.into_iter().map(Into::into).collect();
self.prim()
.create_attribute(tok::A_XFORM_OP_ORDER, sdf::ValueTypeName::TOKEN_ARRAY)?
.set_variability(sdf::Variability::Uniform)?
.set_custom(false)?
.set(sdf::Value::token_vec(tokens))?;
Ok(self)
}
fn set_xform_op(
self,
op: &str,
precision: XformOpPrecision,
value: impl Into<sdf::Value>,
) -> Result<Self, SchemaError>
where
Self: Sized,
{
author_xform_op(self.prim(), op, precision, value.into())?;
append_op(self.prim(), op)?;
Ok(self)
}
fn set_translate(self, value: gf::Vec3d) -> Result<Self, SchemaError>
where
Self: Sized,
{
self.set_xform_op("translate", XformOpPrecision::Double, value)
}
fn set_scale(self, value: gf::Vec3f) -> Result<Self, SchemaError>
where
Self: Sized,
{
self.set_xform_op("scale", XformOpPrecision::Float, value)
}
fn set_rotate_x(self, degrees: f32) -> Result<Self, SchemaError>
where
Self: Sized,
{
self.set_xform_op("rotateX", XformOpPrecision::Float, degrees)
}
fn set_rotate_y(self, degrees: f32) -> Result<Self, SchemaError>
where
Self: Sized,
{
self.set_xform_op("rotateY", XformOpPrecision::Float, degrees)
}
fn set_rotate_z(self, degrees: f32) -> Result<Self, SchemaError>
where
Self: Sized,
{
self.set_xform_op("rotateZ", XformOpPrecision::Float, degrees)
}
fn set_rotate_xyz(self, degrees: gf::Vec3f) -> Result<Self, SchemaError>
where
Self: Sized,
{
self.set_xform_op("rotateXYZ", XformOpPrecision::Float, degrees)
}
fn set_orient(self, q: gf::Quatf) -> Result<Self, SchemaError>
where
Self: Sized,
{
self.set_xform_op("orient", XformOpPrecision::Float, q)
}
fn set_transform(self, matrix: gf::Matrix4d) -> Result<Self, SchemaError>
where
Self: Sized,
{
self.set_xform_op("transform", XformOpPrecision::Double, matrix)
}
}
fn build_op_matrix(prim: &Prim, op_name: &str, time: TimeCode) -> Result<gf::Matrix4d, SchemaError> {
let (inverted, base) = match op_name.strip_prefix(TOKEN_INVERT_PREFIX) {
Some(stripped) => (true, stripped),
None => (false, op_name),
};
let attr = prim.path().append_property(base)?;
let Some(raw) = prim.stage().attribute(attr)?.get_at::<sdf::Value>(time)? else {
return Ok(gf::Matrix4d::IDENTITY);
};
let kind = op_kind(base);
let m = match kind {
"translate" => gf::Matrix4d::translation(value_to_vec3_f64(&raw).unwrap_or([0.0, 0.0, 0.0])),
"translateX" => gf::Matrix4d::translation([value_to_scalar_f64(&raw).unwrap_or(0.0), 0.0, 0.0]),
"translateY" => gf::Matrix4d::translation([0.0, value_to_scalar_f64(&raw).unwrap_or(0.0), 0.0]),
"translateZ" => gf::Matrix4d::translation([0.0, 0.0, value_to_scalar_f64(&raw).unwrap_or(0.0)]),
"scale" => gf::Matrix4d::scale(value_to_vec3_f64(&raw).unwrap_or([1.0, 1.0, 1.0])),
"scaleX" => gf::Matrix4d::scale([value_to_scalar_f64(&raw).unwrap_or(1.0), 1.0, 1.0]),
"scaleY" => gf::Matrix4d::scale([1.0, value_to_scalar_f64(&raw).unwrap_or(1.0), 1.0]),
"scaleZ" => gf::Matrix4d::scale([1.0, 1.0, value_to_scalar_f64(&raw).unwrap_or(1.0)]),
"orient" => gf::Matrix4d::from_quat(value_to_quat_wxyz(&raw).unwrap_or([1.0, 0.0, 0.0, 0.0])),
"rotateX" => gf::Matrix4d::rotation_x(value_to_scalar_f64(&raw).unwrap_or(0.0).to_radians()),
"rotateY" => gf::Matrix4d::rotation_y(value_to_scalar_f64(&raw).unwrap_or(0.0).to_radians()),
"rotateZ" => gf::Matrix4d::rotation_z(value_to_scalar_f64(&raw).unwrap_or(0.0).to_radians()),
"rotateXYZ" | "rotateYXZ" | "rotateZXY" | "rotateXZY" | "rotateYZX" | "rotateZYX" => {
let v = value_to_vec3_f64(&raw).unwrap_or([0.0, 0.0, 0.0]);
let rx = gf::Matrix4d::rotation_x(v[0].to_radians());
let ry = gf::Matrix4d::rotation_y(v[1].to_radians());
let rz = gf::Matrix4d::rotation_z(v[2].to_radians());
match kind {
"rotateXYZ" => rx * ry * rz,
"rotateYXZ" => ry * rx * rz,
"rotateZXY" => rz * rx * ry,
"rotateXZY" => rx * rz * ry,
"rotateYZX" => ry * rz * rx,
"rotateZYX" => rz * ry * rx,
_ => unreachable!("kind guard above"),
}
}
"transform" => match raw {
sdf::Value::Matrix4d(m) => m,
_ => gf::Matrix4d::IDENTITY,
},
_ => gf::Matrix4d::IDENTITY,
};
if inverted {
m.inverse().ok_or_else(|| SchemaError::SingularTransform {
op: op_name.to_string(),
})
} else {
Ok(m)
}
}
fn author_xform_op(prim: &Prim, op: &str, precision: XformOpPrecision, value: sdf::Value) -> Result<(), SchemaError> {
let fallback = op_value_type(op, precision)?;
let name = op_attr_name(op);
let declared = match prim
.attribute(name.as_str())
.get_metadata::<tf::Token>(sdf::FieldKey::TypeName.as_str())?
{
Some(token) => sdf::ValueTypeName::from(token),
None => fallback,
};
let value = declared.coerce(value)?;
prim.create_attribute(name, declared)?.set_custom(false)?.set(value)?;
Ok(())
}
fn op_attr_name(op: &str) -> String {
match op.starts_with(NS_XFORM_OP) {
true => op.to_string(),
false => format!("{NS_XFORM_OP}{op}"),
}
}
fn op_kind(name: &str) -> &str {
let after_ns = name.strip_prefix(NS_XFORM_OP).unwrap_or(name);
after_ns.split(':').next().unwrap_or(after_ns)
}
fn op_value_type(op: &str, precision: XformOpPrecision) -> Result<sdf::ValueTypeName, SchemaError> {
use XformOpPrecision as P;
Ok(match op_kind(op) {
"transform" => sdf::ValueTypeName::MATRIX4D,
"orient" => match precision {
P::Double => sdf::ValueTypeName::QUATD,
P::Float => sdf::ValueTypeName::QUATF,
P::Half => sdf::ValueTypeName::QUATH,
},
"translateX" | "translateY" | "translateZ" | "scaleX" | "scaleY" | "scaleZ" | "rotateX" | "rotateY"
| "rotateZ" => match precision {
P::Double => sdf::ValueTypeName::DOUBLE,
P::Float => sdf::ValueTypeName::FLOAT,
P::Half => sdf::ValueTypeName::HALF,
},
"translate" | "scale" | "rotateXYZ" | "rotateXZY" | "rotateYXZ" | "rotateYZX" | "rotateZXY" | "rotateZYX" => {
match precision {
P::Double => sdf::ValueTypeName::DOUBLE3,
P::Float => sdf::ValueTypeName::FLOAT3,
P::Half => sdf::ValueTypeName::HALF3,
}
}
other => {
return Err(SchemaError::UnknownXformOp { op: other.to_string() });
}
})
}
fn append_op(prim: &Prim, op: &str) -> Result<()> {
prim.append_to_uniform_token_array(tok::A_XFORM_OP_ORDER, op_attr_name(op))?;
Ok(())
}
fn value_to_scalar_f64(v: &sdf::Value) -> Option<f64> {
match v {
sdf::Value::Double(d) => Some(*d),
sdf::Value::Float(f) => Some(*f as f64),
sdf::Value::Half(h) => Some(h.to_f32() as f64),
sdf::Value::Int(i) => Some(*i as f64),
sdf::Value::Int64(i) => Some(*i as f64),
_ => None,
}
}
fn value_to_vec3_f64(v: &sdf::Value) -> Option<[f64; 3]> {
match v {
sdf::Value::Vec3d(a) => Some(<[f64; 3]>::from(*a)),
sdf::Value::Vec3f(a) => Some([a.x as f64, a.y as f64, a.z as f64]),
sdf::Value::Vec3h(a) => Some([a.x.to_f32() as f64, a.y.to_f32() as f64, a.z.to_f32() as f64]),
_ => None,
}
}
fn value_to_quat_wxyz(v: &sdf::Value) -> Option<[f64; 4]> {
match v {
sdf::Value::Quatd(q) => Some(<[f64; 4]>::from(*q)),
sdf::Value::Quatf(q) => Some([q.w as f64, q.x as f64, q.y as f64, q.z as f64]),
sdf::Value::Quath(q) => Some([
q.w.to_f32() as f64,
q.x.to_f32() as f64,
q.y.to_f32() as f64,
q.z.to_f32() as f64,
]),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::{XformOpPrecision, Xformable};
use crate::SchemaError;
use crate::geom::Xform;
use openusd::Result;
use openusd::gf;
use openusd::sdf;
use openusd::usd::Stage;
#[test]
fn scale_into_double_op() -> Result<(), SchemaError> {
let stage = Stage::builder().in_memory("anon.usda")?;
let x = Xform::define(&stage, "/X")?;
stage.create_attribute("/X.xformOp:scale", sdf::ValueTypeName::DOUBLE3)?;
x.set_scale(gf::vec3f(2.0, 2.0, 2.0))?;
assert_eq!(
stage.attribute("/X.xformOp:scale")?.type_name()?,
Some(sdf::ValueTypeName::DOUBLE3),
"the declared precision survives"
);
assert_eq!(
stage.field::<sdf::Value>("/X.xformOp:scale", sdf::FieldKey::Default)?,
Some(sdf::Value::Vec3d(gf::vec3d(2.0, 2.0, 2.0)))
);
Ok(())
}
#[test]
fn op_precision_selects_type() -> Result<(), SchemaError> {
let stage = Stage::builder().in_memory("anon.usda")?;
Xform::define(&stage, "/X")?
.set_xform_op("scale", XformOpPrecision::Double, gf::vec3f(2.0, 2.0, 2.0))?
.set_xform_op("rotateX", XformOpPrecision::Half, 90.0_f32)?
.set_xform_op("orient", XformOpPrecision::Double, gf::Quatf::IDENTITY)?
.set_xform_op("transform", XformOpPrecision::Half, gf::Matrix4d::IDENTITY)?;
let declared = |path: &str| stage.attribute(path)?.type_name();
assert_eq!(declared("/X.xformOp:scale")?, Some(sdf::ValueTypeName::DOUBLE3));
assert_eq!(declared("/X.xformOp:rotateX")?, Some(sdf::ValueTypeName::HALF));
assert_eq!(declared("/X.xformOp:orient")?, Some(sdf::ValueTypeName::QUATD));
assert_eq!(declared("/X.xformOp:transform")?, Some(sdf::ValueTypeName::MATRIX4D));
let default = |path: &str| stage.field::<sdf::Value>(path, sdf::FieldKey::Default);
assert_eq!(
default("/X.xformOp:scale")?,
Some(sdf::Value::Vec3d(gf::vec3d(2.0, 2.0, 2.0)))
);
assert_eq!(
default("/X.xformOp:rotateX")?,
Some(sdf::Value::Half(gf::f16::from_f32(90.0)))
);
assert_eq!(
default("/X.xformOp:orient")?,
Some(sdf::Value::Quatd(gf::Quatd::IDENTITY)),
"a quaternion converts component-wise in (w, x, y, z) order"
);
Ok(())
}
#[test]
fn suffix_keeps_kind() -> Result<(), SchemaError> {
let stage = Stage::builder().in_memory("anon.usda")?;
let x = Xform::define(&stage, "/X")?.set_xform_op(
"translate:pivot",
XformOpPrecision::Float,
gf::vec3f(1.0, 2.0, 3.0),
)?;
assert_eq!(
stage.attribute("/X.xformOp:translate:pivot")?.type_name()?,
Some(sdf::ValueTypeName::FLOAT3)
);
assert_eq!(x.xform_op_order()?, Some(vec!["xformOp:translate:pivot".to_string()]));
Ok(())
}
#[test]
fn unknown_op_rejected() -> Result<(), SchemaError> {
let stage = Stage::builder().in_memory("anon.usda")?;
let x = Xform::define(&stage, "/X")?;
for op in ["bogusOp", "!invert!translate"] {
let error = x
.clone()
.set_xform_op(op, XformOpPrecision::Float, gf::vec3f(1.0, 2.0, 3.0))
.err()
.unwrap_or_else(|| panic!("{op} was accepted"));
assert!(matches!(error, SchemaError::UnknownXformOp { .. }), "{op}: {error:?}");
}
assert_eq!(x.xform_op_order()?, None, "nothing was appended to the stack");
assert_eq!(
stage.field::<sdf::Value>("/X.xformOp:bogusOp", sdf::FieldKey::Default)?,
None,
"and nothing was authored for it"
);
stage.create_attribute("/X.xformOp:bogusOp", sdf::ValueTypeName::FLOAT3)?;
let error = x
.set_xform_op("bogusOp", XformOpPrecision::Float, gf::vec3f(1.0, 2.0, 3.0))
.err()
.unwrap_or_else(|| panic!("a declared bogus op was accepted"));
assert!(matches!(error, SchemaError::UnknownXformOp { .. }), "{error:?}");
Ok(())
}
#[test]
fn unregistered_op_type_rejected() -> Result<(), SchemaError> {
let stage = Stage::builder().in_memory("anon.usda")?;
let x = Xform::define(&stage, "/X")?;
stage.create_attribute("/X.xformOp:scale", "double3d[]")?;
let error = x
.clone()
.set_xform_op("scale", XformOpPrecision::Float, gf::vec3f(2.0, 2.0, 2.0))
.err()
.unwrap_or_else(|| panic!("the unregistered declaration was accepted"));
assert!(matches!(error, SchemaError::Core(_)), "{error:?}");
assert_eq!(
stage.field::<sdf::Value>("/X.xformOp:scale", sdf::FieldKey::Default)?,
None,
"no value was authored"
);
assert_eq!(x.xform_op_order()?, None);
Ok(())
}
#[test]
fn failed_conversion_authors_nothing() -> Result<(), SchemaError> {
let stage = Stage::builder().in_memory("anon.usda")?;
let x = Xform::define(&stage, "/X")?;
let error = x
.clone()
.set_xform_op("scale", XformOpPrecision::Half, gf::vec3f(70000.0, 1.0, 1.0))
.err()
.unwrap_or_else(|| panic!("the out-of-range value was accepted"));
assert!(matches!(error, SchemaError::Core(_)), "{error:?}");
assert_eq!(
stage.field::<sdf::Value>("/X.xformOp:scale", sdf::FieldKey::TypeName)?,
None,
"no declaration was left behind"
);
assert_eq!(x.xform_op_order()?, None);
x.set_xform_op("scale", XformOpPrecision::Double, gf::vec3f(2.0, 2.0, 2.0))?;
assert_eq!(
stage.attribute("/X.xformOp:scale")?.type_name()?,
Some(sdf::ValueTypeName::DOUBLE3)
);
Ok(())
}
#[test]
fn prefixed_op_not_doubled() -> Result<(), SchemaError> {
let stage = Stage::builder().in_memory("anon.usda")?;
let x = Xform::define(&stage, "/X")?.set_xform_op(
"xformOp:translate",
XformOpPrecision::Double,
gf::vec3d(1.0, 2.0, 3.0),
)?;
assert_eq!(x.xform_op_order()?, Some(vec!["xformOp:translate".to_string()]));
assert_eq!(
stage.field::<sdf::Value>("/X.xformOp:translate", sdf::FieldKey::Default)?,
Some(sdf::Value::Vec3d(gf::vec3d(1.0, 2.0, 3.0)))
);
Ok(())
}
#[test]
fn translate_appears_in_order() -> Result<(), SchemaError> {
let stage = Stage::builder().in_memory("anon.usda")?;
let x = Xform::define(&stage, "/X")?.set_translate(gf::vec3d(1.0, 2.0, 3.0))?;
assert_eq!(x.xform_op_order()?, Some(vec!["xformOp:translate".to_string()]));
assert_eq!(
stage.field::<sdf::Value>("/X.xformOp:translate", sdf::FieldKey::Default)?,
Some(sdf::Value::Vec3d(gf::vec3d(1.0, 2.0, 3.0)))
);
Ok(())
}
#[test]
fn trs_preserves_insertion_order() -> Result<(), SchemaError> {
let stage = Stage::builder().in_memory("anon.usda")?;
let x = Xform::define(&stage, "/X")?
.set_translate(gf::vec3d(1.0, 2.0, 3.0))?
.set_rotate_y(90.0)?
.set_scale(gf::vec3f(2.0, 2.0, 2.0))?;
assert_eq!(
x.xform_op_order()?,
Some(vec![
"xformOp:translate".to_string(),
"xformOp:rotateY".to_string(),
"xformOp:scale".to_string(),
])
);
Ok(())
}
#[test]
fn local_to_parent_translate_unrotated() -> Result<(), SchemaError> {
let stage = Stage::builder().in_memory("anon.usda")?;
let x = Xform::define(&stage, "/X")?
.set_translate(gf::vec3d(3.0, 5.0, 7.0))?
.set_rotate_z(90.0)?;
let m = x.local_to_parent_transform(0.0)?;
assert_eq!([m.0[12], m.0[13], m.0[14]], [3.0, 5.0, 7.0]);
Ok(())
}
#[test]
fn re_authoring_op_does_not_duplicate() -> Result<(), SchemaError> {
let stage = Stage::builder().in_memory("anon.usda")?;
let x = Xform::define(&stage, "/X")?
.set_translate(gf::vec3d(1.0, 0.0, 0.0))?
.set_translate(gf::vec3d(2.0, 0.0, 0.0))?;
assert_eq!(x.xform_op_order()?, Some(vec!["xformOp:translate".to_string()]));
Ok(())
}
#[test]
fn rotate_xyz_authors_float3() -> Result<(), SchemaError> {
let stage = Stage::builder().in_memory("anon.usda")?;
Xform::define(&stage, "/X")?.set_rotate_xyz(gf::vec3f(30.0, 45.0, 60.0))?;
assert_eq!(
stage.field::<sdf::Value>("/X.xformOp:rotateXYZ", sdf::FieldKey::Default)?,
Some(sdf::Value::Vec3f(gf::vec3f(30.0, 45.0, 60.0)))
);
Ok(())
}
#[test]
fn orient_writes_quatf() -> Result<(), SchemaError> {
let stage = Stage::builder().in_memory("anon.usda")?;
Xform::define(&stage, "/X")?.set_orient(gf::quatf(1.0, 0.0, 0.0, 0.0))?;
assert_eq!(
stage.field::<sdf::Value>("/X.xformOp:orient", sdf::FieldKey::Default)?,
Some(sdf::Value::quatf(1.0, 0.0, 0.0, 0.0))
);
Ok(())
}
#[test]
fn transform_writes_matrix4d() -> Result<(), SchemaError> {
let stage = Stage::builder().in_memory("anon.usda")?;
let m = gf::Matrix4d([
1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 5.0, 0.0, 0.0, 1.0,
]);
Xform::define(&stage, "/X")?.set_transform(m)?;
match stage.field::<sdf::Value>("/X.xformOp:transform", sdf::FieldKey::Default)? {
Some(sdf::Value::Matrix4d(v)) => assert_eq!(v[12], 5.0),
other => panic!("expected gf::Matrix4d, got {other:?}"),
}
Ok(())
}
}