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
573        for (_, entity) in &mut self.entities {
574            entity.tick(dt, self.time);
575        }
576
577        // Update node world transforms
578        self.flush_transforms();
579
580        // Sync node-attached glyph positions
581        self.sync_node_glyphs();
582
583        // Update stats
584        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    // ── Transform propagation ─────────────────────────────────────────────────
594
595    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        // Collect (node_world, glyph_id) pairs to avoid borrow conflicts
626        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    // ── Spatial queries ──────────────────────────────────────────────────────
642
643    /// Rebuild the BVH if dirty. Call before performing spatial queries.
644    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    /// Find all glyphs within `radius` of `center`.
654    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    /// Raycast against all glyphs, return nearest hit.
666    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    /// Return all entities within radius.
690    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    /// Return the nearest entity to `point`, if any.
698    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    /// Return all entities with a given tag.
709    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    // ── Snapshot ─────────────────────────────────────────────────────────────
717
718    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    // ── Bulk operations ──────────────────────────────────────────────────────
732
733    /// Remove all expired glyphs/particles and despawned entities. Already handled by tick,
734    /// but can be called manually to force GC.
735    pub fn gc(&mut self) {
736        self.entities.retain(|(_, e)| !e.despawn_requested);
737        self.bvh_dirty = true;
738    }
739
740    /// Clear the entire scene.
741    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    /// Spawn a burst of glyphs in a grid pattern.
757    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    /// Spawn a ring of glyphs.
776    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    /// Despawn all glyphs in a list.
794    pub fn despawn_glyphs(&mut self, ids: &[GlyphId]) {
795        for &id in ids { self.despawn_glyph(id); }
796    }
797
798    // ── Event accessors ──────────────────────────────────────────────────────
799
800    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    // ── Diagnostics ──────────────────────────────────────────────────────────
804
805    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
824/// Backward compat alias used in lib.rs.
825pub type SceneGraph = Scene;
826
827// ─── Tests ────────────────────────────────────────────────────────────────────
828
829#[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}