Skip to main content

mittens_engine/engine/ecs/component/
pose_capture.rs

1use 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    /// Query identifying one joint inside the owning GLTF instance.
77    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            // Keep the long-standing infallible constructor useful to runtime callers,
106            // but never allow duplicate entries into the component.
107            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
208/// Write one pose as an independently importable MMS module.
209pub 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
222/// Save every ordered pose child to its own module, then rewrite the library manifest.
223/// Pose filenames are stable by library order and sanitized pose name.
224pub 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}