Skip to main content

mecha10_sim/services/
simulation.rs

1#![allow(dead_code)]
2
3//! Simulation service for scene generation and management
4//!
5//! This service provides operations for generating and managing simulations,
6//! including robot scene generation, environment selection, and validation.
7
8use crate::paths;
9use crate::sim::{EnvironmentSelector, RobotGenerator, RobotProfile};
10use anyhow::Result;
11use std::path::{Path, PathBuf};
12
13/// Simulation service for managing simulations
14///
15/// # Examples
16///
17/// ```rust,ignore
18/// use mecha10_cli::services::SimulationService;
19/// use std::path::Path;
20///
21/// # async fn example() -> anyhow::Result<()> {
22/// let service = SimulationService::new();
23///
24/// // Generate simulation
25/// service.generate(
26///     Path::new("mecha10.json"),
27///     Path::new("simulation/output"),
28///     5, // max environments
29///     0  // min score
30/// )?;
31/// # Ok(())
32/// # }
33/// ```
34pub struct SimulationService {
35    /// Base path for simulation output
36    base_path: PathBuf,
37}
38
39impl SimulationService {
40    /// Create a new simulation service with default paths
41    pub fn new() -> Self {
42        Self {
43            base_path: PathBuf::from(paths::project::SIMULATION_OUTPUT_DIR),
44        }
45    }
46
47    /// Create a simulation service with custom base path
48    ///
49    /// # Arguments
50    ///
51    /// * `base_path` - Base directory for simulation output
52    pub fn with_base_path(base_path: impl Into<PathBuf>) -> Self {
53        Self {
54            base_path: base_path.into(),
55        }
56    }
57
58    /// Generate simulation scenes from project configuration
59    ///
60    /// # Arguments
61    ///
62    /// * `config_path` - Path to mecha10.json
63    /// * `output_path` - Output directory for simulation files
64    /// * `max_envs` - Maximum number of environments to generate
65    /// * `min_score` - Minimum compatibility score (0-100)
66    pub fn generate(
67        &self,
68        config_path: &Path,
69        output_path: &Path,
70        max_envs: usize,
71        min_score: i32,
72    ) -> Result<GenerationResult> {
73        // Load robot profile
74        let profile = RobotProfile::from_config_file(config_path)?;
75
76        // Generate robot scene
77        let robot_output = output_path.join("robot.tscn");
78        let robot_generator = RobotGenerator::from_config_file(config_path)?;
79        robot_generator.generate(&robot_output)?;
80
81        // Select environments
82        let selector = EnvironmentSelector::new()?;
83        let matches = selector.select_environments(&profile, max_envs)?;
84
85        let filtered_matches: Vec<_> = matches.into_iter().filter(|m| m.score >= min_score).collect();
86
87        if filtered_matches.is_empty() {
88            return Err(anyhow::anyhow!(
89                "No matching environments found (min score: {})",
90                min_score
91            ));
92        }
93
94        // Count available environments
95        let _catalog_path = PathBuf::from(paths::framework::ROBOT_TASKS_CATALOG);
96        let base_path = PathBuf::from(paths::framework::ROBOT_TASKS_DIR);
97        let mut available_count = 0;
98
99        for env_match in &filtered_matches {
100            let env_path = base_path.join(&env_match.environment.path);
101            if env_path.exists() {
102                available_count += 1;
103            }
104        }
105
106        Ok(GenerationResult {
107            robot_scene: robot_output,
108            environments: filtered_matches.iter().map(|m| m.environment.id.clone()).collect(),
109            available_count,
110        })
111    }
112
113    /// Generate only the robot scene
114    ///
115    /// # Arguments
116    ///
117    /// * `config_path` - Path to mecha10.json
118    /// * `output_path` - Output path for robot.tscn
119    pub fn generate_robot(&self, config_path: &Path, output_path: &Path) -> Result<()> {
120        let robot_generator = RobotGenerator::from_config_file(config_path)?;
121        robot_generator.generate(output_path)
122    }
123
124    /// List available environments
125    ///
126    /// # Arguments
127    ///
128    /// * `config_path` - Path to mecha10.json
129    /// * `verbose` - Show detailed information
130    pub fn list_environments(&self, config_path: &Path, verbose: bool) -> Result<Vec<EnvironmentInfo>> {
131        let profile = RobotProfile::from_config_file(config_path)?;
132        let selector = EnvironmentSelector::new()?;
133        let matches = selector.select_environments(&profile, 100)?; // Get all
134
135        let base_path = PathBuf::from(paths::framework::ROBOT_TASKS_DIR);
136        let mut environments = Vec::new();
137
138        for env_match in matches {
139            let env_path = base_path.join(&env_match.environment.path);
140            let available = env_path.exists();
141
142            if verbose || available {
143                environments.push(EnvironmentInfo {
144                    id: env_match.environment.id.clone(),
145                    name: env_match.environment.name.clone(),
146                    description: env_match.environment.description.clone(),
147                    score: env_match.score,
148                    available,
149                });
150            }
151        }
152
153        Ok(environments)
154    }
155
156    /// Validate project configuration for simulation
157    ///
158    /// # Arguments
159    ///
160    /// * `config_path` - Path to mecha10.json
161    pub fn validate_config(&self, config_path: &Path) -> Result<ValidationResult> {
162        let profile = RobotProfile::from_config_file(config_path)?;
163
164        let mut errors = Vec::new();
165        let mut warnings = Vec::new();
166
167        // Validate platform
168        if profile.platform.is_empty() {
169            errors.push("Platform not specified".to_string());
170        }
171
172        // Validate sensors
173        if profile.sensors.is_empty() {
174            warnings.push("No sensors configured".to_string());
175        }
176
177        // Validate task nodes
178        if profile.task_nodes.is_empty() {
179            warnings.push("No task nodes configured".to_string());
180        }
181
182        let is_valid = errors.is_empty();
183
184        Ok(ValidationResult {
185            is_valid,
186            errors,
187            warnings,
188            platform: profile.platform,
189            sensor_count: profile.sensors.len(),
190            node_count: profile.task_nodes.len(),
191        })
192    }
193
194    /// Check if simulation is properly set up
195    pub fn is_setup(&self) -> bool {
196        self.base_path.exists() && self.base_path.join("robot.tscn").exists()
197    }
198
199    /// Get the base path for simulations
200    pub fn base_path(&self) -> &Path {
201        &self.base_path
202    }
203
204    /// Resolve a model path from package namespace or plain name
205    ///
206    /// Supports multiple formats:
207    /// - Package namespace: "@mecha10/simulation-models/rover"
208    /// - Plain name: "rover"
209    ///
210    /// Resolution order:
211    /// 1. Framework (monorepo): packages/simulation/models/{name}/
212    /// 2. Project-local: simulation/models/{name}/
213    ///
214    /// # Arguments
215    ///
216    /// * `model_ref` - Model reference (package namespace or plain name)
217    ///
218    /// # Returns
219    ///
220    /// Absolute path to the model directory, or error if not found
221    pub fn resolve_model_path(&self, model_ref: &str) -> Result<PathBuf> {
222        Self::resolve_asset_path(
223            model_ref,
224            AssetPathSpec {
225                kind: "Model",
226                namespace_prefix: "@mecha10/simulation-models/",
227                project_dir: paths::project::SIMULATION_MODELS_DIR,
228                framework_dir: paths::framework::SIMULATION_MODELS_DIR,
229                bundled_subdir: "simulation/models",
230                cached_path: |assets| assets.models_path(),
231            },
232        )
233    }
234
235    /// Resolve an environment path from package namespace or plain name
236    ///
237    /// Supports multiple formats:
238    /// - Package namespace: "@mecha10/simulation-environments/basic_arena"
239    /// - Plain name: "basic_arena"
240    ///
241    /// Resolution order:
242    /// 1. Project-local: simulation/environments/{name}/ (project customizations take precedence)
243    /// 2. Framework (monorepo): packages/simulation/environments/{name}/
244    ///
245    /// # Arguments
246    ///
247    /// * `env_ref` - Environment reference (package namespace or plain name)
248    ///
249    /// # Returns
250    ///
251    /// Absolute path to the environment directory, or error if not found
252    pub fn resolve_environment_path(&self, env_ref: &str) -> Result<PathBuf> {
253        Self::resolve_asset_path(
254            env_ref,
255            AssetPathSpec {
256                kind: "Environment",
257                namespace_prefix: "@mecha10/simulation-environments/",
258                project_dir: paths::project::SIMULATION_ENVIRONMENTS_DIR,
259                framework_dir: paths::framework::SIMULATION_ENVIRONMENTS_DIR,
260                bundled_subdir: "simulation/environments",
261                cached_path: |assets| assets.environments_path(),
262            },
263        )
264    }
265
266    /// Shared resolution logic for `resolve_model_path`/`resolve_environment_path`.
267    ///
268    /// Both look up an asset (model or environment) by package-namespace or plain
269    /// name, checking project-local, `MECHA10_FRAMEWORK_PATH`, compile-time bundled,
270    /// and cached-release locations, in that order.
271    fn resolve_asset_path(asset_ref: &str, spec: AssetPathSpec) -> Result<PathBuf> {
272        // Strip package namespace if present
273        let asset_name = asset_ref.strip_prefix(spec.namespace_prefix).unwrap_or(asset_ref);
274
275        // Try project-local path FIRST (project customizations take precedence)
276        let project_path = PathBuf::from(spec.project_dir).join(asset_name);
277        if project_path.exists() {
278            return Ok(project_path);
279        }
280
281        // Try MECHA10_FRAMEWORK_PATH environment variable
282        if let Ok(framework_path) = std::env::var("MECHA10_FRAMEWORK_PATH") {
283            let framework_asset_path = PathBuf::from(framework_path).join(spec.framework_dir).join(asset_name);
284            if framework_asset_path.exists() {
285                return Ok(framework_asset_path);
286            }
287        }
288
289        // Try compile-time path from CLI package (packages/cli -> packages/simulation)
290        let cli_manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
291        if let Some(packages_dir) = cli_manifest_dir.parent() {
292            let bundled_asset_path = packages_dir.join(spec.bundled_subdir).join(asset_name);
293            if bundled_asset_path.exists() {
294                return Ok(bundled_asset_path);
295            }
296        }
297
298        // Try cached assets from GitHub releases (~/.mecha10/simulation/current/{models,environments}/)
299        let assets_service = crate::services::SimulationAssetsService::new();
300        if let Some(cached_dir) = (spec.cached_path)(&assets_service) {
301            let cached_asset_path = cached_dir.join(asset_name);
302            if cached_asset_path.exists() {
303                return Ok(cached_asset_path);
304            }
305        }
306
307        Err(anyhow::anyhow!(
308            "{} not found: {}\n\nChecked:\n  • Project: {}/{}\n  • Framework: packages/{}/{}\n  • Cached: ~/.mecha10/simulation/current/{}/{}",
309            spec.kind,
310            asset_ref,
311            spec.project_dir,
312            asset_name,
313            spec.bundled_subdir,
314            asset_name,
315            spec.bundled_subdir.rsplit('/').next().unwrap_or(spec.bundled_subdir),
316            asset_name
317        ))
318    }
319}
320
321/// Parameters distinguishing a model-path resolution from an environment-path
322/// resolution in [`SimulationService::resolve_asset_path`].
323struct AssetPathSpec {
324    kind: &'static str,
325    namespace_prefix: &'static str,
326    project_dir: &'static str,
327    framework_dir: &'static str,
328    bundled_subdir: &'static str,
329    cached_path: fn(&crate::services::SimulationAssetsService) -> Option<PathBuf>,
330}
331
332impl Default for SimulationService {
333    fn default() -> Self {
334        Self::new()
335    }
336}
337
338/// Result of simulation generation
339#[derive(Debug)]
340pub struct GenerationResult {
341    pub robot_scene: PathBuf,
342    pub environments: Vec<String>,
343    pub available_count: usize,
344}
345
346/// Information about an environment
347#[derive(Debug, Clone)]
348pub struct EnvironmentInfo {
349    pub id: String,
350    pub name: String,
351    pub description: String,
352    pub score: i32,
353    pub available: bool,
354}
355
356/// Result of configuration validation
357#[derive(Debug)]
358pub struct ValidationResult {
359    pub is_valid: bool,
360    pub errors: Vec<String>,
361    pub warnings: Vec<String>,
362    pub platform: String,
363    pub sensor_count: usize,
364    pub node_count: usize,
365}