mittens_engine/engine/ecs/system/
pose_capture_system.rs1use 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 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 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 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 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}