fbx_dom/objects/
blend_shape_channel.rs1use std::collections::HashMap;
4use std::convert::TryFrom;
5
6use crate::{OwnedDocument, OwnedObject, Property};
7
8use super::{AttrExtractor, FbxObjectTag, FbxTypeMismatch, ShapeGeometry, fbx_object_tag};
9
10const ATTR_DEFORM_PERCENT: &str = "DeformPercent";
11const ATTR_FULL_WEIGHTS: &str = "FullWeights";
12
13#[derive(Debug, PartialEq)]
14pub struct BlendShapeChannel {
15 object: OwnedObject,
16 pub percent: f32,
17 pub full_weights: Vec<f32>,
18}
19
20impl BlendShapeChannel {
21 pub fn inner(&self) -> &OwnedObject {
22 &self.object
23 }
24
25 pub fn into_inner(self) -> OwnedObject {
26 self.object
27 }
28
29 pub fn properties(&self) -> &HashMap<String, Property> {
30 &self.object.properties
31 }
32
33 pub fn property(&self, name: &str) -> Option<&Property> {
34 self.object.properties.get(name)
35 }
36
37 pub fn deform_percent(&self) -> f32 {
38 self.percent
39 }
40
41 pub fn full_weights(&self) -> &[f32] {
42 &self.full_weights
43 }
44
45 pub fn get_shape_geometries<'a>(
47 &'a self,
48 document: &'a OwnedDocument,
49 ) -> Vec<&'a ShapeGeometry> {
50 let channel_id = self.inner().object_index;
51 document
52 .shape_geometries
53 .iter()
54 .filter(|shape| shape.inner().connected_object_ids.contains(&channel_id))
55 .collect()
56 }
57}
58
59impl TryFrom<OwnedObject> for BlendShapeChannel {
60 type Error = FbxTypeMismatch;
61
62 fn try_from(o: OwnedObject) -> Result<Self, Self::Error> {
63 match fbx_object_tag(&o) {
64 FbxObjectTag::BlendShapeChannel => {
65 let percent = o
66 .attributes
67 .extract_case_insensitive(ATTR_DEFORM_PERCENT)
68 .and_then(|a| a.get_tokens().first())
69 .and_then(|t| t.trim().parse::<f32>().ok())
70 .unwrap_or(0.0);
71 let full_weights = o
72 .attributes
73 .extract_case_insensitive(ATTR_FULL_WEIGHTS)
74 .map(|attr| {
75 attr.get_tokens()
76 .iter()
77 .flat_map(|t| t.split(','))
78 .map(|t| t.trim())
79 .filter(|t| !t.is_empty())
80 .filter_map(|t| t.parse::<f32>().ok())
81 .collect::<Vec<f32>>()
82 })
83 .unwrap_or_default();
84 Ok(BlendShapeChannel {
85 object: o,
86 percent,
87 full_weights,
88 })
89 }
90 _ => Err(FbxTypeMismatch::wrong_object_kind(
91 o,
92 "BlendShapeChannel".to_string(),
93 )),
94 }
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use std::collections::HashMap;
101 use std::convert::TryFrom;
102
103 use fbxscii::{ElementAttribute, LeafAttribute};
104
105 use crate::objects::{
106 DEFORMER_BLEND_SHAPE_CHANNEL_CLASS_NAME, DEFORMER_TYPE_NAME, GEOMETRY_SHAPE_CLASS_NAME,
107 GEOMETRY_TYPE_NAME, ShapeGeometry,
108 };
109 use crate::{OwnedDocument, OwnedObject, Property};
110
111 use super::{ATTR_DEFORM_PERCENT, ATTR_FULL_WEIGHTS, BlendShapeChannel};
112
113 fn leaf(tokens: &[&str]) -> ElementAttribute {
114 ElementAttribute::Leaf(Box::new(LeafAttribute {
115 key: String::new(),
116 tokens: tokens.iter().map(|s| (*s).to_string()).collect(),
117 }))
118 }
119
120 #[test]
121 fn parses_percent_and_full_weights() {
122 let mut attrs = HashMap::new();
123 attrs.insert(ATTR_DEFORM_PERCENT.into(), leaf(&["37.5"]));
124 attrs.insert(ATTR_FULL_WEIGHTS.into(), leaf(&["1,0.5,0.25"]));
125 let mut props = HashMap::new();
126 props.insert("Foo".into(), Property::String("bar".into()));
127 let o = OwnedObject {
128 object_index: 14,
129 name: "BlendShapeChannel::A".into(),
130 type_name: DEFORMER_TYPE_NAME.into(),
131 class_name: DEFORMER_BLEND_SHAPE_CHANNEL_CLASS_NAME.into(),
132 properties: props,
133 attributes: attrs,
134 connected_object_ids: vec![],
135 object_property_targets: vec![],
136 pp_property_targets: HashMap::new(),
137 };
138 let c = BlendShapeChannel::try_from(o).unwrap();
139 assert_eq!(c.deform_percent(), 37.5);
140 assert_eq!(c.full_weights(), &[1.0, 0.5, 0.25]);
141 assert_eq!(c.property("Foo"), Some(&Property::String("bar".into())));
142 }
143
144 #[test]
145 fn resolves_shape_geometry_connections() {
146 let channel = BlendShapeChannel::try_from(OwnedObject {
147 object_index: 40,
148 name: "BlendShapeChannel::Conn".into(),
149 type_name: DEFORMER_TYPE_NAME.into(),
150 class_name: DEFORMER_BLEND_SHAPE_CHANNEL_CLASS_NAME.into(),
151 properties: HashMap::new(),
152 attributes: HashMap::new(),
153 connected_object_ids: vec![],
154 object_property_targets: vec![],
155 pp_property_targets: HashMap::new(),
156 })
157 .unwrap();
158
159 let shape = ShapeGeometry::try_from(OwnedObject {
160 object_index: 41,
161 name: "Geometry::Shape".into(),
162 type_name: GEOMETRY_TYPE_NAME.into(),
163 class_name: GEOMETRY_SHAPE_CLASS_NAME.into(),
164 properties: HashMap::new(),
165 attributes: HashMap::from([
166 ("Indexes".to_string(), leaf(&["0"])),
167 ("Vertices".to_string(), leaf(&["0,0,0"])),
168 ]),
169 connected_object_ids: vec![40],
170 object_property_targets: vec![],
171 pp_property_targets: HashMap::new(),
172 })
173 .unwrap();
174
175 let mut owned = OwnedDocument::default();
176 owned.shape_geometries = vec![shape];
177 let linked = channel.get_shape_geometries(&owned);
178 assert_eq!(linked.len(), 1);
179 assert_eq!(linked[0].inner().object_index, 41);
180 }
181}