Skip to main content

gizmo_engine/
spawner.rs

1use crate::color::Color;
2/// Bevy benzeri Command API — setup closure içinde tek satırla nesne spawn etmek için.
3///
4/// # Örnek
5/// ```rust,ignore
6/// .set_setup(|world, renderer| {
7///     let mut cmd = Commands::new(world, renderer);
8///     cmd.spawn_cube(Vec3::new(0.0, 0.0, -10.0), Color::RED).with_name("Oyuncu");
9///     cmd.spawn_camera(Vec3::new(0.0, 2.0, 5.0));
10/// })
11/// ```
12use gizmo_core::{Entity, EntityName, World};
13use gizmo_math::{Quat, Vec3};
14use gizmo_physics::{
15    components::{Collider, RigidBody, Velocity},
16    Transform,
17};
18use gizmo_renderer::{
19    asset::AssetManager,
20    components::{Camera, DirectionalLight, Material, MeshRenderer, PointLight},
21    Renderer,
22};
23
24// ─── Commands ─────────────────────────────────────────────────────────────────
25
26pub struct Commands<'a> {
27    pub world: &'a mut World,
28    pub renderer: &'a Renderer,
29    pub asset_manager: Option<AssetManager>,
30}
31
32impl<'a> Drop for Commands<'a> {
33    fn drop(&mut self) {
34        if let Some(am) = self.asset_manager.take() {
35            self.world.insert_resource(am);
36        }
37    }
38}
39
40impl<'a> Commands<'a> {
41    pub fn new(world: &'a mut World, renderer: &'a Renderer) -> Self {
42        let am = world.remove_resource::<AssetManager>().unwrap_or_default();
43        Self {
44            world,
45            renderer,
46            asset_manager: Some(am),
47        }
48    }
49
50    // ── Primitifler ────────────────────────────────────────────────────────────
51
52    /// Tek satırda renkli bir küp spawn eder. Builder zinciriyle `.with_name()` eklenebilir.
53    pub fn spawn_cube(&mut self, pos: Vec3, color: Color) -> EntityBuilder<'_, 'a> {
54        let mesh = AssetManager::create_cube(&self.renderer.device);
55        let bg = self.asset_manager.as_mut().unwrap().create_white_texture(
56            &self.renderer.device,
57            &self.renderer.queue,
58            &self.renderer.scene.texture_bind_group_layout,
59        );
60        let mat = Material::new(bg).with_unlit(color.to_vec4());
61        let id = spawn_mesh_entity(self.world, pos, mesh, mat);
62        EntityBuilder {
63            commands: self,
64            entity: id,
65        }
66    }
67
68    /// Tek satırda renkli bir küre spawn eder.
69    pub fn spawn_sphere(&mut self, pos: Vec3, radius: f32, color: Color) -> EntityBuilder<'_, 'a> {
70        let mesh = AssetManager::create_sphere(&self.renderer.device, radius, 20, 20);
71        let bg = self.asset_manager.as_mut().unwrap().create_white_texture(
72            &self.renderer.device,
73            &self.renderer.queue,
74            &self.renderer.scene.texture_bind_group_layout,
75        );
76        let mat = Material::new(bg).with_unlit(color.to_vec4());
77        let id = spawn_mesh_entity(self.world, pos, mesh, mat);
78        EntityBuilder {
79            commands: self,
80            entity: id,
81        }
82    }
83
84    /// Tek satırda düzlemsel bir zemin spawn eder.
85    pub fn spawn_plane(&mut self, pos: Vec3, size: f32, color: Color) -> EntityBuilder<'_, 'a> {
86        let mesh = AssetManager::create_plane(&self.renderer.device, size);
87        let bg = self.asset_manager.as_mut().unwrap().create_white_texture(
88            &self.renderer.device,
89            &self.renderer.queue,
90            &self.renderer.scene.texture_bind_group_layout,
91        );
92        let mat = Material::new(bg).with_unlit(color.to_vec4());
93        let id = spawn_mesh_entity(self.world, pos, mesh, mat);
94        EntityBuilder {
95            commands: self,
96            entity: id,
97        }
98    }
99
100    /// Diskten bir .obj modeli yükler ve spawn eder.
101    pub fn spawn_model(&mut self, pos: Vec3, path: &str) -> EntityBuilder<'_, 'a> {
102        let mesh = self
103            .asset_manager
104            .as_mut()
105            .unwrap()
106            .load_obj(&self.renderer.device, path);
107        let bg = self.asset_manager.as_mut().unwrap().create_white_texture(
108            &self.renderer.device,
109            &self.renderer.queue,
110            &self.renderer.scene.texture_bind_group_layout,
111        );
112        let mat = Material::new(bg);
113        let id = spawn_mesh_entity(self.world, pos, mesh, mat);
114        EntityBuilder {
115            commands: self,
116            entity: id,
117        }
118    }
119
120    // ── Kamera ────────────────────────────────────────────────────────────────
121
122    /// Birincil (primary) 3D perspektif kamera spawn eder.
123    /// `yaw = -π/2` (−X'e bakıyor), `pitch = 0` (düz).
124    pub fn spawn_camera(&mut self, pos: Vec3) -> EntityBuilder<'_, 'a> {
125        if let Some(mut cameras) = self.world.query::<gizmo_core::prelude::Mut<Camera>>() {
126            for (_, mut c) in cameras.iter_mut() {
127                c.primary = false;
128            }
129        }
130        let id = self.world.spawn();
131        let trans = Transform::new(pos);
132
133        self.world.add_component(id, trans);
134        self.world.add_component(
135            id,
136            Camera {
137                fov: 60.0_f32.to_radians(),
138                near: 0.1,
139                far: 1000.0,
140                yaw: -std::f32::consts::FRAC_PI_2,
141                pitch: 0.0,
142                primary: true,
143            },
144        );
145        EntityBuilder {
146            commands: self,
147            entity: id,
148        }
149    }
150
151    /// `fov` (derece), `near`, `far` özelleştirilebilir kamera.
152    pub fn spawn_camera_with(
153        &mut self,
154        pos: Vec3,
155        fov_deg: f32,
156        near: f32,
157        far: f32,
158    ) -> EntityBuilder<'_, 'a> {
159        if let Some(mut cameras) = self.world.query::<gizmo_core::prelude::Mut<Camera>>() {
160            for (_, mut c) in cameras.iter_mut() {
161                c.primary = false;
162            }
163        }
164        let id = self.world.spawn();
165        let trans = Transform::new(pos);
166
167        self.world.add_component(id, trans);
168        self.world.add_component(
169            id,
170            Camera {
171                fov: fov_deg.to_radians(),
172                near,
173                far,
174                yaw: -std::f32::consts::FRAC_PI_2,
175                pitch: 0.0,
176                primary: true,
177            },
178        );
179        EntityBuilder {
180            commands: self,
181            entity: id,
182        }
183    }
184
185    // ── Işıklar ─────────────────────────────────────────────────────────────────────────
186
187    /// Point light (nokta ışık) spawn eder.
188    pub fn spawn_point_light(
189        &mut self,
190        pos: Vec3,
191        color: Color,
192        intensity: f32,
193    ) -> EntityBuilder<'_, 'a> {
194        let id = self.world.spawn();
195        let trans = Transform::new(pos);
196
197        self.world.add_component(id, trans);
198        self.world.add_component(
199            id,
200            PointLight::new(
201                gizmo_math::Vec3::new(color.0.x, color.0.y, color.0.z),
202                intensity,
203                10.0,
204            ),
205        );
206        EntityBuilder {
207            commands: self,
208            entity: id,
209        }
210    }
211
212    /// Directional light (güneş/ay) spawn eder.
213    /// `direction`: normallenmiş ışık yönü (aşağı bakan = negatif Y).
214    pub fn spawn_sun(
215        &mut self,
216        _direction: Vec3,
217        color: Color,
218        intensity: f32,
219    ) -> EntityBuilder<'_, 'a> {
220        let id = self.world.spawn();
221        let pos = Vec3::ZERO; // DirectionalLight position is largely irrelevant
222        let trans = Transform::new(pos);
223
224        self.world.add_component(id, trans);
225        self.world.add_component(
226            id,
227            DirectionalLight {
228                color: Vec3::new(color.0.x, color.0.y, color.0.z),
229                intensity,
230                role: crate::renderer::components::LightRole::Sun,
231            },
232        );
233        EntityBuilder {
234            commands: self,
235            entity: id,
236        }
237    }
238
239    // ── Sahne Yardımcıları ────────────────────────────────────────────────────────────
240
241    /// Skybox spawn eder (ters yüzlü çok büyük küp). Renk arka plan rengini belirler.
242    pub fn spawn_skybox(&mut self, color: Color) -> EntityBuilder<'_, 'a> {
243        // Skip existing check since is_skybox is removed
244
245        // Wait, best approach for skybox is ignoring the duplication request if exists, but we must return an EntityBuilder...
246        let mesh = AssetManager::create_inverted_cube(&self.renderer.device);
247        let bg = self.asset_manager.as_mut().unwrap().create_white_texture(
248            &self.renderer.device,
249            &self.renderer.queue,
250            &self.renderer.scene.texture_bind_group_layout,
251        );
252        let mat = Material::new(bg).with_skybox().with_unlit(color.to_vec4());
253        let id = self.world.spawn();
254        let mut trans = Transform::new(Vec3::ZERO);
255        trans.scale = Vec3::new(500.0, 500.0, 500.0);
256        trans.update_local_matrix();
257
258        self.world.add_component(id, trans);
259        self.world.add_component(id, mesh);
260        self.world.add_component(id, mat);
261        self.world.add_component(id, MeshRenderer::new());
262        EntityBuilder {
263            commands: self,
264            entity: id,
265        }
266    }
267
268    // ── Fizik Spawn ─────────────────────────────────────────────────────────────────────────
269
270    /// Fizik simulasyonuna katılan dinamik bir küp spawn eder.
271    /// `half_extents`: Her eksende yarı boyut. `mass`: kg cinsinden kütle (0 = statik).
272    pub fn spawn_rigid_cube(
273        &mut self,
274        pos: Vec3,
275        half_extents: Vec3,
276        color: Color,
277        mass: f32,
278    ) -> EntityBuilder<'_, 'a> {
279        let mesh = AssetManager::create_cube(&self.renderer.device);
280        let bg = self.asset_manager.as_mut().unwrap().create_white_texture(
281            &self.renderer.device,
282            &self.renderer.queue,
283            &self.renderer.scene.texture_bind_group_layout,
284        );
285        let mat = Material::new(bg).with_unlit(color.to_vec4());
286        let id = spawn_mesh_entity(self.world, pos, mesh, mat);
287        // Scale'i half_extents ile eşleştir
288        {
289            let mut trans_store = self.world.borrow_mut::<Transform>();
290            if let Some(trans) = trans_store.get_mut(id.id()) {
291                trans.scale = half_extents * 2.0;
292                trans.update_local_matrix();
293            }
294        }
295        let mut rb = if mass > 0.0 {
296            RigidBody::new(mass, 0.3, 0.5, true)
297        } else {
298            RigidBody::new_static()
299        };
300        let col = Collider::box_collider(half_extents);
301        rb.update_inertia_from_collider(&col);
302        self.world.add_component(id, rb);
303        if mass > 0.0 {
304            self.world.add_component(id, Velocity::new(Vec3::ZERO));
305        }
306        self.world.add_component(id, col);
307        EntityBuilder {
308            commands: self,
309            entity: id,
310        }
311    }
312
313    /// Fizik simulasyonuna katılan dinamik bir küre spawn eder.
314    pub fn spawn_rigid_sphere(
315        &mut self,
316        pos: Vec3,
317        radius: f32,
318        color: Color,
319        mass: f32,
320    ) -> EntityBuilder<'_, 'a> {
321        let mesh = AssetManager::create_sphere(&self.renderer.device, radius, 16, 16);
322        let bg = self.asset_manager.as_mut().unwrap().create_white_texture(
323            &self.renderer.device,
324            &self.renderer.queue,
325            &self.renderer.scene.texture_bind_group_layout,
326        );
327        let mat = Material::new(bg).with_unlit(color.to_vec4());
328        let id = spawn_mesh_entity(self.world, pos, mesh, mat);
329        let mut rb = if mass > 0.0 {
330            RigidBody::new(mass, 0.3, 0.5, true)
331        } else {
332            RigidBody::new_static()
333        };
334        let col = Collider::sphere(radius);
335        rb.update_inertia_from_collider(&col);
336        self.world.add_component(id, rb);
337        if mass > 0.0 {
338            self.world.add_component(id, Velocity::new(Vec3::ZERO));
339        }
340        self.world.add_component(id, col);
341        EntityBuilder {
342            commands: self,
343            entity: id,
344        }
345    }
346
347    /// Statik (hareket etmeyen) zemin düzlemi spawn eder.
348    pub fn spawn_static_plane(
349        &mut self,
350        pos: Vec3,
351        size: f32,
352        color: Color,
353    ) -> EntityBuilder<'_, 'a> {
354        let mesh = AssetManager::create_plane(&self.renderer.device, size);
355        let bg = self.asset_manager.as_mut().unwrap().create_white_texture(
356            &self.renderer.device,
357            &self.renderer.queue,
358            &self.renderer.scene.texture_bind_group_layout,
359        );
360        let mat = Material::new(bg).with_pbr(color.to_vec4(), 0.9, 0.0);
361        let id = spawn_mesh_entity(self.world, pos, mesh, mat);
362        self.world.add_component(id, RigidBody::new_static());
363        self.world.add_component(
364            id,
365            Collider::box_collider(Vec3::new(size / 2.0, 0.05, size / 2.0)),
366        );
367        EntityBuilder {
368            commands: self,
369            entity: id,
370        }
371    }
372
373    // ── Görsel Yardımcılar ──────────────────────────────────────────────────────────────────────
374
375    /// Textureli bir materyal yükler ve bir küpe uygular.
376    pub fn spawn_textured_cube(&mut self, pos: Vec3, texture_path: &str) -> EntityBuilder<'_, 'a> {
377        let mesh = AssetManager::create_cube(&self.renderer.device);
378        let bg = self
379            .asset_manager
380            .as_mut()
381            .unwrap()
382            .load_material_texture(
383                &self.renderer.device,
384                &self.renderer.queue,
385                &self.renderer.scene.texture_bind_group_layout,
386                texture_path,
387            )
388            .unwrap_or_else(|_| {
389                self.asset_manager.as_mut().unwrap().create_white_texture(
390                    &self.renderer.device,
391                    &self.renderer.queue,
392                    &self.renderer.scene.texture_bind_group_layout,
393                )
394            });
395        let mat = Material::new(bg);
396        let id = spawn_mesh_entity(self.world, pos, mesh, mat);
397        EntityBuilder {
398            commands: self,
399            entity: id,
400        }
401    }
402
403    /// Textureli bir materyal yükler ve bir düzleme uygular.
404    pub fn spawn_textured_plane(
405        &mut self,
406        pos: Vec3,
407        size: f32,
408        texture_path: &str,
409    ) -> EntityBuilder<'_, 'a> {
410        let mesh = AssetManager::create_plane(&self.renderer.device, size);
411        let bg = self
412            .asset_manager
413            .as_mut()
414            .unwrap()
415            .load_material_texture(
416                &self.renderer.device,
417                &self.renderer.queue,
418                &self.renderer.scene.texture_bind_group_layout,
419                texture_path,
420            )
421            .unwrap_or_else(|_| {
422                self.asset_manager.as_mut().unwrap().create_white_texture(
423                    &self.renderer.device,
424                    &self.renderer.queue,
425                    &self.renderer.scene.texture_bind_group_layout,
426                )
427            });
428        let mat = Material::new(bg);
429        let id = spawn_mesh_entity(self.world, pos, mesh, mat);
430        EntityBuilder {
431            commands: self,
432            entity: id,
433        }
434    }
435
436    // ── GLTF Yükleme ─────────────────────────────────────────────────────────────────────────────
437
438    /// GLTF/GLB dosyasını yükler ve dünya içinde spawn eder.
439    /// Animasyon ve iskelet hiyerarşisi otomatik oluşturulur.
440    pub fn spawn_gltf(
441        &mut self,
442        pos: Vec3,
443        path: &str,
444        attach_colliders: bool,
445    ) -> Result<EntityBuilder<'_, 'a>, String> {
446        let default_bg = self.asset_manager.as_mut().unwrap().create_white_texture(
447            &self.renderer.device,
448            &self.renderer.queue,
449            &self.renderer.scene.texture_bind_group_layout,
450        );
451        let default_mat = Material::new(default_bg.clone());
452
453        match self.asset_manager.as_mut().unwrap().load_gltf_scene(
454            &self.renderer.device,
455            &self.renderer.queue,
456            &self.renderer.scene.texture_bind_group_layout,
457            default_bg,
458            path,
459        ) {
460            Ok(asset) => {
461                let root = self.world.spawn();
462                let mut trans = Transform::new(pos);
463                trans.update_local_matrix();
464
465                self.world.add_component(root, trans);
466                self.world
467                    .add_component(root, gizmo_physics::GlobalTransform::default());
468                self.world
469                    .add_component(root, EntityName(format!("GLTF: {}", path)));
470                self.world
471                    .add_component(root, gizmo_core::component::Children(Vec::new()));
472
473                for node in &asset.roots {
474                    spawn_gltf_node_flat(
475                        self.world,
476                        node,
477                        root.id(),
478                        default_mat.clone(),
479                        attach_colliders,
480                    );
481                }
482
483                if !asset.animations.is_empty() {
484                    self.world.add_component(
485                        root,
486                        gizmo_renderer::components::AnimationPlayer {
487                            active_animation: 0,
488                            current_time: 0.0,
489                            loop_anim: true,
490                            speed: 1.0,
491                            animations: std::sync::Arc::from(
492                                asset.animations.clone().into_boxed_slice(),
493                            ),
494                            ..Default::default()
495                        },
496                    );
497                }
498
499                Ok(EntityBuilder {
500                    commands: self,
501                    entity: root,
502                })
503            }
504            Err(e) => Err(format!(
505                "[Commands::spawn_gltf] '{}' yuklenemedi: {}",
506                path, e
507            )),
508        }
509    }
510
511    /// Asenkron GLTF yükleme tamamlandığında çağrılacak metot.
512    pub fn spawn_gltf_async_completed(
513        &mut self,
514        completion: gizmo_renderer::async_assets::GltfImportCompletion,
515        pos: Vec3,
516        attach_colliders: bool,
517    ) -> Result<EntityBuilder<'_, 'a>, String> {
518        let default_bg = self.asset_manager.as_mut().unwrap().create_white_texture(
519            &self.renderer.device,
520            &self.renderer.queue,
521            &self.renderer.scene.texture_bind_group_layout,
522        );
523        let default_mat = Material::new(default_bg.clone());
524
525        match self.asset_manager.as_mut().unwrap().load_gltf_from_import(
526            &self.renderer.device,
527            &self.renderer.queue,
528            &self.renderer.scene.texture_bind_group_layout,
529            default_bg,
530            &completion.path,
531            completion.document,
532            completion.buffers,
533            completion.images,
534        ) {
535            Ok(asset) => {
536                let root = self.world.spawn();
537                let mut trans = Transform::new(pos);
538                trans.update_local_matrix();
539
540                self.world.add_component(root, trans);
541                self.world
542                    .add_component(root, gizmo_physics::GlobalTransform::default());
543                self.world
544                    .add_component(root, EntityName(format!("GLTF: {}", completion.path)));
545                self.world
546                    .add_component(root, gizmo_core::component::Children(Vec::new()));
547
548                for node in &asset.roots {
549                    spawn_gltf_node_flat(
550                        self.world,
551                        node,
552                        root.id(),
553                        default_mat.clone(),
554                        attach_colliders,
555                    );
556                }
557
558                if !asset.animations.is_empty() {
559                    self.world.add_component(
560                        root,
561                        gizmo_renderer::components::AnimationPlayer {
562                            active_animation: 0,
563                            current_time: 0.0,
564                            loop_anim: true,
565                            speed: 1.0,
566                            animations: std::sync::Arc::from(
567                                asset.animations.clone().into_boxed_slice(),
568                            ),
569                            ..Default::default()
570                        },
571                    );
572                }
573
574                Ok(EntityBuilder {
575                    commands: self,
576                    entity: root,
577                })
578            }
579            Err(e) => Err(format!(
580                "[Commands::spawn_gltf_async_completed] '{}' yüklenemedi: {}",
581                completion.path, e
582            )),
583        }
584    }
585}
586
587// ─── EntityBuilder — Zincir API ───────────────────────────────────────────────
588
589/// Spawn edilen entity'e ek bileşenler eklemek için zincir builder.
590pub struct EntityBuilder<'b, 'a> {
591    commands: &'b mut Commands<'a>,
592    entity: Entity,
593}
594
595impl<'b, 'a> EntityBuilder<'b, 'a> {
596    /// Entity'e bir isim (tag) ata. Update içinde `world.entity_named("...")` ile bulunabilir.
597    pub fn with_name(self, name: &str) -> Self {
598        self.commands
599            .world
600            .add_component(self.entity, EntityName(name.to_string()));
601        self
602    }
603
604    /// Herhangi bir ek bileşen ekle.
605    pub fn with<C: gizmo_core::Component + 'static>(self, component: C) -> Self {
606        self.commands.world.add_component(self.entity, component);
607        self
608    }
609
610    /// Entity ID'sini tüket ve döndür.
611    pub fn id(self) -> Entity {
612        self.entity
613    }
614}
615
616impl<'b, 'a> From<EntityBuilder<'b, 'a>> for Entity {
617    fn from(b: EntityBuilder<'b, 'a>) -> Entity {
618        b.entity
619    }
620}
621
622// ─── Yardımcı: Mesh entity oluştur ────────────────────────────────────────────────────────────────
623
624fn spawn_mesh_entity(
625    world: &mut World,
626    pos: Vec3,
627    mesh: gizmo_renderer::components::Mesh,
628    mat: Material,
629) -> Entity {
630    let id = world.spawn();
631    let mut trans = Transform::new(pos);
632    trans.update_local_matrix();
633
634    world.add_component(id, trans);
635    world.add_component(id, mesh);
636    world.add_component(id, mat);
637    world.add_component(id, MeshRenderer::new());
638    id
639}
640
641/// GLTF hiyerarşisini düz (flat) olarak spawn eder — parent/child olmadan.
642fn spawn_gltf_node_flat(
643    world: &mut World,
644    node: &gizmo_renderer::asset::GltfNodeData,
645    parent_id: u32,
646    default_mat: Material,
647    attach_colliders: bool,
648) {
649    use gizmo_core::component::{Children, Parent};
650    let entity = world.spawn();
651    let name = node.name.clone().unwrap_or_else(|| "GLTF_Node".to_string());
652    world.add_component(entity, EntityName(name));
653    world.add_component(entity, Parent(parent_id));
654    world.add_component(entity, Children(Vec::new()));
655
656    {
657        let mut ch_store = world.borrow_mut::<Children>();
658        // Safe to push since entity just spawned and didn't trigger any complex re-borrow updates
659        if let Some(parent_ch) = ch_store.get_mut(parent_id) {
660            parent_ch.0.push(entity.id());
661        }
662    }
663
664    let raw_rot = Quat::from_xyzw(
665        node.rotation[0],
666        node.rotation[1],
667        node.rotation[2],
668        node.rotation[3],
669    );
670    let rot = if raw_rot.is_nan() || raw_rot.length() < 0.0001 {
671        Quat::IDENTITY
672    } else {
673        raw_rot.normalize()
674    };
675
676    let t = Transform::new(Vec3::new(
677        node.translation[0],
678        node.translation[1],
679        node.translation[2],
680    ))
681    .with_rotation(rot)
682    .with_scale(Vec3::new(node.scale[0], node.scale[1], node.scale[2]));
683    world.add_component(entity, t);
684    world.add_component(entity, gizmo_physics::GlobalTransform::default());
685
686    let mut newly_added_prims = Vec::new();
687    for (mesh, mat_opt) in node.primitives.iter() {
688        let prim = world.spawn();
689        world.add_component(prim, Transform::new(Vec3::ZERO));
690        world.add_component(prim, gizmo_physics::GlobalTransform::default());
691        world.add_component(prim, Parent(entity.id()));
692        world.add_component(prim, Children(Vec::new()));
693
694        newly_added_prims.push(prim.id());
695
696        world.add_component(prim, mesh.clone());
697        world.add_component(prim, mat_opt.clone().unwrap_or_else(|| default_mat.clone()));
698        world.add_component(prim, MeshRenderer::new());
699
700        if attach_colliders {
701            let extents = (mesh.bounds.max - mesh.bounds.min) / 2.0;
702            let cx = extents.x.max(0.01);
703            let cy = extents.y.max(0.01);
704            let cz = extents.z.max(0.01);
705            world.add_component(prim, gizmo_physics::shape::Collider::new_aabb(cx, cy, cz));
706        }
707    }
708
709    // Pulling borrow_mut OUTSIDE the loop avoiding multiple overlapping mutable queries
710    if !newly_added_prims.is_empty() {
711        {
712            let mut ch_store = world.borrow_mut::<Children>();
713            if let Some(parent_ch) = ch_store.get_mut(entity.id()) {
714                parent_ch.0.extend(newly_added_prims);
715            }
716        }
717    }
718
719    for child_node in &node.children {
720        spawn_gltf_node_flat(
721            world,
722            child_node,
723            entity.id(),
724            default_mat.clone(),
725            attach_colliders,
726        );
727    }
728}
729
730// ─── WorldExt Trait — Update içinde kısa sorgular ─────────────────────────────
731
732/// World üzerine eklenen kolaylık metodları.
733/// `use gizmo::prelude::*;` ile otomatik içeri alınır.
734pub trait WorldExt {
735    /// İsme göre Entity ID'sini (u32) bul.
736    fn entity_named(&self, name: &str) -> Option<u32>;
737
738    /// İsme göre entity'nin Transform'unu değiştir. Transform matrisi otomatik güncellenir.
739    fn move_entity_named<F: FnMut(&mut gizmo_physics::Transform)>(&mut self, name: &str, f: F);
740
741    /// İsme göre entity'nin dünya pozisyonunu al.
742    fn position_of(&self, name: &str) -> Option<Vec3>;
743
744    /// İsme göre herhangi bir bileşeni değiştir.
745    ///
746    /// # Örnek
747    /// ```rust,ignore
748    /// world.modify::<Camera>("Kamera", |cam| { cam.fov = 90.0_f32.to_radians(); });
749    /// world.modify::<Material>("Top", |mat| { mat.albedo = Color::BLUE.to_vec4(); });
750    /// ```
751    fn modify<T: gizmo_core::Component + 'static, F: FnMut(&mut T)>(&mut self, name: &str, f: F);
752}
753
754impl WorldExt for World {
755    fn entity_named(&self, name: &str) -> Option<u32> {
756        let mut names = self.query::<&EntityName>()?;
757        for (id, n) in names.iter_mut() {
758            if n.0 == name {
759                return Some(id);
760            }
761        }
762        None
763    }
764
765    fn move_entity_named<F: FnMut(&mut gizmo_physics::Transform)>(&mut self, name: &str, mut f: F) {
766        let target: Option<u32> = {
767            if let Some(mut names) = self.query::<&EntityName>() {
768                let mut found = None;
769                for (id, n) in names.iter_mut() {
770                    if n.0 == name {
771                        found = Some(id);
772                        break;
773                    }
774                }
775                found
776            } else {
777                None
778            }
779        };
780        if let Some(target_id) = target {
781            if let Some(mut transforms) =
782                self.query::<gizmo_core::prelude::Mut<gizmo_physics::Transform>>()
783            {
784                for (tid, mut trans) in transforms.iter_mut() {
785                    if tid == target_id {
786                        f(&mut trans);
787                        trans.update_local_matrix();
788                    }
789                }
790            }
791        }
792    }
793
794    fn position_of(&self, name: &str) -> Option<Vec3> {
795        let target_id = self.entity_named(name)?;
796        let transforms = self.borrow::<gizmo_physics::components::Transform>();
797        transforms.get(target_id).map(|t| t.position)
798    }
799
800    fn modify<T: gizmo_core::Component + 'static, F: FnMut(&mut T)>(
801        &mut self,
802        name: &str,
803        mut f: F,
804    ) {
805        let target: Option<u32> = {
806            if let Some(mut names) = self.query::<&EntityName>() {
807                let mut found = None;
808                for (id, n) in names.iter_mut() {
809                    if n.0 == name {
810                        found = Some(id);
811                        break;
812                    }
813                }
814                found
815            } else {
816                None
817            }
818        };
819        if let Some(target_id) = target {
820            {
821                let mut storage = self.borrow_mut::<T>();
822                if let Some(comp) = storage.get_mut(target_id) {
823                    f(comp);
824                }
825            }
826        }
827    }
828}
829
830// ─── InputExt Trait — KeyCode doğrudan kabul eden kısaltmalar ─────────────────
831// gizmo-core'da winit bağımlılığı olmadığı için bu trait gizmo crate'inde tanımlıdır.
832
833/// `Input` üzerine eklenen ergonomik metodlar.
834/// `use gizmo::prelude::*;` ile otomatik içeri alınır.
835///
836/// # Örnek
837/// ```rust,ignore
838/// if input.pressed(Key::KeyW) { trans.position.z -= 5.0 * dt; }
839/// if input.just_pressed(Key::Space) { player.jump(); }
840/// ```
841pub trait InputExt {
842    /// Tuş basılı mı? `Key::KeyW`, `Key::Space` gibi `KeyCode` varyantlarını doğrudan alır.
843    fn pressed(&self, keycode: winit::keyboard::KeyCode) -> bool;
844
845    /// Tuş bu frame'de ilk kez mi basıldı? (tek seferlik tetikleme)
846    fn just_pressed(&self, keycode: winit::keyboard::KeyCode) -> bool;
847
848    /// Tuş bu frame'de mi bırakıldı?
849    fn just_released(&self, keycode: winit::keyboard::KeyCode) -> bool;
850}
851
852impl InputExt for gizmo_core::input::Input {
853    #[inline]
854    fn pressed(&self, keycode: winit::keyboard::KeyCode) -> bool {
855        self.is_key_pressed(keycode as u32)
856    }
857    #[inline]
858    fn just_pressed(&self, keycode: winit::keyboard::KeyCode) -> bool {
859        self.is_key_just_pressed(keycode as u32)
860    }
861    #[inline]
862    fn just_released(&self, keycode: winit::keyboard::KeyCode) -> bool {
863        self.is_key_just_released(keycode as u32)
864    }
865}