Skip to main content

proof_engine/scene/
mod.rs

1//! Scene graph — manages all active glyphs, entities, particles, and force fields.
2//! Full implementation with BVH spatial index, scene queries, serialization,
3//! transform hierarchy, layer management, event system, and portals.
4
5pub 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// ─── IDs and handles ──────────────────────────────────────────────────────────
24
25/// Opaque ID for a force field in the scene.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
27pub struct FieldId(pub u32);
28
29/// Opaque ID for a scene node.
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
31pub struct NodeId(pub u32);
32
33/// Opaque ID for a layer.
34#[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/// Opaque ID for a portal (scene transition).
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
49pub struct PortalId(pub u32);
50
51// ─── Scene node (transform hierarchy) ────────────────────────────────────────
52
53/// A node in the scene hierarchy, holding a local transform.
54#[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,  // cached world transform
61    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/// Decomposed 3-D transform.
114#[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// ─── Scene layer ──────────────────────────────────────────────────────────────
159
160/// A render/logic layer controlling visibility and sort order.
161#[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// ─── Scene portal ─────────────────────────────────────────────────────────────
180
181/// A portal connects two points in space and renders the view from the other side.
182#[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// ─── Ambient zone ─────────────────────────────────────────────────────────────
196
197/// Axis-aligned region with ambient light/audio/fog properties.
198#[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        // Smooth fade near edges (within 2 units)
220        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// ─── Scene serialization ──────────────────────────────────────────────────────
229
230/// Serializable snapshot of the scene state.
231#[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    /// Flat list of active glyph positions + colors for quick diffing.
239    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// ─── Scene statistics ─────────────────────────────────────────────────────────
264
265/// Frame statistics for the scene.
266#[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
281// ─── Scene ────────────────────────────────────────────────────────────────────
282
283/// The complete scene: all renderable objects and active forces.
284pub 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    // Hierarchy
296    pub nodes:     HashMap<NodeId, SceneNode>,
297    pub root_nodes: Vec<NodeId>,
298
299    // Layers
300    pub layers:    [SceneLayer; 8],
301
302    // Ambient zones
303    pub zones:     Vec<AmbientZone>,
304
305    // Portals
306    pub portals:   Vec<Portal>,
307
308    // Spatial index (rebuilt on demand)
309    bvh_dirty:     bool,
310    pub bvh:       Option<Bvh>,
311
312    // Events
313    pub events:    SceneEventQueue,
314
315    // Stats
316    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    // ── Glyph management ────────────────────────────────────────────────────
353
354    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    // ── Entity management ────────────────────────────────────────────────────
374
375    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    // ── Field management ─────────────────────────────────────────────────────
399
400    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    // ── Node hierarchy ───────────────────────────────────────────────────────
420
421    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            // Detach glyphs
433            for glyph_id in &node.glyph_ids {
434                self.glyphs.despawn(*glyph_id);
435            }
436            // Detach from parent
437            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            // Recursively destroy children
445            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        // Remove from current root/parent
454        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        // Set new parent
464        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    /// Attach a glyph to a node so it moves with it.
498    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    // ── Layers ───────────────────────────────────────────────────────────────
505
506    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    // ── Ambient zones ────────────────────────────────────────────────────────
517
518    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    /// Find the ambient zone at a world position (first match, blended weight).
523    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    // ── Portals ──────────────────────────────────────────────────────────────
531
532    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    // ── Tick ─────────────────────────────────────────────────────────────────
545
546    /// Advance the scene by `dt` seconds: step physics, age glyphs/particles, apply fields.
547    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        // Apply force fields to glyphs
553        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        // Tick glyph pool
562        self.glyphs.tick(dt);
563
564        // Tick particle pool
565        self.particles.tick(dt);
566
567        // Apply fields to particles
568        for (_, field) in &self.fields {
569            self.particles.apply_field(field, self.time);
570        }
571
572        // Tick entities, then move their glyphs to wherever the springs put
573        // them. Without this second step the formation, cohesion and pulse are
574        // all computed and never seen: entities render as a static cloud.
575        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                    // Matter that has drifted from its slot cools and dims:
583                    // the light is the matter, so a scattered entity is a
584                    // darker one.
585                    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        // Update node world transforms
600        self.flush_transforms();
601
602        // Sync node-attached glyph positions
603        self.sync_node_glyphs();
604
605        // Update stats
606        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    // ── Transform propagation ─────────────────────────────────────────────────
616
617    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        // Collect (node_world, glyph_id) pairs to avoid borrow conflicts
648        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    // ── Spatial queries ──────────────────────────────────────────────────────
664
665    /// Rebuild the BVH if dirty. Call before performing spatial queries.
666    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    /// Find all glyphs within `radius` of `center`.
676    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    /// Raycast against all glyphs, return nearest hit.
688    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    /// Return all entities within radius.
712    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    /// Return the nearest entity to `point`, if any.
720    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    /// Return all entities with a given tag.
731    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    // ── Snapshot ─────────────────────────────────────────────────────────────
739
740    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    // ── Bulk operations ──────────────────────────────────────────────────────
754
755    /// Remove all expired glyphs/particles and despawned entities. Already handled by tick,
756    /// but can be called manually to force GC.
757    pub fn gc(&mut self) {
758        self.entities.retain(|(_, e)| !e.despawn_requested);
759        self.bvh_dirty = true;
760    }
761
762    /// Clear the entire scene.
763    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    /// Spawn a burst of glyphs in a grid pattern.
779    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    /// Spawn a ring of glyphs.
798    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    /// Despawn all glyphs in a list.
816    pub fn despawn_glyphs(&mut self, ids: &[GlyphId]) {
817        for &id in ids { self.despawn_glyph(id); }
818    }
819
820    // ── Event accessors ──────────────────────────────────────────────────────
821
822    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    // ── Diagnostics ──────────────────────────────────────────────────────────
826
827    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
846/// Backward compat alias used in lib.rs.
847pub type SceneGraph = Scene;
848
849// ─── Tests ────────────────────────────────────────────────────────────────────
850
851#[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}