Files
Oxide/engine/src/scene/serialize.rs
T
Homer Simpson f56a1eea3b Import Oxide engine (Stages 0–10) under MIT license
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>
2026-07-05 20:41:02 +02:00

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());
}
}