Skip to main content

gizmo_physics_rigid/
world.rs

1use crate::{
2    components::{RigidBody, Velocity},
3    integrator::Integrator,
4    solver::ConstraintSolver,
5};
6use gizmo_physics_core::broadphase::SpatialHash;
7use gizmo_physics_core::{CollisionEvent, ContactManifold, TriggerEvent};
8use gizmo_physics_core::raycast::{Ray, Raycast, RaycastHit};
9use gizmo_physics_core::components::{Collider, Transform};
10use gizmo_core::entity::Entity;
11
12use std::collections::HashMap;
13
14#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
15pub enum ZoneShape {
16    Box {
17        min: gizmo_math::Vec3,
18        max: gizmo_math::Vec3,
19    },
20    Sphere {
21        center: gizmo_math::Vec3,
22        radius: f32,
23    },
24}
25
26impl ZoneShape {
27    pub fn contains(&self, p: gizmo_math::Vec3) -> bool {
28        match self {
29            ZoneShape::Box { min, max } => {
30                p.x >= min.x
31                    && p.x <= max.x
32                    && p.y >= min.y
33                    && p.y <= max.y
34                    && p.z >= min.z
35                    && p.z <= max.z
36            }
37            ZoneShape::Sphere { center, radius } => {
38                (p - *center).length_squared() <= radius * radius
39            }
40        }
41    }
42}
43
44#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
45pub struct GravityField {
46    pub shape: ZoneShape,
47    pub gravity: gizmo_math::Vec3,
48    pub falloff_radius: f32, // If > 0, gravity drops off
49    pub priority: i32,
50}
51
52#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
53pub struct FluidZone {
54    pub shape: ZoneShape,
55    pub density: f32,        // kg/m^3
56    pub viscosity: f32,      // dynamic viscosity for Stokes drag
57    pub linear_drag: f32,    // fallback linear drag
58    pub quadratic_drag: f32, // fallback quadratic drag
59}
60
61/// Sabit iç fizik frekansı (Hz) - 240Hz (Sub-stepping ile mükemmel çarpışma tespiti)
62const PHYSICS_HZ: f32 = 240.0;
63const FIXED_DT: f32 = 1.0 / PHYSICS_HZ;
64/// Sub-step başına maksimum adım sayısı — spiral'i önler
65const MAX_SUBSTEPS: u32 = 64; // Increased from 8 to support larger DTs without losing simulation time
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
68pub enum Weather {
69    Sunny,
70    Rain,
71    Snow,
72}
73
74impl Default for Weather {
75    fn default() -> Self {
76        Self::Sunny
77    }
78}
79
80/// A compact snapshot of the physics state for rewinding
81#[derive(Clone)]
82pub struct PhysicsStateSnapshot {
83    pub transforms: Vec<Transform>,
84    pub velocities: Vec<Velocity>,
85}
86
87/// Main physics world that manages all physics simulation
88#[derive(serde::Serialize, serde::Deserialize)]
89pub struct PhysicsWorld {
90    pub weather: Weather,
91    
92    #[serde(skip)]
93    pub integrator: Integrator,
94    #[serde(skip)]
95    pub solver: ConstraintSolver,
96    #[serde(skip)]
97    pub spatial_hash: SpatialHash,
98    #[serde(skip)]
99    pub collision_events: Vec<CollisionEvent>,
100    #[serde(skip)]
101    pub trigger_events: Vec<TriggerEvent>,
102    #[serde(skip)]
103    pub fracture_events: Vec<gizmo_physics_core::FractureEvent>,
104    #[serde(skip)]
105    pub fracture_cache: crate::fracture::PreFracturedCache,
106    #[serde(skip)]
107    pub joints: Vec<crate::joints::Joint>,
108    #[serde(skip)]
109    pub joint_solver: crate::joints::JointSolver,
110
111    pub gravity_fields: Vec<GravityField>,
112    pub fluid_zones: Vec<FluidZone>,
113
114    #[serde(skip)]
115    pub(crate) contact_cache: HashMap<(Entity, Entity), (bool, Option<ContactManifold>)>,
116
117    pub accumulator: f32,
118    pub render_alpha: f32,
119
120    #[serde(skip)]
121    pub metrics: crate::island::PhysicsMetrics,
122
123    // SoA (Structure of Arrays) Memory Layout
124    pub entities: Vec<Entity>,
125    pub rigid_bodies: Vec<RigidBody>,
126    pub transforms: Vec<Transform>,
127    pub velocities: Vec<Velocity>,
128    pub colliders: Vec<Collider>,
129    pub entity_index_map: HashMap<u32, usize>,
130
131    // Timeline and Debugging
132    #[serde(skip)]
133    pub is_paused: bool,
134    #[serde(skip)]
135    pub step_once: bool,
136    #[serde(skip)]
137    pub rewind_requested: bool,
138    #[serde(skip)]
139    pub history: std::collections::VecDeque<PhysicsStateSnapshot>,
140    pub max_history_frames: usize,
141
142    #[serde(skip)]
143    pub watchlist: std::collections::HashSet<Entity>,
144}
145
146impl Default for PhysicsWorld {
147    fn default() -> Self {
148        Self::new()
149    }
150}
151
152impl PhysicsWorld {
153    pub fn new() -> Self {
154        Self {
155            weather: Weather::Sunny,
156            integrator: Integrator::default(),
157            solver: ConstraintSolver::default(),
158            spatial_hash: SpatialHash::new(10.0),
159            collision_events: Vec::new(),
160            trigger_events: Vec::new(),
161            fracture_events: Vec::new(),
162            fracture_cache: crate::fracture::PreFracturedCache::new(),
163            joints: Vec::new(),
164            joint_solver: crate::joints::JointSolver::default(),
165            gravity_fields: Vec::new(),
166            fluid_zones: Vec::new(),
167
168
169            contact_cache: HashMap::new(),
170            accumulator: 0.0,
171            render_alpha: 1.0,
172            metrics: crate::island::PhysicsMetrics::default(),
173            entities: Vec::new(),
174            rigid_bodies: Vec::new(),
175            transforms: Vec::new(),
176            velocities: Vec::new(),
177            colliders: Vec::new(),
178            entity_index_map: HashMap::new(),
179            is_paused: false,
180            step_once: false,
181            rewind_requested: false,
182            history: std::collections::VecDeque::new(),
183            max_history_frames: 600, // 5 seconds of history at 120Hz
184            watchlist: std::collections::HashSet::new(),
185        }
186    }
187
188    pub fn with_gravity(mut self, gravity: gizmo_math::Vec3) -> Self {
189        self.integrator.gravity = gravity;
190        self
191    }
192
193
194    pub fn enable_gpu_compute(&mut self) {
195    }
196
197    pub fn with_cell_size(mut self, cell_size: f32) -> Self {
198        self.spatial_hash = SpatialHash::new(cell_size);
199        self
200    }
201
202    // ── SoA Body Management ───────────────────────────────────────────────────
203
204    pub fn add_body(
205        &mut self,
206        entity: Entity,
207        rb: RigidBody,
208        t: Transform,
209        v: Velocity,
210        c: Collider,
211    ) {
212        let idx = self.entities.len();
213
214        let mut aabb = c.compute_aabb(t.position, t.rotation);
215        if rb.ccd_enabled {
216            let movement = v.linear * (1.0 / 60.0); // Fatten by max expected delta movement
217            let min_mov = aabb
218                .min
219                .min((gizmo_math::Vec3::from(aabb.min) + movement).into());
220            let max_mov = aabb
221                .max
222                .max((gizmo_math::Vec3::from(aabb.max) + movement).into());
223            aabb = gizmo_math::Aabb::new(min_mov, max_mov);
224        }
225        self.spatial_hash.insert(entity, aabb);
226
227        self.entities.push(entity);
228        self.rigid_bodies.push(rb);
229        self.transforms.push(t);
230        self.velocities.push(v);
231        self.colliders.push(c);
232        self.entity_index_map.insert(entity.id(), idx);
233    }
234
235    pub fn clear_bodies(&mut self) {
236        self.entities.clear();
237        self.rigid_bodies.clear();
238        self.transforms.clear();
239        self.velocities.clear();
240        self.colliders.clear();
241        self.entity_index_map.clear();
242        self.spatial_hash.clear();
243    }
244
245    pub fn sync_bodies<'a>(
246        &mut self,
247        incoming_bodies: impl Iterator<Item = &'a (Entity, RigidBody, Transform, Velocity, Collider)>,
248    ) {
249        let mut active_ids = std::collections::HashSet::new();
250
251        for (entity, rb, trans, vel, col) in incoming_bodies {
252            let e_id = entity.id();
253            active_ids.insert(e_id);
254
255            if let Some(&idx) = self.entity_index_map.get(&e_id) {
256                // Update existing body without dropping/allocating mappings
257                self.rigid_bodies[idx] = *rb;
258                self.transforms[idx] = *trans;
259                self.velocities[idx] = *vel;
260
261                // Shapes use Arc internally, so clone is cheap
262                self.colliders[idx] = col.clone();
263
264                // Update spatial hash (Fatten for CCD if enabled)
265                let mut aabb = col.compute_aabb(trans.position, trans.rotation);
266                if rb.ccd_enabled {
267                    let movement = vel.linear * (1.0 / 60.0);
268                    let min_mov = aabb
269                        .min
270                        .min((gizmo_math::Vec3::from(aabb.min) + movement).into());
271                    let max_mov = aabb
272                        .max
273                        .max((gizmo_math::Vec3::from(aabb.max) + movement).into());
274                    aabb = gizmo_math::Aabb::new(min_mov, max_mov);
275                }
276                self.spatial_hash.update(*entity, aabb);
277            } else {
278                // Add new body
279                self.add_body(*entity, *rb, *trans, *vel, col.clone());
280            }
281        }
282
283        // Cleanup removed entities
284        let mut i = 0;
285        while i < self.entities.len() {
286            if !active_ids.contains(&self.entities[i].id()) {
287                self.remove_body_at(i);
288            } else {
289                i += 1;
290            }
291        }
292    }
293
294    pub fn remove_body_at(&mut self, idx: usize) {
295        let last_idx = self.entities.len() - 1;
296        let entity = self.entities[idx];
297
298        self.spatial_hash.remove(entity);
299        self.entity_index_map.remove(&entity.id());
300
301        if idx != last_idx {
302            let last_entity = self.entities[last_idx];
303
304            self.entities.swap(idx, last_idx);
305            self.rigid_bodies.swap(idx, last_idx);
306            self.transforms.swap(idx, last_idx);
307            self.velocities.swap(idx, last_idx);
308            self.colliders.swap(idx, last_idx);
309
310            self.entity_index_map.insert(last_entity.id(), idx);
311        }
312
313        self.entities.pop();
314        self.rigid_bodies.pop();
315        self.transforms.pop();
316        self.velocities.pop();
317        self.colliders.pop();
318    }
319
320    // ──────────────────────────────────────────────────────────────────────────
321
322    /// Ana fizik adımı — sabit 120Hz sub-stepping ile
323    /// Render dt'yi (değişken) sabit iç fizik dt'ye dönüştürür.
324    pub fn step(
325        &mut self,
326        
327        
328        dt: f32,
329    ) -> Result<(), gizmo_physics_core::GizmoError> {
330        if self.rewind_requested {
331            self.rewind_requested = false;
332            if let Some(snapshot) = self.history.pop_back() {
333                if snapshot.transforms.len() == self.transforms.len() {
334                    self.transforms = snapshot.transforms;
335                    self.velocities = snapshot.velocities;
336                    tracing::info!("Physics rewound by 1 frame!");
337                } else {
338                    tracing::warn!("Cannot rewind: Entity count changed.");
339                }
340            }
341            return Ok(());
342        }
343
344        if self.is_paused && !self.step_once {
345            // Clear events so we don't dispatch old collisions repeatedly
346            self.collision_events.clear();
347            self.trigger_events.clear();
348            self.fracture_events.clear();
349            return Ok(());
350        }
351
352        // --- STEP ONCE (DEBUG) ---
353        let frame_dt = if self.step_once {
354            self.step_once = false;
355            self.accumulator = 0.0; // Reset accumulator so we step exactly once
356            FIXED_DT
357        } else {
358            dt.min(0.25) // Maksimum 250ms — death-spiral koruması
359        };
360
361        // Olayları her render frame'de temizle
362        self.collision_events.clear();
363        self.trigger_events.clear();
364        self.fracture_events.clear();
365
366        // Birikimci: render dt'yi sub-step'lere böl
367        self.accumulator += frame_dt;
368
369        let mut steps = 0u32;
370        while self.accumulator >= FIXED_DT && steps < MAX_SUBSTEPS {
371            self.step_internal(FIXED_DT)?;
372            self.accumulator -= FIXED_DT;
373            steps += 1;
374        }
375
376        // Alpha: render interpolasyonu için (0 = önceki adım, 1 = mevcut adım)
377        self.render_alpha = self.accumulator / FIXED_DT;
378
379        // Record history snapshot at the end of the frame
380        self.history.push_back(PhysicsStateSnapshot {
381            transforms: self.transforms.clone(),
382            velocities: self.velocities.clone(),
383        });
384        if self.history.len() > self.max_history_frames {
385            self.history.pop_front();
386        }
387
388        Ok(())
389    }
390
391    /// İç fizik adımı — sabit FIXED_DT ile çağrılır
392    /// İç fizik adımı — sabit FIXED_DT ile çağrılır
393    /// Modüler pipeline: her aşama ayrı fonksiyonda (pipeline.rs)
394    fn step_internal(
395        &mut self,
396        
397        
398        dt: f32,
399    ) -> Result<(), gizmo_physics_core::GizmoError> {
400        // Energy Conservation Check: Record initial energy (Zero-cost in release mode)
401        let _initial_energy = if cfg!(debug_assertions) {
402            self.calculate_total_energy()
403        } else {
404            0.0
405        };
406
407        // 0-1. Yerçekimi, sıvı bölgeleri, hız entegrasyonu
408        self.velocity_integration_step(dt)?;
409
410        // 1.5-1.6 Yumuşak cisim ve sıvı simülasyonu
411
412        // 2. Broadphase — uzamsal hash güncelleme
413        self.broadphase_step(dt);
414
415        // 3. Narrowphase — çarpışma tespiti ve olayları
416        let manifolds = self.narrowphase_and_collision_step(dt);
417
418        // 4-4.5 Kısıt çözücü (çarpışma + eklem)
419        self.constraint_solve_step(manifolds, dt);
420
421        // 5-6. Pozisyon entegrasyonu ve uyku durumu
422        self.position_integration_step(dt)?;
423
424        // Energy Conservation Check: Validate energy bounds (Zero-cost in release mode)
425        if cfg!(debug_assertions) {
426            let _final_energy = self.calculate_total_energy();
427        }
428
429        Ok(())
430    }
431
432    /// Get collision events from last step
433    pub fn collision_events(&self) -> &[CollisionEvent] {
434        &self.collision_events
435    }
436
437    /// Get trigger events from last step
438    pub fn trigger_events(&self) -> &[TriggerEvent] {
439        &self.trigger_events
440    }
441
442    /// Apply an impulse to a body at a point
443    pub fn apply_impulse(
444        &self,
445        rb: &RigidBody,
446        transform: &Transform,
447        vel: &mut Velocity,
448        impulse: gizmo_math::Vec3,
449        point: gizmo_math::Vec3,
450    ) {
451        Integrator::apply_impulse_at_point(rb, transform, vel, impulse, point);
452    }
453
454    /// Apply a force to a body
455    pub fn apply_force(
456        &self,
457        rb: &RigidBody,
458        vel: &mut Velocity,
459        force: gizmo_math::Vec3,
460        dt: f32,
461    ) {
462        Integrator::apply_force(rb, vel, force, dt);
463    }
464
465    /// Perform a raycast against all bodies
466    pub fn raycast(&self, ray: &Ray, max_distance: f32) -> Option<RaycastHit> {
467        let mut closest_hit: Option<RaycastHit> = None;
468        let mut closest_distance = max_distance;
469
470        let potential_hits = self
471            .spatial_hash
472            .query_ray(ray.origin, ray.direction, max_distance);
473
474        for (entity, _aabb_t) in potential_hits {
475            if let Some(&i) = self.entity_index_map.get(&entity.id()) {
476                let transform = &self.transforms[i];
477                let collider = &self.colliders[i];
478
479                // Detailed shape test
480                if let Some((distance, normal)) =
481                    Raycast::ray_shape(ray, &collider.shape, transform)
482                {
483                    if distance < closest_distance {
484                        closest_distance = distance;
485                        closest_hit = Some(RaycastHit {
486                            entity,
487                            point: ray.point_at(distance),
488                            normal,
489                            distance,
490                        });
491                    }
492                }
493            }
494        }
495
496        closest_hit
497    }
498
499    /// Perform a raycast and return all hits
500    pub fn raycast_all(&self, ray: &Ray, max_distance: f32) -> Vec<RaycastHit> {
501        let mut hits = Vec::new();
502
503        let potential_hits = self
504            .spatial_hash
505            .query_ray(ray.origin, ray.direction, max_distance);
506
507        for (entity, _aabb_t) in potential_hits {
508            if let Some(&i) = self.entity_index_map.get(&entity.id()) {
509                let transform = &self.transforms[i];
510                let collider = &self.colliders[i];
511
512                // Detailed shape test
513                if let Some((distance, normal)) =
514                    Raycast::ray_shape(ray, &collider.shape, transform)
515                {
516                    hits.push(RaycastHit {
517                        entity,
518                        point: ray.point_at(distance),
519                        normal,
520                        distance,
521                    });
522                }
523            }
524        }
525
526        // Sort by distance
527        hits.sort_by(|a, b| a.distance.total_cmp(&b.distance));
528        hits
529    }
530
531    /// Telemetry and Debugging: Create a JSON snapshot of the physical world state
532    pub fn trigger_snapshot(&self, reason: &str) {
533        tracing::error!("Creating physics snapshot due to: {}", reason);
534        let timestamp = std::time::SystemTime::now()
535            .duration_since(std::time::UNIX_EPOCH)
536            .unwrap()
537            .as_secs();
538        let filename = format!("physics_snapshot_{}.json", timestamp);
539
540        match std::fs::File::create(&filename) {
541            Ok(file) => {
542                if let Err(e) = serde_json::to_writer_pretty(file, self) {
543                    tracing::error!("Failed to serialize physics snapshot: {:?}", e);
544                } else {
545                    tracing::info!("Physics snapshot successfully saved to {}", filename);
546                }
547            }
548            Err(e) => {
549                tracing::error!("Failed to create snapshot file {}: {:?}", filename, e);
550            }
551        }
552    }
553
554    /// Calculate total kinetic and potential energy of the simulation
555    pub fn calculate_total_energy(&self) -> f32 {
556        let default_gravity = self.integrator.gravity;
557        let mut total_energy = 0.0;
558
559        for i in 0..self.entities.len() {
560            let rb = &self.rigid_bodies[i];
561            let vel = &self.velocities[i];
562            let trans = &self.transforms[i];
563
564            if rb.is_dynamic() && !rb.is_sleeping {
565                // Kinetic Energy: 1/2 * m * v^2
566                let ke_linear = 0.5 * rb.mass * vel.linear.length_squared();
567
568                // Rotational Kinetic Energy: 1/2 * I * w^2
569                // Approximation using scalar local inertia for speed
570                let ke_angular = 0.5
571                    * (rb.local_inertia.x * vel.angular.x * vel.angular.x
572                        + rb.local_inertia.y * vel.angular.y * vel.angular.y
573                        + rb.local_inertia.z * vel.angular.z * vel.angular.z);
574
575                // Potential Energy: m * g * h
576                let pe = if rb.use_gravity {
577                    -rb.mass * default_gravity.dot(trans.position)
578                } else {
579                    0.0
580                };
581
582                total_energy += ke_linear + ke_angular + pe;
583            }
584        }
585        total_energy
586    }
587}
588
589#[cfg(test)]
590mod tests {
591    use super::*;
592    use gizmo_core::entity::Entity;
593    use gizmo_math::Vec3;
594
595    #[test]
596    fn test_physics_world_creation() {
597        let world = PhysicsWorld::new();
598        assert_eq!(world.integrator.gravity, Vec3::new(0.0, -9.81, 0.0));
599    }
600
601    #[test]
602    fn test_physics_step() {
603        let mut world = PhysicsWorld::new();
604
605        let entity = Entity::new(1, 0);
606        let rb = RigidBody::default();
607        let transform = Transform::new(Vec3::new(0.0, 10.0, 0.0));
608        let vel = Velocity::default();
609        let collider = Collider::sphere(1.0);
610
611        world.add_body(entity, rb, transform, vel, collider);
612
613        // Simulate for 1 second
614        for _ in 0..60 {
615            let _ = world.step(1.0 / 60.0);
616        }
617
618        // Object should have fallen due to gravity
619        assert!(world.transforms[0].position.y < 10.0);
620    }
621
622    #[test]
623    fn test_high_stack_stability() {
624        let mut world = PhysicsWorld::new();
625        // Akademik doğrulama için iterasyon sayısını yüksek tutalım
626        world.solver.iterations = 30;
627
628        // Ground
629        let mut ground_rb = RigidBody::default();
630        ground_rb.body_type = crate::components::rigid_body::BodyType::Static;
631        ground_rb.wake_up();
632        world.add_body(
633            Entity::new(0, 0),
634            ground_rb,
635            Transform::new(Vec3::new(0.0, -0.5, 0.0)),
636            Velocity::default(),
637            Collider::box_collider(Vec3::new(50.0, 0.5, 50.0)),
638        );
639
640        // 10 Kutuluk bir kule inşa et
641        let box_count = 10;
642        let box_size = 1.0;
643        let half_size = box_size / 2.0;
644
645        for i in 1..=box_count {
646            let mut rb = RigidBody::new(1.0, 0.5, 0.5, true);
647            rb.wake_up(); // Uyumasını engelle ki solver test edilsin
648            
649            let y_pos = half_size + (i - 1) as f32 * box_size;
650            
651            world.add_body(
652                Entity::new(i, 0),
653                rb,
654                Transform::new(Vec3::new(0.0, y_pos, 0.0)),
655                Velocity::default(),
656                Collider::box_collider(Vec3::new(half_size, half_size, half_size)),
657            );
658        }
659
660        // 10 saniye (600 kare) simüle et
661        for i in 0..600 {
662            let _ = world.step(1.0 / 60.0);
663            if i % 60 == 0 {
664                println!("Frame {}: Y={}, X={}, Z={}", i, world.transforms[10].position.y, world.transforms[10].position.x, world.transforms[10].position.z);
665            }
666        }
667
668        // Kule yıkılmamış olmalı (X ve Z ekseninde çok kaymamış olmalı)
669        // En üstteki kutunun durumuna bakalım
670        let top_box_idx = box_count as usize; // Entity ID starts from 1 for boxes, so idx is `box_count` because ground is 0
671        let top_box_pos = world.transforms[top_box_idx].position;
672
673        // Akademik limitler: 10 saniye boyunca dik durmalı, yana yatmamalı
674        assert!(
675            top_box_pos.x.abs() < 0.1,
676            "Top box slid too much on X axis: {}",
677            top_box_pos.x
678        );
679        assert!(
680            top_box_pos.z.abs() < 0.1,
681            "Top box slid too much on Z axis: {}",
682            top_box_pos.z
683        );
684
685        // Yüksekliği korunmalı (Jitter / Penetrasyon testi)
686        let expected_y = half_size + (box_count - 1) as f32 * box_size;
687        let y_error = (top_box_pos.y - expected_y).abs();
688        assert!(
689            y_error < 0.1,
690            "Top box sunk or bounced too much. Expected Y: {}, Actual Y: {}",
691            expected_y,
692            top_box_pos.y
693        );
694    }
695
696    #[test]
697    fn test_ccd_tunneling_prevention() {
698        let mut world = PhysicsWorld::new();
699        // Gravity kapalı ki tam düz uçsun
700        world.integrator.gravity = Vec3::ZERO;
701
702        // İnce bir duvar (kalınlık 0.2m)
703        let mut wall_rb = RigidBody::new_static();
704        wall_rb.wake_up();
705        world.add_body(
706            Entity::new(0, 0),
707            wall_rb,
708            Transform::new(Vec3::new(0.0, 0.0, 0.0)),
709            Velocity::default(),
710            Collider::box_collider(Vec3::new(0.1, 5.0, 5.0)),
711        );
712
713        // Mermi (CCD Açık)
714        let mut bullet_rb = RigidBody::new(1.0, 0.0, 0.0, false);
715        bullet_rb.ccd_enabled = true;
716        bullet_rb.wake_up();
717
718        // Mermiyi duvarın önünden, saniyede 1200 metre hızla (mach 3.5) ateşle!
719        // 1 kare (1/60) saniyede 20 metre yol alır. Duvar sadece 0.2m kalınlığında!
720        world.add_body(
721            Entity::new(1, 0),
722            bullet_rb,
723            Transform::new(Vec3::new(-5.0, 0.0, 0.0)),
724            Velocity::new(Vec3::new(1200.0, 0.0, 0.0)),
725            Collider::sphere(0.2),
726        );
727
728        // 1 Frame simüle et (Tunneling ihtimali olan an)
729        let _ = world.step(1.0 / 60.0);
730
731        let bullet_pos = world.transforms[1].position;
732        let bullet_vel = world.velocities[1].linear;
733
734        // CCD çalıştığı için mermi duvarı GEÇMEMELİ!
735        // Eğer CCD çalışmasaydı, X konumu 15.0 civarında olurdu.
736        // CCD çalıştığı için duvarda durmalı (X <= 0) veya sekip eksi hıza geçmeli.
737        assert!(
738            bullet_pos.x <= 0.0,
739            "TUNNELING FAILED! Bullet phased through the wall. Position: {}",
740            bullet_pos.x
741        );
742        // Hız durdurulmuş veya sekmiş olmalı
743        assert!(
744            bullet_vel.x <= 0.01,
745            "Bullet did not lose velocity after hitting the wall. Vel: {}",
746            bullet_vel.x
747        );
748    }
749
750    #[test]
751    fn test_coulomb_friction_and_sleeping() {
752        let mut world = PhysicsWorld::new();
753        // Sürtünme için yerçekimi şart (Normal kuvveti yaratmak için)
754        world.integrator.gravity = Vec3::new(0.0, -10.0, 0.0);
755
756        // Zemin (Sürtünme: 0.5)
757        let mut ground_rb = RigidBody::new_static();
758        ground_rb.friction = 0.5;
759        ground_rb.wake_up();
760        world.add_body(
761            Entity::new(0, 0),
762            ground_rb,
763            Transform::new(Vec3::new(0.0, -0.5, 0.0)),
764            Velocity::default(),
765            Collider::box_collider(Vec3::new(100.0, 0.5, 100.0)),
766        );
767
768        // Kutu A: Düşük Sürtünme (0.1)
769        let mut box_a = RigidBody::new(1.0, 0.0, 0.1, true);
770        box_a.wake_up();
771        world.add_body(
772            Entity::new(1, 0),
773            box_a,
774            Transform::new(Vec3::new(0.0, 0.5, -2.0)),
775            Velocity::new(Vec3::new(10.0, 0.0, 0.0)),
776            Collider::box_collider(Vec3::splat(0.5)),
777        );
778
779        // Kutu B: Yüksek Sürtünme (0.8)
780        let mut box_b = RigidBody::new(1.0, 0.0, 0.8, true);
781        box_b.wake_up();
782        world.add_body(
783            Entity::new(2, 0),
784            box_b,
785            Transform::new(Vec3::new(0.0, 0.5, 2.0)),
786            Velocity::new(Vec3::new(10.0, 0.0, 0.0)),
787            Collider::box_collider(Vec3::splat(0.5)),
788        );
789
790        // 5 saniye simüle et (300 kare) - İkisi de tamamen durup uyumalı
791        for _ in 0..300 {
792            let _ = world.step(1.0 / 60.0);
793        }
794
795        let pos_a = world.transforms[1].position;
796        let pos_b = world.transforms[2].position;
797        let sleep_a = world.rigid_bodies[1].is_sleeping;
798        let sleep_b = world.rigid_bodies[2].is_sleeping;
799
800        // Yüksek sürtünmeli kutu daha erken durmuş olmalı (Daha az X mesafesi)
801        assert!(
802            pos_b.x < pos_a.x,
803            "High friction box should travel less. Pos A: {}, Pos B: {}",
804            pos_a.x,
805            pos_b.x
806        );
807
808        // İkisi de kinetik enerjisini sıfırlayıp UYKU MODUNA geçmiş olmalı
809        assert!(
810            sleep_a,
811            "Low friction box did not enter sleeping mode!"
812        );
813        assert!(
814            sleep_b,
815            "High friction box did not enter sleeping mode!"
816        );
817    }
818
819    #[test]
820    fn test_car_simulation() {
821        use crate::joints::data::{Joint, JointData, HingeJointData};
822
823        let mut world = PhysicsWorld::new();
824        // Yerçekimi açık (Sürtünme ve ağırlık için gerekli)
825        world.integrator.gravity = Vec3::new(0.0, -10.0, 0.0);
826
827        // --- Zemin ---
828        let mut ground_rb = RigidBody::new_static();
829        ground_rb.friction = 0.8;
830        ground_rb.wake_up();
831        world.add_body(
832            Entity::new(0, 0),
833            ground_rb,
834            Transform::new(Vec3::new(0.0, -0.5, 0.0)),
835            Velocity::default(),
836            Collider::box_collider(Vec3::new(100.0, 0.5, 100.0)),
837        );
838
839        // --- Şasi (Chassis) ---
840        // 1000 kg, sürtünme önemsiz, dinamik
841        let mut chassis_rb = RigidBody::new(1000.0, 0.1, 0.5, true);
842        chassis_rb.wake_up();
843        let chassis_entity = Entity::new(1, 0);
844        let chassis_pos = Vec3::new(0.0, 1.5, 0.0);
845        world.add_body(
846            chassis_entity,
847            chassis_rb,
848            Transform::new(chassis_pos),
849            Velocity::default(),
850            Collider::box_collider(Vec3::new(1.0, 0.5, 2.0)), // Genişlik 2, Yükseklik 1, Uzunluk 4 (Yarıçaplar)
851        );
852
853        // Tekerlek Şablonu
854        let mut wheel_rb = RigidBody::new(50.0, 0.1, 0.9, true); // Yüksek kütle (50kg) ve yüksek sürtünme (0.9)
855        wheel_rb.wake_up();
856
857        let wheel_radius = 0.5;
858        let wheel_offsets = vec![
859            Vec3::new(-1.2, -0.2, 1.5),  // Sol Ön
860            Vec3::new(1.2, -0.2, 1.5),   // Sağ Ön
861            Vec3::new(-1.2, -0.2, -1.5), // Sol Arka
862            Vec3::new(1.2, -0.2, -1.5),  // Sağ Arka
863        ];
864
865        let mut wheel_entities = Vec::new();
866
867        for (i, offset) in wheel_offsets.iter().enumerate() {
868            let wheel_entity = Entity::new(2 + i as u32, 0);
869            wheel_entities.push(wheel_entity);
870
871            world.add_body(
872                wheel_entity,
873                wheel_rb.clone(),
874                Transform::new(chassis_pos + *offset),
875                Velocity::default(),
876                Collider::sphere(wheel_radius),
877            );
878
879            // Menteşe Eklemi (Hinge Joint) oluştur
880            let is_rear = i >= 2;
881            let hinge_data = HingeJointData {
882                axis: Vec3::X, // Tekerlekler X ekseni etrafında dönecek
883                use_limits: false,
884                lower_limit: 0.0,
885                upper_limit: 0.0,
886                use_motor: is_rear, // Sadece arka tekerleklerde motor var
887                motor_target_velocity: if is_rear { 10.0 } else { 0.0 }, // İleri doğru 10 rad/s
888                motor_max_force: if is_rear { 10000.0 } else { 0.0 }, // 10000 N güç
889                current_angle: 0.0,
890            };
891
892            let joint = Joint {
893                entity_a: chassis_entity,
894                entity_b: wheel_entity,
895                local_anchor_a: *offset, // Şasinin lokal uzayında bağlantı noktası
896                local_anchor_b: Vec3::ZERO, // Tekerleğin tam ortası
897                break_force: f32::MAX, // Asla kopmasın
898                break_torque: f32::MAX,
899                is_broken: false,
900                collision_enabled: false, // Şasi ile tekerlek çarpışmasın
901                data: JointData::Hinge(hinge_data),
902            };
903
904            world.joints.push(joint);
905        }
906
907        // --- Simülasyon ---
908        // Motorlar çalışacak ve arabayı 5 saniye boyunca (300 kare) ileri doğru (Z+) sürecek
909        for _ in 0..300 {
910            let _ = world.step(1.0 / 60.0);
911        }
912
913        // Doğrulama
914        let final_chassis_pos = world.transforms[1].position;
915        
916        // 1. İleri Sürüş: Araba Z ekseninde (ileri) hareket etmiş olmalı
917        assert!(
918            final_chassis_pos.z > 3.0,
919            "Araba yeterince ileri gidemedi! Motor veya sürtünme çalışmıyor. Z pozisyonu: {}",
920            final_chassis_pos.z
921        );
922
923        // 2. Denge (Devrilmeme): Arabanın Y pozisyonu stabil kalmalı (uçmamalı veya batmamalı)
924        // Başlangıç Y: 1.5, Tekerlek yarıçapı 0.5. Araba yere oturunca Y ~1.0 - 1.2 civarı olmalı
925        assert!(
926            final_chassis_pos.y > 0.5 && final_chassis_pos.y < 2.0,
927            "Araba devrildi, uçtu veya yere battı! Y pozisyonu: {}",
928            final_chassis_pos.y
929        );
930
931        // 3. X Ekseninde Düz Gitme (Sağa sola savrulmama)
932        assert!(
933            final_chassis_pos.x.abs() < 1.0,
934            "Araba düz gidemedi, sağa sola savruldu! X pozisyonu: {}",
935            final_chassis_pos.x
936        );
937    }
938}