1use crate::game_object::{ComponentKind, GameObjectScene, MeshBinding};
43use crate::lighting::{DirectionalLightDesc, PointLightDesc};
44
45#[derive(Debug, Clone)]
47pub struct RenderConfig {
48 pub bloom_enabled: bool,
49 pub tonemap_enabled: bool,
50 pub ssao_enabled: bool,
51 pub ssr_enabled: bool,
52 pub dof_enabled: bool,
53 pub ssgi_enabled: bool,
54 pub taa_enabled: bool,
55 pub scene_dream_mode: String,
56 pub topology_layer: u8,
57}
58
59impl Default for RenderConfig {
60 fn default() -> Self {
61 Self {
62 bloom_enabled: true,
63 tonemap_enabled: true,
64 ssao_enabled: true,
65 ssr_enabled: false,
66 dof_enabled: false,
67 ssgi_enabled: false,
68 taa_enabled: true,
69 scene_dream_mode: "PbrDefault".into(),
70 topology_layer: 6,
71 }
72 }
73}
74
75#[derive(Debug, Clone)]
77pub enum ExtractedLight {
78 Directional(DirectionalLightDesc),
79 Point(PointLightDesc),
80}
81
82#[derive(Debug, Clone)]
84pub struct ExtractedBody {
85 pub object_id: u64,
86 pub position: [f32; 3],
87 pub shape: BodyShape,
88 pub mass: f32,
89 pub restitution: f32,
90 pub friction: f32,
91 pub is_static: bool,
92 pub tags: Vec<String>,
93}
94
95#[derive(Debug, Clone)]
97pub enum BodyShape {
98 Sphere { radius: f32 },
99 Box { half_extents: [f32; 3] },
100 Capsule { radius: f32, half_height: f32 },
101}
102
103#[derive(Debug, Clone)]
105pub struct ExtractedEmitter {
106 pub object_id: u64,
107 pub position: [f32; 3],
108 pub kind: EmitterKind,
109 pub tags: Vec<String>,
110}
111
112#[derive(Debug, Clone)]
113pub enum EmitterKind {
114 Particle,
115 DreamMatter,
116}
117
118#[derive(Debug)]
124pub struct WovenScene {
125 pub scene: GameObjectScene,
127 pub lights: Vec<ExtractedLight>,
129 pub bodies: Vec<ExtractedBody>,
131 pub emitters: Vec<ExtractedEmitter>,
133 pub render_config: RenderConfig,
135 pub tagged_objects: Vec<(u64, Vec<String>)>,
137 pub warnings: Vec<String>,
139 pub stats: WovenStats,
141}
142
143#[derive(Debug, Clone, Default)]
145pub struct WovenStats {
146 pub total_objects: u32,
147 pub mesh_objects: u32,
148 pub light_objects: u32,
149 pub physics_bodies: u32,
150 pub emitter_count: u32,
151 pub tagged_objects: u32,
152 pub dreamlet_count: u32,
153 pub dreamfab_count: u32,
154}
155
156pub type LoomResult = Result<WovenScene, Vec<String>>;
158
159#[derive(Debug, Clone, Copy, PartialEq, Eq)]
166pub enum BootstrapStage {
167 Validate,
169 Extract,
171 Configure,
173 Sort,
175 Manifest,
177}
178
179impl BootstrapStage {
180 pub const ALL: &'static [BootstrapStage] = &[
181 BootstrapStage::Validate,
182 BootstrapStage::Extract,
183 BootstrapStage::Configure,
184 BootstrapStage::Sort,
185 BootstrapStage::Manifest,
186 ];
187
188 pub fn label(self) -> &'static str {
189 match self {
190 Self::Validate => "Validate",
191 Self::Extract => "Extract",
192 Self::Configure => "Configure",
193 Self::Sort => "Sort",
194 Self::Manifest => "Manifest",
195 }
196 }
197}
198
199#[derive(Debug, Clone, Copy, PartialEq, Eq)]
202pub enum FrameStage {
203 Input,
205 Simulation,
207 Physics,
209 Animation,
211 RenderPrep,
213 Presentation,
215 Cleanup,
217}
218
219impl FrameStage {
220 pub const ALL: &'static [FrameStage] = &[
221 FrameStage::Input,
222 FrameStage::Simulation,
223 FrameStage::Physics,
224 FrameStage::Animation,
225 FrameStage::RenderPrep,
226 FrameStage::Presentation,
227 FrameStage::Cleanup,
228 ];
229
230 pub fn label(self) -> &'static str {
231 match self {
232 Self::Input => "Input",
233 Self::Simulation => "Simulation",
234 Self::Physics => "Physics",
235 Self::Animation => "Animation",
236 Self::RenderPrep => "RenderPrep",
237 Self::Presentation => "Presentation",
238 Self::Cleanup => "Cleanup",
239 }
240 }
241}
242
243pub fn weave(scene: GameObjectScene, render_config: RenderConfig) -> LoomResult {
256 let mut errors = Vec::new();
257 let mut warnings = Vec::new();
258
259 if scene.objects.is_empty() {
261 warnings.push("loom:empty_scene — no objects to weave".into());
262 }
263
264 let mut seen_ids = std::collections::HashSet::new();
265 for (i, obj) in scene.objects.iter().enumerate() {
266 for v in obj
268 .transform
269 .position
270 .iter()
271 .chain(obj.transform.scale.iter())
272 .chain(obj.transform.rotation.iter())
273 {
274 if !v.is_finite() {
275 errors.push(format!(
276 "loom:nan_transform — object '{}' (index {i}) has NaN/Inf",
277 obj.name
278 ));
279 break;
280 }
281 }
282 if obj.transform.scale.iter().any(|s| *s == 0.0) {
284 warnings.push(format!("loom:zero_scale — object '{}' has zero scale axis", obj.name));
285 }
286 if !seen_ids.insert(obj.id) {
288 errors.push(format!(
289 "loom:duplicate_id — object '{}' has duplicate id {}",
290 obj.name, obj.id
291 ));
292 }
293 if obj.name.is_empty() {
295 warnings.push(format!("loom:empty_name — object index {i} has empty name"));
296 }
297 if let Some(parent_id) = obj.parent_id {
299 if parent_id == obj.id {
300 errors.push(format!("loom:self_parent — object '{}' is its own parent", obj.name));
301 }
302 }
303 let mut comp_kinds = std::collections::HashSet::new();
305 for comp in &obj.components {
306 if !comp_kinds.insert(std::mem::discriminant(&comp.kind)) {
307 warnings.push(format!(
308 "loom:duplicate_component — object '{}' has duplicate {:?} component",
309 obj.name, comp.kind
310 ));
311 }
312 }
313 }
314
315 let all_ids: std::collections::HashSet<u64> = scene.objects.iter().map(|o| o.id).collect();
317 for obj in &scene.objects {
318 if let Some(parent_id) = obj.parent_id {
319 if !all_ids.contains(&parent_id) {
320 errors.push(format!(
321 "loom:orphan_parent — object '{}' references non-existent parent {}",
322 obj.name, parent_id
323 ));
324 }
325 }
326 }
327
328 for obj in &scene.objects {
330 let mut current = obj.parent_id;
331 let mut depth = 0u32;
332 while let Some(pid) = current {
333 depth += 1;
334 if depth > 64 {
335 errors.push(format!(
336 "loom:hierarchy_cycle — object '{}' has cycle or depth > 64",
337 obj.name
338 ));
339 break;
340 }
341 if pid == obj.id {
342 errors.push(format!(
343 "loom:hierarchy_cycle — object '{}' is in a parent cycle",
344 obj.name
345 ));
346 break;
347 }
348 current = scene.objects.iter().find(|o| o.id == pid).and_then(|o| o.parent_id);
349 }
350 }
351
352 if !errors.is_empty() {
353 return Err(errors);
354 }
355
356 let mut lights = Vec::new();
358 for obj in &scene.objects {
359 if !obj.has_component(ComponentKind::Light) {
360 continue;
361 }
362 let pos = obj.transform.position;
363 let len = (pos[0] * pos[0] + pos[1] * pos[1] + pos[2] * pos[2]).sqrt();
364
365 let light_props = obj.get_component(ComponentKind::Light).map(|c| &c.properties);
367
368 let intensity = light_props
369 .and_then(|p| p.get("intensity_lux"))
370 .and_then(|v| v.as_f64())
371 .map(|v| v as f32)
372 .unwrap_or(100_000.0);
373
374 let color = light_props
375 .and_then(|p| p.get("color"))
376 .and_then(|v| v.as_str())
377 .and_then(parse_color_3)
378 .unwrap_or([1.0, 0.95, 0.9]);
379
380 let range = light_props
381 .and_then(|p| p.get("range"))
382 .and_then(|v| v.as_f64())
383 .map(|v| v as f32);
384
385 if let Some(range) = range {
386 lights.push(ExtractedLight::Point(PointLightDesc {
388 position: pos,
389 color,
390 intensity_lumens: intensity,
391 range,
392 }));
393 } else if len > 0.001 {
394 lights.push(ExtractedLight::Directional(DirectionalLightDesc {
396 direction: [-pos[0] / len, -pos[1] / len, -pos[2] / len],
397 intensity_lux: intensity,
398 color,
399 }));
400 }
401 }
402
403 let mut bodies = Vec::new();
405 for obj in &scene.objects {
406 let has_physics_tags = obj.property_tags.iter().any(|t| {
408 t == "isDestructible"
409 || t == "isFlammable"
410 || t == "isExplosive"
411 || t == "isPickupable"
412 || t == "isHazard"
413 || t == "isWall"
414 });
415
416 if has_physics_tags || obj.has_component(ComponentKind::Physics) {
417 let scale = obj.transform.scale;
418 let shape = BodyShape::Box {
419 half_extents: [scale[0] * 0.5, scale[1] * 0.5, scale[2] * 0.5],
420 };
421 let mass = if obj.property_tags.iter().any(|t| t == "isWall" || t == "isWalkable") {
422 0.0 } else {
424 1.0
425 };
426
427 bodies.push(ExtractedBody {
428 object_id: obj.id,
429 position: obj.transform.position,
430 shape,
431 mass,
432 restitution: 0.3,
433 friction: 0.5,
434 is_static: mass == 0.0,
435 tags: obj.property_tags.clone(),
436 });
437 }
438 }
439
440 let mut emitters = Vec::new();
442 for obj in &scene.objects {
443 if obj.has_component(ComponentKind::Particle) {
444 emitters.push(ExtractedEmitter {
445 object_id: obj.id,
446 position: obj.transform.position,
447 kind: EmitterKind::Particle,
448 tags: obj.property_tags.clone(),
449 });
450 }
451 if obj.has_component(ComponentKind::DreamMatter) {
452 emitters.push(ExtractedEmitter {
453 object_id: obj.id,
454 position: obj.transform.position,
455 kind: EmitterKind::DreamMatter,
456 tags: obj.property_tags.clone(),
457 });
458 }
459 }
460
461 let tagged_objects: Vec<(u64, Vec<String>)> = scene
463 .objects
464 .iter()
465 .filter(|obj| !obj.property_tags.is_empty())
466 .map(|obj| (obj.id, obj.property_tags.clone()))
467 .collect();
468
469 let mut stats = WovenStats::default();
471 stats.total_objects = scene.objects.len() as u32;
472 for obj in &scene.objects {
473 match &obj.mesh {
474 MeshBinding::Primitive { .. } | MeshBinding::Custom { .. } => stats.mesh_objects += 1,
475 MeshBinding::None => {}
476 }
477 if obj.has_component(ComponentKind::Light) {
478 stats.light_objects += 1;
479 }
480 if obj.property_tags.contains(&"isDreammatter".to_string()) {
481 stats.dreamlet_count += 1;
482 }
483 if obj.has_component(ComponentKind::DreamMatter) && !obj.property_tags.contains(&"isDreammatter".to_string()) {
485 stats.dreamfab_count += 1;
486 }
487 }
488 stats.physics_bodies = bodies.len() as u32;
489 stats.emitter_count = emitters.len() as u32;
490 stats.tagged_objects = tagged_objects.len() as u32;
491
492 Ok(WovenScene {
493 scene,
494 lights,
495 bodies,
496 emitters,
497 render_config,
498 tagged_objects,
499 warnings,
500 stats,
501 })
502}
503
504fn parse_color_3(s: &str) -> Option<[f32; 3]> {
506 let s = s.trim().trim_start_matches('[').trim_end_matches(']');
507 let parts: Vec<&str> = s.split(',').collect();
508 if parts.len() >= 3 {
509 let r = parts[0].trim().parse::<f32>().ok()?;
510 let g = parts[1].trim().parse::<f32>().ok()?;
511 let b = parts[2].trim().parse::<f32>().ok()?;
512 Some([r, g, b])
513 } else {
514 None
515 }
516}
517
518#[cfg(test)]
519mod tests {
520 use super::*;
521 use crate::game_object::ComponentSlot;
522
523 fn test_scene() -> GameObjectScene {
524 let mut scene = GameObjectScene::new("test".into());
525 let id = scene
527 .spawn_primitive("Wall".into(), crate::game_object::PrimitiveKind::Cube)
528 .unwrap();
529 if let Some(obj) = scene.find_mut(id) {
530 obj.property_tags.push("isWall".into());
531 obj.property_tags.push("isDestructible".into());
532 }
533 let light_id = scene.spawn("Sun".into()).unwrap();
535 if let Some(obj) = scene.find_mut(light_id) {
536 obj.transform.position = [3.0, 6.0, 3.0];
537 let _ = obj.add_component(ComponentSlot::new(ComponentKind::Light));
538 }
539 let dm_id = scene.spawn("Fire Effect".into()).unwrap();
541 if let Some(obj) = scene.find_mut(dm_id) {
542 obj.property_tags.push("isDreammatter".into());
543 obj.property_tags.push("isFlammable".into());
544 let _ = obj.add_component(ComponentSlot::new(ComponentKind::DreamMatter));
545 }
546 scene
547 }
548
549 #[test]
550 fn weave_extracts_all() {
551 let scene = test_scene();
552 let woven = weave(scene, RenderConfig::default()).unwrap();
553 assert_eq!(woven.stats.total_objects, 3);
554 assert_eq!(woven.stats.light_objects, 1);
555 assert_eq!(woven.stats.physics_bodies, 2); assert_eq!(woven.stats.emitter_count, 1); assert_eq!(woven.stats.tagged_objects, 2); assert_eq!(woven.stats.dreamlet_count, 1); assert_eq!(woven.lights.len(), 1);
560 }
561
562 #[test]
563 fn weave_rejects_nan() {
564 let mut scene = GameObjectScene::new("bad".into());
565 let id = scene.spawn("NaN Object".into()).unwrap();
566 if let Some(obj) = scene.find_mut(id) {
567 obj.transform.position = [f32::NAN, 0.0, 0.0];
568 }
569 let result = weave(scene, RenderConfig::default());
570 assert!(result.is_err());
571 assert!(result.unwrap_err()[0].contains("nan_transform"));
572 }
573
574 #[test]
575 fn weave_static_body_for_wall() {
576 let scene = test_scene();
577 let woven = weave(scene, RenderConfig::default()).unwrap();
578 let wall_body = woven
579 .bodies
580 .iter()
581 .find(|b| b.tags.contains(&"isWall".to_string()))
582 .unwrap();
583 assert!(wall_body.is_static);
584 assert_eq!(wall_body.mass, 0.0);
585 }
586
587 #[test]
588 fn weave_render_config_preserved() {
589 let scene = test_scene();
590 let config = RenderConfig {
591 bloom_enabled: false,
592 ssao_enabled: true,
593 ..Default::default()
594 };
595 let woven = weave(scene, config).unwrap();
596 assert!(!woven.render_config.bloom_enabled);
597 assert!(woven.render_config.ssao_enabled);
598 }
599
600 #[test]
601 fn weave_empty_scene_warns() {
602 let scene = GameObjectScene::new("empty".into());
603 let woven = weave(scene, RenderConfig::default()).unwrap();
604 assert!(woven.warnings.iter().any(|w| w.contains("empty_scene")));
605 }
606
607 #[test]
608 fn bootstrap_stages_ordered() {
609 assert_eq!(BootstrapStage::ALL.len(), 5);
610 assert_eq!(BootstrapStage::ALL[0], BootstrapStage::Validate);
611 assert_eq!(BootstrapStage::ALL[4], BootstrapStage::Manifest);
612 }
613
614 #[test]
615 fn frame_stages_ordered() {
616 assert_eq!(FrameStage::ALL.len(), 7);
617 assert_eq!(FrameStage::ALL[0], FrameStage::Input);
618 assert_eq!(FrameStage::ALL[6], FrameStage::Cleanup);
619 }
620
621 #[test]
622 fn weave_rejects_inf_transform() {
623 let mut scene = GameObjectScene::new("bad".into());
624 let id = scene.spawn("InfObj".into()).unwrap();
625 if let Some(obj) = scene.find_mut(id) {
626 obj.transform.position = [0.0, f32::INFINITY, 0.0];
627 }
628 let result = weave(scene, RenderConfig::default());
629 assert!(result.is_err());
630 }
631
632 #[test]
633 fn weave_dynamic_body_for_destructible() {
634 let mut scene = GameObjectScene::new("dyn".into());
635 let id = scene
636 .spawn_primitive("Crate".into(), crate::game_object::PrimitiveKind::Cube)
637 .unwrap();
638 if let Some(obj) = scene.find_mut(id) {
639 obj.property_tags.push("isDestructible".into());
640 }
641 let woven = weave(scene, RenderConfig::default()).unwrap();
642 assert_eq!(woven.bodies.len(), 1);
643 let body = &woven.bodies[0];
644 assert!(!body.is_static);
645 assert!(body.mass > 0.0);
646 }
647
648 #[test]
649 fn weave_extracts_point_light_properties() {
650 let mut scene = GameObjectScene::new("lights".into());
651 let id = scene.spawn("PointLight".into()).unwrap();
652 if let Some(obj) = scene.find_mut(id) {
653 obj.transform.position = [5.0, 10.0, 3.0];
654 let mut slot = ComponentSlot::new(ComponentKind::Light);
655 let _ = slot.set_property("type".into(), serde_json::Value::from("point"));
656 let _ = slot.set_property("intensity".into(), serde_json::Value::from(2.5));
657 let _ = slot.set_property("range".into(), serde_json::Value::from(15.0));
658 let _ = obj.add_component(slot);
659 }
660 let woven = weave(scene, RenderConfig::default()).unwrap();
661 assert_eq!(woven.lights.len(), 1);
662 }
663
664 #[test]
665 fn weave_multiple_lights() {
666 let mut scene = GameObjectScene::new("multi_light".into());
667 for i in 0..5 {
668 let id = scene.spawn(format!("Light{i}")).unwrap();
669 if let Some(obj) = scene.find_mut(id) {
670 obj.transform.position = [i as f32 * 3.0, 5.0, 0.0];
671 let _ = obj.add_component(ComponentSlot::new(ComponentKind::Light));
672 }
673 }
674 let woven = weave(scene, RenderConfig::default()).unwrap();
675 assert_eq!(woven.lights.len(), 5);
676 assert_eq!(woven.stats.light_objects, 5);
677 }
678
679 #[test]
680 fn weave_emitter_from_particle_component() {
681 let mut scene = GameObjectScene::new("particles".into());
682 let id = scene.spawn("Sparks".into()).unwrap();
683 if let Some(obj) = scene.find_mut(id) {
684 let _ = obj.add_component(ComponentSlot::new(ComponentKind::Particle));
685 }
686 let woven = weave(scene, RenderConfig::default()).unwrap();
687 assert_eq!(woven.emitters.len(), 1);
688 assert!(matches!(woven.emitters[0].kind, EmitterKind::Particle));
689 }
690
691 #[test]
692 fn weave_large_scene_performance() {
693 let mut scene = GameObjectScene::new("large".into());
694 for i in 0..500 {
695 let id = scene
696 .spawn_primitive(format!("Obj{i}"), crate::game_object::PrimitiveKind::Cube)
697 .unwrap();
698 if let Some(obj) = scene.find_mut(id) {
699 obj.transform.position = [i as f32 * 0.5, 0.0, 0.0];
700 }
701 }
702 let start = std::time::Instant::now();
703 let woven = weave(scene, RenderConfig::default()).unwrap();
704 let elapsed = start.elapsed();
705 assert_eq!(woven.stats.total_objects, 500);
706 assert!(
707 elapsed.as_millis() < 100,
708 "weave should complete under 100ms for 500 objects"
709 );
710 }
711
712 #[test]
713 fn weave_tagged_object_map() {
714 let scene = test_scene();
715 let woven = weave(scene, RenderConfig::default()).unwrap();
716 assert!(woven.tagged_objects.len() >= 2);
718 for (_, tags) in &woven.tagged_objects {
719 assert!(!tags.is_empty());
720 }
721 }
722
723 #[test]
724 fn weave_warnings_are_non_fatal() {
725 let mut scene = GameObjectScene::new("warn".into());
726 let id = scene.spawn("ZeroScale".into()).unwrap();
727 if let Some(obj) = scene.find_mut(id) {
728 obj.transform.scale = [0.0, 0.0, 0.0];
729 }
730 let woven = weave(scene, RenderConfig::default()).unwrap();
732 assert!(!woven.warnings.is_empty());
733 }
734
735 #[test]
736 fn woven_stats_dreamfab_count() {
737 let mut scene = GameObjectScene::new("fabs".into());
738 let id = scene.spawn("HybridFab".into()).unwrap();
740 if let Some(obj) = scene.find_mut(id) {
741 let _ = obj.add_component(ComponentSlot::new(ComponentKind::DreamMatter));
742 }
744 let woven = weave(scene, RenderConfig::default()).unwrap();
745 assert_eq!(woven.stats.dreamfab_count, 1);
746 assert_eq!(woven.stats.dreamlet_count, 0);
747 }
748
749 #[test]
750 fn render_config_default_has_bloom() {
751 let rc = RenderConfig::default();
752 assert!(rc.bloom_enabled);
753 assert!(rc.tonemap_enabled);
754 }
755}