use hisab::Vec3;
use serde::{Deserialize, Serialize};
use crate::world::{Entity, KiranError, World};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SceneDefinition {
pub name: String,
#[serde(default)]
pub description: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub prefabs: Vec<PrefabDef>,
#[serde(default)]
pub entities: Vec<EntityDef>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EntityDef {
pub name: String,
#[serde(default)]
pub position: [f32; 3],
#[serde(default, skip_serializing_if = "Option::is_none")]
pub light_intensity: Option<f32>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tags: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub material: Option<Material>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub children: Vec<EntityDef>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prefab: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sound: Option<SoundDef>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub physics: Option<PhysicsDef>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PhysicsDef {
pub body_type: String,
pub collider: ColliderDef,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ColliderDef {
pub shape: String,
#[serde(default)]
pub radius: Option<f64>,
#[serde(default)]
pub half_extents: Option<[f64; 3]>,
#[serde(default)]
pub half_height: Option<f64>,
#[serde(default)]
pub point_a: Option<[f64; 3]>,
#[serde(default)]
pub point_b: Option<[f64; 3]>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SoundDef {
pub source: String,
#[serde(default = "default_volume")]
pub volume: f32,
#[serde(default = "default_true")]
pub spatial: bool,
#[serde(default)]
pub looping: bool,
}
fn default_volume() -> f32 {
1.0
}
fn default_true() -> bool {
true
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PrefabDef {
pub name: String,
#[serde(default)]
pub position: [f32; 3],
#[serde(default, skip_serializing_if = "Option::is_none")]
pub light_intensity: Option<f32>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tags: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub material: Option<Material>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Position(pub Vec3);
#[derive(Debug, Clone, PartialEq)]
pub struct Transform {
pub position: Vec3,
pub rotation: hisab::Quat,
pub scale: Vec3,
}
impl Default for Transform {
fn default() -> Self {
Self {
position: Vec3::ZERO,
rotation: hisab::Quat::IDENTITY,
scale: Vec3::ONE,
}
}
}
impl Transform {
pub fn from_position(position: Vec3) -> Self {
Self {
position,
..Default::default()
}
}
pub fn with_rotation(mut self, rotation: hisab::Quat) -> Self {
self.rotation = rotation;
self
}
pub fn with_scale(mut self, scale: Vec3) -> Self {
self.scale = scale;
self
}
pub fn matrix(&self) -> hisab::Mat4 {
hisab::Mat4::from_scale_rotation_translation(self.scale, self.rotation, self.position)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct GlobalTransform(pub hisab::Mat4);
impl Default for GlobalTransform {
fn default() -> Self {
Self(hisab::Mat4::IDENTITY)
}
}
pub fn propagate_transforms(world: &mut World) {
let roots: Vec<Entity> = world
.query::<Transform>()
.iter()
.filter(|(e, _)| !world.has_component::<Parent>(*e))
.map(|(e, _)| *e)
.collect();
for root in roots {
propagate_recursive(world, root, hisab::Mat4::IDENTITY);
}
}
fn propagate_recursive(world: &mut World, entity: Entity, parent_matrix: hisab::Mat4) {
let local_matrix = world
.get_component::<Transform>(entity)
.map(|t| t.matrix())
.unwrap_or(hisab::Mat4::IDENTITY);
let global = parent_matrix * local_matrix;
let _ = world.insert_component(entity, GlobalTransform(global));
let children = world
.get_component::<Children>(entity)
.map(|c| c.0.clone())
.unwrap_or_default();
for child in children {
propagate_recursive(world, child, global);
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Name(pub String);
#[derive(Debug, Clone, PartialEq)]
pub struct LightComponent {
pub intensity: f32,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Tags(pub Vec<String>);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Parent(pub Entity);
#[derive(Debug, Clone, PartialEq)]
pub struct Children(pub Vec<Entity>);
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Material {
#[serde(default = "Material::default_color")]
pub color: [f32; 4],
#[serde(default)]
pub texture: Option<String>,
#[serde(default)]
pub metallic: f32,
#[serde(default = "Material::default_roughness")]
pub roughness: f32,
}
impl Default for Material {
fn default() -> Self {
Self {
color: [1.0, 1.0, 1.0, 1.0],
texture: None,
metallic: 0.0,
roughness: 0.5,
}
}
}
impl Material {
fn default_color() -> [f32; 4] {
[1.0, 1.0, 1.0, 1.0]
}
fn default_roughness() -> f32 {
0.5
}
#[cfg(feature = "rendering")]
#[must_use]
pub fn to_material_uniforms(&self) -> crate::gpu::MaterialUniforms {
crate::gpu::MaterialUniforms {
base_color_factor: self.color,
metallic: self.metallic,
roughness: self.roughness,
_pad0: 0.0,
_pad1: 0.0,
}
}
}
pub fn set_parent(world: &mut World, child: Entity, parent: Entity) -> Result<(), KiranError> {
if child == parent {
return Err(KiranError::Scene("cannot parent entity to itself".into()));
}
if let Some(old_parent) = world.get_component::<Parent>(child).map(|p| p.0)
&& let Some(children) = world.get_component_mut::<Children>(old_parent)
{
children.0.retain(|&e| e != child);
}
world.insert_component(child, Parent(parent))?;
if let Some(children) = world.get_component_mut::<Children>(parent) {
if !children.0.contains(&child) {
children.0.push(child);
}
} else {
world.insert_component(parent, Children(vec![child]))?;
}
Ok(())
}
pub fn remove_parent(world: &mut World, child: Entity) {
if let Some(parent) = world.get_component::<Parent>(child).map(|p| p.0)
&& let Some(children) = world.get_component_mut::<Children>(parent)
{
children.0.retain(|&e| e != child);
}
world.remove_component::<Parent>(child);
}
const MAX_SCENE_TOML_SIZE: usize = 10 * 1024 * 1024;
pub fn load_scene(toml_str: &str) -> Result<SceneDefinition, KiranError> {
if toml_str.len() > MAX_SCENE_TOML_SIZE {
tracing::warn!(
size = toml_str.len(),
max = MAX_SCENE_TOML_SIZE,
"scene TOML input exceeds maximum allowed size"
);
return Err(KiranError::Scene(format!(
"scene TOML too large: {} bytes (max {} bytes)",
toml_str.len(),
MAX_SCENE_TOML_SIZE
)));
}
toml::from_str(toml_str).map_err(|e| KiranError::Scene(e.to_string()))
}
pub fn save_scene(
world: &World,
entities: &[Entity],
name: &str,
) -> std::result::Result<String, KiranError> {
let mut entity_defs = Vec::new();
for &entity in entities {
if !world.is_alive(entity) {
continue;
}
let entity_name = world
.get_component::<Name>(entity)
.map(|n| n.0.clone())
.unwrap_or_else(|| format!("entity_{}", entity.index()));
let position = world
.get_component::<Position>(entity)
.map(|p| [p.0.x, p.0.y, p.0.z])
.unwrap_or([0.0, 0.0, 0.0]);
let light_intensity = world
.get_component::<LightComponent>(entity)
.map(|l| l.intensity);
let tags = world
.get_component::<Tags>(entity)
.map(|t| t.0.clone())
.unwrap_or_default();
let material = world.get_component::<Material>(entity).cloned();
entity_defs.push(EntityDef {
name: entity_name,
position,
light_intensity,
tags,
material,
children: vec![],
prefab: None,
sound: None,
physics: None,
});
}
let scene = SceneDefinition {
name: name.to_string(),
description: String::new(),
prefabs: vec![],
entities: entity_defs,
};
toml::to_string(&scene).map_err(|e| KiranError::Scene(e.to_string()))
}
pub fn instance_scene(
world: &mut World,
scene: &SceneDefinition,
parent: Option<Entity>,
) -> std::result::Result<Vec<Entity>, KiranError> {
let mut spawned = Vec::with_capacity(scene.entities.len());
for def in &scene.entities {
let entity = spawn_entity_def(world, def, &scene.prefabs, parent)?;
spawned.push(entity);
}
Ok(spawned)
}
pub fn spawn_scene(world: &mut World, scene: &SceneDefinition) -> Result<Vec<Entity>, KiranError> {
let mut spawned = Vec::with_capacity(scene.entities.len());
for def in &scene.entities {
let entity = spawn_entity_def(world, def, &scene.prefabs, None)?;
spawned.push(entity);
}
Ok(spawned)
}
pub(crate) fn spawn_entity_def(
world: &mut World,
def: &EntityDef,
prefabs: &[PrefabDef],
parent: Option<Entity>,
) -> Result<Entity, KiranError> {
let entity = world.spawn();
let prefab = def
.prefab
.as_ref()
.and_then(|name| prefabs.iter().find(|p| &p.name == name));
world.insert_component(entity, Name(def.name.clone()))?;
let pos = if def.position != [0.0, 0.0, 0.0] {
def.position
} else {
prefab.map_or([0.0, 0.0, 0.0], |p| p.position)
};
world.insert_component(entity, Position(Vec3::new(pos[0], pos[1], pos[2])))?;
let light = def
.light_intensity
.or(prefab.and_then(|p| p.light_intensity));
if let Some(intensity) = light {
world.insert_component(entity, LightComponent { intensity })?;
}
let mut tags = def.tags.clone();
if let Some(p) = prefab {
for tag in &p.tags {
if !tags.contains(tag) {
tags.push(tag.clone());
}
}
}
if !tags.is_empty() {
world.insert_component(entity, Tags(tags))?;
}
let mat = def
.material
.as_ref()
.or(prefab.and_then(|p| p.material.as_ref()));
if let Some(m) = mat {
world.insert_component(entity, m.clone())?;
}
if let Some(sound) = &def.sound {
#[cfg(feature = "audio")]
{
world.insert_component(
entity,
crate::audio::SoundSource {
source: sound.source.clone(),
volume: sound.volume,
spatial: sound.spatial,
looping: sound.looping,
playing: false,
max_distance: 50.0,
pitch: 1.0,
bus: Default::default(),
fade: 1.0,
},
)?;
}
let _ = sound; }
if let Some(phys) = &def.physics {
#[cfg(feature = "physics")]
{
use crate::physics::{Collider, PhysicsEngine, PhysicsPosition, RigidBody};
let rb = match phys.body_type.as_str() {
"static" => RigidBody::fixed(),
"kinematic" => RigidBody::kinematic(),
_ => RigidBody::dynamic(),
};
let collider = match phys.collider.shape.as_str() {
"ball" => Collider::ball(phys.collider.radius.unwrap_or(0.5)),
"box" => {
let he = phys.collider.half_extents.unwrap_or([0.5, 0.5, 0.5]);
Collider::cuboid(he[0], he[1], he[2])
}
"capsule" => Collider::capsule(
phys.collider.half_height.unwrap_or(0.5),
phys.collider.radius.unwrap_or(0.25),
),
"segment" => Collider::segment(
phys.collider.point_a.unwrap_or([0.0, 0.0, 0.0]),
phys.collider.point_b.unwrap_or([1.0, 0.0, 0.0]),
),
_ => Collider::ball(0.5),
};
let phys_pos = PhysicsPosition {
position: [pos[0] as f64, pos[1] as f64, pos[2] as f64],
rotation: 0.0,
};
world.insert_component(entity, rb.clone())?;
world.insert_component(entity, collider.clone())?;
world.insert_component(entity, phys_pos.clone())?;
world.insert_component(entity, crate::physics::Velocity::default())?;
if let Some(engine) = world.get_resource_mut::<PhysicsEngine>() {
engine.register(entity, &rb, &phys_pos, &collider);
}
}
let _ = phys; }
if let Some(parent_entity) = parent {
set_parent(world, entity, parent_entity)?;
}
for child_def in &def.children {
spawn_entity_def(world, child_def, prefabs, Some(entity))?;
}
Ok(entity)
}
#[cfg(test)]
mod tests {
use super::*;
const SAMPLE_SCENE: &str = r#"
name = "Test Level"
description = "A simple test scene"
[[entities]]
name = "Player"
position = [1.0, 2.0, 3.0]
tags = ["controllable", "hero"]
[[entities]]
name = "Sun"
position = [0.0, 100.0, 0.0]
light_intensity = 1.5
[[entities]]
name = "Rock"
position = [5.0, 0.0, -2.0]
"#;
#[test]
fn load_scene_basic() {
let scene = load_scene(SAMPLE_SCENE).unwrap();
assert_eq!(scene.name, "Test Level");
assert_eq!(scene.entities.len(), 3);
}
#[test]
fn load_scene_entity_fields() {
let scene = load_scene(SAMPLE_SCENE).unwrap();
let player = &scene.entities[0];
assert_eq!(player.name, "Player");
assert_eq!(player.position, [1.0, 2.0, 3.0]);
assert_eq!(player.tags, vec!["controllable", "hero"]);
assert!(player.light_intensity.is_none());
let sun = &scene.entities[1];
assert_eq!(sun.light_intensity, Some(1.5));
}
#[test]
fn load_scene_invalid_toml() {
let result = load_scene("not valid toml {{{{");
assert!(result.is_err());
}
#[test]
fn load_scene_missing_name() {
let result = load_scene(r#"description = "no name field""#);
assert!(result.is_err());
}
#[test]
fn spawn_scene_entities() {
let scene = load_scene(SAMPLE_SCENE).unwrap();
let mut world = World::new();
let entities = spawn_scene(&mut world, &scene).unwrap();
assert_eq!(entities.len(), 3);
assert_eq!(world.entity_count(), 3);
}
#[test]
fn spawn_scene_components() {
let scene = load_scene(SAMPLE_SCENE).unwrap();
let mut world = World::new();
let entities = spawn_scene(&mut world, &scene).unwrap();
let name = world.get_component::<Name>(entities[0]).unwrap();
assert_eq!(name.0, "Player");
let pos = world.get_component::<Position>(entities[0]).unwrap();
assert_eq!(pos.0, Vec3::new(1.0, 2.0, 3.0));
let tags = world.get_component::<Tags>(entities[0]).unwrap();
assert_eq!(tags.0.len(), 2);
let light = world.get_component::<LightComponent>(entities[1]).unwrap();
assert_eq!(light.intensity, 1.5);
assert!(world.get_component::<LightComponent>(entities[2]).is_none());
}
#[test]
fn spawn_empty_scene() {
let scene = load_scene(r#"name = "Empty""#).unwrap();
let mut world = World::new();
let entities = spawn_scene(&mut world, &scene).unwrap();
assert!(entities.is_empty());
assert_eq!(world.entity_count(), 0);
}
#[test]
fn scene_roundtrip_toml() {
let scene = SceneDefinition {
name: "Roundtrip".into(),
description: String::new(),
prefabs: vec![],
entities: vec![EntityDef {
name: "A".into(),
position: [1.0, 2.0, 3.0],
light_intensity: None,
tags: vec![],
material: None,
children: vec![],
prefab: None,
sound: None,
physics: None,
}],
};
let serialized = toml::to_string(&scene).unwrap();
let deserialized = load_scene(&serialized).unwrap();
assert_eq!(deserialized.name, "Roundtrip");
assert_eq!(deserialized.entities.len(), 1);
}
#[test]
fn spawn_scene_tags_absent() {
let toml_str = r#"
name = "NoTags"
[[entities]]
name = "Plain"
position = [0.0, 0.0, 0.0]
"#;
let scene = load_scene(toml_str).unwrap();
let mut world = World::new();
let entities = spawn_scene(&mut world, &scene).unwrap();
assert!(world.get_component::<Tags>(entities[0]).is_none());
}
#[test]
fn unicode_entity_names() {
let toml_str = r#"
name = "ユニコード"
[[entities]]
name = "主人公"
position = [0.0, 0.0, 0.0]
tags = ["プレイヤー"]
"#;
let scene = load_scene(toml_str).unwrap();
assert_eq!(scene.name, "ユニコード");
assert_eq!(scene.entities[0].name, "主人公");
let mut world = World::new();
let entities = spawn_scene(&mut world, &scene).unwrap();
let name = world.get_component::<Name>(entities[0]).unwrap();
assert_eq!(name.0, "主人公");
}
#[test]
fn scene_default_position() {
let toml_str = r#"
name = "Defaults"
[[entities]]
name = "Origin"
"#;
let scene = load_scene(toml_str).unwrap();
assert_eq!(scene.entities[0].position, [0.0, 0.0, 0.0]);
}
#[test]
fn set_parent_creates_relationship() {
let mut world = World::new();
let parent = world.spawn();
let child = world.spawn();
set_parent(&mut world, child, parent).unwrap();
assert_eq!(world.get_component::<Parent>(child).unwrap().0, parent);
let children = world.get_component::<Children>(parent).unwrap();
assert_eq!(children.0, vec![child]);
}
#[test]
fn set_parent_reparent() {
let mut world = World::new();
let p1 = world.spawn();
let p2 = world.spawn();
let child = world.spawn();
set_parent(&mut world, child, p1).unwrap();
set_parent(&mut world, child, p2).unwrap();
let c1 = world.get_component::<Children>(p1).unwrap();
assert!(c1.0.is_empty());
let c2 = world.get_component::<Children>(p2).unwrap();
assert_eq!(c2.0, vec![child]);
assert_eq!(world.get_component::<Parent>(child).unwrap().0, p2);
}
#[test]
fn remove_parent_clears() {
let mut world = World::new();
let parent = world.spawn();
let child = world.spawn();
set_parent(&mut world, child, parent).unwrap();
remove_parent(&mut world, child);
assert!(world.get_component::<Parent>(child).is_none());
let children = world.get_component::<Children>(parent).unwrap();
assert!(children.0.is_empty());
}
#[test]
fn multiple_children() {
let mut world = World::new();
let parent = world.spawn();
let c1 = world.spawn();
let c2 = world.spawn();
let c3 = world.spawn();
set_parent(&mut world, c1, parent).unwrap();
set_parent(&mut world, c2, parent).unwrap();
set_parent(&mut world, c3, parent).unwrap();
let children = world.get_component::<Children>(parent).unwrap();
assert_eq!(children.0.len(), 3);
}
#[test]
fn scene_with_material() {
let toml_str = r#"
name = "Material Test"
[[entities]]
name = "Cube"
position = [0.0, 0.0, 0.0]
[entities.material]
color = [1.0, 0.0, 0.0, 1.0]
texture = "textures/brick.png"
"#;
let scene = load_scene(toml_str).unwrap();
let mat = scene.entities[0].material.as_ref().unwrap();
assert_eq!(mat.color, [1.0, 0.0, 0.0, 1.0]);
assert_eq!(mat.texture.as_deref(), Some("textures/brick.png"));
let mut world = World::new();
let entities = spawn_scene(&mut world, &scene).unwrap();
let spawned_mat = world.get_component::<Material>(entities[0]).unwrap();
assert_eq!(spawned_mat.color, [1.0, 0.0, 0.0, 1.0]);
}
#[test]
fn material_default() {
let mat = Material::default();
assert_eq!(mat.color, [1.0, 1.0, 1.0, 1.0]);
assert!(mat.texture.is_none());
}
#[test]
fn scene_with_children() {
let toml_str = r#"
name = "Hierarchy"
[[entities]]
name = "Parent"
position = [0.0, 0.0, 0.0]
[[entities.children]]
name = "Child1"
position = [1.0, 0.0, 0.0]
[[entities.children]]
name = "Child2"
position = [2.0, 0.0, 0.0]
"#;
let scene = load_scene(toml_str).unwrap();
assert_eq!(scene.entities[0].children.len(), 2);
let mut world = World::new();
let entities = spawn_scene(&mut world, &scene).unwrap();
assert_eq!(entities.len(), 1);
assert_eq!(world.entity_count(), 3);
let parent = entities[0];
let children = world.get_component::<Children>(parent).unwrap();
assert_eq!(children.0.len(), 2);
let child1 = children.0[0];
assert_eq!(world.get_component::<Parent>(child1).unwrap().0, parent);
assert_eq!(world.get_component::<Name>(child1).unwrap().0, "Child1");
}
#[test]
fn prefab_template() {
let toml_str = r#"
name = "Prefab Test"
[[prefabs]]
name = "tree"
light_intensity = 0.1
tags = ["vegetation", "static"]
[prefabs.material]
color = [0.2, 0.8, 0.1, 1.0]
[[entities]]
name = "Oak"
position = [5.0, 0.0, 3.0]
prefab = "tree"
tags = ["large"]
[[entities]]
name = "Pine"
position = [10.0, 0.0, 7.0]
prefab = "tree"
"#;
let scene = load_scene(toml_str).unwrap();
let mut world = World::new();
let entities = spawn_scene(&mut world, &scene).unwrap();
let light = world.get_component::<LightComponent>(entities[0]).unwrap();
assert_eq!(light.intensity, 0.1);
let tags = world.get_component::<Tags>(entities[0]).unwrap();
assert!(tags.0.contains(&"large".to_string()));
assert!(tags.0.contains(&"vegetation".to_string()));
let mat = world.get_component::<Material>(entities[0]).unwrap();
assert_eq!(mat.color[1], 0.8);
let pine_light = world.get_component::<LightComponent>(entities[1]).unwrap();
assert_eq!(pine_light.intensity, 0.1);
}
#[test]
fn prefab_entity_overrides() {
let toml_str = r#"
name = "Override"
[[prefabs]]
name = "base"
light_intensity = 1.0
[[entities]]
name = "Custom"
light_intensity = 5.0
prefab = "base"
"#;
let scene = load_scene(toml_str).unwrap();
let mut world = World::new();
let entities = spawn_scene(&mut world, &scene).unwrap();
let light = world.get_component::<LightComponent>(entities[0]).unwrap();
assert_eq!(light.intensity, 5.0);
}
#[test]
fn unknown_prefab_ignored() {
let toml_str = r#"
name = "Missing Prefab"
[[entities]]
name = "Orphan"
prefab = "nonexistent"
"#;
let scene = load_scene(toml_str).unwrap();
let mut world = World::new();
let entities = spawn_scene(&mut world, &scene).unwrap();
assert_eq!(entities.len(), 1);
assert!(world.get_component::<LightComponent>(entities[0]).is_none());
}
#[test]
fn prefab_with_children() {
let toml_str = r#"
name = "Prefab + Hierarchy"
[[prefabs]]
name = "lamp"
light_intensity = 2.0
tags = ["light-source"]
[[entities]]
name = "StreetLamp"
position = [10.0, 0.0, 0.0]
prefab = "lamp"
[[entities.children]]
name = "LampBulb"
position = [0.0, 3.0, 0.0]
prefab = "lamp"
"#;
let scene = load_scene(toml_str).unwrap();
let mut world = World::new();
let entities = spawn_scene(&mut world, &scene).unwrap();
assert_eq!(entities.len(), 1); assert_eq!(world.entity_count(), 2);
let parent = entities[0];
let light = world.get_component::<LightComponent>(parent).unwrap();
assert_eq!(light.intensity, 2.0);
let children = world.get_component::<Children>(parent).unwrap();
let child = children.0[0];
let child_light = world.get_component::<LightComponent>(child).unwrap();
assert_eq!(child_light.intensity, 2.0);
assert_eq!(world.get_component::<Parent>(child).unwrap().0, parent);
}
#[test]
fn deep_hierarchy() {
let toml_str = r#"
name = "Deep"
[[entities]]
name = "Root"
[[entities.children]]
name = "L1"
[[entities.children.children]]
name = "L2"
"#;
let scene = load_scene(toml_str).unwrap();
let mut world = World::new();
let entities = spawn_scene(&mut world, &scene).unwrap();
assert_eq!(entities.len(), 1);
assert_eq!(world.entity_count(), 3);
let root = entities[0];
let l1 = world.get_component::<Children>(root).unwrap().0[0];
let l2 = world.get_component::<Children>(l1).unwrap().0[0];
assert_eq!(world.get_component::<Name>(l2).unwrap().0, "L2");
assert_eq!(world.get_component::<Parent>(l2).unwrap().0, l1);
}
#[test]
fn serialization_skips_empty_fields() {
let scene = SceneDefinition {
name: "Clean".into(),
description: String::new(),
prefabs: vec![],
entities: vec![EntityDef {
name: "Simple".into(),
position: [0.0, 0.0, 0.0],
light_intensity: None,
tags: vec![],
material: None,
children: vec![],
prefab: None,
sound: None,
physics: None,
}],
};
let toml_str = toml::to_string(&scene).unwrap();
assert!(!toml_str.contains("light_intensity"));
assert!(!toml_str.contains("material"));
assert!(!toml_str.contains("prefab"));
assert!(!toml_str.contains("children"));
assert!(!toml_str.contains("prefabs"));
assert!(!toml_str.contains("sound"));
}
#[test]
fn scene_with_sound() {
let toml_str = r#"
name = "Sound Test"
[[entities]]
name = "Radio"
position = [5.0, 1.0, 0.0]
[entities.sound]
source = "sounds/music.ogg"
volume = 0.7
spatial = true
looping = true
"#;
let scene = load_scene(toml_str).unwrap();
let sound = scene.entities[0].sound.as_ref().unwrap();
assert_eq!(sound.source, "sounds/music.ogg");
assert_eq!(sound.volume, 0.7);
assert!(sound.spatial);
assert!(sound.looping);
}
#[test]
fn sound_def_defaults() {
let toml_str = r#"
name = "Defaults"
[[entities]]
name = "Beep"
[entities.sound]
source = "beep.wav"
"#;
let scene = load_scene(toml_str).unwrap();
let sound = scene.entities[0].sound.as_ref().unwrap();
assert_eq!(sound.volume, 1.0);
assert!(sound.spatial);
assert!(!sound.looping);
}
#[test]
fn scene_with_physics() {
let toml_str = r#"
name = "Physics Test"
[[entities]]
name = "Ball"
position = [0.0, 10.0, 0.0]
[entities.physics]
body_type = "dynamic"
[entities.physics.collider]
shape = "ball"
radius = 0.5
[[entities]]
name = "Floor"
position = [0.0, 0.0, 0.0]
[entities.physics]
body_type = "static"
[entities.physics.collider]
shape = "box"
half_extents = [50.0, 0.5, 50.0]
"#;
let scene = load_scene(toml_str).unwrap();
assert_eq!(scene.entities.len(), 2);
let ball_phys = scene.entities[0].physics.as_ref().unwrap();
assert_eq!(ball_phys.body_type, "dynamic");
assert_eq!(ball_phys.collider.shape, "ball");
assert_eq!(ball_phys.collider.radius, Some(0.5));
let floor_phys = scene.entities[1].physics.as_ref().unwrap();
assert_eq!(floor_phys.body_type, "static");
assert_eq!(floor_phys.collider.shape, "box");
assert_eq!(floor_phys.collider.half_extents, Some([50.0, 0.5, 50.0]));
}
#[test]
fn scene_physics_capsule() {
let toml_str = r#"
name = "Capsule"
[[entities]]
name = "Character"
[entities.physics]
body_type = "kinematic"
[entities.physics.collider]
shape = "capsule"
half_height = 0.8
radius = 0.3
"#;
let scene = load_scene(toml_str).unwrap();
let phys = scene.entities[0].physics.as_ref().unwrap();
assert_eq!(phys.body_type, "kinematic");
assert_eq!(phys.collider.half_height, Some(0.8));
assert_eq!(phys.collider.radius, Some(0.3));
}
#[test]
fn full_featured_entity() {
let toml_str = r#"
name = "Full Feature"
[[prefabs]]
name = "base"
tags = ["prefab-tag"]
[[entities]]
name = "FullEntity"
position = [1.0, 2.0, 3.0]
light_intensity = 0.9
tags = ["custom-tag"]
prefab = "base"
[entities.material]
color = [1.0, 0.0, 0.0, 1.0]
texture = "tex.png"
[entities.sound]
source = "sound.wav"
volume = 0.5
[entities.physics]
body_type = "dynamic"
[entities.physics.collider]
shape = "ball"
radius = 1.0
[[entities.children]]
name = "Child"
position = [0.0, 1.0, 0.0]
"#;
let scene = load_scene(toml_str).unwrap();
let mut world = World::new();
let entities = spawn_scene(&mut world, &scene).unwrap();
assert_eq!(entities.len(), 1); assert_eq!(world.entity_count(), 2);
let e = entities[0];
assert_eq!(world.get_component::<Name>(e).unwrap().0, "FullEntity");
assert_eq!(
world.get_component::<Position>(e).unwrap().0,
Vec3::new(1.0, 2.0, 3.0)
);
assert_eq!(
world.get_component::<LightComponent>(e).unwrap().intensity,
0.9
);
let tags = world.get_component::<Tags>(e).unwrap();
assert!(tags.0.contains(&"custom-tag".to_string()));
assert!(tags.0.contains(&"prefab-tag".to_string()));
let mat = world.get_component::<Material>(e).unwrap();
assert_eq!(mat.color[0], 1.0);
assert_eq!(mat.texture.as_deref(), Some("tex.png"));
let children = world.get_component::<Children>(e).unwrap();
assert_eq!(children.0.len(), 1);
}
#[test]
fn scene_definition_partial_eq() {
let a = load_scene(r#"name = "A""#).unwrap();
let b = load_scene(r#"name = "A""#).unwrap();
let c = load_scene(r#"name = "C""#).unwrap();
assert_eq!(a, b);
assert_ne!(a, c);
}
#[cfg(feature = "physics")]
#[test]
fn toml_driven_physics_spawn() {
use crate::physics::{Collider, PhysicsEngine, PhysicsPosition, RigidBody, Velocity};
let toml_str = r#"
name = "Physics Spawn"
[[entities]]
name = "Ball"
position = [0.0, 10.0, 0.0]
[entities.physics]
body_type = "dynamic"
[entities.physics.collider]
shape = "ball"
radius = 0.5
[[entities]]
name = "Floor"
position = [0.0, 0.0, 0.0]
[entities.physics]
body_type = "static"
[entities.physics.collider]
shape = "box"
half_extents = [50.0, 0.5, 50.0]
"#;
let scene = load_scene(toml_str).unwrap();
let mut world = World::new();
world.insert_resource(PhysicsEngine::new());
let entities = spawn_scene(&mut world, &scene).unwrap();
assert_eq!(entities.len(), 2);
let ball = entities[0];
assert!(world.has_component::<RigidBody>(ball));
assert!(world.has_component::<Collider>(ball));
assert!(world.has_component::<PhysicsPosition>(ball));
assert!(world.has_component::<Velocity>(ball));
let floor = entities[1];
assert!(world.has_component::<RigidBody>(floor));
let engine = world.get_resource::<PhysicsEngine>().unwrap();
assert_eq!(engine.entity_count(), 2);
assert!(engine.body_handle(ball).is_some());
assert!(engine.body_handle(floor).is_some());
}
#[cfg(feature = "physics")]
#[test]
fn toml_driven_physics_capsule_and_kinematic() {
use crate::physics::{PhysicsEngine, RigidBody};
let toml_str = r#"
name = "Capsule"
[[entities]]
name = "Character"
position = [5.0, 0.0, 0.0]
[entities.physics]
body_type = "kinematic"
[entities.physics.collider]
shape = "capsule"
half_height = 0.8
radius = 0.3
"#;
let scene = load_scene(toml_str).unwrap();
let mut world = World::new();
world.insert_resource(PhysicsEngine::new());
let entities = spawn_scene(&mut world, &scene).unwrap();
let rb = world.get_component::<RigidBody>(entities[0]).unwrap();
assert_eq!(rb.body_type, impetus::BodyType::Kinematic);
}
#[cfg(feature = "physics")]
#[test]
fn toml_driven_physics_segment() {
use crate::physics::{Collider, PhysicsEngine};
let toml_str = r#"
name = "Segment"
[[entities]]
name = "Wall"
position = [0.0, 0.0, 0.0]
[entities.physics]
body_type = "static"
[entities.physics.collider]
shape = "segment"
point_a = [0.0, 0.0, 0.0]
point_b = [10.0, 0.0, 0.0]
"#;
let scene = load_scene(toml_str).unwrap();
let mut world = World::new();
world.insert_resource(PhysicsEngine::new());
let entities = spawn_scene(&mut world, &scene).unwrap();
let col = world.get_component::<Collider>(entities[0]).unwrap();
match &col.shape {
impetus::ColliderShape::Segment { a, b } => {
assert_eq!(*a, [0.0, 0.0, 0.0]);
assert_eq!(*b, [10.0, 0.0, 0.0]);
}
_ => panic!("expected segment collider"),
}
}
#[cfg(feature = "physics")]
#[test]
fn toml_driven_physics_3d_position() {
use crate::physics::{PhysicsEngine, PhysicsPosition};
let toml_str = r#"
name = "3D"
[[entities]]
name = "Floating"
position = [5.0, 20.0, -10.0]
[entities.physics]
body_type = "dynamic"
[entities.physics.collider]
shape = "ball"
radius = 1.0
"#;
let scene = load_scene(toml_str).unwrap();
let mut world = World::new();
world.insert_resource(PhysicsEngine::new());
let entities = spawn_scene(&mut world, &scene).unwrap();
let pos = world.get_component::<PhysicsPosition>(entities[0]).unwrap();
assert_eq!(pos.position[0], 5.0);
assert_eq!(pos.position[1], 20.0);
assert_eq!(pos.position[2], -10.0);
}
#[test]
fn set_parent_self_parenting_rejected() {
let mut world = World::new();
let e = world.spawn();
let result = set_parent(&mut world, e, e);
assert!(result.is_err());
}
#[test]
fn set_parent_already_same_parent() {
let mut world = World::new();
let parent = world.spawn();
let child = world.spawn();
set_parent(&mut world, child, parent).unwrap();
set_parent(&mut world, child, parent).unwrap();
let children = world.get_component::<Children>(parent).unwrap();
assert_eq!(children.0.len(), 1);
}
#[test]
fn spawn_scene_1000_entities() {
let mut toml = String::from("name = \"Stress\"\n");
for i in 0..1000 {
toml.push_str(&format!(
"[[entities]]\nname = \"E{i}\"\nposition = [{}.0, 0.0, 0.0]\n",
i % 100
));
}
let scene = load_scene(&toml).unwrap();
let mut world = World::new();
let entities = spawn_scene(&mut world, &scene).unwrap();
assert_eq!(entities.len(), 1000);
assert_eq!(world.entity_count(), 1000);
}
#[test]
fn transform_default() {
let t = Transform::default();
assert_eq!(t.position, Vec3::ZERO);
assert_eq!(t.rotation, hisab::Quat::IDENTITY);
assert_eq!(t.scale, Vec3::ONE);
}
#[test]
fn transform_from_position() {
let t = Transform::from_position(Vec3::new(1.0, 2.0, 3.0));
assert_eq!(t.position, Vec3::new(1.0, 2.0, 3.0));
assert_eq!(t.scale, Vec3::ONE);
}
#[test]
fn transform_builder() {
let t =
Transform::from_position(Vec3::new(1.0, 0.0, 0.0)).with_scale(Vec3::new(2.0, 2.0, 2.0));
assert_eq!(t.scale, Vec3::new(2.0, 2.0, 2.0));
}
#[test]
fn transform_matrix_identity() {
let t = Transform::default();
let m = t.matrix();
assert_eq!(m, hisab::Mat4::IDENTITY);
}
#[test]
fn transform_matrix_translation() {
let t = Transform::from_position(Vec3::new(5.0, 10.0, 15.0));
let m = t.matrix();
let col3 = m.col(3);
assert_eq!(col3.x, 5.0);
assert_eq!(col3.y, 10.0);
assert_eq!(col3.z, 15.0);
}
#[test]
fn global_transform_default() {
let gt = GlobalTransform::default();
assert_eq!(gt.0, hisab::Mat4::IDENTITY);
}
#[test]
fn propagate_transforms_basic() {
let mut world = World::new();
let e = world.spawn();
world
.insert_component(e, Transform::from_position(Vec3::new(3.0, 4.0, 5.0)))
.unwrap();
propagate_transforms(&mut world);
let gt = world.get_component::<GlobalTransform>(e).unwrap();
let col3 = gt.0.col(3);
assert_eq!(col3.x, 3.0);
assert_eq!(col3.y, 4.0);
assert_eq!(col3.z, 5.0);
}
#[test]
fn propagate_transforms_hierarchy() {
let mut world = World::new();
let parent = world.spawn();
let child = world.spawn();
world
.insert_component(parent, Transform::from_position(Vec3::new(10.0, 0.0, 0.0)))
.unwrap();
world
.insert_component(child, Transform::from_position(Vec3::new(5.0, 0.0, 0.0)))
.unwrap();
set_parent(&mut world, child, parent).unwrap();
propagate_transforms(&mut world);
let gt = world.get_component::<GlobalTransform>(child).unwrap();
let col3 = gt.0.col(3);
assert!((col3.x - 15.0).abs() < 0.001);
}
#[test]
fn propagate_transforms_no_transform() {
let mut world = World::new();
let e = world.spawn();
world
.insert_component(e, Name("no_transform".into()))
.unwrap();
propagate_transforms(&mut world); }
#[test]
fn transform_as_component() {
let mut world = World::new();
let e = world.spawn();
world
.insert_component(e, Transform::from_position(Vec3::new(1.0, 2.0, 3.0)))
.unwrap();
assert!(world.has_component::<Transform>(e));
let t = world.get_component::<Transform>(e).unwrap();
assert_eq!(t.position, Vec3::new(1.0, 2.0, 3.0));
}
}