use anyhow::{Context, Result};
use std::fs;
use std::path::Path;
use super::profile::RobotProfile;
pub struct RobotGenerator {
profile: RobotProfile,
rl_mode: bool,
}
impl RobotGenerator {
pub fn from_config_file(config_path: &Path) -> Result<Self> {
let profile = RobotProfile::from_config_file(config_path)?;
Ok(Self {
profile,
rl_mode: true, })
}
pub fn from_config_file_with_mode(config_path: &Path, rl_mode: bool) -> Result<Self> {
let profile = RobotProfile::from_config_file(config_path)?;
Ok(Self { profile, rl_mode })
}
pub fn generate(&self, output_path: &Path) -> Result<()> {
println!("🤖 Generating robot scene...");
println!(" Platform: {}", self.profile.platform);
println!(" Sensors: {}", self.profile.sensors.join(", "));
if let Some(parent) = output_path.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("Failed to create output directory: {}", parent.display()))?;
}
let tscn_content = self.generate_tscn();
fs::write(output_path, tscn_content)
.with_context(|| format!("Failed to write robot scene: {}", output_path.display()))?;
println!(" ✅ Generated: {}", output_path.display());
Ok(())
}
fn generate_tscn(&self) -> String {
let mut lines = Vec::new();
let load_steps = if self.rl_mode { 5 } else { 3 };
lines.push(format!("[gd_scene load_steps={} format=3]", load_steps));
lines.push("".to_string());
if self.rl_mode {
lines.push(
"[ext_resource type=\"Script\" path=\"res://components/robot_templates/rover_base_rl.gd\" id=\"1\"]"
.to_string(),
);
lines.push(
"[ext_resource type=\"Script\" path=\"res://components/controllers/rover_ai_controller.gd\" id=\"2\"]"
.to_string(),
);
lines.push("".to_string());
}
lines.push("[sub_resource type=\"CapsuleShape3D\" id=\"CapsuleShape3D_1\"]".to_string());
lines.push(format!("radius = {}", self.get_robot_radius()));
lines.push(format!("height = {}", self.get_robot_height()));
lines.push("".to_string());
lines.push("[sub_resource type=\"CapsuleMesh\" id=\"CapsuleMesh_1\"]".to_string());
lines.push(format!("radius = {}", self.get_robot_radius()));
lines.push(format!("height = {}", self.get_robot_height()));
lines.push("".to_string());
lines.push("[node name=\"Robot\" type=\"CharacterBody3D\"]".to_string());
if self.rl_mode {
lines.push("script = ExtResource(\"1\")".to_string());
}
lines.push("".to_string());
lines.push("[node name=\"CollisionShape3D\" type=\"CollisionShape3D\" parent=\".\"]".to_string());
lines.push("shape = SubResource(\"CapsuleShape3D_1\")".to_string());
lines.push("".to_string());
lines.push("[node name=\"MeshInstance3D\" type=\"MeshInstance3D\" parent=\".\"]".to_string());
lines.push("mesh = SubResource(\"CapsuleMesh_1\")".to_string());
lines.push("".to_string());
if self.rl_mode {
lines.push("[node name=\"AIController3D\" type=\"Node3D\" parent=\".\"]".to_string());
lines.push("script = ExtResource(\"2\")".to_string());
lines.push("".to_string());
}
self.add_sensor_nodes(&mut lines);
lines.push("[node name=\"Markers\" type=\"Node3D\" parent=\".\"]".to_string());
lines.push("".to_string());
lines.push("[node name=\"CenterOfMass\" type=\"Marker3D\" parent=\"Markers\"]".to_string());
lines.push("".to_string());
lines.join("\n")
}
fn add_sensor_nodes(&self, lines: &mut Vec<String>) {
let parent = ".";
if self.profile.sensors.contains(&"camera".to_string()) {
if self.rl_mode {
lines.push(format!(
"[node name=\"CameraRL\" type=\"Node3D\" parent=\"{}\"]",
parent
));
lines.push("script = ExtResource(\"res://components/sensors/camera_rl/camera_rl.gd\")".to_string());
lines.push(format!(
"transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, {}, {})",
self.get_robot_height() / 2.0 - 0.1,
0.3
));
lines.push("resolution = Vector2i(84, 84)".to_string());
lines.push("fov_degrees = 90.0".to_string());
lines.push("observation_enabled = true".to_string());
lines.push("".to_string());
} else {
lines.push(format!(
"[node name=\"Camera\" type=\"Camera3D\" parent=\"{}\"]",
parent
));
lines.push(format!(
"transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, {}, {})",
self.get_robot_height() / 2.0 - 0.1,
0.3
));
lines.push("fov = 60.0".to_string());
lines.push("".to_string());
}
}
if self.profile.sensors.contains(&"lidar".to_string()) {
if self.rl_mode {
lines.push(format!("[node name=\"LidarRL\" type=\"Node3D\" parent=\"{}\"]", parent));
lines.push("script = ExtResource(\"res://components/sensors/lidar_rl/lidar_rl.gd\")".to_string());
lines.push(format!(
"transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, {}, 0)",
self.get_robot_height() / 2.0
));
lines.push("num_rays = 16".to_string());
lines.push("ray_length = 10.0".to_string());
lines.push("observation_enabled = true".to_string());
lines.push("".to_string());
} else {
lines.push(format!("[node name=\"LiDAR\" type=\"Node3D\" parent=\"{}\"]", parent));
lines.push(format!(
"transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, {}, 0)",
self.get_robot_height() / 2.0
));
lines.push("".to_string());
for i in 0..36 {
let angle_deg = i * 10;
let angle_rad = (angle_deg as f32).to_radians();
let x = angle_rad.cos();
let z = angle_rad.sin();
lines.push(format!(
"[node name=\"Ray{}\" type=\"RayCast3D\" parent=\"{}/LiDAR\"]",
i, parent
));
lines.push(format!("target_position = Vector3({}, 0, {})", x * 10.0, z * 10.0));
lines.push("enabled = true".to_string());
lines.push("".to_string());
}
}
}
if self.profile.sensors.contains(&"imu".to_string()) {
lines.push(format!("[node name=\"IMU\" type=\"Marker3D\" parent=\"{}\"]", parent));
lines.push("".to_string());
}
}
fn get_robot_radius(&self) -> f32 {
match self.profile.platform.as_str() {
"differential-drive" => 0.3, "holonomic" => 0.35, "legged" => 0.25, "arm" => 0.15, _ => 0.3, }
}
fn get_robot_height(&self) -> f32 {
match self.profile.platform.as_str() {
"differential-drive" => 1.0, "holonomic" => 1.2, "legged" => 1.5, "arm" => 2.0, _ => 1.0, }
}
}