feat(ecs): add JSON/binary scene serialization and component registry
- Mini JSON writer/parser in voltex_ecs (no renderer dependency) - ComponentRegistry with register/find/register_defaults - serialize_scene_json/deserialize_scene_json with hex-encoded components - serialize_scene_binary/deserialize_scene_binary (VSCN binary format) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
263
crates/voltex_ecs/src/binary_scene.rs
Normal file
263
crates/voltex_ecs/src/binary_scene.rs
Normal file
@@ -0,0 +1,263 @@
|
||||
/// Binary scene format (.vscn binary).
|
||||
///
|
||||
/// Format:
|
||||
/// Header: "VSCN" (4 bytes) + version u32 LE + entity_count u32 LE
|
||||
/// Per entity:
|
||||
/// parent_index i32 LE (-1 = no parent)
|
||||
/// component_count u32 LE
|
||||
/// Per component:
|
||||
/// name_len u16 LE + name bytes
|
||||
/// data_len u32 LE + data bytes
|
||||
|
||||
use std::collections::HashMap;
|
||||
use crate::entity::Entity;
|
||||
use crate::world::World;
|
||||
use crate::transform::Transform;
|
||||
use crate::hierarchy::{add_child, Parent};
|
||||
use crate::component_registry::ComponentRegistry;
|
||||
|
||||
const MAGIC: &[u8; 4] = b"VSCN";
|
||||
const VERSION: u32 = 1;
|
||||
|
||||
/// Serialize all entities with a Transform to the binary scene format.
|
||||
pub fn serialize_scene_binary(world: &World, registry: &ComponentRegistry) -> Vec<u8> {
|
||||
let entities_with_transform: Vec<(Entity, Transform)> = world
|
||||
.query::<Transform>()
|
||||
.map(|(e, t)| (e, *t))
|
||||
.collect();
|
||||
|
||||
let entity_to_index: HashMap<Entity, usize> = entities_with_transform
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, (e, _))| (*e, i))
|
||||
.collect();
|
||||
|
||||
let entity_count = entities_with_transform.len() as u32;
|
||||
let mut buf = Vec::new();
|
||||
|
||||
// Header
|
||||
buf.extend_from_slice(MAGIC);
|
||||
buf.extend_from_slice(&VERSION.to_le_bytes());
|
||||
buf.extend_from_slice(&entity_count.to_le_bytes());
|
||||
|
||||
// Entities
|
||||
for (entity, _) in &entities_with_transform {
|
||||
// Parent index
|
||||
let parent_idx: i32 = if let Some(parent_comp) = world.get::<Parent>(*entity) {
|
||||
entity_to_index.get(&parent_comp.0)
|
||||
.map(|&i| i as i32)
|
||||
.unwrap_or(-1)
|
||||
} else {
|
||||
-1
|
||||
};
|
||||
buf.extend_from_slice(&parent_idx.to_le_bytes());
|
||||
|
||||
// Collect serializable components
|
||||
let mut comp_data: Vec<(&str, Vec<u8>)> = Vec::new();
|
||||
for entry in registry.entries() {
|
||||
if let Some(data) = (entry.serialize)(world, *entity) {
|
||||
comp_data.push((&entry.name, data));
|
||||
}
|
||||
}
|
||||
|
||||
let comp_count = comp_data.len() as u32;
|
||||
buf.extend_from_slice(&comp_count.to_le_bytes());
|
||||
|
||||
for (name, data) in &comp_data {
|
||||
let name_bytes = name.as_bytes();
|
||||
buf.extend_from_slice(&(name_bytes.len() as u16).to_le_bytes());
|
||||
buf.extend_from_slice(name_bytes);
|
||||
buf.extend_from_slice(&(data.len() as u32).to_le_bytes());
|
||||
buf.extend_from_slice(data);
|
||||
}
|
||||
}
|
||||
|
||||
buf
|
||||
}
|
||||
|
||||
/// Deserialize entities from binary scene data.
|
||||
pub fn deserialize_scene_binary(
|
||||
world: &mut World,
|
||||
data: &[u8],
|
||||
registry: &ComponentRegistry,
|
||||
) -> Result<Vec<Entity>, String> {
|
||||
if data.len() < 12 {
|
||||
return Err("Binary scene data too short".into());
|
||||
}
|
||||
|
||||
// Verify magic
|
||||
if &data[0..4] != MAGIC {
|
||||
return Err("Invalid magic bytes — expected VSCN".into());
|
||||
}
|
||||
|
||||
let version = u32::from_le_bytes([data[4], data[5], data[6], data[7]]);
|
||||
if version != 1 {
|
||||
return Err(format!("Unsupported binary scene version: {}", version));
|
||||
}
|
||||
|
||||
let entity_count = u32::from_le_bytes([data[8], data[9], data[10], data[11]]) as usize;
|
||||
let mut pos = 12;
|
||||
|
||||
let mut created: Vec<Entity> = Vec::with_capacity(entity_count);
|
||||
let mut parent_indices: Vec<i32> = Vec::with_capacity(entity_count);
|
||||
|
||||
for _ in 0..entity_count {
|
||||
// Parent index
|
||||
if pos + 4 > data.len() {
|
||||
return Err("Unexpected end of data reading parent index".into());
|
||||
}
|
||||
let parent_idx = i32::from_le_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
|
||||
pos += 4;
|
||||
|
||||
// Component count
|
||||
if pos + 4 > data.len() {
|
||||
return Err("Unexpected end of data reading component count".into());
|
||||
}
|
||||
let comp_count = u32::from_le_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as usize;
|
||||
pos += 4;
|
||||
|
||||
let entity = world.spawn();
|
||||
|
||||
for _ in 0..comp_count {
|
||||
// Name length
|
||||
if pos + 2 > data.len() {
|
||||
return Err("Unexpected end of data reading name length".into());
|
||||
}
|
||||
let name_len = u16::from_le_bytes([data[pos], data[pos + 1]]) as usize;
|
||||
pos += 2;
|
||||
|
||||
// Name
|
||||
if pos + name_len > data.len() {
|
||||
return Err("Unexpected end of data reading component name".into());
|
||||
}
|
||||
let name = std::str::from_utf8(&data[pos..pos + name_len])
|
||||
.map_err(|_| "Invalid UTF-8 in component name".to_string())?;
|
||||
pos += name_len;
|
||||
|
||||
// Data length
|
||||
if pos + 4 > data.len() {
|
||||
return Err("Unexpected end of data reading data length".into());
|
||||
}
|
||||
let data_len = u32::from_le_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as usize;
|
||||
pos += 4;
|
||||
|
||||
// Data
|
||||
if pos + data_len > data.len() {
|
||||
return Err("Unexpected end of data reading component data".into());
|
||||
}
|
||||
let comp_data = &data[pos..pos + data_len];
|
||||
pos += data_len;
|
||||
|
||||
// Deserialize via registry
|
||||
if let Some(entry) = registry.find(name) {
|
||||
(entry.deserialize)(world, entity, comp_data)?;
|
||||
}
|
||||
}
|
||||
|
||||
created.push(entity);
|
||||
parent_indices.push(parent_idx);
|
||||
}
|
||||
|
||||
// Apply parent relationships
|
||||
for (child_idx, &parent_idx) in parent_indices.iter().enumerate() {
|
||||
if parent_idx >= 0 {
|
||||
let pi = parent_idx as usize;
|
||||
if pi < created.len() {
|
||||
let child_entity = created[child_idx];
|
||||
let parent_entity = created[pi];
|
||||
add_child(world, parent_entity, child_entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(created)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::scene::Tag;
|
||||
use voltex_math::Vec3;
|
||||
|
||||
#[test]
|
||||
fn test_binary_roundtrip() {
|
||||
let mut registry = ComponentRegistry::new();
|
||||
registry.register_defaults();
|
||||
let mut world = World::new();
|
||||
let e = world.spawn();
|
||||
world.add(e, Transform::from_position(Vec3::new(5.0, 0.0, -1.0)));
|
||||
world.add(e, Tag("enemy".into()));
|
||||
|
||||
let data = serialize_scene_binary(&world, ®istry);
|
||||
assert_eq!(&data[0..4], b"VSCN");
|
||||
|
||||
let mut world2 = World::new();
|
||||
let entities = deserialize_scene_binary(&mut world2, &data, ®istry).unwrap();
|
||||
assert_eq!(entities.len(), 1);
|
||||
let t = world2.get::<Transform>(entities[0]).unwrap();
|
||||
assert!((t.position.x - 5.0).abs() < 1e-6);
|
||||
assert!((t.position.z - (-1.0)).abs() < 1e-6);
|
||||
let tag = world2.get::<Tag>(entities[0]).unwrap();
|
||||
assert_eq!(tag.0, "enemy");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_binary_with_hierarchy() {
|
||||
let mut registry = ComponentRegistry::new();
|
||||
registry.register_defaults();
|
||||
let mut world = World::new();
|
||||
let a = world.spawn();
|
||||
let b = world.spawn();
|
||||
world.add(a, Transform::new());
|
||||
world.add(b, Transform::new());
|
||||
add_child(&mut world, a, b);
|
||||
|
||||
let data = serialize_scene_binary(&world, ®istry);
|
||||
let mut world2 = World::new();
|
||||
let entities = deserialize_scene_binary(&mut world2, &data, ®istry).unwrap();
|
||||
assert_eq!(entities.len(), 2);
|
||||
assert!(world2.get::<Parent>(entities[1]).is_some());
|
||||
let p = world2.get::<Parent>(entities[1]).unwrap();
|
||||
assert_eq!(p.0, entities[0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_binary_invalid_magic() {
|
||||
let data = vec![0u8; 20];
|
||||
let mut world = World::new();
|
||||
let registry = ComponentRegistry::new();
|
||||
assert!(deserialize_scene_binary(&mut world, &data, ®istry).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_binary_too_short() {
|
||||
let data = vec![0u8; 5];
|
||||
let mut world = World::new();
|
||||
let registry = ComponentRegistry::new();
|
||||
assert!(deserialize_scene_binary(&mut world, &data, ®istry).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_binary_multiple_entities() {
|
||||
let mut registry = ComponentRegistry::new();
|
||||
registry.register_defaults();
|
||||
let mut world = World::new();
|
||||
for i in 0..3 {
|
||||
let e = world.spawn();
|
||||
world.add(e, Transform::from_position(Vec3::new(i as f32, 0.0, 0.0)));
|
||||
world.add(e, Tag(format!("e{}", i)));
|
||||
}
|
||||
|
||||
let data = serialize_scene_binary(&world, ®istry);
|
||||
let mut world2 = World::new();
|
||||
let entities = deserialize_scene_binary(&mut world2, &data, ®istry).unwrap();
|
||||
assert_eq!(entities.len(), 3);
|
||||
|
||||
for (i, &e) in entities.iter().enumerate() {
|
||||
let t = world2.get::<Transform>(e).unwrap();
|
||||
assert!((t.position.x - i as f32).abs() < 1e-6);
|
||||
let tag = world2.get::<Tag>(e).unwrap();
|
||||
assert_eq!(tag.0, format!("e{}", i));
|
||||
}
|
||||
}
|
||||
}
|
||||
198
crates/voltex_ecs/src/component_registry.rs
Normal file
198
crates/voltex_ecs/src/component_registry.rs
Normal file
@@ -0,0 +1,198 @@
|
||||
/// Registration-based component serialization for scene formats.
|
||||
/// Each registered component type has a name, a serialize function,
|
||||
/// and a deserialize function.
|
||||
|
||||
use crate::entity::Entity;
|
||||
use crate::world::World;
|
||||
|
||||
pub type SerializeFn = fn(&World, Entity) -> Option<Vec<u8>>;
|
||||
pub type DeserializeFn = fn(&mut World, Entity, &[u8]) -> Result<(), String>;
|
||||
|
||||
pub struct ComponentEntry {
|
||||
pub name: String,
|
||||
pub serialize: SerializeFn,
|
||||
pub deserialize: DeserializeFn,
|
||||
}
|
||||
|
||||
pub struct ComponentRegistry {
|
||||
entries: Vec<ComponentEntry>,
|
||||
}
|
||||
|
||||
impl ComponentRegistry {
|
||||
pub fn new() -> Self {
|
||||
Self { entries: Vec::new() }
|
||||
}
|
||||
|
||||
pub fn register(&mut self, name: &str, ser: SerializeFn, deser: DeserializeFn) {
|
||||
self.entries.push(ComponentEntry {
|
||||
name: name.to_string(),
|
||||
serialize: ser,
|
||||
deserialize: deser,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn find(&self, name: &str) -> Option<&ComponentEntry> {
|
||||
self.entries.iter().find(|e| e.name == name)
|
||||
}
|
||||
|
||||
pub fn entries(&self) -> &[ComponentEntry] {
|
||||
&self.entries
|
||||
}
|
||||
|
||||
/// Register the default built-in component types: transform and tag.
|
||||
pub fn register_defaults(&mut self) {
|
||||
self.register("transform", serialize_transform, deserialize_transform);
|
||||
self.register("tag", serialize_tag, deserialize_tag);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ComponentRegistry {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
// ── Transform: 9 f32s in little-endian (pos.xyz, rot.xyz, scale.xyz) ─
|
||||
|
||||
fn serialize_transform(world: &World, entity: Entity) -> Option<Vec<u8>> {
|
||||
let t = world.get::<crate::Transform>(entity)?;
|
||||
let mut data = Vec::with_capacity(36);
|
||||
for &v in &[
|
||||
t.position.x, t.position.y, t.position.z,
|
||||
t.rotation.x, t.rotation.y, t.rotation.z,
|
||||
t.scale.x, t.scale.y, t.scale.z,
|
||||
] {
|
||||
data.extend_from_slice(&v.to_le_bytes());
|
||||
}
|
||||
Some(data)
|
||||
}
|
||||
|
||||
fn deserialize_transform(world: &mut World, entity: Entity, data: &[u8]) -> Result<(), String> {
|
||||
if data.len() < 36 {
|
||||
return Err("Transform data too short".into());
|
||||
}
|
||||
let f = |off: usize| f32::from_le_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]]);
|
||||
let t = crate::Transform {
|
||||
position: voltex_math::Vec3::new(f(0), f(4), f(8)),
|
||||
rotation: voltex_math::Vec3::new(f(12), f(16), f(20)),
|
||||
scale: voltex_math::Vec3::new(f(24), f(28), f(32)),
|
||||
};
|
||||
world.add(entity, t);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ── Tag: UTF-8 string bytes ─────────────────────────────────────────
|
||||
|
||||
fn serialize_tag(world: &World, entity: Entity) -> Option<Vec<u8>> {
|
||||
let tag = world.get::<crate::scene::Tag>(entity)?;
|
||||
Some(tag.0.as_bytes().to_vec())
|
||||
}
|
||||
|
||||
fn deserialize_tag(world: &mut World, entity: Entity, data: &[u8]) -> Result<(), String> {
|
||||
let s = std::str::from_utf8(data).map_err(|_| "Invalid UTF-8 in tag".to_string())?;
|
||||
world.add(entity, crate::scene::Tag(s.to_string()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{World, Transform};
|
||||
use voltex_math::Vec3;
|
||||
|
||||
#[test]
|
||||
fn test_register_and_find() {
|
||||
let mut registry = ComponentRegistry::new();
|
||||
registry.register_defaults();
|
||||
assert!(registry.find("transform").is_some());
|
||||
assert!(registry.find("tag").is_some());
|
||||
assert!(registry.find("nonexistent").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_entries_count() {
|
||||
let mut registry = ComponentRegistry::new();
|
||||
registry.register_defaults();
|
||||
assert_eq!(registry.entries().len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialize_transform() {
|
||||
let mut registry = ComponentRegistry::new();
|
||||
registry.register_defaults();
|
||||
let mut world = World::new();
|
||||
let e = world.spawn();
|
||||
world.add(e, Transform::from_position(Vec3::new(1.0, 2.0, 3.0)));
|
||||
|
||||
let entry = registry.find("transform").unwrap();
|
||||
let data = (entry.serialize)(&world, e);
|
||||
assert!(data.is_some());
|
||||
assert_eq!(data.unwrap().len(), 36); // 9 f32s * 4 bytes
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialize_missing_component() {
|
||||
let mut registry = ComponentRegistry::new();
|
||||
registry.register_defaults();
|
||||
let mut world = World::new();
|
||||
let e = world.spawn();
|
||||
// no Transform added
|
||||
|
||||
let entry = registry.find("transform").unwrap();
|
||||
assert!((entry.serialize)(&world, e).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_roundtrip_transform() {
|
||||
let mut registry = ComponentRegistry::new();
|
||||
registry.register_defaults();
|
||||
let mut world = World::new();
|
||||
let e = world.spawn();
|
||||
world.add(e, Transform {
|
||||
position: Vec3::new(1.0, 2.0, 3.0),
|
||||
rotation: Vec3::new(0.1, 0.2, 0.3),
|
||||
scale: Vec3::new(4.0, 5.0, 6.0),
|
||||
});
|
||||
|
||||
let entry = registry.find("transform").unwrap();
|
||||
let data = (entry.serialize)(&world, e).unwrap();
|
||||
|
||||
let mut world2 = World::new();
|
||||
let e2 = world2.spawn();
|
||||
(entry.deserialize)(&mut world2, e2, &data).unwrap();
|
||||
|
||||
let t = world2.get::<Transform>(e2).unwrap();
|
||||
assert!((t.position.x - 1.0).abs() < 1e-6);
|
||||
assert!((t.position.y - 2.0).abs() < 1e-6);
|
||||
assert!((t.position.z - 3.0).abs() < 1e-6);
|
||||
assert!((t.rotation.x - 0.1).abs() < 1e-6);
|
||||
assert!((t.scale.x - 4.0).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_roundtrip_tag() {
|
||||
let mut registry = ComponentRegistry::new();
|
||||
registry.register_defaults();
|
||||
let mut world = World::new();
|
||||
let e = world.spawn();
|
||||
world.add(e, crate::scene::Tag("hello world".to_string()));
|
||||
|
||||
let entry = registry.find("tag").unwrap();
|
||||
let data = (entry.serialize)(&world, e).unwrap();
|
||||
|
||||
let mut world2 = World::new();
|
||||
let e2 = world2.spawn();
|
||||
(entry.deserialize)(&mut world2, e2, &data).unwrap();
|
||||
|
||||
let tag = world2.get::<crate::scene::Tag>(e2).unwrap();
|
||||
assert_eq!(tag.0, "hello world");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_transform_too_short() {
|
||||
let mut world = World::new();
|
||||
let e = world.spawn();
|
||||
let result = deserialize_transform(&mut world, e, &[0u8; 10]);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
476
crates/voltex_ecs/src/json.rs
Normal file
476
crates/voltex_ecs/src/json.rs
Normal file
@@ -0,0 +1,476 @@
|
||||
/// Mini JSON writer and parser for scene serialization.
|
||||
/// No external dependencies — self-contained within voltex_ecs.
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum JsonVal {
|
||||
Null,
|
||||
Bool(bool),
|
||||
Number(f64),
|
||||
Str(String),
|
||||
Array(Vec<JsonVal>),
|
||||
Object(Vec<(String, JsonVal)>),
|
||||
}
|
||||
|
||||
// ── Writer helpers ──────────────────────────────────────────────────
|
||||
|
||||
pub fn json_write_null() -> String {
|
||||
"null".to_string()
|
||||
}
|
||||
|
||||
pub fn json_write_f32(v: f32) -> String {
|
||||
// Emit integer form when the value has no fractional part
|
||||
if v.fract() == 0.0 && v.abs() < 1e15 {
|
||||
format!("{}", v as i64)
|
||||
} else {
|
||||
format!("{}", v)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn json_write_string(s: &str) -> String {
|
||||
let mut out = String::with_capacity(s.len() + 2);
|
||||
out.push('"');
|
||||
for c in s.chars() {
|
||||
match c {
|
||||
'"' => out.push_str("\\\""),
|
||||
'\\' => out.push_str("\\\\"),
|
||||
'\n' => out.push_str("\\n"),
|
||||
'\r' => out.push_str("\\r"),
|
||||
'\t' => out.push_str("\\t"),
|
||||
_ => out.push(c),
|
||||
}
|
||||
}
|
||||
out.push('"');
|
||||
out
|
||||
}
|
||||
|
||||
/// Write a JSON array from pre-formatted element strings.
|
||||
pub fn json_write_array(elements: &[&str]) -> String {
|
||||
let mut out = String::from("[");
|
||||
for (i, elem) in elements.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push(',');
|
||||
}
|
||||
out.push_str(elem);
|
||||
}
|
||||
out.push(']');
|
||||
out
|
||||
}
|
||||
|
||||
/// Write a JSON object from (key, pre-formatted-value) pairs.
|
||||
pub fn json_write_object(pairs: &[(&str, &str)]) -> String {
|
||||
let mut out = String::from("{");
|
||||
for (i, (key, val)) in pairs.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push(',');
|
||||
}
|
||||
out.push_str(&json_write_string(key));
|
||||
out.push(':');
|
||||
out.push_str(val);
|
||||
}
|
||||
out.push('}');
|
||||
out
|
||||
}
|
||||
|
||||
// ── Accessors on JsonVal ────────────────────────────────────────────
|
||||
|
||||
impl JsonVal {
|
||||
pub fn get(&self, key: &str) -> Option<&JsonVal> {
|
||||
match self {
|
||||
JsonVal::Object(pairs) => pairs.iter().find(|(k, _)| k == key).map(|(_, v)| v),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_f64(&self) -> Option<f64> {
|
||||
match self {
|
||||
JsonVal::Number(n) => Some(*n),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> Option<&str> {
|
||||
match self {
|
||||
JsonVal::Str(s) => Some(s.as_str()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_array(&self) -> Option<&Vec<JsonVal>> {
|
||||
match self {
|
||||
JsonVal::Array(a) => Some(a),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_object(&self) -> Option<&Vec<(String, JsonVal)>> {
|
||||
match self {
|
||||
JsonVal::Object(o) => Some(o),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_bool(&self) -> Option<bool> {
|
||||
match self {
|
||||
JsonVal::Bool(b) => Some(*b),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Parser (recursive descent) ──────────────────────────────────────
|
||||
|
||||
pub fn json_parse(input: &str) -> Result<JsonVal, String> {
|
||||
let bytes = input.as_bytes();
|
||||
let (val, pos) = parse_value(bytes, skip_ws(bytes, 0))?;
|
||||
let pos = skip_ws(bytes, pos);
|
||||
if pos != bytes.len() {
|
||||
return Err(format!("Unexpected trailing content at position {}", pos));
|
||||
}
|
||||
Ok(val)
|
||||
}
|
||||
|
||||
fn skip_ws(b: &[u8], mut pos: usize) -> usize {
|
||||
while pos < b.len() && matches!(b[pos], b' ' | b'\t' | b'\n' | b'\r') {
|
||||
pos += 1;
|
||||
}
|
||||
pos
|
||||
}
|
||||
|
||||
fn parse_value(b: &[u8], pos: usize) -> Result<(JsonVal, usize), String> {
|
||||
if pos >= b.len() {
|
||||
return Err("Unexpected end of input".into());
|
||||
}
|
||||
match b[pos] {
|
||||
b'"' => parse_string(b, pos),
|
||||
b'{' => parse_object(b, pos),
|
||||
b'[' => parse_array(b, pos),
|
||||
b't' | b'f' => parse_bool(b, pos),
|
||||
b'n' => parse_null(b, pos),
|
||||
_ => parse_number(b, pos),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_string(b: &[u8], pos: usize) -> Result<(JsonVal, usize), String> {
|
||||
let (s, end) = read_string(b, pos)?;
|
||||
Ok((JsonVal::Str(s), end))
|
||||
}
|
||||
|
||||
fn read_string(b: &[u8], pos: usize) -> Result<(String, usize), String> {
|
||||
if pos >= b.len() || b[pos] != b'"' {
|
||||
return Err(format!("Expected '\"' at position {}", pos));
|
||||
}
|
||||
let mut i = pos + 1;
|
||||
let mut s = String::new();
|
||||
while i < b.len() {
|
||||
match b[i] {
|
||||
b'"' => return Ok((s, i + 1)),
|
||||
b'\\' => {
|
||||
i += 1;
|
||||
if i >= b.len() {
|
||||
return Err("Unexpected end in string escape".into());
|
||||
}
|
||||
match b[i] {
|
||||
b'"' => s.push('"'),
|
||||
b'\\' => s.push('\\'),
|
||||
b'/' => s.push('/'),
|
||||
b'n' => s.push('\n'),
|
||||
b'r' => s.push('\r'),
|
||||
b't' => s.push('\t'),
|
||||
_ => {
|
||||
s.push('\\');
|
||||
s.push(b[i] as char);
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
ch => {
|
||||
s.push(ch as char);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
Err("Unterminated string".into())
|
||||
}
|
||||
|
||||
fn parse_number(b: &[u8], pos: usize) -> Result<(JsonVal, usize), String> {
|
||||
let mut i = pos;
|
||||
// optional minus
|
||||
if i < b.len() && b[i] == b'-' {
|
||||
i += 1;
|
||||
}
|
||||
// digits
|
||||
while i < b.len() && b[i].is_ascii_digit() {
|
||||
i += 1;
|
||||
}
|
||||
// fractional
|
||||
if i < b.len() && b[i] == b'.' {
|
||||
i += 1;
|
||||
while i < b.len() && b[i].is_ascii_digit() {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
// exponent
|
||||
if i < b.len() && (b[i] == b'e' || b[i] == b'E') {
|
||||
i += 1;
|
||||
if i < b.len() && (b[i] == b'+' || b[i] == b'-') {
|
||||
i += 1;
|
||||
}
|
||||
while i < b.len() && b[i].is_ascii_digit() {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
if i == pos {
|
||||
return Err(format!("Expected number at position {}", pos));
|
||||
}
|
||||
let s = std::str::from_utf8(&b[pos..i]).unwrap();
|
||||
let n: f64 = s.parse().map_err(|e| format!("Invalid number '{}': {}", s, e))?;
|
||||
Ok((JsonVal::Number(n), i))
|
||||
}
|
||||
|
||||
fn parse_bool(b: &[u8], pos: usize) -> Result<(JsonVal, usize), String> {
|
||||
if b[pos..].starts_with(b"true") {
|
||||
Ok((JsonVal::Bool(true), pos + 4))
|
||||
} else if b[pos..].starts_with(b"false") {
|
||||
Ok((JsonVal::Bool(false), pos + 5))
|
||||
} else {
|
||||
Err(format!("Expected bool at position {}", pos))
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_null(b: &[u8], pos: usize) -> Result<(JsonVal, usize), String> {
|
||||
if b[pos..].starts_with(b"null") {
|
||||
Ok((JsonVal::Null, pos + 4))
|
||||
} else {
|
||||
Err(format!("Expected null at position {}", pos))
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_array(b: &[u8], pos: usize) -> Result<(JsonVal, usize), String> {
|
||||
let mut i = pos + 1; // skip '['
|
||||
let mut arr = Vec::new();
|
||||
i = skip_ws(b, i);
|
||||
if i < b.len() && b[i] == b']' {
|
||||
return Ok((JsonVal::Array(arr), i + 1));
|
||||
}
|
||||
loop {
|
||||
i = skip_ws(b, i);
|
||||
let (val, next) = parse_value(b, i)?;
|
||||
arr.push(val);
|
||||
i = skip_ws(b, next);
|
||||
if i >= b.len() {
|
||||
return Err("Unterminated array".into());
|
||||
}
|
||||
if b[i] == b']' {
|
||||
return Ok((JsonVal::Array(arr), i + 1));
|
||||
}
|
||||
if b[i] != b',' {
|
||||
return Err(format!("Expected ',' or ']' at position {}", i));
|
||||
}
|
||||
i += 1; // skip ','
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_object(b: &[u8], pos: usize) -> Result<(JsonVal, usize), String> {
|
||||
let mut i = pos + 1; // skip '{'
|
||||
let mut pairs = Vec::new();
|
||||
i = skip_ws(b, i);
|
||||
if i < b.len() && b[i] == b'}' {
|
||||
return Ok((JsonVal::Object(pairs), i + 1));
|
||||
}
|
||||
loop {
|
||||
i = skip_ws(b, i);
|
||||
let (key, next) = read_string(b, i)?;
|
||||
i = skip_ws(b, next);
|
||||
if i >= b.len() || b[i] != b':' {
|
||||
return Err(format!("Expected ':' at position {}", i));
|
||||
}
|
||||
i = skip_ws(b, i + 1);
|
||||
let (val, next) = parse_value(b, i)?;
|
||||
pairs.push((key, val));
|
||||
i = skip_ws(b, next);
|
||||
if i >= b.len() {
|
||||
return Err("Unterminated object".into());
|
||||
}
|
||||
if b[i] == b'}' {
|
||||
return Ok((JsonVal::Object(pairs), i + 1));
|
||||
}
|
||||
if b[i] != b',' {
|
||||
return Err(format!("Expected ',' or '}}' at position {}", i));
|
||||
}
|
||||
i += 1; // skip ','
|
||||
}
|
||||
}
|
||||
|
||||
// ── JsonVal -> String serialization ─────────────────────────────────
|
||||
|
||||
impl JsonVal {
|
||||
/// Serialize this JsonVal to a compact JSON string.
|
||||
pub fn to_json_string(&self) -> String {
|
||||
match self {
|
||||
JsonVal::Null => "null".to_string(),
|
||||
JsonVal::Bool(b) => if *b { "true" } else { "false" }.to_string(),
|
||||
JsonVal::Number(n) => {
|
||||
if n.fract() == 0.0 && n.abs() < 1e15 {
|
||||
format!("{}", *n as i64)
|
||||
} else {
|
||||
format!("{}", n)
|
||||
}
|
||||
}
|
||||
JsonVal::Str(s) => json_write_string(s),
|
||||
JsonVal::Array(arr) => {
|
||||
let mut out = String::from("[");
|
||||
for (i, v) in arr.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push(',');
|
||||
}
|
||||
out.push_str(&v.to_json_string());
|
||||
}
|
||||
out.push(']');
|
||||
out
|
||||
}
|
||||
JsonVal::Object(pairs) => {
|
||||
let mut out = String::from("{");
|
||||
for (i, (k, v)) in pairs.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push(',');
|
||||
}
|
||||
out.push_str(&json_write_string(k));
|
||||
out.push(':');
|
||||
out.push_str(&v.to_json_string());
|
||||
}
|
||||
out.push('}');
|
||||
out
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_write_null() {
|
||||
assert_eq!(json_write_null(), "null");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_number() {
|
||||
assert_eq!(json_write_f32(3.14), "3.14");
|
||||
assert_eq!(json_write_f32(1.0), "1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_string() {
|
||||
assert_eq!(json_write_string("hello"), "\"hello\"");
|
||||
assert_eq!(json_write_string("a\"b"), "\"a\\\"b\"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_array() {
|
||||
assert_eq!(json_write_array(&["1", "2", "3"]), "[1,2,3]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_object() {
|
||||
let pairs = vec![("name", "\"test\""), ("value", "42")];
|
||||
let result = json_write_object(&pairs);
|
||||
assert_eq!(result, r#"{"name":"test","value":42}"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_number() {
|
||||
match json_parse("42.5").unwrap() {
|
||||
JsonVal::Number(n) => assert!((n - 42.5).abs() < 1e-10),
|
||||
_ => panic!("expected number"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_negative_number() {
|
||||
match json_parse("-3.14").unwrap() {
|
||||
JsonVal::Number(n) => assert!((n - (-3.14)).abs() < 1e-10),
|
||||
_ => panic!("expected number"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_string() {
|
||||
assert_eq!(json_parse("\"hello\"").unwrap(), JsonVal::Str("hello".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_string_with_escapes() {
|
||||
assert_eq!(
|
||||
json_parse(r#""a\"b\\c""#).unwrap(),
|
||||
JsonVal::Str("a\"b\\c".into())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_array() {
|
||||
match json_parse("[1,2,3]").unwrap() {
|
||||
JsonVal::Array(a) => assert_eq!(a.len(), 3),
|
||||
_ => panic!("expected array"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_empty_array() {
|
||||
match json_parse("[]").unwrap() {
|
||||
JsonVal::Array(a) => assert_eq!(a.len(), 0),
|
||||
_ => panic!("expected array"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_object() {
|
||||
let val = json_parse(r#"{"x":1,"y":2}"#).unwrap();
|
||||
assert!(matches!(val, JsonVal::Object(_)));
|
||||
assert_eq!(val.get("x").unwrap().as_f64().unwrap(), 1.0);
|
||||
assert_eq!(val.get("y").unwrap().as_f64().unwrap(), 2.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_null() {
|
||||
assert_eq!(json_parse("null").unwrap(), JsonVal::Null);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_bool() {
|
||||
assert_eq!(json_parse("true").unwrap(), JsonVal::Bool(true));
|
||||
assert_eq!(json_parse("false").unwrap(), JsonVal::Bool(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_nested() {
|
||||
let val = json_parse(r#"{"a":[1,2],"b":{"c":3}}"#).unwrap();
|
||||
assert!(matches!(val, JsonVal::Object(_)));
|
||||
let arr = val.get("a").unwrap().as_array().unwrap();
|
||||
assert_eq!(arr.len(), 2);
|
||||
let inner = val.get("b").unwrap();
|
||||
assert_eq!(inner.get("c").unwrap().as_f64().unwrap(), 3.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_whitespace() {
|
||||
let val = json_parse(" { \"a\" : 1 , \"b\" : [ 2 , 3 ] } ").unwrap();
|
||||
assert!(matches!(val, JsonVal::Object(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_val_to_json_string_roundtrip() {
|
||||
let val = JsonVal::Object(vec![
|
||||
("name".into(), JsonVal::Str("test".into())),
|
||||
("count".into(), JsonVal::Number(42.0)),
|
||||
("items".into(), JsonVal::Array(vec![
|
||||
JsonVal::Number(1.0),
|
||||
JsonVal::Null,
|
||||
JsonVal::Bool(true),
|
||||
])),
|
||||
]);
|
||||
let s = val.to_json_string();
|
||||
let parsed = json_parse(&s).unwrap();
|
||||
assert_eq!(parsed, val);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,9 @@ pub mod hierarchy;
|
||||
pub mod world_transform;
|
||||
pub mod scene;
|
||||
pub mod scheduler;
|
||||
pub mod json;
|
||||
pub mod component_registry;
|
||||
pub mod binary_scene;
|
||||
|
||||
pub use entity::{Entity, EntityAllocator};
|
||||
pub use sparse_set::SparseSet;
|
||||
@@ -13,5 +16,7 @@ pub use world::World;
|
||||
pub use transform::Transform;
|
||||
pub use hierarchy::{Parent, Children, add_child, remove_child, despawn_recursive, roots};
|
||||
pub use world_transform::{WorldTransform, propagate_transforms};
|
||||
pub use scene::{Tag, serialize_scene, deserialize_scene};
|
||||
pub use scene::{Tag, serialize_scene, deserialize_scene, serialize_scene_json, deserialize_scene_json};
|
||||
pub use scheduler::{Scheduler, System};
|
||||
pub use component_registry::ComponentRegistry;
|
||||
pub use binary_scene::{serialize_scene_binary, deserialize_scene_binary};
|
||||
|
||||
@@ -4,6 +4,8 @@ use crate::entity::Entity;
|
||||
use crate::world::World;
|
||||
use crate::transform::Transform;
|
||||
use crate::hierarchy::{add_child, Parent};
|
||||
use crate::component_registry::ComponentRegistry;
|
||||
use crate::json::{self, JsonVal};
|
||||
|
||||
/// String tag for entity identification.
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -152,10 +154,160 @@ pub fn deserialize_scene(world: &mut World, source: &str) -> Vec<Entity> {
|
||||
created
|
||||
}
|
||||
|
||||
// ── Hex encoding helpers ────────────────────────────────────────────
|
||||
|
||||
fn bytes_to_hex(data: &[u8]) -> String {
|
||||
let mut s = String::with_capacity(data.len() * 2);
|
||||
for &b in data {
|
||||
s.push_str(&format!("{:02x}", b));
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
fn hex_to_bytes(hex: &str) -> Result<Vec<u8>, String> {
|
||||
if hex.len() % 2 != 0 {
|
||||
return Err("Hex string has odd length".into());
|
||||
}
|
||||
let mut bytes = Vec::with_capacity(hex.len() / 2);
|
||||
let mut i = 0;
|
||||
let chars: Vec<u8> = hex.bytes().collect();
|
||||
while i < chars.len() {
|
||||
let hi = hex_digit(chars[i])?;
|
||||
let lo = hex_digit(chars[i + 1])?;
|
||||
bytes.push((hi << 4) | lo);
|
||||
i += 2;
|
||||
}
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
fn hex_digit(c: u8) -> Result<u8, String> {
|
||||
match c {
|
||||
b'0'..=b'9' => Ok(c - b'0'),
|
||||
b'a'..=b'f' => Ok(c - b'a' + 10),
|
||||
b'A'..=b'F' => Ok(c - b'A' + 10),
|
||||
_ => Err(format!("Invalid hex digit: {}", c as char)),
|
||||
}
|
||||
}
|
||||
|
||||
// ── JSON scene serialization ────────────────────────────────────────
|
||||
|
||||
/// Serialize all entities with a Transform to JSON format using the component registry.
|
||||
/// Format: {"version":1,"entities":[{"parent":null_or_idx,"components":{"name":"hex",...}}]}
|
||||
pub fn serialize_scene_json(world: &World, registry: &ComponentRegistry) -> String {
|
||||
let entities_with_transform: Vec<(Entity, Transform)> = world
|
||||
.query::<Transform>()
|
||||
.map(|(e, t)| (e, *t))
|
||||
.collect();
|
||||
|
||||
let entity_to_index: HashMap<Entity, usize> = entities_with_transform
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, (e, _))| (*e, i))
|
||||
.collect();
|
||||
|
||||
// Build entity JSON values
|
||||
let mut entity_vals = Vec::new();
|
||||
for (entity, _) in &entities_with_transform {
|
||||
// Parent index
|
||||
let parent_val = if let Some(parent_comp) = world.get::<Parent>(*entity) {
|
||||
if let Some(&idx) = entity_to_index.get(&parent_comp.0) {
|
||||
JsonVal::Number(idx as f64)
|
||||
} else {
|
||||
JsonVal::Null
|
||||
}
|
||||
} else {
|
||||
JsonVal::Null
|
||||
};
|
||||
|
||||
// Components
|
||||
let mut comp_pairs = Vec::new();
|
||||
for entry in registry.entries() {
|
||||
if let Some(data) = (entry.serialize)(world, *entity) {
|
||||
comp_pairs.push((entry.name.clone(), JsonVal::Str(bytes_to_hex(&data))));
|
||||
}
|
||||
}
|
||||
|
||||
entity_vals.push(JsonVal::Object(vec![
|
||||
("parent".into(), parent_val),
|
||||
("components".into(), JsonVal::Object(comp_pairs)),
|
||||
]));
|
||||
}
|
||||
|
||||
let root = JsonVal::Object(vec![
|
||||
("version".into(), JsonVal::Number(1.0)),
|
||||
("entities".into(), JsonVal::Array(entity_vals)),
|
||||
]);
|
||||
|
||||
root.to_json_string()
|
||||
}
|
||||
|
||||
/// Deserialize entities from a JSON scene string.
|
||||
pub fn deserialize_scene_json(
|
||||
world: &mut World,
|
||||
json_str: &str,
|
||||
registry: &ComponentRegistry,
|
||||
) -> Result<Vec<Entity>, String> {
|
||||
let root = json::json_parse(json_str)?;
|
||||
|
||||
let version = root.get("version")
|
||||
.and_then(|v| v.as_f64())
|
||||
.ok_or("Missing or invalid 'version'")?;
|
||||
if version as u32 != 1 {
|
||||
return Err(format!("Unsupported version: {}", version));
|
||||
}
|
||||
|
||||
let entities_arr = root.get("entities")
|
||||
.and_then(|v| v.as_array())
|
||||
.ok_or("Missing or invalid 'entities'")?;
|
||||
|
||||
// First pass: create entities and deserialize components
|
||||
let mut created: Vec<Entity> = Vec::with_capacity(entities_arr.len());
|
||||
let mut parent_indices: Vec<Option<usize>> = Vec::with_capacity(entities_arr.len());
|
||||
|
||||
for entity_val in entities_arr {
|
||||
let entity = world.spawn();
|
||||
|
||||
// Parse parent index
|
||||
let parent_idx = match entity_val.get("parent") {
|
||||
Some(JsonVal::Number(n)) => Some(*n as usize),
|
||||
_ => None,
|
||||
};
|
||||
parent_indices.push(parent_idx);
|
||||
|
||||
// Deserialize components
|
||||
if let Some(comps) = entity_val.get("components").and_then(|v| v.as_object()) {
|
||||
for (name, hex_val) in comps {
|
||||
if let Some(hex_str) = hex_val.as_str() {
|
||||
let data = hex_to_bytes(hex_str)?;
|
||||
if let Some(entry) = registry.find(name) {
|
||||
(entry.deserialize)(world, entity, &data)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
created.push(entity);
|
||||
}
|
||||
|
||||
// Second pass: apply parent relationships
|
||||
for (child_idx, parent_idx_opt) in parent_indices.iter().enumerate() {
|
||||
if let Some(parent_idx) = parent_idx_opt {
|
||||
if *parent_idx < created.len() {
|
||||
let child_entity = created[child_idx];
|
||||
let parent_entity = created[*parent_idx];
|
||||
add_child(world, parent_entity, child_entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(created)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::hierarchy::{add_child, roots, Parent};
|
||||
use crate::component_registry::ComponentRegistry;
|
||||
use voltex_math::Vec3;
|
||||
|
||||
#[test]
|
||||
@@ -270,4 +422,75 @@ entity 2
|
||||
let scene_roots = roots(&world);
|
||||
assert_eq!(scene_roots.len(), 2, "should have exactly 2 root entities");
|
||||
}
|
||||
|
||||
// ── JSON scene tests ────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn test_hex_roundtrip() {
|
||||
let data = vec![0u8, 1, 15, 16, 255];
|
||||
let hex = bytes_to_hex(&data);
|
||||
assert_eq!(hex, "00010f10ff");
|
||||
let back = hex_to_bytes(&hex).unwrap();
|
||||
assert_eq!(back, data);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_roundtrip() {
|
||||
let mut registry = ComponentRegistry::new();
|
||||
registry.register_defaults();
|
||||
let mut world = World::new();
|
||||
let e = world.spawn();
|
||||
world.add(e, Transform::from_position(Vec3::new(1.0, 2.0, 3.0)));
|
||||
world.add(e, Tag("player".into()));
|
||||
|
||||
let json = serialize_scene_json(&world, ®istry);
|
||||
assert!(json.contains("\"version\":1"));
|
||||
|
||||
let mut world2 = World::new();
|
||||
let entities = deserialize_scene_json(&mut world2, &json, ®istry).unwrap();
|
||||
assert_eq!(entities.len(), 1);
|
||||
let t = world2.get::<Transform>(entities[0]).unwrap();
|
||||
assert!((t.position.x - 1.0).abs() < 1e-4);
|
||||
assert!((t.position.y - 2.0).abs() < 1e-4);
|
||||
assert!((t.position.z - 3.0).abs() < 1e-4);
|
||||
let tag = world2.get::<Tag>(entities[0]).unwrap();
|
||||
assert_eq!(tag.0, "player");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_with_parent() {
|
||||
let mut registry = ComponentRegistry::new();
|
||||
registry.register_defaults();
|
||||
let mut world = World::new();
|
||||
let parent = world.spawn();
|
||||
let child = world.spawn();
|
||||
world.add(parent, Transform::new());
|
||||
world.add(child, Transform::new());
|
||||
add_child(&mut world, parent, child);
|
||||
|
||||
let json = serialize_scene_json(&world, ®istry);
|
||||
let mut world2 = World::new();
|
||||
let entities = deserialize_scene_json(&mut world2, &json, ®istry).unwrap();
|
||||
assert_eq!(entities.len(), 2);
|
||||
assert!(world2.get::<Parent>(entities[1]).is_some());
|
||||
let parent_comp = world2.get::<Parent>(entities[1]).unwrap();
|
||||
assert_eq!(parent_comp.0, entities[0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_multiple_entities() {
|
||||
let mut registry = ComponentRegistry::new();
|
||||
registry.register_defaults();
|
||||
let mut world = World::new();
|
||||
for i in 0..5 {
|
||||
let e = world.spawn();
|
||||
world.add(e, Transform::from_position(Vec3::new(i as f32, 0.0, 0.0)));
|
||||
world.add(e, Tag(format!("entity_{}", i)));
|
||||
}
|
||||
|
||||
let json = serialize_scene_json(&world, ®istry);
|
||||
let mut world2 = World::new();
|
||||
let entities = deserialize_scene_json(&mut world2, &json, ®istry).unwrap();
|
||||
assert_eq!(entities.len(), 5);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user