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        #[cfg(target_arch = "wasm32")]
535        let timestamp = web_time::SystemTime::now()
536            .duration_since(web_time::SystemTime::UNIX_EPOCH)
537            .unwrap()
538            .as_secs();
539        #[cfg(not(target_arch = "wasm32"))]
540        let timestamp = std::time::SystemTime::now()
541            .duration_since(std::time::SystemTime::UNIX_EPOCH)
542            .unwrap()
543            .as_secs();
544        let filename = format!("physics_snapshot_{}.json", timestamp);
545
546        match std::fs::File::create(&filename) {
547            Ok(file) => {
548                if let Err(e) = serde_json::to_writer_pretty(file, self) {
549                    tracing::error!("Failed to serialize physics snapshot: {:?}", e);
550                } else {
551                    tracing::info!("Physics snapshot successfully saved to {}", filename);
552                }
553            }
554            Err(e) => {
555                tracing::error!("Failed to create snapshot file {}: {:?}", filename, e);
556            }
557        }
558    }
559
560    /// Calculate total kinetic and potential energy of the simulation
561    pub fn calculate_total_energy(&self) -> f32 {
562        let default_gravity = self.integrator.gravity;
563        let mut total_energy = 0.0;
564
565        for i in 0..self.entities.len() {
566            let rb = &self.rigid_bodies[i];
567            let vel = &self.velocities[i];
568            let trans = &self.transforms[i];
569
570            if rb.is_dynamic() && !rb.is_sleeping {
571                // Kinetic Energy: 1/2 * m * v^2
572                let ke_linear = 0.5 * rb.mass * vel.linear.length_squared();
573
574                // Rotational Kinetic Energy: 1/2 * I * w^2
575                // Approximation using scalar local inertia for speed
576                let ke_angular = 0.5
577                    * (rb.local_inertia.x * vel.angular.x * vel.angular.x
578                        + rb.local_inertia.y * vel.angular.y * vel.angular.y
579                        + rb.local_inertia.z * vel.angular.z * vel.angular.z);
580
581                // Potential Energy: m * g * h
582                let pe = if rb.use_gravity {
583                    -rb.mass * default_gravity.dot(trans.position)
584                } else {
585                    0.0
586                };
587
588                total_energy += ke_linear + ke_angular + pe;
589            }
590        }
591        total_energy
592    }
593}
594
595#[cfg(test)]
596mod tests {
597    use super::*;
598    use gizmo_core::entity::Entity;
599    use gizmo_math::Vec3;
600
601    #[test]
602    fn test_physics_world_creation() {
603        let world = PhysicsWorld::new();
604        assert_eq!(world.integrator.gravity, Vec3::new(0.0, -9.81, 0.0));
605    }
606
607    #[test]
608    fn test_physics_step() {
609        let mut world = PhysicsWorld::new();
610
611        let entity = Entity::new(1, 0);
612        let rb = RigidBody::default();
613        let transform = Transform::new(Vec3::new(0.0, 10.0, 0.0));
614        let vel = Velocity::default();
615        let collider = Collider::sphere(1.0);
616
617        world.add_body(entity, rb, transform, vel, collider);
618
619        // Simulate for 1 second
620        for _ in 0..60 {
621            let _ = world.step(1.0 / 60.0);
622        }
623
624        // Object should have fallen due to gravity
625        assert!(world.transforms[0].position.y < 10.0);
626    }
627
628    #[test]
629    fn test_high_stack_stability() {
630        let mut world = PhysicsWorld::new();
631        // Akademik doğrulama için iterasyon sayısını yüksek tutalım
632        world.solver.iterations = 30;
633
634        // Ground
635        let mut ground_rb = RigidBody::default();
636        ground_rb.body_type = crate::components::rigid_body::BodyType::Static;
637        ground_rb.wake_up();
638        world.add_body(
639            Entity::new(0, 0),
640            ground_rb,
641            Transform::new(Vec3::new(0.0, -0.5, 0.0)),
642            Velocity::default(),
643            Collider::box_collider(Vec3::new(50.0, 0.5, 50.0)),
644        );
645
646        // 10 Kutuluk bir kule inşa et
647        let box_count = 10;
648        let box_size = 1.0;
649        let half_size = box_size / 2.0;
650
651        for i in 1..=box_count {
652            let mut rb = RigidBody::new(1.0, 0.5, 0.5, true);
653            rb.wake_up(); // Uyumasını engelle ki solver test edilsin
654            
655            let y_pos = half_size + (i - 1) as f32 * box_size;
656            
657            world.add_body(
658                Entity::new(i, 0),
659                rb,
660                Transform::new(Vec3::new(0.0, y_pos, 0.0)),
661                Velocity::default(),
662                Collider::box_collider(Vec3::new(half_size, half_size, half_size)),
663            );
664        }
665
666        // 10 saniye (600 kare) simüle et
667        for i in 0..600 {
668            let _ = world.step(1.0 / 60.0);
669            if i % 60 == 0 {
670                println!("Frame {}: Y={}, X={}, Z={}", i, world.transforms[10].position.y, world.transforms[10].position.x, world.transforms[10].position.z);
671            }
672        }
673
674        // Kule yıkılmamış olmalı (X ve Z ekseninde çok kaymamış olmalı)
675        // En üstteki kutunun durumuna bakalım
676        let top_box_idx = box_count as usize; // Entity ID starts from 1 for boxes, so idx is `box_count` because ground is 0
677        let top_box_pos = world.transforms[top_box_idx].position;
678
679        // Akademik limitler: 10 saniye boyunca dik durmalı, yana yatmamalı
680        assert!(
681            top_box_pos.x.abs() < 0.1,
682            "Top box slid too much on X axis: {}",
683            top_box_pos.x
684        );
685        assert!(
686            top_box_pos.z.abs() < 0.1,
687            "Top box slid too much on Z axis: {}",
688            top_box_pos.z
689        );
690
691        // Yüksekliği korunmalı (Jitter / Penetrasyon testi)
692        let expected_y = half_size + (box_count - 1) as f32 * box_size;
693        let y_error = (top_box_pos.y - expected_y).abs();
694        assert!(
695            y_error < 0.1,
696            "Top box sunk or bounced too much. Expected Y: {}, Actual Y: {}",
697            expected_y,
698            top_box_pos.y
699        );
700    }
701
702    #[test]
703    fn test_ccd_tunneling_prevention() {
704        let mut world = PhysicsWorld::new();
705        // Gravity kapalı ki tam düz uçsun
706        world.integrator.gravity = Vec3::ZERO;
707
708        // İnce bir duvar (kalınlık 0.2m)
709        let mut wall_rb = RigidBody::new_static();
710        wall_rb.wake_up();
711        world.add_body(
712            Entity::new(0, 0),
713            wall_rb,
714            Transform::new(Vec3::new(0.0, 0.0, 0.0)),
715            Velocity::default(),
716            Collider::box_collider(Vec3::new(0.1, 5.0, 5.0)),
717        );
718
719        // Mermi (CCD Açık)
720        let mut bullet_rb = RigidBody::new(1.0, 0.0, 0.0, false);
721        bullet_rb.ccd_enabled = true;
722        bullet_rb.wake_up();
723
724        // Mermiyi duvarın önünden, saniyede 1200 metre hızla (mach 3.5) ateşle!
725        // 1 kare (1/60) saniyede 20 metre yol alır. Duvar sadece 0.2m kalınlığında!
726        world.add_body(
727            Entity::new(1, 0),
728            bullet_rb,
729            Transform::new(Vec3::new(-5.0, 0.0, 0.0)),
730            Velocity::new(Vec3::new(1200.0, 0.0, 0.0)),
731            Collider::sphere(0.2),
732        );
733
734        // 1 Frame simüle et (Tunneling ihtimali olan an)
735        let _ = world.step(1.0 / 60.0);
736
737        let bullet_pos = world.transforms[1].position;
738        let bullet_vel = world.velocities[1].linear;
739
740        // CCD çalıştığı için mermi duvarı GEÇMEMELİ!
741        // Eğer CCD çalışmasaydı, X konumu 15.0 civarında olurdu.
742        // CCD çalıştığı için duvarda durmalı (X <= 0) veya sekip eksi hıza geçmeli.
743        assert!(
744            bullet_pos.x <= 0.0,
745            "TUNNELING FAILED! Bullet phased through the wall. Position: {}",
746            bullet_pos.x
747        );
748        // Hız durdurulmuş veya sekmiş olmalı
749        assert!(
750            bullet_vel.x <= 0.01,
751            "Bullet did not lose velocity after hitting the wall. Vel: {}",
752            bullet_vel.x
753        );
754    }
755
756    #[test]
757    fn test_coulomb_friction_and_sleeping() {
758        let mut world = PhysicsWorld::new();
759        // Sürtünme için yerçekimi şart (Normal kuvveti yaratmak için)
760        world.integrator.gravity = Vec3::new(0.0, -10.0, 0.0);
761
762        // Zemin (Sürtünme: 0.5)
763        let mut ground_rb = RigidBody::new_static();
764        ground_rb.friction = 0.5;
765        ground_rb.wake_up();
766        world.add_body(
767            Entity::new(0, 0),
768            ground_rb,
769            Transform::new(Vec3::new(0.0, -0.5, 0.0)),
770            Velocity::default(),
771            Collider::box_collider(Vec3::new(100.0, 0.5, 100.0)),
772        );
773
774        // Kutu A: Düşük Sürtünme (0.1)
775        let mut box_a = RigidBody::new(1.0, 0.0, 0.1, true);
776        box_a.wake_up();
777        world.add_body(
778            Entity::new(1, 0),
779            box_a,
780            Transform::new(Vec3::new(0.0, 0.5, -2.0)),
781            Velocity::new(Vec3::new(10.0, 0.0, 0.0)),
782            Collider::box_collider(Vec3::splat(0.5)),
783        );
784
785        // Kutu B: Yüksek Sürtünme (0.8)
786        let mut box_b = RigidBody::new(1.0, 0.0, 0.8, true);
787        box_b.wake_up();
788        world.add_body(
789            Entity::new(2, 0),
790            box_b,
791            Transform::new(Vec3::new(0.0, 0.5, 2.0)),
792            Velocity::new(Vec3::new(10.0, 0.0, 0.0)),
793            Collider::box_collider(Vec3::splat(0.5)),
794        );
795
796        // 5 saniye simüle et (300 kare) - İkisi de tamamen durup uyumalı
797        for _ in 0..300 {
798            let _ = world.step(1.0 / 60.0);
799        }
800
801        let pos_a = world.transforms[1].position;
802        let pos_b = world.transforms[2].position;
803        let sleep_a = world.rigid_bodies[1].is_sleeping;
804        let sleep_b = world.rigid_bodies[2].is_sleeping;
805
806        // Yüksek sürtünmeli kutu daha erken durmuş olmalı (Daha az X mesafesi)
807        assert!(
808            pos_b.x < pos_a.x,
809            "High friction box should travel less. Pos A: {}, Pos B: {}",
810            pos_a.x,
811            pos_b.x
812        );
813
814        // İkisi de kinetik enerjisini sıfırlayıp UYKU MODUNA geçmiş olmalı
815        assert!(
816            sleep_a,
817            "Low friction box did not enter sleeping mode!"
818        );
819        assert!(
820            sleep_b,
821            "High friction box did not enter sleeping mode!"
822        );
823    }
824
825    #[test]
826    fn test_car_simulation() {
827        use crate::joints::data::{Joint, JointData, HingeJointData};
828
829        let mut world = PhysicsWorld::new();
830        // Yerçekimi açık (Sürtünme ve ağırlık için gerekli)
831        world.integrator.gravity = Vec3::new(0.0, -10.0, 0.0);
832
833        // --- Zemin ---
834        let mut ground_rb = RigidBody::new_static();
835        ground_rb.friction = 0.8;
836        ground_rb.wake_up();
837        world.add_body(
838            Entity::new(0, 0),
839            ground_rb,
840            Transform::new(Vec3::new(0.0, -0.5, 0.0)),
841            Velocity::default(),
842            Collider::box_collider(Vec3::new(100.0, 0.5, 100.0)),
843        );
844
845        // --- Şasi (Chassis) ---
846        // 1000 kg, sürtünme önemsiz, dinamik
847        let mut chassis_rb = RigidBody::new(1000.0, 0.1, 0.5, true);
848        chassis_rb.wake_up();
849        let chassis_col = Collider::box_collider(Vec3::new(1.0, 0.5, 2.0)); // Genişlik 2, Yükseklik 1, Uzunluk 4 (Yarıçaplar)
850        chassis_rb.update_inertia_from_collider(&chassis_col);
851        let chassis_entity = Entity::new(1, 0);
852        let chassis_pos = Vec3::new(0.0, 1.5, 0.0);
853        world.add_body(
854            chassis_entity,
855            chassis_rb,
856            Transform::new(chassis_pos),
857            Velocity::default(),
858            chassis_col,
859        );
860
861        // Tekerlek Şablonu
862        let wheel_radius = 0.5;
863        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)
864        wheel_rb.wake_up();
865        let wheel_col = Collider::sphere(wheel_radius);
866        wheel_rb.update_inertia_from_collider(&wheel_col);
867
868        let wheel_offsets = vec![
869            Vec3::new(-1.2, -0.2, 1.5),  // Sol Ön
870            Vec3::new(1.2, -0.2, 1.5),   // Sağ Ön
871            Vec3::new(-1.2, -0.2, -1.5), // Sol Arka
872            Vec3::new(1.2, -0.2, -1.5),  // Sağ Arka
873        ];
874
875        let mut wheel_entities = Vec::new();
876
877        for (i, offset) in wheel_offsets.iter().enumerate() {
878            let wheel_entity = Entity::new(2 + i as u32, 0);
879            wheel_entities.push(wheel_entity);
880
881            world.add_body(
882                wheel_entity,
883                wheel_rb.clone(),
884                Transform::new(chassis_pos + *offset),
885                Velocity::default(),
886                wheel_col.clone(),
887            );
888
889            // Menteşe Eklemi (Hinge Joint) oluştur
890            let is_rear = i >= 2;
891            let hinge_data = HingeJointData {
892                axis: Vec3::X, // Tekerlekler X ekseni etrafında dönecek
893                use_limits: false,
894                lower_limit: 0.0,
895                upper_limit: 0.0,
896                use_motor: is_rear, // Sadece arka tekerleklerde motor var
897                motor_target_velocity: if is_rear { 10.0 } else { 0.0 }, // İleri doğru 10 rad/s
898                motor_max_force: if is_rear { 10000.0 } else { 0.0 }, // 10000 N güç
899                current_angle: 0.0,
900            };
901
902            let joint = Joint {
903                entity_a: chassis_entity,
904                entity_b: wheel_entity,
905                local_anchor_a: *offset, // Şasinin lokal uzayında bağlantı noktası
906                local_anchor_b: Vec3::ZERO, // Tekerleğin tam ortası
907                break_force: f32::MAX, // Asla kopmasın
908                break_torque: f32::MAX,
909                is_broken: false,
910                collision_enabled: false, // Şasi ile tekerlek çarpışmasın
911                data: JointData::Hinge(hinge_data),
912            };
913
914            world.joints.push(joint);
915        }
916        // --- Simülasyon ---
917        // Motorlar çalışacak ve arabayı 5 saniye boyunca (300 kare) ileri doğru (Z+) sürecek
918        for _ in 0..300 {
919            let _ = world.step(1.0 / 60.0);
920        }
921        // Doğrulama
922        let final_chassis_pos = world.transforms[1].position;
923        
924        // 1. İleri Sürüş: Araba Z ekseninde (ileri) hareket etmiş olmalı
925        assert!(
926            final_chassis_pos.z > 3.0,
927            "Araba yeterince ileri gidemedi! Motor veya sürtünme çalışmıyor. Z pozisyonu: {}",
928            final_chassis_pos.z
929        );
930
931        // 2. Denge (Devrilmeme): Arabanın Y pozisyonu stabil kalmalı (uçmamalı veya batmamalı)
932        // Başlangıç Y: 1.5, Tekerlek yarıçapı 0.5. Araba yere oturunca Y ~1.0 - 1.2 civarı olmalı
933        assert!(
934            final_chassis_pos.y > 0.5 && final_chassis_pos.y < 2.0,
935            "Araba devrildi, uçtu veya yere battı! Y pozisyonu: {}",
936            final_chassis_pos.y
937        );
938
939        // 3. X Ekseninde Düz Gitme (Sağa sola savrulmama)
940        assert!(
941            final_chassis_pos.x.abs() < 1.0,
942            "Araba düz gidemedi, sağa sola savruldu! X pozisyonu: {}",
943            final_chassis_pos.x
944        );
945    }
946}