tirbofish/dropbear · commit
e6e9d36a2d217cebca4ea10b41cbc46044cac4e5
moved around some stuff, nothing too big
it was pretty big lol
Signature present but could not be verified.
Unverified
@@ -6,7 +6,7 @@ package.repository = "https://github.com/4tkbytes/dropbear-engine" package.readme = "README.md" resolver = "3" -members = ["dropbear-engine", "eucalyptus", "eucalyptus-core", "eucalyptus-editor", "redback-runtime"] +members = ["dropbear-engine", "eucalyptus-core", "eucalyptus-editor", "redback-runtime"] [workspace.dependencies] anyhow = { version = "1.0", features = ["backtrace"] } @@ -28,7 +28,7 @@ egui_extras = { version = "0.32", features = ["all_loaders"] } env_logger = "0.11" futures = "0.3" gilrs = "0.11" -git2 = "0.20" +git2 = { version = "0.20", features = ["vendored-openssl"] } glam = { version = "0.30", features = ["serde"] } hecs = { version = "0.10", features = ["serde"] } lazy_static = "1.5" @@ -41,11 +41,10 @@ parking_lot = {version = "0.12", features = ["deadlock_detection"] } pollster = "0.4" rfd = "0.15.4" ron = "0.11" -russimp-ng = { version = "3.2" } +russimp-ng = { version = "3.2", features = ["static-link"] } rustyscript = { version = "0.12" } serde = { version = "1.0.219", features = ["derive"] } spin_sleep = "1.3" -tokio = { version = "1", features = ["full"] } transform-gizmo-egui = { git = "https://github.com/4tkbytes/transform-gizmo"} wgpu = "26" winit = { version = "0.30", features = [] } @@ -7,3 +7,34 @@ repository.workspace = true readme.workspace = true [dependencies] +anyhow.workspace = true +app_dirs2.workspace = true +bincode.workspace = true +chrono.workspace = true +dropbear-engine.workspace = true +egui-toast-fork.workspace = true +egui.workspace = true +egui_dock-fork.workspace = true +glam.workspace = true +hecs.workspace = true +log.workspace = true +log-once.workspace = true +once_cell.workspace = true +parking_lot.workspace = true +ron.workspace = true +serde.workspace = true +winit.workspace = true +zip.workspace = true +rustyscript.workspace = true + +[features] +editor = [] + +[target.'cfg(not(target_os = "android"))'.dependencies] +rfd.workspace = true + +[build-dependencies] +anyhow = "1.0" +app_dirs2 = "2.5" +reqwest = { version = "0.12", features = ["blocking"] } +zip = "4.3" @@ -0,0 +1,67 @@ +use std::fs::{self, File}; +use std::io::Cursor; + +fn main() -> anyhow::Result<()> { + // todo: move this into the "setup" process + let repo_zip_url = "https://github.com/4tkbytes/dropbear/archive/refs/heads/main.zip"; + let response = reqwest::blocking::get(repo_zip_url) + .map_err(|e| anyhow::anyhow!("Failed to download repo zip: {}", e))? + .bytes() + .map_err(|e| anyhow::anyhow!("Failed to read zip bytes: {}", e))?; + + let reader = Cursor::new(response); + let mut zip = zip::ZipArchive::new(reader) + .map_err(|e| anyhow::anyhow!("Failed to read zip archive: {}", e))?; + + let app_info = app_dirs2::AppInfo { + name: "Eucalyptus", + author: "4tkbytes", + }; + let app_data_dir = app_dirs2::app_root(app_dirs2::AppDataType::UserData, &app_info) + .map_err(|e| anyhow::anyhow!("Could not determine app data directory: {}", e))?; + + fs::create_dir_all(&app_data_dir) + .map_err(|e| anyhow::anyhow!("Failed to create app data directory: {}", e))?; + + let resource_prefix = "dropbear-main/resources/"; + let mut found_resource = false; + for i in 0..zip.len() { + let mut file = zip.by_index(i).unwrap(); + let name = file.name(); + + if name.starts_with(resource_prefix) && !name.ends_with('/') { + found_resource = true; + let rel_path = &name[resource_prefix.len()..]; + let rel_path = rel_path.strip_prefix('/').unwrap_or(rel_path); + let dest_path = app_data_dir.join(rel_path); + + if let Some(parent) = dest_path.parent() { + fs::create_dir_all(parent) + .map_err(|e| anyhow::anyhow!("Failed to create parent directory: {}", e))?; + } + + println!("Copying {} to {:?}", name, dest_path); + + let mut outfile = File::create(&dest_path) + .map_err(|e| anyhow::anyhow!("Failed to create file: {}", e))?; + std::io::copy(&mut file, &mut outfile) + .map_err(|e| anyhow::anyhow!("Failed to copy file: {}", e))?; + } + } + + if !found_resource { + return Err(anyhow::anyhow!( + "No resources folder found in the github repository [4tkbytes/dropbear] :(" + )); + } + + // fuck you windows :( + #[cfg(target_os = "windows")] + { + println!("cargo:rustc-link-arg=/FORCE:MULTIPLE"); + println!("cargo:rustc-link-arg=/NODEFAULTLIB:libcmt.lib"); + } + + println!("cargo:rerun-if-changed=build.rs"); + Ok(()) +} @@ -0,0 +1,126 @@ +use std::collections::HashSet; +use glam::DVec3; +use serde::{Deserialize, Serialize}; +use dropbear_engine::camera::Camera; +use winit::keyboard::KeyCode; + +#[derive(Debug, Clone)] +pub struct CameraComponent { + pub speed: f64, + pub sensitivity: f64, + pub fov_y: f64, + pub camera_type: CameraType +} + +impl CameraComponent { + pub fn new() -> Self { + Self { + speed: 5.0, + sensitivity: 0.002, + fov_y: 60.0, + camera_type: CameraType::Normal, + } + } + + pub fn update(&mut self, camera: &mut Camera) { + camera.speed = self.speed; + camera.sensitivity = self.sensitivity; + camera.fov_y = self.fov_y; + } + + // setting camera offset is just adding the CameraFollowTarget struct + // to the ecs system +} + +pub struct PlayerCamera; + +impl PlayerCamera { + pub fn new() -> CameraComponent { + CameraComponent { + camera_type: CameraType::Player, + ..CameraComponent::new() + } + } + + pub fn handle_keyboard_input( + camera: &mut Camera, + pressed_keys: &HashSet<KeyCode> + ) { + for key in pressed_keys { + match key { + KeyCode::KeyW => camera.move_forwards(), + KeyCode::KeyA => camera.move_left(), + KeyCode::KeyD => camera.move_right(), + KeyCode::KeyS => camera.move_back(), + KeyCode::ShiftLeft => camera.move_down(), + KeyCode::Space => camera.move_up(), + _ => {} + } + } + } + + pub fn handle_mouse_input(camera: &mut Camera, component: &CameraComponent, mouse_delta: Option<(f64, f64)>) { + if let Some((dx, dy)) = mouse_delta { + camera.track_mouse_delta(dx * component.sensitivity, dy * component.sensitivity); + } + } +} + +pub struct DebugCamera; + +impl DebugCamera { + pub fn new() -> CameraComponent { + CameraComponent { + camera_type: CameraType::Debug, + ..CameraComponent::new() + } + } + + pub fn handle_keyboard_input( + camera: &mut Camera, + pressed_keys: &HashSet<KeyCode> + ) { + for key in pressed_keys { + match key { + KeyCode::KeyW => camera.move_forwards(), + KeyCode::KeyA => camera.move_left(), + KeyCode::KeyD => camera.move_right(), + KeyCode::KeyS => camera.move_back(), + KeyCode::ShiftLeft => camera.move_down(), + KeyCode::Space => camera.move_up(), + _ => {} + } + } + } + + pub fn handle_mouse_input(camera: &mut Camera, component: &CameraComponent, mouse_delta: Option<(f64, f64)>) { + if let Some((dx, dy)) = mouse_delta { + camera.track_mouse_delta(dx * component.sensitivity, dy * component.sensitivity); + } + } +} + +#[derive(Debug, Default, Clone)] +pub struct CameraFollowTarget { + pub follow_target: String, + pub offset: DVec3, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum CameraType { + Normal, + Debug, + Player, +} + +impl Default for CameraType { + fn default() -> Self { + Self::Normal + } +} + +#[derive(Debug, Clone)] +pub enum CameraAction { + SetPlayerTarget { entity: hecs::Entity, offset: DVec3 }, + ClearPlayerTarget, +} @@ -1,14 +1,5 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} +pub mod logging; +pub mod camera; +pub mod states; +pub mod utils; +pub mod scripting; @@ -0,0 +1,242 @@ +//! This crate will allow for logging to specific locations. Essentially, just removing boilerplate +//! +//! # Supported logging locations: +//! - Toasts (egui) +//! - Console +//! - File (to be implemented) + +#[cfg(feature = "editor")] +use egui::Context; + +#[cfg(feature = "editor")] +use egui_toast_fork::Toasts; + +#[cfg(feature = "editor")] +use once_cell::sync::Lazy; +#[cfg(feature = "editor")] +use parking_lot::Mutex; + +#[cfg(feature = "editor")] +pub static GLOBAL_TOASTS: Lazy<Mutex<Toasts>> = Lazy::new(|| { + Mutex::new( + Toasts::new() + .anchor(egui::Align2::RIGHT_BOTTOM, (-10.0, -10.0)) + .direction(egui::Direction::BottomUp), + ) +}); + +/// Renders the toasts. Requires an egui context. +/// +/// Useful when paired with a function that contains [`crate`] +#[cfg(feature = "editor")] +pub fn render(context: &Context) { + let mut toasts = GLOBAL_TOASTS.lock(); + toasts.show(context); +} + +/// Fatal log macro +/// +/// This is useful for when there is a fatal error like a missing file cannot be found. +/// +/// This macro creates a toast under the [`egui_toast_fork::ToastKind::Error`] and logs +/// with [`log::error!`] +#[macro_export] +macro_rules! fatal { + ($($arg:tt)*) => {{ + let _msg = format!($($arg)*); + log::error!("{}", _msg); + + #[cfg(feature = "editor")] + { + use egui_toast_fork::{Toast, ToastKind}; + use eucalyptus_core::logging::GLOBAL_TOASTS; + let mut toasts = GLOBAL_TOASTS.lock(); + toasts.add(Toast { + text: _msg.into(), + kind: ToastKind::Error, + options: egui_toast_fork::ToastOptions::default() + .duration_in_seconds(3.0) + .show_progress(true), + style: egui_toast_fork::ToastStyle::default(), + }); + } + }}; +} + +/// Success log macro +/// +/// This is useful for when loading a save is successful. +/// +/// This macro creates a toast under the [`egui_toast_fork::ToastKind::Success`] and logs +/// with [`log::info!`] +#[macro_export] +macro_rules! success { + ($($arg:tt)*) => {{ + let _msg = format!($($arg)*); + log::debug!("{}", _msg); + + #[cfg(feature = "editor")] + { + use egui_toast_fork::{Toast, ToastKind}; + use eucalyptus_core::logging::GLOBAL_TOASTS; + let mut toasts = GLOBAL_TOASTS.lock(); + toasts.add(Toast { + text: _msg.into(), + kind: ToastKind::Success, + options: egui_toast_fork::ToastOptions::default() + .duration_in_seconds(3.0) + .show_progress(true), + style: egui_toast_fork::ToastStyle::default(), + }); + }; + } + }; +} + +/// Warn log macro +/// +/// This is useful for when there is a non-fatal error like unable to copy. +/// +/// This macro creates a toast under the [`egui_toast_fork::ToastKind::Warning`] and logs +/// with [`log::warn!`] +#[macro_export] +macro_rules! warn { + ($($arg:tt)*) => {{ + let _msg = format!($($arg)*); + log::warn!("{}", _msg); + + #[cfg(feature = "editor")] + { + use egui_toast_fork::{Toast, ToastKind}; + use eucalyptus_core::logging::GLOBAL_TOASTS; + let mut toasts = GLOBAL_TOASTS.lock(); + toasts.add(Toast { + text: _msg.into(), + kind: ToastKind::Warning, + options: egui_toast_fork::ToastOptions::default() + .duration_in_seconds(3.0) + .show_progress(true), + style: egui_toast_fork::ToastStyle::default(), + }); + } + }}; +} + +/// Info log macro +/// +/// This is useful for notifying the user of a change, where it doesn't have to be important. +/// +/// This macro creates a toast under the [`egui_toast_fork::ToastKind::Info`] and logs +/// with [`log::debug!`] +#[macro_export] +macro_rules! info { + ($($arg:tt)*) => {{ + let _msg = format!($($arg)*); + log::debug!("{}", _msg); + + #[cfg(feature = "editor")] + { + use egui_toast_fork::{Toast, ToastKind}; + use eucalyptus_core::logging::GLOBAL_TOASTS; + let mut toasts = GLOBAL_TOASTS.lock(); + toasts.add(Toast { + text: _msg.into(), + kind: ToastKind::Info, + options: egui_toast_fork::ToastOptions::default() + .duration_in_seconds(1.0) + .show_progress(false), + style: egui_toast_fork::ToastStyle::default(), + }); + } + }}; +} + +/// Macro for logging info without the console +/// +/// This macro should be "info_toast", however in the case that I ever need to add some more functionality, +/// this would be useful. +/// +/// Its feature-heavy counterpart would be [`crate::success!`]. +/// +/// It creates a toast under [`egui_toast_fork::ToastKind::Info`]. +#[macro_export] +macro_rules! info_without_console { + ($($arg:tt)*) => { + let _msg = format!($($arg)*); + + #[cfg(feature = "editor")] + { + use egui_toast_fork::{Toast, ToastKind}; + use eucalyptus_core::logging::GLOBAL_TOASTS; + let mut toasts = GLOBAL_TOASTS.lock(); + toasts.add(Toast { + text: _msg.into(), + kind: ToastKind::Info, + options: egui_toast_fork::ToastOptions::default() + .duration_in_seconds(1.0) + .show_progress(false), + style: egui_toast_fork::ToastStyle::default(), + }); + } + }; +} + +/// Macro for logging a successful action without the console +/// +/// This macro should be "success_toast", however in the case that I ever need to add some more functionality, +/// this would be useful. +/// +/// Its feature-heavy counterpart would be [`crate::success!`]. +/// +/// It creates a toast under [`egui_toast_fork::ToastKind::Success`]. +#[macro_export] +macro_rules! success_without_console { + ($($arg:tt)*) => { + let _msg = format!($($arg)*); + + #[cfg(feature = "editor")] + { + use egui_toast_fork::{Toast, ToastKind}; + use eucalyptus_core::logging::GLOBAL_TOASTS; + let mut toasts = GLOBAL_TOASTS.lock(); + toasts.add(Toast { + text: _msg.into(), + kind: ToastKind::Success, + options: egui_toast_fork::ToastOptions::default() + .duration_in_seconds(3.0) + .show_progress(true), + style: egui_toast_fork::ToastStyle::default(), + }); + } + }; +} + +/// Macro for logging a successful action without the console +/// +/// This macro should be "success_toast", however in the case that I ever need to add some more functionality, +/// this would be useful. +/// +/// Its feature-heavy counterpart would be [`crate::warn!`]. +/// +/// It creates a toast under [`egui_toast_fork::ToastKind::Warning`]. +#[macro_export] +macro_rules! warn_without_console { + ($($arg:tt)*) => { + let _msg = format!($($arg)*); + + #[cfg(feature = "editor")] + { + use egui_toast_fork::{Toast, ToastKind}; + use eucalyptus_core::logging::GLOBAL_TOASTS; + let mut toasts = GLOBAL_TOASTS.lock(); + toasts.add(Toast { + text: _msg.into(), + kind: ToastKind::Warning, + options: egui_toast_fork::ToastOptions::default() + .duration_in_seconds(3.0) + .show_progress(true), + style: egui_toast_fork::ToastStyle::default(), + }); + } + }; +} @@ -0,0 +1,386 @@ +use glam::DVec3; +use rustyscript::{serde_json, Runtime}; +use serde::{Deserialize, Serialize}; + +use crate::{camera::CameraType, scripting::ScriptableModule}; + +impl ScriptableModule for SerializableCamera { + fn register(runtime: &mut Runtime) -> anyhow::Result<()> { + runtime.register_function("moveForward", Self::move_forward)?; + runtime.register_function("moveBackward", Self::move_backward)?; + runtime.register_function("moveLeft", Self::move_left)?; + runtime.register_function("moveRight", Self::move_right)?; + runtime.register_function("moveUp", Self::move_up)?; + runtime.register_function("moveDown", Self::move_down)?; + + // Mouse look + runtime.register_function("trackMouseDelta", Self::track_mouse_delta)?; + + // Camera switching and management + runtime.register_function("switchToCameraByLabel", Self::switch_to_camera_by_label)?; + runtime.register_function("getActiveCameraLabel", Self::get_active_camera_label)?; + runtime.register_function("getAllCameraLabels", Self::get_all_camera_labels)?; + runtime.register_function("getCamerasByType", Self::get_cameras_by_type)?; + + // Multi-camera manipulation + runtime.register_function("manipulateCameraByLabel", Self::manipulate_camera_by_label)?; + runtime.register_function("getCameraByLabel", Self::get_camera_by_label)?; + runtime.register_function("setCameraByLabel", Self::set_camera_by_label)?; + + Ok(()) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SerializableCamera { + pub label: String, + pub eye: DVec3, + pub target: DVec3, + pub up: DVec3, + pub aspect: f64, + pub fov: f64, + pub near: f64, + pub far: f64, + pub yaw: f64, + pub pitch: f64, + + pub speed: f64, + pub sensitivity: f64, + + pub camera_type: CameraType, +} + +impl SerializableCamera { + /// Moves the camera forward. + /// + /// # Parameters + /// * args[0] - Self as a Camera. The value of the speed can be adjusted + pub fn move_forward(args: &[serde_json::Value]) -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 1 { + return Err(rustyscript::Error::Runtime("moveForward requires 1 arguments".to_string())); + } + + let mut camera: SerializableCamera = serde_json::from_value(args[0].clone()) + .map_err(|e| rustyscript::Error::Runtime(format!("Invalid camera: {}", e)))?; + + let forward = (camera.target - camera.eye).normalize(); + camera.eye += forward * camera.speed; + camera.target += forward * camera.speed; + + serde_json::to_value(camera) + .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize Camera: {}", e))) + } + + /// Moves the camera back. + /// + /// # Parameters + /// * args[0] - Self as a Camera. The value of the speed can be adjusted + pub fn move_backward(args: &[serde_json::Value]) -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 1 { + return Err(rustyscript::Error::Runtime("moveBackward requires 1 arguments".to_string())); + } + + let mut camera: SerializableCamera = serde_json::from_value(args[0].clone()) + .map_err(|e| rustyscript::Error::Runtime(format!("Invalid camera: {}", e)))?; + + let forward = (camera.target - camera.eye).normalize(); + camera.eye -= forward * camera.speed; + camera.target -= forward * camera.speed; + + serde_json::to_value(camera) + .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize Camera: {}", e))) + } + + /// Moves the camera left. + /// + /// # Parameters + /// * args[0] - Self as a Camera. The value of the speed can be adjusted + pub fn move_left(args: &[serde_json::Value]) -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 1 { + return Err(rustyscript::Error::Runtime("moveLeft requires 1 arguments".to_string())); + } + + let mut camera: SerializableCamera = serde_json::from_value(args[0].clone()) + .map_err(|e| rustyscript::Error::Runtime(format!("Invalid camera: {}", e)))?; + + let forward = (camera.target - camera.eye).normalize(); + let right = camera.up.cross(forward).normalize(); + camera.eye -= right * camera.speed; + camera.target -= right * camera.speed; + + serde_json::to_value(camera) + .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize Camera: {}", e))) + } + + /// Moves the camera right. + /// + /// # Parameters + /// * args[0] - Self as a Camera. The value of the speed can be adjusted + pub fn move_right(args: &[serde_json::Value]) -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 1 { + return Err(rustyscript::Error::Runtime("moveRight requires 1 arguments".to_string())); + } + + let mut camera: SerializableCamera = serde_json::from_value(args[0].clone()) + .map_err(|e| rustyscript::Error::Runtime(format!("Invalid camera: {}", e)))?; + + let forward = (camera.target - camera.eye).normalize(); + let right = camera.up.cross(forward).normalize(); + camera.eye += right * camera.speed; + camera.target += right * camera.speed; + + serde_json::to_value(camera) + .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize Camera: {}", e))) + } + + /// Moves the camera up. + /// + /// # Parameters + /// * args[0] - Self as a Camera. The value of the speed can be adjusted + pub fn move_up(args: &[serde_json::Value]) -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 1 { + return Err(rustyscript::Error::Runtime("moveUp requires 1 arguments".to_string())); + } + + let mut camera: SerializableCamera = serde_json::from_value(args[0].clone()) + .map_err(|e| rustyscript::Error::Runtime(format!("Invalid camera: {}", e)))?; + + let up = camera.up.normalize(); + camera.eye += up * camera.speed; + camera.target += up * camera.speed; + + serde_json::to_value(camera) + .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize Camera: {}", e))) + } + + /// Moves the camera down. + /// + /// # Parameters + /// * args[0] - Self as a Camera. The value of the speed can be adjusted + pub fn move_down(args: &[serde_json::Value]) -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 1 { + return Err(rustyscript::Error::Runtime("moveDown requires 1 arguments".to_string())); + } + + let mut camera: SerializableCamera = serde_json::from_value(args[0].clone()) + .map_err(|e| rustyscript::Error::Runtime(format!("Invalid camera: {}", e)))?; + + let up = camera.up.normalize(); + camera.eye -= up * camera.speed; + camera.target -= up * camera.speed; + + serde_json::to_value(camera) + .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize Camera: {}", e))) + } + + /// Sets the tracking of the camera to the mouse delta + /// + /// # Parameters + /// * args[0] - Self as a Camera + /// * args[1] - The dx value of the mouse position as a number + /// * args[2] - The dy value of the mouse position as a number + pub fn track_mouse_delta(args: &[serde_json::Value]) -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 3 { + return Err(rustyscript::Error::Runtime("trackMouseDelta requires 3 arguments (camera, dx, dy)".to_string())); + } + + let mut camera: SerializableCamera = serde_json::from_value(args[0].clone()) + .map_err(|e| rustyscript::Error::Runtime(format!("Invalid camera: {}", e)))?; + + let dx = args[1].as_f64().ok_or_else(|| { + rustyscript::Error::Runtime("dx must be a number".to_string()) + })?; + + let dy = args[2].as_f64().ok_or_else(|| { + rustyscript::Error::Runtime("dy must be a number".to_string()) + })?; + + camera.yaw += dx * camera.sensitivity; + camera.pitch += dy * camera.sensitivity; + + camera.pitch = camera.pitch.clamp(-89.0_f64.to_radians(), 89.0_f64.to_radians()); + + let direction = DVec3::new( + camera.yaw.cos() * camera.pitch.cos(), + camera.pitch.sin(), + camera.yaw.sin() * camera.pitch.cos(), + ); + camera.target = camera.eye + direction; + + serde_json::to_value(camera) + .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize Camera: {}", e))) + } + + pub fn switch_to_camera_by_label(args: &[serde_json::Value]) -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 1 { + return Err(rustyscript::Error::Runtime("switchToCameraByLabel requires 1 argument (label)".to_string())); + } + + let label = args[0].as_str().ok_or_else(|| { + rustyscript::Error::Runtime("Label must be a string".to_string()) + })?; + + let action = CameraAction { + action: "switch_camera".to_string(), + label: Some(label.to_string()), + camera_type: None, + camera_data: None, + property: None, + value: None, + }; + + serde_json::to_value(action) + .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize camera action: {}", e))) + } + + /// Get the label of the currently active camera + pub fn get_active_camera_label(_args: &[serde_json::Value]) -> Result<serde_json::Value, rustyscript::Error> { + let action = CameraAction { + action: "get_active_camera".to_string(), + label: None, + camera_type: None, + camera_data: None, + property: None, + value: None, + }; + + serde_json::to_value(action) + .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize camera action: {}", e))) + } + + /// Get all camera labels in the world + pub fn get_all_camera_labels(_args: &[serde_json::Value]) -> Result<serde_json::Value, rustyscript::Error> { + let action = CameraAction { + action: "get_all_cameras".to_string(), + label: None, + camera_type: None, + camera_data: None, + property: None, + value: None, + }; + + serde_json::to_value(action) + .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize camera action: {}", e))) + } + + /// Get cameras by type + pub fn get_cameras_by_type(args: &[serde_json::Value]) -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 1 { + return Err(rustyscript::Error::Runtime("getCamerasByType requires 1 argument (camera_type)".to_string())); + } + + let camera_type = args[0].as_str().ok_or_else(|| { + rustyscript::Error::Runtime("Camera type must be a string".to_string()) + })?; + + // Validate camera type + match camera_type { + "Normal" | "Debug" | "Player" => { + let action = CameraAction { + action: "get_cameras_by_type".to_string(), + label: None, + camera_type: Some(camera_type.to_string()), + camera_data: None, + property: None, + value: None, + }; + + serde_json::to_value(action) + .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize camera action: {}", e))) + } + _ => Err(rustyscript::Error::Runtime("Invalid camera type. Use 'Normal', 'Debug', or 'Player'".to_string())) + } + } + + /// Manipulate a specific camera by label without switching to it + pub fn manipulate_camera_by_label(args: &[serde_json::Value]) -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 3 { + return Err(rustyscript::Error::Runtime("manipulateCameraByLabel requires 3 arguments (label, property, value)".to_string())); + } + + let label = args[0].as_str().ok_or_else(|| { + rustyscript::Error::Runtime("Label must be a string".to_string()) + })?; + + let property = args[1].as_str().ok_or_else(|| { + rustyscript::Error::Runtime("Property must be a string".to_string()) + })?; + + // Validate property + match property { + "position" | "target" | "speed" | "sensitivity" | "fov" | "yaw" | "pitch" => { + let action = CameraAction { + action: "manipulate_camera".to_string(), + label: Some(label.to_string()), + camera_type: None, + camera_data: None, + property: Some(property.to_string()), + value: Some(args[2].clone()), + }; + + serde_json::to_value(action) + .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize camera action: {}", e))) + } + _ => Err(rustyscript::Error::Runtime("Invalid property. Use 'position', 'target', 'speed', 'sensitivity', 'fov', 'yaw', or 'pitch'".to_string())) + } + } + + /// Get a camera's data by label + pub fn get_camera_by_label(args: &[serde_json::Value]) -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 1 { + return Err(rustyscript::Error::Runtime("getCameraByLabel requires 1 argument (label)".to_string())); + } + + let label = args[0].as_str().ok_or_else(|| { + rustyscript::Error::Runtime("Label must be a string".to_string()) + })?; + + let action = CameraAction { + action: "get_camera_by_label".to_string(), + label: Some(label.to_string()), + camera_type: None, + camera_data: None, + property: None, + value: None, + }; + + serde_json::to_value(action) + .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize camera action: {}", e))) + } + + /// Set a camera's complete data by label + pub fn set_camera_by_label(args: &[serde_json::Value]) -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 2 { + return Err(rustyscript::Error::Runtime("setCameraByLabel requires 2 arguments (label, camera_data)".to_string())); + } + + let label = args[0].as_str().ok_or_else(|| { + rustyscript::Error::Runtime("Label must be a string".to_string()) + })?; + + let camera_data: SerializableCamera = serde_json::from_value(args[1].clone()) + .map_err(|e| rustyscript::Error::Runtime(format!("Invalid camera data: {}", e)))?; + + let action = CameraAction { + action: "set_camera_by_label".to_string(), + label: Some(label.to_string()), + camera_type: None, + camera_data: Some(camera_data), + property: None, + value: None, + }; + + serde_json::to_value(action) + .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize camera action: {}", e))) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CameraAction { + pub action: String, + pub label: Option<String>, + pub camera_type: Option<String>, + pub camera_data: Option<SerializableCamera>, + pub property: Option<String>, + pub value: Option<serde_json::Value>, +} @@ -0,0 +1,332 @@ +use crate::{scripting::ScriptableModule, states::{ModelProperties, PropertyValue}}; +use rustyscript::{serde_json, Runtime}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Serialize, Deserialize)] +pub struct SerializableModelProperties { + pub properties: std::collections::HashMap<String, serde_json::Value>, +} + +impl From<&ModelProperties> for SerializableModelProperties { + fn from(props: &ModelProperties) -> Self { + let mut properties = std::collections::HashMap::new(); + + for (key, value) in props.custom_properties.iter() { + let json_value = match value { + PropertyValue::String(s) => serde_json::Value::String(s.clone()), + PropertyValue::Int(i) => serde_json::Value::Number(serde_json::Number::from(*i)), + PropertyValue::Float(f) => serde_json::Value::Number( + serde_json::Number::from_f64(*f as f64).unwrap_or(serde_json::Number::from(0)) + ), + PropertyValue::Bool(b) => serde_json::Value::Bool(*b), + PropertyValue::Vec3(v) => serde_json::Value::Array(vec![ + serde_json::Value::Number(serde_json::Number::from_f64(v[0] as f64).unwrap()), + serde_json::Value::Number(serde_json::Number::from_f64(v[1] as f64).unwrap()), + serde_json::Value::Number(serde_json::Number::from_f64(v[2] as f64).unwrap()), + ]), + }; + properties.insert(key.clone(), json_value); + } + + Self { properties } + } +} + +impl SerializableModelProperties { + pub fn to_model_properties(&self) -> ModelProperties { + let mut props = ModelProperties::default(); + + for (key, value) in &self.properties { + let property_value = match value { + serde_json::Value::String(s) => PropertyValue::String(s.clone()), + serde_json::Value::Number(n) => { + if let Some(i) = n.as_i64() { + PropertyValue::Int(i) + } else if let Some(f) = n.as_f64() { + PropertyValue::Float(f) + } else { + continue; + } + }, + serde_json::Value::Bool(b) => PropertyValue::Bool(*b), + serde_json::Value::Array(arr) => { + if arr.len() == 3 { + if let (Some(x), Some(y), Some(z)) = ( + arr[0].as_f64(), + arr[1].as_f64(), + arr[2].as_f64(), + ) { + PropertyValue::Vec3([x as f32, y as f32, z as f32]) + } else { + continue; + } + } else { + continue; + } + }, + _ => continue, + }; + props.set_property(key.clone(), property_value); + } + + props + } +} + +impl ScriptableModule for ModelProperties { + fn register(runtime: &mut Runtime) -> anyhow::Result<()> { + runtime.register_function("getProperty", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 2 { + return Err(rustyscript::Error::Runtime("getProperty requires 2 arguments (properties, key)".to_string())); + } + + let props: SerializableModelProperties = serde_json::from_value(args[0].clone()) + .map_err(|e| rustyscript::Error::Runtime(format!("Invalid properties: {}", e)))?; + + let key = args[1].as_str() + .ok_or_else(|| rustyscript::Error::Runtime("Key must be a string".to_string()))?; + + let value = props.properties.get(key) + .cloned() + .unwrap_or(serde_json::Value::Null); + + Ok(value) + })?; + + // Set property (string) + runtime.register_function("setPropertyString", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 3 { + return Err(rustyscript::Error::Runtime("setPropertyString requires 3 arguments (properties, key, value)".to_string())); + } + + let mut props: SerializableModelProperties = serde_json::from_value(args[0].clone()) + .map_err(|e| rustyscript::Error::Runtime(format!("Invalid properties: {}", e)))?; + + let key = args[1].as_str() + .ok_or_else(|| rustyscript::Error::Runtime("Key must be a string".to_string()))?; + + let value = args[2].as_str() + .ok_or_else(|| rustyscript::Error::Runtime("Value must be a string".to_string()))?; + + props.properties.insert(key.to_string(), serde_json::Value::String(value.to_string())); + + serde_json::to_value(props) + .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize properties: {}", e))) + })?; + + // Set property (int) + runtime.register_function("setPropertyInt", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 3 { + return Err(rustyscript::Error::Runtime("setPropertyInt requires 3 arguments (properties, key, value)".to_string())); + } + + let mut props: SerializableModelProperties = serde_json::from_value(args[0].clone()) + .map_err(|e| rustyscript::Error::Runtime(format!("Invalid properties: {}", e)))?; + + let key = args[1].as_str() + .ok_or_else(|| rustyscript::Error::Runtime("Key must be a string".to_string()))?; + + let value = args[2].as_i64() + .ok_or_else(|| rustyscript::Error::Runtime("Value must be an integer".to_string()))?; + + props.properties.insert(key.to_string(), serde_json::Value::Number(serde_json::Number::from(value))); + + serde_json::to_value(props) + .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize properties: {}", e))) + })?; + + // Set property (float) + runtime.register_function("setPropertyFloat", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 3 { + return Err(rustyscript::Error::Runtime("setPropertyFloat requires 3 arguments (properties, key, value)".to_string())); + } + + let mut props: SerializableModelProperties = serde_json::from_value(args[0].clone()) + .map_err(|e| rustyscript::Error::Runtime(format!("Invalid properties: {}", e)))?; + + let key = args[1].as_str() + .ok_or_else(|| rustyscript::Error::Runtime("Key must be a string".to_string()))?; + + let value = args[2].as_f64() + .ok_or_else(|| rustyscript::Error::Runtime("Value must be a number".to_string()))?; + + props.properties.insert(key.to_string(), serde_json::Value::Number( + serde_json::Number::from_f64(value).unwrap_or(serde_json::Number::from(0)) + )); + + serde_json::to_value(props) + .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize properties: {}", e))) + })?; + + // Set property (bool) + runtime.register_function("setPropertyBool", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 3 { + return Err(rustyscript::Error::Runtime("setPropertyBool requires 3 arguments (properties, key, value)".to_string())); + } + + let mut props: SerializableModelProperties = serde_json::from_value(args[0].clone()) + .map_err(|e| rustyscript::Error::Runtime(format!("Invalid properties: {}", e)))?; + + let key = args[1].as_str() + .ok_or_else(|| rustyscript::Error::Runtime("Key must be a string".to_string()))?; + + let value = args[2].as_bool() + .ok_or_else(|| rustyscript::Error::Runtime("Value must be a boolean".to_string()))?; + + props.properties.insert(key.to_string(), serde_json::Value::Bool(value)); + + serde_json::to_value(props) + .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize properties: {}", e))) + })?; + + // Set property (Vec3) + runtime.register_function("setPropertyVec3", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 3 { + return Err(rustyscript::Error::Runtime("setPropertyVec3 requires 3 arguments (properties, key, value)".to_string())); + } + + let mut props: SerializableModelProperties = serde_json::from_value(args[0].clone()) + .map_err(|e| rustyscript::Error::Runtime(format!("Invalid properties: {}", e)))?; + + let key = args[1].as_str() + .ok_or_else(|| rustyscript::Error::Runtime("Key must be a string".to_string()))?; + + let value = args[2].as_array() + .ok_or_else(|| rustyscript::Error::Runtime("Value must be an array".to_string()))?; + + if value.len() != 3 { + return Err(rustyscript::Error::Runtime("Vec3 array must have exactly 3 elements".to_string())); + } + + let x = value[0].as_f64().ok_or_else(|| rustyscript::Error::Runtime("Vec3 elements must be numbers".to_string()))?; + let y = value[1].as_f64().ok_or_else(|| rustyscript::Error::Runtime("Vec3 elements must be numbers".to_string()))?; + let z = value[2].as_f64().ok_or_else(|| rustyscript::Error::Runtime("Vec3 elements must be numbers".to_string()))?; + + let vec3_array = serde_json::Value::Array(vec![ + serde_json::Value::Number(serde_json::Number::from_f64(x).unwrap()), + serde_json::Value::Number(serde_json::Number::from_f64(y).unwrap()), + serde_json::Value::Number(serde_json::Number::from_f64(z).unwrap()), + ]); + + props.properties.insert(key.to_string(), vec3_array); + + serde_json::to_value(props) + .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize properties: {}", e))) + })?; + + // Typed getters + runtime.register_function("getString", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 2 { + return Err(rustyscript::Error::Runtime("getString requires 2 arguments (properties, key)".to_string())); + } + + let props: SerializableModelProperties = serde_json::from_value(args[0].clone()) + .map_err(|e| rustyscript::Error::Runtime(format!("Invalid properties: {}", e)))?; + + let key = args[1].as_str() + .ok_or_else(|| rustyscript::Error::Runtime("Key must be a string".to_string()))?; + + let value = props.properties.get(key) + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + + Ok(serde_json::Value::String(value)) + })?; + + runtime.register_function("getInt", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 2 { + return Err(rustyscript::Error::Runtime("getInt requires 2 arguments (properties, key)".to_string())); + } + + let props: SerializableModelProperties = serde_json::from_value(args[0].clone()) + .map_err(|e| rustyscript::Error::Runtime(format!("Invalid properties: {}", e)))?; + + let key = args[1].as_str() + .ok_or_else(|| rustyscript::Error::Runtime("Key must be a string".to_string()))?; + + let value = props.properties.get(key) + .and_then(|v| v.as_i64()) + .unwrap_or(0); + + Ok(serde_json::Value::Number(serde_json::Number::from(value))) + })?; + + runtime.register_function("getFloat", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 2 { + return Err(rustyscript::Error::Runtime("getFloat requires 2 arguments (properties, key)".to_string())); + } + + let props: SerializableModelProperties = serde_json::from_value(args[0].clone()) + .map_err(|e| rustyscript::Error::Runtime(format!("Invalid properties: {}", e)))?; + + let key = args[1].as_str() + .ok_or_else(|| rustyscript::Error::Runtime("Key must be a string".to_string()))?; + + let value = props.properties.get(key) + .and_then(|v| v.as_f64()) + .unwrap_or(0.0); + + Ok(serde_json::Value::Number(serde_json::Number::from_f64(value).unwrap())) + })?; + + runtime.register_function("getBool", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 2 { + return Err(rustyscript::Error::Runtime("getBool requires 2 arguments (properties, key)".to_string())); + } + + let props: SerializableModelProperties = serde_json::from_value(args[0].clone()) + .map_err(|e| rustyscript::Error::Runtime(format!("Invalid properties: {}", e)))?; + + let key = args[1].as_str() + .ok_or_else(|| rustyscript::Error::Runtime("Key must be a string".to_string()))?; + + let value = props.properties.get(key) + .and_then(|v| v.as_bool()) + .unwrap_or(false); + + Ok(serde_json::Value::Bool(value)) + })?; + + runtime.register_function("getVec3", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 2 { + return Err(rustyscript::Error::Runtime("getVec3 requires 2 arguments (properties, key)".to_string())); + } + + let props: SerializableModelProperties = serde_json::from_value(args[0].clone()) + .map_err(|e| rustyscript::Error::Runtime(format!("Invalid properties: {}", e)))?; + + let key = args[1].as_str() + .ok_or_else(|| rustyscript::Error::Runtime("Key must be a string".to_string()))?; + + let value = props.properties.get(key) + .and_then(|v| v.as_array()) + .cloned() + .unwrap_or_else(|| vec![ + serde_json::Value::Number(serde_json::Number::from(0)), + serde_json::Value::Number(serde_json::Number::from(0)), + serde_json::Value::Number(serde_json::Number::from(0)), + ]); + + Ok(serde_json::Value::Array(value)) + })?; + + runtime.register_function("hasProperty", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 2 { + return Err(rustyscript::Error::Runtime("hasProperty requires 2 arguments (properties, key)".to_string())); + } + + let props: SerializableModelProperties = serde_json::from_value(args[0].clone()) + .map_err(|e| rustyscript::Error::Runtime(format!("Invalid properties: {}", e)))?; + + let key = args[1].as_str() + .ok_or_else(|| rustyscript::Error::Runtime("Key must be a string".to_string()))?; + + let has_property = props.properties.contains_key(key); + Ok(serde_json::Value::Bool(has_property)) + })?; + + log::info!("[Script] Initialised entity custom properties module"); + Ok(()) + } +} @@ -0,0 +1,288 @@ +use std::{collections::{HashMap, HashSet}, time::{Duration, Instant}}; + +use rustyscript::{serde_json, Runtime}; +use serde::{Deserialize, Serialize}; +use winit::{event::MouseButton, keyboard::KeyCode}; + +use crate::scripting::ScriptableModule; + +#[derive(Clone)] +pub struct InputState { + #[allow(dead_code)] + pub last_key_press_times: HashMap<KeyCode, Instant>, + #[allow(dead_code)] + pub double_press_threshold: Duration, + pub mouse_pos: (f64, f64), + pub mouse_button: HashSet<MouseButton>, + pub pressed_keys: HashSet<KeyCode>, + pub mouse_delta: Option<(f64, f64)>, + pub is_cursor_locked: bool, +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct SerializableInputState { + pub mouse_pos: (f64, f64), + pub pressed_keys: Vec<String>, + pub mouse_delta: Option<(f64, f64)>, + pub is_cursor_locked: bool, +} + +impl From<&InputState> for SerializableInputState { + fn from(input_state: &InputState) -> Self { + Self { + mouse_pos: input_state.mouse_pos, + pressed_keys: input_state.pressed_keys.iter() + .filter_map(|key| keycode_to_string(*key)) + .collect(), + mouse_delta: input_state.mouse_delta, + is_cursor_locked: input_state.is_cursor_locked, + } + } +} + +impl Default for InputState { + fn default() -> Self { + Self::new() + } +} + +impl ScriptableModule for InputState { + fn register(runtime: &mut Runtime) -> anyhow::Result<()> { + runtime.register_function("isKeyPressed", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 2 { + return Err(rustyscript::Error::Runtime("isKeyPressed requires 2 arguments (inputState, keyCode)".to_string())); + } + + let input_state: SerializableInputState = serde_json::from_value(args[0].clone()) + .map_err(|e| rustyscript::Error::Runtime(format!("Invalid input state: {}", e)))?; + + let key_code_str = args[1].as_str() + .ok_or_else(|| rustyscript::Error::Runtime("Key code must be a string".to_string()))?; + + let is_pressed = input_state.pressed_keys.contains(&key_code_str.to_string()); + Ok(serde_json::Value::Bool(is_pressed)) + })?; + + runtime.register_function("getMouseX", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 1 { + return Err(rustyscript::Error::Runtime("getMouseX requires 1 argument (inputState)".to_string())); + } + + let input_state: SerializableInputState = serde_json::from_value(args[0].clone()) + .map_err(|e| rustyscript::Error::Runtime(format!("Invalid input state: {}", e)))?; + + Ok(serde_json::Value::Number(serde_json::Number::from_f64(input_state.mouse_pos.0).unwrap())) + })?; + + runtime.register_function("getMouseY", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 1 { + return Err(rustyscript::Error::Runtime("getMouseY requires 1 argument (inputState)".to_string())); + } + + let input_state: SerializableInputState = serde_json::from_value(args[0].clone()) + .map_err(|e| rustyscript::Error::Runtime(format!("Invalid input state: {}", e)))?; + + Ok(serde_json::Value::Number(serde_json::Number::from_f64(input_state.mouse_pos.1).unwrap())) + })?; + + runtime.register_function("getMouseDeltaX", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 1 { + return Err(rustyscript::Error::Runtime("getMouseDeltaX requires 1 argument (inputState)".to_string())); + } + + let input_state: SerializableInputState = serde_json::from_value(args[0].clone()) + .map_err(|e| rustyscript::Error::Runtime(format!("Invalid input state: {}", e)))?; + + let delta_x = input_state.mouse_delta.map(|d| d.0).unwrap_or(0.0); + Ok(serde_json::Value::Number(serde_json::Number::from_f64(delta_x).unwrap())) + })?; + + runtime.register_function("getMouseDeltaY", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 1 { + return Err(rustyscript::Error::Runtime("getMouseDeltaY requires 1 argument (inputState)".to_string())); + } + + let input_state: SerializableInputState = serde_json::from_value(args[0].clone()) + .map_err(|e| rustyscript::Error::Runtime(format!("Invalid input state: {}", e)))?; + + let delta_y = input_state.mouse_delta.map(|d| d.1).unwrap_or(0.0); + Ok(serde_json::Value::Number(serde_json::Number::from_f64(delta_y).unwrap())) + })?; + + runtime.register_function("lockCursor", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 1 { + return Err(rustyscript::Error::Runtime("lockCursor requires 1 argument (locked)".to_string())); + } + + let _locked = args[0].as_bool() + .ok_or_else(|| rustyscript::Error::Runtime("Locked parameter must be a boolean".to_string()))?; + + // This function would need to communicate back to the engine to actually lock the cursor + // For now, just return success + Ok(serde_json::Value::Bool(true)) + })?; + + log::info!("[Script] Initialised input module"); + Ok(()) + } +} + +impl InputState { + pub fn new() -> Self { + Self { + mouse_pos: Default::default(), + mouse_button: Default::default(), + pressed_keys: HashSet::new(), + last_key_press_times: HashMap::new(), + double_press_threshold: Duration::from_millis(300), + mouse_delta: None, + is_cursor_locked: false, + } + } + + pub fn lock_cursor(&mut self, toggle: bool) { + self.is_cursor_locked = toggle; + } +} + +/// Helper function to convert string to KeyCode +#[allow(dead_code)] +fn string_to_keycode(key_str: &str) -> Option<KeyCode> { + match key_str { + "KeyA" => Some(KeyCode::KeyA), + "KeyB" => Some(KeyCode::KeyB), + "KeyC" => Some(KeyCode::KeyC), + "KeyD" => Some(KeyCode::KeyD), + "KeyE" => Some(KeyCode::KeyE), + "KeyF" => Some(KeyCode::KeyF), + "KeyG" => Some(KeyCode::KeyG), + "KeyH" => Some(KeyCode::KeyH), + "KeyI" => Some(KeyCode::KeyI), + "KeyJ" => Some(KeyCode::KeyJ), + "KeyK" => Some(KeyCode::KeyK), + "KeyL" => Some(KeyCode::KeyL), + "KeyM" => Some(KeyCode::KeyM), + "KeyN" => Some(KeyCode::KeyN), + "KeyO" => Some(KeyCode::KeyO), + "KeyP" => Some(KeyCode::KeyP), + "KeyQ" => Some(KeyCode::KeyQ), + "KeyR" => Some(KeyCode::KeyR), + "KeyS" => Some(KeyCode::KeyS), + "KeyT" => Some(KeyCode::KeyT), + "KeyU" => Some(KeyCode::KeyU), + "KeyV" => Some(KeyCode::KeyV), + "KeyW" => Some(KeyCode::KeyW), + "KeyX" => Some(KeyCode::KeyX), + "KeyY" => Some(KeyCode::KeyY), + "KeyZ" => Some(KeyCode::KeyZ), + "Space" => Some(KeyCode::Space), + "ShiftLeft" => Some(KeyCode::ShiftLeft), + "ShiftRight" => Some(KeyCode::ShiftRight), + "ControlLeft" => Some(KeyCode::ControlLeft), + "ControlRight" => Some(KeyCode::ControlRight), + "AltLeft" => Some(KeyCode::AltLeft), + "AltRight" => Some(KeyCode::AltRight), + "Escape" => Some(KeyCode::Escape), + "Enter" => Some(KeyCode::Enter), + "Tab" => Some(KeyCode::Tab), + "ArrowUp" => Some(KeyCode::ArrowUp), + "ArrowDown" => Some(KeyCode::ArrowDown), + "ArrowLeft" => Some(KeyCode::ArrowLeft), + "ArrowRight" => Some(KeyCode::ArrowRight), + "Digit0" => Some(KeyCode::Digit0), + "Digit1" => Some(KeyCode::Digit1), + "Digit2" => Some(KeyCode::Digit2), + "Digit3" => Some(KeyCode::Digit3), + "Digit4" => Some(KeyCode::Digit4), + "Digit5" => Some(KeyCode::Digit5), + "Digit6" => Some(KeyCode::Digit6), + "Digit7" => Some(KeyCode::Digit7), + "Digit8" => Some(KeyCode::Digit8), + "Digit9" => Some(KeyCode::Digit9), + "F1" => Some(KeyCode::F1), + "F2" => Some(KeyCode::F2), + "F3" => Some(KeyCode::F3), + "F4" => Some(KeyCode::F4), + "F5" => Some(KeyCode::F5), + "F6" => Some(KeyCode::F6), + "F7" => Some(KeyCode::F7), + "F8" => Some(KeyCode::F8), + "F9" => Some(KeyCode::F9), + "F10" => Some(KeyCode::F10), + "F11" => Some(KeyCode::F11), + "F12" => Some(KeyCode::F12), + _ => None, + } +} + +/// Helper function to convert KeyCode to string +fn keycode_to_string(key_code: KeyCode) -> Option<String> { + match key_code { + KeyCode::KeyA => Some("KeyA".to_string()), + KeyCode::KeyB => Some("KeyB".to_string()), + KeyCode::KeyC => Some("KeyC".to_string()), + KeyCode::KeyD => Some("KeyD".to_string()), + KeyCode::KeyE => Some("KeyE".to_string()), + KeyCode::KeyF => Some("KeyF".to_string()), + KeyCode::KeyG => Some("KeyG".to_string()), + KeyCode::KeyH => Some("KeyH".to_string()), + KeyCode::KeyI => Some("KeyI".to_string()), + KeyCode::KeyJ => Some("KeyJ".to_string()), + KeyCode::KeyK => Some("KeyK".to_string()), + KeyCode::KeyL => Some("KeyL".to_string()), + KeyCode::KeyM => Some("KeyM".to_string()), + KeyCode::KeyN => Some("KeyN".to_string()), + KeyCode::KeyO => Some("KeyO".to_string()), + KeyCode::KeyP => Some("KeyP".to_string()), + KeyCode::KeyQ => Some("KeyQ".to_string()), + KeyCode::KeyR => Some("KeyR".to_string()), + KeyCode::KeyS => Some("KeyS".to_string()), + KeyCode::KeyT => Some("KeyT".to_string()), + KeyCode::KeyU => Some("KeyU".to_string()), + KeyCode::KeyV => Some("KeyV".to_string()), + KeyCode::KeyW => Some("KeyW".to_string()), + KeyCode::KeyX => Some("KeyX".to_string()), + KeyCode::KeyY => Some("KeyY".to_string()), + KeyCode::KeyZ => Some("KeyZ".to_string()), + KeyCode::Space => Some("Space".to_string()), + KeyCode::ShiftLeft => Some("ShiftLeft".to_string()), + KeyCode::ShiftRight => Some("ShiftRight".to_string()), + KeyCode::ControlLeft => Some("ControlLeft".to_string()), + KeyCode::ControlRight => Some("ControlRight".to_string()), + KeyCode::AltLeft => Some("AltLeft".to_string()), + KeyCode::AltRight => Some("AltRight".to_string()), + KeyCode::Escape => Some("Escape".to_string()), + KeyCode::Enter => Some("Enter".to_string()), + KeyCode::Tab => Some("Tab".to_string()), + KeyCode::ArrowUp => Some("ArrowUp".to_string()), + KeyCode::ArrowDown => Some("ArrowDown".to_string()), + KeyCode::ArrowLeft => Some("ArrowLeft".to_string()), + KeyCode::ArrowRight => Some("ArrowRight".to_string()), + KeyCode::Digit0 => Some("Digit0".to_string()), + KeyCode::Digit1 => Some("Digit1".to_string()), + KeyCode::Digit2 => Some("Digit2".to_string()), + KeyCode::Digit3 => Some("Digit3".to_string()), + KeyCode::Digit4 => Some("Digit4".to_string()), + KeyCode::Digit5 => Some("Digit5".to_string()), + KeyCode::Digit6 => Some("Digit6".to_string()), + KeyCode::Digit7 => Some("Digit7".to_string()), + KeyCode::Digit8 => Some("Digit8".to_string()), + KeyCode::Digit9 => Some("Digit9".to_string()), + KeyCode::F1 => Some("F1".to_string()), + KeyCode::F2 => Some("F2".to_string()), + KeyCode::F3 => Some("F3".to_string()), + KeyCode::F4 => Some("F4".to_string()), + KeyCode::F5 => Some("F5".to_string()), + KeyCode::F6 => Some("F6".to_string()), + KeyCode::F7 => Some("F7".to_string()), + KeyCode::F8 => Some("F8".to_string()), + KeyCode::F9 => Some("F9".to_string()), + KeyCode::F10 => Some("F10".to_string()), + KeyCode::F11 => Some("F11".to_string()), + KeyCode::F12 => Some("F12".to_string()), + _ => None, + } +} + +#[derive(Clone, Copy)] +pub struct Key(pub KeyCode); @@ -0,0 +1,11 @@ +use rustyscript::Runtime; + +use crate::scripting::ScriptableModule; + +pub struct Lighting; + +impl ScriptableModule for Lighting { + fn register(_runtime: &mut Runtime) -> anyhow::Result<()> { + Ok(()) + } +} @@ -0,0 +1,166 @@ +use dropbear_engine::entity::Transform; +use glam::{DQuat, DVec3}; +use rustyscript::{serde_json, Runtime}; + +use crate::scripting::ScriptableModule; + +pub struct Math; + +impl ScriptableModule for Math { + fn register(runtime: &mut Runtime) -> anyhow::Result<()> { + runtime.register_function("createTransform", |_args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + let transform = Transform::new(); + serde_json::to_value(transform) + .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize Transform: {}", e))) + })?; + + runtime.register_function("transformTranslate", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 2 { + return Err(rustyscript::Error::Runtime("transformTranslate requires 2 arguments".to_string())); + } + + let mut transform: Transform = serde_json::from_value(args[0].clone()) + .map_err(|e| rustyscript::Error::Runtime(format!("Invalid transform: {}", e)))?; + + let translation = if let Some(array) = args[1].as_array() { + if array.len() != 3 { + return Err(rustyscript::Error::Runtime("Translation array must have 3 elements".to_string())); + } + DVec3::new( + array[0].as_f64().unwrap_or(0.0), + array[1].as_f64().unwrap_or(0.0), + array[2].as_f64().unwrap_or(0.0), + ) + } else { + return Err(rustyscript::Error::Runtime("Translation must be an array".to_string())); + }; + + transform.position += translation; + serde_json::to_value(transform) + .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize Transform: {}", e))) + })?; + + runtime.register_function("transformRotateX", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 2 { + return Err(rustyscript::Error::Runtime("transformRotateX requires 2 arguments".to_string())); + } + + let mut transform: Transform = serde_json::from_value(args[0].clone()) + .map_err(|e| rustyscript::Error::Runtime(format!("Invalid transform: {}", e)))?; + + let angle = args[1].as_f64().unwrap_or(0.0); + let rotation = DQuat::from_rotation_x(angle); + transform.rotation = rotation * transform.rotation; + + serde_json::to_value(transform) + .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize Transform: {}", e))) + })?; + + runtime.register_function("transformRotateY", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 2 { + return Err(rustyscript::Error::Runtime("transformRotateY requires 2 arguments".to_string())); + } + + let mut transform: Transform = serde_json::from_value(args[0].clone()) + .map_err(|e| rustyscript::Error::Runtime(format!("Invalid transform: {}", e)))?; + + let angle = args[1].as_f64().unwrap_or(0.0); + let rotation = DQuat::from_rotation_y(angle); + transform.rotation = rotation * transform.rotation; + + serde_json::to_value(transform) + .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize Transform: {}", e))) + })?; + + runtime.register_function("transformRotateZ", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 2 { + return Err(rustyscript::Error::Runtime("transformRotateZ requires 2 arguments".to_string())); + } + + let mut transform: Transform = serde_json::from_value(args[0].clone()) + .map_err(|e| rustyscript::Error::Runtime(format!("Invalid transform: {}", e)))?; + + let angle = args[1].as_f64().unwrap_or(0.0); + let rotation = DQuat::from_rotation_z(angle); + transform.rotation = rotation * transform.rotation; + + serde_json::to_value(transform) + .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize Transform: {}", e))) + })?; + + runtime.register_function("transformScale", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 2 { + return Err(rustyscript::Error::Runtime("transformScale requires 2 arguments".to_string())); + } + + let mut transform: Transform = serde_json::from_value(args[0].clone()) + .map_err(|e| rustyscript::Error::Runtime(format!("Invalid transform: {}", e)))?; + + let scale = if let Some(num) = args[1].as_f64() { + DVec3::splat(num) + } else if let Some(array) = args[1].as_array() { + if array.len() != 3 { + return Err(rustyscript::Error::Runtime("Scale array must have 3 elements".to_string())); + } + DVec3::new( + array[0].as_f64().unwrap_or(1.0), + array[1].as_f64().unwrap_or(1.0), + array[2].as_f64().unwrap_or(1.0), + ) + } else { + return Err(rustyscript::Error::Runtime("Scale must be a number or array".to_string())); + }; + + transform.scale *= scale; + serde_json::to_value(transform) + .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize Transform: {}", e))) + })?; + + // this shouldn't be here as there is no need for a matrix... + runtime.register_function("transformMatrix", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 1 { + return Err(rustyscript::Error::Runtime("transformMatrix requires 1 argument".to_string())); + } + + let transform: Transform = serde_json::from_value(args[0].clone()) + .map_err(|e| rustyscript::Error::Runtime(format!("Invalid transform: {}", e)))?; + + let matrix = transform.matrix(); + serde_json::to_value(matrix) + .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize matrix: {}", e))) + })?; + + runtime.register_function("createVec3", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + let x = args.get(0).and_then(|v| v.as_f64()).unwrap_or(0.0); + let y = args.get(1).and_then(|v| v.as_f64()).unwrap_or(0.0); + let z = args.get(2).and_then(|v| v.as_f64()).unwrap_or(0.0); + + let vec3 = DVec3::new(x, y, z); + serde_json::to_value(vec3) + .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize Vec3: {}", e))) + })?; + + runtime.register_function("createQuatIdentity", |_args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + let quat = DQuat::IDENTITY; + serde_json::to_value(quat) + .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize Quaternion: {}", e))) + })?; + + runtime.register_function("createQuatFromEuler", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + if args.len() != 3 { + return Err(rustyscript::Error::Runtime("createQuatFromEuler requires 3 arguments".to_string())); + } + + let x = args[0].as_f64().unwrap_or(0.0); + let y = args[1].as_f64().unwrap_or(0.0); + let z = args[2].as_f64().unwrap_or(0.0); + + let quat = DQuat::from_euler(glam::EulerRot::XYZ, x, y, z); + serde_json::to_value(quat) + .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize Quaternion: {}", e))) + })?; + + log::info!("[Script] Initialised math module"); + Ok(()) + } +} @@ -0,0 +1,833 @@ +pub mod camera; +pub mod entity; +pub mod math; +pub mod input; +pub mod lighting; + +use dropbear_engine::camera::Camera; +use dropbear_engine::entity::{AdoptedEntity, Transform}; +use dropbear_engine::lighting::{Light, LightComponent}; +use glam::DVec3; +use hecs::World; +use rustyscript::{serde_json, Module, ModuleHandle, Runtime, RuntimeOptions}; +use std::path::PathBuf; +use std::{collections::HashMap, fs}; +use crate::camera::CameraComponent; +use crate::states::{EntityNode, ModelProperties, ScriptComponent, PROJECT, SOURCE}; + +/// A trait that describes a module that can be registered. +pub trait ScriptableModule { + /// Registers the functions for the dropbear typescript API + fn register(runtime: &mut Runtime) -> anyhow::Result<()>; + // /// Gathers the information into a serializable format that can be sent over to the + // /// dropbear typescript API + // fn gather(world: &World, entity_id: hecs::Entity); + // /// Fetches the mutated information from the dropbear typescript module and applys it to the world + // fn release(world: &mut World, entity_id: hecs::Entity, data: &serde_json::Value) -> anyhow::Result<()>; +} + +pub const TEMPLATE_SCRIPT: &'static str = include_str!("../../../resources/template.ts"); + +pub enum ScriptAction { + AttachScript { + script_path: PathBuf, + script_name: String, + }, + CreateAndAttachScript { + script_path: PathBuf, + script_name: String, + }, + RemoveScript, + EditScript, +} + +pub fn move_script_to_src(script_path: &PathBuf) -> anyhow::Result<PathBuf> { + let project_path = { + let project = PROJECT.read(); + project.project_path.clone() + }; + + let src_path = { + let source_config = SOURCE.read(); + source_config.path.clone() + }; + + let scripts_dir = src_path; + fs::create_dir_all(&scripts_dir)?; + + let filename = script_path + .file_name() + .ok_or_else(|| anyhow::anyhow!("Invalid script path: no filename"))?; + + let dest_path = scripts_dir.join(filename); + + if dest_path.exists() { + log::info!( + "Script file already exists at {:?}, returning existing path", + dest_path + ); + return Ok(dest_path); + } + + const MAX_RETRIES: usize = 5; + const RETRY_DELAY_MS: u64 = 60; + + let mut last_err: Option<std::io::Error> = None; + for attempt in 0..=MAX_RETRIES { + match fs::copy(script_path, &dest_path) { + Ok(_) => { + log::info!("Copied script from {:?} to {:?}", script_path, dest_path); + last_err = None; + break; + } + Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => { + log::warn!( + "Script file already exists at {:?}, continuing anyway", + dest_path + ); + last_err = None; + break; + } + Err(e) => { + if e.raw_os_error() == Some(32) && attempt < MAX_RETRIES { + log::warn!( + "Sharing violation copying script (attempt {}/{}). Retrying in {}ms: {}", + attempt + 1, + MAX_RETRIES, + RETRY_DELAY_MS, + e + ); + std::thread::sleep(std::time::Duration::from_millis(RETRY_DELAY_MS)); + last_err = Some(e); + continue; + } else { + return Err(e.into()); + } + } + } + } + if let Some(e) = last_err { + return Err(e.into()); + } + + { + let source_config = SOURCE.read(); + source_config.write_to(&project_path)?; + } + + log::info!("Moved script from {:?} to {:?}", script_path, dest_path); + Ok(dest_path) +} + +pub fn convert_entity_to_group( + world: &World, + entity_id: hecs::Entity, +) -> anyhow::Result<EntityNode> { + if let Ok(mut query) = world.query_one::<(&AdoptedEntity, &Transform)>(entity_id) { + if let Some((adopted, _transform)) = query.get() { + let entity_name = adopted.model().label.clone(); + + let script_node = if let Ok(script) = world.get::<&ScriptComponent>(entity_id) { + Some(EntityNode::Script { + name: script.name.clone(), + path: script.path.clone(), + }) + } else { + None + }; + + let entity_node = EntityNode::Entity { + id: entity_id, + name: entity_name.clone(), + }; + + if let Some(script_node) = script_node { + Ok(EntityNode::Group { + name: entity_name, + children: vec![entity_node, script_node], + collapsed: false, + }) + } else { + Ok(entity_node) + } + } else { + Err(anyhow::anyhow!("Failed to get entity components")) + } + } else { + Err(anyhow::anyhow!("Failed to query entity {:?}", entity_id)) + } +} + +pub fn attach_script_to_entity( + world: &mut World, + entity_id: hecs::Entity, + script_component: ScriptComponent, +) -> anyhow::Result<()> { + if let Err(e) = world.insert_one(entity_id, script_component) { + return Err(anyhow::anyhow!("Failed to attach script to entity: {}", e)); + } + + log::info!("Successfully attached script to entity {:?}", entity_id); + Ok(()) +} + +pub struct ScriptManager { + pub runtime: Runtime, + compiled_scripts: HashMap<String, ModuleHandle>, + entity_script_data: HashMap<hecs::Entity, serde_json::Value>, +} + +impl ScriptManager { + pub fn new() -> anyhow::Result<Self> { + let mut runtime = Runtime::new(RuntimeOptions::default())?; + let dropbear_content = include_str!("../../../resources/dropbear.ts"); + let dropbear_module = Module::new("./dropbear.ts", dropbear_content); + runtime.load_module(&dropbear_module)?; + log::debug!("Loaded dropbear module"); + + let mut result = Self { + runtime, + compiled_scripts: HashMap::new(), + entity_script_data: HashMap::new(), + }; + + result = result + .register_module::<input::InputState>()? + .register_module::<math::Math>()? + .register_module::<ModelProperties>()? + .register_module::<camera::SerializableCamera>()? + .register_module::<lighting::Lighting>()?; + + // Register utility functions + result.runtime.register_function("log", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + let msg = args.get(0) + .and_then(|v| v.as_str()) + .unwrap_or("undefined"); + println!("[Script] {}", msg); + Ok(serde_json::Value::Null) + })?; + + result.runtime.register_function("time", |_args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { + let time = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs_f64(); + Ok(serde_json::Value::Number(serde_json::Number::from_f64(time).unwrap())) + })?; + log::debug!("Initialised ScriptManager"); + Ok(result) + } + + /// Allows for the registering of other modules to use with the dropbear typescript + /// API + /// + /// # Parameters + /// * A turbofish to a struct that uses the ScriptableModule trait + /// + /// # Examples + /// ```rust + /// use eucalyptus_core::scripting::{camera, input, lighting, ScriptManager}; + /// let script_manager = ScriptManager::new()? + /// .register_module::<input::InputState>()? + /// .register_module::<camera::SerializableCamera>()? + /// .register_module::<lighting::Lighting>()?; + /// ``` + pub fn register_module<T: ScriptableModule>(mut self) -> anyhow::Result<Self> { + T::register(&mut self.runtime)?; + Ok(self) + } + + pub fn init_entity_script( + &mut self, + entity_id: hecs::Entity, + script_name: &str, + world: &mut World, + input_state: &input::InputState, + ) -> anyhow::Result<()> { + log_once::debug_once!("init_entity_script: {} for {:?}", script_name, entity_id); + + if let Some(module) = self.compiled_scripts.get(script_name).cloned() { + // construct RawSceneData-like payload + let mut scene_map = serde_json::Map::new(); + + // entities array + let mut entities_arr: Vec<serde_json::Value> = Vec::new(); + for (id, (adopted, transform)) in world.query::<(&AdoptedEntity, &Transform)>().iter() { + // try to get ModelProperties for this entity + let props_value = if let Ok(mut pq) = world.query_one::<&ModelProperties>(id) { + if let Some(p) = pq.get() { + serde_json::to_value(p)? + } else { + serde_json::Value::Object(serde_json::Map::new()) + } + } else { + serde_json::Value::Object(serde_json::Map::new()) + }; + + let mut ent_obj = serde_json::Map::new(); + ent_obj.insert("label".to_string(), serde_json::Value::String(adopted.label().clone())); + + // properties object expected shape: { custom_properties: Record<string, any> } or full ModelProperties + ent_obj.insert("properties".to_string(), props_value); + + ent_obj.insert("transform".to_string(), serde_json::to_value(transform)?); + + entities_arr.push(serde_json::Value::Object(ent_obj)); + } + scene_map.insert("entities".to_string(), serde_json::Value::Array(entities_arr)); + + // cameras snapshot (keep minimal fields) + let mut cameras_arr: Vec<serde_json::Value> = Vec::new(); + for (_id, (cam, _comp)) in world.query::<(&Camera, &CameraComponent)>().iter() { + cameras_arr.push(serde_json::json!({ + "label": cam.label, + "data": { + "eye": cam.eye.to_array(), + "target": cam.target.to_array(), + "up": cam.up.to_array(), + "aspect": cam.aspect, + "fov": cam.fov_y, + "near": cam.znear, + "far": cam.zfar, + "yaw": cam.yaw, + "pitch": cam.pitch, + "speed": cam.speed, + "sensitivity": cam.sensitivity + } + })); + } + scene_map.insert("cameras".to_string(), serde_json::Value::Array(cameras_arr)); + + // lights snapshot (label only) + let mut lights_arr: Vec<serde_json::Value> = Vec::new(); + for (_id, (_transform, _light_comp, light)) in world.query::<(&Transform, &LightComponent, &Light)>().iter() { + lights_arr.push(serde_json::json!({ + "label": light.label(), + })); + } + scene_map.insert("lights".to_string(), serde_json::Value::Array(lights_arr)); + + // current entity label (if available) + if let Ok(mut q) = world.query_one::<&AdoptedEntity>(entity_id) { + if let Some(adopted) = q.get() { + scene_map.insert("current_entity".to_string(), serde_json::Value::String(adopted.label().clone())); + } + } + + // input state + let serializable_input = input::SerializableInputState::from(input_state); + let mut payload = serde_json::Map::new(); + payload.insert("scene".to_string(), serde_json::Value::Object(scene_map)); + payload.insert("input".to_string(), serde_json::to_value(&serializable_input)?); + + let script_data_value = serde_json::Value::Object(payload); + + // call onLoad (module scripts call dropbear.start(s) and return dropbear.end()) + match self.runtime.call_function::<serde_json::Value>(Some(&module), "onLoad", &vec![script_data_value.clone()]) { + Ok(result) => { + log::debug!("Called onLoad for entity {:?}", entity_id); + // apply whatever the script returned (expect Partial RawSceneData shape) + let _ = self.apply_script_data_to_world(entity_id, &result, world); + // store initial data (script-managed) + self.entity_script_data.insert(entity_id, result); + } + Err(e) => { + log::warn!("onLoad missing/failed for entity {:?}: {}", entity_id, e); + // store the payload we sent so future updates have context + self.entity_script_data.insert(entity_id, script_data_value); + } + } + + Ok(()) + } else { + Err(anyhow::anyhow!("Script '{}' not found", script_name)) + } + } + + pub fn update_entity_script( + &mut self, + entity_id: hecs::Entity, + script_name: &str, + world: &mut World, + input_state: &input::InputState, + dt: f32, + ) -> anyhow::Result<()> { + log_once::debug_once!("Update entity script name: {}", script_name); + + if let Some(module) = self.compiled_scripts.get(script_name).cloned() { + // Build the same RawSceneData-like payload as init + let mut scene_map = serde_json::Map::new(); + + let mut entities_arr: Vec<serde_json::Value> = Vec::new(); + for (id, (adopted, transform)) in world.query::<(&AdoptedEntity, &Transform)>().iter() { + let props_value = if let Ok(mut pq) = world.query_one::<&ModelProperties>(id) { + if let Some(p) = pq.get() { + serde_json::to_value(p)? + } else { + serde_json::Value::Object(serde_json::Map::new()) + } + } else { + serde_json::Value::Object(serde_json::Map::new()) + }; + + let mut ent_obj = serde_json::Map::new(); + ent_obj.insert("label".to_string(), serde_json::Value::String(adopted.label().clone())); + ent_obj.insert("properties".to_string(), props_value); + ent_obj.insert("transform".to_string(), serde_json::to_value(transform)?); + entities_arr.push(serde_json::Value::Object(ent_obj)); + } + scene_map.insert("entities".to_string(), serde_json::Value::Array(entities_arr)); + + // cameras + let mut cameras_arr: Vec<serde_json::Value> = Vec::new(); + for (_id, (cam, _comp)) in world.query::<(&Camera, &CameraComponent)>().iter() { + cameras_arr.push(serde_json::json!({ + "label": cam.label, + "data": { + "eye": cam.eye.to_array(), + "target": cam.target.to_array(), + "up": cam.up.to_array(), + "aspect": cam.aspect, + "fov": cam.fov_y, + "near": cam.znear, + "far": cam.zfar, + "yaw": cam.yaw, + "pitch": cam.pitch, + "speed": cam.speed, + "sensitivity": cam.sensitivity + } + })); + } + scene_map.insert("cameras".to_string(), serde_json::Value::Array(cameras_arr)); + + // lights + let mut lights_arr: Vec<serde_json::Value> = Vec::new(); + for (_id, (_transform, _light_comp, light)) in world.query::<(&Transform, &LightComponent, &Light)>().iter() { + lights_arr.push(serde_json::json!({ + "label": light.label(), + })); + } + scene_map.insert("lights".to_string(), serde_json::Value::Array(lights_arr)); + + // current entity label + if let Ok(mut q) = world.query_one::<&AdoptedEntity>(entity_id) { + if let Some(adopted) = q.get() { + scene_map.insert("current_entity".to_string(), serde_json::Value::String(adopted.label().clone())); + } + } + + let serializable_input = input::SerializableInputState::from(input_state); + let mut payload = serde_json::Map::new(); + payload.insert("scene".to_string(), serde_json::Value::Object(scene_map)); + payload.insert("input".to_string(), serde_json::to_value(&serializable_input)?); + + let script_data_value = serde_json::Value::Object(payload); + let dt_value = serde_json::Value::Number(serde_json::Number::from_f64(dt as f64).unwrap()); + + // call onUpdate(s, dt) + match self.runtime.call_function::<serde_json::Value>(Some(&module), "onUpdate", &vec![script_data_value.clone(), dt_value]) { + Ok(result) => { + log::trace!("Called update for entity {:?}", entity_id); + // delegate applying of returned data to the helper + let _ = self.apply_script_data_to_world(entity_id, &result, world); + // store latest returned data + self.entity_script_data.insert(entity_id, result); + } + Err(e) => { + log_once::error_once!("Script execution error for entity {:?}: {}", entity_id, e); + } + } + } else { + log_once::error_once!("Unable to fetch compiled scripts for entity {:?}. Script Name: {}", entity_id, script_name); + } + Ok(()) + } + + fn apply_script_data_to_world( + &self, + entity_id: hecs::Entity, + script_data: &serde_json::Value, + world: &mut World, + ) -> anyhow::Result<()> { + if let Some(obj) = script_data.as_object() { + if let Some(entities_val) = obj.get("entities").and_then(|v| v.as_array()) { + for ent in entities_val { + if let Some(ent_obj) = ent.as_object() { + if let Some(label) = ent_obj.get("label").and_then(|l| l.as_str()) { + if let Some(target_entity) = world + .query_mut::<(&AdoptedEntity,)>() + .into_iter() + .find_map(|(e, (adopted,))| { + if adopted.label() == label { + Some(e) + } else { + None + } + }) { + if let Some(transform_val) = ent_obj.get("transform") { + if let Ok(new_t) = serde_json::from_value::<Transform>(transform_val.clone()) { + if let Ok(mut tq) = world.query_one::<&mut Transform>(target_entity) { + if let Some(tref) = tq.get() { + *tref = new_t; + log::trace!("apply: updated transform for {:?}", target_entity); + } + } + } else { + log::trace!("apply: failed to deserialize transform for label '{}'", label); + } + } + if let Some(props_val) = ent_obj.get("properties") { + if let Ok(new_props) = serde_json::from_value::<ModelProperties>(props_val.clone()) { + if let Ok(mut pq) = world.query_one::<&mut ModelProperties>(target_entity) { + if let Some(p) = pq.get() { + *p = new_props; + log::trace!("apply: updated properties for {:?}", target_entity); + } + } else { + if let Err(e) = world.insert_one(target_entity, new_props) { + log::warn!("apply: failed to insert properties for {:?}: {}", target_entity, e); + } + } + } else { + log::trace!("apply: failed to deserialize properties for label '{}'", label); + } + } + } else { + log::trace!("apply: no entity in world matching label '{}'", label); + } + } + } + } + } else { + // No entities array: treat the object as direct payload for the single entity + // apply transform if present + if let Some(transform_value) = obj.get("transform") { + if let Ok(updated_transform) = serde_json::from_value::<Transform>(transform_value.clone()) { + if let Ok(mut transform_query) = world.query_one::<&mut Transform>(entity_id) { + if let Some(transform) = transform_query.get() { + *transform = updated_transform; + log::trace!("apply: Updated transform for entity {:?}", entity_id); + } + } + } + } + // apply properties if present (support "properties" or legacy "entity") + if let Some(props_value) = obj.get("properties").or_else(|| obj.get("entity")) { + if let Ok(updated_properties) = serde_json::from_value::<ModelProperties>(props_value.clone()) { + if let Ok(mut properties_query) = world.query_one::<&mut ModelProperties>(entity_id) { + if let Some(properties) = properties_query.get() { + *properties = updated_properties; + log::trace!("apply: Updated properties for entity {:?}", entity_id); + } + } else { + if let Err(e) = world.insert_one(entity_id, updated_properties) { + log::warn!("apply: Failed to insert updated properties for entity {:?}: {}", entity_id, e); + } + } + } + } + + // cameras: if script returned camera array with full data, apply by label + if let Some(cameras_val) = obj.get("cameras").and_then(|v| v.as_array()) { + for cam in cameras_val { + if let Some(cam_obj) = cam.as_object() { + if let Some(label) = cam_obj.get("label").and_then(|l| l.as_str()) { + if let Some(data_obj) = cam_obj.get("data").and_then(|d| d.as_object()) { + // find camera entity by label + if let Some(cam_entity) = world + .query::<&Camera>() + .iter() + .find_map(|(e, camera)| if camera.label == label { Some(e) } else { None }) { + if let Ok(mut cam_q) = world.query_one::<&mut Camera>(cam_entity) { + if let Some(cam_struct) = cam_q.get() { + // update common camera fields if present + if let Some(eye_val) = data_obj.get("eye") { + if let Ok(arr) = serde_json::from_value::<[f64; 3]>(eye_val.clone()) { + cam_struct.eye = DVec3::from_array(arr); + } + } + if let Some(target_val) = data_obj.get("target") { + if let Ok(arr) = serde_json::from_value::<[f64; 3]>(target_val.clone()) { + cam_struct.target = DVec3::from_array(arr); + } + } + if let Some(up_val) = data_obj.get("up") { + if let Ok(arr) = serde_json::from_value::<[f64; 3]>(up_val.clone()) { + cam_struct.up = DVec3::from_array(arr); + } + } + if let Some(aspect) = data_obj.get("aspect").and_then(|v| v.as_f64()) { + cam_struct.aspect = aspect; + } + if let Some(fov) = data_obj.get("fov").and_then(|v| v.as_f64()) { + cam_struct.fov_y = fov; + } + if let Some(near) = data_obj.get("near").and_then(|v| v.as_f64()) { + cam_struct.znear = near; + } + if let Some(far) = data_obj.get("far").and_then(|v| v.as_f64()) { + cam_struct.zfar = far; + } + if let Some(yaw) = data_obj.get("yaw").and_then(|v| v.as_f64()) { + cam_struct.yaw = yaw; + } + if let Some(pitch) = data_obj.get("pitch").and_then(|v| v.as_f64()) { + cam_struct.pitch = pitch; + } + if let Some(speed) = data_obj.get("speed").and_then(|v| v.as_f64()) { + cam_struct.speed = speed; + } + if let Some(sensitivity) = data_obj.get("sensitivity").and_then(|v| v.as_f64()) { + cam_struct.sensitivity = sensitivity; + } + log::trace!("apply: Updated camera '{}'", label); + } + } + } + } + } + } + } + } + } + } else { + log::trace!("apply: script_data not an object for entity {:?}", entity_id); + } + + Ok(()) + } + + pub fn load_script_from_source(&mut self, script_name: &String, script_content: &String) -> anyhow::Result<String> { + let module = Module::new(&script_name, script_content); + + match self.runtime.load_module(&module) { + Ok(module) => { + self.compiled_scripts.insert(script_name.clone(), module); + log::info!("Loaded script: {}", script_name); + Ok(script_name.to_string()) + } + Err(e) => { + log::error!("Compiling module for script [{}] returned an error: {}", script_name, e); + Err(e.into()) + } + } + } + + pub fn load_script(&mut self, script_path: &PathBuf) -> anyhow::Result<String> { + log::debug!("Reading script content"); + let script_content = fs::read_to_string(script_path)?; + log::debug!("Fetching script name"); + let script_name = script_path + .file_name() + .unwrap_or_default() + .to_string_lossy() + .to_string(); + log::debug!("Script name: {}", script_name); + + log::debug!("Creating module for typescript runtime"); + let module = Module::new(&script_name, &script_content); + + log::debug!("Loading module"); + match self.runtime.load_module(&module) { + Ok(module) => { + self.compiled_scripts.insert(script_name.clone(), module); + log::info!("Loaded script: {}", script_name); + Ok(script_name) + } + Err(e) => { + log::error!("Compiling module for script path [{}] returned an error: {}", script_path.display(), e); + Err(e.into()) + } + } + } + + pub fn remove_entity_script(&mut self, entity_id: hecs::Entity) { + self.entity_script_data.remove(&entity_id); + } + + pub fn handle_camera_actions(&self, world: &mut hecs::World, active_camera: &mut Option<hecs::Entity>, actions: Vec<serde_json::Value>) -> anyhow::Result<()> { + use crate::camera::{CameraComponent, CameraType}; + use dropbear_engine::camera::Camera; + + for action_value in actions { + if let Ok(action) = serde_json::from_value::<crate::scripting::camera::CameraAction>(action_value) { + match action.action.as_str() { + "switch_camera" => { + if let Some(label) = action.label { + let camera_entity = world + .query::<&Camera>() + .iter() + .find(|(_, camera)| camera.label == label) + .map(|(entity, _)| entity); + + if let Some(entity) = camera_entity { + *active_camera = Some(entity); + log::info!("Switched to camera with label: {}", label); + } else { + log::warn!("Camera with label '{}' not found", label); + } + } + } + "get_active_camera" => { + if let Some(active_entity) = active_camera { + if let Ok(camera) = world.get::<&Camera>(*active_entity) { + log::info!("Active camera label: {}", camera.label); + } + } + } + "get_all_cameras" => { + let labels: Vec<String> = world + .query::<&Camera>() + .iter() + .map(|(_, camera)| camera.label.clone()) + .collect(); + log::info!("All camera labels: {:?}", labels); + } + "get_cameras_by_type" => { + if let Some(type_str) = action.camera_type { + let target_type = match type_str.as_str() { + "Normal" => CameraType::Normal, + "Debug" => CameraType::Debug, + "Player" => CameraType::Player, + _ => continue, + }; + + let labels: Vec<String> = world + .query::<(&Camera, &CameraComponent)>() + .iter() + .filter_map(|(_, (camera, component))| { + if component.camera_type == target_type { + Some(camera.label.clone()) + } else { + None + } + }) + .collect(); + log::info!("Cameras of type {:?}: {:?}", target_type, labels); + } + } + "manipulate_camera" => { + if let (Some(label), Some(property), Some(value)) = (action.label, action.property, action.value) { + let camera_entity = world + .query::<&Camera>() + .iter() + .find(|(_, camera)| camera.label == label) + .map(|(entity, _)| entity); + + if let Some(entity) = camera_entity { + match property.as_str() { + "position" => { + if let Ok(pos_array) = serde_json::from_value::<[f64; 3]>(value) { + if let Ok(mut camera) = world.get::<&mut Camera>(entity) { + camera.eye = DVec3::from_array(pos_array); + log::info!("Updated camera '{}' position to {:?}", label, pos_array); + } + } + } + "target" => { + if let Ok(target_array) = serde_json::from_value::<[f64; 3]>(value) { + if let Ok(mut camera) = world.get::<&mut Camera>(entity) { + camera.target = DVec3::from_array(target_array); + log::info!("Updated camera '{}' target to {:?}", label, target_array); + } + } + } + "speed" => { + if let Some(speed) = value.as_f64() { + if let Ok(mut camera) = world.get::<&mut Camera>(entity) { + camera.speed = speed; + } + if let Ok(mut component) = world.get::<&mut CameraComponent>(entity) { + component.speed = speed; + } + log::info!("Updated camera '{}' speed to {}", label, speed); + } + } + "sensitivity" => { + if let Some(sensitivity) = value.as_f64() { + if let Ok(mut camera) = world.get::<&mut Camera>(entity) { + camera.sensitivity = sensitivity; + } + if let Ok(mut component) = world.get::<&mut CameraComponent>(entity) { + component.sensitivity = sensitivity; + } + log::info!("Updated camera '{}' sensitivity to {}", label, sensitivity); + } + } + "fov" => { + if let Some(fov) = value.as_f64() { + if let Ok(mut camera) = world.get::<&mut Camera>(entity) { + camera.fov_y = fov; + } + if let Ok(mut component) = world.get::<&mut CameraComponent>(entity) { + component.fov_y = fov; + } + log::info!("Updated camera '{}' FOV to {}", label, fov); + } + } + "yaw" => { + if let Some(yaw) = value.as_f64() { + if let Ok(mut camera) = world.get::<&mut Camera>(entity) { + camera.yaw = yaw; + } + log::info!("Updated camera '{}' yaw to {}", label, yaw); + } + } + "pitch" => { + if let Some(pitch) = value.as_f64() { + if let Ok(mut camera) = world.get::<&mut Camera>(entity) { + camera.pitch = pitch; + } + log::info!("Updated camera '{}' pitch to {}", label, pitch); + } + } + _ => { + log::warn!("Unknown camera property: {}", property); + } + } + } else { + log::warn!("Camera with label '{}' not found for manipulation", label); + } + } + } + "get_camera_by_label" => { + if let Some(label) = action.label { + log::info!("Getting camera data for label: {}", label); + } + } + "set_camera_by_label" => { + if let (Some(label), Some(camera_data)) = (action.label, action.camera_data) { + // Find and update the entire camera + let camera_entity = world + .query::<&Camera>() + .iter() + .find(|(_, camera)| camera.label == label) + .map(|(entity, _)| entity); + + if let Some(entity) = camera_entity { + if let Ok(mut camera) = world.get::<&mut Camera>(entity) { + camera.eye = camera_data.eye; + camera.target = camera_data.target; + camera.up = camera_data.up; + camera.aspect = camera_data.aspect; + camera.fov_y = camera_data.fov; + camera.znear = camera_data.near; + camera.zfar = camera_data.far; + camera.yaw = camera_data.yaw; + camera.pitch = camera_data.pitch; + camera.speed = camera_data.speed; + camera.sensitivity = camera_data.sensitivity; + log::info!("Updated complete camera data for label: {}", label); + } + } + } + } + _ => { + log::warn!("Unknown camera action: {}", action.action); + } + } + } + } + Ok(()) + } +} @@ -0,0 +1,985 @@ +use crate::camera::DebugCamera; +use crate::utils::PROTO_TEXTURE; +use dropbear_engine::starter::plane::PlaneBuilder; +use glam::DVec3; +use std::fmt::{Display, Formatter}; +use std::{fmt, fs}; +use std::collections::HashMap; +use std::path::PathBuf; +use chrono::Utc; +use egui_dock_fork::DockState; +use parking_lot::RwLock; +use once_cell::sync::Lazy; +use ron::ser::PrettyConfig; +use serde::{Deserialize, Serialize}; +use dropbear_engine::camera::Camera; +use dropbear_engine::entity::{AdoptedEntity, Transform}; +use dropbear_engine::graphics::Graphics; +use dropbear_engine::lighting::{Light, LightComponent}; +use dropbear_engine::model::Model; +use dropbear_engine::utils::{ResourceReference, ResourceReferenceType}; +use crate::camera::{CameraComponent, CameraFollowTarget, CameraType}; + +pub static PROJECT: Lazy<RwLock<ProjectConfig>> = + Lazy::new(|| RwLock::new(ProjectConfig::default())); + +pub static RESOURCES: Lazy<RwLock<ResourceConfig>> = + Lazy::new(|| RwLock::new(ResourceConfig::default())); + +pub static SOURCE: Lazy<RwLock<SourceConfig>> = Lazy::new(|| RwLock::new(SourceConfig::default())); + +pub static SCENES: Lazy<RwLock<Vec<SceneConfig>>> = Lazy::new(|| RwLock::new(Vec::new())); + +/// The root config file, responsible for building and other metadata. +/// +/// # Location +/// This file is {project_name}.eucp and is located at {project_dir}/ +#[derive(Debug, Deserialize, Serialize, Default)] +pub struct ProjectConfig { + pub project_name: String, + pub project_path: PathBuf, + pub date_created: String, + pub date_last_accessed: String, + #[serde(default)] + pub dock_layout: Option<DockState<EditorTab>>, + #[serde(default)] + pub editor_settings: EditorSettings, +} + +impl ProjectConfig { + /// Creates a new instance of the ProjectConfig. This function is typically used when creating + /// a new project, with it creating new defaults for everything. + pub fn new(project_name: String, project_path: &PathBuf) -> Self { + let date_created = format!("{}", Utc::now().format("%Y-%m-%d %H:%M:%S")); + let date_last_accessed = format!("{}", Utc::now().format("%Y-%m-%d %H:%M:%S")); + // #[cfg(not(feature = "editor"))] + // { + // let mut result = Self { + // project_name, + // project_path: project_path.to_path_buf(), + // date_created, + // date_last_accessed, + // }; + // let _ = result.load_config_to_memory(); // TODO: Deal with later... + // result + // } + + let mut result = Self { + project_name, + project_path: project_path.to_path_buf(), + date_created, + date_last_accessed, + editor_settings: Default::default(), + dock_layout: None, + }; + let _ = result.load_config_to_memory(); + result + } + + /// This function writes the [`ProjectConfig`] struct (and other PathBufs) to a file of the choice + /// under the PathBuf path parameter. + /// + /// # Parameters + /// * path - The root **folder** of the project. + + pub fn write_to(&mut self, path: &PathBuf) -> anyhow::Result<()> { + self.load_config_to_memory()?; + self.date_last_accessed = format!("{}", Utc::now().format("%Y-%m-%d %H:%M:%S")); + // self.assets = Assets::walk(path); + let ron_str = ron::ser::to_string_pretty(&self, PrettyConfig::default()) + .map_err(|e| anyhow::anyhow!("RON serialization error: {}", e))?; + let config_path = path.join(format!("{}.eucp", self.project_name.clone().to_lowercase())); + self.project_path = path.to_path_buf(); + + fs::write(&config_path, ron_str).map_err(|e| anyhow::anyhow!(e.to_string()))?; + Ok(()) + } + + /// This function reads from the RON and traverses down the different folders to add more information + /// to the ProjectConfig, such as Assets location and other stuff. + /// + /// # Parameters + /// * path - The root config **file** for the project + pub fn read_from(path: &PathBuf) -> anyhow::Result<Self> { + let ron_str = fs::read_to_string(path)?; + let mut config: ProjectConfig = ron::de::from_str(&ron_str.as_str())?; + config.project_path = path.parent().unwrap().to_path_buf(); + log::info!("Loaded project!"); + log::debug!("Loaded config info"); + log::debug!("Updating with new content"); + config.load_config_to_memory()?; + config.write_to_all()?; + log::debug!("Successfully updated!"); + Ok(config) + } + + /// This function loads a `source.eucc`, `resources.eucc` or a `{scene}.eucs` config file into memory, allowing + /// you to reference and load the nodes located inside them. + pub fn load_config_to_memory(&mut self) -> anyhow::Result<()> { + let project_root = PathBuf::from(&self.project_path); + + // resource config + match ResourceConfig::read_from(&project_root) { + Ok(resources) => { + let mut cfg = RESOURCES.write(); + *cfg = resources; + } + Err(e) => { + if let Some(io_err) = e.downcast_ref::<std::io::Error>() { + if io_err.kind() == std::io::ErrorKind::NotFound { + log::warn!("resources.eucc not found, creating default."); + let default = ResourceConfig { + path: project_root.join("resources"), + nodes: vec![], + }; + default.write_to(&project_root)?; + { + let mut cfg = RESOURCES.write(); + *cfg = default; + } + } else { + log::warn!("Failed to load resources.eucc: {}", e); + } + } else { + log::warn!("Failed to load resources.eucc: {}", e); + } + } + } + + // src config + let mut source_config = SOURCE.write(); + match SourceConfig::read_from(&project_root) { + Ok(source) => *source_config = source, + Err(e) => { + if let Some(io_err) = e.downcast_ref::<std::io::Error>() { + if io_err.kind() == std::io::ErrorKind::NotFound { + log::warn!("source.eucc not found, creating default."); + let default = SourceConfig { + path: project_root.join("src"), + nodes: vec![], + }; + default.write_to(&project_root)?; + *source_config = default; + } else { + log::warn!("Failed to load source.eucc: {}", e); + } + } else { + log::warn!("Failed to load source.eucc: {}", e); + } + } + } + + // scenes + let mut scene_configs = SCENES.write(); + scene_configs.clear(); + + // iterate through each scene file in the folder + let scene_folder = &project_root.join("scenes"); + + if !scene_folder.exists() { + fs::create_dir_all(scene_folder)?; + } + + for scene_entry in fs::read_dir(scene_folder)? { + let scene_entry = scene_entry?; + let path = scene_entry.path(); + + if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("eucs") { + match SceneConfig::read_from(&path) { + Ok(scene) => { + log::debug!("Loaded scene: {}", scene.scene_name); + scene_configs.push(scene); + } + Err(e) => { + if let Some(io_err) = e.downcast_ref::<std::io::Error>() { + if io_err.kind() == std::io::ErrorKind::NotFound { + log::warn!("Scene file {:?} not found", path); + } else { + log::warn!("Failed to load scene file {:?}: {}", path, e); + } + } else { + log::warn!("Failed to load scene file {:?}: {}", path, e); + } + } + } + } + } + + if scene_configs.is_empty() { + log::info!("No scenes found, creating default scene"); + let default_scene = + SceneConfig::new("Default".to_string(), scene_folder.join("default.eucs")); + default_scene.write_to(&project_root)?; + scene_configs.push(default_scene); + } + + Ok(()) + } + + /// # Parameters + /// * path - The root folder of the project + pub fn write_to_all(&mut self) -> anyhow::Result<()> { + let path = PathBuf::from(self.project_path.clone()); + + { + let resources_config = RESOURCES.read(); + resources_config.write_to(&path)?; + } + + { + let source_config = SOURCE.read(); + source_config.write_to(&path)?; + } + + { + let scene_configs = SCENES.read(); + for scene in scene_configs.iter() { + scene.write_to(&path)?; + } + } + + self.write_to(&path)?; + Ok(()) + } +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub enum Node { + File(File), + Folder(Folder), +} + +#[derive(Default, Debug, Serialize, Deserialize, Clone)] +pub struct File { + pub name: String, + pub path: PathBuf, + pub resource_type: Option<ResourceType>, +} + +#[derive(Default, Debug, Serialize, Deserialize, Clone)] +pub struct Folder { + pub name: String, + pub path: PathBuf, + pub nodes: Vec<Node>, +} + +/// The type of resource +#[derive(Debug, Serialize, Deserialize, Clone, Hash)] +pub enum ResourceType { + Unknown, + Model, + Thumbnail, + Texture, + Shader, +} + +impl Display for ResourceType { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + let str = match self { + ResourceType::Unknown => "unknown", + ResourceType::Model => "model", + ResourceType::Texture => "texture", + ResourceType::Shader => "shader", + ResourceType::Thumbnail => "thumbnail", + }; + write!(f, "{}", str) + } +} + +/// The resource config. +#[derive(Default, Debug, Serialize, Deserialize)] +pub struct ResourceConfig { + /// The path to the resource folder. + pub path: PathBuf, + /// The files and folders of the assets + pub nodes: Vec<Node>, +} + +impl ResourceConfig { + /// Builds a resource path from the ProjectConfiguration's project_path (or a string) + #[allow(dead_code)] + pub fn build_path(project_path: String) -> PathBuf { + PathBuf::from(project_path).join("resources/resources.eucc") + } + + /// # Parameters + /// - path: The root **folder** of the project + pub fn write_to(&self, path: &PathBuf) -> anyhow::Result<()> { + let resource_dir = path.join("resources"); + let updated_config = ResourceConfig { + path: resource_dir.clone(), + nodes: collect_nodes(&resource_dir, path, vec!["thumbnails"].as_slice()), + }; + let ron_str = ron::ser::to_string_pretty(&updated_config, PrettyConfig::default()) + .map_err(|e| anyhow::anyhow!("RON serialization error: {}", e))?; + let config_path = path.join("resources").join("resources.eucc"); + fs::create_dir_all(config_path.parent().unwrap())?; + fs::write(&config_path, ron_str).map_err(|e| anyhow::anyhow!(e.to_string()))?; + Ok(()) + } + + /// Updates the in-memory ResourceConfig by re-scanning the resource directory. + pub fn update_mem(&mut self) -> anyhow::Result<ResourceConfig> { + let resource_dir = self.path.clone(); + let project_path = resource_dir.parent().unwrap_or(&resource_dir).to_path_buf(); + let updated_config = ResourceConfig { + path: resource_dir.clone(), + nodes: collect_nodes(&resource_dir, &project_path, vec!["thumbnails"].as_slice()), + }; + Ok(updated_config) + } + + /// # Parameters + /// - path: The location to the **resources.eucc** file + pub fn read_from(path: &PathBuf) -> anyhow::Result<Self> { + let config_path = path.join("resources").join("resources.eucc"); + let ron_str = fs::read_to_string(&config_path)?; + let config: ResourceConfig = ron::de::from_str(&ron_str) + .map_err(|e| anyhow::anyhow!("RON deserialization error: {}", e))?; + Ok(config) + } +} + +#[derive(Default, Debug, Serialize, Deserialize, Clone)] +pub struct SourceConfig { + /// The path to the resource folder. + pub path: PathBuf, + /// The files and folders of the assets + pub nodes: Vec<Node>, +} + +impl SourceConfig { + /// Builds a source path from the ProjectConfiguration's project_path (or a string) + #[allow(dead_code)] + pub fn build_path(project_path: String) -> PathBuf { + PathBuf::from(project_path).join("src/source.eucc") + } + + /// # Parameters + /// - path: The root **folder** of the project + pub fn write_to(&self, path: &PathBuf) -> anyhow::Result<()> { + let resource_dir = path.join("src"); + let updated_config = SourceConfig { + path: resource_dir.clone(), + nodes: collect_nodes(&resource_dir, path, vec!["scripts"].as_slice()), + }; + + let ron_str = ron::ser::to_string_pretty(&updated_config, PrettyConfig::default()) + .map_err(|e| anyhow::anyhow!("RON serialisation error: {}", e))?; + let config_path = path.join("src").join("source.eucc"); + fs::create_dir_all(config_path.parent().unwrap())?; + fs::write(&config_path, ron_str).map_err(|e| anyhow::anyhow!(e.to_string()))?; + Ok(()) + } + + /// # Parameters + /// - path: The location to the **source.eucc** file + pub fn read_from(path: &PathBuf) -> anyhow::Result<Self> { + let config_path = path.join("src").join("source.eucc"); + let ron_str = fs::read_to_string(&config_path)?; + let config: SourceConfig = ron::de::from_str(&ron_str) + .map_err(|e| anyhow::anyhow!("RON deserialization error: {}", e))?; + Ok(config) + } +} + +fn collect_nodes(dir: &PathBuf, project_path: &PathBuf, exclude_list: &[&str]) -> Vec<Node> { + let mut nodes = Vec::new(); + if let Ok(entries) = fs::read_dir(dir) { + for entry in entries.flatten() { + let entry_path = entry.path(); + let name = entry_path + .file_name() + .unwrap_or_default() + .to_string_lossy() + .to_string(); + + if entry_path.is_dir() && exclude_list.iter().any(|ex| &ex.to_string() == &name) { + log::debug!("Skipped past folder {:?}", name); + continue; + } + + if entry_path.is_dir() { + let folder_nodes = collect_nodes(&entry_path, project_path, exclude_list); + nodes.push(Node::Folder(Folder { + name, + path: entry_path.clone(), + nodes: folder_nodes, + })); + } else { + let parent_folder = entry_path + .parent() + .and_then(|p| p.file_name()) + .map(|n| n.to_string_lossy().to_lowercase()) + .unwrap_or_default(); + + let resource_type = if parent_folder.contains("model") { + Some(ResourceType::Model) + } else if parent_folder.contains("texture") { + Some(ResourceType::Texture) + } else if parent_folder.contains("shader") { + Some(ResourceType::Shader) + } else { + Some(ResourceType::Unknown) + }; + + nodes.push(Node::File(File { + name, + path: entry_path.clone(), + resource_type, + })); + } + } + } + nodes +} + +#[derive(Clone, Debug, Serialize, Deserialize, Hash)] +pub enum EntityNode { + Entity { + id: hecs::Entity, + name: String, + }, + Script { + name: String, + path: PathBuf, + }, + Light { + id: hecs::Entity, + name: String, + }, + Camera { + id: hecs::Entity, + name: String, + camera_type: CameraType, + }, + Group { + name: String, + children: Vec<EntityNode>, + collapsed: bool, + }, +} + +#[derive(Default, Debug, Serialize, Deserialize, Clone)] +pub struct ScriptComponent { + pub name: String, + pub path: PathBuf, +} + +impl EntityNode { + pub fn from_world(world: &hecs::World) -> Vec<Self> { + let mut nodes = Vec::new(); + let mut handled = std::collections::HashSet::new(); + + for (id, (script, _transform, adopted)) in world + .query::<( + &ScriptComponent, + &dropbear_engine::entity::Transform, + &dropbear_engine::entity::AdoptedEntity, + )>() + .iter() + { + let name = adopted.model().label.clone(); + + // grouped entity (entity and script) + nodes.push(EntityNode::Group { + name: name.clone(), + children: vec![ + EntityNode::Entity { + id, + name: name.clone(), + }, + EntityNode::Script { + name: script.name.clone(), + path: script.path.clone(), + }, + ], + collapsed: false, + }); + handled.insert(id); + } + + // single entity + for (id, (_, adopted)) in world + .query::<( + &dropbear_engine::entity::Transform, + &dropbear_engine::entity::AdoptedEntity, + )>() + .iter() + { + if handled.contains(&id) { + continue; + } + let name = adopted.model().label.clone(); + + nodes.push(EntityNode::Entity { id, name }); + } + + // lights + for (id, (_transform, _light_comp, light)) in world + .query::<(&dropbear_engine::entity::Transform, &LightComponent, &Light)>() + .iter() + { + if handled.contains(&id) { + continue; + } + nodes.push(EntityNode::Light { + id, + name: light.label().to_string(), + }); + handled.insert(id); + } + + for (entity, (camera, component)) in world + .query::<(&Camera, &CameraComponent)>() + .iter() + { + if world.get::<&AdoptedEntity>(entity).is_err() { + nodes.push(EntityNode::Camera { + id: entity, + name: camera.label.clone(), + camera_type: component.camera_type, + }); + } + } + + nodes + } +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CameraConfig { + pub label: String, + pub camera_type: CameraType, + + pub position: [f64; 3], + pub target: [f64; 3], + pub up: [f64; 3], + pub aspect: f64, + pub fov: f32, + pub near: f32, + pub far: f32, + + pub speed: f32, + pub sensitivity: f32, + + pub follow_target_entity_label: Option<String>, + pub follow_offset: Option<[f64; 3]>, +} + +impl Default for CameraConfig { + fn default() -> Self { + let default = CameraComponent::new(); + Self { + position: [0.0, 1.0, 2.0], + target: [0.0, 0.0, 0.0], + up: [0.0, 1.0, 0.0], + aspect: 16.0 / 9.0, + fov: 45.0, + near: 0.1, + far: 100.0, + follow_target_entity_label: None, + follow_offset: None, + label: String::new(), + camera_type: CameraType::Normal, + speed: default.speed as f32, + sensitivity: default.sensitivity as f32, + } + } +} + +impl CameraConfig { + pub fn from_ecs_camera(camera: &Camera, component: &CameraComponent, follow_target: Option<&CameraFollowTarget>) -> Self { + Self { + position: camera.position().to_array(), + target: camera.target.to_array(), + label: camera.label.clone(), + camera_type: component.camera_type, + up: camera.up.to_array(), + aspect: camera.aspect, + fov: camera.fov_y as f32, + near: camera.znear as f32, + far: camera.zfar as f32, + speed: component.speed as f32, + sensitivity: component.sensitivity as f32, + follow_target_entity_label: if let Some(target) = follow_target { Some(target.follow_target.clone()) } else { None }, + follow_offset: if let Some(target) = follow_target { Some(target.offset.to_array()) } else { None }, + } + } +} + +#[derive(Debug, Serialize, Deserialize, Clone, Default)] +pub struct SceneEntity { + pub model_path: ResourceReference, + pub label: String, + pub transform: Transform, + pub properties: ModelProperties, + pub script: Option<ScriptComponent>, + + #[serde(skip)] + #[allow(dead_code)] + pub entity_id: Option<hecs::Entity>, +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +pub struct ModelProperties { + pub custom_properties: HashMap<String, PropertyValue>, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum PropertyValue { + String(String), + Int(i64), + Float(f64), + Bool(bool), + Vec3([f32; 3]), +} + +impl ModelProperties { + pub fn new() -> Self { + Self { + custom_properties: HashMap::new(), + } + } + + pub fn set_property(&mut self, key: String, value: PropertyValue) { + self.custom_properties.insert(key, value); + } + + pub fn get_property(&self, key: &str) -> Option<&PropertyValue> { + self.custom_properties.get(key) + } +} + +impl Default for ModelProperties { + fn default() -> Self { + Self::new() + } +} + +#[derive(Default, Debug, Serialize, Deserialize, Clone)] +pub struct SceneConfig { + pub scene_name: String, + pub entities: Vec<SceneEntity>, + pub cameras: Vec<CameraConfig>, + pub lights: Vec<LightConfig>, + // todo later + // pub settings: SceneSettings, + #[serde(skip)] + pub path: PathBuf, +} + +impl SceneConfig { + /// Creates a new instance of the scene config + pub fn new(scene_name: String, path: PathBuf) -> Self { + Self { + scene_name, + path, + entities: Vec::new(), + cameras: Vec::new(), + lights: Vec::new(), + } + } + + /// Write the scene config to a .eucs file + pub fn write_to(&self, project_path: &PathBuf) -> anyhow::Result<()> { + let ron_str = ron::ser::to_string_pretty(&self, PrettyConfig::default()) + .map_err(|e| anyhow::anyhow!("RON serialization error: {}", e))?; + + let scenes_dir = project_path.join("scenes"); + fs::create_dir_all(&scenes_dir)?; + + let config_path = scenes_dir.join(format!("{}.eucs", self.scene_name)); + fs::write(&config_path, ron_str).map_err(|e| anyhow::anyhow!(e.to_string()))?; + Ok(()) + } + + /// Read a scene config from a .eucs file + pub fn read_from(scene_path: &PathBuf) -> anyhow::Result<Self> { + let ron_str = fs::read_to_string(scene_path)?; + let mut config: SceneConfig = ron::de::from_str(&ron_str) + .map_err(|e| anyhow::anyhow!("RON deserialization error: {}", e))?; + + config.path = scene_path.clone(); + Ok(config) + } + + pub fn load_into_world( + &self, + world: &mut hecs::World, + graphics: &Graphics, + ) -> anyhow::Result<hecs::Entity> { + log::info!( + "Loading scene [{}], clearing world with {} entities", + self.scene_name, + world.len() + ); + world.clear(); + + #[allow(unused_variables)] + let project_config = if cfg!(feature = "editor") { + let cfg = PROJECT.read(); + { + cfg.project_path.clone() + } + } else { + log::debug!("Not using the editor feature, returning empty pathbuffer"); + PathBuf::new() + }; + + log::info!("World cleared, now has {} entities", world.len()); + + for entity_config in &self.entities { + log::debug!("Loading entity: {}", entity_config.label); + match &entity_config.model_path.ref_type { + ResourceReferenceType::File(reference) => { + let path: PathBuf = { + if cfg!(feature = "editor") { + log::debug!("Using feature editor"); + entity_config.model_path.to_project_path(project_config.clone()) + .ok_or_else(|| anyhow::anyhow!("Unable to convert resource reference [{}] to project path", reference))? + } else { + log::debug!("Using feature data-only"); + entity_config.model_path.to_executable_path()? + } + }; + log::debug!("Path for entity {} is {} from reference {}", entity_config.label, path.display(), reference); + let adopted = AdoptedEntity::new( + graphics, + &path, + Some(&entity_config.label), + )?; + + let transform = entity_config.transform; + + if let Some(script_config) = &entity_config.script { + let script = ScriptComponent { + name: script_config.name.clone(), + path: script_config.path.clone(), + }; + world.spawn((adopted, transform, script, entity_config.properties.clone())); + } else { + world.spawn((adopted, transform, entity_config.properties.clone())); + } + log::debug!("Loaded!"); + } + ResourceReferenceType::Bytes(bytes) => { + log::info!("Loading entity from bytes [Len: {}]", bytes.len()); + let bytes = bytes.to_owned(); + + let model = Model::load_from_memory(graphics, bytes, Some(&entity_config.label))?; + let transform = entity_config.transform; + + let adopted = AdoptedEntity::adopt( + graphics, + model, + Some(&entity_config.label), + ); + + if let Some(script_config) = &entity_config.script { + let script = ScriptComponent { + name: script_config.name.clone(), + path: script_config.path.clone(), + }; + world.spawn((adopted, transform, script, entity_config.properties.clone())); + } else { + world.spawn((adopted, transform, entity_config.properties.clone())); + } + log::debug!("Loaded!"); + } + ResourceReferenceType::Plane => { + let width = entity_config.properties.custom_properties.get("width").ok_or_else(|| anyhow::anyhow!("Entity has no width property"))?; + let width = match width { + PropertyValue::Float(width) => width, + _ => panic!("Entity has a width property that is not a float"), + }; + let height = entity_config.properties.custom_properties.get("height").ok_or_else(|| anyhow::anyhow!("Entity has no height property"))?; + let height = match height { + PropertyValue::Float(height) => height, + _ => panic!("Entity has a height property that is not a float"), + }; + let tiles_x = entity_config.properties.custom_properties.get("tiles_x").ok_or_else(|| anyhow::anyhow!("Entity has no tiles_x property"))?; + let tiles_x = match tiles_x { + PropertyValue::Int(tiles_x) => tiles_x, + _ => panic!("Entity has a tiles_x property that is not an int"), + }; + let tiles_z = entity_config.properties.custom_properties.get("tiles_z").ok_or_else(|| anyhow::anyhow!("Entity has no tiles_z property"))?; + let tiles_z = match tiles_z { + PropertyValue::Int(tiles_z) => tiles_z, + _ => panic!("Entity has a tiles_z property that is not an int"), + }; + + let plane = PlaneBuilder::new().with_size(*width as f32, *height as f32).with_tiles(*tiles_x as u32, *tiles_z as u32).build(graphics, PROTO_TEXTURE, Some(entity_config.label.clone().as_str()))?; + let transform = entity_config.transform; + + if let Some(script_config) = &entity_config.script { + let script = ScriptComponent { + name: script_config.name.clone(), + path: script_config.path.clone(), + }; + world.spawn((plane, transform, script, entity_config.properties.clone())); + } else { + world.spawn((plane, transform, entity_config.properties.clone())); + } + } + ResourceReferenceType::None => panic!("Entity has a resource reference of None, which cannot be loaded or referenced"), + } + } + + for light_config in &self.lights { + log::debug!("Loading light: {}", light_config.label); + + let light = Light::new( + graphics, + &light_config.light_component, + &light_config.transform, + Some(&light_config.label), + ); + + world.spawn((light_config.light_component.clone(), light_config.transform, light, ModelProperties::default())); + } + + for camera_config in &self.cameras { + log::debug!("Loading camera {} of type {:?}", camera_config.label, camera_config.camera_type); + + let camera = Camera::new( + graphics, + DVec3::from_array(camera_config.position), + DVec3::from_array(camera_config.target), + DVec3::from_array(camera_config.up), + camera_config.aspect, + camera_config.fov as f64, + camera_config.near as f64, + camera_config.far as f64, + camera_config.speed as f64, + camera_config.sensitivity as f64, + Some(&camera_config.label), + ); + + let component = CameraComponent { + speed: camera_config.speed as f64, + sensitivity: camera_config.sensitivity as f64, + fov_y: camera_config.fov as f64, + camera_type: camera_config.camera_type, + }; + + if let (Some(target_label), Some(offset)) = (&camera_config.follow_target_entity_label, &camera_config.follow_offset) { + let follow_target = CameraFollowTarget { + follow_target: target_label.clone(), + offset: DVec3::from_array(*offset), + }; + world.spawn((camera, component, follow_target)); + } else { + world.spawn((camera, component)); + } + } + + if world.query::<(&LightComponent, &Light)>().iter().next().is_none() { + log::info!("No lights in scene, spawning default light"); + let default_transform = Transform { + position: glam::DVec3::new(2.0, 4.0, 2.0), + ..Default::default() + }; + let default_component = LightComponent::directional(glam::DVec3::ONE, 1.0); + let default_light = Light::new(graphics, &default_component, &default_transform, Some("Default Light")); + world.spawn((default_component, default_transform, default_light, ModelProperties::default())); + } + + log::info!("Loaded {} entities, {} lights and {} cameras", self.entities.len(), self.lights.len(), self.cameras.len()); + #[cfg(feature = "editor")] + { + // Editor mode - look for debug camera, create one if none exists + let debug_camera = world + .query::<(&Camera, &CameraComponent)>() + .iter() + .find_map(|(entity, (_, component))| { + if matches!(component.camera_type, CameraType::Debug) { + Some(entity) + } else { + None + } + }); + + if let Some(camera_entity) = debug_camera { + log::info!("Using existing debug camera for editor"); + Ok(camera_entity) + } else { + log::info!("No debug camera found, creating viewport camera for editor"); + let camera = Camera::predetermined(graphics, Some("Viewport Camera")); + let component = DebugCamera::new(); + let camera_entity = world.spawn((camera, component)); + Ok(camera_entity) + } + } + + #[cfg(not(feature = "editor"))] + { + // Runtime mode - look for player camera, panic if none exists + let player_camera = world + .query::<(&Camera, &CameraComponent)>() + .iter() + .find_map(|(entity, (_, component))| { + if matches!(component.camera_type, CameraType::Player) { + Some(entity) + } else { + None + } + }); + + if let Some(camera_entity) = player_camera { + log::info!("Using player camera for runtime"); + Ok(camera_entity) + } else { + panic!("Runtime mode requires a player camera, but none was found in the scene!"); + } + } + } +} + +#[derive(bincode::Decode, bincode::Encode, serde::Serialize, serde::Deserialize, Debug)] +pub struct RuntimeData { + #[bincode(with_serde)] + pub project_config: ProjectConfig, + #[bincode(with_serde)] + pub source_config: SourceConfig, + #[bincode(with_serde)] + pub scene_data: Vec<SceneConfig>, + #[bincode(with_serde)] + pub scripts: HashMap<String, String>, // name, script_content +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct LightConfig { + pub label: String, + pub transform: Transform, + pub light_component: LightComponent, + pub enabled: bool, + + #[serde(skip)] + pub entity_id: Option<hecs::Entity>, +} + +impl Default for LightConfig { + fn default() -> Self { + Self { + label: "New Light".to_string(), + transform: Transform::default(), + light_component: LightComponent::default(), + enabled: true, + entity_id: None, + } + } +} + +#[derive(Debug, Serialize, Deserialize, Clone, Default)] +pub struct EditorSettings { + pub is_debug_menu_shown: bool, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub enum EditorTab { + AssetViewer, // bottom side, + ResourceInspector, // left side, + ModelEntityList, // right side, + Viewport, // middle, +} @@ -0,0 +1,48 @@ +use dropbear_engine::entity::Transform; +use std::path::PathBuf; + +use crate::states::{ModelProperties, Node}; + +pub const PROTO_TEXTURE: &[u8] = include_bytes!("../../resources/proto.png"); + +pub fn search_nodes_recursively<'a, F>(nodes: &'a [Node], matcher: &F, results: &mut Vec<&'a Node>) +where + F: Fn(&Node) -> bool, +{ + for node in nodes { + if matcher(node) { + results.push(node); + } + match node { + Node::File(_) => {} + Node::Folder(folder) => { + search_nodes_recursively(&folder.nodes, matcher, results); + } + } + } +} + +/// Progress events for project creation +pub enum ProjectProgress { + Step { + _progress: f32, + _message: String, + }, + #[allow(dead_code)] // idk why its giving me this warning :( + Error(String), + Done, +} + +pub enum ViewportMode { + None, + CameraMove, + Gizmo, +} + +#[derive(Clone, Debug)] +pub struct PendingSpawn { + pub asset_path: PathBuf, + pub asset_name: String, + pub transform: Transform, + pub properties: ModelProperties, +} @@ -7,3 +7,42 @@ repository.workspace = true readme.workspace = true [dependencies] +eucalyptus-core = { path = "../eucalyptus-core", features = ["editor"] } +anyhow.workspace = true +app_dirs2.workspace = true +bincode.workspace = true +bytemuck.workspace = true +dropbear-engine.workspace = true +egui-toast-fork.workspace = true +egui.workspace = true +egui_dnd.workspace = true +egui_dock-fork.workspace = true +egui_extras.workspace = true +gilrs.workspace = true +git2 = { workspace = true, features = ["vendored-openssl"]} +glam.workspace = true +hecs.workspace = true +log.workspace = true +log-once.workspace = true +model_to_image.workspace = true +open.workspace = true +parking_lot.workspace = true +transform-gizmo-egui.workspace = true +wgpu.workspace = true +winit.workspace = true +clap.workspace = true +walkdir.workspace = true +zip.workspace = true + +[target.'cfg(not(target_os = "android"))'.dependencies] +rfd.workspace = true + +[features] +default = ["editor"] +editor = ["eucalyptus-core/editor"] + +[build-dependencies] +anyhow = "1.0" +app_dirs2 = "2.5" +reqwest = { version = "0.12", features = ["blocking"] } +zip = "4.3" @@ -0,0 +1,67 @@ +use std::fs::{self, File}; +use std::io::Cursor; + +fn main() -> anyhow::Result<()> { + // todo: move this into the "setup" process + let repo_zip_url = "https://github.com/4tkbytes/dropbear/archive/refs/heads/main.zip"; + let response = reqwest::blocking::get(repo_zip_url) + .map_err(|e| anyhow::anyhow!("Failed to download repo zip: {}", e))? + .bytes() + .map_err(|e| anyhow::anyhow!("Failed to read zip bytes: {}", e))?; + + let reader = Cursor::new(response); + let mut zip = zip::ZipArchive::new(reader) + .map_err(|e| anyhow::anyhow!("Failed to read zip archive: {}", e))?; + + let app_info = app_dirs2::AppInfo { + name: "Eucalyptus", + author: "4tkbytes", + }; + let app_data_dir = app_dirs2::app_root(app_dirs2::AppDataType::UserData, &app_info) + .map_err(|e| anyhow::anyhow!("Could not determine app data directory: {}", e))?; + + fs::create_dir_all(&app_data_dir) + .map_err(|e| anyhow::anyhow!("Failed to create app data directory: {}", e))?; + + let resource_prefix = "dropbear-main/resources/"; + let mut found_resource = false; + for i in 0..zip.len() { + let mut file = zip.by_index(i).unwrap(); + let name = file.name(); + + if name.starts_with(resource_prefix) && !name.ends_with('/') { + found_resource = true; + let rel_path = &name[resource_prefix.len()..]; + let rel_path = rel_path.strip_prefix('/').unwrap_or(rel_path); + let dest_path = app_data_dir.join(rel_path); + + if let Some(parent) = dest_path.parent() { + fs::create_dir_all(parent) + .map_err(|e| anyhow::anyhow!("Failed to create parent directory: {}", e))?; + } + + println!("Copying {} to {:?}", name, dest_path); + + let mut outfile = File::create(&dest_path) + .map_err(|e| anyhow::anyhow!("Failed to create file: {}", e))?; + std::io::copy(&mut file, &mut outfile) + .map_err(|e| anyhow::anyhow!("Failed to copy file: {}", e))?; + } + } + + if !found_resource { + return Err(anyhow::anyhow!( + "No resources folder found in the github repository [4tkbytes/dropbear] :(" + )); + } + + // fuck you windows :( + #[cfg(target_os = "windows")] + { + println!("cargo:rustc-link-arg=/FORCE:MULTIPLE"); + println!("cargo:rustc-link-arg=/NODEFAULTLIB:libcmt.lib"); + } + + println!("cargo:rerun-if-changed=build.rs"); + Ok(()) +} @@ -0,0 +1,469 @@ +use std::{collections::HashMap, fs, path::PathBuf, process::Command}; + +use clap::ArgMatches; +use zip::write::SimpleFileOptions; + +use eucalyptus_core::states::{ProjectConfig, RuntimeData, SCENES, SOURCE}; + +pub fn package(project_path: PathBuf, _sub_matches: &ArgMatches) -> anyhow::Result<()> { + if !project_path.exists() { + return Err(anyhow::anyhow!("Unable to locate project config file")); + } + + let build_dir = project_path.parent().ok_or(anyhow::anyhow!("Unable to get parent"))?.join("build"); + + // check health + println!("Checking health (checking if commands exist)"); + health()?; + println!("Health check completed!"); + + let clone_dir = build_dir.join("redback-runtime"); + + if clone_dir.exists() { + println!("Repository directory exists, checking for updates..."); + if should_update_repository(&clone_dir)? { + println!("Repository has changes or is outdated, removing and re-cloning..."); + std::fs::remove_dir_all(&clone_dir)?; + clone_repository(&build_dir)?; + } else { + println!("Repository is up to date, skipping clone"); + } + } else { + println!("Cloning repository"); + clone_repository(&build_dir)?; + } + + let project_config = ProjectConfig::read_from(&project_path)?; + let project_name = project_config.project_name.clone(); + + // cd into redback-runtime folder and compile redback-runtime using cargo + let runtime_dir = build_dir.join("redback-runtime"); + if !runtime_dir.exists() { + return Err(anyhow::anyhow!("redback-runtime directory not found after cloning")); + } + + let cargo_toml_path = runtime_dir.join("Cargo.toml"); + if cargo_toml_path.exists() { + let cargo_toml_content = std::fs::read_to_string(&cargo_toml_path)?; + let modified_content = cargo_toml_content.replace( + r#"name = "redback-runtime""#, + &format!(r#"name = "{}""#, project_name) + ); + std::fs::write(&cargo_toml_path, modified_content)?; + println!("Updated Cargo.toml with project name: {}", project_name); + } + + println!("Building {} for release", project_name); + let mut cargo_build = Command::new("cargo") + .args(&["build", "--release"]) + .current_dir(&runtime_dir) + .stdout(std::process::Stdio::inherit()) + .stderr(std::process::Stdio::inherit()) + .spawn()?; + + let exit_status = cargo_build.wait()?; + + if !exit_status.success() { + return Err(anyhow::anyhow!("Failed to build {}", project_name)); + } + println!("{} built successfully!", project_name); + + let target_dir = runtime_dir.join("target").join("release"); + let exe_name = if cfg!(target_os = "windows") { + format!("{}.exe", project_name) + } else { + project_name.clone() + }; + + let built_exe = target_dir.join(&exe_name); + if !built_exe.exists() { + return Err(anyhow::anyhow!("Built executable not found at: {}", built_exe.display())); + } + + println!("Building project data (.eupak file)"); + build(project_path.clone())?; + + let output_dir = project_path.parent().ok_or(anyhow::anyhow!("Unable to get parent"))?.join("build").join("package"); + std::fs::create_dir_all(&output_dir)?; + + let output_exe = output_dir.join(&exe_name); + + println!("Copying executable to: {}", output_exe.display()); + std::fs::copy(&built_exe, &output_exe)?; + + let eupak_source = project_path.parent().ok_or(anyhow::anyhow!("Unable to get parent"))?.join("build").join("output").join(format!("{}.eupak", project_name)); + let eupak_dest = output_dir.join(format!("{}.eupak", project_name)); + + if !eupak_source.exists() { + return Err(anyhow::anyhow!("Expected .eupak file not found at: {}", eupak_source.display())); + } + + println!("Copying .eupak file to: {}", eupak_dest.display()); + std::fs::copy(&eupak_source, &eupak_dest)?; + + let project_resources = project_path.parent().ok_or(anyhow::anyhow!("Unable to get parent"))?.join("resources"); + if project_resources.exists() { + println!("Copying resources folder..."); + let output_resources = output_dir.join("resources"); + copy_resources_folder(&project_resources, &output_resources)?; + } + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = std::fs::metadata(&output_exe)?.permissions(); + perms.set_mode(0o755); // rwxr-xr-x + std::fs::set_permissions(&output_exe, perms)?; + } + + copy_system_libraries(&output_dir)?; + + println!("Creating zip package..."); + let zip_path = project_path.parent().ok_or(anyhow::anyhow!("Unable to get parent"))?.join("build").join(format!("{}.zip", project_name)); + create_zip_package(&output_dir, &zip_path)?; + + println!("Cleaning up temporary files"); + if build_dir.join("redback-runtime").exists() { + std::fs::remove_dir_all(build_dir.join("redback-runtime"))?; + } + if build_dir.join("output").exists() { + std::fs::remove_dir_all(build_dir.join("output"))?; + } + + println!("\n✓ Package completed successfully!"); + println!("Output directory: {}", output_dir.display()); + println!("Zip package: {}", zip_path.display()); + println!("Executable: {}", exe_name); + println!("Data file: {}.eupak", project_name); + + Ok(()) +} + +fn clone_repository(build_dir: &PathBuf) -> anyhow::Result<()> { + git2::build::RepoBuilder::new() + .clone("https://github.com/4tkbytes/redback-runtime", &build_dir.join("redback-runtime"))?; + println!("Repository cloned successfully!"); + Ok(()) +} + +fn should_update_repository(repo_dir: &PathBuf) -> anyhow::Result<bool> { + let repo = match git2::Repository::open(repo_dir) { + Ok(repo) => repo, + Err(_) => { + return Ok(true); + } + }; + + let statuses = repo.statuses(None)?; + if !statuses.is_empty() { + println!("Found local changes in repository"); + return Ok(true); + } + + let mut remote = repo.find_remote("origin")?; + remote.fetch(&["refs/heads/*:refs/remotes/origin/*"], None, None)?; + + let head = repo.head()?.target().ok_or(anyhow::anyhow!("No HEAD commit"))?; + + let remote_ref = if let Ok(remote_main) = repo.find_reference("refs/remotes/origin/main") { + remote_main + } else if let Ok(remote_master) = repo.find_reference("refs/remotes/origin/master") { + remote_master + } else { + return Ok(true); + }; + + let remote_commit = remote_ref.target().ok_or(anyhow::anyhow!("No remote commit"))?; + + Ok(head != remote_commit) +} + +fn copy_resources_folder(src: &PathBuf, dest: &PathBuf) -> anyhow::Result<()> { + std::fs::create_dir_all(dest)?; + + for entry in fs::read_dir(src)? { + let entry = entry?; + let src_path = entry.path(); + let file_name = entry.file_name(); + + if file_name == "resources.eucc" { + continue; + } + + let dest_path = dest.join(&file_name); + + if src_path.is_dir() { + copy_resources_folder(&src_path, &dest_path)?; + } else { + std::fs::copy(&src_path, &dest_path)?; + } + } + + Ok(()) +} + +fn copy_system_libraries(output_dir: &PathBuf) -> anyhow::Result<()> { + #[cfg(target_os = "windows")] + { + let dll_paths = vec![ + "C:\\vcpkg\\installed\\x64-windows\\bin\\assimp-vc143-mt.dll", + "C:\\vcpkg\\installed\\x64-windows\\bin\\assimp.dll", + "C:\\Program Files\\Assimp\\bin\\assimp.dll", + "C:\\Program Files (x86)\\Assimp\\bin\\assimp.dll", + ]; + + for dll_path in dll_paths { + if std::path::Path::new(dll_path).exists() { + let dll_name = std::path::Path::new(dll_path).file_name().unwrap(); + let dest = output_dir.join(dll_name); + std::fs::copy(dll_path, dest)?; + println!("Copied system library: {}", dll_name.to_string_lossy()); + break; + } + } + } + + #[cfg(any(target_os = "linux", target_os = "macos"))] + { + let lib_dir = output_dir.join("lib"); + std::fs::create_dir_all(&lib_dir)?; + + let lib_paths = vec![ + "/usr/lib/libassimp.so", + "/usr/lib/x86_64-linux-gnu/libassimp.so", + "/usr/lib64/libassimp.so", + "/usr/local/lib/libassimp.so", + "/opt/homebrew/lib/libassimp.dylib", + "/usr/local/lib/libassimp.dylib", + ]; + + for lib_path in lib_paths { + if std::path::Path::new(lib_path).exists() { + let lib_name = std::path::Path::new(lib_path).file_name().unwrap(); + let dest = lib_dir.join(lib_name); + std::fs::copy(lib_path, dest)?; + println!("Copied system library: {}", lib_name.to_string_lossy()); + break; + } + } + } + + Ok(()) +} + +fn create_zip_package(source_dir: &PathBuf, zip_path: &PathBuf) -> anyhow::Result<()> { + let file = fs::File::create(zip_path)?; + let mut zip = zip::ZipWriter::new(file); + + let walkdir = walkdir::WalkDir::new(source_dir); + for entry in walkdir { + let entry = entry?; + let path = entry.path(); + + if path.is_file() { + let relative_path = path.strip_prefix(source_dir)?; + let name = relative_path.to_string_lossy(); + + let options: SimpleFileOptions = Default::default(); + zip.start_file(name, options.into())?; + let mut file = std::fs::File::open(path)?; + std::io::copy(&mut file, &mut zip)?; + } + } + + zip.finish()?; + println!("Created zip package: {}", zip_path.display()); + Ok(()) +} + +pub fn read_from_eupak(eupak_path: PathBuf) -> anyhow::Result<()> { + let bytes = std::fs::read(&eupak_path)?; + let (content, _): (RuntimeData, usize) = bincode::decode_from_slice(&bytes, bincode::config::standard())?; + println!("{} contents: {:#?}", eupak_path.display(), content); + Ok(()) +} + +pub fn build( + project_path: PathBuf, + // _sub_matches: &ArgMatches + ) -> anyhow::Result<PathBuf> { + println!(" > Starting build"); + if !project_path.exists() { + return Err(anyhow::anyhow!(format!("Unable to locate project config file: [{}]", project_path.display()))); + } + // ProjectConfig::read_from(&project_path)?.load_config_to_memory()?; + + let mut project_config = ProjectConfig::read_from(&project_path)?; + log::info!(" > Reading from project config"); + project_config.load_config_to_memory()?; + log::info!(" > Loading config to memory"); + + let source_config = { + let source_guard = SOURCE.read(); + source_guard.clone() + }; + log::info!(" > Copied source config"); + + let scene_data = { + let scenes_guard = SCENES.read(); + scenes_guard.clone() + }; + log::info!(" > Copied scene data"); + + let build_dir = project_path.parent().unwrap().join("build").join("output"); + fs::create_dir_all(&build_dir)?; + log::info!(" > Created build dir"); + + let project_name = project_config.project_name.clone(); + + let mut scripts = HashMap::new(); + let script_dir = project_path.parent().unwrap().join("src"); + if script_dir.exists() { + for entry in fs::read_dir(&script_dir)? { + let entry = entry?; + let path = entry.path(); + if let Some(ext) = path.extension() { + if ext == "rhai" { + let name = path.file_name().unwrap().to_string_lossy().to_string(); + let contents = fs::read_to_string(&path)?; + println!(" > Copied script info from [{}]", name); + scripts.insert(name, contents); + } + } + } + } + + let runtime_data = RuntimeData { + project_config, + source_config, + scene_data, + scripts + }; + log::info!(" > Created runtime data structures"); + + let runtime_file = build_dir.join(format!("{}.eupak", project_name)); + let serialized = bincode::serde::encode_to_vec(runtime_data, bincode::config::standard())?; + std::fs::write(&runtime_file, serialized)?; + log::info!(" > Written the file to build location"); + + println!("Build completed successfully. Output at {:?}", runtime_file.display()); + Ok(runtime_file) +} + +pub fn health() -> anyhow::Result<()> { + let mut all_healthy = true; + + match Command::new("cargo").arg("--version").output() { + Ok(output) => { + if output.status.success() { + let version = String::from_utf8_lossy(&output.stdout).trim().to_string(); + println!("Does cargo exist? ✓ YES - {}", version); + + match Command::new("rustc").arg("--version").output() { + Ok(rustc_output) => { + if rustc_output.status.success() { + let rustc_version = String::from_utf8_lossy(&rustc_output.stdout).trim().to_string(); + println!("Does rustc compiler exist? ✓ YES - {}", rustc_version); + } else { + println!("Does rustc compiler exist? ✗ NO - rustc command failed"); + all_healthy = false; + } + } + Err(_) => { + println!("Does rustc compiler exist? ✗ NO - rustc not found in PATH"); + all_healthy = false; + } + } + } else { + println!("Does cargo exist? ✗ NO - cargo command failed"); + println!("Does rustc compiler exist? ✗ NO - cargo failed, cannot check rustc"); + all_healthy = false; + } + } + Err(_) => { + println!("Does cargo exist? ✗ NO - cargo not found in PATH"); + println!("Does rustc compiler exist? ✗ NO - cargo not found, cannot check rustc"); + all_healthy = false; + } + } + + let assimp_found = check_assimp_availability(); + if assimp_found { + println!("Does assimp lib exist? ✓ YES - Found assimp library"); + } else { + println!("Does assimp lib exist? ⚠ MAYBE - Could not definitively locate assimp, but it may be available through system package manager or vcpkg"); + } + + if all_healthy { + println!("\n✓ All core tools are available!"); + } else { + println!("\n✗ Some required tools are missing. Please install Rust and Cargo."); + return Err(anyhow::anyhow!("Health check failed - missing required tools")); + } + + Ok(()) +} + +fn check_assimp_availability() -> bool { + #[cfg(target_os = "windows")] + { + let common_paths = vec![ + "C:\\vcpkg\\installed\\x64-windows\\lib\\assimp-vc143-mt.lib", + "C:\\vcpkg\\installed\\x64-windows\\lib\\assimp.lib", + "C:\\vcpkg\\installed\\x86-windows\\lib\\assimp-vc143-mt.lib", + "C:\\vcpkg\\installed\\x86-windows\\lib\\assimp.lib", + "C:\\Program Files\\Assimp\\lib\\assimp.lib", + "C:\\Program Files (x86)\\Assimp\\lib\\assimp.lib", + ]; + + for path in common_paths { + if std::path::Path::new(path).exists() { + return true; + } + } + + if let Ok(output) = Command::new("where").arg("assimp.dll").output() { + if output.status.success() { + return true; + } + } + } + + #[cfg(any(target_os = "linux", target_os = "macos"))] + { + let common_paths = vec![ + "/usr/lib/libassimp.so", + "/usr/lib/x86_64-linux-gnu/libassimp.so", + "/usr/lib64/libassimp.so", + "/usr/local/lib/libassimp.so", + "/opt/homebrew/lib/libassimp.dylib", + "/usr/local/lib/libassimp.dylib", + ]; + + for path in common_paths { + if std::path::Path::new(path).exists() { + return true; + } + } + + if let Ok(output) = Command::new("pkg-config").args(&["--exists", "assimp"]).output() { + if output.status.success() { + return true; + } + } + + #[cfg(target_os = "linux")] + { + if let Ok(output) = Command::new("ldconfig").args(&["-p"]).output() { + if output.status.success() { + let output_str = String::from_utf8_lossy(&output.stdout); + if output_str.contains("libassimp") { + return true; + } + } + } + } + } + + false +} @@ -0,0 +1,148 @@ +use std::time::Instant; +use egui::{CollapsingHeader, Ui}; +use hecs::Entity; +use dropbear_engine::camera::Camera; +use eucalyptus_core::camera::{CameraAction, CameraComponent, CameraFollowTarget, CameraType}; +use crate::editor::component::Component; +use crate::editor::{EntityType, Signal, StaticallyKept, UndoableAction}; + +impl Component for Camera { + fn inspect(&mut self, entity: &mut Entity, cfg: &mut StaticallyKept, ui: &mut Ui, undo_stack: &mut Vec<UndoableAction>, _signal: &mut Signal, _label: &mut String) { + let _ = _signal; + ui.group(|ui| { + CollapsingHeader::new("Camera").default_open(true).show(ui, |ui| { + ui.horizontal(|ui| { + ui.label("Name: "); + let resp = ui.text_edit_singleline(&mut self.label); + + if resp.changed() { + if cfg.old_label_entity.is_none() { + cfg.old_label_entity = Some(entity.clone()); + cfg.label_original = Some(self.label.clone()); + } + cfg.label_last_edit = Some(Instant::now()); + } + + if resp.lost_focus() { + if let Some(ent) = cfg.old_label_entity.take() { + if ent == *entity { + if let Some(orig) = cfg.label_original.take() { + UndoableAction::push_to_undo( + undo_stack, + UndoableAction::Label(ent, orig, EntityType::Entity), + ); + log::debug!("Pushed camera label change to undo stack"); + } + } else { + cfg.label_original = None; + } + } + cfg.label_last_edit = None; + } + }); + + ui.horizontal(|ui| { + ui.label("Position:"); + ui.label(format!("{:.2}, {:.2}, {:.2}", self.eye.x, self.eye.y, self.eye.z)); + }); + + ui.horizontal(|ui| { + ui.label("Target:"); + ui.label(format!("{:.2}, {:.2}, {:.2}", self.target.x, self.target.y, self.target.z)); + }); + }); + }); + } +} + +#[derive(Debug)] +pub enum UndoableCameraAction { + Speed(Entity, f64), + Sensitivity(Entity, f64), + FOV(Entity, f64), + Type(Entity, CameraType), +} + +impl Component for CameraComponent { + fn inspect(&mut self, _entity: &mut Entity, _cfg: &mut StaticallyKept, ui: &mut Ui, _undo_stack: &mut Vec<UndoableAction>, _signal: &mut Signal, _label: &mut String) { + ui.group(|ui| { + CollapsingHeader::new("Camera Component").default_open(true).show(ui, |ui| { + ui.horizontal(|ui| { + ui.label("Type:"); + egui::ComboBox::from_label("") + .selected_text(format!("{:?}", self.camera_type)) + .show_ui(ui, |ui| { + ui.selectable_value(&mut self.camera_type, CameraType::Normal, "Normal"); + if !matches!(self.camera_type, CameraType::Player) { + ui.selectable_value(&mut self.camera_type, CameraType::Debug, "Debug"); + } else { + ui.add_enabled(false, egui::Button::new("Debug")); + ui.label("Debug not available for player cameras"); + } + ui.selectable_value(&mut self.camera_type, CameraType::Player, "Player"); + }); + }); + + ui.horizontal(|ui| { + ui.label("Speed:"); + ui.add(egui::DragValue::new(&mut self.speed).speed(0.1).range(0.1..=20.0)); + }); + + ui.horizontal(|ui| { + ui.label("Sensitivity:"); + ui.add(egui::DragValue::new(&mut self.sensitivity).speed(0.0001).range(0.0001..=1.0)); + }); + + ui.horizontal(|ui| { + ui.label("FOV:"); + ui.add(egui::Slider::new(&mut self.fov_y, 10.0..=120.0).suffix("°")); + }); + + if matches!(self.camera_type, CameraType::Player) { + ui.separator(); + ui.label("Player Camera Controls:"); + if ui.button("Set as Active Camera").clicked() { + // This would need to be implemented via signal + log::info!("Set player camera as active (not implemented)"); + } + } + }); + }); + } +} + +impl Component for CameraFollowTarget { + fn inspect(&mut self, _entity: &mut Entity, _cfg: &mut StaticallyKept, ui: &mut Ui, _undo_stack: &mut Vec<UndoableAction>, signal: &mut Signal, _label: &mut String) { + ui.group(|ui| { + CollapsingHeader::new("Camera Follow Target").default_open(true).show(ui, |ui| { + ui.horizontal(|ui| { + ui.label("Target Entity:"); + ui.text_edit_singleline(&mut self.follow_target); + }); + + ui.horizontal(|ui| { + ui.label("Offset:"); + }); + + ui.horizontal(|ui| { + ui.label("X:"); + ui.add(egui::DragValue::new(&mut self.offset.x).speed(0.1)); + }); + + ui.horizontal(|ui| { + ui.label("Y:"); + ui.add(egui::DragValue::new(&mut self.offset.y).speed(0.1)); + }); + + ui.horizontal(|ui| { + ui.label("Z:"); + ui.add(egui::DragValue::new(&mut self.offset.z).speed(0.1)); + }); + + if ui.button("Clear Target").clicked() { + *signal = Signal::CameraAction(CameraAction::ClearPlayerTarget); + } + }); + }); + } +} @@ -0,0 +1,18 @@ +//! Used to aid with debugging any issues with the editor. +use egui::Ui; +use crate::editor::Signal; + +/// Show a menu bar for debug. A new "Debug" menu button will show up on the editors menu bar. +pub(crate) fn show_menu_bar(ui: &mut Ui, signal: &mut Signal) { + ui.menu_button("Debug", |ui_debug| { + if ui_debug.button("Panic").clicked() { + log::warn!("Panic caused on purpose from Menu Button Click"); + panic!("Testing out panicking with new panic module, this is a test") + } + + if ui_debug.button("Show Entities Loaded").clicked() { + log::info!("Show Entities Loaded under Debug Menu is clicked"); + *signal = Signal::LogEntities; + } + }); +} @@ -0,0 +1,666 @@ +//! This module should describe the different components that are editable in the resource inspector. + +use std::time::Instant; +use egui::{CollapsingHeader, ComboBox, Ui}; +use glam::Vec3; +use hecs::Entity; +use dropbear_engine::attenuation::ATTENUATION_PRESETS; +use dropbear_engine::entity::{AdoptedEntity, Transform}; +use dropbear_engine::lighting::{Light, LightComponent, LightType}; +use crate::editor::{EntityType, Signal, StaticallyKept, UndoableAction}; +use eucalyptus_core::scripting::{ScriptAction, TEMPLATE_SCRIPT}; +use eucalyptus_core::states::ScriptComponent; +use eucalyptus_core::warn; + +pub trait Component { + fn inspect(&mut self, entity: &mut hecs::Entity, cfg: &mut StaticallyKept, ui: &mut Ui, undo_stack: &mut Vec<UndoableAction>, signal: &mut Signal, label: &mut String); +} + +impl Component for Transform { + fn inspect(&mut self, entity: &mut Entity, cfg: &mut StaticallyKept, ui: &mut Ui, undo_stack: &mut Vec<UndoableAction>, _signal: &mut Signal, _label: &mut String) { + ui.group(|ui| { + CollapsingHeader::new("Transform").default_open(true).show( + ui, + |ui| { + ui.horizontal(|ui| { + ui.label("Position:"); + }); + + ui.horizontal(|ui| { + ui.label("X:"); + let response = ui.add( + egui::DragValue::new(&mut self.position.x) + .speed(0.1) + .fixed_decimals(3), + ); + + if response.drag_started() { + cfg.transform_old_entity = Some(entity.clone()); + cfg.transform_original_transform = Some((*self).clone()); + cfg.transform_in_progress = true; + } + + if response.drag_stopped() && cfg.transform_in_progress { + if let Some(ent) = cfg.transform_old_entity.take() { + if let Some(orig) = cfg.transform_original_transform.take() { + UndoableAction::push_to_undo( + undo_stack, + UndoableAction::Transform(ent, orig), + ); + log::debug!("Pushed X transform change to undo stack"); + } + } + cfg.transform_in_progress = false; + } + }); + ui.horizontal(|ui| { + ui.label("Y:"); + let response = ui.add( + egui::DragValue::new(&mut self.position.y) + .speed(0.1) + .fixed_decimals(3), + ); + + if response.drag_started() { + cfg.transform_old_entity = Some(entity.clone()); + cfg.transform_original_transform = Some((*self).clone()); + cfg.transform_in_progress = true; + } + + if response.drag_stopped() && cfg.transform_in_progress { + if let Some(ent) = cfg.transform_old_entity.take() { + if let Some(orig) = cfg.transform_original_transform.take() { + UndoableAction::push_to_undo( + undo_stack, + UndoableAction::Transform(ent, orig), + ); + log::debug!("Pushed Y transform change to undo stack"); + } + } + cfg.transform_in_progress = false; + } + }); + + ui.horizontal(|ui| { + ui.label("Z:"); + let response = ui.add( + egui::DragValue::new(&mut self.position.z) + .speed(0.1) + .fixed_decimals(3), + ); + + if response.drag_started() { + cfg.transform_old_entity = Some(entity.clone()); + cfg.transform_original_transform = Some((*self).clone()); + cfg.transform_in_progress = true; + } + + if response.drag_stopped() && cfg.transform_in_progress { + if let Some(ent) = cfg.transform_old_entity.take() { + if let Some(orig) = cfg.transform_original_transform.take() { + UndoableAction::push_to_undo( + undo_stack, + UndoableAction::Transform(ent, orig), + ); + log::debug!("Pushed Z transform change to undo stack"); + } + } + cfg.transform_in_progress = false; + } + }); + if ui.button("Reset Position").clicked() { + self.position = glam::DVec3::ZERO; + } + + ui.add_space(5.0); + + ui.horizontal(|ui| { + ui.label("Rotation:"); + }); + + let mut rotation_changed = false; + let (mut x_rot, mut y_rot, mut z_rot) = + self.rotation.to_euler(glam::EulerRot::XYZ); + + ui.horizontal(|ui| { + ui.label("X:"); + let response = ui.add( + egui::Slider::new( + &mut x_rot, + -std::f64::consts::PI + ..=std::f64::consts::PI, + ) + .step_by(0.01) + .custom_formatter(|n, _| { + format!("{:.1}°", n.to_degrees()) + }) + .custom_parser(|s| { + s.trim_end_matches('°') + .parse::<f64>() + .ok() + .map(|v| v.to_radians()) + }), + ); + + if response.drag_started() { + cfg.transform_old_entity = Some(entity.clone()); + cfg.transform_original_transform = Some((*self).clone()); + cfg.transform_in_progress = true; + } + + if response.changed() { + rotation_changed = true; + } + + if response.drag_stopped() && cfg.transform_in_progress { + if let Some(ent) = cfg.transform_old_entity.take() { + if let Some(orig) = cfg.transform_original_transform.take() { + UndoableAction::push_to_undo( + undo_stack, + UndoableAction::Transform(ent, orig), + ); + log::debug!("Pushed X rotation change to undo stack"); + } + } + cfg.transform_in_progress = false; + } + }); + + ui.horizontal(|ui| { + ui.label("Y:"); + let response = ui.add( + egui::Slider::new( + &mut y_rot, + -std::f64::consts::PI + ..=std::f64::consts::PI, + ) + .step_by(0.01) + .custom_formatter(|n, _| { + format!("{:.1}°", n.to_degrees()) + }) + .custom_parser(|s| { + s.trim_end_matches('°') + .parse::<f64>() + .ok() + .map(|v| v.to_radians()) + }), + ); + + if response.drag_started() { + cfg.transform_old_entity = Some(entity.clone()); + cfg.transform_original_transform = Some((*self).clone()); + cfg.transform_in_progress = true; + } + + if response.changed() { + rotation_changed = true; + } + + if response.drag_stopped() && cfg.transform_in_progress { + if let Some(ent) = cfg.transform_old_entity.take() { + if let Some(orig) = cfg.transform_original_transform.take() { + UndoableAction::push_to_undo( + undo_stack, + UndoableAction::Transform(ent, orig), + ); + log::debug!("Pushed Y rotation change to undo stack"); + } + } + cfg.transform_in_progress = false; + } + }); + + ui.horizontal(|ui| { + ui.label("Z:"); + let response = ui.add( + egui::Slider::new( + &mut z_rot, + -std::f64::consts::PI + ..=std::f64::consts::PI, + ) + .step_by(0.01) + .custom_formatter(|n, _| { + format!("{:.1}°", n.to_degrees()) + }) + .custom_parser(|s| { + s.trim_end_matches('°') + .parse::<f64>() + .ok() + .map(|v| v.to_radians()) + }), + ); + + if response.drag_started() { + cfg.transform_old_entity = Some(entity.clone()); + cfg.transform_original_transform = Some((*self).clone()); + cfg.transform_in_progress = true; + } + + if response.changed() { + rotation_changed = true; + } + + if response.drag_stopped() && cfg.transform_in_progress { + if let Some(ent) = cfg.transform_old_entity.take() { + if let Some(orig) = cfg.transform_original_transform.take() { + UndoableAction::push_to_undo( + undo_stack, + UndoableAction::Transform(ent, orig), + ); + log::debug!("Pushed Z rotation change to undo stack"); + } + } + cfg.transform_in_progress = false; + } + }); + + if rotation_changed { + self.rotation = glam::DQuat::from_euler( + glam::EulerRot::XYZ, + x_rot, + y_rot, + z_rot, + ); + } + if ui.button("Reset Rotation").clicked() { + self.rotation = glam::DQuat::IDENTITY; + } + ui.add_space(5.0); + + ui.horizontal(|ui| { + ui.label("Scale:"); + ui.with_layout( + egui::Layout::right_to_left(egui::Align::Center), + |ui| { + let lock_icon = if cfg.scale_locked { + "🔒" + } else { + "🔓" + }; + if ui + .button(lock_icon) + .on_hover_text("Lock uniform scaling") + .clicked() + { + cfg.scale_locked = !cfg.scale_locked; + } + }, + ); + }); + + let mut scale_changed = false; + let mut new_scale = self.scale; + + ui.horizontal(|ui| { + ui.label("X:"); + let response = ui.add( + egui::DragValue::new(&mut new_scale.x) + .speed(0.01) + .fixed_decimals(3), + ); + + if response.drag_started() { + cfg.transform_old_entity = Some(entity.clone()); + cfg.transform_original_transform = Some((*self).clone()); + cfg.transform_in_progress = true; + } + + if response.changed() { + scale_changed = true; + if cfg.scale_locked { + let scale_factor = new_scale.x / self.scale.x; + new_scale.y = self.scale.y * scale_factor; + new_scale.z = self.scale.z * scale_factor; + } + } + + if response.drag_stopped() && cfg.transform_in_progress { + if let Some(ent) = cfg.transform_old_entity.take() { + if let Some(orig) = cfg.transform_original_transform.take() { + UndoableAction::push_to_undo( + undo_stack, + UndoableAction::Transform(ent, orig), + ); + log::debug!("Pushed X scale change to undo stack"); + } + } + cfg.transform_in_progress = false; + } + }); + + ui.horizontal(|ui| { + ui.label("Y:"); + let y_slider = egui::DragValue::new(&mut new_scale.y) + .speed(0.01) + .fixed_decimals(3); + + let response = ui.add_enabled(!cfg.scale_locked, y_slider); + + if response.drag_started() && !cfg.scale_locked { + cfg.transform_old_entity = Some(entity.clone()); + cfg.transform_original_transform = Some((*self).clone()); + cfg.transform_in_progress = true; + } + + if response.changed() { + scale_changed = true; + } + + if response.drag_stopped() && cfg.transform_in_progress { + if let Some(ent) = cfg.transform_old_entity.take() { + if let Some(orig) = cfg.transform_original_transform.take() { + UndoableAction::push_to_undo( + undo_stack, + UndoableAction::Transform(ent, orig), + ); + log::debug!("Pushed Y scale change to undo stack"); + } + } + cfg.transform_in_progress = false; + } + }); + + ui.horizontal(|ui| { + ui.label("Z:"); + let z_slider = egui::DragValue::new(&mut new_scale.z) + .speed(0.01) + .fixed_decimals(3); + + let response = ui.add_enabled(!cfg.scale_locked, z_slider); + + if response.drag_started() && !cfg.scale_locked { + cfg.transform_old_entity = Some(entity.clone()); + cfg.transform_original_transform = Some((*self).clone()); + cfg.transform_in_progress = true; + } + + if response.changed() { + scale_changed = true; + } + + if response.drag_stopped() && cfg.transform_in_progress { + if let Some(ent) = cfg.transform_old_entity.take() { + if let Some(orig) = cfg.transform_original_transform.take() { + UndoableAction::push_to_undo( + undo_stack, + UndoableAction::Transform(ent, orig), + ); + log::debug!("Pushed Z scale change to undo stack"); + } + } + cfg.transform_in_progress = false; + } + }); + + if scale_changed { + self.scale = new_scale; + } + if ui.button("Reset Scale").clicked() { + self.scale = glam::DVec3::ONE; + } + ui.add_space(5.0); + + // maybe use? probs not :/ + // if pos_changed || rotation_changed || scale_changed { + // ui.colored_label(egui::Color32::YELLOW, "Transform modified"); + // } + }, + ); + }); + } +} + +impl Component for ScriptComponent { + fn inspect(&mut self, _entity: &mut Entity, _cfg: &mut StaticallyKept, ui: &mut Ui, _undo_stack: &mut Vec<UndoableAction>, signal: &mut Signal, label: &mut String) { + let script_loc = self.path.to_str().unwrap_or("").to_string(); + + ui.group(|ui| { + CollapsingHeader::new("Scripting") + .default_open(true) + .show(ui, |ui| { + ui.horizontal(|ui| { + if ui.button("Browse").clicked() { + if let Some(script_file) = rfd::FileDialog::new() + .add_filter("Typescript", &["ts"]) + .pick_file() + { + let script_name = script_file + .file_stem() + .unwrap_or_default() + .to_string_lossy() + .to_string(); + let lib_path = &script_file.clone().parent().unwrap().join("dropbear.ts"); + if let Err(_) = std::fs::read(lib_path) { + log::warn!("dropbear.ts library does not exist in project source directory, copying..."); + if let Err(e) = std::fs::write(lib_path, include_str!("../../../resources/dropbear.ts")) { + log::error!("Non-fatal error: Creating library file failed: {}", e); + } else { + log::info!("Wrote dropbear.ts library file!"); + } + }; + *signal = Signal::ScriptAction(ScriptAction::AttachScript { + script_path: script_file, + script_name, + }); + } + } + + if ui.button("New").clicked() { + if let Some(script_path) = rfd::FileDialog::new() + .add_filter("TypeScript", &["ts"]) + .set_file_name(format!("{}_script.ts", label)) + .save_file() + { + // check if dropbear module exists + // todo: change this to an %APPDATA% file instead of to memory. + let lib_path = &script_path.clone().parent().unwrap().join("dropbear.ts"); + if let Err(_) = std::fs::read(lib_path) { + log::warn!("dropbear.ts library does not exist in project source directory, copying..."); + if let Err(e) = std::fs::write(lib_path, include_str!("../../../resources/dropbear.ts")) { + log::error!("Non-fatal error: Creating library file failed: {}", e); + } else { + log::info!("Wrote dropbear.ts library file!"); + } + }; + match std::fs::write(&script_path, TEMPLATE_SCRIPT) { + Ok(_) => { + let script_name = script_path + .file_stem() + .unwrap_or_default() + .to_string_lossy() + .to_string(); + *signal = Signal::ScriptAction(ScriptAction::CreateAndAttachScript { + script_path, + script_name, + }); + }, + Err(e) => { + warn!("Failed to create script file: {}", e); + }, + } + } + } + }); + + ui.separator(); + + ui.horizontal_wrapped(|ui| { + ui.label("Script Location:"); + ui.label(script_loc); + }); + + if ui.button("Remove").clicked() { + *signal = Signal::ScriptAction(ScriptAction::RemoveScript); + } + ui.separator(); + ui.horizontal(|ui| { + if ui.button("Edit Script").clicked() { + *signal = Signal::ScriptAction(ScriptAction::EditScript); + } + }); + }); + }); + } +} + +impl Component for AdoptedEntity { + fn inspect(&mut self, entity: &mut Entity, cfg: &mut StaticallyKept, ui: &mut Ui, undo_stack: &mut Vec<UndoableAction>, _signal: &mut Signal, _label: &mut String) { + // label + ui.group(|ui| { + ui.horizontal(|ui| { + ui.label("Name: "); + + let resp = ui.text_edit_singleline(self.label_mut()); + + if resp.changed() { + if cfg.old_label_entity.is_none() { + cfg.old_label_entity = Some(entity.clone()); + cfg.label_original = Some(self.label().clone()); + } + cfg.label_last_edit = Some(Instant::now()); + } + + if resp.lost_focus() { + if let Some(ent) = cfg.old_label_entity.take() { + if ent == *entity { + if let Some(orig) = cfg.label_original.take() { + UndoableAction::push_to_undo( + undo_stack, + UndoableAction::Label(ent, orig, EntityType::Entity), + ); + log::debug!("Pushed label change to undo stack (immediate)"); + } + } else { + cfg.label_original = None; + } + } + cfg.label_last_edit = None; + } + }) + }); + } +} + +impl Component for Light { + fn inspect(&mut self, entity: &mut Entity, cfg: &mut StaticallyKept, ui: &mut Ui, undo_stack: &mut Vec<UndoableAction>, _signal: &mut Signal, _label: &mut String) { + ui.group(|ui| { + ui.horizontal(|ui| { + ui.label("Name: "); + + let resp = ui.text_edit_singleline(&mut self.label); + + if resp.changed() { + if cfg.old_label_entity.is_none() { + cfg.old_label_entity = Some(entity.clone()); + cfg.label_original = Some(self.label.clone().to_string()); + } + cfg.label_last_edit = Some(Instant::now()); + } + + if resp.lost_focus() { + if let Some(ent) = cfg.old_label_entity.take() { + if ent == *entity { + if let Some(orig) = cfg.label_original.take() { + UndoableAction::push_to_undo( + undo_stack, + UndoableAction::Label(ent, orig, EntityType::Light), + ); + log::debug!("Pushed label change to undo stack (immediate)"); + } + } else { + cfg.label_original = None; + } + } + cfg.label_last_edit = None; + } + }) + }); + } +} + +impl Component for LightComponent { + fn inspect(&mut self, _entity: &mut Entity, _cfg: &mut StaticallyKept, ui: &mut Ui, _undo_stack: &mut Vec<UndoableAction>, _signal: &mut Signal, _label: &mut String) { + ui.group(|ui| { + ui.horizontal(|ui| { + ComboBox::new("light_type", "Light Type") + // .width(ui.available_width()) + .selected_text(self.light_type.to_string()) + .show_ui(ui, |ui| { + ui.selectable_value(&mut self.light_type, LightType::Directional, "Directional"); + ui.selectable_value(&mut self.light_type, LightType::Point, "Point"); + ui.selectable_value(&mut self.light_type, LightType::Spot, "Spot"); + }); + }); + + // let is_dir = matches!(self.light_type, LightType::Directional); + let is_point = matches!(self.light_type, LightType::Point); + let is_spot = matches!(self.light_type, LightType::Spot); + + // colour + ui.separator(); + let mut colour = self.colour.clone().as_vec3().to_array(); + ui.horizontal(|ui| { + ui.label("Colour"); + egui::color_picker::color_edit_button_rgb(ui, &mut colour) + }); + self.colour = Vec3::from_array(colour).as_dvec3(); + + // intensity + ui.separator(); + ui.horizontal(|ui| { + ui.label("Intensity"); + ui.add(egui::Slider::new(&mut self.intensity, 0.0..=1.0)); + }); + + // enabled and visible + ui.separator(); + ui.horizontal(|ui| { + ui.checkbox(&mut self.enabled, "Enabled"); + ui.checkbox(&mut self.visible, "Visible"); + }); + + if is_spot || is_point { + // attenuation + ui.separator(); + ui.horizontal(|ui| { + ComboBox::new("Range", "Range") + // .width(ui.available_width()) + .selected_text(format!("Range {}", self.attenuation.range.to_string())) + .show_ui(ui, |ui| { + for (preset, label) in ATTENUATION_PRESETS { + ui.selectable_value(&mut self.attenuation, preset.clone(), *label); + } + }); + }); + } + + if is_spot { + // cutoff angles + ui.horizontal(|ui| { + ui.add( + egui::Slider::new(&mut self.cutoff_angle, 1.0..=89.0) + .text("Inner") + .suffix("°") + .step_by(0.1) + ); + }); + + ui.horizontal(|ui| { + ui.add( + egui::Slider::new(&mut self.outer_cutoff_angle, 1.0..=90.0) + .text("Outer") + .suffix("°") + .step_by(0.1) + ); + }); + + if self.outer_cutoff_angle <= self.cutoff_angle { + self.outer_cutoff_angle = self.cutoff_angle + 1.0; + } + + let cone_softness = self.outer_cutoff_angle - self.cutoff_angle; + ui.label(format!("Soft edge: {:.1}°", cone_softness)); + } + }); + } +} @@ -0,0 +1,1024 @@ +use crate::editor::ViewportMode; +use super::*; +use std::{collections::HashSet, sync::LazyLock}; + +use dropbear_engine::{entity::Transform, lighting::{Light, LightComponent}}; +use egui; +use egui_dock_fork::TabViewer; +use egui_extras; +use log; +use parking_lot::Mutex; +use transform_gizmo_egui::{ + math::{DMat4, DVec3}, EnumSet, Gizmo, GizmoConfig, GizmoExt, GizmoMode +}; +use eucalyptus_core::states::{Node, ResourceType, RESOURCES}; +use eucalyptus_core::utils::PendingSpawn; +use crate::APP_INFO; +use crate::editor::component::Component; +use crate::editor::scene::PENDING_SPAWNS; + +pub struct EditorTabViewer<'a> { + pub view: egui::TextureId, + pub nodes: Vec<EntityNode>, + pub tex_size: Extent3d, + pub gizmo: &'a mut Gizmo, + pub world: &'a mut World, + pub selected_entity: &'a mut Option<hecs::Entity>, + pub viewport_mode: &'a mut ViewportMode, + pub undo_stack: &'a mut Vec<UndoableAction>, + pub signal: &'a mut Signal, + pub gizmo_mode: &'a mut EnumSet<GizmoMode>, + pub editor_mode: &'a mut EditorState, + pub active_camera: &'a mut Option<hecs::Entity>, +} + +impl<'a> EditorTabViewer<'a> { + fn spawn_entity_at_pos( + &mut self, + asset: &DraggedAsset, + position: DVec3, + properties: Option<ModelProperties>, + ) -> anyhow::Result<()> { + let mut transform = Transform::default(); + transform.position = position; + { + let mut pending_spawns = PENDING_SPAWNS.lock(); + if let Some(props) = properties { + pending_spawns.push(PendingSpawn { + asset_path: asset.path.clone(), + asset_name: asset.name.clone(), + transform, + properties: props, + }); + } else { + pending_spawns.push(PendingSpawn { + asset_path: asset.path.clone(), + asset_name: asset.name.clone(), + transform, + properties: ModelProperties::default(), + }); + } + Ok(()) + } + } +} + +#[derive(Clone, Debug)] +pub struct DraggedAsset { + pub name: String, + pub path: PathBuf, +} + +pub static TABS_GLOBAL: LazyLock<Mutex<StaticallyKept>> = + LazyLock::new(|| Mutex::new(StaticallyKept::default())); + + +/// Variables kept statically. +/// +/// The entire module (including the tab viewer) due to it +/// being part of an update/render function, therefore this is used to ensure +/// progress is not lost. +#[derive(Default)] +pub struct StaticallyKept { + show_context_menu: bool, + context_menu_pos: egui::Pos2, + context_menu_tab: Option<EditorTab>, + is_focused: bool, + old_pos: Transform, + pub(crate) scale_locked: bool, + + pub(crate) old_label_entity: Option<hecs::Entity>, + pub(crate) label_original: Option<String>, + pub(crate) label_last_edit: Option<Instant>, + + pub(crate) transform_old_entity: Option<hecs::Entity>, + pub(crate) transform_original_transform: Option<Transform>, + + pub(crate) transform_in_progress: bool, +} + +impl StaticallyKept {} + +impl<'a> TabViewer for EditorTabViewer<'a> { + type Tab = EditorTab; + + fn title(&mut self, tab: &mut Self::Tab) -> egui::WidgetText { + match tab { + EditorTab::Viewport => "Viewport".into(), + EditorTab::ModelEntityList => "Model/Entity List".into(), + EditorTab::AssetViewer => "Asset Viewer".into(), + EditorTab::ResourceInspector => "Resource Inspector".into(), + } + } + + fn ui(&mut self, ui: &mut egui::Ui, tab: &mut Self::Tab) { + let mut cfg = TABS_GLOBAL.lock(); + + ui.ctx().input(|i| { + if i.pointer.button_pressed(egui::PointerButton::Secondary) { + if let Some(pos) = i.pointer.hover_pos() { + if ui.available_rect_before_wrap().contains(pos) { + cfg.show_context_menu = true; + cfg.context_menu_pos = pos; + cfg.context_menu_tab = Some(tab.clone()); + } + } + } + }); + + match tab { + EditorTab::Viewport => { + // ------------------- Template for querying active camera ----------------- + // if let Some(active_camera) = self.active_camera { + // if let Ok(mut q) = self.world.query_one::<(&Camera, &CameraComponent, Option<&CameraFollowTarget>)>(*active_camera) { + // if let Some((camera, component, follow_target)) = q.get() { + + // } else { + // log_once::warn_once!("Unable to fetch the query result of camera: {:?}", active_camera) + // } + // } else { + // log_once::warn_once!("Unable to query camera, component and option<camerafollowtarget> for active camera: {:?}", active_camera); + // } + // } else { + // log_once::warn_once!("No active camera found"); + // } + // ------------------------------------------------------------------------- + + let available_rect = ui.available_rect_before_wrap(); + let available_size = available_rect.size(); + + let tex_aspect = self.tex_size.width as f32 / self.tex_size.height as f32; + let available_aspect = available_size.x / available_size.y; + + let (display_width, display_height) = if available_aspect > tex_aspect { + let height = available_size.y * 0.95; + let width = height * tex_aspect; + (width, height) + } else { + let width = available_size.x * 0.95; + let height = width / tex_aspect; + (width, height) + }; + + let center_x = available_rect.center().x; + let center_y = available_rect.center().y; + + let image_rect = egui::Rect::from_center_size( + egui::pos2(center_x, center_y), + egui::vec2(display_width, display_height) + ); + + let (_rect, _response) = ui.allocate_exact_size( + available_size, + egui::Sense::click_and_drag() + ); + + let _image_response = ui.allocate_rect(image_rect, egui::Sense::click_and_drag()); + + ui.scope_builder(egui::UiBuilder::new().max_rect(image_rect), |ui| { + ui.add_sized( + [display_width, display_height], + egui::Image::new(( + self.view, + [display_width, display_height].into(), + )) + .fit_to_exact_size([display_width, display_height].into()) + ) + }); + + let image_response = ui.interact(image_rect, ui.id().with("viewport_image"), egui::Sense::click_and_drag()); + + if image_response.clicked() { + if let Some(active_camera) = self.active_camera { + if let Ok(mut q) = self.world.query_one::<(&Camera, &CameraComponent, Option<&CameraFollowTarget>)>(*active_camera) { + if let Some((camera, _, _)) = q.get() { + if let Some(click_pos) = ui.ctx().input(|i| i.pointer.interact_pos()) { + let viewport_rect = image_response.rect; + let local_pos = click_pos - viewport_rect.min; + + let ndc_x = (2.0 * local_pos.x / viewport_rect.width()) - 1.0; + let ndc_y = 1.0 - (2.0 * local_pos.y / viewport_rect.height()); + + let view_matrix = + glam::DMat4::look_at_lh(camera.eye, camera.target, camera.up); + + let proj_matrix = glam::DMat4::perspective_lh( + camera.fov_y.to_radians(), + camera.aspect, + camera.znear, + camera.zfar, + ); + + if !view_matrix.is_finite() { + log::error!("Invalid view matrix"); + return; + } + if !proj_matrix.is_finite() { + log::error!("Invalid projection matrix"); + return; + } + + let view_proj = proj_matrix * view_matrix; + let inv_view_proj = view_proj.inverse(); + + if !inv_view_proj.is_finite() { + log::error!("Cannot invert view-projection matrix"); + return; + } + + let ray_start_ndc = glam::DVec4::new(ndc_x as f64, ndc_y as f64, 0.0, 1.0); + let ray_end_ndc = glam::DVec4::new(ndc_x as f64, ndc_y as f64, 1.0, 1.0); + + let ray_start_world = inv_view_proj * ray_start_ndc; + let ray_end_world = inv_view_proj * ray_end_ndc; + + if ray_start_world.w == 0.0 || ray_end_world.w == 0.0 { + log::error!("Invalid homogeneous coordinates"); + return; + } + + let ray_start = ray_start_world.truncate() / ray_start_world.w; + let ray_end = ray_end_world.truncate() / ray_end_world.w; + + if !ray_start.is_finite() || !ray_end.is_finite() { + log::error!( + "Invalid ray points - start: {:?}, end: {:?}", + ray_start, + ray_end + ); + return; + } + + let ray_direction = (ray_end - ray_start).normalize(); + + if !ray_direction.is_finite() { + log::error!("Invalid ray direction: {:?}", ray_direction); + return; + } + + let mut closest_distance = f64::INFINITY; + let mut selected_entity_id: Option<hecs::Entity> = None; + let mut entity_count = 0; + + for (entity_id, (transform, _)) in + self.world.query::<(&Transform, &AdoptedEntity)>().iter() + { + entity_count += 1; + let entity_pos = transform.position; + let sphere_radius = transform.scale.max_element() * 1.5; + + let to_sphere = entity_pos - ray_start; + let projection = to_sphere.dot(ray_direction); + + if projection > 0.0 { + let closest_point = ray_start + ray_direction * projection; + let distance_to_sphere = (closest_point - entity_pos).length(); + + if distance_to_sphere <= sphere_radius { + let discriminant = sphere_radius * sphere_radius + - distance_to_sphere * distance_to_sphere; + if discriminant >= 0.0 { + let intersection_distance = + projection - discriminant.sqrt(); + + if intersection_distance < closest_distance { + closest_distance = intersection_distance; + selected_entity_id = Some(entity_id); + } + } + } + } + } + + log::debug!("Total entities checked: {}", entity_count); + + if !matches!(self.editor_mode, EditorState::Playing) { + if let Some(entity_id) = selected_entity_id { + *self.selected_entity = Some(entity_id); + log::debug!("Selected entity: {:?}", entity_id); + } else { + // *self.selected_entity = None; + if entity_count == 0 { + log::debug!("No entities in world to select"); + } else { + log::debug!( + "No entity hit by ray (checked {} entities)", + entity_count + ); + } + } + } + } + } else { + log_once::warn_once!("Unable to fetch the query result of camera: {:?}", active_camera) + } + } else { + log_once::warn_once!("Unable to query camera, component and option<camerafollowtarget> for active camera: {:?}", active_camera); + } + } else { + log_once::warn_once!("No active camera found"); + } + + } + + let snapping = ui.input(|input| input.modifiers.shift); + + // Note to self: fuck you >:( + // Note to self: ok wow thats pretty rude im trying my best >﹏< + // Note to self: finally holy shit i got it working + + if let Some(active_camera) = self.active_camera { + if let Ok(mut q) = self.world.query_one::<(&Camera, &CameraComponent, Option<&CameraFollowTarget>)>(*active_camera) { + if let Some((camera, _, _)) = q.get() { + self.gizmo.update_config(GizmoConfig { + view_matrix: DMat4::look_at_lh( + DVec3::new( + camera.eye.x as f64, + camera.eye.y as f64, + camera.eye.z as f64, + ), + DVec3::new( + camera.target.x as f64, + camera.target.y as f64, + camera.target.z as f64, + ), + DVec3::new(camera.up.x as f64, camera.up.y as f64, camera.up.z as f64), + ) + .into(), + projection_matrix: DMat4::perspective_infinite_reverse_lh( + camera.fov_y as f64, + display_width as f64 / display_height as f64, + camera.znear as f64, + ) + .into(), + viewport: image_rect, + modes: *self.gizmo_mode, + orientation: transform_gizmo_egui::GizmoOrientation::Global, + snapping, + snap_distance: 1.0, + ..Default::default() + }); + } else { + log_once::warn_once!("Unable to fetch the query result of camera: {:?}", active_camera) + } + } else { + log_once::warn_once!("Unable to query camera, component and option<camerafollowtarget> for active camera: {:?}", active_camera); + } + } else { + log_once::warn_once!("No active camera found"); + } + + if !matches!(self.viewport_mode, ViewportMode::None) { + if let Some(entity_id) = self.selected_entity { + if let Ok(transform) = + self.world.query_one_mut::<&mut Transform>(*entity_id) + { + let was_focused = cfg.is_focused; + cfg.is_focused = self.gizmo.is_focused(); + + if cfg.is_focused && !was_focused { + cfg.old_pos = *transform; + } + + let gizmo_transform = + transform_gizmo_egui::math::Transform::from_scale_rotation_translation( + transform.scale, + transform.rotation, + transform.position, + ); + + if let Some((_result, new_transforms)) = + self.gizmo.interact(ui, &[gizmo_transform]) + { + if let Some(new_transform) = new_transforms.first() { + transform.position = new_transform.translation.into(); + transform.rotation = new_transform.rotation.into(); + transform.scale = new_transform.scale.into(); + } + } + + if was_focused && !cfg.is_focused { + let transform_changed = cfg.old_pos.position != transform.position + || cfg.old_pos.rotation != transform.rotation + || cfg.old_pos.scale != transform.scale; + + if transform_changed { + UndoableAction::push_to_undo( + &mut self.undo_stack, + UndoableAction::Transform( + entity_id.clone(), + cfg.old_pos.clone(), + ), + ); + log::debug!("Pushed transform action to stack"); + } + } + } + } + } + } + EditorTab::ModelEntityList => { + ui.label("Model/Entity List"); + show_entity_tree( + ui, + &mut self.nodes, + &mut self.selected_entity, + "Model Entity Asset List", + ); + } + EditorTab::AssetViewer => { + egui_extras::install_image_loaders(ui.ctx()); + + let mut assets: Vec<(String, String, PathBuf, ResourceType)> = Vec::new(); + { + let res = RESOURCES.read(); + egui_extras::install_image_loaders(ui.ctx()); + + fn recursive_search_nodes_and_attach_thumbnail( + res: &Vec<Node>, + assets: &mut Vec<(String, String, PathBuf, ResourceType)>, + logged: &mut HashSet<String>, + ) { + for node in res { + match node { + Node::File(file) => { + if !logged.contains(&file.name) { + logged.insert(file.name.clone()); + log::debug!( + "Adding image for {} of type {}", + file.name, + file.resource_type.as_ref().unwrap() + ); + } + if let Some(ref res_type) = file.resource_type { + match res_type { + ResourceType::Model => { + let ad_dir = app_dirs2::get_app_root( + app_dirs2::AppDataType::UserData, + &APP_INFO, + ) + .unwrap(); + + let model_thumbnail = + ad_dir.join(format!("{}.png", file.name)); + + if !model_thumbnail.exists() { + // gen image + log::debug!( + "Model thumbnail [{}] does not exist, generating one now", + file.name + ); + let mut model = match model_to_image::ModelToImageBuilder::new(&file.path) + .with_size((600, 600)) + .build() { + Ok(v) => v, + Err(e) => panic!("Error occurred while loading file from path: {}", e), + }; + if let Err(e) = + model.render().unwrap().write_to(Some( + &ad_dir + .join(format!("{}.png", file.name)), + )) + { + log::error!( + "Failed to write model thumbnail for {}: {}", + file.name, + e + ); + } + } + + let image_uri = + model_thumbnail.to_string_lossy().to_string(); + + assets.push(( + format!("file://{}", image_uri), + file.name.clone(), + file.path.clone(), + res_type.clone(), + )) + } + ResourceType::Texture => assets.push(( + format!( + "file://{}", + file.path.to_string_lossy().to_string() + ), + file.name.clone(), + file.path.clone(), + res_type.clone(), + )), + _ => { + if file + .path + .clone() + .extension() + .unwrap() + .to_str() + .unwrap() + .contains("euc") + { + continue; + } + assets.push(( + "NO_TEXTURE".into(), + file.name.clone(), + file.path.clone(), + res_type.clone(), + )) + } + } + } + } + Node::Folder(folder) => { + recursive_search_nodes_and_attach_thumbnail( + &folder.nodes, + assets, + logged, + ) + } + } + } + } + + let mut logged = LOGGED.lock(); + recursive_search_nodes_and_attach_thumbnail( + &res.nodes, + &mut assets, + &mut logged, + ); + } + + egui::ScrollArea::vertical().show(ui, |ui| { + let max_columns = 6; + let available_width = ui.clip_rect().width() - ui.spacing().indent; + let margin = 16.0; + let usable_width = available_width - margin; + let label_height = 20.0; + let padding = 8.0; + let min_card_width = 60.0; + + let mut columns = max_columns; + for test_columns in (1..=max_columns).rev() { + let card_width = usable_width / test_columns as f32; + if card_width >= min_card_width { + columns = test_columns; + break; + } + } + + let card_width = usable_width / columns as f32; + let image_size = (card_width - label_height - padding).max(32.0); + let card_height = image_size + label_height + padding; + + for row_start in (0..assets.len()).step_by(columns) { + let row_end = (row_start + columns).min(assets.len()); + let row_items = &mut assets[row_start..row_end]; + + ui.horizontal(|ui| { + ui.set_max_width(usable_width); + + egui_dnd::dnd(ui, format!("asset_row_{}", row_start / columns)) + .show_vec( + row_items, + |ui, (image, asset_name, asset_path, asset_type), handle, state| { + let card_size = egui::vec2(card_width, card_height); + handle.ui(ui, |ui| { + let (rect, card_response) = ui.allocate_exact_size( + card_size, + egui::Sense::click(), + ); + + let mut card_ui = ui.new_child( + egui::UiBuilder::new().max_rect(rect).layout( + egui::Layout::top_down(egui::Align::Center), + ), + ); + + let image_response = card_ui.add_sized( + [image_size, image_size], + egui::ImageButton::new(image.clone()).frame(false), + ); + + let is_hovered = card_response.hovered() || image_response.hovered() || state.dragged; + let is_d_clicked = card_response.double_clicked() || image_response.double_clicked(); + + if is_d_clicked { + if matches!(asset_type, ResourceType::Model) { + let mut spawn_position = DVec3::default(); + if let Some(active_camera) = self.active_camera { + if let Ok(mut q) = self.world.query_one::<(&Camera, &CameraComponent, Option<&CameraFollowTarget>)>(*active_camera) { + if let Some((camera, _, _)) = q.get() { + spawn_position = camera.eye; + } else { + log_once::warn_once!("Unable to fetch the query result of camera: {:?}", active_camera) + } + } else { + log_once::warn_once!("Unable to query camera, component and option<camerafollowtarget> for active camera: {:?}", active_camera); + } + } else { + log_once::warn_once!("No active camera found"); + } + + let asset = DraggedAsset { + name: asset_name.clone(), + path: asset_path.clone(), + // asset_type: asset_type.clone(), + }; + + match self.spawn_entity_at_pos(&asset, spawn_position, None) { + Ok(()) => { + log::debug!("double click spawned {} at camera pos {:?}", + asset.name, spawn_position + ); + + success!("Spawned {} at camera", asset.name); + } + Err(e) => { + log::error!( + "Failed to spawn {} at camera: {}", + asset.name, + e); + + fatal!("Failed to spawn {}: {}", + asset.name, e); + } + } + } + } + + if is_hovered || state.dragged { + ui.painter().rect_filled( + rect, + 6.0, + if state.dragged { + egui::Color32::from_rgb(80, 80, 100) + } else { + egui::Color32::from_rgb(60, 60, 80) + }, + ); + } + + card_ui.vertical_centered(|ui| { + ui.label( + egui::RichText::new(asset_name.clone()) + .strong() + .color(egui::Color32::WHITE), + ); + }); + }); + }, + ); + }); + ui.add_space(8.0); + } + }); + } + EditorTab::ResourceInspector => { + if let Some(entity) = self.selected_entity { + if let Ok((e, transform, _props, script, camera, camera_component, follow_target)) = self + .world + .query_one_mut::<(&mut AdoptedEntity, Option<&mut Transform>, Option<&ModelProperties>, Option<&mut ScriptComponent>, Option<&mut Camera>, Option<&mut CameraComponent>, Option<&mut CameraFollowTarget>)>(*entity) { + e.inspect(entity, &mut cfg, ui, self.undo_stack, self.signal, &mut String::new()); + if let Some(t) = transform { + t.inspect(entity, &mut cfg, ui, self.undo_stack, self.signal, e.label_mut()); + } + + if let (Some(camera), Some(camera_component)) = (camera, camera_component) { + ui.separator(); + ui.label("Camera Components:"); + camera.inspect(entity, &mut cfg, ui, self.undo_stack, self.signal, &mut String::new()); + camera_component.inspect(entity, &mut cfg, ui, self.undo_stack, self.signal, &mut camera.label.clone()); + + if let Some(target) = follow_target { + target.inspect(entity, &mut cfg, ui, self.undo_stack, self.signal, &mut camera.label.clone()); + } + } + + // if let Some(props) = _props { + // props.inspect(entity, &mut cfg, ui, self.undo_stack, self.signal, e.label_mut()); + // } + + if let Some(script) = script { + script.inspect(entity, &mut cfg, ui, self.undo_stack, self.signal, e.label_mut()); + } + + if let Some(t) = cfg.label_last_edit { + if t.elapsed() >= Duration::from_millis(500) { + if let Some(ent) = cfg.old_label_entity.take() { + if let Some(orig) = cfg.label_original.take() { + UndoableAction::push_to_undo( + &mut self.undo_stack, + UndoableAction::Label(ent, orig, EntityType::Entity), + ); + log::debug!("Pushed label change to undo stack after 500ms debounce period"); + } + } + cfg.label_last_edit = None; + } + } + } else { + log_once::debug_once!("Unable to query entity inside resource inspector"); + } + + if let Ok((light, transform, props)) = self.world.query_one_mut::<(&mut Light, &mut Transform, &mut LightComponent)>(*entity) { + light.inspect(entity, &mut cfg, ui, self.undo_stack, self.signal, &mut String::new()); + transform.inspect(entity, &mut cfg, ui, self.undo_stack, self.signal, &mut light.label); + props.inspect(entity, &mut cfg, ui, self.undo_stack, self.signal, &mut light.label); + if let Some(t) = cfg.label_last_edit { + if t.elapsed() >= Duration::from_millis(500) { + if let Some(ent) = cfg.old_label_entity.take() { + if let Some(orig) = cfg.label_original.take() { + UndoableAction::push_to_undo( + &mut self.undo_stack, + UndoableAction::Label(ent, orig, EntityType::Light), + ); + log::debug!("Pushed label change to undo stack after 500ms debounce period"); + } + } + cfg.label_last_edit = None; + } + } + } else { + log_once::debug_once!("Unable to query light inside resource inspector"); + } + + if let Ok((camera, camera_component, follow_target)) = self.world.query_one_mut::<(&mut Camera, &mut CameraComponent, Option<&mut CameraFollowTarget>)>(*entity) { + camera.inspect(entity, &mut cfg, ui, self.undo_stack, self.signal, &mut String::new()); + camera_component.inspect(entity, &mut cfg, ui, self.undo_stack, self.signal, &mut camera.label.clone()); + if let Some(target) = follow_target { + target.inspect(entity, &mut cfg, ui, self.undo_stack, self.signal, &mut camera.label.clone()); + } + + ui.separator(); + ui.label("Camera Controls:"); + if ui.button("Set as Active Camera").clicked() { + *self.active_camera = Some(*entity); + log::info!("Set camera '{}' as active", camera.label); + } + + if matches!(self.editor_mode, EditorState::Editing) { + if ui.button("Switch to This Camera").clicked() { + *self.active_camera = Some(*entity); + log::info!("Switched to camera '{}'", camera.label); + } + } + } + } else { + ui.label("No entity selected, therefore no info to provide. Go on, what are you waiting for? Click an entity!"); + } + } + } + + let mut menu_action: Option<EditorTabMenuAction> = None; + let area = egui::Area::new("context_menu".into()) + .fixed_pos(cfg.context_menu_pos) + .order(egui::Order::Foreground); + + if cfg.show_context_menu { + let menu_tab = cfg + .context_menu_tab + .clone() + .unwrap_or(EditorTab::ModelEntityList); + + let mut popup_rect = None; + + area.show(ui.ctx(), |ui| { + egui::Frame::popup(ui.style()).show(ui, |ui| { + popup_rect.replace(ui.max_rect()); + + match menu_tab { + EditorTab::AssetViewer => { + ui.set_min_width(150.0); + if ui.selectable_label(false, "Import resource").clicked() { + menu_action = Some(EditorTabMenuAction::ImportResource); + } + if ui.selectable_label(false, "Refresh assets").clicked() { + menu_action = Some(EditorTabMenuAction::RefreshAssets); + } + } + EditorTab::ModelEntityList => { + ui.set_min_width(150.0); + if ui.selectable_label(false, "Add Entity").clicked() { + menu_action = Some(EditorTabMenuAction::AddEntity); + } + if ui.selectable_label(false, "Delete Entity").clicked() { + menu_action = Some(EditorTabMenuAction::DeleteEntity); + } + } + EditorTab::ResourceInspector => { + ui.set_min_width(150.0); + if ui.selectable_label(false, "Add Component").clicked() { + menu_action = Some(EditorTabMenuAction::AddComponent); + } + } + EditorTab::Viewport => { + ui.set_min_width(150.0); + if ui.selectable_label(false, "Viewport Option").clicked() { + menu_action = Some(EditorTabMenuAction::ViewportOption); + } + } + } + }) + }); + + if let Some(action) = menu_action { + if Some(tab.clone()) == cfg.context_menu_tab { + match action { + EditorTabMenuAction::ImportResource => { + log::debug!("Import Resource clicked"); + + match import_object() { + Ok(_) => { + success!("Resource(s) imported successfully!"); + } + Err(e) => { + warn!("Failed to import resource(s): {e}"); + } + } + cfg.show_context_menu = false; + cfg.context_menu_tab = None; + return; + } + EditorTabMenuAction::RefreshAssets => { + log::debug!("Refresh assets clicked"); + { + let mut res = RESOURCES.write(); + match res.update_mem() { + Ok(res_cfg) => { + *res = res_cfg; + success!("Assets refreshed successfully!"); + } + Err(e) => { + fatal!("Failed to refresh assets: {}", e); + } + } + } + cfg.show_context_menu = false; + cfg.context_menu_tab = None; + return; + } + EditorTabMenuAction::AddEntity => { + log::debug!("Add Entity clicked"); + *self.signal = Signal::CreateEntity; + cfg.show_context_menu = false; + cfg.context_menu_tab = None; + return; + } + EditorTabMenuAction::DeleteEntity => { + log::debug!("Delete Entity clicked"); + *self.signal = Signal::Delete; + cfg.show_context_menu = false; + cfg.context_menu_tab = None; + return; + } + EditorTabMenuAction::AddComponent => { + log::debug!("Add Component clicked"); + if let Some(entity) = self.selected_entity { + if let Ok(..) = self.world.query_one_mut::<&AdoptedEntity>(*entity) { + log::debug!("Queried selected entity, it is an entity"); + *self.signal = Signal::AddComponent(*entity, EntityType::Entity); + } + + if let Ok(..) = self.world.query_one_mut::<&Light>(*entity) { + log::debug!("Queried selected entity, it is a light"); + *self.signal = Signal::AddComponent(*entity, EntityType::Light); + } + } else { + warn!("What are you adding a component to? Theres no entity selected..."); + } + + cfg.show_context_menu = false; + cfg.context_menu_tab = None; + return; + } + EditorTabMenuAction::ViewportOption => { + log::debug!("Viewport Option clicked"); + cfg.show_context_menu = false; + cfg.context_menu_tab = None; + return; + }, + EditorTabMenuAction::RemoveComponent => { + log::debug!("Remove Component clicked"); + if let Some(entity) = self.selected_entity { + if let Ok(script) = self.world.query_one_mut::<&ScriptComponent>(*entity) { + log::debug!("Queried selected entity, it has a script component"); + *self.signal = Signal::RemoveComponent(*entity, ComponentType::Script(script.clone())); + } else { + warn!("Selected entity does not have a script component to remove"); + } + } else { + panic!("Paradoxical error: Cannot remove a component when its not selected..."); + } + + cfg.show_context_menu = false; + cfg.context_menu_tab = None; + return; + } + } + } + } + + if let Some(rect) = popup_rect { + if cfg.show_context_menu && Some(tab.clone()) == cfg.context_menu_tab { + if ui + .ctx() + .input(|i| i.pointer.button_clicked(egui::PointerButton::Primary)) + { + if let Some(pos) = ui.ctx().input(|i| i.pointer.interact_pos()) { + if !rect.contains(pos) { + cfg.show_context_menu = false; + cfg.context_menu_tab = None; + } + } + } + } + } + } + } +} + + +pub(crate) fn import_object() -> anyhow::Result<()> { + let model_ext = vec!["glb", "fbx", "obj"]; + let texture_ext = vec!["png"]; + + let files = rfd::FileDialog::new() + .add_filter("All Files", &["*"]) + .add_filter("Model", &model_ext) + .add_filter("Texture", &texture_ext) + .pick_files(); + if let Some(files) = files { + for file in files { + let ext = file.extension().unwrap().to_str().unwrap(); + let mut copied = false; + for mde in model_ext.iter() { + if ext.contains(mde) { + // copy over to models folder + { + let project = PROJECT.read(); + let models_dir = PathBuf::from(project.project_path.clone()) + .join("resources") + .join("models"); + if !models_dir.exists() { + std::fs::create_dir_all(&models_dir)?; + } + let dest = models_dir.join(file.file_name().unwrap()); + std::fs::copy(&file, &dest)?; + log::info!("Copied model file to {:?}", dest); + copied = true; + } + } + } + for tex in texture_ext.iter() { + if ext.contains(tex) { + // copy over to textures folder + { + let project = PROJECT.read(); + let textures_dir = PathBuf::from(project.project_path.clone()) + .join("resources") + .join("textures"); + if !textures_dir.exists() { + std::fs::create_dir_all(&textures_dir)?; + } + let dest = textures_dir.join(file.file_name().unwrap()); + std::fs::copy(&file, &dest)?; + log::info!("Copied texture file to {:?}", dest); + copied = true; + } + } + } + + if !copied { + { + let project = PROJECT.read(); + // everything else copies over to resources root dir + let resources_dir = + PathBuf::from(project.project_path.clone()).join("resources"); + if !resources_dir.exists() { + std::fs::create_dir_all(&resources_dir)?; + } + let dest = resources_dir.join(file.file_name().unwrap()); + std::fs::copy(&file, &dest)?; + log::info!("Copied other resource file to {:?}", dest); + } + } + } + // save it all to ensure the eucc recognises it + let mut proj = PROJECT.write(); + proj.write_to_all()?; + Ok(()) + } else { + return Err(anyhow::anyhow!("File dialogue returned None")); + } +} + +#[derive(Debug, Clone, Copy)] +pub enum EditorTabMenuAction { + ImportResource, + RefreshAssets, + AddEntity, + DeleteEntity, + AddComponent, + RemoveComponent, + ViewportOption, +} @@ -0,0 +1,287 @@ +use super::*; +use dropbear_engine::{ + entity::{AdoptedEntity, Transform}, + input::{Controller, Keyboard, Mouse}, +}; +use gilrs::{Button, GamepadId}; +use log; +use transform_gizmo_egui::GizmoMode; +use winit::{ + dpi::PhysicalPosition, event::MouseButton, event_loop::ActiveEventLoop, keyboard::KeyCode, +}; +use eucalyptus_core::success_without_console; + +impl Keyboard for Editor { + fn key_down(&mut self, key: KeyCode, _event_loop: &ActiveEventLoop) { + #[cfg(not(target_os = "macos"))] + let ctrl_pressed = self.input_state.pressed_keys.contains(&KeyCode::ControlLeft) + || self.input_state.pressed_keys.contains(&KeyCode::ControlRight); + #[cfg(target_os = "macos")] + let ctrl_pressed = self.input_state.pressed_keys.contains(&KeyCode::SuperLeft) + || self.input_state.pressed_keys.contains(&KeyCode::SuperRight); + + let _alt_pressed = self.input_state.pressed_keys.contains(&KeyCode::AltLeft) + || self.input_state.pressed_keys.contains(&KeyCode::AltRight); + + let shift_pressed = self.input_state.pressed_keys.contains(&KeyCode::ShiftLeft) + || self.input_state.pressed_keys.contains(&KeyCode::ShiftRight); + + let is_double_press = self.double_key_pressed(key); + + let is_playing = matches!(self.editor_state, EditorState::Playing); + + match key { + KeyCode::KeyG => { + if self.is_viewport_focused && !is_playing { + self.viewport_mode = ViewportMode::Gizmo; + info!("Switched to Viewport::Gizmo"); + + if let Some(window) = &self.window { + window.set_cursor_visible(true); + } + } else { + self.input_state.pressed_keys.insert(key); + } + } + KeyCode::KeyF => { + if self.is_viewport_focused && !is_playing { + self.viewport_mode = ViewportMode::CameraMove; + info!("Switched to Viewport::CameraMove"); + if let Some(window) = &self.window { + window.set_cursor_visible(false); + + let size = window.inner_size(); + let center = winit::dpi::PhysicalPosition::new( + size.width as f64 / 2.0, + size.height as f64 / 2.0, + ); + let _ = window.set_cursor_position(center); + } + } else { + self.input_state.pressed_keys.insert(key); + } + } + KeyCode::Delete => { + if !is_playing { + if let Some(_) = &self.selected_entity { + self.signal = Signal::Delete; + } else { + warn!("Failed to delete: No entity selected"); + } + } else { + self.input_state.pressed_keys.insert(key); + } + } + KeyCode::Escape => { + if is_double_press { + if let Some(_) = &self.selected_entity { + self.selected_entity = None; + log::debug!("Deselected entity"); + } + } else if self.is_viewport_focused && !is_playing { + self.viewport_mode = ViewportMode::None; + info!("Switched to Viewport::None"); + if let Some(window) = &self.window { + window.set_cursor_visible(true); + } + } else { + self.input_state.pressed_keys.insert(key); + } + } + KeyCode::KeyQ => { + if ctrl_pressed && !is_playing { + match self.save_project_config() { + Ok(_) => {} + Err(e) => { + fatal!("Error saving project: {}", e); + } + } + log::info!("Successfully saved project, about to quit..."); + success_without_console!("Successfully saved project"); + self.scene_command = SceneCommand::Quit; + } else if is_playing { + warn!("Unable to save-quit project, please pause your playing state, then try again"); + } + } + KeyCode::KeyC => { + if ctrl_pressed && !is_playing { + if let Some(entity) = &self.selected_entity { + let query = self + .world + .query_one::<(&AdoptedEntity, &Transform, &ModelProperties)>(*entity); + if let Ok(mut q) = query { + if let Some((e, t, props)) = q.get() { + let s_entity = SceneEntity { + model_path: e.model().path.clone(), + label: e.model().label.clone(), + transform: *t, + properties: props.clone(), + script: None, + entity_id: None, + }; + self.signal = Signal::Copy(s_entity); + + info!("Copied!"); + + log::debug!("Copied selected entity"); + } else { + warn!( + "Unable to copy entity: Unable to fetch world entity properties" + ); + } + } else { + warn!("Unable to copy entity: Unable to obtain lock"); + } + } else { + warn!("Unable to copy entity: None selected"); + } + } else if matches!(self.viewport_mode, ViewportMode::Gizmo) { + info!("GizmoMode set to scale"); + self.gizmo_mode = GizmoMode::all_scale(); + } else { + self.input_state.pressed_keys.insert(key); + } + } + KeyCode::KeyV => { + if ctrl_pressed && !is_playing { + match &self.signal { + Signal::Copy(entity) => { + self.signal = Signal::Paste(entity.clone()); + } + _ => { + warn!("Unable to paste: You haven't selected anything!"); + } + } + } + else { + self.input_state.pressed_keys.insert(key); + } + } + KeyCode::KeyS => { + if ctrl_pressed { + if !is_playing { + match self.save_project_config() { + Ok(_) => { + success!("Successfully saved project"); + } + Err(e) => { + fatal!("Error saving project: {}", e); + } + } + } else { + warn!("Unable to save project config, please quit your playing and try again"); + } + + } else { + self.input_state.pressed_keys.insert(key); + } + } + KeyCode::KeyZ => { + if ctrl_pressed && !is_playing { + if shift_pressed { + // redo + } else { + // undo + log::debug!("Undo signal sent"); + self.signal = Signal::Undo; + } + } else if matches!(self.viewport_mode, ViewportMode::Gizmo) && !is_playing { + info!("GizmoMode set to translate"); + self.gizmo_mode = GizmoMode::all_translate(); + } else { + self.input_state.pressed_keys.insert(key); + } + } + KeyCode::F1 => { + if !is_playing { + if self.is_using_debug_camera() { + self.switch_to_player_camera(); + } else { + self.switch_to_debug_camera(); + } + } + } + KeyCode::KeyX => { + if matches!(self.viewport_mode, ViewportMode::Gizmo) && !is_playing { + info!("GizmoMode set to rotate"); + self.gizmo_mode = GizmoMode::all_rotate(); + } else { + self.input_state.pressed_keys.insert(key); + } + } + KeyCode::KeyP => { + if !is_playing { + if ctrl_pressed { + self.signal = Signal::Play + } + } + } + _ => { + self.input_state.pressed_keys.insert(key); + } + } + } + + fn key_up(&mut self, key: KeyCode, _event_loop: &ActiveEventLoop) { + self.input_state.pressed_keys.remove(&key); + } +} + +impl Mouse for Editor { + fn mouse_move(&mut self, position: PhysicalPosition<f64>) { + if (self.is_viewport_focused + && matches!(self.viewport_mode, ViewportMode::CameraMove)) + || (matches!(self.editor_state, EditorState::Playing) && !self.input_state.is_cursor_locked) + { + if let Some(window) = &self.window { + let size = window.inner_size(); + let center = + PhysicalPosition::new(size.width as f64 / 2.0, size.height as f64 / 2.0); + + let dx = position.x - center.x; + let dy = position.y - center.y; + if let Some(active_camera) = self.active_camera { + if let Ok(mut q) = self.world.query_one::<(&mut Camera, &CameraComponent, Option<&CameraFollowTarget>)>(active_camera) { + if let Some((camera, _, _)) = q.get() { + camera.track_mouse_delta(dx, dy); + } else { + log_once::warn_once!("Unable to fetch the query result of camera: {:?}", active_camera) + } + } else { + log_once::warn_once!("Unable to query camera, component and option<camerafollowtarget> for active camera: {:?}", active_camera); + } + } else { + log_once::warn_once!("No active camera found"); + } + + let _ = window.set_cursor_position(center); + window.set_cursor_visible(false); + } + } + self.input_state.mouse_pos = (position.x, position.y); + } + + fn mouse_down(&mut self, button: MouseButton) { + match button { + _ => { self.input_state.mouse_button.insert(button); } + } + } + + fn mouse_up(&mut self, button: MouseButton) { + self.input_state.mouse_button.remove(&button); + } +} + +impl Controller for Editor { + fn button_down(&mut self, _button: Button, _id: GamepadId) {} + + fn button_up(&mut self, _button: Button, _id: GamepadId) {} + + fn left_stick_changed(&mut self, _x: f32, _y: f32, _id: GamepadId) {} + + fn right_stick_changed(&mut self, _x: f32, _y: f32, _id: GamepadId) {} + + fn on_connect(&mut self, _id: GamepadId) {} + + fn on_disconnect(&mut self, _id: GamepadId) {} +} @@ -0,0 +1,892 @@ +pub mod dock; +pub mod input; +pub mod scene; +pub mod component; + +pub(crate) use crate::editor::dock::*; + +use std::{ + collections::{HashMap, HashSet}, + path::PathBuf, + sync::{Arc, LazyLock}, + time::{Duration, Instant}, +}; + +use dropbear_engine::{ + camera::Camera, entity::{AdoptedEntity, Transform}, graphics::Graphics, lighting::{Light, LightManager}, scene::SceneCommand +}; +use egui::{self, Context}; +use egui_dock_fork::{DockArea, DockState, NodeIndex, Style}; +use hecs::{World}; +use log; +use parking_lot::Mutex; +use transform_gizmo_egui::{EnumSet, Gizmo, GizmoMode}; +use wgpu::{Color, Extent3d, RenderPipeline}; +use winit::{keyboard::KeyCode, window::Window}; +use eucalyptus_core::camera::{CameraAction, CameraComponent, CameraFollowTarget, CameraType, DebugCamera}; +use eucalyptus_core::{fatal, info, states, success, warn}; +use eucalyptus_core::scripting::input::InputState; +use eucalyptus_core::scripting::{ScriptAction, ScriptManager}; +use eucalyptus_core::states::{CameraConfig, EditorTab, EntityNode, LightConfig, ModelProperties, SceneEntity, ScriptComponent, PROJECT, SCENES}; +use eucalyptus_core::utils::ViewportMode; +use crate::build::build; +use crate::camera::UndoableCameraAction; +use crate::debug; + +pub struct Editor { + scene_command: SceneCommand, + world: World, + dock_state: DockState<EditorTab>, + texture_id: Option<egui::TextureId>, + size: Extent3d, + render_pipeline: Option<RenderPipeline>, + light_manager: LightManager, + color: Color, + + active_camera: Option<hecs::Entity>, + + is_viewport_focused: bool, + // is_cursor_locked: bool, + window: Option<Arc<Window>>, + + show_new_project: bool, + project_name: String, + project_path: Option<PathBuf>, + pending_scene_switch: bool, + + gizmo: Gizmo, + selected_entity: Option<hecs::Entity>, + viewport_mode: ViewportMode, + + signal: Signal, + undo_stack: Vec<UndoableAction>, + // todo: add redo (later) + // redo_stack: Vec<UndoableAction>, + + editor_state: EditorState, + gizmo_mode: EnumSet<GizmoMode>, + + script_manager: ScriptManager, + play_mode_backup: Option<PlayModeBackup>, + + input_state: InputState, +} + +impl Editor { + pub fn new() -> Self { + let tabs = vec![EditorTab::Viewport]; + let mut dock_state = DockState::new(tabs); + + let surface = dock_state.main_surface_mut(); + let [_old, right] = + surface.split_right(NodeIndex::root(), 0.25, vec![EditorTab::ModelEntityList]); + let [_old, _] = + surface.split_left(NodeIndex::root(), 0.20, vec![EditorTab::ResourceInspector]); + let [_old, _] = surface.split_below(right, 0.5, vec![EditorTab::AssetViewer]); + + // this shit doesnt work :( + // nvm it works (sorta) + std::thread::spawn(move || { + loop { + std::thread::sleep(Duration::from_secs(1)); + let deadlocks = parking_lot::deadlock::check_deadlock(); + if deadlocks.is_empty() { + continue; + } + + log::error!("{} deadlocks detected", deadlocks.len()); + for (i, threads) in deadlocks.iter().enumerate() { + log::error!("Deadlock #{}", i); + for t in threads { + log::error!("Thread Id {:#?}", t.thread_id()); + log::error!("{:#?}", t.backtrace()); + } + } + } + }); + + Self { + scene_command: SceneCommand::None, + dock_state, + texture_id: None, + size: Extent3d::default(), + render_pipeline: None, + color: Color::default(), + is_viewport_focused: false, + // is_cursor_locked: false, + window: None, + world: World::new(), + show_new_project: false, + project_name: String::new(), + project_path: None, + pending_scene_switch: false, + gizmo: Gizmo::default(), + selected_entity: None, + viewport_mode: ViewportMode::None, + signal: Signal::None, + undo_stack: Vec::new(), + script_manager: ScriptManager::new().unwrap(), + editor_state: EditorState::Editing, + gizmo_mode: EnumSet::empty(), + play_mode_backup: None, + input_state: InputState::new(), + light_manager: LightManager::new(), + active_camera: None, + // ..Default::default() + // note to self: DO NOT USE ..DEFAULT::DEFAULT(), IT WILL CAUSE OVERFLOW + } + } + + fn double_key_pressed(&mut self, key: KeyCode) -> bool { + let now = Instant::now(); + + if let Some(last_time) = self.input_state.last_key_press_times.get(&key) { + let time_diff = now.duration_since(*last_time); + + if time_diff <= self.input_state.double_press_threshold { + self.input_state.last_key_press_times.remove(&key); + return true; + } + } + + self.input_state.last_key_press_times.insert(key, now); + false + } + + /// Save the current world state to the active scene + pub fn save_current_scene(&mut self) -> anyhow::Result<()> { + let mut scenes = SCENES.write(); + + let scene_index = if scenes.is_empty() { + return Err(anyhow::anyhow!("No scenes loaded to save")); + } else { + 0 + }; + + let scene = &mut scenes[scene_index]; + scene.entities.clear(); + scene.lights.clear(); + scene.cameras.clear(); + + for (id, (adopted, transform, properties, script)) in self + .world + .query::<( + &AdoptedEntity, + Option<&Transform>, + &ModelProperties, + Option<&ScriptComponent>, + )>() + .iter() + { + let transform = transform.unwrap_or(&Transform::default()).clone(); + + let scene_entity = SceneEntity { + model_path: adopted.model().path.clone(), + label: adopted.model().label.clone(), + transform, + properties: properties.clone(), + script: script.cloned(), + entity_id: Some(id), + }; + + scene.entities.push(scene_entity); + log::debug!("Pushed entity: {}", adopted.label()); + } + + for (id, (light_component, transform, light)) in self + .world + .query::<( + &dropbear_engine::lighting::LightComponent, + &Transform, + &Light, + )>() + .iter() + { + let light_config = LightConfig { + label: light.label().to_string(), + transform: *transform, + light_component: light_component.clone(), + enabled: light_component.enabled, + entity_id: Some(id), + }; + + scene.lights.push(light_config); + log::debug!("Pushed light into lights: {}", light.label()); + } + + for (_id, (camera, component, follow_target)) in self + .world + .query::<(&Camera, &CameraComponent, Option<&CameraFollowTarget>)>() + .iter() + { + let camera_config = CameraConfig::from_ecs_camera(camera, component, follow_target); + scene.cameras.push(camera_config); + log::debug!("Pushed camera into cameras: {}", camera.label); + } + + + log::info!( + "Saved {} entities and camera configs to scene '{}'", + scene.entities.len(), + scene.scene_name + ); + + Ok(()) + } + + pub fn save_project_config(&mut self) -> anyhow::Result<()> { + self.save_current_scene()?; + + { + let mut config = PROJECT.write(); + config.dock_layout = Some(self.dock_state.clone()); + } + + { + let (scene_clone, project_path) = { + let scenes = SCENES.read(); + let project = PROJECT.read(); + (scenes[0].clone(), project.project_path.clone()) + }; + + scene_clone.write_to(&project_path)?; + + let mut config = PROJECT.write(); + config.write_to_all()?; + } + + Ok(()) + } + + pub fn load_project_config(&mut self, graphics: &Graphics) -> anyhow::Result<()> { + let config = PROJECT.read(); + + self.project_path = Some(config.project_path.clone()); + + if let Some(layout) = &config.dock_layout { + self.dock_state = layout.clone(); + } + + { + let scenes = SCENES.read(); + if let Some(first_scene) = scenes.first() { + self.active_camera = Some(first_scene.load_into_world(&mut self.world, graphics)?); + + log::info!( + "Successfully loaded scene with {} entities and {} camera configs", + first_scene.entities.len(), + first_scene.cameras.len(), + ); + } else { + let existing_debug_camera = self.world + .query::<(&Camera, &CameraComponent)>() + .iter() + .find_map(|(entity, (_, component))| { + if matches!(component.camera_type, CameraType::Debug) { + Some(entity) + } else { + None + } + }); + + if let Some(camera_entity) = existing_debug_camera { + log::info!("Using existing debug camera"); + self.active_camera = Some(camera_entity); + } else { + log::info!("No scenes found, creating default debug camera"); + + let debug_camera = Camera::predetermined(graphics, Some("Debug Camera")); + let component = DebugCamera::new(); + + let e = self.world.spawn((debug_camera, component)); + self.active_camera = Some(e); + } + } + } + + Ok(()) + } + + pub fn show_ui(&mut self, ctx: &Context) { + egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| { + egui::MenuBar::new().ui(ui, |ui| { + ui.menu_button("File", |ui| { + if ui + .button("Main Menu (New + Open + Editor Settings)") + .clicked() + { + self.scene_command = SceneCommand::SwitchScene("main_menu".into()); + } + + if ui.button("Save").clicked() { + match self.save_project_config() { + Ok(_) => {} + Err(e) => { + fatal!("Error saving project: {}", e); + } + } + success!("Successfully saved project"); + } + if ui.button("Project Settings").clicked() {}; + if matches!(self.editor_state, EditorState::Playing) { + if ui.button("Stop").clicked() { + self.signal = Signal::StopPlaying; + } + } else { + if ui.button("Play").clicked() { + self.signal = Signal::Play; + } + } + ui.menu_button("Export", |ui| { + // todo: create a window for better build menu + if ui.button("Build").clicked() { + { + let proj = PROJECT.read(); + match build(proj.project_path.join(format!("{}.eucp", proj.project_name.clone())).clone()) { + Ok(thingy) => success!("Project output at {}", thingy.display()), + Err(e) => { + fatal!("Unable to build project [{}]: {}", proj.project_path.clone().display(), e); + }, + } + } + } + ui.label("Package"); // todo: create a window for label + }); + ui.separator(); + if ui.button("Quit").clicked() { + match self.save_project_config() { + Ok(_) => {} + Err(e) => { + fatal!("Error saving project: {}", e); + } + } + success!("Successfully saved project"); + } + }); + ui.menu_button("Edit", |ui| { + if ui.button("Copy").clicked() { + if let Some(entity) = &self.selected_entity { + let query = self.world.query_one::<(&AdoptedEntity, &Transform, &ModelProperties)>(*entity); + if let Ok(mut q) = query { + if let Some((e, t, props)) = q.get() { + let s_entity = states::SceneEntity { + model_path: e.model().path.clone(), + label: e.model().label.clone(), + transform: *t, + properties: props.clone(), + script: None, + entity_id: None, + }; + self.signal = Signal::Copy(s_entity); + + info!("Copied selected entity!"); + } else { + warn!("Unable to copy entity: Unable to fetch world entity properties"); + } + } else { + warn!("Unable to copy entity: Unable to obtain lock"); + } + } else { + warn!("Unable to copy entity: None selected"); + } + } + + if ui.button("Paste").clicked() { + match &self.signal { + Signal::Copy(entity) => { + self.signal = Signal::Paste(entity.clone()); + } + _ => { + warn!("Unable to paste: You haven't selected anything!"); + } + } + } + + if ui.button("Undo").clicked() { + self.signal = Signal::Undo; + } + ui.label("Redo"); + }); + + ui.menu_button("Window", |ui_window| { + if ui_window.button("Open Asset Viewer").clicked() { + self.dock_state.push_to_focused_leaf(EditorTab::AssetViewer); + } + if ui_window.button("Open Resource Inspector").clicked() { + self.dock_state + .push_to_focused_leaf(EditorTab::ResourceInspector); + } + if ui_window.button("Open Entity List").clicked() { + self.dock_state + .push_to_focused_leaf(EditorTab::ModelEntityList); + } + if ui_window.button("Open Viewport").clicked() { + self.dock_state.push_to_focused_leaf(EditorTab::Viewport); + } + }); + { + let cfg = PROJECT.read(); + if cfg.editor_settings.is_debug_menu_shown { + debug::show_menu_bar(ui, &mut self.signal); + } + } + }); + }); + + egui::CentralPanel::default().show(&ctx, |ui| { + DockArea::new(&mut self.dock_state) + .style(Style::from_egui(ui.style().as_ref())) + .show_inside( + ui, + &mut EditorTabViewer { + view: self.texture_id.unwrap(), + nodes: EntityNode::from_world(&self.world), + gizmo: &mut self.gizmo, + tex_size: self.size, + world: &mut self.world, + selected_entity: &mut self.selected_entity, + viewport_mode: &mut self.viewport_mode, + undo_stack: &mut self.undo_stack, + signal: &mut self.signal, + active_camera: &mut self.active_camera, + gizmo_mode: &mut self.gizmo_mode, + editor_mode: &mut self.editor_state, + }, + ); + }); + + crate::utils::show_new_project_window( + ctx, + &mut self.show_new_project, + &mut self.project_name, + &mut self.project_path, + |name, path| { + crate::utils::start_project_creation(name.to_string(), Some(path.clone())); + self.pending_scene_switch = true; + }, + ); + + if self.pending_scene_switch { + self.scene_command = SceneCommand::SwitchScene("editor".to_string()); + self.pending_scene_switch = false; + } + } + + pub fn switch_to_debug_camera(&mut self) { + let debug_camera = self.world.query::<(&Camera, &CameraComponent)>() + .iter() + .find_map(|(e, (_cam, comp))| { + if matches!(comp.camera_type, CameraType::Debug) { + Some(e) + } else { + None + } + }); + + if let Some(camera_entity) = debug_camera { + self.active_camera = Some(camera_entity); + info!("Switched to debug camera"); + } else { + warn!("No debug camera found in the world"); + } + } + + pub fn switch_to_player_camera(&mut self) { + let player_camera = self.world.query::<(&Camera, &CameraComponent)>() + .iter() + .find_map(|(e, (_cam, comp))| { + if matches!(comp.camera_type, CameraType::Player) { + Some(e) + } else { + None + } + }); + + if let Some(camera_entity) = player_camera { + self.active_camera = Some(camera_entity); + info!("Switched to player camera"); + } else { + warn!("No player camera found in the world"); + } + } + + pub fn is_using_debug_camera(&self) -> bool { + if let Some(active_camera_entity) = self.active_camera { + if let Ok(mut query) = self.world.query_one::<&CameraComponent>(active_camera_entity) { + if let Some(component) = query.get() { + return matches!(component.camera_type, CameraType::Debug); + } + } + } + false + } +} + +pub static LOGGED: LazyLock<Mutex<HashSet<String>>> = LazyLock::new(|| Mutex::new(HashSet::new())); + +fn show_entity_tree( + ui: &mut egui::Ui, + nodes: &mut Vec<EntityNode>, + selected: &mut Option<hecs::Entity>, + id_source: &str, +) { + egui_dnd::Dnd::new(ui, id_source).show_vec(nodes, |ui, item, handle, _dragging| match item + .clone() + { + EntityNode::Entity { id, name } => { + ui.horizontal(|ui| { + handle.ui(ui, |ui| { + ui.label("⏹️"); + }); + let resp = ui.selectable_label(selected.as_ref().eq(&Some(&id)), name); + if resp.clicked() { + *selected = Some(id); + } + }); + } + EntityNode::Script { name, path: _ } => { + ui.horizontal(|ui| { + handle.ui(ui, |ui| { + ui.label("📜"); + }); + ui.label(format!("{name}")); + }); + } + EntityNode::Group { + ref name, + ref mut children, + ref mut collapsed, + } => { + ui.horizontal(|ui| { + handle.ui(ui, |ui| { + let header = egui::CollapsingHeader::new(name) + .default_open(!*collapsed) + .show(ui, |ui| { + show_entity_tree(ui, children, selected, name); + }); + *collapsed = !header.body_returned.is_some(); + }); + }); + } + EntityNode::Light { + id, + name, + } => { + ui.horizontal(|ui| { + handle.ui(ui, |ui| { + ui.label("💡"); + }); + let resp = ui.selectable_label(selected.as_ref().eq(&Some(&id)), name); + if resp.clicked() { + *selected = Some(id); + } + }); + } + EntityNode::Camera { id, name, camera_type } => { + ui.horizontal(|ui| { + handle.ui(ui, |ui| { + let icon = match camera_type { + CameraType::Debug => "🎥", // Debug camera + CameraType::Player => "📹", // Player camera + CameraType::Normal => "📷", // Normal camera + }; + ui.label(icon); + }); + let display_name = format!("{} ({})", name, match camera_type { + CameraType::Debug => "Debug", + CameraType::Player => "Player", + CameraType::Normal => "Normal", + }); + let resp = ui.selectable_label(selected.as_ref().eq(&Some(&id)), display_name); + if resp.clicked() { + *selected = Some(id); + } + }); + } + }); +} + +/// Describes an action that is undoable +#[derive(Debug)] +pub enum UndoableAction { + Transform(hecs::Entity, Transform), + Spawn(hecs::Entity), + Label(hecs::Entity, String, EntityType), + RemoveComponent(hecs::Entity, ComponentType), + #[allow(dead_code)] + CameraAction(UndoableCameraAction), +} +#[derive(Debug)] +pub enum EntityType { + Entity, + Light, + Camera, +} + +impl UndoableAction { + pub fn push_to_undo(undo_stack: &mut Vec<UndoableAction>, action: Self) { + undo_stack.push(action); + // log::debug!("Undo Stack contents: {:#?}", undo_stack); + } + + pub fn undo(&self, world: &mut hecs::World) -> anyhow::Result<()> { + match self { + UndoableAction::Transform(entity, transform) => { + if let Ok(mut q) = world.query_one::<&mut Transform>(*entity) { + if let Some(e_t) = q.get() { + *e_t = *transform; + log::debug!("Reverted transform"); + Ok(()) + } else { + Err(anyhow::anyhow!("Unable to query the entity")) + } + } else { + Err(anyhow::anyhow!("Could not find an entity to query")) + } + } + UndoableAction::Spawn(entity) => { + if world.despawn(*entity).is_ok() { + log::debug!("Undid spawn by despawning entity {:?}", entity); + Ok(()) + } else { + Err(anyhow::anyhow!("Failed to despawn entity {:?}", entity)) + } + } + UndoableAction::Label(entity, original_label, entity_type) => { + match entity_type { + EntityType::Entity => { + if let Ok(mut q) = world.query_one::<&mut AdoptedEntity>(*entity) { + if let Some(adopted) = q.get() { + adopted.model_mut().label = original_label.clone(); + log::debug!("Reverted label for entity {:?} to '{}'", entity, original_label); + Ok(()) + } else { + Err(anyhow::anyhow!("Unable to query the entity for label revert")) + } + } else { + Err(anyhow::anyhow!("Could not find an entity to query for label revert")) + } + }, + EntityType::Light => { + if let Ok(mut q) = world.query_one::<&mut Light>(*entity) { + if let Some(adopted) = q.get() { + adopted.label = original_label.clone(); + log::debug!("Reverted label for light {:?} to '{}'", entity, original_label); + Ok(()) + } else { + Err(anyhow::anyhow!("Unable to query the light for label revert")) + } + } else { + Err(anyhow::anyhow!("Could not find a light to query for label revert")) + } + }, + EntityType::Camera => { + if let Ok(mut q) = world.query_one::<&mut Camera>(*entity) { + if let Some(adopted) = q.get() { + adopted.label = original_label.clone(); + log::debug!("Reverted label for camera {:?} to '{}'", entity, original_label); + Ok(()) + } else { + Err(anyhow::anyhow!("Unable to query the camera for label revert")) + } + } else { + Err(anyhow::anyhow!("Could not find a camera to query for label revert")) + } + } + } + }, + UndoableAction::RemoveComponent(entity, c_type) => { + match c_type { + ComponentType::Script(component) => { + world.insert_one(*entity, component.clone())?; + }, + ComponentType::Camera(camera, component, follow) => { + if let Some(f) = follow { + world.insert(*entity, (camera.clone(), component.clone(), f.clone()))?; + } else { + world.insert(*entity, (camera.clone(), component.clone()))?; + } + } + } + Ok(()) + }, + UndoableAction::CameraAction(action) => { + match action { + UndoableCameraAction::Speed(entity, speed) => { + if let Ok((cam, comp)) = world.query_one_mut::<(&mut Camera, &mut CameraComponent)>(*entity) { + comp.speed = *speed; + comp.update(cam); + } + }, + UndoableCameraAction::Sensitivity(entity, sensitivity) => { + if let Ok((cam, comp)) = world.query_one_mut::<(&mut Camera, &mut CameraComponent)>(*entity) { + comp.sensitivity = *sensitivity; + comp.update(cam); + } + }, + UndoableCameraAction::FOV(entity, fov) => { + if let Ok((cam, comp)) = world.query_one_mut::<(&mut Camera, &mut CameraComponent)>(*entity) { + comp.fov_y = *fov; + comp.update(cam); + } + }, + UndoableCameraAction::Type(entity, camera_type) => { + if let Ok((cam, comp)) = world.query_one_mut::<(&mut Camera, &mut CameraComponent)>(*entity) { + comp.camera_type = *camera_type; + comp.update(cam); + } + }, + }; + Ok(()) + } + } + } +} + +/// This enum will be used to describe the type of command/signal. This is only between +/// the editor and unlike SceneCommand, this will ping a signal everywhere in that scene +pub enum Signal { + None, + Copy(SceneEntity), + Paste(SceneEntity), + Delete, + Undo, + ScriptAction(ScriptAction), + CameraAction(CameraAction), + Play, + StopPlaying, + AddComponent(hecs::Entity, EntityType), + RemoveComponent(hecs::Entity, ComponentType), + CreateEntity, + LogEntities, +} + +impl Default for Editor { + fn default() -> Self { + Editor::new() + } +} + +#[derive(Debug)] +pub enum ComponentType { + Script(ScriptComponent), + Camera(Camera, CameraComponent, Option<CameraFollowTarget>), +} + +#[derive(Clone)] +pub struct PlayModeBackup { + entities: Vec<(hecs::Entity, Transform, ModelProperties, Option<ScriptComponent>)>, + camera_data: Vec<(hecs::Entity, Camera, CameraComponent, Option<CameraFollowTarget>)>, +} + +impl PlayModeBackup { + pub fn create_backup(editor: &mut Editor) -> anyhow::Result<()> { + let mut entities = Vec::new(); + + for (entity_id, (_, transform, properties)) in editor + .world + .query::<(&AdoptedEntity, &Transform, &ModelProperties)>() + .iter() + { + let script = editor.world.query_one::<&ScriptComponent>(entity_id).ok().and_then(|mut s| { + s.get().map(|script| script.clone()) + }); + entities.push((entity_id, *transform, properties.clone(), script)); + } + + let mut camera_data = Vec::new(); + + for (entity_id, (camera, component, follow_target)) in editor + .world + .query::<(&Camera, &CameraComponent, Option<&CameraFollowTarget>)>() + .iter() + { + camera_data.push((entity_id, camera.clone(), component.clone(), follow_target.cloned())); + } + + editor.play_mode_backup = Some(PlayModeBackup { + entities, + camera_data, + }); + + log::info!("Created play mode backup with {} entities and {} cameras", + editor.play_mode_backup.as_ref().unwrap().entities.len(), + editor.play_mode_backup.as_ref().unwrap().camera_data.len() + ); + Ok(()) + } + + pub fn restore(editor: &mut Editor) -> anyhow::Result<()> { + if let Some(backup) = &editor.play_mode_backup { + // Restore entity states + for (entity_id, original_transform, original_properties, original_script) in &backup.entities { + if let Ok(mut transform) = editor.world.get::<&mut Transform>(*entity_id) { + *transform = *original_transform; + } + + if let Ok(mut properties) = editor.world.get::<&mut ModelProperties>(*entity_id) { + *properties = original_properties.clone(); + } + + let has_script = editor.world.get::<&ScriptComponent>(*entity_id).is_ok(); + match (has_script, original_script) { + (true, Some(original)) => { + if let Ok(mut script) = editor.world.get::<&mut ScriptComponent>(*entity_id) { + *script = original.clone(); + } + } + (true, None) => { + let _ = editor.world.remove_one::<ScriptComponent>(*entity_id); + } + (false, Some(original)) => { + let _ = editor.world.insert_one(*entity_id, original.clone()); + } + (false, None) => { + // No change needed + } + } + } + + // Restore camera states + for (entity_id, original_camera, original_component, original_follow_target) in &backup.camera_data { + if let Ok(mut camera) = editor.world.get::<&mut Camera>(*entity_id) { + *camera = original_camera.clone(); + } + + if let Ok(mut component) = editor.world.get::<&mut CameraComponent>(*entity_id) { + *component = original_component.clone(); + } + + let has_follow_target = editor.world.get::<&CameraFollowTarget>(*entity_id).is_ok(); + match (has_follow_target, original_follow_target) { + (true, Some(original)) => { + if let Ok(mut follow_target) = editor.world.get::<&mut CameraFollowTarget>(*entity_id) { + *follow_target = original.clone(); + } + } + (true, None) => { + let _ = editor.world.remove_one::<CameraFollowTarget>(*entity_id); + } + (false, Some(original)) => { + let _ = editor.world.insert_one(*entity_id, original.clone()); + } + (false, None) => { + // No change needed + } + } + } + + log::info!("Restored scene from play mode backup"); + + editor.play_mode_backup = None; + Ok(()) + } else { + Err(anyhow::anyhow!("No play mode backup found to restore")) + } + } +} + +pub enum EditorState { + Editing, + Playing, +} @@ -0,0 +1,929 @@ +use egui::{Align2, Image}; +use dropbear_engine::{ + entity::{AdoptedEntity, Transform}, graphics::{Graphics, Shader}, lighting::{Light, LightComponent}, model::{DrawLight, DrawModel}, scene::{Scene, SceneCommand} +}; +use log; +use parking_lot::Mutex; +use wgpu::Color; +use wgpu::util::DeviceExt; +use winit::{event_loop::ActiveEventLoop, keyboard::KeyCode}; +use dropbear_engine::graphics::InstanceRaw; +use dropbear_engine::model::Model; +use dropbear_engine::starter::plane::PlaneBuilder; +use eucalyptus_core::camera::PlayerCamera; +use eucalyptus_core::{logging, scripting, success_without_console, warn_without_console}; +use eucalyptus_core::states::PropertyValue; +use eucalyptus_core::utils::{PendingSpawn, PROTO_TEXTURE}; +use super::*; + +pub static PENDING_SPAWNS: LazyLock<Mutex<Vec<PendingSpawn>>> = + LazyLock::new(|| Mutex::new(Vec::new())); + +impl Scene for Editor { + fn load(&mut self, graphics: &mut Graphics) { + if self.active_camera.is_none() { + self.load_project_config(graphics).unwrap(); + } + + let shader = Shader::new( + graphics, + include_str!("../../../resources/shaders/shader.wgsl"), + Some("viewport_shader"), + ); + + self.light_manager.create_light_array_resources(graphics); + + let texture_bind_group = &graphics.texture_bind_group().clone(); + if let Some(active_camera) = self.active_camera { + if let Ok(mut q) = self.world.query_one::<(&Camera, &CameraComponent, Option<&CameraFollowTarget>)>(active_camera) { + if let Some((camera, _component, _follow_target)) = q.get() { + let pipeline = graphics.create_render_pipline( + &shader, + vec![ + texture_bind_group, + camera.layout(), + self.light_manager.layout() + ], + None, + ); + self.render_pipeline = Some(pipeline); + + self.light_manager.create_render_pipeline( + graphics, + include_str!("../../../resources/shaders/light.wgsl"), + camera, + Some("Light Pipeline") + ); + } else { + log_once::warn_once!("Unable to fetch the query result of camera: {:?}", active_camera) + } + } else { + log_once::warn_once!("Unable to query camera, component and option<camerafollowtarget> for active camera: {:?}", active_camera); + } + } else { + log_once::warn_once!("No active camera found"); + } + + self.window = Some(graphics.state.window.clone()); + } + + fn update(&mut self, dt: f32, graphics: &mut Graphics) { + if let Some((_, tab)) = self.dock_state.find_active_focused() { + self.is_viewport_focused = matches!(tab, EditorTab::Viewport); + } else { + self.is_viewport_focused = false; + } + + { + let mut pending_spawns = PENDING_SPAWNS.lock(); + for spawn in pending_spawns.drain(..) { + match AdoptedEntity::new(graphics, &spawn.asset_path, Some(&spawn.asset_name)) { + Ok(adopted) => { + let entity_id = + self.world + .spawn((adopted, spawn.transform, spawn.properties)); + self.selected_entity = Some(entity_id); + + UndoableAction::push_to_undo( + &mut self.undo_stack, + UndoableAction::Spawn(entity_id), + ); + + log::info!( + "Successfully spawned {} with ID {:?}", + spawn.asset_name, + entity_id + ); + } + Err(e) => { + log::error!("Failed to spawn {}: {}", spawn.asset_name, e); + } + } + } + } + + if matches!(self.editor_state, EditorState::Playing) { + if self.input_state.pressed_keys.contains(&KeyCode::Escape) { + self.signal = Signal::StopPlaying; + } + + let mut script_entities = Vec::new(); + for (entity_id, script) in self.world.query::<&mut ScriptComponent>().iter() { + log_once::debug_once!("Script Entity -> id: {:?}, component: {:?}", entity_id, script); + script.name = script.path.file_name().unwrap().to_str().unwrap().to_string(); + script_entities.push((entity_id, script.name.clone())); + } + + if script_entities.is_empty() { + log_once::warn_once!("Script entities is empty"); + } + + for (entity_id, script_name) in script_entities { + if let Err(e) = self.script_manager.update_entity_script(entity_id, &script_name, &mut self.world, &self.input_state, dt) { + log_once::warn_once!("Failed to update script '{}' for entity {:?}: {}", script_name, entity_id, e); + } + } + } + + if self.is_viewport_focused + && matches!(self.viewport_mode, ViewportMode::CameraMove) + // && self.is_using_debug_camera() + { + let movement_keys: std::collections::HashSet<KeyCode> = self + .input_state + .pressed_keys + .iter() + .filter(|&&key| { + matches!( + key, + KeyCode::KeyW + | KeyCode::KeyA + | KeyCode::KeyS + | KeyCode::KeyD + | KeyCode::Space + | KeyCode::ShiftLeft + ) + }) + .copied() + .collect(); + + // Handle camera input through ECS + if let Some(active_camera) = self.active_camera { + if let Ok(mut query) = self.world.query_one::<(&mut Camera, &CameraComponent)>(active_camera) { + if let Some((camera, component)) = query.get() { + // Handle keyboard input based on camera type + match component.camera_type { + CameraType::Debug => { + DebugCamera::handle_keyboard_input(camera, &movement_keys); + DebugCamera::handle_mouse_input(camera, component, self.input_state.mouse_delta); + } + CameraType::Player => { + PlayerCamera::handle_keyboard_input(camera, &movement_keys); + PlayerCamera::handle_mouse_input(camera, component, self.input_state.mouse_delta); + } + CameraType::Normal => { + // Handle normal camera input if needed + DebugCamera::handle_keyboard_input(camera, &movement_keys); + DebugCamera::handle_mouse_input(camera, component, self.input_state.mouse_delta); + } + } + } + } + } + } + + match &self.signal { + Signal::Paste(scene_entity) => { + match AdoptedEntity::new( + graphics, + &scene_entity.model_path.to_project_path(self.project_path.clone().unwrap()).unwrap(), + Some(&scene_entity.label), + ) { + Ok(adopted) => { + let entity_id = self.world.spawn(( + adopted, + scene_entity.transform, + ModelProperties::default(), + )); + self.selected_entity = Some(entity_id); + log::debug!( + "Successfully paste-spawned {} with ID {:?}", + scene_entity.label, + entity_id + ); + + success_without_console!("Paste!"); + self.signal = Signal::Copy(scene_entity.clone()); + } + Err(e) => { + warn!("Failed to paste-spawn {}: {}", scene_entity.label, e); + } + } + } + Signal::Delete => { + if let Some(sel_e) = &self.selected_entity { + let is_viewport_cam = if let Ok(mut q) = self.world.query_one::<&CameraComponent>(*sel_e) { if let Some(c) = q.get() { if matches!(c.camera_type, CameraType::Debug) { true } else { false } } else { false } } else { false }; + if is_viewport_cam { + warn!("You can't delete the viewport camera"); + self.signal = Signal::None; + } else { + match self.world.despawn(*sel_e) { + Ok(_) => { + info!("Decimated entity"); + self.signal = Signal::None; + } + Err(e) => { + warn!("Failed to delete entity: {}", e); + self.signal = Signal::None; + } + } + } + } + } + Signal::Undo => { + if let Some(action) = self.undo_stack.pop() { + match action.undo(&mut self.world) { + Ok(_) => { + info!("Undid action"); + } + Err(e) => { + warn!("Failed to undo action: {}", e); + } + } + } else { + warn_without_console!("Nothing to undo"); + log::debug!("No undoable actions in stack"); + } + self.signal = Signal::None; + } + Signal::None => {} + Signal::Copy(_) => {} + Signal::ScriptAction(action) => match action { + ScriptAction::AttachScript { + script_path, + script_name, + } => { + if let Some(selected_entity) = self.selected_entity { + match scripting::move_script_to_src(script_path) { + Ok(moved_path) => { + let new_script = ScriptComponent { + name: script_name.clone(), + path: moved_path.clone(), + }; + + let replaced = if let Ok(mut sc) = + self.world.get::<&mut ScriptComponent>(selected_entity) + { + sc.name = new_script.name.clone(); + sc.path = new_script.path.clone(); + true + } else { + match scripting::attach_script_to_entity( + &mut self.world, + selected_entity, + new_script.clone(), + ) { + Ok(_) => false, + Err(e) => { + fatal!( + "Failed to attach script to entity {:?}: {}", + selected_entity, + e + ); + self.signal = Signal::None; + return; + } + } + }; + + if let Err(e) = scripting::convert_entity_to_group( + &self.world, + selected_entity, + ) { + log::warn!("convert_entity_to_group failed (non-fatal): {}", e); + } + + success!( + "{} script '{}' at {} to entity {:?}", + if replaced { "Reattached" } else { "Attached" }, + script_name, + moved_path.display(), + selected_entity + ); + } + Err(e) => { + fatal!("Move failed: {}", e); + } + } + } else { + fatal!("AttachScript requested but no entity is selected"); + } + + self.signal = Signal::None; + } + ScriptAction::CreateAndAttachScript { + script_path, + script_name, + } => { + if let Some(selected_entity) = self.selected_entity { + let new_script = ScriptComponent { + name: script_name.clone(), + path: script_path.clone(), + }; + + let replaced = if let Ok(mut sc) = + self.world.get::<&mut ScriptComponent>(selected_entity) + { + sc.name = new_script.name.clone(); + sc.path = new_script.path.clone(); + true + } else { + match scripting::attach_script_to_entity( + &mut self.world, + selected_entity, + new_script.clone(), + ) { + Ok(_) => false, + Err(e) => { + fatal!("Failed to attach new script: {}", e); + self.signal = Signal::None; + return; + } + } + }; + + if let Err(e) = + scripting::convert_entity_to_group(&self.world, selected_entity) + { + log::warn!("convert_entity_to_group failed (non-fatal): {}", e); + } + + success!( + "{} new script '{}' at {} to entity {:?}", + if replaced { "Replaced" } else { "Attached" }, + script_name, + script_path.display(), + selected_entity + ); + } else { + warn_without_console!("No selected entity to attach new script"); + log::warn!("CreateAndAttachScript requested but no entity is selected"); + } + self.signal = Signal::None; + } + ScriptAction::RemoveScript => { + if let Some(selected_entity) = self.selected_entity { + if let Ok(script) = self.world.remove_one::<ScriptComponent>(selected_entity) { + success!("Removed script from entity {:?}", selected_entity); + + if let Err(e) = scripting::convert_entity_to_group( + &self.world, + selected_entity, + ) { + log::warn!("convert_entity_to_group failed (non-fatal): {}", e); + } + log::debug!("Pushing remove component to undo stack"); + UndoableAction::push_to_undo(&mut self.undo_stack, UndoableAction::RemoveComponent(selected_entity, ComponentType::Script(script))); + } else { + warn!( + "No script component found on entity {:?}", + selected_entity + ); + } + } else { + warn!("No entity selected to remove script from"); + } + + self.signal = Signal::None; + } + ScriptAction::EditScript => { + if let Some(selected_entity) = self.selected_entity { + if let Ok(mut q) = self.world.query_one::<&ScriptComponent>(selected_entity) + { + if let Some(script) = q.get() { + match open::that(script.path.clone()) { + Ok(()) => { + success!("Opened {}", script.name) + } + Err(e) => { + warn!("Error while opening {}: {}", script.name, e); + } + } + } + } else { + warn!( + "No script component found on entity {:?}", + selected_entity + ); + } + } else { + warn!("No entity selected to edit script"); + } + self.signal = Signal::None; + } + }, + Signal::Play => { + // Check if a player camera target exists + let has_player_camera_target = self.world + .query::<(&Camera, &CameraComponent, &CameraFollowTarget)>() + .iter() + .any(|(_, (_, comp, _))| matches!(comp.camera_type, CameraType::Player)); + + if has_player_camera_target { + if let Err(e) = PlayModeBackup::create_backup(self) { + fatal!("Failed to create play mode backup: {}", e); + self.signal = Signal::None; + return; + } + + self.editor_state = EditorState::Playing; + + self.switch_to_player_camera(); + + let mut script_entities = Vec::new(); + for (entity_id, script) in self.world.query::<&ScriptComponent>().iter() { + script_entities.push((entity_id, script.clone())); + } + + for (entity_id, script) in script_entities { + log::debug!("Initialising entity script [{}] from path: {}", script.name, script.path.display()); + match self.script_manager.load_script(&script.path) { + Ok(script_name) => { + if let Err(e) = self.script_manager.init_entity_script(entity_id, &script_name, &mut self.world, &self.input_state) { + log::warn!("Failed to initialise script '{}' for entity {:?}: {}", script.name, entity_id, e); + self.signal = Signal::StopPlaying; + } else { + success_without_console!("You are in play mode now! Press Escape to exit"); + log::info!("You are in play mode now! Press Escape to exit"); + } + } + Err(e) => { + // todo: proper error menu + fatal!("Failed to load script '{}': {}", script.name, e); + self.signal = Signal::StopPlaying; + } + } + } + } else { + fatal!("Unable to build: Player camera not attached to an entity"); + } + + self.signal = Signal::None; + } + Signal::StopPlaying => { + if let Err(e) = PlayModeBackup::restore(self) { + warn!("Failed to restore from play mode backup: {}", e); + log::warn!("Failed to restore scene state: {}", e); + } + + self.editor_state = EditorState::Editing; + + self.switch_to_debug_camera(); + + for (entity_id, _) in self.world.query::<&ScriptComponent>().iter() { + self.script_manager.remove_entity_script(entity_id); + } + + success!("Exited play mode"); + log::info!("Back to the editor you go..."); + + self.signal = Signal::None; + }, + Signal::CameraAction(action) => match action { + CameraAction::SetPlayerTarget { entity, offset } => { + // Find player camera and add/update CameraFollowTarget component + let player_camera = self.world + .query::<(&Camera, &CameraComponent)>() + .iter() + .find_map(|(e, (_, comp))| { + if matches!(comp.camera_type, CameraType::Player) { + Some(e) + } else { + None + } + }); + + if let Some(camera_entity) = player_camera { + let mut follow_target = (false, CameraFollowTarget::default()); + // Find the target entity label + if let Ok(mut query) = self.world.query_one::<&AdoptedEntity>(*entity) { + if let Some(adopted) = query.get() { + follow_target = (true, CameraFollowTarget { + follow_target: adopted.label().to_string(), + offset: *offset, + }); + } + } + + if follow_target.0 { + let _ = self.world.insert_one(camera_entity, follow_target); + info!("Set player camera target to entity {:?}", entity); + } + } + self.signal = Signal::None; + } + CameraAction::ClearPlayerTarget => { + let player_camera = self.world + .query::<(&Camera, &CameraComponent)>() + .iter() + .find_map(|(e, (_, comp))| { + if matches!(comp.camera_type, CameraType::Player) { + Some(e) + } else { + None + } + }); + + if let Some(camera_entity) = player_camera { + let _ = self.world.remove_one::<CameraFollowTarget>(camera_entity); + } + info!("Cleared player camera target"); + self.signal = Signal::None; + } + }, + Signal::AddComponent(entity, e_type) => { + match e_type { + EntityType::Entity => { + if let Ok(e) = self.world.query_one_mut::<&AdoptedEntity>(*entity) { + let mut local_signal: Option<Signal> = None; + let label = e.label().clone(); + let mut show = true; + egui::Window::new(format!("Add component for {}", label)) + .title_bar(true) + .open(&mut show) + .scroll([false, true]) + .anchor(Align2::CENTER_CENTER, [0.0, 0.0]) + .enabled(true) + .show(&graphics.get_egui_context(), |ui| { + if ui.add_sized([ui.available_width(), 30.0], egui::Button::new("Scripting")).clicked() { + log::debug!("Adding scripting component to entity [{}]", label); + if let Err(e) = self.world.insert_one(*entity, ScriptComponent::default()) { + warn!("Failed to add scripting component to entity: {}", e); + } else { + success!("Added the scripting component"); + } + local_signal = Some(Signal::None); + } + if ui.add_sized([ui.available_width(), 30.0], egui::Button::new("Camera")).clicked() { + log::debug!("Adding camera component to entity [{}]", label); + + let has_camera = self.world.query_one::<(&Camera, &CameraComponent)>(*entity).is_ok(); + + if has_camera { + warn!("Entity [{}] already has a camera component", label); + } else { + let camera = Camera::predetermined(graphics, Some(&format!("{} Camera", label))); + let component = CameraComponent::new(); + + if let Err(e) = self.world.insert(*entity, (camera, component)) { + warn!("Failed to add camera component to entity: {}", e); + } else { + success!("Added the camera component"); + } + } + local_signal = Some(Signal::None); + } + }); + if !show { + self.signal = Signal::None; + } + if let Some(signal) = local_signal { + self.signal = signal + } + } else { + log_once::warn_once!("Failed to add component to entity: no entity component found"); + } + } + EntityType::Light => { + if let Ok(light) = self.world.query_one_mut::<&Light>(*entity) { + let mut show = true; + egui::Window::new(format!("Add component for {}", light.label)) + .scroll([false, true]) + .anchor(Align2::CENTER_CENTER, [0.0, 0.0]) + .enabled(true) + .open(&mut show) + .title_bar(true) + .show(&graphics.get_egui_context(), |ui| { + if ui.add_sized([ui.available_width(), 30.0], egui::Button::new("Scripting")).clicked() { + log::debug!("Adding scripting component to light [{}]", light.label); + + success!("Added the scripting component to light [{}]", light.label); + self.signal = Signal::None; + } + }); + if !show { + self.signal = Signal::None; + } + } else { + log_once::warn_once!("Failed to add component to light: no light component found"); + } + }, + EntityType::Camera => { + if let Ok((cam, _comp)) = self.world.query_one_mut::<(&Camera, &CameraComponent)>(*entity) { + let mut show = true; + egui::Window::new(format!("Add component for {}", cam.label)) + .scroll([false, true]) + .anchor(Align2::CENTER_CENTER, [0.0, 0.0]) + .enabled(true) + .open(&mut show) + .title_bar(true) + .show(&graphics.get_egui_context(), |ui| { + egui_extras::install_image_loaders(ui.ctx()); + ui.add(Image::from_bytes("bytes://theres_nothing", include_bytes!("../../../resources/theres_nothing.jpg"))); + ui.label("Theres nothing..."); + // // scripting + // if ui.add_sized([ui.available_width(), 30.0], egui::Button::new("Scripting")).clicked() { + // log::debug!("Adding scripting component to camera [{}]", cam.label); + + // success!("Added the scripting component to camera [{}]", cam.label); + // self.signal = Signal::None; + // } + }); + if !show { + self.signal = Signal::None; + } + } else { + log_once::warn_once!("Failed to add component to light: no light component found"); + } + } + } + }, + Signal::RemoveComponent(entity, c_type) => { + match c_type { + ComponentType::Script(_) => { + match self.world.remove_one::<ScriptComponent>(*entity) { + Ok(component) => { + success!("Removed script component from entity {:?}", entity); + UndoableAction::push_to_undo(&mut self.undo_stack, UndoableAction::RemoveComponent(*entity, ComponentType::Script(component))); + } + Err(e) => { + warn!("Failed to remove script component from entity: {}", e); + } + }; + self.signal = Signal::None; + }, + ComponentType::Camera(_, _, follow) => { + if let Some(_) = follow { + match self.world.remove::<(Camera, CameraComponent, CameraFollowTarget)>(*entity) { + Ok(component) => { + success!("Removed camera component from entity {:?}", entity); + UndoableAction::push_to_undo(&mut self.undo_stack, UndoableAction::RemoveComponent(*entity, ComponentType::Camera(component.0, component.1, Some(component.2)))); + } + Err(e) => { + warn!("Failed to remove camera component from entity: {}", e); + } + }; + } else { + match self.world.remove::<(Camera, CameraComponent)>(*entity) { + Ok(component) => { + success!("Removed camera component from entity {:?}", entity); + UndoableAction::push_to_undo(&mut self.undo_stack, UndoableAction::RemoveComponent(*entity, ComponentType::Camera(component.0, component.1, None))); + } + Err(e) => { + warn!("Failed to remove script component from entity: {}", e); + } + }; + } + } + } + } + Signal::CreateEntity => { + // self.show_add_entity_window = true; + let mut show = true; + egui::Window::new("Add Entity") + .scroll([false, true]) + .anchor(Align2::CENTER_CENTER, [0.0, 0.0]) + .enabled(true) + .open(&mut show) + .title_bar(true) + .show(&graphics.get_egui_context(), |ui| { + if ui.add_sized([ui.available_width(), 30.0], egui::Button::new("Model")).clicked() { + log::debug!("Creating new model"); + warn!("Instead of using the `Add Entity` window, double click on the imported model in the asset \n\ + viewer to import a new model, then tweak the settings to how you wish after!"); + self.signal = Signal::None; + } + + if ui.add_sized([ui.available_width(), 30.0], egui::Button::new("Light")).clicked() { + log::debug!("Creating new lighting"); + let transform = Transform::new(); + let component = LightComponent::default(); + let light = Light::new(graphics, &component, &transform, Some("Light")); + self.world.spawn((light, component, transform)); + success!("Created new light"); + + // always ensure the signal is reset after action is dun + self.signal = Signal::None; + } + + if ui.add_sized([ui.available_width(), 30.0], egui::Button::new("Plane")).clicked() { + log::debug!("Creating new plane"); + let plane = PlaneBuilder::new() + .with_size(500.0, 200.0) + .build( + graphics, + PROTO_TEXTURE, + Some("Plane") + ).unwrap(); + let transform = Transform::new(); + let mut props = ModelProperties::new(); + props.custom_properties.insert("width".to_string(), PropertyValue::Float(500.0)); + props.custom_properties.insert("height".to_string(), PropertyValue::Float(200.0)); + props.custom_properties.insert("tiles_x".to_string(), PropertyValue::Int(500)); + props.custom_properties.insert("tiles_z".to_string(), PropertyValue::Int(200)); + self.world.spawn((plane, transform, props)); + success!("Created new plane"); + + self.signal = Signal::None; + } + + if ui.add_sized([ui.available_width(), 30.0], egui::Button::new("Cube")).clicked() { + log::debug!("Creating new cube"); + let model = Model::load_from_memory( + graphics, + include_bytes!("../../../resources/cube.obj").to_vec(), + Some("Cube") + ); + match model { + Ok(model) => { + let cube = AdoptedEntity::adopt( + graphics, + model, + Some("Cube") + ); + self.world.spawn((cube, Transform::new(), ModelProperties::new())); + } + Err(e) => { + fatal!("Failed to load cube model: {}", e); + } + } + success!("Created new cube"); + + self.signal = Signal::None; + } + + if ui.add_sized([ui.available_width(), 30.0], egui::Button::new("Cube")).clicked() { + log::debug!("Creating new cube"); + let model = Model::load_from_memory( + graphics, + include_bytes!("../../../resources/cube.obj").to_vec(), + Some("Cube") + ); + match model { + Ok(model) => { + let cube = AdoptedEntity::adopt( + graphics, + model, + Some("Cube") + ); + self.world.spawn((cube, Transform::new(), ModelProperties::new())); + } + Err(e) => { + fatal!("Failed to load cube model: {}", e); + } + } + success!("Created new cube"); + + self.signal = Signal::None; + } + + if ui.add_sized([ui.available_width(), 30.0], egui::Button::new("Camera")).clicked() { + log::debug!("Creating new cube"); + let camera = Camera::predetermined(graphics, None); + let component = CameraComponent::new(); + self.world.spawn((camera, component)); + success!("Created new camera"); + + self.signal = Signal::None; + } + }); + if !show { + self.signal = Signal::None; + } + }, + Signal::LogEntities => { + log::info!("===================="); + for entity in self.world.iter() { + if let Some(entity) = entity.get::<&AdoptedEntity>() { + log::info!("Model: {:?}", entity.label()); + } + + if let Some(entity) = entity.get::<&Light>() { + log::info!("Light: {:?}", entity.label()); + } + } + log::info!("===================="); + self.signal = Signal::None; + } + } + + let current_size = graphics.state.viewport_texture.size; + self.size = current_size; + + let new_aspect = current_size.width as f64 / current_size.height as f64; + if let Some(active_camera) = self.active_camera { + if let Ok(mut query) = self.world.query_one::<&mut Camera>(active_camera) { + if let Some(camera) = query.get() { + camera.aspect = new_aspect; + } + } + } + + for (_entity_id, (camera, _component, follow_target)) in self + .world + .query::<(&mut Camera, &CameraComponent, Option<&CameraFollowTarget>)>() + .iter() + { + if let Some(target) = follow_target { + for (_target_entity_id, (adopted, transform)) in self + .world + .query::<(&AdoptedEntity, &Transform)>() + .iter() + { + if adopted.label() == &target.follow_target { + let target_pos = transform.position; + camera.eye = target_pos + target.offset; + camera.target = target_pos; + break; + } + } + } + } + + for (_entity_id, (camera, component)) in self.world.query::<(&mut Camera, &mut CameraComponent)>().iter() { + component.update(camera); + camera.update(graphics); + } + + let query = self.world.query_mut::<(&mut AdoptedEntity, &Transform)>(); + for (_, (entity, transform)) in query { + entity.update(&graphics, transform); + } + + let light_query = self.world.query_mut::<(&mut LightComponent, &Transform, &mut Light)>(); + for (_, (light_component, transform, light)) in light_query { + light.update(light_component, transform); + } + + self.light_manager.update(graphics, &self.world); + } + + fn render(&mut self, graphics: &mut Graphics) { + // cornflower blue + let color = Color { + r: 100.0 / 255.0, + g: 149.0 / 255.0, + b: 237.0 / 255.0, + a: 1.0, + }; + + self.color = color.clone(); + self.size = graphics.state.viewport_texture.size.clone(); + self.texture_id = Some(graphics.state.texture_id.clone()); + self.show_ui(&graphics.get_egui_context()); + + self.window = Some(graphics.state.window.clone()); + logging::render(&graphics.get_egui_context()); + if let Some(pipeline) = &self.render_pipeline { + if let Some(active_camera) = self.active_camera { + if let Ok(mut query) = self.world.query_one::<&Camera>(active_camera) { + if let Some(camera) = query.get() { + let mut light_query = self.world.query::<(&Light, &LightComponent)>(); + let mut entity_query = self.world.query::<(&AdoptedEntity, &Transform)>(); + { + let mut render_pass = graphics.clear_colour(color); + + if let Some(light_pipeline) = &self.light_manager.pipeline { + render_pass.set_pipeline(light_pipeline); + for (_, (light, component)) in light_query.iter() { + if component.enabled { + render_pass.set_vertex_buffer(1, light.instance_buffer.as_ref().unwrap().slice(..)); + render_pass.draw_light_model( + light.model(), + camera.bind_group(), + light.bind_group(), + ); + } + } + } + + let mut model_batches: HashMap<*const Model, Vec<InstanceRaw>> = HashMap::new(); + + for (_, (entity, _)) in entity_query.iter() { + let model_ptr = entity.model() as *const Model; + let instance_raw = entity.instance.to_raw(); + model_batches.entry(model_ptr).or_insert(Vec::new()).push(instance_raw); + } + + render_pass.set_pipeline(pipeline); + + for (model_ptr, instances) in model_batches { + let model = unsafe { &*model_ptr }; + + let instance_buffer = graphics.state.device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Batched Instance Buffer"), + contents: bytemuck::cast_slice(&instances), + usage: wgpu::BufferUsages::VERTEX, + }); + + render_pass.set_vertex_buffer(1, instance_buffer.slice(..)); + render_pass.draw_model_instanced( + model, + 0..instances.len() as u32, + camera.bind_group(), + self.light_manager.bind_group(), + ); + } + } + } + } + } + } + } + + fn exit(&mut self, _event_loop: &ActiveEventLoop) {} + + fn run_command(&mut self) -> SceneCommand { + std::mem::replace(&mut self.scene_command, SceneCommand::None) + } +} @@ -1,3 +1,168 @@ -fn main() { - println!("Hello, world!"); +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] +mod utils; +mod camera; +mod editor; +mod build; +mod menu; +mod debug; + +use std::{cell::RefCell, rc::Rc, fs, path::PathBuf}; +use clap::{Arg, Command}; + +use dropbear_engine::{WindowConfiguration, scene}; + +pub const APP_INFO: app_dirs2::AppInfo = app_dirs2::AppInfo { + name: "Eucalyptus", + author: "4tkbytes", +}; + +fn main() -> anyhow::Result<()> { + #[cfg(target_os = "android")] + compile_error!("The `editor` feature is not supported on Android. If you are attempting\ + to use the Eucalyptus editor on Android, please don't. Instead, use the `data-only` feature\ + to use with dependencies or create your own game on Desktop. Sorry :("); + let matches = Command::new("eucalyptus-editor") + .about("A visual game editor") + .version(env!("CARGO_PKG_VERSION")) + .subcommand_required(false) + .arg_required_else_help(false) + .subcommand( + Command::new("build") + .about("Build a eucalyptus project, but only the .eupak file and its resources") + .arg( + Arg::new("project") + .help("Path to the .eucp project file") + .value_name("PROJECT_FILE") + .required(false), + ), + ) + .subcommand( + Command::new("package") + .about("Package a eucalyptus project, which compiles the runtime and the resource .eupak file") + .arg( + Arg::new("project") + .help("Path to the .eucp project file") + .value_name("PROJECT_FILE") + .required(false), + ), + ) + .subcommand(Command::new("read") + .about("Reads and displays the contents of a .eupak file for debugging") + .arg( + Arg::new("eupak_file") + .help("Path to the .eupak file") + .value_name("RESOURCE_FILE") + .required(false), + ), + ) + .subcommand(Command::new("health").about("Check the health of the eucalyptus installation")) + .get_matches(); + + match matches.subcommand() { + Some(("build", sub_matches)) => { + let project_path = match sub_matches.get_one::<String>("project") { + Some(path) => PathBuf::from(path), + None => match find_eucp_file() { + Ok(path) => path, + Err(e) => { + eprintln!("Error: {}", e); + std::process::exit(1); + } + }, + }; + + crate::build::build(project_path)?; + } + Some(("package", sub_matches)) => { + let project_path = match sub_matches.get_one::<String>("project") { + Some(path) => PathBuf::from(path), + None => match find_eucp_file() { + Ok(path) => path, + Err(e) => { + eprintln!("Error: {}", e); + std::process::exit(1); + } + }, + }; + + crate::build::package(project_path, sub_matches)?; + } + Some(("health", _)) => { + crate::build::health()?; + } + Some(("read", sub_matches)) => { + let project_path = match sub_matches.get_one::<String>("eupak_file") { + Some(path) => PathBuf::from(path), + None => match find_eucp_file() { + Ok(path) => path, + Err(e) => { + eprintln!("Error: {}", e); + std::process::exit(1); + } + }, + }; + + crate::build::read_from_eupak(project_path)?; + } + None => { + let config = WindowConfiguration { + title: "Eucalyptus, built with dropbear".into(), + windowed_mode: dropbear_engine::WindowedModes::Maximised, + max_fps: dropbear_engine::App::NO_FPS_CAP, + app_info: APP_INFO, + }; + + let _app = dropbear_engine::run_app!(config, |mut scene_manager, mut input_manager| { + let main_menu = Rc::new(RefCell::new(crate::menu::MainMenu::new())); + let editor = Rc::new(RefCell::new(crate::editor::Editor::new())); + + scene::add_scene_with_input( + &mut scene_manager, + &mut input_manager, + main_menu, + "main_menu", + ); + scene::add_scene_with_input( + &mut scene_manager, + &mut input_manager, + editor, + "editor", + ); + + scene_manager.switch("main_menu"); + + (scene_manager, input_manager) + }) + .unwrap(); + } + _ => unreachable!(), + } + Ok(()) +} + +fn find_eucp_file() -> Result<PathBuf, String> { + let current_dir = std::env::current_dir().map_err(|_| "Failed to get current directory")?; + + let entries = fs::read_dir(¤t_dir).map_err(|_| "Failed to read current directory")?; + + let mut eucp_files = Vec::new(); + + for entry in entries { + if let Ok(entry) = entry { + if let Some(file_name) = entry.file_name().to_str() { + if file_name.ends_with(".eucp") { + eucp_files.push(entry.path()); + } + } + } + } + + match eucp_files.len() { + 0 => Err("No .eucp files found in current directory".to_string()), + 1 => Ok(eucp_files[0].clone()), + _ => Err(format!( + "Multiple .eucp files found: {:#?}. Please specify which one to use.", + eucp_files + )), + } } @@ -0,0 +1,384 @@ +use std::{ + fs, + sync::mpsc::{self, Receiver}, +}; + +use anyhow::anyhow; +use dropbear_engine::{ + input::{Controller, Keyboard, Mouse}, + scene::{Scene, SceneCommand}, +}; +use egui::{self, FontId, Frame, RichText}; +use egui_toast_fork::{ToastOptions, Toasts}; +use gilrs; +use git2::Repository; +use log::{self, debug}; +use winit::{ + dpi::PhysicalPosition, event::MouseButton, event_loop::ActiveEventLoop, keyboard::KeyCode, +}; + +use eucalyptus_core::states::{PROJECT, ProjectConfig}; + +#[derive(Default)] +pub struct MainMenu { + scene_command: SceneCommand, + show_new_project: bool, + project_name: String, + project_path: Option<std::path::PathBuf>, + project_error: Option<Vec<String>>, + + progress_rx: Option<Receiver<ProjectProgress>>, + + show_progress: bool, + progress: f32, + progress_message: String, + toast: Toasts, + is_in_file_dialogue: bool, +} + +pub enum ProjectProgress { + Step { + progress: f32, + message: String, + }, + #[allow(dead_code)] + Error(String), + Done, +} + +impl MainMenu { + pub fn new() -> Self { + Self { + show_progress: false, + toast: Toasts::new() + .anchor(egui::Align2::RIGHT_BOTTOM, (-10.0, -10.0)) + .direction(egui::Direction::BottomUp), + ..Default::default() + } + } + + fn start_project_creation(&mut self) { + let (tx, rx) = mpsc::channel(); + let project_name = self.project_name.clone(); + let project_path = self.project_path.clone(); + + self.progress_rx = Some(rx); + self.show_progress = true; + self.progress = 0.0; + self.progress_message = "Starting project creation...".to_string(); + + std::thread::spawn(move || { + let mut errors = Vec::new(); + let folders = [ + ("git", 0.1, "Creating a git folder..."), + ("src", 0.2, "Creating src folder..."), + ("resources/models", 0.3, "Creating models folder..."), + ("resources/shaders", 0.4, "Creating shader folder..."), + ("resources/textures", 0.5, "Creating textures folder..."), + ("src2", 0.6, "Creating project config file..."), + ("scenes", 0.7, "Creating the scenes folder"), + ]; + + if let Some(path) = &project_path { + for (folder, progress, message) in folders { + tx.send(ProjectProgress::Step { + progress, + message: message.to_string(), + }) + .ok(); + + let full_path = path.join(folder); + let result: anyhow::Result<()> = if folder == "src" { + if !full_path.exists() { + fs::create_dir(&full_path) + .map_err(|e| anyhow::anyhow!(e)) + .map(|_| ()) + } else { + Ok(()) + } + } else if folder == "git" { + match Repository::init(path) { + Ok(_) => Ok(()), + Err(e) => { + if matches!(e.code(), git2::ErrorCode::Exists) { + log::warn!("Git repository already exists"); + Ok(()) + } else { + Err(anyhow!(e)) + } + } + } + } else if folder == "src2" { + if let Some(path) = &project_path { + let mut config = ProjectConfig::new(project_name.clone(), &path); + let _ = config.write_to_all(); + let mut global = PROJECT.write(); + *global = config; + Ok(()) + } else { + Err(anyhow!("Project path not found")) + } + } else { + if !full_path.exists() { + fs::create_dir_all(&full_path) + .map_err(|e| anyhow!(e)) + .map(|_| ()) + } else { + log::warn!("{:?} already exists", full_path); + Ok(()) + } + }; + if let Err(e) = result { + tx.send(ProjectProgress::Error(e.to_string())).ok(); + errors.push(e); + } + } + tx.send(ProjectProgress::Step { + progress: 1.0, + message: "Project creation complete!".to_string(), + }) + .ok(); + + tx.send(ProjectProgress::Done).ok(); + } + }); + + log::debug!("Starting project creation"); + } +} + +impl Scene for MainMenu { + fn load(&mut self, _graphics: &mut dropbear_engine::graphics::Graphics) { + log::info!("Loaded menu scene"); + } + + fn update(&mut self, _dt: f32, _graphics: &mut dropbear_engine::graphics::Graphics) {} + + fn render(&mut self, graphics: &mut dropbear_engine::graphics::Graphics) { + let screen_size: (f32, f32) = ( + graphics.state.window.inner_size().width as f32 - 100.0, + graphics.state.window.inner_size().height as f32 - 100.0, + ); + let egui_ctx = graphics.get_egui_context(); + + egui::CentralPanel::default() + .frame(Frame::new()) + .show(&egui_ctx, |ui| { + ui.vertical_centered(|ui| { + ui.add_space(64.0); + ui.label(RichText::new("Eucalyptus").font(FontId::proportional(32.0))); + ui.add_space(40.0); + + let button_size = egui::vec2(300.0, 60.0); + + if ui + .add_sized(button_size, egui::Button::new("New Project")) + .clicked() + { + log::debug!("Creating new project"); + self.show_new_project = true; + } + ui.add_space(20.0); + + if ui + .add_sized(button_size, egui::Button::new("Open Project")) + .clicked() + { + log::debug!("Opening project"); + self.is_in_file_dialogue = true; + if let Some(path) = rfd::FileDialog::new() + .add_filter("Eucalyptus Configuration Files", &["eucp"]) + .pick_file() + { + match ProjectConfig::read_from(&path) { + Ok(config) => { + log::info!("Loaded project!"); + let mut global = PROJECT.write(); + *global = config; + // println!("Loaded config info: {:#?}", global); + self.scene_command = + SceneCommand::SwitchScene(String::from("editor")); + } + Err(e) => if e.to_string().contains("missing field") { + self.toast.add(egui_toast_fork::Toast { + kind: egui_toast_fork::ToastKind::Error, + text: format!("Your project version is not up to date with the current project version").into(), + options: ToastOptions::default() + .duration_in_seconds(10.0) + .show_progress(true), + ..Default::default() + }); + log::error!("Failed to load project: {}", e); + } + }; + } else { + log::warn!("File dialog returned \"None\""); + } + self.is_in_file_dialogue = false; + } + ui.add_space(20.0); + + if ui + .add_sized(button_size, egui::Button::new("Settings")) + .clicked() + { + log::debug!("Settings (not implemented)"); + } + ui.add_space(20.0); + + if ui + .add_sized(button_size, egui::Button::new("Quit")) + .clicked() + { + self.scene_command = SceneCommand::Quit + } + ui.add_space(20.0); + }); + }); + + let mut show_new_project = self.show_new_project; + egui::Window::new("Create new project") + .open(&mut show_new_project) + .resizable(true) + .collapsible(false) + .fixed_size(screen_size) + .show(&egui_ctx, |ui| { + ui.vertical(|ui| { + ui.heading("Project Name:"); + ui.add_space(5.0); + + ui.text_edit_singleline(&mut self.project_name); + ui.add_space(10.0); + + ui.heading("Project Location: "); + ui.add_space(5.0); + + if let Some(ref path) = self.project_path { + ui.label(format!("Chosen location: {}", path.display())); + ui.add_space(5.0); + } + + ui.add_space(5.0); + if ui.button("Choose Location").clicked() { + self.is_in_file_dialogue = true; + if let Some(path) = rfd::FileDialog::new() + .set_title("Save Project") + .set_file_name(&self.project_name) + .pick_folder() + { + self.project_path = Some(path); + log::debug!("Project will be saved at: {:?}", self.project_path); + } + self.is_in_file_dialogue = false; + } + + let can_create = self.project_path.is_some() && !self.project_name.is_empty(); + if ui + .add_enabled(can_create, egui::Button::new("Create Project")) + .clicked() + { + log::info!("Creating new project at {:?}", self.project_path); + self.start_project_creation(); + ui.ctx().request_repaint(); + } + }); + }); + self.show_new_project = show_new_project; + + if let Some(rx) = self.progress_rx.as_mut() { + while let Ok(progress) = rx.try_recv() { + match progress { + ProjectProgress::Step { progress, message } => { + self.progress = progress; + self.progress_message = message; + } + ProjectProgress::Error(err) => { + self.project_error.get_or_insert_with(Vec::new).push(err); + } + ProjectProgress::Done if self.project_error.is_none() => { + self.is_in_file_dialogue = false; + self.show_new_project = false; + self.show_progress = false; + self.scene_command = SceneCommand::SwitchScene("editor".to_string()); + } + ProjectProgress::Done => {} + } + } + } + + if self.show_progress { + egui::Window::new("Creating Project...") + .collapsible(true) + .resizable(false) + .fixed_size([400.0, 120.0]) + .show(&egui_ctx, |ui| { + ui.label(&self.progress_message); + ui.add_space(10.0); + + ui.add(egui::ProgressBar::new(self.progress).show_percentage()); + if let Some(errors) = &self.project_error { + ui.colored_label(egui::Color32::RED, "Errors:"); + for err in errors { + ui.label(err); + } + } + }); + } + + self.toast.show(&graphics.get_egui_context()); + } + + fn exit(&mut self, _event_loop: &ActiveEventLoop) { + log::info!("Exiting menu scene"); + } + + fn run_command(&mut self) -> SceneCommand { + std::mem::replace(&mut self.scene_command, SceneCommand::None) + } +} + +impl Keyboard for MainMenu { + fn key_down(&mut self, key: KeyCode, event_loop: &ActiveEventLoop) { + if key == KeyCode::Escape { + if !self.show_new_project && !self.is_in_file_dialogue { + event_loop.exit(); + } + } + } + + fn key_up(&mut self, _key: KeyCode, _event_loop: &ActiveEventLoop) {} +} + +impl Mouse for MainMenu { + fn mouse_move(&mut self, _position: PhysicalPosition<f64>) {} + + fn mouse_down(&mut self, _button: MouseButton) {} + + fn mouse_up(&mut self, _button: MouseButton) {} +} + +impl Controller for MainMenu { + fn button_down(&mut self, button: gilrs::Button, id: gilrs::GamepadId) { + debug!("Controller button {:?} pressed! [{}]", button, id); + } + + fn button_up(&mut self, button: gilrs::Button, id: gilrs::GamepadId) { + debug!("Controller button {:?} released! [{}]", button, id); + } + + fn left_stick_changed(&mut self, x: f32, y: f32, id: gilrs::GamepadId) { + debug!("Left stick changed: x = {} | y = {} | id = {}", x, y, id); + } + + fn right_stick_changed(&mut self, x: f32, y: f32, id: gilrs::GamepadId) { + debug!("Right stick changed: x = {} | y = {} | id = {}", x, y, id); + } + + fn on_connect(&mut self, id: gilrs::GamepadId) { + debug!("Controller connected [{}]", id); + } + + fn on_disconnect(&mut self, id: gilrs::GamepadId) { + debug!("Controller disconnected [{}]", id); + } +} @@ -0,0 +1,219 @@ +use std::fs; +use std::path::PathBuf; +use std::sync::mpsc; +use std::sync::mpsc::Receiver; +use anyhow::anyhow; +use egui::Context; +use egui_toast_fork::{Toast, ToastOptions, Toasts}; +use git2::Repository; +use dropbear_engine::camera::Camera; +use dropbear_engine::scene::SceneCommand; +use eucalyptus_core::states::{ProjectConfig, PROJECT}; +use eucalyptus_core::utils::ProjectProgress; + +pub fn show_new_project_window<F>( + ctx: &Context, + show_new_project: &mut bool, + project_name: &mut String, + project_path: &mut Option<PathBuf>, + on_create: F, +) where + F: FnOnce(&str, &PathBuf), +{ + let screen_size = egui::vec2(400.0, 220.0); + + let mut open = *show_new_project; + egui::Window::new("Create new project") + .open(&mut open) + .resizable(true) + .collapsible(false) + .fixed_size(screen_size) + .show(ctx, |ui| { + ui.vertical(|ui| { + ui.heading("Project Name:"); + ui.add_space(5.0); + + ui.text_edit_singleline(project_name); + ui.add_space(10.0); + + ui.heading("Project Location: "); + ui.add_space(5.0); + + if let Some(path) = project_path { + ui.label(format!("Chosen location: {}", path.display())); + ui.add_space(5.0); + } + + ui.add_space(5.0); + if ui.button("Choose Location").clicked() { + if let Some(path) = rfd::FileDialog::new() + .set_title("Save Project") + .set_file_name(project_name.clone()) + .pick_folder() + { + *project_path = Some(path); + } + } + + let can_create = project_path.is_some() && !project_name.is_empty(); + if ui + .add_enabled(can_create, egui::Button::new("Create Project")) + .clicked() + { + if let Some(path) = project_path { + on_create(project_name, path); + } + ui.ctx().request_repaint(); + } + }); + }); + *show_new_project = open; +} + +/// Converts a click on a screen (like a viewport) coordinate relative to the world +#[allow(dead_code)] +pub fn screen_to_world_coords( + camera: &Camera, + screen_pos: egui::Pos2, + viewport_rect: egui::Rect, +) -> (glam::DVec3, glam::DVec3) { + let viewport_width = viewport_rect.width() as f64; + let viewport_height = viewport_rect.height() as f64; + + let ndc_x = 2.0 * (screen_pos.x as f64 - viewport_rect.min.x as f64) / viewport_width - 1.0; + let ndc_y = 1.0 - 2.0 * (screen_pos.y as f64 - viewport_rect.min.y as f64) / viewport_height; + + let inv_view = camera.view_mat.inverse(); + let inv_proj = camera.proj_mat.inverse(); + + let clip_near = glam::DVec4::new(ndc_x, ndc_y, 0.0, 1.0); + let clip_far = glam::DVec4::new(ndc_x, ndc_y, 1.0, 1.0); + + let view_near = inv_proj * clip_near; + let view_far = inv_proj * clip_far; + + let world_near = inv_view * glam::DVec4::new(view_near.x, view_near.y, view_near.z, 1.0); + let world_far = inv_view * glam::DVec4::new(view_far.x, view_far.y, view_far.z, 1.0); + + let world_near = world_near.truncate() / world_near.w; + let world_far = world_far.truncate() / world_far.w; + + (world_near, world_far) +} + +/// Start creating a new project in a background thread. +/// Returns a Receiver for progress updates. +pub fn start_project_creation( + project_name: String, + project_path: Option<PathBuf>, +) -> Option<Receiver<ProjectProgress>> { + let (tx, rx) = mpsc::channel(); + let project_path = project_path.clone(); + + std::thread::spawn(move || { + let folders = [ + ("git", 0.1, "Creating a git folder..."), + ("src", 0.2, "Creating src folder..."), + ("resources/models", 0.4, "Creating models folder..."), + ("resources/shaders", 0.6, "Creating shader folder..."), + ("resources/textures", 0.8, "Creating textures folder..."), + ("src2", 0.9, "Creating project config file..."), + ]; + + if let Some(path) = &project_path { + for (folder, progress, message) in folders { + tx.send(ProjectProgress::Step { + _progress: progress, + _message: message.to_string(), + }) + .ok(); + + let full_path = path.join(folder); + let result: anyhow::Result<()> = if folder == "src" { + if !full_path.exists() { + fs::create_dir(&full_path) + .map_err(|e| anyhow::anyhow!(e)) + .map(|_| ()) + } else { + Ok(()) + } + } else if folder == "git" { + match Repository::init(path) { + Ok(_) => Ok(()), + Err(e) => { + if matches!(e.code(), git2::ErrorCode::Exists) { + Ok(()) + } else { + Err(anyhow!(e)) + } + } + } + } else if folder == "src2" { + if let Some(path) = &project_path { + let mut config = ProjectConfig::new(project_name.clone(), &path); + let _ = config.write_to_all(); + let mut global = PROJECT.write(); + *global = config; + Ok(()) + } else { + Err(anyhow!("Project path not found")) + } + } else { + if !full_path.exists() { + fs::create_dir_all(&full_path) + .map_err(|e| anyhow!(e)) + .map(|_| ()) + } else { + Ok(()) + } + }; + if let Err(e) = result { + tx.send(ProjectProgress::Error(e.to_string())).ok(); + } + } + tx.send(ProjectProgress::Step { + _progress: 1.0, + _message: "Project creation complete!".to_string(), + }) + .ok(); + tx.send(ProjectProgress::Done).ok(); + } + }); + + Some(rx) +} + +#[allow(dead_code)] +pub fn open_project( + scene_command: &mut SceneCommand, + toast: &mut Toasts, +) -> Result<Option<SceneCommand>, String> { + if let Some(path) = rfd::FileDialog::new() + .add_filter("Eucalyptus Project Configuration Files", &["eucp"]) + .pick_file() + { + match ProjectConfig::read_from(&path) { + Ok(config) => { + let mut global = PROJECT.write(); + *global = config; + *scene_command = SceneCommand::SwitchScene("editor".to_string()); + Ok(Some(SceneCommand::SwitchScene("editor".to_string()))) + } + Err(e) => { + if e.to_string().contains("missing field") { + toast.add(Toast { + kind: egui_toast_fork::ToastKind::Error, + text: "Project version is not up to date.".into(), + options: ToastOptions::default() + .duration_in_seconds(5.0) + .show_progress(true), + ..Default::default() + }); + } + Err(format!("Failed to load project: {e}")) + } + } + } else { + Err("File dialog returned None".to_string()) + } +} @@ -1,109 +0,0 @@ -[package] -name = "eucalyptus" -description = "A visual game editor for the dropbear game engine" -default-run = "eucalyptus" -exclude = ["resources"] - -version.workspace = true -edition.workspace = true -license-file.workspace = true -repository.workspace = true -readme.workspace = true - -[dependencies] -anyhow = { workspace = true, optional = true } -app_dirs2 = { workspace = true, optional = true } -bincode = { workspace = true, optional = true } -chrono = { workspace = true, optional = true } -dropbear-engine = { workspace = true, optional = true } -egui-toast-fork = { workspace = true, optional = true } -egui = { workspace = true, optional = true } -egui_dnd = { workspace = true, optional = true } -egui_dock-fork = { workspace = true, optional = true } -egui_extras = { workspace = true, optional = true } -gilrs = { workspace = true, optional = true } -git2 = { workspace = true, optional = true, features = ["vendored-openssl"]} -glam = { workspace = true, optional = true } -hecs = { workspace = true, optional = true } -log = { workspace = true, optional = true } -log-once = { workspace = true, optional = true } -model_to_image = { workspace = true, optional = true } -once_cell = { workspace = true, optional = true } -open = { workspace = true, optional = true } -parking_lot = { workspace = true, optional = true } -ron = { workspace = true, optional = true } -serde = { workspace = true, optional = true } -tokio = { workspace = true, optional = true } -transform-gizmo-egui = { workspace = true, optional = true } -wgpu = { workspace = true, optional = true } -winit = { workspace = true, optional = true } -clap = { workspace = true, optional = true } -walkdir = { workspace = true, optional = true } -zip = { workspace = true, optional = true } -bytemuck = "1.23.2" - -rustyscript.workspace = true - -[target.'cfg(not(target_os = "android"))'.dependencies] -rfd = { workspace = true, optional = true } - -[features] -editor = [ - "anyhow", - "app_dirs2", - "bincode", - "chrono", - "dropbear-engine", - "egui-toast-fork", - "egui", - "egui_dnd", - "egui_dock-fork", - "egui_extras", - "gilrs", - "git2", - "glam", - "hecs", - "log", - "log-once", - "model_to_image", - "once_cell", - "open", - "parking_lot", - "rfd", - "ron", - "serde", - "tokio", - "transform-gizmo-egui", - "wgpu", - "winit", - "clap", - "walkdir", - "zip" -] -# default = ["editor"] - -data-only = [ - "serde", - "bincode", - "anyhow", - "tokio", - "glam", - "log", - "dropbear-engine", - "winit", - "hecs", - "ron", - "log-once", - "once_cell", - "egui", - "chrono", - "parking_lot", - "walkdir", - "zip" -] - -[build-dependencies] -anyhow = "1.0" -app_dirs2 = "2.5" -reqwest = { version = "0.12", features = ["blocking"] } -zip = "4.3" @@ -1,3 +0,0 @@ -# eucalyptus - -The game editor for the dropbear game engine. The game engine is powered by [dropbear](https://github.com/4tkbytes/dropbear/tree/main/dropbear-engine) and uses the [redback](https://github.com/4tkbytes/dropbear/tree/main/redback) runtime to run and load your game in an easy way. @@ -1,67 +0,0 @@ -use std::fs::{self, File}; -use std::io::Cursor; - -fn main() -> anyhow::Result<()> { - // todo: move this into the "setup" process - let repo_zip_url = "https://github.com/4tkbytes/dropbear/archive/refs/heads/main.zip"; - let response = reqwest::blocking::get(repo_zip_url) - .map_err(|e| anyhow::anyhow!("Failed to download repo zip: {}", e))? - .bytes() - .map_err(|e| anyhow::anyhow!("Failed to read zip bytes: {}", e))?; - - let reader = Cursor::new(response); - let mut zip = zip::ZipArchive::new(reader) - .map_err(|e| anyhow::anyhow!("Failed to read zip archive: {}", e))?; - - let app_info = app_dirs2::AppInfo { - name: "Eucalyptus", - author: "4tkbytes", - }; - let app_data_dir = app_dirs2::app_root(app_dirs2::AppDataType::UserData, &app_info) - .map_err(|e| anyhow::anyhow!("Could not determine app data directory: {}", e))?; - - fs::create_dir_all(&app_data_dir) - .map_err(|e| anyhow::anyhow!("Failed to create app data directory: {}", e))?; - - let resource_prefix = "dropbear-main/resources/"; - let mut found_resource = false; - for i in 0..zip.len() { - let mut file = zip.by_index(i).unwrap(); - let name = file.name(); - - if name.starts_with(resource_prefix) && !name.ends_with('/') { - found_resource = true; - let rel_path = &name[resource_prefix.len()..]; - let rel_path = rel_path.strip_prefix('/').unwrap_or(rel_path); - let dest_path = app_data_dir.join(rel_path); - - if let Some(parent) = dest_path.parent() { - fs::create_dir_all(parent) - .map_err(|e| anyhow::anyhow!("Failed to create parent directory: {}", e))?; - } - - println!("Copying {} to {:?}", name, dest_path); - - let mut outfile = File::create(&dest_path) - .map_err(|e| anyhow::anyhow!("Failed to create file: {}", e))?; - std::io::copy(&mut file, &mut outfile) - .map_err(|e| anyhow::anyhow!("Failed to copy file: {}", e))?; - } - } - - if !found_resource { - return Err(anyhow::anyhow!( - "No resources folder found in the github repository [4tkbytes/dropbear] :(" - )); - } - - // fuck you windows :( - #[cfg(target_os = "windows")] - { - println!("cargo:rustc-link-arg=/FORCE:MULTIPLE"); - println!("cargo:rustc-link-arg=/NODEFAULTLIB:libcmt.lib"); - } - - println!("cargo:rerun-if-changed=build.rs"); - Ok(()) -} @@ -1,469 +0,0 @@ -use std::{collections::HashMap, fs, path::PathBuf, process::Command}; - -use clap::ArgMatches; -use zip::write::SimpleFileOptions; - -use crate::states::{ProjectConfig, RuntimeData, SCENES, SOURCE}; - -pub fn package(project_path: PathBuf, _sub_matches: &ArgMatches) -> anyhow::Result<()> { - if !project_path.exists() { - return Err(anyhow::anyhow!("Unable to locate project config file")); - } - - let build_dir = project_path.parent().ok_or(anyhow::anyhow!("Unable to get parent"))?.join("build"); - - // check health - println!("Checking health (checking if commands exist)"); - health()?; - println!("Health check completed!"); - - let clone_dir = build_dir.join("redback-runtime"); - - if clone_dir.exists() { - println!("Repository directory exists, checking for updates..."); - if should_update_repository(&clone_dir)? { - println!("Repository has changes or is outdated, removing and re-cloning..."); - std::fs::remove_dir_all(&clone_dir)?; - clone_repository(&build_dir)?; - } else { - println!("Repository is up to date, skipping clone"); - } - } else { - println!("Cloning repository"); - clone_repository(&build_dir)?; - } - - let project_config = ProjectConfig::read_from(&project_path)?; - let project_name = project_config.project_name.clone(); - - // cd into redback-runtime folder and compile redback-runtime using cargo - let runtime_dir = build_dir.join("redback-runtime"); - if !runtime_dir.exists() { - return Err(anyhow::anyhow!("redback-runtime directory not found after cloning")); - } - - let cargo_toml_path = runtime_dir.join("Cargo.toml"); - if cargo_toml_path.exists() { - let cargo_toml_content = std::fs::read_to_string(&cargo_toml_path)?; - let modified_content = cargo_toml_content.replace( - r#"name = "redback-runtime""#, - &format!(r#"name = "{}""#, project_name) - ); - std::fs::write(&cargo_toml_path, modified_content)?; - println!("Updated Cargo.toml with project name: {}", project_name); - } - - println!("Building {} for release", project_name); - let mut cargo_build = Command::new("cargo") - .args(&["build", "--release"]) - .current_dir(&runtime_dir) - .stdout(std::process::Stdio::inherit()) - .stderr(std::process::Stdio::inherit()) - .spawn()?; - - let exit_status = cargo_build.wait()?; - - if !exit_status.success() { - return Err(anyhow::anyhow!("Failed to build {}", project_name)); - } - println!("{} built successfully!", project_name); - - let target_dir = runtime_dir.join("target").join("release"); - let exe_name = if cfg!(target_os = "windows") { - format!("{}.exe", project_name) - } else { - project_name.clone() - }; - - let built_exe = target_dir.join(&exe_name); - if !built_exe.exists() { - return Err(anyhow::anyhow!("Built executable not found at: {}", built_exe.display())); - } - - println!("Building project data (.eupak file)"); - build(project_path.clone())?; - - let output_dir = project_path.parent().ok_or(anyhow::anyhow!("Unable to get parent"))?.join("build").join("package"); - std::fs::create_dir_all(&output_dir)?; - - let output_exe = output_dir.join(&exe_name); - - println!("Copying executable to: {}", output_exe.display()); - std::fs::copy(&built_exe, &output_exe)?; - - let eupak_source = project_path.parent().ok_or(anyhow::anyhow!("Unable to get parent"))?.join("build").join("output").join(format!("{}.eupak", project_name)); - let eupak_dest = output_dir.join(format!("{}.eupak", project_name)); - - if !eupak_source.exists() { - return Err(anyhow::anyhow!("Expected .eupak file not found at: {}", eupak_source.display())); - } - - println!("Copying .eupak file to: {}", eupak_dest.display()); - std::fs::copy(&eupak_source, &eupak_dest)?; - - let project_resources = project_path.parent().ok_or(anyhow::anyhow!("Unable to get parent"))?.join("resources"); - if project_resources.exists() { - println!("Copying resources folder..."); - let output_resources = output_dir.join("resources"); - copy_resources_folder(&project_resources, &output_resources)?; - } - - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - let mut perms = std::fs::metadata(&output_exe)?.permissions(); - perms.set_mode(0o755); // rwxr-xr-x - std::fs::set_permissions(&output_exe, perms)?; - } - - copy_system_libraries(&output_dir)?; - - println!("Creating zip package..."); - let zip_path = project_path.parent().ok_or(anyhow::anyhow!("Unable to get parent"))?.join("build").join(format!("{}.zip", project_name)); - create_zip_package(&output_dir, &zip_path)?; - - println!("Cleaning up temporary files"); - if build_dir.join("redback-runtime").exists() { - std::fs::remove_dir_all(build_dir.join("redback-runtime"))?; - } - if build_dir.join("output").exists() { - std::fs::remove_dir_all(build_dir.join("output"))?; - } - - println!("\n✓ Package completed successfully!"); - println!("Output directory: {}", output_dir.display()); - println!("Zip package: {}", zip_path.display()); - println!("Executable: {}", exe_name); - println!("Data file: {}.eupak", project_name); - - Ok(()) -} - -fn clone_repository(build_dir: &PathBuf) -> anyhow::Result<()> { - git2::build::RepoBuilder::new() - .clone("https://github.com/4tkbytes/redback-runtime", &build_dir.join("redback-runtime"))?; - println!("Repository cloned successfully!"); - Ok(()) -} - -fn should_update_repository(repo_dir: &PathBuf) -> anyhow::Result<bool> { - let repo = match git2::Repository::open(repo_dir) { - Ok(repo) => repo, - Err(_) => { - return Ok(true); - } - }; - - let statuses = repo.statuses(None)?; - if !statuses.is_empty() { - println!("Found local changes in repository"); - return Ok(true); - } - - let mut remote = repo.find_remote("origin")?; - remote.fetch(&["refs/heads/*:refs/remotes/origin/*"], None, None)?; - - let head = repo.head()?.target().ok_or(anyhow::anyhow!("No HEAD commit"))?; - - let remote_ref = if let Ok(remote_main) = repo.find_reference("refs/remotes/origin/main") { - remote_main - } else if let Ok(remote_master) = repo.find_reference("refs/remotes/origin/master") { - remote_master - } else { - return Ok(true); - }; - - let remote_commit = remote_ref.target().ok_or(anyhow::anyhow!("No remote commit"))?; - - Ok(head != remote_commit) -} - -fn copy_resources_folder(src: &PathBuf, dest: &PathBuf) -> anyhow::Result<()> { - std::fs::create_dir_all(dest)?; - - for entry in fs::read_dir(src)? { - let entry = entry?; - let src_path = entry.path(); - let file_name = entry.file_name(); - - if file_name == "resources.eucc" { - continue; - } - - let dest_path = dest.join(&file_name); - - if src_path.is_dir() { - copy_resources_folder(&src_path, &dest_path)?; - } else { - std::fs::copy(&src_path, &dest_path)?; - } - } - - Ok(()) -} - -fn copy_system_libraries(output_dir: &PathBuf) -> anyhow::Result<()> { - #[cfg(target_os = "windows")] - { - let dll_paths = vec![ - "C:\\vcpkg\\installed\\x64-windows\\bin\\assimp-vc143-mt.dll", - "C:\\vcpkg\\installed\\x64-windows\\bin\\assimp.dll", - "C:\\Program Files\\Assimp\\bin\\assimp.dll", - "C:\\Program Files (x86)\\Assimp\\bin\\assimp.dll", - ]; - - for dll_path in dll_paths { - if std::path::Path::new(dll_path).exists() { - let dll_name = std::path::Path::new(dll_path).file_name().unwrap(); - let dest = output_dir.join(dll_name); - std::fs::copy(dll_path, dest)?; - println!("Copied system library: {}", dll_name.to_string_lossy()); - break; - } - } - } - - #[cfg(any(target_os = "linux", target_os = "macos"))] - { - let lib_dir = output_dir.join("lib"); - std::fs::create_dir_all(&lib_dir)?; - - let lib_paths = vec![ - "/usr/lib/libassimp.so", - "/usr/lib/x86_64-linux-gnu/libassimp.so", - "/usr/lib64/libassimp.so", - "/usr/local/lib/libassimp.so", - "/opt/homebrew/lib/libassimp.dylib", - "/usr/local/lib/libassimp.dylib", - ]; - - for lib_path in lib_paths { - if std::path::Path::new(lib_path).exists() { - let lib_name = std::path::Path::new(lib_path).file_name().unwrap(); - let dest = lib_dir.join(lib_name); - std::fs::copy(lib_path, dest)?; - println!("Copied system library: {}", lib_name.to_string_lossy()); - break; - } - } - } - - Ok(()) -} - -fn create_zip_package(source_dir: &PathBuf, zip_path: &PathBuf) -> anyhow::Result<()> { - let file = std::fs::File::create(zip_path)?; - let mut zip = zip::ZipWriter::new(file); - - let walkdir = walkdir::WalkDir::new(source_dir); - for entry in walkdir { - let entry = entry?; - let path = entry.path(); - - if path.is_file() { - let relative_path = path.strip_prefix(source_dir)?; - let name = relative_path.to_string_lossy(); - - let options: SimpleFileOptions = Default::default(); - zip.start_file(name, options.into())?; - let mut file = std::fs::File::open(path)?; - std::io::copy(&mut file, &mut zip)?; - } - } - - zip.finish()?; - println!("Created zip package: {}", zip_path.display()); - Ok(()) -} - -pub fn read_from_eupak(eupak_path: PathBuf) -> anyhow::Result<()> { - let bytes = std::fs::read(&eupak_path)?; - let (content, _): (RuntimeData, usize) = bincode::decode_from_slice(&bytes, bincode::config::standard())?; - println!("{} contents: {:#?}", eupak_path.display(), content); - Ok(()) -} - -pub fn build( - project_path: PathBuf, - // _sub_matches: &ArgMatches - ) -> anyhow::Result<PathBuf> { - println!(" > Starting build"); - if !project_path.exists() { - return Err(anyhow::anyhow!(format!("Unable to locate project config file: [{}]", project_path.display()))); - } - // ProjectConfig::read_from(&project_path)?.load_config_to_memory()?; - - let mut project_config = ProjectConfig::read_from(&project_path)?; - log::info!(" > Reading from project config"); - project_config.load_config_to_memory()?; - log::info!(" > Loading config to memory"); - - let source_config = { - let source_guard = SOURCE.read().map_err(|_| anyhow::anyhow!("Unable to lock SOURCE"))?; - source_guard.clone() - }; - log::info!(" > Copied source config"); - - let scene_data = { - let scenes_guard = SCENES.read().map_err(|_| anyhow::anyhow!("Unable to lock SCENES"))?; - scenes_guard.clone() - }; - log::info!(" > Copied scene data"); - - let build_dir = project_path.parent().unwrap().join("build").join("output"); - fs::create_dir_all(&build_dir)?; - log::info!(" > Created build dir"); - - let project_name = project_config.project_name.clone(); - - let mut scripts = HashMap::new(); - let script_dir = project_path.parent().unwrap().join("src"); - if script_dir.exists() { - for entry in fs::read_dir(&script_dir)? { - let entry = entry?; - let path = entry.path(); - if let Some(ext) = path.extension() { - if ext == "rhai" { - let name = path.file_name().unwrap().to_string_lossy().to_string(); - let contents = fs::read_to_string(&path)?; - println!(" > Copied script info from [{}]", name); - scripts.insert(name, contents); - } - } - } - } - - let runtime_data = RuntimeData { - project_config, - source_config, - scene_data, - scripts - }; - log::info!(" > Created runtime data structures"); - - let runtime_file = build_dir.join(format!("{}.eupak", project_name)); - let serialized = bincode::serde::encode_to_vec(runtime_data, bincode::config::standard())?; - std::fs::write(&runtime_file, serialized)?; - log::info!(" > Written the file to build location"); - - println!("Build completed successfully. Output at {:?}", runtime_file.display()); - Ok(runtime_file) -} - -pub fn health() -> anyhow::Result<()> { - let mut all_healthy = true; - - match Command::new("cargo").arg("--version").output() { - Ok(output) => { - if output.status.success() { - let version = String::from_utf8_lossy(&output.stdout).trim().to_string(); - println!("Does cargo exist? ✓ YES - {}", version); - - match Command::new("rustc").arg("--version").output() { - Ok(rustc_output) => { - if rustc_output.status.success() { - let rustc_version = String::from_utf8_lossy(&rustc_output.stdout).trim().to_string(); - println!("Does rustc compiler exist? ✓ YES - {}", rustc_version); - } else { - println!("Does rustc compiler exist? ✗ NO - rustc command failed"); - all_healthy = false; - } - } - Err(_) => { - println!("Does rustc compiler exist? ✗ NO - rustc not found in PATH"); - all_healthy = false; - } - } - } else { - println!("Does cargo exist? ✗ NO - cargo command failed"); - println!("Does rustc compiler exist? ✗ NO - cargo failed, cannot check rustc"); - all_healthy = false; - } - } - Err(_) => { - println!("Does cargo exist? ✗ NO - cargo not found in PATH"); - println!("Does rustc compiler exist? ✗ NO - cargo not found, cannot check rustc"); - all_healthy = false; - } - } - - let assimp_found = check_assimp_availability(); - if assimp_found { - println!("Does assimp lib exist? ✓ YES - Found assimp library"); - } else { - println!("Does assimp lib exist? ⚠ MAYBE - Could not definitively locate assimp, but it may be available through system package manager or vcpkg"); - } - - if all_healthy { - println!("\n✓ All core tools are available!"); - } else { - println!("\n✗ Some required tools are missing. Please install Rust and Cargo."); - return Err(anyhow::anyhow!("Health check failed - missing required tools")); - } - - Ok(()) -} - -fn check_assimp_availability() -> bool { - #[cfg(target_os = "windows")] - { - let common_paths = vec![ - "C:\\vcpkg\\installed\\x64-windows\\lib\\assimp-vc143-mt.lib", - "C:\\vcpkg\\installed\\x64-windows\\lib\\assimp.lib", - "C:\\vcpkg\\installed\\x86-windows\\lib\\assimp-vc143-mt.lib", - "C:\\vcpkg\\installed\\x86-windows\\lib\\assimp.lib", - "C:\\Program Files\\Assimp\\lib\\assimp.lib", - "C:\\Program Files (x86)\\Assimp\\lib\\assimp.lib", - ]; - - for path in common_paths { - if std::path::Path::new(path).exists() { - return true; - } - } - - if let Ok(output) = Command::new("where").arg("assimp.dll").output() { - if output.status.success() { - return true; - } - } - } - - #[cfg(any(target_os = "linux", target_os = "macos"))] - { - let common_paths = vec![ - "/usr/lib/libassimp.so", - "/usr/lib/x86_64-linux-gnu/libassimp.so", - "/usr/lib64/libassimp.so", - "/usr/local/lib/libassimp.so", - "/opt/homebrew/lib/libassimp.dylib", - "/usr/local/lib/libassimp.dylib", - ]; - - for path in common_paths { - if std::path::Path::new(path).exists() { - return true; - } - } - - if let Ok(output) = Command::new("pkg-config").args(&["--exists", "assimp"]).output() { - if output.status.success() { - return true; - } - } - - #[cfg(target_os = "linux")] - { - if let Ok(output) = Command::new("ldconfig").args(&["-p"]).output() { - if output.status.success() { - let output_str = String::from_utf8_lossy(&output.stdout); - if output_str.contains("libassimp") { - return true; - } - } - } - } - } - - false -} @@ -1,276 +0,0 @@ -use std::{collections::HashSet, time::Instant}; - -use dropbear_engine::camera::Camera; -use egui::{CollapsingHeader, Ui}; -use glam::DVec3; -use hecs::Entity; -use serde::{Deserialize, Serialize}; -use winit::keyboard::KeyCode; - -use crate::editor::{component::Component, EntityType, Signal, StaticallyKept, UndoableAction}; - -#[derive(Debug, Clone)] -pub struct CameraComponent { - pub speed: f64, - pub sensitivity: f64, - pub fov_y: f64, - pub camera_type: CameraType -} - -impl CameraComponent { - pub fn new() -> Self { - Self { - speed: 5.0, - sensitivity: 0.002, - fov_y: 60.0, - camera_type: CameraType::Normal, - } - } - - pub fn update(&mut self, camera: &mut Camera) { - camera.speed = self.speed; - camera.sensitivity = self.sensitivity; - camera.fov_y = self.fov_y; - } - - // setting camera offset is just adding the CameraFollowTarget struct - // to the ecs system -} - -pub struct PlayerCamera; - -impl PlayerCamera { - pub fn new() -> CameraComponent { - CameraComponent { - camera_type: CameraType::Player, - ..CameraComponent::new() - } - } - - pub fn handle_keyboard_input( - camera: &mut Camera, - pressed_keys: &HashSet<KeyCode> - ) { - for key in pressed_keys { - match key { - KeyCode::KeyW => camera.move_forwards(), - KeyCode::KeyA => camera.move_left(), - KeyCode::KeyD => camera.move_right(), - KeyCode::KeyS => camera.move_back(), - KeyCode::ShiftLeft => camera.move_down(), - KeyCode::Space => camera.move_up(), - _ => {} - } - } - } - - pub fn handle_mouse_input(camera: &mut Camera, component: &CameraComponent, mouse_delta: Option<(f64, f64)>) { - if let Some((dx, dy)) = mouse_delta { - camera.track_mouse_delta(dx * component.sensitivity, dy * component.sensitivity); - } - } -} - -pub struct DebugCamera; - -impl DebugCamera { - pub fn new() -> CameraComponent { - CameraComponent { - camera_type: CameraType::Debug, - ..CameraComponent::new() - } - } - - pub fn handle_keyboard_input( - camera: &mut Camera, - pressed_keys: &HashSet<KeyCode> - ) { - for key in pressed_keys { - match key { - KeyCode::KeyW => camera.move_forwards(), - KeyCode::KeyA => camera.move_left(), - KeyCode::KeyD => camera.move_right(), - KeyCode::KeyS => camera.move_back(), - KeyCode::ShiftLeft => camera.move_down(), - KeyCode::Space => camera.move_up(), - _ => {} - } - } - } - - pub fn handle_mouse_input(camera: &mut Camera, component: &CameraComponent, mouse_delta: Option<(f64, f64)>) { - if let Some((dx, dy)) = mouse_delta { - camera.track_mouse_delta(dx * component.sensitivity, dy * component.sensitivity); - } - } -} - -#[derive(Debug, Default, Clone)] -pub struct CameraFollowTarget { - pub follow_target: String, - pub offset: DVec3, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub enum CameraType { - Normal, - Debug, - Player, -} - -impl Default for CameraType { - fn default() -> Self { - Self::Normal - } -} - -#[derive(Debug, Clone)] -pub enum CameraAction { - SetPlayerTarget { entity: hecs::Entity, offset: DVec3 }, - ClearPlayerTarget, -} - -#[cfg(feature = "editor")] -impl Component for Camera { - fn inspect(&mut self, entity: &mut Entity, cfg: &mut StaticallyKept, ui: &mut Ui, undo_stack: &mut Vec<UndoableAction>, _signal: &mut Signal, _label: &mut String) { - let _ = _signal; - ui.group(|ui| { - CollapsingHeader::new("Camera").default_open(true).show(ui, |ui| { - ui.horizontal(|ui| { - ui.label("Name: "); - let resp = ui.text_edit_singleline(&mut self.label); - - if resp.changed() { - if cfg.old_label_entity.is_none() { - cfg.old_label_entity = Some(entity.clone()); - cfg.label_original = Some(self.label.clone()); - } - cfg.label_last_edit = Some(Instant::now()); - } - - if resp.lost_focus() { - if let Some(ent) = cfg.old_label_entity.take() { - if ent == *entity { - if let Some(orig) = cfg.label_original.take() { - UndoableAction::push_to_undo( - undo_stack, - UndoableAction::Label(ent, orig, EntityType::Entity), - ); - log::debug!("Pushed camera label change to undo stack"); - } - } else { - cfg.label_original = None; - } - } - cfg.label_last_edit = None; - } - }); - - ui.horizontal(|ui| { - ui.label("Position:"); - ui.label(format!("{:.2}, {:.2}, {:.2}", self.eye.x, self.eye.y, self.eye.z)); - }); - - ui.horizontal(|ui| { - ui.label("Target:"); - ui.label(format!("{:.2}, {:.2}, {:.2}", self.target.x, self.target.y, self.target.z)); - }); - }); - }); - } -} - -#[derive(Debug)] -#[cfg(feature = "editor")] -pub enum UndoableCameraAction { - Speed(hecs::Entity, f64), - Sensitivity(hecs::Entity, f64), - FOV(hecs::Entity, f64), - Type(hecs::Entity, CameraType), -} - -#[cfg(feature = "editor")] -impl Component for CameraComponent { - fn inspect(&mut self, _entity: &mut Entity, _cfg: &mut StaticallyKept, ui: &mut Ui, _undo_stack: &mut Vec<UndoableAction>, _signal: &mut Signal, _label: &mut String) { - ui.group(|ui| { - CollapsingHeader::new("Camera Component").default_open(true).show(ui, |ui| { - ui.horizontal(|ui| { - ui.label("Type:"); - egui::ComboBox::from_label("") - .selected_text(format!("{:?}", self.camera_type)) - .show_ui(ui, |ui| { - ui.selectable_value(&mut self.camera_type, CameraType::Normal, "Normal"); - if !matches!(self.camera_type, CameraType::Player) { - ui.selectable_value(&mut self.camera_type, CameraType::Debug, "Debug"); - } else { - ui.add_enabled(false, egui::Button::new("Debug")); - ui.label("Debug not available for player cameras"); - } - ui.selectable_value(&mut self.camera_type, CameraType::Player, "Player"); - }); - }); - - ui.horizontal(|ui| { - ui.label("Speed:"); - ui.add(egui::DragValue::new(&mut self.speed).speed(0.1).range(0.1..=20.0)); - }); - - ui.horizontal(|ui| { - ui.label("Sensitivity:"); - ui.add(egui::DragValue::new(&mut self.sensitivity).speed(0.0001).range(0.0001..=1.0)); - }); - - ui.horizontal(|ui| { - ui.label("FOV:"); - ui.add(egui::Slider::new(&mut self.fov_y, 10.0..=120.0).suffix("°")); - }); - - if matches!(self.camera_type, CameraType::Player) { - ui.separator(); - ui.label("Player Camera Controls:"); - if ui.button("Set as Active Camera").clicked() { - // This would need to be implemented via signal - log::info!("Set player camera as active (not implemented)"); - } - } - }); - }); - } -} - -#[cfg(feature = "editor")] -impl Component for CameraFollowTarget { - fn inspect(&mut self, _entity: &mut Entity, _cfg: &mut StaticallyKept, ui: &mut Ui, _undo_stack: &mut Vec<UndoableAction>, signal: &mut Signal, _label: &mut String) { - ui.group(|ui| { - CollapsingHeader::new("Camera Follow Target").default_open(true).show(ui, |ui| { - ui.horizontal(|ui| { - ui.label("Target Entity:"); - ui.text_edit_singleline(&mut self.follow_target); - }); - - ui.horizontal(|ui| { - ui.label("Offset:"); - }); - - ui.horizontal(|ui| { - ui.label("X:"); - ui.add(egui::DragValue::new(&mut self.offset.x).speed(0.1)); - }); - - ui.horizontal(|ui| { - ui.label("Y:"); - ui.add(egui::DragValue::new(&mut self.offset.y).speed(0.1)); - }); - - ui.horizontal(|ui| { - ui.label("Z:"); - ui.add(egui::DragValue::new(&mut self.offset.z).speed(0.1)); - }); - - if ui.button("Clear Target").clicked() { - *signal = Signal::CameraAction(CameraAction::ClearPlayerTarget); - } - }); - }); - } -} @@ -1,348 +0,0 @@ -//! Old code for managing cameras. Currently in a deprecated state, and used for reference -//! in the case something doesn't match up with my camera controllers -//! or I need advice. - -use std::{any::Any, collections::HashSet}; - -use dropbear_engine::{camera::Camera, entity::Transform}; -use glam::DVec3; -use serde::{Deserialize, Serialize}; -use winit::keyboard::KeyCode; - -pub trait CameraController: std::fmt::Debug { - fn update(&mut self, camera: &mut Camera, dt: f32); - fn handle_keyboard_input(&mut self, camera: &mut Camera, pressed_keys: &HashSet<KeyCode>); - fn handle_mouse_input(&mut self, camera: &mut Camera, mouse_delta: Option<(f64, f64)>); - - fn as_any(&self) -> &dyn Any; - fn as_any_mut(&mut self) -> &mut dyn Any; -} - -#[derive(Debug)] -pub struct DebugCameraController { - #[allow(dead_code)] - pub speed: f64, - pub sensitivity: f64, -} - -impl DebugCameraController { - pub fn new() -> Self { - Self { - speed: 0.125, - sensitivity: 0.002, - } - } -} - -impl CameraController for DebugCameraController { - fn update(&mut self, _camera: &mut Camera, _dt: f32) { - // Debug camera doesn't need frame-based updates - } - - fn handle_keyboard_input( - &mut self, - camera: &mut Camera, - pressed_keys: &std::collections::HashSet<KeyCode>, - ) { - for key in pressed_keys { - match key { - KeyCode::KeyW => camera.move_forwards(), - KeyCode::KeyA => camera.move_left(), - KeyCode::KeyD => camera.move_right(), - KeyCode::KeyS => camera.move_back(), - KeyCode::ShiftLeft => camera.move_down(), - KeyCode::Space => camera.move_up(), - _ => {} - } - } - } - - fn handle_mouse_input(&mut self, camera: &mut Camera, mouse_delta: Option<(f64, f64)>) { - if let Some((dx, dy)) = mouse_delta { - camera.track_mouse_delta(dx * self.sensitivity, dy * self.sensitivity); - } - } - - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } -} - -#[allow(dead_code)] -#[derive(Debug)] -pub struct PlayerCameraController { - pub follow_target: Option<hecs::Entity>, - pub offset: DVec3, - pub follow_speed: f64, - pub look_sensitivity: f64, -} - -#[allow(dead_code)] -impl PlayerCameraController { - pub fn new() -> Self { - Self { - follow_target: None, - offset: DVec3::new(0.0, 2.0, -5.0), - follow_speed: 5.0, - look_sensitivity: 0.002, - } - } - - pub fn set_follow_target(&mut self, entity: hecs::Entity) { - self.follow_target = Some(entity); - } - - pub fn clear_follow_target(&mut self) { - self.follow_target = None; - } - - pub fn set_offset(&mut self, offset: DVec3) { - self.offset = offset; - } - - pub fn get_follow_target(&self) -> Option<hecs::Entity> { - self.follow_target - } -} - -impl CameraController for PlayerCameraController { - fn update(&mut self, _camera: &mut Camera, _dt: f32) { - // todo: implement following the entity - } - - fn handle_keyboard_input( - &mut self, - camera: &mut Camera, - pressed_keys: &std::collections::HashSet<KeyCode>, - ) { - // todo: handle keyboard input, make it custom according to user - for key in pressed_keys { - match key { - KeyCode::KeyW => camera.move_forwards(), - KeyCode::KeyA => camera.move_left(), - KeyCode::KeyD => camera.move_right(), - KeyCode::KeyS => camera.move_back(), - KeyCode::ShiftLeft => camera.move_down(), - KeyCode::Space => camera.move_up(), - _ => {} - } - } - } - - fn handle_mouse_input(&mut self, camera: &mut Camera, mouse_delta: Option<(f64, f64)>) { - if let Some((dx, dy)) = mouse_delta { - camera.track_mouse_delta(dx * self.look_sensitivity, dy * self.look_sensitivity); - } - } - - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } -} - -use std::collections::HashMap; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub enum CameraType { - Debug, - Player, -} - -#[derive(Debug)] -pub struct CameraManager { - cameras: HashMap<CameraType, Camera>, - controllers: HashMap<CameraType, Box<dyn CameraController>>, - active_camera: CameraType, -} - -impl CameraManager { - pub fn new() -> Self { - Self { - cameras: HashMap::new(), - controllers: HashMap::new(), - active_camera: CameraType::Debug, - } - } - - pub fn add_camera( - &mut self, - camera_type: CameraType, - camera: Camera, - controller: Box<dyn CameraController>, - ) { - self.cameras.insert(camera_type, camera); - self.controllers.insert(camera_type, controller); - } - - pub fn set_active(&mut self, camera_type: CameraType) { - if self.cameras.contains_key(&camera_type) { - self.active_camera = camera_type; - } - } - - pub fn get_active(&self) -> Option<&Camera> { - self.cameras.get(&self.active_camera) - } - - pub fn get_active_mut(&mut self) -> Option<&mut Camera> { - self.cameras.get_mut(&self.active_camera) - } - - pub fn get_active_type(&self) -> CameraType { - self.active_camera - } - - pub fn update_all(&mut self, dt: f32, graphics: &dropbear_engine::graphics::Graphics) { - for (camera_type, camera) in self.cameras.iter_mut() { - if let Some(controller) = self.controllers.get_mut(camera_type) { - controller.update(camera, dt); - } - camera.update(graphics); - } - } - - pub fn handle_input( - &mut self, - pressed_keys: &std::collections::HashSet<KeyCode>, - mouse_delta: Option<(f64, f64)>, - ) { - if let Some(camera) = self.cameras.get_mut(&self.active_camera) { - if let Some(controller) = self.controllers.get_mut(&self.active_camera) { - controller.handle_keyboard_input(camera, pressed_keys); - controller.handle_mouse_input(camera, mouse_delta); - } - } - } - - pub fn update_camera_following(&mut self, world: &hecs::World, _dt: f32) { - if let Some(player_camera) = self.cameras.get_mut(&CameraType::Player) { - if let Some(player_controller) = self.controllers.get_mut(&CameraType::Player) { - if let Some(controller) = player_controller - .as_any_mut() - .downcast_mut::<PlayerCameraController>() - { - if let Some(target_entity) = controller.follow_target { - match world.get::<&Transform>(target_entity) { - Ok(transform) => { - let target_pos = transform.position + controller.offset; - - let raw_dir = player_camera.target - player_camera.eye; - let look_direction = if raw_dir.length() > 1e-6 { - raw_dir.normalize() - } else { - let fwd = player_camera.forward(); - if fwd.length().is_finite() && fwd.length() > 1e-6 { - fwd - } else { - glam::DVec3::new(0.0, 0.0, 1.0) - } - }; - - player_camera.eye = target_pos; - player_camera.target = target_pos + look_direction; - } - Err(_) => { - log_once::error_once!("Follow target entity {:?} not present in world", target_entity); - } - } - } else { - log_once::error_once!("Unable to follow camera: No follow target exists in controller"); - } - } else { - log_once::error_once!("Unable to follow camera: Failed to downcast mut"); - } - } else { - log_once::error_once!("Unable to follow camera: No player controller exists"); - } - } else { - log_once::error_once!("Unable to follow camera: No camera exists for player"); - } - } - - #[allow(dead_code)] - pub fn set_player_camera_target(&mut self, entity: hecs::Entity, offset: DVec3) { - if let Some(controller) = self.controllers.get_mut(&CameraType::Player) { - if let Some(player_controller) = controller - .as_any_mut() - .downcast_mut::<PlayerCameraController>() - { - player_controller.set_follow_target(entity); - player_controller.set_offset(offset); - } - } - } - - #[allow(dead_code)] - pub fn clear_player_camera_target(&mut self) { - if let Some(controller) = self.controllers.get_mut(&CameraType::Player) { - if let Some(player_controller) = controller - .as_any_mut() - .downcast_mut::<PlayerCameraController>() - { - player_controller.clear_follow_target(); - } - } - } - - #[allow(dead_code)] - pub fn get_player_camera_target(&self) -> Option<hecs::Entity> { - if let Some(controller) = self.controllers.get(&CameraType::Player) { - if let Some(player_controller) = - controller.as_any().downcast_ref::<PlayerCameraController>() - { - return player_controller.get_follow_target(); - } - } - None - } - - pub fn get_camera(&self, camera_type: &CameraType) -> Option<&Camera> { - self.cameras.get(camera_type) - } - - #[allow(dead_code)] - pub fn get_camera_mut(&mut self, camera_type: &CameraType) -> Option<&mut Camera> { - self.cameras.get_mut(camera_type) - } - - #[allow(dead_code)] - pub fn has_camera(&self, camera_type: &CameraType) -> bool { - self.cameras.contains_key(camera_type) - } - - #[allow(dead_code)] - pub fn remove_camera(&mut self, camera_type: &CameraType) -> Option<Camera> { - self.controllers.remove(camera_type); - self.cameras.remove(camera_type) - } - - pub fn clear_cameras(&mut self) { - self.cameras.clear(); - self.controllers.clear(); - } - - pub fn get_player_camera_offset(&self) -> Option<DVec3> { - if let Some(controller) = self.controllers.get(&CameraType::Player) { - if let Some(player_controller) = - controller.as_any().downcast_ref::<PlayerCameraController>() - { - return Some(player_controller.offset); - } - } - None - } -} - -#[derive(Debug)] -pub enum CameraAction { - SetPlayerTarget { entity: hecs::Entity, offset: DVec3 }, - ClearPlayerTarget, -} @@ -1,18 +0,0 @@ -//! Used to aid with debugging any issues with the editor. -use egui::Ui; -use crate::editor::Signal; - -/// Show a menu bar for debug. A new "Debug" menu button will show up on the editors menu bar. -pub(crate) fn show_menu_bar(ui: &mut Ui, signal: &mut Signal) { - ui.menu_button("Debug", |ui_debug| { - if ui_debug.button("Panic").clicked() { - log::warn!("Panic caused on purpose from Menu Button Click"); - panic!("Testing out panicking with new panic module, this is a test") - } - - if ui_debug.button("Show Entities Loaded").clicked() { - log::info!("Show Entities Loaded under Debug Menu is clicked"); - *signal = Signal::LogEntities; - } - }); -} @@ -1,1115 +0,0 @@ -// Modules for the dropbear-engine scripting component -// Made by 4tkbytes -// EDIT THIS IF YOU WISH, I RECOMMEND YOU NOT TOUCH IT - -/** - * A class describing the position, scale and rotation to be able to manipulate - * the entity's location. - */ -export class Transform { - /** - * A {@link Vector3} describing the position of the entity - */ - position: Vector3; - /** - * A {@link Quaternion} describing the rotation of the entity - */ - rotation: Quaternion; - /** - * A {@link Vector3} describing the scale of the entity - */ - scale: Vector3; - - public constructor(position?: Vector3, rotation?: Quaternion, scale?: Vector3) { - this.position = position || Vector3.zero(); - this.rotation = rotation || Quaternion.identity(); - this.scale = scale || Vector3.one(); - } - - /** - * Translates/Offsets the position of the entity by a {@link Vector3} - * - * # Example - * @example - * ```ts - * let transform = new Transform(); - * transform.translate(new Vector3(1.0, 1.0, 1.0)); - * ``` - * - * @param movement - A {@link Vector3} for position - */ - public translate(movement: Vector3) { - this.position.x += movement.x; - this.position.y += movement.y; - this.position.z += movement.z; - } - - /** - * Rotates the transformables rotation on the X axis - * - * # Example - * @example - * ```ts - * let transform = new Transform(); - * transform.rotateX(dbMath.degreesToRadians(180)); - * ``` - * - * @param angle - The angle in radians - */ - public rotateX(angle: number) { - const rotQuat = Quaternion.fromAxisAngle(new Vector3(1, 0, 0), angle); - this.rotation = Quaternion.multiply(this.rotation, rotQuat); - - } - - /** - * Rotates the transformables rotation on the Y axis - * - * # Example - * ```ts - * let transform = new Transform(); - * transform.rotateY(dbMath.degreesToRadians(180)) - * ``` - * - * @param angle - The angle in radians - */ - public rotateY(angle: number) { - const rotQuat = Quaternion.fromAxisAngle(new Vector3(0, 1, 0), angle); - this.rotation = Quaternion.multiply(this.rotation, rotQuat); - } - - /** - * Rotates the transformables rotation on the Z axis - * - * # Example - * ```ts - * let transform = new Transform(); - * transform.rotateZ(dbMath.degreesToRadians(180)) - * ``` - * - * @param angle - The angle in radians - */ - public rotateZ(angle: number) { - const rotQuat = Quaternion.fromAxisAngle(new Vector3(0, 0, 1), angle); - this.rotation = Quaternion.multiply(this.rotation, rotQuat); - } - - /** - * Uniformly scales the entity by a multiplier. - * - * # Example - * ```ts - * let transform = new Transform(); - * transform.scaleUniform(2.0) - * ``` - * - * @param scale - A number that the scale multiplies by - */ - public scaleUniform(scale: number) { - this.scale.x *= scale; - this.scale.y *= scale; - this.scale.z *= scale; - } - - /** - * Individually scales the entity by a multiplier by using a {@link Vector3} - * - * # Example - * ```ts - * let transform = new Transform(); - * transform.scaleIndividual(new Vector3(1.0, 2.0, 1.5)) - * ``` - * - * @param scale - A Vector3 representing the scale.x, scale.y and scale.z values - */ - public scaleIndividual(scale: Vector3) { - this.scale.x *= scale.x; - this.scale.y *= scale.y; - this.scale.z *= scale.z; - } -} - -/** - * Utilities for math functions that do not exist in the TypeScript Math module. - */ -export const dbMath = { - /** - * Convert from degrees to radians - * - * # Example - * @example - * ```ts - * console.log(dbMath.degreesToRadians(180)) // expect Math.PI - * ``` - * - * @param deg - The angle in degrees - * @returns The angle in radians - */ - degreesToRadians:(deg: number):number => { - return deg * (Math.PI / 180.0); - }, - - /** - * Convert from radians to degrees - * - * # Example - * @example - * ```ts - * console.log(dbMath.radiansToDegrees(Math.PI)) // expect 180 - * ``` - * - * @param rad - The angle in radians - * @returns The angle in degrees - */ - radiansToDegrees:(rad: number):number => { - return (180 * rad) / Math.PI; - }, - - /** - * Constrains a number to lie within a specified range. If value is less than min, returns min. - * If value is greater than max, returns max. Otherwise, returns value. - * - * @example - * ```ts - * dropbear.dbMath.clamp(5, 0, 10) // → 5 - * dropbear.dbMath.clamp(-3, 0, 10) // → 0 - * dropbear.dbMath.clamp(15, 0, 10) // → 10 - * ``` - * - * @param value - The input value to clamp - * @param min - The lower bound of the range - * @param max - The upper bound of the range - * @returns - The clamped value - */ - clamp: (value: number, min: number, max: number): number => { - return Math.min(Math.max(value, min), max); - } -} - -/** - * A Vector of 3 components: an X, Y and Z. - */ -export class Vector3 { - /** - * The X value - */ - public x: number; - - /** - * The Y value - */ - public y: number; - /** - * The Z value - */ - public z: number; - - public constructor(x: number, y: number, z: number) { - this.x = x; - this.y = y; - this.z = z; - } - - /** - * Converts the {@link Vector3} class to a primitive number array - * - * # Example - * @example - * ```ts - * let vec = new Vector3(1.0, 1.5, 2.0); - * console.log(vec.as_array()) // expect [1.0, 1.5, 2.0] - * ``` - * - * @returns A number array representing the x, y, z values - */ - public as_array(): [number, number, number] { - return [this.x, this.y, this.z]; - } - - /** - * An alternative static constructor to create a {@link Vector3} with all values - * set to 0.0 - * - * # Example - * ```ts - * let vec = Vector3.zero(); - * console.log(vec); // expect [0.0, 0.0, 0.0] - * ``` - * - * @returns A new Vector3 instance with all values set to 0.0 - */ - public static zero(): Vector3 { - return new Vector3(0.0, 0.0, 0.0); - } - - /** - * An alternative static constructor to create a {@link Vector3} with all values - * set to 1.0 - * - * # Example - * ```ts - * let vec = Vector3.one(); - * console.log(vec); // expect [1.0, 1.0, 1.0] - * ``` - * - * @returns A new Vector3 instance with all values set to 1.0 - */ - public static one(): Vector3 { - return new Vector3(1.0, 1.0, 1.0); - } - - /** - * An alternative static constructor that creates a Vector3 from a number array. - * - * # Example - * @example - * ```ts - * let arr = [1.0, 1.5, 2.0]; - * let vec = Vector3.fromArray(arr); - * console.log(vec.to_array() === arr) // expect to print 'true' - * ``` - * - * @param arr - The number array to convert - * @returns - A new Vector3 instance - */ - public static fromArray(arr: [number, number, number]): Vector3 { - return new Vector3(arr[0], arr[1], arr[2]); - } - - /** - * Add another Vector3 to this vector and return the result as a new Vector3. - * - * @param rhs - Right-hand side vector to add. - * @returns A new Vector3 equal to (this + rhs). - * - * @example - * const a = new Vector3(1, 2, 3); - * const b = new Vector3(4, 5, 6); - * const c = a.add(b); // Vector3(5,7,9) - */ - public add(rhs: Vector3): Vector3 { - return new Vector3(this.x + rhs.x, this.y + rhs.y, this.z + rhs.z); - } - - /** - * Subtract another Vector3 from this vector and return the result as a new Vector3. - * - * @param rhs - Right-hand side vector to subtract. - * @returns A new Vector3 equal to (this - rhs). - * - * @example - * const a = new Vector3(5, 7, 9); - * const b = new Vector3(1, 2, 3); - * const c = a.subtract(b); // Vector3(4,5,6) - */ - public subtract(rhs: Vector3): Vector3 { - return new Vector3(this.x - rhs.x, this.y - rhs.y, this.z - rhs.z); - } - - /** - * Multiply this vector by a scalar and return the result as a new Vector3. - * - * @param rhs - Scalar multiplier. - * @returns A new Vector3 scaled by rhs. - * - * @example - * const v = new Vector3(1, 2, 3); - * const s = v.multiply(2); // Vector3(2,4,6) - */ - public multiply(rhs: number): Vector3 { - return new Vector3(this.x * rhs, this.y * rhs, this.z * rhs); - } - - /** - * Compute the Euclidean length (magnitude) of this vector. - * - * @returns The length sqrt(x*x + y*y + z*z). - * - * @example - * const v = new Vector3(1, 2, 2); - * console.log(v.length()); // 3 - */ - public length(): number { - return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z); - } - - /** - * Return a new Vector3 representing the normalized (unit) direction of this vector. - * If the vector has zero length, returns Vector3.zero(). - * - * @returns A normalized Vector3 or Vector3.zero() when length is 0. - * - * @example - * const v = new Vector3(0, 3, 4); - * const n = v.normalize(); // Vector3(0, 0.6, 0.8) - */ - public normalize(): Vector3 { - const len = this.length(); - if (len === 0) return Vector3.zero(); - return new Vector3(this.x / len, this.y / len, this.z / len); - } -} - -/** - * Quaternion representing a rotation in 3D space. - * - * Components are stored as (x, y, z, w) where w is the scalar part. - * Useful helpers are provided to construct quaternions from Euler angles, - * axis/angle, multiply them, and convert to/from arrays. - */ -export class Quaternion { - public x: number; - public y: number; - public z: number; - public w: number; - - /** - * Create a new quaternion. - * - * @param x - X component (default 0) - * @param y - Y component (default 0) - * @param z - Z component (default 0) - * @param w - W (scalar) component (default 1) - * - * @example - * const q = new Quaternion(); // identity - * const q2 = new Quaternion(0.1, 0.2, 0.3, 0.9); - */ - public constructor(x: number = 0, y: number = 0, z: number = 0, w: number = 1) { - this.x = x; - this.y = y; - this.z = z; - this.w = w; - } - - /** - * Construct a quaternion from Euler angles (radians). - * - * The parameters represent rotations about the X, Y and Z axes respectively. - * Angles must be provided in radians. - * - * @param x - rotation about the X axis in radians - * @param y - rotation about the Y axis in radians - * @param z - rotation about the Z axis in radians - * @returns A new Quaternion representing the composite rotation - * - * @example - * const q = Quaternion.fromEuler(Math.PI/2, 0, 0); // 90° around X - */ - public static fromEuler(x: number, y: number, z: number): Quaternion { - // Convert euler angles to quaternion - const cx = Math.cos(x * 0.5); - const sx = Math.sin(x * 0.5); - const cy = Math.cos(y * 0.5); - const sy = Math.sin(y * 0.5); - const cz = Math.cos(z * 0.5); - const sz = Math.sin(z * 0.5); - - return new Quaternion( - sx * cy * cz - cx * sy * sz, - cx * sy * cz + sx * cy * sz, - cx * cy * sz - sx * sy * cz, - cx * cy * cz + sx * sy * sz - ); - } - - /** - * Construct a quaternion representing a rotation around an axis. - * - * The axis will be normalized internally. Angle is in radians. - * - * @param axis - Rotation axis as a Vector3 - * @param angle - Rotation angle in radians - * @returns A new Quaternion representing the axis-angle rotation - * - * @example - * const q = Quaternion.fromAxisAngle(new Vector3(0,1,0), Math.PI); // 180° around Y - */ - public static fromAxisAngle(axis: Vector3, angle: number): Quaternion { - const halfAngle = angle * 0.5; - const sin = Math.sin(halfAngle); - const normalizedAxis = axis.normalize(); - - return new Quaternion( - normalizedAxis.x * sin, - normalizedAxis.y * sin, - normalizedAxis.z * sin, - Math.cos(halfAngle) - ); - } - - /** - * Multiply two quaternions. - * - * The result corresponds to the composition a * b (apply b, then a) using - * the multiplication implemented here. - * - * @param a - Left quaternion - * @param b - Right quaternion - * @returns The product quaternion - * - * @example - * const r = Quaternion.multiply(q1, q2); - */ - public static multiply(a: Quaternion, b: Quaternion): Quaternion { - return new Quaternion( - a.w * b.x + a.x * b.w + a.y * b.z - a.z * b.y, - a.w * b.y - a.x * b.z + a.y * b.w + a.z * b.x, - a.w * b.z + a.x * b.y - a.y * b.x + a.z * b.w, - a.w * b.w - a.x * b.x - a.y * b.y - a.z * b.z - ); - } - - /** - * Create a quaternion from a numeric array [x, y, z, w]. - * - * @param arr - Array containing quaternion components in order [x, y, z, w] - * @returns A new Quaternion with components taken from the array - * - * @example - * const q = Quaternion.fromArray([0, 0, 0, 1]); - */ - public static fromArray(arr: [number, number, number, number]): Quaternion { - return new Quaternion(arr[0], arr[1], arr[2], arr[3]); - } - - /** - * Return the identity quaternion (no rotation). - * - * @returns Quaternion equal to (0, 0, 0, 1) - * - * @example - * const id = Quaternion.identity(); - */ - public static identity(): Quaternion { - return new Quaternion(0.0, 0.0, 0.0, 1.0); - } - - /** - * Convert this quaternion to a numeric array [x, y, z, w]. - * - * @returns An array containing the quaternion components - * - * @example - * const arr = q.as_array(); // [x, y, z, w] - */ - public as_array(): [number, number, number, number] { - return [this.x, this.y, this.z, this.w]; - } -} - -/** - * Values of the Key codes. - */ -export const Keys = { - KeyA: "KeyA", KeyB: "KeyB", KeyC: "KeyC", KeyD: "KeyD", KeyE: "KeyE", KeyF: "KeyF", - KeyG: "KeyG", KeyH: "KeyH", KeyI: "KeyI", KeyJ: "KeyJ", KeyK: "KeyK", KeyL: "KeyL", - KeyM: "KeyM", KeyN: "KeyN", KeyO: "KeyO", KeyP: "KeyP", KeyQ: "KeyQ", KeyR: "KeyR", - KeyS: "KeyS", KeyT: "KeyT", KeyU: "KeyU", KeyV: "KeyV", KeyW: "KeyW", KeyX: "KeyX", - KeyY: "KeyY", KeyZ: "KeyZ", - Digit0: "Digit0", Digit1: "Digit1", Digit2: "Digit2", Digit3: "Digit3", Digit4: "Digit4", - Digit5: "Digit5", Digit6: "Digit6", Digit7: "Digit7", Digit8: "Digit8", Digit9: "Digit9", - Space: "Space", - ShiftLeft: "ShiftLeft", ShiftRight: "ShiftRight", - ControlLeft: "ControlLeft", ControlRight: "ControlRight", - AltLeft: "AltLeft", AltRight: "AltRight", - Escape: "Escape", Enter: "Enter", Tab: "Tab", - ArrowUp: "ArrowUp", ArrowDown: "ArrowDown", ArrowLeft: "ArrowLeft", ArrowRight: "ArrowRight", - F1: "F1", F2: "F2", F3: "F3", F4: "F4", F5: "F5", F6: "F6", - F7: "F7", F8: "F8", F9: "F9", F10: "F10", F11: "F11", F12: "F12", -} as const; - -export type KeyCode = typeof Keys[keyof typeof Keys]; - -/** - * The properties of the entity. - * From the eucalyptus editor, you are able to make custom - * properties such as 'speed' or 'health'. This class allows - * you to create and edit new value - * during the runtime of the script. - * - * Usage: - * ```ts - * props.setNumber("hp", 100); - * const hp = props.getNumber("hp"); // 100 - * ``` - */ -export class EntityProperties { - private data: Record<string, any>; - - /** - * Create an EntityProperties wrapper. - * - * @param data - Optional initial properties object. A shallow reference is kept. - */ - public constructor(data?: Record<string, any>) { - this.data = data || {}; - } - - /** - * Get the raw underlying properties object. - * - * Use this to serialize properties back to the host or perform bulk operations. - * - * @returns The raw Record<string, any> used to store properties. - * - * @example - * const raw = props.getRawProperties(); - */ - public getRawProperties(): Record<string, any> { - return this.data; - } - - /** - * Set a string property. - * - * @param key - Property key. - * @param value - String value to set. - * - * @example - * props.setString("tag", "friendly"); - */ - public setString(key: string, value: string): void { - this.data[key] = value; - } - - /** - * Set a numeric property. - * - * @param key - Property key. - * @param value - Number value to set. - * - * @example - * props.setNumber("speed", 4.2); - */ - public setNumber(key: string, value: number): void { - this.data[key] = value; - } - - /** - * Set a boolean property. - * - * @param key - Property key. - * @param value - Boolean value to set. - * - * @example - * props.setBool("isActive", true); - */ - public setBool(key: string, value: boolean): void { - this.data[key] = value; - } - - /** - * Get a string property. - * - * If the property is missing or falsy, an empty string ("") is returned. - * - * @param key - Property key. - * @returns The string value or "" when missing. - * - * @example - * const name = props.getString("name"); - */ - public getString(key: string): string { - return this.data[key] || ""; - } - - /** - * Get a numeric property. - * - * If the property is missing or falsy, 0 is returned. - * - * @param key - Property key. - * @returns The number value or 0 when missing. - * - * @example - * const speed = props.getNumber("speed"); - */ - public getNumber(key: string): number { - return this.data[key] || 0; - } - - /** - * Get a boolean property. - * - * If the property is missing or falsy, false is returned. - * - * @param key - Property key. - * @returns The boolean value or false when missing. - * - * @example - * const active = props.getBool("isActive"); - */ - public getBool(key: string): boolean { - return this.data[key] || false; - } - - /** - * Checks if the entity has a specific property as per a key. - * - * If the property exists, it will return true. If not, it will return false - * @param key - Property key. - * @returns - True is the value exists, false if not - * - * @example - * ```ts - * if props.hasProperty("speed") { - * let speed = props.getNumber("speed"); - * speed = speed+10; - * } else { - * console.log("No value as speed exists"); - * } - * ``` - */ - public hasProperty(key: string): boolean { - return key in this.data; - } -} - -export class Entity { - public label: string; - public transform: Transform; - public properties: EntityProperties; - - /** - * Creates a new instance of entity - * @param entityData - The entty data, typically parsed as an argument in the load or update functions - */ - constructor(label: string, properties: EntityData, transform: TransformData) { - this.label = label; - this.transform = createTransformFromData(transform); - this.properties = new EntityProperties(properties); - } - - /** - * Moves the player forward on the Z axis - * @param distance - The distance (as a number) it moves forward by - */ - moveForward(distance: number): void { - const movement = new Vector3(0, 0, -distance); - this.transform.translate(movement); - } - - /** - * Moves the player back on the Z axis - * @param distance - The distance (as a number) it moves back by - */ - moveBack(distance: number): void { - const movement = new Vector3(0, 0, distance); - this.transform.translate(movement); - } - - /** - * Moves the player left on the X axis - * @param distance - The distance (as a number) it moves left by - */ - moveLeft(distance: number): void { - const movement = new Vector3(-distance, 0, 0); - this.transform.translate(movement); - } - - /** - * Moves the player right on the X axis - * @param distance - The distance (as a number) it moves right by - */ - moveRight(distance: number): void { - const movement = new Vector3(distance, 0, 0); - this.transform.translate(movement); - } - - /** - * Moves the player up on the Y axis - * @param distance - The distance (as a number) it moves up by - */ - moveUp(distance: number): void { - const movement = new Vector3(0, distance, 0); - this.transform.translate(movement); - } - - /** - * Moves the player down on the Y axis - * @param distance - The distance (as a number) it moves down by - */ - moveDown(distance: number): void { - const movement = new Vector3(0, -distance, 0); - this.transform.translate(movement); - } -} - - -/** - * Helper function that creates a new {@link Transform} from - * a {@link TransformData} - * @param data - The raw transformable ({@link TransformData})data - * @returns - An instance of a {@link Transform} - */ -function createTransformFromData(data: TransformData): Transform { - const position = Vector3.fromArray(data.position); - const rotation = Quaternion.fromArray(data.rotation); - const scale = Vector3.fromArray(data.scale); - return new Transform(position, rotation, scale); -} - -/** - * Camera class for controlling and manipulating cameras in the scene. - * Provides functionality for camera movement, switching, and property manipulation. - */ -export class Camera { - public label: string; - - public eye: Vector3; - public target: Vector3; - public up: Vector3; - public aspect: number; - public fov: number; - public near: number; - public far: number; - public yaw: number; - public pitch: number; - public speed: number; - public sensitivity: number; - public camera_type: string; - - - /** - * Create a new Camera instance. - * - * @param data - Optional camera data to initialize from - */ - constructor(label: string, data: CameraData) { - this.eye = Vector3.fromArray(data.eye); - this.target = Vector3.fromArray(data.target); - this.up = Vector3.fromArray(data.up); - this.aspect = data.aspect; - this.fov = data.fov; - this.near = data.near; - this.far = data.far; - this.yaw = data.yaw; - this.pitch = data.pitch; - this.speed = data.speed; - this.sensitivity = data.sensitivity; - this.camera_type = data.camera_type; - this.label = label; - } - - /** - * Track mouse movement for camera look controls. - * - * @param delta - Mouse delta X and Y - * - * @example - * ```ts - * let delta = entity.getMouseDelta(); - * camera.track - * ``` - */ - trackMouseDelta(delta: [number, number]): void { - this.yaw += delta[0] * this.sensitivity; - this.pitch += delta[1] * this.sensitivity; - - this.pitch = dbMath.clamp(this.pitch, dbMath.degreesToRadians(-89.0), dbMath.degreesToRadians(89.0)); - - - let direction = new Vector3( - Math.cos(this.yaw) * Math.cos(this.pitch), - Math.sin(this.pitch), - Math.sin(this.yaw) * Math.cos(this.pitch), - ); - this.target = this.eye.add(direction); - } -} - -export class Light { - -} - -/** - * A raw format for storing the transform data, typically used as a - * FFI by the engine - */ -export interface TransformData { - position: [number, number, number]; - rotation: [number, number, number, number]; // quaternion [x, y, z, w] - scale: [number, number, number]; -} - -/** - * A raw format for storing the custom entity properties from the engines - * raw FFI - */ -export interface EntityData { - custom_properties: Record<string, any>; -} - -/** - * A raw format for storing the input data - */ -export interface InputData { - mouse_pos: [number, number]; - pressed_keys: string[]; - mouse_delta: [number, number] | null; - is_cursor_locked: boolean; -} - -/** - * A raw format for storing the camera data - */ -export interface CameraData { - eye: [number, number, number]; - target: [number, number, number]; - up: [number, number, number]; - aspect: number; - fov: number; - near: number; - far: number; - yaw: number; - pitch: number; - speed: number; - sensitivity: number; - camera_type: string; -} - -/** - * A raw format for storing the light data - */ -export interface LightData { - -} - -/** - * A raw format for storing the scene data - */ -export interface RawSceneData { - entities: [{ - label: string, - properties: EntityData, - transform: TransformData - }]; - cameras: [{ - label: string, - data: CameraData - }]; - lights: [{ - label: string, - data: LightData - }]; - input: InputData; -} - -/** - * A wrapper class aimed to aid with input data - */ -export class Input { - public inputData?: InputData - - // constructor(data: InputData) { - // this.inputData = data; - // } - - // empty because global variable - constructor() { - - } - - /** - * Checks if a key is pressed - * @param key - The specific keycode - * @returns - True is pressed, false if not - * - * @example - * ```ts - * if entity.isKeyPressed(Keys::KeyW) { - * console.log("The W key is pressed"); - * } - * ``` - */ - isKeyPressed(key: KeyCode): boolean { - if (!this.inputData) return false; - return this.inputData.pressed_keys.indexOf(key) !== -1 - } - - /** - * Fetches the mouse position - * @returns - The x,y position of the mouse - */ - getMousePosition(): [number, number] { - return this.inputData?.mouse_pos || [0, 0]; - } - - /** - * Fetches the change in the mouse position from the center (as it gets reset each frame) - * @returns - The dx,dy position of the mouse - */ - getMouseDelta(): [number, number] { - return this.inputData?.mouse_delta || [0.0, 0.0]; - } -} - -/** - * The class containing all the different entities in this scene. - */ -export class Scene { - // ensure none of these are public - current_entity?: string; - entities?: Entity[]; - cameras?: Camera[]; - lights?: Light[]; - - // Sets everything to default - constructor() { - this.cameras = []; - this.entities = []; - this.lights = []; - } - - /** - * Returns a **reference** to the camera in the scene - * @param label - The label of the camera as set by you from the editor - */ - public getCamera(label: string): Camera | undefined { - return this.cameras?.find(c => c.label === label); - } - - /** - * Returns a **reference** to the light in the scene - * @param label - The label of the light as set by you from the editor - */ - public getLight(label: string): Light | undefined { - return this.lights?.find(l => (l as any).label === label); - } - - /** - * Fetches an entity as per its label. Returns the actual Entity instance (mutable reference). - * @param label - The label of the entity - */ - public getEntity(label: string): Entity | undefined { - return this.entities?.find(e => e.label === label); - } - - /** - * Fetches the current entity this script is attached to (returns mutable reference). - */ - public getCurrentEntity(): Entity | undefined { - if (!this.current_entity) { - console.error("Unable to get entity: Have you added dropbear.start(s) yet?"); - return undefined; - } - const ent = this.getEntity(this.current_entity); - if (!ent) { - console.error(`Unable to get entity: no entity with label "${this.current_entity}" found`); - } - return ent; - } -} - -/** - * Starts the specific function by filling the scene data. - * @param data - */ -export function start(data: RawSceneData) { - data.cameras.forEach(camera => { - scene.cameras?.push(new Camera(camera.label, camera.data)) - }); - data.entities.forEach(entity => { - scene.entities?.push(new Entity(entity.label, entity.properties, entity.transform)) - }); - data.lights.forEach(light => { - scene.lights?.push(light) - }); - input.inputData = data.input; -} - -/** - * Ends the scripting function by returning a Partial RawSceneData for the - * rust client to take. - */ -export function end(): Partial<RawSceneData> { - const out: Partial<RawSceneData> = {}; - - if (scene.entities && scene.entities.length) { - out.entities = scene.entities.map(e => { - return { - label: e.label, - properties: { - custom_properties: e.properties.getRawProperties() - } as EntityData, - transform: { - position: e.transform.position.as_array(), - rotation: e.transform.rotation.as_array(), - scale: e.transform.scale.as_array() - } as TransformData - }; - }) as any; - } - - if (scene.cameras && scene.cameras.length) { - out.cameras = scene.cameras.map(c => { - return { - label: c.label, - data: { - eye: c.eye.as_array(), - target: c.target.as_array(), - up: c.up.as_array(), - aspect: c.aspect, - fov: c.fov, - near: c.near, - far: c.far, - yaw: c.yaw, - pitch: c.pitch, - speed: c.speed, - sensitivity: c.sensitivity, - camera_type: c.camera_type - } as CameraData - }; - }) as any; - } - - if (scene.lights && scene.lights.length) { - out.lights = scene.lights.map(l => { - return { - label: (l as any).label || "", - data: {} as LightData - }; - }) as any; - } - - if (input.inputData) { - out.input = input.inputData; - } - - return out; -} - -// global variables -/** - * A global variable that contains all the information about the scene. - * - * To use the variable, you need to run {@link start()} at the start of - * the function and return {@link end()} at the end to send to the engine. - * - * @example - * ```ts - * export function onUpdate(s, dt: number) { - * dropbear.start(s); - * console.log("I'm being updated!"); - * return dropbear.end(); - * } - * ``` - */ -export const scene = new Scene(); -/** - * A global variable that contains all the information about the inputs - * such as the keyboard and the mouse. - * - * To use the variable, you need to run {@link start()} at the start of - * the function and return {@link end()} at the end to send to the engine. - * - * @example - * ```ts - * export function onUpdate(s, dt: number) { - * dropbear.start(s); - * console.log("I'm being updated!"); - * return dropbear.end(); - * } - */ -export const input = new Input(); @@ -1,665 +0,0 @@ -//! This module should describe the different components that are editable in the resource inspector. - -use std::time::Instant; -use egui::{CollapsingHeader, ComboBox, Ui}; -use glam::Vec3; -use hecs::Entity; -use dropbear_engine::attenuation::ATTENUATION_PRESETS; -use dropbear_engine::entity::{AdoptedEntity, Transform}; -use dropbear_engine::lighting::{Light, LightComponent, LightType}; -use crate::editor::{EntityType, Signal, StaticallyKept, UndoableAction}; -use crate::scripting::{ScriptAction, TEMPLATE_SCRIPT}; -use crate::states::ScriptComponent; - -pub trait Component { - fn inspect(&mut self, entity: &mut hecs::Entity, cfg: &mut StaticallyKept, ui: &mut Ui, undo_stack: &mut Vec<UndoableAction>, signal: &mut Signal, label: &mut String); -} - -impl Component for Transform { - fn inspect(&mut self, entity: &mut Entity, cfg: &mut StaticallyKept, ui: &mut Ui, undo_stack: &mut Vec<UndoableAction>, _signal: &mut Signal, _label: &mut String) { - ui.group(|ui| { - CollapsingHeader::new("Transform").default_open(true).show( - ui, - |ui| { - ui.horizontal(|ui| { - ui.label("Position:"); - }); - - ui.horizontal(|ui| { - ui.label("X:"); - let response = ui.add( - egui::DragValue::new(&mut self.position.x) - .speed(0.1) - .fixed_decimals(3), - ); - - if response.drag_started() { - cfg.transform_old_entity = Some(entity.clone()); - cfg.transform_original_transform = Some((*self).clone()); - cfg.transform_in_progress = true; - } - - if response.drag_stopped() && cfg.transform_in_progress { - if let Some(ent) = cfg.transform_old_entity.take() { - if let Some(orig) = cfg.transform_original_transform.take() { - UndoableAction::push_to_undo( - undo_stack, - UndoableAction::Transform(ent, orig), - ); - log::debug!("Pushed X transform change to undo stack"); - } - } - cfg.transform_in_progress = false; - } - }); - ui.horizontal(|ui| { - ui.label("Y:"); - let response = ui.add( - egui::DragValue::new(&mut self.position.y) - .speed(0.1) - .fixed_decimals(3), - ); - - if response.drag_started() { - cfg.transform_old_entity = Some(entity.clone()); - cfg.transform_original_transform = Some((*self).clone()); - cfg.transform_in_progress = true; - } - - if response.drag_stopped() && cfg.transform_in_progress { - if let Some(ent) = cfg.transform_old_entity.take() { - if let Some(orig) = cfg.transform_original_transform.take() { - UndoableAction::push_to_undo( - undo_stack, - UndoableAction::Transform(ent, orig), - ); - log::debug!("Pushed Y transform change to undo stack"); - } - } - cfg.transform_in_progress = false; - } - }); - - ui.horizontal(|ui| { - ui.label("Z:"); - let response = ui.add( - egui::DragValue::new(&mut self.position.z) - .speed(0.1) - .fixed_decimals(3), - ); - - if response.drag_started() { - cfg.transform_old_entity = Some(entity.clone()); - cfg.transform_original_transform = Some((*self).clone()); - cfg.transform_in_progress = true; - } - - if response.drag_stopped() && cfg.transform_in_progress { - if let Some(ent) = cfg.transform_old_entity.take() { - if let Some(orig) = cfg.transform_original_transform.take() { - UndoableAction::push_to_undo( - undo_stack, - UndoableAction::Transform(ent, orig), - ); - log::debug!("Pushed Z transform change to undo stack"); - } - } - cfg.transform_in_progress = false; - } - }); - if ui.button("Reset Position").clicked() { - self.position = glam::DVec3::ZERO; - } - - ui.add_space(5.0); - - ui.horizontal(|ui| { - ui.label("Rotation:"); - }); - - let mut rotation_changed = false; - let (mut x_rot, mut y_rot, mut z_rot) = - self.rotation.to_euler(glam::EulerRot::XYZ); - - ui.horizontal(|ui| { - ui.label("X:"); - let response = ui.add( - egui::Slider::new( - &mut x_rot, - -std::f64::consts::PI - ..=std::f64::consts::PI, - ) - .step_by(0.01) - .custom_formatter(|n, _| { - format!("{:.1}°", n.to_degrees()) - }) - .custom_parser(|s| { - s.trim_end_matches('°') - .parse::<f64>() - .ok() - .map(|v| v.to_radians()) - }), - ); - - if response.drag_started() { - cfg.transform_old_entity = Some(entity.clone()); - cfg.transform_original_transform = Some((*self).clone()); - cfg.transform_in_progress = true; - } - - if response.changed() { - rotation_changed = true; - } - - if response.drag_stopped() && cfg.transform_in_progress { - if let Some(ent) = cfg.transform_old_entity.take() { - if let Some(orig) = cfg.transform_original_transform.take() { - UndoableAction::push_to_undo( - undo_stack, - UndoableAction::Transform(ent, orig), - ); - log::debug!("Pushed X rotation change to undo stack"); - } - } - cfg.transform_in_progress = false; - } - }); - - ui.horizontal(|ui| { - ui.label("Y:"); - let response = ui.add( - egui::Slider::new( - &mut y_rot, - -std::f64::consts::PI - ..=std::f64::consts::PI, - ) - .step_by(0.01) - .custom_formatter(|n, _| { - format!("{:.1}°", n.to_degrees()) - }) - .custom_parser(|s| { - s.trim_end_matches('°') - .parse::<f64>() - .ok() - .map(|v| v.to_radians()) - }), - ); - - if response.drag_started() { - cfg.transform_old_entity = Some(entity.clone()); - cfg.transform_original_transform = Some((*self).clone()); - cfg.transform_in_progress = true; - } - - if response.changed() { - rotation_changed = true; - } - - if response.drag_stopped() && cfg.transform_in_progress { - if let Some(ent) = cfg.transform_old_entity.take() { - if let Some(orig) = cfg.transform_original_transform.take() { - UndoableAction::push_to_undo( - undo_stack, - UndoableAction::Transform(ent, orig), - ); - log::debug!("Pushed Y rotation change to undo stack"); - } - } - cfg.transform_in_progress = false; - } - }); - - ui.horizontal(|ui| { - ui.label("Z:"); - let response = ui.add( - egui::Slider::new( - &mut z_rot, - -std::f64::consts::PI - ..=std::f64::consts::PI, - ) - .step_by(0.01) - .custom_formatter(|n, _| { - format!("{:.1}°", n.to_degrees()) - }) - .custom_parser(|s| { - s.trim_end_matches('°') - .parse::<f64>() - .ok() - .map(|v| v.to_radians()) - }), - ); - - if response.drag_started() { - cfg.transform_old_entity = Some(entity.clone()); - cfg.transform_original_transform = Some((*self).clone()); - cfg.transform_in_progress = true; - } - - if response.changed() { - rotation_changed = true; - } - - if response.drag_stopped() && cfg.transform_in_progress { - if let Some(ent) = cfg.transform_old_entity.take() { - if let Some(orig) = cfg.transform_original_transform.take() { - UndoableAction::push_to_undo( - undo_stack, - UndoableAction::Transform(ent, orig), - ); - log::debug!("Pushed Z rotation change to undo stack"); - } - } - cfg.transform_in_progress = false; - } - }); - - if rotation_changed { - self.rotation = glam::DQuat::from_euler( - glam::EulerRot::XYZ, - x_rot, - y_rot, - z_rot, - ); - } - if ui.button("Reset Rotation").clicked() { - self.rotation = glam::DQuat::IDENTITY; - } - ui.add_space(5.0); - - ui.horizontal(|ui| { - ui.label("Scale:"); - ui.with_layout( - egui::Layout::right_to_left(egui::Align::Center), - |ui| { - let lock_icon = if cfg.scale_locked { - "🔒" - } else { - "🔓" - }; - if ui - .button(lock_icon) - .on_hover_text("Lock uniform scaling") - .clicked() - { - cfg.scale_locked = !cfg.scale_locked; - } - }, - ); - }); - - let mut scale_changed = false; - let mut new_scale = self.scale; - - ui.horizontal(|ui| { - ui.label("X:"); - let response = ui.add( - egui::DragValue::new(&mut new_scale.x) - .speed(0.01) - .fixed_decimals(3), - ); - - if response.drag_started() { - cfg.transform_old_entity = Some(entity.clone()); - cfg.transform_original_transform = Some((*self).clone()); - cfg.transform_in_progress = true; - } - - if response.changed() { - scale_changed = true; - if cfg.scale_locked { - let scale_factor = new_scale.x / self.scale.x; - new_scale.y = self.scale.y * scale_factor; - new_scale.z = self.scale.z * scale_factor; - } - } - - if response.drag_stopped() && cfg.transform_in_progress { - if let Some(ent) = cfg.transform_old_entity.take() { - if let Some(orig) = cfg.transform_original_transform.take() { - UndoableAction::push_to_undo( - undo_stack, - UndoableAction::Transform(ent, orig), - ); - log::debug!("Pushed X scale change to undo stack"); - } - } - cfg.transform_in_progress = false; - } - }); - - ui.horizontal(|ui| { - ui.label("Y:"); - let y_slider = egui::DragValue::new(&mut new_scale.y) - .speed(0.01) - .fixed_decimals(3); - - let response = ui.add_enabled(!cfg.scale_locked, y_slider); - - if response.drag_started() && !cfg.scale_locked { - cfg.transform_old_entity = Some(entity.clone()); - cfg.transform_original_transform = Some((*self).clone()); - cfg.transform_in_progress = true; - } - - if response.changed() { - scale_changed = true; - } - - if response.drag_stopped() && cfg.transform_in_progress { - if let Some(ent) = cfg.transform_old_entity.take() { - if let Some(orig) = cfg.transform_original_transform.take() { - UndoableAction::push_to_undo( - undo_stack, - UndoableAction::Transform(ent, orig), - ); - log::debug!("Pushed Y scale change to undo stack"); - } - } - cfg.transform_in_progress = false; - } - }); - - ui.horizontal(|ui| { - ui.label("Z:"); - let z_slider = egui::DragValue::new(&mut new_scale.z) - .speed(0.01) - .fixed_decimals(3); - - let response = ui.add_enabled(!cfg.scale_locked, z_slider); - - if response.drag_started() && !cfg.scale_locked { - cfg.transform_old_entity = Some(entity.clone()); - cfg.transform_original_transform = Some((*self).clone()); - cfg.transform_in_progress = true; - } - - if response.changed() { - scale_changed = true; - } - - if response.drag_stopped() && cfg.transform_in_progress { - if let Some(ent) = cfg.transform_old_entity.take() { - if let Some(orig) = cfg.transform_original_transform.take() { - UndoableAction::push_to_undo( - undo_stack, - UndoableAction::Transform(ent, orig), - ); - log::debug!("Pushed Z scale change to undo stack"); - } - } - cfg.transform_in_progress = false; - } - }); - - if scale_changed { - self.scale = new_scale; - } - if ui.button("Reset Scale").clicked() { - self.scale = glam::DVec3::ONE; - } - ui.add_space(5.0); - - // maybe use? probs not :/ - // if pos_changed || rotation_changed || scale_changed { - // ui.colored_label(egui::Color32::YELLOW, "Transform modified"); - // } - }, - ); - }); - } -} - -impl Component for ScriptComponent { - fn inspect(&mut self, _entity: &mut Entity, _cfg: &mut StaticallyKept, ui: &mut Ui, _undo_stack: &mut Vec<UndoableAction>, signal: &mut Signal, label: &mut String) { - let script_loc = self.path.to_str().unwrap_or("").to_string(); - - ui.group(|ui| { - CollapsingHeader::new("Scripting") - .default_open(true) - .show(ui, |ui| { - ui.horizontal(|ui| { - if ui.button("Browse").clicked() { - if let Some(script_file) = rfd::FileDialog::new() - .add_filter("Typescript", &["ts"]) - .pick_file() - { - let script_name = script_file - .file_stem() - .unwrap_or_default() - .to_string_lossy() - .to_string(); - let lib_path = &script_file.clone().parent().unwrap().join("dropbear.ts"); - if let Err(_) = std::fs::read(lib_path) { - log::warn!("dropbear.ts library does not exist in project source directory, copying..."); - if let Err(e) = std::fs::write(lib_path, include_str!("../dropbear.ts")) { - log::error!("Non-fatal error: Creating library file failed: {}", e); - } else { - log::info!("Wrote dropbear.ts library file!"); - } - }; - *signal = Signal::ScriptAction(ScriptAction::AttachScript { - script_path: script_file, - script_name, - }); - } - } - - if ui.button("New").clicked() { - if let Some(script_path) = rfd::FileDialog::new() - .add_filter("TypeScript", &["ts"]) - .set_file_name(format!("{}_script.ts", label)) - .save_file() - { - // check if dropbear module exists - // todo: change this to an %APPDATA% file instead of to memory. - let lib_path = &script_path.clone().parent().unwrap().join("dropbear.ts"); - if let Err(_) = std::fs::read(lib_path) { - log::warn!("dropbear.ts library does not exist in project source directory, copying..."); - if let Err(e) = std::fs::write(lib_path, include_str!("../dropbear.ts")) { - log::error!("Non-fatal error: Creating library file failed: {}", e); - } else { - log::info!("Wrote dropbear.ts library file!"); - } - }; - match std::fs::write(&script_path, TEMPLATE_SCRIPT) { - Ok(_) => { - let script_name = script_path - .file_stem() - .unwrap_or_default() - .to_string_lossy() - .to_string(); - *signal = Signal::ScriptAction(ScriptAction::CreateAndAttachScript { - script_path, - script_name, - }); - }, - Err(e) => { - crate::warn!("Failed to create script file: {}", e); - }, - } - } - } - }); - - ui.separator(); - - ui.horizontal_wrapped(|ui| { - ui.label("Script Location:"); - ui.label(script_loc); - }); - - if ui.button("Remove").clicked() { - *signal = Signal::ScriptAction(ScriptAction::RemoveScript); - } - ui.separator(); - ui.horizontal(|ui| { - if ui.button("Edit Script").clicked() { - *signal = Signal::ScriptAction(ScriptAction::EditScript); - } - }); - }); - }); - } -} - -impl Component for AdoptedEntity { - fn inspect(&mut self, entity: &mut Entity, cfg: &mut StaticallyKept, ui: &mut Ui, undo_stack: &mut Vec<UndoableAction>, _signal: &mut Signal, _label: &mut String) { - // label - ui.group(|ui| { - ui.horizontal(|ui| { - ui.label("Name: "); - - let resp = ui.text_edit_singleline(self.label_mut()); - - if resp.changed() { - if cfg.old_label_entity.is_none() { - cfg.old_label_entity = Some(entity.clone()); - cfg.label_original = Some(self.label().clone()); - } - cfg.label_last_edit = Some(Instant::now()); - } - - if resp.lost_focus() { - if let Some(ent) = cfg.old_label_entity.take() { - if ent == *entity { - if let Some(orig) = cfg.label_original.take() { - UndoableAction::push_to_undo( - undo_stack, - UndoableAction::Label(ent, orig, EntityType::Entity), - ); - log::debug!("Pushed label change to undo stack (immediate)"); - } - } else { - cfg.label_original = None; - } - } - cfg.label_last_edit = None; - } - }) - }); - } -} - -impl Component for Light { - fn inspect(&mut self, entity: &mut Entity, cfg: &mut StaticallyKept, ui: &mut Ui, undo_stack: &mut Vec<UndoableAction>, _signal: &mut Signal, _label: &mut String) { - ui.group(|ui| { - ui.horizontal(|ui| { - ui.label("Name: "); - - let resp = ui.text_edit_singleline(&mut self.label); - - if resp.changed() { - if cfg.old_label_entity.is_none() { - cfg.old_label_entity = Some(entity.clone()); - cfg.label_original = Some(self.label.clone().to_string()); - } - cfg.label_last_edit = Some(Instant::now()); - } - - if resp.lost_focus() { - if let Some(ent) = cfg.old_label_entity.take() { - if ent == *entity { - if let Some(orig) = cfg.label_original.take() { - UndoableAction::push_to_undo( - undo_stack, - UndoableAction::Label(ent, orig, EntityType::Light), - ); - log::debug!("Pushed label change to undo stack (immediate)"); - } - } else { - cfg.label_original = None; - } - } - cfg.label_last_edit = None; - } - }) - }); - } -} - -impl Component for LightComponent { - fn inspect(&mut self, _entity: &mut Entity, _cfg: &mut StaticallyKept, ui: &mut Ui, _undo_stack: &mut Vec<UndoableAction>, _signal: &mut Signal, _label: &mut String) { - ui.group(|ui| { - ui.horizontal(|ui| { - ComboBox::new("light_type", "Light Type") - // .width(ui.available_width()) - .selected_text(self.light_type.to_string()) - .show_ui(ui, |ui| { - ui.selectable_value(&mut self.light_type, LightType::Directional, "Directional"); - ui.selectable_value(&mut self.light_type, LightType::Point, "Point"); - ui.selectable_value(&mut self.light_type, LightType::Spot, "Spot"); - }); - }); - - // let is_dir = matches!(self.light_type, LightType::Directional); - let is_point = matches!(self.light_type, LightType::Point); - let is_spot = matches!(self.light_type, LightType::Spot); - - // colour - ui.separator(); - let mut colour = self.colour.clone().as_vec3().to_array(); - ui.horizontal(|ui| { - ui.label("Colour"); - egui::color_picker::color_edit_button_rgb(ui, &mut colour) - }); - self.colour = Vec3::from_array(colour).as_dvec3(); - - // intensity - ui.separator(); - ui.horizontal(|ui| { - ui.label("Intensity"); - ui.add(egui::Slider::new(&mut self.intensity, 0.0..=1.0)); - }); - - // enabled and visible - ui.separator(); - ui.horizontal(|ui| { - ui.checkbox(&mut self.enabled, "Enabled"); - ui.checkbox(&mut self.visible, "Visible"); - }); - - if is_spot || is_point { - // attenuation - ui.separator(); - ui.horizontal(|ui| { - ComboBox::new("Range", "Range") - // .width(ui.available_width()) - .selected_text(format!("Range {}", self.attenuation.range.to_string())) - .show_ui(ui, |ui| { - for (preset, label) in ATTENUATION_PRESETS { - ui.selectable_value(&mut self.attenuation, preset.clone(), *label); - } - }); - }); - } - - if is_spot { - // cutoff angles - ui.horizontal(|ui| { - ui.add( - egui::Slider::new(&mut self.cutoff_angle, 1.0..=89.0) - .text("Inner") - .suffix("°") - .step_by(0.1) - ); - }); - - ui.horizontal(|ui| { - ui.add( - egui::Slider::new(&mut self.outer_cutoff_angle, 1.0..=90.0) - .text("Outer") - .suffix("°") - .step_by(0.1) - ); - }); - - if self.outer_cutoff_angle <= self.cutoff_angle { - self.outer_cutoff_angle = self.cutoff_angle + 1.0; - } - - let cone_softness = self.outer_cutoff_angle - self.cutoff_angle; - ui.label(format!("Soft edge: {:.1}°", cone_softness)); - } - }); - } -} @@ -1,1031 +0,0 @@ -use super::*; -use std::{collections::HashSet, sync::LazyLock}; - -use dropbear_engine::{entity::Transform, lighting::{Light, LightComponent}}; -use egui; -use egui_dock_fork::TabViewer; -use egui_extras; -use log; -use parking_lot::Mutex; -use serde::{Deserialize, Serialize}; -use transform_gizmo_egui::{ - math::{DMat4, DVec3}, EnumSet, Gizmo, GizmoConfig, GizmoExt, GizmoMode -}; - -use crate::{ - APP_INFO, - editor::scene::PENDING_SPAWNS, - states::{EntityNode, Node, RESOURCES, ResourceType}, - utils::PendingSpawn, -}; -use crate::editor::component::Component; - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -pub enum EditorTab { - AssetViewer, // bottom side, - ResourceInspector, // left side, - ModelEntityList, // right side, - Viewport, // middle, -} - -pub struct EditorTabViewer<'a> { - pub view: egui::TextureId, - pub nodes: Vec<EntityNode>, - pub tex_size: Extent3d, - pub gizmo: &'a mut Gizmo, - pub world: &'a mut hecs::World, - pub selected_entity: &'a mut Option<hecs::Entity>, - pub viewport_mode: &'a mut ViewportMode, - pub undo_stack: &'a mut Vec<UndoableAction>, - pub signal: &'a mut Signal, - pub gizmo_mode: &'a mut EnumSet<GizmoMode>, - pub editor_mode: &'a mut EditorState, - pub active_camera: &'a mut Option<hecs::Entity>, -} - -impl<'a> EditorTabViewer<'a> { - fn spawn_entity_at_pos( - &mut self, - asset: &DraggedAsset, - position: glam::DVec3, - properties: Option<ModelProperties>, - ) -> anyhow::Result<()> { - let mut transform = Transform::default(); - transform.position = position; - { - let mut pending_spawns = PENDING_SPAWNS.lock(); - if let Some(props) = properties { - pending_spawns.push(PendingSpawn { - asset_path: asset.path.clone(), - asset_name: asset.name.clone(), - transform, - properties: props, - }); - } else { - pending_spawns.push(PendingSpawn { - asset_path: asset.path.clone(), - asset_name: asset.name.clone(), - transform, - properties: ModelProperties::default(), - }); - } - Ok(()) - } - } -} - -#[derive(Clone, Debug)] -pub struct DraggedAsset { - pub name: String, - pub path: PathBuf, -} - -pub static TABS_GLOBAL: LazyLock<Mutex<StaticallyKept>> = - LazyLock::new(|| Mutex::new(StaticallyKept::default())); - - -/// Variables kept statically. -/// -/// The entire module (including the tab viewer) due to it -/// being part of an update/render function, therefore this is used to ensure -/// progress is not lost. -#[derive(Default)] -pub struct StaticallyKept { - show_context_menu: bool, - context_menu_pos: egui::Pos2, - context_menu_tab: Option<EditorTab>, - is_focused: bool, - old_pos: Transform, - pub(crate) scale_locked: bool, - - pub(crate) old_label_entity: Option<hecs::Entity>, - pub(crate) label_original: Option<String>, - pub(crate) label_last_edit: Option<Instant>, - - pub(crate) transform_old_entity: Option<hecs::Entity>, - pub(crate) transform_original_transform: Option<Transform>, - - pub(crate) transform_in_progress: bool, -} - -impl StaticallyKept {} - -impl<'a> TabViewer for EditorTabViewer<'a> { - type Tab = EditorTab; - - fn title(&mut self, tab: &mut Self::Tab) -> egui::WidgetText { - match tab { - EditorTab::Viewport => "Viewport".into(), - EditorTab::ModelEntityList => "Model/Entity List".into(), - EditorTab::AssetViewer => "Asset Viewer".into(), - EditorTab::ResourceInspector => "Resource Inspector".into(), - } - } - - fn ui(&mut self, ui: &mut egui::Ui, tab: &mut Self::Tab) { - let mut cfg = TABS_GLOBAL.lock(); - - ui.ctx().input(|i| { - if i.pointer.button_pressed(egui::PointerButton::Secondary) { - if let Some(pos) = i.pointer.hover_pos() { - if ui.available_rect_before_wrap().contains(pos) { - cfg.show_context_menu = true; - cfg.context_menu_pos = pos; - cfg.context_menu_tab = Some(tab.clone()); - } - } - } - }); - - match tab { - EditorTab::Viewport => { - // ------------------- Template for querying active camera ----------------- - // if let Some(active_camera) = self.active_camera { - // if let Ok(mut q) = self.world.query_one::<(&Camera, &CameraComponent, Option<&CameraFollowTarget>)>(*active_camera) { - // if let Some((camera, component, follow_target)) = q.get() { - - // } else { - // log_once::warn_once!("Unable to fetch the query result of camera: {:?}", active_camera) - // } - // } else { - // log_once::warn_once!("Unable to query camera, component and option<camerafollowtarget> for active camera: {:?}", active_camera); - // } - // } else { - // log_once::warn_once!("No active camera found"); - // } - // ------------------------------------------------------------------------- - - let available_rect = ui.available_rect_before_wrap(); - let available_size = available_rect.size(); - - let tex_aspect = self.tex_size.width as f32 / self.tex_size.height as f32; - let available_aspect = available_size.x / available_size.y; - - let (display_width, display_height) = if available_aspect > tex_aspect { - let height = available_size.y * 0.95; - let width = height * tex_aspect; - (width, height) - } else { - let width = available_size.x * 0.95; - let height = width / tex_aspect; - (width, height) - }; - - let center_x = available_rect.center().x; - let center_y = available_rect.center().y; - - let image_rect = egui::Rect::from_center_size( - egui::pos2(center_x, center_y), - egui::vec2(display_width, display_height) - ); - - let (_rect, _response) = ui.allocate_exact_size( - available_size, - egui::Sense::click_and_drag() - ); - - let _image_response = ui.allocate_rect(image_rect, egui::Sense::click_and_drag()); - - ui.scope_builder(egui::UiBuilder::new().max_rect(image_rect), |ui| { - ui.add_sized( - [display_width, display_height], - egui::Image::new(( - self.view, - [display_width, display_height].into(), - )) - .fit_to_exact_size([display_width, display_height].into()) - ) - }); - - let image_response = ui.interact(image_rect, ui.id().with("viewport_image"), egui::Sense::click_and_drag()); - - if image_response.clicked() { - if let Some(active_camera) = self.active_camera { - if let Ok(mut q) = self.world.query_one::<(&Camera, &CameraComponent, Option<&CameraFollowTarget>)>(*active_camera) { - if let Some((camera, _, _)) = q.get() { - if let Some(click_pos) = ui.ctx().input(|i| i.pointer.interact_pos()) { - let viewport_rect = image_response.rect; - let local_pos = click_pos - viewport_rect.min; - - let ndc_x = (2.0 * local_pos.x / viewport_rect.width()) - 1.0; - let ndc_y = 1.0 - (2.0 * local_pos.y / viewport_rect.height()); - - let view_matrix = - glam::DMat4::look_at_lh(camera.eye, camera.target, camera.up); - - let proj_matrix = glam::DMat4::perspective_lh( - camera.fov_y.to_radians(), - camera.aspect, - camera.znear, - camera.zfar, - ); - - if !view_matrix.is_finite() { - log::error!("Invalid view matrix"); - return; - } - if !proj_matrix.is_finite() { - log::error!("Invalid projection matrix"); - return; - } - - let view_proj = proj_matrix * view_matrix; - let inv_view_proj = view_proj.inverse(); - - if !inv_view_proj.is_finite() { - log::error!("Cannot invert view-projection matrix"); - return; - } - - let ray_start_ndc = glam::DVec4::new(ndc_x as f64, ndc_y as f64, 0.0, 1.0); - let ray_end_ndc = glam::DVec4::new(ndc_x as f64, ndc_y as f64, 1.0, 1.0); - - let ray_start_world = inv_view_proj * ray_start_ndc; - let ray_end_world = inv_view_proj * ray_end_ndc; - - if ray_start_world.w == 0.0 || ray_end_world.w == 0.0 { - log::error!("Invalid homogeneous coordinates"); - return; - } - - let ray_start = ray_start_world.truncate() / ray_start_world.w; - let ray_end = ray_end_world.truncate() / ray_end_world.w; - - if !ray_start.is_finite() || !ray_end.is_finite() { - log::error!( - "Invalid ray points - start: {:?}, end: {:?}", - ray_start, - ray_end - ); - return; - } - - let ray_direction = (ray_end - ray_start).normalize(); - - if !ray_direction.is_finite() { - log::error!("Invalid ray direction: {:?}", ray_direction); - return; - } - - let mut closest_distance = f64::INFINITY; - let mut selected_entity_id: Option<hecs::Entity> = None; - let mut entity_count = 0; - - for (entity_id, (transform, _)) in - self.world.query::<(&Transform, &AdoptedEntity)>().iter() - { - entity_count += 1; - let entity_pos = transform.position; - let sphere_radius = transform.scale.max_element() * 1.5; - - let to_sphere = entity_pos - ray_start; - let projection = to_sphere.dot(ray_direction); - - if projection > 0.0 { - let closest_point = ray_start + ray_direction * projection; - let distance_to_sphere = (closest_point - entity_pos).length(); - - if distance_to_sphere <= sphere_radius { - let discriminant = sphere_radius * sphere_radius - - distance_to_sphere * distance_to_sphere; - if discriminant >= 0.0 { - let intersection_distance = - projection - discriminant.sqrt(); - - if intersection_distance < closest_distance { - closest_distance = intersection_distance; - selected_entity_id = Some(entity_id); - } - } - } - } - } - - log::debug!("Total entities checked: {}", entity_count); - - if !matches!(self.editor_mode, EditorState::Playing) { - if let Some(entity_id) = selected_entity_id { - *self.selected_entity = Some(entity_id); - log::debug!("Selected entity: {:?}", entity_id); - } else { - // *self.selected_entity = None; - if entity_count == 0 { - log::debug!("No entities in world to select"); - } else { - log::debug!( - "No entity hit by ray (checked {} entities)", - entity_count - ); - } - } - } - } - } else { - log_once::warn_once!("Unable to fetch the query result of camera: {:?}", active_camera) - } - } else { - log_once::warn_once!("Unable to query camera, component and option<camerafollowtarget> for active camera: {:?}", active_camera); - } - } else { - log_once::warn_once!("No active camera found"); - } - - } - - let snapping = ui.input(|input| input.modifiers.shift); - - // Note to self: fuck you >:( - // Note to self: ok wow thats pretty rude im trying my best >﹏< - // Note to self: finally holy shit i got it working - - if let Some(active_camera) = self.active_camera { - if let Ok(mut q) = self.world.query_one::<(&Camera, &CameraComponent, Option<&CameraFollowTarget>)>(*active_camera) { - if let Some((camera, _, _)) = q.get() { - self.gizmo.update_config(GizmoConfig { - view_matrix: DMat4::look_at_lh( - DVec3::new( - camera.eye.x as f64, - camera.eye.y as f64, - camera.eye.z as f64, - ), - DVec3::new( - camera.target.x as f64, - camera.target.y as f64, - camera.target.z as f64, - ), - DVec3::new(camera.up.x as f64, camera.up.y as f64, camera.up.z as f64), - ) - .into(), - projection_matrix: DMat4::perspective_infinite_reverse_lh( - camera.fov_y as f64, - display_width as f64 / display_height as f64, - camera.znear as f64, - ) - .into(), - viewport: image_rect, - modes: *self.gizmo_mode, - orientation: transform_gizmo_egui::GizmoOrientation::Global, - snapping, - snap_distance: 1.0, - ..Default::default() - }); - } else { - log_once::warn_once!("Unable to fetch the query result of camera: {:?}", active_camera) - } - } else { - log_once::warn_once!("Unable to query camera, component and option<camerafollowtarget> for active camera: {:?}", active_camera); - } - } else { - log_once::warn_once!("No active camera found"); - } - - if !matches!(self.viewport_mode, crate::utils::ViewportMode::None) { - if let Some(entity_id) = self.selected_entity { - if let Ok(transform) = - self.world.query_one_mut::<&mut Transform>(*entity_id) - { - let was_focused = cfg.is_focused; - cfg.is_focused = self.gizmo.is_focused(); - - if cfg.is_focused && !was_focused { - cfg.old_pos = *transform; - } - - let gizmo_transform = - transform_gizmo_egui::math::Transform::from_scale_rotation_translation( - transform.scale, - transform.rotation, - transform.position, - ); - - if let Some((_result, new_transforms)) = - self.gizmo.interact(ui, &[gizmo_transform]) - { - if let Some(new_transform) = new_transforms.first() { - transform.position = new_transform.translation.into(); - transform.rotation = new_transform.rotation.into(); - transform.scale = new_transform.scale.into(); - } - } - - if was_focused && !cfg.is_focused { - let transform_changed = cfg.old_pos.position != transform.position - || cfg.old_pos.rotation != transform.rotation - || cfg.old_pos.scale != transform.scale; - - if transform_changed { - UndoableAction::push_to_undo( - &mut self.undo_stack, - UndoableAction::Transform( - entity_id.clone(), - cfg.old_pos.clone(), - ), - ); - log::debug!("Pushed transform action to stack"); - } - } - } - } - } - } - EditorTab::ModelEntityList => { - ui.label("Model/Entity List"); - show_entity_tree( - ui, - &mut self.nodes, - &mut self.selected_entity, - "Model Entity Asset List", - ); - } - EditorTab::AssetViewer => { - egui_extras::install_image_loaders(ui.ctx()); - - let mut assets: Vec<(String, String, PathBuf, ResourceType)> = Vec::new(); - { - let res = RESOURCES.read().unwrap(); - egui_extras::install_image_loaders(ui.ctx()); - - fn recursive_search_nodes_and_attach_thumbnail( - res: &Vec<Node>, - assets: &mut Vec<(String, String, PathBuf, ResourceType)>, - logged: &mut HashSet<String>, - ) { - for node in res { - match node { - Node::File(file) => { - if !logged.contains(&file.name) { - logged.insert(file.name.clone()); - log::debug!( - "Adding image for {} of type {}", - file.name, - file.resource_type.as_ref().unwrap() - ); - } - if let Some(ref res_type) = file.resource_type { - match res_type { - crate::states::ResourceType::Model => { - let ad_dir = app_dirs2::get_app_root( - app_dirs2::AppDataType::UserData, - &APP_INFO, - ) - .unwrap(); - - let model_thumbnail = - ad_dir.join(format!("{}.png", file.name)); - - if !model_thumbnail.exists() { - // gen image - log::debug!( - "Model thumbnail [{}] does not exist, generating one now", - file.name - ); - let mut model = match model_to_image::ModelToImageBuilder::new(&file.path) - .with_size((600, 600)) - .build() { - Ok(v) => v, - Err(e) => panic!("Error occurred while loading file from path: {}", e), - }; - if let Err(e) = - model.render().unwrap().write_to(Some( - &ad_dir - .join(format!("{}.png", file.name)), - )) - { - log::error!( - "Failed to write model thumbnail for {}: {}", - file.name, - e - ); - } - } - - let image_uri = - model_thumbnail.to_string_lossy().to_string(); - - assets.push(( - format!("file://{}", image_uri), - file.name.clone(), - file.path.clone(), - res_type.clone(), - )) - } - ResourceType::Texture => assets.push(( - format!( - "file://{}", - file.path.to_string_lossy().to_string() - ), - file.name.clone(), - file.path.clone(), - res_type.clone(), - )), - _ => { - if file - .path - .clone() - .extension() - .unwrap() - .to_str() - .unwrap() - .contains("euc") - { - continue; - } - assets.push(( - "NO_TEXTURE".into(), - file.name.clone(), - file.path.clone(), - res_type.clone(), - )) - } - } - } - } - Node::Folder(folder) => { - recursive_search_nodes_and_attach_thumbnail( - &folder.nodes, - assets, - logged, - ) - } - } - } - } - - let mut logged = LOGGED.lock(); - recursive_search_nodes_and_attach_thumbnail( - &res.nodes, - &mut assets, - &mut logged, - ); - } - - egui::ScrollArea::vertical().show(ui, |ui| { - let max_columns = 6; - let available_width = ui.clip_rect().width() - ui.spacing().indent; - let margin = 16.0; - let usable_width = available_width - margin; - let label_height = 20.0; - let padding = 8.0; - let min_card_width = 60.0; - - let mut columns = max_columns; - for test_columns in (1..=max_columns).rev() { - let card_width = usable_width / test_columns as f32; - if card_width >= min_card_width { - columns = test_columns; - break; - } - } - - let card_width = usable_width / columns as f32; - let image_size = (card_width - label_height - padding).max(32.0); - let card_height = image_size + label_height + padding; - - for row_start in (0..assets.len()).step_by(columns) { - let row_end = (row_start + columns).min(assets.len()); - let row_items = &mut assets[row_start..row_end]; - - ui.horizontal(|ui| { - ui.set_max_width(usable_width); - - egui_dnd::dnd(ui, format!("asset_row_{}", row_start / columns)) - .show_vec( - row_items, - |ui, (image, asset_name, asset_path, asset_type), handle, state| { - let card_size = egui::vec2(card_width, card_height); - handle.ui(ui, |ui| { - let (rect, card_response) = ui.allocate_exact_size( - card_size, - egui::Sense::click(), - ); - - let mut card_ui = ui.new_child( - egui::UiBuilder::new().max_rect(rect).layout( - egui::Layout::top_down(egui::Align::Center), - ), - ); - - let image_response = card_ui.add_sized( - [image_size, image_size], - egui::ImageButton::new(image.clone()).frame(false), - ); - - let is_hovered = card_response.hovered() || image_response.hovered() || state.dragged; - let is_d_clicked = card_response.double_clicked() || image_response.double_clicked(); - - if is_d_clicked { - if matches!(asset_type, ResourceType::Model) { - let mut spawn_position = DVec3::default(); - if let Some(active_camera) = self.active_camera { - if let Ok(mut q) = self.world.query_one::<(&Camera, &CameraComponent, Option<&CameraFollowTarget>)>(*active_camera) { - if let Some((camera, _, _)) = q.get() { - spawn_position = camera.eye; - } else { - log_once::warn_once!("Unable to fetch the query result of camera: {:?}", active_camera) - } - } else { - log_once::warn_once!("Unable to query camera, component and option<camerafollowtarget> for active camera: {:?}", active_camera); - } - } else { - log_once::warn_once!("No active camera found"); - } - - let asset = DraggedAsset { - name: asset_name.clone(), - path: asset_path.clone(), - // asset_type: asset_type.clone(), - }; - - match self.spawn_entity_at_pos(&asset, spawn_position, None) { - Ok(()) => { - log::debug!("double click spawned {} at camera pos {:?}", - asset.name, spawn_position - ); - - crate::success!("Spawned {} at camera", asset.name); - } - Err(e) => { - log::error!( - "Failed to spawn {} at camera: {}", - asset.name, - e); - - crate::fatal!("Failed to spawn {}: {}", - asset.name, e); - } - } - } - } - - if is_hovered || state.dragged { - ui.painter().rect_filled( - rect, - 6.0, - if state.dragged { - egui::Color32::from_rgb(80, 80, 100) - } else { - egui::Color32::from_rgb(60, 60, 80) - }, - ); - } - - card_ui.vertical_centered(|ui| { - ui.label( - egui::RichText::new(asset_name.clone()) - .strong() - .color(egui::Color32::WHITE), - ); - }); - }); - }, - ); - }); - ui.add_space(8.0); - } - }); - } - EditorTab::ResourceInspector => { - if let Some(entity) = self.selected_entity { - if let Ok((e, transform, _props, script, camera, camera_component, follow_target)) = self - .world - .query_one_mut::<(&mut AdoptedEntity, Option<&mut Transform>, Option<&ModelProperties>, Option<&mut ScriptComponent>, Option<&mut Camera>, Option<&mut CameraComponent>, Option<&mut CameraFollowTarget>)>(*entity) { - e.inspect(entity, &mut cfg, ui, self.undo_stack, self.signal, &mut String::new()); - if let Some(t) = transform { - t.inspect(entity, &mut cfg, ui, self.undo_stack, self.signal, e.label_mut()); - } - - if let (Some(camera), Some(camera_component)) = (camera, camera_component) { - ui.separator(); - ui.label("Camera Components:"); - camera.inspect(entity, &mut cfg, ui, self.undo_stack, self.signal, &mut String::new()); - camera_component.inspect(entity, &mut cfg, ui, self.undo_stack, self.signal, &mut camera.label.clone()); - - if let Some(target) = follow_target { - target.inspect(entity, &mut cfg, ui, self.undo_stack, self.signal, &mut camera.label.clone()); - } - } - - // if let Some(props) = _props { - // props.inspect(entity, &mut cfg, ui, self.undo_stack, self.signal, e.label_mut()); - // } - - if let Some(script) = script { - script.inspect(entity, &mut cfg, ui, self.undo_stack, self.signal, e.label_mut()); - } - - if let Some(t) = cfg.label_last_edit { - if t.elapsed() >= Duration::from_millis(500) { - if let Some(ent) = cfg.old_label_entity.take() { - if let Some(orig) = cfg.label_original.take() { - UndoableAction::push_to_undo( - &mut self.undo_stack, - UndoableAction::Label(ent, orig, EntityType::Entity), - ); - log::debug!("Pushed label change to undo stack after 500ms debounce period"); - } - } - cfg.label_last_edit = None; - } - } - } else { - log_once::debug_once!("Unable to query entity inside resource inspector"); - } - - if let Ok((light, transform, props)) = self.world.query_one_mut::<(&mut Light, &mut Transform, &mut LightComponent)>(*entity) { - light.inspect(entity, &mut cfg, ui, self.undo_stack, self.signal, &mut String::new()); - transform.inspect(entity, &mut cfg, ui, self.undo_stack, self.signal, &mut light.label); - props.inspect(entity, &mut cfg, ui, self.undo_stack, self.signal, &mut light.label); - if let Some(t) = cfg.label_last_edit { - if t.elapsed() >= Duration::from_millis(500) { - if let Some(ent) = cfg.old_label_entity.take() { - if let Some(orig) = cfg.label_original.take() { - UndoableAction::push_to_undo( - &mut self.undo_stack, - UndoableAction::Label(ent, orig, EntityType::Light), - ); - log::debug!("Pushed label change to undo stack after 500ms debounce period"); - } - } - cfg.label_last_edit = None; - } - } - } else { - log_once::debug_once!("Unable to query light inside resource inspector"); - } - - if let Ok((camera, camera_component, follow_target)) = self.world.query_one_mut::<(&mut Camera, &mut CameraComponent, Option<&mut CameraFollowTarget>)>(*entity) { - camera.inspect(entity, &mut cfg, ui, self.undo_stack, self.signal, &mut String::new()); - camera_component.inspect(entity, &mut cfg, ui, self.undo_stack, self.signal, &mut camera.label.clone()); - if let Some(target) = follow_target { - target.inspect(entity, &mut cfg, ui, self.undo_stack, self.signal, &mut camera.label.clone()); - } - - ui.separator(); - ui.label("Camera Controls:"); - if ui.button("Set as Active Camera").clicked() { - *self.active_camera = Some(*entity); - log::info!("Set camera '{}' as active", camera.label); - } - - if matches!(self.editor_mode, EditorState::Editing) { - if ui.button("Switch to This Camera").clicked() { - *self.active_camera = Some(*entity); - log::info!("Switched to camera '{}'", camera.label); - } - } - } - } else { - ui.label("No entity selected, therefore no info to provide. Go on, what are you waiting for? Click an entity!"); - } - } - } - - let mut menu_action: Option<EditorTabMenuAction> = None; - let area = egui::Area::new("context_menu".into()) - .fixed_pos(cfg.context_menu_pos) - .order(egui::Order::Foreground); - - if cfg.show_context_menu { - let menu_tab = cfg - .context_menu_tab - .clone() - .unwrap_or(EditorTab::ModelEntityList); - - let mut popup_rect = None; - - area.show(ui.ctx(), |ui| { - egui::Frame::popup(ui.style()).show(ui, |ui| { - popup_rect.replace(ui.max_rect()); - - match menu_tab { - EditorTab::AssetViewer => { - ui.set_min_width(150.0); - if ui.selectable_label(false, "Import resource").clicked() { - menu_action = Some(EditorTabMenuAction::ImportResource); - } - if ui.selectable_label(false, "Refresh assets").clicked() { - menu_action = Some(EditorTabMenuAction::RefreshAssets); - } - } - EditorTab::ModelEntityList => { - ui.set_min_width(150.0); - if ui.selectable_label(false, "Add Entity").clicked() { - menu_action = Some(EditorTabMenuAction::AddEntity); - } - if ui.selectable_label(false, "Delete Entity").clicked() { - menu_action = Some(EditorTabMenuAction::DeleteEntity); - } - } - EditorTab::ResourceInspector => { - ui.set_min_width(150.0); - if ui.selectable_label(false, "Add Component").clicked() { - menu_action = Some(EditorTabMenuAction::AddComponent); - } - } - EditorTab::Viewport => { - ui.set_min_width(150.0); - if ui.selectable_label(false, "Viewport Option").clicked() { - menu_action = Some(EditorTabMenuAction::ViewportOption); - } - } - } - }) - }); - - if let Some(action) = menu_action { - if Some(tab.clone()) == cfg.context_menu_tab { - match action { - EditorTabMenuAction::ImportResource => { - log::debug!("Import Resource clicked"); - - match import_object() { - Ok(_) => { - crate::success!("Resource(s) imported successfully!"); - } - Err(e) => { - crate::warn!("Failed to import resource(s): {e}"); - } - } - cfg.show_context_menu = false; - cfg.context_menu_tab = None; - return; - } - EditorTabMenuAction::RefreshAssets => { - log::debug!("Refresh assets clicked"); - if let Ok(mut res) = RESOURCES.write() { - match res.update_mem() { - Ok(res_cfg) => { - *res = res_cfg; - crate::success!("Assets refreshed successfully!"); - } - Err(e) => { - crate::fatal!("Failed to refresh assets: {}", e); - } - } - } - cfg.show_context_menu = false; - cfg.context_menu_tab = None; - return; - } - EditorTabMenuAction::AddEntity => { - log::debug!("Add Entity clicked"); - *self.signal = Signal::CreateEntity; - cfg.show_context_menu = false; - cfg.context_menu_tab = None; - return; - } - EditorTabMenuAction::DeleteEntity => { - log::debug!("Delete Entity clicked"); - *self.signal = Signal::Delete; - cfg.show_context_menu = false; - cfg.context_menu_tab = None; - return; - } - EditorTabMenuAction::AddComponent => { - log::debug!("Add Component clicked"); - if let Some(entity) = self.selected_entity { - if let Ok(..) = self.world.query_one_mut::<&AdoptedEntity>(*entity) { - log::debug!("Queried selected entity, it is an entity"); - *self.signal = Signal::AddComponent(*entity, EntityType::Entity); - } - - if let Ok(..) = self.world.query_one_mut::<&Light>(*entity) { - log::debug!("Queried selected entity, it is a light"); - *self.signal = Signal::AddComponent(*entity, EntityType::Light); - } - } else { - crate::warn!("What are you adding a component to? Theres no entity selected..."); - } - - cfg.show_context_menu = false; - cfg.context_menu_tab = None; - return; - } - EditorTabMenuAction::ViewportOption => { - log::debug!("Viewport Option clicked"); - cfg.show_context_menu = false; - cfg.context_menu_tab = None; - return; - }, - EditorTabMenuAction::RemoveComponent => { - log::debug!("Remove Component clicked"); - if let Some(entity) = self.selected_entity { - if let Ok(script) = self.world.query_one_mut::<&ScriptComponent>(*entity) { - log::debug!("Queried selected entity, it has a script component"); - *self.signal = Signal::RemoveComponent(*entity, ComponentType::Script(script.clone())); - } else { - crate::warn!("Selected entity does not have a script component to remove"); - } - } else { - panic!("Paradoxical error: Cannot remove a component when its not selected..."); - } - - cfg.show_context_menu = false; - cfg.context_menu_tab = None; - return; - } - } - } - } - - if let Some(rect) = popup_rect { - if cfg.show_context_menu && Some(tab.clone()) == cfg.context_menu_tab { - if ui - .ctx() - .input(|i| i.pointer.button_clicked(egui::PointerButton::Primary)) - { - if let Some(pos) = ui.ctx().input(|i| i.pointer.interact_pos()) { - if !rect.contains(pos) { - cfg.show_context_menu = false; - cfg.context_menu_tab = None; - } - } - } - } - } - } - } -} - - -pub(crate) fn import_object() -> anyhow::Result<()> { - let model_ext = vec!["glb", "fbx", "obj"]; - let texture_ext = vec!["png"]; - - let files = rfd::FileDialog::new() - .add_filter("All Files", &["*"]) - .add_filter("Model", &model_ext) - .add_filter("Texture", &texture_ext) - .pick_files(); - if let Some(files) = files { - for file in files { - let ext = file.extension().unwrap().to_str().unwrap(); - let mut copied = false; - for mde in model_ext.iter() { - if ext.contains(mde) { - // copy over to models folder - if let Some(project) = crate::states::PROJECT.read().ok() { - let models_dir = PathBuf::from(project.project_path.clone()) - .join("resources") - .join("models"); - if !models_dir.exists() { - std::fs::create_dir_all(&models_dir)?; - } - let dest = models_dir.join(file.file_name().unwrap()); - std::fs::copy(&file, &dest)?; - log::info!("Copied model file to {:?}", dest); - copied = true; - } - } - } - for tex in texture_ext.iter() { - if ext.contains(tex) { - // copy over to textures folder - if let Some(project) = crate::states::PROJECT.read().ok() { - let textures_dir = PathBuf::from(project.project_path.clone()) - .join("resources") - .join("textures"); - if !textures_dir.exists() { - std::fs::create_dir_all(&textures_dir)?; - } - let dest = textures_dir.join(file.file_name().unwrap()); - std::fs::copy(&file, &dest)?; - log::info!("Copied texture file to {:?}", dest); - copied = true; - } - } - } - - if !copied { - if let Some(project) = crate::states::PROJECT.read().ok() { - // everything else copies over to resources root dir - let resources_dir = - PathBuf::from(project.project_path.clone()).join("resources"); - if !resources_dir.exists() { - std::fs::create_dir_all(&resources_dir)?; - } - let dest = resources_dir.join(file.file_name().unwrap()); - std::fs::copy(&file, &dest)?; - log::info!("Copied other resource file to {:?}", dest); - } - } - } - // save it all to ensure the eucc recognises it - let mut proj = PROJECT.write().unwrap(); - proj.write_to_all()?; - Ok(()) - } else { - return Err(anyhow::anyhow!("File dialogue returned None")); - } -} - -#[derive(Debug, Clone, Copy)] -pub enum EditorTabMenuAction { - ImportResource, - RefreshAssets, - AddEntity, - DeleteEntity, - AddComponent, - RemoveComponent, - ViewportOption, -} @@ -1,286 +0,0 @@ -use super::*; -use dropbear_engine::{ - entity::{AdoptedEntity, Transform}, - input::{Controller, Keyboard, Mouse}, -}; -use gilrs::{Button, GamepadId}; -use log; -use transform_gizmo_egui::GizmoMode; -use winit::{ - dpi::PhysicalPosition, event::MouseButton, event_loop::ActiveEventLoop, keyboard::KeyCode, -}; - -impl Keyboard for Editor { - fn key_down(&mut self, key: KeyCode, _event_loop: &ActiveEventLoop) { - #[cfg(not(target_os = "macos"))] - let ctrl_pressed = self.input_state.pressed_keys.contains(&KeyCode::ControlLeft) - || self.input_state.pressed_keys.contains(&KeyCode::ControlRight); - #[cfg(target_os = "macos")] - let ctrl_pressed = self.input_state.pressed_keys.contains(&KeyCode::SuperLeft) - || self.input_state.pressed_keys.contains(&KeyCode::SuperRight); - - let _alt_pressed = self.input_state.pressed_keys.contains(&KeyCode::AltLeft) - || self.input_state.pressed_keys.contains(&KeyCode::AltRight); - - let shift_pressed = self.input_state.pressed_keys.contains(&KeyCode::ShiftLeft) - || self.input_state.pressed_keys.contains(&KeyCode::ShiftRight); - - let is_double_press = self.double_key_pressed(key); - - let is_playing = matches!(self.editor_state, EditorState::Playing); - - match key { - KeyCode::KeyG => { - if self.is_viewport_focused && !is_playing { - self.viewport_mode = crate::utils::ViewportMode::Gizmo; - crate::info!("Switched to Viewport::Gizmo"); - - if let Some(window) = &self.window { - window.set_cursor_visible(true); - } - } else { - self.input_state.pressed_keys.insert(key); - } - } - KeyCode::KeyF => { - if self.is_viewport_focused && !is_playing { - self.viewport_mode = crate::utils::ViewportMode::CameraMove; - crate::info!("Switched to Viewport::CameraMove"); - if let Some(window) = &self.window { - window.set_cursor_visible(false); - - let size = window.inner_size(); - let center = winit::dpi::PhysicalPosition::new( - size.width as f64 / 2.0, - size.height as f64 / 2.0, - ); - let _ = window.set_cursor_position(center); - } - } else { - self.input_state.pressed_keys.insert(key); - } - } - KeyCode::Delete => { - if !is_playing { - if let Some(_) = &self.selected_entity { - self.signal = Signal::Delete; - } else { - crate::warn!("Failed to delete: No entity selected"); - } - } else { - self.input_state.pressed_keys.insert(key); - } - } - KeyCode::Escape => { - if is_double_press { - if let Some(_) = &self.selected_entity { - self.selected_entity = None; - log::debug!("Deselected entity"); - } - } else if self.is_viewport_focused && !is_playing { - self.viewport_mode = crate::utils::ViewportMode::None; - crate::info!("Switched to Viewport::None"); - if let Some(window) = &self.window { - window.set_cursor_visible(true); - } - } else { - self.input_state.pressed_keys.insert(key); - } - } - KeyCode::KeyQ => { - if ctrl_pressed && !is_playing { - match self.save_project_config() { - Ok(_) => {} - Err(e) => { - crate::fatal!("Error saving project: {}", e); - } - } - log::info!("Successfully saved project, about to quit..."); - crate::success_without_console!("Successfully saved project"); - self.scene_command = SceneCommand::Quit; - } else if is_playing { - crate::warn!("Unable to save-quit project, please pause your playing state, then try again"); - } - } - KeyCode::KeyC => { - if ctrl_pressed && !is_playing { - if let Some(entity) = &self.selected_entity { - let query = self - .world - .query_one::<(&AdoptedEntity, &Transform, &ModelProperties)>(*entity); - if let Ok(mut q) = query { - if let Some((e, t, props)) = q.get() { - let s_entity = SceneEntity { - model_path: e.model().path.clone(), - label: e.model().label.clone(), - transform: *t, - properties: props.clone(), - script: None, - entity_id: None, - }; - self.signal = Signal::Copy(s_entity); - - crate::info!("Copied!"); - - log::debug!("Copied selected entity"); - } else { - crate::warn!( - "Unable to copy entity: Unable to fetch world entity properties" - ); - } - } else { - crate::warn!("Unable to copy entity: Unable to obtain lock"); - } - } else { - crate::warn!("Unable to copy entity: None selected"); - } - } else if matches!(self.viewport_mode, ViewportMode::Gizmo) { - crate::info!("GizmoMode set to scale"); - self.gizmo_mode = GizmoMode::all_scale(); - } else { - self.input_state.pressed_keys.insert(key); - } - } - KeyCode::KeyV => { - if ctrl_pressed && !is_playing { - match &self.signal { - Signal::Copy(entity) => { - self.signal = Signal::Paste(entity.clone()); - } - _ => { - crate::warn!("Unable to paste: You haven't selected anything!"); - } - } - } - else { - self.input_state.pressed_keys.insert(key); - } - } - KeyCode::KeyS => { - if ctrl_pressed { - if !is_playing { - match self.save_project_config() { - Ok(_) => { - crate::success!("Successfully saved project"); - } - Err(e) => { - crate::fatal!("Error saving project: {}", e); - } - } - } else { - crate::warn!("Unable to save project config, please quit your playing and try again"); - } - - } else { - self.input_state.pressed_keys.insert(key); - } - } - KeyCode::KeyZ => { - if ctrl_pressed && !is_playing { - if shift_pressed { - // redo - } else { - // undo - log::debug!("Undo signal sent"); - self.signal = Signal::Undo; - } - } else if matches!(self.viewport_mode, ViewportMode::Gizmo) && !is_playing { - crate::info!("GizmoMode set to translate"); - self.gizmo_mode = GizmoMode::all_translate(); - } else { - self.input_state.pressed_keys.insert(key); - } - } - KeyCode::F1 => { - if !is_playing { - if self.is_using_debug_camera() { - self.switch_to_player_camera(); - } else { - self.switch_to_debug_camera(); - } - } - } - KeyCode::KeyX => { - if matches!(self.viewport_mode, ViewportMode::Gizmo) && !is_playing { - crate::info!("GizmoMode set to rotate"); - self.gizmo_mode = GizmoMode::all_rotate(); - } else { - self.input_state.pressed_keys.insert(key); - } - } - KeyCode::KeyP => { - if !is_playing { - if ctrl_pressed { - self.signal = Signal::Play - } - } - } - _ => { - self.input_state.pressed_keys.insert(key); - } - } - } - - fn key_up(&mut self, key: KeyCode, _event_loop: &ActiveEventLoop) { - self.input_state.pressed_keys.remove(&key); - } -} - -impl Mouse for Editor { - fn mouse_move(&mut self, position: PhysicalPosition<f64>) { - if (self.is_viewport_focused - && matches!(self.viewport_mode, crate::utils::ViewportMode::CameraMove)) - || (matches!(self.editor_state, EditorState::Playing) && !self.input_state.is_cursor_locked) - { - if let Some(window) = &self.window { - let size = window.inner_size(); - let center = - PhysicalPosition::new(size.width as f64 / 2.0, size.height as f64 / 2.0); - - let dx = position.x - center.x; - let dy = position.y - center.y; - if let Some(active_camera) = self.active_camera { - if let Ok(mut q) = self.world.query_one::<(&mut Camera, &CameraComponent, Option<&CameraFollowTarget>)>(active_camera) { - if let Some((camera, _, _)) = q.get() { - camera.track_mouse_delta(dx, dy); - } else { - log_once::warn_once!("Unable to fetch the query result of camera: {:?}", active_camera) - } - } else { - log_once::warn_once!("Unable to query camera, component and option<camerafollowtarget> for active camera: {:?}", active_camera); - } - } else { - log_once::warn_once!("No active camera found"); - } - - let _ = window.set_cursor_position(center); - window.set_cursor_visible(false); - } - } - self.input_state.mouse_pos = (position.x, position.y); - } - - fn mouse_down(&mut self, button: MouseButton) { - match button { - _ => { self.input_state.mouse_button.insert(button); } - } - } - - fn mouse_up(&mut self, button: MouseButton) { - self.input_state.mouse_button.remove(&button); - } -} - -impl Controller for Editor { - fn button_down(&mut self, _button: Button, _id: GamepadId) {} - - fn button_up(&mut self, _button: Button, _id: GamepadId) {} - - fn left_stick_changed(&mut self, _x: f32, _y: f32, _id: GamepadId) {} - - fn right_stick_changed(&mut self, _x: f32, _y: f32, _id: GamepadId) {} - - fn on_connect(&mut self, _id: GamepadId) {} - - fn on_disconnect(&mut self, _id: GamepadId) {} -} @@ -1,886 +0,0 @@ -pub mod dock; -pub mod input; -pub mod scene; -pub mod component; - -pub(crate) use crate::editor::dock::*; - -use std::{ - collections::{HashMap, HashSet}, - path::PathBuf, - sync::{Arc, LazyLock}, - time::{Duration, Instant}, -}; - -use dropbear_engine::{ - camera::Camera, entity::{AdoptedEntity, Transform}, graphics::Graphics, lighting::{Light, LightManager}, scene::SceneCommand -}; -use egui::{self, Context}; -use egui_dock_fork::{DockArea, DockState, NodeIndex, Style}; -use hecs::{World}; -use log; -use parking_lot::Mutex; -use transform_gizmo_egui::{EnumSet, Gizmo, GizmoMode}; -use wgpu::{Color, Extent3d, RenderPipeline}; -use winit::{keyboard::KeyCode, window::Window}; - -use crate::{build::build, camera::{ - CameraAction, CameraComponent, CameraFollowTarget, CameraType, DebugCamera, UndoableCameraAction -}, debug, scripting::{input::InputState, ScriptAction, ScriptManager}, states::{CameraConfig, EntityNode, LightConfig, ModelProperties, SceneEntity, ScriptComponent, PROJECT, SCENES}, utils::ViewportMode}; - -pub struct Editor { - scene_command: SceneCommand, - world: World, - dock_state: DockState<EditorTab>, - texture_id: Option<egui::TextureId>, - size: Extent3d, - render_pipeline: Option<RenderPipeline>, - light_manager: LightManager, - color: Color, - - active_camera: Option<hecs::Entity>, - - is_viewport_focused: bool, - // is_cursor_locked: bool, - window: Option<Arc<Window>>, - - show_new_project: bool, - project_name: String, - project_path: Option<PathBuf>, - pending_scene_switch: bool, - - gizmo: Gizmo, - selected_entity: Option<hecs::Entity>, - viewport_mode: ViewportMode, - - signal: Signal, - undo_stack: Vec<UndoableAction>, - // todo: add redo (later) - // redo_stack: Vec<UndoableAction>, - - editor_state: EditorState, - gizmo_mode: EnumSet<GizmoMode>, - - script_manager: ScriptManager, - play_mode_backup: Option<PlayModeBackup>, - - input_state: InputState, -} - -impl Editor { - pub fn new() -> Self { - let tabs = vec![EditorTab::Viewport]; - let mut dock_state = DockState::new(tabs); - - let surface = dock_state.main_surface_mut(); - let [_old, right] = - surface.split_right(NodeIndex::root(), 0.25, vec![EditorTab::ModelEntityList]); - let [_old, _] = - surface.split_left(NodeIndex::root(), 0.20, vec![EditorTab::ResourceInspector]); - let [_old, _] = surface.split_below(right, 0.5, vec![EditorTab::AssetViewer]); - - // this shit doesnt work :( - // nvm it works (sorta) - std::thread::spawn(move || { - loop { - std::thread::sleep(Duration::from_secs(1)); - let deadlocks = parking_lot::deadlock::check_deadlock(); - if deadlocks.is_empty() { - continue; - } - - log::error!("{} deadlocks detected", deadlocks.len()); - for (i, threads) in deadlocks.iter().enumerate() { - log::error!("Deadlock #{}", i); - for t in threads { - log::error!("Thread Id {:#?}", t.thread_id()); - log::error!("{:#?}", t.backtrace()); - } - } - } - }); - - Self { - scene_command: SceneCommand::None, - dock_state, - texture_id: None, - size: Extent3d::default(), - render_pipeline: None, - color: Color::default(), - is_viewport_focused: false, - // is_cursor_locked: false, - window: None, - world: World::new(), - show_new_project: false, - project_name: String::new(), - project_path: None, - pending_scene_switch: false, - gizmo: Gizmo::default(), - selected_entity: None, - viewport_mode: ViewportMode::None, - signal: Signal::None, - undo_stack: Vec::new(), - script_manager: ScriptManager::new().unwrap(), - editor_state: EditorState::Editing, - gizmo_mode: EnumSet::empty(), - play_mode_backup: None, - input_state: InputState::new(), - light_manager: LightManager::new(), - active_camera: None, - // ..Default::default() - // note to self: DO NOT USE ..DEFAULT::DEFAULT(), IT WILL CAUSE OVERFLOW - } - } - - fn double_key_pressed(&mut self, key: KeyCode) -> bool { - let now = Instant::now(); - - if let Some(last_time) = self.input_state.last_key_press_times.get(&key) { - let time_diff = now.duration_since(*last_time); - - if time_diff <= self.input_state.double_press_threshold { - self.input_state.last_key_press_times.remove(&key); - return true; - } - } - - self.input_state.last_key_press_times.insert(key, now); - false - } - - /// Save the current world state to the active scene - pub fn save_current_scene(&mut self) -> anyhow::Result<()> { - let mut scenes = SCENES.write().unwrap(); - - let scene_index = if scenes.is_empty() { - return Err(anyhow::anyhow!("No scenes loaded to save")); - } else { - 0 - }; - - let scene = &mut scenes[scene_index]; - scene.entities.clear(); - scene.lights.clear(); - scene.cameras.clear(); - - for (id, (adopted, transform, properties, script)) in self - .world - .query::<( - &AdoptedEntity, - Option<&Transform>, - &ModelProperties, - Option<&ScriptComponent>, - )>() - .iter() - { - let transform = transform.unwrap_or(&Transform::default()).clone(); - - let scene_entity = SceneEntity { - model_path: adopted.model().path.clone(), - label: adopted.model().label.clone(), - transform, - properties: properties.clone(), - script: script.cloned(), - entity_id: Some(id), - }; - - scene.entities.push(scene_entity); - log::debug!("Pushed entity: {}", adopted.label()); - } - - for (id, (light_component, transform, light)) in self - .world - .query::<( - &dropbear_engine::lighting::LightComponent, - &Transform, - &Light, - )>() - .iter() - { - let light_config = LightConfig { - label: light.label().to_string(), - transform: *transform, - light_component: light_component.clone(), - enabled: light_component.enabled, - entity_id: Some(id), - }; - - scene.lights.push(light_config); - log::debug!("Pushed light into lights: {}", light.label()); - } - - for (_id, (camera, component, follow_target)) in self - .world - .query::<(&Camera, &CameraComponent, Option<&CameraFollowTarget>)>() - .iter() - { - let camera_config = CameraConfig::from_ecs_camera(camera, component, follow_target); - scene.cameras.push(camera_config); - log::debug!("Pushed camera into cameras: {}", camera.label); - } - - - log::info!( - "Saved {} entities and camera configs to scene '{}'", - scene.entities.len(), - scene.scene_name - ); - - Ok(()) - } - - pub fn save_project_config(&mut self) -> anyhow::Result<()> { - self.save_current_scene()?; - - { - let mut config = PROJECT.write().unwrap(); - config.dock_layout = Some(self.dock_state.clone()); - } - - { - let (scene_clone, project_path) = { - let scenes = SCENES.read().unwrap(); - let project = PROJECT.read().unwrap(); - (scenes[0].clone(), project.project_path.clone()) - }; - - scene_clone.write_to(&project_path)?; - - let mut config = PROJECT.write().unwrap(); - config.write_to_all()?; - } - - Ok(()) - } - - pub fn load_project_config(&mut self, graphics: &Graphics) -> anyhow::Result<()> { - let config = PROJECT.read().unwrap(); - - self.project_path = Some(config.project_path.clone()); - - if let Some(layout) = &config.dock_layout { - self.dock_state = layout.clone(); - } - - { - let scenes = SCENES.read().unwrap(); - if let Some(first_scene) = scenes.first() { - self.active_camera = Some(first_scene.load_into_world(&mut self.world, graphics)?); - - log::info!( - "Successfully loaded scene with {} entities and {} camera configs", - first_scene.entities.len(), - first_scene.cameras.len(), - ); - } else { - let existing_debug_camera = self.world - .query::<(&Camera, &CameraComponent)>() - .iter() - .find_map(|(entity, (_, component))| { - if matches!(component.camera_type, CameraType::Debug) { - Some(entity) - } else { - None - } - }); - - if let Some(camera_entity) = existing_debug_camera { - log::info!("Using existing debug camera"); - self.active_camera = Some(camera_entity); - } else { - log::info!("No scenes found, creating default debug camera"); - - let debug_camera = Camera::predetermined(graphics, Some("Debug Camera")); - let component = DebugCamera::new(); - - let e = self.world.spawn((debug_camera, component)); - self.active_camera = Some(e); - } - } - } - - Ok(()) - } - - pub fn show_ui(&mut self, ctx: &Context) { - egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| { - egui::MenuBar::new().ui(ui, |ui| { - ui.menu_button("File", |ui| { - if ui - .button("Main Menu (New + Open + Editor Settings)") - .clicked() - { - self.scene_command = SceneCommand::SwitchScene("main_menu".into()); - } - - if ui.button("Save").clicked() { - match self.save_project_config() { - Ok(_) => {} - Err(e) => { - crate::fatal!("Error saving project: {}", e); - } - } - crate::success!("Successfully saved project"); - } - if ui.button("Project Settings").clicked() {}; - if matches!(self.editor_state, EditorState::Playing) { - if ui.button("Stop").clicked() { - self.signal = Signal::StopPlaying; - } - } else { - if ui.button("Play").clicked() { - self.signal = Signal::Play; - } - } - ui.menu_button("Export", |ui| { - // todo: create a window for better build menu - if ui.button("Build").clicked() { - { - if let Ok(proj) = PROJECT.read() { - match build(proj.project_path.join(format!("{}.eucp", proj.project_name.clone())).clone()) { - Ok(thingy) => crate::success!("Project output at {}", thingy.display()), - Err(e) => { - crate::fatal!("Unable to build project [{}]: {}", proj.project_path.clone().display(), e); - }, - } - } - } - } - ui.label("Package"); // todo: create a window for label - }); - ui.separator(); - if ui.button("Quit").clicked() { - match self.save_project_config() { - Ok(_) => {} - Err(e) => { - crate::fatal!("Error saving project: {}", e); - } - } - crate::success!("Successfully saved project"); - } - }); - ui.menu_button("Edit", |ui| { - if ui.button("Copy").clicked() { - if let Some(entity) = &self.selected_entity { - let query = self.world.query_one::<(&AdoptedEntity, &Transform, &ModelProperties)>(*entity); - if let Ok(mut q) = query { - if let Some((e, t, props)) = q.get() { - let s_entity = crate::states::SceneEntity { - model_path: e.model().path.clone(), - label: e.model().label.clone(), - transform: *t, - properties: props.clone(), - script: None, - entity_id: None, - }; - self.signal = Signal::Copy(s_entity); - - crate::info!("Copied selected entity!"); - } else { - crate::warn!("Unable to copy entity: Unable to fetch world entity properties"); - } - } else { - crate::warn!("Unable to copy entity: Unable to obtain lock"); - } - } else { - crate::warn!("Unable to copy entity: None selected"); - } - } - - if ui.button("Paste").clicked() { - match &self.signal { - Signal::Copy(entity) => { - self.signal = Signal::Paste(entity.clone()); - } - _ => { - crate::warn!("Unable to paste: You haven't selected anything!"); - } - } - } - - if ui.button("Undo").clicked() { - self.signal = Signal::Undo; - } - ui.label("Redo"); - }); - - ui.menu_button("Window", |ui_window| { - if ui_window.button("Open Asset Viewer").clicked() { - self.dock_state.push_to_focused_leaf(EditorTab::AssetViewer); - } - if ui_window.button("Open Resource Inspector").clicked() { - self.dock_state - .push_to_focused_leaf(EditorTab::ResourceInspector); - } - if ui_window.button("Open Entity List").clicked() { - self.dock_state - .push_to_focused_leaf(EditorTab::ModelEntityList); - } - if ui_window.button("Open Viewport").clicked() { - self.dock_state.push_to_focused_leaf(EditorTab::Viewport); - } - }); - if let Ok(cfg) = PROJECT.read() { - if cfg.editor_settings.is_debug_menu_shown { - debug::show_menu_bar(ui, &mut self.signal); - } - } - }); - }); - - egui::CentralPanel::default().show(&ctx, |ui| { - DockArea::new(&mut self.dock_state) - .style(Style::from_egui(ui.style().as_ref())) - .show_inside( - ui, - &mut EditorTabViewer { - view: self.texture_id.unwrap(), - nodes: EntityNode::from_world(&self.world), - gizmo: &mut self.gizmo, - tex_size: self.size, - world: &mut self.world, - selected_entity: &mut self.selected_entity, - viewport_mode: &mut self.viewport_mode, - undo_stack: &mut self.undo_stack, - signal: &mut self.signal, - active_camera: &mut self.active_camera, - gizmo_mode: &mut self.gizmo_mode, - editor_mode: &mut self.editor_state, - }, - ); - }); - - crate::utils::show_new_project_window( - ctx, - &mut self.show_new_project, - &mut self.project_name, - &mut self.project_path, - |name, path| { - crate::utils::start_project_creation(name.to_string(), Some(path.clone())); - self.pending_scene_switch = true; - }, - ); - - if self.pending_scene_switch { - self.scene_command = SceneCommand::SwitchScene("editor".to_string()); - self.pending_scene_switch = false; - } - } - - pub fn switch_to_debug_camera(&mut self) { - let debug_camera = self.world.query::<(&Camera, &CameraComponent)>() - .iter() - .find_map(|(e, (_cam, comp))| { - if matches!(comp.camera_type, CameraType::Debug) { - Some(e) - } else { - None - } - }); - - if let Some(camera_entity) = debug_camera { - self.active_camera = Some(camera_entity); - crate::info!("Switched to debug camera"); - } else { - crate::warn!("No debug camera found in the world"); - } - } - - pub fn switch_to_player_camera(&mut self) { - let player_camera = self.world.query::<(&Camera, &CameraComponent)>() - .iter() - .find_map(|(e, (_cam, comp))| { - if matches!(comp.camera_type, CameraType::Player) { - Some(e) - } else { - None - } - }); - - if let Some(camera_entity) = player_camera { - self.active_camera = Some(camera_entity); - crate::info!("Switched to player camera"); - } else { - crate::warn!("No player camera found in the world"); - } - } - - pub fn is_using_debug_camera(&self) -> bool { - if let Some(active_camera_entity) = self.active_camera { - if let Ok(mut query) = self.world.query_one::<&CameraComponent>(active_camera_entity) { - if let Some(component) = query.get() { - return matches!(component.camera_type, CameraType::Debug); - } - } - } - false - } -} - -pub static LOGGED: LazyLock<Mutex<HashSet<String>>> = LazyLock::new(|| Mutex::new(HashSet::new())); - -fn show_entity_tree( - ui: &mut egui::Ui, - nodes: &mut Vec<EntityNode>, - selected: &mut Option<hecs::Entity>, - id_source: &str, -) { - egui_dnd::Dnd::new(ui, id_source).show_vec(nodes, |ui, item, handle, _dragging| match item - .clone() - { - EntityNode::Entity { id, name } => { - ui.horizontal(|ui| { - handle.ui(ui, |ui| { - ui.label("⏹️"); - }); - let resp = ui.selectable_label(selected.as_ref().eq(&Some(&id)), name); - if resp.clicked() { - *selected = Some(id); - } - }); - } - EntityNode::Script { name, path: _ } => { - ui.horizontal(|ui| { - handle.ui(ui, |ui| { - ui.label("📜"); - }); - ui.label(format!("{name}")); - }); - } - EntityNode::Group { - ref name, - ref mut children, - ref mut collapsed, - } => { - ui.horizontal(|ui| { - handle.ui(ui, |ui| { - let header = egui::CollapsingHeader::new(name) - .default_open(!*collapsed) - .show(ui, |ui| { - show_entity_tree(ui, children, selected, name); - }); - *collapsed = !header.body_returned.is_some(); - }); - }); - } - EntityNode::Light { - id, - name, - } => { - ui.horizontal(|ui| { - handle.ui(ui, |ui| { - ui.label("💡"); - }); - let resp = ui.selectable_label(selected.as_ref().eq(&Some(&id)), name); - if resp.clicked() { - *selected = Some(id); - } - }); - } - EntityNode::Camera { id, name, camera_type } => { - ui.horizontal(|ui| { - handle.ui(ui, |ui| { - let icon = match camera_type { - CameraType::Debug => "🎥", // Debug camera - CameraType::Player => "📹", // Player camera - CameraType::Normal => "📷", // Normal camera - }; - ui.label(icon); - }); - let display_name = format!("{} ({})", name, match camera_type { - CameraType::Debug => "Debug", - CameraType::Player => "Player", - CameraType::Normal => "Normal", - }); - let resp = ui.selectable_label(selected.as_ref().eq(&Some(&id)), display_name); - if resp.clicked() { - *selected = Some(id); - } - }); - } - }); -} - -/// Describes an action that is undoable -#[derive(Debug)] -pub enum UndoableAction { - Transform(hecs::Entity, Transform), - Spawn(hecs::Entity), - Label(hecs::Entity, String, EntityType), - RemoveComponent(hecs::Entity, ComponentType), - CameraAction(UndoableCameraAction), -} -#[derive(Debug)] -pub enum EntityType { - Entity, - Light, - Camera, -} - -impl UndoableAction { - pub fn push_to_undo(undo_stack: &mut Vec<UndoableAction>, action: Self) { - undo_stack.push(action); - // log::debug!("Undo Stack contents: {:#?}", undo_stack); - } - - pub fn undo(&self, world: &mut hecs::World) -> anyhow::Result<()> { - match self { - UndoableAction::Transform(entity, transform) => { - if let Ok(mut q) = world.query_one::<&mut Transform>(*entity) { - if let Some(e_t) = q.get() { - *e_t = *transform; - log::debug!("Reverted transform"); - Ok(()) - } else { - Err(anyhow::anyhow!("Unable to query the entity")) - } - } else { - Err(anyhow::anyhow!("Could not find an entity to query")) - } - } - UndoableAction::Spawn(entity) => { - if world.despawn(*entity).is_ok() { - log::debug!("Undid spawn by despawning entity {:?}", entity); - Ok(()) - } else { - Err(anyhow::anyhow!("Failed to despawn entity {:?}", entity)) - } - } - UndoableAction::Label(entity, original_label, entity_type) => { - match entity_type { - EntityType::Entity => { - if let Ok(mut q) = world.query_one::<&mut AdoptedEntity>(*entity) { - if let Some(adopted) = q.get() { - adopted.model_mut().label = original_label.clone(); - log::debug!("Reverted label for entity {:?} to '{}'", entity, original_label); - Ok(()) - } else { - Err(anyhow::anyhow!("Unable to query the entity for label revert")) - } - } else { - Err(anyhow::anyhow!("Could not find an entity to query for label revert")) - } - }, - EntityType::Light => { - if let Ok(mut q) = world.query_one::<&mut Light>(*entity) { - if let Some(adopted) = q.get() { - adopted.label = original_label.clone(); - log::debug!("Reverted label for light {:?} to '{}'", entity, original_label); - Ok(()) - } else { - Err(anyhow::anyhow!("Unable to query the light for label revert")) - } - } else { - Err(anyhow::anyhow!("Could not find a light to query for label revert")) - } - }, - EntityType::Camera => { - if let Ok(mut q) = world.query_one::<&mut Camera>(*entity) { - if let Some(adopted) = q.get() { - adopted.label = original_label.clone(); - log::debug!("Reverted label for camera {:?} to '{}'", entity, original_label); - Ok(()) - } else { - Err(anyhow::anyhow!("Unable to query the camera for label revert")) - } - } else { - Err(anyhow::anyhow!("Could not find a camera to query for label revert")) - } - } - } - }, - UndoableAction::RemoveComponent(entity, c_type) => { - match c_type { - ComponentType::Script(component) => { - world.insert_one(*entity, component.clone())?; - }, - ComponentType::Camera(camera, component, follow) => { - if let Some(f) = follow { - world.insert(*entity, (camera.clone(), component.clone(), f.clone()))?; - } else { - world.insert(*entity, (camera.clone(), component.clone()))?; - } - } - } - Ok(()) - }, - UndoableAction::CameraAction(action) => { - match action { - UndoableCameraAction::Speed(entity, speed) => { - if let Ok((cam, comp)) = world.query_one_mut::<(&mut Camera, &mut CameraComponent)>(*entity) { - comp.speed = *speed; - comp.update(cam); - } - }, - UndoableCameraAction::Sensitivity(entity, sensitivity) => { - if let Ok((cam, comp)) = world.query_one_mut::<(&mut Camera, &mut CameraComponent)>(*entity) { - comp.sensitivity = *sensitivity; - comp.update(cam); - } - }, - UndoableCameraAction::FOV(entity, fov) => { - if let Ok((cam, comp)) = world.query_one_mut::<(&mut Camera, &mut CameraComponent)>(*entity) { - comp.fov_y = *fov; - comp.update(cam); - } - }, - UndoableCameraAction::Type(entity, camera_type) => { - if let Ok((cam, comp)) = world.query_one_mut::<(&mut Camera, &mut CameraComponent)>(*entity) { - comp.camera_type = *camera_type; - comp.update(cam); - } - }, - }; - Ok(()) - } - } - } -} - -/// This enum will be used to describe the type of command/signal. This is only between -/// the editor and unlike SceneCommand, this will ping a signal everywhere in that scene -pub enum Signal { - None, - Copy(SceneEntity), - Paste(SceneEntity), - Delete, - Undo, - ScriptAction(ScriptAction), - CameraAction(CameraAction), - Play, - StopPlaying, - AddComponent(hecs::Entity, EntityType), - RemoveComponent(hecs::Entity, ComponentType), - CreateEntity, - LogEntities, -} - -impl Default for Editor { - fn default() -> Self { - Editor::new() - } -} - -#[derive(Debug)] -pub enum ComponentType { - Script(ScriptComponent), - Camera(Camera, CameraComponent, Option<CameraFollowTarget>), -} - -#[derive(Clone)] -pub struct PlayModeBackup { - entities: Vec<(hecs::Entity, Transform, ModelProperties, Option<ScriptComponent>)>, - camera_data: Vec<(hecs::Entity, Camera, CameraComponent, Option<CameraFollowTarget>)>, -} - -impl PlayModeBackup { - pub fn create_backup(editor: &mut Editor) -> anyhow::Result<()> { - let mut entities = Vec::new(); - - for (entity_id, (_, transform, properties)) in editor - .world - .query::<(&AdoptedEntity, &Transform, &ModelProperties)>() - .iter() - { - let script = editor.world.query_one::<&ScriptComponent>(entity_id).ok().and_then(|mut s| { - s.get().map(|script| script.clone()) - }); - entities.push((entity_id, *transform, properties.clone(), script)); - } - - let mut camera_data = Vec::new(); - - for (entity_id, (camera, component, follow_target)) in editor - .world - .query::<(&Camera, &CameraComponent, Option<&CameraFollowTarget>)>() - .iter() - { - camera_data.push((entity_id, camera.clone(), component.clone(), follow_target.cloned())); - } - - editor.play_mode_backup = Some(PlayModeBackup { - entities, - camera_data, - }); - - log::info!("Created play mode backup with {} entities and {} cameras", - editor.play_mode_backup.as_ref().unwrap().entities.len(), - editor.play_mode_backup.as_ref().unwrap().camera_data.len() - ); - Ok(()) - } - - pub fn restore(editor: &mut Editor) -> anyhow::Result<()> { - if let Some(backup) = &editor.play_mode_backup { - // Restore entity states - for (entity_id, original_transform, original_properties, original_script) in &backup.entities { - if let Ok(mut transform) = editor.world.get::<&mut Transform>(*entity_id) { - *transform = *original_transform; - } - - if let Ok(mut properties) = editor.world.get::<&mut ModelProperties>(*entity_id) { - *properties = original_properties.clone(); - } - - let has_script = editor.world.get::<&ScriptComponent>(*entity_id).is_ok(); - match (has_script, original_script) { - (true, Some(original)) => { - if let Ok(mut script) = editor.world.get::<&mut ScriptComponent>(*entity_id) { - *script = original.clone(); - } - } - (true, None) => { - let _ = editor.world.remove_one::<ScriptComponent>(*entity_id); - } - (false, Some(original)) => { - let _ = editor.world.insert_one(*entity_id, original.clone()); - } - (false, None) => { - // No change needed - } - } - } - - // Restore camera states - for (entity_id, original_camera, original_component, original_follow_target) in &backup.camera_data { - if let Ok(mut camera) = editor.world.get::<&mut Camera>(*entity_id) { - *camera = original_camera.clone(); - } - - if let Ok(mut component) = editor.world.get::<&mut CameraComponent>(*entity_id) { - *component = original_component.clone(); - } - - let has_follow_target = editor.world.get::<&CameraFollowTarget>(*entity_id).is_ok(); - match (has_follow_target, original_follow_target) { - (true, Some(original)) => { - if let Ok(mut follow_target) = editor.world.get::<&mut CameraFollowTarget>(*entity_id) { - *follow_target = original.clone(); - } - } - (true, None) => { - let _ = editor.world.remove_one::<CameraFollowTarget>(*entity_id); - } - (false, Some(original)) => { - let _ = editor.world.insert_one(*entity_id, original.clone()); - } - (false, None) => { - // No change needed - } - } - } - - log::info!("Restored scene from play mode backup"); - - editor.play_mode_backup = None; - Ok(()) - } else { - Err(anyhow::anyhow!("No play mode backup found to restore")) - } - } -} - -pub enum EditorState { - Editing, - Playing, -} @@ -1,930 +0,0 @@ -use egui::{Align2, Image}; -use dropbear_engine::{ - entity::{AdoptedEntity, Transform}, graphics::{Graphics, Shader}, lighting::{Light, LightComponent}, model::{DrawLight, DrawModel}, scene::{Scene, SceneCommand} -}; -use log; -use parking_lot::Mutex; -use wgpu::Color; -use wgpu::util::DeviceExt; -use winit::{event_loop::ActiveEventLoop, keyboard::KeyCode}; -use dropbear_engine::graphics::InstanceRaw; -use dropbear_engine::model::Model; -use dropbear_engine::starter::plane::PlaneBuilder; -use super::*; -use crate::{ - camera::PlayerCamera, utils::PendingSpawn -}; -use crate::states::PropertyValue; -use crate::utils::PROTO_TEXTURE; - -pub static PENDING_SPAWNS: LazyLock<Mutex<Vec<PendingSpawn>>> = - LazyLock::new(|| Mutex::new(Vec::new())); - -impl Scene for Editor { - fn load(&mut self, graphics: &mut Graphics) { - if self.active_camera.is_none() { - self.load_project_config(graphics).unwrap(); - } - - let shader = Shader::new( - graphics, - include_str!("../shader.wgsl"), - Some("viewport_shader"), - ); - - self.light_manager.create_light_array_resources(graphics); - - let texture_bind_group = &graphics.texture_bind_group().clone(); - if let Some(active_camera) = self.active_camera { - if let Ok(mut q) = self.world.query_one::<(&Camera, &CameraComponent, Option<&CameraFollowTarget>)>(active_camera) { - if let Some((camera, _component, _follow_target)) = q.get() { - let pipeline = graphics.create_render_pipline( - &shader, - vec![ - texture_bind_group, - camera.layout(), - self.light_manager.layout() - ], - None, - ); - self.render_pipeline = Some(pipeline); - - self.light_manager.create_render_pipeline( - graphics, - include_str!("../light.wgsl"), - camera, - Some("Light Pipeline") - ); - } else { - log_once::warn_once!("Unable to fetch the query result of camera: {:?}", active_camera) - } - } else { - log_once::warn_once!("Unable to query camera, component and option<camerafollowtarget> for active camera: {:?}", active_camera); - } - } else { - log_once::warn_once!("No active camera found"); - } - - self.window = Some(graphics.state.window.clone()); - } - - fn update(&mut self, dt: f32, graphics: &mut Graphics) { - if let Some((_, tab)) = self.dock_state.find_active_focused() { - self.is_viewport_focused = matches!(tab, EditorTab::Viewport); - } else { - self.is_viewport_focused = false; - } - - { - let mut pending_spawns = PENDING_SPAWNS.lock(); - for spawn in pending_spawns.drain(..) { - match AdoptedEntity::new(graphics, &spawn.asset_path, Some(&spawn.asset_name)) { - Ok(adopted) => { - let entity_id = - self.world - .spawn((adopted, spawn.transform, spawn.properties)); - self.selected_entity = Some(entity_id); - - UndoableAction::push_to_undo( - &mut self.undo_stack, - UndoableAction::Spawn(entity_id), - ); - - log::info!( - "Successfully spawned {} with ID {:?}", - spawn.asset_name, - entity_id - ); - } - Err(e) => { - log::error!("Failed to spawn {}: {}", spawn.asset_name, e); - } - } - } - } - - if matches!(self.editor_state, EditorState::Playing) { - if self.input_state.pressed_keys.contains(&KeyCode::Escape) { - self.signal = Signal::StopPlaying; - } - - let mut script_entities = Vec::new(); - for (entity_id, script) in self.world.query::<&mut ScriptComponent>().iter() { - log_once::debug_once!("Script Entity -> id: {:?}, component: {:?}", entity_id, script); - script.name = script.path.file_name().unwrap().to_str().unwrap().to_string(); - script_entities.push((entity_id, script.name.clone())); - } - - if script_entities.is_empty() { - log_once::warn_once!("Script entities is empty"); - } - - for (entity_id, script_name) in script_entities { - if let Err(e) = self.script_manager.update_entity_script(entity_id, &script_name, &mut self.world, &self.input_state, dt) { - log_once::warn_once!("Failed to update script '{}' for entity {:?}: {}", script_name, entity_id, e); - } - } - } - - if self.is_viewport_focused - && matches!(self.viewport_mode, crate::utils::ViewportMode::CameraMove) - // && self.is_using_debug_camera() - { - let movement_keys: std::collections::HashSet<KeyCode> = self - .input_state - .pressed_keys - .iter() - .filter(|&&key| { - matches!( - key, - KeyCode::KeyW - | KeyCode::KeyA - | KeyCode::KeyS - | KeyCode::KeyD - | KeyCode::Space - | KeyCode::ShiftLeft - ) - }) - .copied() - .collect(); - - // Handle camera input through ECS - if let Some(active_camera) = self.active_camera { - if let Ok(mut query) = self.world.query_one::<(&mut Camera, &CameraComponent)>(active_camera) { - if let Some((camera, component)) = query.get() { - // Handle keyboard input based on camera type - match component.camera_type { - CameraType::Debug => { - DebugCamera::handle_keyboard_input(camera, &movement_keys); - DebugCamera::handle_mouse_input(camera, component, self.input_state.mouse_delta); - } - CameraType::Player => { - PlayerCamera::handle_keyboard_input(camera, &movement_keys); - PlayerCamera::handle_mouse_input(camera, component, self.input_state.mouse_delta); - } - CameraType::Normal => { - // Handle normal camera input if needed - DebugCamera::handle_keyboard_input(camera, &movement_keys); - DebugCamera::handle_mouse_input(camera, component, self.input_state.mouse_delta); - } - } - } - } - } - } - - match &self.signal { - Signal::Paste(scene_entity) => { - match AdoptedEntity::new( - graphics, - &scene_entity.model_path.to_project_path(self.project_path.clone().unwrap()).unwrap(), - Some(&scene_entity.label), - ) { - Ok(adopted) => { - let entity_id = self.world.spawn(( - adopted, - scene_entity.transform, - ModelProperties::default(), - )); - self.selected_entity = Some(entity_id); - log::debug!( - "Successfully paste-spawned {} with ID {:?}", - scene_entity.label, - entity_id - ); - - crate::success_without_console!("Paste!"); - self.signal = Signal::Copy(scene_entity.clone()); - } - Err(e) => { - crate::warn!("Failed to paste-spawn {}: {}", scene_entity.label, e); - } - } - } - Signal::Delete => { - if let Some(sel_e) = &self.selected_entity { - let is_viewport_cam = if let Ok(mut q) = self.world.query_one::<&CameraComponent>(*sel_e) { if let Some(c) = q.get() { if matches!(c.camera_type, CameraType::Debug) { true } else { false } } else { false } } else { false }; - if is_viewport_cam { - crate::warn!("You can't delete the viewport camera"); - self.signal = Signal::None; - } else { - match self.world.despawn(*sel_e) { - Ok(_) => { - crate::info!("Decimated entity"); - self.signal = Signal::None; - } - Err(e) => { - crate::warn!("Failed to delete entity: {}", e); - self.signal = Signal::None; - } - } - } - } - } - Signal::Undo => { - if let Some(action) = self.undo_stack.pop() { - match action.undo(&mut self.world) { - Ok(_) => { - crate::info!("Undid action"); - } - Err(e) => { - crate::warn!("Failed to undo action: {}", e); - } - } - } else { - crate::warn_without_console!("Nothing to undo"); - log::debug!("No undoable actions in stack"); - } - self.signal = Signal::None; - } - Signal::None => {} - Signal::Copy(_) => {} - Signal::ScriptAction(action) => match action { - ScriptAction::AttachScript { - script_path, - script_name, - } => { - if let Some(selected_entity) = self.selected_entity { - match crate::scripting::move_script_to_src(script_path) { - Ok(moved_path) => { - let new_script = ScriptComponent { - name: script_name.clone(), - path: moved_path.clone(), - }; - - let replaced = if let Ok(mut sc) = - self.world.get::<&mut ScriptComponent>(selected_entity) - { - sc.name = new_script.name.clone(); - sc.path = new_script.path.clone(); - true - } else { - match crate::scripting::attach_script_to_entity( - &mut self.world, - selected_entity, - new_script.clone(), - ) { - Ok(_) => false, - Err(e) => { - crate::fatal!( - "Failed to attach script to entity {:?}: {}", - selected_entity, - e - ); - self.signal = Signal::None; - return; - } - } - }; - - if let Err(e) = crate::scripting::convert_entity_to_group( - &self.world, - selected_entity, - ) { - log::warn!("convert_entity_to_group failed (non-fatal): {}", e); - } - - crate::success!( - "{} script '{}' at {} to entity {:?}", - if replaced { "Reattached" } else { "Attached" }, - script_name, - moved_path.display(), - selected_entity - ); - } - Err(e) => { - crate::fatal!("Move failed: {}", e); - } - } - } else { - crate::fatal!("AttachScript requested but no entity is selected"); - } - - self.signal = Signal::None; - } - ScriptAction::CreateAndAttachScript { - script_path, - script_name, - } => { - if let Some(selected_entity) = self.selected_entity { - let new_script = ScriptComponent { - name: script_name.clone(), - path: script_path.clone(), - }; - - let replaced = if let Ok(mut sc) = - self.world.get::<&mut ScriptComponent>(selected_entity) - { - sc.name = new_script.name.clone(); - sc.path = new_script.path.clone(); - true - } else { - match crate::scripting::attach_script_to_entity( - &mut self.world, - selected_entity, - new_script.clone(), - ) { - Ok(_) => false, - Err(e) => { - crate::fatal!("Failed to attach new script: {}", e); - self.signal = Signal::None; - return; - } - } - }; - - if let Err(e) = - crate::scripting::convert_entity_to_group(&self.world, selected_entity) - { - log::warn!("convert_entity_to_group failed (non-fatal): {}", e); - } - - crate::success!( - "{} new script '{}' at {} to entity {:?}", - if replaced { "Replaced" } else { "Attached" }, - script_name, - script_path.display(), - selected_entity - ); - } else { - crate::warn_without_console!("No selected entity to attach new script"); - log::warn!("CreateAndAttachScript requested but no entity is selected"); - } - self.signal = Signal::None; - } - ScriptAction::RemoveScript => { - if let Some(selected_entity) = self.selected_entity { - if let Ok(script) = self.world.remove_one::<ScriptComponent>(selected_entity) { - crate::success!("Removed script from entity {:?}", selected_entity); - - if let Err(e) = crate::scripting::convert_entity_to_group( - &self.world, - selected_entity, - ) { - log::warn!("convert_entity_to_group failed (non-fatal): {}", e); - } - log::debug!("Pushing remove component to undo stack"); - UndoableAction::push_to_undo(&mut self.undo_stack, UndoableAction::RemoveComponent(selected_entity, ComponentType::Script(script))); - } else { - crate::warn!( - "No script component found on entity {:?}", - selected_entity - ); - } - } else { - crate::warn!("No entity selected to remove script from"); - } - - self.signal = Signal::None; - } - ScriptAction::EditScript => { - if let Some(selected_entity) = self.selected_entity { - if let Ok(mut q) = self.world.query_one::<&ScriptComponent>(selected_entity) - { - if let Some(script) = q.get() { - match open::that(script.path.clone()) { - Ok(()) => { - crate::success!("Opened {}", script.name) - } - Err(e) => { - crate::warn!("Error while opening {}: {}", script.name, e); - } - } - } - } else { - crate::warn!( - "No script component found on entity {:?}", - selected_entity - ); - } - } else { - crate::warn!("No entity selected to edit script"); - } - self.signal = Signal::None; - } - }, - Signal::Play => { - // Check if a player camera target exists - let has_player_camera_target = self.world - .query::<(&Camera, &CameraComponent, &CameraFollowTarget)>() - .iter() - .any(|(_, (_, comp, _))| matches!(comp.camera_type, CameraType::Player)); - - if has_player_camera_target { - if let Err(e) = PlayModeBackup::create_backup(self) { - crate::fatal!("Failed to create play mode backup: {}", e); - self.signal = Signal::None; - return; - } - - self.editor_state = EditorState::Playing; - - self.switch_to_player_camera(); - - let mut script_entities = Vec::new(); - for (entity_id, script) in self.world.query::<&ScriptComponent>().iter() { - script_entities.push((entity_id, script.clone())); - } - - for (entity_id, script) in script_entities { - log::debug!("Initialising entity script [{}] from path: {}", script.name, script.path.display()); - match self.script_manager.load_script(&script.path) { - Ok(script_name) => { - if let Err(e) = self.script_manager.init_entity_script(entity_id, &script_name, &mut self.world, &self.input_state) { - log::warn!("Failed to initialise script '{}' for entity {:?}: {}", script.name, entity_id, e); - self.signal = Signal::StopPlaying; - } else { - crate::success_without_console!("You are in play mode now! Press Escape to exit"); - log::info!("You are in play mode now! Press Escape to exit"); - } - } - Err(e) => { - // todo: proper error menu - crate::fatal!("Failed to load script '{}': {}", script.name, e); - self.signal = Signal::StopPlaying; - } - } - } - } else { - crate::fatal!("Unable to build: Player camera not attached to an entity"); - } - - self.signal = Signal::None; - } - Signal::StopPlaying => { - if let Err(e) = PlayModeBackup::restore(self) { - crate::warn!("Failed to restore from play mode backup: {}", e); - log::warn!("Failed to restore scene state: {}", e); - } - - self.editor_state = EditorState::Editing; - - self.switch_to_debug_camera(); - - for (entity_id, _) in self.world.query::<&ScriptComponent>().iter() { - self.script_manager.remove_entity_script(entity_id); - } - - crate::success!("Exited play mode"); - log::info!("Back to the editor you go..."); - - self.signal = Signal::None; - }, - Signal::CameraAction(action) => match action { - CameraAction::SetPlayerTarget { entity, offset } => { - // Find player camera and add/update CameraFollowTarget component - let player_camera = self.world - .query::<(&Camera, &CameraComponent)>() - .iter() - .find_map(|(e, (_, comp))| { - if matches!(comp.camera_type, CameraType::Player) { - Some(e) - } else { - None - } - }); - - if let Some(camera_entity) = player_camera { - let mut follow_target = (false, CameraFollowTarget::default()); - // Find the target entity label - if let Ok(mut query) = self.world.query_one::<&AdoptedEntity>(*entity) { - if let Some(adopted) = query.get() { - follow_target = (true, CameraFollowTarget { - follow_target: adopted.label().to_string(), - offset: *offset, - }); - } - } - - if follow_target.0 { - let _ = self.world.insert_one(camera_entity, follow_target); - crate::info!("Set player camera target to entity {:?}", entity); - } - } - self.signal = Signal::None; - } - CameraAction::ClearPlayerTarget => { - let player_camera = self.world - .query::<(&Camera, &CameraComponent)>() - .iter() - .find_map(|(e, (_, comp))| { - if matches!(comp.camera_type, CameraType::Player) { - Some(e) - } else { - None - } - }); - - if let Some(camera_entity) = player_camera { - let _ = self.world.remove_one::<CameraFollowTarget>(camera_entity); - } - crate::info!("Cleared player camera target"); - self.signal = Signal::None; - } - }, - Signal::AddComponent(entity, e_type) => { - match e_type { - EntityType::Entity => { - if let Ok(e) = self.world.query_one_mut::<&AdoptedEntity>(*entity) { - let mut local_signal: Option<Signal> = None; - let label = e.label().clone(); - let mut show = true; - egui::Window::new(format!("Add component for {}", label)) - .title_bar(true) - .open(&mut show) - .scroll([false, true]) - .anchor(Align2::CENTER_CENTER, [0.0, 0.0]) - .enabled(true) - .show(&graphics.get_egui_context(), |ui| { - if ui.add_sized([ui.available_width(), 30.0], egui::Button::new("Scripting")).clicked() { - log::debug!("Adding scripting component to entity [{}]", label); - if let Err(e) = self.world.insert_one(*entity, ScriptComponent::default()) { - crate::warn!("Failed to add scripting component to entity: {}", e); - } else { - crate::success!("Added the scripting component"); - } - local_signal = Some(Signal::None); - } - if ui.add_sized([ui.available_width(), 30.0], egui::Button::new("Camera")).clicked() { - log::debug!("Adding camera component to entity [{}]", label); - - let has_camera = self.world.query_one::<(&Camera, &CameraComponent)>(*entity).is_ok(); - - if has_camera { - crate::warn!("Entity [{}] already has a camera component", label); - } else { - let camera = Camera::predetermined(graphics, Some(&format!("{} Camera", label))); - let component = CameraComponent::new(); - - if let Err(e) = self.world.insert(*entity, (camera, component)) { - crate::warn!("Failed to add camera component to entity: {}", e); - } else { - crate::success!("Added the camera component"); - } - } - local_signal = Some(Signal::None); - } - }); - if !show { - self.signal = Signal::None; - } - if let Some(signal) = local_signal { - self.signal = signal - } - } else { - log_once::warn_once!("Failed to add component to entity: no entity component found"); - } - } - EntityType::Light => { - if let Ok(light) = self.world.query_one_mut::<&Light>(*entity) { - let mut show = true; - egui::Window::new(format!("Add component for {}", light.label)) - .scroll([false, true]) - .anchor(Align2::CENTER_CENTER, [0.0, 0.0]) - .enabled(true) - .open(&mut show) - .title_bar(true) - .show(&graphics.get_egui_context(), |ui| { - if ui.add_sized([ui.available_width(), 30.0], egui::Button::new("Scripting")).clicked() { - log::debug!("Adding scripting component to light [{}]", light.label); - - crate::success!("Added the scripting component to light [{}]", light.label); - self.signal = Signal::None; - } - }); - if !show { - self.signal = Signal::None; - } - } else { - log_once::warn_once!("Failed to add component to light: no light component found"); - } - }, - EntityType::Camera => { - if let Ok((cam, _comp)) = self.world.query_one_mut::<(&Camera, &CameraComponent)>(*entity) { - let mut show = true; - egui::Window::new(format!("Add component for {}", cam.label)) - .scroll([false, true]) - .anchor(Align2::CENTER_CENTER, [0.0, 0.0]) - .enabled(true) - .open(&mut show) - .title_bar(true) - .show(&graphics.get_egui_context(), |ui| { - egui_extras::install_image_loaders(ui.ctx()); - ui.add(Image::from_bytes("bytes://theres_nothing", include_bytes!("../../../resources/theres_nothing.jpg"))); - ui.label("Theres nothing..."); - // // scripting - // if ui.add_sized([ui.available_width(), 30.0], egui::Button::new("Scripting")).clicked() { - // log::debug!("Adding scripting component to camera [{}]", cam.label); - - // crate::success!("Added the scripting component to camera [{}]", cam.label); - // self.signal = Signal::None; - // } - }); - if !show { - self.signal = Signal::None; - } - } else { - log_once::warn_once!("Failed to add component to light: no light component found"); - } - } - } - }, - Signal::RemoveComponent(entity, c_type) => { - match c_type { - ComponentType::Script(_) => { - match self.world.remove_one::<ScriptComponent>(*entity) { - Ok(component) => { - crate::success!("Removed script component from entity {:?}", entity); - UndoableAction::push_to_undo(&mut self.undo_stack, UndoableAction::RemoveComponent(*entity, ComponentType::Script(component))); - } - Err(e) => { - crate::warn!("Failed to remove script component from entity: {}", e); - } - }; - self.signal = Signal::None; - }, - ComponentType::Camera(_, _, follow) => { - if let Some(_) = follow { - match self.world.remove::<(Camera, CameraComponent, CameraFollowTarget)>(*entity) { - Ok(component) => { - crate::success!("Removed camera component from entity {:?}", entity); - UndoableAction::push_to_undo(&mut self.undo_stack, UndoableAction::RemoveComponent(*entity, ComponentType::Camera(component.0, component.1, Some(component.2)))); - } - Err(e) => { - crate::warn!("Failed to remove camera component from entity: {}", e); - } - }; - } else { - match self.world.remove::<(Camera, CameraComponent)>(*entity) { - Ok(component) => { - crate::success!("Removed camera component from entity {:?}", entity); - UndoableAction::push_to_undo(&mut self.undo_stack, UndoableAction::RemoveComponent(*entity, ComponentType::Camera(component.0, component.1, None))); - } - Err(e) => { - crate::warn!("Failed to remove script component from entity: {}", e); - } - }; - } - } - } - } - Signal::CreateEntity => { - // self.show_add_entity_window = true; - let mut show = true; - egui::Window::new("Add Entity") - .scroll([false, true]) - .anchor(Align2::CENTER_CENTER, [0.0, 0.0]) - .enabled(true) - .open(&mut show) - .title_bar(true) - .show(&graphics.get_egui_context(), |ui| { - if ui.add_sized([ui.available_width(), 30.0], egui::Button::new("Model")).clicked() { - log::debug!("Creating new model"); - crate::warn!("Instead of using the `Add Entity` window, double click on the imported model in the asset \n\ - viewer to import a new model, then tweak the settings to how you wish after!"); - self.signal = Signal::None; - } - - if ui.add_sized([ui.available_width(), 30.0], egui::Button::new("Light")).clicked() { - log::debug!("Creating new lighting"); - let transform = Transform::new(); - let component = LightComponent::default(); - let light = Light::new(graphics, &component, &transform, Some("Light")); - self.world.spawn((light, component, transform)); - crate::success!("Created new light"); - - // always ensure the signal is reset after action is dun - self.signal = Signal::None; - } - - if ui.add_sized([ui.available_width(), 30.0], egui::Button::new("Plane")).clicked() { - log::debug!("Creating new plane"); - let plane = PlaneBuilder::new() - .with_size(500.0, 200.0) - .build( - graphics, - PROTO_TEXTURE, - Some("Plane") - ).unwrap(); - let transform = Transform::new(); - let mut props = ModelProperties::new(); - props.custom_properties.insert("width".to_string(), PropertyValue::Float(500.0)); - props.custom_properties.insert("height".to_string(), PropertyValue::Float(200.0)); - props.custom_properties.insert("tiles_x".to_string(), PropertyValue::Int(500)); - props.custom_properties.insert("tiles_z".to_string(), PropertyValue::Int(200)); - self.world.spawn((plane, transform, props)); - crate::success!("Created new plane"); - - self.signal = Signal::None; - } - - if ui.add_sized([ui.available_width(), 30.0], egui::Button::new("Cube")).clicked() { - log::debug!("Creating new cube"); - let model = Model::load_from_memory( - graphics, - include_bytes!("../../../resources/cube.obj").to_vec(), - Some("Cube") - ); - match model { - Ok(model) => { - let cube = AdoptedEntity::adopt( - graphics, - model, - Some("Cube") - ); - self.world.spawn((cube, Transform::new(), ModelProperties::new())); - } - Err(e) => { - crate::fatal!("Failed to load cube model: {}", e); - } - } - crate::success!("Created new cube"); - - self.signal = Signal::None; - } - - if ui.add_sized([ui.available_width(), 30.0], egui::Button::new("Cube")).clicked() { - log::debug!("Creating new cube"); - let model = Model::load_from_memory( - graphics, - include_bytes!("../../../resources/cube.obj").to_vec(), - Some("Cube") - ); - match model { - Ok(model) => { - let cube = AdoptedEntity::adopt( - graphics, - model, - Some("Cube") - ); - self.world.spawn((cube, Transform::new(), ModelProperties::new())); - } - Err(e) => { - crate::fatal!("Failed to load cube model: {}", e); - } - } - crate::success!("Created new cube"); - - self.signal = Signal::None; - } - - if ui.add_sized([ui.available_width(), 30.0], egui::Button::new("Camera")).clicked() { - log::debug!("Creating new cube"); - let camera = Camera::predetermined(graphics, None); - let component = CameraComponent::new(); - self.world.spawn((camera, component)); - crate::success!("Created new camera"); - - self.signal = Signal::None; - } - }); - if !show { - self.signal = Signal::None; - } - }, - Signal::LogEntities => { - log::info!("===================="); - for entity in self.world.iter() { - if let Some(entity) = entity.get::<&AdoptedEntity>() { - log::info!("Model: {:?}", entity.label()); - } - - if let Some(entity) = entity.get::<&Light>() { - log::info!("Light: {:?}", entity.label()); - } - } - log::info!("===================="); - self.signal = Signal::None; - } - } - - let current_size = graphics.state.viewport_texture.size; - self.size = current_size; - - let new_aspect = current_size.width as f64 / current_size.height as f64; - if let Some(active_camera) = self.active_camera { - if let Ok(mut query) = self.world.query_one::<&mut Camera>(active_camera) { - if let Some(camera) = query.get() { - camera.aspect = new_aspect; - } - } - } - - for (_entity_id, (camera, _component, follow_target)) in self - .world - .query::<(&mut Camera, &CameraComponent, Option<&CameraFollowTarget>)>() - .iter() - { - if let Some(target) = follow_target { - for (_target_entity_id, (adopted, transform)) in self - .world - .query::<(&AdoptedEntity, &Transform)>() - .iter() - { - if adopted.label() == &target.follow_target { - let target_pos = transform.position; - camera.eye = target_pos + target.offset; - camera.target = target_pos; - break; - } - } - } - } - - for (_entity_id, (camera, component)) in self.world.query::<(&mut Camera, &mut CameraComponent)>().iter() { - component.update(camera); - camera.update(graphics); - } - - let query = self.world.query_mut::<(&mut AdoptedEntity, &Transform)>(); - for (_, (entity, transform)) in query { - entity.update(&graphics, transform); - } - - let light_query = self.world.query_mut::<(&mut LightComponent, &Transform, &mut Light)>(); - for (_, (light_component, transform, light)) in light_query { - light.update(light_component, transform); - } - - self.light_manager.update(graphics, &self.world); - } - - fn render(&mut self, graphics: &mut Graphics) { - // cornflower blue - let color = Color { - r: 100.0 / 255.0, - g: 149.0 / 255.0, - b: 237.0 / 255.0, - a: 1.0, - }; - - self.color = color.clone(); - self.size = graphics.state.viewport_texture.size.clone(); - self.texture_id = Some(graphics.state.texture_id.clone()); - self.show_ui(&graphics.get_egui_context()); - - self.window = Some(graphics.state.window.clone()); - crate::logging::render(&graphics.get_egui_context()); - if let Some(pipeline) = &self.render_pipeline { - if let Some(active_camera) = self.active_camera { - if let Ok(mut query) = self.world.query_one::<&Camera>(active_camera) { - if let Some(camera) = query.get() { - let mut light_query = self.world.query::<(&Light, &LightComponent)>(); - let mut entity_query = self.world.query::<(&AdoptedEntity, &Transform)>(); - { - let mut render_pass = graphics.clear_colour(color); - - if let Some(light_pipeline) = &self.light_manager.pipeline { - render_pass.set_pipeline(light_pipeline); - for (_, (light, component)) in light_query.iter() { - if component.enabled { - render_pass.set_vertex_buffer(1, light.instance_buffer.as_ref().unwrap().slice(..)); - render_pass.draw_light_model( - light.model(), - camera.bind_group(), - light.bind_group(), - ); - } - } - } - - let mut model_batches: HashMap<*const Model, Vec<InstanceRaw>> = HashMap::new(); - - for (_, (entity, _)) in entity_query.iter() { - let model_ptr = entity.model() as *const Model; - let instance_raw = entity.instance.to_raw(); - model_batches.entry(model_ptr).or_insert(Vec::new()).push(instance_raw); - } - - render_pass.set_pipeline(pipeline); - - for (model_ptr, instances) in model_batches { - let model = unsafe { &*model_ptr }; - - let instance_buffer = graphics.state.device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("Batched Instance Buffer"), - contents: bytemuck::cast_slice(&instances), - usage: wgpu::BufferUsages::VERTEX, - }); - - render_pass.set_vertex_buffer(1, instance_buffer.slice(..)); - render_pass.draw_model_instanced( - model, - 0..instances.len() as u32, - camera.bind_group(), - self.light_manager.bind_group(), - ); - } - } - } - } - } - } - } - - fn exit(&mut self, _event_loop: &ActiveEventLoop) {} - - fn run_command(&mut self) -> SceneCommand { - std::mem::replace(&mut self.scene_command, SceneCommand::None) - } -} @@ -1,21 +0,0 @@ -#[cfg(feature = "editor")] -pub mod editor; -#[cfg(feature = "editor")] -pub mod menu; -#[cfg(feature = "editor")] -pub mod build; -#[cfg(feature = "editor")] -pub mod debug; - - -pub mod camera; -pub mod logging; -pub mod scripting; -pub mod states; -pub mod utils; - -#[cfg(feature = "editor")] -pub const APP_INFO: app_dirs2::AppInfo = app_dirs2::AppInfo { - name: "Eucalyptus", - author: "4tkbytes", -}; @@ -1,61 +0,0 @@ -// Shader for rendering the white cube for lighting (or diff depending on colour) - -struct Camera { - view_pos: vec4<f32>, - view_proj: mat4x4<f32>, -} -@group(0) @binding(0) -var<uniform> camera: Camera; - -struct Light { - position: vec4<f32>, - direction: vec4<f32>, // x, y, z, outer_cutoff_angle - color: vec4<f32>, // r, g, b, light_type (0, 1, 2) - constant: f32, - lin: f32, - quadratic: f32, - cutoff: f32, -} - -@group(1) @binding(0) -var<uniform> light: Light; - -struct InstanceInput { - @location(5) model_matrix_0: vec4<f32>, - @location(6) model_matrix_1: vec4<f32>, - @location(7) model_matrix_2: vec4<f32>, - @location(8) model_matrix_3: vec4<f32>, -} - -struct VertexInput { - @location(0) position: vec3<f32>, -}; - -struct VertexOutput { - @builtin(position) clip_position: vec4<f32>, - @location(0) color: vec3<f32>, -}; - -@vertex -fn vs_main( - model: VertexInput, - instance: InstanceInput, -) -> VertexOutput { - let model_matrix = mat4x4<f32>( - instance.model_matrix_0, - instance.model_matrix_1, - instance.model_matrix_2, - instance.model_matrix_3, - ); - var out: VertexOutput; - out.clip_position = camera.view_proj * model_matrix * vec4<f32>(model.position, 1.0); - out.color = light.color.xyz; - return out; -} - -// Fragment shader - -@fragment -fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> { - return vec4<f32>(in.color, 1.0); -} @@ -1,242 +0,0 @@ -//! This crate will allow for logging to specific locations. Essentially, just removing boilerplate -//! -//! # Supported logging locations: -//! - Toasts (egui) -//! - Console -//! - File (to be implemented) - -#[cfg(feature = "editor")] -use egui::Context; - -#[cfg(feature = "editor")] -use egui_toast_fork::Toasts; - -#[cfg(feature = "editor")] -use once_cell::sync::Lazy; -#[cfg(feature = "editor")] -use parking_lot::Mutex; - -#[cfg(feature = "editor")] -pub static GLOBAL_TOASTS: Lazy<Mutex<Toasts>> = Lazy::new(|| { - Mutex::new( - Toasts::new() - .anchor(egui::Align2::RIGHT_BOTTOM, (-10.0, -10.0)) - .direction(egui::Direction::BottomUp), - ) -}); - -/// Renders the toasts. Requires an egui context. -/// -/// Useful when paired with a function that contains [`crate`] -#[cfg(feature = "editor")] -pub(crate) fn render(context: &Context) { - let mut toasts = GLOBAL_TOASTS.lock(); - toasts.show(context); -} - -/// Fatal log macro -/// -/// This is useful for when there is a fatal error like a missing file cannot be found. -/// -/// This macro creates a toast under the [`egui_toast_fork::ToastKind::Error`] and logs -/// with [`log::error!`] -#[macro_export] -macro_rules! fatal { - ($($arg:tt)*) => {{ - let _msg = format!($($arg)*); - log::error!("{}", _msg); - - #[cfg(feature = "editor")] - { - use egui_toast_fork::{Toast, ToastKind}; - use crate::logging::GLOBAL_TOASTS; - let mut toasts = GLOBAL_TOASTS.lock(); - toasts.add(Toast { - text: _msg.into(), - kind: ToastKind::Error, - options: egui_toast_fork::ToastOptions::default() - .duration_in_seconds(3.0) - .show_progress(true), - style: egui_toast_fork::ToastStyle::default(), - }); - } - }}; -} - -/// Success log macro -/// -/// This is useful for when loading a save is successful. -/// -/// This macro creates a toast under the [`egui_toast_fork::ToastKind::Success`] and logs -/// with [`log::info!`] -#[macro_export] -macro_rules! success { - ($($arg:tt)*) => {{ - let _msg = format!($($arg)*); - log::debug!("{}", _msg); - - #[cfg(feature = "editor")] - { - use egui_toast_fork::{Toast, ToastKind}; - use crate::logging::GLOBAL_TOASTS; - let mut toasts = GLOBAL_TOASTS.lock(); - toasts.add(Toast { - text: _msg.into(), - kind: ToastKind::Success, - options: egui_toast_fork::ToastOptions::default() - .duration_in_seconds(3.0) - .show_progress(true), - style: egui_toast_fork::ToastStyle::default(), - }); - }; - } - }; -} - -/// Warn log macro -/// -/// This is useful for when there is a non-fatal error like unable to copy. -/// -/// This macro creates a toast under the [`egui_toast_fork::ToastKind::Warning`] and logs -/// with [`log::warn!`] -#[macro_export] -macro_rules! warn { - ($($arg:tt)*) => {{ - let _msg = format!($($arg)*); - log::warn!("{}", _msg); - - #[cfg(feature = "editor")] - { - use egui_toast_fork::{Toast, ToastKind}; - use crate::logging::GLOBAL_TOASTS; - let mut toasts = GLOBAL_TOASTS.lock(); - toasts.add(Toast { - text: _msg.into(), - kind: ToastKind::Warning, - options: egui_toast_fork::ToastOptions::default() - .duration_in_seconds(3.0) - .show_progress(true), - style: egui_toast_fork::ToastStyle::default(), - }); - } - }}; -} - -/// Info log macro -/// -/// This is useful for notifying the user of a change, where it doesn't have to be important. -/// -/// This macro creates a toast under the [`egui_toast_fork::ToastKind::Info`] and logs -/// with [`log::debug!`] -#[macro_export] -macro_rules! info { - ($($arg:tt)*) => {{ - let _msg = format!($($arg)*); - log::debug!("{}", _msg); - - #[cfg(feature = "editor")] - { - use egui_toast_fork::{Toast, ToastKind}; - use crate::logging::GLOBAL_TOASTS; - let mut toasts = GLOBAL_TOASTS.lock(); - toasts.add(Toast { - text: _msg.into(), - kind: ToastKind::Info, - options: egui_toast_fork::ToastOptions::default() - .duration_in_seconds(1.0) - .show_progress(false), - style: egui_toast_fork::ToastStyle::default(), - }); - } - }}; -} - -/// Macro for logging info without the console -/// -/// This macro should be "info_toast", however in the case that I ever need to add some more functionality, -/// this would be useful. -/// -/// Its feature-heavy counterpart would be [`crate::success!`]. -/// -/// It creates a toast under [`egui_toast_fork::ToastKind::Info`]. -#[macro_export] -macro_rules! info_without_console { - ($($arg:tt)*) => { - let _msg = format!($($arg)*); - - #[cfg(feature = "editor")] - { - use egui_toast_fork::{Toast, ToastKind}; - use crate::logging::GLOBAL_TOASTS; - let mut toasts = GLOBAL_TOASTS.lock(); - toasts.add(Toast { - text: _msg.into(), - kind: ToastKind::Info, - options: egui_toast_fork::ToastOptions::default() - .duration_in_seconds(1.0) - .show_progress(false), - style: egui_toast_fork::ToastStyle::default(), - }); - } - }; -} - -/// Macro for logging a successful action without the console -/// -/// This macro should be "success_toast", however in the case that I ever need to add some more functionality, -/// this would be useful. -/// -/// Its feature-heavy counterpart would be [`crate::success!`]. -/// -/// It creates a toast under [`egui_toast_fork::ToastKind::Success`]. -#[macro_export] -macro_rules! success_without_console { - ($($arg:tt)*) => { - let _msg = format!($($arg)*); - - #[cfg(feature = "editor")] - { - use egui_toast_fork::{Toast, ToastKind}; - use crate::logging::GLOBAL_TOASTS; - let mut toasts = GLOBAL_TOASTS.lock(); - toasts.add(Toast { - text: _msg.into(), - kind: ToastKind::Success, - options: egui_toast_fork::ToastOptions::default() - .duration_in_seconds(3.0) - .show_progress(true), - style: egui_toast_fork::ToastStyle::default(), - }); - } - }; -} - -/// Macro for logging a successful action without the console -/// -/// This macro should be "success_toast", however in the case that I ever need to add some more functionality, -/// this would be useful. -/// -/// Its feature-heavy counterpart would be [`crate::warn!`]. -/// -/// It creates a toast under [`egui_toast_fork::ToastKind::Warning`]. -#[macro_export] -macro_rules! warn_without_console { - ($($arg:tt)*) => { - let _msg = format!($($arg)*); - - #[cfg(feature = "editor")] - { - use egui_toast_fork::{Toast, ToastKind}; - use crate::logging::GLOBAL_TOASTS; - let mut toasts = GLOBAL_TOASTS.lock(); - toasts.add(Toast { - text: _msg.into(), - kind: ToastKind::Warning, - options: egui_toast_fork::ToastOptions::default() - .duration_in_seconds(3.0) - .show_progress(true), - style: egui_toast_fork::ToastStyle::default(), - }); - } - }; -} @@ -1,181 +0,0 @@ -#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] - -#[cfg(feature = "editor")] -use std::{cell::RefCell, rc::Rc, fs, path::PathBuf}; -#[cfg(feature = "editor")] -use clap::{Arg, Command}; - -#[cfg(feature = "editor")] -use dropbear_engine::{WindowConfiguration, scene}; -use eucalyptus::APP_INFO; - -// #[tokio::main] -#[cfg(feature = "editor")] -// async -fn main() -> anyhow::Result<()> { - println!("Editor feature: {}", cfg!(feature = "editor")); - println!("Data-only feature: {}", cfg!(feature = "data-only")); - - if cfg!(feature = "data-only") { - panic!("You are using the data-only feature while compiled with the editor feature? Huh?"); - } - #[cfg(target_os = "android")] - compile_error!("The `editor` feature is not supported on Android. If you are attempting\ - to use the Eucalyptus editor on Android, please don't. Instead, use the `data-only` feature\ - to use with dependencies or create your own game on Desktop. Sorry :("); - let matches = Command::new("eucalyptus") - .about("A visual game editor") - .version("1.0.0") - .subcommand_required(false) - .arg_required_else_help(false) - .subcommand( - Command::new("build") - .about("Build a eucalyptus project, but only the .eupak file and its resources") - .arg( - Arg::new("project") - .help("Path to the .eucp project file") - .value_name("PROJECT_FILE") - .required(false), - ), - ) - .subcommand( - Command::new("package") - .about("Package a eucalyptus project, which compiles the runtime and the resource .eupak file") - .arg( - Arg::new("project") - .help("Path to the .eucp project file") - .value_name("PROJECT_FILE") - .required(false), - ), - ) - .subcommand(Command::new("read") - .about("Reads and displays the contents of a .eupak file for debugging") - .arg( - Arg::new("eupak_file") - .help("Path to the .eupak file") - .value_name("RESOURCE_FILE") - .required(false), - ), - ) - .subcommand(Command::new("health").about("Check the health of the eucalyptus installation")) - .get_matches(); - - match matches.subcommand() { - Some(("build", sub_matches)) => { - let project_path = match sub_matches.get_one::<String>("project") { - Some(path) => PathBuf::from(path), - None => match find_eucp_file() { - Ok(path) => path, - Err(e) => { - eprintln!("Error: {}", e); - std::process::exit(1); - } - }, - }; - - eucalyptus::build::build(project_path)?; - } - Some(("package", sub_matches)) => { - let project_path = match sub_matches.get_one::<String>("project") { - Some(path) => PathBuf::from(path), - None => match find_eucp_file() { - Ok(path) => path, - Err(e) => { - eprintln!("Error: {}", e); - std::process::exit(1); - } - }, - }; - - eucalyptus::build::package(project_path, sub_matches)?; - } - Some(("health", _)) => { - eucalyptus::build::health()?; - } - Some(("read", sub_matches)) => { - let project_path = match sub_matches.get_one::<String>("eupak_file") { - Some(path) => PathBuf::from(path), - None => match find_eucp_file() { - Ok(path) => path, - Err(e) => { - eprintln!("Error: {}", e); - std::process::exit(1); - } - }, - }; - - eucalyptus::build::read_from_eupak(project_path)?; - } - None => { - let config = WindowConfiguration { - title: "Eucalyptus, built with dropbear".into(), - windowed_mode: dropbear_engine::WindowedModes::Maximised, - max_fps: dropbear_engine::App::NO_FPS_CAP, - app_info: APP_INFO, - }; - - let _app = dropbear_engine::run_app!(config, |mut scene_manager, mut input_manager| { - let main_menu = Rc::new(RefCell::new(eucalyptus::menu::MainMenu::new())); - let editor = Rc::new(RefCell::new(eucalyptus::editor::Editor::new())); - - scene::add_scene_with_input( - &mut scene_manager, - &mut input_manager, - main_menu, - "main_menu", - ); - scene::add_scene_with_input( - &mut scene_manager, - &mut input_manager, - editor, - "editor", - ); - - scene_manager.switch("main_menu"); - - (scene_manager, input_manager) - }) - .unwrap(); - } - _ => unreachable!(), - } - Ok(()) -} - -#[cfg(not(feature = "editor"))] -fn main() { - compile_error!("\n\nYou have not enabled the \"editor\" feature, therefore cannot use the eucalyptus editor. -Either import as a lib to use its structs and enums or enable the editor feature or enable the \"editor\" feature or just the default features\n\n"); -} - -#[cfg(all(feature = "editor", feature = "data-only"))] -compile_error!("Features `editor` and `data-only` cannot be enabled at the same time"); - - -#[cfg(feature = "editor")] -fn find_eucp_file() -> Result<PathBuf, String> { - let current_dir = std::env::current_dir().map_err(|_| "Failed to get current directory")?; - - let entries = fs::read_dir(¤t_dir).map_err(|_| "Failed to read current directory")?; - - let mut eucp_files = Vec::new(); - - for entry in entries { - if let Ok(entry) = entry { - if let Some(file_name) = entry.file_name().to_str() { - if file_name.ends_with(".eucp") { - eucp_files.push(entry.path()); - } - } - } - } - - match eucp_files.len() { - 0 => Err("No .eucp files found in current directory".to_string()), - 1 => Ok(eucp_files[0].clone()), - _ => Err(format!( - "Multiple .eucp files found: {:#?}. Please specify which one to use.", - eucp_files - )), - } -} @@ -1,384 +0,0 @@ -use std::{ - fs, - sync::mpsc::{self, Receiver}, -}; - -use anyhow::anyhow; -use dropbear_engine::{ - input::{Controller, Keyboard, Mouse}, - scene::{Scene, SceneCommand}, -}; -use egui::{self, FontId, Frame, RichText}; -use egui_toast_fork::{ToastOptions, Toasts}; -use gilrs; -use git2::Repository; -use log::{self, debug}; -use winit::{ - dpi::PhysicalPosition, event::MouseButton, event_loop::ActiveEventLoop, keyboard::KeyCode, -}; - -use crate::states::{PROJECT, ProjectConfig}; - -#[derive(Default)] -pub struct MainMenu { - scene_command: SceneCommand, - show_new_project: bool, - project_name: String, - project_path: Option<std::path::PathBuf>, - project_error: Option<Vec<String>>, - - progress_rx: Option<Receiver<ProjectProgress>>, - - show_progress: bool, - progress: f32, - progress_message: String, - toast: Toasts, - is_in_file_dialogue: bool, -} - -pub enum ProjectProgress { - Step { - progress: f32, - message: String, - }, - #[allow(dead_code)] - Error(String), - Done, -} - -impl MainMenu { - pub fn new() -> Self { - Self { - show_progress: false, - toast: Toasts::new() - .anchor(egui::Align2::RIGHT_BOTTOM, (-10.0, -10.0)) - .direction(egui::Direction::BottomUp), - ..Default::default() - } - } - - fn start_project_creation(&mut self) { - let (tx, rx) = mpsc::channel(); - let project_name = self.project_name.clone(); - let project_path = self.project_path.clone(); - - self.progress_rx = Some(rx); - self.show_progress = true; - self.progress = 0.0; - self.progress_message = "Starting project creation...".to_string(); - - std::thread::spawn(move || { - let mut errors = Vec::new(); - let folders = [ - ("git", 0.1, "Creating a git folder..."), - ("src", 0.2, "Creating src folder..."), - ("resources/models", 0.3, "Creating models folder..."), - ("resources/shaders", 0.4, "Creating shader folder..."), - ("resources/textures", 0.5, "Creating textures folder..."), - ("src2", 0.6, "Creating project config file..."), - ("scenes", 0.7, "Creating the scenes folder"), - ]; - - if let Some(path) = &project_path { - for (folder, progress, message) in folders { - tx.send(ProjectProgress::Step { - progress, - message: message.to_string(), - }) - .ok(); - - let full_path = path.join(folder); - let result: anyhow::Result<()> = if folder == "src" { - if !full_path.exists() { - fs::create_dir(&full_path) - .map_err(|e| anyhow::anyhow!(e)) - .map(|_| ()) - } else { - Ok(()) - } - } else if folder == "git" { - match Repository::init(path) { - Ok(_) => Ok(()), - Err(e) => { - if matches!(e.code(), git2::ErrorCode::Exists) { - log::warn!("Git repository already exists"); - Ok(()) - } else { - Err(anyhow!(e)) - } - } - } - } else if folder == "src2" { - if let Some(path) = &project_path { - let mut config = ProjectConfig::new(project_name.clone(), &path); - let _ = config.write_to_all(); - let mut global = PROJECT.write().unwrap(); - *global = config; - Ok(()) - } else { - Err(anyhow!("Project path not found")) - } - } else { - if !full_path.exists() { - fs::create_dir_all(&full_path) - .map_err(|e| anyhow!(e)) - .map(|_| ()) - } else { - log::warn!("{:?} already exists", full_path); - Ok(()) - } - }; - if let Err(e) = result { - tx.send(ProjectProgress::Error(e.to_string())).ok(); - errors.push(e); - } - } - tx.send(ProjectProgress::Step { - progress: 1.0, - message: "Project creation complete!".to_string(), - }) - .ok(); - - tx.send(ProjectProgress::Done).ok(); - } - }); - - log::debug!("Starting project creation"); - } -} - -impl Scene for MainMenu { - fn load(&mut self, _graphics: &mut dropbear_engine::graphics::Graphics) { - log::info!("Loaded menu scene"); - } - - fn update(&mut self, _dt: f32, _graphics: &mut dropbear_engine::graphics::Graphics) {} - - fn render(&mut self, graphics: &mut dropbear_engine::graphics::Graphics) { - let screen_size: (f32, f32) = ( - graphics.state.window.inner_size().width as f32 - 100.0, - graphics.state.window.inner_size().height as f32 - 100.0, - ); - let egui_ctx = graphics.get_egui_context(); - - egui::CentralPanel::default() - .frame(Frame::new()) - .show(&egui_ctx, |ui| { - ui.vertical_centered(|ui| { - ui.add_space(64.0); - ui.label(RichText::new("Eucalyptus").font(FontId::proportional(32.0))); - ui.add_space(40.0); - - let button_size = egui::vec2(300.0, 60.0); - - if ui - .add_sized(button_size, egui::Button::new("New Project")) - .clicked() - { - log::debug!("Creating new project"); - self.show_new_project = true; - } - ui.add_space(20.0); - - if ui - .add_sized(button_size, egui::Button::new("Open Project")) - .clicked() - { - log::debug!("Opening project"); - self.is_in_file_dialogue = true; - if let Some(path) = rfd::FileDialog::new() - .add_filter("Eucalyptus Configuration Files", &["eucp"]) - .pick_file() - { - match ProjectConfig::read_from(&path) { - Ok(config) => { - log::info!("Loaded project!"); - let mut global = PROJECT.write().unwrap(); - *global = config; - // println!("Loaded config info: {:#?}", global); - self.scene_command = - SceneCommand::SwitchScene(String::from("editor")); - } - Err(e) => if e.to_string().contains("missing field") { - self.toast.add(egui_toast_fork::Toast { - kind: egui_toast_fork::ToastKind::Error, - text: format!("Your project version is not up to date with the current project version").into(), - options: ToastOptions::default() - .duration_in_seconds(10.0) - .show_progress(true), - ..Default::default() - }); - log::error!("Failed to load project: {}", e); - } - }; - } else { - log::warn!("File dialog returned \"None\""); - } - self.is_in_file_dialogue = false; - } - ui.add_space(20.0); - - if ui - .add_sized(button_size, egui::Button::new("Settings")) - .clicked() - { - log::debug!("Settings (not implemented)"); - } - ui.add_space(20.0); - - if ui - .add_sized(button_size, egui::Button::new("Quit")) - .clicked() - { - self.scene_command = SceneCommand::Quit - } - ui.add_space(20.0); - }); - }); - - let mut show_new_project = self.show_new_project; - egui::Window::new("Create new project") - .open(&mut show_new_project) - .resizable(true) - .collapsible(false) - .fixed_size(screen_size) - .show(&egui_ctx, |ui| { - ui.vertical(|ui| { - ui.heading("Project Name:"); - ui.add_space(5.0); - - ui.text_edit_singleline(&mut self.project_name); - ui.add_space(10.0); - - ui.heading("Project Location: "); - ui.add_space(5.0); - - if let Some(ref path) = self.project_path { - ui.label(format!("Chosen location: {}", path.display())); - ui.add_space(5.0); - } - - ui.add_space(5.0); - if ui.button("Choose Location").clicked() { - self.is_in_file_dialogue = true; - if let Some(path) = rfd::FileDialog::new() - .set_title("Save Project") - .set_file_name(&self.project_name) - .pick_folder() - { - self.project_path = Some(path); - log::debug!("Project will be saved at: {:?}", self.project_path); - } - self.is_in_file_dialogue = false; - } - - let can_create = self.project_path.is_some() && !self.project_name.is_empty(); - if ui - .add_enabled(can_create, egui::Button::new("Create Project")) - .clicked() - { - log::info!("Creating new project at {:?}", self.project_path); - self.start_project_creation(); - ui.ctx().request_repaint(); - } - }); - }); - self.show_new_project = show_new_project; - - if let Some(rx) = self.progress_rx.as_mut() { - while let Ok(progress) = rx.try_recv() { - match progress { - ProjectProgress::Step { progress, message } => { - self.progress = progress; - self.progress_message = message; - } - ProjectProgress::Error(err) => { - self.project_error.get_or_insert_with(Vec::new).push(err); - } - ProjectProgress::Done if self.project_error.is_none() => { - self.is_in_file_dialogue = false; - self.show_new_project = false; - self.show_progress = false; - self.scene_command = SceneCommand::SwitchScene("editor".to_string()); - } - ProjectProgress::Done => {} - } - } - } - - if self.show_progress { - egui::Window::new("Creating Project...") - .collapsible(true) - .resizable(false) - .fixed_size([400.0, 120.0]) - .show(&egui_ctx, |ui| { - ui.label(&self.progress_message); - ui.add_space(10.0); - - ui.add(egui::ProgressBar::new(self.progress).show_percentage()); - if let Some(errors) = &self.project_error { - ui.colored_label(egui::Color32::RED, "Errors:"); - for err in errors { - ui.label(err); - } - } - }); - } - - self.toast.show(&graphics.get_egui_context()); - } - - fn exit(&mut self, _event_loop: &ActiveEventLoop) { - log::info!("Exiting menu scene"); - } - - fn run_command(&mut self) -> SceneCommand { - std::mem::replace(&mut self.scene_command, SceneCommand::None) - } -} - -impl Keyboard for MainMenu { - fn key_down(&mut self, key: KeyCode, event_loop: &ActiveEventLoop) { - if key == KeyCode::Escape { - if !self.show_new_project && !self.is_in_file_dialogue { - event_loop.exit(); - } - } - } - - fn key_up(&mut self, _key: KeyCode, _event_loop: &ActiveEventLoop) {} -} - -impl Mouse for MainMenu { - fn mouse_move(&mut self, _position: PhysicalPosition<f64>) {} - - fn mouse_down(&mut self, _button: MouseButton) {} - - fn mouse_up(&mut self, _button: MouseButton) {} -} - -impl Controller for MainMenu { - fn button_down(&mut self, button: gilrs::Button, id: gilrs::GamepadId) { - debug!("Controller button {:?} pressed! [{}]", button, id); - } - - fn button_up(&mut self, button: gilrs::Button, id: gilrs::GamepadId) { - debug!("Controller button {:?} released! [{}]", button, id); - } - - fn left_stick_changed(&mut self, x: f32, y: f32, id: gilrs::GamepadId) { - debug!("Left stick changed: x = {} | y = {} | id = {}", x, y, id); - } - - fn right_stick_changed(&mut self, x: f32, y: f32, id: gilrs::GamepadId) { - debug!("Right stick changed: x = {} | y = {} | id = {}", x, y, id); - } - - fn on_connect(&mut self, id: gilrs::GamepadId) { - debug!("Controller connected [{}]", id); - } - - fn on_disconnect(&mut self, id: gilrs::GamepadId) { - debug!("Controller disconnected [{}]", id); - } -} @@ -1,232 +0,0 @@ -use std::collections::HashSet; -use std::sync::Arc; - -use dropbear_engine::async_trait::async_trait; -use dropbear_engine::camera::Camera; -use dropbear_engine::entity::{AdoptedEntity, Transform}; -use dropbear_engine::graphics::{Graphics, Shader}; -use dropbear_engine::hecs::World; -use dropbear_engine::input::Controller; -use dropbear_engine::nalgebra::{Point3, Vector3}; -use dropbear_engine::scene::SceneCommand; -use dropbear_engine::wgpu::{Color, RenderPipeline}; -use dropbear_engine::winit::dpi::PhysicalPosition; -use dropbear_engine::winit::event::MouseButton; -use dropbear_engine::winit::window::Window; -use dropbear_engine::{gilrs, hecs}; -use dropbear_engine::{ - input::{Keyboard, Mouse}, - log::debug, - scene::Scene, - winit::{event_loop::ActiveEventLoop, keyboard::KeyCode}, -}; - -// this file purely exists as reference. do not add to module, and probably will be deleted sooner or later - -pub struct TestingScene1 { - world: hecs::World, - render_pipeline: Option<RenderPipeline>, - camera: Camera, - pressed_keys: HashSet<KeyCode>, - is_cursor_locked: bool, - window: Option<Arc<Window>>, - scene_command: SceneCommand, -} - -impl TestingScene1 { - pub fn new() -> Self { - debug!("TestingScene1 instance created"); - Self { - world: World::new(), - is_cursor_locked: true, - render_pipeline: None, - camera: Camera::default(), - pressed_keys: Default::default(), - window: Default::default(), - scene_command: Default::default(), - } - } -} - -impl Scene for TestingScene1 { - fn load(&mut self, graphics: &mut Graphics) { - let shader = Shader::new( - graphics, - include_str!("../../dropbear-engine/resources/shaders/shader.wgsl"), - Some("default"), - ); - - let horse_model = - AdoptedEntity::new(graphics, "models/low_poly_horse.glb", Some("horse")).unwrap(); - - self.world.spawn((horse_model, Transform::default())); - - let camera = Camera::new( - graphics, - Point3::new(0.0, 1.0, 2.0), - Point3::new(0.0, 0.0, 0.0), - Vector3::y(), - (graphics.state.config.width / graphics.state.config.height) as f32, - 45.0, - 0.1, - 100.0, - 0.125, - 0.002, - ); - - let pipeline = graphics.create_render_pipline( - &shader, - vec![&graphics.state.texture_bind_layout, camera.layout()], - ); - - self.camera = camera; - self.window = Some(graphics.state.window.clone()); - - // ensure that this is the last line - self.render_pipeline = Some(pipeline); - } - - fn update(&mut self, _dt: f32, graphics: &mut Graphics) { - // hold down movement - for key in &self.pressed_keys { - match key { - KeyCode::KeyW => self.camera.move_forwards(), - KeyCode::KeyA => self.camera.move_left(), - KeyCode::KeyD => self.camera.move_right(), - KeyCode::KeyS => self.camera.move_back(), - KeyCode::ShiftLeft => self.camera.move_down(), - KeyCode::Space => self.camera.move_up(), - _ => {} - } - } - - if !self.is_cursor_locked { - self.window.as_mut().unwrap().set_cursor_visible(true); - } - - let query = self.world.query_mut::<(&mut AdoptedEntity, &Transform)>(); - for (_, (entity, transform)) in query { - entity.update(&graphics, transform); - } - - self.camera.update(graphics); - } - - fn render(&mut self, graphics: &mut Graphics) { - // cornflower blue - let color = Color { - r: 100.0 / 255.0, - g: 149.0 / 255.0, - b: 237.0 / 255.0, - a: 1.0, - }; - - // Note: new versions use egui rendering for viewport instead - // of using the full window - let texture_id = graphics.state.texture_id.clone(); - let (display_width, display_height) = graphics.screen_size; - egui::CentralPanel::default() - .frame(egui::Frame::new()) - .show(graphics.get_egui_context(), |ui| { - let rect = ui.max_rect(); - ui.put( - rect, - egui::Image::new((texture_id, [display_width, display_height].into())) - .fit_to_exact_size([display_width, display_height].into()), - ); - }); - - // render everything here - if let Some(pipeline) = &self.render_pipeline { - { - let mut query = self.world.query::<(&AdoptedEntity, &Transform)>(); - let mut render_pass = graphics.clear_colour(color); - render_pass.set_pipeline(pipeline); - - for (_, (entity, _)) in query.iter() { - entity.render(&mut render_pass, &self.camera); - } - } - } - - self.window = Some(graphics.state.window.clone()); - } - - fn exit(&mut self, _event_loop: &ActiveEventLoop) {} - - fn run_command(&mut self) -> SceneCommand { - std::mem::replace(&mut self.scene_command, SceneCommand::None) - } -} - -impl Keyboard for TestingScene1 { - fn key_down(&mut self, key: KeyCode, event_loop: &ActiveEventLoop) { - // debug!("Key pressed: {:?}", key); - match key { - KeyCode::Escape => event_loop.exit(), - KeyCode::F1 => self.is_cursor_locked = !self.is_cursor_locked, - // KeyCode::F2 => self.switch_to = Some("testing_scene_2".into()), - _ => { - self.pressed_keys.insert(key); - } - } - } - - fn key_up(&mut self, key: KeyCode, _event_loop: &ActiveEventLoop) { - // debug!("Key released: {:?}", key); - self.pressed_keys.remove(&key); - } -} - -impl Mouse for TestingScene1 { - fn mouse_down(&mut self, _button: MouseButton) { - // debug!("Mouse button pressed: {:?}", button) - } - - fn mouse_up(&mut self, _button: MouseButton) { - // debug!("Mouse button released: {:?}", button); - } - - fn mouse_move(&mut self, position: PhysicalPosition<f64>) { - if self.is_cursor_locked { - if let Some(window) = &self.window { - let size = window.inner_size(); - let center = - PhysicalPosition::new(size.width as f64 / 2.0, size.height as f64 / 2.0); - - let dx = position.x - center.x; - let dy = position.y - center.y; - self.camera.track_mouse_delta(dx as f32, dy as f32); - - window.set_cursor_position(center).ok(); - window.set_cursor_visible(false); - } - } - } -} - -impl Controller for TestingScene1 { - fn button_down(&mut self, button: gilrs::Button, id: gilrs::GamepadId) { - debug!("Controller button {:?} pressed! [{}]", button, id); - } - - fn button_up(&mut self, button: gilrs::Button, id: gilrs::GamepadId) { - debug!("Controller button {:?} released! [{}]", button, id); - } - - fn left_stick_changed(&mut self, x: f32, y: f32, id: gilrs::GamepadId) { - debug!("Left stick changed: x = {} | y = {} | id = {}", x, y, id); - } - - fn right_stick_changed(&mut self, x: f32, y: f32, id: gilrs::GamepadId) { - debug!("Right stick changed: x = {} | y = {} | id = {}", x, y, id); - } - - fn on_connect(&mut self, id: gilrs::GamepadId) { - debug!("Controller connected [{}]", id); - } - - fn on_disconnect(&mut self, id: gilrs::GamepadId) { - debug!("Controller disconnected [{}]", id); - } -} @@ -1,386 +0,0 @@ -use glam::DVec3; -use rustyscript::{serde_json, Runtime}; -use serde::{Deserialize, Serialize}; - -use crate::{camera::CameraType, scripting::ScriptableModule}; - -impl ScriptableModule for SerializableCamera { - fn register(runtime: &mut Runtime) -> anyhow::Result<()> { - runtime.register_function("moveForward", Self::move_forward)?; - runtime.register_function("moveBackward", Self::move_backward)?; - runtime.register_function("moveLeft", Self::move_left)?; - runtime.register_function("moveRight", Self::move_right)?; - runtime.register_function("moveUp", Self::move_up)?; - runtime.register_function("moveDown", Self::move_down)?; - - // Mouse look - runtime.register_function("trackMouseDelta", Self::track_mouse_delta)?; - - // Camera switching and management - runtime.register_function("switchToCameraByLabel", Self::switch_to_camera_by_label)?; - runtime.register_function("getActiveCameraLabel", Self::get_active_camera_label)?; - runtime.register_function("getAllCameraLabels", Self::get_all_camera_labels)?; - runtime.register_function("getCamerasByType", Self::get_cameras_by_type)?; - - // Multi-camera manipulation - runtime.register_function("manipulateCameraByLabel", Self::manipulate_camera_by_label)?; - runtime.register_function("getCameraByLabel", Self::get_camera_by_label)?; - runtime.register_function("setCameraByLabel", Self::set_camera_by_label)?; - - Ok(()) - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SerializableCamera { - pub label: String, - pub eye: DVec3, - pub target: DVec3, - pub up: DVec3, - pub aspect: f64, - pub fov: f64, - pub near: f64, - pub far: f64, - pub yaw: f64, - pub pitch: f64, - - pub speed: f64, - pub sensitivity: f64, - - pub camera_type: CameraType, -} - -impl SerializableCamera { - /// Moves the camera forward. - /// - /// # Parameters - /// * args[0] - Self as a Camera. The value of the speed can be adjusted - pub fn move_forward(args: &[serde_json::Value]) -> Result<serde_json::Value, rustyscript::Error> { - if args.len() != 1 { - return Err(rustyscript::Error::Runtime("moveForward requires 1 arguments".to_string())); - } - - let mut camera: SerializableCamera = serde_json::from_value(args[0].clone()) - .map_err(|e| rustyscript::Error::Runtime(format!("Invalid camera: {}", e)))?; - - let forward = (camera.target - camera.eye).normalize(); - camera.eye += forward * camera.speed; - camera.target += forward * camera.speed; - - serde_json::to_value(camera) - .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize Camera: {}", e))) - } - - /// Moves the camera back. - /// - /// # Parameters - /// * args[0] - Self as a Camera. The value of the speed can be adjusted - pub fn move_backward(args: &[serde_json::Value]) -> Result<serde_json::Value, rustyscript::Error> { - if args.len() != 1 { - return Err(rustyscript::Error::Runtime("moveBackward requires 1 arguments".to_string())); - } - - let mut camera: SerializableCamera = serde_json::from_value(args[0].clone()) - .map_err(|e| rustyscript::Error::Runtime(format!("Invalid camera: {}", e)))?; - - let forward = (camera.target - camera.eye).normalize(); - camera.eye -= forward * camera.speed; - camera.target -= forward * camera.speed; - - serde_json::to_value(camera) - .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize Camera: {}", e))) - } - - /// Moves the camera left. - /// - /// # Parameters - /// * args[0] - Self as a Camera. The value of the speed can be adjusted - pub fn move_left(args: &[serde_json::Value]) -> Result<serde_json::Value, rustyscript::Error> { - if args.len() != 1 { - return Err(rustyscript::Error::Runtime("moveLeft requires 1 arguments".to_string())); - } - - let mut camera: SerializableCamera = serde_json::from_value(args[0].clone()) - .map_err(|e| rustyscript::Error::Runtime(format!("Invalid camera: {}", e)))?; - - let forward = (camera.target - camera.eye).normalize(); - let right = camera.up.cross(forward).normalize(); - camera.eye -= right * camera.speed; - camera.target -= right * camera.speed; - - serde_json::to_value(camera) - .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize Camera: {}", e))) - } - - /// Moves the camera right. - /// - /// # Parameters - /// * args[0] - Self as a Camera. The value of the speed can be adjusted - pub fn move_right(args: &[serde_json::Value]) -> Result<serde_json::Value, rustyscript::Error> { - if args.len() != 1 { - return Err(rustyscript::Error::Runtime("moveRight requires 1 arguments".to_string())); - } - - let mut camera: SerializableCamera = serde_json::from_value(args[0].clone()) - .map_err(|e| rustyscript::Error::Runtime(format!("Invalid camera: {}", e)))?; - - let forward = (camera.target - camera.eye).normalize(); - let right = camera.up.cross(forward).normalize(); - camera.eye += right * camera.speed; - camera.target += right * camera.speed; - - serde_json::to_value(camera) - .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize Camera: {}", e))) - } - - /// Moves the camera up. - /// - /// # Parameters - /// * args[0] - Self as a Camera. The value of the speed can be adjusted - pub fn move_up(args: &[serde_json::Value]) -> Result<serde_json::Value, rustyscript::Error> { - if args.len() != 1 { - return Err(rustyscript::Error::Runtime("moveUp requires 1 arguments".to_string())); - } - - let mut camera: SerializableCamera = serde_json::from_value(args[0].clone()) - .map_err(|e| rustyscript::Error::Runtime(format!("Invalid camera: {}", e)))?; - - let up = camera.up.normalize(); - camera.eye += up * camera.speed; - camera.target += up * camera.speed; - - serde_json::to_value(camera) - .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize Camera: {}", e))) - } - - /// Moves the camera down. - /// - /// # Parameters - /// * args[0] - Self as a Camera. The value of the speed can be adjusted - pub fn move_down(args: &[serde_json::Value]) -> Result<serde_json::Value, rustyscript::Error> { - if args.len() != 1 { - return Err(rustyscript::Error::Runtime("moveDown requires 1 arguments".to_string())); - } - - let mut camera: SerializableCamera = serde_json::from_value(args[0].clone()) - .map_err(|e| rustyscript::Error::Runtime(format!("Invalid camera: {}", e)))?; - - let up = camera.up.normalize(); - camera.eye -= up * camera.speed; - camera.target -= up * camera.speed; - - serde_json::to_value(camera) - .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize Camera: {}", e))) - } - - /// Sets the tracking of the camera to the mouse delta - /// - /// # Parameters - /// * args[0] - Self as a Camera - /// * args[1] - The dx value of the mouse position as a number - /// * args[2] - The dy value of the mouse position as a number - pub fn track_mouse_delta(args: &[serde_json::Value]) -> Result<serde_json::Value, rustyscript::Error> { - if args.len() != 3 { - return Err(rustyscript::Error::Runtime("trackMouseDelta requires 3 arguments (camera, dx, dy)".to_string())); - } - - let mut camera: SerializableCamera = serde_json::from_value(args[0].clone()) - .map_err(|e| rustyscript::Error::Runtime(format!("Invalid camera: {}", e)))?; - - let dx = args[1].as_f64().ok_or_else(|| { - rustyscript::Error::Runtime("dx must be a number".to_string()) - })?; - - let dy = args[2].as_f64().ok_or_else(|| { - rustyscript::Error::Runtime("dy must be a number".to_string()) - })?; - - camera.yaw += dx * camera.sensitivity; - camera.pitch += dy * camera.sensitivity; - - camera.pitch = camera.pitch.clamp(-89.0_f64.to_radians(), 89.0_f64.to_radians()); - - let direction = DVec3::new( - camera.yaw.cos() * camera.pitch.cos(), - camera.pitch.sin(), - camera.yaw.sin() * camera.pitch.cos(), - ); - camera.target = camera.eye + direction; - - serde_json::to_value(camera) - .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize Camera: {}", e))) - } - - pub fn switch_to_camera_by_label(args: &[serde_json::Value]) -> Result<serde_json::Value, rustyscript::Error> { - if args.len() != 1 { - return Err(rustyscript::Error::Runtime("switchToCameraByLabel requires 1 argument (label)".to_string())); - } - - let label = args[0].as_str().ok_or_else(|| { - rustyscript::Error::Runtime("Label must be a string".to_string()) - })?; - - let action = CameraAction { - action: "switch_camera".to_string(), - label: Some(label.to_string()), - camera_type: None, - camera_data: None, - property: None, - value: None, - }; - - serde_json::to_value(action) - .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize camera action: {}", e))) - } - - /// Get the label of the currently active camera - pub fn get_active_camera_label(_args: &[serde_json::Value]) -> Result<serde_json::Value, rustyscript::Error> { - let action = CameraAction { - action: "get_active_camera".to_string(), - label: None, - camera_type: None, - camera_data: None, - property: None, - value: None, - }; - - serde_json::to_value(action) - .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize camera action: {}", e))) - } - - /// Get all camera labels in the world - pub fn get_all_camera_labels(_args: &[serde_json::Value]) -> Result<serde_json::Value, rustyscript::Error> { - let action = CameraAction { - action: "get_all_cameras".to_string(), - label: None, - camera_type: None, - camera_data: None, - property: None, - value: None, - }; - - serde_json::to_value(action) - .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize camera action: {}", e))) - } - - /// Get cameras by type - pub fn get_cameras_by_type(args: &[serde_json::Value]) -> Result<serde_json::Value, rustyscript::Error> { - if args.len() != 1 { - return Err(rustyscript::Error::Runtime("getCamerasByType requires 1 argument (camera_type)".to_string())); - } - - let camera_type = args[0].as_str().ok_or_else(|| { - rustyscript::Error::Runtime("Camera type must be a string".to_string()) - })?; - - // Validate camera type - match camera_type { - "Normal" | "Debug" | "Player" => { - let action = CameraAction { - action: "get_cameras_by_type".to_string(), - label: None, - camera_type: Some(camera_type.to_string()), - camera_data: None, - property: None, - value: None, - }; - - serde_json::to_value(action) - .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize camera action: {}", e))) - } - _ => Err(rustyscript::Error::Runtime("Invalid camera type. Use 'Normal', 'Debug', or 'Player'".to_string())) - } - } - - /// Manipulate a specific camera by label without switching to it - pub fn manipulate_camera_by_label(args: &[serde_json::Value]) -> Result<serde_json::Value, rustyscript::Error> { - if args.len() != 3 { - return Err(rustyscript::Error::Runtime("manipulateCameraByLabel requires 3 arguments (label, property, value)".to_string())); - } - - let label = args[0].as_str().ok_or_else(|| { - rustyscript::Error::Runtime("Label must be a string".to_string()) - })?; - - let property = args[1].as_str().ok_or_else(|| { - rustyscript::Error::Runtime("Property must be a string".to_string()) - })?; - - // Validate property - match property { - "position" | "target" | "speed" | "sensitivity" | "fov" | "yaw" | "pitch" => { - let action = CameraAction { - action: "manipulate_camera".to_string(), - label: Some(label.to_string()), - camera_type: None, - camera_data: None, - property: Some(property.to_string()), - value: Some(args[2].clone()), - }; - - serde_json::to_value(action) - .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize camera action: {}", e))) - } - _ => Err(rustyscript::Error::Runtime("Invalid property. Use 'position', 'target', 'speed', 'sensitivity', 'fov', 'yaw', or 'pitch'".to_string())) - } - } - - /// Get a camera's data by label - pub fn get_camera_by_label(args: &[serde_json::Value]) -> Result<serde_json::Value, rustyscript::Error> { - if args.len() != 1 { - return Err(rustyscript::Error::Runtime("getCameraByLabel requires 1 argument (label)".to_string())); - } - - let label = args[0].as_str().ok_or_else(|| { - rustyscript::Error::Runtime("Label must be a string".to_string()) - })?; - - let action = CameraAction { - action: "get_camera_by_label".to_string(), - label: Some(label.to_string()), - camera_type: None, - camera_data: None, - property: None, - value: None, - }; - - serde_json::to_value(action) - .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize camera action: {}", e))) - } - - /// Set a camera's complete data by label - pub fn set_camera_by_label(args: &[serde_json::Value]) -> Result<serde_json::Value, rustyscript::Error> { - if args.len() != 2 { - return Err(rustyscript::Error::Runtime("setCameraByLabel requires 2 arguments (label, camera_data)".to_string())); - } - - let label = args[0].as_str().ok_or_else(|| { - rustyscript::Error::Runtime("Label must be a string".to_string()) - })?; - - let camera_data: SerializableCamera = serde_json::from_value(args[1].clone()) - .map_err(|e| rustyscript::Error::Runtime(format!("Invalid camera data: {}", e)))?; - - let action = CameraAction { - action: "set_camera_by_label".to_string(), - label: Some(label.to_string()), - camera_type: None, - camera_data: Some(camera_data), - property: None, - value: None, - }; - - serde_json::to_value(action) - .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize camera action: {}", e))) - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CameraAction { - pub action: String, - pub label: Option<String>, - pub camera_type: Option<String>, - pub camera_data: Option<SerializableCamera>, - pub property: Option<String>, - pub value: Option<serde_json::Value>, -} @@ -1,332 +0,0 @@ -use crate::{scripting::ScriptableModule, states::{ModelProperties, PropertyValue}}; -use rustyscript::{serde_json, Runtime}; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Serialize, Deserialize)] -pub struct SerializableModelProperties { - pub properties: std::collections::HashMap<String, serde_json::Value>, -} - -impl From<&ModelProperties> for SerializableModelProperties { - fn from(props: &ModelProperties) -> Self { - let mut properties = std::collections::HashMap::new(); - - for (key, value) in props.custom_properties.iter() { - let json_value = match value { - PropertyValue::String(s) => serde_json::Value::String(s.clone()), - PropertyValue::Int(i) => serde_json::Value::Number(serde_json::Number::from(*i)), - PropertyValue::Float(f) => serde_json::Value::Number( - serde_json::Number::from_f64(*f as f64).unwrap_or(serde_json::Number::from(0)) - ), - PropertyValue::Bool(b) => serde_json::Value::Bool(*b), - PropertyValue::Vec3(v) => serde_json::Value::Array(vec![ - serde_json::Value::Number(serde_json::Number::from_f64(v[0] as f64).unwrap()), - serde_json::Value::Number(serde_json::Number::from_f64(v[1] as f64).unwrap()), - serde_json::Value::Number(serde_json::Number::from_f64(v[2] as f64).unwrap()), - ]), - }; - properties.insert(key.clone(), json_value); - } - - Self { properties } - } -} - -impl SerializableModelProperties { - pub fn to_model_properties(&self) -> ModelProperties { - let mut props = ModelProperties::default(); - - for (key, value) in &self.properties { - let property_value = match value { - serde_json::Value::String(s) => PropertyValue::String(s.clone()), - serde_json::Value::Number(n) => { - if let Some(i) = n.as_i64() { - PropertyValue::Int(i) - } else if let Some(f) = n.as_f64() { - PropertyValue::Float(f) - } else { - continue; - } - }, - serde_json::Value::Bool(b) => PropertyValue::Bool(*b), - serde_json::Value::Array(arr) => { - if arr.len() == 3 { - if let (Some(x), Some(y), Some(z)) = ( - arr[0].as_f64(), - arr[1].as_f64(), - arr[2].as_f64(), - ) { - PropertyValue::Vec3([x as f32, y as f32, z as f32]) - } else { - continue; - } - } else { - continue; - } - }, - _ => continue, - }; - props.set_property(key.clone(), property_value); - } - - props - } -} - -impl ScriptableModule for ModelProperties { - fn register(runtime: &mut Runtime) -> anyhow::Result<()> { - runtime.register_function("getProperty", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { - if args.len() != 2 { - return Err(rustyscript::Error::Runtime("getProperty requires 2 arguments (properties, key)".to_string())); - } - - let props: SerializableModelProperties = serde_json::from_value(args[0].clone()) - .map_err(|e| rustyscript::Error::Runtime(format!("Invalid properties: {}", e)))?; - - let key = args[1].as_str() - .ok_or_else(|| rustyscript::Error::Runtime("Key must be a string".to_string()))?; - - let value = props.properties.get(key) - .cloned() - .unwrap_or(serde_json::Value::Null); - - Ok(value) - })?; - - // Set property (string) - runtime.register_function("setPropertyString", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { - if args.len() != 3 { - return Err(rustyscript::Error::Runtime("setPropertyString requires 3 arguments (properties, key, value)".to_string())); - } - - let mut props: SerializableModelProperties = serde_json::from_value(args[0].clone()) - .map_err(|e| rustyscript::Error::Runtime(format!("Invalid properties: {}", e)))?; - - let key = args[1].as_str() - .ok_or_else(|| rustyscript::Error::Runtime("Key must be a string".to_string()))?; - - let value = args[2].as_str() - .ok_or_else(|| rustyscript::Error::Runtime("Value must be a string".to_string()))?; - - props.properties.insert(key.to_string(), serde_json::Value::String(value.to_string())); - - serde_json::to_value(props) - .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize properties: {}", e))) - })?; - - // Set property (int) - runtime.register_function("setPropertyInt", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { - if args.len() != 3 { - return Err(rustyscript::Error::Runtime("setPropertyInt requires 3 arguments (properties, key, value)".to_string())); - } - - let mut props: SerializableModelProperties = serde_json::from_value(args[0].clone()) - .map_err(|e| rustyscript::Error::Runtime(format!("Invalid properties: {}", e)))?; - - let key = args[1].as_str() - .ok_or_else(|| rustyscript::Error::Runtime("Key must be a string".to_string()))?; - - let value = args[2].as_i64() - .ok_or_else(|| rustyscript::Error::Runtime("Value must be an integer".to_string()))?; - - props.properties.insert(key.to_string(), serde_json::Value::Number(serde_json::Number::from(value))); - - serde_json::to_value(props) - .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize properties: {}", e))) - })?; - - // Set property (float) - runtime.register_function("setPropertyFloat", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { - if args.len() != 3 { - return Err(rustyscript::Error::Runtime("setPropertyFloat requires 3 arguments (properties, key, value)".to_string())); - } - - let mut props: SerializableModelProperties = serde_json::from_value(args[0].clone()) - .map_err(|e| rustyscript::Error::Runtime(format!("Invalid properties: {}", e)))?; - - let key = args[1].as_str() - .ok_or_else(|| rustyscript::Error::Runtime("Key must be a string".to_string()))?; - - let value = args[2].as_f64() - .ok_or_else(|| rustyscript::Error::Runtime("Value must be a number".to_string()))?; - - props.properties.insert(key.to_string(), serde_json::Value::Number( - serde_json::Number::from_f64(value).unwrap_or(serde_json::Number::from(0)) - )); - - serde_json::to_value(props) - .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize properties: {}", e))) - })?; - - // Set property (bool) - runtime.register_function("setPropertyBool", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { - if args.len() != 3 { - return Err(rustyscript::Error::Runtime("setPropertyBool requires 3 arguments (properties, key, value)".to_string())); - } - - let mut props: SerializableModelProperties = serde_json::from_value(args[0].clone()) - .map_err(|e| rustyscript::Error::Runtime(format!("Invalid properties: {}", e)))?; - - let key = args[1].as_str() - .ok_or_else(|| rustyscript::Error::Runtime("Key must be a string".to_string()))?; - - let value = args[2].as_bool() - .ok_or_else(|| rustyscript::Error::Runtime("Value must be a boolean".to_string()))?; - - props.properties.insert(key.to_string(), serde_json::Value::Bool(value)); - - serde_json::to_value(props) - .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize properties: {}", e))) - })?; - - // Set property (Vec3) - runtime.register_function("setPropertyVec3", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { - if args.len() != 3 { - return Err(rustyscript::Error::Runtime("setPropertyVec3 requires 3 arguments (properties, key, value)".to_string())); - } - - let mut props: SerializableModelProperties = serde_json::from_value(args[0].clone()) - .map_err(|e| rustyscript::Error::Runtime(format!("Invalid properties: {}", e)))?; - - let key = args[1].as_str() - .ok_or_else(|| rustyscript::Error::Runtime("Key must be a string".to_string()))?; - - let value = args[2].as_array() - .ok_or_else(|| rustyscript::Error::Runtime("Value must be an array".to_string()))?; - - if value.len() != 3 { - return Err(rustyscript::Error::Runtime("Vec3 array must have exactly 3 elements".to_string())); - } - - let x = value[0].as_f64().ok_or_else(|| rustyscript::Error::Runtime("Vec3 elements must be numbers".to_string()))?; - let y = value[1].as_f64().ok_or_else(|| rustyscript::Error::Runtime("Vec3 elements must be numbers".to_string()))?; - let z = value[2].as_f64().ok_or_else(|| rustyscript::Error::Runtime("Vec3 elements must be numbers".to_string()))?; - - let vec3_array = serde_json::Value::Array(vec![ - serde_json::Value::Number(serde_json::Number::from_f64(x).unwrap()), - serde_json::Value::Number(serde_json::Number::from_f64(y).unwrap()), - serde_json::Value::Number(serde_json::Number::from_f64(z).unwrap()), - ]); - - props.properties.insert(key.to_string(), vec3_array); - - serde_json::to_value(props) - .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize properties: {}", e))) - })?; - - // Typed getters - runtime.register_function("getString", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { - if args.len() != 2 { - return Err(rustyscript::Error::Runtime("getString requires 2 arguments (properties, key)".to_string())); - } - - let props: SerializableModelProperties = serde_json::from_value(args[0].clone()) - .map_err(|e| rustyscript::Error::Runtime(format!("Invalid properties: {}", e)))?; - - let key = args[1].as_str() - .ok_or_else(|| rustyscript::Error::Runtime("Key must be a string".to_string()))?; - - let value = props.properties.get(key) - .and_then(|v| v.as_str()) - .unwrap_or("") - .to_string(); - - Ok(serde_json::Value::String(value)) - })?; - - runtime.register_function("getInt", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { - if args.len() != 2 { - return Err(rustyscript::Error::Runtime("getInt requires 2 arguments (properties, key)".to_string())); - } - - let props: SerializableModelProperties = serde_json::from_value(args[0].clone()) - .map_err(|e| rustyscript::Error::Runtime(format!("Invalid properties: {}", e)))?; - - let key = args[1].as_str() - .ok_or_else(|| rustyscript::Error::Runtime("Key must be a string".to_string()))?; - - let value = props.properties.get(key) - .and_then(|v| v.as_i64()) - .unwrap_or(0); - - Ok(serde_json::Value::Number(serde_json::Number::from(value))) - })?; - - runtime.register_function("getFloat", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { - if args.len() != 2 { - return Err(rustyscript::Error::Runtime("getFloat requires 2 arguments (properties, key)".to_string())); - } - - let props: SerializableModelProperties = serde_json::from_value(args[0].clone()) - .map_err(|e| rustyscript::Error::Runtime(format!("Invalid properties: {}", e)))?; - - let key = args[1].as_str() - .ok_or_else(|| rustyscript::Error::Runtime("Key must be a string".to_string()))?; - - let value = props.properties.get(key) - .and_then(|v| v.as_f64()) - .unwrap_or(0.0); - - Ok(serde_json::Value::Number(serde_json::Number::from_f64(value).unwrap())) - })?; - - runtime.register_function("getBool", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { - if args.len() != 2 { - return Err(rustyscript::Error::Runtime("getBool requires 2 arguments (properties, key)".to_string())); - } - - let props: SerializableModelProperties = serde_json::from_value(args[0].clone()) - .map_err(|e| rustyscript::Error::Runtime(format!("Invalid properties: {}", e)))?; - - let key = args[1].as_str() - .ok_or_else(|| rustyscript::Error::Runtime("Key must be a string".to_string()))?; - - let value = props.properties.get(key) - .and_then(|v| v.as_bool()) - .unwrap_or(false); - - Ok(serde_json::Value::Bool(value)) - })?; - - runtime.register_function("getVec3", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { - if args.len() != 2 { - return Err(rustyscript::Error::Runtime("getVec3 requires 2 arguments (properties, key)".to_string())); - } - - let props: SerializableModelProperties = serde_json::from_value(args[0].clone()) - .map_err(|e| rustyscript::Error::Runtime(format!("Invalid properties: {}", e)))?; - - let key = args[1].as_str() - .ok_or_else(|| rustyscript::Error::Runtime("Key must be a string".to_string()))?; - - let value = props.properties.get(key) - .and_then(|v| v.as_array()) - .cloned() - .unwrap_or_else(|| vec![ - serde_json::Value::Number(serde_json::Number::from(0)), - serde_json::Value::Number(serde_json::Number::from(0)), - serde_json::Value::Number(serde_json::Number::from(0)), - ]); - - Ok(serde_json::Value::Array(value)) - })?; - - runtime.register_function("hasProperty", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { - if args.len() != 2 { - return Err(rustyscript::Error::Runtime("hasProperty requires 2 arguments (properties, key)".to_string())); - } - - let props: SerializableModelProperties = serde_json::from_value(args[0].clone()) - .map_err(|e| rustyscript::Error::Runtime(format!("Invalid properties: {}", e)))?; - - let key = args[1].as_str() - .ok_or_else(|| rustyscript::Error::Runtime("Key must be a string".to_string()))?; - - let has_property = props.properties.contains_key(key); - Ok(serde_json::Value::Bool(has_property)) - })?; - - log::info!("[Script] Initialised entity custom properties module"); - Ok(()) - } -} @@ -1,288 +0,0 @@ -use std::{collections::{HashMap, HashSet}, time::{Duration, Instant}}; - -use rustyscript::{serde_json, Runtime}; -use serde::{Deserialize, Serialize}; -use winit::{event::MouseButton, keyboard::KeyCode}; - -use crate::scripting::ScriptableModule; - -#[derive(Clone)] -pub struct InputState { - #[allow(dead_code)] - pub last_key_press_times: HashMap<KeyCode, Instant>, - #[allow(dead_code)] - pub double_press_threshold: Duration, - pub mouse_pos: (f64, f64), - pub mouse_button: HashSet<MouseButton>, - pub pressed_keys: HashSet<KeyCode>, - pub mouse_delta: Option<(f64, f64)>, - pub is_cursor_locked: bool, -} - -#[derive(Clone, Serialize, Deserialize)] -pub struct SerializableInputState { - pub mouse_pos: (f64, f64), - pub pressed_keys: Vec<String>, - pub mouse_delta: Option<(f64, f64)>, - pub is_cursor_locked: bool, -} - -impl From<&InputState> for SerializableInputState { - fn from(input_state: &InputState) -> Self { - Self { - mouse_pos: input_state.mouse_pos, - pressed_keys: input_state.pressed_keys.iter() - .filter_map(|key| keycode_to_string(*key)) - .collect(), - mouse_delta: input_state.mouse_delta, - is_cursor_locked: input_state.is_cursor_locked, - } - } -} - -impl Default for InputState { - fn default() -> Self { - Self::new() - } -} - -impl ScriptableModule for InputState { - fn register(runtime: &mut Runtime) -> anyhow::Result<()> { - runtime.register_function("isKeyPressed", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { - if args.len() != 2 { - return Err(rustyscript::Error::Runtime("isKeyPressed requires 2 arguments (inputState, keyCode)".to_string())); - } - - let input_state: SerializableInputState = serde_json::from_value(args[0].clone()) - .map_err(|e| rustyscript::Error::Runtime(format!("Invalid input state: {}", e)))?; - - let key_code_str = args[1].as_str() - .ok_or_else(|| rustyscript::Error::Runtime("Key code must be a string".to_string()))?; - - let is_pressed = input_state.pressed_keys.contains(&key_code_str.to_string()); - Ok(serde_json::Value::Bool(is_pressed)) - })?; - - runtime.register_function("getMouseX", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { - if args.len() != 1 { - return Err(rustyscript::Error::Runtime("getMouseX requires 1 argument (inputState)".to_string())); - } - - let input_state: SerializableInputState = serde_json::from_value(args[0].clone()) - .map_err(|e| rustyscript::Error::Runtime(format!("Invalid input state: {}", e)))?; - - Ok(serde_json::Value::Number(serde_json::Number::from_f64(input_state.mouse_pos.0).unwrap())) - })?; - - runtime.register_function("getMouseY", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { - if args.len() != 1 { - return Err(rustyscript::Error::Runtime("getMouseY requires 1 argument (inputState)".to_string())); - } - - let input_state: SerializableInputState = serde_json::from_value(args[0].clone()) - .map_err(|e| rustyscript::Error::Runtime(format!("Invalid input state: {}", e)))?; - - Ok(serde_json::Value::Number(serde_json::Number::from_f64(input_state.mouse_pos.1).unwrap())) - })?; - - runtime.register_function("getMouseDeltaX", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { - if args.len() != 1 { - return Err(rustyscript::Error::Runtime("getMouseDeltaX requires 1 argument (inputState)".to_string())); - } - - let input_state: SerializableInputState = serde_json::from_value(args[0].clone()) - .map_err(|e| rustyscript::Error::Runtime(format!("Invalid input state: {}", e)))?; - - let delta_x = input_state.mouse_delta.map(|d| d.0).unwrap_or(0.0); - Ok(serde_json::Value::Number(serde_json::Number::from_f64(delta_x).unwrap())) - })?; - - runtime.register_function("getMouseDeltaY", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { - if args.len() != 1 { - return Err(rustyscript::Error::Runtime("getMouseDeltaY requires 1 argument (inputState)".to_string())); - } - - let input_state: SerializableInputState = serde_json::from_value(args[0].clone()) - .map_err(|e| rustyscript::Error::Runtime(format!("Invalid input state: {}", e)))?; - - let delta_y = input_state.mouse_delta.map(|d| d.1).unwrap_or(0.0); - Ok(serde_json::Value::Number(serde_json::Number::from_f64(delta_y).unwrap())) - })?; - - runtime.register_function("lockCursor", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { - if args.len() != 1 { - return Err(rustyscript::Error::Runtime("lockCursor requires 1 argument (locked)".to_string())); - } - - let _locked = args[0].as_bool() - .ok_or_else(|| rustyscript::Error::Runtime("Locked parameter must be a boolean".to_string()))?; - - // This function would need to communicate back to the engine to actually lock the cursor - // For now, just return success - Ok(serde_json::Value::Bool(true)) - })?; - - log::info!("[Script] Initialised input module"); - Ok(()) - } -} - -impl InputState { - pub fn new() -> Self { - Self { - mouse_pos: Default::default(), - mouse_button: Default::default(), - pressed_keys: HashSet::new(), - last_key_press_times: HashMap::new(), - double_press_threshold: Duration::from_millis(300), - mouse_delta: None, - is_cursor_locked: false, - } - } - - pub fn lock_cursor(&mut self, toggle: bool) { - self.is_cursor_locked = toggle; - } -} - -/// Helper function to convert string to KeyCode -#[allow(dead_code)] -fn string_to_keycode(key_str: &str) -> Option<KeyCode> { - match key_str { - "KeyA" => Some(KeyCode::KeyA), - "KeyB" => Some(KeyCode::KeyB), - "KeyC" => Some(KeyCode::KeyC), - "KeyD" => Some(KeyCode::KeyD), - "KeyE" => Some(KeyCode::KeyE), - "KeyF" => Some(KeyCode::KeyF), - "KeyG" => Some(KeyCode::KeyG), - "KeyH" => Some(KeyCode::KeyH), - "KeyI" => Some(KeyCode::KeyI), - "KeyJ" => Some(KeyCode::KeyJ), - "KeyK" => Some(KeyCode::KeyK), - "KeyL" => Some(KeyCode::KeyL), - "KeyM" => Some(KeyCode::KeyM), - "KeyN" => Some(KeyCode::KeyN), - "KeyO" => Some(KeyCode::KeyO), - "KeyP" => Some(KeyCode::KeyP), - "KeyQ" => Some(KeyCode::KeyQ), - "KeyR" => Some(KeyCode::KeyR), - "KeyS" => Some(KeyCode::KeyS), - "KeyT" => Some(KeyCode::KeyT), - "KeyU" => Some(KeyCode::KeyU), - "KeyV" => Some(KeyCode::KeyV), - "KeyW" => Some(KeyCode::KeyW), - "KeyX" => Some(KeyCode::KeyX), - "KeyY" => Some(KeyCode::KeyY), - "KeyZ" => Some(KeyCode::KeyZ), - "Space" => Some(KeyCode::Space), - "ShiftLeft" => Some(KeyCode::ShiftLeft), - "ShiftRight" => Some(KeyCode::ShiftRight), - "ControlLeft" => Some(KeyCode::ControlLeft), - "ControlRight" => Some(KeyCode::ControlRight), - "AltLeft" => Some(KeyCode::AltLeft), - "AltRight" => Some(KeyCode::AltRight), - "Escape" => Some(KeyCode::Escape), - "Enter" => Some(KeyCode::Enter), - "Tab" => Some(KeyCode::Tab), - "ArrowUp" => Some(KeyCode::ArrowUp), - "ArrowDown" => Some(KeyCode::ArrowDown), - "ArrowLeft" => Some(KeyCode::ArrowLeft), - "ArrowRight" => Some(KeyCode::ArrowRight), - "Digit0" => Some(KeyCode::Digit0), - "Digit1" => Some(KeyCode::Digit1), - "Digit2" => Some(KeyCode::Digit2), - "Digit3" => Some(KeyCode::Digit3), - "Digit4" => Some(KeyCode::Digit4), - "Digit5" => Some(KeyCode::Digit5), - "Digit6" => Some(KeyCode::Digit6), - "Digit7" => Some(KeyCode::Digit7), - "Digit8" => Some(KeyCode::Digit8), - "Digit9" => Some(KeyCode::Digit9), - "F1" => Some(KeyCode::F1), - "F2" => Some(KeyCode::F2), - "F3" => Some(KeyCode::F3), - "F4" => Some(KeyCode::F4), - "F5" => Some(KeyCode::F5), - "F6" => Some(KeyCode::F6), - "F7" => Some(KeyCode::F7), - "F8" => Some(KeyCode::F8), - "F9" => Some(KeyCode::F9), - "F10" => Some(KeyCode::F10), - "F11" => Some(KeyCode::F11), - "F12" => Some(KeyCode::F12), - _ => None, - } -} - -/// Helper function to convert KeyCode to string -fn keycode_to_string(key_code: KeyCode) -> Option<String> { - match key_code { - KeyCode::KeyA => Some("KeyA".to_string()), - KeyCode::KeyB => Some("KeyB".to_string()), - KeyCode::KeyC => Some("KeyC".to_string()), - KeyCode::KeyD => Some("KeyD".to_string()), - KeyCode::KeyE => Some("KeyE".to_string()), - KeyCode::KeyF => Some("KeyF".to_string()), - KeyCode::KeyG => Some("KeyG".to_string()), - KeyCode::KeyH => Some("KeyH".to_string()), - KeyCode::KeyI => Some("KeyI".to_string()), - KeyCode::KeyJ => Some("KeyJ".to_string()), - KeyCode::KeyK => Some("KeyK".to_string()), - KeyCode::KeyL => Some("KeyL".to_string()), - KeyCode::KeyM => Some("KeyM".to_string()), - KeyCode::KeyN => Some("KeyN".to_string()), - KeyCode::KeyO => Some("KeyO".to_string()), - KeyCode::KeyP => Some("KeyP".to_string()), - KeyCode::KeyQ => Some("KeyQ".to_string()), - KeyCode::KeyR => Some("KeyR".to_string()), - KeyCode::KeyS => Some("KeyS".to_string()), - KeyCode::KeyT => Some("KeyT".to_string()), - KeyCode::KeyU => Some("KeyU".to_string()), - KeyCode::KeyV => Some("KeyV".to_string()), - KeyCode::KeyW => Some("KeyW".to_string()), - KeyCode::KeyX => Some("KeyX".to_string()), - KeyCode::KeyY => Some("KeyY".to_string()), - KeyCode::KeyZ => Some("KeyZ".to_string()), - KeyCode::Space => Some("Space".to_string()), - KeyCode::ShiftLeft => Some("ShiftLeft".to_string()), - KeyCode::ShiftRight => Some("ShiftRight".to_string()), - KeyCode::ControlLeft => Some("ControlLeft".to_string()), - KeyCode::ControlRight => Some("ControlRight".to_string()), - KeyCode::AltLeft => Some("AltLeft".to_string()), - KeyCode::AltRight => Some("AltRight".to_string()), - KeyCode::Escape => Some("Escape".to_string()), - KeyCode::Enter => Some("Enter".to_string()), - KeyCode::Tab => Some("Tab".to_string()), - KeyCode::ArrowUp => Some("ArrowUp".to_string()), - KeyCode::ArrowDown => Some("ArrowDown".to_string()), - KeyCode::ArrowLeft => Some("ArrowLeft".to_string()), - KeyCode::ArrowRight => Some("ArrowRight".to_string()), - KeyCode::Digit0 => Some("Digit0".to_string()), - KeyCode::Digit1 => Some("Digit1".to_string()), - KeyCode::Digit2 => Some("Digit2".to_string()), - KeyCode::Digit3 => Some("Digit3".to_string()), - KeyCode::Digit4 => Some("Digit4".to_string()), - KeyCode::Digit5 => Some("Digit5".to_string()), - KeyCode::Digit6 => Some("Digit6".to_string()), - KeyCode::Digit7 => Some("Digit7".to_string()), - KeyCode::Digit8 => Some("Digit8".to_string()), - KeyCode::Digit9 => Some("Digit9".to_string()), - KeyCode::F1 => Some("F1".to_string()), - KeyCode::F2 => Some("F2".to_string()), - KeyCode::F3 => Some("F3".to_string()), - KeyCode::F4 => Some("F4".to_string()), - KeyCode::F5 => Some("F5".to_string()), - KeyCode::F6 => Some("F6".to_string()), - KeyCode::F7 => Some("F7".to_string()), - KeyCode::F8 => Some("F8".to_string()), - KeyCode::F9 => Some("F9".to_string()), - KeyCode::F10 => Some("F10".to_string()), - KeyCode::F11 => Some("F11".to_string()), - KeyCode::F12 => Some("F12".to_string()), - _ => None, - } -} - -#[derive(Clone, Copy)] -pub struct Key(pub KeyCode); @@ -1,11 +0,0 @@ -use rustyscript::Runtime; - -use crate::scripting::ScriptableModule; - -pub struct Lighting; - -impl ScriptableModule for Lighting { - fn register(_runtime: &mut Runtime) -> anyhow::Result<()> { - Ok(()) - } -} @@ -1,166 +0,0 @@ -use dropbear_engine::entity::Transform; -use glam::{DQuat, DVec3}; -use rustyscript::{serde_json, Runtime}; - -use crate::scripting::ScriptableModule; - -pub struct Math; - -impl ScriptableModule for Math { - fn register(runtime: &mut Runtime) -> anyhow::Result<()> { - runtime.register_function("createTransform", |_args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { - let transform = Transform::new(); - serde_json::to_value(transform) - .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize Transform: {}", e))) - })?; - - runtime.register_function("transformTranslate", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { - if args.len() != 2 { - return Err(rustyscript::Error::Runtime("transformTranslate requires 2 arguments".to_string())); - } - - let mut transform: Transform = serde_json::from_value(args[0].clone()) - .map_err(|e| rustyscript::Error::Runtime(format!("Invalid transform: {}", e)))?; - - let translation = if let Some(array) = args[1].as_array() { - if array.len() != 3 { - return Err(rustyscript::Error::Runtime("Translation array must have 3 elements".to_string())); - } - DVec3::new( - array[0].as_f64().unwrap_or(0.0), - array[1].as_f64().unwrap_or(0.0), - array[2].as_f64().unwrap_or(0.0), - ) - } else { - return Err(rustyscript::Error::Runtime("Translation must be an array".to_string())); - }; - - transform.position += translation; - serde_json::to_value(transform) - .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize Transform: {}", e))) - })?; - - runtime.register_function("transformRotateX", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { - if args.len() != 2 { - return Err(rustyscript::Error::Runtime("transformRotateX requires 2 arguments".to_string())); - } - - let mut transform: Transform = serde_json::from_value(args[0].clone()) - .map_err(|e| rustyscript::Error::Runtime(format!("Invalid transform: {}", e)))?; - - let angle = args[1].as_f64().unwrap_or(0.0); - let rotation = DQuat::from_rotation_x(angle); - transform.rotation = rotation * transform.rotation; - - serde_json::to_value(transform) - .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize Transform: {}", e))) - })?; - - runtime.register_function("transformRotateY", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { - if args.len() != 2 { - return Err(rustyscript::Error::Runtime("transformRotateY requires 2 arguments".to_string())); - } - - let mut transform: Transform = serde_json::from_value(args[0].clone()) - .map_err(|e| rustyscript::Error::Runtime(format!("Invalid transform: {}", e)))?; - - let angle = args[1].as_f64().unwrap_or(0.0); - let rotation = DQuat::from_rotation_y(angle); - transform.rotation = rotation * transform.rotation; - - serde_json::to_value(transform) - .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize Transform: {}", e))) - })?; - - runtime.register_function("transformRotateZ", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { - if args.len() != 2 { - return Err(rustyscript::Error::Runtime("transformRotateZ requires 2 arguments".to_string())); - } - - let mut transform: Transform = serde_json::from_value(args[0].clone()) - .map_err(|e| rustyscript::Error::Runtime(format!("Invalid transform: {}", e)))?; - - let angle = args[1].as_f64().unwrap_or(0.0); - let rotation = DQuat::from_rotation_z(angle); - transform.rotation = rotation * transform.rotation; - - serde_json::to_value(transform) - .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize Transform: {}", e))) - })?; - - runtime.register_function("transformScale", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { - if args.len() != 2 { - return Err(rustyscript::Error::Runtime("transformScale requires 2 arguments".to_string())); - } - - let mut transform: Transform = serde_json::from_value(args[0].clone()) - .map_err(|e| rustyscript::Error::Runtime(format!("Invalid transform: {}", e)))?; - - let scale = if let Some(num) = args[1].as_f64() { - DVec3::splat(num) - } else if let Some(array) = args[1].as_array() { - if array.len() != 3 { - return Err(rustyscript::Error::Runtime("Scale array must have 3 elements".to_string())); - } - DVec3::new( - array[0].as_f64().unwrap_or(1.0), - array[1].as_f64().unwrap_or(1.0), - array[2].as_f64().unwrap_or(1.0), - ) - } else { - return Err(rustyscript::Error::Runtime("Scale must be a number or array".to_string())); - }; - - transform.scale *= scale; - serde_json::to_value(transform) - .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize Transform: {}", e))) - })?; - - // this shouldn't be here as there is no need for a matrix... - runtime.register_function("transformMatrix", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { - if args.len() != 1 { - return Err(rustyscript::Error::Runtime("transformMatrix requires 1 argument".to_string())); - } - - let transform: Transform = serde_json::from_value(args[0].clone()) - .map_err(|e| rustyscript::Error::Runtime(format!("Invalid transform: {}", e)))?; - - let matrix = transform.matrix(); - serde_json::to_value(matrix) - .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize matrix: {}", e))) - })?; - - runtime.register_function("createVec3", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { - let x = args.get(0).and_then(|v| v.as_f64()).unwrap_or(0.0); - let y = args.get(1).and_then(|v| v.as_f64()).unwrap_or(0.0); - let z = args.get(2).and_then(|v| v.as_f64()).unwrap_or(0.0); - - let vec3 = DVec3::new(x, y, z); - serde_json::to_value(vec3) - .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize Vec3: {}", e))) - })?; - - runtime.register_function("createQuatIdentity", |_args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { - let quat = DQuat::IDENTITY; - serde_json::to_value(quat) - .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize Quaternion: {}", e))) - })?; - - runtime.register_function("createQuatFromEuler", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { - if args.len() != 3 { - return Err(rustyscript::Error::Runtime("createQuatFromEuler requires 3 arguments".to_string())); - } - - let x = args[0].as_f64().unwrap_or(0.0); - let y = args[1].as_f64().unwrap_or(0.0); - let z = args[2].as_f64().unwrap_or(0.0); - - let quat = DQuat::from_euler(glam::EulerRot::XYZ, x, y, z); - serde_json::to_value(quat) - .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize Quaternion: {}", e))) - })?; - - log::info!("[Script] Initialised math module"); - Ok(()) - } -} @@ -1,833 +0,0 @@ -pub mod camera; -pub mod entity; -pub mod math; -pub mod input; -pub mod lighting; - -use dropbear_engine::camera::Camera; -use dropbear_engine::entity::{AdoptedEntity, Transform}; -use dropbear_engine::lighting::{Light, LightComponent}; -use glam::DVec3; -use hecs::World; -use rustyscript::{serde_json, Module, ModuleHandle, Runtime, RuntimeOptions}; -use std::path::PathBuf; -use std::{collections::HashMap, fs}; - -/// A trait that describes a module that can be registered. -pub trait ScriptableModule { - /// Registers the functions for the dropbear typescript API - fn register(runtime: &mut Runtime) -> anyhow::Result<()>; - // /// Gathers the information into a serializable format that can be sent over to the - // /// dropbear typescript API - // fn gather(world: &World, entity_id: hecs::Entity); - // /// Fetches the mutated information from the dropbear typescript module and applys it to the world - // fn release(world: &mut World, entity_id: hecs::Entity, data: &serde_json::Value) -> anyhow::Result<()>; -} - -use crate::camera::CameraComponent; -use crate::states::{EntityNode, ModelProperties, ScriptComponent, PROJECT, SOURCE}; - -pub const TEMPLATE_SCRIPT: &'static str = include_str!("../template.ts"); - -pub enum ScriptAction { - AttachScript { - script_path: PathBuf, - script_name: String, - }, - CreateAndAttachScript { - script_path: PathBuf, - script_name: String, - }, - RemoveScript, - EditScript, -} - -pub fn move_script_to_src(script_path: &PathBuf) -> anyhow::Result<PathBuf> { - let project_path = { - let project = PROJECT.read().unwrap(); - project.project_path.clone() - }; - - let src_path = { - let source_config = SOURCE.read().unwrap(); - source_config.path.clone() - }; - - let scripts_dir = src_path; - fs::create_dir_all(&scripts_dir)?; - - let filename = script_path - .file_name() - .ok_or_else(|| anyhow::anyhow!("Invalid script path: no filename"))?; - - let dest_path = scripts_dir.join(filename); - - if dest_path.exists() { - log::info!( - "Script file already exists at {:?}, returning existing path", - dest_path - ); - return Ok(dest_path); - } - - const MAX_RETRIES: usize = 5; - const RETRY_DELAY_MS: u64 = 60; - - let mut last_err: Option<std::io::Error> = None; - for attempt in 0..=MAX_RETRIES { - match fs::copy(script_path, &dest_path) { - Ok(_) => { - log::info!("Copied script from {:?} to {:?}", script_path, dest_path); - last_err = None; - break; - } - Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => { - log::warn!( - "Script file already exists at {:?}, continuing anyway", - dest_path - ); - last_err = None; - break; - } - Err(e) => { - if e.raw_os_error() == Some(32) && attempt < MAX_RETRIES { - log::warn!( - "Sharing violation copying script (attempt {}/{}). Retrying in {}ms: {}", - attempt + 1, - MAX_RETRIES, - RETRY_DELAY_MS, - e - ); - std::thread::sleep(std::time::Duration::from_millis(RETRY_DELAY_MS)); - last_err = Some(e); - continue; - } else { - return Err(e.into()); - } - } - } - } - if let Some(e) = last_err { - return Err(e.into()); - } - - { - let source_config = SOURCE.read().unwrap(); - source_config.write_to(&project_path)?; - } - - log::info!("Moved script from {:?} to {:?}", script_path, dest_path); - Ok(dest_path) -} - -pub fn convert_entity_to_group( - world: &World, - entity_id: hecs::Entity, -) -> anyhow::Result<EntityNode> { - if let Ok(mut query) = world.query_one::<(&AdoptedEntity, &Transform)>(entity_id) { - if let Some((adopted, _transform)) = query.get() { - let entity_name = adopted.model().label.clone(); - - let script_node = if let Ok(script) = world.get::<&ScriptComponent>(entity_id) { - Some(EntityNode::Script { - name: script.name.clone(), - path: script.path.clone(), - }) - } else { - None - }; - - let entity_node = EntityNode::Entity { - id: entity_id, - name: entity_name.clone(), - }; - - if let Some(script_node) = script_node { - Ok(EntityNode::Group { - name: entity_name, - children: vec![entity_node, script_node], - collapsed: false, - }) - } else { - Ok(entity_node) - } - } else { - Err(anyhow::anyhow!("Failed to get entity components")) - } - } else { - Err(anyhow::anyhow!("Failed to query entity {:?}", entity_id)) - } -} - -pub fn attach_script_to_entity( - world: &mut World, - entity_id: hecs::Entity, - script_component: ScriptComponent, -) -> anyhow::Result<()> { - if let Err(e) = world.insert_one(entity_id, script_component) { - return Err(anyhow::anyhow!("Failed to attach script to entity: {}", e)); - } - - log::info!("Successfully attached script to entity {:?}", entity_id); - Ok(()) -} - -pub struct ScriptManager { - pub runtime: Runtime, - compiled_scripts: HashMap<String, ModuleHandle>, - entity_script_data: HashMap<hecs::Entity, serde_json::Value>, -} - -impl ScriptManager { - pub fn new() -> anyhow::Result<Self> { - let mut runtime = Runtime::new(RuntimeOptions::default())?; - let dropbear_content = include_str!("../dropbear.ts"); - let dropbear_module = Module::new("./dropbear.ts", dropbear_content); - runtime.load_module(&dropbear_module)?; - log::debug!("Loaded dropbear module"); - - let mut result = Self { - runtime, - compiled_scripts: HashMap::new(), - entity_script_data: HashMap::new(), - }; - - result = result - .register_module::<input::InputState>()? - .register_module::<math::Math>()? - .register_module::<ModelProperties>()? - .register_module::<camera::SerializableCamera>()? - .register_module::<lighting::Lighting>()?; - - // Register utility functions - result.runtime.register_function("log", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { - let msg = args.get(0) - .and_then(|v| v.as_str()) - .unwrap_or("undefined"); - println!("[Script] {}", msg); - Ok(serde_json::Value::Null) - })?; - - result.runtime.register_function("time", |_args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { - let time = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs_f64(); - Ok(serde_json::Value::Number(serde_json::Number::from_f64(time).unwrap())) - })?; - log::debug!("Initialised ScriptManager"); - Ok(result) - } - - /// Allows for the registering of other modules to use with the dropbear typescript - /// API - /// - /// # Parameters - /// * A turbofish to a struct that uses the ScriptableModule trait - /// - /// # Examples - /// ```rust - /// let script_manager = ScriptManager::new()? - /// .register_module::<input::InputState>()? - /// .register_module::<camera::Camera>()? - /// .register_module::<lighting::LightingModule>()?; - /// ``` - pub fn register_module<T: ScriptableModule>(mut self) -> anyhow::Result<Self> { - T::register(&mut self.runtime)?; - Ok(self) - } - - pub fn init_entity_script( - &mut self, - entity_id: hecs::Entity, - script_name: &str, - world: &mut World, - input_state: &input::InputState, - ) -> anyhow::Result<()> { - log_once::debug_once!("init_entity_script: {} for {:?}", script_name, entity_id); - - if let Some(module) = self.compiled_scripts.get(script_name).cloned() { - // construct RawSceneData-like payload - let mut scene_map = serde_json::Map::new(); - - // entities array - let mut entities_arr: Vec<serde_json::Value> = Vec::new(); - for (id, (adopted, transform)) in world.query::<(&AdoptedEntity, &Transform)>().iter() { - // try to get ModelProperties for this entity - let props_value = if let Ok(mut pq) = world.query_one::<&ModelProperties>(id) { - if let Some(p) = pq.get() { - serde_json::to_value(p)? - } else { - serde_json::Value::Object(serde_json::Map::new()) - } - } else { - serde_json::Value::Object(serde_json::Map::new()) - }; - - let mut ent_obj = serde_json::Map::new(); - ent_obj.insert("label".to_string(), serde_json::Value::String(adopted.label().clone())); - - // properties object expected shape: { custom_properties: Record<string, any> } or full ModelProperties - ent_obj.insert("properties".to_string(), props_value); - - ent_obj.insert("transform".to_string(), serde_json::to_value(transform)?); - - entities_arr.push(serde_json::Value::Object(ent_obj)); - } - scene_map.insert("entities".to_string(), serde_json::Value::Array(entities_arr)); - - // cameras snapshot (keep minimal fields) - let mut cameras_arr: Vec<serde_json::Value> = Vec::new(); - for (_id, (cam, _comp)) in world.query::<(&Camera, &CameraComponent)>().iter() { - cameras_arr.push(serde_json::json!({ - "label": cam.label, - "data": { - "eye": cam.eye.to_array(), - "target": cam.target.to_array(), - "up": cam.up.to_array(), - "aspect": cam.aspect, - "fov": cam.fov_y, - "near": cam.znear, - "far": cam.zfar, - "yaw": cam.yaw, - "pitch": cam.pitch, - "speed": cam.speed, - "sensitivity": cam.sensitivity - } - })); - } - scene_map.insert("cameras".to_string(), serde_json::Value::Array(cameras_arr)); - - // lights snapshot (label only) - let mut lights_arr: Vec<serde_json::Value> = Vec::new(); - for (_id, (_transform, _light_comp, light)) in world.query::<(&Transform, &LightComponent, &Light)>().iter() { - lights_arr.push(serde_json::json!({ - "label": light.label(), - })); - } - scene_map.insert("lights".to_string(), serde_json::Value::Array(lights_arr)); - - // current entity label (if available) - if let Ok(mut q) = world.query_one::<&AdoptedEntity>(entity_id) { - if let Some(adopted) = q.get() { - scene_map.insert("current_entity".to_string(), serde_json::Value::String(adopted.label().clone())); - } - } - - // input state - let serializable_input = input::SerializableInputState::from(input_state); - let mut payload = serde_json::Map::new(); - payload.insert("scene".to_string(), serde_json::Value::Object(scene_map)); - payload.insert("input".to_string(), serde_json::to_value(&serializable_input)?); - - let script_data_value = serde_json::Value::Object(payload); - - // call onLoad (module scripts call dropbear.start(s) and return dropbear.end()) - match self.runtime.call_function::<serde_json::Value>(Some(&module), "onLoad", &vec![script_data_value.clone()]) { - Ok(result) => { - log::debug!("Called onLoad for entity {:?}", entity_id); - // apply whatever the script returned (expect Partial RawSceneData shape) - let _ = self.apply_script_data_to_world(entity_id, &result, world); - // store initial data (script-managed) - self.entity_script_data.insert(entity_id, result); - } - Err(e) => { - log::warn!("onLoad missing/failed for entity {:?}: {}", entity_id, e); - // store the payload we sent so future updates have context - self.entity_script_data.insert(entity_id, script_data_value); - } - } - - Ok(()) - } else { - Err(anyhow::anyhow!("Script '{}' not found", script_name)) - } - } - - pub fn update_entity_script( - &mut self, - entity_id: hecs::Entity, - script_name: &str, - world: &mut World, - input_state: &input::InputState, - dt: f32, - ) -> anyhow::Result<()> { - log_once::debug_once!("Update entity script name: {}", script_name); - - if let Some(module) = self.compiled_scripts.get(script_name).cloned() { - // Build the same RawSceneData-like payload as init - let mut scene_map = serde_json::Map::new(); - - let mut entities_arr: Vec<serde_json::Value> = Vec::new(); - for (id, (adopted, transform)) in world.query::<(&AdoptedEntity, &Transform)>().iter() { - let props_value = if let Ok(mut pq) = world.query_one::<&ModelProperties>(id) { - if let Some(p) = pq.get() { - serde_json::to_value(p)? - } else { - serde_json::Value::Object(serde_json::Map::new()) - } - } else { - serde_json::Value::Object(serde_json::Map::new()) - }; - - let mut ent_obj = serde_json::Map::new(); - ent_obj.insert("label".to_string(), serde_json::Value::String(adopted.label().clone())); - ent_obj.insert("properties".to_string(), props_value); - ent_obj.insert("transform".to_string(), serde_json::to_value(transform)?); - entities_arr.push(serde_json::Value::Object(ent_obj)); - } - scene_map.insert("entities".to_string(), serde_json::Value::Array(entities_arr)); - - // cameras - let mut cameras_arr: Vec<serde_json::Value> = Vec::new(); - for (_id, (cam, _comp)) in world.query::<(&Camera, &CameraComponent)>().iter() { - cameras_arr.push(serde_json::json!({ - "label": cam.label, - "data": { - "eye": cam.eye.to_array(), - "target": cam.target.to_array(), - "up": cam.up.to_array(), - "aspect": cam.aspect, - "fov": cam.fov_y, - "near": cam.znear, - "far": cam.zfar, - "yaw": cam.yaw, - "pitch": cam.pitch, - "speed": cam.speed, - "sensitivity": cam.sensitivity - } - })); - } - scene_map.insert("cameras".to_string(), serde_json::Value::Array(cameras_arr)); - - // lights - let mut lights_arr: Vec<serde_json::Value> = Vec::new(); - for (_id, (_transform, _light_comp, light)) in world.query::<(&Transform, &LightComponent, &Light)>().iter() { - lights_arr.push(serde_json::json!({ - "label": light.label(), - })); - } - scene_map.insert("lights".to_string(), serde_json::Value::Array(lights_arr)); - - // current entity label - if let Ok(mut q) = world.query_one::<&AdoptedEntity>(entity_id) { - if let Some(adopted) = q.get() { - scene_map.insert("current_entity".to_string(), serde_json::Value::String(adopted.label().clone())); - } - } - - let serializable_input = input::SerializableInputState::from(input_state); - let mut payload = serde_json::Map::new(); - payload.insert("scene".to_string(), serde_json::Value::Object(scene_map)); - payload.insert("input".to_string(), serde_json::to_value(&serializable_input)?); - - let script_data_value = serde_json::Value::Object(payload); - let dt_value = serde_json::Value::Number(serde_json::Number::from_f64(dt as f64).unwrap()); - - // call onUpdate(s, dt) - match self.runtime.call_function::<serde_json::Value>(Some(&module), "onUpdate", &vec![script_data_value.clone(), dt_value]) { - Ok(result) => { - log::trace!("Called update for entity {:?}", entity_id); - // delegate applying of returned data to the helper - let _ = self.apply_script_data_to_world(entity_id, &result, world); - // store latest returned data - self.entity_script_data.insert(entity_id, result); - } - Err(e) => { - log_once::error_once!("Script execution error for entity {:?}: {}", entity_id, e); - } - } - } else { - log_once::error_once!("Unable to fetch compiled scripts for entity {:?}. Script Name: {}", entity_id, script_name); - } - Ok(()) - } - - fn apply_script_data_to_world( - &self, - entity_id: hecs::Entity, - script_data: &serde_json::Value, - world: &mut World, - ) -> anyhow::Result<()> { - if let Some(obj) = script_data.as_object() { - if let Some(entities_val) = obj.get("entities").and_then(|v| v.as_array()) { - for ent in entities_val { - if let Some(ent_obj) = ent.as_object() { - if let Some(label) = ent_obj.get("label").and_then(|l| l.as_str()) { - if let Some(target_entity) = world - .query_mut::<(&AdoptedEntity,)>() - .into_iter() - .find_map(|(e, (adopted,))| { - if adopted.label() == label { - Some(e) - } else { - None - } - }) { - if let Some(transform_val) = ent_obj.get("transform") { - if let Ok(new_t) = serde_json::from_value::<Transform>(transform_val.clone()) { - if let Ok(mut tq) = world.query_one::<&mut Transform>(target_entity) { - if let Some(tref) = tq.get() { - *tref = new_t; - log::trace!("apply: updated transform for {:?}", target_entity); - } - } - } else { - log::trace!("apply: failed to deserialize transform for label '{}'", label); - } - } - if let Some(props_val) = ent_obj.get("properties") { - if let Ok(new_props) = serde_json::from_value::<ModelProperties>(props_val.clone()) { - if let Ok(mut pq) = world.query_one::<&mut ModelProperties>(target_entity) { - if let Some(p) = pq.get() { - *p = new_props; - log::trace!("apply: updated properties for {:?}", target_entity); - } - } else { - if let Err(e) = world.insert_one(target_entity, new_props) { - log::warn!("apply: failed to insert properties for {:?}: {}", target_entity, e); - } - } - } else { - log::trace!("apply: failed to deserialize properties for label '{}'", label); - } - } - } else { - log::trace!("apply: no entity in world matching label '{}'", label); - } - } - } - } - } else { - // No entities array: treat the object as direct payload for the single entity - // apply transform if present - if let Some(transform_value) = obj.get("transform") { - if let Ok(updated_transform) = serde_json::from_value::<Transform>(transform_value.clone()) { - if let Ok(mut transform_query) = world.query_one::<&mut Transform>(entity_id) { - if let Some(transform) = transform_query.get() { - *transform = updated_transform; - log::trace!("apply: Updated transform for entity {:?}", entity_id); - } - } - } - } - // apply properties if present (support "properties" or legacy "entity") - if let Some(props_value) = obj.get("properties").or_else(|| obj.get("entity")) { - if let Ok(updated_properties) = serde_json::from_value::<ModelProperties>(props_value.clone()) { - if let Ok(mut properties_query) = world.query_one::<&mut ModelProperties>(entity_id) { - if let Some(properties) = properties_query.get() { - *properties = updated_properties; - log::trace!("apply: Updated properties for entity {:?}", entity_id); - } - } else { - if let Err(e) = world.insert_one(entity_id, updated_properties) { - log::warn!("apply: Failed to insert updated properties for entity {:?}: {}", entity_id, e); - } - } - } - } - - // cameras: if script returned camera array with full data, apply by label - if let Some(cameras_val) = obj.get("cameras").and_then(|v| v.as_array()) { - for cam in cameras_val { - if let Some(cam_obj) = cam.as_object() { - if let Some(label) = cam_obj.get("label").and_then(|l| l.as_str()) { - if let Some(data_obj) = cam_obj.get("data").and_then(|d| d.as_object()) { - // find camera entity by label - if let Some(cam_entity) = world - .query::<&Camera>() - .iter() - .find_map(|(e, camera)| if camera.label == label { Some(e) } else { None }) { - if let Ok(mut cam_q) = world.query_one::<&mut Camera>(cam_entity) { - if let Some(cam_struct) = cam_q.get() { - // update common camera fields if present - if let Some(eye_val) = data_obj.get("eye") { - if let Ok(arr) = serde_json::from_value::<[f64; 3]>(eye_val.clone()) { - cam_struct.eye = DVec3::from_array(arr); - } - } - if let Some(target_val) = data_obj.get("target") { - if let Ok(arr) = serde_json::from_value::<[f64; 3]>(target_val.clone()) { - cam_struct.target = DVec3::from_array(arr); - } - } - if let Some(up_val) = data_obj.get("up") { - if let Ok(arr) = serde_json::from_value::<[f64; 3]>(up_val.clone()) { - cam_struct.up = DVec3::from_array(arr); - } - } - if let Some(aspect) = data_obj.get("aspect").and_then(|v| v.as_f64()) { - cam_struct.aspect = aspect; - } - if let Some(fov) = data_obj.get("fov").and_then(|v| v.as_f64()) { - cam_struct.fov_y = fov; - } - if let Some(near) = data_obj.get("near").and_then(|v| v.as_f64()) { - cam_struct.znear = near; - } - if let Some(far) = data_obj.get("far").and_then(|v| v.as_f64()) { - cam_struct.zfar = far; - } - if let Some(yaw) = data_obj.get("yaw").and_then(|v| v.as_f64()) { - cam_struct.yaw = yaw; - } - if let Some(pitch) = data_obj.get("pitch").and_then(|v| v.as_f64()) { - cam_struct.pitch = pitch; - } - if let Some(speed) = data_obj.get("speed").and_then(|v| v.as_f64()) { - cam_struct.speed = speed; - } - if let Some(sensitivity) = data_obj.get("sensitivity").and_then(|v| v.as_f64()) { - cam_struct.sensitivity = sensitivity; - } - log::trace!("apply: Updated camera '{}'", label); - } - } - } - } - } - } - } - } - } - } else { - log::trace!("apply: script_data not an object for entity {:?}", entity_id); - } - - Ok(()) - } - - pub fn load_script_from_source(&mut self, script_name: &String, script_content: &String) -> anyhow::Result<String> { - let module = Module::new(&script_name, script_content); - - match self.runtime.load_module(&module) { - Ok(module) => { - self.compiled_scripts.insert(script_name.clone(), module); - log::info!("Loaded script: {}", script_name); - Ok(script_name.to_string()) - } - Err(e) => { - log::error!("Compiling module for script [{}] returned an error: {}", script_name, e); - Err(e.into()) - } - } - } - - pub fn load_script(&mut self, script_path: &PathBuf) -> anyhow::Result<String> { - log::debug!("Reading script content"); - let script_content = fs::read_to_string(script_path)?; - log::debug!("Fetching script name"); - let script_name = script_path - .file_name() - .unwrap_or_default() - .to_string_lossy() - .to_string(); - log::debug!("Script name: {}", script_name); - - log::debug!("Creating module for typescript runtime"); - let module = Module::new(&script_name, &script_content); - - log::debug!("Loading module"); - match self.runtime.load_module(&module) { - Ok(module) => { - self.compiled_scripts.insert(script_name.clone(), module); - log::info!("Loaded script: {}", script_name); - Ok(script_name) - } - Err(e) => { - log::error!("Compiling module for script path [{}] returned an error: {}", script_path.display(), e); - Err(e.into()) - } - } - } - - pub fn remove_entity_script(&mut self, entity_id: hecs::Entity) { - self.entity_script_data.remove(&entity_id); - } - - pub fn handle_camera_actions(&self, world: &mut hecs::World, active_camera: &mut Option<hecs::Entity>, actions: Vec<serde_json::Value>) -> anyhow::Result<()> { - use crate::camera::{CameraComponent, CameraType}; - use dropbear_engine::camera::Camera; - - for action_value in actions { - if let Ok(action) = serde_json::from_value::<crate::scripting::camera::CameraAction>(action_value) { - match action.action.as_str() { - "switch_camera" => { - if let Some(label) = action.label { - let camera_entity = world - .query::<&Camera>() - .iter() - .find(|(_, camera)| camera.label == label) - .map(|(entity, _)| entity); - - if let Some(entity) = camera_entity { - *active_camera = Some(entity); - log::info!("Switched to camera with label: {}", label); - } else { - log::warn!("Camera with label '{}' not found", label); - } - } - } - "get_active_camera" => { - if let Some(active_entity) = active_camera { - if let Ok(camera) = world.get::<&Camera>(*active_entity) { - log::info!("Active camera label: {}", camera.label); - } - } - } - "get_all_cameras" => { - let labels: Vec<String> = world - .query::<&Camera>() - .iter() - .map(|(_, camera)| camera.label.clone()) - .collect(); - log::info!("All camera labels: {:?}", labels); - } - "get_cameras_by_type" => { - if let Some(type_str) = action.camera_type { - let target_type = match type_str.as_str() { - "Normal" => CameraType::Normal, - "Debug" => CameraType::Debug, - "Player" => CameraType::Player, - _ => continue, - }; - - let labels: Vec<String> = world - .query::<(&Camera, &CameraComponent)>() - .iter() - .filter_map(|(_, (camera, component))| { - if component.camera_type == target_type { - Some(camera.label.clone()) - } else { - None - } - }) - .collect(); - log::info!("Cameras of type {:?}: {:?}", target_type, labels); - } - } - "manipulate_camera" => { - if let (Some(label), Some(property), Some(value)) = (action.label, action.property, action.value) { - let camera_entity = world - .query::<&Camera>() - .iter() - .find(|(_, camera)| camera.label == label) - .map(|(entity, _)| entity); - - if let Some(entity) = camera_entity { - match property.as_str() { - "position" => { - if let Ok(pos_array) = serde_json::from_value::<[f64; 3]>(value) { - if let Ok(mut camera) = world.get::<&mut Camera>(entity) { - camera.eye = DVec3::from_array(pos_array); - log::info!("Updated camera '{}' position to {:?}", label, pos_array); - } - } - } - "target" => { - if let Ok(target_array) = serde_json::from_value::<[f64; 3]>(value) { - if let Ok(mut camera) = world.get::<&mut Camera>(entity) { - camera.target = DVec3::from_array(target_array); - log::info!("Updated camera '{}' target to {:?}", label, target_array); - } - } - } - "speed" => { - if let Some(speed) = value.as_f64() { - if let Ok(mut camera) = world.get::<&mut Camera>(entity) { - camera.speed = speed; - } - if let Ok(mut component) = world.get::<&mut CameraComponent>(entity) { - component.speed = speed; - } - log::info!("Updated camera '{}' speed to {}", label, speed); - } - } - "sensitivity" => { - if let Some(sensitivity) = value.as_f64() { - if let Ok(mut camera) = world.get::<&mut Camera>(entity) { - camera.sensitivity = sensitivity; - } - if let Ok(mut component) = world.get::<&mut CameraComponent>(entity) { - component.sensitivity = sensitivity; - } - log::info!("Updated camera '{}' sensitivity to {}", label, sensitivity); - } - } - "fov" => { - if let Some(fov) = value.as_f64() { - if let Ok(mut camera) = world.get::<&mut Camera>(entity) { - camera.fov_y = fov; - } - if let Ok(mut component) = world.get::<&mut CameraComponent>(entity) { - component.fov_y = fov; - } - log::info!("Updated camera '{}' FOV to {}", label, fov); - } - } - "yaw" => { - if let Some(yaw) = value.as_f64() { - if let Ok(mut camera) = world.get::<&mut Camera>(entity) { - camera.yaw = yaw; - } - log::info!("Updated camera '{}' yaw to {}", label, yaw); - } - } - "pitch" => { - if let Some(pitch) = value.as_f64() { - if let Ok(mut camera) = world.get::<&mut Camera>(entity) { - camera.pitch = pitch; - } - log::info!("Updated camera '{}' pitch to {}", label, pitch); - } - } - _ => { - log::warn!("Unknown camera property: {}", property); - } - } - } else { - log::warn!("Camera with label '{}' not found for manipulation", label); - } - } - } - "get_camera_by_label" => { - if let Some(label) = action.label { - log::info!("Getting camera data for label: {}", label); - } - } - "set_camera_by_label" => { - if let (Some(label), Some(camera_data)) = (action.label, action.camera_data) { - // Find and update the entire camera - let camera_entity = world - .query::<&Camera>() - .iter() - .find(|(_, camera)| camera.label == label) - .map(|(entity, _)| entity); - - if let Some(entity) = camera_entity { - if let Ok(mut camera) = world.get::<&mut Camera>(entity) { - camera.eye = camera_data.eye; - camera.target = camera_data.target; - camera.up = camera_data.up; - camera.aspect = camera_data.aspect; - camera.fov_y = camera_data.fov; - camera.znear = camera_data.near; - camera.zfar = camera_data.far; - camera.yaw = camera_data.yaw; - camera.pitch = camera_data.pitch; - camera.speed = camera_data.speed; - camera.sensitivity = camera_data.sensitivity; - log::info!("Updated complete camera data for label: {}", label); - } - } - } - } - _ => { - log::warn!("Unknown camera action: {}", action.action); - } - } - } - } - Ok(()) - } -} @@ -1,206 +0,0 @@ -// Main shader for standard objects. -const MAX_LIGHTS: u32 = 8; - -struct CameraUniform { - view_pos: vec4<f32>, - view_proj: mat4x4<f32>, -}; -@group(1) @binding(0) -var<uniform> camera: CameraUniform; - -struct Light { - position: vec4<f32>, // x, y, z, unused - direction: vec4<f32>, // x, y, z, unused - color: vec4<f32>, // r, g, b, light_type (0, 1, 2) - constant: f32, - lin: f32, // linear - quadratic: f32, - cutoff: f32, -} - -struct LightArray { - lights: array<Light, MAX_LIGHTS>, - light_count: u32, - ambient_strength: f32, -} - -@group(2) @binding(0) -var<uniform> light_array: LightArray; - -struct InstanceInput { - @location(5) model_matrix_0: vec4<f32>, - @location(6) model_matrix_1: vec4<f32>, - @location(7) model_matrix_2: vec4<f32>, - @location(8) model_matrix_3: vec4<f32>, - - @location(9) normal_matrix_0: vec3<f32>, - @location(10) normal_matrix_1: vec3<f32>, - @location(11) normal_matrix_2: vec3<f32>, -}; - -struct VertexInput { - @location(0) position: vec3<f32>, - @location(1) tex_coords: vec2<f32>, - @location(2) normal: vec3<f32>, -}; - -struct VertexOutput { - @builtin(position) clip_position: vec4<f32>, - @location(0) tex_coords: vec2<f32>, - @location(1) world_normal: vec3<f32>, - @location(2) world_position: vec3<f32>, -}; - -@vertex -fn vs_main( - model: VertexInput, - instance: InstanceInput, -) -> VertexOutput { - let model_matrix = mat4x4<f32>( - instance.model_matrix_0, - instance.model_matrix_1, - instance.model_matrix_2, - instance.model_matrix_3, - ); - let normal_matrix = mat3x3<f32>( - instance.normal_matrix_0, - instance.normal_matrix_1, - instance.normal_matrix_2, - ); - var out: VertexOutput; - out.tex_coords = model.tex_coords; - out.world_normal = normal_matrix * model.normal; - var world_position: vec4<f32> = model_matrix * vec4<f32>(model.position, 1.0); - out.world_position = world_position.xyz; - out.clip_position = camera.view_proj * world_position; - return out; -} - -@group(0) @binding(0) -var t_diffuse: texture_2d<f32>; -@group(0) @binding(1) -var s_diffuse: sampler; - -fn calculate_light(light: Light, world_pos: vec3<f32>, world_normal: vec3<f32>, view_dir: vec3<f32>) -> vec3<f32> { - let light_dir = normalize(light.position.xyz - world_pos); - - // dihfuse - let diffuse_strength = max(dot(world_normal, light_dir), 0.0); - let diffuse_color = light.color.xyz * diffuse_strength; - - // specular - let half_dir = normalize(view_dir + light_dir); - let specular_strength = pow(max(dot(world_normal, half_dir), 0.0), 32.0); - let specular_color = specular_strength * light.color.xyz; - - return diffuse_color + specular_color; -} - -fn directional_light( - light: Light, - world_normal: vec3<f32>, - view_dir: vec3<f32>, - tex_color: vec3<f32> -) -> vec3<f32> { - let light_dir = normalize(-light.direction.xyz); - - let ambient = light.color.xyz * light_array.ambient_strength * tex_color; - - let diff = max(dot(world_normal, light_dir), 0.0); - let diffuse = light.color.xyz * diff * tex_color; - - let reflect_dir = reflect(-light_dir, world_normal); - let spec = pow(max(dot(view_dir, reflect_dir), 0.0), 32.0); - let specular = light.color.xyz * spec * tex_color; - - return ambient + diffuse + specular; -} - -// https://learnopengl.com/code_viewer_gh.php?code=src/2.lighting/5.2.light_casters_point/5.2.light_casters.fs -// deal with later. current issue: it is showing only yellow and white in point light (weird...) -// note: fixed, forgot to push attenuation values to gpu lol -fn point_light(light: Light, world_pos: vec3<f32>, world_normal: vec3<f32>, view_dir: vec3<f32>, tex_color: vec3<f32>) -> vec3<f32> { - let norm = normalize(world_normal); - let light_dir = normalize(light.position.xyz - world_pos); - let diff = max(dot(norm, light_dir), 0.0); - let diffuse = light.color.xyz * diff * tex_color; - - let shininess = 32.0; - let reflect_dir = reflect(-light_dir, norm); - let spec = pow(max(dot(view_dir, reflect_dir), 0.0), shininess); - let specular = light.color.xyz * spec * tex_color; - - let distance = length(light.position.xyz - world_pos); - let attenuation = 1.0 / (light.constant + (light.lin * distance) + (light.quadratic * (distance * distance))); - - return (diffuse + specular) * attenuation; -} - -fn spot_light(light: Light, world_pos: vec3<f32>, world_normal: vec3<f32>, view_dir: vec3<f32>, tex_color: vec3<f32>) -> vec3<f32> { - let outer_cutoff = light.direction.w; - let ambient = light.color.xyz * light_array.ambient_strength * tex_color; - - let norm = normalize(world_normal); - let light_dir = normalize(light.position.xyz - world_pos); - let diff = max(dot(norm, light_dir), 0.0); - var diffuse = light.color.xyz * diff * tex_color; - - let shininess = 32.0; - let reflect_dir = reflect(-light_dir, norm); - let spec = pow(max(dot(view_dir, reflect_dir), 0.0), shininess); - var specular = light.color.xyz * spec * tex_color; - - let theta = dot(light_dir, normalize(-light.direction.xyz)); - let epsilon = light.cutoff - outer_cutoff; - let intensity = clamp((theta - outer_cutoff) / epsilon, 0.0, 1.0); - - diffuse *= intensity; - specular *= intensity; - - let distance = length(light.position.xyz - world_pos); - let attenuation = 1.0 / (light.constant + (light.lin * distance) + (light.quadratic * (distance * distance))); - - let ambient_attenuated = ambient * attenuation; - let diffuse_attenuated = diffuse * attenuation; - let specular_attenuated = specular * attenuation; - - return ambient_attenuated + diffuse_attenuated + specular_attenuated; -} - -@fragment -fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> { - var tex_color = textureSample(t_diffuse, s_diffuse, in.tex_coords); - if (tex_color.a < 0.1) { - discard; - } - - let view_dir = normalize(camera.view_pos.xyz - in.world_position); - let world_normal = normalize(in.world_normal); - - var final_color = vec3<f32>(0.0); - - var total_ambient = vec3<f32>(0.0); - for (var i: u32 = 0u; i < light_array.light_count; i = i + 1u) { - let light = light_array.lights[i]; - total_ambient += light.color.xyz * light_array.ambient_strength; - } - - for (var i: u32 = 0u; i < light_array.light_count; i = i + 1u) { - let light = light_array.lights[i]; - - // light type is color.w - if light.color.w == 0.0 { - final_color += directional_light(light, world_normal, view_dir, tex_color.xyz); - } else if light.color.w == 1.0 { - // point - final_color += point_light(light, in.world_position, world_normal, view_dir, tex_color.xyz); - } else if light.color.w == 2.0 { - final_color += spot_light(light, in.world_position, world_normal, view_dir, tex_color.xyz); - } - } - - // Combine ambient and lighting - final_color = (total_ambient * tex_color.xyz) + final_color; - - return vec4<f32>(final_color, tex_color.a); -} @@ -1,1019 +0,0 @@ -//! In this module, it will describe all the different types for -//! storing configuration files (.eucp for project and .eucc for config files for subdirectories). -//! -//! There is a singleton that is used for other crates to access, -//! as well as public structs related to that config and docs (hopefully). - -use std::{ - collections::HashMap, - fmt::{self, Display, Formatter}, - fs, - path::PathBuf, - sync::RwLock, -}; - -use bincode::{Encode, Decode}; -use chrono::Utc; -use dropbear_engine::{ - camera::Camera, - entity::{AdoptedEntity, Transform}, - graphics::Graphics, lighting::{Light, LightComponent}, -}; - -#[cfg(feature = "editor")] -use egui_dock_fork::DockState; - -use glam::DVec3; -use hecs; -use log; -use once_cell::sync::Lazy; -use ron::ser::PrettyConfig; -use serde::{Deserialize, Serialize}; -use dropbear_engine::model::Model; -use dropbear_engine::starter::plane::PlaneBuilder; -use dropbear_engine::utils::{ResourceReference, ResourceReferenceType}; -use crate::camera::{CameraComponent, CameraFollowTarget, CameraType, DebugCamera}; -#[cfg(feature = "editor")] -use crate::editor::EditorTab; -use crate::utils::PROTO_TEXTURE; - -pub static PROJECT: Lazy<RwLock<ProjectConfig>> = - Lazy::new(|| RwLock::new(ProjectConfig::default())); - -pub static RESOURCES: Lazy<RwLock<ResourceConfig>> = - Lazy::new(|| RwLock::new(ResourceConfig::default())); - -pub static SOURCE: Lazy<RwLock<SourceConfig>> = Lazy::new(|| RwLock::new(SourceConfig::default())); - -pub static SCENES: Lazy<RwLock<Vec<SceneConfig>>> = Lazy::new(|| RwLock::new(Vec::new())); - -/// The root config file, responsible for building and other metadata. -/// -/// # Location -/// This file is {project_name}.eucp and is located at {project_dir}/ -#[derive(Debug, Deserialize, Serialize, Default)] -#[cfg(feature = "editor")] -pub struct ProjectConfig { - pub project_name: String, - pub project_path: PathBuf, - pub date_created: String, - pub date_last_accessed: String, - #[serde(default)] - pub dock_layout: Option<DockState<EditorTab>>, - #[serde(default)] - pub editor_settings: EditorSettings, -} - -#[derive(Debug, Deserialize, Serialize, Default)] -#[cfg(not(feature = "editor"))] -pub struct ProjectConfig { - pub project_name: String, - pub project_path: PathBuf, - pub date_created: String, - pub date_last_accessed: String, - // #[serde(default)] - // pub dock_layout: Option<DockState<EditorTab>>, -} - -impl ProjectConfig { - /// Creates a new instance of the ProjectConfig. This function is typically used when creating - /// a new project, with it creating new defaults for everything. - pub fn new(project_name: String, project_path: &PathBuf) -> Self { - let date_created = format!("{}", Utc::now().format("%Y-%m-%d %H:%M:%S")); - let date_last_accessed = format!("{}", Utc::now().format("%Y-%m-%d %H:%M:%S")); - #[cfg(not(feature = "editor"))] - { - let mut result = Self { - project_name, - project_path: project_path.to_path_buf(), - date_created, - date_last_accessed, - }; - let _ = result.load_config_to_memory(); // TODO: Deal with later... - result - } - - #[cfg(feature = "editor")] - { - let mut result = Self { - project_name, - project_path: project_path.to_path_buf(), - date_created, - date_last_accessed, - editor_settings: Default::default(), - dock_layout: None, - }; - let _ = result.load_config_to_memory(); // TODO: Deal with later... - result - } - } - - /// This function writes the [`ProjectConfig`] struct (and other PathBufs) to a file of the choice - /// under the PathBuf path parameter. - /// - /// # Parameters - /// * path - The root **folder** of the project. - - pub fn write_to(&mut self, path: &PathBuf) -> anyhow::Result<()> { - self.load_config_to_memory()?; - self.date_last_accessed = format!("{}", Utc::now().format("%Y-%m-%d %H:%M:%S")); - // self.assets = Assets::walk(path); - let ron_str = ron::ser::to_string_pretty(&self, PrettyConfig::default()) - .map_err(|e| anyhow::anyhow!("RON serialization error: {}", e))?; - let config_path = path.join(format!("{}.eucp", self.project_name.clone().to_lowercase())); - self.project_path = path.to_path_buf(); - - fs::write(&config_path, ron_str).map_err(|e| anyhow::anyhow!(e.to_string()))?; - Ok(()) - } - - /// This function reads from the RON and traverses down the different folders to add more information - /// to the ProjectConfig, such as Assets location and other stuff. - /// - /// # Parameters - /// * path - The root config **file** for the project - pub fn read_from(path: &PathBuf) -> anyhow::Result<Self> { - let ron_str = fs::read_to_string(path)?; - let mut config: ProjectConfig = ron::de::from_str(&ron_str.as_str())?; - config.project_path = path.parent().unwrap().to_path_buf(); - log::info!("Loaded project!"); - log::debug!("Loaded config info"); - log::debug!("Updating with new content"); - config.load_config_to_memory()?; - config.write_to_all()?; - log::debug!("Successfully updated!"); - Ok(config) - } - - /// This function loads a `source.eucc`, `resources.eucc` or a `{scene}.eucs` config file into memory, allowing - /// you to reference and load the nodes located inside them. - pub fn load_config_to_memory(&mut self) -> anyhow::Result<()> { - let project_root = PathBuf::from(&self.project_path); - - // resource config - match ResourceConfig::read_from(&project_root) { - Ok(resources) => { - let mut cfg = RESOURCES.write().unwrap(); - *cfg = resources; - } - Err(e) => { - if let Some(io_err) = e.downcast_ref::<std::io::Error>() { - if io_err.kind() == std::io::ErrorKind::NotFound { - log::warn!("resources.eucc not found, creating default."); - let default = ResourceConfig { - path: project_root.join("resources"), - nodes: vec![], - }; - default.write_to(&project_root)?; - { - let mut cfg = RESOURCES.write().unwrap(); - *cfg = default; - } - } else { - log::warn!("Failed to load resources.eucc: {}", e); - } - } else { - log::warn!("Failed to load resources.eucc: {}", e); - } - } - } - - // src config - let mut source_config = SOURCE.write().unwrap(); - match SourceConfig::read_from(&project_root) { - Ok(source) => *source_config = source, - Err(e) => { - if let Some(io_err) = e.downcast_ref::<std::io::Error>() { - if io_err.kind() == std::io::ErrorKind::NotFound { - log::warn!("source.eucc not found, creating default."); - let default = SourceConfig { - path: project_root.join("src"), - nodes: vec![], - }; - default.write_to(&project_root)?; - *source_config = default; - } else { - log::warn!("Failed to load source.eucc: {}", e); - } - } else { - log::warn!("Failed to load source.eucc: {}", e); - } - } - } - - // scenes - let mut scene_configs = SCENES.write().unwrap(); - scene_configs.clear(); - - // iterate through each scene file in the folder - let scene_folder = &project_root.join("scenes"); - - if !scene_folder.exists() { - fs::create_dir_all(scene_folder)?; - } - - for scene_entry in fs::read_dir(scene_folder)? { - let scene_entry = scene_entry?; - let path = scene_entry.path(); - - if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("eucs") { - match SceneConfig::read_from(&path) { - Ok(scene) => { - log::debug!("Loaded scene: {}", scene.scene_name); - scene_configs.push(scene); - } - Err(e) => { - if let Some(io_err) = e.downcast_ref::<std::io::Error>() { - if io_err.kind() == std::io::ErrorKind::NotFound { - log::warn!("Scene file {:?} not found", path); - } else { - log::warn!("Failed to load scene file {:?}: {}", path, e); - } - } else { - log::warn!("Failed to load scene file {:?}: {}", path, e); - } - } - } - } - } - - if scene_configs.is_empty() { - log::info!("No scenes found, creating default scene"); - let default_scene = - SceneConfig::new("Default".to_string(), scene_folder.join("default.eucs")); - default_scene.write_to(&project_root)?; - scene_configs.push(default_scene); - } - - Ok(()) - } - - /// # Parameters - /// * path - The root folder of the project - pub fn write_to_all(&mut self) -> anyhow::Result<()> { - let path = PathBuf::from(self.project_path.clone()); - - { - let resources_config = RESOURCES.read().unwrap(); - resources_config.write_to(&path)?; - } - - { - let source_config = SOURCE.read().unwrap(); - source_config.write_to(&path)?; - } - - { - let scene_configs = SCENES.read().unwrap(); - for scene in scene_configs.iter() { - scene.write_to(&path)?; - } - } - - self.write_to(&path)?; - Ok(()) - } -} - -#[allow(dead_code)] -pub(crate) fn path_contains_folder(path: &PathBuf, folder: &str) -> bool { - path.components().any(|comp| comp.as_os_str() == folder) -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub enum Node { - File(File), - Folder(Folder), -} - -#[derive(Default, Debug, Serialize, Deserialize, Clone)] -pub struct File { - pub name: String, - pub path: PathBuf, - pub resource_type: Option<ResourceType>, -} - -#[derive(Default, Debug, Serialize, Deserialize, Clone)] -pub struct Folder { - pub name: String, - pub path: PathBuf, - pub nodes: Vec<Node>, -} - -/// The type of resource -#[derive(Debug, Serialize, Deserialize, Clone, Hash)] -pub enum ResourceType { - Unknown, - Model, - Thumbnail, - Texture, - Shader, -} - -impl Display for ResourceType { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - let str = match self { - ResourceType::Unknown => "unknown", - ResourceType::Model => "model", - ResourceType::Texture => "texture", - ResourceType::Shader => "shader", - ResourceType::Thumbnail => "thumbnail", - }; - write!(f, "{}", str) - } -} - -/// This is the resource config. -#[derive(Default, Debug, Serialize, Deserialize)] -pub struct ResourceConfig { - /// The path to the resource folder. - pub path: PathBuf, - /// The files and folders of the assets - pub nodes: Vec<Node>, -} - -impl ResourceConfig { - /// Builds a resource path from the ProjectConfiguration's project_path (or a string) - #[allow(dead_code)] - pub fn build_path(project_path: String) -> PathBuf { - PathBuf::from(project_path).join("resources/resources.eucc") - } - - /// # Parameters - /// - path: The root **folder** of the project - pub fn write_to(&self, path: &PathBuf) -> anyhow::Result<()> { - let resource_dir = path.join("resources"); - let updated_config = ResourceConfig { - path: resource_dir.clone(), - nodes: collect_nodes(&resource_dir, path, vec!["thumbnails"].as_slice()), - }; - let ron_str = ron::ser::to_string_pretty(&updated_config, PrettyConfig::default()) - .map_err(|e| anyhow::anyhow!("RON serialization error: {}", e))?; - let config_path = path.join("resources").join("resources.eucc"); - fs::create_dir_all(config_path.parent().unwrap())?; - fs::write(&config_path, ron_str).map_err(|e| anyhow::anyhow!(e.to_string()))?; - Ok(()) - } - - /// Updates the in-memory ResourceConfig by re-scanning the resource directory. - pub fn update_mem(&mut self) -> anyhow::Result<ResourceConfig> { - let resource_dir = self.path.clone(); - let project_path = resource_dir.parent().unwrap_or(&resource_dir).to_path_buf(); - let updated_config = ResourceConfig { - path: resource_dir.clone(), - nodes: collect_nodes(&resource_dir, &project_path, vec!["thumbnails"].as_slice()), - }; - Ok(updated_config) - } - - /// # Parameters - /// - path: The location to the **resources.eucc** file - pub fn read_from(path: &PathBuf) -> anyhow::Result<Self> { - let config_path = path.join("resources").join("resources.eucc"); - let ron_str = fs::read_to_string(&config_path)?; - let config: ResourceConfig = ron::de::from_str(&ron_str) - .map_err(|e| anyhow::anyhow!("RON deserialization error: {}", e))?; - Ok(config) - } -} - -#[derive(Default, Debug, Serialize, Deserialize, Clone)] -pub struct SourceConfig { - /// The path to the resource folder. - pub path: PathBuf, - /// The files and folders of the assets - pub nodes: Vec<Node>, -} - -impl SourceConfig { - /// Builds a source path from the ProjectConfiguration's project_path (or a string) - #[allow(dead_code)] - pub fn build_path(project_path: String) -> PathBuf { - PathBuf::from(project_path).join("src/source.eucc") - } - - /// # Parameters - /// - path: The root **folder** of the project - pub fn write_to(&self, path: &PathBuf) -> anyhow::Result<()> { - let resource_dir = path.join("src"); - let updated_config = SourceConfig { - path: resource_dir.clone(), - nodes: collect_nodes(&resource_dir, path, vec!["scripts"].as_slice()), - }; - - let ron_str = ron::ser::to_string_pretty(&updated_config, PrettyConfig::default()) - .map_err(|e| anyhow::anyhow!("RON serialisation error: {}", e))?; - let config_path = path.join("src").join("source.eucc"); - fs::create_dir_all(config_path.parent().unwrap())?; - fs::write(&config_path, ron_str).map_err(|e| anyhow::anyhow!(e.to_string()))?; - Ok(()) - } - - /// # Parameters - /// - path: The location to the **source.eucc** file - pub fn read_from(path: &PathBuf) -> anyhow::Result<Self> { - let config_path = path.join("src").join("source.eucc"); - let ron_str = fs::read_to_string(&config_path)?; - let config: SourceConfig = ron::de::from_str(&ron_str) - .map_err(|e| anyhow::anyhow!("RON deserialization error: {}", e))?; - Ok(config) - } -} - -fn collect_nodes(dir: &PathBuf, project_path: &PathBuf, exclude_list: &[&str]) -> Vec<Node> { - let mut nodes = Vec::new(); - if let Ok(entries) = fs::read_dir(dir) { - for entry in entries.flatten() { - let entry_path = entry.path(); - let name = entry_path - .file_name() - .unwrap_or_default() - .to_string_lossy() - .to_string(); - - if entry_path.is_dir() && exclude_list.iter().any(|ex| &ex.to_string() == &name) { - log::debug!("Skipped past folder {:?}", name); - continue; - } - - if entry_path.is_dir() { - let folder_nodes = collect_nodes(&entry_path, project_path, exclude_list); - nodes.push(Node::Folder(Folder { - name, - path: entry_path.clone(), - nodes: folder_nodes, - })); - } else { - let parent_folder = entry_path - .parent() - .and_then(|p| p.file_name()) - .map(|n| n.to_string_lossy().to_lowercase()) - .unwrap_or_default(); - - let resource_type = if parent_folder.contains("model") { - Some(ResourceType::Model) - } else if parent_folder.contains("texture") { - Some(ResourceType::Texture) - } else if parent_folder.contains("shader") { - Some(ResourceType::Shader) - } else { - Some(ResourceType::Unknown) - }; - - nodes.push(Node::File(File { - name, - path: entry_path.clone(), - resource_type, - })); - } - } - } - nodes -} - -#[derive(Clone, Debug, Serialize, Deserialize, Hash)] -pub enum EntityNode { - Entity { - id: hecs::Entity, - name: String, - }, - Script { - name: String, - path: PathBuf, - }, - Light { - id: hecs::Entity, - name: String, - }, - Camera { - id: hecs::Entity, - name: String, - camera_type: CameraType, - }, - Group { - name: String, - children: Vec<EntityNode>, - collapsed: bool, - }, -} - -#[derive(Default, Debug, Serialize, Deserialize, Clone)] -pub struct ScriptComponent { - pub name: String, - pub path: PathBuf, -} - -impl EntityNode { - pub fn from_world(world: &hecs::World) -> Vec<Self> { - let mut nodes = Vec::new(); - let mut handled = std::collections::HashSet::new(); - - for (id, (script, _transform, adopted)) in world - .query::<( - &ScriptComponent, - &dropbear_engine::entity::Transform, - &dropbear_engine::entity::AdoptedEntity, - )>() - .iter() - { - let name = adopted.model().label.clone(); - - // grouped entity (entity + script) - nodes.push(EntityNode::Group { - name: name.clone(), - children: vec![ - EntityNode::Entity { - id, - name: name.clone(), - }, - EntityNode::Script { - name: script.name.clone(), - path: script.path.clone(), - }, - ], - collapsed: false, - }); - handled.insert(id); - } - - // single entity - for (id, (_, adopted)) in world - .query::<( - &dropbear_engine::entity::Transform, - &dropbear_engine::entity::AdoptedEntity, - )>() - .iter() - { - if handled.contains(&id) { - continue; - } - let name = adopted.model().label.clone(); - - nodes.push(EntityNode::Entity { id, name }); - } - - // lights - for (id, (_transform, _light_comp, light)) in world - .query::<(&dropbear_engine::entity::Transform, &LightComponent, &Light)>() - .iter() - { - if handled.contains(&id) { - continue; - } - nodes.push(EntityNode::Light { - id, - name: light.label().to_string(), - }); - handled.insert(id); - } - - for (entity, (camera, component)) in world - .query::<(&Camera, &CameraComponent)>() - .iter() - { - if world.get::<&AdoptedEntity>(entity).is_err() { - nodes.push(EntityNode::Camera { - id: entity, - name: camera.label.clone(), - camera_type: component.camera_type, - }); - } - } - - nodes - } -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct CameraConfig { - pub label: String, - pub camera_type: CameraType, - - pub position: [f64; 3], - pub target: [f64; 3], - pub up: [f64; 3], - pub aspect: f64, - pub fov: f32, - pub near: f32, - pub far: f32, - - pub speed: f32, - pub sensitivity: f32, - - pub follow_target_entity_label: Option<String>, - pub follow_offset: Option<[f64; 3]>, -} - -impl Default for CameraConfig { - fn default() -> Self { - let default = CameraComponent::new(); - Self { - position: [0.0, 1.0, 2.0], - target: [0.0, 0.0, 0.0], - up: [0.0, 1.0, 0.0], - aspect: 16.0 / 9.0, - fov: 45.0, - near: 0.1, - far: 100.0, - follow_target_entity_label: None, - follow_offset: None, - label: String::new(), - camera_type: CameraType::Normal, - speed: default.speed as f32, - sensitivity: default.sensitivity as f32, - } - } -} - -impl CameraConfig { - pub fn from_ecs_camera(camera: &Camera, component: &CameraComponent, follow_target: Option<&CameraFollowTarget>) -> Self { - Self { - position: camera.position().to_array(), - target: camera.target.to_array(), - label: camera.label.clone(), - camera_type: component.camera_type, - up: camera.up.to_array(), - aspect: camera.aspect, - fov: camera.fov_y as f32, - near: camera.znear as f32, - far: camera.zfar as f32, - speed: component.speed as f32, - sensitivity: component.sensitivity as f32, - follow_target_entity_label: if let Some(target) = follow_target { Some(target.follow_target.clone()) } else { None }, - follow_offset: if let Some(target) = follow_target { Some(target.offset.to_array()) } else { None }, - } - } -} - -#[derive(Debug, Serialize, Deserialize, Clone, Default)] -pub struct SceneEntity { - pub model_path: ResourceReference, - pub label: String, - pub transform: Transform, - pub properties: ModelProperties, - pub script: Option<ScriptComponent>, - - #[serde(skip)] - #[allow(dead_code)] - pub entity_id: Option<hecs::Entity>, -} - -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -pub struct ModelProperties { - pub custom_properties: HashMap<String, PropertyValue>, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub enum PropertyValue { - String(String), - Int(i64), - Float(f64), - Bool(bool), - Vec3([f32; 3]), -} - -impl ModelProperties { - pub fn new() -> Self { - Self { - custom_properties: HashMap::new(), - } - } - - pub fn set_property(&mut self, key: String, value: PropertyValue) { - self.custom_properties.insert(key, value); - } - - pub fn get_property(&self, key: &str) -> Option<&PropertyValue> { - self.custom_properties.get(key) - } -} - -impl Default for ModelProperties { - fn default() -> Self { - Self::new() - } -} - -#[derive(Default, Debug, Serialize, Deserialize, Clone)] -pub struct SceneConfig { - pub scene_name: String, - pub entities: Vec<SceneEntity>, - pub cameras: Vec<CameraConfig>, - pub lights: Vec<LightConfig>, - // todo later - // pub settings: SceneSettings, - #[serde(skip)] - pub path: PathBuf, -} - -impl SceneConfig { - /// Creates a new instance of the scene config - pub fn new(scene_name: String, path: PathBuf) -> Self { - Self { - scene_name, - path, - entities: Vec::new(), - cameras: Vec::new(), - lights: Vec::new(), - } - } - - /// Write the scene config to a .eucs file - pub fn write_to(&self, project_path: &PathBuf) -> anyhow::Result<()> { - let ron_str = ron::ser::to_string_pretty(&self, PrettyConfig::default()) - .map_err(|e| anyhow::anyhow!("RON serialization error: {}", e))?; - - let scenes_dir = project_path.join("scenes"); - fs::create_dir_all(&scenes_dir)?; - - let config_path = scenes_dir.join(format!("{}.eucs", self.scene_name)); - fs::write(&config_path, ron_str).map_err(|e| anyhow::anyhow!(e.to_string()))?; - Ok(()) - } - - /// Read a scene config from a .eucs file - pub fn read_from(scene_path: &PathBuf) -> anyhow::Result<Self> { - let ron_str = fs::read_to_string(scene_path)?; - let mut config: SceneConfig = ron::de::from_str(&ron_str) - .map_err(|e| anyhow::anyhow!("RON deserialization error: {}", e))?; - - config.path = scene_path.clone(); - Ok(config) - } - - pub fn load_into_world( - &self, - world: &mut hecs::World, - graphics: &Graphics, - ) -> anyhow::Result<hecs::Entity> { - log::info!( - "Loading scene [{}], clearing world with {} entities", - self.scene_name, - world.len() - ); - world.clear(); - - #[allow(unused_variables)] - let project_config = if cfg!(feature = "data-only") { - if let Ok(cfg) = PROJECT.read() { - cfg.project_path.clone() - } else { - log::warn!("Unable to retrieve a lock from the PROJECT config"); - PathBuf::new() - } - } else { - log::warn!("Feature is data only, no need for project config"); - PathBuf::new() - }; - - log::info!("World cleared, now has {} entities", world.len()); - - for entity_config in &self.entities { - log::debug!("Loading entity: {}", entity_config.label); - match &entity_config.model_path.ref_type { - ResourceReferenceType::File(reference) => { - let path: PathBuf = { - if cfg!(feature = "data-only") { - log::debug!("Using feature data-only"); - entity_config.model_path.to_executable_path()? - } else if cfg!(feature = "editor") { - log::debug!("Using feature editor"); - entity_config.model_path.to_project_path(project_config.clone()) - .ok_or_else(|| anyhow::anyhow!("Unable to convert resource reference [{}] to project path", reference))? - } else { - log::debug!("Fuck you"); - panic!("Not using either the data-only feature or the editor feature, which resolve the path of the ResourceReference"); - } - }; - log::debug!("Path for entity {} is {} from reference {}", entity_config.label, path.display(), reference); - let adopted = AdoptedEntity::new( - graphics, - &path, - Some(&entity_config.label), - )?; - - let transform = entity_config.transform; - - if let Some(script_config) = &entity_config.script { - let script = ScriptComponent { - name: script_config.name.clone(), - path: script_config.path.clone(), - }; - world.spawn((adopted, transform, script, entity_config.properties.clone())); - } else { - world.spawn((adopted, transform, entity_config.properties.clone())); - } - log::debug!("Loaded!"); - } - ResourceReferenceType::Bytes(bytes) => { - log::info!("Loading entity from bytes [Len: {}]", bytes.len()); - let bytes = bytes.to_owned(); - - let model = Model::load_from_memory(graphics, bytes, Some(&entity_config.label))?; - let transform = entity_config.transform; - - let adopted = AdoptedEntity::adopt( - graphics, - model, - Some(&entity_config.label), - ); - - if let Some(script_config) = &entity_config.script { - let script = ScriptComponent { - name: script_config.name.clone(), - path: script_config.path.clone(), - }; - world.spawn((adopted, transform, script, entity_config.properties.clone())); - } else { - world.spawn((adopted, transform, entity_config.properties.clone())); - } - log::debug!("Loaded!"); - } - ResourceReferenceType::Plane => { - let width = entity_config.properties.custom_properties.get("width").ok_or_else(|| anyhow::anyhow!("Entity has no width property"))?; - let width = match width { - PropertyValue::Float(width) => width, - _ => panic!("Entity has a width property that is not a float"), - }; - let height = entity_config.properties.custom_properties.get("height").ok_or_else(|| anyhow::anyhow!("Entity has no height property"))?; - let height = match height { - PropertyValue::Float(height) => height, - _ => panic!("Entity has a height property that is not a float"), - }; - let tiles_x = entity_config.properties.custom_properties.get("tiles_x").ok_or_else(|| anyhow::anyhow!("Entity has no tiles_x property"))?; - let tiles_x = match tiles_x { - PropertyValue::Int(tiles_x) => tiles_x, - _ => panic!("Entity has a tiles_x property that is not an int"), - }; - let tiles_z = entity_config.properties.custom_properties.get("tiles_z").ok_or_else(|| anyhow::anyhow!("Entity has no tiles_z property"))?; - let tiles_z = match tiles_z { - PropertyValue::Int(tiles_z) => tiles_z, - _ => panic!("Entity has a tiles_z property that is not an int"), - }; - - let plane = PlaneBuilder::new().with_size(*width as f32, *height as f32).with_tiles(*tiles_x as u32, *tiles_z as u32).build(graphics, PROTO_TEXTURE, Some(entity_config.label.clone().as_str()))?; - let transform = entity_config.transform; - - if let Some(script_config) = &entity_config.script { - let script = ScriptComponent { - name: script_config.name.clone(), - path: script_config.path.clone(), - }; - world.spawn((plane, transform, script, entity_config.properties.clone())); - } else { - world.spawn((plane, transform, entity_config.properties.clone())); - } - } - ResourceReferenceType::None => panic!("Entity has a resource reference of None, which cannot be loaded or referenced"), - } - } - - for light_config in &self.lights { - log::debug!("Loading light: {}", light_config.label); - - let light = Light::new( - graphics, - &light_config.light_component, - &light_config.transform, - Some(&light_config.label), - ); - - world.spawn((light_config.light_component.clone(), light_config.transform, light, ModelProperties::default())); - } - - for camera_config in &self.cameras { - log::debug!("Loading camera {} of type {:?}", camera_config.label, camera_config.camera_type); - - let camera = Camera::new( - graphics, - DVec3::from_array(camera_config.position), - DVec3::from_array(camera_config.target), - DVec3::from_array(camera_config.up), - camera_config.aspect, - camera_config.fov as f64, - camera_config.near as f64, - camera_config.far as f64, - camera_config.speed as f64, - camera_config.sensitivity as f64, - Some(&camera_config.label), - ); - - let component = CameraComponent { - speed: camera_config.speed as f64, - sensitivity: camera_config.sensitivity as f64, - fov_y: camera_config.fov as f64, - camera_type: camera_config.camera_type, - }; - - if let (Some(target_label), Some(offset)) = (&camera_config.follow_target_entity_label, &camera_config.follow_offset) { - let follow_target = CameraFollowTarget { - follow_target: target_label.clone(), - offset: DVec3::from_array(*offset), - }; - world.spawn((camera, component, follow_target)); - } else { - world.spawn((camera, component)); - } - } - - if world.query::<(&LightComponent, &Light)>().iter().next().is_none() { - log::info!("No lights in scene, spawning default light"); - let default_transform = Transform { - position: glam::DVec3::new(2.0, 4.0, 2.0), - ..Default::default() - }; - let default_component = LightComponent::directional(glam::DVec3::ONE, 1.0); - let default_light = Light::new(graphics, &default_component, &default_transform, Some("Default Light")); - world.spawn((default_component, default_transform, default_light, ModelProperties::default())); - } - - log::info!("Loaded {} entities, {} lights and {} cameras", self.entities.len(), self.lights.len(), self.cameras.len()); - #[cfg(feature = "editor")] - { - // Editor mode - look for debug camera, create one if none exists - let debug_camera = world - .query::<(&Camera, &CameraComponent)>() - .iter() - .find_map(|(entity, (_, component))| { - if matches!(component.camera_type, CameraType::Debug) { - Some(entity) - } else { - None - } - }); - - if let Some(camera_entity) = debug_camera { - log::info!("Using existing debug camera for editor"); - Ok(camera_entity) - } else { - log::info!("No debug camera found, creating viewport camera for editor"); - let camera = Camera::predetermined(graphics, Some("Viewport Camera")); - let component = DebugCamera::new(); - let camera_entity = world.spawn((camera, component)); - Ok(camera_entity) - } - } - - #[cfg(not(feature = "editor"))] - { - // Runtime mode - look for player camera, panic if none exists - let player_camera = world - .query::<(&Camera, &CameraComponent)>() - .iter() - .find_map(|(entity, (_, component))| { - if matches!(component.camera_type, CameraType::Player) { - Some(entity) - } else { - None - } - }); - - if let Some(camera_entity) = player_camera { - log::info!("Using player camera for runtime"); - Ok(camera_entity) - } else { - panic!("Runtime mode requires a player camera, but none was found in the scene!"); - } - } - } -} - -#[derive(Decode, Encode, serde::Serialize, serde::Deserialize, Debug)] -pub struct RuntimeData { - #[bincode(with_serde)] - pub project_config: ProjectConfig, - #[bincode(with_serde)] - pub source_config: SourceConfig, - #[bincode(with_serde)] - pub scene_data: Vec<SceneConfig>, - #[bincode(with_serde)] - pub scripts: HashMap<String, String>, // name, script_content -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct LightConfig { - pub label: String, - pub transform: Transform, - pub light_component: LightComponent, - pub enabled: bool, - - #[serde(skip)] - pub entity_id: Option<hecs::Entity>, -} - -impl Default for LightConfig { - fn default() -> Self { - Self { - label: "New Light".to_string(), - transform: Transform::default(), - light_component: LightComponent::default(), - enabled: true, - entity_id: None, - } - } -} - -#[derive(Debug, Serialize, Deserialize, Clone, Default)] -pub struct EditorSettings { - pub is_debug_menu_shown: bool, -} @@ -1,25 +0,0 @@ -// dropbear-engine script template for eucalyptus -import * as dropbear from "./dropbear"; - -export function onLoad(s) { - dropbear.start(s); - // ------- Your own code here ------- - console.log("I have been awoken"); - - - // ---------------------------------- - // Do not remove anything outside unless - // you know what you are doing. - return dropbear.end(); -} - -export function onUpdate(s, dt: number) { - dropbear.start(s); - // ------- Your own code here ------- - console.log("I'm being updated!"); - - - // ---------------------------------- - // Same thing over here! - return dropbear.end(); -} @@ -1,280 +0,0 @@ -#[cfg(feature = "editor")] -use std::{ - fs, sync::mpsc::{self, Receiver}, -}; - -use std::path::PathBuf; - -#[cfg(feature = "editor")] -use anyhow::anyhow; -use dropbear_engine::{camera::Camera, entity::Transform}; -#[cfg(feature = "editor")] -use dropbear_engine::scene::SceneCommand; -#[cfg(feature = "editor")] -use egui::Context; - -#[cfg(feature = "editor")] -use egui_toast_fork::{Toast, ToastOptions, Toasts}; -#[cfg(feature = "editor")] -use git2::Repository; - -use crate::states::{ModelProperties, Node}; -#[cfg(feature = "editor")] -use crate::states::{PROJECT, ProjectConfig}; - -pub const PROTO_TEXTURE: &[u8] = include_bytes!("../../resources/proto.png"); - -pub fn search_nodes_recursively<'a, F>(nodes: &'a [Node], matcher: &F, results: &mut Vec<&'a Node>) -where - F: Fn(&Node) -> bool, -{ - for node in nodes { - if matcher(node) { - results.push(node); - } - match node { - Node::File(_) => {} - Node::Folder(folder) => { - search_nodes_recursively(&folder.nodes, matcher, results); - } - } - } -} - -/// Progress events for project creation -pub enum ProjectProgress { - Step { - _progress: f32, - _message: String, - }, - #[allow(dead_code)] // idk why its giving me this warning :( - Error(String), - Done, -} - -/// Open a project file and update the global state. -/// Returns Ok(Some(SceneCommand::SwitchScene)) on success, or an error string on failure. -#[allow(dead_code)] -#[cfg(feature = "editor")] -pub fn open_project( - scene_command: &mut SceneCommand, - toast: &mut Toasts, -) -> Result<Option<SceneCommand>, String> { - if let Some(path) = rfd::FileDialog::new() - .add_filter("Eucalyptus Project Configuration Files", &["eucp"]) - .pick_file() - { - match ProjectConfig::read_from(&path) { - Ok(config) => { - let mut global = PROJECT.write().unwrap(); - *global = config; - *scene_command = SceneCommand::SwitchScene("editor".to_string()); - Ok(Some(SceneCommand::SwitchScene("editor".to_string()))) - } - Err(e) => { - if e.to_string().contains("missing field") { - toast.add(Toast { - kind: egui_toast_fork::ToastKind::Error, - text: "Project version is not up to date.".into(), - options: ToastOptions::default() - .duration_in_seconds(5.0) - .show_progress(true), - ..Default::default() - }); - } - Err(format!("Failed to load project: {e}")) - } - } - } else { - Err("File dialog returned None".to_string()) - } -} - -/// Start creating a new project in a background thread. -/// Returns a Receiver for progress updates. -/// -#[cfg(feature = "editor")] -pub fn start_project_creation( - project_name: String, - project_path: Option<PathBuf>, -) -> Option<Receiver<ProjectProgress>> { - let (tx, rx) = mpsc::channel(); - let project_path = project_path.clone(); - - std::thread::spawn(move || { - let folders = [ - ("git", 0.1, "Creating a git folder..."), - ("src", 0.2, "Creating src folder..."), - ("resources/models", 0.4, "Creating models folder..."), - ("resources/shaders", 0.6, "Creating shader folder..."), - ("resources/textures", 0.8, "Creating textures folder..."), - ("src2", 0.9, "Creating project config file..."), - ]; - - if let Some(path) = &project_path { - for (folder, progress, message) in folders { - tx.send(ProjectProgress::Step { - _progress: progress, - _message: message.to_string(), - }) - .ok(); - - let full_path = path.join(folder); - let result: anyhow::Result<()> = if folder == "src" { - if !full_path.exists() { - fs::create_dir(&full_path) - .map_err(|e| anyhow::anyhow!(e)) - .map(|_| ()) - } else { - Ok(()) - } - } else if folder == "git" { - match Repository::init(path) { - Ok(_) => Ok(()), - Err(e) => { - if matches!(e.code(), git2::ErrorCode::Exists) { - Ok(()) - } else { - Err(anyhow!(e)) - } - } - } - } else if folder == "src2" { - if let Some(path) = &project_path { - let mut config = ProjectConfig::new(project_name.clone(), &path); - let _ = config.write_to_all(); - let mut global = PROJECT.write().unwrap(); - *global = config; - Ok(()) - } else { - Err(anyhow!("Project path not found")) - } - } else { - if !full_path.exists() { - fs::create_dir_all(&full_path) - .map_err(|e| anyhow!(e)) - .map(|_| ()) - } else { - Ok(()) - } - }; - if let Err(e) = result { - tx.send(ProjectProgress::Error(e.to_string())).ok(); - } - } - tx.send(ProjectProgress::Step { - _progress: 1.0, - _message: "Project creation complete!".to_string(), - }) - .ok(); - tx.send(ProjectProgress::Done).ok(); - } - }); - - Some(rx) -} - -#[cfg(feature = "editor")] -pub fn show_new_project_window<F>( - ctx: &Context, - show_new_project: &mut bool, - project_name: &mut String, - project_path: &mut Option<PathBuf>, - on_create: F, -) where - F: FnOnce(&str, &PathBuf), -{ - let screen_size = egui::vec2(400.0, 220.0); - - let mut open = *show_new_project; - egui::Window::new("Create new project") - .open(&mut open) - .resizable(true) - .collapsible(false) - .fixed_size(screen_size) - .show(ctx, |ui| { - ui.vertical(|ui| { - ui.heading("Project Name:"); - ui.add_space(5.0); - - ui.text_edit_singleline(project_name); - ui.add_space(10.0); - - ui.heading("Project Location: "); - ui.add_space(5.0); - - if let Some(path) = project_path { - ui.label(format!("Chosen location: {}", path.display())); - ui.add_space(5.0); - } - - ui.add_space(5.0); - if ui.button("Choose Location").clicked() { - if let Some(path) = rfd::FileDialog::new() - .set_title("Save Project") - .set_file_name(project_name.clone()) - .pick_folder() - { - *project_path = Some(path); - } - } - - let can_create = project_path.is_some() && !project_name.is_empty(); - if ui - .add_enabled(can_create, egui::Button::new("Create Project")) - .clicked() - { - if let Some(path) = project_path { - on_create(project_name, path); - } - ui.ctx().request_repaint(); - } - }); - }); - *show_new_project = open; -} - -/// Converts a click on a screen (like a viewport) coordinate relative to the world -#[allow(dead_code)] -pub fn screen_to_world_coords( - camera: &Camera, - screen_pos: egui::Pos2, - viewport_rect: egui::Rect, -) -> (glam::DVec3, glam::DVec3) { - let viewport_width = viewport_rect.width() as f64; - let viewport_height = viewport_rect.height() as f64; - - let ndc_x = 2.0 * (screen_pos.x as f64 - viewport_rect.min.x as f64) / viewport_width - 1.0; - let ndc_y = 1.0 - 2.0 * (screen_pos.y as f64 - viewport_rect.min.y as f64) / viewport_height; - - let inv_view = camera.view_mat.inverse(); - let inv_proj = camera.proj_mat.inverse(); - - let clip_near = glam::DVec4::new(ndc_x, ndc_y, 0.0, 1.0); - let clip_far = glam::DVec4::new(ndc_x, ndc_y, 1.0, 1.0); - - let view_near = inv_proj * clip_near; - let view_far = inv_proj * clip_far; - - let world_near = inv_view * glam::DVec4::new(view_near.x, view_near.y, view_near.z, 1.0); - let world_far = inv_view * glam::DVec4::new(view_far.x, view_far.y, view_far.z, 1.0); - - let world_near = world_near.truncate() / world_near.w; - let world_far = world_far.truncate() / world_far.w; - - (world_near, world_far) -} - -pub enum ViewportMode { - None, - CameraMove, - Gizmo, -} - -#[derive(Clone, Debug)] -pub struct PendingSpawn { - pub asset_path: PathBuf, - pub asset_name: String, - pub transform: Transform, - pub properties: ModelProperties, -} @@ -1 +1 @@ -Subproject commit bec111eb14ef956694099d6b7e9520b97814da09 +Subproject commit 5ff4de97ac7fbc6d6ffd92316eea52d27bec5e70 @@ -0,0 +1,1115 @@ +// Modules for the dropbear-engine scripting component +// Made by 4tkbytes +// EDIT THIS IF YOU WISH, I RECOMMEND YOU NOT TOUCH IT + +/** + * A class describing the position, scale and rotation to be able to manipulate + * the entity's location. + */ +export class Transform { + /** + * A {@link Vector3} describing the position of the entity + */ + position: Vector3; + /** + * A {@link Quaternion} describing the rotation of the entity + */ + rotation: Quaternion; + /** + * A {@link Vector3} describing the scale of the entity + */ + scale: Vector3; + + public constructor(position?: Vector3, rotation?: Quaternion, scale?: Vector3) { + this.position = position || Vector3.zero(); + this.rotation = rotation || Quaternion.identity(); + this.scale = scale || Vector3.one(); + } + + /** + * Translates/Offsets the position of the entity by a {@link Vector3} + * + * # Example + * @example + * ```ts + * let transform = new Transform(); + * transform.translate(new Vector3(1.0, 1.0, 1.0)); + * ``` + * + * @param movement - A {@link Vector3} for position + */ + public translate(movement: Vector3) { + this.position.x += movement.x; + this.position.y += movement.y; + this.position.z += movement.z; + } + + /** + * Rotates the transformables rotation on the X axis + * + * # Example + * @example + * ```ts + * let transform = new Transform(); + * transform.rotateX(dbMath.degreesToRadians(180)); + * ``` + * + * @param angle - The angle in radians + */ + public rotateX(angle: number) { + const rotQuat = Quaternion.fromAxisAngle(new Vector3(1, 0, 0), angle); + this.rotation = Quaternion.multiply(this.rotation, rotQuat); + + } + + /** + * Rotates the transformables rotation on the Y axis + * + * # Example + * ```ts + * let transform = new Transform(); + * transform.rotateY(dbMath.degreesToRadians(180)) + * ``` + * + * @param angle - The angle in radians + */ + public rotateY(angle: number) { + const rotQuat = Quaternion.fromAxisAngle(new Vector3(0, 1, 0), angle); + this.rotation = Quaternion.multiply(this.rotation, rotQuat); + } + + /** + * Rotates the transformables rotation on the Z axis + * + * # Example + * ```ts + * let transform = new Transform(); + * transform.rotateZ(dbMath.degreesToRadians(180)) + * ``` + * + * @param angle - The angle in radians + */ + public rotateZ(angle: number) { + const rotQuat = Quaternion.fromAxisAngle(new Vector3(0, 0, 1), angle); + this.rotation = Quaternion.multiply(this.rotation, rotQuat); + } + + /** + * Uniformly scales the entity by a multiplier. + * + * # Example + * ```ts + * let transform = new Transform(); + * transform.scaleUniform(2.0) + * ``` + * + * @param scale - A number that the scale multiplies by + */ + public scaleUniform(scale: number) { + this.scale.x *= scale; + this.scale.y *= scale; + this.scale.z *= scale; + } + + /** + * Individually scales the entity by a multiplier by using a {@link Vector3} + * + * # Example + * ```ts + * let transform = new Transform(); + * transform.scaleIndividual(new Vector3(1.0, 2.0, 1.5)) + * ``` + * + * @param scale - A Vector3 representing the scale.x, scale.y and scale.z values + */ + public scaleIndividual(scale: Vector3) { + this.scale.x *= scale.x; + this.scale.y *= scale.y; + this.scale.z *= scale.z; + } +} + +/** + * Utilities for math functions that do not exist in the TypeScript Math module. + */ +export const dbMath = { + /** + * Convert from degrees to radians + * + * # Example + * @example + * ```ts + * console.log(dbMath.degreesToRadians(180)) // expect Math.PI + * ``` + * + * @param deg - The angle in degrees + * @returns The angle in radians + */ + degreesToRadians:(deg: number):number => { + return deg * (Math.PI / 180.0); + }, + + /** + * Convert from radians to degrees + * + * # Example + * @example + * ```ts + * console.log(dbMath.radiansToDegrees(Math.PI)) // expect 180 + * ``` + * + * @param rad - The angle in radians + * @returns The angle in degrees + */ + radiansToDegrees:(rad: number):number => { + return (180 * rad) / Math.PI; + }, + + /** + * Constrains a number to lie within a specified range. If value is less than min, returns min. + * If value is greater than max, returns max. Otherwise, returns value. + * + * @example + * ```ts + * dropbear.dbMath.clamp(5, 0, 10) // → 5 + * dropbear.dbMath.clamp(-3, 0, 10) // → 0 + * dropbear.dbMath.clamp(15, 0, 10) // → 10 + * ``` + * + * @param value - The input value to clamp + * @param min - The lower bound of the range + * @param max - The upper bound of the range + * @returns - The clamped value + */ + clamp: (value: number, min: number, max: number): number => { + return Math.min(Math.max(value, min), max); + } +} + +/** + * A Vector of 3 components: an X, Y and Z. + */ +export class Vector3 { + /** + * The X value + */ + public x: number; + + /** + * The Y value + */ + public y: number; + /** + * The Z value + */ + public z: number; + + public constructor(x: number, y: number, z: number) { + this.x = x; + this.y = y; + this.z = z; + } + + /** + * Converts the {@link Vector3} class to a primitive number array + * + * # Example + * @example + * ```ts + * let vec = new Vector3(1.0, 1.5, 2.0); + * console.log(vec.as_array()) // expect [1.0, 1.5, 2.0] + * ``` + * + * @returns A number array representing the x, y, z values + */ + public as_array(): [number, number, number] { + return [this.x, this.y, this.z]; + } + + /** + * An alternative static constructor to create a {@link Vector3} with all values + * set to 0.0 + * + * # Example + * ```ts + * let vec = Vector3.zero(); + * console.log(vec); // expect [0.0, 0.0, 0.0] + * ``` + * + * @returns A new Vector3 instance with all values set to 0.0 + */ + public static zero(): Vector3 { + return new Vector3(0.0, 0.0, 0.0); + } + + /** + * An alternative static constructor to create a {@link Vector3} with all values + * set to 1.0 + * + * # Example + * ```ts + * let vec = Vector3.one(); + * console.log(vec); // expect [1.0, 1.0, 1.0] + * ``` + * + * @returns A new Vector3 instance with all values set to 1.0 + */ + public static one(): Vector3 { + return new Vector3(1.0, 1.0, 1.0); + } + + /** + * An alternative static constructor that creates a Vector3 from a number array. + * + * # Example + * @example + * ```ts + * let arr = [1.0, 1.5, 2.0]; + * let vec = Vector3.fromArray(arr); + * console.log(vec.to_array() === arr) // expect to print 'true' + * ``` + * + * @param arr - The number array to convert + * @returns - A new Vector3 instance + */ + public static fromArray(arr: [number, number, number]): Vector3 { + return new Vector3(arr[0], arr[1], arr[2]); + } + + /** + * Add another Vector3 to this vector and return the result as a new Vector3. + * + * @param rhs - Right-hand side vector to add. + * @returns A new Vector3 equal to (this + rhs). + * + * @example + * const a = new Vector3(1, 2, 3); + * const b = new Vector3(4, 5, 6); + * const c = a.add(b); // Vector3(5,7,9) + */ + public add(rhs: Vector3): Vector3 { + return new Vector3(this.x + rhs.x, this.y + rhs.y, this.z + rhs.z); + } + + /** + * Subtract another Vector3 from this vector and return the result as a new Vector3. + * + * @param rhs - Right-hand side vector to subtract. + * @returns A new Vector3 equal to (this - rhs). + * + * @example + * const a = new Vector3(5, 7, 9); + * const b = new Vector3(1, 2, 3); + * const c = a.subtract(b); // Vector3(4,5,6) + */ + public subtract(rhs: Vector3): Vector3 { + return new Vector3(this.x - rhs.x, this.y - rhs.y, this.z - rhs.z); + } + + /** + * Multiply this vector by a scalar and return the result as a new Vector3. + * + * @param rhs - Scalar multiplier. + * @returns A new Vector3 scaled by rhs. + * + * @example + * const v = new Vector3(1, 2, 3); + * const s = v.multiply(2); // Vector3(2,4,6) + */ + public multiply(rhs: number): Vector3 { + return new Vector3(this.x * rhs, this.y * rhs, this.z * rhs); + } + + /** + * Compute the Euclidean length (magnitude) of this vector. + * + * @returns The length sqrt(x*x + y*y + z*z). + * + * @example + * const v = new Vector3(1, 2, 2); + * console.log(v.length()); // 3 + */ + public length(): number { + return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z); + } + + /** + * Return a new Vector3 representing the normalized (unit) direction of this vector. + * If the vector has zero length, returns Vector3.zero(). + * + * @returns A normalized Vector3 or Vector3.zero() when length is 0. + * + * @example + * const v = new Vector3(0, 3, 4); + * const n = v.normalize(); // Vector3(0, 0.6, 0.8) + */ + public normalize(): Vector3 { + const len = this.length(); + if (len === 0) return Vector3.zero(); + return new Vector3(this.x / len, this.y / len, this.z / len); + } +} + +/** + * Quaternion representing a rotation in 3D space. + * + * Components are stored as (x, y, z, w) where w is the scalar part. + * Useful helpers are provided to construct quaternions from Euler angles, + * axis/angle, multiply them, and convert to/from arrays. + */ +export class Quaternion { + public x: number; + public y: number; + public z: number; + public w: number; + + /** + * Create a new quaternion. + * + * @param x - X component (default 0) + * @param y - Y component (default 0) + * @param z - Z component (default 0) + * @param w - W (scalar) component (default 1) + * + * @example + * const q = new Quaternion(); // identity + * const q2 = new Quaternion(0.1, 0.2, 0.3, 0.9); + */ + public constructor(x: number = 0, y: number = 0, z: number = 0, w: number = 1) { + this.x = x; + this.y = y; + this.z = z; + this.w = w; + } + + /** + * Construct a quaternion from Euler angles (radians). + * + * The parameters represent rotations about the X, Y and Z axes respectively. + * Angles must be provided in radians. + * + * @param x - rotation about the X axis in radians + * @param y - rotation about the Y axis in radians + * @param z - rotation about the Z axis in radians + * @returns A new Quaternion representing the composite rotation + * + * @example + * const q = Quaternion.fromEuler(Math.PI/2, 0, 0); // 90° around X + */ + public static fromEuler(x: number, y: number, z: number): Quaternion { + // Convert euler angles to quaternion + const cx = Math.cos(x * 0.5); + const sx = Math.sin(x * 0.5); + const cy = Math.cos(y * 0.5); + const sy = Math.sin(y * 0.5); + const cz = Math.cos(z * 0.5); + const sz = Math.sin(z * 0.5); + + return new Quaternion( + sx * cy * cz - cx * sy * sz, + cx * sy * cz + sx * cy * sz, + cx * cy * sz - sx * sy * cz, + cx * cy * cz + sx * sy * sz + ); + } + + /** + * Construct a quaternion representing a rotation around an axis. + * + * The axis will be normalized internally. Angle is in radians. + * + * @param axis - Rotation axis as a Vector3 + * @param angle - Rotation angle in radians + * @returns A new Quaternion representing the axis-angle rotation + * + * @example + * const q = Quaternion.fromAxisAngle(new Vector3(0,1,0), Math.PI); // 180° around Y + */ + public static fromAxisAngle(axis: Vector3, angle: number): Quaternion { + const halfAngle = angle * 0.5; + const sin = Math.sin(halfAngle); + const normalizedAxis = axis.normalize(); + + return new Quaternion( + normalizedAxis.x * sin, + normalizedAxis.y * sin, + normalizedAxis.z * sin, + Math.cos(halfAngle) + ); + } + + /** + * Multiply two quaternions. + * + * The result corresponds to the composition a * b (apply b, then a) using + * the multiplication implemented here. + * + * @param a - Left quaternion + * @param b - Right quaternion + * @returns The product quaternion + * + * @example + * const r = Quaternion.multiply(q1, q2); + */ + public static multiply(a: Quaternion, b: Quaternion): Quaternion { + return new Quaternion( + a.w * b.x + a.x * b.w + a.y * b.z - a.z * b.y, + a.w * b.y - a.x * b.z + a.y * b.w + a.z * b.x, + a.w * b.z + a.x * b.y - a.y * b.x + a.z * b.w, + a.w * b.w - a.x * b.x - a.y * b.y - a.z * b.z + ); + } + + /** + * Create a quaternion from a numeric array [x, y, z, w]. + * + * @param arr - Array containing quaternion components in order [x, y, z, w] + * @returns A new Quaternion with components taken from the array + * + * @example + * const q = Quaternion.fromArray([0, 0, 0, 1]); + */ + public static fromArray(arr: [number, number, number, number]): Quaternion { + return new Quaternion(arr[0], arr[1], arr[2], arr[3]); + } + + /** + * Return the identity quaternion (no rotation). + * + * @returns Quaternion equal to (0, 0, 0, 1) + * + * @example + * const id = Quaternion.identity(); + */ + public static identity(): Quaternion { + return new Quaternion(0.0, 0.0, 0.0, 1.0); + } + + /** + * Convert this quaternion to a numeric array [x, y, z, w]. + * + * @returns An array containing the quaternion components + * + * @example + * const arr = q.as_array(); // [x, y, z, w] + */ + public as_array(): [number, number, number, number] { + return [this.x, this.y, this.z, this.w]; + } +} + +/** + * Values of the Key codes. + */ +export const Keys = { + KeyA: "KeyA", KeyB: "KeyB", KeyC: "KeyC", KeyD: "KeyD", KeyE: "KeyE", KeyF: "KeyF", + KeyG: "KeyG", KeyH: "KeyH", KeyI: "KeyI", KeyJ: "KeyJ", KeyK: "KeyK", KeyL: "KeyL", + KeyM: "KeyM", KeyN: "KeyN", KeyO: "KeyO", KeyP: "KeyP", KeyQ: "KeyQ", KeyR: "KeyR", + KeyS: "KeyS", KeyT: "KeyT", KeyU: "KeyU", KeyV: "KeyV", KeyW: "KeyW", KeyX: "KeyX", + KeyY: "KeyY", KeyZ: "KeyZ", + Digit0: "Digit0", Digit1: "Digit1", Digit2: "Digit2", Digit3: "Digit3", Digit4: "Digit4", + Digit5: "Digit5", Digit6: "Digit6", Digit7: "Digit7", Digit8: "Digit8", Digit9: "Digit9", + Space: "Space", + ShiftLeft: "ShiftLeft", ShiftRight: "ShiftRight", + ControlLeft: "ControlLeft", ControlRight: "ControlRight", + AltLeft: "AltLeft", AltRight: "AltRight", + Escape: "Escape", Enter: "Enter", Tab: "Tab", + ArrowUp: "ArrowUp", ArrowDown: "ArrowDown", ArrowLeft: "ArrowLeft", ArrowRight: "ArrowRight", + F1: "F1", F2: "F2", F3: "F3", F4: "F4", F5: "F5", F6: "F6", + F7: "F7", F8: "F8", F9: "F9", F10: "F10", F11: "F11", F12: "F12", +} as const; + +export type KeyCode = typeof Keys[keyof typeof Keys]; + +/** + * The properties of the entity. + * From the eucalyptus editor, you are able to make custom + * properties such as 'speed' or 'health'. This class allows + * you to create and edit new value + * during the runtime of the script. + * + * Usage: + * ```ts + * props.setNumber("hp", 100); + * const hp = props.getNumber("hp"); // 100 + * ``` + */ +export class EntityProperties { + private data: Record<string, any>; + + /** + * Create an EntityProperties wrapper. + * + * @param data - Optional initial properties object. A shallow reference is kept. + */ + public constructor(data?: Record<string, any>) { + this.data = data || {}; + } + + /** + * Get the raw underlying properties object. + * + * Use this to serialize properties back to the host or perform bulk operations. + * + * @returns The raw Record<string, any> used to store properties. + * + * @example + * const raw = props.getRawProperties(); + */ + public getRawProperties(): Record<string, any> { + return this.data; + } + + /** + * Set a string property. + * + * @param key - Property key. + * @param value - String value to set. + * + * @example + * props.setString("tag", "friendly"); + */ + public setString(key: string, value: string): void { + this.data[key] = value; + } + + /** + * Set a numeric property. + * + * @param key - Property key. + * @param value - Number value to set. + * + * @example + * props.setNumber("speed", 4.2); + */ + public setNumber(key: string, value: number): void { + this.data[key] = value; + } + + /** + * Set a boolean property. + * + * @param key - Property key. + * @param value - Boolean value to set. + * + * @example + * props.setBool("isActive", true); + */ + public setBool(key: string, value: boolean): void { + this.data[key] = value; + } + + /** + * Get a string property. + * + * If the property is missing or falsy, an empty string ("") is returned. + * + * @param key - Property key. + * @returns The string value or "" when missing. + * + * @example + * const name = props.getString("name"); + */ + public getString(key: string): string { + return this.data[key] || ""; + } + + /** + * Get a numeric property. + * + * If the property is missing or falsy, 0 is returned. + * + * @param key - Property key. + * @returns The number value or 0 when missing. + * + * @example + * const speed = props.getNumber("speed"); + */ + public getNumber(key: string): number { + return this.data[key] || 0; + } + + /** + * Get a boolean property. + * + * If the property is missing or falsy, false is returned. + * + * @param key - Property key. + * @returns The boolean value or false when missing. + * + * @example + * const active = props.getBool("isActive"); + */ + public getBool(key: string): boolean { + return this.data[key] || false; + } + + /** + * Checks if the entity has a specific property as per a key. + * + * If the property exists, it will return true. If not, it will return false + * @param key - Property key. + * @returns - True is the value exists, false if not + * + * @example + * ```ts + * if props.hasProperty("speed") { + * let speed = props.getNumber("speed"); + * speed = speed+10; + * } else { + * console.log("No value as speed exists"); + * } + * ``` + */ + public hasProperty(key: string): boolean { + return key in this.data; + } +} + +export class Entity { + public label: string; + public transform: Transform; + public properties: EntityProperties; + + /** + * Creates a new instance of entity + * @param entityData - The entty data, typically parsed as an argument in the load or update functions + */ + constructor(label: string, properties: EntityData, transform: TransformData) { + this.label = label; + this.transform = createTransformFromData(transform); + this.properties = new EntityProperties(properties); + } + + /** + * Moves the player forward on the Z axis + * @param distance - The distance (as a number) it moves forward by + */ + moveForward(distance: number): void { + const movement = new Vector3(0, 0, -distance); + this.transform.translate(movement); + } + + /** + * Moves the player back on the Z axis + * @param distance - The distance (as a number) it moves back by + */ + moveBack(distance: number): void { + const movement = new Vector3(0, 0, distance); + this.transform.translate(movement); + } + + /** + * Moves the player left on the X axis + * @param distance - The distance (as a number) it moves left by + */ + moveLeft(distance: number): void { + const movement = new Vector3(-distance, 0, 0); + this.transform.translate(movement); + } + + /** + * Moves the player right on the X axis + * @param distance - The distance (as a number) it moves right by + */ + moveRight(distance: number): void { + const movement = new Vector3(distance, 0, 0); + this.transform.translate(movement); + } + + /** + * Moves the player up on the Y axis + * @param distance - The distance (as a number) it moves up by + */ + moveUp(distance: number): void { + const movement = new Vector3(0, distance, 0); + this.transform.translate(movement); + } + + /** + * Moves the player down on the Y axis + * @param distance - The distance (as a number) it moves down by + */ + moveDown(distance: number): void { + const movement = new Vector3(0, -distance, 0); + this.transform.translate(movement); + } +} + + +/** + * Helper function that creates a new {@link Transform} from + * a {@link TransformData} + * @param data - The raw transformable ({@link TransformData})data + * @returns - An instance of a {@link Transform} + */ +function createTransformFromData(data: TransformData): Transform { + const position = Vector3.fromArray(data.position); + const rotation = Quaternion.fromArray(data.rotation); + const scale = Vector3.fromArray(data.scale); + return new Transform(position, rotation, scale); +} + +/** + * Camera class for controlling and manipulating cameras in the scene. + * Provides functionality for camera movement, switching, and property manipulation. + */ +export class Camera { + public label: string; + + public eye: Vector3; + public target: Vector3; + public up: Vector3; + public aspect: number; + public fov: number; + public near: number; + public far: number; + public yaw: number; + public pitch: number; + public speed: number; + public sensitivity: number; + public camera_type: string; + + + /** + * Create a new Camera instance. + * + * @param data - Optional camera data to initialize from + */ + constructor(label: string, data: CameraData) { + this.eye = Vector3.fromArray(data.eye); + this.target = Vector3.fromArray(data.target); + this.up = Vector3.fromArray(data.up); + this.aspect = data.aspect; + this.fov = data.fov; + this.near = data.near; + this.far = data.far; + this.yaw = data.yaw; + this.pitch = data.pitch; + this.speed = data.speed; + this.sensitivity = data.sensitivity; + this.camera_type = data.camera_type; + this.label = label; + } + + /** + * Track mouse movement for camera look controls. + * + * @param delta - Mouse delta X and Y + * + * @example + * ```ts + * let delta = entity.getMouseDelta(); + * camera.track + * ``` + */ + trackMouseDelta(delta: [number, number]): void { + this.yaw += delta[0] * this.sensitivity; + this.pitch += delta[1] * this.sensitivity; + + this.pitch = dbMath.clamp(this.pitch, dbMath.degreesToRadians(-89.0), dbMath.degreesToRadians(89.0)); + + + let direction = new Vector3( + Math.cos(this.yaw) * Math.cos(this.pitch), + Math.sin(this.pitch), + Math.sin(this.yaw) * Math.cos(this.pitch), + ); + this.target = this.eye.add(direction); + } +} + +export class Light { + +} + +/** + * A raw format for storing the transform data, typically used as a + * FFI by the engine + */ +export interface TransformData { + position: [number, number, number]; + rotation: [number, number, number, number]; // quaternion [x, y, z, w] + scale: [number, number, number]; +} + +/** + * A raw format for storing the custom entity properties from the engines + * raw FFI + */ +export interface EntityData { + custom_properties: Record<string, any>; +} + +/** + * A raw format for storing the input data + */ +export interface InputData { + mouse_pos: [number, number]; + pressed_keys: string[]; + mouse_delta: [number, number] | null; + is_cursor_locked: boolean; +} + +/** + * A raw format for storing the camera data + */ +export interface CameraData { + eye: [number, number, number]; + target: [number, number, number]; + up: [number, number, number]; + aspect: number; + fov: number; + near: number; + far: number; + yaw: number; + pitch: number; + speed: number; + sensitivity: number; + camera_type: string; +} + +/** + * A raw format for storing the light data + */ +export interface LightData { + +} + +/** + * A raw format for storing the scene data + */ +export interface RawSceneData { + entities: [{ + label: string, + properties: EntityData, + transform: TransformData + }]; + cameras: [{ + label: string, + data: CameraData + }]; + lights: [{ + label: string, + data: LightData + }]; + input: InputData; +} + +/** + * A wrapper class aimed to aid with input data + */ +export class Input { + public inputData?: InputData + + // constructor(data: InputData) { + // this.inputData = data; + // } + + // empty because global variable + constructor() { + + } + + /** + * Checks if a key is pressed + * @param key - The specific keycode + * @returns - True is pressed, false if not + * + * @example + * ```ts + * if entity.isKeyPressed(Keys::KeyW) { + * console.log("The W key is pressed"); + * } + * ``` + */ + isKeyPressed(key: KeyCode): boolean { + if (!this.inputData) return false; + return this.inputData.pressed_keys.indexOf(key) !== -1 + } + + /** + * Fetches the mouse position + * @returns - The x,y position of the mouse + */ + getMousePosition(): [number, number] { + return this.inputData?.mouse_pos || [0, 0]; + } + + /** + * Fetches the change in the mouse position from the center (as it gets reset each frame) + * @returns - The dx,dy position of the mouse + */ + getMouseDelta(): [number, number] { + return this.inputData?.mouse_delta || [0.0, 0.0]; + } +} + +/** + * The class containing all the different entities in this scene. + */ +export class Scene { + // ensure none of these are public + current_entity?: string; + entities?: Entity[]; + cameras?: Camera[]; + lights?: Light[]; + + // Sets everything to default + constructor() { + this.cameras = []; + this.entities = []; + this.lights = []; + } + + /** + * Returns a **reference** to the camera in the scene + * @param label - The label of the camera as set by you from the editor + */ + public getCamera(label: string): Camera | undefined { + return this.cameras?.find(c => c.label === label); + } + + /** + * Returns a **reference** to the light in the scene + * @param label - The label of the light as set by you from the editor + */ + public getLight(label: string): Light | undefined { + return this.lights?.find(l => (l as any).label === label); + } + + /** + * Fetches an entity as per its label. Returns the actual Entity instance (mutable reference). + * @param label - The label of the entity + */ + public getEntity(label: string): Entity | undefined { + return this.entities?.find(e => e.label === label); + } + + /** + * Fetches the current entity this script is attached to (returns mutable reference). + */ + public getCurrentEntity(): Entity | undefined { + if (!this.current_entity) { + console.error("Unable to get entity: Have you added dropbear.start(s) yet?"); + return undefined; + } + const ent = this.getEntity(this.current_entity); + if (!ent) { + console.error(`Unable to get entity: no entity with label "${this.current_entity}" found`); + } + return ent; + } +} + +/** + * Starts the specific function by filling the scene data. + * @param data + */ +export function start(data: RawSceneData) { + data.cameras.forEach(camera => { + scene.cameras?.push(new Camera(camera.label, camera.data)) + }); + data.entities.forEach(entity => { + scene.entities?.push(new Entity(entity.label, entity.properties, entity.transform)) + }); + data.lights.forEach(light => { + scene.lights?.push(light) + }); + input.inputData = data.input; +} + +/** + * Ends the scripting function by returning a Partial RawSceneData for the + * rust client to take. + */ +export function end(): Partial<RawSceneData> { + const out: Partial<RawSceneData> = {}; + + if (scene.entities && scene.entities.length) { + out.entities = scene.entities.map(e => { + return { + label: e.label, + properties: { + custom_properties: e.properties.getRawProperties() + } as EntityData, + transform: { + position: e.transform.position.as_array(), + rotation: e.transform.rotation.as_array(), + scale: e.transform.scale.as_array() + } as TransformData + }; + }) as any; + } + + if (scene.cameras && scene.cameras.length) { + out.cameras = scene.cameras.map(c => { + return { + label: c.label, + data: { + eye: c.eye.as_array(), + target: c.target.as_array(), + up: c.up.as_array(), + aspect: c.aspect, + fov: c.fov, + near: c.near, + far: c.far, + yaw: c.yaw, + pitch: c.pitch, + speed: c.speed, + sensitivity: c.sensitivity, + camera_type: c.camera_type + } as CameraData + }; + }) as any; + } + + if (scene.lights && scene.lights.length) { + out.lights = scene.lights.map(l => { + return { + label: (l as any).label || "", + data: {} as LightData + }; + }) as any; + } + + if (input.inputData) { + out.input = input.inputData; + } + + return out; +} + +// global variables +/** + * A global variable that contains all the information about the scene. + * + * To use the variable, you need to run {@link start()} at the start of + * the function and return {@link end()} at the end to send to the engine. + * + * @example + * ```ts + * export function onUpdate(s, dt: number) { + * dropbear.start(s); + * console.log("I'm being updated!"); + * return dropbear.end(); + * } + * ``` + */ +export const scene = new Scene(); +/** + * A global variable that contains all the information about the inputs + * such as the keyboard and the mouse. + * + * To use the variable, you need to run {@link start()} at the start of + * the function and return {@link end()} at the end to send to the engine. + * + * @example + * ```ts + * export function onUpdate(s, dt: number) { + * dropbear.start(s); + * console.log("I'm being updated!"); + * return dropbear.end(); + * } + */ +export const input = new Input(); @@ -0,0 +1,61 @@ +// Shader for rendering the white cube for lighting (or diff depending on colour) + +struct Camera { + view_pos: vec4<f32>, + view_proj: mat4x4<f32>, +} +@group(0) @binding(0) +var<uniform> camera: Camera; + +struct Light { + position: vec4<f32>, + direction: vec4<f32>, // x, y, z, outer_cutoff_angle + color: vec4<f32>, // r, g, b, light_type (0, 1, 2) + constant: f32, + lin: f32, + quadratic: f32, + cutoff: f32, +} + +@group(1) @binding(0) +var<uniform> light: Light; + +struct InstanceInput { + @location(5) model_matrix_0: vec4<f32>, + @location(6) model_matrix_1: vec4<f32>, + @location(7) model_matrix_2: vec4<f32>, + @location(8) model_matrix_3: vec4<f32>, +} + +struct VertexInput { + @location(0) position: vec3<f32>, +}; + +struct VertexOutput { + @builtin(position) clip_position: vec4<f32>, + @location(0) color: vec3<f32>, +}; + +@vertex +fn vs_main( + model: VertexInput, + instance: InstanceInput, +) -> VertexOutput { + let model_matrix = mat4x4<f32>( + instance.model_matrix_0, + instance.model_matrix_1, + instance.model_matrix_2, + instance.model_matrix_3, + ); + var out: VertexOutput; + out.clip_position = camera.view_proj * model_matrix * vec4<f32>(model.position, 1.0); + out.color = light.color.xyz; + return out; +} + +// Fragment shader + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> { + return vec4<f32>(in.color, 1.0); +} @@ -0,0 +1,206 @@ +// Main shader for standard objects. +const MAX_LIGHTS: u32 = 8; + +struct CameraUniform { + view_pos: vec4<f32>, + view_proj: mat4x4<f32>, +}; +@group(1) @binding(0) +var<uniform> camera: CameraUniform; + +struct Light { + position: vec4<f32>, // x, y, z, unused + direction: vec4<f32>, // x, y, z, unused + color: vec4<f32>, // r, g, b, light_type (0, 1, 2) + constant: f32, + lin: f32, // linear + quadratic: f32, + cutoff: f32, +} + +struct LightArray { + lights: array<Light, MAX_LIGHTS>, + light_count: u32, + ambient_strength: f32, +} + +@group(2) @binding(0) +var<uniform> light_array: LightArray; + +struct InstanceInput { + @location(5) model_matrix_0: vec4<f32>, + @location(6) model_matrix_1: vec4<f32>, + @location(7) model_matrix_2: vec4<f32>, + @location(8) model_matrix_3: vec4<f32>, + + @location(9) normal_matrix_0: vec3<f32>, + @location(10) normal_matrix_1: vec3<f32>, + @location(11) normal_matrix_2: vec3<f32>, +}; + +struct VertexInput { + @location(0) position: vec3<f32>, + @location(1) tex_coords: vec2<f32>, + @location(2) normal: vec3<f32>, +}; + +struct VertexOutput { + @builtin(position) clip_position: vec4<f32>, + @location(0) tex_coords: vec2<f32>, + @location(1) world_normal: vec3<f32>, + @location(2) world_position: vec3<f32>, +}; + +@vertex +fn vs_main( + model: VertexInput, + instance: InstanceInput, +) -> VertexOutput { + let model_matrix = mat4x4<f32>( + instance.model_matrix_0, + instance.model_matrix_1, + instance.model_matrix_2, + instance.model_matrix_3, + ); + let normal_matrix = mat3x3<f32>( + instance.normal_matrix_0, + instance.normal_matrix_1, + instance.normal_matrix_2, + ); + var out: VertexOutput; + out.tex_coords = model.tex_coords; + out.world_normal = normal_matrix * model.normal; + var world_position: vec4<f32> = model_matrix * vec4<f32>(model.position, 1.0); + out.world_position = world_position.xyz; + out.clip_position = camera.view_proj * world_position; + return out; +} + +@group(0) @binding(0) +var t_diffuse: texture_2d<f32>; +@group(0) @binding(1) +var s_diffuse: sampler; + +fn calculate_light(light: Light, world_pos: vec3<f32>, world_normal: vec3<f32>, view_dir: vec3<f32>) -> vec3<f32> { + let light_dir = normalize(light.position.xyz - world_pos); + + // dihfuse + let diffuse_strength = max(dot(world_normal, light_dir), 0.0); + let diffuse_color = light.color.xyz * diffuse_strength; + + // specular + let half_dir = normalize(view_dir + light_dir); + let specular_strength = pow(max(dot(world_normal, half_dir), 0.0), 32.0); + let specular_color = specular_strength * light.color.xyz; + + return diffuse_color + specular_color; +} + +fn directional_light( + light: Light, + world_normal: vec3<f32>, + view_dir: vec3<f32>, + tex_color: vec3<f32> +) -> vec3<f32> { + let light_dir = normalize(-light.direction.xyz); + + let ambient = light.color.xyz * light_array.ambient_strength * tex_color; + + let diff = max(dot(world_normal, light_dir), 0.0); + let diffuse = light.color.xyz * diff * tex_color; + + let reflect_dir = reflect(-light_dir, world_normal); + let spec = pow(max(dot(view_dir, reflect_dir), 0.0), 32.0); + let specular = light.color.xyz * spec * tex_color; + + return ambient + diffuse + specular; +} + +// https://learnopengl.com/code_viewer_gh.php?code=src/2.lighting/5.2.light_casters_point/5.2.light_casters.fs +// deal with later. current issue: it is showing only yellow and white in point light (weird...) +// note: fixed, forgot to push attenuation values to gpu lol +fn point_light(light: Light, world_pos: vec3<f32>, world_normal: vec3<f32>, view_dir: vec3<f32>, tex_color: vec3<f32>) -> vec3<f32> { + let norm = normalize(world_normal); + let light_dir = normalize(light.position.xyz - world_pos); + let diff = max(dot(norm, light_dir), 0.0); + let diffuse = light.color.xyz * diff * tex_color; + + let shininess = 32.0; + let reflect_dir = reflect(-light_dir, norm); + let spec = pow(max(dot(view_dir, reflect_dir), 0.0), shininess); + let specular = light.color.xyz * spec * tex_color; + + let distance = length(light.position.xyz - world_pos); + let attenuation = 1.0 / (light.constant + (light.lin * distance) + (light.quadratic * (distance * distance))); + + return (diffuse + specular) * attenuation; +} + +fn spot_light(light: Light, world_pos: vec3<f32>, world_normal: vec3<f32>, view_dir: vec3<f32>, tex_color: vec3<f32>) -> vec3<f32> { + let outer_cutoff = light.direction.w; + let ambient = light.color.xyz * light_array.ambient_strength * tex_color; + + let norm = normalize(world_normal); + let light_dir = normalize(light.position.xyz - world_pos); + let diff = max(dot(norm, light_dir), 0.0); + var diffuse = light.color.xyz * diff * tex_color; + + let shininess = 32.0; + let reflect_dir = reflect(-light_dir, norm); + let spec = pow(max(dot(view_dir, reflect_dir), 0.0), shininess); + var specular = light.color.xyz * spec * tex_color; + + let theta = dot(light_dir, normalize(-light.direction.xyz)); + let epsilon = light.cutoff - outer_cutoff; + let intensity = clamp((theta - outer_cutoff) / epsilon, 0.0, 1.0); + + diffuse *= intensity; + specular *= intensity; + + let distance = length(light.position.xyz - world_pos); + let attenuation = 1.0 / (light.constant + (light.lin * distance) + (light.quadratic * (distance * distance))); + + let ambient_attenuated = ambient * attenuation; + let diffuse_attenuated = diffuse * attenuation; + let specular_attenuated = specular * attenuation; + + return ambient_attenuated + diffuse_attenuated + specular_attenuated; +} + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> { + var tex_color = textureSample(t_diffuse, s_diffuse, in.tex_coords); + if (tex_color.a < 0.1) { + discard; + } + + let view_dir = normalize(camera.view_pos.xyz - in.world_position); + let world_normal = normalize(in.world_normal); + + var final_color = vec3<f32>(0.0); + + var total_ambient = vec3<f32>(0.0); + for (var i: u32 = 0u; i < light_array.light_count; i = i + 1u) { + let light = light_array.lights[i]; + total_ambient += light.color.xyz * light_array.ambient_strength; + } + + for (var i: u32 = 0u; i < light_array.light_count; i = i + 1u) { + let light = light_array.lights[i]; + + // light type is color.w + if light.color.w == 0.0 { + final_color += directional_light(light, world_normal, view_dir, tex_color.xyz); + } else if light.color.w == 1.0 { + // point + final_color += point_light(light, in.world_position, world_normal, view_dir, tex_color.xyz); + } else if light.color.w == 2.0 { + final_color += spot_light(light, in.world_position, world_normal, view_dir, tex_color.xyz); + } + } + + // Combine ambient and lighting + final_color = (total_ambient * tex_color.xyz) + final_color; + + return vec4<f32>(final_color, tex_color.a); +} @@ -0,0 +1,25 @@ +// dropbear-engine script template for eucalyptus +import * as dropbear from "./dropbear"; + +export function onLoad(s) { + dropbear.start(s); + // ------- Your own code here ------- + console.log("I have been awoken"); + + + // ---------------------------------- + // Do not remove anything outside unless + // you know what you are doing. + return dropbear.end(); +} + +export function onUpdate(s, dt: number) { + dropbear.start(s); + // ------- Your own code here ------- + console.log("I'm being updated!"); + + + // ---------------------------------- + // Same thing over here! + return dropbear.end(); +}