1pub mod node;
6pub mod field_manager;
7pub mod spawn_system;
8pub mod bvh;
9pub mod query;
10pub mod events;
11
12use crate::glyph::{Glyph, GlyphId, GlyphPool};
13use crate::entity::{AmorphousEntity, EntityId};
14use crate::particle::ParticlePool;
15use crate::math::ForceField;
16use glam::{Vec3, Vec4, Quat, Mat4};
17use std::collections::HashMap;
18
19pub use bvh::{Bvh, BvhNode, Aabb};
20pub use query::{SceneQuery, RaycastHit, FrustumQuery, SphereQuery};
21pub use events::{SceneEvent, SceneEventQueue, EventKind};
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
27pub struct FieldId(pub u32);
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
31pub struct NodeId(pub u32);
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
35pub struct LayerId(pub u8);
36
37impl LayerId {
38 pub const BACKGROUND: Self = LayerId(0);
39 pub const TERRAIN: Self = LayerId(1);
40 pub const WORLD: Self = LayerId(2);
41 pub const ENTITIES: Self = LayerId(3);
42 pub const PARTICLES: Self = LayerId(4);
43 pub const UI: Self = LayerId(5);
44 pub const DEBUG: Self = LayerId(6);
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
49pub struct PortalId(pub u32);
50
51#[derive(Debug, Clone)]
55pub struct SceneNode {
56 pub id: NodeId,
57 pub parent: Option<NodeId>,
58 pub children: Vec<NodeId>,
59 pub local: Transform3D,
60 pub world: Mat4, pub dirty: bool,
62 pub name: String,
63 pub visible: bool,
64 pub layer: LayerId,
65 pub tag: u32,
66 pub glyph_ids: Vec<GlyphId>,
67}
68
69impl SceneNode {
70 pub fn new(id: NodeId, name: impl Into<String>) -> Self {
71 Self {
72 id,
73 parent: None,
74 children: Vec::new(),
75 local: Transform3D::identity(),
76 world: Mat4::IDENTITY,
77 dirty: true,
78 name: name.into(),
79 visible: true,
80 layer: LayerId::WORLD,
81 tag: 0,
82 glyph_ids: Vec::new(),
83 }
84 }
85
86 pub fn with_position(mut self, pos: Vec3) -> Self {
87 self.local.position = pos;
88 self.dirty = true;
89 self
90 }
91
92 pub fn with_scale(mut self, scale: Vec3) -> Self {
93 self.local.scale = scale;
94 self.dirty = true;
95 self
96 }
97
98 pub fn with_rotation(mut self, rot: Quat) -> Self {
99 self.local.rotation = rot;
100 self.dirty = true;
101 self
102 }
103
104 pub fn local_matrix(&self) -> Mat4 {
105 Mat4::from_scale_rotation_translation(
106 self.local.scale,
107 self.local.rotation,
108 self.local.position,
109 )
110 }
111}
112
113#[derive(Debug, Clone, PartialEq)]
115pub struct Transform3D {
116 pub position: Vec3,
117 pub rotation: Quat,
118 pub scale: Vec3,
119}
120
121impl Transform3D {
122 pub fn identity() -> Self {
123 Self { position: Vec3::ZERO, rotation: Quat::IDENTITY, scale: Vec3::ONE }
124 }
125
126 pub fn from_position(p: Vec3) -> Self {
127 Self { position: p, ..Self::identity() }
128 }
129
130 pub fn to_matrix(&self) -> Mat4 {
131 Mat4::from_scale_rotation_translation(self.scale, self.rotation, self.position)
132 }
133
134 pub fn lerp(&self, other: &Self, t: f32) -> Self {
135 Self {
136 position: self.position.lerp(other.position, t),
137 rotation: self.rotation.slerp(other.rotation, t),
138 scale: self.scale.lerp(other.scale, t),
139 }
140 }
141
142 pub fn inverse(&self) -> Self {
143 let inv_scale = Vec3::new(1.0 / self.scale.x, 1.0 / self.scale.y, 1.0 / self.scale.z);
144 let inv_rot = self.rotation.inverse();
145 let inv_pos = inv_rot * (-self.position * inv_scale);
146 Self { position: inv_pos, rotation: inv_rot, scale: inv_scale }
147 }
148
149 pub fn transform_point(&self, p: Vec3) -> Vec3 {
150 self.position + self.rotation * (self.scale * p)
151 }
152
153 pub fn transform_direction(&self, d: Vec3) -> Vec3 {
154 self.rotation * d
155 }
156}
157
158#[derive(Debug, Clone)]
162pub struct SceneLayer {
163 pub id: LayerId,
164 pub name: String,
165 pub visible: bool,
166 pub z_order: i32,
167 pub opaque: bool,
168 pub cast_shadows: bool,
169 pub receive_shadows: bool,
170}
171
172impl SceneLayer {
173 pub fn new(id: LayerId, name: impl Into<String>) -> Self {
174 Self { id, name: name.into(), visible: true, z_order: 0, opaque: true,
175 cast_shadows: true, receive_shadows: true }
176 }
177}
178
179#[derive(Debug, Clone)]
183pub struct Portal {
184 pub id: PortalId,
185 pub origin: Vec3,
186 pub target: Vec3,
187 pub normal: Vec3,
188 pub extent: Vec2,
189 pub active: bool,
190 pub linked: Option<PortalId>,
191}
192
193use glam::Vec2;
194
195#[derive(Debug, Clone)]
199pub struct AmbientZone {
200 pub min: Vec3,
201 pub max: Vec3,
202 pub ambient_color: Vec4,
203 pub fog_density: f32,
204 pub fog_color: Vec4,
205 pub reverb_wet: f32,
206 pub wind_strength: f32,
207 pub gravity_scale: f32,
208 pub name: String,
209}
210
211impl AmbientZone {
212 pub fn contains(&self, p: Vec3) -> bool {
213 p.x >= self.min.x && p.x <= self.max.x &&
214 p.y >= self.min.y && p.y <= self.max.y &&
215 p.z >= self.min.z && p.z <= self.max.z
216 }
217
218 pub fn blend_factor(&self, p: Vec3) -> f32 {
219 let margin = 2.0;
221 let dx = ((p.x - self.min.x).min(self.max.x - p.x) / margin).clamp(0.0, 1.0);
222 let dy = ((p.y - self.min.y).min(self.max.y - p.y) / margin).clamp(0.0, 1.0);
223 let dz = ((p.z - self.min.z).min(self.max.z - p.z) / margin).clamp(0.0, 1.0);
224 dx.min(dy).min(dz)
225 }
226}
227
228#[derive(Debug, Clone)]
232pub struct SceneSnapshot {
233 pub time: f32,
234 pub glyph_count: usize,
235 pub entity_count: usize,
236 pub field_count: usize,
237 pub node_count: usize,
238 pub glyph_positions: Vec<[f32; 3]>,
240 pub entity_positions: Vec<[f32; 3]>,
241 pub field_positions: Vec<[f32; 3]>,
242}
243
244impl SceneSnapshot {
245 pub fn diff(&self, other: &SceneSnapshot) -> SnapshotDiff {
246 SnapshotDiff {
247 glyph_delta: other.glyph_count as i32 - self.glyph_count as i32,
248 entity_delta: other.entity_count as i32 - self.entity_count as i32,
249 field_delta: other.field_count as i32 - self.field_count as i32,
250 time_delta: other.time - self.time,
251 }
252 }
253}
254
255#[derive(Debug, Clone)]
256pub struct SnapshotDiff {
257 pub glyph_delta: i32,
258 pub entity_delta: i32,
259 pub field_delta: i32,
260 pub time_delta: f32,
261}
262
263#[derive(Debug, Clone, Default)]
267pub struct SceneStats {
268 pub glyph_count: usize,
269 pub particle_count: usize,
270 pub entity_count: usize,
271 pub field_count: usize,
272 pub node_count: usize,
273 pub portal_count: usize,
274 pub zone_count: usize,
275 pub visible_glyphs: usize,
276 pub culled_glyphs: usize,
277 pub tick_count: u64,
278 pub elapsed_secs: f32,
279}
280
281pub struct Scene {
285 pub glyphs: GlyphPool,
286 pub particles: ParticlePool,
287 pub entities: Vec<(EntityId, AmorphousEntity)>,
288 pub fields: Vec<(FieldId, ForceField)>,
289 next_field_id: u32,
290 next_entity_id: u32,
291 next_node_id: u32,
292 next_portal_id: u32,
293 pub time: f32,
294
295 pub nodes: HashMap<NodeId, SceneNode>,
297 pub root_nodes: Vec<NodeId>,
298
299 pub layers: [SceneLayer; 8],
301
302 pub zones: Vec<AmbientZone>,
304
305 pub portals: Vec<Portal>,
307
308 bvh_dirty: bool,
310 pub bvh: Option<Bvh>,
311
312 pub events: SceneEventQueue,
314
315 pub stats: SceneStats,
317}
318
319impl Scene {
320 pub fn new() -> Self {
321 Self {
322 glyphs: GlyphPool::new(8192),
323 particles: ParticlePool::new(4096),
324 entities: Vec::new(),
325 fields: Vec::new(),
326 next_field_id: 0,
327 next_entity_id: 0,
328 next_node_id: 1,
329 next_portal_id: 0,
330 time: 0.0,
331 nodes: HashMap::new(),
332 root_nodes: Vec::new(),
333 layers: [
334 SceneLayer::new(LayerId::BACKGROUND, "Background"),
335 SceneLayer::new(LayerId::TERRAIN, "Terrain"),
336 SceneLayer::new(LayerId::WORLD, "World"),
337 SceneLayer::new(LayerId::ENTITIES, "Entities"),
338 SceneLayer::new(LayerId::PARTICLES, "Particles"),
339 SceneLayer::new(LayerId::UI, "UI"),
340 SceneLayer::new(LayerId::DEBUG, "Debug"),
341 SceneLayer::new(LayerId(7), "Overlay"),
342 ],
343 zones: Vec::new(),
344 portals: Vec::new(),
345 bvh_dirty: false,
346 bvh: None,
347 events: SceneEventQueue::new(),
348 stats: SceneStats::default(),
349 }
350 }
351
352 pub fn spawn_glyph(&mut self, glyph: Glyph) -> GlyphId {
355 self.bvh_dirty = true;
356 self.glyphs.spawn(glyph)
357 }
358
359 pub fn despawn_glyph(&mut self, id: GlyphId) {
360 self.glyphs.despawn(id);
361 self.bvh_dirty = true;
362 }
363
364 pub fn get_glyph(&self, id: GlyphId) -> Option<&Glyph> {
365 self.glyphs.get(id)
366 }
367
368 pub fn get_glyph_mut(&mut self, id: GlyphId) -> Option<&mut Glyph> {
369 self.bvh_dirty = true;
370 self.glyphs.get_mut(id)
371 }
372
373 pub fn spawn_entity(&mut self, entity: AmorphousEntity) -> EntityId {
376 let id = EntityId(self.next_entity_id);
377 self.next_entity_id += 1;
378 self.events.push(SceneEvent { kind: EventKind::EntitySpawned(id), time: self.time });
379 self.entities.push((id, entity));
380 self.bvh_dirty = true;
381 id
382 }
383
384 pub fn despawn_entity(&mut self, id: EntityId) {
385 self.entities.retain(|(eid, _)| *eid != id);
386 self.events.push(SceneEvent { kind: EventKind::EntityDespawned(id), time: self.time });
387 self.bvh_dirty = true;
388 }
389
390 pub fn get_entity(&self, id: EntityId) -> Option<&AmorphousEntity> {
391 self.entities.iter().find(|(eid, _)| *eid == id).map(|(_, e)| e)
392 }
393
394 pub fn get_entity_mut(&mut self, id: EntityId) -> Option<&mut AmorphousEntity> {
395 self.entities.iter_mut().find(|(eid, _)| *eid == id).map(|(_, e)| e)
396 }
397
398 pub fn add_field(&mut self, field: ForceField) -> FieldId {
401 let id = FieldId(self.next_field_id);
402 self.next_field_id += 1;
403 self.fields.push((id, field));
404 id
405 }
406
407 pub fn remove_field(&mut self, id: FieldId) {
408 self.fields.retain(|(fid, _)| *fid != id);
409 }
410
411 pub fn get_field(&self, id: FieldId) -> Option<&ForceField> {
412 self.fields.iter().find(|(fid, _)| *fid == id).map(|(_, f)| f)
413 }
414
415 pub fn get_field_mut(&mut self, id: FieldId) -> Option<&mut ForceField> {
416 self.fields.iter_mut().find(|(fid, _)| *fid == id).map(|(_, f)| f)
417 }
418
419 pub fn create_node(&mut self, name: impl Into<String>) -> NodeId {
422 let id = NodeId(self.next_node_id);
423 self.next_node_id += 1;
424 let node = SceneNode::new(id, name);
425 self.nodes.insert(id, node);
426 self.root_nodes.push(id);
427 id
428 }
429
430 pub fn destroy_node(&mut self, id: NodeId) {
431 if let Some(node) = self.nodes.remove(&id) {
432 for glyph_id in &node.glyph_ids {
434 self.glyphs.despawn(*glyph_id);
435 }
436 if let Some(parent_id) = node.parent {
438 if let Some(parent) = self.nodes.get_mut(&parent_id) {
439 parent.children.retain(|c| *c != id);
440 }
441 } else {
442 self.root_nodes.retain(|r| *r != id);
443 }
444 let children: Vec<NodeId> = node.children.clone();
446 for child in children {
447 self.destroy_node(child);
448 }
449 }
450 }
451
452 pub fn attach_node(&mut self, child: NodeId, parent: NodeId) {
453 if let Some(node) = self.nodes.get(&child) {
455 if let Some(old_parent) = node.parent {
456 if let Some(p) = self.nodes.get_mut(&old_parent) {
457 p.children.retain(|c| *c != child);
458 }
459 } else {
460 self.root_nodes.retain(|r| *r != child);
461 }
462 }
463 if let Some(node) = self.nodes.get_mut(&child) {
465 node.parent = Some(parent);
466 node.dirty = true;
467 }
468 if let Some(parent_node) = self.nodes.get_mut(&parent) {
469 parent_node.children.push(child);
470 }
471 }
472
473 pub fn detach_node(&mut self, id: NodeId) {
474 if let Some(node) = self.nodes.get_mut(&id) {
475 node.parent = None;
476 node.dirty = true;
477 }
478 self.root_nodes.push(id);
479 }
480
481 pub fn get_node(&self, id: NodeId) -> Option<&SceneNode> {
482 self.nodes.get(&id)
483 }
484
485 pub fn get_node_mut(&mut self, id: NodeId) -> Option<&mut SceneNode> {
486 self.nodes.get_mut(&id)
487 }
488
489 pub fn find_node_by_name(&self, name: &str) -> Option<NodeId> {
490 self.nodes.values().find(|n| n.name == name).map(|n| n.id)
491 }
492
493 pub fn find_nodes_by_tag(&self, tag: u32) -> Vec<NodeId> {
494 self.nodes.values().filter(|n| n.tag == tag).map(|n| n.id).collect()
495 }
496
497 pub fn attach_glyph_to_node(&mut self, glyph_id: GlyphId, node_id: NodeId) {
499 if let Some(node) = self.nodes.get_mut(&node_id) {
500 node.glyph_ids.push(glyph_id);
501 }
502 }
503
504 pub fn set_layer_visible(&mut self, layer: LayerId, visible: bool) {
507 if let Some(l) = self.layers.get_mut(layer.0 as usize) {
508 l.visible = visible;
509 }
510 }
511
512 pub fn is_layer_visible(&self, layer: LayerId) -> bool {
513 self.layers.get(layer.0 as usize).map(|l| l.visible).unwrap_or(true)
514 }
515
516 pub fn add_zone(&mut self, zone: AmbientZone) { self.zones.push(zone); }
519
520 pub fn remove_zone(&mut self, name: &str) { self.zones.retain(|z| z.name != name); }
521
522 pub fn zone_at(&self, pos: Vec3) -> Option<(&AmbientZone, f32)> {
524 self.zones.iter()
525 .filter(|z| z.contains(pos))
526 .map(|z| (z, z.blend_factor(pos)))
527 .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap())
528 }
529
530 pub fn add_portal(&mut self, origin: Vec3, target: Vec3, normal: Vec3, extent: Vec2) -> PortalId {
533 let id = PortalId(self.next_portal_id);
534 self.next_portal_id += 1;
535 self.portals.push(Portal { id, origin, target, normal, extent, active: true, linked: None });
536 id
537 }
538
539 pub fn link_portals(&mut self, a: PortalId, b: PortalId) {
540 if let Some(pa) = self.portals.iter_mut().find(|p| p.id == a) { pa.linked = Some(b); }
541 if let Some(pb) = self.portals.iter_mut().find(|p| p.id == b) { pb.linked = Some(a); }
542 }
543
544 pub fn tick(&mut self, dt: f32) {
548 self.time += dt;
549 self.stats.tick_count += 1;
550 self.stats.elapsed_secs += dt;
551
552 for (_, glyph) in self.glyphs.iter_mut() {
554 let mut total_force = Vec3::ZERO;
555 for (_, field) in &self.fields {
556 total_force += field.force_at(glyph.position, glyph.mass, glyph.charge, self.time);
557 }
558 glyph.acceleration = total_force / glyph.mass.max(0.001);
559 }
560
561 self.glyphs.tick(dt);
563
564 self.particles.tick(dt);
566
567 for (_, field) in &self.fields {
569 self.particles.apply_field(field, self.time);
570 }
571
572 for (_, entity) in &mut self.entities {
574 entity.tick(dt, self.time);
575 }
576
577 self.flush_transforms();
579
580 self.sync_node_glyphs();
582
583 self.stats.glyph_count = self.glyphs.count();
585 self.stats.particle_count = self.particles.count();
586 self.stats.entity_count = self.entities.len();
587 self.stats.field_count = self.fields.len();
588 self.stats.node_count = self.nodes.len();
589 self.stats.portal_count = self.portals.len();
590 self.stats.zone_count = self.zones.len();
591 }
592
593 fn flush_transforms(&mut self) {
596 let roots: Vec<NodeId> = self.root_nodes.clone();
597 for root in roots {
598 self.update_world_transform(root, Mat4::IDENTITY);
599 }
600 }
601
602 fn update_world_transform(&mut self, id: NodeId, parent_world: Mat4) {
603 let local_mat = if let Some(node) = self.nodes.get(&id) {
604 if !node.dirty { return; }
605 node.local_matrix()
606 } else {
607 return;
608 };
609
610 let world = parent_world * local_mat;
611 let children: Vec<NodeId> = if let Some(node) = self.nodes.get_mut(&id) {
612 node.world = world;
613 node.dirty = false;
614 node.children.clone()
615 } else {
616 return;
617 };
618
619 for child in children {
620 self.update_world_transform(child, world);
621 }
622 }
623
624 fn sync_node_glyphs(&mut self) {
625 let updates: Vec<(Mat4, GlyphId)> = self.nodes.values()
627 .flat_map(|n| {
628 let w = n.world;
629 n.glyph_ids.iter().map(move |&gid| (w, gid)).collect::<Vec<_>>()
630 })
631 .collect();
632
633 for (world, glyph_id) in updates {
634 if let Some(glyph) = self.glyphs.get_mut(glyph_id) {
635 let pos = world.transform_point3(Vec3::ZERO);
636 glyph.position = pos;
637 }
638 }
639 }
640
641 pub fn rebuild_bvh(&mut self) {
645 if !self.bvh_dirty { return; }
646 let aabbs: Vec<(GlyphId, Aabb)> = self.glyphs.iter()
647 .map(|(id, g)| (id, Aabb::from_point(g.position, 1.0)))
648 .collect();
649 self.bvh = Some(Bvh::build(&aabbs));
650 self.bvh_dirty = false;
651 }
652
653 pub fn glyphs_in_sphere(&self, center: Vec3, radius: f32) -> Vec<GlyphId> {
655 if let Some(ref bvh) = self.bvh {
656 bvh.sphere_query(center, radius)
657 } else {
658 self.glyphs.iter()
659 .filter(|(_, g)| (g.position - center).length() <= radius)
660 .map(|(id, _)| id)
661 .collect()
662 }
663 }
664
665 pub fn raycast_glyphs(&self, origin: Vec3, direction: Vec3, max_dist: f32) -> Option<RaycastHit> {
667 let dir = direction.normalize_or_zero();
668 let mut best: Option<RaycastHit> = None;
669 for (id, glyph) in self.glyphs.iter() {
670 let delta = glyph.position - origin;
671 let t = delta.dot(dir);
672 if t < 0.0 || t > max_dist { continue; }
673 let perp = delta - dir * t;
674 let hit_radius = 0.6;
675 if perp.length_squared() <= hit_radius * hit_radius {
676 if best.as_ref().map(|b: &RaycastHit| t < b.distance).unwrap_or(true) {
677 best = Some(RaycastHit {
678 glyph_id: id,
679 distance: t,
680 point: origin + dir * t,
681 normal: -dir,
682 });
683 }
684 }
685 }
686 best
687 }
688
689 pub fn entities_in_sphere(&self, center: Vec3, radius: f32) -> Vec<EntityId> {
691 self.entities.iter()
692 .filter(|(_, e)| (e.position - center).length() <= radius)
693 .map(|(id, _)| *id)
694 .collect()
695 }
696
697 pub fn nearest_entity(&self, point: Vec3) -> Option<EntityId> {
699 self.entities.iter()
700 .min_by(|a, b| {
701 let da = (a.1.position - point).length_squared();
702 let db = (b.1.position - point).length_squared();
703 da.partial_cmp(&db).unwrap()
704 })
705 .map(|(id, _)| *id)
706 }
707
708 pub fn entities_with_tag(&self, tag: &str) -> Vec<EntityId> {
710 self.entities.iter()
711 .filter(|(_, e)| e.tags.contains(&tag.to_string()))
712 .map(|(id, _)| *id)
713 .collect()
714 }
715
716 pub fn snapshot(&self) -> SceneSnapshot {
719 SceneSnapshot {
720 time: self.time,
721 glyph_count: self.glyphs.count(),
722 entity_count: self.entities.len(),
723 field_count: self.fields.len(),
724 node_count: self.nodes.len(),
725 glyph_positions: self.glyphs.iter().map(|(_, g)| g.position.to_array()).collect(),
726 entity_positions: self.entities.iter().map(|(_, e)| e.position.to_array()).collect(),
727 field_positions: Vec::new(),
728 }
729 }
730
731 pub fn gc(&mut self) {
736 self.entities.retain(|(_, e)| !e.despawn_requested);
737 self.bvh_dirty = true;
738 }
739
740 pub fn clear(&mut self) {
742 self.glyphs = GlyphPool::new(8192);
743 self.particles = ParticlePool::new(4096);
744 self.entities.clear();
745 self.fields.clear();
746 self.nodes.clear();
747 self.root_nodes.clear();
748 self.zones.clear();
749 self.portals.clear();
750 self.bvh = None;
751 self.bvh_dirty = false;
752 self.events.clear();
753 self.stats = SceneStats::default();
754 }
755
756 pub fn spawn_glyph_grid(
758 &mut self,
759 origin: Vec3,
760 cols: u32, rows: u32,
761 spacing: f32,
762 glyph_fn: impl Fn(u32, u32) -> Glyph,
763 ) -> Vec<GlyphId> {
764 let mut ids = Vec::with_capacity((cols * rows) as usize);
765 for row in 0..rows {
766 for col in 0..cols {
767 let mut g = glyph_fn(col, row);
768 g.position = origin + Vec3::new(col as f32 * spacing, 0.0, row as f32 * spacing);
769 ids.push(self.spawn_glyph(g));
770 }
771 }
772 ids
773 }
774
775 pub fn spawn_glyph_ring(
777 &mut self,
778 center: Vec3,
779 radius: f32,
780 count: u32,
781 glyph_fn: impl Fn(u32) -> Glyph,
782 ) -> Vec<GlyphId> {
783 let mut ids = Vec::with_capacity(count as usize);
784 for i in 0..count {
785 let angle = i as f32 / count as f32 * std::f32::consts::TAU;
786 let mut g = glyph_fn(i);
787 g.position = center + Vec3::new(angle.cos() * radius, 0.0, angle.sin() * radius);
788 ids.push(self.spawn_glyph(g));
789 }
790 ids
791 }
792
793 pub fn despawn_glyphs(&mut self, ids: &[GlyphId]) {
795 for &id in ids { self.despawn_glyph(id); }
796 }
797
798 pub fn drain_events(&mut self) -> Vec<SceneEvent> { self.events.drain() }
801 pub fn push_event(&mut self, e: SceneEvent) { self.events.push(e); }
802
803 pub fn diagnostics(&self) -> String {
806 format!(
807 "Scene t={:.2}s | glyphs={} particles={} entities={} fields={} nodes={} zones={} portals={}",
808 self.time,
809 self.stats.glyph_count,
810 self.stats.particle_count,
811 self.stats.entity_count,
812 self.stats.field_count,
813 self.stats.node_count,
814 self.stats.zone_count,
815 self.stats.portal_count,
816 )
817 }
818}
819
820impl Default for Scene {
821 fn default() -> Self { Self::new() }
822}
823
824pub type SceneGraph = Scene;
826
827#[cfg(test)]
830mod tests {
831 use super::*;
832
833 fn make_glyph(pos: Vec3) -> Glyph {
834 Glyph { position: pos, ..Default::default() }
835 }
836
837 #[test]
838 fn scene_spawn_despawn_glyph() {
839 let mut s = Scene::new();
840 let id = s.spawn_glyph(make_glyph(Vec3::ZERO));
841 assert_eq!(s.glyphs.count(), 1);
842 s.despawn_glyph(id);
843 assert_eq!(s.glyphs.count(), 0);
844 }
845
846 #[test]
847 fn scene_spawn_entity() {
848 let mut s = Scene::new();
849 let e = AmorphousEntity { position: Vec3::ONE, ..Default::default() };
850 let id = s.spawn_entity(e);
851 assert!(s.get_entity(id).is_some());
852 s.despawn_entity(id);
853 assert!(s.get_entity(id).is_none());
854 }
855
856 #[test]
857 fn scene_fields() {
858 let mut s = Scene::new();
859 let field = ForceField::Gravity { center: Vec3::ZERO, strength: 9.81, falloff: crate::math::Falloff::InverseSquare };
860 let id = s.add_field(field);
861 assert!(s.get_field(id).is_some());
862 s.remove_field(id);
863 assert!(s.get_field(id).is_none());
864 }
865
866 #[test]
867 fn scene_node_hierarchy() {
868 let mut s = Scene::new();
869 let parent = s.create_node("parent");
870 let child = s.create_node("child");
871 s.attach_node(child, parent);
872 assert_eq!(s.get_node(child).unwrap().parent, Some(parent));
873 assert!(s.get_node(parent).unwrap().children.contains(&child));
874 }
875
876 #[test]
877 fn scene_node_find_by_name() {
878 let mut s = Scene::new();
879 s.create_node("the_node");
880 let id = s.find_node_by_name("the_node");
881 assert!(id.is_some());
882 }
883
884 #[test]
885 fn scene_tick_advances_time() {
886 let mut s = Scene::new();
887 s.tick(0.016);
888 assert!((s.time - 0.016).abs() < 1e-5);
889 }
890
891 #[test]
892 fn scene_glyphs_in_sphere() {
893 let mut s = Scene::new();
894 s.spawn_glyph(make_glyph(Vec3::ZERO));
895 s.spawn_glyph(make_glyph(Vec3::new(100.0, 0.0, 0.0)));
896 let hits = s.glyphs_in_sphere(Vec3::ZERO, 5.0);
897 assert_eq!(hits.len(), 1);
898 }
899
900 #[test]
901 fn scene_raycast_glyphs() {
902 let mut s = Scene::new();
903 s.spawn_glyph(make_glyph(Vec3::new(0.0, 0.0, 10.0)));
904 let hit = s.raycast_glyphs(Vec3::ZERO, Vec3::Z, 100.0);
905 assert!(hit.is_some());
906 assert!((hit.unwrap().distance - 10.0).abs() < 1.0);
907 }
908
909 #[test]
910 fn scene_layer_visibility() {
911 let mut s = Scene::new();
912 s.set_layer_visible(LayerId::ENTITIES, false);
913 assert!(!s.is_layer_visible(LayerId::ENTITIES));
914 s.set_layer_visible(LayerId::ENTITIES, true);
915 assert!(s.is_layer_visible(LayerId::ENTITIES));
916 }
917
918 #[test]
919 fn scene_ambient_zone() {
920 let mut s = Scene::new();
921 s.add_zone(AmbientZone {
922 min: Vec3::splat(-5.0), max: Vec3::splat(5.0),
923 ambient_color: Vec4::ONE, fog_density: 0.0,
924 fog_color: Vec4::ZERO, reverb_wet: 0.0,
925 wind_strength: 0.0, gravity_scale: 1.0,
926 name: "test_zone".into(),
927 });
928 let result = s.zone_at(Vec3::ZERO);
929 assert!(result.is_some());
930 let outside = s.zone_at(Vec3::new(100.0, 0.0, 0.0));
931 assert!(outside.is_none());
932 }
933
934 #[test]
935 fn scene_portal() {
936 let mut s = Scene::new();
937 let a = s.add_portal(Vec3::ZERO, Vec3::new(100.0, 0.0, 0.0), Vec3::Z, Vec2::splat(2.0));
938 let b = s.add_portal(Vec3::new(100.0, 0.0, 0.0), Vec3::ZERO, Vec3::NEG_Z, Vec2::splat(2.0));
939 s.link_portals(a, b);
940 assert_eq!(s.portals[0].linked, Some(b));
941 assert_eq!(s.portals[1].linked, Some(a));
942 }
943
944 #[test]
945 fn scene_snapshot_diff() {
946 let mut s = Scene::new();
947 let snap1 = s.snapshot();
948 s.spawn_glyph(make_glyph(Vec3::ZERO));
949 let snap2 = s.snapshot();
950 let diff = snap1.diff(&snap2);
951 assert_eq!(diff.glyph_delta, 1);
952 }
953
954 #[test]
955 fn scene_spawn_glyph_ring() {
956 let mut s = Scene::new();
957 let ids = s.spawn_glyph_ring(Vec3::ZERO, 5.0, 8, |_| make_glyph(Vec3::ZERO));
958 assert_eq!(ids.len(), 8);
959 }
960
961 #[test]
962 fn scene_spawn_glyph_grid() {
963 let mut s = Scene::new();
964 let ids = s.spawn_glyph_grid(Vec3::ZERO, 4, 4, 1.0, |_, _| make_glyph(Vec3::ZERO));
965 assert_eq!(ids.len(), 16);
966 }
967
968 #[test]
969 fn scene_clear() {
970 let mut s = Scene::new();
971 s.spawn_glyph(make_glyph(Vec3::ZERO));
972 s.spawn_entity(AmorphousEntity::default());
973 s.clear();
974 assert_eq!(s.stats.glyph_count, 0);
975 }
976
977 #[test]
978 fn scene_diagnostics_string() {
979 let s = Scene::new();
980 let d = s.diagnostics();
981 assert!(d.contains("Scene"));
982 }
983
984 #[test]
985 fn transform3d_lerp() {
986 let a = Transform3D::identity();
987 let b = Transform3D::from_position(Vec3::new(10.0, 0.0, 0.0));
988 let mid = a.lerp(&b, 0.5);
989 assert!((mid.position.x - 5.0).abs() < 0.01);
990 }
991
992 #[test]
993 fn transform3d_transform_point() {
994 let t = Transform3D { position: Vec3::new(1.0, 2.0, 3.0), ..Transform3D::identity() };
995 let p = t.transform_point(Vec3::ZERO);
996 assert_eq!(p, Vec3::new(1.0, 2.0, 3.0));
997 }
998}