use std::collections::HashMap;
use crate::fbx_node::{FbxNode, FbxProperty};
pub(crate) struct PropertyTemplates<'a> {
by_object_type: HashMap<&'a str, (&'a str, &'a FbxNode)>,
}
impl<'a> PropertyTemplates<'a> {
pub(crate) fn build(nodes: &'a [FbxNode]) -> Self {
let mut by_object_type = HashMap::new();
for definitions in nodes.iter().filter(|node| node.name == "Definitions") {
for object_type in definitions
.children
.iter()
.filter(|child| child.name == "ObjectType")
{
let Some(FbxProperty::String(type_name)) = object_type.properties.first() else {
continue;
};
let Some(template) = object_type
.children
.iter()
.find(|child| child.name == "PropertyTemplate")
else {
continue;
};
let Some(FbxProperty::String(class)) = template.properties.first() else {
continue;
};
let Some(properties) = template
.children
.iter()
.find(|child| child.name == "Properties70")
else {
continue;
};
by_object_type
.entry(type_name.as_str())
.or_insert((class.as_str(), properties));
}
}
Self { by_object_type }
}
pub(crate) fn for_object(&self, object: &FbxNode) -> Option<&'a FbxNode> {
let (class, properties) = self.by_object_type.get(object.name.as_str())?;
if object.name == "NodeAttribute" {
let object_class = match object.properties.get(2) {
Some(FbxProperty::String(class)) => class.as_str(),
_ => return None,
};
if !attribute_class_matches(class, object_class) {
return None;
}
}
Some(properties)
}
}
fn attribute_class_matches(template_class: &str, object_class: &str) -> bool {
match template_class {
"FbxCamera" => object_class == "Camera",
"FbxLight" => object_class == "Light",
"FbxSkeleton" => matches!(object_class, "LimbNode" | "Limb" | "Root"),
"FbxNull" => object_class == "Null",
"FbxLODGroup" => object_class == "LodGroup",
_ => false,
}
}
#[derive(Clone, Copy)]
pub(crate) struct ObjectProperties<'a> {
object: &'a FbxNode,
template: Option<&'a FbxNode>,
}
impl<'a> ObjectProperties<'a> {
pub(crate) fn new(object: &'a FbxNode, templates: &PropertyTemplates<'a>) -> Self {
Self {
object,
template: templates.for_object(object),
}
}
pub(crate) fn node(&self) -> &'a FbxNode {
self.object
}
pub(crate) fn template(&self) -> Option<&'a FbxNode> {
self.template
}
pub(crate) fn get(&self, name: &str) -> Option<&'a FbxNode> {
self.object
.children
.iter()
.filter(|child| child.name == "Properties70")
.find_map(|properties| find_property(properties, name))
.or_else(|| self.template.and_then(|block| find_property(block, name)))
}
}
pub(crate) fn find_property<'a>(properties70: &'a FbxNode, name: &str) -> Option<&'a FbxNode> {
properties70.children.iter().find(|entry| {
entry.name == "P"
&& matches!(entry.properties.first(), Some(FbxProperty::String(key)) if key == name)
})
}
#[cfg(test)]
mod tests {
use super::*;
fn property(name: &str, value: f64) -> FbxNode {
FbxNode {
name: "P".to_string(),
properties: vec![
FbxProperty::String(name.to_string()),
FbxProperty::String(String::new()),
FbxProperty::String(String::new()),
FbxProperty::String(String::new()),
FbxProperty::F64(value),
],
children: Vec::new(),
}
}
fn properties70(entries: Vec<FbxNode>) -> FbxNode {
FbxNode {
name: "Properties70".to_string(),
properties: Vec::new(),
children: entries,
}
}
fn definitions(object_type: &str, class: &str, entries: Vec<FbxNode>) -> FbxNode {
FbxNode {
name: "Definitions".to_string(),
properties: Vec::new(),
children: vec![FbxNode {
name: "ObjectType".to_string(),
properties: vec![FbxProperty::String(object_type.to_string())],
children: vec![FbxNode {
name: "PropertyTemplate".to_string(),
properties: vec![FbxProperty::String(class.to_string())],
children: vec![properties70(entries)],
}],
}],
}
}
fn object(name: &str, class: &str, entries: Vec<FbxNode>) -> FbxNode {
FbxNode {
name: name.to_string(),
properties: vec![
FbxProperty::I64(1),
FbxProperty::String("Thing".to_string()),
FbxProperty::String(class.to_string()),
],
children: vec![properties70(entries)],
}
}
fn value_of(entry: &FbxNode) -> f64 {
match entry.properties.get(4) {
Some(FbxProperty::F64(value)) => *value,
other => panic!("expected an f64 value, got {other:?}"),
}
}
#[test]
fn an_object_overrides_the_class_default() {
let nodes = vec![definitions(
"Model",
"FbxNode",
vec![property("Lcl Translation", 0.0)],
)];
let templates = PropertyTemplates::build(&nodes);
let model = object("Model", "Mesh", vec![property("Lcl Translation", 7.5)]);
let properties = ObjectProperties::new(&model, &templates);
assert_eq!(
value_of(properties.get("Lcl Translation").expect("present")),
7.5
);
}
#[test]
fn a_class_default_fills_in_what_the_object_leaves_out() {
let nodes = vec![definitions(
"Model",
"FbxNode",
vec![property("RotationOrder", 2.0)],
)];
let templates = PropertyTemplates::build(&nodes);
let model = object("Model", "Mesh", Vec::new());
let properties = ObjectProperties::new(&model, &templates);
assert_eq!(
value_of(properties.get("RotationOrder").expect("from the template")),
2.0
);
assert!(properties.get("Lcl Scaling").is_none());
}
#[test]
fn an_attribute_template_applies_only_to_its_own_class() {
let nodes = vec![definitions(
"NodeAttribute",
"FbxCamera",
vec![property("FocalLength", 34.893)],
)];
let templates = PropertyTemplates::build(&nodes);
let camera = object("NodeAttribute", "Camera", Vec::new());
assert_eq!(
value_of(
ObjectProperties::new(&camera, &templates)
.get("FocalLength")
.expect("the camera template applies to a camera")
),
34.893
);
let light = object("NodeAttribute", "Light", Vec::new());
assert!(
ObjectProperties::new(&light, &templates)
.get("FocalLength")
.is_none(),
"a camera's focal length must not reach a light"
);
}
#[test]
fn a_skeleton_template_applies_to_a_limb_node() {
let nodes = vec![definitions(
"NodeAttribute",
"FbxSkeleton",
vec![property("Size", 33.0)],
)];
let templates = PropertyTemplates::build(&nodes);
for class in ["LimbNode", "Limb", "Root"] {
let limb = object("NodeAttribute", class, Vec::new());
assert!(
ObjectProperties::new(&limb, &templates)
.get("Size")
.is_some(),
"FbxSkeleton must apply to {class}"
);
}
let null = object("NodeAttribute", "Null", Vec::new());
assert!(ObjectProperties::new(&null, &templates)
.get("Size")
.is_none());
}
#[test]
fn a_model_template_applies_to_every_model_class() {
let nodes = vec![definitions(
"Model",
"FbxNode",
vec![property("InheritType", 1.0)],
)];
let templates = PropertyTemplates::build(&nodes);
for class in ["Mesh", "LimbNode", "Camera", "Null", "IKEffector"] {
let model = object("Model", class, Vec::new());
assert!(
ObjectProperties::new(&model, &templates)
.get("InheritType")
.is_some(),
"the FbxNode template must apply to Model::{class}"
);
}
}
#[test]
fn a_document_without_definitions_reads_as_before() {
let templates = PropertyTemplates::build(&[]);
let model = object("Model", "Mesh", vec![property("Lcl Scaling", 2.0)]);
let properties = ObjectProperties::new(&model, &templates);
assert_eq!(
value_of(properties.get("Lcl Scaling").expect("present")),
2.0
);
assert!(properties.get("RotationOrder").is_none());
assert!(properties.template().is_none());
}
}