use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::path::Path;
use super::environment_config::{ImageObject, SceneObject};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PresetConfig {
pub id: String,
pub version: String,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub objects: Vec<SceneObject>,
}
impl PresetConfig {
pub fn load(path: &Path) -> Result<Self> {
super::load_json(path, "preset config")
}
pub fn image_objects(&self) -> impl Iterator<Item = &ImageObject> {
self.objects.iter().filter_map(|obj| match obj {
SceneObject::Image(image) => Some(image),
SceneObject::Cube(_) => None,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sim::environment_config::SceneObject;
use std::path::PathBuf;
fn repo_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(Path::parent)
.expect("repo root")
.to_path_buf()
}
#[test]
fn parses_a_minimal_preset() {
let json = r#"{
"id": "cats",
"version": "1.0.0",
"description": "Cat detection targets",
"objects": []
}"#;
let config: PresetConfig = serde_json::from_str(json).unwrap();
assert_eq!(config.id, "cats");
assert!(config.objects.is_empty());
}
#[test]
fn parses_a_preset_with_image_objects() {
let json = r#"{
"id": "cats",
"version": "1.0.0",
"objects": [
{
"type": "image",
"source": "configs/simulation/images/aiko.jpg",
"position": [-3.0, 0.5, 0.0],
"rotation": [0, 90, 0]
}
]
}"#;
let config: PresetConfig = serde_json::from_str(json).unwrap();
let images: Vec<&ImageObject> = config.image_objects().collect();
assert_eq!(images.len(), 1);
assert_eq!(images[0].source, "configs/simulation/images/aiko.jpg");
}
#[test]
fn rejects_malformed_object_entry() {
let json = r#"{
"id": "broken",
"version": "1.0.0",
"objects": [{"type": "image"}]
}"#;
let err = serde_json::from_str::<PresetConfig>(json).unwrap_err();
assert!(err.to_string().contains("source") || err.to_string().contains("position"));
}
#[test]
fn loads_the_bundled_cats_preset() {
let root = repo_root();
let path = root.join(
"packages/mecha10-templates/templates/configs/simulation/environments/basic_arena/presets/cats/preset.json",
);
let config = PresetConfig::load(&path).expect("basic_arena's cats preset.json should parse");
assert_eq!(config.id, "cats");
let images: Vec<&ImageObject> = config.image_objects().collect();
assert!(
images.iter().any(|i| i.source.ends_with("aiko.jpg")),
"cats preset should include aiko.jpg"
);
assert!(
images.iter().any(|i| i.source.ends_with("phoebe.jpg")),
"cats preset should include phoebe.jpg"
);
}
#[test]
fn loads_the_bundled_default_preset() {
let root = repo_root();
let path = root.join(
"packages/mecha10-templates/templates/configs/simulation/environments/basic_arena/presets/default/preset.json",
);
let config = PresetConfig::load(&path).expect("basic_arena's default preset.json should parse");
assert_eq!(config.id, "default");
assert!(
!config.objects.is_empty(),
"default preset should carry the generic/neutral image set"
);
assert!(
config
.image_objects()
.all(|i| !i.source.ends_with("aiko.jpg") && !i.source.ends_with("phoebe.jpg")),
"default preset should be the generic/neutral set, not the cat detection targets"
);
}
#[test]
fn loads_the_bundled_objects_patrol_preset_with_motion() {
let root = repo_root();
let path = root.join(
"packages/mecha10-templates/templates/configs/simulation/environments/basic_arena/presets/objects_patrol/preset.json",
);
let config = PresetConfig::load(&path).expect("basic_arena's objects_patrol preset.json should parse");
assert!(!config.objects.is_empty());
for obj in &config.objects {
match obj {
SceneObject::Image(image) => {
assert!(
image.motion.is_some(),
"every object in objects_patrol should declare a motion block, {} did not",
image.source
);
}
SceneObject::Cube(_) => panic!("objects_patrol should only contain image objects"),
}
}
}
}