Skip to main content

proof_engine/editor/
scene_editor.rs

1
2//! Scene editor — entity management, component inspector, scene serialization, prefab integration.
3
4use glam::{Vec2, Vec3, Vec4, Quat, Mat4};
5use std::collections::HashMap;
6
7// ---------------------------------------------------------------------------
8// Component system
9// ---------------------------------------------------------------------------
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub enum ComponentKind {
13    Transform,
14    MeshRenderer,
15    SkinnedMeshRenderer,
16    Camera,
17    DirectionalLight,
18    PointLight,
19    SpotLight,
20    AreaLight,
21    AudioSource,
22    AudioListener,
23    Rigidbody,
24    BoxCollider,
25    SphereCollider,
26    CapsuleCollider,
27    MeshCollider,
28    TerrainCollider,
29    Animator,
30    ParticleSystem,
31    TrailRenderer,
32    LineRenderer,
33    ReflectionProbe,
34    LightProbe,
35    NavMeshAgent,
36    NavMeshObstacle,
37    LodGroup,
38    DecalProjector,
39    PostProcessVolume,
40    BillboardRenderer,
41    SpeedTreeRenderer,
42    OcclusionCullingProxy,
43    SortingGroup,
44    Canvas,
45    TextMesh,
46    Sprite,
47    Script,
48    Custom,
49}
50
51impl ComponentKind {
52    pub fn label(self) -> &'static str {
53        match self {
54            ComponentKind::Transform => "Transform",
55            ComponentKind::MeshRenderer => "Mesh Renderer",
56            ComponentKind::SkinnedMeshRenderer => "Skinned Mesh Renderer",
57            ComponentKind::Camera => "Camera",
58            ComponentKind::DirectionalLight => "Directional Light",
59            ComponentKind::PointLight => "Point Light",
60            ComponentKind::SpotLight => "Spot Light",
61            ComponentKind::AreaLight => "Area Light",
62            ComponentKind::AudioSource => "Audio Source",
63            ComponentKind::AudioListener => "Audio Listener",
64            ComponentKind::Rigidbody => "Rigidbody",
65            ComponentKind::BoxCollider => "Box Collider",
66            ComponentKind::SphereCollider => "Sphere Collider",
67            ComponentKind::CapsuleCollider => "Capsule Collider",
68            ComponentKind::MeshCollider => "Mesh Collider",
69            ComponentKind::TerrainCollider => "Terrain Collider",
70            ComponentKind::Animator => "Animator",
71            ComponentKind::ParticleSystem => "Particle System",
72            ComponentKind::TrailRenderer => "Trail Renderer",
73            ComponentKind::LineRenderer => "Line Renderer",
74            ComponentKind::ReflectionProbe => "Reflection Probe",
75            ComponentKind::LightProbe => "Light Probe Group",
76            ComponentKind::NavMeshAgent => "Nav Mesh Agent",
77            ComponentKind::NavMeshObstacle => "Nav Mesh Obstacle",
78            ComponentKind::LodGroup => "LOD Group",
79            ComponentKind::DecalProjector => "Decal Projector",
80            ComponentKind::PostProcessVolume => "Post Process Volume",
81            ComponentKind::BillboardRenderer => "Billboard Renderer",
82            ComponentKind::SpeedTreeRenderer => "SpeedTree Renderer",
83            ComponentKind::OcclusionCullingProxy => "Occlusion Culling Proxy",
84            ComponentKind::SortingGroup => "Sorting Group",
85            ComponentKind::Canvas => "Canvas",
86            ComponentKind::TextMesh => "Text Mesh",
87            ComponentKind::Sprite => "Sprite",
88            ComponentKind::Script => "Script",
89            ComponentKind::Custom => "Custom",
90        }
91    }
92
93    pub fn category(self) -> &'static str {
94        match self {
95            ComponentKind::Transform => "Core",
96            ComponentKind::MeshRenderer | ComponentKind::SkinnedMeshRenderer
97            | ComponentKind::BillboardRenderer | ComponentKind::SpeedTreeRenderer => "Rendering",
98            ComponentKind::Camera => "Camera",
99            ComponentKind::DirectionalLight | ComponentKind::PointLight | ComponentKind::SpotLight | ComponentKind::AreaLight => "Lighting",
100            ComponentKind::AudioSource | ComponentKind::AudioListener => "Audio",
101            ComponentKind::Rigidbody | ComponentKind::BoxCollider | ComponentKind::SphereCollider
102            | ComponentKind::CapsuleCollider | ComponentKind::MeshCollider | ComponentKind::TerrainCollider => "Physics",
103            ComponentKind::Animator | ComponentKind::ParticleSystem | ComponentKind::TrailRenderer
104            | ComponentKind::LineRenderer => "Animation",
105            ComponentKind::ReflectionProbe | ComponentKind::LightProbe => "Lighting",
106            ComponentKind::NavMeshAgent | ComponentKind::NavMeshObstacle => "AI",
107            ComponentKind::LodGroup | ComponentKind::OcclusionCullingProxy | ComponentKind::SortingGroup => "Optimization",
108            ComponentKind::DecalProjector | ComponentKind::PostProcessVolume => "Effects",
109            ComponentKind::Canvas | ComponentKind::TextMesh | ComponentKind::Sprite => "UI",
110            ComponentKind::Script | ComponentKind::Custom => "Scripts",
111        }
112    }
113
114    pub fn icon(self) -> &'static str {
115        match self {
116            ComponentKind::Camera => "📷",
117            ComponentKind::DirectionalLight | ComponentKind::PointLight | ComponentKind::SpotLight => "💡",
118            ComponentKind::MeshRenderer | ComponentKind::SkinnedMeshRenderer => "⬡",
119            ComponentKind::Rigidbody => "⚙",
120            ComponentKind::AudioSource | ComponentKind::AudioListener => "♪",
121            ComponentKind::ParticleSystem => "✦",
122            _ => "▪",
123        }
124    }
125}
126
127// ---------------------------------------------------------------------------
128// Component value store
129// ---------------------------------------------------------------------------
130
131#[derive(Debug, Clone)]
132pub enum ComponentField {
133    Float(f32),
134    Float2(Vec2),
135    Float3(Vec3),
136    Float4(Vec4),
137    Int(i32),
138    Uint(u32),
139    Bool(bool),
140    String(String),
141    Color(Vec4),
142    AssetRef(Option<u64>),
143    Enum(u32, Vec<String>),
144    LayerMask(u32),
145}
146
147impl ComponentField {
148    pub fn type_label(&self) -> &'static str {
149        match self {
150            ComponentField::Float(_) => "Float",
151            ComponentField::Float2(_) => "Vector2",
152            ComponentField::Float3(_) => "Vector3",
153            ComponentField::Float4(_) => "Vector4",
154            ComponentField::Int(_) => "Int",
155            ComponentField::Uint(_) => "UInt",
156            ComponentField::Bool(_) => "Bool",
157            ComponentField::String(_) => "String",
158            ComponentField::Color(_) => "Color",
159            ComponentField::AssetRef(_) => "AssetRef",
160            ComponentField::Enum(_, _) => "Enum",
161            ComponentField::LayerMask(_) => "LayerMask",
162        }
163    }
164}
165
166#[derive(Debug, Clone)]
167pub struct Component {
168    pub id: u32,
169    pub kind: ComponentKind,
170    pub enabled: bool,
171    pub fields: HashMap<String, ComponentField>,
172    pub script_name: Option<String>,
173    pub expanded: bool,
174}
175
176impl Component {
177    pub fn new(id: u32, kind: ComponentKind) -> Self {
178        let mut comp = Self {
179            id, kind, enabled: true, fields: HashMap::new(), script_name: None, expanded: true,
180        };
181        comp.populate_defaults();
182        comp
183    }
184
185    fn populate_defaults(&mut self) {
186        match self.kind {
187            ComponentKind::Transform => {
188                self.fields.insert("position".into(), ComponentField::Float3(Vec3::ZERO));
189                self.fields.insert("rotation".into(), ComponentField::Float3(Vec3::ZERO));
190                self.fields.insert("scale".into(), ComponentField::Float3(Vec3::ONE));
191            }
192            ComponentKind::Camera => {
193                self.fields.insert("fov".into(), ComponentField::Float(60.0));
194                self.fields.insert("near_clip".into(), ComponentField::Float(0.1));
195                self.fields.insert("far_clip".into(), ComponentField::Float(1000.0));
196                self.fields.insert("orthographic".into(), ComponentField::Bool(false));
197                self.fields.insert("depth".into(), ComponentField::Int(0));
198            }
199            ComponentKind::MeshRenderer => {
200                self.fields.insert("mesh_id".into(), ComponentField::AssetRef(None));
201                self.fields.insert("material_id".into(), ComponentField::AssetRef(None));
202                self.fields.insert("cast_shadows".into(), ComponentField::Bool(true));
203                self.fields.insert("receive_shadows".into(), ComponentField::Bool(true));
204            }
205            ComponentKind::PointLight => {
206                self.fields.insert("color".into(), ComponentField::Color(Vec4::ONE));
207                self.fields.insert("intensity".into(), ComponentField::Float(1.0));
208                self.fields.insert("range".into(), ComponentField::Float(10.0));
209                self.fields.insert("shadow_type".into(), ComponentField::Enum(0, vec!["No Shadows".into(), "Hard Shadows".into(), "Soft Shadows".into()]));
210            }
211            ComponentKind::Rigidbody => {
212                self.fields.insert("mass".into(), ComponentField::Float(1.0));
213                self.fields.insert("drag".into(), ComponentField::Float(0.0));
214                self.fields.insert("angular_drag".into(), ComponentField::Float(0.05));
215                self.fields.insert("use_gravity".into(), ComponentField::Bool(true));
216                self.fields.insert("is_kinematic".into(), ComponentField::Bool(false));
217            }
218            ComponentKind::AudioSource => {
219                self.fields.insert("clip_id".into(), ComponentField::AssetRef(None));
220                self.fields.insert("volume".into(), ComponentField::Float(1.0));
221                self.fields.insert("pitch".into(), ComponentField::Float(1.0));
222                self.fields.insert("loop_audio".into(), ComponentField::Bool(false));
223                self.fields.insert("play_on_awake".into(), ComponentField::Bool(true));
224                self.fields.insert("spatial_blend".into(), ComponentField::Float(1.0));
225            }
226            _ => {}
227        }
228    }
229
230    pub fn get_float(&self, field: &str) -> f32 {
231        match self.fields.get(field) {
232            Some(ComponentField::Float(v)) => *v,
233            _ => 0.0,
234        }
235    }
236    pub fn get_bool(&self, field: &str) -> bool {
237        match self.fields.get(field) {
238            Some(ComponentField::Bool(v)) => *v,
239            _ => false,
240        }
241    }
242    pub fn get_vec3(&self, field: &str) -> Vec3 {
243        match self.fields.get(field) {
244            Some(ComponentField::Float3(v)) => *v,
245            _ => Vec3::ZERO,
246        }
247    }
248}
249
250// ---------------------------------------------------------------------------
251// Scene entity
252// ---------------------------------------------------------------------------
253
254#[derive(Debug, Clone)]
255pub struct SceneEntity {
256    pub id: u64,
257    pub name: String,
258    pub parent: Option<u64>,
259    pub children: Vec<u64>,
260    pub components: Vec<Component>,
261    pub active: bool,
262    pub static_flags: StaticFlags,
263    pub layer: u8,
264    pub tag: String,
265    pub prefab_id: Option<u64>,
266    pub is_prefab_root: bool,
267    pub scene_id: Option<u32>,
268    pub next_component_id: u32,
269}
270
271#[derive(Debug, Clone, Copy, Default)]
272pub struct StaticFlags {
273    pub batching: bool,
274    pub navigation: bool,
275    pub occluder: bool,
276    pub occludee: bool,
277    pub lightmap: bool,
278    pub reflection: bool,
279}
280
281impl StaticFlags {
282    pub fn all() -> Self { Self { batching: true, navigation: true, occluder: true, occludee: true, lightmap: true, reflection: true } }
283    pub fn is_any_static(self) -> bool { self.batching || self.navigation || self.occluder || self.occludee || self.lightmap || self.reflection }
284}
285
286impl SceneEntity {
287    pub fn new(id: u64, name: impl Into<String>) -> Self {
288        let mut entity = Self {
289            id, name: name.into(), parent: None, children: Vec::new(),
290            components: Vec::new(), active: true,
291            static_flags: StaticFlags::default(), layer: 0,
292            tag: "Untagged".into(), prefab_id: None, is_prefab_root: false,
293            scene_id: None, next_component_id: 1,
294        };
295        entity.add_component(ComponentKind::Transform);
296        entity
297    }
298
299    pub fn add_component(&mut self, kind: ComponentKind) -> u32 {
300        let id = self.next_component_id;
301        self.next_component_id += 1;
302        self.components.push(Component::new(id, kind));
303        id
304    }
305
306    pub fn remove_component(&mut self, id: u32) {
307        if self.components.len() <= 1 { return; } // Can't remove last
308        self.components.retain(|c| c.id != id);
309    }
310
311    pub fn get_component(&self, kind: ComponentKind) -> Option<&Component> {
312        self.components.iter().find(|c| c.kind == kind)
313    }
314
315    pub fn get_component_mut(&mut self, kind: ComponentKind) -> Option<&mut Component> {
316        self.components.iter_mut().find(|c| c.kind == kind)
317    }
318
319    pub fn has_component(&self, kind: ComponentKind) -> bool {
320        self.components.iter().any(|c| c.kind == kind)
321    }
322
323    pub fn world_transform(&self) -> Mat4 {
324        let transform = self.get_component(ComponentKind::Transform);
325        if let Some(t) = transform {
326            let pos = t.get_vec3("position");
327            let rot_euler = t.get_vec3("rotation");
328            let scale = t.get_vec3("scale");
329            let scale = if scale == Vec3::ZERO { Vec3::ONE } else { scale };
330            let rot = Quat::from_euler(
331                glam::EulerRot::XYZ,
332                rot_euler.x.to_radians(),
333                rot_euler.y.to_radians(),
334                rot_euler.z.to_radians(),
335            );
336            Mat4::from_scale_rotation_translation(scale, rot, pos)
337        } else {
338            Mat4::IDENTITY
339        }
340    }
341
342    pub fn set_position(&mut self, pos: Vec3) {
343        if let Some(t) = self.get_component_mut(ComponentKind::Transform) {
344            t.fields.insert("position".into(), ComponentField::Float3(pos));
345        }
346    }
347
348    pub fn position(&self) -> Vec3 {
349        self.get_component(ComponentKind::Transform)
350            .map(|t| t.get_vec3("position"))
351            .unwrap_or(Vec3::ZERO)
352    }
353}
354
355// ---------------------------------------------------------------------------
356// Scene
357// ---------------------------------------------------------------------------
358
359#[derive(Debug, Clone)]
360pub struct Scene {
361    pub name: String,
362    pub path: Option<String>,
363    pub entities: Vec<SceneEntity>,
364    pub is_dirty: bool,
365    pub ambient_color: Vec3,
366    pub ambient_intensity: f32,
367    pub skybox_material: Option<u64>,
368    pub fog_enabled: bool,
369    pub fog_color: Vec3,
370    pub fog_density: f32,
371    pub gravity: Vec3,
372    pub time_scale: f32,
373    pub physics_layer_matrix: [[bool; 32]; 32],
374    pub next_entity_id: u64,
375}
376
377impl Scene {
378    pub fn new(name: impl Into<String>) -> Self {
379        Self {
380            name: name.into(),
381            path: None,
382            entities: Vec::new(),
383            is_dirty: false,
384            ambient_color: Vec3::new(0.2, 0.2, 0.3),
385            ambient_intensity: 1.0,
386            skybox_material: None,
387            fog_enabled: false,
388            fog_color: Vec3::new(0.5, 0.6, 0.7),
389            fog_density: 0.01,
390            gravity: Vec3::new(0.0, -9.81, 0.0),
391            time_scale: 1.0,
392            physics_layer_matrix: [[true; 32]; 32],
393            next_entity_id: 1,
394        }
395    }
396
397    pub fn create_entity(&mut self, name: impl Into<String>) -> u64 {
398        let id = self.next_entity_id;
399        self.next_entity_id += 1;
400        let entity = SceneEntity::new(id, name);
401        self.entities.push(entity);
402        self.is_dirty = true;
403        id
404    }
405
406    pub fn destroy_entity(&mut self, id: u64) {
407        // Collect children to destroy recursively
408        let children: Vec<u64> = self.entities.iter()
409            .find(|e| e.id == id)
410            .map(|e| e.children.clone())
411            .unwrap_or_default();
412        for child in children {
413            self.destroy_entity(child);
414        }
415        self.entities.retain(|e| e.id != id);
416        // Remove from parent's children list
417        for entity in &mut self.entities {
418            entity.children.retain(|&c| c != id);
419        }
420        self.is_dirty = true;
421    }
422
423    pub fn set_parent(&mut self, child_id: u64, parent_id: Option<u64>) {
424        // Remove from old parent
425        let old_parent = self.entities.iter().find(|e| e.id == child_id).and_then(|e| e.parent);
426        if let Some(op) = old_parent {
427            if let Some(op_entity) = self.entities.iter_mut().find(|e| e.id == op) {
428                op_entity.children.retain(|&c| c != child_id);
429            }
430        }
431        // Set new parent
432        if let Some(pid) = parent_id {
433            if let Some(parent) = self.entities.iter_mut().find(|e| e.id == pid) {
434                parent.children.push(child_id);
435            }
436        }
437        if let Some(child) = self.entities.iter_mut().find(|e| e.id == child_id) {
438            child.parent = parent_id;
439        }
440        self.is_dirty = true;
441    }
442
443    pub fn find_entity(&self, id: u64) -> Option<&SceneEntity> {
444        self.entities.iter().find(|e| e.id == id)
445    }
446
447    pub fn find_entity_mut(&mut self, id: u64) -> Option<&mut SceneEntity> {
448        self.entities.iter_mut().find(|e| e.id == id)
449    }
450
451    pub fn find_by_name(&self, name: &str) -> Option<&SceneEntity> {
452        self.entities.iter().find(|e| e.name == name)
453    }
454
455    pub fn root_entities(&self) -> impl Iterator<Item = &SceneEntity> {
456        self.entities.iter().filter(|e| e.parent.is_none())
457    }
458
459    pub fn entity_count(&self) -> usize { self.entities.len() }
460
461    pub fn entities_with_component(&self, kind: ComponentKind) -> impl Iterator<Item = &SceneEntity> {
462        self.entities.iter().filter(move |e| e.has_component(kind))
463    }
464
465    pub fn populate_default_scene(&mut self) {
466        // Camera
467        let cam = self.create_entity("Main Camera");
468        if let Some(e) = self.find_entity_mut(cam) {
469            e.add_component(ComponentKind::Camera);
470            e.add_component(ComponentKind::AudioListener);
471            e.set_position(Vec3::new(0.0, 1.0, -10.0));
472            e.tag = "MainCamera".into();
473        }
474        // Directional light
475        let light = self.create_entity("Directional Light");
476        if let Some(e) = self.find_entity_mut(light) {
477            e.add_component(ComponentKind::DirectionalLight);
478        }
479        // Ground plane
480        let ground = self.create_entity("Ground");
481        if let Some(e) = self.find_entity_mut(ground) {
482            e.add_component(ComponentKind::MeshRenderer);
483            e.add_component(ComponentKind::BoxCollider);
484            e.set_position(Vec3::ZERO);
485            e.static_flags = StaticFlags::all();
486        }
487        // Test object
488        let cube = self.create_entity("Cube");
489        if let Some(e) = self.find_entity_mut(cube) {
490            e.add_component(ComponentKind::MeshRenderer);
491            e.add_component(ComponentKind::BoxCollider);
492            e.add_component(ComponentKind::Rigidbody);
493            e.set_position(Vec3::new(0.0, 2.0, 0.0));
494        }
495    }
496
497    pub fn serialize_to_json(&self) -> String {
498        // Simplified JSON-like representation
499        let mut json = format!("{{\"scene\":\"{}\",\"entities\":[", self.name);
500        for (i, e) in self.entities.iter().enumerate() {
501            if i > 0 { json.push(','); }
502            let pos = e.position();
503            json.push_str(&format!("{{\"id\":{},\"name\":\"{}\",\"active\":{},\"components\":{},\"position\":[{:.3},{:.3},{:.3}]}}",
504                e.id, e.name, e.active, e.components.len(), pos.x, pos.y, pos.z));
505        }
506        json.push_str("]}");
507        json
508    }
509}
510
511// ---------------------------------------------------------------------------
512// Scene editor
513// ---------------------------------------------------------------------------
514
515#[derive(Debug, Clone, Copy, PartialEq)]
516pub enum SceneEditorMode { Normal, Prefab, Terrain, Animation }
517
518#[derive(Debug, Clone)]
519pub struct MultiSelection {
520    pub entity_ids: Vec<u64>,
521    pub pivot: Vec3,
522    pub bounding_box: [Vec3; 2],
523}
524
525impl MultiSelection {
526    pub fn empty() -> Self {
527        Self { entity_ids: Vec::new(), pivot: Vec3::ZERO, bounding_box: [Vec3::ZERO, Vec3::ZERO] }
528    }
529    pub fn is_empty(&self) -> bool { self.entity_ids.is_empty() }
530    pub fn contains(&self, id: u64) -> bool { self.entity_ids.contains(&id) }
531}
532
533#[derive(Debug, Clone)]
534pub struct SceneEditorState {
535    pub scene: Scene,
536    pub mode: SceneEditorMode,
537    pub selection: MultiSelection,
538    pub clipboard: Vec<SceneEntity>,
539    pub undo_stack: Vec<String>,  // JSON snapshots
540    pub undo_pos: usize,
541    pub search_query: String,
542    pub filter_by_component: Option<ComponentKind>,
543    pub expand_all: bool,
544    pub show_active_only: bool,
545    pub show_gizmos: bool,
546    pub gizmo_size: f32,
547    pub rename_target: Option<u64>,
548    pub rename_buffer: String,
549}
550
551impl SceneEditorState {
552    pub fn new() -> Self {
553        let mut scene = Scene::new("SampleScene");
554        scene.populate_default_scene();
555        Self {
556            scene,
557            mode: SceneEditorMode::Normal,
558            selection: MultiSelection::empty(),
559            clipboard: Vec::new(),
560            undo_stack: Vec::new(),
561            undo_pos: 0,
562            search_query: String::new(),
563            filter_by_component: None,
564            expand_all: false,
565            show_active_only: false,
566            show_gizmos: true,
567            gizmo_size: 1.0,
568            rename_target: None,
569            rename_buffer: String::new(),
570        }
571    }
572
573    pub fn select(&mut self, id: u64) {
574        self.selection.entity_ids = vec![id];
575        self.update_selection_pivot();
576    }
577
578    pub fn add_to_selection(&mut self, id: u64) {
579        if !self.selection.contains(id) {
580            self.selection.entity_ids.push(id);
581        }
582        self.update_selection_pivot();
583    }
584
585    pub fn deselect_all(&mut self) {
586        self.selection = MultiSelection::empty();
587    }
588
589    fn update_selection_pivot(&mut self) {
590        if self.selection.is_empty() { return; }
591        let positions: Vec<Vec3> = self.selection.entity_ids.iter()
592            .filter_map(|&id| self.scene.find_entity(id))
593            .map(|e| e.position())
594            .collect();
595        if positions.is_empty() { return; }
596        let sum: Vec3 = positions.iter().sum();
597        self.selection.pivot = sum / positions.len() as f32;
598    }
599
600    pub fn snapshot(&mut self) {
601        let json = self.scene.serialize_to_json();
602        self.undo_stack.truncate(self.undo_pos);
603        self.undo_stack.push(json);
604        self.undo_pos = self.undo_stack.len();
605        self.scene.is_dirty = true;
606    }
607
608    pub fn delete_selection(&mut self) {
609        self.snapshot();
610        let ids: Vec<u64> = self.selection.entity_ids.drain(..).collect();
611        for id in ids {
612            self.scene.destroy_entity(id);
613        }
614    }
615
616    pub fn duplicate_selection(&mut self) {
617        self.snapshot();
618        let ids: Vec<u64> = self.selection.entity_ids.clone();
619        let mut new_ids = Vec::new();
620        for id in ids {
621            if let Some(entity) = self.scene.find_entity(id) {
622                let mut new_entity = entity.clone();
623                let new_id = self.scene.next_entity_id;
624                self.scene.next_entity_id += 1;
625                new_entity.id = new_id;
626                new_entity.name = format!("{} (Copy)", new_entity.name);
627                let pos = new_entity.position();
628                new_entity.set_position(pos + Vec3::new(0.5, 0.0, 0.5));
629                new_entity.parent = None;
630                new_entity.children.clear();
631                self.scene.entities.push(new_entity);
632                new_ids.push(new_id);
633            }
634        }
635        self.selection.entity_ids = new_ids;
636    }
637
638    pub fn create_empty(&mut self, name: &str) -> u64 {
639        self.snapshot();
640        self.scene.create_entity(name)
641    }
642
643    pub fn visible_entities(&self) -> Vec<&SceneEntity> {
644        self.scene.entities.iter().filter(|e| {
645            let active_ok = !self.show_active_only || e.active;
646            let comp_ok = self.filter_by_component.map(|k| e.has_component(k)).unwrap_or(true);
647            let search_ok = self.search_query.is_empty() ||
648                e.name.to_lowercase().contains(&self.search_query.to_lowercase());
649            active_ok && comp_ok && search_ok
650        }).collect()
651    }
652}
653
654// ---------------------------------------------------------------------------
655// Tests
656// ---------------------------------------------------------------------------
657#[cfg(test)]
658mod tests {
659    use super::*;
660
661    #[test]
662    fn test_scene_create_entity() {
663        let mut scene = Scene::new("Test");
664        let id = scene.create_entity("TestEntity");
665        assert!(scene.find_entity(id).is_some());
666    }
667
668    #[test]
669    fn test_entity_components() {
670        let mut entity = SceneEntity::new(1, "test");
671        entity.add_component(ComponentKind::MeshRenderer);
672        assert!(entity.has_component(ComponentKind::Transform));
673        assert!(entity.has_component(ComponentKind::MeshRenderer));
674    }
675
676    #[test]
677    fn test_scene_parenting() {
678        let mut scene = Scene::new("Test");
679        let parent = scene.create_entity("Parent");
680        let child = scene.create_entity("Child");
681        scene.set_parent(child, Some(parent));
682        assert_eq!(scene.find_entity(child).unwrap().parent, Some(parent));
683        assert!(scene.find_entity(parent).unwrap().children.contains(&child));
684    }
685
686    #[test]
687    fn test_destroy_propagates() {
688        let mut scene = Scene::new("Test");
689        let parent = scene.create_entity("Parent");
690        let child = scene.create_entity("Child");
691        scene.set_parent(child, Some(parent));
692        scene.destroy_entity(parent);
693        assert!(scene.find_entity(parent).is_none());
694        assert!(scene.find_entity(child).is_none());
695    }
696
697    #[test]
698    fn test_scene_editor() {
699        let mut ed = SceneEditorState::new();
700        assert!(!ed.scene.entities.is_empty());
701        let id = ed.create_empty("NewEntity");
702        ed.select(id);
703        assert_eq!(ed.selection.entity_ids.len(), 1);
704        ed.duplicate_selection();
705        assert!(ed.scene.entity_count() > 5);
706    }
707}