use std::path::Path;
use dreamwell_engine::dream_file::{self, DreamSceneV1};
use dreamwell_engine::game_object::{
GameObjectScene, GameObject, MeshBinding, PrimitiveKind, Transform,
};
pub struct LoadedScene {
pub scene: DreamSceneV1,
pub game_objects: GameObjectScene,
pub warnings: Vec<String>,
}
fn parse_primitive(name: &str) -> Option<PrimitiveKind> {
match name {
"Cube" => Some(PrimitiveKind::Cube),
"Sphere" => Some(PrimitiveKind::Sphere),
"Cylinder" => Some(PrimitiveKind::Cylinder),
"Cone" => Some(PrimitiveKind::Cone),
"Torus" => Some(PrimitiveKind::Torus),
"Capsule" => Some(PrimitiveKind::Capsule),
"Plane" => Some(PrimitiveKind::Plane),
"Pyramid" => Some(PrimitiveKind::Pyramid),
"Wedge" => Some(PrimitiveKind::Wedge),
_ => None,
}
}
fn euler_degrees_to_quat(euler: [f32; 3]) -> [f32; 4] {
let (rx, ry, rz) = (
euler[0].to_radians() * 0.5,
euler[1].to_radians() * 0.5,
euler[2].to_radians() * 0.5,
);
let (sx, cx) = rx.sin_cos();
let (sy, cy) = ry.sin_cos();
let (sz, cz) = rz.sin_cos();
[
sx * cy * cz - cx * sy * sz,
cx * sy * cz + sx * cy * sz,
cx * cy * sz - sx * sy * cz,
cx * cy * cz + sx * sy * sz,
]
}
fn build_game_object_scene(scene: &DreamSceneV1) -> GameObjectScene {
let mut gos = GameObjectScene::new(scene.name.clone());
for obj in &scene.objects {
let color = obj.material.as_ref()
.map(|m| m.base_color)
.unwrap_or([0.7, 0.7, 0.7, 1.0]);
let mesh = if let Some(ref prim_name) = obj.primitive {
if let Some(kind) = parse_primitive(prim_name) {
MeshBinding::Primitive { kind, color }
} else {
log::warn!("scene_loader: unknown primitive '{}' for object '{}'", prim_name, obj.id);
continue;
}
} else if let Some(ref asset_key) = obj.asset_ref {
MeshBinding::Custom { asset_key: asset_key.clone() }
} else {
MeshBinding::None
};
let go = GameObject {
id: gos.objects.len() as u64,
name: obj.name.clone(),
transform: Transform {
position: obj.position,
rotation: euler_degrees_to_quat(obj.rotation),
scale: obj.scale,
},
mesh,
visible: true,
parent_id: None,
components: Vec::new(),
property_tags: Vec::new(),
topology_layer: 9,
};
gos.objects.push(go);
}
gos
}
pub fn load_scene_from_dream(dream_path: &Path) -> Result<LoadedScene, String> {
log::info!("Loading scene from {}", dream_path.display());
let scene = dream_file::read_dream_file(dream_path)?;
finalize_loaded_scene(scene)
}
pub fn load_scene_from_json(json_path: &Path) -> Result<LoadedScene, String> {
log::info!("Compiling scene from {}", json_path.display());
let json_str = std::fs::read_to_string(json_path)
.map_err(|e| format!("scene_read:{e}"))?;
let scene = dream_file::compile_scene_json(&json_str)?;
finalize_loaded_scene(scene)
}
pub fn load_scene_auto(path: &Path) -> Result<LoadedScene, String> {
match path.extension().and_then(|e| e.to_str()) {
Some("dream") => load_scene_from_dream(path),
Some("json") => load_scene_from_json(path),
_ => Err(format!("scene_loader:unsupported extension for {}", path.display())),
}
}
fn finalize_loaded_scene(scene: DreamSceneV1) -> Result<LoadedScene, String> {
let (warnings, errors) = dream_file::reality_check(&scene);
if !errors.is_empty() {
return Err(format!(
"Scene '{}' failed reality check: {}",
scene.scene_id,
errors.join("; ")
));
}
for w in &warnings {
log::warn!("scene_loader:{}: {w}", scene.scene_id);
}
log::info!(
"Scene '{}': {} objects, {} dir lights, {} point lights, {} colliders, {} POIs",
scene.scene_id,
scene.objects.len(),
scene.directional_lights.len(),
scene.point_lights.len(),
scene.colliders.len(),
scene.pois.len(),
);
let game_objects = build_game_object_scene(&scene);
Ok(LoadedScene {
scene,
game_objects,
warnings,
})
}
pub fn resolve_project_dir(args: &[String]) -> Option<std::path::PathBuf> {
if let Some(i) = args.iter().position(|a| a == "--project-dir") {
if let Some(path_str) = args.get(i + 1) {
let p = std::path::PathBuf::from(path_str);
if p.join("tapestry.json").exists() || p.join("tapestry.dream").exists() {
return Some(p);
}
log::warn!("--project-dir '{}' has no tapestry file", p.display());
}
}
let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let workspace = manifest_dir.parent()?.parent()?;
let benchmark_project = workspace.join("dreamwell-benchmark-project");
if benchmark_project.join("tapestry.json").exists() {
return Some(benchmark_project);
}
None
}
pub fn resolve_scene_path(project_dir: &Path, scene_name: &str) -> Option<std::path::PathBuf> {
let dream_path = project_dir.join("scenes").join(scene_name).join("scene.dream");
if dream_path.exists() {
return Some(dream_path);
}
let json_path = project_dir.join("scenes").join(scene_name).join("scene.json");
if json_path.exists() {
return Some(json_path);
}
None
}
pub fn list_project_scenes(project_dir: &Path) -> Vec<String> {
let scenes_dir = project_dir.join("scenes");
if !scenes_dir.is_dir() {
return Vec::new();
}
let mut scenes = Vec::new();
if let Ok(entries) = std::fs::read_dir(&scenes_dir) {
for entry in entries.flatten() {
if entry.file_type().map(|t| t.is_dir()).unwrap_or(false) {
let name = entry.file_name().to_string_lossy().to_string();
let has_scene = entry.path().join("scene.json").exists()
|| entry.path().join("scene.dream").exists();
if has_scene {
scenes.push(name);
}
}
}
}
scenes.sort();
scenes
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn euler_identity_to_quat() {
let q = euler_degrees_to_quat([0.0, 0.0, 0.0]);
assert!((q[0]).abs() < 1e-6);
assert!((q[1]).abs() < 1e-6);
assert!((q[2]).abs() < 1e-6);
assert!((q[3] - 1.0).abs() < 1e-6);
}
#[test]
fn parse_all_primitives() {
assert!(parse_primitive("Cube").is_some());
assert!(parse_primitive("Sphere").is_some());
assert!(parse_primitive("Cylinder").is_some());
assert!(parse_primitive("Cone").is_some());
assert!(parse_primitive("Torus").is_some());
assert!(parse_primitive("Capsule").is_some());
assert!(parse_primitive("Plane").is_some());
assert!(parse_primitive("Pyramid").is_some());
assert!(parse_primitive("Wedge").is_some());
assert!(parse_primitive("Unknown").is_none());
}
#[test]
fn resolve_project_finds_benchmark() {
let project = resolve_project_dir(&[]);
if let Some(p) = &project {
assert!(p.join("tapestry.json").exists() || p.join("tapestry.dream").exists());
}
}
#[test]
fn list_scenes_from_benchmark_project() {
if let Some(project) = resolve_project_dir(&[]) {
let scenes = list_project_scenes(&project);
assert!(!scenes.is_empty(), "Expected at least 1 scene in benchmark project");
assert!(scenes.contains(&"keynote".to_string()), "Expected keynote scene");
}
}
#[test]
fn load_keynote_from_json() {
if let Some(project) = resolve_project_dir(&[]) {
let path = project.join("scenes").join("keynote").join("scene.json");
if path.exists() {
let loaded = load_scene_from_json(&path).unwrap();
assert_eq!(loaded.scene.scene_id, "keynote");
assert!(!loaded.game_objects.objects.is_empty());
assert!(loaded.warnings.is_empty() || true); }
}
}
#[test]
fn load_all_benchmark_scenes() {
if let Some(project) = resolve_project_dir(&[]) {
let scenes = list_project_scenes(&project);
for name in &scenes {
if let Some(path) = resolve_scene_path(&project, name) {
let result = load_scene_auto(&path);
assert!(result.is_ok(), "Failed to load scene '{name}': {:?}", result.err());
let loaded = result.unwrap();
assert_eq!(loaded.scene.scene_id, *name,
"scene_id mismatch for '{name}'");
}
}
}
}
#[test]
fn build_game_objects_from_keynote() {
if let Some(project) = resolve_project_dir(&[]) {
let path = project.join("scenes").join("keynote").join("scene.json");
if path.exists() {
let loaded = load_scene_from_json(&path).unwrap();
assert!(loaded.game_objects.objects.len() >= 10,
"Expected 10+ objects, got {}", loaded.game_objects.objects.len());
let ground = &loaded.game_objects.objects[0];
assert_eq!(ground.transform.scale, [80.0, 1.0, 80.0]);
match &ground.mesh {
MeshBinding::Primitive { kind, .. } => {
assert_eq!(*kind, PrimitiveKind::Plane);
}
_ => panic!("Ground should be a Plane primitive"),
}
}
}
}
#[test]
fn dream_roundtrip_preserves_scene() {
if let Some(project) = resolve_project_dir(&[]) {
let json_path = project.join("scenes").join("keynote").join("scene.json");
if !json_path.exists() { return; }
let loaded_json = load_scene_from_json(&json_path).unwrap();
let dir = std::env::temp_dir().join("dreamwell_loader_roundtrip");
std::fs::create_dir_all(&dir).unwrap();
let dream_path = dir.join("test.dream");
dream_file::write_dream_file(&dream_path, &loaded_json.scene).unwrap();
let loaded_dream = load_scene_from_dream(&dream_path).unwrap();
assert_eq!(loaded_json.scene.scene_id, loaded_dream.scene.scene_id);
assert_eq!(loaded_json.game_objects.objects.len(),
loaded_dream.game_objects.objects.len());
assert_eq!(loaded_json.scene.directional_lights.len(),
loaded_dream.scene.directional_lights.len());
std::fs::remove_dir_all(&dir).ok();
}
}
}