1
2use glam::{Vec2, Vec3, Vec4, Quat, Mat4};
5use std::collections::HashMap;
6
7#[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#[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#[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; } 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#[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 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 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 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 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 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 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 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 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 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#[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>, 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#[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}