1use std::io::{Read, Write, Seek, SeekFrom, Cursor};
35use std::path::PathBuf;
36use glam::{Vec3, Vec4};
37
38const MAGIC: &[u8; 8] = b"PROOFSCN";
43const SCENE_VERSION: u32 = 4;
44
45bitflags::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#[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#[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#[derive(Debug, Clone, Default)]
129pub struct SceneMetadata {
130 pub name: String,
131 pub author: String,
132 pub description: String,
133 pub created_at: u64, 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, pub scale: Vec3,
145 pub visible: bool,
146 pub tags: Vec<String>,
147 pub sdf_graph: Option<String>, 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#[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
244fn 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
270fn 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
323pub struct SceneSerializer;
328
329impl SceneSerializer {
330 pub fn write_binary(scene: &Scene) -> Vec<u8> {
333 let mut buf: Vec<u8> = Vec::with_capacity(65_536);
334
335 buf.extend_from_slice(MAGIC);
337 write_u32(&mut buf, SCENE_VERSION);
338 write_u32(&mut buf, scene.flags.bits());
339
340 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 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 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 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 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 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 write_u8(&mut buf, SectionKind::Eof as u8);
432 buf
433 }
434
435 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 }
562 }
563 }
564
565 Ok(scene)
566 }
567
568 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 pub fn estimate_size(scene: &Scene) -> usize {
634 8 + 4 + 4 + 128 + 64 + scene.entities.len() * 80
639 + scene.lights.len() * 60
640 + scene.force_fields.len() * 52
641 + scene.kit_params.len() * 32
642 + 1 }
644}
645
646pub 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 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#[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}