use crate::ecs::camera::components::Projection;
use crate::ecs::graphics::resources::Atmosphere;
use crate::ecs::transform::commands::mark_local_transform_dirty;
use crate::ecs::ui::types::Rect;
use crate::ecs::world::commands::{
spawn_cone_at, spawn_cube_at, spawn_cylinder_at, spawn_plane_at, spawn_sphere_at,
spawn_torus_at,
};
use crate::ecs::world::{Entity, World};
use crate::render::wgpu::passes::geometry::UiLayer;
use nalgebra_glm::{Vec2, Vec3, Vec4};
use std::collections::HashMap;
use winit::keyboard::KeyCode;
pub fn format_entity(entity: Entity) -> String {
format!("entity_{}_{}", entity.id, entity.generation)
}
pub fn parse_entity(s: &str) -> Option<Entity> {
let parts: Vec<&str> = s.split('_').collect();
if parts.len() != 3 || parts[0] != "entity" {
return None;
}
let id: u32 = parts[1].parse().ok()?;
let generation: u32 = parts[2].parse().ok()?;
Some(Entity { id, generation })
}
pub trait Command<C>: Send + Sync {
fn name(&self) -> &str;
fn description(&self) -> &str;
fn usage(&self) -> &str;
fn execute(&self, args: &[&str], world: &mut World, context: &mut C) -> String;
}
pub struct CommandRegistry<C> {
builtin_commands: HashMap<String, Box<dyn Command<C>>>,
app_commands: HashMap<String, Box<dyn Command<C>>>,
}
impl<C> Default for CommandRegistry<C> {
fn default() -> Self {
Self::new()
}
}
impl<C> CommandRegistry<C> {
pub fn new() -> Self {
Self {
builtin_commands: HashMap::new(),
app_commands: HashMap::new(),
}
}
pub fn register(&mut self, command: Box<dyn Command<C>>) {
self.app_commands
.insert(command.name().to_string(), command);
}
pub fn register_builtin(&mut self, command: Box<dyn Command<C>>) {
self.builtin_commands
.insert(command.name().to_string(), command);
}
pub fn get(&self, name: &str) -> Option<&dyn Command<C>> {
self.builtin_commands
.get(name)
.or_else(|| self.app_commands.get(name))
.map(|c| c.as_ref())
}
pub fn contains(&self, name: &str) -> bool {
self.builtin_commands.contains_key(name) || self.app_commands.contains_key(name)
}
pub fn builtin_command_names(&self) -> Vec<&str> {
let mut names: Vec<_> = self.builtin_commands.keys().map(|s| s.as_str()).collect();
names.sort();
names
}
pub fn app_command_names(&self) -> Vec<&str> {
let mut names: Vec<_> = self.app_commands.keys().map(|s| s.as_str()).collect();
names.sort();
names
}
}
pub struct ShellState<C> {
pub context: C,
pub visible: bool,
pub input_buffer: String,
pub history: Vec<String>,
pub output: Vec<OutputLine>,
pub history_index: Option<usize>,
pub registry: CommandRegistry<C>,
pub animation_progress: f32,
pub scroll_to_bottom: bool,
pub height: f32,
pub scroll_offset: f32,
pub dragging_resize: bool,
pub drag_start_y: f32,
pub drag_start_height: f32,
pending_enter: bool,
pending_up: bool,
pending_down: bool,
pending_escape: bool,
}
pub struct OutputLine {
pub text: String,
pub is_command: bool,
}
impl<C: Default> Default for ShellState<C> {
fn default() -> Self {
Self::new(C::default())
}
}
impl<C> ShellState<C> {
pub fn new(context: C) -> Self {
Self {
context,
visible: false,
input_buffer: String::new(),
history: Vec::new(),
output: vec![OutputLine {
text: "Type 'help' for available commands.".to_string(),
is_command: false,
}],
history_index: None,
registry: CommandRegistry::new(),
animation_progress: 0.0,
scroll_to_bottom: true,
height: 100.0,
scroll_offset: 0.0,
dragging_resize: false,
drag_start_y: 0.0,
drag_start_height: 0.0,
pending_enter: false,
pending_up: false,
pending_down: false,
pending_escape: false,
}
}
pub fn toggle(&mut self) {
self.visible = !self.visible;
}
pub fn register_command(&mut self, command: Box<dyn Command<C>>) {
self.registry.register(command);
}
pub fn insert_text(&mut self, text: &str) {
if !self.input_buffer.is_empty() && !self.input_buffer.ends_with(' ') {
self.input_buffer.push(' ');
}
self.input_buffer.push_str(text);
}
pub fn insert_entity(&mut self, entity: Entity) {
self.insert_text(&format_entity(entity));
}
pub fn handle_key(&mut self, key: KeyCode, pressed: bool) {
if pressed {
match key {
KeyCode::Enter => self.pending_enter = true,
KeyCode::ArrowUp => self.pending_up = true,
KeyCode::ArrowDown => self.pending_down = true,
KeyCode::Escape => self.pending_escape = true,
KeyCode::Backspace => {
self.input_buffer.pop();
}
_ => {}
}
}
}
pub fn execute_command(&mut self, world: &mut World) {
let input = self.input_buffer.trim().to_string();
if input.is_empty() {
return;
}
self.output.push(OutputLine {
text: format!("> {}", input),
is_command: true,
});
self.history.push(input.clone());
self.history_index = None;
let parts: Vec<&str> = input.split_whitespace().collect();
if parts.is_empty() {
self.input_buffer.clear();
return;
}
let command_name = parts[0];
let args = &parts[1..];
if command_name == "clear" {
self.output.clear();
} else if command_name == "help" {
if args.is_empty() {
let mut help_text = String::from("Commands:\n");
help_text.push_str(" clear - Clear the console output\n");
help_text.push_str(" help - List all available commands\n");
let builtin_names = self.registry.builtin_command_names();
if !builtin_names.is_empty() {
help_text.push_str("\nBuilt-in:\n");
for name in builtin_names {
if let Some(cmd) = self.registry.get(name) {
help_text.push_str(&format!(" {} - {}\n", name, cmd.description()));
}
}
}
let app_names = self.registry.app_command_names();
if !app_names.is_empty() {
help_text.push_str("\nApp:\n");
for name in app_names {
if let Some(cmd) = self.registry.get(name) {
help_text.push_str(&format!(" {} - {}\n", name, cmd.description()));
}
}
}
self.output.push(OutputLine {
text: help_text,
is_command: false,
});
} else if let Some(cmd) = self.registry.get(args[0]) {
self.output.push(OutputLine {
text: format!(
"{}\nUsage: {}\n{}",
cmd.name(),
cmd.usage(),
cmd.description()
),
is_command: false,
});
} else {
self.output.push(OutputLine {
text: format!("Unknown command: {}", args[0]),
is_command: false,
});
}
} else if self.registry.contains(command_name) {
if let Some(cmd) = self.registry.get(command_name) {
let result = cmd.execute(args, world, &mut self.context);
if !result.is_empty() {
self.output.push(OutputLine {
text: result,
is_command: false,
});
}
}
} else {
self.output.push(OutputLine {
text: format!("Unknown command: {}", command_name),
is_command: false,
});
}
self.input_buffer.clear();
self.scroll_to_bottom = true;
}
pub fn history_up(&mut self) {
if self.history.is_empty() {
return;
}
match self.history_index {
None => {
self.history_index = Some(self.history.len() - 1);
}
Some(index) if index > 0 => {
self.history_index = Some(index - 1);
}
_ => {}
}
if let Some(index) = self.history_index {
self.input_buffer = self.history[index].clone();
}
}
pub fn history_down(&mut self) {
match self.history_index {
Some(index) if index < self.history.len() - 1 => {
self.history_index = Some(index + 1);
self.input_buffer = self.history[index + 1].clone();
}
Some(_) => {
self.history_index = None;
self.input_buffer.clear();
}
None => {}
}
}
pub fn update_animation(&mut self, delta_time: f32) {
let target = if self.visible { 1.0 } else { 0.0 };
let speed = 8.0;
self.animation_progress += (target - self.animation_progress) * speed * delta_time;
self.animation_progress = self.animation_progress.clamp(0.0, 1.0);
}
pub fn should_render(&self) -> bool {
self.animation_progress > 0.001
}
pub fn register_builtin_commands(&mut self) {
self.registry.register_builtin(Box::new(SpawnCubeCmd));
self.registry.register_builtin(Box::new(SpawnSphereCmd));
self.registry.register_builtin(Box::new(SpawnCylinderCmd));
self.registry.register_builtin(Box::new(SpawnConeCmd));
self.registry.register_builtin(Box::new(SpawnTorusCmd));
self.registry.register_builtin(Box::new(SpawnPlaneCmd));
self.registry.register_builtin(Box::new(GridCmd));
self.registry.register_builtin(Box::new(AtmosphereCmd));
self.registry.register_builtin(Box::new(FovCmd));
self.registry.register_builtin(Box::new(TimescaleCmd));
self.registry.register_builtin(Box::new(FpsCmd));
self.registry.register_builtin(Box::new(QuitCmd));
self.registry.register_builtin(Box::new(PosCmd));
self.registry.register_builtin(Box::new(TeleportCmd));
self.registry.register_builtin(Box::new(UnlitCmd));
self.registry.register_builtin(Box::new(BloomCmd));
self.registry.register_builtin(Box::new(RotateCmd));
self.registry.register_builtin(Box::new(ScaleCmd));
self.registry.register_builtin(Box::new(MoveCmd));
self.registry.register_builtin(Box::new(DeleteCmd));
self.registry.register_builtin(Box::new(InspectCmd));
}
}
struct SpawnCubeCmd;
impl<C> Command<C> for SpawnCubeCmd {
fn name(&self) -> &str {
"spawn_cube"
}
fn description(&self) -> &str {
"Spawn a cube at the specified position"
}
fn usage(&self) -> &str {
"spawn_cube <x> <y> <z>"
}
fn execute(&self, args: &[&str], world: &mut World, _context: &mut C) -> String {
if args.len() != 3 {
return "Usage: spawn_cube <x> <y> <z>".to_string();
}
let x: f32 = match args[0].parse() {
Ok(value) => value,
Err(_) => return "Invalid x coordinate".to_string(),
};
let y: f32 = match args[1].parse() {
Ok(value) => value,
Err(_) => return "Invalid y coordinate".to_string(),
};
let z: f32 = match args[2].parse() {
Ok(value) => value,
Err(_) => return "Invalid z coordinate".to_string(),
};
spawn_cube_at(world, Vec3::new(x, y, z));
format!("Spawned cube at ({}, {}, {})", x, y, z)
}
}
struct SpawnSphereCmd;
impl<C> Command<C> for SpawnSphereCmd {
fn name(&self) -> &str {
"spawn_sphere"
}
fn description(&self) -> &str {
"Spawn a sphere at the specified position"
}
fn usage(&self) -> &str {
"spawn_sphere <x> <y> <z>"
}
fn execute(&self, args: &[&str], world: &mut World, _context: &mut C) -> String {
if args.len() != 3 {
return "Usage: spawn_sphere <x> <y> <z>".to_string();
}
let x: f32 = match args[0].parse() {
Ok(value) => value,
Err(_) => return "Invalid x coordinate".to_string(),
};
let y: f32 = match args[1].parse() {
Ok(value) => value,
Err(_) => return "Invalid y coordinate".to_string(),
};
let z: f32 = match args[2].parse() {
Ok(value) => value,
Err(_) => return "Invalid z coordinate".to_string(),
};
spawn_sphere_at(world, Vec3::new(x, y, z));
format!("Spawned sphere at ({}, {}, {})", x, y, z)
}
}
struct SpawnCylinderCmd;
impl<C> Command<C> for SpawnCylinderCmd {
fn name(&self) -> &str {
"spawn_cylinder"
}
fn description(&self) -> &str {
"Spawn a cylinder at the specified position"
}
fn usage(&self) -> &str {
"spawn_cylinder <x> <y> <z>"
}
fn execute(&self, args: &[&str], world: &mut World, _context: &mut C) -> String {
if args.len() != 3 {
return "Usage: spawn_cylinder <x> <y> <z>".to_string();
}
let x: f32 = match args[0].parse() {
Ok(value) => value,
Err(_) => return "Invalid x coordinate".to_string(),
};
let y: f32 = match args[1].parse() {
Ok(value) => value,
Err(_) => return "Invalid y coordinate".to_string(),
};
let z: f32 = match args[2].parse() {
Ok(value) => value,
Err(_) => return "Invalid z coordinate".to_string(),
};
spawn_cylinder_at(world, Vec3::new(x, y, z));
format!("Spawned cylinder at ({}, {}, {})", x, y, z)
}
}
struct SpawnConeCmd;
impl<C> Command<C> for SpawnConeCmd {
fn name(&self) -> &str {
"spawn_cone"
}
fn description(&self) -> &str {
"Spawn a cone at the specified position"
}
fn usage(&self) -> &str {
"spawn_cone <x> <y> <z>"
}
fn execute(&self, args: &[&str], world: &mut World, _context: &mut C) -> String {
if args.len() != 3 {
return "Usage: spawn_cone <x> <y> <z>".to_string();
}
let x: f32 = match args[0].parse() {
Ok(value) => value,
Err(_) => return "Invalid x coordinate".to_string(),
};
let y: f32 = match args[1].parse() {
Ok(value) => value,
Err(_) => return "Invalid y coordinate".to_string(),
};
let z: f32 = match args[2].parse() {
Ok(value) => value,
Err(_) => return "Invalid z coordinate".to_string(),
};
spawn_cone_at(world, Vec3::new(x, y, z));
format!("Spawned cone at ({}, {}, {})", x, y, z)
}
}
struct SpawnTorusCmd;
impl<C> Command<C> for SpawnTorusCmd {
fn name(&self) -> &str {
"spawn_torus"
}
fn description(&self) -> &str {
"Spawn a torus at the specified position"
}
fn usage(&self) -> &str {
"spawn_torus <x> <y> <z>"
}
fn execute(&self, args: &[&str], world: &mut World, _context: &mut C) -> String {
if args.len() != 3 {
return "Usage: spawn_torus <x> <y> <z>".to_string();
}
let x: f32 = match args[0].parse() {
Ok(value) => value,
Err(_) => return "Invalid x coordinate".to_string(),
};
let y: f32 = match args[1].parse() {
Ok(value) => value,
Err(_) => return "Invalid y coordinate".to_string(),
};
let z: f32 = match args[2].parse() {
Ok(value) => value,
Err(_) => return "Invalid z coordinate".to_string(),
};
spawn_torus_at(world, Vec3::new(x, y, z));
format!("Spawned torus at ({}, {}, {})", x, y, z)
}
}
struct SpawnPlaneCmd;
impl<C> Command<C> for SpawnPlaneCmd {
fn name(&self) -> &str {
"spawn_plane"
}
fn description(&self) -> &str {
"Spawn a plane at the specified position"
}
fn usage(&self) -> &str {
"spawn_plane <x> <y> <z>"
}
fn execute(&self, args: &[&str], world: &mut World, _context: &mut C) -> String {
if args.len() != 3 {
return "Usage: spawn_plane <x> <y> <z>".to_string();
}
let x: f32 = match args[0].parse() {
Ok(value) => value,
Err(_) => return "Invalid x coordinate".to_string(),
};
let y: f32 = match args[1].parse() {
Ok(value) => value,
Err(_) => return "Invalid y coordinate".to_string(),
};
let z: f32 = match args[2].parse() {
Ok(value) => value,
Err(_) => return "Invalid z coordinate".to_string(),
};
spawn_plane_at(world, Vec3::new(x, y, z));
format!("Spawned plane at ({}, {}, {})", x, y, z)
}
}
struct GridCmd;
impl<C> Command<C> for GridCmd {
fn name(&self) -> &str {
"grid"
}
fn description(&self) -> &str {
"Toggle grid visibility (on/off)"
}
fn usage(&self) -> &str {
"grid <on|off>"
}
fn execute(&self, args: &[&str], world: &mut World, _context: &mut C) -> String {
if args.is_empty() {
let state = if world.resources.graphics.show_grid {
"on"
} else {
"off"
};
return format!("Grid is currently {}", state);
}
match args[0].to_lowercase().as_str() {
"on" | "true" | "1" => {
world.resources.graphics.show_grid = true;
"Grid enabled".to_string()
}
"off" | "false" | "0" => {
world.resources.graphics.show_grid = false;
"Grid disabled".to_string()
}
_ => "Usage: grid <on|off>".to_string(),
}
}
}
struct AtmosphereCmd;
impl<C> Command<C> for AtmosphereCmd {
fn name(&self) -> &str {
"atmosphere"
}
fn description(&self) -> &str {
"Set the atmosphere type"
}
fn usage(&self) -> &str {
"atmosphere <none|sky|cloudy|space|nebula|sunset>"
}
fn execute(&self, args: &[&str], world: &mut World, _context: &mut C) -> String {
if args.is_empty() {
let current = match world.resources.graphics.atmosphere {
Atmosphere::None => "none",
Atmosphere::Sky => "sky",
Atmosphere::CloudySky => "cloudy",
Atmosphere::Space => "space",
Atmosphere::Nebula => "nebula",
Atmosphere::Sunset => "sunset",
_ => "other",
};
return format!("Current atmosphere: {}", current);
}
let atmosphere = match args[0].to_lowercase().as_str() {
"none" => Atmosphere::None,
"sky" => Atmosphere::Sky,
"cloudy" | "cloudysky" => Atmosphere::CloudySky,
"space" => Atmosphere::Space,
"nebula" => Atmosphere::Nebula,
"sunset" => Atmosphere::Sunset,
_ => {
return "Unknown atmosphere type. Usage: atmosphere <none|sky|cloudy|space|nebula|sunset>".to_string();
}
};
world.resources.graphics.atmosphere = atmosphere;
format!("Atmosphere set to {}", args[0].to_lowercase())
}
}
struct FovCmd;
impl<C> Command<C> for FovCmd {
fn name(&self) -> &str {
"fov"
}
fn description(&self) -> &str {
"Get or set camera field of view in degrees"
}
fn usage(&self) -> &str {
"fov [degrees]"
}
fn execute(&self, args: &[&str], world: &mut World, _context: &mut C) -> String {
let Some(camera_entity) = world.resources.active_camera else {
return "No active camera".to_string();
};
let Some(camera) = world.get_camera_mut(camera_entity) else {
return "Camera component not found".to_string();
};
let Projection::Perspective(perspective) = &mut camera.projection else {
return "Camera is not using perspective projection".to_string();
};
if args.is_empty() {
let fov_degrees = perspective.y_fov_rad.to_degrees();
return format!("Current FOV: {:.1} degrees", fov_degrees);
}
let fov: f32 = match args[0].parse() {
Ok(value) => value,
Err(_) => return "Invalid FOV value".to_string(),
};
if !(1.0..=179.0).contains(&fov) {
return "FOV must be between 1 and 179 degrees".to_string();
}
perspective.y_fov_rad = fov.to_radians();
format!("FOV set to {:.1} degrees", fov)
}
}
struct TimescaleCmd;
impl<C> Command<C> for TimescaleCmd {
fn name(&self) -> &str {
"timescale"
}
fn description(&self) -> &str {
"Get or set time scale (1.0 = normal, 0.5 = slow-mo, 2.0 = fast)"
}
fn usage(&self) -> &str {
"timescale [value]"
}
fn execute(&self, args: &[&str], world: &mut World, _context: &mut C) -> String {
if args.is_empty() {
return format!(
"Current timescale: {:.2}",
world.resources.window.timing.time_speed
);
}
let scale: f32 = match args[0].parse() {
Ok(value) => value,
Err(_) => return "Invalid timescale value".to_string(),
};
if !(0.0..=10.0).contains(&scale) {
return "Timescale must be between 0.0 and 10.0".to_string();
}
world.resources.window.timing.time_speed = scale;
format!("Timescale set to {:.2}", scale)
}
}
struct FpsCmd;
impl<C> Command<C> for FpsCmd {
fn name(&self) -> &str {
"fps"
}
fn description(&self) -> &str {
"Show current frames per second"
}
fn usage(&self) -> &str {
"fps"
}
fn execute(&self, _args: &[&str], world: &mut World, _context: &mut C) -> String {
let timing = &world.resources.window.timing;
format!(
"FPS: {:.1} | Frame time: {:.2}ms | Uptime: {:.1}s",
timing.frames_per_second,
timing.delta_time * 1000.0,
timing.uptime_milliseconds as f32 / 1000.0
)
}
}
struct QuitCmd;
impl<C> Command<C> for QuitCmd {
fn name(&self) -> &str {
"quit"
}
fn description(&self) -> &str {
"Exit the application"
}
fn usage(&self) -> &str {
"quit"
}
fn execute(&self, _args: &[&str], world: &mut World, _context: &mut C) -> String {
world.resources.window.should_exit = true;
"Exiting...".to_string()
}
}
struct PosCmd;
impl<C> Command<C> for PosCmd {
fn name(&self) -> &str {
"pos"
}
fn description(&self) -> &str {
"Show current camera position"
}
fn usage(&self) -> &str {
"pos"
}
fn execute(&self, _args: &[&str], world: &mut World, _context: &mut C) -> String {
let Some(camera_entity) = world.resources.active_camera else {
return "No active camera".to_string();
};
let Some(transform) = world.get_local_transform(camera_entity) else {
return "Camera transform not found".to_string();
};
format!(
"Position: ({:.2}, {:.2}, {:.2})",
transform.translation.x, transform.translation.y, transform.translation.z
)
}
}
struct TeleportCmd;
impl<C> Command<C> for TeleportCmd {
fn name(&self) -> &str {
"tp"
}
fn description(&self) -> &str {
"Teleport camera to specified position"
}
fn usage(&self) -> &str {
"tp <x> <y> <z>"
}
fn execute(&self, args: &[&str], world: &mut World, _context: &mut C) -> String {
if args.len() != 3 {
return "Usage: tp <x> <y> <z>".to_string();
}
let Some(camera_entity) = world.resources.active_camera else {
return "No active camera".to_string();
};
let x: f32 = match args[0].parse() {
Ok(value) => value,
Err(_) => return "Invalid x coordinate".to_string(),
};
let y: f32 = match args[1].parse() {
Ok(value) => value,
Err(_) => return "Invalid y coordinate".to_string(),
};
let z: f32 = match args[2].parse() {
Ok(value) => value,
Err(_) => return "Invalid z coordinate".to_string(),
};
let Some(transform) = world.get_local_transform_mut(camera_entity) else {
return "Camera transform not found".to_string();
};
transform.translation = Vec3::new(x, y, z);
mark_local_transform_dirty(world, camera_entity);
format!("Teleported to ({:.2}, {:.2}, {:.2})", x, y, z)
}
}
struct UnlitCmd;
impl<C> Command<C> for UnlitCmd {
fn name(&self) -> &str {
"unlit"
}
fn description(&self) -> &str {
"Toggle unlit rendering mode"
}
fn usage(&self) -> &str {
"unlit [on|off]"
}
fn execute(&self, args: &[&str], world: &mut World, _context: &mut C) -> String {
if args.is_empty() {
let state = if world.resources.graphics.unlit_mode {
"on"
} else {
"off"
};
return format!("Unlit mode is currently {}", state);
}
match args[0].to_lowercase().as_str() {
"on" | "true" | "1" => {
world.resources.graphics.unlit_mode = true;
"Unlit mode enabled".to_string()
}
"off" | "false" | "0" => {
world.resources.graphics.unlit_mode = false;
"Unlit mode disabled".to_string()
}
_ => "Usage: unlit [on|off]".to_string(),
}
}
}
struct BloomCmd;
impl<C> Command<C> for BloomCmd {
fn name(&self) -> &str {
"bloom"
}
fn description(&self) -> &str {
"Toggle bloom effect or set intensity"
}
fn usage(&self) -> &str {
"bloom [on|off|<intensity>]"
}
fn execute(&self, args: &[&str], world: &mut World, _context: &mut C) -> String {
if args.is_empty() {
let state = if world.resources.graphics.bloom_enabled {
format!(
"on (intensity: {:.2})",
world.resources.graphics.bloom_intensity
)
} else {
"off".to_string()
};
return format!("Bloom is currently {}", state);
}
match args[0].to_lowercase().as_str() {
"on" | "true" | "1" => {
world.resources.graphics.bloom_enabled = true;
"Bloom enabled".to_string()
}
"off" | "false" | "0" => {
world.resources.graphics.bloom_enabled = false;
"Bloom disabled".to_string()
}
_ => {
if let Ok(intensity) = args[0].parse::<f32>() {
if (0.0..=5.0).contains(&intensity) {
world.resources.graphics.bloom_enabled = true;
world.resources.graphics.bloom_intensity = intensity;
return format!("Bloom intensity set to {:.2}", intensity);
}
return "Bloom intensity must be between 0.0 and 5.0".to_string();
}
"Usage: bloom [on|off|<intensity>]".to_string()
}
}
}
}
struct RotateCmd;
impl<C> Command<C> for RotateCmd {
fn name(&self) -> &str {
"rotate"
}
fn description(&self) -> &str {
"Rotate an entity by degrees on each axis"
}
fn usage(&self) -> &str {
"rotate <entity> <x> <y> <z>"
}
fn execute(&self, args: &[&str], world: &mut World, _context: &mut C) -> String {
if args.len() != 4 {
return "Usage: rotate <entity> <x> <y> <z>".to_string();
}
let Some(entity) = parse_entity(args[0]) else {
return format!("Invalid entity format: {}", args[0]);
};
let x: f32 = match args[1].parse() {
Ok(value) => value,
Err(_) => return "Invalid x rotation".to_string(),
};
let y: f32 = match args[2].parse() {
Ok(value) => value,
Err(_) => return "Invalid y rotation".to_string(),
};
let z: f32 = match args[3].parse() {
Ok(value) => value,
Err(_) => return "Invalid z rotation".to_string(),
};
let Some(transform) = world.get_local_transform_mut(entity) else {
return "Entity not found or has no transform".to_string();
};
let rot_x = nalgebra_glm::quat_angle_axis(x.to_radians(), &Vec3::new(1.0, 0.0, 0.0));
let rot_y = nalgebra_glm::quat_angle_axis(y.to_radians(), &Vec3::new(0.0, 1.0, 0.0));
let rot_z = nalgebra_glm::quat_angle_axis(z.to_radians(), &Vec3::new(0.0, 0.0, 1.0));
let rotation = rot_z * rot_y * rot_x;
transform.rotation = rotation * transform.rotation;
mark_local_transform_dirty(world, entity);
format!("Rotated {} by ({}, {}, {}) degrees", args[0], x, y, z)
}
}
struct ScaleCmd;
impl<C> Command<C> for ScaleCmd {
fn name(&self) -> &str {
"scale"
}
fn description(&self) -> &str {
"Scale an entity uniformly or on each axis"
}
fn usage(&self) -> &str {
"scale <entity> <factor> OR scale <entity> <x> <y> <z>"
}
fn execute(&self, args: &[&str], world: &mut World, _context: &mut C) -> String {
if args.len() < 2 {
return "Usage: scale <entity> <factor> OR scale <entity> <x> <y> <z>".to_string();
}
let Some(entity) = parse_entity(args[0]) else {
return format!("Invalid entity format: {}", args[0]);
};
let scale = if args.len() == 2 {
let factor: f32 = match args[1].parse() {
Ok(value) => value,
Err(_) => return "Invalid scale factor".to_string(),
};
Vec3::new(factor, factor, factor)
} else if args.len() == 4 {
let x: f32 = match args[1].parse() {
Ok(value) => value,
Err(_) => return "Invalid x scale".to_string(),
};
let y: f32 = match args[2].parse() {
Ok(value) => value,
Err(_) => return "Invalid y scale".to_string(),
};
let z: f32 = match args[3].parse() {
Ok(value) => value,
Err(_) => return "Invalid z scale".to_string(),
};
Vec3::new(x, y, z)
} else {
return "Usage: scale <entity> <factor> OR scale <entity> <x> <y> <z>".to_string();
};
let new_scale = {
let Some(transform) = world.get_local_transform_mut(entity) else {
return "Entity not found or has no transform".to_string();
};
transform.scale = transform.scale.component_mul(&scale);
transform.scale
};
mark_local_transform_dirty(world, entity);
format!(
"Scaled {} to ({:.2}, {:.2}, {:.2})",
args[0], new_scale.x, new_scale.y, new_scale.z
)
}
}
struct MoveCmd;
impl<C> Command<C> for MoveCmd {
fn name(&self) -> &str {
"move"
}
fn description(&self) -> &str {
"Move an entity by offset or to absolute position"
}
fn usage(&self) -> &str {
"move <entity> <x> <y> <z>"
}
fn execute(&self, args: &[&str], world: &mut World, _context: &mut C) -> String {
if args.len() != 4 {
return "Usage: move <entity> <x> <y> <z>".to_string();
}
let Some(entity) = parse_entity(args[0]) else {
return format!("Invalid entity format: {}", args[0]);
};
let x: f32 = match args[1].parse() {
Ok(value) => value,
Err(_) => return "Invalid x position".to_string(),
};
let y: f32 = match args[2].parse() {
Ok(value) => value,
Err(_) => return "Invalid y position".to_string(),
};
let z: f32 = match args[3].parse() {
Ok(value) => value,
Err(_) => return "Invalid z position".to_string(),
};
let Some(transform) = world.get_local_transform_mut(entity) else {
return "Entity not found or has no transform".to_string();
};
transform.translation = Vec3::new(x, y, z);
mark_local_transform_dirty(world, entity);
format!("Moved {} to ({:.2}, {:.2}, {:.2})", args[0], x, y, z)
}
}
struct DeleteCmd;
impl<C> Command<C> for DeleteCmd {
fn name(&self) -> &str {
"delete"
}
fn description(&self) -> &str {
"Delete an entity from the world"
}
fn usage(&self) -> &str {
"delete <entity>"
}
fn execute(&self, args: &[&str], world: &mut World, _context: &mut C) -> String {
if args.is_empty() {
return "Usage: delete <entity>".to_string();
}
let Some(entity) = parse_entity(args[0]) else {
return format!("Invalid entity format: {}", args[0]);
};
if world.get_local_transform(entity).is_none() {
return "Entity not found".to_string();
}
world.despawn_entities(&[entity]);
format!("Deleted {}", args[0])
}
}
struct InspectCmd;
impl<C> Command<C> for InspectCmd {
fn name(&self) -> &str {
"inspect"
}
fn description(&self) -> &str {
"Show information about an entity"
}
fn usage(&self) -> &str {
"inspect <entity>"
}
fn execute(&self, args: &[&str], world: &mut World, _context: &mut C) -> String {
if args.is_empty() {
return "Usage: inspect <entity>".to_string();
}
let Some(entity) = parse_entity(args[0]) else {
return format!("Invalid entity format: {}", args[0]);
};
let mut info = format!("Entity: {}\n", args[0]);
if let Some(name) = world.get_name(entity) {
info.push_str(&format!("Name: {}\n", name.0));
}
if let Some(transform) = world.get_local_transform(entity) {
info.push_str(&format!(
"Position: ({:.2}, {:.2}, {:.2})\n",
transform.translation.x, transform.translation.y, transform.translation.z
));
info.push_str(&format!(
"Scale: ({:.2}, {:.2}, {:.2})\n",
transform.scale.x, transform.scale.y, transform.scale.z
));
} else {
return "Entity not found".to_string();
}
if let Some(visibility) = world.get_visibility(entity) {
info.push_str(&format!("Visible: {}\n", visibility.visible));
}
info
}
}
pub fn shell_retained_ui<C>(shell: &mut ShellState<C>, world: &mut World) {
if !shell.should_render() {
return;
}
let screen_size = world
.resources
.window
.handle
.as_ref()
.map(|handle| {
let size = handle.inner_size();
Vec2::new(size.width as f32, size.height as f32)
})
.unwrap_or(Vec2::new(1920.0, 1080.0));
let screen_width = screen_size.x;
let screen_height = screen_size.y;
let mouse_pos = Vec2::new(
world.resources.input.mouse.position.x,
world.resources.input.mouse.position.y,
);
let mouse_down = world
.resources
.input
.mouse
.state
.contains(crate::ecs::world::resources::MouseState::LEFT_CLICKED);
let mouse_just_pressed = world
.resources
.input
.mouse
.state
.contains(crate::ecs::world::resources::MouseState::LEFT_JUST_PRESSED);
let min_height = 100.0;
let max_height = screen_height * 0.9;
shell.height = shell.height.clamp(min_height, max_height);
let shell_height = shell.height;
let current_height = shell_height * shell.animation_progress;
let resize_handle_height = 8.0;
let resize_area = Rect::new(
0.0,
current_height - resize_handle_height,
screen_width,
resize_handle_height * 2.0,
);
let mouse_in_resize = resize_area.contains(mouse_pos) && shell.animation_progress > 0.95;
if mouse_in_resize && mouse_just_pressed && !shell.dragging_resize {
shell.dragging_resize = true;
shell.drag_start_y = mouse_pos.y;
shell.drag_start_height = shell.height;
}
if shell.dragging_resize {
if mouse_down {
let delta = mouse_pos.y - shell.drag_start_y;
shell.height = (shell.drag_start_height + delta).clamp(min_height, max_height);
} else {
shell.dragging_resize = false;
}
}
let bg_color = Vec4::new(0.06, 0.06, 0.08, 0.95);
let header_color = Vec4::new(0.39, 0.78, 0.39, 1.0);
let hint_color = Vec4::new(0.47, 0.47, 0.47, 1.0);
let command_color = Vec4::new(0.59, 0.78, 1.0, 1.0);
let output_color = Vec4::new(0.78, 0.78, 0.78, 1.0);
let border_color = Vec4::new(0.4, 0.4, 0.45, 1.0);
let transparent = Vec4::new(0.0, 0.0, 0.0, 0.0);
let panel_clip = Rect::new(0.0, 0.0, screen_width, current_height);
let ui = &mut world.resources.retained_ui;
ui.draw_overlay_rect(crate::render::wgpu::passes::geometry::UiRect {
position: Vec2::new(0.0, 0.0),
size: Vec2::new(screen_width, current_height),
color: bg_color,
corner_radius: 0.0,
border_width: 0.0,
border_color: transparent,
rotation: 0.0,
clip_rect: Some(panel_clip),
layer: UiLayer::Tooltips,
z_index: 0,
});
let padding = 10.0;
let font_size = 14.0;
let line_height = font_size + 6.0;
let header_height = 24.0;
let input_height = 28.0;
let separator_height = 1.0;
let header_y = 4.0;
ui.draw_overlay_text(
"Console",
Vec2::new(padding, header_y),
crate::ecs::text::components::TextProperties {
font_size,
color: header_color,
..Default::default()
},
Some(panel_clip),
UiLayer::Tooltips,
1,
);
let hint_text = "Alt+C to close";
let hint_width = hint_text.len() as f32 * font_size * 0.5;
ui.draw_overlay_text(
hint_text,
Vec2::new(screen_width - hint_width - padding, header_y + 2.0),
crate::ecs::text::components::TextProperties {
font_size: font_size * 0.85,
color: hint_color,
..Default::default()
},
Some(panel_clip),
UiLayer::Tooltips,
1,
);
let top_separator_y = header_height;
ui.draw_overlay_rect(crate::render::wgpu::passes::geometry::UiRect {
position: Vec2::new(0.0, top_separator_y),
size: Vec2::new(screen_width, separator_height),
color: border_color,
corner_radius: 0.0,
border_width: 0.0,
border_color: transparent,
rotation: 0.0,
clip_rect: Some(panel_clip),
layer: UiLayer::Tooltips,
z_index: 1,
});
let bottom_separator_y = current_height - input_height - separator_height;
ui.draw_overlay_rect(crate::render::wgpu::passes::geometry::UiRect {
position: Vec2::new(0.0, bottom_separator_y),
size: Vec2::new(screen_width, separator_height),
color: border_color,
corner_radius: 0.0,
border_width: 0.0,
border_color: transparent,
rotation: 0.0,
clip_rect: Some(panel_clip),
layer: UiLayer::Tooltips,
z_index: 1,
});
let output_top = top_separator_y + separator_height + 2.0;
let output_bottom = bottom_separator_y - 2.0;
let output_height = (output_bottom - output_top).max(0.0);
let output_clip = Rect::new(0.0, output_top, screen_width, output_height);
let mut total_content_height = 0.0;
for line in &shell.output {
let line_count = line.text.lines().count().max(1);
total_content_height += line_count as f32 * line_height;
}
if shell.scroll_to_bottom {
shell.scroll_offset = (total_content_height - output_height).max(0.0);
shell.scroll_to_bottom = false;
}
shell.scroll_offset = shell
.scroll_offset
.clamp(0.0, (total_content_height - output_height).max(0.0));
if output_height > 0.0 {
let mut y_pos = output_top - shell.scroll_offset;
for line in &shell.output {
let color = if line.is_command {
command_color
} else {
output_color
};
for text_line in line.text.lines() {
if y_pos + line_height > output_top && y_pos < output_bottom {
ui.draw_overlay_text(
text_line,
Vec2::new(padding, y_pos),
crate::ecs::text::components::TextProperties {
font_size,
color,
..Default::default()
},
Some(output_clip),
UiLayer::Tooltips,
2,
);
}
y_pos += line_height;
}
}
}
let input_y = current_height - input_height + (input_height - font_size) * 0.5;
ui.draw_overlay_text(
">",
Vec2::new(padding, input_y),
crate::ecs::text::components::TextProperties {
font_size,
color: header_color,
..Default::default()
},
Some(panel_clip),
UiLayer::Tooltips,
2,
);
let cursor = if shell.visible && shell.animation_progress > 0.9 {
"_"
} else {
""
};
let input_display = format!("{}{}", shell.input_buffer, cursor);
ui.draw_overlay_text(
&input_display,
Vec2::new(padding + font_size + 4.0, input_y),
crate::ecs::text::components::TextProperties {
font_size,
color: output_color,
..Default::default()
},
Some(panel_clip),
UiLayer::Tooltips,
2,
);
ui.draw_overlay_rect(crate::render::wgpu::passes::geometry::UiRect {
position: Vec2::new(0.0, current_height - 2.0),
size: Vec2::new(screen_width, 2.0),
color: border_color,
corner_radius: 0.0,
border_width: 0.0,
border_color: transparent,
rotation: 0.0,
clip_rect: Some(panel_clip),
layer: UiLayer::Tooltips,
z_index: 3,
});
if shell.animation_progress > 0.95 {
let handle_color = if shell.dragging_resize || mouse_in_resize {
Vec4::new(0.6, 0.8, 1.0, 1.0)
} else {
Vec4::new(0.5, 0.5, 0.55, 0.8)
};
let handle_width = 40.0;
let handle_height_px = 3.0;
ui.draw_overlay_rect(crate::render::wgpu::passes::geometry::UiRect {
position: Vec2::new((screen_width - handle_width) * 0.5, current_height + 4.0),
size: Vec2::new(handle_width, handle_height_px),
color: handle_color,
corner_radius: 1.5,
border_width: 0.0,
border_color: transparent,
rotation: 0.0,
clip_rect: None,
layer: UiLayer::Tooltips,
z_index: 10,
});
}
if shell.visible && shell.animation_progress > 0.9 {
if shell.pending_enter {
shell.pending_enter = false;
shell.execute_command(world);
}
if shell.pending_up {
shell.pending_up = false;
shell.history_up();
}
if shell.pending_down {
shell.pending_down = false;
shell.history_down();
}
if shell.pending_escape {
shell.pending_escape = false;
shell.visible = false;
}
}
}
#[cfg(feature = "egui")]
pub fn shell_ui<C>(shell: &mut ShellState<C>, ui_context: &egui::Context, world: &mut World) {
if !shell.should_render() {
return;
}
let screen_rect = ui_context.input(|i| i.viewport_rect());
let shell_height = shell.height.min(screen_rect.height() * 0.9);
let current_height = shell_height * shell.animation_progress;
let panel_rect = egui::Rect::from_min_size(
egui::pos2(0.0, 0.0),
egui::vec2(screen_rect.width(), current_height),
);
egui::Area::new(egui::Id::new("shell_console"))
.fixed_pos(egui::pos2(0.0, 0.0))
.order(egui::Order::Foreground)
.show(ui_context, |ui| {
ui.set_clip_rect(panel_rect);
egui::Frame::new()
.fill(egui::Color32::from_rgba_unmultiplied(15, 15, 20, 230))
.show(ui, |ui| {
ui.set_min_size(egui::vec2(screen_rect.width(), shell_height));
ui.set_max_size(egui::vec2(screen_rect.width(), shell_height));
ui.vertical(|ui| {
ui.add_space(4.0);
ui.horizontal(|ui| {
ui.add_space(8.0);
ui.label(
egui::RichText::new("Console")
.color(egui::Color32::from_rgb(100, 200, 100))
.strong(),
);
ui.with_layout(
egui::Layout::right_to_left(egui::Align::Center),
|ui| {
ui.add_space(8.0);
ui.label(
egui::RichText::new("Alt+C or Escape to close")
.color(egui::Color32::from_rgb(120, 120, 120))
.small(),
);
},
);
});
ui.add_space(2.0);
ui.separator();
let available_height = shell_height - 70.0;
egui::ScrollArea::vertical()
.max_height(available_height)
.auto_shrink([false, false])
.stick_to_bottom(shell.scroll_to_bottom)
.show(ui, |ui| {
ui.add_space(4.0);
for line in &shell.output {
ui.horizontal(|ui| {
ui.add_space(8.0);
let color = if line.is_command {
egui::Color32::from_rgb(150, 200, 255)
} else {
egui::Color32::from_rgb(200, 200, 200)
};
ui.label(
egui::RichText::new(&line.text)
.color(color)
.family(egui::FontFamily::Monospace),
);
});
}
ui.add_space(4.0);
});
shell.scroll_to_bottom = false;
ui.separator();
ui.horizontal(|ui| {
ui.add_space(8.0);
ui.label(
egui::RichText::new(">")
.color(egui::Color32::from_rgb(100, 200, 100))
.family(egui::FontFamily::Monospace),
);
let response = ui.add(
egui::TextEdit::singleline(&mut shell.input_buffer)
.desired_width(screen_rect.width() - 40.0)
.font(egui::FontId::monospace(14.0))
.text_color(egui::Color32::from_rgb(220, 220, 220))
.frame(false),
);
if shell.visible && shell.animation_progress > 0.9 {
response.request_focus();
}
if response.has_focus() {
if ui.input(|i| i.key_pressed(egui::Key::Enter)) {
shell.execute_command(world);
}
if ui.input(|i| i.key_pressed(egui::Key::ArrowUp)) {
shell.history_up();
}
if ui.input(|i| i.key_pressed(egui::Key::ArrowDown)) {
shell.history_down();
}
if ui.input(|i| i.key_pressed(egui::Key::Escape)) {
shell.visible = false;
}
if ui.input(|i| i.modifiers.alt && i.key_pressed(egui::Key::C)) {
shell.visible = false;
}
}
});
ui.add_space(4.0);
});
});
});
}