1use serde::{Deserialize, Serialize};
13
14pub const MAX_SCENE_OBJECTS: usize = 65536;
16
17pub const MAX_COMPONENTS_PER_OBJECT: usize = 32;
19
20#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
25pub struct Transform {
26 pub position: [f32; 3],
27 pub rotation: [f32; 4],
29 pub scale: [f32; 3],
30}
31
32impl Default for Transform {
33 fn default() -> Self {
34 Self {
35 position: [0.0; 3],
36 rotation: [0.0, 0.0, 0.0, 1.0],
37 scale: [1.0, 1.0, 1.0],
38 }
39 }
40}
41
42impl Transform {
43 pub fn to_matrix(&self) -> [f32; 16] {
45 let [qx, qy, qz, qw] = self.rotation;
46 let [sx, sy, sz] = self.scale;
47 let [px, py, pz] = self.position;
48
49 let x2 = qx + qx;
50 let y2 = qy + qy;
51 let z2 = qz + qz;
52 let xx = qx * x2;
53 let xy = qx * y2;
54 let xz = qx * z2;
55 let yy = qy * y2;
56 let yz = qy * z2;
57 let zz = qz * z2;
58 let wx = qw * x2;
59 let wy = qw * y2;
60 let wz = qw * z2;
61
62 [
63 (1.0 - (yy + zz)) * sx,
64 (xy + wz) * sx,
65 (xz - wy) * sx,
66 0.0,
67 (xy - wz) * sy,
68 (1.0 - (xx + zz)) * sy,
69 (yz + wx) * sy,
70 0.0,
71 (xz + wy) * sz,
72 (yz - wx) * sz,
73 (1.0 - (xx + yy)) * sz,
74 0.0,
75 px,
76 py,
77 pz,
78 1.0,
79 ]
80 }
81
82 pub fn validate(&self) -> Result<(), String> {
84 if self.position.iter().any(|p| !p.is_finite()) {
85 return Err("transform_invalid_position:contains NaN or Inf".into());
86 }
87 let [qx, qy, qz, qw] = self.rotation;
88 let len_sq = qx * qx + qy * qy + qz * qz + qw * qw;
89 if !len_sq.is_finite() || (len_sq - 1.0).abs() > 0.01 {
90 return Err(format!(
91 "transform_invalid_rotation:quaternion length² {len_sq} not ≈1.0"
92 ));
93 }
94 if self.scale.iter().any(|&s| s <= 0.0 || !s.is_finite()) {
95 return Err("transform_invalid_scale:must be positive and finite".into());
96 }
97 Ok(())
98 }
99}
100
101#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
105pub enum PrimitiveKind {
106 Cube,
107 Sphere,
108 Cylinder,
109 Cone,
110 Plane,
111 Torus,
112 Capsule,
113 Pyramid,
114 Wedge,
115}
116
117impl PrimitiveKind {
118 pub const ALL: &'static [PrimitiveKind] = &[
119 Self::Cube,
120 Self::Sphere,
121 Self::Cylinder,
122 Self::Cone,
123 Self::Plane,
124 Self::Torus,
125 Self::Capsule,
126 Self::Pyramid,
127 Self::Wedge,
128 ];
129
130 pub fn name(self) -> &'static str {
131 match self {
132 Self::Cube => "Cube",
133 Self::Sphere => "Sphere",
134 Self::Cylinder => "Cylinder",
135 Self::Cone => "Cone",
136 Self::Plane => "Plane",
137 Self::Torus => "Torus",
138 Self::Capsule => "Capsule",
139 Self::Pyramid => "Pyramid",
140 Self::Wedge => "Wedge",
141 }
142 }
143}
144
145#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
147pub enum MeshBinding {
148 #[default]
150 None,
151 Primitive { kind: PrimitiveKind, color: [f32; 4] },
153 Custom { asset_key: String },
155}
156
157#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
163pub enum ComponentKind {
164 Physics,
166 Collider,
168 WaymarkScript,
171 Audio,
173 Light,
175 Particle,
177 Camera,
179 Trigger,
181 Animation,
183 DreamMatter,
187 FbxAnimation,
190 Custom,
192}
193
194#[derive(Debug, Clone, Serialize, Deserialize)]
197pub struct ComponentSlot {
198 pub kind: ComponentKind,
199 pub enabled: bool,
200 pub properties: serde_json::Value,
203}
204
205impl ComponentSlot {
206 pub fn new(kind: ComponentKind) -> Self {
207 Self {
208 kind,
209 enabled: true,
210 properties: serde_json::Value::Object(serde_json::Map::new()),
211 }
212 }
213
214 pub fn set_property(&mut self, key: &str, value: serde_json::Value) -> Result<(), String> {
216 if let serde_json::Value::Object(ref mut map) = self.properties {
217 if map.len() >= 256 && !map.contains_key(key) {
218 return Err("component_property_limit:max 256 properties".into());
219 }
220 map.insert(key.to_string(), value);
221 Ok(())
222 } else {
223 Err("component_properties_not_object:must be a JSON object".into())
224 }
225 }
226
227 pub fn get_property(&self, key: &str) -> Option<&serde_json::Value> {
228 self.properties.get(key)
229 }
230}
231
232#[derive(Debug, Clone, Serialize, Deserialize)]
242pub struct GameObject {
243 pub id: u64,
244 pub name: String,
245 pub transform: Transform,
246 pub mesh: MeshBinding,
247 pub visible: bool,
248 pub parent_id: Option<u64>,
249 pub components: Vec<ComponentSlot>,
250 #[serde(default, skip_serializing_if = "Vec::is_empty")]
253 pub property_tags: Vec<String>,
254 #[serde(default = "default_topology_layer")]
258 pub topology_layer: u8,
259}
260
261fn default_topology_layer() -> u8 {
262 9
263}
264
265impl GameObject {
266 pub fn new(id: u64, name: String) -> Self {
268 Self {
269 id,
270 name,
271 transform: Transform::default(),
272 mesh: MeshBinding::None,
273 visible: true,
274 parent_id: None,
275 components: Vec::new(),
276 property_tags: Vec::new(),
277 topology_layer: 9,
278 }
279 }
280
281 pub fn with_primitive(id: u64, name: String, kind: PrimitiveKind) -> Self {
283 Self {
284 mesh: MeshBinding::Primitive {
285 kind,
286 color: [0.7, 0.7, 0.7, 1.0],
287 },
288 ..Self::new(id, name)
289 }
290 }
291
292 pub fn add_component(&mut self, slot: ComponentSlot) -> Result<(), String> {
294 if self.components.len() >= MAX_COMPONENTS_PER_OBJECT {
295 return Err(format!(
296 "game_object_component_limit:max {MAX_COMPONENTS_PER_OBJECT} components"
297 ));
298 }
299 self.components.push(slot);
300 Ok(())
301 }
302
303 pub fn remove_component(&mut self, kind: ComponentKind) -> bool {
305 if let Some(pos) = self.components.iter().position(|c| c.kind == kind) {
306 self.components.swap_remove(pos);
307 true
308 } else {
309 false
310 }
311 }
312
313 pub fn get_component(&self, kind: ComponentKind) -> Option<&ComponentSlot> {
314 self.components.iter().find(|c| c.kind == kind)
315 }
316
317 pub fn get_component_mut(&mut self, kind: ComponentKind) -> Option<&mut ComponentSlot> {
318 self.components.iter_mut().find(|c| c.kind == kind)
319 }
320
321 pub fn has_component(&self, kind: ComponentKind) -> bool {
323 self.components.iter().any(|c| c.kind == kind)
324 }
325
326 pub fn validate(&self) -> Result<(), String> {
328 self.transform.validate()?;
329 if self.name.len() > 256 {
330 return Err("game_object_name_too_long:max 256 chars".into());
331 }
332 if self.components.len() > MAX_COMPONENTS_PER_OBJECT {
333 return Err(format!(
334 "game_object_too_many_components:{} > {MAX_COMPONENTS_PER_OBJECT}",
335 self.components.len()
336 ));
337 }
338 Ok(())
339 }
340}
341
342#[derive(Debug, Clone, Default, Serialize, Deserialize)]
347pub struct GameObjectScene {
348 pub name: String,
349 pub objects: Vec<GameObject>,
350 next_id: u64,
351}
352
353impl GameObjectScene {
354 pub fn new(name: String) -> Self {
355 Self {
356 name,
357 objects: Vec::new(),
358 next_id: 0,
359 }
360 }
361
362 pub fn spawn(&mut self, name: String) -> Result<u64, String> {
364 if self.objects.len() >= MAX_SCENE_OBJECTS {
365 return Err(format!("scene_object_limit:max {MAX_SCENE_OBJECTS}"));
366 }
367 let id = self.next_id;
368 self.next_id = self.next_id.saturating_add(1);
369 self.objects.push(GameObject::new(id, name));
370 Ok(id)
371 }
372
373 pub fn spawn_primitive(&mut self, name: String, kind: PrimitiveKind) -> Result<u64, String> {
375 if self.objects.len() >= MAX_SCENE_OBJECTS {
376 return Err(format!("scene_object_limit:max {MAX_SCENE_OBJECTS}"));
377 }
378 let id = self.next_id;
379 self.next_id = self.next_id.saturating_add(1);
380 self.objects.push(GameObject::with_primitive(id, name, kind));
381 Ok(id)
382 }
383
384 pub fn find(&self, id: u64) -> Option<&GameObject> {
385 self.objects.iter().find(|o| o.id == id)
386 }
387
388 pub fn find_mut(&mut self, id: u64) -> Option<&mut GameObject> {
389 self.objects.iter_mut().find(|o| o.id == id)
390 }
391
392 pub fn despawn(&mut self, id: u64) -> bool {
394 let Some(pos) = self.objects.iter().position(|o| o.id == id) else {
395 return false;
396 };
397 self.objects.swap_remove(pos);
398 for obj in &mut self.objects {
399 if obj.parent_id == Some(id) {
400 obj.parent_id = None;
401 }
402 }
403 true
404 }
405
406 pub fn len(&self) -> usize {
407 self.objects.len()
408 }
409
410 pub fn is_empty(&self) -> bool {
411 self.objects.is_empty()
412 }
413
414 pub fn validate(&self) -> Result<(), String> {
416 if self.objects.len() > MAX_SCENE_OBJECTS {
417 return Err(format!(
418 "scene_object_limit:{} > {MAX_SCENE_OBJECTS}",
419 self.objects.len()
420 ));
421 }
422 for obj in &self.objects {
423 obj.validate().map_err(|e| format!("object '{}': {}", obj.name, e))?;
424 }
425 Ok(())
426 }
427
428 pub fn roots(&self) -> impl Iterator<Item = &GameObject> {
430 self.objects.iter().filter(|o| o.parent_id.is_none())
431 }
432
433 pub fn children_of(&self, parent_id: u64) -> impl Iterator<Item = &GameObject> {
435 self.objects.iter().filter(move |o| o.parent_id == Some(parent_id))
436 }
437
438 pub fn propagate_transforms(&self) -> Vec<[f32; 16]> {
445 use crate::transform::mat4_mul;
446
447 let len = self.objects.len();
448 let mut world: Vec<[f32; 16]> =
449 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];
450 if len == 0 {
451 return world;
452 }
453
454 let mut id_to_idx: std::collections::HashMap<u64, usize> = std::collections::HashMap::with_capacity(len);
456 let mut children: Vec<Vec<usize>> = vec![Vec::new(); len];
457 let mut roots: Vec<usize> = Vec::new();
458
459 for (i, obj) in self.objects.iter().enumerate() {
460 id_to_idx.insert(obj.id, i);
461 }
462 for (i, obj) in self.objects.iter().enumerate() {
463 if let Some(pid) = obj.parent_id {
464 if let Some(&pi) = id_to_idx.get(&pid) {
465 children[pi].push(i);
466 } else {
467 roots.push(i); }
469 } else {
470 roots.push(i);
471 }
472 }
473
474 let mut queue = std::collections::VecDeque::with_capacity(len);
476 for &ri in &roots {
477 world[ri] = self.objects[ri].transform.to_matrix();
478 queue.push_back(ri);
479 }
480
481 while let Some(idx) = queue.pop_front() {
482 for &ci in &children[idx] {
483 let local = self.objects[ci].transform.to_matrix();
484 world[ci] = mat4_mul(&world[idx], &local);
485 queue.push_back(ci);
486 }
487 }
488
489 world
490 }
491
492 pub fn visible_mesh_count(&self) -> usize {
494 self.objects
495 .iter()
496 .filter(|o| o.visible && !matches!(o.mesh, MeshBinding::None))
497 .count()
498 }
499}
500
501#[cfg(test)]
504mod tests {
505 use super::*;
506
507 #[test]
508 fn transform_default_identity() {
509 let t = Transform::default();
510 assert_eq!(t.position, [0.0; 3]);
511 assert_eq!(t.rotation, [0.0, 0.0, 0.0, 1.0]);
512 assert_eq!(t.scale, [1.0, 1.0, 1.0]);
513 }
514
515 #[test]
516 fn transform_identity_matrix() {
517 let t = Transform::default();
518 let m = t.to_matrix();
519 #[rustfmt::skip]
520 let expected = [
521 1.0, 0.0, 0.0, 0.0,
522 0.0, 1.0, 0.0, 0.0,
523 0.0, 0.0, 1.0, 0.0,
524 0.0, 0.0, 0.0, 1.0,
525 ];
526 for (a, b) in m.iter().zip(expected.iter()) {
527 assert!((a - b).abs() < 1e-6, "matrix mismatch: {a} vs {b}");
528 }
529 }
530
531 #[test]
532 fn transform_translation_in_matrix() {
533 let t = Transform {
534 position: [1.0, 2.0, 3.0],
535 ..Default::default()
536 };
537 let m = t.to_matrix();
538 assert_eq!(m[12], 1.0);
539 assert_eq!(m[13], 2.0);
540 assert_eq!(m[14], 3.0);
541 }
542
543 #[test]
544 fn transform_scale_in_matrix() {
545 let t = Transform {
546 scale: [2.0, 3.0, 4.0],
547 ..Default::default()
548 };
549 let m = t.to_matrix();
550 assert!((m[0] - 2.0).abs() < 1e-6);
551 assert!((m[5] - 3.0).abs() < 1e-6);
552 assert!((m[10] - 4.0).abs() < 1e-6);
553 }
554
555 #[test]
556 fn transform_validate_ok() {
557 assert!(Transform::default().validate().is_ok());
558 }
559
560 #[test]
561 fn transform_validate_zero_scale() {
562 let t = Transform {
563 scale: [0.0, 1.0, 1.0],
564 ..Default::default()
565 };
566 assert!(t.validate().is_err());
567 }
568
569 #[test]
570 fn transform_validate_nan_position() {
571 let t = Transform {
572 position: [f32::NAN, 0.0, 0.0],
573 ..Default::default()
574 };
575 assert!(t.validate().is_err());
576 }
577
578 #[test]
579 fn transform_validate_non_unit_quat() {
580 let t = Transform {
581 rotation: [1.0, 1.0, 1.0, 1.0],
582 ..Default::default()
583 };
584 assert!(t.validate().is_err());
585 }
586
587 #[test]
588 fn game_object_new_defaults() {
589 let obj = GameObject::new(0, "Test".into());
590 assert_eq!(obj.id, 0);
591 assert!(obj.visible);
592 assert!(obj.components.is_empty());
593 assert_eq!(obj.mesh, MeshBinding::None);
594 assert_eq!(obj.parent_id, None);
595 }
596
597 #[test]
598 fn game_object_with_primitive() {
599 let obj = GameObject::with_primitive(1, "Cube".into(), PrimitiveKind::Cube);
600 assert!(matches!(
601 obj.mesh,
602 MeshBinding::Primitive {
603 kind: PrimitiveKind::Cube,
604 ..
605 }
606 ));
607 }
608
609 #[test]
610 fn game_object_add_remove_component() {
611 let mut obj = GameObject::new(0, "T".into());
612 obj.add_component(ComponentSlot::new(ComponentKind::Physics)).unwrap();
613 assert!(obj.has_component(ComponentKind::Physics));
614 assert!(obj.remove_component(ComponentKind::Physics));
615 assert!(!obj.has_component(ComponentKind::Physics));
616 }
617
618 #[test]
619 fn game_object_component_limit() {
620 let mut obj = GameObject::new(0, "T".into());
621 for _ in 0..MAX_COMPONENTS_PER_OBJECT {
622 obj.add_component(ComponentSlot::new(ComponentKind::Custom)).unwrap();
623 }
624 assert!(obj.add_component(ComponentSlot::new(ComponentKind::Custom)).is_err());
625 }
626
627 #[test]
628 fn component_slot_properties() {
629 let mut slot = ComponentSlot::new(ComponentKind::WaymarkScript);
630 slot.set_property("speed", serde_json::json!(5.0)).unwrap();
631 slot.set_property("health", serde_json::json!(100)).unwrap();
632 assert_eq!(slot.get_property("speed"), Some(&serde_json::json!(5.0)));
633 assert_eq!(slot.get_property("missing"), None);
634 }
635
636 #[test]
637 fn scene_spawn_and_find() {
638 let mut scene = GameObjectScene::new("Test".into());
639 let id = scene.spawn("Player".into()).unwrap();
640 assert_eq!(scene.len(), 1);
641 assert_eq!(scene.find(id).unwrap().name, "Player");
642 }
643
644 #[test]
645 fn scene_spawn_primitive() {
646 let mut scene = GameObjectScene::new("Test".into());
647 let id = scene.spawn_primitive("Cube".into(), PrimitiveKind::Cube).unwrap();
648 let obj = scene.find(id).unwrap();
649 assert!(matches!(
650 obj.mesh,
651 MeshBinding::Primitive {
652 kind: PrimitiveKind::Cube,
653 ..
654 }
655 ));
656 }
657
658 #[test]
659 fn scene_despawn_clears_parent_refs() {
660 let mut scene = GameObjectScene::new("Test".into());
661 let parent = scene.spawn("Parent".into()).unwrap();
662 let child = scene.spawn("Child".into()).unwrap();
663 scene.find_mut(child).unwrap().parent_id = Some(parent);
664 scene.despawn(parent);
665 assert_eq!(scene.find(child).unwrap().parent_id, None);
666 }
667
668 #[test]
669 fn scene_hierarchy() {
670 let mut scene = GameObjectScene::new("Test".into());
671 let p = scene.spawn("Parent".into()).unwrap();
672 let c1 = scene.spawn("C1".into()).unwrap();
673 let c2 = scene.spawn("C2".into()).unwrap();
674 scene.find_mut(c1).unwrap().parent_id = Some(p);
675 scene.find_mut(c2).unwrap().parent_id = Some(p);
676 assert_eq!(scene.roots().count(), 1);
677 assert_eq!(scene.children_of(p).count(), 2);
678 }
679
680 #[test]
681 fn scene_serialize_roundtrip() {
682 let mut scene = GameObjectScene::new("TestScene".into());
683 let id = scene.spawn_primitive("Cube".into(), PrimitiveKind::Cube).unwrap();
684 scene.find_mut(id).unwrap().transform.position = [1.0, 2.0, 3.0];
685
686 let mut slot = ComponentSlot::new(ComponentKind::WaymarkScript);
687 slot.set_property("speed", serde_json::json!(5.0)).unwrap();
688 scene.find_mut(id).unwrap().add_component(slot).unwrap();
689
690 let json = serde_json::to_string_pretty(&scene).unwrap();
691 let restored: GameObjectScene = serde_json::from_str(&json).unwrap();
692
693 assert_eq!(restored.name, "TestScene");
694 assert_eq!(restored.len(), 1);
695 assert_eq!(restored.objects[0].transform.position, [1.0, 2.0, 3.0]);
696 assert_eq!(restored.objects[0].components[0].kind, ComponentKind::WaymarkScript);
697 }
698
699 #[test]
700 fn scene_validate_ok() {
701 let mut scene = GameObjectScene::new("Ok".into());
702 scene.spawn_primitive("Cube".into(), PrimitiveKind::Cube).unwrap();
703 assert!(scene.validate().is_ok());
704 }
705
706 #[test]
707 fn scene_validate_bad_transform() {
708 let mut scene = GameObjectScene::new("Bad".into());
709 let id = scene.spawn("Bad".into()).unwrap();
710 scene.find_mut(id).unwrap().transform.scale = [0.0, 1.0, 1.0];
711 assert!(scene.validate().is_err());
712 }
713
714 #[test]
715 fn primitive_kind_all_count() {
716 assert_eq!(PrimitiveKind::ALL.len(), 9);
717 }
718
719 #[test]
720 fn mesh_binding_default_is_none() {
721 assert_eq!(MeshBinding::default(), MeshBinding::None);
722 }
723
724 #[test]
725 fn game_object_validate_name_too_long() {
726 let obj = GameObject::new(0, "x".repeat(257));
727 assert!(obj.validate().is_err());
728 }
729
730 #[test]
731 fn visible_mesh_count() {
732 let mut scene = GameObjectScene::new("Test".into());
733 scene.spawn_primitive("A".into(), PrimitiveKind::Cube).unwrap();
734 scene.spawn("Empty".into()).unwrap(); let id = scene.spawn_primitive("Hidden".into(), PrimitiveKind::Sphere).unwrap();
736 scene.find_mut(id).unwrap().visible = false;
737 assert_eq!(scene.visible_mesh_count(), 1);
738 }
739
740 #[test]
741 fn propagate_transforms_root_only() {
742 let mut scene = GameObjectScene::new("Test".into());
743 let id = scene.spawn("A".into()).unwrap();
744 scene.find_mut(id).unwrap().transform.position = [1.0, 2.0, 3.0];
745 let world = scene.propagate_transforms();
746 assert_eq!(world.len(), 1);
747 assert_eq!(world[0][12], 1.0);
748 assert_eq!(world[0][13], 2.0);
749 assert_eq!(world[0][14], 3.0);
750 }
751
752 #[test]
753 fn propagate_transforms_parent_child() {
754 let mut scene = GameObjectScene::new("Test".into());
755 let p = scene.spawn("Parent".into()).unwrap();
756 scene.find_mut(p).unwrap().transform.position = [10.0, 0.0, 0.0];
757 let c = scene.spawn("Child".into()).unwrap();
758 scene.find_mut(c).unwrap().transform.position = [5.0, 0.0, 0.0];
759 scene.find_mut(c).unwrap().parent_id = Some(p);
760
761 let world = scene.propagate_transforms();
762 assert!((world[1][12] - 15.0).abs() < 1e-5);
764 }
765
766 #[test]
767 fn propagate_transforms_empty_scene() {
768 let scene = GameObjectScene::new("Empty".into());
769 let world = scene.propagate_transforms();
770 assert!(world.is_empty());
771 }
772
773 #[test]
774 fn next_id_monotonic() {
775 let mut scene = GameObjectScene::new("Test".into());
776 let a = scene.spawn("A".into()).unwrap();
777 let b = scene.spawn("B".into()).unwrap();
778 assert_eq!(a, 0);
779 assert_eq!(b, 1);
780 scene.despawn(a);
781 let c = scene.spawn("C".into()).unwrap();
782 assert_eq!(c, 2); }
784}