tirbofish/dropbear · commit
d18cb60e38502095cea8a533bca61b0f0add9f26
wip: implementing new classes such as SceneMetadata.kt, ResourceReference.kt and AssetEntry.kt
wip: attempts to do viewport clicking within the editor. would be good for stuff like interactive UI in billboarding (idk if its implemented yet).
fix: changed default ambient strength to 0.1 instead of 1.0 to not make it look like its bad
Signature present but could not be verified.
Unverified
@@ -1,6 +1,7 @@ //! Input management and input state. pub mod gamepad; +pub mod ndc; use crate::scripting::jni::utils::ToJObject; use crate::scripting::native::DropbearNativeError; @@ -0,0 +1,62 @@ +use glam::{Vec3, Mat4, Vec4}; + +/// Helpers for converting from a 2D display to a 3D object, such as billboard ui and some +/// other applications. +// viewport aint even work lmaoooo +pub struct NormalisedDeviceCoordinates; + +impl NormalisedDeviceCoordinates { + pub fn screen_to_ray( + touch_pos: impl Into<[f32; 2]>, + screen_dims: impl Into<[f32; 2]>, + inv_proj: Mat4, + inv_view: Mat4, + ) -> (Vec3, Vec3) { + let [touch_x, touch_y] = touch_pos.into(); + let [screen_width, screen_height] = screen_dims.into(); + + let ndc_x = (touch_x / screen_width) * 2.0 - 1.0; + let ndc_y = -(touch_y / screen_height) * 2.0 + 1.0; // flip Y, screen Y is down + + // unproject to view space + let clip_near = Vec4::new(ndc_x, ndc_y, 0.0, 1.0); // z=0 = near plane + let mut view_near = inv_proj * clip_near; + view_near /= view_near.w; + + // to world space + let world_near = inv_view * view_near; + + // ray origin = camera position, direction = toward unprojected point + let ray_origin = (inv_view * Vec4::new(0.0, 0.0, 0.0, 1.0)).truncate(); + let ray_dir = (world_near.truncate() - ray_origin).normalize(); + + (ray_origin, ray_dir) + } + + pub fn ray_aabb( + origin: Vec3, + dir: Vec3, + aabb_min: Vec3, + aabb_max: Vec3, + ) -> Option<f32> { + let inv_dir = 1.0 / dir; + let t1 = (aabb_min - origin) * inv_dir; + let t2 = (aabb_max - origin) * inv_dir; + let t_min = t1.min(t2).max_element(); + let t_max = t1.max(t2).min_element(); + if t_max >= t_min && t_max > 0.0 { Some(t_min) } else { None } + } + + pub fn ray_plane( + origin: Vec3, + dir: Vec3, + plane_normal: Vec3, + plane_d: f32, + ) -> Option<Vec3> { + let denom = plane_normal.dot(dir); + if denom.abs() < 1e-10 { return None; } // ray parallel to plane + let t = (plane_d - plane_normal.dot(origin)) / denom; + if t < 0.0 { return None; } // intersection behind ray + Some(origin + dir * t) + } +} @@ -46,7 +46,6 @@ pub struct AssetEntry { /// Path to the compiled output (e.g. `compiled/meshes/f47ac10b.eucmdl`), /// relative to the project root. - /// `None` for `Embedded` and `Procedural` assets which have no compiled form. pub compiled_path: Option<PathBuf>, /// SHA-256 (or xxHash) of the source file at last successful import. @@ -95,7 +95,7 @@ impl SceneSettings { show_hitboxes: false, overlay_hud: false, overlay_billboard: true, - ambient_strength: 1.0, + ambient_strength: 0.1, } } @@ -1,1484 +0,0 @@ -use dropbear_engine::asset::ASSET_REGISTRY; -use dropbear_engine::model::Model; -use dropbear_engine::texture::TextureBuilder; -use dropbear_engine::{graphics::NO_TEXTURE, utils::ResourceReference}; -use egui_ltreeview::{Action, NodeBuilder, TreeViewBuilder}; -use eucalyptus_core::ser::model::EucalyptusModel; -use eucalyptus_core::states::PROJECT; -use eucalyptus_core::utils::ResolveReference; -use hecs::Entity; -use log::{info, warn}; -use std::hash::{Hash, Hasher}; -use std::{cmp::Ordering, fs, hash::DefaultHasher, io, path::{Path, PathBuf}}; - -use crate::editor::{ - AssetDivision, AssetNodeInfo, AssetNodeKind, ComponentNodeSelection, DraggedAsset, - EditorTabDock, EditorTabDockDescriptor, EditorTabViewer, FsEntry, ResourceDivision, - SceneDivision, ScriptDivision, Signal, StaticallyKept, TABS_GLOBAL, -}; -use eucalyptus_core::component::DRAGGED_ASSET_ID; -use eucalyptus_core::hierarchy::Hierarchy; -use crate::editor::page::EditorTabVisibility; - -impl<'a> EditorTabViewer<'a> { - pub(crate) fn show_asset_viewer(&mut self, ui: &mut egui::Ui) { - let mut cfg = TABS_GLOBAL.lock(); - cfg.asset_node_assets.clear(); - cfg.asset_node_info.clear(); - if let Some(rename) = &cfg.asset_rename { - if !rename.original_path.exists() { - cfg.asset_rename = None; - } - } - - if let Some(mut rename) = cfg.asset_rename.take() { - let rename_id = egui::Id::new("asset_rename_input"); - let mut should_apply = false; - - ui.horizontal(|ui| { - ui.label("Rename"); - let response = ui.add(egui::TextEdit::singleline(&mut rename.buffer).id(rename_id)); - if rename.just_started { - ui.ctx().memory_mut(|m| m.request_focus(rename_id)); - rename.just_started = false; - } - - let enter = ui.input(|input| input.key_pressed(egui::Key::Enter)); - should_apply = (enter && response.has_focus()) || response.lost_focus(); - }); - - if should_apply { - let is_dir = rename.original_path.is_dir(); - self.apply_asset_rename(&rename, is_dir); - } else { - cfg.asset_rename = Some(rename); - } - - ui.separator(); - } - - let project_root = { - let project = PROJECT.read(); - if project.project_path.as_os_str().is_empty() { - ui.label("Open a project to browse assets."); - return; - } - project.project_path.clone() - }; - - let (_resp, action) = - egui_ltreeview::TreeView::new(egui::Id::new("asset_viewer")).show(ui, |builder| { - builder.node(Self::dir_node("euca://")); - self.build_resource_branch(&mut cfg, builder, &project_root); - self.build_scripts_branch(&mut cfg, builder, &project_root); - self.build_scene_branch(&mut cfg, builder, &project_root); - builder.close_dir(); - }); - - for a in action { - match a { - Action::SetSelected(selected) => { - log_once::debug_once!("Selected: {:?}", selected); - } - Action::Move(moved) => { - log_once::debug_once!("Moved: {:?}", moved); - self.handle_asset_move(&mut cfg, &moved); - } - Action::Drag(dragged) => { - log_once::debug_once!("Dragged: {:?}", dragged); - - if let Some(&node_id) = dragged.source.first() { - if let Some(asset) = cfg.asset_node_assets.get(&node_id).cloned() { - cfg.dragged_asset = Some(asset.clone()); - ui.ctx().data_mut(|d| { - d.insert_temp( - egui::Id::new(DRAGGED_ASSET_ID), - Some(asset.path.clone()), - ) - }); - } - } - } - Action::Activate(activated) => { - log_once::debug_once!("Activated: {:?}", activated); - } - Action::DragExternal(_) => {} - Action::MoveExternal(_) => {} - } - } - } - - fn build_resource_branch( - &mut self, - cfg: &mut StaticallyKept, - builder: &mut TreeViewBuilder<u64>, - project_root: &Path, - ) { - let label = "euca://resources"; - let resources_root = project_root.join("resources"); - let root_info = AssetNodeInfo { - path: resources_root.clone(), - division: AssetDivision::Resources, - kind: AssetNodeKind::Resource(ResourceDivision::Folder), - is_dir: true, - is_division_root: true, - allow_add_folder: true, - }; - Self::register_asset_node(cfg, label, root_info.clone()); - let node_id = Self::asset_node_id(label); - let menu = Self::dir_node_kind(label, "resources", root_info.kind).context_menu(|ui| { - self.asset_dir_context_menu(cfg, ui, node_id, &root_info, "New Folder") - }); - builder.node(menu); - if resources_root.exists() { - self.walk_resource_directory(cfg, builder, &resources_root, &resources_root); - } else { - Self::add_placeholder_leaf( - builder, - "euca://resources/missing", - "missing", - AssetNodeKind::Resource(ResourceDivision::File), - ); - } - builder.close_dir(); - } - - fn walk_resource_directory( - &mut self, - cfg: &mut StaticallyKept, - builder: &mut TreeViewBuilder<u64>, - base_path: &Path, - current_path: &Path, - ) { - let entries = match Self::sorted_entries(current_path) { - Ok(entries) => entries, - Err(err) => { - log_once::warn_once!( - "Unable to enumerate resources at '{}': {}", - current_path.parent().unwrap_or(current_path).display(), - err - ); - return; - } - }; - - for entry in entries { - let full_label = Self::resource_label(base_path, &entry.path); - if entry.is_dir { - let dir_info = AssetNodeInfo { - path: entry.path.clone(), - division: AssetDivision::Resources, - kind: AssetNodeKind::Resource(ResourceDivision::Folder), - is_dir: true, - is_division_root: false, - allow_add_folder: true, - }; - Self::register_asset_node(cfg, &full_label, dir_info.clone()); - let node_id = Self::asset_node_id(&full_label); - let menu = Self::dir_node_kind(&full_label, &entry.name, dir_info.kind) - .context_menu(|ui| { - self.asset_dir_context_menu(cfg, ui, node_id, &dir_info, "New Folder") - }); - builder.node(menu); - self.walk_resource_directory(cfg, builder, base_path, &entry.path); - builder.close_dir(); - } else { - if entry.name.ends_with(".eucmeta") { - continue; - } - - let reference = ResourceReference::from_euca_uri(&full_label) - .unwrap_or_else(|_| ResourceReference::default()); - cfg.asset_node_assets.insert( - Self::asset_node_id(&full_label), - DraggedAsset { - name: entry.name.clone(), - path: reference.clone(), - }, - ); - let is_model = Self::is_model_file(&entry.name); - let is_texture = Self::is_texture_file(&entry.name); - let ext = entry - .path - .extension() - .and_then(|v| v.to_str()) - .map(|v| v.to_ascii_lowercase()); - let is_eucbin = matches!(ext.as_deref(), Some("eucbin")); - let is_eucmdl = matches!(ext.as_deref(), Some("eucmdl")); - let entry_name = entry.name.clone(); - let reference_for_menu = reference.clone(); - let file_info = AssetNodeInfo { - path: entry.path.clone(), - division: AssetDivision::Resources, - kind: AssetNodeKind::Resource(ResourceDivision::File), - is_dir: false, - is_division_root: false, - allow_add_folder: false, - }; - Self::register_asset_node(cfg, &full_label, file_info.clone()); - let node_id = Self::asset_node_id(&full_label); - let menu = Self::leaf_node_kind(&full_label, &entry.name, file_info.kind) - .context_menu(|ui| { - self.asset_file_context_menu(cfg, ui, node_id, &file_info); - ui.separator(); - - if is_model || is_eucmdl { - if ui.button("Load model to memory").clicked() { - ui.close(); - self.queue_model_load( - reference_for_menu.clone(), - entry_name.clone(), - ); - info!("Loading model {}", entry_name); - } - } - - if is_texture { - if ui.button("Load texture to memory").clicked() { - ui.close(); - self.queue_texture_load( - reference_for_menu.clone(), - entry_name.clone(), - false, - ); - info!("Loading texture {}", entry_name); - } - } - - if is_eucbin { - if ui.button("Load as model to memory").clicked() { - ui.close(); - self.queue_model_load( - reference_for_menu.clone(), - entry_name.clone(), - ); - info!("Loading eucbin as model {}", entry_name); - } - - if ui.button("Load as texture to memory").clicked() { - ui.close(); - self.queue_texture_load( - reference_for_menu.clone(), - entry_name.clone(), - true, - ); - info!("Loading eucbin as texture {}", entry_name); - } - } - }); - builder.node(menu); - } - } - } - - fn resource_label(base_path: &Path, path: &Path) -> String { - let relative = path - .strip_prefix(base_path) - .map(|rel| rel.to_string_lossy().replace('\\', "/")) - .unwrap_or_else(|_| path.to_string_lossy().replace('\\', "/")); - if relative.is_empty() { - "euca://resources".to_string() - } else { - format!("euca://resources/{}", relative) - } - } - - fn build_scripts_branch( - &mut self, - cfg: &mut StaticallyKept, - builder: &mut TreeViewBuilder<u64>, - project_root: &Path, - ) { - let label = "euca://scripts"; - let scripts_root = project_root.join("src"); - let root_info = AssetNodeInfo { - path: scripts_root.clone(), - division: AssetDivision::Scripts, - kind: AssetNodeKind::Script(ScriptDivision::Package), - is_dir: true, - is_division_root: true, - allow_add_folder: false, - }; - Self::register_asset_node(cfg, label, root_info.clone()); - let node_id = Self::asset_node_id(label); - let menu = Self::dir_node_kind(label, "scripts", root_info.kind).context_menu(|ui| { - self.asset_dir_context_menu(cfg, ui, node_id, &root_info, "New Package") - }); - builder.node(menu); - if !scripts_root.exists() { - Self::add_placeholder_leaf( - builder, - "euca://scripts/missing", - "missing", - AssetNodeKind::Script(ScriptDivision::Script), - ); - builder.close_dir(); - return; - } - - let entries = match Self::sorted_entries(&scripts_root) { - Ok(entries) => entries, - Err(err) => { - log::warn!( - "Unable to enumerate scripts at '{}': {}", - scripts_root.display(), - err - ); - builder.close_dir(); - return; - } - }; - - let mut had_content = false; - for entry in entries { - if entry.is_dir { - let source_label = format!("{}/{}", label, entry.name); - let kotlin_root = entry.path.join("kotlin"); - let source_info = AssetNodeInfo { - path: kotlin_root, - division: AssetDivision::Scripts, - kind: AssetNodeKind::Script(ScriptDivision::Package), - is_dir: true, - is_division_root: true, - allow_add_folder: true, - }; - Self::register_asset_node(cfg, &source_label, source_info.clone()); - let node_id = Self::asset_node_id(&source_label); - let menu = Self::dir_node_kind(&source_label, &entry.name, source_info.kind) - .context_menu(|ui| { - self.asset_dir_context_menu(cfg, ui, node_id, &source_info, "New Package") - }); - builder.node(menu); - if self.build_script_source_set(cfg, builder, &entry.path, &source_label) { - had_content = true; - } - builder.close_dir(); - } else if !entry.name.eq_ignore_ascii_case("source.eucc") { - let file_label = format!("{}/{}", label, entry.name); - let file_info = AssetNodeInfo { - path: entry.path.clone(), - division: AssetDivision::Scripts, - kind: AssetNodeKind::Script(ScriptDivision::Script), - is_dir: false, - is_division_root: false, - allow_add_folder: false, - }; - Self::register_asset_node(cfg, &file_label, file_info.clone()); - let node_id = Self::asset_node_id(&file_label); - let menu = Self::leaf_node_kind(&file_label, &entry.name, file_info.kind) - .context_menu(|ui| self.asset_file_context_menu(cfg, ui, node_id, &file_info)); - builder.node(menu); - had_content = true; - } - } - - if !had_content { - Self::add_placeholder_leaf( - builder, - "euca://scripts/empty", - "empty", - AssetNodeKind::Script(ScriptDivision::Script), - ); - } - - builder.close_dir(); - } - - fn build_script_source_set( - &mut self, - cfg: &mut StaticallyKept, - builder: &mut TreeViewBuilder<u64>, - source_path: &Path, - source_label: &str, - ) -> bool { - let entries = match Self::sorted_entries(source_path) { - Ok(entries) => entries, - Err(err) => { - log::warn!( - "Unable to enumerate source set at '{}': {}", - source_path.display(), - err - ); - Self::add_placeholder_leaf( - builder, - &format!("{source_label}/unreadable"), - "unreadable", - AssetNodeKind::Script(ScriptDivision::Script), - ); - return true; - } - }; - - let mut had_content = false; - for entry in entries { - if entry.is_dir { - if entry.name.eq_ignore_ascii_case("kotlin") { - if self.build_kotlin_tree(cfg, builder, &entry.path, source_label) { - had_content = true; - } - } else { - let child_label = format!("{}/{}", source_label, entry.name); - let dir_info = AssetNodeInfo { - path: entry.path.clone(), - division: AssetDivision::Scripts, - kind: AssetNodeKind::Script(ScriptDivision::Package), - is_dir: true, - is_division_root: false, - allow_add_folder: true, - }; - Self::register_asset_node(cfg, &child_label, dir_info.clone()); - let node_id = Self::asset_node_id(&child_label); - let menu = Self::dir_node_kind(&child_label, &entry.name, dir_info.kind) - .context_menu(|ui| { - self.asset_dir_context_menu(cfg, ui, node_id, &dir_info, "New Package") - }); - builder.node(menu); - self.build_plain_directory( - cfg, - builder, - &entry.path, - &child_label, - AssetDivision::Scripts, - AssetNodeKind::Script(ScriptDivision::Package), - AssetNodeKind::Script(ScriptDivision::Script), - "New Package", - ); - builder.close_dir(); - had_content = true; - } - } else if !entry.name.eq_ignore_ascii_case("source.eucc") { - let file_label = format!("{}/{}", source_label, entry.name); - let file_info = AssetNodeInfo { - path: entry.path.clone(), - division: AssetDivision::Scripts, - kind: AssetNodeKind::Script(ScriptDivision::Script), - is_dir: false, - is_division_root: false, - allow_add_folder: false, - }; - Self::register_asset_node(cfg, &file_label, file_info.clone()); - let node_id = Self::asset_node_id(&file_label); - let menu = Self::leaf_node_kind(&file_label, &entry.name, file_info.kind) - .context_menu(|ui| self.asset_file_context_menu(cfg, ui, node_id, &file_info)); - builder.node(menu); - had_content = true; - } - } - - if !had_content { - Self::add_placeholder_leaf( - builder, - &format!("{source_label}/empty"), - "empty", - AssetNodeKind::Script(ScriptDivision::Script), - ); - had_content = true; - } - - had_content - } - - fn build_plain_directory( - &mut self, - cfg: &mut StaticallyKept, - builder: &mut TreeViewBuilder<u64>, - dir_path: &Path, - parent_label: &str, - division: AssetDivision, - dir_kind: AssetNodeKind, - file_kind: AssetNodeKind, - new_folder_label: &str, - ) { - let entries = match Self::sorted_entries(dir_path) { - Ok(entries) => entries, - Err(err) => { - log::warn!( - "Unable to enumerate directory '{}': {}", - dir_path.display(), - err - ); - Self::add_placeholder_leaf( - builder, - &format!("{parent_label}/unreadable"), - "unreadable", - file_kind, - ); - return; - } - }; - - if entries.is_empty() { - Self::add_placeholder_leaf( - builder, - &format!("{parent_label}/empty"), - "empty", - file_kind, - ); - return; - } - - for entry in entries { - let child_label = format!("{}/{}", parent_label, entry.name); - if entry.is_dir { - let dir_info = AssetNodeInfo { - path: entry.path.clone(), - division, - kind: dir_kind, - is_dir: true, - is_division_root: false, - allow_add_folder: true, - }; - Self::register_asset_node(cfg, &child_label, dir_info.clone()); - let node_id = Self::asset_node_id(&child_label); - let menu = Self::dir_node_kind(&child_label, &entry.name, dir_info.kind) - .context_menu(|ui| { - self.asset_dir_context_menu(cfg, ui, node_id, &dir_info, new_folder_label) - }); - builder.node(menu); - self.build_plain_directory( - cfg, - builder, - &entry.path, - &child_label, - division, - dir_kind, - file_kind, - new_folder_label, - ); - builder.close_dir(); - } else { - let file_info = AssetNodeInfo { - path: entry.path.clone(), - division, - kind: file_kind, - is_dir: false, - is_division_root: false, - allow_add_folder: false, - }; - Self::register_asset_node(cfg, &child_label, file_info.clone()); - let node_id = Self::asset_node_id(&child_label); - let menu = Self::leaf_node_kind(&child_label, &entry.name, file_info.kind) - .context_menu(|ui| self.asset_file_context_menu(cfg, ui, node_id, &file_info)); - builder.node(menu); - } - } - } - - fn build_kotlin_tree( - &mut self, - cfg: &mut StaticallyKept, - builder: &mut TreeViewBuilder<u64>, - kotlin_path: &Path, - source_label: &str, - ) -> bool { - let entries = match Self::sorted_entries(kotlin_path) { - Ok(entries) => entries, - Err(err) => { - log::warn!( - "Unable to enumerate kotlin sources at '{}': {}", - kotlin_path.display(), - err - ); - Self::add_placeholder_leaf( - builder, - &format!("{source_label}/unreadable"), - "unreadable", - AssetNodeKind::Script(ScriptDivision::Script), - ); - return true; - } - }; - - if entries.is_empty() { - Self::add_placeholder_leaf( - builder, - &format!("{source_label}/no_kotlin_files"), - "no kotlin files", - AssetNodeKind::Script(ScriptDivision::Script), - ); - return true; - } - - let mut had_entries = false; - for entry in entries { - if entry.is_dir { - self.build_kotlin_package_collapsed( - cfg, - builder, - &entry.path, - source_label, - vec![entry.name.clone()], - ); - had_entries = true; - } else { - let file_id = format!("{}/{}", source_label, entry.name); - let file_info = AssetNodeInfo { - path: entry.path.clone(), - division: AssetDivision::Scripts, - kind: AssetNodeKind::Script(ScriptDivision::Script), - is_dir: false, - is_division_root: false, - allow_add_folder: false, - }; - Self::register_asset_node(cfg, &file_id, file_info.clone()); - let node_id = Self::asset_node_id(&file_id); - let menu = Self::leaf_node_kind(&file_id, &entry.name, file_info.kind) - .context_menu(|ui| self.asset_file_context_menu(cfg, ui, node_id, &file_info)); - builder.node(menu); - had_entries = true; - } - } - - had_entries - } - - fn build_kotlin_package_collapsed( - &mut self, - cfg: &mut StaticallyKept, - builder: &mut TreeViewBuilder<u64>, - dir_path: &Path, - parent_path_str: &str, - accumulated_parts: Vec<String>, - ) { - let entries = match Self::sorted_entries(dir_path) { - Ok(entries) => entries, - Err(err) => { - let package_suffix = accumulated_parts.join("."); - let full_path_str = format!("{}/{}", parent_path_str, package_suffix); - log::warn!( - "Unable to enumerate package '{}' ({}): {}", - package_suffix, - dir_path.display(), - err - ); - Self::add_placeholder_leaf( - builder, - &format!("{full_path_str}/unreadable"), - "unreadable", - AssetNodeKind::Script(ScriptDivision::Script), - ); - return; - } - }; - - let subdirs: Vec<&FsEntry> = entries.iter().filter(|e| e.is_dir).collect(); - let files: Vec<&FsEntry> = entries.iter().filter(|e| !e.is_dir).collect(); - - if files.is_empty() && subdirs.len() == 1 { - let subdir = subdirs[0]; - let mut new_parts = accumulated_parts; - new_parts.push(subdir.name.clone()); - self.build_kotlin_package_collapsed( - cfg, - builder, - &subdir.path, - parent_path_str, - new_parts, - ); - } else { - let package_suffix = accumulated_parts.join("."); - let full_path_str = format!("{}/{}", parent_path_str, package_suffix); - - let dir_info = AssetNodeInfo { - path: dir_path.to_path_buf(), - division: AssetDivision::Scripts, - kind: AssetNodeKind::Script(ScriptDivision::Package), - is_dir: true, - is_division_root: false, - allow_add_folder: true, - }; - Self::register_asset_node(cfg, &full_path_str, dir_info.clone()); - let node_id = Self::asset_node_id(&full_path_str); - let menu = Self::dir_node_kind(&full_path_str, &package_suffix, dir_info.kind) - .context_menu(|ui| { - self.asset_dir_context_menu(cfg, ui, node_id, &dir_info, "New Package") - }); - builder.node(menu); - - for file in files { - let file_id = format!("{}/{}", full_path_str, file.name); - let file_info = AssetNodeInfo { - path: file.path.clone(), - division: AssetDivision::Scripts, - kind: AssetNodeKind::Script(ScriptDivision::Script), - is_dir: false, - is_division_root: false, - allow_add_folder: false, - }; - Self::register_asset_node(cfg, &file_id, file_info.clone()); - let node_id = Self::asset_node_id(&file_id); - let menu = Self::leaf_node_kind(&file_id, &file.name, file_info.kind) - .context_menu(|ui| self.asset_file_context_menu(cfg, ui, node_id, &file_info)); - builder.node(menu); - } - - for subdir in subdirs { - self.build_kotlin_package_collapsed( - cfg, - builder, - &subdir.path, - &full_path_str, - vec![subdir.name.clone()], - ); - } - - builder.close_dir(); - } - } - - fn build_scene_branch( - &mut self, - cfg: &mut StaticallyKept, - builder: &mut TreeViewBuilder<u64>, - project_root: &Path, - ) { - let label = "euca://scenes"; - let scenes_root = project_root.join("resources").join("scenes"); - let root_info = AssetNodeInfo { - path: scenes_root.clone(), - division: AssetDivision::Scenes, - kind: AssetNodeKind::Scene(SceneDivision::Folder), - is_dir: true, - is_division_root: true, - allow_add_folder: true, - }; - Self::register_asset_node(cfg, label, root_info.clone()); - let node_id = Self::asset_node_id(label); - let menu = Self::dir_node_kind(label, "scenes", root_info.kind).context_menu(|ui| { - self.asset_dir_context_menu(cfg, ui, node_id, &root_info, "New Folder") - }); - builder.node(menu); - if !scenes_root.exists() { - Self::add_placeholder_leaf( - builder, - "euca://scenes/missing", - "missing", - AssetNodeKind::Scene(SceneDivision::Scene), - ); - builder.close_dir(); - return; - } - - let entries = match Self::sorted_entries(&scenes_root) { - Ok(entries) => entries, - Err(err) => { - log::warn!( - "Unable to enumerate scenes at '{}': {}", - scenes_root.display(), - err - ); - Self::add_placeholder_leaf( - builder, - "euca://scenes/unreadable", - "unreadable", - AssetNodeKind::Scene(SceneDivision::Scene), - ); - builder.close_dir(); - return; - } - }; - - let mut had_entries = false; - for entry in entries { - if entry.is_dir { - let child_label = format!("{}/{}", label, entry.name); - let dir_info = AssetNodeInfo { - path: entry.path.clone(), - division: AssetDivision::Scenes, - kind: AssetNodeKind::Scene(SceneDivision::Folder), - is_dir: true, - is_division_root: false, - allow_add_folder: true, - }; - Self::register_asset_node(cfg, &child_label, dir_info.clone()); - let node_id = Self::asset_node_id(&child_label); - let menu = Self::dir_node_kind(&child_label, &entry.name, dir_info.kind) - .context_menu(|ui| { - self.asset_dir_context_menu(cfg, ui, node_id, &dir_info, "New Folder") - }); - builder.node(menu); - self.build_plain_directory( - cfg, - builder, - &entry.path, - &child_label, - AssetDivision::Scenes, - AssetNodeKind::Scene(SceneDivision::Folder), - AssetNodeKind::Scene(SceneDivision::Scene), - "New Folder", - ); - builder.close_dir(); - had_entries = true; - } else if entry - .path - .extension() - .and_then(|ext| ext.to_str()) - .map(|ext| ext.eq_ignore_ascii_case("eucs")) - .unwrap_or(false) - { - let file_label = format!("{}/{}", label, entry.name); - let file_info = AssetNodeInfo { - path: entry.path.clone(), - division: AssetDivision::Scenes, - kind: AssetNodeKind::Scene(SceneDivision::Scene), - is_dir: false, - is_division_root: false, - allow_add_folder: false, - }; - Self::register_asset_node(cfg, &file_label, file_info.clone()); - let node_id = Self::asset_node_id(&file_label); - let menu = Self::leaf_node_kind(&file_label, &entry.name, file_info.kind) - .context_menu(|ui| self.asset_file_context_menu(cfg, ui, node_id, &file_info)); - builder.node(menu); - had_entries = true; - } - } - - if !had_entries { - Self::add_placeholder_leaf( - builder, - "euca://scenes/no_scenes", - "no scenes", - AssetNodeKind::Scene(SceneDivision::Scene), - ); - } - - builder.close_dir(); - } - - fn sorted_entries(path: &Path) -> io::Result<Vec<FsEntry>> { - let mut entries = Vec::new(); - for entry in fs::read_dir(path)? { - let entry = entry?; - let file_type = entry.file_type()?; - let name = entry.file_name().to_string_lossy().to_string(); - entries.push(FsEntry { - path: entry.path(), - name_lower: name.to_lowercase(), - name, - is_dir: file_type.is_dir(), - }); - } - - entries.sort_by(|a, b| match b.is_dir.cmp(&a.is_dir) { - Ordering::Equal => a.name_lower.cmp(&b.name_lower), - other => other, - }); - - Ok(entries) - } - - fn asset_node_id(label: &str) -> u64 { - let mut hasher = DefaultHasher::new(); - label.hash(&mut hasher); - let mut id = hasher.finish(); - if id == 0 { - id = 1; - } - id - } - - fn register_asset_node(cfg: &mut StaticallyKept, id_source: &str, info: AssetNodeInfo) -> u64 { - let node_id = Self::asset_node_id(id_source); - cfg.asset_node_info.insert(node_id, info); - node_id - } - - fn dir_node<'ui>(label: &str) -> NodeBuilder<'ui, u64> { - Self::with_icon_kind( - NodeBuilder::dir(Self::asset_node_id(label)).label(label.to_string()), - AssetNodeKind::Resource(ResourceDivision::Folder), - ) - } - - fn dir_node_kind<'ui>( - id_source: &str, - label: &str, - kind: AssetNodeKind, - ) -> NodeBuilder<'ui, u64> { - Self::with_icon_kind( - NodeBuilder::dir(Self::asset_node_id(id_source)).label(label.to_string()), - kind, - ) - } - - fn leaf_node_kind<'ui>( - id_source: &str, - label: &str, - kind: AssetNodeKind, - ) -> NodeBuilder<'ui, u64> { - Self::with_icon_kind( - NodeBuilder::leaf(Self::asset_node_id(id_source)).label(label.to_string()), - kind, - ) - } - - fn with_icon_kind( - builder: NodeBuilder<u64>, - kind: AssetNodeKind, - ) -> NodeBuilder<u64> { - builder.icon(move |ui| { - egui_extras::install_image_loaders(ui.ctx()); - Self::draw_asset_icon(ui, kind) - }) - } - - fn draw_asset_icon(ui: &mut egui::Ui, _kind: AssetNodeKind) { - let image = egui::Image::from_bytes("bytes://asset-viewer-icon", NO_TEXTURE) - .max_size(egui::vec2(14.0, 14.0)); - ui.add(image); - } - - fn add_placeholder_leaf( - builder: &mut TreeViewBuilder<u64>, - id_source: &str, - label: &str, - kind: AssetNodeKind, - ) { - builder.node(Self::leaf_node_kind(id_source, label, kind)); - } - - fn asset_dir_context_menu( - &mut self, - cfg: &mut StaticallyKept, - ui: &mut egui::Ui, - node_id: u64, - info: &AssetNodeInfo, - new_folder_label: &str, - ) { - if info.allow_add_folder { - if ui.button(new_folder_label).clicked() { - ui.close(); - let base_name = if info.division == AssetDivision::Scripts { - "newpackage" - } else { - "New Folder" - }; - self.create_asset_folder(&info.path, base_name); - } - } - - if ui.button("Paste").clicked() { - ui.close(); - if !info.path.exists() { - if let Err(err) = fs::create_dir_all(&info.path) { - warn!("Unable to create folder '{}': {}", info.path.display(), err); - return; - } - } - self.signal.push_back(Signal::AssetPaste { - target_dir: info.path.clone(), - division: info.division, - }); - } - - if ui.button("Reveal Folder").clicked() { - ui.close(); - if let Err(err) = open::that(&info.path) { - warn!("Unable to reveal folder: {}", err); - } - } - - if !info.is_division_root && ui.button("Rename").clicked() { - let current_name = info - .path - .file_name() - .and_then(|name| name.to_str()) - .unwrap_or("") - .to_string(); - cfg.asset_rename = Some(crate::editor::AssetRenameState { - node_id, - original_path: info.path.clone(), - buffer: current_name, - just_started: true, - }); - } - - if !info.is_division_root && ui.button("Delete").clicked() { - ui.close(); - self.delete_asset_entry(info); - } - } - - fn asset_file_context_menu( - &mut self, - cfg: &mut StaticallyKept, - ui: &mut egui::Ui, - node_id: u64, - info: &AssetNodeInfo, - ) { - if matches!(info.kind, AssetNodeKind::Script(ScriptDivision::Script)) - && ui.button("Open Script").clicked() - { - ui.close(); - if let Err(err) = open::that(&info.path) { - warn!("Unable to open script: {}", err); - } - } - - if ui.button("Rename").clicked() { - let current_name = info - .path - .file_name() - .and_then(|name| name.to_str()) - .unwrap_or("") - .to_string(); - cfg.asset_rename = Some(crate::editor::AssetRenameState { - node_id, - original_path: info.path.clone(), - buffer: current_name, - just_started: true, - }); - } - - if ui.button("Copy").clicked() { - ui.close(); - self.signal.push_back(Signal::AssetCopy { - source: info.path.clone(), - division: info.division, - }); - } - - if ui.button("Delete").clicked() { - ui.close(); - self.delete_asset_entry(info); - } - } - - fn apply_asset_rename(&self, rename: &crate::editor::AssetRenameState, is_dir: bool) { - let trimmed = rename.buffer.trim(); - if trimmed.is_empty() { - warn!("Rename cancelled: empty name"); - return; - } - - let Some(parent) = rename.original_path.parent() else { - warn!("Unable to rename: missing parent directory"); - return; - }; - - let mut new_name = trimmed.to_string(); - if !is_dir { - if let Some(ext) = rename.original_path.extension().and_then(|e| e.to_str()) { - let has_ext = Path::new(&new_name) - .extension() - .and_then(|e| e.to_str()) - .is_some(); - if !has_ext { - new_name = format!("{new_name}.{ext}"); - } - } - } - - let target_path = parent.join(&new_name); - if target_path == rename.original_path { - return; - } - - if target_path.exists() { - warn!("Rename target already exists: {}", target_path.display()); - return; - } - - if let Err(err) = fs::rename(&rename.original_path, &target_path) { - warn!( - "Failed to rename '{}': {}", - rename.original_path.display(), - err - ); - } else { - info!("Renamed to {}", target_path.display()); - let old_meta = PathBuf::from(format!("{}.eucmeta", rename.original_path.display())); - if old_meta.exists() { - let new_meta = PathBuf::from(format!("{}.eucmeta", target_path.display())); - if let Err(e) = fs::rename(&old_meta, &new_meta) { - warn!("Failed to move .eucmeta sidecar '{}': {}", old_meta.display(), e); - } - } - } - } - - fn delete_asset_entry(&self, info: &AssetNodeInfo) { - if info.is_division_root { - warn!("Cannot delete division root"); - return; - } - - let result = if info.is_dir { - fs::remove_dir_all(&info.path) - } else { - fs::remove_file(&info.path) - }; - - if let Err(err) = result { - warn!("Failed to delete '{}': {}", info.path.display(), err); - } else { - info!("Deleted {}", info.path.display()); - if !info.is_dir { - let meta = PathBuf::from(format!("{}.eucmeta", info.path.display())); - if meta.exists() { - if let Err(e) = fs::remove_file(&meta) { - warn!("Failed to remove .eucmeta sidecar '{}': {}", meta.display(), e); - } - } - } - } - } - - fn create_asset_folder(&self, base_dir: &Path, base_name: &str) { - if let Err(err) = fs::create_dir_all(base_dir) { - warn!("Unable to create folder '{}': {}", base_dir.display(), err); - return; - } - - let mut candidate = base_dir.join(base_name); - if candidate.exists() { - let mut index = 1; - loop { - let suffix = if base_name.contains(' ') { - format!(" {}", index) - } else { - format!("{}", index) - }; - candidate = base_dir.join(format!("{base_name}{suffix}")); - if !candidate.exists() { - break; - } - index += 1; - } - } - - if let Err(err) = fs::create_dir_all(&candidate) { - warn!("Unable to create folder '{}': {}", candidate.display(), err); - } else { - info!("Created folder {}", candidate.display()); - } - } - - fn handle_asset_move( - &mut self, - cfg: &mut StaticallyKept, - drag: &egui_ltreeview::DragAndDrop<u64>, - ) { - let Some(&source_id) = drag.source.first() else { - return; - }; - let Some(source_info) = cfg.asset_node_info.get(&source_id).cloned() else { - return; - }; - let Some(target_info) = cfg.asset_node_info.get(&drag.target).cloned() else { - return; - }; - - if source_info.is_division_root { - warn!("Cannot move division root"); - return; - } - - if source_info.division != target_info.division { - warn!("Cannot move assets across divisions"); - return; - } - - let target_dir = if target_info.is_dir { - target_info.path.clone() - } else { - target_info - .path - .parent() - .unwrap_or(&target_info.path) - .to_path_buf() - }; - - if !target_dir.exists() { - if let Err(err) = fs::create_dir_all(&target_dir) { - warn!("Target directory does not exist: {}", err); - return; - } - } - - if source_info.is_dir && target_dir.starts_with(&source_info.path) { - warn!("Cannot move a folder into itself"); - return; - } - - let Some(name) = source_info.path.file_name() else { - warn!("Unable to move: invalid file name"); - return; - }; - - let target_path = target_dir.join(name); - if target_path == source_info.path { - return; - } - - if target_path.exists() { - warn!("Target already exists: {}", target_path.display()); - return; - } - - if let Err(err) = fs::rename(&source_info.path, &target_path) { - warn!("Failed to move '{}': {}", source_info.path.display(), err); - } else { - info!("Moved to {}", target_path.display()); - if !source_info.is_dir { - let old_meta = PathBuf::from(format!("{}.eucmeta", source_info.path.display())); - if old_meta.exists() { - let new_meta = PathBuf::from(format!("{}.eucmeta", target_path.display())); - if let Err(e) = fs::rename(&old_meta, &new_meta) { - warn!("Failed to move .eucmeta sidecar '{}': {}", old_meta.display(), e); - } - } - } - } - } - - fn is_model_file(name: &str) -> bool { - let name = name.to_ascii_lowercase(); - name.ends_with(".glb") || name.ends_with(".gltf") - } - - fn is_texture_file(name: &str) -> bool { - let name = name.to_ascii_lowercase(); - name.ends_with(".png") - || name.ends_with(".jpg") - || name.ends_with(".jpeg") - || name.ends_with(".tga") - || name.ends_with(".bmp") - || name.ends_with(".webp") - } - - fn queue_model_load(&self, reference: ResourceReference, label: String) { - if ASSET_REGISTRY - .read() - .get_model_handle_by_reference(&reference) - .is_some() - { - eucalyptus_core::info!("Model already loaded: {}", label); - return; - } - - let graphics = self.graphics.clone(); - let queue = graphics.future_queue.clone(); - queue.push(async move { - let path = reference.resolve()?; - let buffer = fs::read(&path)?; - let extension = path - .extension() - .and_then(|ext| ext.to_str()) - .map(|ext| ext.to_ascii_lowercase()); - - let handle = match extension.as_deref() { - Some("eucmdl") => { - let model = rkyv::from_bytes::<EucalyptusModel, rkyv::rancor::Error>(&buffer) - .map_err(|e| anyhow::anyhow!( - "Unable to deserialize .eucmdl model '{}': {}", - path.display(), - e - ))?; - - let runtime_model = model.load(reference.clone(), graphics.clone()); - let mut registry = ASSET_REGISTRY.write(); - registry.add_model_with_label(label.clone(), runtime_model) - } - _ => match Model::load_from_memory_raw( - graphics.clone(), - buffer, - Some(reference.clone()), - Some(label.as_str()), - ASSET_REGISTRY.clone(), - ) - .await - { - Ok(v) => v, - Err(e) => { - eucalyptus_core::warn!("Unable to load model {}: {}", reference, e); - return Err(e); - } - }, - }; - - let mut registry = ASSET_REGISTRY.write(); - registry.label_model(label.clone(), handle); - - eucalyptus_core::success!("Loaded model {}", label); - Ok::<(), anyhow::Error>(()) - }); - } - - fn queue_texture_load(&self, reference: ResourceReference, label: String, strict_image_decode: bool) { - if ASSET_REGISTRY - .read() - .get_texture_handle_by_reference(&reference) - .is_some() - { - eucalyptus_core::info!("Texture already loaded: {}", label); - return; - } - - let graphics = self.graphics.clone(); - let queue = graphics.future_queue.clone(); - queue.push(async move { - let path = reference.resolve()?; - let bytes = fs::read(&path)?; - - if strict_image_decode - && let Err(err) = image::load_from_memory(bytes.as_slice()) - { - let error = anyhow::anyhow!( - "'{}' is not a texture-compatible eucbin payload: {}", - path.display(), - err - ); - eucalyptus_core::warn!("{}", error); - return Err(error); - } - - let mut texture = TextureBuilder::new(&graphics.device) - .with_bytes(graphics.clone(), bytes.as_slice()) - .label(label.as_str()) - .build(); - texture.reference = Some(reference.clone()); - let mut registry = ASSET_REGISTRY.write(); - registry.add_texture_with_label(label.clone(), texture); - Ok::<(), anyhow::Error>(()) - }); - } - - pub(crate) fn handle_tree_selection(&mut self, cfg: &mut StaticallyKept, items: &[u64]) { - for node_id in items { - self.resolve_tree_node(cfg, *node_id); - } - } - - pub(crate) fn handle_tree_activate( - &mut self, - cfg: &mut StaticallyKept, - activate: &egui_ltreeview::Activate<u64>, - ) { - self.handle_tree_selection(cfg, &activate.selected); - } - - pub(crate) fn handle_tree_drag( - &mut self, - cfg: &mut StaticallyKept, - drag: &egui_ltreeview::DragAndDrop<u64>, - ) { - if let Some(&node_id) = drag.source.first() { - if let Some(selection) = cfg.component_selection(node_id) { - cfg.pending_component_drag = Some(selection); - self.inspect_component_selection(cfg, selection); - } - } - } - - pub(crate) fn handle_tree_move( - &mut self, - cfg: &mut StaticallyKept, - drag: &egui_ltreeview::DragAndDrop<u64>, - ) { - let selection = cfg.pending_component_drag.take().or_else(|| { - drag.source - .first() - .and_then(|node_id| cfg.component_selection(*node_id)) - }); - - if let Some(selection) = selection { - self.inspect_component_selection(cfg, selection); - return; - } - - let target_entity = Self::entity_from_node_id(drag.target); - - for &source_id in &drag.source { - let Some(source_entity) = Self::entity_from_node_id(source_id) else { continue }; - - if cfg.component_selection(source_id).is_some() { - continue; - } - - if drag.target == u64::MAX { - Hierarchy::remove_parent(self.world, source_entity); - } else if let Some(target_entity) = target_entity { - if source_entity == target_entity { - continue; - } - if Hierarchy::is_descendant_of(self.world, target_entity, source_entity) { - continue; - } - Hierarchy::set_parent(self.world, source_entity, target_entity); - } - } - } - - fn resolve_tree_node(&mut self, cfg: &mut StaticallyKept, node_id: u64) { - if node_id == u64::MAX { - log_once::debug_once!("Root node has been selected"); - cfg.root_node_selected = true; - *self.selected_entity = None; - } else if let Some(selection) = cfg.component_selection(node_id) { - cfg.root_node_selected = false; - *self.selected_entity = selection.entity(); - self.inspect_component_selection(cfg, selection); - } else if let Some(entity) = Self::entity_from_node_id(node_id) { - cfg.root_node_selected = false; - *self.selected_entity = Some(entity); - } - } - - fn inspect_component_selection( - &mut self, - cfg: &mut StaticallyKept, - selection: ComponentNodeSelection, - ) { - cfg.remember_component_lookup(selection); - let component_id = selection.component_type_id; - let matches = self - .component_registry - .find_entities_by_numeric_id(self.world, component_id); - let descriptor = self - .component_registry - .get_descriptor_by_numeric_id(component_id); - - if matches.is_empty() { - log::warn!("Component id #{} not found in world", component_id); - return; - } - - let name = descriptor - .map(|desc| desc.fqtn.as_str()) - .unwrap_or("<unknown>"); - for entity in matches { - log::debug!( - "Serializable component '{}' (id #{}) attached to entity {:?}", - name, - component_id, - entity - ); - } - } - - fn entity_from_node_id(node_id: u64) -> Option<Entity> { - if node_id == u64::MAX { - None - } else { - Entity::from_bits(node_id) - } - } -} - -pub struct AssetViewerDock; - -impl EditorTabDock for AssetViewerDock { - fn desc() -> EditorTabDockDescriptor { - EditorTabDockDescriptor { - id: "asset_viewer", - title: "Asset Viewer".to_string(), - visibility: EditorTabVisibility::all(), - } - } - - fn display(viewer: &mut EditorTabViewer<'_>, ui: &mut egui::Ui) { - viewer.show_asset_viewer(ui); - } -} @@ -1,162 +0,0 @@ -use std::path::PathBuf; - -use egui::{Margin, RichText}; - -use crate::editor::{ - EditorTabDock, EditorTabDockDescriptor, EditorTabViewer - , -}; -use crate::editor::console::{ConsoleItem, ErrorLevel}; -use crate::editor::page::EditorTabVisibility; - -impl<'a> EditorTabViewer<'a> { - pub fn build_console(&mut self, ui: &mut egui::Ui) { - fn analyse_error(log: &Vec<String>) -> Vec<ConsoleItem> { - fn parse_compiler_location(line: &str) -> Option<(ErrorLevel, PathBuf, String)> { - let trimmed = line.trim_start(); - let (error_level, rest) = if let Some(r) = trimmed.strip_prefix("e: file:///") { - (ErrorLevel::Error, r) - } else if let Some(r) = trimmed.strip_prefix("w: file:///") { - (ErrorLevel::Warn, r) - } else { - return None; - }; - - let location = rest.split_whitespace().next()?; - - let mut segments = location.rsplitn(3, ':'); - let column = segments.next()?; - let row = segments.next()?; - let path = segments.next()?; - - Some((error_level, PathBuf::from(path), format!("{row}:{column}"))) - } - - let mut list: Vec<ConsoleItem> = Vec::new(); - for (index, line) in log.iter().enumerate() { - if line.contains("The required library") { - list.push(ConsoleItem { - error_level: ErrorLevel::Error, - msg: line.clone(), - file_location: None, - line_ref: None, - id: index as u64, - }); - } else if let Some((error_level, path, loc)) = parse_compiler_location(line) { - list.push(ConsoleItem { - error_level, - msg: line.clone(), - file_location: Some(path), - line_ref: Some(loc), - id: index as u64, - }); - } else { - list.push(ConsoleItem { - error_level: ErrorLevel::Info, - msg: line.clone(), - file_location: None, - line_ref: None, - id: index as u64, - }); - } - } - list - } - - let logs = analyse_error(&self.build_logs); - - egui::ScrollArea::vertical() - .auto_shrink([false, false]) - .stick_to_bottom(true) - .show(ui, |ui| { - if logs.is_empty() { - ui.label("Build output will appear here once available."); - return; - } - - for item in &logs { - let (bg_color, text_color, stroke_color) = match item.error_level { - ErrorLevel::Error => ( - egui::Color32::from_rgb(60, 20, 20), - egui::Color32::from_rgb(255, 200, 200), - egui::Color32::from_rgb(255, 200, 200), - ), - ErrorLevel::Warn => ( - egui::Color32::from_rgb(40, 40, 10), - egui::Color32::from_rgb(255, 255, 200), - egui::Color32::from_rgb(255, 255, 200), - ), - ErrorLevel::Info => ( - egui::Color32::TRANSPARENT, - ui.style().visuals.text_color(), - egui::Color32::TRANSPARENT, - ), - }; - - if matches!(item.error_level, ErrorLevel::Info) { - ui.label(RichText::new(&item.msg).monospace()); - } else { - let available_width = ui.available_width(); - let frame = egui::Frame::new() - .inner_margin(Margin::symmetric(8, 6)) - .fill(bg_color) - .stroke(egui::Stroke::new(1.0, stroke_color)); - - let response = frame - .show(ui, |ui| { - ui.set_width(available_width - 10.0); - ui.horizontal(|ui| { - ui.label( - RichText::new(&item.msg).color(text_color).monospace(), - ); - }); - }) - .response; - - if response.clicked() { - log::debug!("Log item clicked: {}", &item.id); - if let (Some(path), Some(loc)) = (&item.file_location, &item.line_ref) { - let location_arg = format!("{}:{}", path.display(), loc); - - match std::process::Command::new("code") - .args(["-g", &location_arg]) - .spawn() - .map(|_| ()) - { - Ok(()) => { - log::info!( - "Launched Visual Studio Code at the error: {}", - &location_arg - ); - } - Err(e) => { - eucalyptus_core::warn!( - "Failed to open '{}' in VS Code: {}", - location_arg, - e - ); - } - } - } - } - } - } - }); - } -} - -pub struct BuildConsoleDock; - -impl EditorTabDock for BuildConsoleDock { - fn desc() -> EditorTabDockDescriptor { - EditorTabDockDescriptor { - id: "build_console", - title: "Build Output".to_string(), - visibility: EditorTabVisibility::all(), // idk about this one - } - } - - fn display(viewer: &mut EditorTabViewer<'_>, ui: &mut egui::Ui) { - viewer.build_console(ui); - } -} @@ -1,104 +0,0 @@ -use parking_lot::Mutex; -use std::io::{BufRead, BufReader}; -use std::net::{TcpListener, TcpStream}; -use std::path::PathBuf; -use std::sync::Arc; - -pub struct EucalyptusConsole { - pub buffer: Arc<Mutex<Vec<String>>>, - pub history: Vec<String>, - - pub show_info: bool, - pub show_warning: bool, - pub show_error: bool, - pub show_debug: bool, - pub show_trace: bool, - pub auto_scroll: bool, -} - -impl EucalyptusConsole { - /// Creates a new instance of a [EucalyptusConsole], and - pub fn new(port: Option<&str>) -> Self { - let result = Self { - buffer: Arc::new(Default::default()), - history: vec![], - show_info: true, - show_warning: true, - show_error: true, - show_debug: false, - show_trace: false, - auto_scroll: true, - }; - - let buf_clone = result.buffer.clone(); - let addr = format!("127.0.0.1:{}", port.unwrap_or("56624")); - std::thread::spawn(move || { - let listener = TcpListener::bind(&addr).unwrap(); - - log::info!("eucalyptus-editor debug console started at {}", addr); - - loop { - for stream in listener.incoming() { - match stream { - Ok(stream) => { - let buf_clone = buf_clone.clone(); - std::thread::spawn(move || { - EucalyptusConsole::handle_client(stream, buf_clone) - }); - } - Err(e) => { - eprintln!("Connection failed: {}", e); - } - } - } - } - }); - - result - } - - fn handle_client(stream: TcpStream, buf: Arc<Mutex<Vec<String>>>) { - let peer_addr = stream.peer_addr().unwrap(); - println!("New connection from: {}", peer_addr); - - let reader = BufReader::new(stream); - - for line in reader.lines() { - match line { - Ok(text) => { - buf.lock().push(text); - } - Err(e) => { - buf.lock() - .push(format!("Error reading from {}: {}", peer_addr, e)); - break; - } - } - } - - println!("Connection closed: {}", peer_addr); - } - - /// Drains all from the thread-safe buffer and adds to the history, while returning that value. - /// - /// It is recommended to use this function. - pub fn take(&mut self) -> Vec<String> { - let buf = self.buffer.lock().drain(..).collect::<Vec<String>>(); - buf.iter().for_each(|v| self.history.push(v.clone())); - buf - } -} - -pub enum ErrorLevel { - Info, - Warn, - Error, -} - -pub struct ConsoleItem { - pub id: u64, - pub error_level: ErrorLevel, - pub msg: String, - pub file_location: Option<PathBuf>, - pub line_ref: Option<String>, -} @@ -2,17 +2,34 @@ use super::*; use crate::editor::ViewportMode; use std::hash::Hasher; use std::{collections::HashMap, hash::Hash, path::PathBuf, sync::LazyLock}; -use crate::editor::console::EucalyptusConsole; +use crate::editor::docks::console::EucalyptusConsole; use crate::plugin::PluginRegistry; use dropbear_engine::entity::{EntityTransform, Transform}; use dropbear_engine::utils::ResourceReference; use egui::{self}; use egui_dock::TabViewer; +use glam::Vec3; use hecs::{Entity, World}; use parking_lot::Mutex; use transform_gizmo_egui::{EnumSet, Gizmo, GizmoMode, GizmoOrientation}; use crate::editor::page::EditorTabVisibility; +/// State for an active click-and-drag operation on a 3D entity in the viewport. +pub struct DragState { + /// The entity being dragged. + pub entity: hecs::Entity, + /// World-space normal of the drag plane (camera-facing at the initial hit point). + pub plane_normal: Vec3, + /// Plane equation constant: `plane_normal · p == plane_d`. + pub plane_d: f32, + /// Offset from the entity's world origin to the initial pick point. + pub pick_offset: Vec3, + /// Snapshot of the entity's `EntityTransform` at the start of the drag, used for undo. + pub initial_entity_transform: Option<EntityTransform>, + /// Snapshot of the entity's plain `Transform` at the start of the drag (Transform-only entities). + pub initial_transform: Option<Transform>, +} + pub struct EditorTabViewer<'a> { pub view: egui::TextureId, pub tex_size: Extent3d, @@ -34,6 +51,7 @@ pub struct EditorTabViewer<'a> { pub eucalyptus_console: &'a mut EucalyptusConsole, pub current_scene_name: &'a mut Option<String>, pub ui_editor: &'a mut UiEditor, + pub viewport_drag: &'a mut Option<DragState>, } pub type EditorTabId = u64; @@ -0,0 +1,1484 @@ +use dropbear_engine::asset::ASSET_REGISTRY; +use dropbear_engine::model::Model; +use dropbear_engine::texture::TextureBuilder; +use dropbear_engine::{graphics::NO_TEXTURE, utils::ResourceReference}; +use egui_ltreeview::{Action, NodeBuilder, TreeViewBuilder}; +use eucalyptus_core::ser::model::EucalyptusModel; +use eucalyptus_core::states::PROJECT; +use eucalyptus_core::utils::ResolveReference; +use hecs::Entity; +use log::{info, warn}; +use std::hash::{Hash, Hasher}; +use std::{cmp::Ordering, fs, hash::DefaultHasher, io, path::{Path, PathBuf}}; + +use crate::editor::{ + AssetDivision, AssetNodeInfo, AssetNodeKind, ComponentNodeSelection, DraggedAsset, + EditorTabDock, EditorTabDockDescriptor, EditorTabViewer, FsEntry, ResourceDivision, + SceneDivision, ScriptDivision, Signal, StaticallyKept, TABS_GLOBAL, +}; +use eucalyptus_core::component::DRAGGED_ASSET_ID; +use eucalyptus_core::hierarchy::Hierarchy; +use crate::editor::page::EditorTabVisibility; + +impl<'a> EditorTabViewer<'a> { + pub(crate) fn show_asset_viewer(&mut self, ui: &mut egui::Ui) { + let mut cfg = TABS_GLOBAL.lock(); + cfg.asset_node_assets.clear(); + cfg.asset_node_info.clear(); + if let Some(rename) = &cfg.asset_rename { + if !rename.original_path.exists() { + cfg.asset_rename = None; + } + } + + if let Some(mut rename) = cfg.asset_rename.take() { + let rename_id = egui::Id::new("asset_rename_input"); + let mut should_apply = false; + + ui.horizontal(|ui| { + ui.label("Rename"); + let response = ui.add(egui::TextEdit::singleline(&mut rename.buffer).id(rename_id)); + if rename.just_started { + ui.ctx().memory_mut(|m| m.request_focus(rename_id)); + rename.just_started = false; + } + + let enter = ui.input(|input| input.key_pressed(egui::Key::Enter)); + should_apply = (enter && response.has_focus()) || response.lost_focus(); + }); + + if should_apply { + let is_dir = rename.original_path.is_dir(); + self.apply_asset_rename(&rename, is_dir); + } else { + cfg.asset_rename = Some(rename); + } + + ui.separator(); + } + + let project_root = { + let project = PROJECT.read(); + if project.project_path.as_os_str().is_empty() { + ui.label("Open a project to browse assets."); + return; + } + project.project_path.clone() + }; + + let (_resp, action) = + egui_ltreeview::TreeView::new(egui::Id::new("asset_viewer")).show(ui, |builder| { + builder.node(Self::dir_node("euca://")); + self.build_resource_branch(&mut cfg, builder, &project_root); + self.build_scripts_branch(&mut cfg, builder, &project_root); + self.build_scene_branch(&mut cfg, builder, &project_root); + builder.close_dir(); + }); + + for a in action { + match a { + Action::SetSelected(selected) => { + log_once::debug_once!("Selected: {:?}", selected); + } + Action::Move(moved) => { + log_once::debug_once!("Moved: {:?}", moved); + self.handle_asset_move(&mut cfg, &moved); + } + Action::Drag(dragged) => { + log_once::debug_once!("Dragged: {:?}", dragged); + + if let Some(&node_id) = dragged.source.first() { + if let Some(asset) = cfg.asset_node_assets.get(&node_id).cloned() { + cfg.dragged_asset = Some(asset.clone()); + ui.ctx().data_mut(|d| { + d.insert_temp( + egui::Id::new(DRAGGED_ASSET_ID), + Some(asset.path.clone()), + ) + }); + } + } + } + Action::Activate(activated) => { + log_once::debug_once!("Activated: {:?}", activated); + } + Action::DragExternal(_) => {} + Action::MoveExternal(_) => {} + } + } + } + + fn build_resource_branch( + &mut self, + cfg: &mut StaticallyKept, + builder: &mut TreeViewBuilder<u64>, + project_root: &Path, + ) { + let label = "euca://resources"; + let resources_root = project_root.join("resources"); + let root_info = AssetNodeInfo { + path: resources_root.clone(), + division: AssetDivision::Resources, + kind: AssetNodeKind::Resource(ResourceDivision::Folder), + is_dir: true, + is_division_root: true, + allow_add_folder: true, + }; + Self::register_asset_node(cfg, label, root_info.clone()); + let node_id = Self::asset_node_id(label); + let menu = Self::dir_node_kind(label, "resources", root_info.kind).context_menu(|ui| { + self.asset_dir_context_menu(cfg, ui, node_id, &root_info, "New Folder") + }); + builder.node(menu); + if resources_root.exists() { + self.walk_resource_directory(cfg, builder, &resources_root, &resources_root); + } else { + Self::add_placeholder_leaf( + builder, + "euca://resources/missing", + "missing", + AssetNodeKind::Resource(ResourceDivision::File), + ); + } + builder.close_dir(); + } + + fn walk_resource_directory( + &mut self, + cfg: &mut StaticallyKept, + builder: &mut TreeViewBuilder<u64>, + base_path: &Path, + current_path: &Path, + ) { + let entries = match Self::sorted_entries(current_path) { + Ok(entries) => entries, + Err(err) => { + log_once::warn_once!( + "Unable to enumerate resources at '{}': {}", + current_path.parent().unwrap_or(current_path).display(), + err + ); + return; + } + }; + + for entry in entries { + let full_label = Self::resource_label(base_path, &entry.path); + if entry.is_dir { + let dir_info = AssetNodeInfo { + path: entry.path.clone(), + division: AssetDivision::Resources, + kind: AssetNodeKind::Resource(ResourceDivision::Folder), + is_dir: true, + is_division_root: false, + allow_add_folder: true, + }; + Self::register_asset_node(cfg, &full_label, dir_info.clone()); + let node_id = Self::asset_node_id(&full_label); + let menu = Self::dir_node_kind(&full_label, &entry.name, dir_info.kind) + .context_menu(|ui| { + self.asset_dir_context_menu(cfg, ui, node_id, &dir_info, "New Folder") + }); + builder.node(menu); + self.walk_resource_directory(cfg, builder, base_path, &entry.path); + builder.close_dir(); + } else { + if entry.name.ends_with(".eucmeta") { + continue; + } + + let reference = ResourceReference::from_euca_uri(&full_label) + .unwrap_or_else(|_| ResourceReference::default()); + cfg.asset_node_assets.insert( + Self::asset_node_id(&full_label), + DraggedAsset { + name: entry.name.clone(), + path: reference.clone(), + }, + ); + let is_model = Self::is_model_file(&entry.name); + let is_texture = Self::is_texture_file(&entry.name); + let ext = entry + .path + .extension() + .and_then(|v| v.to_str()) + .map(|v| v.to_ascii_lowercase()); + let is_eucbin = matches!(ext.as_deref(), Some("eucbin")); + let is_eucmdl = matches!(ext.as_deref(), Some("eucmdl")); + let entry_name = entry.name.clone(); + let reference_for_menu = reference.clone(); + let file_info = AssetNodeInfo { + path: entry.path.clone(), + division: AssetDivision::Resources, + kind: AssetNodeKind::Resource(ResourceDivision::File), + is_dir: false, + is_division_root: false, + allow_add_folder: false, + }; + Self::register_asset_node(cfg, &full_label, file_info.clone()); + let node_id = Self::asset_node_id(&full_label); + let menu = Self::leaf_node_kind(&full_label, &entry.name, file_info.kind) + .context_menu(|ui| { + self.asset_file_context_menu(cfg, ui, node_id, &file_info); + ui.separator(); + + if is_model || is_eucmdl { + if ui.button("Load model to memory").clicked() { + ui.close(); + self.queue_model_load( + reference_for_menu.clone(), + entry_name.clone(), + ); + info!("Loading model {}", entry_name); + } + } + + if is_texture { + if ui.button("Load texture to memory").clicked() { + ui.close(); + self.queue_texture_load( + reference_for_menu.clone(), + entry_name.clone(), + false, + ); + info!("Loading texture {}", entry_name); + } + } + + if is_eucbin { + if ui.button("Load as model to memory").clicked() { + ui.close(); + self.queue_model_load( + reference_for_menu.clone(), + entry_name.clone(), + ); + info!("Loading eucbin as model {}", entry_name); + } + + if ui.button("Load as texture to memory").clicked() { + ui.close(); + self.queue_texture_load( + reference_for_menu.clone(), + entry_name.clone(), + true, + ); + info!("Loading eucbin as texture {}", entry_name); + } + } + }); + builder.node(menu); + } + } + } + + fn resource_label(base_path: &Path, path: &Path) -> String { + let relative = path + .strip_prefix(base_path) + .map(|rel| rel.to_string_lossy().replace('\\', "/")) + .unwrap_or_else(|_| path.to_string_lossy().replace('\\', "/")); + if relative.is_empty() { + "euca://resources".to_string() + } else { + format!("euca://resources/{}", relative) + } + } + + fn build_scripts_branch( + &mut self, + cfg: &mut StaticallyKept, + builder: &mut TreeViewBuilder<u64>, + project_root: &Path, + ) { + let label = "euca://scripts"; + let scripts_root = project_root.join("src"); + let root_info = AssetNodeInfo { + path: scripts_root.clone(), + division: AssetDivision::Scripts, + kind: AssetNodeKind::Script(ScriptDivision::Package), + is_dir: true, + is_division_root: true, + allow_add_folder: false, + }; + Self::register_asset_node(cfg, label, root_info.clone()); + let node_id = Self::asset_node_id(label); + let menu = Self::dir_node_kind(label, "scripts", root_info.kind).context_menu(|ui| { + self.asset_dir_context_menu(cfg, ui, node_id, &root_info, "New Package") + }); + builder.node(menu); + if !scripts_root.exists() { + Self::add_placeholder_leaf( + builder, + "euca://scripts/missing", + "missing", + AssetNodeKind::Script(ScriptDivision::Script), + ); + builder.close_dir(); + return; + } + + let entries = match Self::sorted_entries(&scripts_root) { + Ok(entries) => entries, + Err(err) => { + log::warn!( + "Unable to enumerate scripts at '{}': {}", + scripts_root.display(), + err + ); + builder.close_dir(); + return; + } + }; + + let mut had_content = false; + for entry in entries { + if entry.is_dir { + let source_label = format!("{}/{}", label, entry.name); + let kotlin_root = entry.path.join("kotlin"); + let source_info = AssetNodeInfo { + path: kotlin_root, + division: AssetDivision::Scripts, + kind: AssetNodeKind::Script(ScriptDivision::Package), + is_dir: true, + is_division_root: true, + allow_add_folder: true, + }; + Self::register_asset_node(cfg, &source_label, source_info.clone()); + let node_id = Self::asset_node_id(&source_label); + let menu = Self::dir_node_kind(&source_label, &entry.name, source_info.kind) + .context_menu(|ui| { + self.asset_dir_context_menu(cfg, ui, node_id, &source_info, "New Package") + }); + builder.node(menu); + if self.build_script_source_set(cfg, builder, &entry.path, &source_label) { + had_content = true; + } + builder.close_dir(); + } else if !entry.name.eq_ignore_ascii_case("source.eucc") { + let file_label = format!("{}/{}", label, entry.name); + let file_info = AssetNodeInfo { + path: entry.path.clone(), + division: AssetDivision::Scripts, + kind: AssetNodeKind::Script(ScriptDivision::Script), + is_dir: false, + is_division_root: false, + allow_add_folder: false, + }; + Self::register_asset_node(cfg, &file_label, file_info.clone()); + let node_id = Self::asset_node_id(&file_label); + let menu = Self::leaf_node_kind(&file_label, &entry.name, file_info.kind) + .context_menu(|ui| self.asset_file_context_menu(cfg, ui, node_id, &file_info)); + builder.node(menu); + had_content = true; + } + } + + if !had_content { + Self::add_placeholder_leaf( + builder, + "euca://scripts/empty", + "empty", + AssetNodeKind::Script(ScriptDivision::Script), + ); + } + + builder.close_dir(); + } + + fn build_script_source_set( + &mut self, + cfg: &mut StaticallyKept, + builder: &mut TreeViewBuilder<u64>, + source_path: &Path, + source_label: &str, + ) -> bool { + let entries = match Self::sorted_entries(source_path) { + Ok(entries) => entries, + Err(err) => { + log::warn!( + "Unable to enumerate source set at '{}': {}", + source_path.display(), + err + ); + Self::add_placeholder_leaf( + builder, + &format!("{source_label}/unreadable"), + "unreadable", + AssetNodeKind::Script(ScriptDivision::Script), + ); + return true; + } + }; + + let mut had_content = false; + for entry in entries { + if entry.is_dir { + if entry.name.eq_ignore_ascii_case("kotlin") { + if self.build_kotlin_tree(cfg, builder, &entry.path, source_label) { + had_content = true; + } + } else { + let child_label = format!("{}/{}", source_label, entry.name); + let dir_info = AssetNodeInfo { + path: entry.path.clone(), + division: AssetDivision::Scripts, + kind: AssetNodeKind::Script(ScriptDivision::Package), + is_dir: true, + is_division_root: false, + allow_add_folder: true, + }; + Self::register_asset_node(cfg, &child_label, dir_info.clone()); + let node_id = Self::asset_node_id(&child_label); + let menu = Self::dir_node_kind(&child_label, &entry.name, dir_info.kind) + .context_menu(|ui| { + self.asset_dir_context_menu(cfg, ui, node_id, &dir_info, "New Package") + }); + builder.node(menu); + self.build_plain_directory( + cfg, + builder, + &entry.path, + &child_label, + AssetDivision::Scripts, + AssetNodeKind::Script(ScriptDivision::Package), + AssetNodeKind::Script(ScriptDivision::Script), + "New Package", + ); + builder.close_dir(); + had_content = true; + } + } else if !entry.name.eq_ignore_ascii_case("source.eucc") { + let file_label = format!("{}/{}", source_label, entry.name); + let file_info = AssetNodeInfo { + path: entry.path.clone(), + division: AssetDivision::Scripts, + kind: AssetNodeKind::Script(ScriptDivision::Script), + is_dir: false, + is_division_root: false, + allow_add_folder: false, + }; + Self::register_asset_node(cfg, &file_label, file_info.clone()); + let node_id = Self::asset_node_id(&file_label); + let menu = Self::leaf_node_kind(&file_label, &entry.name, file_info.kind) + .context_menu(|ui| self.asset_file_context_menu(cfg, ui, node_id, &file_info)); + builder.node(menu); + had_content = true; + } + } + + if !had_content { + Self::add_placeholder_leaf( + builder, + &format!("{source_label}/empty"), + "empty", + AssetNodeKind::Script(ScriptDivision::Script), + ); + had_content = true; + } + + had_content + } + + fn build_plain_directory( + &mut self, + cfg: &mut StaticallyKept, + builder: &mut TreeViewBuilder<u64>, + dir_path: &Path, + parent_label: &str, + division: AssetDivision, + dir_kind: AssetNodeKind, + file_kind: AssetNodeKind, + new_folder_label: &str, + ) { + let entries = match Self::sorted_entries(dir_path) { + Ok(entries) => entries, + Err(err) => { + log::warn!( + "Unable to enumerate directory '{}': {}", + dir_path.display(), + err + ); + Self::add_placeholder_leaf( + builder, + &format!("{parent_label}/unreadable"), + "unreadable", + file_kind, + ); + return; + } + }; + + if entries.is_empty() { + Self::add_placeholder_leaf( + builder, + &format!("{parent_label}/empty"), + "empty", + file_kind, + ); + return; + } + + for entry in entries { + let child_label = format!("{}/{}", parent_label, entry.name); + if entry.is_dir { + let dir_info = AssetNodeInfo { + path: entry.path.clone(), + division, + kind: dir_kind, + is_dir: true, + is_division_root: false, + allow_add_folder: true, + }; + Self::register_asset_node(cfg, &child_label, dir_info.clone()); + let node_id = Self::asset_node_id(&child_label); + let menu = Self::dir_node_kind(&child_label, &entry.name, dir_info.kind) + .context_menu(|ui| { + self.asset_dir_context_menu(cfg, ui, node_id, &dir_info, new_folder_label) + }); + builder.node(menu); + self.build_plain_directory( + cfg, + builder, + &entry.path, + &child_label, + division, + dir_kind, + file_kind, + new_folder_label, + ); + builder.close_dir(); + } else { + let file_info = AssetNodeInfo { + path: entry.path.clone(), + division, + kind: file_kind, + is_dir: false, + is_division_root: false, + allow_add_folder: false, + }; + Self::register_asset_node(cfg, &child_label, file_info.clone()); + let node_id = Self::asset_node_id(&child_label); + let menu = Self::leaf_node_kind(&child_label, &entry.name, file_info.kind) + .context_menu(|ui| self.asset_file_context_menu(cfg, ui, node_id, &file_info)); + builder.node(menu); + } + } + } + + fn build_kotlin_tree( + &mut self, + cfg: &mut StaticallyKept, + builder: &mut TreeViewBuilder<u64>, + kotlin_path: &Path, + source_label: &str, + ) -> bool { + let entries = match Self::sorted_entries(kotlin_path) { + Ok(entries) => entries, + Err(err) => { + log::warn!( + "Unable to enumerate kotlin sources at '{}': {}", + kotlin_path.display(), + err + ); + Self::add_placeholder_leaf( + builder, + &format!("{source_label}/unreadable"), + "unreadable", + AssetNodeKind::Script(ScriptDivision::Script), + ); + return true; + } + }; + + if entries.is_empty() { + Self::add_placeholder_leaf( + builder, + &format!("{source_label}/no_kotlin_files"), + "no kotlin files", + AssetNodeKind::Script(ScriptDivision::Script), + ); + return true; + } + + let mut had_entries = false; + for entry in entries { + if entry.is_dir { + self.build_kotlin_package_collapsed( + cfg, + builder, + &entry.path, + source_label, + vec![entry.name.clone()], + ); + had_entries = true; + } else { + let file_id = format!("{}/{}", source_label, entry.name); + let file_info = AssetNodeInfo { + path: entry.path.clone(), + division: AssetDivision::Scripts, + kind: AssetNodeKind::Script(ScriptDivision::Script), + is_dir: false, + is_division_root: false, + allow_add_folder: false, + }; + Self::register_asset_node(cfg, &file_id, file_info.clone()); + let node_id = Self::asset_node_id(&file_id); + let menu = Self::leaf_node_kind(&file_id, &entry.name, file_info.kind) + .context_menu(|ui| self.asset_file_context_menu(cfg, ui, node_id, &file_info)); + builder.node(menu); + had_entries = true; + } + } + + had_entries + } + + fn build_kotlin_package_collapsed( + &mut self, + cfg: &mut StaticallyKept, + builder: &mut TreeViewBuilder<u64>, + dir_path: &Path, + parent_path_str: &str, + accumulated_parts: Vec<String>, + ) { + let entries = match Self::sorted_entries(dir_path) { + Ok(entries) => entries, + Err(err) => { + let package_suffix = accumulated_parts.join("."); + let full_path_str = format!("{}/{}", parent_path_str, package_suffix); + log::warn!( + "Unable to enumerate package '{}' ({}): {}", + package_suffix, + dir_path.display(), + err + ); + Self::add_placeholder_leaf( + builder, + &format!("{full_path_str}/unreadable"), + "unreadable", + AssetNodeKind::Script(ScriptDivision::Script), + ); + return; + } + }; + + let subdirs: Vec<&FsEntry> = entries.iter().filter(|e| e.is_dir).collect(); + let files: Vec<&FsEntry> = entries.iter().filter(|e| !e.is_dir).collect(); + + if files.is_empty() && subdirs.len() == 1 { + let subdir = subdirs[0]; + let mut new_parts = accumulated_parts; + new_parts.push(subdir.name.clone()); + self.build_kotlin_package_collapsed( + cfg, + builder, + &subdir.path, + parent_path_str, + new_parts, + ); + } else { + let package_suffix = accumulated_parts.join("."); + let full_path_str = format!("{}/{}", parent_path_str, package_suffix); + + let dir_info = AssetNodeInfo { + path: dir_path.to_path_buf(), + division: AssetDivision::Scripts, + kind: AssetNodeKind::Script(ScriptDivision::Package), + is_dir: true, + is_division_root: false, + allow_add_folder: true, + }; + Self::register_asset_node(cfg, &full_path_str, dir_info.clone()); + let node_id = Self::asset_node_id(&full_path_str); + let menu = Self::dir_node_kind(&full_path_str, &package_suffix, dir_info.kind) + .context_menu(|ui| { + self.asset_dir_context_menu(cfg, ui, node_id, &dir_info, "New Package") + }); + builder.node(menu); + + for file in files { + let file_id = format!("{}/{}", full_path_str, file.name); + let file_info = AssetNodeInfo { + path: file.path.clone(), + division: AssetDivision::Scripts, + kind: AssetNodeKind::Script(ScriptDivision::Script), + is_dir: false, + is_division_root: false, + allow_add_folder: false, + }; + Self::register_asset_node(cfg, &file_id, file_info.clone()); + let node_id = Self::asset_node_id(&file_id); + let menu = Self::leaf_node_kind(&file_id, &file.name, file_info.kind) + .context_menu(|ui| self.asset_file_context_menu(cfg, ui, node_id, &file_info)); + builder.node(menu); + } + + for subdir in subdirs { + self.build_kotlin_package_collapsed( + cfg, + builder, + &subdir.path, + &full_path_str, + vec![subdir.name.clone()], + ); + } + + builder.close_dir(); + } + } + + fn build_scene_branch( + &mut self, + cfg: &mut StaticallyKept, + builder: &mut TreeViewBuilder<u64>, + project_root: &Path, + ) { + let label = "euca://scenes"; + let scenes_root = project_root.join("../../../../../resources").join("scenes"); + let root_info = AssetNodeInfo { + path: scenes_root.clone(), + division: AssetDivision::Scenes, + kind: AssetNodeKind::Scene(SceneDivision::Folder), + is_dir: true, + is_division_root: true, + allow_add_folder: true, + }; + Self::register_asset_node(cfg, label, root_info.clone()); + let node_id = Self::asset_node_id(label); + let menu = Self::dir_node_kind(label, "scenes", root_info.kind).context_menu(|ui| { + self.asset_dir_context_menu(cfg, ui, node_id, &root_info, "New Folder") + }); + builder.node(menu); + if !scenes_root.exists() { + Self::add_placeholder_leaf( + builder, + "euca://scenes/missing", + "missing", + AssetNodeKind::Scene(SceneDivision::Scene), + ); + builder.close_dir(); + return; + } + + let entries = match Self::sorted_entries(&scenes_root) { + Ok(entries) => entries, + Err(err) => { + log::warn!( + "Unable to enumerate scenes at '{}': {}", + scenes_root.display(), + err + ); + Self::add_placeholder_leaf( + builder, + "euca://scenes/unreadable", + "unreadable", + AssetNodeKind::Scene(SceneDivision::Scene), + ); + builder.close_dir(); + return; + } + }; + + let mut had_entries = false; + for entry in entries { + if entry.is_dir { + let child_label = format!("{}/{}", label, entry.name); + let dir_info = AssetNodeInfo { + path: entry.path.clone(), + division: AssetDivision::Scenes, + kind: AssetNodeKind::Scene(SceneDivision::Folder), + is_dir: true, + is_division_root: false, + allow_add_folder: true, + }; + Self::register_asset_node(cfg, &child_label, dir_info.clone()); + let node_id = Self::asset_node_id(&child_label); + let menu = Self::dir_node_kind(&child_label, &entry.name, dir_info.kind) + .context_menu(|ui| { + self.asset_dir_context_menu(cfg, ui, node_id, &dir_info, "New Folder") + }); + builder.node(menu); + self.build_plain_directory( + cfg, + builder, + &entry.path, + &child_label, + AssetDivision::Scenes, + AssetNodeKind::Scene(SceneDivision::Folder), + AssetNodeKind::Scene(SceneDivision::Scene), + "New Folder", + ); + builder.close_dir(); + had_entries = true; + } else if entry + .path + .extension() + .and_then(|ext| ext.to_str()) + .map(|ext| ext.eq_ignore_ascii_case("eucs")) + .unwrap_or(false) + { + let file_label = format!("{}/{}", label, entry.name); + let file_info = AssetNodeInfo { + path: entry.path.clone(), + division: AssetDivision::Scenes, + kind: AssetNodeKind::Scene(SceneDivision::Scene), + is_dir: false, + is_division_root: false, + allow_add_folder: false, + }; + Self::register_asset_node(cfg, &file_label, file_info.clone()); + let node_id = Self::asset_node_id(&file_label); + let menu = Self::leaf_node_kind(&file_label, &entry.name, file_info.kind) + .context_menu(|ui| self.asset_file_context_menu(cfg, ui, node_id, &file_info)); + builder.node(menu); + had_entries = true; + } + } + + if !had_entries { + Self::add_placeholder_leaf( + builder, + "euca://scenes/no_scenes", + "no scenes", + AssetNodeKind::Scene(SceneDivision::Scene), + ); + } + + builder.close_dir(); + } + + fn sorted_entries(path: &Path) -> io::Result<Vec<FsEntry>> { + let mut entries = Vec::new(); + for entry in fs::read_dir(path)? { + let entry = entry?; + let file_type = entry.file_type()?; + let name = entry.file_name().to_string_lossy().to_string(); + entries.push(FsEntry { + path: entry.path(), + name_lower: name.to_lowercase(), + name, + is_dir: file_type.is_dir(), + }); + } + + entries.sort_by(|a, b| match b.is_dir.cmp(&a.is_dir) { + Ordering::Equal => a.name_lower.cmp(&b.name_lower), + other => other, + }); + + Ok(entries) + } + + fn asset_node_id(label: &str) -> u64 { + let mut hasher = DefaultHasher::new(); + label.hash(&mut hasher); + let mut id = hasher.finish(); + if id == 0 { + id = 1; + } + id + } + + fn register_asset_node(cfg: &mut StaticallyKept, id_source: &str, info: AssetNodeInfo) -> u64 { + let node_id = Self::asset_node_id(id_source); + cfg.asset_node_info.insert(node_id, info); + node_id + } + + fn dir_node<'ui>(label: &str) -> NodeBuilder<'ui, u64> { + Self::with_icon_kind( + NodeBuilder::dir(Self::asset_node_id(label)).label(label.to_string()), + AssetNodeKind::Resource(ResourceDivision::Folder), + ) + } + + fn dir_node_kind<'ui>( + id_source: &str, + label: &str, + kind: AssetNodeKind, + ) -> NodeBuilder<'ui, u64> { + Self::with_icon_kind( + NodeBuilder::dir(Self::asset_node_id(id_source)).label(label.to_string()), + kind, + ) + } + + fn leaf_node_kind<'ui>( + id_source: &str, + label: &str, + kind: AssetNodeKind, + ) -> NodeBuilder<'ui, u64> { + Self::with_icon_kind( + NodeBuilder::leaf(Self::asset_node_id(id_source)).label(label.to_string()), + kind, + ) + } + + fn with_icon_kind( + builder: NodeBuilder<u64>, + kind: AssetNodeKind, + ) -> NodeBuilder<u64> { + builder.icon(move |ui| { + egui_extras::install_image_loaders(ui.ctx()); + Self::draw_asset_icon(ui, kind) + }) + } + + fn draw_asset_icon(ui: &mut egui::Ui, _kind: AssetNodeKind) { + let image = egui::Image::from_bytes("bytes://asset-viewer-icon", NO_TEXTURE) + .max_size(egui::vec2(14.0, 14.0)); + ui.add(image); + } + + fn add_placeholder_leaf( + builder: &mut TreeViewBuilder<u64>, + id_source: &str, + label: &str, + kind: AssetNodeKind, + ) { + builder.node(Self::leaf_node_kind(id_source, label, kind)); + } + + fn asset_dir_context_menu( + &mut self, + cfg: &mut StaticallyKept, + ui: &mut egui::Ui, + node_id: u64, + info: &AssetNodeInfo, + new_folder_label: &str, + ) { + if info.allow_add_folder { + if ui.button(new_folder_label).clicked() { + ui.close(); + let base_name = if info.division == AssetDivision::Scripts { + "newpackage" + } else { + "New Folder" + }; + self.create_asset_folder(&info.path, base_name); + } + } + + if ui.button("Paste").clicked() { + ui.close(); + if !info.path.exists() { + if let Err(err) = fs::create_dir_all(&info.path) { + warn!("Unable to create folder '{}': {}", info.path.display(), err); + return; + } + } + self.signal.push_back(Signal::AssetPaste { + target_dir: info.path.clone(), + division: info.division, + }); + } + + if ui.button("Reveal Folder").clicked() { + ui.close(); + if let Err(err) = open::that(&info.path) { + warn!("Unable to reveal folder: {}", err); + } + } + + if !info.is_division_root && ui.button("Rename").clicked() { + let current_name = info + .path + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or("") + .to_string(); + cfg.asset_rename = Some(crate::editor::AssetRenameState { + node_id, + original_path: info.path.clone(), + buffer: current_name, + just_started: true, + }); + } + + if !info.is_division_root && ui.button("Delete").clicked() { + ui.close(); + self.delete_asset_entry(info); + } + } + + fn asset_file_context_menu( + &mut self, + cfg: &mut StaticallyKept, + ui: &mut egui::Ui, + node_id: u64, + info: &AssetNodeInfo, + ) { + if matches!(info.kind, AssetNodeKind::Script(ScriptDivision::Script)) + && ui.button("Open Script").clicked() + { + ui.close(); + if let Err(err) = open::that(&info.path) { + warn!("Unable to open script: {}", err); + } + } + + if ui.button("Rename").clicked() { + let current_name = info + .path + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or("") + .to_string(); + cfg.asset_rename = Some(crate::editor::AssetRenameState { + node_id, + original_path: info.path.clone(), + buffer: current_name, + just_started: true, + }); + } + + if ui.button("Copy").clicked() { + ui.close(); + self.signal.push_back(Signal::AssetCopy { + source: info.path.clone(), + division: info.division, + }); + } + + if ui.button("Delete").clicked() { + ui.close(); + self.delete_asset_entry(info); + } + } + + fn apply_asset_rename(&self, rename: &crate::editor::AssetRenameState, is_dir: bool) { + let trimmed = rename.buffer.trim(); + if trimmed.is_empty() { + warn!("Rename cancelled: empty name"); + return; + } + + let Some(parent) = rename.original_path.parent() else { + warn!("Unable to rename: missing parent directory"); + return; + }; + + let mut new_name = trimmed.to_string(); + if !is_dir { + if let Some(ext) = rename.original_path.extension().and_then(|e| e.to_str()) { + let has_ext = Path::new(&new_name) + .extension() + .and_then(|e| e.to_str()) + .is_some(); + if !has_ext { + new_name = format!("{new_name}.{ext}"); + } + } + } + + let target_path = parent.join(&new_name); + if target_path == rename.original_path { + return; + } + + if target_path.exists() { + warn!("Rename target already exists: {}", target_path.display()); + return; + } + + if let Err(err) = fs::rename(&rename.original_path, &target_path) { + warn!( + "Failed to rename '{}': {}", + rename.original_path.display(), + err + ); + } else { + info!("Renamed to {}", target_path.display()); + let old_meta = PathBuf::from(format!("{}.eucmeta", rename.original_path.display())); + if old_meta.exists() { + let new_meta = PathBuf::from(format!("{}.eucmeta", target_path.display())); + if let Err(e) = fs::rename(&old_meta, &new_meta) { + warn!("Failed to move .eucmeta sidecar '{}': {}", old_meta.display(), e); + } + } + } + } + + fn delete_asset_entry(&self, info: &AssetNodeInfo) { + if info.is_division_root { + warn!("Cannot delete division root"); + return; + } + + let result = if info.is_dir { + fs::remove_dir_all(&info.path) + } else { + fs::remove_file(&info.path) + }; + + if let Err(err) = result { + warn!("Failed to delete '{}': {}", info.path.display(), err); + } else { + info!("Deleted {}", info.path.display()); + if !info.is_dir { + let meta = PathBuf::from(format!("{}.eucmeta", info.path.display())); + if meta.exists() { + if let Err(e) = fs::remove_file(&meta) { + warn!("Failed to remove .eucmeta sidecar '{}': {}", meta.display(), e); + } + } + } + } + } + + fn create_asset_folder(&self, base_dir: &Path, base_name: &str) { + if let Err(err) = fs::create_dir_all(base_dir) { + warn!("Unable to create folder '{}': {}", base_dir.display(), err); + return; + } + + let mut candidate = base_dir.join(base_name); + if candidate.exists() { + let mut index = 1; + loop { + let suffix = if base_name.contains(' ') { + format!(" {}", index) + } else { + format!("{}", index) + }; + candidate = base_dir.join(format!("{base_name}{suffix}")); + if !candidate.exists() { + break; + } + index += 1; + } + } + + if let Err(err) = fs::create_dir_all(&candidate) { + warn!("Unable to create folder '{}': {}", candidate.display(), err); + } else { + info!("Created folder {}", candidate.display()); + } + } + + fn handle_asset_move( + &mut self, + cfg: &mut StaticallyKept, + drag: &egui_ltreeview::DragAndDrop<u64>, + ) { + let Some(&source_id) = drag.source.first() else { + return; + }; + let Some(source_info) = cfg.asset_node_info.get(&source_id).cloned() else { + return; + }; + let Some(target_info) = cfg.asset_node_info.get(&drag.target).cloned() else { + return; + }; + + if source_info.is_division_root { + warn!("Cannot move division root"); + return; + } + + if source_info.division != target_info.division { + warn!("Cannot move assets across divisions"); + return; + } + + let target_dir = if target_info.is_dir { + target_info.path.clone() + } else { + target_info + .path + .parent() + .unwrap_or(&target_info.path) + .to_path_buf() + }; + + if !target_dir.exists() { + if let Err(err) = fs::create_dir_all(&target_dir) { + warn!("Target directory does not exist: {}", err); + return; + } + } + + if source_info.is_dir && target_dir.starts_with(&source_info.path) { + warn!("Cannot move a folder into itself"); + return; + } + + let Some(name) = source_info.path.file_name() else { + warn!("Unable to move: invalid file name"); + return; + }; + + let target_path = target_dir.join(name); + if target_path == source_info.path { + return; + } + + if target_path.exists() { + warn!("Target already exists: {}", target_path.display()); + return; + } + + if let Err(err) = fs::rename(&source_info.path, &target_path) { + warn!("Failed to move '{}': {}", source_info.path.display(), err); + } else { + info!("Moved to {}", target_path.display()); + if !source_info.is_dir { + let old_meta = PathBuf::from(format!("{}.eucmeta", source_info.path.display())); + if old_meta.exists() { + let new_meta = PathBuf::from(format!("{}.eucmeta", target_path.display())); + if let Err(e) = fs::rename(&old_meta, &new_meta) { + warn!("Failed to move .eucmeta sidecar '{}': {}", old_meta.display(), e); + } + } + } + } + } + + fn is_model_file(name: &str) -> bool { + let name = name.to_ascii_lowercase(); + name.ends_with(".glb") || name.ends_with(".gltf") + } + + fn is_texture_file(name: &str) -> bool { + let name = name.to_ascii_lowercase(); + name.ends_with(".png") + || name.ends_with(".jpg") + || name.ends_with(".jpeg") + || name.ends_with(".tga") + || name.ends_with(".bmp") + || name.ends_with(".webp") + } + + fn queue_model_load(&self, reference: ResourceReference, label: String) { + if ASSET_REGISTRY + .read() + .get_model_handle_by_reference(&reference) + .is_some() + { + eucalyptus_core::info!("Model already loaded: {}", label); + return; + } + + let graphics = self.graphics.clone(); + let queue = graphics.future_queue.clone(); + queue.push(async move { + let path = reference.resolve()?; + let buffer = fs::read(&path)?; + let extension = path + .extension() + .and_then(|ext| ext.to_str()) + .map(|ext| ext.to_ascii_lowercase()); + + let handle = match extension.as_deref() { + Some("eucmdl") => { + let model = rkyv::from_bytes::<EucalyptusModel, rkyv::rancor::Error>(&buffer) + .map_err(|e| anyhow::anyhow!( + "Unable to deserialize .eucmdl model '{}': {}", + path.display(), + e + ))?; + + let runtime_model = model.load(reference.clone(), graphics.clone()); + let mut registry = ASSET_REGISTRY.write(); + registry.add_model_with_label(label.clone(), runtime_model) + } + _ => match Model::load_from_memory_raw( + graphics.clone(), + buffer, + Some(reference.clone()), + Some(label.as_str()), + ASSET_REGISTRY.clone(), + ) + .await + { + Ok(v) => v, + Err(e) => { + eucalyptus_core::warn!("Unable to load model {}: {}", reference, e); + return Err(e); + } + }, + }; + + let mut registry = ASSET_REGISTRY.write(); + registry.label_model(label.clone(), handle); + + eucalyptus_core::success!("Loaded model {}", label); + Ok::<(), anyhow::Error>(()) + }); + } + + fn queue_texture_load(&self, reference: ResourceReference, label: String, strict_image_decode: bool) { + if ASSET_REGISTRY + .read() + .get_texture_handle_by_reference(&reference) + .is_some() + { + eucalyptus_core::info!("Texture already loaded: {}", label); + return; + } + + let graphics = self.graphics.clone(); + let queue = graphics.future_queue.clone(); + queue.push(async move { + let path = reference.resolve()?; + let bytes = fs::read(&path)?; + + if strict_image_decode + && let Err(err) = image::load_from_memory(bytes.as_slice()) + { + let error = anyhow::anyhow!( + "'{}' is not a texture-compatible eucbin payload: {}", + path.display(), + err + ); + eucalyptus_core::warn!("{}", error); + return Err(error); + } + + let mut texture = TextureBuilder::new(&graphics.device) + .with_bytes(graphics.clone(), bytes.as_slice()) + .label(label.as_str()) + .build(); + texture.reference = Some(reference.clone()); + let mut registry = ASSET_REGISTRY.write(); + registry.add_texture_with_label(label.clone(), texture); + Ok::<(), anyhow::Error>(()) + }); + } + + pub(crate) fn handle_tree_selection(&mut self, cfg: &mut StaticallyKept, items: &[u64]) { + for node_id in items { + self.resolve_tree_node(cfg, *node_id); + } + } + + pub(crate) fn handle_tree_activate( + &mut self, + cfg: &mut StaticallyKept, + activate: &egui_ltreeview::Activate<u64>, + ) { + self.handle_tree_selection(cfg, &activate.selected); + } + + pub(crate) fn handle_tree_drag( + &mut self, + cfg: &mut StaticallyKept, + drag: &egui_ltreeview::DragAndDrop<u64>, + ) { + if let Some(&node_id) = drag.source.first() { + if let Some(selection) = cfg.component_selection(node_id) { + cfg.pending_component_drag = Some(selection); + self.inspect_component_selection(cfg, selection); + } + } + } + + pub(crate) fn handle_tree_move( + &mut self, + cfg: &mut StaticallyKept, + drag: &egui_ltreeview::DragAndDrop<u64>, + ) { + let selection = cfg.pending_component_drag.take().or_else(|| { + drag.source + .first() + .and_then(|node_id| cfg.component_selection(*node_id)) + }); + + if let Some(selection) = selection { + self.inspect_component_selection(cfg, selection); + return; + } + + let target_entity = Self::entity_from_node_id(drag.target); + + for &source_id in &drag.source { + let Some(source_entity) = Self::entity_from_node_id(source_id) else { continue }; + + if cfg.component_selection(source_id).is_some() { + continue; + } + + if drag.target == u64::MAX { + Hierarchy::remove_parent(self.world, source_entity); + } else if let Some(target_entity) = target_entity { + if source_entity == target_entity { + continue; + } + if Hierarchy::is_descendant_of(self.world, target_entity, source_entity) { + continue; + } + Hierarchy::set_parent(self.world, source_entity, target_entity); + } + } + } + + fn resolve_tree_node(&mut self, cfg: &mut StaticallyKept, node_id: u64) { + if node_id == u64::MAX { + log_once::debug_once!("Root node has been selected"); + cfg.root_node_selected = true; + *self.selected_entity = None; + } else if let Some(selection) = cfg.component_selection(node_id) { + cfg.root_node_selected = false; + *self.selected_entity = selection.entity(); + self.inspect_component_selection(cfg, selection); + } else if let Some(entity) = Self::entity_from_node_id(node_id) { + cfg.root_node_selected = false; + *self.selected_entity = Some(entity); + } + } + + fn inspect_component_selection( + &mut self, + cfg: &mut StaticallyKept, + selection: ComponentNodeSelection, + ) { + cfg.remember_component_lookup(selection); + let component_id = selection.component_type_id; + let matches = self + .component_registry + .find_entities_by_numeric_id(self.world, component_id); + let descriptor = self + .component_registry + .get_descriptor_by_numeric_id(component_id); + + if matches.is_empty() { + log::warn!("Component id #{} not found in world", component_id); + return; + } + + let name = descriptor + .map(|desc| desc.fqtn.as_str()) + .unwrap_or("<unknown>"); + for entity in matches { + log::debug!( + "Serializable component '{}' (id #{}) attached to entity {:?}", + name, + component_id, + entity + ); + } + } + + fn entity_from_node_id(node_id: u64) -> Option<Entity> { + if node_id == u64::MAX { + None + } else { + Entity::from_bits(node_id) + } + } +} + +pub struct AssetViewerDock; + +impl EditorTabDock for AssetViewerDock { + fn desc() -> EditorTabDockDescriptor { + EditorTabDockDescriptor { + id: "asset_viewer", + title: "Asset Viewer".to_string(), + visibility: EditorTabVisibility::all(), + } + } + + fn display(viewer: &mut EditorTabViewer<'_>, ui: &mut egui::Ui) { + viewer.show_asset_viewer(ui); + } +} @@ -0,0 +1,162 @@ +use std::path::PathBuf; + +use egui::{Margin, RichText}; + +use crate::editor::{ + EditorTabDock, EditorTabDockDescriptor, EditorTabViewer + , +}; +use crate::editor::docks::console::{ConsoleItem, ErrorLevel}; +use crate::editor::page::EditorTabVisibility; + +impl<'a> EditorTabViewer<'a> { + pub fn build_console(&mut self, ui: &mut egui::Ui) { + fn analyse_error(log: &Vec<String>) -> Vec<ConsoleItem> { + fn parse_compiler_location(line: &str) -> Option<(ErrorLevel, PathBuf, String)> { + let trimmed = line.trim_start(); + let (error_level, rest) = if let Some(r) = trimmed.strip_prefix("e: file:///") { + (ErrorLevel::Error, r) + } else if let Some(r) = trimmed.strip_prefix("w: file:///") { + (ErrorLevel::Warn, r) + } else { + return None; + }; + + let location = rest.split_whitespace().next()?; + + let mut segments = location.rsplitn(3, ':'); + let column = segments.next()?; + let row = segments.next()?; + let path = segments.next()?; + + Some((error_level, PathBuf::from(path), format!("{row}:{column}"))) + } + + let mut list: Vec<ConsoleItem> = Vec::new(); + for (index, line) in log.iter().enumerate() { + if line.contains("The required library") { + list.push(ConsoleItem { + error_level: ErrorLevel::Error, + msg: line.clone(), + file_location: None, + line_ref: None, + id: index as u64, + }); + } else if let Some((error_level, path, loc)) = parse_compiler_location(line) { + list.push(ConsoleItem { + error_level, + msg: line.clone(), + file_location: Some(path), + line_ref: Some(loc), + id: index as u64, + }); + } else { + list.push(ConsoleItem { + error_level: ErrorLevel::Info, + msg: line.clone(), + file_location: None, + line_ref: None, + id: index as u64, + }); + } + } + list + } + + let logs = analyse_error(&self.build_logs); + + egui::ScrollArea::vertical() + .auto_shrink([false, false]) + .stick_to_bottom(true) + .show(ui, |ui| { + if logs.is_empty() { + ui.label("Build output will appear here once available."); + return; + } + + for item in &logs { + let (bg_color, text_color, stroke_color) = match item.error_level { + ErrorLevel::Error => ( + egui::Color32::from_rgb(60, 20, 20), + egui::Color32::from_rgb(255, 200, 200), + egui::Color32::from_rgb(255, 200, 200), + ), + ErrorLevel::Warn => ( + egui::Color32::from_rgb(40, 40, 10), + egui::Color32::from_rgb(255, 255, 200), + egui::Color32::from_rgb(255, 255, 200), + ), + ErrorLevel::Info => ( + egui::Color32::TRANSPARENT, + ui.style().visuals.text_color(), + egui::Color32::TRANSPARENT, + ), + }; + + if matches!(item.error_level, ErrorLevel::Info) { + ui.label(RichText::new(&item.msg).monospace()); + } else { + let available_width = ui.available_width(); + let frame = egui::Frame::new() + .inner_margin(Margin::symmetric(8, 6)) + .fill(bg_color) + .stroke(egui::Stroke::new(1.0, stroke_color)); + + let response = frame + .show(ui, |ui| { + ui.set_width(available_width - 10.0); + ui.horizontal(|ui| { + ui.label( + RichText::new(&item.msg).color(text_color).monospace(), + ); + }); + }) + .response; + + if response.clicked() { + log::debug!("Log item clicked: {}", &item.id); + if let (Some(path), Some(loc)) = (&item.file_location, &item.line_ref) { + let location_arg = format!("{}:{}", path.display(), loc); + + match std::process::Command::new("code") + .args(["-g", &location_arg]) + .spawn() + .map(|_| ()) + { + Ok(()) => { + log::info!( + "Launched Visual Studio Code at the error: {}", + &location_arg + ); + } + Err(e) => { + eucalyptus_core::warn!( + "Failed to open '{}' in VS Code: {}", + location_arg, + e + ); + } + } + } + } + } + } + }); + } +} + +pub struct BuildConsoleDock; + +impl EditorTabDock for BuildConsoleDock { + fn desc() -> EditorTabDockDescriptor { + EditorTabDockDescriptor { + id: "build_console", + title: "Build Output".to_string(), + visibility: EditorTabVisibility::all(), // idk about this one + } + } + + fn display(viewer: &mut EditorTabViewer<'_>, ui: &mut egui::Ui) { + viewer.build_console(ui); + } +} @@ -0,0 +1,104 @@ +use parking_lot::Mutex; +use std::io::{BufRead, BufReader}; +use std::net::{TcpListener, TcpStream}; +use std::path::PathBuf; +use std::sync::Arc; + +pub struct EucalyptusConsole { + pub buffer: Arc<Mutex<Vec<String>>>, + pub history: Vec<String>, + + pub show_info: bool, + pub show_warning: bool, + pub show_error: bool, + pub show_debug: bool, + pub show_trace: bool, + pub auto_scroll: bool, +} + +impl EucalyptusConsole { + /// Creates a new instance of a [EucalyptusConsole], and + pub fn new(port: Option<&str>) -> Self { + let result = Self { + buffer: Arc::new(Default::default()), + history: vec![], + show_info: true, + show_warning: true, + show_error: true, + show_debug: false, + show_trace: false, + auto_scroll: true, + }; + + let buf_clone = result.buffer.clone(); + let addr = format!("127.0.0.1:{}", port.unwrap_or("56624")); + std::thread::spawn(move || { + let listener = TcpListener::bind(&addr).unwrap(); + + log::info!("eucalyptus-editor debug console started at {}", addr); + + loop { + for stream in listener.incoming() { + match stream { + Ok(stream) => { + let buf_clone = buf_clone.clone(); + std::thread::spawn(move || { + EucalyptusConsole::handle_client(stream, buf_clone) + }); + } + Err(e) => { + eprintln!("Connection failed: {}", e); + } + } + } + } + }); + + result + } + + fn handle_client(stream: TcpStream, buf: Arc<Mutex<Vec<String>>>) { + let peer_addr = stream.peer_addr().unwrap(); + println!("New connection from: {}", peer_addr); + + let reader = BufReader::new(stream); + + for line in reader.lines() { + match line { + Ok(text) => { + buf.lock().push(text); + } + Err(e) => { + buf.lock() + .push(format!("Error reading from {}: {}", peer_addr, e)); + break; + } + } + } + + println!("Connection closed: {}", peer_addr); + } + + /// Drains all from the thread-safe buffer and adds to the history, while returning that value. + /// + /// It is recommended to use this function. + pub fn take(&mut self) -> Vec<String> { + let buf = self.buffer.lock().drain(..).collect::<Vec<String>>(); + buf.iter().for_each(|v| self.history.push(v.clone())); + buf + } +} + +pub enum ErrorLevel { + Info, + Warn, + Error, +} + +pub struct ConsoleItem { + pub id: u64, + pub error_level: ErrorLevel, + pub msg: String, + pub file_location: Option<PathBuf>, + pub line_ref: Option<String>, +} @@ -0,0 +1,308 @@ +use egui_ltreeview::{NodeBuilder, TreeViewBuilder}; +use eucalyptus_core::{ + component::ComponentRegistry, + hierarchy::{Children, Hierarchy, Parent}, + physics::{collider::ColliderGroup, rigidbody::RigidBody}, + states::{Label, PROJECT}, +}; +use hecs::{Entity, World}; +use std::collections::{BTreeMap, HashMap, VecDeque}; + +use crate::editor::{Editor, EditorTabDock, EditorTabDockDescriptor, EditorTabViewer, Signal, StaticallyKept, TABS_GLOBAL}; +use crate::editor::page::EditorTabVisibility; + +impl<'a> EditorTabViewer<'a> { + pub(crate) fn entity_list(&mut self, ui: &mut egui::Ui) { + puffin::profile_function!(); + let mut cfg = TABS_GLOBAL.lock(); + + let (_response, action) = { + puffin::profile_scope!("entity_list.tree_build"); + egui_ltreeview::TreeView::new(egui::Id::new( + "model_entity_list", + )) + .show(ui, |builder| { + let current_scene_name = { + PROJECT + .read() + .last_opened_scene + .clone() + .unwrap_or("Scene".to_string()) + }; + builder.node( + // root node/scene display + NodeBuilder::dir(u64::MAX) + .label(format!("Scene: {}", current_scene_name)) + .context_menu(|ui| { + if ui.button("New Empty Entity").clicked() { + let label = Editor::unique_label_for_world(self.world, "Blank Entity"); + self.world.spawn((label,)); + ui.close(); + } + ui.menu_button("Import Template", |_| { + + }); + }), + ); + // the root scene must be the biggest number possible to remove any ambiguity + + fn add_entity_to_tree( + builder: &mut TreeViewBuilder<u64>, + entity: Entity, + world: &mut World, + registry: &ComponentRegistry, + component_ids_by_entity: &HashMap<Entity, Vec<u64>>, + rigidbody_component_id: Option<u64>, + cfg: &mut StaticallyKept, + signal: &mut VecDeque<Signal>, + ) -> anyhow::Result<()> { + puffin::profile_scope!("entity_list.add_entity_to_tree"); + let entity_id = entity.to_bits().get(); + let label = if let Ok(label) = world.query_one::<&Label>(entity).get() + { + label.clone() + } else { + anyhow::bail!( + "This entity [{}] is expected to contain Label", + entity_id + ); + }; + + builder.node( + NodeBuilder::dir(entity_id) + .label(label.as_str()) + .context_menu(|ui| { + ui.menu_button("New", |ui| { + if ui.button("Child").clicked() { + let label = Editor::unique_label_for_world(world, "New Entity"); + let child = world.spawn((label,)); + Hierarchy::set_parent(world, child, entity); + ui.close(); + } + }); + ui.menu_button("Add", |ui| { + let mut grouped_components: BTreeMap<String, Vec<(u64, &eucalyptus_core::component::ComponentDescriptor)>> = + BTreeMap::new(); + + for (id, desc) in registry.iter_available_components() { + let category = desc + .category + .clone() + .unwrap_or_else(|| "Uncategorised".to_string()); + grouped_components + .entry(category) + .or_default() + .push((id, desc)); + } + + for (category, components) in grouped_components.iter_mut() { + components.sort_by(|a, b| a.1.type_name.cmp(&b.1.type_name)); + + ui.menu_button(category, |ui| { + for (id, desc) in components.iter() { + let response = ui.button(desc.type_name.as_str()); + + if let Some(description) = desc.description.as_ref() { + response.clone().on_hover_text(description); + } + + if response.clicked() { + if let Some(component) = + registry.create_default_component(*id) + { + signal.push_back(Signal::AddComponent(entity, component)); + } + ui.close(); + } + } + }); + } + }); + if ui.button("Create Template").clicked() { + eucalyptus_core::fatal!("Not implemented yet"); + } + }), + ); + + if let Some(component_ids) = component_ids_by_entity.get(&entity) { + let display_id = crate::features::is_enabled(crate::features::ShowComponentTypeIDInEditor); + let has_rigidbody = world.get::<&RigidBody>(entity).is_ok(); + let has_collider = world.get::<&ColliderGroup>(entity).is_ok(); + + for component_type_id in component_ids { + let component_node_id = + cfg.component_node_id(entity, *component_type_id); + let display = registry + .get_descriptor_by_numeric_id(*component_type_id) + .map(|desc| { + if display_id { + format!("{} (id #{component_type_id})", desc.type_name) + } else { + desc.type_name.clone() + } + }) + .unwrap_or_else(|| { + if display_id { + format!("Unknown (id #{component_type_id})") + } else { + String::from("Unknown") + } + }); + + let node = NodeBuilder::leaf(component_node_id) + .label_ui(|ui| { + ui.label(display.clone()); + + if has_rigidbody + && !has_collider + && Some(*component_type_id) == rigidbody_component_id + { + ui.add_space(4.0); + ui.small_button("⚠") + .on_hover_text("RigidBody has no colliders! Add the ColliderGroup component"); + } + }) + .context_menu(|ui| { + if ui.button("Remove Component").clicked() { + registry.remove_component_by_id( + world, + entity, + *component_type_id, + ); + ui.close(); + } + }); + + builder.node(node); + } + } + + let children_entities = if let Ok(children) = world.get::<&Children>(entity) { + children.children().to_vec() + } else { + Vec::new() + }; + + for child in children_entities { + if let Err(e) = + add_entity_to_tree( + builder, + child, + world, + registry, + component_ids_by_entity, + rigidbody_component_id, + cfg, + signal, + ) + { + log_once::error_once!( + "Failed to add child entity to tree, skipping: {}", + e + ); + continue; + } + } + + builder.close_dir(); + Ok(()) + } + + let mut component_ids_by_entity: HashMap<Entity, Vec<u64>> = HashMap::new(); + let rigidbody_component_id = self + .component_registry + .iter_available_components() + .find_map(|(id, desc)| { + if desc.fqtn == "eucalyptus_core::physics::rigidbody::RigidBody" { + Some(id) + } else { + None + } + }); + { + puffin::profile_scope!("entity_list.index_components"); + for (component_id, _) in self.component_registry.iter_available_components() { + for entity in self + .component_registry + .find_entities_by_numeric_id(self.world, component_id) + { + component_ids_by_entity + .entry(entity) + .or_default() + .push(component_id); + } + } + } + + let root_entities: Vec<Entity> = self + .world + .query::<Entity>() + .without::<&Parent>() + .iter() + .map(|e| e) + .collect(); + + for entity in root_entities { + puffin::profile_scope!("entity_list.root_entity"); + if let Err(e) = add_entity_to_tree( + builder, + entity, + &mut self.world, + &self.component_registry, + &component_ids_by_entity, + rigidbody_component_id, + &mut cfg, + self.signal, + ) { + log_once::error_once!( + "Failed to add child entity to tree, skipping: {}", + e + ); + } + } + + builder.close_dir(); + }) + }; + + puffin::profile_scope!("entity_list.actions"); + for i in action { + match i { + egui_ltreeview::Action::SetSelected(items) => { + log_once::debug_once!("Selected: {:?}", items); + self.handle_tree_selection(&mut cfg, &items); + } + egui_ltreeview::Action::Move(drag_and_drop) => { + log_once::debug_once!("Moved: {:?}", drag_and_drop); + self.handle_tree_move(&mut cfg, &drag_and_drop); + } + egui_ltreeview::Action::Drag(drag_and_drop) => { + log_once::debug_once!("Dragged: {:?}", drag_and_drop); + self.handle_tree_drag(&mut cfg, &drag_and_drop); + } + egui_ltreeview::Action::Activate(activate) => { + log_once::debug_once!("Activated: {:?}", activate); + self.handle_tree_activate(&mut cfg, &activate); + } + egui_ltreeview::Action::DragExternal(_drag_and_drop_external) => {} + egui_ltreeview::Action::MoveExternal(_drag_and_drop_external) => {} + } + } + } +} + +pub struct EntityListDock; + +impl EditorTabDock for EntityListDock { + fn desc() -> EditorTabDockDescriptor { + EditorTabDockDescriptor { + id: "entity_list", + title: "Model/Entity List".to_string(), + visibility: EditorTabVisibility::GameEditor, + } + } + + fn display(viewer: &mut EditorTabViewer<'_>, ui: &mut egui::Ui) { + viewer.entity_list(ui); + } +} @@ -0,0 +1,6 @@ +pub mod asset_viewer; +pub mod build_console; +pub mod console; +pub mod entity_list; +pub mod resource; +pub mod viewport; @@ -0,0 +1,139 @@ +use hecs::Entity; +use eucalyptus_core::entity_status::EntityStatus; +use crate::editor::{EditorTabDock, EditorTabDockDescriptor, EditorTabViewer, TABS_GLOBAL}; +use dropbear_engine::camera::Camera; +use eucalyptus_core::camera::{CameraComponent, CameraType}; +use crate::editor::page::EditorTabVisibility; + +impl<'a> EditorTabViewer<'a> { + pub(crate) fn resource_inspector(&mut self, ui: &mut egui::Ui) { + let mut cfg = TABS_GLOBAL.lock(); + + let local_scene_settings = cfg.root_node_selected; + + if let Some(entity) = self.selected_entity { + let inspect_entity = *entity; + + if !cfg.root_node_selected { + ui.label(format!("Entity ID: {}", inspect_entity.id())); + ui.separator(); + + // entity status + { + let (mut hidden, mut disabled) = self + .world + .get::<&EntityStatus>(inspect_entity) + .map(|s| (s.hidden, s.disabled)) + .unwrap_or((false, false)); + + let mut status_changed = false; + ui.horizontal(|ui| { + if ui.checkbox(&mut hidden, "Hidden").changed() { + status_changed = true; + } + if ui.checkbox(&mut disabled, "Disabled").changed() { + status_changed = true; + } + }); + + if status_changed { + let has_status = self.world.get::<&EntityStatus>(inspect_entity).is_ok(); + if has_status { + if let Ok(mut s) = self.world.get::<&mut EntityStatus>(inspect_entity) { + s.hidden = hidden; + s.disabled = disabled; + } + } else { + let _ = self.world.insert_one( + inspect_entity, + EntityStatus { hidden, disabled }, + ); + } + } + } + ui.separator(); + + let mut local_unset_comp = false; + if let Ok((_, comp)) = self.world.query_one::<(&Camera, &CameraComponent)>(inspect_entity).get() { + let is_active = self + .active_camera + .lock() + .map_or(false, |active| active == inspect_entity); + ui.horizontal(|ui| { + let label = if is_active { + "Viewing Through This Camera" + } else { + "View Through This Camera" + }; + if ui + .add_enabled(!is_active, egui::Button::new(label)) + .clicked() + { + let mut active_camera = self.active_camera.lock(); + *active_camera = Some(inspect_entity); + } + + let mut is_starting = comp.starting_camera; + let is_starting_label = if comp.camera_type == CameraType::Debug { + is_starting = true; + "Cannot set a Debug camera as starting" + } else if is_starting { + "Already set as starting" + } else { + "Set as starting" + }; + + if ui + .add_enabled(!is_starting, egui::Button::new(is_starting_label)) + .clicked() + { + local_unset_comp = true; + } + }); + ui.separator(); + } + + if local_unset_comp { + for (e, comp) in self.world.query::<(Entity, &mut CameraComponent)>().iter() { + if e == inspect_entity { + comp.starting_camera = true; + continue; + } + log::debug!("Unset starting camera for entity {:?}", e); + comp.starting_camera = false; + } + } + + self.component_registry.inspect_components( + self.world, + inspect_entity, + ui, + self.graphics.clone(), + ); + } + } else if !local_scene_settings { + ui.label("No entity selected, therefore no info to provide. Go on, what are you waiting for? Click an entity!"); + } + + if local_scene_settings { + log_once::debug_once!("Rendering scene settings"); + self.scene_settings(&mut cfg, ui); + } + } +} + +pub struct ResourceInspectorDock; + +impl EditorTabDock for ResourceInspectorDock { + fn desc() -> EditorTabDockDescriptor { + EditorTabDockDescriptor { + id: "inspector", + title: "Resource Inspector".to_string(), + visibility: EditorTabVisibility::GameEditor, + } + } + + fn display(viewer: &mut EditorTabViewer<'_>, ui: &mut egui::Ui) { + viewer.resource_inspector(ui); + } +} @@ -0,0 +1,485 @@ +use crate::editor::{DragState, EditorTabDock, EditorTabDockDescriptor, EditorTabViewer, Signal, TABS_GLOBAL, UndoableAction}; +use dropbear_engine::asset::ASSET_REGISTRY; +use dropbear_engine::camera::Camera; +use dropbear_engine::entity::{EntityTransform, MeshRenderer, Transform}; +use dropbear_engine::lighting::Light; +use eucalyptus_core::camera::CameraComponent; +use eucalyptus_core::hierarchy::EntityTransformExt; +use eucalyptus_core::utils::ViewportMode; +use glam::DVec3; +use hecs::Entity; +use transform_gizmo_egui::{GizmoConfig, GizmoExt, GizmoOrientation}; +use eucalyptus_core::input::ndc::NormalisedDeviceCoordinates; +use crate::editor::page::EditorTabVisibility; + +impl<'a> EditorTabViewer<'a> { + fn on_pointer_down(&mut self, touch_pos: [f32; 2], screen_size: [f32; 2], camera: &Camera) { + if self.gizmo.is_focused() { + return; + } + + let inv_proj = camera.proj_mat.inverse().as_mat4(); + let inv_view = camera.view_mat.inverse().as_mat4(); + let (ray_origin, ray_dir) = + NormalisedDeviceCoordinates::screen_to_ray(touch_pos, screen_size, inv_proj, inv_view); + + let mut closest: Option<(Entity, f32)> = None; + + for (entity, transform, mesh) in + self.world.query::<(Entity, &EntityTransform, &MeshRenderer)>().iter() + { + let (aabb_min, aabb_max) = compute_world_aabb(transform, mesh); + if let Some(t) = + NormalisedDeviceCoordinates::ray_aabb(ray_origin, ray_dir, aabb_min, aabb_max) + { + if closest.map_or(true, |(_, best)| t < best) { + closest = Some((entity, t)); + } + } + } + + for (entity, transform, mesh) in + self.world.query::<(Entity, &Transform, &MeshRenderer)>().iter() + { + let (aabb_min, aabb_max) = compute_transform_aabb(transform, mesh); + if let Some(t) = + NormalisedDeviceCoordinates::ray_aabb(ray_origin, ray_dir, aabb_min, aabb_max) + { + if closest.map_or(true, |(_, best)| t < best) { + closest = Some((entity, t)); + } + } + } + + if let Some((entity, t)) = closest { + let hit_point = ray_origin + ray_dir * t; + + let entity_origin: glam::Vec3 = + if let Ok(et) = self.world.get::<&EntityTransform>(entity) { + let p = et.world().position; + glam::Vec3::new(p.x as f32, p.y as f32, p.z as f32) + } else if let Ok(tr) = self.world.get::<&Transform>(entity) { + let p = tr.position; + glam::Vec3::new(p.x as f32, p.y as f32, p.z as f32) + } else { + hit_point + }; + + let plane_normal = -ray_dir; + let plane_d = plane_normal.dot(hit_point); + let pick_offset = hit_point - entity_origin; + + let initial_entity_transform = + self.world.get::<&EntityTransform>(entity).ok().map(|et| *et); + let initial_transform = if initial_entity_transform.is_none() { + self.world.get::<&Transform>(entity).ok().map(|tr| *tr) + } else { + None + }; + + *self.selected_entity = Some(entity); + *self.viewport_drag = Some(DragState { + entity, + plane_normal, + plane_d, + pick_offset, + initial_entity_transform, + initial_transform, + }); + log::debug!("Pointer down - {:?}", entity); + } else { + *self.selected_entity = None; + *self.viewport_drag = None; + } + } + + fn on_pointer_move(&mut self, touch_pos: [f32; 2], screen_size: [f32; 2], camera: &Camera) { + let drag = match self.viewport_drag.as_ref() { + Some(d) => d, + None => return, + }; + + let inv_proj = camera.proj_mat.inverse().as_mat4(); + let inv_view = camera.view_mat.inverse().as_mat4(); + let (ray_origin, ray_dir) = + NormalisedDeviceCoordinates::screen_to_ray(touch_pos, screen_size, inv_proj, inv_view); + + let new_world_pos = match NormalisedDeviceCoordinates::ray_plane( + ray_origin, + ray_dir, + drag.plane_normal, + drag.plane_d, + ) { + Some(p) => p - drag.pick_offset, + None => return, + }; + + let entity = drag.entity; + let new_pos = DVec3::new( + new_world_pos.x as f64, + new_world_pos.y as f64, + new_world_pos.z as f64, + ); + + if let Ok(mut et) = self.world.get::<&mut EntityTransform>(entity) { + et.world_mut().position = new_pos; + } else if let Ok(mut tr) = self.world.get::<&mut Transform>(entity) { + tr.position = new_pos; + } + + log::debug!("Pointer moved - {:?}", entity); + } + + fn on_pointer_up(&mut self) { + let drag = match self.viewport_drag.take() { + Some(d) => d, + None => return, + }; + + if let Some(initial_et) = drag.initial_entity_transform { + if let Ok(current_et) = self.world.get::<&EntityTransform>(drag.entity) { + if *current_et != initial_et { + self.undo_stack + .push(UndoableAction::EntityTransform(drag.entity, initial_et)); + log::debug!("Pushed viewport drag entity-transform to undo stack"); + } + } + } else if let Some(initial_tr) = drag.initial_transform { + if let Ok(current_tr) = self.world.get::<&Transform>(drag.entity) { + if *current_tr != initial_tr { + self.undo_stack + .push(UndoableAction::Transform(drag.entity, initial_tr)); + log::debug!("Pushed viewport drag transform to undo stack"); + } + } + } + } + + pub(crate) fn viewport_tab(&mut self, ui: &mut egui::Ui) { + let mut cfg = TABS_GLOBAL.lock(); + + log_once::debug_once!("Viewport focused"); + + let available_rect = ui.available_rect_before_wrap(); + let available_size = available_rect.size(); + let pixels_per_point = ui.ctx().pixels_per_point(); + + let desired_width = (available_size.x * pixels_per_point).max(1.0).round() as u32; + let desired_height = (available_size.y * pixels_per_point).max(1.0).round() as u32; + if self.tex_size.width != desired_width || self.tex_size.height != desired_height { + if self.signal.is_empty() { + self.signal.push_back(Signal::UpdateViewportSize((desired_width as f32, desired_height as f32))); + } + } + + 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; + let width = height * tex_aspect; + (width, height) + } else { + let width = available_size.x; + 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 (_full, _) = ui.allocate_exact_size(available_size, egui::Sense::hover()); + + ui.painter().image( + self.view, + image_rect, + egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)), + egui::Color32::WHITE, + ); + + let image_response = ui.interact( + image_rect, + egui::Id::new("viewport_image_interaction"), + egui::Sense::click_and_drag(), + ); + + let snapping = ui.input(|input| input.modifiers.shift); + let active_camera_entity: Option<Entity> = *self.active_camera.lock(); + if let Some(active_camera) = active_camera_entity { + let camera_data = { + if let Ok((cam, _comp)) = self + .world + .query_one::<(&Camera, &CameraComponent)>(active_camera) + .get() + { + Some(cam.clone()) + } else { + log::warn!("Queried camera but found no value"); + None + } + }; + + if let Some(camera) = camera_data { + self.gizmo.update_config(GizmoConfig { + view_matrix: camera.view_mat.into(), + projection_matrix: camera.proj_mat.into(), + viewport: image_rect, + modes: *self.gizmo_mode, + orientation: *self.gizmo_orientation, + snapping, + snap_distance: 1.0, + ..Default::default() + }); + + // viewport 3d clicking / dragging + if image_response.clicked() || image_response.drag_started() { + if let Some(pos) = image_response.interact_pointer_pos() { + let local_pos = pos - image_rect.min; + let touch_pos = [local_pos.x, local_pos.y]; + let screen_size = [image_rect.width(), image_rect.height()]; + self.on_pointer_down(touch_pos, screen_size, &camera); + } + } + + if image_response.dragged() { + if let Some(pos) = image_response.interact_pointer_pos() { + let local_pos = pos - image_rect.min; + let touch_pos = [local_pos.x, local_pos.y]; + let screen_size = [image_rect.width(), image_rect.height()]; + self.on_pointer_move(touch_pos, screen_size, &camera); + } + } + + if image_response.drag_stopped() { + self.on_pointer_up(); + } + } + } + + if !matches!(self.viewport_mode, ViewportMode::None) + && let Some(entity_id) = self.selected_entity + { + let mut handled = false; + let mut updated_light_transform: Option<Transform> = None; + + if let Ok(mut entity_transform) = self + .world + .get::<&mut EntityTransform>(*entity_id) + { + let was_focused = cfg.is_focused; + cfg.is_focused = self.gizmo.is_focused(); + + if cfg.is_focused && !was_focused { + cfg.entity_transform_original = Some(*entity_transform); + } + + let synced = entity_transform.propagate(&self.world, *entity_id); + let gizmo_transform = + transform_gizmo_egui::math::Transform::from_scale_rotation_translation( + synced.scale, + synced.rotation, + synced.position, + ); + + if let Some((_result, new_transforms)) = self.gizmo.interact(ui, &[gizmo_transform]) + && let Some(new_transform) = new_transforms.first() + { + let new_synced_pos: glam::DVec3 = new_transform.translation.into(); + let new_synced_rot: glam::DQuat = new_transform.rotation.into(); + let new_synced_scale: glam::DVec3 = new_transform.scale.into(); + + let prev_world_pos = synced.position; + let prev_world_rot = synced.rotation; + let prev_world_scale = synced.scale; + + let safe = |v: f64| if v.abs() < 1e-9 { 1.0 } else { v }; + + let delta_pos = new_synced_pos - prev_world_pos; + let delta_rot = new_synced_rot * prev_world_rot.inverse(); + let delta_scale = glam::DVec3::new( + new_synced_scale.x / safe(prev_world_scale.x), + new_synced_scale.y / safe(prev_world_scale.y), + new_synced_scale.z / safe(prev_world_scale.z), + ); + + match *self.gizmo_orientation { + GizmoOrientation::Global => { + let world = entity_transform.world_mut(); + world.position += delta_pos; + world.rotation = delta_rot * world.rotation; + world.scale *= delta_scale; + } + GizmoOrientation::Local => { + let world_rot = entity_transform.world().rotation; + let world_scale = entity_transform.world().scale; + + let safe_ws = glam::DVec3::new( + safe(world_scale.x), + safe(world_scale.y), + safe(world_scale.z), + ); + + let local = entity_transform.local_mut(); + + local.position += world_rot.inverse() * delta_pos / safe_ws; + + local.rotation = world_rot.inverse() * delta_rot * world_rot * local.rotation; + + local.scale *= delta_scale; + } + } + + updated_light_transform = Some(entity_transform.sync()); + } + + if was_focused && !cfg.is_focused { + if let Some(original) = cfg.entity_transform_original { + if original != *entity_transform { + self.undo_stack.push(UndoableAction::EntityTransform(*entity_id, original)); + log::debug!("Pushed entity transform action to stack"); + } + } + } + + handled = true; + } + + if !handled { + if let Ok(transform) = self.world.query_one::<&mut Transform>(*entity_id).get() { + 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]) + && 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(); + updated_light_transform = Some(*transform); + } + + 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 { + self.undo_stack.push( + UndoableAction::Transform(*entity_id, cfg.old_pos) + ); + log::debug!("Pushed transform action to stack"); + } + } + } + } + + if let Some(updated_transform) = updated_light_transform { + if let Ok(mut light) = self.world.get::<&mut Light>(*entity_id) { + let forward = DVec3::new(0.0, -1.0, 0.0); + light.component.position = updated_transform.position; + light.component.direction = + (updated_transform.rotation * forward).normalize_or_zero(); + } + } + } + } +} + +/// Compute a world-space AABB for an entity that has an [`EntityTransform`]. +fn compute_world_aabb(transform: &EntityTransform, mesh: &MeshRenderer) -> (glam::Vec3, glam::Vec3) { + aabb_for_world_matrix(transform.world().matrix().as_mat4(), mesh) +} + +/// Compute a world-space AABB for an entity that only has a plain [`Transform`]. +fn compute_transform_aabb(transform: &Transform, mesh: &MeshRenderer) -> (glam::Vec3, glam::Vec3) { + aabb_for_world_matrix(transform.matrix().as_mat4(), mesh) +} + +/// Transform the model's local AABB by `world_mat` and return the resulting world AABB. +/// +/// If the model hasn't loaded yet a unit box centred on the origin is used as a fallback. +fn aabb_for_world_matrix( + world_mat: glam::Mat4, + mesh: &MeshRenderer, +) -> (glam::Vec3, glam::Vec3) { + use glam::Vec3; + + let (local_min, local_max) = { + let registry = ASSET_REGISTRY.read(); + let bounds = registry.get_model(mesh.model()).and_then(|model| { + let mut lo = Vec3::splat(f32::INFINITY); + let mut hi = Vec3::splat(f32::NEG_INFINITY); + for m in &model.meshes { + for v in &m.vertices { + let p = Vec3::from(v.position); + lo = lo.min(p); + hi = hi.max(p); + } + } + if lo.x <= hi.x { Some((lo, hi)) } else { None } + }); + bounds.unwrap_or((Vec3::splat(-0.5), Vec3::splat(0.5))) + }; + + let s = mesh.import_scale(); + let local_min = local_min * s; + let local_max = local_max * s; + + let corners = [ + Vec3::new(local_min.x, local_min.y, local_min.z), + Vec3::new(local_max.x, local_min.y, local_min.z), + Vec3::new(local_min.x, local_max.y, local_min.z), + Vec3::new(local_max.x, local_max.y, local_min.z), + Vec3::new(local_min.x, local_min.y, local_max.z), + Vec3::new(local_max.x, local_min.y, local_max.z), + Vec3::new(local_min.x, local_max.y, local_max.z), + Vec3::new(local_max.x, local_max.y, local_max.z), + ]; + + let mut world_min = Vec3::splat(f32::INFINITY); + let mut world_max = Vec3::splat(f32::NEG_INFINITY); + for &corner in &corners { + let wc = world_mat.transform_point3(corner); + world_min = world_min.min(wc); + world_max = world_max.max(wc); + } + (world_min, world_max) +} + + +pub struct ViewportDock; + +impl EditorTabDock for ViewportDock { + fn desc() -> EditorTabDockDescriptor { + EditorTabDockDescriptor { + id: "viewport", + title: "Viewport".to_string(), + visibility: EditorTabVisibility::GameEditor, + } + } + + fn display(viewer: &mut EditorTabViewer<'_>, ui: &mut egui::Ui) { + viewer.viewport_tab(ui); + } +} @@ -1,308 +0,0 @@ -use egui_ltreeview::{NodeBuilder, TreeViewBuilder}; -use eucalyptus_core::{ - component::ComponentRegistry, - hierarchy::{Children, Hierarchy, Parent}, - physics::{collider::ColliderGroup, rigidbody::RigidBody}, - states::{Label, PROJECT}, -}; -use hecs::{Entity, World}; -use std::collections::{BTreeMap, HashMap, VecDeque}; - -use crate::editor::{Editor, EditorTabDock, EditorTabDockDescriptor, EditorTabViewer, Signal, StaticallyKept, TABS_GLOBAL}; -use crate::editor::page::EditorTabVisibility; - -impl<'a> EditorTabViewer<'a> { - pub(crate) fn entity_list(&mut self, ui: &mut egui::Ui) { - puffin::profile_function!(); - let mut cfg = TABS_GLOBAL.lock(); - - let (_response, action) = { - puffin::profile_scope!("entity_list.tree_build"); - egui_ltreeview::TreeView::new(egui::Id::new( - "model_entity_list", - )) - .show(ui, |builder| { - let current_scene_name = { - PROJECT - .read() - .last_opened_scene - .clone() - .unwrap_or("Scene".to_string()) - }; - builder.node( - // root node/scene display - NodeBuilder::dir(u64::MAX) - .label(format!("Scene: {}", current_scene_name)) - .context_menu(|ui| { - if ui.button("New Empty Entity").clicked() { - let label = Editor::unique_label_for_world(self.world, "Blank Entity"); - self.world.spawn((label,)); - ui.close(); - } - ui.menu_button("Import Template", |_| { - - }); - }), - ); - // the root scene must be the biggest number possible to remove any ambiguity - - fn add_entity_to_tree( - builder: &mut TreeViewBuilder<u64>, - entity: Entity, - world: &mut World, - registry: &ComponentRegistry, - component_ids_by_entity: &HashMap<Entity, Vec<u64>>, - rigidbody_component_id: Option<u64>, - cfg: &mut StaticallyKept, - signal: &mut VecDeque<Signal>, - ) -> anyhow::Result<()> { - puffin::profile_scope!("entity_list.add_entity_to_tree"); - let entity_id = entity.to_bits().get(); - let label = if let Ok(label) = world.query_one::<&Label>(entity).get() - { - label.clone() - } else { - anyhow::bail!( - "This entity [{}] is expected to contain Label", - entity_id - ); - }; - - builder.node( - NodeBuilder::dir(entity_id) - .label(label.as_str()) - .context_menu(|ui| { - ui.menu_button("New", |ui| { - if ui.button("Child").clicked() { - let label = Editor::unique_label_for_world(world, "New Entity"); - let child = world.spawn((label,)); - Hierarchy::set_parent(world, child, entity); - ui.close(); - } - }); - ui.menu_button("Add", |ui| { - let mut grouped_components: BTreeMap<String, Vec<(u64, &eucalyptus_core::component::ComponentDescriptor)>> = - BTreeMap::new(); - - for (id, desc) in registry.iter_available_components() { - let category = desc - .category - .clone() - .unwrap_or_else(|| "Uncategorised".to_string()); - grouped_components - .entry(category) - .or_default() - .push((id, desc)); - } - - for (category, components) in grouped_components.iter_mut() { - components.sort_by(|a, b| a.1.type_name.cmp(&b.1.type_name)); - - ui.menu_button(category, |ui| { - for (id, desc) in components.iter() { - let response = ui.button(desc.type_name.as_str()); - - if let Some(description) = desc.description.as_ref() { - response.clone().on_hover_text(description); - } - - if response.clicked() { - if let Some(component) = - registry.create_default_component(*id) - { - signal.push_back(Signal::AddComponent(entity, component)); - } - ui.close(); - } - } - }); - } - }); - if ui.button("Create Template").clicked() { - eucalyptus_core::fatal!("Not implemented yet"); - } - }), - ); - - if let Some(component_ids) = component_ids_by_entity.get(&entity) { - let display_id = crate::features::is_enabled(crate::features::ShowComponentTypeIDInEditor); - let has_rigidbody = world.get::<&RigidBody>(entity).is_ok(); - let has_collider = world.get::<&ColliderGroup>(entity).is_ok(); - - for component_type_id in component_ids { - let component_node_id = - cfg.component_node_id(entity, *component_type_id); - let display = registry - .get_descriptor_by_numeric_id(*component_type_id) - .map(|desc| { - if display_id { - format!("{} (id #{component_type_id})", desc.type_name) - } else { - desc.type_name.clone() - } - }) - .unwrap_or_else(|| { - if display_id { - format!("Unknown (id #{component_type_id})") - } else { - String::from("Unknown") - } - }); - - let node = NodeBuilder::leaf(component_node_id) - .label_ui(|ui| { - ui.label(display.clone()); - - if has_rigidbody - && !has_collider - && Some(*component_type_id) == rigidbody_component_id - { - ui.add_space(4.0); - ui.small_button("⚠") - .on_hover_text("RigidBody has no colliders! Add the ColliderGroup component"); - } - }) - .context_menu(|ui| { - if ui.button("Remove Component").clicked() { - registry.remove_component_by_id( - world, - entity, - *component_type_id, - ); - ui.close(); - } - }); - - builder.node(node); - } - } - - let children_entities = if let Ok(children) = world.get::<&Children>(entity) { - children.children().to_vec() - } else { - Vec::new() - }; - - for child in children_entities { - if let Err(e) = - add_entity_to_tree( - builder, - child, - world, - registry, - component_ids_by_entity, - rigidbody_component_id, - cfg, - signal, - ) - { - log_once::error_once!( - "Failed to add child entity to tree, skipping: {}", - e - ); - continue; - } - } - - builder.close_dir(); - Ok(()) - } - - let mut component_ids_by_entity: HashMap<Entity, Vec<u64>> = HashMap::new(); - let rigidbody_component_id = self - .component_registry - .iter_available_components() - .find_map(|(id, desc)| { - if desc.fqtn == "eucalyptus_core::physics::rigidbody::RigidBody" { - Some(id) - } else { - None - } - }); - { - puffin::profile_scope!("entity_list.index_components"); - for (component_id, _) in self.component_registry.iter_available_components() { - for entity in self - .component_registry - .find_entities_by_numeric_id(self.world, component_id) - { - component_ids_by_entity - .entry(entity) - .or_default() - .push(component_id); - } - } - } - - let root_entities: Vec<Entity> = self - .world - .query::<Entity>() - .without::<&Parent>() - .iter() - .map(|e| e) - .collect(); - - for entity in root_entities { - puffin::profile_scope!("entity_list.root_entity"); - if let Err(e) = add_entity_to_tree( - builder, - entity, - &mut self.world, - &self.component_registry, - &component_ids_by_entity, - rigidbody_component_id, - &mut cfg, - self.signal, - ) { - log_once::error_once!( - "Failed to add child entity to tree, skipping: {}", - e - ); - } - } - - builder.close_dir(); - }) - }; - - puffin::profile_scope!("entity_list.actions"); - for i in action { - match i { - egui_ltreeview::Action::SetSelected(items) => { - log_once::debug_once!("Selected: {:?}", items); - self.handle_tree_selection(&mut cfg, &items); - } - egui_ltreeview::Action::Move(drag_and_drop) => { - log_once::debug_once!("Moved: {:?}", drag_and_drop); - self.handle_tree_move(&mut cfg, &drag_and_drop); - } - egui_ltreeview::Action::Drag(drag_and_drop) => { - log_once::debug_once!("Dragged: {:?}", drag_and_drop); - self.handle_tree_drag(&mut cfg, &drag_and_drop); - } - egui_ltreeview::Action::Activate(activate) => { - log_once::debug_once!("Activated: {:?}", activate); - self.handle_tree_activate(&mut cfg, &activate); - } - egui_ltreeview::Action::DragExternal(_drag_and_drop_external) => {} - egui_ltreeview::Action::MoveExternal(_drag_and_drop_external) => {} - } - } - } -} - -pub struct EntityListDock; - -impl EditorTabDock for EntityListDock { - fn desc() -> EditorTabDockDescriptor { - EditorTabDockDescriptor { - id: "entity_list", - title: "Model/Entity List".to_string(), - visibility: EditorTabVisibility::GameEditor, - } - } - - fn display(viewer: &mut EditorTabViewer<'_>, ui: &mut egui::Ui) { - viewer.entity_list(ui); - } -} @@ -1,22 +1,17 @@ -pub mod asset_viewer; -pub mod build_console; -pub mod console; pub mod dock; -pub mod entity_list; pub mod input; -pub mod resource; pub mod scene; pub mod settings; -pub mod viewport; pub mod page; pub mod ui; +pub mod docks; pub(crate) use crate::editor::dock::*; use crate::about::AboutWindow; use crate::build::build; use crate::debug; -use crate::editor::console::EucalyptusConsole; +use docks::console::EucalyptusConsole; use crate::editor::settings::editor::{EditorSettingsWindow, EDITOR_SETTINGS}; use crate::editor::settings::project::ProjectSettingsWindow; use crate::plugin::PluginRegistry; @@ -136,6 +131,7 @@ pub struct Editor { pub previously_selected_entity: Option<hecs::Entity>, pub selected_entity: Option<hecs::Entity>, pub viewport_mode: ViewportMode, + pub viewport_drag: Option<crate::editor::dock::DragState>, pub(crate) signal: VecDeque<Signal>, pub(crate) undo_stack: Vec<UndoableAction>, @@ -281,6 +277,7 @@ impl Editor { previously_selected_entity: None, selected_entity: None, viewport_mode: ViewportMode::None, + viewport_drag: None, signal: VecDeque::new(), undo_stack: Vec::new(), // script_manager: ScriptManager::new()?, @@ -1411,6 +1408,7 @@ impl Editor { eucalyptus_console: &mut self.console, current_scene_name: &mut self.current_scene_name, ui_editor: &mut self.ui_editor, + viewport_drag: &mut self.viewport_drag, }, ); @@ -1,139 +0,0 @@ -use hecs::Entity; -use eucalyptus_core::entity_status::EntityStatus; -use crate::editor::{EditorTabDock, EditorTabDockDescriptor, EditorTabViewer, TABS_GLOBAL}; -use dropbear_engine::camera::Camera; -use eucalyptus_core::camera::{CameraComponent, CameraType}; -use crate::editor::page::EditorTabVisibility; - -impl<'a> EditorTabViewer<'a> { - pub(crate) fn resource_inspector(&mut self, ui: &mut egui::Ui) { - let mut cfg = TABS_GLOBAL.lock(); - - let local_scene_settings = cfg.root_node_selected; - - if let Some(entity) = self.selected_entity { - let inspect_entity = *entity; - - if !cfg.root_node_selected { - ui.label(format!("Entity ID: {}", inspect_entity.id())); - ui.separator(); - - // entity status - { - let (mut hidden, mut disabled) = self - .world - .get::<&EntityStatus>(inspect_entity) - .map(|s| (s.hidden, s.disabled)) - .unwrap_or((false, false)); - - let mut status_changed = false; - ui.horizontal(|ui| { - if ui.checkbox(&mut hidden, "Hidden").changed() { - status_changed = true; - } - if ui.checkbox(&mut disabled, "Disabled").changed() { - status_changed = true; - } - }); - - if status_changed { - let has_status = self.world.get::<&EntityStatus>(inspect_entity).is_ok(); - if has_status { - if let Ok(mut s) = self.world.get::<&mut EntityStatus>(inspect_entity) { - s.hidden = hidden; - s.disabled = disabled; - } - } else { - let _ = self.world.insert_one( - inspect_entity, - EntityStatus { hidden, disabled }, - ); - } - } - } - ui.separator(); - - let mut local_unset_comp = false; - if let Ok((_, comp)) = self.world.query_one::<(&Camera, &CameraComponent)>(inspect_entity).get() { - let is_active = self - .active_camera - .lock() - .map_or(false, |active| active == inspect_entity); - ui.horizontal(|ui| { - let label = if is_active { - "Viewing Through This Camera" - } else { - "View Through This Camera" - }; - if ui - .add_enabled(!is_active, egui::Button::new(label)) - .clicked() - { - let mut active_camera = self.active_camera.lock(); - *active_camera = Some(inspect_entity); - } - - let mut is_starting = comp.starting_camera; - let is_starting_label = if comp.camera_type == CameraType::Debug { - is_starting = true; - "Cannot set a Debug camera as starting" - } else if is_starting { - "Already set as starting" - } else { - "Set as starting" - }; - - if ui - .add_enabled(!is_starting, egui::Button::new(is_starting_label)) - .clicked() - { - local_unset_comp = true; - } - }); - ui.separator(); - } - - if local_unset_comp { - for (e, comp) in self.world.query::<(Entity, &mut CameraComponent)>().iter() { - if e == inspect_entity { - comp.starting_camera = true; - continue; - } - log::debug!("Unset starting camera for entity {:?}", e); - comp.starting_camera = false; - } - } - - self.component_registry.inspect_components( - self.world, - inspect_entity, - ui, - self.graphics.clone(), - ); - } - } else if !local_scene_settings { - ui.label("No entity selected, therefore no info to provide. Go on, what are you waiting for? Click an entity!"); - } - - if local_scene_settings { - log_once::debug_once!("Rendering scene settings"); - self.scene_settings(&mut cfg, ui); - } - } -} - -pub struct ResourceInspectorDock; - -impl EditorTabDock for ResourceInspectorDock { - fn desc() -> EditorTabDockDescriptor { - EditorTabDockDescriptor { - id: "inspector", - title: "Resource Inspector".to_string(), - visibility: EditorTabVisibility::GameEditor, - } - } - - fn display(viewer: &mut EditorTabViewer<'_>, ui: &mut egui::Ui) { - viewer.resource_inspector(ui); - } -} @@ -1,250 +0,0 @@ -use crate::editor::{EditorTabDock, EditorTabDockDescriptor, EditorTabViewer, Signal, TABS_GLOBAL, UndoableAction}; -use dropbear_engine::camera::Camera; -use dropbear_engine::entity::{EntityTransform, Transform}; -use dropbear_engine::lighting::Light; -use eucalyptus_core::camera::CameraComponent; -use eucalyptus_core::utils::ViewportMode; -use glam::DVec3; -use transform_gizmo_egui::{GizmoConfig, GizmoExt, GizmoOrientation}; -use eucalyptus_core::hierarchy::EntityTransformExt; -use crate::editor::page::EditorTabVisibility; - -impl<'a> EditorTabViewer<'a> { - pub(crate) fn viewport_tab(&mut self, ui: &mut egui::Ui) { - let mut cfg = TABS_GLOBAL.lock(); - - log_once::debug_once!("Viewport focused"); - - let available_rect = ui.available_rect_before_wrap(); - let available_size = available_rect.size(); - let pixels_per_point = ui.ctx().pixels_per_point(); - - let desired_width = (available_size.x * pixels_per_point).max(1.0).round() as u32; - let desired_height = (available_size.y * pixels_per_point).max(1.0).round() as u32; - if self.tex_size.width != desired_width || self.tex_size.height != desired_height { - if self.signal.is_empty() { - self.signal.push_back(Signal::UpdateViewportSize((desired_width as f32, desired_height as f32))); - } - } - - 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; - let width = height * tex_aspect; - (width, height) - } else { - let width = available_size.x; - 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 snapping = ui.input(|input| input.modifiers.shift); - - let active_cam = self.active_camera.lock(); - if let Some(active_camera) = *active_cam { - let camera_data = { - if let Ok((cam, _comp)) = self - .world - .query_one::<(&Camera, &CameraComponent)>(active_camera) - .get() - { - Some(cam.clone()) - } else { - log::warn!("Queried camera but found no value"); - None - } - }; - - if let Some(camera) = camera_data { - self.gizmo.update_config(GizmoConfig { - view_matrix: camera.view_mat.into(), - projection_matrix: camera.proj_mat.into(), - viewport: image_rect, - modes: *self.gizmo_mode, - orientation: *self.gizmo_orientation, - snapping, - snap_distance: 1.0, - ..Default::default() - }); - } - } - - if !matches!(self.viewport_mode, ViewportMode::None) - && let Some(entity_id) = self.selected_entity - { - let mut handled = false; - let mut updated_light_transform: Option<Transform> = None; - - if let Ok(mut entity_transform) = self - .world - .get::<&mut EntityTransform>(*entity_id) - { - let was_focused = cfg.is_focused; - cfg.is_focused = self.gizmo.is_focused(); - - if cfg.is_focused && !was_focused { - cfg.entity_transform_original = Some(*entity_transform); - } - - let synced = entity_transform.propagate(&self.world, *entity_id); - let gizmo_transform = - transform_gizmo_egui::math::Transform::from_scale_rotation_translation( - synced.scale, - synced.rotation, - synced.position, - ); - - if let Some((_result, new_transforms)) = self.gizmo.interact(ui, &[gizmo_transform]) - && let Some(new_transform) = new_transforms.first() - { - let new_synced_pos: glam::DVec3 = new_transform.translation.into(); - let new_synced_rot: glam::DQuat = new_transform.rotation.into(); - let new_synced_scale: glam::DVec3 = new_transform.scale.into(); - - let prev_world_pos = synced.position; - let prev_world_rot = synced.rotation; - let prev_world_scale = synced.scale; - - let safe = |v: f64| if v.abs() < 1e-9 { 1.0 } else { v }; - - let delta_pos = new_synced_pos - prev_world_pos; - let delta_rot = new_synced_rot * prev_world_rot.inverse(); - let delta_scale = glam::DVec3::new( - new_synced_scale.x / safe(prev_world_scale.x), - new_synced_scale.y / safe(prev_world_scale.y), - new_synced_scale.z / safe(prev_world_scale.z), - ); - - match *self.gizmo_orientation { - GizmoOrientation::Global => { - let world = entity_transform.world_mut(); - world.position += delta_pos; - world.rotation = delta_rot * world.rotation; - world.scale *= delta_scale; - } - GizmoOrientation::Local => { - let world_rot = entity_transform.world().rotation; - let world_scale = entity_transform.world().scale; - - let safe_ws = glam::DVec3::new( - safe(world_scale.x), - safe(world_scale.y), - safe(world_scale.z), - ); - - let local = entity_transform.local_mut(); - - local.position += world_rot.inverse() * delta_pos / safe_ws; - - local.rotation = world_rot.inverse() * delta_rot * world_rot * local.rotation; - - local.scale *= delta_scale; - } - } - - updated_light_transform = Some(entity_transform.sync()); - } - - if was_focused && !cfg.is_focused { - if let Some(original) = cfg.entity_transform_original { - if original != *entity_transform { - self.undo_stack.push(UndoableAction::EntityTransform(*entity_id, original)); - log::debug!("Pushed entity transform action to stack"); - } - } - } - - handled = true; - } - - if !handled { - if let Ok(transform) = self.world.query_one::<&mut Transform>(*entity_id).get() { - 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]) - && 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(); - updated_light_transform = Some(*transform); - } - - 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 { - self.undo_stack.push( - UndoableAction::Transform(*entity_id, cfg.old_pos) - ); - log::debug!("Pushed transform action to stack"); - } - } - } - } - - if let Some(updated_transform) = updated_light_transform { - if let Ok(mut light) = self.world.get::<&mut Light>(*entity_id) { - let forward = DVec3::new(0.0, -1.0, 0.0); - light.component.position = updated_transform.position; - light.component.direction = - (updated_transform.rotation * forward).normalize_or_zero(); - } - } - } - } -} - -pub struct ViewportDock; - -impl EditorTabDock for ViewportDock { - fn desc() -> EditorTabDockDescriptor { - EditorTabDockDescriptor { - id: "viewport", - title: "Viewport".to_string(), - visibility: EditorTabVisibility::GameEditor, - } - } - - fn display(viewer: &mut EditorTabViewer<'_>, ui: &mut egui::Ui) { - viewer.viewport_tab(ui); - } -} @@ -10,10 +10,16 @@ pub mod signal; pub mod spawn; pub mod stats; pub mod utils; + +use editor::docks::asset_viewer::AssetViewerDock; +use editor::docks::build_console::BuildConsoleDock; +use editor::docks::entity_list::EntityListDock; +use editor::docks::resource::ResourceInspectorDock; +use editor::docks::viewport::ViewportDock; pub use redback_runtime as runtime; use crate::editor::{ - EditorTabRegistry, asset_viewer::AssetViewerDock, build_console::BuildConsoleDock, dock::ConsoleDock, entity_list::EntityListDock, resource::ResourceInspectorDock, ui::viewport::UICanvas, viewport::ViewportDock + dock::ConsoleDock, ui::viewport::UICanvas, EditorTabRegistry }; use crate::editor::ui::inspector::UIInspector; use crate::editor::ui::widget_tree::UIWidgetTree; @@ -1,5 +1,6 @@ package com.dropbear +import com.dropbear.asset.AssetEntry import com.dropbear.asset.AssetType import com.dropbear.asset.Handle import com.dropbear.ffi.NativeEngine @@ -43,6 +44,10 @@ class DropbearEngine(val native: NativeEngine) { return entityRef } + fun getAssetEntry(displayName: String): AssetEntry { + + } + /** * Fetches the asset information from the internal AssetRegistry (located in * `dropbear_engine::asset::AssetRegistry`). @@ -50,6 +55,7 @@ class DropbearEngine(val native: NativeEngine) { * ## Warning * The eucalyptus asset URI (or `euca://`) is case-sensitive. */ + // idk if this still works... fun <T : AssetType> getAsset(eucaURI: String): Handle<T>? { val id = com.dropbear.getAsset(eucaURI) if (id == null || id <= 0L) return null @@ -0,0 +1,13 @@ +package com.dropbear.asset + +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid + +class AssetEntry @OptIn(ExperimentalUuidApi::class) constructor( + val uuid: Uuid, + val name: String, + val assetType: AssetKind, + val location: ResourceReference, + val dependencies: List<Uuid> +) { +} @@ -1,6 +1,10 @@ package com.dropbear.asset enum class AssetKind { + Mesh, Texture, - Model + Audio, + Material, + Scene, + Script, } @@ -0,0 +1,36 @@ +package com.dropbear.asset + +/** + * A resolved pointer to asset data, handed to the loader. + * + * You should never construct this by hand with a raw string — + * always derive it from an `AssetEntry` via `AssetEntry::to_reference()`, or from a `PackedAssetEntry` + * via the pak reader. + * + * This is the *loading strategy*, not the identity. + * + * Identity is always the [kotlin.uuid.Uuid] on [AssetEntry]. +*/ +sealed class ResourceReference { + /** + * A file within the project's `resources/` directory. + */ + data class File(val path: String) : ResourceReference() + + /** + * Bytes compiled into the eucalyptus-editor binary via `include_bytes!` (rust macro). + */ + data class Embedded(val bytes: ByteArray): ResourceReference() + + data class Packed( + val offset: ULong, + val length: ULong + ): ResourceReference() + + /** + * No backing data; generated entirely at runtime. + * + * @param foo Does nothing. It's `null` by default to make the kotlin compiler happy. + */ + data class Procedural(val foo: Any? = null): ResourceReference() +} @@ -3,7 +3,14 @@ package com.dropbear.scene /** * A manager for dealing with scene loading and scene querying. */ -class SceneManager() { +class SceneManager { + /** + * Returns the metadata of a scene. + */ + fun getSceneMetadata(sceneName: String): SceneMetadata? { + return getSceneMetadataNative(sceneName) + } + /** * Loads the resources of a scene asynchronously. * @@ -42,4 +49,5 @@ class SceneManager() { internal expect fun SceneManager.loadSceneAsyncNative(sceneName: String): SceneLoadHandle? internal expect fun SceneManager.loadSceneAsyncNative(sceneName: String, loadingScene: String): SceneLoadHandle? -internal expect fun SceneManager.switchToSceneImmediateNative(sceneName: String) +internal expect fun SceneManager.switchToSceneImmediateNative(sceneName: String) +internal expect fun SceneManager.getSceneMetadataNative(sceneName: String): SceneMetadata? @@ -1,10 +1,18 @@ package com.dropbear.scene +import com.dropbear.EntityRef + /** * A class that describes a `eucalyptus_core::scene::SceneConfig` and other information * related to that specific scene. - * - * Currently not used at this moment, will probably be implemented. */ -@Deprecated(message = "It is not implemented yet", level = DeprecationLevel.WARNING) -class SceneMetadata(val id: Long) +class SceneMetadata( + val id: Long, + val name: String, + val settings: SceneSettings +) { + val entities: List<EntityRef> + get() = getEntities() +} + +expect fun SceneMetadata.getEntities(): List<EntityRef> @@ -0,0 +1,48 @@ +package com.dropbear.scene + +class SceneSettings { + /** + * Returns whether the scene's assets has been preloaded. + */ + // must be val, cannot be changed (how could it possibly do anything if changed) + val preloaded: Boolean + get() = getPreload() + + /** + * Shows hitboxes/wireframe for colliders for that specific scene. + */ + var showHitboxes: Boolean + get() = getHitboxState() + set(value) = setHitboxState(value) + + /** + * Overlays the HUD on top of the viewport if it exists as an entity. + */ + var overlayHUD: Boolean + get() = getOverlayHUDState() + set(value) = setOverlayHUDState(value) + + /** + * Overlays all billboard ui for all entities if it exists as a component. + */ + var overlayBillboard: Boolean + get() = getOverlayBillboardState() + set(value) = setOverlayBillboardState(value) + + /** + * Controls the strength of ambient/IBL lighting for this scene. + */ + var ambientStrength: Double + get() = getAmbientStrength() + set(value) = setAmbientStrength(value) +} + +expect fun SceneSettings.getPreload(): Boolean +expect fun SceneSettings.getHitboxState(): Boolean +expect fun SceneSettings.setHitboxState(value: Boolean) +expect fun SceneSettings.getOverlayHUDState(): Boolean +expect fun SceneSettings.setOverlayHUDState(value: Boolean) +expect fun SceneSettings.getOverlayBillboardState(): Boolean +expect fun SceneSettings.setOverlayBillboardState(value: Boolean) +expect fun SceneSettings.getAmbientStrength(): Double +expect fun SceneSettings.setAmbientStrength(value: Double)