tirbofish/dropbear · commit
5734766f3d3179f7c4e327a5466317c40a6851a4
fix: general editor fixes, improvements to components and optimised the model rendering (as well as animations playing).
Signature present but could not be verified.
Unverified
@@ -3,7 +3,7 @@ use std::sync::Arc; use glam::Mat4; use wgpu::util::DeviceExt; use crate::graphics::SharedGraphicsContext; -use crate::model::{Animation, AnimationInterpolation, ChannelValues, Model, NodeTransform}; +use crate::model::{AnimationInterpolation, ChannelValues, Model, NodeTransform}; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct AnimationComponent { @@ -245,6 +245,13 @@ impl AssetRegistry { } pub fn update_model(&mut self, handle: Handle<Model>, model: Model) -> Option<Model> { + if let Some(existing) = self.models.get(&handle.id) { + if existing.label.eq_ignore_ascii_case("light cube") { + log::warn!("Attempted to update protected model '{}'", existing.label); + return None; + } + } + self.models.insert(handle.id, model) } @@ -252,6 +259,10 @@ impl AssetRegistry { self.models.get(&handle.id) } + pub fn get_model_mut(&mut self, handle: Handle<Model>) -> Option<&mut Model> { + self.models.get_mut(&handle.id) + } + pub fn get_model_by_label(&self, label: &str) -> Option<&Model> { self.model_labels .get(label) @@ -7,10 +7,10 @@ use std::{ }; use crate::{ - asset::{ASSET_REGISTRY}, + asset::ASSET_REGISTRY, graphics::{Instance, SharedGraphicsContext}, model::Model, - texture::Texture, + texture::Texture, utils::ResourceReference, }; use egui::{Ui}; use crate::asset::Handle; @@ -292,7 +292,8 @@ impl MeshRenderer { let path = path.as_ref().to_path_buf(); let handle = Model::load_from_memory_raw( graphics.clone(), - std::fs::read(path)?, + std::fs::read(&path)?, + Some(ResourceReference::from_path(&path)?), label, ASSET_REGISTRY.clone(), ).await?; @@ -275,6 +275,7 @@ pub struct Light { pub buffer: UniformBuffer<LightUniform>, pub bind_group: BindGroup, pub instance_buffer: ResizableBuffer<InstanceInput>, + pub component: LightComponent, } impl Light { @@ -364,12 +365,15 @@ impl Light { buffer, bind_group, instance_buffer, + component: light.clone(), } } - pub fn update(&mut self, graphics: &SharedGraphicsContext, light: &LightComponent) { + pub fn update(&mut self, graphics: &SharedGraphicsContext) { puffin::profile_function!(); + let light = &mut self.component; + self.uniform.position = dvec3_to_uniform_array(light.position); self.uniform.direction = @@ -16,7 +16,8 @@ use gltf::texture::MinFilter; use puffin::profile_scope; use wgpu::{BufferAddress, VertexAttribute, VertexBufferLayout, util::DeviceExt, BindGroup}; -#[derive(Clone)] +// do not derive clone otherwise it wil take too much memory +// #[derive(Clone)] pub struct Model { pub hash: u64, // also the id related to the handle pub label: String, @@ -28,7 +29,7 @@ pub struct Model { pub nodes: Vec<Node>, } -#[derive(Clone)] +// #[derive(Clone)] pub struct Mesh { pub name: String, pub vertex_buffer: wgpu::Buffer, @@ -43,6 +44,9 @@ pub struct Material { pub name: String, pub diffuse_texture: Texture, pub normal_texture: Texture, + pub emissive_texture: Option<Texture>, + pub metallic_roughness_texture: Option<Texture>, + pub occlusion_texture: Option<Texture>, pub bind_group: wgpu::BindGroup, pub tint: [f32; 4], pub emissive_factor: [f32; 3], @@ -57,9 +61,6 @@ pub struct Material { pub tint_buffer: UniformBuffer<MaterialUniform>, pub texture_tag: Option<String>, pub wrap_mode: TextureWrapMode, - pub emissive_texture: Option<Texture>, - pub metallic_roughness_texture: Option<Texture>, - pub occlusion_texture: Option<Texture>, } #[derive(Clone, Copy, Eq, PartialEq, Debug, Serialize, Deserialize, Default)] @@ -283,6 +284,7 @@ struct GLTFTextureInformation { impl GLTFTextureInformation { fn fetch(tex: &gltf::Texture<'_>, images: &Vec<gltf::image::Data>) -> GLTFTextureInformation { + puffin::profile_function!(); let sampler = tex.sampler(); let mag_filter = match sampler.mag_filter() { @@ -460,11 +462,6 @@ impl Model { .and_then(|info| process_texture(info.texture())); let emissive_factor = material.emissive_factor(); - let emissive_factor = [ - emissive_factor[0], - emissive_factor[1], - emissive_factor[2], - ]; let metallic_factor = pbr.metallic_factor(); let roughness_factor = pbr.roughness_factor(); let alpha_mode = material.alpha_mode(); @@ -829,6 +826,7 @@ impl Model { pub async fn load_from_memory_raw<B>( graphics: Arc<SharedGraphicsContext>, buffer: B, + optional_resref: Option<ResourceReference>, label: Option<&str>, registry: Arc<RwLock<AssetRegistry>>, ) -> anyhow::Result<Handle<Model>> @@ -885,9 +883,9 @@ impl Model { let extract = |info: Option<GLTFTextureInformation>| -> (Option<(Vec<u8>, (u32, u32))>, Option<wgpu::SamplerDescriptor<'static>>) { if let Some(info) = info { - (Some((info.pixels, (info.width, info.height))), Some(info.sampler)) + (Some((info.pixels, (info.width, info.height))), Some(info.sampler)) } else { - (None, None) + (None, None) } }; @@ -1094,12 +1092,35 @@ impl Model { }); } + if let Some(resref) = optional_resref.clone() { + for material in &mut materials { + material.diffuse_texture.reference = Some(resref.clone()); + material.normal_texture.reference = Some(resref.clone()); + + if let Some(texture) = material.emissive_texture.as_mut() { + texture.reference = Some(resref.clone()); + } + + if let Some(texture) = material.metallic_roughness_texture.as_mut() { + texture.reference = Some(resref.clone()); + } + + if let Some(texture) = material.occlusion_texture.as_mut() { + texture.reference = Some(resref.clone()); + } + } + } + log::debug!("Successfully loaded model [{:?}]", label); + let model_path = optional_resref + .clone() + .unwrap_or_else(|| ResourceReference::from_bytes(buffer.as_ref())); + let model = Model { label: model_label.to_string(), hash, - path: ResourceReference::from_bytes(buffer.as_ref()), + path: model_path, meshes: gpu_meshes, materials, skins, @@ -6,7 +6,7 @@ use wgpu::{BufferAddress, CompareFunction, DepthBiasState, StencilState}; use crate::buffer::{StorageBuffer}; use crate::entity::{EntityTransform, Transform}; use crate::graphics::SharedGraphicsContext; -use crate::lighting::{Light, LightArrayUniform, LightComponent, MAX_LIGHTS}; +use crate::lighting::{Light, LightArrayUniform, MAX_LIGHTS}; use crate::model::{ModelVertex, Vertex}; use crate::pipelines::DropbearShaderPipeline; use crate::shader::Shader; @@ -135,22 +135,24 @@ impl LightCubePipeline { let mut light_index: usize = 0; - for (light_component, s_trans, e_trans, light) in world - .query::<(&LightComponent, Option<&Transform>, Option<&EntityTransform>, &mut Light)>() + for (s_trans, e_trans, light) in world + .query::<(Option<&Transform>, Option<&EntityTransform>, &mut Light)>() .iter() { + light.update(graphics.as_ref()); + let instance: InstanceInput = if let Some(transform) = e_trans { let sync_transform = transform.sync(); sync_transform.matrix().into() } else if let Some(transform) = s_trans { transform.matrix().into() } else { - light_component.to_transform().matrix().into() + panic!("No Transform or EntityTransform available for this light cube"); }; light.instance_buffer.write(&graphics.device, &graphics.queue, &[instance]); - if light_component.enabled && light_index < MAX_LIGHTS { + if light.component.enabled && light_index < MAX_LIGHTS { let uniform = *light.uniform(); light.buffer.write(&graphics.queue, &uniform); @@ -34,8 +34,6 @@ struct MaterialUniform { uv_tiling: vec2<f32>, } -const c_max_lights: u32 = 10u; - @group(0) @binding(0) var t_diffuse: texture_2d<f32>; @group(0) @binding(1) @@ -257,7 +255,7 @@ fn s_fs_main(in: VertexOutput) -> @location(0) vec4<f32> { let ambient = vec3<f32>(1.0) * u_globals.ambient_strength * base_colour.xyz; var final_color = ambient; - for(var i = 0u; i < min(u_globals.num_lights, c_max_lights); i += 1u) { + for(var i = 0u; i < u_globals.num_lights; i += 1u) { let light = s_light_array[i]; let light_type = i32(light.color.w + 0.1); @@ -303,6 +303,10 @@ impl Texture { label: Option<&str>, ) -> Self { puffin::profile_function!(label.unwrap_or("")); + if let Some(l) = label { + log::debug!("Loading texture: {l}"); + } + let hash = AssetRegistry::hash_bytes(bytes); let (diffuse_rgba, dimensions) = { @@ -146,14 +146,12 @@ impl ResourceReference { /// Creates a new `ResourceReference` from bytes pub fn from_bytes(bytes: impl AsRef<[u8]>) -> Self { - puffin::profile_function!(); Self { ref_type: ResourceReferenceType::Bytes(bytes.as_ref().to_vec()), } } pub fn from_reference(ref_type: ResourceReferenceType) -> Self { - puffin::profile_function!(format!("{:?}", ref_type)); match ref_type { ResourceReferenceType::File(reference) => { let canonical = canonicalize_euca_uri(&reference) @@ -168,7 +166,6 @@ impl ResourceReference { /// Creates a [`ResourceReference`] directly from an euca URI (e.g. `euca://models/cube.glb`). pub fn from_euca_uri(uri: impl AsRef<str>) -> anyhow::Result<Self> { - puffin::profile_function!(uri.as_ref()); let canonical = canonicalize_euca_uri(uri.as_ref())?; Ok(Self { ref_type: ResourceReferenceType::File(canonical), @@ -485,11 +485,12 @@ impl Component for MeshRenderer { } ResourceReferenceType::File(reference) => { log::debug!("Loading model from file: {:?}", ser.handle); - let path = ser.handle.resolve()?; + let path = ser.handle.clone().resolve()?; let buffer = std::fs::read(&path)?; Model::load_from_memory_raw( graphics.clone(), buffer, + Some(ser.handle.clone()), Some(reference), ASSET_REGISTRY.clone(), ) @@ -500,6 +501,7 @@ impl Component for MeshRenderer { Model::load_from_memory_raw( graphics.clone(), bytes, + Some(ser.handle.clone()), None, ASSET_REGISTRY.clone(), ) @@ -598,7 +600,6 @@ impl Component for MeshRenderer { let mut texture_override: HashMap<String, SerializedMaterialCustomisation> = HashMap::new(); for (label, mat) in &self.material_snapshot { - // Save texture references directly from the material snapshot let diffuse_texture = mat.diffuse_texture.reference.clone(); let normal_texture = mat.normal_texture.reference.clone(); let emissive_texture = mat.emissive_texture.as_ref().and_then(|t| t.reference.clone()); @@ -748,9 +749,8 @@ impl InspectableComponent for MeshRenderer { CollapsingHeader::new("Mesh Renderer").show(ui, |ui| { let registry = ASSET_REGISTRY.read(); - let current_model = registry.get_model(self.model()).cloned(); + let current_model = registry.get_model(self.model()); let model_list = registry.list_models(); - drop(registry); let model_reference = current_model .as_ref() @@ -866,6 +866,9 @@ impl InspectableComponent for MeshRenderer { if handle.is_null() { continue; } + if label.eq_ignore_ascii_case("light cube") { + continue; + } let is_selected = self.model() == *handle; if ui.selectable_label(is_selected, label).clicked() { selected_model = Some(*handle); @@ -887,6 +890,14 @@ impl InspectableComponent for MeshRenderer { if let Some(handle) = ASSET_REGISTRY.read().get_model_handle_by_reference(&reference) { + if let Some(model) = ASSET_REGISTRY.read().get_model(handle) { + if model.label.eq_ignore_ascii_case("light cube") { + ui.ctx().data_mut(|d| { + d.insert_temp(drag_id, None::<ResourceReference>) + }); + return; + } + } self.set_model(handle); self.reset_texture_override(); } @@ -1,14 +1,13 @@ use std::sync::Arc; -use egui::{CollapsingHeader, DragValue, Ui}; +use egui::{CollapsingHeader, ComboBox, DragValue, Ui}; use crate::ptr::WorldPtr; use crate::scripting::jni::utils::{FromJObject, ToJObject}; use crate::scripting::native::DropbearNativeError; use crate::scripting::result::DropbearNativeResult; use crate::types::NVector3; -use dropbear_engine::asset::ASSET_REGISTRY; use dropbear_engine::entity::{EntityTransform, Transform}; -use dropbear_engine::lighting::{Light, LightComponent, LightType}; -use glam::{DQuat, DVec3}; +use dropbear_engine::lighting::{Light, LightType}; +use glam::{DQuat, DVec3, Vec3}; use hecs::{Entity, World}; use jni::objects::{JObject, JValue}; use jni::JNIEnv; @@ -21,7 +20,7 @@ impl SerializedComponent for SerializedLight {} impl Component for Light { type SerializedForm = SerializedLight; - type RequiredComponentTypes = (Self, LightComponent, Transform); + type RequiredComponentTypes = (Self, Transform); fn descriptor() -> ComponentDescriptor { ComponentDescriptor { @@ -45,124 +44,134 @@ impl Component for Light { ).await; let transform = light_component.to_transform(); - Ok((light, light_component, transform)) + Ok((light, transform)) }) } fn update_component(&mut self, world: &World, _physics: &mut crate::physics::PhysicsState, entity: Entity, _dt: f32, graphics: Arc<SharedGraphicsContext>) { - if let Ok(comp) = world.query_one::<&LightComponent>(entity).get() { - let mut synced = comp.clone(); - if let Ok(entity_transform) = world.query_one::<&EntityTransform>(entity).get() { - let transform = entity_transform.sync(); - synced.position = transform.position; - synced.direction = (transform.rotation * DVec3::new(0.0, 0.0, -1.0)).normalize_or_zero(); - } else if let Ok(transform) = world.query_one::<&Transform>(entity).get() { - synced.position = transform.position; - synced.direction = (transform.rotation * DVec3::new(0.0, 0.0, -1.0)).normalize_or_zero(); - } - - self.update(&graphics, &synced); + let synced = &mut self.component; + if let Ok(entity_transform) = world.query_one::<&EntityTransform>(entity).get() { + let transform = entity_transform.sync(); + synced.position = transform.position; + synced.direction = (transform.rotation * DVec3::new(0.0, 0.0, -1.0)).normalize_or_zero(); + } else if let Ok(transform) = world.query_one::<&Transform>(entity).get() { + synced.position = transform.position; + synced.direction = (transform.rotation * DVec3::new(0.0, 0.0, -1.0)).normalize_or_zero(); } + + self.update(&graphics); } - fn save(&self, world: &World, entity: Entity) -> Box<dyn SerializedComponent> { - if let Ok(comp) = world.query_one::<&LightComponent>(entity).get() { - Box::new(SerializedLight { - label: self.label.clone(), - light_component: comp.clone(), - enabled: comp.enabled, - entity_id: Some(entity), - }) - } else { - Box::new(SerializedLight::default()) - } + fn save(&self, _: &World, entity: Entity) -> Box<dyn SerializedComponent> { + Box::new(SerializedLight { + label: self.label.clone(), + light_component: self.component.clone(), + entity_id: Some(entity), + }) } } impl InspectableComponent for Light { fn inspect(&mut self, ui: &mut Ui, _graphics: Arc<SharedGraphicsContext>) { CollapsingHeader::new("Light").default_open(true).show(ui, |ui| { - ui.horizontal(|ui| { - ui.label("Label"); - ui.text_edit_singleline(&mut self.label); - }); - - let registry = ASSET_REGISTRY.read(); - let current_label = registry - .get_label_from_model_handle(self.cube_model) - .unwrap_or_else(|| "(unlabeled)".to_string()); - - ui.horizontal(|ui| { - ui.label("Cube Model"); - ui.label(current_label.as_str()); - }); - - let label_id = ui.make_persistent_id("light_cube_model_label"); - let mut model_label = ui - .data_mut(|d| d.get_temp::<String>(label_id)) - .unwrap_or_else(|| current_label.clone()); - - let mut invalid_label = false; - ui.horizontal(|ui| { - ui.label("Model Label"); - let response = ui.text_edit_singleline(&mut model_label); - if response.changed() { - ui.data_mut(|d| d.insert_temp(label_id, model_label.clone())); - } + ui.add_space(6.0); + ui.label("Uniform"); - if ui.button("Apply").clicked() { - if let Some(handle) = registry.get_model_handle_from_label(&model_label) { - self.cube_model = handle; - ui.data_mut(|d| d.insert_temp(label_id, model_label.clone())); - } else { - invalid_label = true; - } - } + ui.label("Light Type"); + ComboBox::from_id_salt("Light Type").show_ui(ui, |ui| { + ui.selectable_value(&mut self.component.light_type, LightType::Directional, "Directional"); + ui.selectable_value(&mut self.component.light_type, LightType::Point, "Point"); + ui.selectable_value(&mut self.component.light_type, LightType::Spot, "Spot"); }); - if invalid_label { - ui.label("Unknown model label"); + let mut display_pos = |yueye: &mut Ui| { + yueye.horizontal(|yueye| { + yueye.label("Position"); + yueye.add(DragValue::new(&mut self.component.position.x).speed(0.01)); + yueye.add(DragValue::new(&mut self.component.position.y).speed(0.01)); + yueye.add(DragValue::new(&mut self.component.position.z).speed(0.01)); + }); + }; + + let mut display_dir = |yueye: &mut Ui| { + yueye.horizontal(|yueye| { + yueye.label("Direction"); + yueye.add(DragValue::new(&mut self.component.direction.x).speed(0.01)); + yueye.add(DragValue::new(&mut self.component.direction.y).speed(0.01)); + yueye.add(DragValue::new(&mut self.component.direction.z).speed(0.01)); + }); + }; + + match self.component.light_type { + LightType::Directional => { + display_dir(ui); + }, + LightType::Point => { + display_pos(ui); + }, + LightType::Spot => { + display_pos(ui); + display_dir(ui); + }, } - ui.add_space(6.0); - ui.label("Uniform"); + let mut colour_rgb = [ + self.component.colour[0] as f32, + self.component.colour[1] as f32, + self.component.colour[2] as f32, + ]; + egui::color_picker::color_edit_button_rgb(ui, &mut colour_rgb); + self.component.colour = Vec3::from_array(colour_rgb).as_dvec3(); ui.horizontal(|ui| { - ui.label("Position"); - ui.add(DragValue::new(&mut self.uniform.position[0]).speed(0.01)); - ui.add(DragValue::new(&mut self.uniform.position[1]).speed(0.01)); - ui.add(DragValue::new(&mut self.uniform.position[2]).speed(0.01)); + ui.label("Intensity"); + ui.add(DragValue::new(&mut self.component.intensity).speed(0.05)); }); + if matches!(self.component.light_type, LightType::Point | LightType::Spot) { + ui.horizontal(|ui| { + ui.label("Attenuation"); + ui.add(DragValue::new(&mut self.component.attenuation.constant).speed(0.01)); + ui.add(DragValue::new(&mut self.component.attenuation.linear).speed(0.01)); + ui.add(DragValue::new(&mut self.component.attenuation.quadratic).speed(0.01)); + }); + } + ui.horizontal(|ui| { - ui.label("Direction"); - ui.add(DragValue::new(&mut self.uniform.direction[0]).speed(0.01)); - ui.add(DragValue::new(&mut self.uniform.direction[1]).speed(0.01)); - ui.add(DragValue::new(&mut self.uniform.direction[2]).speed(0.01)); + ui.checkbox(&mut self.component.enabled, "Enabled"); + ui.checkbox(&mut self.component.visible, "Visible"); }); - let mut colour_rgb = [ - self.uniform.colour[0], - self.uniform.colour[1], - self.uniform.colour[2], - ]; - if egui::color_picker::color_edit_button_rgb(ui, &mut colour_rgb).changed() { - self.uniform.colour[0] = colour_rgb[0]; - self.uniform.colour[1] = colour_rgb[1]; - self.uniform.colour[2] = colour_rgb[2]; + if matches!(self.component.light_type, LightType::Spot) { + ui.horizontal(|ui| { + ui.label("Cutoff"); + ui.add(DragValue::new(&mut self.component.cutoff_angle).speed(0.1)); + }); + ui.horizontal(|ui| { + ui.label("Outer Cutoff"); + ui.add(DragValue::new(&mut self.component.outer_cutoff_angle).speed(0.1)); + }); + + if self.component.outer_cutoff_angle <= self.component.cutoff_angle { + self.component.outer_cutoff_angle = self.component.cutoff_angle + 1.0; + } } + ui.separator(); + ui.label("Shadows"); + ui.checkbox(&mut self.component.cast_shadows, "Cast Shadows"); ui.horizontal(|ui| { - ui.label("Attenuation"); - ui.add(DragValue::new(&mut self.uniform.constant).speed(0.01)); - ui.add(DragValue::new(&mut self.uniform.linear).speed(0.01)); - ui.add(DragValue::new(&mut self.uniform.quadratic).speed(0.01)); + ui.label("Depth"); + ui.add(DragValue::new(&mut self.component.depth.start).speed(0.1)); + ui.label(".."); + ui.add(DragValue::new(&mut self.component.depth.end).speed(0.1)); }); - ui.horizontal(|ui| { - ui.label("Cutoff"); - ui.add(DragValue::new(&mut self.uniform.cutoff).speed(0.01)); - }); + if self.component.depth.end < self.component.depth.start { + self.component.depth.end = self.component.depth.start; + } + + }); } } @@ -505,9 +514,9 @@ fn get_colour( entity: Entity, ) -> DropbearNativeResult<NColour> { let light = world - .get::<&LightComponent>(entity) + .get::<&Light>(entity) .map_err(|_| DropbearNativeError::MissingComponent)?; - Ok(NColour::from_linear_rgb(light.colour)) + Ok(NColour::from_linear_rgb(light.component.colour)) } #[dropbear_macro::export( @@ -522,9 +531,9 @@ fn set_colour( colour: &NColour, ) -> DropbearNativeResult<()> { let mut light = world - .get::<&mut LightComponent>(entity) + .get::<&mut Light>(entity) .map_err(|_| DropbearNativeError::MissingComponent)?; - light.colour = (*colour).to_linear_rgb(); + light.component.colour = (*colour).to_linear_rgb(); Ok(()) } @@ -539,9 +548,9 @@ fn get_light_type( entity: Entity, ) -> DropbearNativeResult<i32> { let light = world - .get::<&LightComponent>(entity) + .get::<&Light>(entity) .map_err(|_| DropbearNativeError::MissingComponent)?; - Ok(light.light_type as i32) + Ok(light.component.light_type as i32) } #[dropbear_macro::export( @@ -556,10 +565,10 @@ fn set_light_type( light_type: i32, ) -> DropbearNativeResult<()> { let mut light = world - .get::<&mut LightComponent>(entity) + .get::<&mut Light>(entity) .map_err(|_| DropbearNativeError::MissingComponent)?; - light.light_type = match light_type { + light.component.light_type = match light_type { 0 => LightType::Directional, 1 => LightType::Point, 2 => LightType::Spot, @@ -580,9 +589,9 @@ fn get_intensity( entity: Entity, ) -> DropbearNativeResult<f64> { let light = world - .get::<&LightComponent>(entity) + .get::<&Light>(entity) .map_err(|_| DropbearNativeError::MissingComponent)?; - Ok(light.intensity as f64) + Ok(light.component.intensity as f64) } #[dropbear_macro::export( @@ -597,9 +606,9 @@ fn set_intensity( intensity: f64, ) -> DropbearNativeResult<()> { let mut light = world - .get::<&mut LightComponent>(entity) + .get::<&mut Light>(entity) .map_err(|_| DropbearNativeError::MissingComponent)?; - light.intensity = intensity as f32; + light.component.intensity = intensity as f32; Ok(()) } @@ -614,13 +623,13 @@ fn get_attenuation( entity: Entity, ) -> DropbearNativeResult<NAttenuation> { let light = world - .get::<&LightComponent>(entity) + .get::<&Light>(entity) .map_err(|_| DropbearNativeError::MissingComponent)?; Ok(NAttenuation { - constant: light.attenuation.constant, - linear: light.attenuation.linear, - quadratic: light.attenuation.quadratic, + constant: light.component.attenuation.constant, + linear: light.component.attenuation.linear, + quadratic: light.component.attenuation.quadratic, }) } @@ -636,12 +645,12 @@ fn set_attenuation( attenuation: &NAttenuation, ) -> DropbearNativeResult<()> { let mut light = world - .get::<&mut LightComponent>(entity) + .get::<&mut Light>(entity) .map_err(|_| DropbearNativeError::MissingComponent)?; - light.attenuation.constant = attenuation.constant; - light.attenuation.linear = attenuation.linear; - light.attenuation.quadratic = attenuation.quadratic; + light.component.attenuation.constant = attenuation.constant; + light.component.attenuation.linear = attenuation.linear; + light.component.attenuation.quadratic = attenuation.quadratic; Ok(()) } @@ -656,9 +665,9 @@ fn get_enabled( entity: Entity, ) -> DropbearNativeResult<bool> { let light = world - .get::<&LightComponent>(entity) + .get::<&Light>(entity) .map_err(|_| DropbearNativeError::MissingComponent)?; - Ok(light.enabled) + Ok(light.component.enabled) } #[dropbear_macro::export( @@ -673,9 +682,9 @@ fn set_enabled( enabled: bool, ) -> DropbearNativeResult<()> { let mut light = world - .get::<&mut LightComponent>(entity) + .get::<&mut Light>(entity) .map_err(|_| DropbearNativeError::MissingComponent)?; - light.enabled = enabled; + light.component.enabled = enabled; Ok(()) } @@ -690,9 +699,9 @@ fn get_cutoff_angle( entity: Entity, ) -> DropbearNativeResult<f64> { let light = world - .get::<&LightComponent>(entity) + .get::<&Light>(entity) .map_err(|_| DropbearNativeError::MissingComponent)?; - Ok(light.cutoff_angle as f64) + Ok(light.component.cutoff_angle as f64) } #[dropbear_macro::export( @@ -707,9 +716,9 @@ fn set_cutoff_angle( cutoff_angle: f64, ) -> DropbearNativeResult<()> { let mut light = world - .get::<&mut LightComponent>(entity) + .get::<&mut Light>(entity) .map_err(|_| DropbearNativeError::MissingComponent)?; - light.cutoff_angle = cutoff_angle as f32; + light.component.cutoff_angle = cutoff_angle as f32; Ok(()) } @@ -724,9 +733,9 @@ fn get_outer_cutoff_angle( entity: Entity, ) -> DropbearNativeResult<f64> { let light = world - .get::<&LightComponent>(entity) + .get::<&Light>(entity) .map_err(|_| DropbearNativeError::MissingComponent)?; - Ok(light.outer_cutoff_angle as f64) + Ok(light.component.outer_cutoff_angle as f64) } #[dropbear_macro::export( @@ -741,9 +750,9 @@ fn set_outer_cutoff_angle( outer_cutoff_angle: f64, ) -> DropbearNativeResult<()> { let mut light = world - .get::<&mut LightComponent>(entity) + .get::<&mut Light>(entity) .map_err(|_| DropbearNativeError::MissingComponent)?; - light.outer_cutoff_angle = outer_cutoff_angle as f32; + light.component.outer_cutoff_angle = outer_cutoff_angle as f32; Ok(()) } @@ -758,9 +767,9 @@ fn get_casts_shadows( entity: Entity, ) -> DropbearNativeResult<bool> { let light = world - .get::<&LightComponent>(entity) + .get::<&Light>(entity) .map_err(|_| DropbearNativeError::MissingComponent)?; - Ok(light.cast_shadows) + Ok(light.component.cast_shadows) } #[dropbear_macro::export( @@ -775,9 +784,9 @@ fn set_casts_shadows( casts_shadows: bool, ) -> DropbearNativeResult<()> { let mut light = world - .get::<&mut LightComponent>(entity) + .get::<&mut Light>(entity) .map_err(|_| DropbearNativeError::MissingComponent)?; - light.cast_shadows = casts_shadows; + light.component.cast_shadows = casts_shadows; Ok(()) } @@ -792,12 +801,12 @@ fn get_depth( entity: Entity, ) -> DropbearNativeResult<NRange> { let light = world - .get::<&LightComponent>(entity) + .get::<&Light>(entity) .map_err(|_| DropbearNativeError::MissingComponent)?; Ok(NRange { - start: light.depth.start, - end: light.depth.end, + start: light.component.depth.start, + end: light.component.depth.end, }) } @@ -820,8 +829,8 @@ fn set_depth( } let mut light = world - .get::<&mut LightComponent>(entity) + .get::<&mut Light>(entity) .map_err(|_| DropbearNativeError::MissingComponent)?; - light.depth = depth.start..depth.end; + light.component.depth = depth.start..depth.end; Ok(()) } @@ -242,26 +242,17 @@ fn set_material_tint( b: f32, a: f32, ) -> DropbearNativeResult<()> { - let renderer = world - .get::<&MeshRenderer>(entity) + let _ = asset; + let mut renderer = world + .get::<&mut MeshRenderer>(entity) .map_err(|_| DropbearNativeError::NoSuchComponent)?; - let handle = renderer.model(); - let mut registry = asset.write(); - let model = registry - .get_model(handle) - .cloned() - .ok_or(DropbearNativeError::AssetNotFound)?; - let mut model = model; - - let index = shared::resolve_target_material_index(&model, &material_name) + let material = renderer + .material_snapshot + .get_mut(&material_name) .ok_or(DropbearNativeError::InvalidArgument)?; - if let Some(material) = model.materials.get_mut(index) { - material.tint = [r, g, b, a]; - material.sync_uniform(graphics); - } - - registry.update_model(handle, model); + material.tint = [r, g, b, a]; + material.sync_uniform(graphics); Ok(()) } @@ -381,7 +381,7 @@ impl SceneConfig { ) -> anyhow::Result<()> { let mut has_light = false; if world - .query::<(&LightComponent, &Light)>() + .query::<&Light>() .iter() .next() .is_some() @@ -400,9 +400,6 @@ impl SceneConfig { }); } let comp = LightComponent::directional(glam::DVec3::ONE, 1.0); - let light_direction = LightComponent::default_direction(); - let rotation = - DQuat::from_rotation_arc(DVec3::new(0.0, 0.0, -1.0), light_direction); let light = Light::new(graphics.clone(), comp.clone(), Some("Default Light")) .await; @@ -410,7 +407,6 @@ impl SceneConfig { let light_config = SerializedLight { label: "Default Light".to_string(), light_component: comp.clone(), - enabled: true, entity_id: None, }; @@ -298,7 +298,6 @@ pub struct Property { pub struct SerializedLight { pub label: String, pub light_component: LightComponent, - pub enabled: bool, #[serde(skip)] pub entity_id: Option<hecs::Entity>, @@ -309,7 +308,6 @@ impl Default for SerializedLight { Self { label: "Default Light".to_string(), light_component: LightComponent::default(), - enabled: true, entity_id: None, } } @@ -279,6 +279,15 @@ impl ResolveReference for ResourceReference { if !project_path.as_os_str().is_empty() { let root = project_path.join("resources"); + if let Some(resolved) = try_resolve_resource_from_root(relative, &root) { + return Ok(resolved); + } + + let fallback = project_path.join(relative); + if fallback.exists() { + return Ok(fallback); + } + return resolve_resource_from_root(relative, &root); } } @@ -293,6 +302,11 @@ impl ResolveReference for ResourceReference { } } +fn try_resolve_resource_from_root(relative: &str, root: &Path) -> Option<PathBuf> { + let resolved = root.join(relative); + resolved.exists().then_some(resolved) +} + fn runtime_resources_dir() -> anyhow::Result<PathBuf> { let current_exe = std::env::current_exe()?; let dir = current_exe @@ -11,7 +11,11 @@ use eucalyptus_core::states::PROJECT; use hecs::Entity; use log::{info, warn}; -use crate::editor::{ComponentNodeSelection, DraggedAsset, EditorTabViewer, FsEntry, StaticallyKept, TABS_GLOBAL}; +use crate::editor::{ + AssetDivision, AssetNodeInfo, AssetNodeKind, ComponentNodeSelection, DraggedAsset, + EditorTabViewer, FsEntry, ResourceDivision, SceneDivision, ScriptDivision, StaticallyKept, + Signal, TABS_GLOBAL, +}; use eucalyptus_core::component::DRAGGED_ASSET_ID; #[derive(Clone, Copy, Debug)] @@ -26,6 +30,41 @@ enum TextureSlot { 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(); @@ -39,8 +78,8 @@ impl<'a> EditorTabViewer<'a> { 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); + self.build_scripts_branch(&mut cfg, builder, &project_root); + self.build_scene_branch(&mut cfg, builder, &project_root); builder.close_dir(); }); @@ -51,6 +90,7 @@ impl<'a> EditorTabViewer<'a> { } 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); @@ -80,12 +120,29 @@ impl<'a> EditorTabViewer<'a> { project_root: &Path, ) { let label = "euca://resources"; - builder.node(Self::dir_node_labeled(label, "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"); + Self::add_placeholder_leaf( + builder, + "euca://resources/missing", + "missing", + AssetNodeKind::Resource(ResourceDivision::File), + ); } builder.close_dir(); } @@ -112,7 +169,19 @@ impl<'a> EditorTabViewer<'a> { for entry in entries { let full_label = Self::resource_label(base_path, &entry.path); if entry.is_dir { - builder.node(Self::dir_node_labeled(&full_label, &entry.name)); + 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 { @@ -133,7 +202,20 @@ impl<'a> EditorTabViewer<'a> { let is_texture = Self::is_texture_file(&entry.name); let entry_name = entry.name.clone(); let reference_for_menu = reference.clone(); - let menu = Self::leaf_node_labeled(&full_label, &entry.name).context_menu(|ui| { + 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 { if ui.button("Load to memory").clicked() { ui.close(); @@ -205,15 +287,33 @@ impl<'a> EditorTabViewer<'a> { } fn build_scripts_branch( - _cfg: &mut StaticallyKept, + &mut self, + cfg: &mut StaticallyKept, builder: &mut TreeViewBuilder<u64>, project_root: &Path, ) { let label = "euca://scripts"; - builder.node(Self::dir_node_labeled(label, "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"); + Self::add_placeholder_leaf( + builder, + "euca://scripts/missing", + "missing", + AssetNodeKind::Script(ScriptDivision::Script), + ); builder.close_dir(); return; } @@ -235,26 +335,58 @@ impl<'a> EditorTabViewer<'a> { for entry in entries { if entry.is_dir { let source_label = format!("{}/{}", label, entry.name); - builder.node(Self::dir_node_labeled(&source_label, &entry.name)); - if Self::build_script_source_set(builder, &entry.path, &source_label) { + 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); - builder.node(Self::leaf_node_labeled(&file_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"); + 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, @@ -271,6 +403,7 @@ impl<'a> EditorTabViewer<'a> { builder, &format!("{source_label}/unreadable"), "unreadable", + AssetNodeKind::Script(ScriptDivision::Script), ); return true; } @@ -280,25 +413,63 @@ impl<'a> EditorTabViewer<'a> { for entry in entries { if entry.is_dir { if entry.name.eq_ignore_ascii_case("kotlin") { - if Self::build_kotlin_tree(builder, &entry.path, source_label) { + if self.build_kotlin_tree(cfg, builder, &entry.path, source_label) { had_content = true; } } else { let child_label = format!("{}/{}", source_label, entry.name); - builder.node(Self::dir_node_labeled(&child_label, &entry.name)); - Self::build_plain_directory(builder, &entry.path, &child_label); + 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); - builder.node(Self::leaf_node_labeled(&file_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"); + Self::add_placeholder_leaf( + builder, + &format!("{source_label}/empty"), + "empty", + AssetNodeKind::Script(ScriptDivision::Script), + ); had_content = true; } @@ -306,9 +477,15 @@ impl<'a> EditorTabViewer<'a> { } 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, @@ -322,29 +499,70 @@ impl<'a> EditorTabViewer<'a> { builder, &format!("{parent_label}/unreadable"), "unreadable", + file_kind, ); return; } }; if entries.is_empty() { - Self::add_placeholder_leaf(builder, &format!("{parent_label}/empty"), "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 { - builder.node(Self::dir_node_labeled(&child_label, &entry.name)); - Self::build_plain_directory(builder, &entry.path, &child_label); + 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 { - builder.node(Self::leaf_node_labeled(&child_label, &entry.name)); + 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, @@ -361,6 +579,7 @@ impl<'a> EditorTabViewer<'a> { builder, &format!("{source_label}/unreadable"), "unreadable", + AssetNodeKind::Script(ScriptDivision::Script), ); return true; } @@ -371,6 +590,7 @@ impl<'a> EditorTabViewer<'a> { builder, &format!("{source_label}/no_kotlin_files"), "no kotlin files", + AssetNodeKind::Script(ScriptDivision::Script), ); return true; } @@ -378,7 +598,8 @@ impl<'a> EditorTabViewer<'a> { let mut had_entries = false; for entry in entries { if entry.is_dir { - Self::build_kotlin_package_collapsed( + self.build_kotlin_package_collapsed( + cfg, builder, &entry.path, source_label, @@ -387,7 +608,19 @@ impl<'a> EditorTabViewer<'a> { had_entries = true; } else { let file_id = format!("{}/{}", source_label, entry.name); - builder.node(Self::leaf_node_labeled(&file_id, &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; } } @@ -396,6 +629,8 @@ impl<'a> EditorTabViewer<'a> { } fn build_kotlin_package_collapsed( + &mut self, + cfg: &mut StaticallyKept, builder: &mut TreeViewBuilder<u64>, dir_path: &Path, parent_path_str: &str, @@ -416,6 +651,7 @@ impl<'a> EditorTabViewer<'a> { builder, &format!("{full_path_str}/unreadable"), "unreadable", + AssetNodeKind::Script(ScriptDivision::Script), ); return; } @@ -428,20 +664,51 @@ impl<'a> EditorTabViewer<'a> { let subdir = subdirs[0]; let mut new_parts = accumulated_parts; new_parts.push(subdir.name.clone()); - Self::build_kotlin_package_collapsed(builder, &subdir.path, parent_path_str, new_parts); + 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); - builder.node(Self::dir_node_labeled(&full_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); - builder.node(Self::leaf_node_labeled(&file_id, &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( + self.build_kotlin_package_collapsed( + cfg, builder, &subdir.path, &full_path_str, @@ -454,15 +721,33 @@ impl<'a> EditorTabViewer<'a> { } fn build_scene_branch( - _cfg: &mut StaticallyKept, + &mut self, + cfg: &mut StaticallyKept, builder: &mut TreeViewBuilder<u64>, project_root: &Path, ) { let label = "euca://scenes"; - builder.node(Self::dir_node_labeled(label, "scenes")); let scenes_root = project_root.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"); + Self::add_placeholder_leaf( + builder, + "euca://scenes/missing", + "missing", + AssetNodeKind::Scene(SceneDivision::Scene), + ); builder.close_dir(); return; } @@ -475,7 +760,12 @@ impl<'a> EditorTabViewer<'a> { scenes_root.display(), err ); - Self::add_placeholder_leaf(builder, "euca://scenes/unreadable", "unreadable"); + Self::add_placeholder_leaf( + builder, + "euca://scenes/unreadable", + "unreadable", + AssetNodeKind::Scene(SceneDivision::Scene), + ); builder.close_dir(); return; } @@ -485,8 +775,29 @@ impl<'a> EditorTabViewer<'a> { for entry in entries { if entry.is_dir { let child_label = format!("{}/{}", label, entry.name); - builder.node(Self::dir_node_labeled(&child_label, &entry.name)); - Self::build_plain_directory(builder, &entry.path, &child_label); + 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 @@ -497,13 +808,30 @@ impl<'a> EditorTabViewer<'a> { .unwrap_or(false) { let file_label = format!("{}/{}", label, entry.name); - builder.node(Self::leaf_node_labeled(&file_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"); + Self::add_placeholder_leaf( + builder, + "euca://scenes/no_scenes", + "no scenes", + AssetNodeKind::Scene(SceneDivision::Scene), + ); } builder.close_dir(); @@ -541,33 +869,320 @@ impl<'a> EditorTabViewer<'a> { 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(NodeBuilder::dir(Self::asset_node_id(label)).label(label.to_string())) + Self::with_icon_kind( + NodeBuilder::dir(Self::asset_node_id(label)).label(label.to_string()), + AssetNodeKind::Resource(ResourceDivision::Folder), + ) } - fn dir_node_labeled<'ui>(id_source: &str, label: &str) -> NodeBuilder<'ui, u64> { - Self::with_icon(NodeBuilder::dir(Self::asset_node_id(id_source)).label(label.to_string())) + 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_labeled<'ui>(id_source: &str, label: &str) -> NodeBuilder<'ui, u64> { - Self::with_icon(NodeBuilder::leaf(Self::asset_node_id(id_source)).label(label.to_string())) + 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<'ui>(builder: NodeBuilder<'ui, u64>) -> NodeBuilder<'ui, u64> { - builder.icon(|ui| { + fn with_icon_kind<'ui>(builder: NodeBuilder<'ui, u64>, kind: AssetNodeKind) -> NodeBuilder<'ui, u64> { + builder.icon(move |ui| { egui_extras::install_image_loaders(ui.ctx()); - Self::draw_asset_icon(ui) + Self::draw_asset_icon(ui, kind) }) } - fn draw_asset_icon(ui: &mut egui::Ui) { + 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) { - builder.node(Self::leaf_node_labeled(id_source, label)); + 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 = 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 = 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()); + } + } + + 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()); + } + } + + 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()); + } } @@ -604,19 +1219,18 @@ impl<'a> EditorTabViewer<'a> { let handle = Model::load_from_memory_raw( graphics.clone(), buffer, + Some(reference.clone()), None, ASSET_REGISTRY.clone(), ) .await?; let mut registry = ASSET_REGISTRY.write(); - if let Some(model) = registry.get_model(handle).cloned() { - let mut model = model; + if let Some(model) = registry.get_model_mut(handle) { model.path = reference.clone(); model.label = label.clone(); - registry.update_model(handle, model); - registry.label_model(label.clone(), handle); } + registry.label_model(label.clone(), handle); Ok::<(), anyhow::Error>(()) }); @@ -926,6 +926,9 @@ impl InspectableComponent for MeshRenderer { if i.path.as_uri().is_none() { continue; } + if i.label.eq_ignore_ascii_case("light cube") { + continue; + } let is_selected = self.asset_handle() == *i.key(); if ui @@ -114,6 +114,8 @@ pub struct StaticallyKept { pub(crate) dragged_asset: Option<DraggedAsset>, pub(crate) asset_node_assets: HashMap<u64, DraggedAsset>, + pub(crate) asset_node_info: HashMap<u64, AssetNodeInfo>, + pub(crate) asset_rename: Option<AssetRenameState>, pub(crate) component_node_ids: HashMap<ComponentNodeKey, u64>, pub(crate) component_node_lookup: HashMap<u64, ComponentNodeKey>, @@ -302,6 +304,56 @@ pub(crate) struct FsEntry { pub(crate) is_dir: bool, } +#[derive(Clone, Debug)] +pub(crate) struct AssetRenameState { + pub(crate) node_id: u64, + pub(crate) original_path: PathBuf, + pub(crate) buffer: String, + pub(crate) just_started: bool, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum AssetDivision { + Resources, + Scripts, + Scenes, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ResourceDivision { + File, + Folder, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ScriptDivision { + Package, + Script, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SceneDivision { + Scene, + Folder, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum AssetNodeKind { + Resource(ResourceDivision), + Script(ScriptDivision), + Scene(SceneDivision), +} + +#[derive(Clone, Debug)] +pub(crate) struct AssetNodeInfo { + pub(crate) path: PathBuf, + pub(crate) division: AssetDivision, + pub(crate) kind: AssetNodeKind, + pub(crate) is_dir: bool, + pub(crate) is_division_root: bool, + pub(crate) allow_add_folder: bool, +} + #[derive(Debug, Clone, Copy)] pub enum EditorTabMenuAction { ImportResource, @@ -84,6 +84,7 @@ pub struct Editor { pub texture_id: Option<egui::TextureId>, pub size: Extent3d, pub instance_buffer_cache: HashMap<u64, ResizableBuffer<InstanceRaw>>, + pub animated_instance_buffer: Option<ResizableBuffer<InstanceRaw>>, pub collider_wireframe_geometry_cache: HashMap<ColliderShapeKey, WireframeGeometry>, pub collider_instance_buffer: Option<ResizableBuffer<ColliderInstanceRaw>>, pub color: Color, @@ -177,6 +178,8 @@ pub struct Editor { pub(crate) play_mode_process: Option<std::process::Child>, pub(crate) play_mode_pid: Option<u32>, pub(crate) play_mode_exit_rx: Option<std::sync::mpsc::Receiver<()>>, + + pub(crate) asset_clipboard: Option<AssetClipboard>, } impl Editor { @@ -275,8 +278,10 @@ impl Editor { play_mode_process: None, play_mode_pid: None, play_mode_exit_rx: None, + asset_clipboard: None, collider_wireframe_pipeline: None, instance_buffer_cache: HashMap::new(), + animated_instance_buffer: None, collider_wireframe_geometry_cache: HashMap::new(), collider_instance_buffer: None, mipmapper: None, @@ -1521,6 +1526,14 @@ pub enum Signal { None, Copy(SceneEntity), Paste(SceneEntity), + AssetCopy { + source: PathBuf, + division: AssetDivision, + }, + AssetPaste { + target_dir: PathBuf, + division: AssetDivision, + }, Delete, Undo, Play, @@ -1531,6 +1544,12 @@ pub enum Signal { UpdateViewportSize((f32, f32)), } +#[derive(Clone, Debug)] +pub struct AssetClipboard { + pub source: PathBuf, + pub division: AssetDivision, +} + #[derive(Debug)] pub enum EditorState { Editing, @@ -12,7 +12,7 @@ use dropbear_engine::asset::{ASSET_REGISTRY, Handle}; use dropbear_engine::graphics::{CommandEncoder, InstanceRaw}; use dropbear_engine::{ entity::{EntityTransform, MeshRenderer, Transform}, - lighting::{Light, LightComponent, MAX_LIGHTS}, + lighting::{Light, MAX_LIGHTS}, model::{DrawLight, DrawModel}, scene::{Scene, SceneCommand}, }; @@ -191,7 +191,6 @@ impl Scene for Editor { self.world.spawn(( label_component, l, - LightComponent::default(), Transform::default(), CustomProperties::default(), )); @@ -346,11 +345,15 @@ impl Scene for Editor { }); } + if let Some(light_pipeline) = &mut self.light_cube_pipeline { + light_pipeline.update(graphics.clone(), &self.world); + } + let lights = { let mut lights = Vec::new(); - let mut query = self.world.query::<(&Light, &LightComponent)>(); - for (light, comp) in query.iter() { - lights.push((light.clone(), comp.clone())); + let mut query = self.world.query::<&Light>(); + for light in query.iter() { + lights.push(light.clone()); } lights }; @@ -358,8 +361,7 @@ impl Scene for Editor { if let Some(globals) = &mut self.shader_globals { let enabled_count = lights .iter() - .filter(|(_, comp)| comp.enabled) - .take(MAX_LIGHTS) + .filter(|light| light.component.enabled) .count() as u32; globals.set_num_lights(enabled_count); globals.write(&graphics.queue); @@ -391,23 +393,27 @@ impl Scene for Editor { let registry = ASSET_REGISTRY.read(); let mut prepared_models = Vec::new(); for (handle, instances) in static_batches { - let Some(model) = registry.get_model(Handle::new(handle)).cloned() else { + let Some(model) = registry.get_model(Handle::new(handle)) else { log_once::error_once!("Missing model handle {} in registry", handle); continue; }; - let instance_buffer = graphics.device.create_buffer_init( - &wgpu::util::BufferInitDescriptor { - label: Some("Runtime Instance Buffer"), - contents: bytemuck::cast_slice(&instances), - usage: wgpu::BufferUsages::VERTEX, - }, - ); + let instance_buffer = self + .instance_buffer_cache + .entry(handle) + .or_insert_with(|| { + ResizableBuffer::new( + &graphics.device, + instances.len().max(1), + wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + "Runtime Instance Buffer", + ) + }); + instance_buffer.write(&graphics.device, &graphics.queue, &instances); - prepared_models.push((model, instance_buffer, instances.len() as u32)); + prepared_models.push((model, handle, instances.len() as u32)); } - let registry = ASSET_REGISTRY.read(); { let mut render_pass = encoder .begin_render_pass(&wgpu::RenderPassDescriptor { @@ -434,9 +440,9 @@ impl Scene for Editor { }); if let Some(light_pipeline) = &self.light_cube_pipeline { render_pass.set_pipeline(light_pipeline.pipeline()); - for (light, component) in &lights { + for light in &lights { render_pass.set_vertex_buffer(1, light.instance_buffer.buffer().slice(..)); - if !component.visible { + if !light.component.visible { continue; } @@ -485,7 +491,7 @@ impl Scene for Editor { // model rendering if let Some(lcp) = &self.light_cube_pipeline { - for (model, instance_buffer, instance_count) in prepared_models { + for (model, handle, instance_count) in prepared_models { let globals_bind_group = &self .shader_globals .as_ref() @@ -516,7 +522,14 @@ impl Scene for Editor { timestamp_writes: None, }); render_pass.set_pipeline(pipeline.pipeline()); - render_pass.set_vertex_buffer(1, instance_buffer.slice(..)); + if let Some(instance_buffer) = self.instance_buffer_cache.get(&handle) { + render_pass.set_vertex_buffer( + 1, + instance_buffer.slice(instance_count as usize), + ); + } else { + continue; + } render_pass.set_bind_group(3, globals_bind_group, &[]); for mesh in &model.meshes { @@ -541,18 +554,22 @@ impl Scene for Editor { .bind_group; for (handle, instance, skin_bind_group) in animated_instances { - let Some(model) = registry.get_model(Handle::new(handle)).cloned() else { + let Some(model) = registry.get_model(Handle::new(handle)) else { log_once::error_once!("Missing model handle {} in registry", handle); continue; }; - let instance_buffer = graphics.device.create_buffer_init( - &wgpu::util::BufferInitDescriptor { - label: Some("Runtime Animated Instance Buffer"), - contents: bytemuck::cast_slice(&[instance]), - usage: wgpu::BufferUsages::VERTEX, - }, - ); + let instance_buffer = self + .animated_instance_buffer + .get_or_insert_with(|| { + ResizableBuffer::new( + &graphics.device, + 1, + wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + "Runtime Animated Instance Buffer", + ) + }); + instance_buffer.write(&graphics.device, &graphics.queue, &[instance]); let mut render_pass = encoder .begin_render_pass(&wgpu::RenderPassDescriptor { @@ -579,7 +596,7 @@ impl Scene for Editor { }); render_pass.set_pipeline(pipeline.pipeline()); - render_pass.set_vertex_buffer(1, instance_buffer.slice(..)); + render_pass.set_vertex_buffer(1, instance_buffer.slice(1)); render_pass.set_bind_group(3, globals_bind_group, &[]); for mesh in &model.meshes { @@ -1,7 +1,7 @@ use transform_gizmo_egui::{GizmoConfig, GizmoExt, GizmoOrientation}; use dropbear_engine::camera::Camera; use dropbear_engine::entity::{EntityTransform, Transform}; -use dropbear_engine::lighting::LightComponent; +use dropbear_engine::lighting::{Light}; use eucalyptus_core::camera::CameraComponent; use eucalyptus_core::utils::ViewportMode; use crate::editor::{EditorTabViewer, Signal, TABS_GLOBAL, UndoableAction}; @@ -226,10 +226,10 @@ impl<'a> EditorTabViewer<'a> { } if let Some(updated_transform) = updated_light_transform { - if let Ok(mut light_component) = self.world.get::<&mut LightComponent>(*entity_id) { + if let Ok(mut light) = self.world.get::<&mut Light>(*entity_id) { let forward = DVec3::new(0.0, 0.0, -1.0); - light_component.position = updated_transform.position; - light_component.direction = (updated_transform.rotation * forward).normalize_or_zero(); + light.component.position = updated_transform.position; + light.component.direction = (updated_transform.rotation * forward).normalize_or_zero(); } } } @@ -1,4 +1,4 @@ -use crate::editor::{Editor, EditorState, Signal}; +use crate::editor::{AssetClipboard, Editor, EditorState, Signal}; use dropbear_engine::graphics::SharedGraphicsContext; use dropbear_engine::utils::{ relative_path_from_euca, EUCA_SCHEME, @@ -9,6 +9,7 @@ use eucalyptus_core::camera::{CameraComponent, CameraType}; use eucalyptus_core::scripting::{build_jvm, BuildStatus}; use eucalyptus_core::states::{EditorTab, PROJECT}; use eucalyptus_core::{fatal, info, success, success_without_console, warn, warn_without_console}; +use std::fs; use std::path::PathBuf; use std::sync::Arc; use winit::keyboard::KeyCode; @@ -44,6 +45,64 @@ impl SignalController for Editor { Ok::<(), anyhow::Error>(()) } Signal::Copy(_) => Ok(()), + Signal::AssetCopy { source, division } => { + self.asset_clipboard = Some(AssetClipboard { + source: source.clone(), + division: *division, + }); + self.signal = Signal::None; + Ok(()) + } + Signal::AssetPaste { target_dir, division } => { + let clipboard = self.asset_clipboard.clone(); + if clipboard.is_none() { + warn!("Nothing copied to paste"); + self.signal = Signal::None; + return Ok(()); + } + + let clipboard = clipboard.unwrap(); + if clipboard.division != *division { + warn!("Cannot paste across different asset divisions"); + self.signal = Signal::None; + return Ok(()); + } + + if !clipboard.source.is_file() { + warn!("Copied asset is not a file"); + self.signal = Signal::None; + return Ok(()); + } + + if !target_dir.exists() { + warn!("Target directory does not exist"); + self.signal = Signal::None; + return Ok(()); + } + + let Some(file_name) = clipboard.source.file_name() else { + warn!("Unable to paste: invalid file name"); + self.signal = Signal::None; + return Ok(()); + }; + + let target_path = target_dir.join(file_name); + if target_path.exists() { + warn!("Target already exists: {}", target_path.display()); + self.signal = Signal::None; + return Ok(()); + } + + if let Err(err) = fs::copy(&clipboard.source, &target_path) { + warn!("Unable to paste file: {}", err); + self.signal = Signal::None; + return Ok(()); + } + + info!("Pasted asset to {}", target_path.display()); + self.signal = Signal::None; + Ok(()) + } Signal::Paste(scene_entity) => { let mut scene_entity = scene_entity.clone(); scene_entity.label = Editor::unique_label_for_world( @@ -14,7 +14,7 @@ use eucalyptus_core::physics::collider::{ColliderShapeKey, WireframeGeometry}; use futures::executor; use hecs::{Entity, World}; use dropbear_engine::future::{FutureHandle, FutureQueue}; -use dropbear_engine::graphics::SharedGraphicsContext; +use dropbear_engine::graphics::{InstanceRaw, SharedGraphicsContext}; use dropbear_engine::scene::SceneCommand; use eucalyptus_core::input::InputState; use eucalyptus_core::scripting::{ScriptManager, ScriptTarget}; @@ -122,6 +122,8 @@ pub struct PlayMode { light_cube_pipeline: Option<LightCubePipeline>, main_pipeline: Option<MainRenderPipeline>, shader_globals: Option<GlobalsUniform>, + instance_buffer_cache: HashMap<u64, ResizableBuffer<InstanceRaw>>, + animated_instance_buffer: Option<ResizableBuffer<InstanceRaw>>, collider_wireframe_pipeline: Option<ColliderWireframePipeline>, sky_pipeline: Option<SkyPipeline>, default_skinning_buffer: Option<wgpu::Buffer>, @@ -187,6 +189,8 @@ impl PlayMode { main_pipeline: None, light_cube_pipeline: None, shader_globals: None, + instance_buffer_cache: HashMap::new(), + animated_instance_buffer: None, scripts_ready: false, has_initial_resize_done: false, physics_pipeline: Default::default(), @@ -9,7 +9,6 @@ use eucalyptus_core::physics::collider::ColliderShapeKey; use eucalyptus_core::physics::collider::shader::ColliderInstanceRaw; use glam::{vec2, DMat4, DQuat, DVec3, Mat4, Quat, Vec2}; use hecs::Entity; -use wgpu::Color; use wgpu::util::DeviceExt; use winit::event_loop::ActiveEventLoop; use winit::event::WindowEvent; @@ -19,9 +18,9 @@ use dropbear_engine::camera::Camera; use dropbear_engine::buffer::ResizableBuffer; use dropbear_engine::entity::{EntityTransform, MeshRenderer, Transform}; use dropbear_engine::graphics::{InstanceRaw, SharedGraphicsContext}; -use dropbear_engine::lighting::{Light, LightComponent}; +use dropbear_engine::lighting::{Light}; use dropbear_engine::lighting::MAX_LIGHTS; -use dropbear_engine::model::{DrawLight, DrawModel, Model}; +use dropbear_engine::model::{DrawLight, DrawModel}; use dropbear_engine::scene::{Scene, SceneCommand}; use eucalyptus_core::command::CommandBufferPoller; use eucalyptus_core::hierarchy::{EntityTransformExt, Parent}; @@ -33,7 +32,7 @@ use eucalyptus_core::states::SCENES; use eucalyptus_core::scene::loading::{IsSceneLoaded, SceneLoadResult, SCENE_LOADER}; use crate::PlayMode; use eucalyptus_core::physics::collider::shader::create_wireframe_geometry; -use kino_ui::widgets::{Anchor, Border, Fill}; +use kino_ui::widgets::{Border, Fill}; use kino_ui::widgets::rect::Rectangle; impl Scene for PlayMode { @@ -619,11 +618,15 @@ impl Scene for PlayMode { }); } + if let Some(light_pipeline) = &mut self.light_cube_pipeline { + light_pipeline.update(graphics.clone(), &self.world); + } + let lights = { let mut lights = Vec::new(); - let mut query = self.world.query::<(&Light, &LightComponent)>(); - for (light, comp) in query.iter() { - lights.push((light.clone(), comp.clone())); + let mut query = self.world.query::<&Light>(); + for light in query.iter() { + lights.push(light.clone()); } lights }; @@ -631,8 +634,7 @@ impl Scene for PlayMode { if let Some(globals) = &mut self.shader_globals { let enabled_count = lights .iter() - .filter(|(_, comp)| comp.enabled) - .take(MAX_LIGHTS) + .filter(|light| light.component.enabled) .count() as u32; globals.set_num_lights(enabled_count); globals.write(&graphics.queue); @@ -664,31 +666,25 @@ impl Scene for PlayMode { let registry = ASSET_REGISTRY.read(); let mut prepared_models = Vec::new(); for (handle, instances) in static_batches { - let Some(model) = registry.get_model(Handle::new(handle)).cloned() else { + let Some(model) = registry.get_model(Handle::new(handle)) else { log_once::error_once!("Missing model handle {} in registry", handle); continue; }; - let instance_buffer = graphics.device.create_buffer_init( - &wgpu::util::BufferInitDescriptor { - label: Some("Runtime Instance Buffer"), - contents: bytemuck::cast_slice(&instances), - usage: wgpu::BufferUsages::VERTEX, - }, - ); - - prepared_models.push((model, instance_buffer, instances.len() as u32)); - } - - { - let mut query = self.world.query::<( - &mut LightComponent, - &mut Light, - )>(); + let instance_buffer = self + .instance_buffer_cache + .entry(handle) + .or_insert_with(|| { + ResizableBuffer::new( + &graphics.device, + instances.len().max(1), + wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + "Runtime Instance Buffer", + ) + }); + instance_buffer.write(&graphics.device, &graphics.queue, &instances); - for (light_component, light) in query.iter() { - light.update(graphics.as_ref(), light_component); - } + prepared_models.push((model, handle, instances.len() as u32)); } let registry = ASSET_REGISTRY.read(); @@ -718,9 +714,9 @@ impl Scene for PlayMode { }); if let Some(light_pipeline) = &self.light_cube_pipeline { render_pass.set_pipeline(light_pipeline.pipeline()); - for (light, component) in &lights { + for light in &lights { render_pass.set_vertex_buffer(1, light.instance_buffer.buffer().slice(..)); - if !component.visible { + if !light.component.visible { continue; } @@ -769,7 +765,7 @@ impl Scene for PlayMode { // model rendering if let Some(lcp) = &self.light_cube_pipeline { - for (model, instance_buffer, instance_count) in prepared_models { + for (model, handle, instance_count) in prepared_models { let globals_bind_group = &self .shader_globals .as_ref() @@ -800,7 +796,14 @@ impl Scene for PlayMode { timestamp_writes: None, }); render_pass.set_pipeline(pipeline.pipeline()); - render_pass.set_vertex_buffer(1, instance_buffer.slice(..)); + if let Some(instance_buffer) = self.instance_buffer_cache.get(&handle) { + render_pass.set_vertex_buffer( + 1, + instance_buffer.slice(instance_count as usize), + ); + } else { + continue; + } render_pass.set_bind_group(3, globals_bind_group, &[]); for mesh in &model.meshes { @@ -825,18 +828,22 @@ impl Scene for PlayMode { .bind_group; for (handle, instance, skin_bind_group) in animated_instances { - let Some(model) = registry.get_model(Handle::new(handle)).cloned() else { + let Some(model) = registry.get_model(Handle::new(handle)) else { log_once::error_once!("Missing model handle {} in registry", handle); continue; }; - let instance_buffer = graphics.device.create_buffer_init( - &wgpu::util::BufferInitDescriptor { - label: Some("Runtime Animated Instance Buffer"), - contents: bytemuck::cast_slice(&[instance]), - usage: wgpu::BufferUsages::VERTEX, - }, - ); + let instance_buffer = self + .animated_instance_buffer + .get_or_insert_with(|| { + ResizableBuffer::new( + &graphics.device, + 1, + wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + "Runtime Animated Instance Buffer", + ) + }); + instance_buffer.write(&graphics.device, &graphics.queue, &[instance]); let mut render_pass = encoder .begin_render_pass(&wgpu::RenderPassDescriptor { @@ -863,7 +870,7 @@ impl Scene for PlayMode { }); render_pass.set_pipeline(pipeline.pipeline()); - render_pass.set_vertex_buffer(1, instance_buffer.slice(..)); + render_pass.set_vertex_buffer(1, instance_buffer.slice(1)); render_pass.set_bind_group(3, globals_bind_group, &[]); for mesh in &model.meshes {