//! RON serialization for [`Scene`]. //! //! `hecs::Entity` handles are runtime values that are not stable across a //! save/load, so the scene is flattened to a list of records with array //! indices standing in for entity references. The list is built in a //! deterministic pre-order walk of the hierarchy, so a serialize → deserialize //! → serialize cycle is byte-for-byte stable. use std::collections::HashMap; use hecs::Entity; use serde::{Deserialize, Serialize}; use super::{Node, Scene, SceneError}; use crate::math::Transform; /// One entity in the flattened scene. `children` holds indices into the /// surrounding [`SceneData::nodes`] list. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] struct NodeRecord { name: String, enabled: bool, transform: Transform, children: Vec, } /// The serializable form of a [`Scene`]: a flat node list plus the indices of /// the root nodes. Parent links are implied by the `children` arrays. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] struct SceneData { nodes: Vec, roots: Vec, } impl Scene { /// Serializes the scene to a pretty-printed RON string. pub fn to_ron(&self) -> Result { let data = self.to_data(); ron::ser::to_string_pretty(&data, ron::ser::PrettyConfig::default()) .map_err(|e| SceneError::Serialize(e.to_string())) } /// Reconstructs a scene from a RON string produced by [`to_ron`](Self::to_ron). pub fn from_ron(ron: &str) -> Result { let data: SceneData = ron::from_str(ron).map_err(|e| SceneError::Deserialize(e.to_string()))?; Scene::from_data(&data) } /// Flattens the hierarchy into index-based records via a deterministic /// pre-order walk (roots in order, then each subtree depth-first). fn to_data(&self) -> SceneData { let mut index: HashMap = HashMap::with_capacity(self.len()); let mut order: Vec = Vec::with_capacity(self.len()); for &root in self.roots() { self.assign_indices(root, &mut index, &mut order); } let nodes = order .iter() .map(|&entity| { let node = self .get::(entity) .expect("entity in hierarchy must have a Node"); let transform = self .local_transform(entity) .expect("entity in hierarchy must have a Transform"); NodeRecord { name: node.name.clone(), enabled: node.enabled, transform, children: self.children(entity).iter().map(|c| index[c]).collect(), } }) .collect(); let roots = self.roots().iter().map(|r| index[r]).collect(); SceneData { nodes, roots } } /// Pre-order index assignment helper for [`to_data`](Self::to_data). fn assign_indices( &self, entity: Entity, index: &mut HashMap, order: &mut Vec, ) { index.insert(entity, order.len()); order.push(entity); for &child in self.children(entity) { self.assign_indices(child, index, order); } } /// Rebuilds a scene from flattened records, validating index references. fn from_data(data: &SceneData) -> Result { let mut scene = Scene::new(); let n = data.nodes.len(); // Spawn every entity first (as a root), so all indices resolve before // wiring up parent/child links. let entities: Vec = data .nodes .iter() .map(|rec| { scene.spawn( Node { name: rec.name.clone(), enabled: rec.enabled, }, rec.transform, ) }) .collect(); // Re-link: each record's children become children of that record's // entity (and are removed from the root list). for (i, rec) in data.nodes.iter().enumerate() { for &child_idx in &rec.children { let child = *entities .get(child_idx) .ok_or(SceneError::Deserialize(format!( "child index {child_idx} out of range (have {n} nodes)" )))?; scene .set_parent(child, Some(entities[i])) .map_err(|e| SceneError::Deserialize(e.to_string()))?; } } // Validate the declared roots match the entities left parentless. The // re-link above already produced the correct root set; we just confirm // the file's `roots` list is consistent so corrupt input is rejected. for &root_idx in &data.roots { let entity = *entities .get(root_idx) .ok_or(SceneError::Deserialize(format!( "root index {root_idx} out of range (have {n} nodes)" )))?; if scene.parent(entity).is_some() { return Err(SceneError::Deserialize(format!( "node {root_idx} is listed as a root but is also a child" ))); } } Ok(scene) } } #[cfg(test)] mod tests { use super::*; use crate::math::{Quat, Vec3}; use crate::scene::DespawnPolicy; /// Builds a small, varied scene used by the round-trip tests. fn sample() -> Scene { let mut scene = Scene::new(); let root = scene.spawn( Node { name: "root".into(), enabled: true, }, Transform::from_translation(Vec3::new(1.0, 2.0, 3.0)), ); let arm = scene.spawn_child( root, "arm", Transform::from_rotation(Quat::from_rotation_y(0.5)), ); scene.spawn_child(arm, "hand", Transform::from_scale(Vec3::splat(2.0))); let mut disabled = Node::new("disabled"); disabled.enabled = false; scene.spawn_child(root, disabled, Transform::IDENTITY); // A second independent root, to exercise multi-root serialization. scene.spawn("other-root", Transform::from_translation(Vec3::NEG_X)); scene } #[test] fn round_trip_preserves_structure() { let scene = sample(); let ron = scene.to_ron().unwrap(); let restored = Scene::from_ron(&ron).unwrap(); // Re-serializing the restored scene yields identical text: structure, // names, flags, transforms, and ordering all survived. assert_eq!(ron, restored.to_ron().unwrap()); assert_eq!(scene.len(), restored.len()); assert_eq!(scene.roots().len(), restored.roots().len()); } #[test] fn round_trip_preserves_world_transforms() { let scene = sample(); let restored = Scene::from_ron(&scene.to_ron().unwrap()).unwrap(); // Compare resolved world transforms by name, since entity ids differ // across the rebuild. let by_name = |s: &Scene| -> Vec<(String, Vec3)> { let worlds = s.world_transforms(); let mut v: Vec<_> = worlds .iter() .map(|(&e, t)| (s.name(e).unwrap(), t.translation)) .collect(); v.sort_by(|a, b| a.0.cmp(&b.0)); v }; let a = by_name(&scene); let b = by_name(&restored); assert_eq!(a.len(), b.len()); for ((na, ta), (nb, tb)) in a.iter().zip(b.iter()) { assert_eq!(na, nb); assert!((*ta - *tb).length() <= 1e-5, "{na}: {ta} vs {tb}"); } } #[test] fn empty_scene_round_trips() { let scene = Scene::new(); let restored = Scene::from_ron(&scene.to_ron().unwrap()).unwrap(); assert!(restored.is_empty()); } #[test] fn reordering_after_edits_still_round_trips() { // Mutating the scene (despawn) must not break index bookkeeping. let mut scene = sample(); let root = scene.roots()[0]; let kid = scene.children(root)[0]; scene.despawn(kid, DespawnPolicy::DetachChildren); let ron = scene.to_ron().unwrap(); assert_eq!(ron, Scene::from_ron(&ron).unwrap().to_ron().unwrap()); } #[test] fn corrupt_child_index_is_rejected() { let bad = r#"(nodes: [(name: "a", enabled: true, transform: (translation: (0,0,0), rotation: (0,0,0,1), scale: (1,1,1)), children: [5])], roots: [0])"#; assert!(Scene::from_ron(bad).is_err()); } }