mecha10-cli 0.1.47

Mecha10 CLI tool
Documentation
//! Simulation generator - main orchestration

mod sensors;

use anyhow::{Context, Result};
use std::fs;
use std::path::Path;

use super::profile::RobotProfile;
use super::selector::EnvironmentSelector;

pub struct SimulationGenerator {
    selector: EnvironmentSelector,
}

impl SimulationGenerator {
    /// Create new generator with default catalog
    pub fn new() -> Result<Self> {
        let selector = EnvironmentSelector::new()?;
        Ok(Self { selector })
    }

    /// Generate simulation with automatic environment selection
    pub fn generate_with_auto_selection(
        &self,
        config_path: &Path,
        output_path: &Path,
        max_envs: usize,
        min_score: i32,
    ) -> Result<()> {
        println!("🤖 Mecha10 Simulation Generator\n");

        // Load robot profile
        println!("📋 Analyzing robot configuration...");
        let profile = RobotProfile::from_config_file(config_path)?;

        println!("  Platform: {}", profile.platform);
        println!("  Sensors: {}", profile.sensors.join(", "));
        println!(
            "  Task nodes: {}",
            profile
                .task_nodes
                .iter()
                .map(|n| n.name.as_str())
                .collect::<Vec<_>>()
                .join(", ")
        );
        println!();

        // Select environments
        println!("🎯 Auto-selecting environments...");
        let matches = self.selector.select_environments(&profile, max_envs)?;

        let filtered_matches: Vec<_> = matches.into_iter().filter(|m| m.score >= min_score).collect();

        if filtered_matches.is_empty() {
            println!("❌ No matching environments found (min score: {})", min_score);
            println!("\nTry:");
            println!("  - Lowering --min-score");
            println!("  - Adding more sensors");
            println!("  - Checking required_sensors in catalog");
            return Ok(());
        }

        for env_match in &filtered_matches {
            println!("{} (score: {})", env_match.environment.name, env_match.score);
        }
        println!();

        // Generate scenes
        println!("🔨 Generating robot scenes...");
        fs::create_dir_all(output_path)?;

        for env_match in &filtered_matches {
            let env_output = output_path.join(&env_match.environment.id);
            self.generate_scene_for_environment(config_path, &env_output, &env_match.environment)?;
            println!("{}/scene.tscn", env_match.environment.id);
        }

        println!();
        println!("✅ Generation complete!");
        println!("\nOutput: {}", output_path.display());
        println!("\nNext steps:");
        println!(
            "  1. Open in Godot: godot {}/{}/scene.tscn",
            output_path.display(),
            filtered_matches[0].environment.id
        );
        println!("  2. Test manually with arrow keys");
        println!(
            "  3. Train with RL: mecha10 rl train --env {}",
            filtered_matches[0].environment.id
        );

        Ok(())
    }

    /// Generate simulation with specific environment
    pub fn generate_with_environment(&self, config_path: &Path, output_path: &Path, env_id: &str) -> Result<()> {
        println!("🤖 Mecha10 Simulation Generator\n");

        // Find environment
        let env = self
            .selector
            .catalog()
            .find_by_id(env_id)
            .with_context(|| format!("Environment not found: {}", env_id))?;

        println!("📋 Using environment: {}", env.name);
        println!();

        // Generate scene
        println!("🔨 Generating robot scene...");
        fs::create_dir_all(output_path)?;

        self.generate_scene_for_environment(config_path, output_path, env)?;
        println!("  ✓ scene.tscn");

        println!();
        println!("✅ Generation complete!");
        println!("\nOutput: {}/scene.tscn", output_path.display());

        Ok(())
    }

    /// Generate scene for specific environment
    fn generate_scene_for_environment(
        &self,
        config_path: &Path,
        output_path: &Path,
        environment: &super::catalog::Environment,
    ) -> Result<()> {
        fs::create_dir_all(output_path)?;

        // Load robot profile
        let profile = RobotProfile::from_config_file(config_path)?;

        // Create new scene
        let mut scene = super::scene::GodotScene::new();
        scene.uid = Some(format!("uid://generated_robot_{}", environment.id));

        // 1. Add robot template as base
        let template_path = self.get_robot_template_path(&profile.platform)?;
        let robot_id = scene.add_ext_resource("PackedScene", &template_path);

        // Create root node (instance of robot template)
        let robot_node = super::scene::SceneNode::instance("Robot", &robot_id);
        scene.add_node(robot_node);

        // 2. Add sensors with detailed configurations (with per-sensor error recovery)
        let mut sensor_errors = Vec::new();
        let mut sensors_added = 0;

        for sensor_config in &profile.sensor_configs {
            let result = match sensor_config.sensor_type.as_str() {
                "lidar" => sensors::add_lidar_sensor_with_config(&mut scene, sensor_config),
                "camera" => sensors::add_camera_sensor_with_config(&mut scene, sensor_config),
                "imu" => sensors::add_imu_sensor_with_config(&mut scene, sensor_config),
                "motor" => continue, // Motor is part of robot template, skip
                _ => {
                    println!(
                        "⚠️  Warning: Unknown sensor type '{}' for sensor '{}'",
                        sensor_config.sensor_type, sensor_config.name
                    );
                    continue;
                }
            };

            match result {
                Ok(_) => {
                    sensors_added += 1;
                }
                Err(e) => {
                    let error_msg = format!(
                        "Failed to add {} sensor '{}': {}",
                        sensor_config.sensor_type, sensor_config.name, e
                    );
                    println!("⚠️  {}", error_msg);
                    sensor_errors.push(error_msg);
                }
            }
        }

        // Report summary
        if !sensor_errors.is_empty() {
            println!("\n⚠️  Sensor Errors Summary:");
            for error in &sensor_errors {
                println!("   - {}", error);
            }
            println!("   Successfully added: {} sensors", sensors_added);
            println!("   Failed: {} sensors", sensor_errors.len());
        }

        // Only fail if ALL sensors failed
        if sensors_added == 0 && !profile.sensor_configs.is_empty() {
            anyhow::bail!(
                "Failed to add any sensors. {} error(s) occurred. Scene generation aborted.",
                sensor_errors.len()
            );
        }

        // 3. Save generated scene
        scene.save(&output_path.join("scene.tscn"))?;

        Ok(())
    }

    /// Get robot template path based on platform
    fn get_robot_template_path(&self, platform: &str) -> Result<String> {
        let template_name = match platform {
            "rover" | "differential-drive" => "rover_base_rl",
            "humanoid" => "humanoid_base_rl",
            "arm" => "arm_base_rl",
            _ => "rover_base_rl", // Default
        };

        Ok(format!(
            "res://packages/godot-components/robot_templates/{}.tscn",
            template_name
        ))
    }
}