use crate::game_object::{ComponentKind, GameObjectScene, MeshBinding};
use crate::lighting::{DirectionalLightDesc, PointLightDesc};
#[derive(Debug, Clone)]
pub struct RenderConfig {
pub bloom_enabled: bool,
pub tonemap_enabled: bool,
pub ssao_enabled: bool,
pub ssr_enabled: bool,
pub dof_enabled: bool,
pub ssgi_enabled: bool,
pub taa_enabled: bool,
pub scene_dream_mode: String,
pub topology_layer: u8,
}
impl Default for RenderConfig {
fn default() -> Self {
Self {
bloom_enabled: true,
tonemap_enabled: true,
ssao_enabled: true,
ssr_enabled: false,
dof_enabled: false,
ssgi_enabled: false,
taa_enabled: true,
scene_dream_mode: "PbrDefault".into(),
topology_layer: 6,
}
}
}
#[derive(Debug, Clone)]
pub enum ExtractedLight {
Directional(DirectionalLightDesc),
Point(PointLightDesc),
}
#[derive(Debug, Clone)]
pub struct ExtractedBody {
pub object_id: u64,
pub position: [f32; 3],
pub shape: BodyShape,
pub mass: f32,
pub restitution: f32,
pub friction: f32,
pub is_static: bool,
pub tags: Vec<String>,
}
#[derive(Debug, Clone)]
pub enum BodyShape {
Sphere { radius: f32 },
Box { half_extents: [f32; 3] },
Capsule { radius: f32, half_height: f32 },
}
#[derive(Debug, Clone)]
pub struct ExtractedEmitter {
pub object_id: u64,
pub position: [f32; 3],
pub kind: EmitterKind,
pub tags: Vec<String>,
}
#[derive(Debug, Clone)]
pub enum EmitterKind {
Particle,
DreamMatter,
}
#[derive(Debug)]
pub struct WovenScene {
pub scene: GameObjectScene,
pub lights: Vec<ExtractedLight>,
pub bodies: Vec<ExtractedBody>,
pub emitters: Vec<ExtractedEmitter>,
pub render_config: RenderConfig,
pub tagged_objects: Vec<(u64, Vec<String>)>,
pub warnings: Vec<String>,
pub stats: WovenStats,
}
#[derive(Debug, Clone, Default)]
pub struct WovenStats {
pub total_objects: u32,
pub mesh_objects: u32,
pub light_objects: u32,
pub physics_bodies: u32,
pub emitter_count: u32,
pub tagged_objects: u32,
pub dreamlet_count: u32,
pub dreamfab_count: u32,
}
pub type LoomResult = Result<WovenScene, Vec<String>>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BootstrapStage {
Validate,
Extract,
Configure,
Sort,
Manifest,
}
impl BootstrapStage {
pub const ALL: &'static [BootstrapStage] = &[
BootstrapStage::Validate,
BootstrapStage::Extract,
BootstrapStage::Configure,
BootstrapStage::Sort,
BootstrapStage::Manifest,
];
pub fn label(self) -> &'static str {
match self {
Self::Validate => "Validate",
Self::Extract => "Extract",
Self::Configure => "Configure",
Self::Sort => "Sort",
Self::Manifest => "Manifest",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FrameStage {
Input,
Simulation,
Physics,
Animation,
RenderPrep,
Presentation,
Cleanup,
}
impl FrameStage {
pub const ALL: &'static [FrameStage] = &[
FrameStage::Input,
FrameStage::Simulation,
FrameStage::Physics,
FrameStage::Animation,
FrameStage::RenderPrep,
FrameStage::Presentation,
FrameStage::Cleanup,
];
pub fn label(self) -> &'static str {
match self {
Self::Input => "Input",
Self::Simulation => "Simulation",
Self::Physics => "Physics",
Self::Animation => "Animation",
Self::RenderPrep => "RenderPrep",
Self::Presentation => "Presentation",
Self::Cleanup => "Cleanup",
}
}
}
pub fn weave(scene: GameObjectScene, render_config: RenderConfig) -> LoomResult {
let mut errors = Vec::new();
let mut warnings = Vec::new();
if scene.objects.is_empty() {
warnings.push("loom:empty_scene — no objects to weave".into());
}
let mut seen_ids = std::collections::HashSet::new();
for (i, obj) in scene.objects.iter().enumerate() {
for v in obj
.transform
.position
.iter()
.chain(obj.transform.scale.iter())
.chain(obj.transform.rotation.iter())
{
if !v.is_finite() {
errors.push(format!(
"loom:nan_transform — object '{}' (index {i}) has NaN/Inf",
obj.name
));
break;
}
}
if obj.transform.scale.iter().any(|s| *s == 0.0) {
warnings.push(format!("loom:zero_scale — object '{}' has zero scale axis", obj.name));
}
if !seen_ids.insert(obj.id) {
errors.push(format!(
"loom:duplicate_id — object '{}' has duplicate id {}",
obj.name, obj.id
));
}
if obj.name.is_empty() {
warnings.push(format!("loom:empty_name — object index {i} has empty name"));
}
if let Some(parent_id) = obj.parent_id {
if parent_id == obj.id {
errors.push(format!("loom:self_parent — object '{}' is its own parent", obj.name));
}
}
let mut comp_kinds = std::collections::HashSet::new();
for comp in &obj.components {
if !comp_kinds.insert(std::mem::discriminant(&comp.kind)) {
warnings.push(format!(
"loom:duplicate_component — object '{}' has duplicate {:?} component",
obj.name, comp.kind
));
}
}
}
let all_ids: std::collections::HashSet<u64> = scene.objects.iter().map(|o| o.id).collect();
for obj in &scene.objects {
if let Some(parent_id) = obj.parent_id {
if !all_ids.contains(&parent_id) {
errors.push(format!(
"loom:orphan_parent — object '{}' references non-existent parent {}",
obj.name, parent_id
));
}
}
}
for obj in &scene.objects {
let mut current = obj.parent_id;
let mut depth = 0u32;
while let Some(pid) = current {
depth += 1;
if depth > 64 {
errors.push(format!(
"loom:hierarchy_cycle — object '{}' has cycle or depth > 64",
obj.name
));
break;
}
if pid == obj.id {
errors.push(format!(
"loom:hierarchy_cycle — object '{}' is in a parent cycle",
obj.name
));
break;
}
current = scene.objects.iter().find(|o| o.id == pid).and_then(|o| o.parent_id);
}
}
if !errors.is_empty() {
return Err(errors);
}
let mut lights = Vec::new();
for obj in &scene.objects {
if !obj.has_component(ComponentKind::Light) {
continue;
}
let pos = obj.transform.position;
let len = (pos[0] * pos[0] + pos[1] * pos[1] + pos[2] * pos[2]).sqrt();
let light_props = obj.get_component(ComponentKind::Light).map(|c| &c.properties);
let intensity = light_props
.and_then(|p| p.get("intensity_lux"))
.and_then(|v| v.as_f64())
.map(|v| v as f32)
.unwrap_or(100_000.0);
let color = light_props
.and_then(|p| p.get("color"))
.and_then(|v| v.as_str())
.and_then(parse_color_3)
.unwrap_or([1.0, 0.95, 0.9]);
let range = light_props
.and_then(|p| p.get("range"))
.and_then(|v| v.as_f64())
.map(|v| v as f32);
if let Some(range) = range {
lights.push(ExtractedLight::Point(PointLightDesc {
position: pos,
color,
intensity_lumens: intensity,
range,
}));
} else if len > 0.001 {
lights.push(ExtractedLight::Directional(DirectionalLightDesc {
direction: [-pos[0] / len, -pos[1] / len, -pos[2] / len],
intensity_lux: intensity,
color,
}));
}
}
let mut bodies = Vec::new();
for obj in &scene.objects {
let has_physics_tags = obj.property_tags.iter().any(|t| {
t == "isDestructible"
|| t == "isFlammable"
|| t == "isExplosive"
|| t == "isPickupable"
|| t == "isHazard"
|| t == "isWall"
});
if has_physics_tags || obj.has_component(ComponentKind::Physics) {
let scale = obj.transform.scale;
let shape = BodyShape::Box {
half_extents: [scale[0] * 0.5, scale[1] * 0.5, scale[2] * 0.5],
};
let mass = if obj.property_tags.iter().any(|t| t == "isWall" || t == "isWalkable") {
0.0 } else {
1.0
};
bodies.push(ExtractedBody {
object_id: obj.id,
position: obj.transform.position,
shape,
mass,
restitution: 0.3,
friction: 0.5,
is_static: mass == 0.0,
tags: obj.property_tags.clone(),
});
}
}
let mut emitters = Vec::new();
for obj in &scene.objects {
if obj.has_component(ComponentKind::Particle) {
emitters.push(ExtractedEmitter {
object_id: obj.id,
position: obj.transform.position,
kind: EmitterKind::Particle,
tags: obj.property_tags.clone(),
});
}
if obj.has_component(ComponentKind::DreamMatter) {
emitters.push(ExtractedEmitter {
object_id: obj.id,
position: obj.transform.position,
kind: EmitterKind::DreamMatter,
tags: obj.property_tags.clone(),
});
}
}
let tagged_objects: Vec<(u64, Vec<String>)> = scene
.objects
.iter()
.filter(|obj| !obj.property_tags.is_empty())
.map(|obj| (obj.id, obj.property_tags.clone()))
.collect();
let mut stats = WovenStats::default();
stats.total_objects = scene.objects.len() as u32;
for obj in &scene.objects {
match &obj.mesh {
MeshBinding::Primitive { .. } | MeshBinding::Custom { .. } => stats.mesh_objects += 1,
MeshBinding::None => {}
}
if obj.has_component(ComponentKind::Light) {
stats.light_objects += 1;
}
if obj.property_tags.contains(&"isDreammatter".to_string()) {
stats.dreamlet_count += 1;
}
if obj.has_component(ComponentKind::DreamMatter) && !obj.property_tags.contains(&"isDreammatter".to_string()) {
stats.dreamfab_count += 1;
}
}
stats.physics_bodies = bodies.len() as u32;
stats.emitter_count = emitters.len() as u32;
stats.tagged_objects = tagged_objects.len() as u32;
Ok(WovenScene {
scene,
lights,
bodies,
emitters,
render_config,
tagged_objects,
warnings,
stats,
})
}
fn parse_color_3(s: &str) -> Option<[f32; 3]> {
let s = s.trim().trim_start_matches('[').trim_end_matches(']');
let parts: Vec<&str> = s.split(',').collect();
if parts.len() >= 3 {
let r = parts[0].trim().parse::<f32>().ok()?;
let g = parts[1].trim().parse::<f32>().ok()?;
let b = parts[2].trim().parse::<f32>().ok()?;
Some([r, g, b])
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::game_object::ComponentSlot;
fn test_scene() -> GameObjectScene {
let mut scene = GameObjectScene::new("test".into());
let id = scene
.spawn_primitive("Wall".into(), crate::game_object::PrimitiveKind::Cube)
.unwrap();
if let Some(obj) = scene.find_mut(id) {
obj.property_tags.push("isWall".into());
obj.property_tags.push("isDestructible".into());
}
let light_id = scene.spawn("Sun".into()).unwrap();
if let Some(obj) = scene.find_mut(light_id) {
obj.transform.position = [3.0, 6.0, 3.0];
let _ = obj.add_component(ComponentSlot::new(ComponentKind::Light));
}
let dm_id = scene.spawn("Fire Effect".into()).unwrap();
if let Some(obj) = scene.find_mut(dm_id) {
obj.property_tags.push("isDreammatter".into());
obj.property_tags.push("isFlammable".into());
let _ = obj.add_component(ComponentSlot::new(ComponentKind::DreamMatter));
}
scene
}
#[test]
fn weave_extracts_all() {
let scene = test_scene();
let woven = weave(scene, RenderConfig::default()).unwrap();
assert_eq!(woven.stats.total_objects, 3);
assert_eq!(woven.stats.light_objects, 1);
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);
}
#[test]
fn weave_rejects_nan() {
let mut scene = GameObjectScene::new("bad".into());
let id = scene.spawn("NaN Object".into()).unwrap();
if let Some(obj) = scene.find_mut(id) {
obj.transform.position = [f32::NAN, 0.0, 0.0];
}
let result = weave(scene, RenderConfig::default());
assert!(result.is_err());
assert!(result.unwrap_err()[0].contains("nan_transform"));
}
#[test]
fn weave_static_body_for_wall() {
let scene = test_scene();
let woven = weave(scene, RenderConfig::default()).unwrap();
let wall_body = woven
.bodies
.iter()
.find(|b| b.tags.contains(&"isWall".to_string()))
.unwrap();
assert!(wall_body.is_static);
assert_eq!(wall_body.mass, 0.0);
}
#[test]
fn weave_render_config_preserved() {
let scene = test_scene();
let config = RenderConfig {
bloom_enabled: false,
ssao_enabled: true,
..Default::default()
};
let woven = weave(scene, config).unwrap();
assert!(!woven.render_config.bloom_enabled);
assert!(woven.render_config.ssao_enabled);
}
#[test]
fn weave_empty_scene_warns() {
let scene = GameObjectScene::new("empty".into());
let woven = weave(scene, RenderConfig::default()).unwrap();
assert!(woven.warnings.iter().any(|w| w.contains("empty_scene")));
}
#[test]
fn bootstrap_stages_ordered() {
assert_eq!(BootstrapStage::ALL.len(), 5);
assert_eq!(BootstrapStage::ALL[0], BootstrapStage::Validate);
assert_eq!(BootstrapStage::ALL[4], BootstrapStage::Manifest);
}
#[test]
fn frame_stages_ordered() {
assert_eq!(FrameStage::ALL.len(), 7);
assert_eq!(FrameStage::ALL[0], FrameStage::Input);
assert_eq!(FrameStage::ALL[6], FrameStage::Cleanup);
}
#[test]
fn weave_rejects_inf_transform() {
let mut scene = GameObjectScene::new("bad".into());
let id = scene.spawn("InfObj".into()).unwrap();
if let Some(obj) = scene.find_mut(id) {
obj.transform.position = [0.0, f32::INFINITY, 0.0];
}
let result = weave(scene, RenderConfig::default());
assert!(result.is_err());
}
#[test]
fn weave_dynamic_body_for_destructible() {
let mut scene = GameObjectScene::new("dyn".into());
let id = scene
.spawn_primitive("Crate".into(), crate::game_object::PrimitiveKind::Cube)
.unwrap();
if let Some(obj) = scene.find_mut(id) {
obj.property_tags.push("isDestructible".into());
}
let woven = weave(scene, RenderConfig::default()).unwrap();
assert_eq!(woven.bodies.len(), 1);
let body = &woven.bodies[0];
assert!(!body.is_static);
assert!(body.mass > 0.0);
}
#[test]
fn weave_extracts_point_light_properties() {
let mut scene = GameObjectScene::new("lights".into());
let id = scene.spawn("PointLight".into()).unwrap();
if let Some(obj) = scene.find_mut(id) {
obj.transform.position = [5.0, 10.0, 3.0];
let mut slot = ComponentSlot::new(ComponentKind::Light);
let _ = slot.set_property("type".into(), serde_json::Value::from("point"));
let _ = slot.set_property("intensity".into(), serde_json::Value::from(2.5));
let _ = slot.set_property("range".into(), serde_json::Value::from(15.0));
let _ = obj.add_component(slot);
}
let woven = weave(scene, RenderConfig::default()).unwrap();
assert_eq!(woven.lights.len(), 1);
}
#[test]
fn weave_multiple_lights() {
let mut scene = GameObjectScene::new("multi_light".into());
for i in 0..5 {
let id = scene.spawn(format!("Light{i}")).unwrap();
if let Some(obj) = scene.find_mut(id) {
obj.transform.position = [i as f32 * 3.0, 5.0, 0.0];
let _ = obj.add_component(ComponentSlot::new(ComponentKind::Light));
}
}
let woven = weave(scene, RenderConfig::default()).unwrap();
assert_eq!(woven.lights.len(), 5);
assert_eq!(woven.stats.light_objects, 5);
}
#[test]
fn weave_emitter_from_particle_component() {
let mut scene = GameObjectScene::new("particles".into());
let id = scene.spawn("Sparks".into()).unwrap();
if let Some(obj) = scene.find_mut(id) {
let _ = obj.add_component(ComponentSlot::new(ComponentKind::Particle));
}
let woven = weave(scene, RenderConfig::default()).unwrap();
assert_eq!(woven.emitters.len(), 1);
assert!(matches!(woven.emitters[0].kind, EmitterKind::Particle));
}
#[test]
fn weave_large_scene_performance() {
let mut scene = GameObjectScene::new("large".into());
for i in 0..500 {
let id = scene
.spawn_primitive(format!("Obj{i}"), crate::game_object::PrimitiveKind::Cube)
.unwrap();
if let Some(obj) = scene.find_mut(id) {
obj.transform.position = [i as f32 * 0.5, 0.0, 0.0];
}
}
let start = std::time::Instant::now();
let woven = weave(scene, RenderConfig::default()).unwrap();
let elapsed = start.elapsed();
assert_eq!(woven.stats.total_objects, 500);
assert!(
elapsed.as_millis() < 100,
"weave should complete under 100ms for 500 objects"
);
}
#[test]
fn weave_tagged_object_map() {
let scene = test_scene();
let woven = weave(scene, RenderConfig::default()).unwrap();
assert!(woven.tagged_objects.len() >= 2);
for (_, tags) in &woven.tagged_objects {
assert!(!tags.is_empty());
}
}
#[test]
fn weave_warnings_are_non_fatal() {
let mut scene = GameObjectScene::new("warn".into());
let id = scene.spawn("ZeroScale".into()).unwrap();
if let Some(obj) = scene.find_mut(id) {
obj.transform.scale = [0.0, 0.0, 0.0];
}
let woven = weave(scene, RenderConfig::default()).unwrap();
assert!(!woven.warnings.is_empty());
}
#[test]
fn woven_stats_dreamfab_count() {
let mut scene = GameObjectScene::new("fabs".into());
let id = scene.spawn("HybridFab".into()).unwrap();
if let Some(obj) = scene.find_mut(id) {
let _ = obj.add_component(ComponentSlot::new(ComponentKind::DreamMatter));
}
let woven = weave(scene, RenderConfig::default()).unwrap();
assert_eq!(woven.stats.dreamfab_count, 1);
assert_eq!(woven.stats.dreamlet_count, 0);
}
#[test]
fn render_config_default_has_bloom() {
let rc = RenderConfig::default();
assert!(rc.bloom_enabled);
assert!(rc.tonemap_enabled);
}
}