use serde::{Deserialize, Serialize};
pub const MAX_SCENE_OBJECTS: usize = 65536;
pub const MAX_COMPONENTS_PER_OBJECT: usize = 32;
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Transform {
pub position: [f32; 3],
pub rotation: [f32; 4],
pub scale: [f32; 3],
}
impl Default for Transform {
fn default() -> Self {
Self {
position: [0.0; 3],
rotation: [0.0, 0.0, 0.0, 1.0],
scale: [1.0, 1.0, 1.0],
}
}
}
impl Transform {
pub fn to_matrix(&self) -> [f32; 16] {
let [qx, qy, qz, qw] = self.rotation;
let [sx, sy, sz] = self.scale;
let [px, py, pz] = self.position;
let x2 = qx + qx;
let y2 = qy + qy;
let z2 = qz + qz;
let xx = qx * x2;
let xy = qx * y2;
let xz = qx * z2;
let yy = qy * y2;
let yz = qy * z2;
let zz = qz * z2;
let wx = qw * x2;
let wy = qw * y2;
let wz = qw * z2;
[
(1.0 - (yy + zz)) * sx,
(xy + wz) * sx,
(xz - wy) * sx,
0.0,
(xy - wz) * sy,
(1.0 - (xx + zz)) * sy,
(yz + wx) * sy,
0.0,
(xz + wy) * sz,
(yz - wx) * sz,
(1.0 - (xx + yy)) * sz,
0.0,
px,
py,
pz,
1.0,
]
}
pub fn validate(&self) -> Result<(), String> {
if self.position.iter().any(|p| !p.is_finite()) {
return Err("transform_invalid_position:contains NaN or Inf".into());
}
let [qx, qy, qz, qw] = self.rotation;
let len_sq = qx * qx + qy * qy + qz * qz + qw * qw;
if !len_sq.is_finite() || (len_sq - 1.0).abs() > 0.01 {
return Err(format!(
"transform_invalid_rotation:quaternion length² {len_sq} not ≈1.0"
));
}
if self.scale.iter().any(|&s| s <= 0.0 || !s.is_finite()) {
return Err("transform_invalid_scale:must be positive and finite".into());
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum PrimitiveKind {
Cube,
Sphere,
Cylinder,
Cone,
Plane,
Torus,
Capsule,
Pyramid,
Wedge,
}
impl PrimitiveKind {
pub const ALL: &'static [PrimitiveKind] = &[
Self::Cube,
Self::Sphere,
Self::Cylinder,
Self::Cone,
Self::Plane,
Self::Torus,
Self::Capsule,
Self::Pyramid,
Self::Wedge,
];
pub fn name(self) -> &'static str {
match self {
Self::Cube => "Cube",
Self::Sphere => "Sphere",
Self::Cylinder => "Cylinder",
Self::Cone => "Cone",
Self::Plane => "Plane",
Self::Torus => "Torus",
Self::Capsule => "Capsule",
Self::Pyramid => "Pyramid",
Self::Wedge => "Wedge",
}
}
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub enum MeshBinding {
#[default]
None,
Primitive { kind: PrimitiveKind, color: [f32; 4] },
Custom { asset_key: String },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ComponentKind {
Physics,
Collider,
WaymarkScript,
Audio,
Light,
Particle,
Camera,
Trigger,
Animation,
DreamMatter,
FbxAnimation,
Custom,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComponentSlot {
pub kind: ComponentKind,
pub enabled: bool,
pub properties: serde_json::Value,
}
impl ComponentSlot {
pub fn new(kind: ComponentKind) -> Self {
Self {
kind,
enabled: true,
properties: serde_json::Value::Object(serde_json::Map::new()),
}
}
pub fn set_property(&mut self, key: &str, value: serde_json::Value) -> Result<(), String> {
if let serde_json::Value::Object(ref mut map) = self.properties {
if map.len() >= 256 && !map.contains_key(key) {
return Err("component_property_limit:max 256 properties".into());
}
map.insert(key.to_string(), value);
Ok(())
} else {
Err("component_properties_not_object:must be a JSON object".into())
}
}
pub fn get_property(&self, key: &str) -> Option<&serde_json::Value> {
self.properties.get(key)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GameObject {
pub id: u64,
pub name: String,
pub transform: Transform,
pub mesh: MeshBinding,
pub visible: bool,
pub parent_id: Option<u64>,
pub components: Vec<ComponentSlot>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub property_tags: Vec<String>,
#[serde(default = "default_topology_layer")]
pub topology_layer: u8,
}
fn default_topology_layer() -> u8 {
9
}
impl GameObject {
pub fn new(id: u64, name: String) -> Self {
Self {
id,
name,
transform: Transform::default(),
mesh: MeshBinding::None,
visible: true,
parent_id: None,
components: Vec::new(),
property_tags: Vec::new(),
topology_layer: 9,
}
}
pub fn with_primitive(id: u64, name: String, kind: PrimitiveKind) -> Self {
Self {
mesh: MeshBinding::Primitive {
kind,
color: [0.7, 0.7, 0.7, 1.0],
},
..Self::new(id, name)
}
}
pub fn add_component(&mut self, slot: ComponentSlot) -> Result<(), String> {
if self.components.len() >= MAX_COMPONENTS_PER_OBJECT {
return Err(format!(
"game_object_component_limit:max {MAX_COMPONENTS_PER_OBJECT} components"
));
}
self.components.push(slot);
Ok(())
}
pub fn remove_component(&mut self, kind: ComponentKind) -> bool {
if let Some(pos) = self.components.iter().position(|c| c.kind == kind) {
self.components.swap_remove(pos);
true
} else {
false
}
}
pub fn get_component(&self, kind: ComponentKind) -> Option<&ComponentSlot> {
self.components.iter().find(|c| c.kind == kind)
}
pub fn get_component_mut(&mut self, kind: ComponentKind) -> Option<&mut ComponentSlot> {
self.components.iter_mut().find(|c| c.kind == kind)
}
pub fn has_component(&self, kind: ComponentKind) -> bool {
self.components.iter().any(|c| c.kind == kind)
}
pub fn validate(&self) -> Result<(), String> {
self.transform.validate()?;
if self.name.len() > 256 {
return Err("game_object_name_too_long:max 256 chars".into());
}
if self.components.len() > MAX_COMPONENTS_PER_OBJECT {
return Err(format!(
"game_object_too_many_components:{} > {MAX_COMPONENTS_PER_OBJECT}",
self.components.len()
));
}
Ok(())
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct GameObjectScene {
pub name: String,
pub objects: Vec<GameObject>,
next_id: u64,
}
impl GameObjectScene {
pub fn new(name: String) -> Self {
Self {
name,
objects: Vec::new(),
next_id: 0,
}
}
pub fn spawn(&mut self, name: String) -> Result<u64, String> {
if self.objects.len() >= MAX_SCENE_OBJECTS {
return Err(format!("scene_object_limit:max {MAX_SCENE_OBJECTS}"));
}
let id = self.next_id;
self.next_id = self.next_id.saturating_add(1);
self.objects.push(GameObject::new(id, name));
Ok(id)
}
pub fn spawn_primitive(&mut self, name: String, kind: PrimitiveKind) -> Result<u64, String> {
if self.objects.len() >= MAX_SCENE_OBJECTS {
return Err(format!("scene_object_limit:max {MAX_SCENE_OBJECTS}"));
}
let id = self.next_id;
self.next_id = self.next_id.saturating_add(1);
self.objects.push(GameObject::with_primitive(id, name, kind));
Ok(id)
}
pub fn find(&self, id: u64) -> Option<&GameObject> {
self.objects.iter().find(|o| o.id == id)
}
pub fn find_mut(&mut self, id: u64) -> Option<&mut GameObject> {
self.objects.iter_mut().find(|o| o.id == id)
}
pub fn despawn(&mut self, id: u64) -> bool {
let Some(pos) = self.objects.iter().position(|o| o.id == id) else {
return false;
};
self.objects.swap_remove(pos);
for obj in &mut self.objects {
if obj.parent_id == Some(id) {
obj.parent_id = None;
}
}
true
}
pub fn len(&self) -> usize {
self.objects.len()
}
pub fn is_empty(&self) -> bool {
self.objects.is_empty()
}
pub fn validate(&self) -> Result<(), String> {
if self.objects.len() > MAX_SCENE_OBJECTS {
return Err(format!(
"scene_object_limit:{} > {MAX_SCENE_OBJECTS}",
self.objects.len()
));
}
for obj in &self.objects {
obj.validate().map_err(|e| format!("object '{}': {}", obj.name, e))?;
}
Ok(())
}
pub fn roots(&self) -> impl Iterator<Item = &GameObject> {
self.objects.iter().filter(|o| o.parent_id.is_none())
}
pub fn children_of(&self, parent_id: u64) -> impl Iterator<Item = &GameObject> {
self.objects.iter().filter(move |o| o.parent_id == Some(parent_id))
}
pub fn propagate_transforms(&self) -> Vec<[f32; 16]> {
use crate::transform::mat4_mul;
let len = self.objects.len();
let mut world: Vec<[f32; 16]> =
vec![[1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,]; len];
if len == 0 {
return world;
}
let mut id_to_idx: std::collections::HashMap<u64, usize> = std::collections::HashMap::with_capacity(len);
let mut children: Vec<Vec<usize>> = vec![Vec::new(); len];
let mut roots: Vec<usize> = Vec::new();
for (i, obj) in self.objects.iter().enumerate() {
id_to_idx.insert(obj.id, i);
}
for (i, obj) in self.objects.iter().enumerate() {
if let Some(pid) = obj.parent_id {
if let Some(&pi) = id_to_idx.get(&pid) {
children[pi].push(i);
} else {
roots.push(i); }
} else {
roots.push(i);
}
}
let mut queue = std::collections::VecDeque::with_capacity(len);
for &ri in &roots {
world[ri] = self.objects[ri].transform.to_matrix();
queue.push_back(ri);
}
while let Some(idx) = queue.pop_front() {
for &ci in &children[idx] {
let local = self.objects[ci].transform.to_matrix();
world[ci] = mat4_mul(&world[idx], &local);
queue.push_back(ci);
}
}
world
}
pub fn visible_mesh_count(&self) -> usize {
self.objects
.iter()
.filter(|o| o.visible && !matches!(o.mesh, MeshBinding::None))
.count()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn transform_default_identity() {
let t = Transform::default();
assert_eq!(t.position, [0.0; 3]);
assert_eq!(t.rotation, [0.0, 0.0, 0.0, 1.0]);
assert_eq!(t.scale, [1.0, 1.0, 1.0]);
}
#[test]
fn transform_identity_matrix() {
let t = Transform::default();
let m = t.to_matrix();
#[rustfmt::skip]
let expected = [
1.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0,
];
for (a, b) in m.iter().zip(expected.iter()) {
assert!((a - b).abs() < 1e-6, "matrix mismatch: {a} vs {b}");
}
}
#[test]
fn transform_translation_in_matrix() {
let t = Transform {
position: [1.0, 2.0, 3.0],
..Default::default()
};
let m = t.to_matrix();
assert_eq!(m[12], 1.0);
assert_eq!(m[13], 2.0);
assert_eq!(m[14], 3.0);
}
#[test]
fn transform_scale_in_matrix() {
let t = Transform {
scale: [2.0, 3.0, 4.0],
..Default::default()
};
let m = t.to_matrix();
assert!((m[0] - 2.0).abs() < 1e-6);
assert!((m[5] - 3.0).abs() < 1e-6);
assert!((m[10] - 4.0).abs() < 1e-6);
}
#[test]
fn transform_validate_ok() {
assert!(Transform::default().validate().is_ok());
}
#[test]
fn transform_validate_zero_scale() {
let t = Transform {
scale: [0.0, 1.0, 1.0],
..Default::default()
};
assert!(t.validate().is_err());
}
#[test]
fn transform_validate_nan_position() {
let t = Transform {
position: [f32::NAN, 0.0, 0.0],
..Default::default()
};
assert!(t.validate().is_err());
}
#[test]
fn transform_validate_non_unit_quat() {
let t = Transform {
rotation: [1.0, 1.0, 1.0, 1.0],
..Default::default()
};
assert!(t.validate().is_err());
}
#[test]
fn game_object_new_defaults() {
let obj = GameObject::new(0, "Test".into());
assert_eq!(obj.id, 0);
assert!(obj.visible);
assert!(obj.components.is_empty());
assert_eq!(obj.mesh, MeshBinding::None);
assert_eq!(obj.parent_id, None);
}
#[test]
fn game_object_with_primitive() {
let obj = GameObject::with_primitive(1, "Cube".into(), PrimitiveKind::Cube);
assert!(matches!(
obj.mesh,
MeshBinding::Primitive {
kind: PrimitiveKind::Cube,
..
}
));
}
#[test]
fn game_object_add_remove_component() {
let mut obj = GameObject::new(0, "T".into());
obj.add_component(ComponentSlot::new(ComponentKind::Physics)).unwrap();
assert!(obj.has_component(ComponentKind::Physics));
assert!(obj.remove_component(ComponentKind::Physics));
assert!(!obj.has_component(ComponentKind::Physics));
}
#[test]
fn game_object_component_limit() {
let mut obj = GameObject::new(0, "T".into());
for _ in 0..MAX_COMPONENTS_PER_OBJECT {
obj.add_component(ComponentSlot::new(ComponentKind::Custom)).unwrap();
}
assert!(obj.add_component(ComponentSlot::new(ComponentKind::Custom)).is_err());
}
#[test]
fn component_slot_properties() {
let mut slot = ComponentSlot::new(ComponentKind::WaymarkScript);
slot.set_property("speed", serde_json::json!(5.0)).unwrap();
slot.set_property("health", serde_json::json!(100)).unwrap();
assert_eq!(slot.get_property("speed"), Some(&serde_json::json!(5.0)));
assert_eq!(slot.get_property("missing"), None);
}
#[test]
fn scene_spawn_and_find() {
let mut scene = GameObjectScene::new("Test".into());
let id = scene.spawn("Player".into()).unwrap();
assert_eq!(scene.len(), 1);
assert_eq!(scene.find(id).unwrap().name, "Player");
}
#[test]
fn scene_spawn_primitive() {
let mut scene = GameObjectScene::new("Test".into());
let id = scene.spawn_primitive("Cube".into(), PrimitiveKind::Cube).unwrap();
let obj = scene.find(id).unwrap();
assert!(matches!(
obj.mesh,
MeshBinding::Primitive {
kind: PrimitiveKind::Cube,
..
}
));
}
#[test]
fn scene_despawn_clears_parent_refs() {
let mut scene = GameObjectScene::new("Test".into());
let parent = scene.spawn("Parent".into()).unwrap();
let child = scene.spawn("Child".into()).unwrap();
scene.find_mut(child).unwrap().parent_id = Some(parent);
scene.despawn(parent);
assert_eq!(scene.find(child).unwrap().parent_id, None);
}
#[test]
fn scene_hierarchy() {
let mut scene = GameObjectScene::new("Test".into());
let p = scene.spawn("Parent".into()).unwrap();
let c1 = scene.spawn("C1".into()).unwrap();
let c2 = scene.spawn("C2".into()).unwrap();
scene.find_mut(c1).unwrap().parent_id = Some(p);
scene.find_mut(c2).unwrap().parent_id = Some(p);
assert_eq!(scene.roots().count(), 1);
assert_eq!(scene.children_of(p).count(), 2);
}
#[test]
fn scene_serialize_roundtrip() {
let mut scene = GameObjectScene::new("TestScene".into());
let id = scene.spawn_primitive("Cube".into(), PrimitiveKind::Cube).unwrap();
scene.find_mut(id).unwrap().transform.position = [1.0, 2.0, 3.0];
let mut slot = ComponentSlot::new(ComponentKind::WaymarkScript);
slot.set_property("speed", serde_json::json!(5.0)).unwrap();
scene.find_mut(id).unwrap().add_component(slot).unwrap();
let json = serde_json::to_string_pretty(&scene).unwrap();
let restored: GameObjectScene = serde_json::from_str(&json).unwrap();
assert_eq!(restored.name, "TestScene");
assert_eq!(restored.len(), 1);
assert_eq!(restored.objects[0].transform.position, [1.0, 2.0, 3.0]);
assert_eq!(restored.objects[0].components[0].kind, ComponentKind::WaymarkScript);
}
#[test]
fn scene_validate_ok() {
let mut scene = GameObjectScene::new("Ok".into());
scene.spawn_primitive("Cube".into(), PrimitiveKind::Cube).unwrap();
assert!(scene.validate().is_ok());
}
#[test]
fn scene_validate_bad_transform() {
let mut scene = GameObjectScene::new("Bad".into());
let id = scene.spawn("Bad".into()).unwrap();
scene.find_mut(id).unwrap().transform.scale = [0.0, 1.0, 1.0];
assert!(scene.validate().is_err());
}
#[test]
fn primitive_kind_all_count() {
assert_eq!(PrimitiveKind::ALL.len(), 9);
}
#[test]
fn mesh_binding_default_is_none() {
assert_eq!(MeshBinding::default(), MeshBinding::None);
}
#[test]
fn game_object_validate_name_too_long() {
let obj = GameObject::new(0, "x".repeat(257));
assert!(obj.validate().is_err());
}
#[test]
fn visible_mesh_count() {
let mut scene = GameObjectScene::new("Test".into());
scene.spawn_primitive("A".into(), PrimitiveKind::Cube).unwrap();
scene.spawn("Empty".into()).unwrap(); let id = scene.spawn_primitive("Hidden".into(), PrimitiveKind::Sphere).unwrap();
scene.find_mut(id).unwrap().visible = false;
assert_eq!(scene.visible_mesh_count(), 1);
}
#[test]
fn propagate_transforms_root_only() {
let mut scene = GameObjectScene::new("Test".into());
let id = scene.spawn("A".into()).unwrap();
scene.find_mut(id).unwrap().transform.position = [1.0, 2.0, 3.0];
let world = scene.propagate_transforms();
assert_eq!(world.len(), 1);
assert_eq!(world[0][12], 1.0);
assert_eq!(world[0][13], 2.0);
assert_eq!(world[0][14], 3.0);
}
#[test]
fn propagate_transforms_parent_child() {
let mut scene = GameObjectScene::new("Test".into());
let p = scene.spawn("Parent".into()).unwrap();
scene.find_mut(p).unwrap().transform.position = [10.0, 0.0, 0.0];
let c = scene.spawn("Child".into()).unwrap();
scene.find_mut(c).unwrap().transform.position = [5.0, 0.0, 0.0];
scene.find_mut(c).unwrap().parent_id = Some(p);
let world = scene.propagate_transforms();
assert!((world[1][12] - 15.0).abs() < 1e-5);
}
#[test]
fn propagate_transforms_empty_scene() {
let scene = GameObjectScene::new("Empty".into());
let world = scene.propagate_transforms();
assert!(world.is_empty());
}
#[test]
fn next_id_monotonic() {
let mut scene = GameObjectScene::new("Test".into());
let a = scene.spawn("A".into()).unwrap();
let b = scene.spawn("B".into()).unwrap();
assert_eq!(a, 0);
assert_eq!(b, 1);
scene.despawn(a);
let c = scene.spawn("C".into()).unwrap();
assert_eq!(c, 2); }
}