Skip to main content

mittens_engine/engine/ecs/system/
pose_capture_system.rs

1use crate::engine::ecs::component::{
2    EditorComponent, PoseBoneEntry, PoseCaptureComponent, PoseCaptureLibraryComponent,
3    PoseCapturePoseComponent, PoseTargetRef, TransformComponent,
4};
5use crate::engine::ecs::rx::SignalEmitter;
6use crate::engine::ecs::{ComponentId, IntentValue, World};
7use std::collections::HashSet;
8
9#[derive(Debug, Default)]
10pub struct PoseCaptureSystem;
11
12impl PoseCaptureSystem {
13    pub fn new() -> Self {
14        Self
15    }
16
17    pub fn handle_capture(
18        &self,
19        world: &mut World,
20        emit: &mut dyn SignalEmitter,
21        request_target: ComponentId,
22        pose_name: Option<String>,
23    ) {
24        let targets = self.resolve_capture_targets(world, request_target);
25        if targets.is_empty() {
26            println!(
27                "[PoseCaptureSystem] no PoseCaptureComponent targets resolved from {:?}",
28                request_target
29            );
30            return;
31        }
32
33        for capture_target in targets {
34            let pose_id =
35                match self.capture_target_pose(world, emit, capture_target, pose_name.clone()) {
36                    Some(pose_id) => pose_id,
37                    None => continue,
38                };
39
40            use crate::engine::ecs::EventSignal;
41            // Publish completion from the request origin. Editor panel handlers are
42            // scoped to their panel tree; publishing from the captured GLTF target
43            // leaves that tree and the new pose row never gets projected.
44            emit.push_event(
45                request_target,
46                EventSignal::DataEvent {
47                    name: "pose_captured".to_string(),
48                    payload: Some(pose_id),
49                },
50            );
51        }
52    }
53
54    pub fn handle_apply(
55        &self,
56        world: &mut World,
57        emit: &mut dyn SignalEmitter,
58        target: ComponentId,
59        pose_id: ComponentId,
60    ) {
61        let Some(pose) = world.get_component_by_id_as::<PoseCapturePoseComponent>(pose_id) else {
62            println!(
63                "[PoseCaptureSystem] pose {:?} has no PoseCapturePoseComponent",
64                pose_id
65            );
66            return;
67        };
68
69        println!(
70            "[PoseCaptureSystem] applying pose '{}' to target {:?}",
71            pose.name, target
72        );
73
74        // Resolution is a validation phase. Do not enqueue any mutations unless
75        // every query resolves uniquely inside this particular GLTF instance.
76        let Some(gltf_id) = self.owning_gltf(world, target) else {
77            println!("[PoseCaptureSystem] target {:?} has no owning GLTF", target);
78            return;
79        };
80        let mut resolved = Vec::with_capacity(pose.entries.len());
81        for entry in &pose.entries {
82            let matches = self.resolve_joint_query(world, gltf_id, &entry.query);
83            if matches.len() != 1 {
84                println!(
85                    "[PoseCaptureSystem] joint query '{}' resolved {} times; pose not applied",
86                    entry.query,
87                    matches.len()
88                );
89                return;
90            }
91            resolved.push((matches[0], entry));
92        }
93        for (tc_id, entry) in resolved {
94            emit.push_intent_now(
95                tc_id,
96                IntentValue::UpdateTransform {
97                    component_ids: vec![tc_id],
98                    translation: entry.translation,
99                    rotation_quat_xyzw: entry.rotation,
100                    scale: entry.scale,
101                },
102            );
103        }
104    }
105
106    fn ensure_library(
107        &self,
108        world: &mut World,
109        target: ComponentId,
110        emit: &mut dyn SignalEmitter,
111    ) -> ComponentId {
112        // Find existing library as child of target
113        for &child in world.children_of(target) {
114            if world
115                .get_component_by_id_as::<PoseCaptureLibraryComponent>(child)
116                .is_some()
117            {
118                return child;
119            }
120        }
121
122        // Create new library
123        let library = PoseCaptureLibraryComponent::new(PoseTargetRef::Query("TODO".to_string()));
124        let library_id = world.add_component(library);
125        let _ = world.add_child(target, library_id);
126        world.init_component_tree(library_id, emit);
127        library_id
128    }
129
130    fn capture_target_pose(
131        &self,
132        world: &mut World,
133        emit: &mut dyn SignalEmitter,
134        target: ComponentId,
135        pose_name: Option<String>,
136    ) -> Option<ComponentId> {
137        if world
138            .get_component_by_id_as::<PoseCaptureComponent>(target)
139            .is_none()
140        {
141            return None;
142        }
143
144        let library_id = self.ensure_library(world, target, emit);
145        let gltf_id = self.owning_gltf(world, target)?;
146        let transforms = world
147            .get_component_by_id_as::<crate::engine::ecs::component::GLTFComponent>(gltf_id)?
148            .armature_joint_transforms
149            .clone();
150        let mut entries = Vec::new();
151        let mut queries = HashSet::new();
152        for tc_id in transforms {
153            if let Some(tc) = world.get_component_by_id_as::<TransformComponent>(tc_id) {
154                if let Some(label) = world
155                    .component_label(tc_id)
156                    .filter(|label| !label.is_empty())
157                {
158                    let query = format!("#{label}");
159                    if !queries.insert(query.clone()) {
160                        println!(
161                            "[PoseCaptureSystem] duplicate captured joint query '{query}'; pose not captured"
162                        );
163                        return None;
164                    }
165                    entries.push(PoseBoneEntry {
166                        query,
167                        translation: tc.transform.translation,
168                        rotation: tc.transform.rotation,
169                        scale: tc.transform.scale,
170                    });
171                }
172            }
173        }
174
175        let existing_pose_count = world
176            .children_of(library_id)
177            .iter()
178            .filter(|&&child| {
179                world
180                    .get_component_by_id_as::<PoseCapturePoseComponent>(child)
181                    .is_some()
182            })
183            .count();
184        let name = pose_name.unwrap_or_else(|| format!("pose_{existing_pose_count}"));
185        println!(
186            "[PoseCaptureSystem] capturing pose '{}' for target {:?} with {} entries",
187            name,
188            target,
189            entries.len()
190        );
191
192        let pose =
193            PoseCapturePoseComponent::new(name, PoseTargetRef::Query("TODO".to_string()), entries);
194        let pose_id = world.add_component(pose);
195        if let Err(e) = world.add_child(library_id, pose_id) {
196            println!("[PoseCaptureSystem] failed to add pose to library: {}", e);
197            return None;
198        }
199        world.init_component_tree(pose_id, emit);
200        Some(pose_id)
201    }
202
203    fn resolve_capture_targets(
204        &self,
205        world: &World,
206        request_target: ComponentId,
207    ) -> Vec<ComponentId> {
208        let selected_targets = self.selected_pose_capture_targets(world);
209        if !selected_targets.is_empty() {
210            return selected_targets;
211        }
212
213        if let Some(target) = self.pose_capture_ancestor(world, request_target) {
214            return vec![target];
215        }
216
217        world
218            .all_components()
219            .filter(|&id| {
220                world
221                    .get_component_by_id_as::<PoseCaptureComponent>(id)
222                    .is_some()
223            })
224            .collect()
225    }
226
227    fn selected_pose_capture_targets(&self, world: &World) -> Vec<ComponentId> {
228        let mut seen = HashSet::new();
229        let mut targets = Vec::new();
230        for id in world.all_components() {
231            let Some(editor) = world.get_component_by_id_as::<EditorComponent>(id) else {
232                continue;
233            };
234            let Some(selected) = editor.selected else {
235                continue;
236            };
237            let Some(target) = self.pose_capture_ancestor(world, selected) else {
238                continue;
239            };
240            if seen.insert(target) {
241                targets.push(target);
242            }
243        }
244        targets
245    }
246
247    fn pose_capture_ancestor(&self, world: &World, start: ComponentId) -> Option<ComponentId> {
248        let mut current = Some(start);
249        while let Some(id) = current {
250            if world
251                .get_component_by_id_as::<PoseCaptureComponent>(id)
252                .is_some()
253            {
254                return Some(id);
255            }
256            current = world.parent_of(id);
257        }
258        None
259    }
260
261    fn owning_gltf(&self, world: &World, start: ComponentId) -> Option<ComponentId> {
262        let mut current = Some(start);
263        while let Some(id) = current {
264            if world
265                .get_component_by_id_as::<crate::engine::ecs::component::GLTFComponent>(id)
266                .is_some()
267            {
268                return Some(id);
269            }
270            current = world.parent_of(id);
271        }
272        None
273    }
274
275    fn resolve_joint_query(
276        &self,
277        world: &World,
278        gltf_id: ComponentId,
279        query: &str,
280    ) -> Vec<ComponentId> {
281        let Some(gltf) =
282            world.get_component_by_id_as::<crate::engine::ecs::component::GLTFComponent>(gltf_id)
283        else {
284            return Vec::new();
285        };
286        let query_root = world.parent_of(gltf_id).unwrap_or(gltf_id);
287        let owned: HashSet<ComponentId> = gltf.armature_joint_transforms.iter().copied().collect();
288        world
289            .find_all_components(query_root, query)
290            .into_iter()
291            .filter(|id| owned.contains(id))
292            .collect()
293    }
294}