1use crate::commands::{CommandQueue, ScriptCommand};
6use gizmo_math::Vec3;
7use mlua::prelude::*;
8use std::sync::Arc;
9
10const MIN_COLLIDER_DIM: f32 = 1e-4;
13
14fn sanitize_dim(v: f32) -> f32 {
17 if v.is_finite() && v > MIN_COLLIDER_DIM {
18 v
19 } else {
20 MIN_COLLIDER_DIM
21 }
22}
23
24pub fn register_physics_api(lua: &Lua, command_queue: Arc<CommandQueue>) -> Result<(), LuaError> {
26 let physics_table = lua.create_table()?;
27
28 {
30 let cq = command_queue.clone();
31 physics_table.set(
32 "apply_force",
33 lua.create_function(move |_, (id, fx, fy, fz): (u32, f32, f32, f32)| {
34 cq.push(ScriptCommand::ApplyForce(id, Vec3::new(fx, fy, fz)));
35 Ok(())
36 })?,
37 )?;
38 }
39
40 {
42 let cq = command_queue.clone();
43 physics_table.set(
44 "apply_impulse",
45 lua.create_function(move |_, (id, ix, iy, iz): (u32, f32, f32, f32)| {
46 cq.push(ScriptCommand::ApplyImpulse(id, Vec3::new(ix, iy, iz)));
47 Ok(())
48 })?,
49 )?;
50 }
51
52 {
54 let cq = command_queue.clone();
55 physics_table.set(
56 "add_rigidbody",
57 lua.create_function(
58 move |_, (id, mass, use_gravity): (u32, f32, bool)| {
61 cq.push(ScriptCommand::AddRigidBody {
62 id,
63 mass,
64 use_gravity,
65 });
66 Ok(())
67 },
68 )?,
69 )?;
70 }
71
72 {
74 let cq = command_queue.clone();
75 physics_table.set(
76 "add_box_collider",
77 lua.create_function(move |_, (id, hx, hy, hz): (u32, f32, f32, f32)| {
78 cq.push(ScriptCommand::AddBoxCollider {
79 id,
80 hx: sanitize_dim(hx),
81 hy: sanitize_dim(hy),
82 hz: sanitize_dim(hz),
83 });
84 Ok(())
85 })?,
86 )?;
87 }
88
89 {
90 let cq = command_queue.clone();
91 physics_table.set(
92 "add_sphere_collider",
93 lua.create_function(move |_, (id, radius): (u32, f32)| {
94 cq.push(ScriptCommand::AddSphereCollider {
95 id,
96 radius: sanitize_dim(radius),
97 });
98 Ok(())
99 })?,
100 )?;
101 }
102
103 lua.globals().set("physics", physics_table)?;
104
105 Ok(())
106}
107
108pub fn update_physics_api(
110 lua: &Lua,
111 world: &gizmo_core::World,
112) -> Result<(), LuaError> {
113 let physics_table: LuaTable = lua.globals().get("physics")?;
114
115 let triggers = lua.create_table()?;
116 let collisions = lua.create_table()?;
117
118 if let Ok(physics_world) = world.try_get_resource::<gizmo_physics_rigid::world::PhysicsWorld>() {
119 for (i, t_event) in physics_world.trigger_events().iter().enumerate() {
121 let ev = lua.create_table()?;
122 ev.set("trigger_id", t_event.trigger_entity.id())?;
123 ev.set("other_id", t_event.other_entity.id())?;
124 let status = match t_event.event_type {
125 gizmo_physics_core::collision::CollisionEventType::Started => "enter",
126 gizmo_physics_core::collision::CollisionEventType::Persisting => "stay",
127 gizmo_physics_core::collision::CollisionEventType::Ended => "exit",
128 };
129 ev.set("status", status)?;
130 triggers.set(i + 1, ev)?;
131 }
132
133 for (i, c_event) in physics_world.collision_events().iter().enumerate() {
135 let ev = lua.create_table()?;
136 ev.set("entity_a", c_event.entity_a.id())?;
137 ev.set("entity_b", c_event.entity_b.id())?;
138 let status = match c_event.event_type {
139 gizmo_physics_core::collision::CollisionEventType::Started => "enter",
140 gizmo_physics_core::collision::CollisionEventType::Persisting => "stay",
141 gizmo_physics_core::collision::CollisionEventType::Ended => "exit",
142 };
143 ev.set("status", status)?;
144 collisions.set(i + 1, ev)?;
145 }
146 }
147
148 physics_table.set("triggers", triggers)?;
150 physics_table.set("collisions", collisions)?;
151
152 Ok(())
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158 use gizmo_core::World;
159 use gizmo_physics_rigid::world::PhysicsWorld;
160 use gizmo_physics_core::collision::{TriggerEvent, CollisionEvent, CollisionEventType};
161 use mlua::Lua;
162
163 #[test]
164 fn test_update_physics_api() {
165 let lua = Lua::new();
166 let globals = lua.globals();
167
168 let physics_table = lua.create_table().unwrap();
170 globals.set("physics", physics_table).unwrap();
171
172 let mut world = World::new();
173 let ent1 = world.spawn();
174 let ent2 = world.spawn();
175
176 let mut physics_world = PhysicsWorld::new();
177 physics_world.trigger_events.push(TriggerEvent {
178 trigger_entity: gizmo_physics_core::BodyHandle::from_id(ent1.id()),
179 other_entity: gizmo_physics_core::BodyHandle::from_id(ent2.id()),
180 event_type: CollisionEventType::Started,
181 });
182
183 physics_world.collision_events.push(CollisionEvent {
184 entity_a: gizmo_physics_core::BodyHandle::from_id(ent1.id()),
185 entity_b: gizmo_physics_core::BodyHandle::from_id(ent2.id()),
186 event_type: CollisionEventType::Started,
187 contact_points: Default::default(),
188 });
189
190 world.insert_resource(physics_world);
191
192 update_physics_api(&lua, &world).unwrap();
193
194 let script = r#"
195 local triggers = physics.triggers
196 local collisions = physics.collisions
197 assert(#triggers == 1)
198 assert(triggers[1].status == "enter")
199 assert(#collisions == 1)
200 assert(collisions[1].status == "enter")
201 "#;
202
203 lua.load(script).exec().unwrap();
204 }
205
206 #[test]
207 fn sanitize_dim_rejects_bad_values() {
208 assert_eq!(sanitize_dim(f32::NAN), MIN_COLLIDER_DIM);
209 assert_eq!(sanitize_dim(f32::INFINITY), MIN_COLLIDER_DIM);
210 assert_eq!(sanitize_dim(f32::NEG_INFINITY), MIN_COLLIDER_DIM);
211 assert_eq!(sanitize_dim(-5.0), MIN_COLLIDER_DIM);
212 assert_eq!(sanitize_dim(0.0), MIN_COLLIDER_DIM);
213 assert_eq!(sanitize_dim(2.5), 2.5); }
215
216 #[test]
217 fn box_collider_dims_are_sanitized_from_lua() {
218 let lua = Lua::new();
219 let cq = Arc::new(CommandQueue::new());
220 register_physics_api(&lua, cq.clone()).unwrap();
221 lua.load("physics.add_box_collider(1, -5.0, 0/0, 2.0)").exec().unwrap();
223
224 let cmds = cq.drain();
225 assert_eq!(cmds.len(), 1);
226 match &cmds[0] {
227 ScriptCommand::AddBoxCollider { hx, hy, hz, .. } => {
228 assert!(hx.is_finite() && *hx > 0.0, "negative hx must be clamped, got {hx}");
229 assert!(hy.is_finite() && *hy > 0.0, "NaN hy must be clamped, got {hy}");
230 assert_eq!(*hz, 2.0, "valid hz must pass through untouched");
231 }
232 other => panic!("expected AddBoxCollider, got {other:?}"),
233 }
234 }
235
236 #[test]
237 fn sphere_collider_radius_is_sanitized_from_lua() {
238 let lua = Lua::new();
239 let cq = Arc::new(CommandQueue::new());
240 register_physics_api(&lua, cq.clone()).unwrap();
241 lua.load("physics.add_sphere_collider(7, -3.0)").exec().unwrap();
242
243 let cmds = cq.drain();
244 assert_eq!(cmds.len(), 1);
245 match &cmds[0] {
246 ScriptCommand::AddSphereCollider { radius, .. } => {
247 assert!(radius.is_finite() && *radius > 0.0, "radius clamped, got {radius}");
248 }
249 other => panic!("expected AddSphereCollider, got {other:?}"),
250 }
251 }
252}