Skip to main content

mittens_engine/engine/ecs/component/
secondary_motion.rs

1use super::{Component, ComponentRef, ce_helpers::*};
2use crate::engine::ecs::ComponentId;
3use crate::scripting::ast::Expression;
4
5fn ref_expr(value: &ComponentRef) -> Expression {
6    match value {
7        ComponentRef::Guid(guid) => Expression::String(format!("@uuid:{guid}")),
8        ComponentRef::Query(query) => Expression::String(query.clone()),
9    }
10}
11
12fn ref_surface(value: &ComponentRef) -> String {
13    match value {
14        ComponentRef::Guid(guid) => format!("@uuid:{guid}"),
15        ComponentRef::Query(query) => query.clone(),
16    }
17}
18
19#[derive(Debug, Clone, Default)]
20pub struct SecondaryMotionComponent {
21    component: Option<ComponentId>,
22}
23impl SecondaryMotionComponent {
24    pub fn new() -> Self {
25        Self::default()
26    }
27}
28impl Component for SecondaryMotionComponent {
29    fn name(&self) -> &'static str {
30        "secondary_motion"
31    }
32    fn set_id(&mut self, id: ComponentId) {
33        self.component = Some(id);
34    }
35    fn as_any(&self) -> &dyn std::any::Any {
36        self
37    }
38    fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
39        self
40    }
41    fn to_mms_ast(
42        &self,
43        _world: &crate::engine::ecs::World,
44    ) -> crate::scripting::ast::ComponentExpression {
45        ce("SecondaryMotion")
46    }
47}
48
49#[derive(Debug, Clone)]
50pub struct SpringBoneComponent {
51    pub stable_name: String,
52    pub center: Option<ComponentRef>,
53    pub enabled: bool,
54    pub virtual_end_length_ratio: Option<f32>,
55    component: Option<ComponentId>,
56}
57impl SpringBoneComponent {
58    pub fn new(name: impl Into<String>) -> Self {
59        Self {
60            stable_name: name.into(),
61            center: None,
62            enabled: true,
63            virtual_end_length_ratio: None,
64            component: None,
65        }
66    }
67    pub fn center(mut self, target: ComponentRef) -> Self {
68        self.center = Some(target);
69        self
70    }
71    pub fn enabled(mut self, enabled: bool) -> Self {
72        self.enabled = enabled;
73        self
74    }
75    pub fn virtual_end_length_ratio(mut self, ratio: f32) -> Self {
76        self.virtual_end_length_ratio = Some(ratio.max(0.0));
77        self
78    }
79}
80impl Component for SpringBoneComponent {
81    fn name(&self) -> &'static str {
82        "spring_bone"
83    }
84    fn set_id(&mut self, id: ComponentId) {
85        self.component = Some(id);
86    }
87    fn as_any(&self) -> &dyn std::any::Any {
88        self
89    }
90    fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
91        self
92    }
93    fn to_mms_ast(
94        &self,
95        _world: &crate::engine::ecs::World,
96    ) -> crate::scripting::ast::ComponentExpression {
97        let mut out = ce_call("SpringBone", "new", vec![s(&self.stable_name)]);
98        if let Some(target) = &self.center {
99            out = out.with_call("center", vec![ref_expr(target)]);
100        }
101        if !self.enabled {
102            out = out.with_call("enabled", vec![b(false)]);
103        }
104        if let Some(r) = self.virtual_end_length_ratio {
105            out = out.with_call("virtual_end_length_ratio", vec![num(r as f64)]);
106        }
107        out
108    }
109}
110
111#[derive(Debug, Clone)]
112pub struct SpringJointComponent {
113    pub node: ComponentRef,
114    pub stiffness: f32,
115    pub drag_force: f32,
116    pub gravity_power: f32,
117    pub gravity_dir: [f32; 3],
118    component: Option<ComponentId>,
119}
120impl SpringJointComponent {
121    pub fn new(node: ComponentRef) -> Self {
122        Self {
123            node,
124            stiffness: 1.0,
125            drag_force: 0.4,
126            gravity_power: 0.0,
127            gravity_dir: [0.0, -1.0, 0.0],
128            component: None,
129        }
130    }
131    pub fn query(selector: impl Into<String>) -> Self {
132        Self::new(ComponentRef::Query(selector.into()))
133    }
134    pub fn stiffness(mut self, value: f32) -> Self {
135        self.stiffness = value.max(0.0);
136        self
137    }
138    pub fn drag_force(mut self, value: f32) -> Self {
139        self.drag_force = value.clamp(0.0, 1.0);
140        self
141    }
142    pub fn gravity(mut self, power: f32, direction: [f32; 3]) -> Self {
143        self.gravity_power = power;
144        self.gravity_dir = direction;
145        self
146    }
147}
148impl Component for SpringJointComponent {
149    fn name(&self) -> &'static str {
150        "spring_joint"
151    }
152    fn set_id(&mut self, id: ComponentId) {
153        self.component = Some(id);
154    }
155    fn as_any(&self) -> &dyn std::any::Any {
156        self
157    }
158    fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
159        self
160    }
161    fn to_mms_ast(
162        &self,
163        _world: &crate::engine::ecs::World,
164    ) -> crate::scripting::ast::ComponentExpression {
165        ce_call("SpringJoint", "new", vec![ref_expr(&self.node)])
166            .with_call("stiffness", vec![num(self.stiffness as f64)])
167            .with_call("drag_force", vec![num(self.drag_force as f64)])
168            .with_call(
169                "gravity",
170                vec![
171                    num(self.gravity_power as f64),
172                    num(self.gravity_dir[0] as f64),
173                    num(self.gravity_dir[1] as f64),
174                    num(self.gravity_dir[2] as f64),
175                ],
176            )
177    }
178}
179
180pub const GENERATED_SIDECAR_MARKER: &str = "// @generated by cat-engine secondary motion\n";
181
182/// Export the authored metadata below one GLTF instance to `<asset>.mms`.
183/// Existing hand-authored modules are deliberately never overwritten.
184pub fn export_secondary_motion_sidecar(
185    world: &crate::engine::ecs::World,
186    gltf_id: ComponentId,
187) -> Result<std::path::PathBuf, String> {
188    let gltf = world
189        .get_component_by_id_as::<super::GLTFComponent>(gltf_id)
190        .ok_or("target is not a GLTF component")?;
191    let metadata = world
192        .children_of(gltf_id)
193        .iter()
194        .copied()
195        .find(|id| {
196            world
197                .get_component_by_id_as::<SecondaryMotionComponent>(*id)
198                .is_some()
199        })
200        .ok_or("GLTF has no SecondaryMotion child")?;
201    let path = std::path::PathBuf::from(format!("{}.mms", gltf.uri));
202    if let Ok(old) = std::fs::read_to_string(&path) {
203        if !old.starts_with(GENERATED_SIDECAR_MARKER) {
204            return Err(format!(
205                "refusing to overwrite hand-authored module '{}'",
206                path.display()
207            ));
208        }
209    }
210    fn q(value: &str) -> String {
211        format!("\"{}\"", value.replace('\\', "\\\\").replace('"', "\\\""))
212    }
213    let mut text = String::from(GENERATED_SIDECAR_MARKER);
214    text.push_str("export fn secondary_motion() {\n    return SecondaryMotion {\n");
215    for chain_id in world.children_of(metadata) {
216        let Some(chain) = world.get_component_by_id_as::<SpringBoneComponent>(*chain_id) else {
217            continue;
218        };
219        text.push_str(&format!(
220            "        SpringBone.new({})",
221            q(&chain.stable_name)
222        ));
223        if let Some(center) = &chain.center {
224            text.push_str(&format!(".center({})", q(&ref_surface(center))));
225        }
226        if !chain.enabled {
227            text.push_str(".enabled(false)");
228        }
229        if let Some(r) = chain.virtual_end_length_ratio {
230            text.push_str(&format!(".virtual_end_length_ratio({r})"));
231        }
232        text.push_str(" {\n");
233        for joint_id in world.children_of(*chain_id) {
234            let Some(j) = world.get_component_by_id_as::<SpringJointComponent>(*joint_id) else {
235                continue;
236            };
237            text.push_str(&format!("            SpringJoint.new({}).stiffness({}).drag_force({}).gravity({}, [{}, {}, {}])\n",q(&ref_surface(&j.node)),j.stiffness,j.drag_force,j.gravity_power,j.gravity_dir[0],j.gravity_dir[1],j.gravity_dir[2]));
238        }
239        text.push_str("        }\n");
240    }
241    text.push_str("    }\n}\n");
242    let parent = path.parent().unwrap_or(std::path::Path::new("."));
243    std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
244    let tmp = path.with_extension(format!(
245        "{}tmp",
246        path.extension().and_then(|s| s.to_str()).unwrap_or("mms.")
247    ));
248    std::fs::write(&tmp, text).map_err(|e| e.to_string())?;
249    std::fs::rename(&tmp, &path).map_err(|e| e.to_string())?;
250    Ok(path)
251}