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 {
576 entity.tick(dt, self.time);
577 let positions = entity.step_glyph_physics(dt);
578 for (slot, id) in entity.glyph_ids.iter().enumerate() {
579 let Some(pos) = positions.get(slot) else { break };
580 if let Some(g) = self.glyphs.get_mut(*id) {
581 g.position = *pos;
582 let drift = entity
586 .cohesion_system
587 .as_ref()
588 .and_then(|cs| cs.glyphs.get(slot))
589 .map(|gc| gc.drift)
590 .unwrap_or(0.0);
591 let bound = (1.0 - drift * 0.25).clamp(0.15, 1.0);
592 g.emission = 0.8 * bound;
593 g.color.w = bound;
594 g.temperature = entity.entity_temperature;
595 }
596 }
597 }
598
599 self.flush_transforms();
601
602 self.sync_node_glyphs();
604
605 self.stats.glyph_count = self.glyphs.count();
607 self.stats.particle_count = self.particles.count();
608 self.stats.entity_count = self.entities.len();
609 self.stats.field_count = self.fields.len();
610 self.stats.node_count = self.nodes.len();
611 self.stats.portal_count = self.portals.len();
612 self.stats.zone_count = self.zones.len();
613 }
614
615 fn flush_transforms(&mut self) {
618 let roots: Vec<NodeId> = self.root_nodes.clone();
619 for root in roots {
620 self.update_world_transform(root, Mat4::IDENTITY);
621 }
622 }
623
624 fn update_world_transform(&mut self, id: NodeId, parent_world: Mat4) {
625 let local_mat = if let Some(node) = self.nodes.get(&id) {
626 if !node.dirty { return; }
627 node.local_matrix()
628 } else {
629 return;
630 };
631
632 let world = parent_world * local_mat;
633 let children: Vec<NodeId> = if let Some(node) = self.nodes.get_mut(&id) {
634 node.world = world;
635 node.dirty = false;
636 node.children.clone()
637 } else {
638 return;
639 };
640
641 for child in children {
642 self.update_world_transform(child, world);
643 }
644 }
645
646 fn sync_node_glyphs(&mut self) {
647 let updates: Vec<(Mat4, GlyphId)> = self.nodes.values()
649 .flat_map(|n| {
650 let w = n.world;
651 n.glyph_ids.iter().map(move |&gid| (w, gid)).collect::<Vec<_>>()
652 })
653 .collect();
654
655 for (world, glyph_id) in updates {
656 if let Some(glyph) = self.glyphs.get_mut(glyph_id) {
657 let pos = world.transform_point3(Vec3::ZERO);
658 glyph.position = pos;
659 }
660 }
661 }
662
663 pub fn rebuild_bvh(&mut self) {
667 if !self.bvh_dirty { return; }
668 let aabbs: Vec<(GlyphId, Aabb)> = self.glyphs.iter()
669 .map(|(id, g)| (id, Aabb::from_point(g.position, 1.0)))
670 .collect();
671 self.bvh = Some(Bvh::build(&aabbs));
672 self.bvh_dirty = false;
673 }
674
675 pub fn glyphs_in_sphere(&self, center: Vec3, radius: f32) -> Vec<GlyphId> {
677 if let Some(ref bvh) = self.bvh {
678 bvh.sphere_query(center, radius)
679 } else {
680 self.glyphs.iter()
681 .filter(|(_, g)| (g.position - center).length() <= radius)
682 .map(|(id, _)| id)
683 .collect()
684 }
685 }
686
687 pub fn raycast_glyphs(&self, origin: Vec3, direction: Vec3, max_dist: f32) -> Option<RaycastHit> {
689 let dir = direction.normalize_or_zero();
690 let mut best: Option<RaycastHit> = None;
691 for (id, glyph) in self.glyphs.iter() {
692 let delta = glyph.position - origin;
693 let t = delta.dot(dir);
694 if t < 0.0 || t > max_dist { continue; }
695 let perp = delta - dir * t;
696 let hit_radius = 0.6;
697 if perp.length_squared() <= hit_radius * hit_radius {
698 if best.as_ref().map(|b: &RaycastHit| t < b.distance).unwrap_or(true) {
699 best = Some(RaycastHit {
700 glyph_id: id,
701 distance: t,
702 point: origin + dir * t,
703 normal: -dir,
704 });
705 }
706 }
707 }
708 best
709 }
710
711 pub fn entities_in_sphere(&self, center: Vec3, radius: f32) -> Vec<EntityId> {
713 self.entities.iter()
714 .filter(|(_, e)| (e.position - center).length() <= radius)
715 .map(|(id, _)| *id)
716 .collect()
717 }
718
719 pub fn nearest_entity(&self, point: Vec3) -> Option<EntityId> {
721 self.entities.iter()
722 .min_by(|a, b| {
723 let da = (a.1.position - point).length_squared();
724 let db = (b.1.position - point).length_squared();
725 da.partial_cmp(&db).unwrap()
726 })
727 .map(|(id, _)| *id)
728 }
729
730 pub fn entities_with_tag(&self, tag: &str) -> Vec<EntityId> {
732 self.entities.iter()
733 .filter(|(_, e)| e.tags.contains(&tag.to_string()))
734 .map(|(id, _)| *id)
735 .collect()
736 }
737
738 pub fn snapshot(&self) -> SceneSnapshot {
741 SceneSnapshot {
742 time: self.time,
743 glyph_count: self.glyphs.count(),
744 entity_count: self.entities.len(),
745 field_count: self.fields.len(),
746 node_count: self.nodes.len(),
747 glyph_positions: self.glyphs.iter().map(|(_, g)| g.position.to_array()).collect(),
748 entity_positions: self.entities.iter().map(|(_, e)| e.position.to_array()).collect(),
749 field_positions: Vec::new(),
750 }
751 }
752
753 pub fn gc(&mut self) {
758 self.entities.retain(|(_, e)| !e.despawn_requested);
759 self.bvh_dirty = true;
760 }
761
762 pub fn clear(&mut self) {
764 self.glyphs = GlyphPool::new(8192);
765 self.particles = ParticlePool::new(4096);
766 self.entities.clear();
767 self.fields.clear();
768 self.nodes.clear();
769 self.root_nodes.clear();
770 self.zones.clear();
771 self.portals.clear();
772 self.bvh = None;
773 self.bvh_dirty = false;
774 self.events.clear();
775 self.stats = SceneStats::default();
776 }
777
778 pub fn spawn_glyph_grid(
780 &mut self,
781 origin: Vec3,
782 cols: u32, rows: u32,
783 spacing: f32,
784 glyph_fn: impl Fn(u32, u32) -> Glyph,
785 ) -> Vec<GlyphId> {
786 let mut ids = Vec::with_capacity((cols * rows) as usize);
787 for row in 0..rows {
788 for col in 0..cols {
789 let mut g = glyph_fn(col, row);
790 g.position = origin + Vec3::new(col as f32 * spacing, 0.0, row as f32 * spacing);
791 ids.push(self.spawn_glyph(g));
792 }
793 }
794 ids
795 }
796
797 pub fn spawn_glyph_ring(
799 &mut self,
800 center: Vec3,
801 radius: f32,
802 count: u32,
803 glyph_fn: impl Fn(u32) -> Glyph,
804 ) -> Vec<GlyphId> {
805 let mut ids = Vec::with_capacity(count as usize);
806 for i in 0..count {
807 let angle = i as f32 / count as f32 * std::f32::consts::TAU;
808 let mut g = glyph_fn(i);
809 g.position = center + Vec3::new(angle.cos() * radius, 0.0, angle.sin() * radius);
810 ids.push(self.spawn_glyph(g));
811 }
812 ids
813 }
814
815 pub fn despawn_glyphs(&mut self, ids: &[GlyphId]) {
817 for &id in ids { self.despawn_glyph(id); }
818 }
819
820 pub fn drain_events(&mut self) -> Vec<SceneEvent> { self.events.drain() }
823 pub fn push_event(&mut self, e: SceneEvent) { self.events.push(e); }
824
825 pub fn diagnostics(&self) -> String {
828 format!(
829 "Scene t={:.2}s | glyphs={} particles={} entities={} fields={} nodes={} zones={} portals={}",
830 self.time,
831 self.stats.glyph_count,
832 self.stats.particle_count,
833 self.stats.entity_count,
834 self.stats.field_count,
835 self.stats.node_count,
836 self.stats.zone_count,
837 self.stats.portal_count,
838 )
839 }
840}
841
842impl Default for Scene {
843 fn default() -> Self { Self::new() }
844}
845
846pub type SceneGraph = Scene;
848
849#[cfg(test)]
852mod tests {
853 use super::*;
854
855 fn make_glyph(pos: Vec3) -> Glyph {
856 Glyph { position: pos, ..Default::default() }
857 }
858
859 #[test]
860 fn scene_spawn_despawn_glyph() {
861 let mut s = Scene::new();
862 let id = s.spawn_glyph(make_glyph(Vec3::ZERO));
863 assert_eq!(s.glyphs.count(), 1);
864 s.despawn_glyph(id);
865 assert_eq!(s.glyphs.count(), 0);
866 }
867
868 #[test]
869 fn scene_spawn_entity() {
870 let mut s = Scene::new();
871 let e = AmorphousEntity { position: Vec3::ONE, ..Default::default() };
872 let id = s.spawn_entity(e);
873 assert!(s.get_entity(id).is_some());
874 s.despawn_entity(id);
875 assert!(s.get_entity(id).is_none());
876 }
877
878 #[test]
879 fn scene_fields() {
880 let mut s = Scene::new();
881 let field = ForceField::Gravity { center: Vec3::ZERO, strength: 9.81, falloff: crate::math::Falloff::InverseSquare };
882 let id = s.add_field(field);
883 assert!(s.get_field(id).is_some());
884 s.remove_field(id);
885 assert!(s.get_field(id).is_none());
886 }
887
888 #[test]
889 fn scene_node_hierarchy() {
890 let mut s = Scene::new();
891 let parent = s.create_node("parent");
892 let child = s.create_node("child");
893 s.attach_node(child, parent);
894 assert_eq!(s.get_node(child).unwrap().parent, Some(parent));
895 assert!(s.get_node(parent).unwrap().children.contains(&child));
896 }
897
898 #[test]
899 fn scene_node_find_by_name() {
900 let mut s = Scene::new();
901 s.create_node("the_node");
902 let id = s.find_node_by_name("the_node");
903 assert!(id.is_some());
904 }
905
906 #[test]
907 fn scene_tick_advances_time() {
908 let mut s = Scene::new();
909 s.tick(0.016);
910 assert!((s.time - 0.016).abs() < 1e-5);
911 }
912
913 #[test]
914 fn scene_glyphs_in_sphere() {
915 let mut s = Scene::new();
916 s.spawn_glyph(make_glyph(Vec3::ZERO));
917 s.spawn_glyph(make_glyph(Vec3::new(100.0, 0.0, 0.0)));
918 let hits = s.glyphs_in_sphere(Vec3::ZERO, 5.0);
919 assert_eq!(hits.len(), 1);
920 }
921
922 #[test]
923 fn scene_raycast_glyphs() {
924 let mut s = Scene::new();
925 s.spawn_glyph(make_glyph(Vec3::new(0.0, 0.0, 10.0)));
926 let hit = s.raycast_glyphs(Vec3::ZERO, Vec3::Z, 100.0);
927 assert!(hit.is_some());
928 assert!((hit.unwrap().distance - 10.0).abs() < 1.0);
929 }
930
931 #[test]
932 fn scene_layer_visibility() {
933 let mut s = Scene::new();
934 s.set_layer_visible(LayerId::ENTITIES, false);
935 assert!(!s.is_layer_visible(LayerId::ENTITIES));
936 s.set_layer_visible(LayerId::ENTITIES, true);
937 assert!(s.is_layer_visible(LayerId::ENTITIES));
938 }
939
940 #[test]
941 fn scene_ambient_zone() {
942 let mut s = Scene::new();
943 s.add_zone(AmbientZone {
944 min: Vec3::splat(-5.0), max: Vec3::splat(5.0),
945 ambient_color: Vec4::ONE, fog_density: 0.0,
946 fog_color: Vec4::ZERO, reverb_wet: 0.0,
947 wind_strength: 0.0, gravity_scale: 1.0,
948 name: "test_zone".into(),
949 });
950 let result = s.zone_at(Vec3::ZERO);
951 assert!(result.is_some());
952 let outside = s.zone_at(Vec3::new(100.0, 0.0, 0.0));
953 assert!(outside.is_none());
954 }
955
956 #[test]
957 fn scene_portal() {
958 let mut s = Scene::new();
959 let a = s.add_portal(Vec3::ZERO, Vec3::new(100.0, 0.0, 0.0), Vec3::Z, Vec2::splat(2.0));
960 let b = s.add_portal(Vec3::new(100.0, 0.0, 0.0), Vec3::ZERO, Vec3::NEG_Z, Vec2::splat(2.0));
961 s.link_portals(a, b);
962 assert_eq!(s.portals[0].linked, Some(b));
963 assert_eq!(s.portals[1].linked, Some(a));
964 }
965
966 #[test]
967 fn scene_snapshot_diff() {
968 let mut s = Scene::new();
969 let snap1 = s.snapshot();
970 s.spawn_glyph(make_glyph(Vec3::ZERO));
971 let snap2 = s.snapshot();
972 let diff = snap1.diff(&snap2);
973 assert_eq!(diff.glyph_delta, 1);
974 }
975
976 #[test]
977 fn scene_spawn_glyph_ring() {
978 let mut s = Scene::new();
979 let ids = s.spawn_glyph_ring(Vec3::ZERO, 5.0, 8, |_| make_glyph(Vec3::ZERO));
980 assert_eq!(ids.len(), 8);
981 }
982
983 #[test]
984 fn scene_spawn_glyph_grid() {
985 let mut s = Scene::new();
986 let ids = s.spawn_glyph_grid(Vec3::ZERO, 4, 4, 1.0, |_, _| make_glyph(Vec3::ZERO));
987 assert_eq!(ids.len(), 16);
988 }
989
990 #[test]
991 fn scene_clear() {
992 let mut s = Scene::new();
993 s.spawn_glyph(make_glyph(Vec3::ZERO));
994 s.spawn_entity(AmorphousEntity::default());
995 s.clear();
996 assert_eq!(s.stats.glyph_count, 0);
997 }
998
999 #[test]
1000 fn scene_diagnostics_string() {
1001 let s = Scene::new();
1002 let d = s.diagnostics();
1003 assert!(d.contains("Scene"));
1004 }
1005
1006 #[test]
1007 fn transform3d_lerp() {
1008 let a = Transform3D::identity();
1009 let b = Transform3D::from_position(Vec3::new(10.0, 0.0, 0.0));
1010 let mid = a.lerp(&b, 0.5);
1011 assert!((mid.position.x - 5.0).abs() < 0.01);
1012 }
1013
1014 #[test]
1015 fn transform3d_transform_point() {
1016 let t = Transform3D { position: Vec3::new(1.0, 2.0, 3.0), ..Transform3D::identity() };
1017 let p = t.transform_point(Vec3::ZERO);
1018 assert_eq!(p, Vec3::new(1.0, 2.0, 3.0));
1019 }
1020}