mittens_engine/engine/ecs/component/
secondary_motion.rs1use 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
182pub 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}