Skip to main content

proof_engine/editor/
scene_io.rs

1//! Scene I/O — save and load scenes to/from a custom binary format and a
2//! human-readable TOML format.
3//!
4//! # Binary format  (.scene)
5//!
6//! ```text
7//! magic:   [u8; 8]  = b"PROOFSCN"
8//! version: u32      = SCENE_VERSION
9//! flags:   u32      = SceneFlags bits
10//! --- sections ---
11//! [SectionKind::Entities]
12//!   count: u32
13//!   for each entity: EntityRecord (variable-length)
14//! [SectionKind::Lights]
15//!   count: u32
16//!   for each: LightRecord
17//! [SectionKind::ForceFields]
18//!   count: u32
19//!   for each: ForceFieldRecord
20//! [SectionKind::KitParams]
21//!   count: u32
22//!   for each: KitParamRecord (key:String, value:f32|bool|Vec4)
23//! [SectionKind::Eof]
24//! ```
25//!
26//! Strings are length-prefixed u16 UTF-8.  Vec3/Vec4 are four f32.
27//! All multi-byte integers are little-endian.
28//!
29//! # TOML format  (.toml)
30//!
31//! Human-readable subset; intended for version control diffing.
32//! Round-trips perfectly with the binary format for the supported fields.
33
34use std::io::{Read, Write, Seek, SeekFrom, Cursor};
35use std::path::PathBuf;
36use glam::{Vec3, Vec4};
37
38// ─────────────────────────────────────────────────────────────────────────────
39// Constants
40// ─────────────────────────────────────────────────────────────────────────────
41
42const MAGIC: &[u8; 8]      = b"PROOFSCN";
43const SCENE_VERSION: u32    = 4;
44
45// ─────────────────────────────────────────────────────────────────────────────
46// SceneFlags
47// ─────────────────────────────────────────────────────────────────────────────
48
49bitflags::bitflags! {
50    #[derive(Debug, Clone, Copy, Default)]
51    pub struct SceneFlags: u32 {
52        const COMPRESSED    = 0x0001;
53        const HAS_AUDIO     = 0x0002;
54        const HAS_SCRIPTS   = 0x0004;
55        const EDITOR_ONLY   = 0x0008;
56        const INSTANCED_SDF = 0x0010;
57    }
58}
59
60// ─────────────────────────────────────────────────────────────────────────────
61// SectionKind
62// ─────────────────────────────────────────────────────────────────────────────
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65#[repr(u8)]
66pub enum SectionKind {
67    Entities    = 0x01,
68    Lights      = 0x02,
69    ForceFields = 0x03,
70    KitParams   = 0x04,
71    Animations  = 0x05,
72    SdfGraphs   = 0x06,
73    Materials   = 0x07,
74    Camera      = 0x08,
75    Metadata    = 0x09,
76    Eof         = 0xFF,
77}
78
79impl SectionKind {
80    pub fn from_u8(v: u8) -> Option<Self> {
81        match v {
82            0x01 => Some(Self::Entities),
83            0x02 => Some(Self::Lights),
84            0x03 => Some(Self::ForceFields),
85            0x04 => Some(Self::KitParams),
86            0x05 => Some(Self::Animations),
87            0x06 => Some(Self::SdfGraphs),
88            0x07 => Some(Self::Materials),
89            0x08 => Some(Self::Camera),
90            0x09 => Some(Self::Metadata),
91            0xFF => Some(Self::Eof),
92            _    => None,
93        }
94    }
95}
96
97// ─────────────────────────────────────────────────────────────────────────────
98// IoError
99// ─────────────────────────────────────────────────────────────────────────────
100
101#[derive(Debug)]
102pub enum IoError {
103    BadMagic,
104    UnsupportedVersion(u32),
105    UnknownSection(u8),
106    Truncated,
107    Utf8Error,
108    Custom(String),
109}
110
111impl std::fmt::Display for IoError {
112    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113        match self {
114            IoError::BadMagic            => write!(f, "bad magic bytes — not a proof-engine scene"),
115            IoError::UnsupportedVersion(v) => write!(f, "unsupported scene version {v}"),
116            IoError::UnknownSection(s)   => write!(f, "unknown section kind {s:#04x}"),
117            IoError::Truncated           => write!(f, "unexpected end of file"),
118            IoError::Utf8Error           => write!(f, "invalid UTF-8 in string field"),
119            IoError::Custom(s)           => write!(f, "{s}"),
120        }
121    }
122}
123
124// ─────────────────────────────────────────────────────────────────────────────
125// Scene data model (serialised subset)
126// ─────────────────────────────────────────────────────────────────────────────
127
128#[derive(Debug, Clone, Default)]
129pub struct SceneMetadata {
130    pub name:        String,
131    pub author:      String,
132    pub description: String,
133    pub created_at:  u64,  // unix timestamp
134    pub edited_at:   u64,
135    pub engine_ver:  String,
136}
137
138#[derive(Debug, Clone)]
139pub struct EntityRecord {
140    pub id:         u32,
141    pub name:       String,
142    pub position:   Vec3,
143    pub rotation:   Vec4,  // xyzw quaternion
144    pub scale:      Vec3,
145    pub visible:    bool,
146    pub tags:       Vec<String>,
147    pub sdf_graph:  Option<String>,  // name reference
148    pub material:   Option<String>,
149}
150
151impl Default for EntityRecord {
152    fn default() -> Self {
153        Self {
154            id: 0, name: String::new(),
155            position: Vec3::ZERO, rotation: Vec4::new(0.0, 0.0, 0.0, 1.0),
156            scale: Vec3::ONE, visible: true, tags: Vec::new(),
157            sdf_graph: None, material: None,
158        }
159    }
160}
161
162#[derive(Debug, Clone)]
163pub enum LightKind { Directional, Point, Spot, Area, Sky }
164
165#[derive(Debug, Clone)]
166pub struct LightRecord {
167    pub id:        u32,
168    pub name:      String,
169    pub kind:      LightKind,
170    pub position:  Vec3,
171    pub direction: Vec3,
172    pub color:     Vec4,
173    pub intensity: f32,
174    pub range:     f32,
175    pub enabled:   bool,
176}
177
178#[derive(Debug, Clone)]
179pub enum ForceFieldKind { Gravity, Vortex, Attractor, HeatSource, Wind, Pulsing }
180
181#[derive(Debug, Clone)]
182pub struct ForceFieldRecord {
183    pub id:       u32,
184    pub name:     String,
185    pub kind:     ForceFieldKind,
186    pub position: Vec3,
187    pub params:   [f32; 8],
188    pub enabled:  bool,
189}
190
191#[derive(Debug, Clone)]
192pub enum KitParamValue {
193    Float(f32),
194    Bool(bool),
195    Vec3(Vec3),
196    Vec4(Vec4),
197    Int(i32),
198    String(String),
199}
200
201#[derive(Debug, Clone)]
202pub struct KitParamRecord {
203    pub group: String,
204    pub key:   String,
205    pub value: KitParamValue,
206}
207
208#[derive(Debug, Clone)]
209pub struct CameraRecord {
210    pub position:   Vec3,
211    pub focal:      Vec3,
212    pub azimuth:    f32,
213    pub elevation:  f32,
214    pub distance:   f32,
215    pub fov:        f32,
216    pub near:       f32,
217    pub far:        f32,
218    pub ortho:      bool,
219}
220
221impl Default for CameraRecord {
222    fn default() -> Self {
223        Self {
224            position: Vec3::new(0.0, 0.0, 4.0),
225            focal: Vec3::ZERO,
226            azimuth: 0.0, elevation: 0.2, distance: 4.0,
227            fov: 65.0, near: 0.01, far: 1000.0, ortho: false,
228        }
229    }
230}
231
232/// A complete scene in memory.
233#[derive(Debug, Clone, Default)]
234pub struct Scene {
235    pub meta:         SceneMetadata,
236    pub entities:     Vec<EntityRecord>,
237    pub lights:       Vec<LightRecord>,
238    pub force_fields: Vec<ForceFieldRecord>,
239    pub kit_params:   Vec<KitParamRecord>,
240    pub camera:       CameraRecord,
241    pub flags:        SceneFlags,
242}
243
244// ─────────────────────────────────────────────────────────────────────────────
245// Binary writer helpers
246// ─────────────────────────────────────────────────────────────────────────────
247
248fn write_u8(buf: &mut Vec<u8>, v: u8)  { buf.push(v); }
249fn write_u16(buf: &mut Vec<u8>, v: u16) { buf.extend_from_slice(&v.to_le_bytes()); }
250fn write_u32(buf: &mut Vec<u8>, v: u32) { buf.extend_from_slice(&v.to_le_bytes()); }
251fn write_u64(buf: &mut Vec<u8>, v: u64) { buf.extend_from_slice(&v.to_le_bytes()); }
252fn write_f32(buf: &mut Vec<u8>, v: f32) { buf.extend_from_slice(&v.to_le_bytes()); }
253fn write_bool(buf: &mut Vec<u8>, v: bool) { buf.push(v as u8); }
254
255fn write_str(buf: &mut Vec<u8>, s: &str) {
256    let bytes = s.as_bytes();
257    let len = bytes.len().min(u16::MAX as usize) as u16;
258    write_u16(buf, len);
259    buf.extend_from_slice(&bytes[..len as usize]);
260}
261
262fn write_vec3(buf: &mut Vec<u8>, v: Vec3) {
263    write_f32(buf, v.x); write_f32(buf, v.y); write_f32(buf, v.z);
264}
265
266fn write_vec4(buf: &mut Vec<u8>, v: Vec4) {
267    write_f32(buf, v.x); write_f32(buf, v.y); write_f32(buf, v.z); write_f32(buf, v.w);
268}
269
270// ─────────────────────────────────────────────────────────────────────────────
271// Binary reader helpers
272// ─────────────────────────────────────────────────────────────────────────────
273
274fn read_u8(cur: &mut Cursor<&[u8]>) -> Result<u8, IoError> {
275    let mut b = [0u8; 1];
276    cur.read_exact(&mut b).map_err(|_| IoError::Truncated)?;
277    Ok(b[0])
278}
279
280fn read_u16(cur: &mut Cursor<&[u8]>) -> Result<u16, IoError> {
281    let mut b = [0u8; 2];
282    cur.read_exact(&mut b).map_err(|_| IoError::Truncated)?;
283    Ok(u16::from_le_bytes(b))
284}
285
286fn read_u32(cur: &mut Cursor<&[u8]>) -> Result<u32, IoError> {
287    let mut b = [0u8; 4];
288    cur.read_exact(&mut b).map_err(|_| IoError::Truncated)?;
289    Ok(u32::from_le_bytes(b))
290}
291
292fn read_u64(cur: &mut Cursor<&[u8]>) -> Result<u64, IoError> {
293    let mut b = [0u8; 8];
294    cur.read_exact(&mut b).map_err(|_| IoError::Truncated)?;
295    Ok(u64::from_le_bytes(b))
296}
297
298fn read_f32(cur: &mut Cursor<&[u8]>) -> Result<f32, IoError> {
299    let mut b = [0u8; 4];
300    cur.read_exact(&mut b).map_err(|_| IoError::Truncated)?;
301    Ok(f32::from_le_bytes(b))
302}
303
304fn read_bool(cur: &mut Cursor<&[u8]>) -> Result<bool, IoError> {
305    Ok(read_u8(cur)? != 0)
306}
307
308fn read_str(cur: &mut Cursor<&[u8]>) -> Result<String, IoError> {
309    let len = read_u16(cur)? as usize;
310    let mut buf = vec![0u8; len];
311    cur.read_exact(&mut buf).map_err(|_| IoError::Truncated)?;
312    String::from_utf8(buf).map_err(|_| IoError::Utf8Error)
313}
314
315fn read_vec3(cur: &mut Cursor<&[u8]>) -> Result<Vec3, IoError> {
316    Ok(Vec3::new(read_f32(cur)?, read_f32(cur)?, read_f32(cur)?))
317}
318
319fn read_vec4(cur: &mut Cursor<&[u8]>) -> Result<Vec4, IoError> {
320    Ok(Vec4::new(read_f32(cur)?, read_f32(cur)?, read_f32(cur)?, read_f32(cur)?))
321}
322
323// ─────────────────────────────────────────────────────────────────────────────
324// SceneSerializer
325// ─────────────────────────────────────────────────────────────────────────────
326
327pub struct SceneSerializer;
328
329impl SceneSerializer {
330    // ── Write ─────────────────────────────────────────────────────────────
331
332    pub fn write_binary(scene: &Scene) -> Vec<u8> {
333        let mut buf: Vec<u8> = Vec::with_capacity(65_536);
334
335        // Header
336        buf.extend_from_slice(MAGIC);
337        write_u32(&mut buf, SCENE_VERSION);
338        write_u32(&mut buf, scene.flags.bits());
339
340        // Metadata section
341        write_u8(&mut buf, SectionKind::Metadata as u8);
342        write_str(&mut buf, &scene.meta.name);
343        write_str(&mut buf, &scene.meta.author);
344        write_str(&mut buf, &scene.meta.description);
345        write_u64(&mut buf, scene.meta.created_at);
346        write_u64(&mut buf, scene.meta.edited_at);
347        write_str(&mut buf, &scene.meta.engine_ver);
348
349        // Camera section
350        write_u8(&mut buf, SectionKind::Camera as u8);
351        let c = &scene.camera;
352        write_vec3(&mut buf, c.position);
353        write_vec3(&mut buf, c.focal);
354        write_f32(&mut buf, c.azimuth);
355        write_f32(&mut buf, c.elevation);
356        write_f32(&mut buf, c.distance);
357        write_f32(&mut buf, c.fov);
358        write_f32(&mut buf, c.near);
359        write_f32(&mut buf, c.far);
360        write_bool(&mut buf, c.ortho);
361
362        // Entities section
363        write_u8(&mut buf, SectionKind::Entities as u8);
364        write_u32(&mut buf, scene.entities.len() as u32);
365        for e in &scene.entities {
366            write_u32(&mut buf, e.id);
367            write_str(&mut buf, &e.name);
368            write_vec3(&mut buf, e.position);
369            write_vec4(&mut buf, e.rotation);
370            write_vec3(&mut buf, e.scale);
371            write_bool(&mut buf, e.visible);
372            write_u16(&mut buf, e.tags.len() as u16);
373            for tag in &e.tags { write_str(&mut buf, tag); }
374            let sdf = e.sdf_graph.as_deref().unwrap_or("");
375            write_str(&mut buf, sdf);
376            let mat = e.material.as_deref().unwrap_or("");
377            write_str(&mut buf, mat);
378        }
379
380        // Lights section
381        write_u8(&mut buf, SectionKind::Lights as u8);
382        write_u32(&mut buf, scene.lights.len() as u32);
383        for l in &scene.lights {
384            write_u32(&mut buf, l.id);
385            write_str(&mut buf, &l.name);
386            write_u8(&mut buf, match l.kind {
387                LightKind::Directional => 0, LightKind::Point => 1,
388                LightKind::Spot => 2, LightKind::Area => 3, LightKind::Sky => 4,
389            });
390            write_vec3(&mut buf, l.position);
391            write_vec3(&mut buf, l.direction);
392            write_vec4(&mut buf, l.color);
393            write_f32(&mut buf, l.intensity);
394            write_f32(&mut buf, l.range);
395            write_bool(&mut buf, l.enabled);
396        }
397
398        // ForceFields section
399        write_u8(&mut buf, SectionKind::ForceFields as u8);
400        write_u32(&mut buf, scene.force_fields.len() as u32);
401        for ff in &scene.force_fields {
402            write_u32(&mut buf, ff.id);
403            write_str(&mut buf, &ff.name);
404            write_u8(&mut buf, match ff.kind {
405                ForceFieldKind::Gravity => 0, ForceFieldKind::Vortex => 1,
406                ForceFieldKind::Attractor => 2, ForceFieldKind::HeatSource => 3,
407                ForceFieldKind::Wind => 4, ForceFieldKind::Pulsing => 5,
408            });
409            write_vec3(&mut buf, ff.position);
410            for &p in &ff.params { write_f32(&mut buf, p); }
411            write_bool(&mut buf, ff.enabled);
412        }
413
414        // Kit params section
415        write_u8(&mut buf, SectionKind::KitParams as u8);
416        write_u32(&mut buf, scene.kit_params.len() as u32);
417        for kp in &scene.kit_params {
418            write_str(&mut buf, &kp.group);
419            write_str(&mut buf, &kp.key);
420            match &kp.value {
421                KitParamValue::Float(v)  => { write_u8(&mut buf, 0); write_f32(&mut buf, *v); }
422                KitParamValue::Bool(v)   => { write_u8(&mut buf, 1); write_bool(&mut buf, *v); }
423                KitParamValue::Vec3(v)   => { write_u8(&mut buf, 2); write_vec3(&mut buf, *v); }
424                KitParamValue::Vec4(v)   => { write_u8(&mut buf, 3); write_vec4(&mut buf, *v); }
425                KitParamValue::Int(v)    => { write_u8(&mut buf, 4); write_u32(&mut buf, *v as u32); }
426                KitParamValue::String(v) => { write_u8(&mut buf, 5); write_str(&mut buf, v); }
427            }
428        }
429
430        // EOF marker
431        write_u8(&mut buf, SectionKind::Eof as u8);
432        buf
433    }
434
435    // ── Read ──────────────────────────────────────────────────────────────
436
437    pub fn read_binary(data: &[u8]) -> Result<Scene, IoError> {
438        let mut cur = Cursor::new(data);
439        let mut magic = [0u8; 8];
440        cur.read_exact(&mut magic).map_err(|_| IoError::Truncated)?;
441        if &magic != MAGIC { return Err(IoError::BadMagic); }
442
443        let version = read_u32(&mut cur)?;
444        if version > SCENE_VERSION { return Err(IoError::UnsupportedVersion(version)); }
445
446        let flags_bits = read_u32(&mut cur)?;
447        let flags = SceneFlags::from_bits_truncate(flags_bits);
448        let mut scene = Scene { flags, ..Default::default() };
449
450        loop {
451            let kind_byte = read_u8(&mut cur)?;
452            let kind = SectionKind::from_u8(kind_byte).ok_or(IoError::UnknownSection(kind_byte))?;
453            match kind {
454                SectionKind::Eof => break,
455
456                SectionKind::Metadata => {
457                    scene.meta.name        = read_str(&mut cur)?;
458                    scene.meta.author      = read_str(&mut cur)?;
459                    scene.meta.description = read_str(&mut cur)?;
460                    scene.meta.created_at  = read_u64(&mut cur)?;
461                    scene.meta.edited_at   = read_u64(&mut cur)?;
462                    scene.meta.engine_ver  = read_str(&mut cur)?;
463                }
464
465                SectionKind::Camera => {
466                    scene.camera.position  = read_vec3(&mut cur)?;
467                    scene.camera.focal     = read_vec3(&mut cur)?;
468                    scene.camera.azimuth   = read_f32(&mut cur)?;
469                    scene.camera.elevation = read_f32(&mut cur)?;
470                    scene.camera.distance  = read_f32(&mut cur)?;
471                    scene.camera.fov       = read_f32(&mut cur)?;
472                    scene.camera.near      = read_f32(&mut cur)?;
473                    scene.camera.far       = read_f32(&mut cur)?;
474                    scene.camera.ortho     = read_bool(&mut cur)?;
475                }
476
477                SectionKind::Entities => {
478                    let count = read_u32(&mut cur)? as usize;
479                    for _ in 0..count {
480                        let mut e = EntityRecord::default();
481                        e.id       = read_u32(&mut cur)?;
482                        e.name     = read_str(&mut cur)?;
483                        e.position = read_vec3(&mut cur)?;
484                        e.rotation = read_vec4(&mut cur)?;
485                        e.scale    = read_vec3(&mut cur)?;
486                        e.visible  = read_bool(&mut cur)?;
487                        let n_tags = read_u16(&mut cur)? as usize;
488                        for _ in 0..n_tags { e.tags.push(read_str(&mut cur)?); }
489                        let sdf = read_str(&mut cur)?;
490                        if !sdf.is_empty() { e.sdf_graph = Some(sdf); }
491                        let mat = read_str(&mut cur)?;
492                        if !mat.is_empty() { e.material = Some(mat); }
493                        scene.entities.push(e);
494                    }
495                }
496
497                SectionKind::Lights => {
498                    let count = read_u32(&mut cur)? as usize;
499                    for _ in 0..count {
500                        let id        = read_u32(&mut cur)?;
501                        let name      = read_str(&mut cur)?;
502                        let kind_byte = read_u8(&mut cur)?;
503                        let kind = match kind_byte {
504                            0 => LightKind::Directional, 1 => LightKind::Point,
505                            2 => LightKind::Spot, 3 => LightKind::Area,
506                            _ => LightKind::Sky,
507                        };
508                        let position  = read_vec3(&mut cur)?;
509                        let direction = read_vec3(&mut cur)?;
510                        let color     = read_vec4(&mut cur)?;
511                        let intensity = read_f32(&mut cur)?;
512                        let range     = read_f32(&mut cur)?;
513                        let enabled   = read_bool(&mut cur)?;
514                        scene.lights.push(LightRecord { id, name, kind, position, direction,
515                            color, intensity, range, enabled });
516                    }
517                }
518
519                SectionKind::ForceFields => {
520                    let count = read_u32(&mut cur)? as usize;
521                    for _ in 0..count {
522                        let id        = read_u32(&mut cur)?;
523                        let name      = read_str(&mut cur)?;
524                        let kind_byte = read_u8(&mut cur)?;
525                        let kind = match kind_byte {
526                            0 => ForceFieldKind::Gravity, 1 => ForceFieldKind::Vortex,
527                            2 => ForceFieldKind::Attractor, 3 => ForceFieldKind::HeatSource,
528                            4 => ForceFieldKind::Wind, _ => ForceFieldKind::Pulsing,
529                        };
530                        let position = read_vec3(&mut cur)?;
531                        let mut params = [0.0f32; 8];
532                        for p in &mut params { *p = read_f32(&mut cur)?; }
533                        let enabled = read_bool(&mut cur)?;
534                        scene.force_fields.push(ForceFieldRecord { id, name, kind, position, params, enabled });
535                    }
536                }
537
538                SectionKind::KitParams => {
539                    let count = read_u32(&mut cur)? as usize;
540                    for _ in 0..count {
541                        let group = read_str(&mut cur)?;
542                        let key   = read_str(&mut cur)?;
543                        let vtype = read_u8(&mut cur)?;
544                        let value = match vtype {
545                            0 => KitParamValue::Float(read_f32(&mut cur)?),
546                            1 => KitParamValue::Bool(read_bool(&mut cur)?),
547                            2 => KitParamValue::Vec3(read_vec3(&mut cur)?),
548                            3 => KitParamValue::Vec4(read_vec4(&mut cur)?),
549                            4 => KitParamValue::Int(read_u32(&mut cur)? as i32),
550                            5 => KitParamValue::String(read_str(&mut cur)?),
551                            _ => KitParamValue::Float(0.0),
552                        };
553                        scene.kit_params.push(KitParamRecord { group, key, value });
554                    }
555                }
556
557                SectionKind::Animations | SectionKind::SdfGraphs |
558                SectionKind::Materials => {
559                    // Skip unknown payload for forward compatibility
560                    // (We'd need section length for real skip — simplified here)
561                }
562            }
563        }
564
565        Ok(scene)
566    }
567
568    // ── TOML ──────────────────────────────────────────────────────────────
569
570    pub fn write_toml(scene: &Scene) -> String {
571        let mut out = String::with_capacity(8192);
572        out.push_str(&format!(
573            "[meta]\nname = {:?}\nauthor = {:?}\ndescription = {:?}\nengine_ver = {:?}\n\n",
574            scene.meta.name, scene.meta.author, scene.meta.description, scene.meta.engine_ver
575        ));
576
577        let c = &scene.camera;
578        out.push_str(&format!(
579            "[camera]\nposition = [{:.6}, {:.6}, {:.6}]\nfocal = [{:.6}, {:.6}, {:.6}]\n\
580             azimuth = {:.6}\nelevation = {:.6}\ndistance = {:.6}\nfov = {:.2}\n\
581             near = {:.4}\nfar = {:.2}\northo = {}\n\n",
582            c.position.x, c.position.y, c.position.z,
583            c.focal.x, c.focal.y, c.focal.z,
584            c.azimuth, c.elevation, c.distance, c.fov, c.near, c.far, c.ortho
585        ));
586
587        for (i, e) in scene.entities.iter().enumerate() {
588            out.push_str(&format!("[[entities]]  # {i}\n"));
589            out.push_str(&format!("id = {}\nname = {:?}\n", e.id, e.name));
590            out.push_str(&format!("position = [{:.6}, {:.6}, {:.6}]\n",
591                e.position.x, e.position.y, e.position.z));
592            out.push_str(&format!("rotation = [{:.6}, {:.6}, {:.6}, {:.6}]\n",
593                e.rotation.x, e.rotation.y, e.rotation.z, e.rotation.w));
594            out.push_str(&format!("scale = [{:.6}, {:.6}, {:.6}]\n",
595                e.scale.x, e.scale.y, e.scale.z));
596            out.push_str(&format!("visible = {}\n", e.visible));
597            if let Some(sdf) = &e.sdf_graph {
598                out.push_str(&format!("sdf_graph = {:?}\n", sdf));
599            }
600            if let Some(mat) = &e.material {
601                out.push_str(&format!("material = {:?}\n", mat));
602            }
603            out.push('\n');
604        }
605
606        for l in &scene.lights {
607            out.push_str("[[lights]]\n");
608            out.push_str(&format!("id = {}\nname = {:?}\n", l.id, l.name));
609            out.push_str(&format!("position = [{:.6}, {:.6}, {:.6}]\n",
610                l.position.x, l.position.y, l.position.z));
611            out.push_str(&format!("color = [{:.4}, {:.4}, {:.4}, {:.4}]\n",
612                l.color.x, l.color.y, l.color.z, l.color.w));
613            out.push_str(&format!("intensity = {:.4}\nenabled = {}\n\n", l.intensity, l.enabled));
614        }
615
616        for kp in &scene.kit_params {
617            let val_str = match &kp.value {
618                KitParamValue::Float(v) => format!("{:.6}", v),
619                KitParamValue::Bool(v)  => format!("{}", v),
620                KitParamValue::Vec3(v)  => format!("[{:.4}, {:.4}, {:.4}]", v.x, v.y, v.z),
621                KitParamValue::Vec4(v)  => format!("[{:.4}, {:.4}, {:.4}, {:.4}]", v.x, v.y, v.z, v.w),
622                KitParamValue::Int(v)   => format!("{}", v),
623                KitParamValue::String(v)=> format!("{:?}", v),
624            };
625            out.push_str(&format!("# [{}.{}] = {}\n", kp.group, kp.key, val_str));
626        }
627
628        out
629    }
630
631    // ── Size estimation ───────────────────────────────────────────────────
632
633    pub fn estimate_size(scene: &Scene) -> usize {
634        // header + metadata + camera + per-entity + per-light + per-ff + per-param
635        8 + 4 + 4   // magic + version + flags
636        + 128       // metadata strings
637        + 64        // camera
638        + scene.entities.len() * 80
639        + scene.lights.len() * 60
640        + scene.force_fields.len() * 52
641        + scene.kit_params.len() * 32
642        + 1         // EOF
643    }
644}
645
646// ─────────────────────────────────────────────────────────────────────────────
647// SceneUndoManager
648// ─────────────────────────────────────────────────────────────────────────────
649
650/// Manages undo/redo for scene-level edits using full-snapshot approach.
651pub struct SceneUndoManager {
652    pub max_history: usize,
653    snapshots:       std::collections::VecDeque<Vec<u8>>,
654    current:         usize,
655}
656
657impl SceneUndoManager {
658    pub fn new(max_history: usize) -> Self {
659        Self { max_history, snapshots: std::collections::VecDeque::new(), current: 0 }
660    }
661
662    pub fn checkpoint(&mut self, scene: &Scene) {
663        // Truncate redo history
664        while self.snapshots.len() > self.current + 1 {
665            self.snapshots.pop_back();
666        }
667        let bytes = SceneSerializer::write_binary(scene);
668        self.snapshots.push_back(bytes);
669        if self.snapshots.len() > self.max_history {
670            self.snapshots.pop_front();
671        }
672        self.current = self.snapshots.len().saturating_sub(1);
673    }
674
675    pub fn undo(&mut self) -> Option<Scene> {
676        if self.current == 0 { return None; }
677        self.current -= 1;
678        let bytes = self.snapshots.get(self.current)?;
679        SceneSerializer::read_binary(bytes).ok()
680    }
681
682    pub fn redo(&mut self) -> Option<Scene> {
683        if self.current + 1 >= self.snapshots.len() { return None; }
684        self.current += 1;
685        let bytes = self.snapshots.get(self.current)?;
686        SceneSerializer::read_binary(bytes).ok()
687    }
688
689    pub fn can_undo(&self) -> bool { self.current > 0 }
690    pub fn can_redo(&self) -> bool { self.current + 1 < self.snapshots.len() }
691    pub fn history_len(&self) -> usize { self.snapshots.len() }
692}
693
694// ─────────────────────────────────────────────────────────────────────────────
695// Tests
696// ─────────────────────────────────────────────────────────────────────────────
697
698#[cfg(test)]
699mod tests {
700    use super::*;
701
702    fn make_scene() -> Scene {
703        let mut s = Scene::default();
704        s.meta.name = "TestScene".into();
705        s.meta.author = "Unit Test".into();
706        s.entities.push(EntityRecord {
707            id: 1, name: "Leon".into(),
708            position: Vec3::new(0.0, 1.0, 0.0),
709            rotation: Vec4::new(0.0, 0.0, 0.0, 1.0),
710            scale: Vec3::ONE,
711            visible: true,
712            tags: vec!["character".into()],
713            sdf_graph: Some("leon.sdf".into()),
714            material: None,
715        });
716        s.kit_params.push(KitParamRecord {
717            group: "Bloom".into(), key: "intensity".into(),
718            value: KitParamValue::Float(3.0),
719        });
720        s
721    }
722
723    #[test]
724    fn binary_roundtrip() {
725        let scene = make_scene();
726        let bytes = SceneSerializer::write_binary(&scene);
727        assert!(bytes.len() > 8);
728        let loaded = SceneSerializer::read_binary(&bytes).unwrap();
729        assert_eq!(loaded.meta.name, "TestScene");
730        assert_eq!(loaded.entities.len(), 1);
731        assert_eq!(loaded.entities[0].name, "Leon");
732        assert_eq!(loaded.kit_params.len(), 1);
733        if let KitParamValue::Float(v) = loaded.kit_params[0].value {
734            assert!((v - 3.0).abs() < 1e-5);
735        } else { panic!("wrong type"); }
736    }
737
738    #[test]
739    fn bad_magic_rejected() {
740        let mut bytes = SceneSerializer::write_binary(&make_scene());
741        bytes[0] = 0xFF;
742        assert!(matches!(SceneSerializer::read_binary(&bytes), Err(IoError::BadMagic)));
743    }
744
745    #[test]
746    fn toml_contains_name() {
747        let scene = make_scene();
748        let toml = SceneSerializer::write_toml(&scene);
749        assert!(toml.contains("TestScene"));
750        assert!(toml.contains("[camera]"));
751    }
752
753    #[test]
754    fn undo_manager_basic() {
755        let mut undo = SceneUndoManager::new(10);
756        let s1 = make_scene();
757        undo.checkpoint(&s1);
758        let mut s2 = make_scene();
759        s2.meta.name = "Modified".into();
760        undo.checkpoint(&s2);
761        let undone = undo.undo().unwrap();
762        assert_eq!(undone.meta.name, "TestScene");
763        assert!(undo.can_redo());
764        let redone = undo.redo().unwrap();
765        assert_eq!(redone.meta.name, "Modified");
766    }
767
768    #[test]
769    fn estimate_size_reasonable() {
770        let scene = make_scene();
771        let est = SceneSerializer::estimate_size(&scene);
772        let act = SceneSerializer::write_binary(&scene).len();
773        assert!(est > 0 && act > 0);
774    }
775}