Skip to main content

gizmo_engine/
bundles.rs

1//! Bevy tarzı önceden tanımlanmış Bundle yapıları.
2//!
3//! Bir entity'ye birden fazla bileşeni tek seferde eklemek için kullanılır.
4//!
5//! ```ignore
6//! world.spawn_bundle(CameraBundle {
7//!     position: Vec3::new(0.0, 3.0, 10.0),
8//!     fov: 60.0_f32.to_radians(),
9//!     ..default()
10//! });
11//! ```
12
13use crate::core::{Bundle, Entity, EntityName, World};
14use crate::math::{Quat, Vec3};
15use gizmo_physics_core::Transform;
16use crate::renderer::components::{
17    Camera, DirectionalLight, LightRole, Material, Mesh, MeshRenderer, PointLight, SpotLight,
18};
19
20// ============================================================
21//  DirectionalLightBundle
22// ============================================================
23
24/// Yönlü ışık (güneş) için hazır bundle.
25#[derive(Debug, Clone, Copy, PartialEq)]
26pub struct DirectionalLightBundle {
27    pub rotation: Quat,
28    pub color: Vec3,
29    pub intensity: f32,
30    pub role: LightRole,
31}
32
33impl Default for DirectionalLightBundle {
34    fn default() -> Self {
35        Self {
36            rotation: Quat::from_rotation_x(-std::f32::consts::PI / 4.0),
37            color: Vec3::new(1.0, 1.0, 1.0),
38            intensity: 3.0,
39            role: LightRole::Sun,
40        }
41    }
42}
43
44impl Bundle for DirectionalLightBundle {
45    fn get_infos() -> Vec<gizmo_core::archetype::ComponentInfo> { vec![] }
46    unsafe fn write_to_archetype(self, _arch: &mut gizmo_core::archetype::Archetype, _row: usize, _tick: u32) {}
47    fn apply(self, world: &mut World, entity: Entity) {
48        world.add_component(
49            entity,
50            Transform::new(Vec3::ZERO).with_rotation(self.rotation),
51        );
52        world.add_component(entity, gizmo_physics_core::components::GlobalTransform::default());
53        world.add_component(
54            entity,
55            DirectionalLight::new(self.color, self.intensity, self.role),
56        );
57    }
58}
59
60// ============================================================
61//  PointLightBundle
62// ============================================================
63
64/// Nokta ışığı için hazır bundle.
65#[derive(Debug, Clone, Copy, PartialEq)]
66pub struct PointLightBundle {
67    pub position: Vec3,
68    pub color: Vec3,
69    pub intensity: f32,
70    pub radius: f32,
71}
72
73impl Default for PointLightBundle {
74    fn default() -> Self {
75        Self {
76            position: Vec3::ZERO,
77            color: Vec3::new(1.0, 1.0, 1.0),
78            intensity: 5.0,
79            radius: 20.0,
80        }
81    }
82}
83
84impl Bundle for PointLightBundle {
85    fn get_infos() -> Vec<gizmo_core::archetype::ComponentInfo> { vec![] }
86    unsafe fn write_to_archetype(self, _arch: &mut gizmo_core::archetype::Archetype, _row: usize, _tick: u32) {}
87    fn apply(self, world: &mut World, entity: Entity) {
88        world.add_component(entity, Transform::new(self.position));
89        world.add_component(entity, gizmo_physics_core::components::GlobalTransform::default());
90        world.add_component(
91            entity,
92            PointLight::new(self.color, self.intensity, self.radius),
93        );
94    }
95}
96
97// ============================================================
98//  SpotLightBundle
99// ============================================================
100
101/// Spot ışığı için hazır bundle.
102#[derive(Debug, Clone, Copy, PartialEq)]
103pub struct SpotLightBundle {
104    pub position: Vec3,
105    pub rotation: Quat,
106    pub color: Vec3,
107    pub intensity: f32,
108    pub radius: f32,
109    pub inner_angle: f32,
110    pub outer_angle: f32,
111}
112
113impl Default for SpotLightBundle {
114    fn default() -> Self {
115        Self {
116            position: Vec3::ZERO,
117            rotation: Quat::IDENTITY,
118            color: Vec3::new(1.0, 1.0, 1.0),
119            intensity: 10.0,
120            radius: 30.0,
121            inner_angle: 0.4,
122            outer_angle: 0.6,
123        }
124    }
125}
126
127impl Bundle for SpotLightBundle {
128    fn get_infos() -> Vec<gizmo_core::archetype::ComponentInfo> { vec![] }
129    unsafe fn write_to_archetype(self, _arch: &mut gizmo_core::archetype::Archetype, _row: usize, _tick: u32) {}
130    fn apply(self, world: &mut World, entity: Entity) {
131        world.add_component(
132            entity,
133            Transform::new(self.position).with_rotation(self.rotation),
134        );
135        world.add_component(entity, gizmo_physics_core::components::GlobalTransform::default());
136        world.add_component(
137            entity,
138            SpotLight::new(
139                self.color,
140                self.intensity,
141                self.radius,
142                self.inner_angle,
143                self.outer_angle,
144            ),
145        );
146    }
147}
148
149// ============================================================
150//  CameraBundle
151// ============================================================
152
153/// Kamera için hazır bundle.
154#[derive(Debug, Clone, Copy, PartialEq)]
155pub struct CameraBundle {
156    pub position: Vec3,
157    pub fov: f32,
158    pub near: f32,
159    pub far: f32,
160    pub yaw: f32,
161    pub pitch: f32,
162    pub exposure: f32,
163    pub primary: bool,
164}
165
166impl Default for CameraBundle {
167    fn default() -> Self {
168        Self {
169            position: Vec3::new(0.0, 5.0, 10.0),
170            fov: std::f32::consts::FRAC_PI_3,
171            near: 0.1,
172            far: 1500.0,
173            yaw: 0.0,
174            pitch: 0.0,
175            exposure: 1.0,
176            primary: true,
177        }
178    }
179}
180
181impl Bundle for CameraBundle {
182    fn get_infos() -> Vec<gizmo_core::archetype::ComponentInfo> { vec![] }
183    unsafe fn write_to_archetype(self, _arch: &mut gizmo_core::archetype::Archetype, _row: usize, _tick: u32) {}
184    fn apply(self, world: &mut World, entity: Entity) {
185        world.add_component(entity, Transform::new(self.position));
186        world.add_component(entity, gizmo_physics_core::components::GlobalTransform::default());
187        let mut cam = Camera::new(
188            self.fov,
189            self.near,
190            self.far,
191            self.yaw,
192            self.pitch,
193            self.primary,
194        );
195        cam.exposure = self.exposure;
196        world.add_component(entity, cam);
197    }
198}
199
200// ============================================================
201//  MeshBundle
202// ============================================================
203
204/// Mesh + Material + MeshRenderer için hazır bundle.
205///
206/// ```ignore
207/// world.spawn_bundle(
208///     MeshBundle::new(renderer.create_cube(), my_material)
209///         .with_name("Oyuncu")
210///         .at(Vec3::new(0.0, 5.0, 0.0))
211/// );
212/// ```
213pub struct MeshBundle {
214    pub position: Vec3,
215    pub rotation: Quat,
216    pub scale: Vec3,
217    pub mesh: crate::core::asset::Handle<Mesh>,
218    pub material: crate::core::asset::Handle<Material>,
219    pub name: Option<String>,
220}
221
222impl MeshBundle {
223    /// Yeni bir MeshBundle oluşturur (mesh ve material zorunlu).
224    pub fn new(
225        mesh: crate::core::asset::Handle<Mesh>,
226        material: crate::core::asset::Handle<Material>,
227    ) -> Self {
228        Self {
229            position: Vec3::ZERO,
230            rotation: Quat::IDENTITY,
231            scale: Vec3::ONE,
232            mesh,
233            material,
234            name: None,
235        }
236    }
237
238    /// Pozisyon ayarlar.
239    pub fn at(mut self, position: Vec3) -> Self {
240        self.position = position;
241        self
242    }
243
244    /// Rotasyon ayarlar.
245    pub fn with_rotation(mut self, rotation: Quat) -> Self {
246        self.rotation = rotation;
247        self
248    }
249
250    /// Ölçek ayarlar.
251    pub fn with_scale(mut self, scale: Vec3) -> Self {
252        self.scale = scale;
253        self
254    }
255
256    /// İsim verir.
257    pub fn with_name(mut self, name: &str) -> Self {
258        self.name = Some(name.to_string());
259        self
260    }
261}
262
263impl Bundle for MeshBundle {
264    fn get_infos() -> Vec<gizmo_core::archetype::ComponentInfo> { vec![] }
265    unsafe fn write_to_archetype(self, _arch: &mut gizmo_core::archetype::Archetype, _row: usize, _tick: u32) {}
266    fn apply(self, world: &mut World, entity: Entity) {
267        world.add_component(
268            entity,
269            Transform::new(self.position)
270                .with_rotation(self.rotation)
271                .with_scale(self.scale),
272        );
273        world.add_component(entity, gizmo_physics_core::components::GlobalTransform::default());
274        world.add_component(entity, self.mesh);
275        world.add_component(entity, self.material);
276        world.add_component(entity, MeshRenderer::new());
277        if let Some(name) = self.name {
278            world.add_component(entity, EntityName(name));
279        }
280    }
281}
282
283// ============================================================
284//  RigidBodyBundle
285// ============================================================
286
287use gizmo_physics_core::Collider;
288use gizmo_physics_rigid::components::{RigidBody, Velocity};
289
290/// Fizik nesnesi oluşturmak için sıfır-yük (zero-overhead) Bundle.
291/// Velocity veya Collider eklemeyi unutma hatalarını önler.
292#[derive(Debug, Clone, PartialEq, Default)]
293pub struct RigidBodyBundle {
294    pub rigid_body: RigidBody,
295    pub velocity: Velocity,
296    pub collider: Collider,
297}
298
299
300impl RigidBodyBundle {
301    pub fn dynamic(mass: f32) -> Self {
302        Self {
303            rigid_body: RigidBody::new(mass, true),
304            ..Default::default()
305        }
306    }
307
308    pub fn static_body() -> Self {
309        Self {
310            rigid_body: RigidBody::new_static(),
311            ..Default::default()
312        }
313    }
314
315    /// Kinematic body — user-driven motion (moving platforms, scripted blades).
316    /// `new_kinematic` turns CCD on by default, so fast kinematic movers get
317    /// tunnelling prevention without a separate `.with_ccd()`.
318    pub fn kinematic() -> Self {
319        Self {
320            rigid_body: RigidBody::new_kinematic(),
321            ..Default::default()
322        }
323    }
324
325    pub fn with_collider(mut self, collider: Collider) -> Self {
326        self.collider = collider;
327        self
328    }
329
330    /// Give the body an initial linear velocity (the bundle otherwise spawns at rest).
331    pub fn with_velocity(mut self, linear: Vec3) -> Self {
332        self.velocity = Velocity::new(linear);
333        self
334    }
335
336    /// Enable Continuous Collision Detection: the body is swept against obstacles
337    /// each substep so it can't tunnel through thin/other geometry at high speed.
338    /// Off by default (discrete detection) — turn it on for bullets, fast balls,
339    /// anything that moves more than its own thickness per frame.
340    pub fn with_ccd(mut self) -> Self {
341        self.rigid_body.ccd_enabled = true;
342        self
343    }
344
345    /// Fiziksel hava direnci (½·ρ·Cd·A·v²) açar → düşen/uçan cisim doğal terminal hıza
346    /// oturur. `cd` sürükleme katsayısı (küre ~0.47, küp ~1.05), `area` frontal alan (m²).
347    /// Örn: `RigidBodyBundle::dynamic(2.0).with_air_drag(0.47, 0.5)`.
348    pub fn with_air_drag(mut self, cd: f32, area: f32) -> Self {
349        self.rigid_body = self.rigid_body.with_air_drag(cd, area);
350        self
351    }
352
353    /// Collider'ın zıplaklığını (restitution) ayarlar.
354    /// Örn: `RigidBodyBundle::dynamic(1.0).with_collider(Collider::sphere(0.5)).with_restitution(0.9)`.
355    pub fn with_restitution(mut self, restitution: f32) -> Self {
356        self.collider = self.collider.with_restitution(restitution);
357        self
358    }
359
360    /// Collider'ın sürtünmesini ayarlar (statik = dinamik).
361    pub fn with_friction(mut self, friction: f32) -> Self {
362        self.collider = self.collider.with_friction(friction);
363        self
364    }
365
366    /// Lineer + açısal sönümü ayarlar (kaba enerji kaybı). Gerçekçi hava direnci için
367    /// `with_air_drag`.
368    pub fn with_damping(mut self, linear: f32, angular: f32) -> Self {
369        self.rigid_body = self.rigid_body.with_damping(linear, angular);
370        self
371    }
372
373    /// Yerçekimini aç/kapat.
374    pub fn with_gravity(mut self, enabled: bool) -> Self {
375        self.rigid_body = self.rigid_body.with_gravity(enabled);
376        self
377    }
378
379    /// Kütle merkezini (gövde-yerel) ayarlar.
380    pub fn with_center_of_mass(mut self, com: Vec3) -> Self {
381        self.rigid_body = self.rigid_body.with_center_of_mass(com);
382        self
383    }
384
385    /// Üç dönme eksenini kilitler — cisim devrilmez (karakter, dik nesneler).
386    pub fn lock_rotation(mut self) -> Self {
387        self.rigid_body = self.rigid_body.lock_rotation();
388        self
389    }
390
391    /// Başlangıç açısal hızı verir (rad/s).
392    pub fn with_angular_velocity(mut self, angular: Vec3) -> Self {
393        self.velocity.angular = angular;
394        self
395    }
396}
397
398impl Bundle for RigidBodyBundle {
399    fn get_infos() -> Vec<gizmo_core::archetype::ComponentInfo> {
400        <(RigidBody, Velocity, Collider)>::get_infos()
401    }
402
403    unsafe fn write_to_archetype(self, arch: &mut gizmo_core::archetype::Archetype, row: usize, tick: u32) {
404        let mut rb = self.rigid_body;
405        rb.update_inertia_from_collider(&self.collider);
406        (rb, self.velocity, self.collider).write_to_archetype(arch, row, tick)
407    }
408
409    fn apply(self, world: &mut World, entity: Entity) {
410        let mut rb = self.rigid_body;
411        // Derive rotational inertia from the collider shape so callers don't have to
412        // remember `calculate_*_inertia` — the default inertia otherwise gives wrong
413        // spin dynamics. No-op for static/kinematic bodies (the calculators guard on
414        // `is_dynamic`), and idempotent if the caller already set a matching inertia.
415        rb.update_inertia_from_collider(&self.collider);
416        (rb, self.velocity, self.collider).apply(world, entity)
417    }
418}
419
420#[cfg(test)]
421mod tests {
422    use super::*;
423
424    #[test]
425    fn ergonomic_collider_and_bundle_builders() {
426        // Collider zıplaklık/sürtünme kısayolları — tam PhysicsMaterial kurmadan.
427        let c = Collider::sphere(0.5).with_restitution(0.9).with_friction(0.3);
428        assert_eq!(c.material.restitution, 0.9);
429        assert_eq!(c.material.static_friction, 0.3);
430        assert_eq!(c.material.dynamic_friction, 0.3);
431
432        // Bundle: collider + hava direnci + zıplaklık TEK zincirde.
433        let b = RigidBodyBundle::dynamic(2.0)
434            .with_collider(Collider::sphere(0.5))
435            .with_air_drag(0.47, 0.8)
436            .with_restitution(0.85);
437        assert_eq!(b.rigid_body.drag_coefficient, 0.47);
438        assert_eq!(b.rigid_body.drag_area, 0.8);
439        assert_eq!(b.collider.material.restitution, 0.85);
440
441        // Genişletilmiş akıcı set: damping + gravity + lock + COM + açısal hız tek zincirde.
442        let b2 = RigidBodyBundle::dynamic(1.0)
443            .with_damping(0.1, 0.2)
444            .with_gravity(false)
445            .lock_rotation()
446            .with_center_of_mass(Vec3::new(0.0, 0.5, 0.0))
447            .with_angular_velocity(Vec3::new(0.0, 3.0, 0.0));
448        assert_eq!(b2.rigid_body.linear_damping, 0.1);
449        assert!(!b2.rigid_body.use_gravity);
450        assert!(b2.rigid_body.lock_rotation_x);
451        assert_eq!(b2.rigid_body.center_of_mass, Vec3::new(0.0, 0.5, 0.0));
452        assert_eq!(b2.velocity.angular, Vec3::new(0.0, 3.0, 0.0));
453    }
454
455    #[test]
456    fn rigid_body_bundle_derives_inertia_from_collider() {
457        // Spawn purely via the bundle — no manual calculate_*_inertia.
458        let mut world = World::new();
459        let e = world
460            .spawn_bundle(RigidBodyBundle::dynamic(2.0).with_collider(Collider::sphere(0.5)));
461
462        let rbs = world.borrow::<RigidBody>();
463        let rb = rbs.get(e.id()).expect("rigid body spawned");
464
465        // Solid sphere: I = 0.4·m·r² = 0.4·2·0.25 = 0.2 per axis (calculate_sphere_inertia).
466        assert!(
467            (rb.local_inertia - Vec3::splat(0.2)).length() < 1e-6,
468            "bundle must derive sphere inertia from the collider, got {:?}",
469            rb.local_inertia
470        );
471        // Regression: must NOT be the un-derived default Vec3::splat(1.0).
472        assert!(
473            (rb.local_inertia - Vec3::splat(1.0)).length() > 1e-3,
474            "inertia must be derived, not left at the default"
475        );
476    }
477
478    #[test]
479    fn rigid_body_bundle_static_body_inertia_derivation_is_noop() {
480        // Static bodies must spawn fine; inertia derivation is a no-op for them.
481        let mut world = World::new();
482        let e = world
483            .spawn_bundle(RigidBodyBundle::static_body().with_collider(Collider::sphere(0.5)));
484        assert!(world.borrow::<RigidBody>().get(e.id()).is_some());
485    }
486}