Skip to main content

gizmo_scripting/
api_physics.rs

1//! Physics API — Lua'ya sunulan fizik sistemi fonksiyonları
2//!
3//! Kuvvet uygulama, raycast ve yerçekimi ayarı gibi işlemler için kullanılır.
4
5use crate::commands::{CommandQueue, ScriptCommand};
6use gizmo_math::Vec3;
7use mlua::prelude::*;
8use std::sync::Arc;
9
10/// Bir collider boyutunun alt sınırı. Script'ten gelen negatif/NaN/sonsuz/sıfır değerler
11/// (yazım hatası) bu değere kelepçelenir — dejenere AABB veya GJK'da NaN üretmesinler.
12const MIN_COLLIDER_DIM: f32 = 1e-4;
13
14/// Collider boyutunu güvene al: sonlu ve pozitif değilse küçük pozitif bir extent'e çek.
15/// `NaN`/`-inf`/`inf`/negatif/sıfır hepsi tek dalda `MIN_COLLIDER_DIM`'e düşer.
16fn 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
24/// Physics API fonksiyonlarını Lua'ya kaydeder
25pub fn register_physics_api(lua: &Lua, command_queue: Arc<CommandQueue>) -> Result<(), LuaError> {
26    let physics_table = lua.create_table()?;
27
28    // === KUVVET UYGULA ===
29    {
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    // === İMPULS UYGULA ===
41    {
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    // === RIGIDBODY EKLE ===
53    {
54        let cq = command_queue.clone();
55        physics_table.set(
56            "add_rigidbody",
57            lua.create_function(
58                // Contact friction/restitution live on the collider material, not
59                // the body, so `add_rigidbody` no longer takes them.
60                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    // === COLLIDER EKLE ===
73    {
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
108/// Her frame güncel fizik olaylarını (Tetikleyiciler, Çarpışmalar) Lua'ya aktarır
109pub 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        // Trigger (Tetikleyici) Olayları
120        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        // Fiziksel Çarpışma Olayları
134        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    // Her frame listeleri güncelle
149    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        // build_physics_api initializes the `physics` global table
169        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); // valid values pass through
214    }
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        // hx negative, hy NaN (0/0), hz valid — script typo hardening.
222        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}