mittens_engine/engine/ecs/component/
pose_capture.rs1use crate::engine::ecs::ComponentId;
2use crate::engine::ecs::component::Component;
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
6pub enum PoseCaptureTargetMode {
7 WholeSubtree,
8 SkinnedJointsOnly,
9 NamedRoot { selector_or_name: String },
10}
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct PoseCaptureComponent {
14 pub label: Option<String>,
15 pub target_mode: PoseCaptureTargetMode,
16 pub include_scale: bool,
17 pub store_rest_deltas: bool,
18 #[serde(skip)]
19 component: Option<ComponentId>,
20}
21
22impl PoseCaptureComponent {
23 pub fn new() -> Self {
24 Self {
25 label: None,
26 target_mode: PoseCaptureTargetMode::WholeSubtree,
27 include_scale: true,
28 store_rest_deltas: false,
29 component: None,
30 }
31 }
32
33 pub fn with_label(mut self, label: impl Into<String>) -> Self {
34 self.label = Some(label.into());
35 self
36 }
37}
38
39impl Component for PoseCaptureComponent {
40 fn name(&self) -> &'static str {
41 "pose_capture"
42 }
43
44 fn set_id(&mut self, component: ComponentId) {
45 self.component = Some(component);
46 }
47
48 fn as_any(&self) -> &dyn std::any::Any {
49 self
50 }
51
52 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
53 self
54 }
55
56 fn to_mms_ast(
57 &self,
58 _world: &crate::engine::ecs::World,
59 ) -> crate::scripting::ast::ComponentExpression {
60 use crate::engine::ecs::component::ce_helpers::*;
61 let mut ce = ce_call("PoseCapture", "new", vec![]);
62 if let Some(label) = &self.label {
63 ce = ce.with_call("with_label", vec![s(label)]);
64 }
65 ce
66 }
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub enum PoseTargetRef {
71 Query(String),
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct PoseBoneEntry {
76 pub query: String,
78 pub translation: [f32; 3],
79 pub rotation: [f32; 4],
80 pub scale: [f32; 3],
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct PoseCapturePoseComponent {
85 pub name: String,
86 pub target_root_ref: PoseTargetRef,
87 pub entries: Vec<PoseBoneEntry>,
88 #[serde(skip)]
89 component: Option<ComponentId>,
90}
91
92impl PoseCapturePoseComponent {
93 pub fn new(
94 name: impl Into<String>,
95 target_root_ref: PoseTargetRef,
96 entries: Vec<PoseBoneEntry>,
97 ) -> Self {
98 let mut pose = Self {
99 name: name.into(),
100 target_root_ref,
101 entries: Vec::with_capacity(entries.len()),
102 component: None,
103 };
104 for entry in entries {
105 assert!(
108 pose.push_joint(entry).is_ok(),
109 "duplicate joint query in pose"
110 );
111 }
112 pose
113 }
114
115 pub fn push_joint(&mut self, entry: PoseBoneEntry) -> Result<&mut Self, String> {
116 if self
117 .entries
118 .iter()
119 .any(|existing| existing.query == entry.query)
120 {
121 return Err(format!("duplicate joint query '{}'", entry.query));
122 }
123 self.entries.push(entry);
124 Ok(self)
125 }
126}
127
128impl Component for PoseCapturePoseComponent {
129 fn name(&self) -> &'static str {
130 "pose_capture_pose"
131 }
132
133 fn set_id(&mut self, component: ComponentId) {
134 self.component = Some(component);
135 }
136
137 fn as_any(&self) -> &dyn std::any::Any {
138 self
139 }
140
141 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
142 self
143 }
144
145 fn to_mms_ast(
146 &self,
147 _world: &crate::engine::ecs::World,
148 ) -> crate::scripting::ast::ComponentExpression {
149 use crate::engine::ecs::component::ce_helpers::*;
150 let mut ce = ce_call("PoseCapturePose", "new", vec![s(&self.name)]);
151 for entry in &self.entries {
152 ce = ce.with_call(
153 "joint",
154 vec![
155 s(&entry.query),
156 array(nums(entry.translation.map(f64::from))),
157 array(nums(entry.rotation.map(f64::from))),
158 array(nums(entry.scale.map(f64::from))),
159 ],
160 );
161 }
162 ce
163 }
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct PoseCaptureLibraryComponent {
168 pub target_root_ref: PoseTargetRef,
169 #[serde(skip)]
170 component: Option<ComponentId>,
171}
172
173impl PoseCaptureLibraryComponent {
174 pub fn new(target_root_ref: PoseTargetRef) -> Self {
175 Self {
176 target_root_ref,
177 component: None,
178 }
179 }
180}
181
182impl Component for PoseCaptureLibraryComponent {
183 fn name(&self) -> &'static str {
184 "pose_capture_library"
185 }
186
187 fn set_id(&mut self, component: ComponentId) {
188 self.component = Some(component);
189 }
190
191 fn as_any(&self) -> &dyn std::any::Any {
192 self
193 }
194
195 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
196 self
197 }
198
199 fn to_mms_ast(
200 &self,
201 _world: &crate::engine::ecs::World,
202 ) -> crate::scripting::ast::ComponentExpression {
203 use crate::engine::ecs::component::ce_helpers::*;
204 ce_call("PoseCaptureLibrary", "new", vec![])
205 }
206}
207
208pub fn save_pose_asset(
210 world: &crate::engine::ecs::World,
211 pose_id: ComponentId,
212 path: &std::path::Path,
213) -> Result<(), String> {
214 let pose = world
215 .get_component_by_id_as::<PoseCapturePoseComponent>(pose_id)
216 .ok_or_else(|| format!("component {pose_id:?} is not a pose"))?;
217 let expression = crate::scripting::unparser::unparse_component(&pose.to_mms_ast(world));
218 let text = format!("export fn pose() {{\n return {expression}\n}}\n");
219 write_asset_atomically(path, &text)
220}
221
222pub fn save_pose_library_asset(
225 world: &crate::engine::ecs::World,
226 library_id: ComponentId,
227 manifest_path: &std::path::Path,
228) -> Result<Vec<std::path::PathBuf>, String> {
229 if world
230 .get_component_by_id_as::<PoseCaptureLibraryComponent>(library_id)
231 .is_none()
232 {
233 return Err(format!("component {library_id:?} is not a pose library"));
234 }
235 let parent = manifest_path.parent().unwrap_or(std::path::Path::new("."));
236 let stem = manifest_path
237 .file_stem()
238 .and_then(|value| value.to_str())
239 .unwrap_or("poses");
240 let mut paths = Vec::new();
241 for &child in world.children_of(library_id) {
242 let Some(pose) = world.get_component_by_id_as::<PoseCapturePoseComponent>(child) else {
243 continue;
244 };
245 let slug: String = pose
246 .name
247 .chars()
248 .map(|ch| {
249 if ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' {
250 ch
251 } else {
252 '_'
253 }
254 })
255 .collect();
256 let path = parent.join(format!("{stem}.{:03}-{slug}.pose.mms", paths.len()));
257 save_pose_asset(world, child, &path)?;
258 paths.push(path);
259 }
260
261 let mut manifest = String::new();
262 for (index, path) in paths.iter().enumerate() {
263 let relative = path
264 .file_name()
265 .and_then(|value| value.to_str())
266 .ok_or_else(|| format!("pose asset path is not valid UTF-8: {}", path.display()))?;
267 manifest.push_str(&format!(
268 "import {{ pose as pose_{index} }} from \"{}\"\n",
269 relative.replace('\\', "\\\\").replace('"', "\\\"")
270 ));
271 }
272 manifest.push_str("\nPoseCaptureLibrary.new() {\n");
273 for index in 0..paths.len() {
274 manifest.push_str(&format!(" pose_{index}()\n"));
275 }
276 manifest.push_str("}\n");
277 write_asset_atomically(manifest_path, &manifest)?;
278 Ok(paths)
279}
280
281fn write_asset_atomically(path: &std::path::Path, text: &str) -> Result<(), String> {
282 if let Some(parent) = path.parent() {
283 std::fs::create_dir_all(parent)
284 .map_err(|error| format!("cannot create {}: {error}", parent.display()))?;
285 }
286 let tmp = path.with_extension(format!(
287 "{}tmp",
288 path.extension()
289 .and_then(|value| value.to_str())
290 .unwrap_or("")
291 ));
292 std::fs::write(&tmp, text)
293 .map_err(|error| format!("cannot write {}: {error}", tmp.display()))?;
294 std::fs::rename(&tmp, path)
295 .map_err(|error| format!("cannot replace {}: {error}", path.display()))
296}