f56a1eea3b
Full project snapshot migrated to new Gitea remote without history: engine, editor, physics, script, examples, tests, docs, and assets. Relicensed from GPLv3 to MIT and updated repo URLs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
243 lines
8.6 KiB
Rust
243 lines
8.6 KiB
Rust
//! 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<usize>,
|
|
}
|
|
|
|
/// 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<NodeRecord>,
|
|
roots: Vec<usize>,
|
|
}
|
|
|
|
impl Scene {
|
|
/// Serializes the scene to a pretty-printed RON string.
|
|
pub fn to_ron(&self) -> Result<String, SceneError> {
|
|
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<Scene, SceneError> {
|
|
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<Entity, usize> = HashMap::with_capacity(self.len());
|
|
let mut order: Vec<Entity> = 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::<Node>(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<Entity, usize>,
|
|
order: &mut Vec<Entity>,
|
|
) {
|
|
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<Scene, SceneError> {
|
|
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<Entity> = 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());
|
|
}
|
|
}
|