tirbofish/dropbear · diff
i have no clue what i have done here
Signature present but could not be verified.
Unverified
@@ -126,6 +126,35 @@ impl AssetRegistry { pub fn contains_label(&self, label: &str) -> bool { self.texture_labels.contains_key(label) || self.model_labels.contains_key(label) } + + /// Flushes away all unused assets and returns the count of flushed models. + /// + /// Can be used to reduce the memory being used however, to reuse them, you will need to reimport + /// them into memory. + pub fn flush_unused(&mut self) -> usize { + let mut counter = 0; + + // textures + self.models.retain(|id, arc| { + let is_null = *id == 0; + let is_protected = arc.label.eq_ignore_ascii_case("light cube"); + if is_null || is_protected || Arc::get_mut(arc).is_none() { counter+=1; false } else { true } + }); + self.model_labels.retain(|_, handle| self.models.contains_key(&handle.id)); + + // models + self.textures.retain(|_, arc| if Arc::get_mut(arc).is_none() { counter+=1; false } else { true }); + self.texture_labels.retain(|_, handle| self.textures.contains_key(&handle.id)); + + counter + } + + pub fn flush_everything(&mut self) { + self.models.clear(); + self.model_labels.clear(); + self.textures.clear(); + self.texture_labels.clear(); + } } /// Texture stuff @@ -12,6 +12,212 @@ use crate::{ utils::ResourceReference, }; use egui::Ui; +use std::hash::Hash; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RotationEditorMode { + EulerDegrees, + EulerRadians, + Quaternion, +} + +impl Default for RotationEditorMode { + fn default() -> Self { + Self::EulerDegrees + } +} + +impl RotationEditorMode { + fn label(self) -> &'static str { + match self { + Self::EulerDegrees => "Euler (degrees)", + Self::EulerRadians => "Euler (radians)", + Self::Quaternion => "Quaternion", + } + } +} + +pub fn inspect_rotation_dquat( + ui: &mut Ui, + id_source: impl Hash, + rotation: &mut DQuat, +) -> bool { + let mode_id = ui.make_persistent_id(("rotation_mode", id_source)); + let mut mode = ui + .ctx() + .data_mut(|d| d.get_temp::<RotationEditorMode>(mode_id).unwrap_or_default()); + + ui.horizontal(|ui| { + ui.label("Mode"); + egui::ComboBox::from_id_salt(mode_id) + .selected_text(mode.label()) + .show_ui(ui, |ui| { + ui.selectable_value( + &mut mode, + RotationEditorMode::EulerDegrees, + RotationEditorMode::EulerDegrees.label(), + ); + ui.selectable_value( + &mut mode, + RotationEditorMode::EulerRadians, + RotationEditorMode::EulerRadians.label(), + ); + ui.selectable_value( + &mut mode, + RotationEditorMode::Quaternion, + RotationEditorMode::Quaternion.label(), + ); + }); + }); + + ui.ctx().data_mut(|d| d.insert_temp(mode_id, mode)); + + match mode { + RotationEditorMode::EulerDegrees => { + let (mut x, mut y, mut z) = rotation.to_euler(glam::EulerRot::XYZ); + x = x.to_degrees(); + y = y.to_degrees(); + z = z.to_degrees(); + + let changed = ui + .horizontal(|ui| { + ui.colored_label(egui::Color32::from_rgb(200, 80, 80), "X:"); + let cx = ui + .add( + egui::DragValue::new(&mut x) + .speed(1.0) + .suffix("°") + .fixed_decimals(1), + ) + .changed(); + + ui.colored_label(egui::Color32::from_rgb(80, 200, 80), "Y:"); + let cy = ui + .add( + egui::DragValue::new(&mut y) + .speed(1.0) + .suffix("°") + .fixed_decimals(1), + ) + .changed(); + + ui.colored_label(egui::Color32::from_rgb(80, 120, 220), "Z:"); + let cz = ui + .add( + egui::DragValue::new(&mut z) + .speed(1.0) + .suffix("°") + .fixed_decimals(1), + ) + .changed(); + + cx || cy || cz + }) + .inner; + + if changed { + *rotation = DQuat::from_euler( + glam::EulerRot::XYZ, + x.to_radians(), + y.to_radians(), + z.to_radians(), + ); + } + changed + } + RotationEditorMode::EulerRadians => { + let (mut x, mut y, mut z) = rotation.to_euler(glam::EulerRot::XYZ); + let changed = ui + .horizontal(|ui| { + ui.colored_label(egui::Color32::from_rgb(200, 80, 80), "X:"); + let cx = ui + .add(egui::DragValue::new(&mut x).speed(0.01).fixed_decimals(3)) + .changed(); + + ui.colored_label(egui::Color32::from_rgb(80, 200, 80), "Y:"); + let cy = ui + .add(egui::DragValue::new(&mut y).speed(0.01).fixed_decimals(3)) + .changed(); + + ui.colored_label(egui::Color32::from_rgb(80, 120, 220), "Z:"); + let cz = ui + .add(egui::DragValue::new(&mut z).speed(0.01).fixed_decimals(3)) + .changed(); + + cx || cy || cz + }) + .inner; + + if changed { + *rotation = DQuat::from_euler(glam::EulerRot::XYZ, x, y, z); + } + changed + } + RotationEditorMode::Quaternion => { + // Store raw (unnormalized) components in temp data keyed to this editor + // instance so that normalization on one frame doesn't cause other components + // to jump on the next frame while the user is still dragging. + let raw_id = mode_id.with("raw_quat"); + let stored = ui.ctx().data(|d| d.get_temp::<[f64; 4]>(raw_id)); + let [mut x, mut y, mut z, mut w] = + stored.unwrap_or([rotation.x, rotation.y, rotation.z, rotation.w]); + + let mut changed = false; + let mut any_dragging = false; + + ui.horizontal(|ui| { + ui.colored_label(egui::Color32::from_rgb(200, 80, 80), "X:"); + let rx = ui.add(egui::DragValue::new(&mut x).speed(0.01).fixed_decimals(4)); + changed |= rx.changed(); + any_dragging |= rx.dragged(); + + ui.colored_label(egui::Color32::from_rgb(80, 200, 80), "Y:"); + let ry = ui.add(egui::DragValue::new(&mut y).speed(0.01).fixed_decimals(4)); + changed |= ry.changed(); + any_dragging |= ry.dragged(); + + ui.colored_label(egui::Color32::from_rgb(80, 120, 220), "Z:"); + let rz = ui.add(egui::DragValue::new(&mut z).speed(0.01).fixed_decimals(4)); + changed |= rz.changed(); + any_dragging |= rz.dragged(); + + ui.colored_label(egui::Color32::from_rgb(220, 180, 80), "W:"); + let rw = ui.add(egui::DragValue::new(&mut w).speed(0.01).fixed_decimals(4)); + changed |= rw.changed(); + any_dragging |= rw.dragged(); + }); + + if any_dragging || changed { + // Keep our raw (possibly unnormalized) values in temp storage so the + // next frame doesn't re-read from the normalized rotation and make + // unedited components jump. + ui.ctx().data_mut(|d| d.insert_temp(raw_id, [x, y, z, w])); + } else { + // Nothing is being edited — sync temp storage back to the real rotation + // so external changes (gizmo, script) are reflected. + ui.ctx() + .data_mut(|d| d.insert_temp(raw_id, [rotation.x, rotation.y, rotation.z, rotation.w])); + } + + if changed { + let q = DQuat::from_xyzw(x, y, z, w); + if q.length_squared() > 1e-12 { + *rotation = q.normalize(); + } + } + changed + } + } +} + +pub fn inspect_rotation_quat(ui: &mut Ui, id_source: impl Hash, rotation: &mut Quat) -> bool { + let mut dquat = rotation.as_dquat(); + let changed = inspect_rotation_dquat(ui, id_source, &mut dquat); + if changed { + *rotation = dquat.as_quat(); + } + changed +} /// A type of transform that is attached to all entities. It contains the local and world transforms. #[derive(Default, Debug, Deserialize, Serialize, Copy, PartialEq, Clone)] @@ -204,60 +410,12 @@ impl Transform { ui.label("Rotation:"); }); - let (mut x, mut y, mut z) = self.rotation.to_euler(glam::EulerRot::XYZ); - x = x.to_degrees(); - y = y.to_degrees(); - z = z.to_degrees(); - - let changed = ui - .horizontal(|ui| { - ui.colored_label(egui::Color32::from_rgb(200, 80, 80), "X:"); - let cx = ui - .add( - egui::DragValue::new(&mut x) - .speed(1.0) - .suffix("°") - .fixed_decimals(1), - ) - .changed(); - - ui.colored_label(egui::Color32::from_rgb(80, 200, 80), "Y:"); - let cy = ui - .add( - egui::DragValue::new(&mut y) - .speed(1.0) - .suffix("°") - .fixed_decimals(1), - ) - .changed(); - - ui.colored_label(egui::Color32::from_rgb(80, 120, 220), "Z:"); - let cz = ui - .add( - egui::DragValue::new(&mut z) - .speed(1.0) - .suffix("°") - .fixed_decimals(1), - ) - .changed(); - - cx || cy || cz - }) - .inner; + let _ = inspect_rotation_dquat(ui, "transform_rotation", &mut self.rotation); if ui.button("Reset Rotation").clicked() { self.rotation = DQuat::IDENTITY; } - if changed { - self.rotation = DQuat::from_euler( - glam::EulerRot::XYZ, - x.to_radians(), - y.to_radians(), - z.to_radians(), - ); - } - ui.add_space(4.0); // Scale @@ -10,6 +10,8 @@ use std::fmt::{Display, Formatter}; use std::sync::Arc; use dropbear_utils::Dirty; +const LIGHT_FORWARD_AXIS: DVec3 = DVec3::new(0.0, -1.0, 0.0); + pub const MAX_LIGHTS: usize = 10; #[repr(C)] @@ -52,7 +54,7 @@ impl Default for LightUniform { fn default() -> Self { Self { position: [0.0, 0.0, 0.0, 1.0], - direction: [0.0, 0.0, -1.0, 1.0], + direction: [0.0, -1.0, 0.0, 1.0], colour: [1.0, 1.0, 1.0, 1.0], // light_type: 0, constant: 0.0, @@ -159,7 +161,7 @@ impl Default for LightComponent { fn default() -> Self { Self { position: DVec3::ZERO, - direction: DVec3::new(0.0, 0.0, -1.0), + direction: LIGHT_FORWARD_AXIS, colour: DVec3::ONE, light_type: LightType::Point, intensity: 1.0, @@ -221,7 +223,7 @@ impl LightComponent { return DQuat::IDENTITY; } - let forward = DVec3::new(0.0, 0.0, -1.0); + let forward = LIGHT_FORWARD_AXIS; let normalized_direction = direction.normalize(); let dot = forward.dot(normalized_direction); @@ -14,7 +14,7 @@ struct CameraUniform { struct Light { position: vec4<f32>, direction: vec4<f32>, // x, y, z, outer_cutoff_angle - color: vec4<f32>, // r, g, b, light_type (0, 1, 2) + color: vec4<f32>, // r, g, b, light_type (0=directional, 1=point, 2=spot) constant: f32, lin: f32, quadratic: f32, @@ -32,10 +32,10 @@ struct MaterialUniform { alpha_cutoff: f32, uv_tiling: vec2<f32>, - has_normal_texture: u32, // 0,1 bool - has_emissive_texture: u32, // 0,1 bool - has_metallic_texture: u32, // 0,1 bool - has_occlusion_texture: u32, // 0,1 bool + has_normal_texture: u32, + has_emissive_texture: u32, + has_metallic_texture: u32, + has_occlusion_texture: u32, } struct MorphTargetInfo { @@ -95,8 +95,8 @@ var env_map: texture_cube<f32>; var env_sampler: sampler; struct InstanceInput { - @location(8) model_matrix_0: vec4<f32>, - @location(9) model_matrix_1: vec4<f32>, + @location(8) model_matrix_0: vec4<f32>, + @location(9) model_matrix_1: vec4<f32>, @location(10) model_matrix_2: vec4<f32>, @location(11) model_matrix_3: vec4<f32>, @@ -113,7 +113,6 @@ struct VertexInput { @location(3) tex_coords0: vec2<f32>, @location(4) tex_coords1: vec2<f32>, @location(5) colour0: vec4<f32>, - @location(6) joints: vec4<u32>, @location(7) weights: vec4<f32>, }; @@ -135,20 +134,16 @@ fn apply_morph(base_pos: vec3<f32>, vertex_id: u32) -> vec3<f32> { let idx = u_morph_info.base_offset + (t * u_morph_info.num_vertices + vertex_id) * 3u; let delta = vec3<f32>( s_morph_deltas[idx], - s_morph_deltas[idx + 1], - s_morph_deltas[idx + 2], + s_morph_deltas[idx + 1u], + s_morph_deltas[idx + 2u], ); result += delta * weight; } - return result; } @vertex -fn vs_main( - model: VertexInput, - instance: InstanceInput, -) -> VertexOutput { +fn vs_main(model: VertexInput, instance: InstanceInput) -> VertexOutput { let model_matrix = mat4x4<f32>( instance.model_matrix_0, instance.model_matrix_1, @@ -165,18 +160,16 @@ fn vs_main( vec4<f32>(1.0, 0.0, 0.0, 0.0), vec4<f32>(0.0, 1.0, 0.0, 0.0), vec4<f32>(0.0, 0.0, 1.0, 0.0), - vec4<f32>(0.0, 0.0, 0.0, 1.0) + vec4<f32>(0.0, 0.0, 0.0, 1.0), ); - if (dot(model.weights, vec4<f32>(1.0)) > 0.0) { let j = model.joints; let w = model.weights; - skin_matrix = - (s_skinning[j.x] * w.x) + - (s_skinning[j.y] * w.y) + - (s_skinning[j.z] * w.z) + - (s_skinning[j.w] * w.w); + s_skinning[j.x] * w.x + + s_skinning[j.y] * w.y + + s_skinning[j.z] * w.z + + s_skinning[j.w] * w.w; } let morphed_pos = select( @@ -186,128 +179,145 @@ fn vs_main( ); let world_position = model_matrix * skin_matrix * vec4<f32>(morphed_pos, 1.0); - let skin_normal = (skin_matrix * vec4<f32>(model.normal, 0.0)).xyz; - let skin_tangent = (skin_matrix * vec4<f32>(model.tangent.xyz, 0.0)).xyz; + let skin_normal = (skin_matrix * vec4<f32>(model.normal, 0.0)).xyz; + let skin_tangent = (skin_matrix * vec4<f32>(model.tangent.xyz, 0.0)).xyz; - let world_normal = normalize(normal_matrix * skin_normal); - let world_tangent = normalize(normal_matrix * skin_tangent); + let world_normal = normalize(normal_matrix * skin_normal); + let world_tangent = normalize(normal_matrix * skin_tangent); let world_bitangent = normalize(cross(world_normal, world_tangent) * model.tangent.w); var out: VertexOutput; - out.clip_position = u_camera.view_proj * world_position; - out.tex_coords = model.tex_coords0; - out.world_normal = world_normal; - out.world_position = world_position.xyz; - out.world_tangent = world_tangent; - out.world_bitangent = world_bitangent; + out.clip_position = u_camera.view_proj * world_position; + out.tex_coords = model.tex_coords0; + out.world_normal = world_normal; + out.world_position = world_position.xyz; + out.world_tangent = world_tangent; + out.world_bitangent = world_bitangent; out.world_view_position = u_camera.view_pos.xyz; return out; } -fn directional_light( - light: Light, - world_normal: vec3<f32>, - view_dir: vec3<f32>, - tex_color: vec3<f32>, - world_pos: vec3<f32> + +fn fresnel_schlick(cos_theta: f32, f0: vec3<f32>) -> vec3<f32> { + return f0 + (vec3<f32>(1.0) - f0) * pow(clamp(1.0 - cos_theta, 0.0, 1.0), 5.0); +} + +fn apply_normal_map( + world_normal_in: vec3<f32>, + world_tangent_in: vec3<f32>, + world_bitangent_in: vec3<f32>, + normal_sample_raw: vec3<f32>, + normal_scale: f32, ) -> vec3<f32> { - let light_dir = normalize(-light.direction.xyz); + let unpacked = normalize((normal_sample_raw * 2.0 - 1.0) * vec3<f32>(normal_scale, normal_scale, 1.0)); - let diff = max(dot(world_normal, light_dir), 0.0); - let diffuse = light.color.xyz * diff * tex_color; + let n = normalize(world_normal_in); + var t = normalize(world_tangent_in); + // Gram-Schmidt re-orthogonalise + t = normalize(t - n * dot(n, t)); - let reflect_dir = reflect(-light_dir, world_normal); - let spec = pow(max(dot(view_dir, reflect_dir), 0.0), 32.0); - let specular = light.color.xyz * spec * tex_color; + let b_in = normalize(world_bitangent_in); + let handedness = select(-1.0, 1.0, dot(cross(n, t), b_in) >= 0.0); + let b = cross(n, t) * handedness; - return diffuse + specular; + let tbn = mat3x3<f32>(t, b, n); + return normalize(tbn * unpacked); } -fn point_light( - light: Light, - world_pos: vec3<f32>, +fn eval_brdf( + light_dir: vec3<f32>, world_normal: vec3<f32>, - view_dir: vec3<f32>, - tex_color: vec3<f32> + view_dir: vec3<f32>, + light_color: vec3<f32>, + tex_color: vec3<f32>, + metallic: f32, + roughness: f32, ) -> vec3<f32> { - let light_dir = normalize(light.position.xyz - world_pos); + let r = clamp(roughness, 0.05, 1.0); - let distance = length(light.position.xyz - world_pos); - let attenuation = 1.0 / (light.constant + (light.lin * distance) + (light.quadratic * (distance * distance))); + let n_dot_l = max(dot(world_normal, light_dir), 0.0); + let diffuse = light_color * n_dot_l * tex_color * (1.0 - metallic); - let diff = max(dot(world_normal, light_dir), 0.0); - let diffuse = light.color.xyz * diff * tex_color; + let half_dir = normalize(light_dir + view_dir); + let n_dot_h = max(dot(world_normal, half_dir), 0.0); + let shininess = pow(2.0, (1.0 - r) * 10.0); // maps [0,1] roughness → [2, 1024] + let spec_val = pow(n_dot_h, shininess); - let reflect_dir = reflect(-light_dir, world_normal); - let spec = pow(max(dot(view_dir, reflect_dir), 0.0), 32.0); - let specular = light.color.xyz * spec * tex_color; + let f0 = mix(vec3<f32>(0.04), tex_color, metallic); + let n_dot_v = max(dot(world_normal, view_dir), 0.0); + let fresnel = fresnel_schlick(max(dot(half_dir, view_dir), 0.0), f0); + let specular = light_color * spec_val * fresnel; - return (diffuse + specular) * attenuation; + return diffuse + specular; } -fn spot_light( - light: Light, - world_pos: vec3<f32>, +fn directional_light( + light: Light, world_normal: vec3<f32>, - view_dir: vec3<f32>, - tex_color: vec3<f32> + view_dir: vec3<f32>, + tex_color: vec3<f32>, + metallic: f32, + roughness: f32, ) -> vec3<f32> { - let light_dir = normalize(light.position.xyz - world_pos); - let theta = dot(light_dir, normalize(-light.direction.xyz)); - let outer_cutoff = light.direction.w; - - let epsilon = light.cutoff - outer_cutoff; - let intensity = clamp((theta - outer_cutoff) / epsilon, 0.0, 1.0); - - let distance = length(light.position.xyz - world_pos); - let attenuation = 1.0 / (light.constant + (light.lin * distance) + (light.quadratic * (distance * distance))); + let light_dir = normalize(-light.direction.xyz); + return eval_brdf(light_dir, world_normal, view_dir, light.color.xyz, tex_color, metallic, roughness); +} - let diff = max(dot(world_normal, light_dir), 0.0); - let diffuse = light.color.xyz * diff * tex_color * intensity; +fn point_light( + light: Light, + world_pos: vec3<f32>, + world_normal: vec3<f32>, + view_dir: vec3<f32>, + tex_color: vec3<f32>, + metallic: f32, + roughness: f32, +) -> vec3<f32> { + let to_light = light.position.xyz - world_pos; + let distance = length(to_light); + let light_dir = normalize(to_light); - let reflect_dir = reflect(-light_dir, world_normal); - let spec = pow(max(dot(view_dir, reflect_dir), 0.0), 32.0); - let specular = light.color.xyz * spec * tex_color * intensity; + let attenuation = 1.0 / (light.constant + light.lin * distance + light.quadratic * (distance * distance)); - return (diffuse + specular) * attenuation; + return eval_brdf(light_dir, world_normal, view_dir, light.color.xyz, tex_color, metallic, roughness) + * attenuation; } -fn apply_normal_map( - world_normal_in: vec3<f32>, - world_tangent_in: vec3<f32>, - world_bitangent_in: vec3<f32>, - normal_sample_raw: vec3<f32>, - normal_scale: f32, +fn spot_light( + light: Light, + world_pos: vec3<f32>, + world_normal: vec3<f32>, + view_dir: vec3<f32>, + tex_color: vec3<f32>, + metallic: f32, + roughness: f32, ) -> vec3<f32> { - let unpacked = normalize((normal_sample_raw * 2.0 - 1.0) * vec3<f32>(normal_scale, normal_scale, 1.0)); + let to_light = light.position.xyz - world_pos; + let distance = length(to_light); + let light_dir = normalize(to_light); - let n = normalize(world_normal_in); - var t = normalize(world_tangent_in); - t = normalize(t - n * dot(n, t)); + let theta = dot(light_dir, normalize(-light.direction.xyz)); + let outer_cutoff = light.direction.w; + let epsilon = light.cutoff - outer_cutoff; + let intensity = clamp((theta - outer_cutoff) / epsilon, 0.0, 1.0); - let b_in = normalize(world_bitangent_in); - let handedness = select(-1.0, 1.0, dot(cross(n, t), b_in) >= 0.0); - let b = cross(n, t) * handedness; + let attenuation = 1.0 / (light.constant + light.lin * distance + light.quadratic * (distance * distance)); - let tbn = mat3x3<f32>(t, b, n); - return normalize(tbn * unpacked); -} - -fn fresnel_schlick(cos_theta: f32, f0: vec3<f32>) -> vec3<f32> { - return f0 + (vec3<f32>(1.0) - f0) * pow(clamp(1.0 - cos_theta, 0.0, 1.0), 5.0); + return eval_brdf(light_dir, world_normal, view_dir, light.color.xyz, tex_color, metallic, roughness) + * attenuation * intensity; } @fragment fn s_fs_main(in: VertexOutput) -> @location(0) vec4<f32> { let uv = in.tex_coords * u_material.uv_tiling; - let tex_color = textureSample(t_diffuse, s_diffuse, uv); + // base colour + let tex_color = textureSample(t_diffuse, s_diffuse, uv); let base_colour = tex_color * u_material.base_colour; - if (base_colour.a < u_material.alpha_cutoff) { discard; } + // normal var world_normal: vec3<f32>; if (u_material.has_normal_texture != 0u) { let normal_sample_raw = textureSample(t_normal, s_normal, uv).xyz; @@ -322,20 +332,25 @@ fn s_fs_main(in: VertexOutput) -> @location(0) vec4<f32> { world_normal = normalize(in.world_normal); } - var metallic = u_material.metallic; + // metallic and roughness + var metallic = u_material.metallic; var roughness = u_material.roughness; if (u_material.has_metallic_texture != 0u) { - let mr = textureSample(t_metallic, s_metallic, uv); + let mr = textureSample(t_metallic, s_metallic, uv); metallic *= mr.b; roughness *= mr.g; } + metallic = clamp(metallic, 0.0, 1.0); + roughness = clamp(roughness, 0.05, 1.0); + // occlusion var occlusion = 1.0; if (u_material.has_occlusion_texture != 0u) { - let occ = textureSample(t_occlusion, s_occlusion, uv).r; + let occ = textureSample(t_occlusion, s_occlusion, uv).r; occlusion = 1.0 + u_material.occlusion_strength * (occ - 1.0); } + // emissive var emissive = u_material.emissive * u_material.emissive_strength; if (u_material.has_emissive_texture != 0u) { let emissive_tex = textureSample(t_emissive, s_emissive, uv).rgb; @@ -344,32 +359,32 @@ fn s_fs_main(in: VertexOutput) -> @location(0) vec4<f32> { let view_dir = normalize(u_camera.view_pos.xyz - in.world_position); - let ambient = vec3<f32>(1.0) * u_globals.ambient_strength * base_colour.rgb * occlusion; - + // ambient + let ambient = u_globals.ambient_strength * base_colour.rgb * occlusion; var final_color = ambient; for (var i = 0u; i < u_globals.num_lights; i += 1u) { - let light = s_light_array[i]; + let light = s_light_array[i]; let light_type = i32(light.color.w + 0.1); + if (light_type == 0) { - final_color += directional_light(light, world_normal, view_dir, base_colour.rgb, in.world_position); + final_color += directional_light(light, world_normal, view_dir, base_colour.rgb, metallic, roughness); } else if (light_type == 1) { - final_color += point_light(light, in.world_position, world_normal, view_dir, base_colour.rgb); + final_color += point_light(light, in.world_position, world_normal, view_dir, base_colour.rgb, metallic, roughness); } else if (light_type == 2) { - final_color += spot_light(light, in.world_position, world_normal, view_dir, base_colour.rgb); + final_color += spot_light(light, in.world_position, world_normal, view_dir, base_colour.rgb, metallic, roughness); } } - let world_reflect = reflect(-view_dir, world_normal); - let reflection = textureSample(env_map, env_sampler, world_reflect).rgb; - let n_dot_v = max(dot(world_normal, view_dir), 0.0); - let f0 = mix(vec3<f32>(0.04), base_colour.rgb, metallic); - let fresnel = fresnel_schlick(n_dot_v, f0); - let reflection_weight = fresnel * (1.0 - roughness) * mix(vec3<f32>(1.0), base_colour.rgb, metallic); - final_color += reflection * reflection_weight; + let world_reflect = reflect(-view_dir, world_normal); + let reflection = textureSample(env_map, env_sampler, world_reflect).rgb; + let n_dot_v = max(dot(world_normal, view_dir), 0.0); + let f0 = mix(vec3<f32>(0.04), base_colour.rgb, metallic); + let fresnel = fresnel_schlick(n_dot_v, f0); + let reflection_weight = fresnel * pow(1.0 - roughness, 2.0) * mix(vec3<f32>(1.0), base_colour.rgb, metallic); + final_color += reflection * reflection_weight; final_color += emissive; - // return vec4<f32>(uv.x, uv.y, 0.0, 1.0); return vec4<f32>(final_color, base_colour.a); } @@ -101,8 +101,7 @@ pub enum ResourceReferenceType { /// [`include_bytes!`] macro, this type stores it. Bytes(Arc<[u8]>), - /// An object that can be generated at runtime with the usage of vertices and indices, as well - /// as a solid grey mesh. + /// Any object that can be generated at runtime instead of from a file. ProcObj(ProcedurallyGeneratedObject), } @@ -1,5 +1,6 @@ use std::sync::Arc; use hecs::{Entity, World}; +use dropbear_engine::entity::inspect_rotation_quat; use dropbear_engine::graphics::SharedGraphicsContext; use crate::component::{Component, ComponentDescriptor, ComponentInitFuture, InspectableComponent, SerializedComponent}; use crate::physics::PhysicsState; @@ -114,14 +115,12 @@ impl InspectableComponent for BillboardComponent { } if let Some(rotation) = &mut self.rotation { - ui.horizontal(|ui| { - ui.label("Rotation"); - ui.add(egui::DragValue::new(&mut rotation.x).speed(0.01)); - ui.add(egui::DragValue::new(&mut rotation.y).speed(0.01)); - ui.add(egui::DragValue::new(&mut rotation.z).speed(0.01)); - ui.add(egui::DragValue::new(&mut rotation.w).speed(0.01)); - }); - *rotation = rotation.normalize(); + ui.label("Rotation"); + let _ = inspect_rotation_quat( + ui, + ("billboard_rotation", entity.to_bits()), + rotation, + ); } }); @@ -1,7 +1,7 @@ use crate::hierarchy::EntityTransformExt; use crate::physics::PhysicsState; use crate::ser::model::EucalyptusModel; -use crate::states::{Label, SerializedMaterialCustomisation, SerializedMeshRenderer}; +use crate::states::{SerializedMaterialCustomisation, SerializedMeshRenderer}; use crate::utils::{AsFile, ResolveReference}; use downcast_rs::{Downcast, impl_downcast}; use dropbear_engine::asset::{ASSET_REGISTRY, Handle}; @@ -594,8 +594,9 @@ impl Component for MeshRenderer { .await? } ResourceReferenceType::ProcObj(obj) => { - log::warn!("ResourceReferenceType::Bytes is unsupported and is highly NOT recommended to be used for serialization as it will explode the memory on save"); - log::warn!("Please serialize to a file when you get the chance to do so (could also be an editor bug...)"); + // log::warn!("ResourceReferenceType::Bytes is unsupported and is highly NOT recommended to be used for serialization as it will explode the memory on save"); + // log::warn!("Please serialize to a file when you get the chance to do so (could also be an editor bug...)"); + // warnings removed, only use in euca-runner on release. obj.build_model(graphics.clone(), None, None, ASSET_REGISTRY.clone()) } }; @@ -622,16 +623,16 @@ impl Component for MeshRenderer { match resource { Some(TextureReference::Resource(dif)) => match &dif.ref_type { ResourceReferenceType::None => None, - ResourceReferenceType::File(_) => { + ResourceReferenceType::File(file_path) => { let path = dif.resolve().ok()?; let bytes = std::fs::read(&path).ok()?; let mut texture = TextureBuilder::new(&graphics.device) .with_bytes(graphics.clone(), bytes.as_slice()) - .label(label.as_str()) + .label(file_path.as_str()) .build(); texture.reference = Some(dif.clone()); let mut registry = ASSET_REGISTRY.write(); - Some(Ok(registry.add_texture_with_label(label.clone(), texture))) + Some(Ok(registry.add_texture_with_label(file_path.clone(), texture))) } ResourceReferenceType::Bytes(bytes) => { let texture = TextureBuilder::new(&graphics.device) @@ -710,65 +711,56 @@ impl Component for MeshRenderer { } } - fn save(&self, world: &World, entity: Entity) -> Box<dyn SerializedComponent> { - let entity_label_raw = world - .query_one::<&Label>(entity) - .get() - .map(|label| label.as_str().to_string()) - .unwrap_or_else(|_| "unnamed_entity".to_string()); - - let sanitize_segment = |segment: &str| -> String { - let mut result = String::with_capacity(segment.len()); - for ch in segment.chars() { - if ch.is_ascii_alphanumeric() { - result.push(ch.to_ascii_lowercase()); - } else if ch == '_' || ch == '-' { - result.push(ch); - } else { - result.push('_'); - } - } - - let trimmed = result.trim_matches('_').to_string(); - if trimmed.is_empty() { - "unnamed_entity".to_string() - } else { - trimmed - } - }; - - let proc_obj_type_name = |ty: &ProcObjType| -> &'static str { - match ty { - ProcObjType::Cuboid => "cuboid", - } - }; - - let entity_label = sanitize_segment(entity_label_raw.as_str()); + fn save(&self, _world: &World, _entity: Entity) -> Box<dyn SerializedComponent> { + // let entity_label_raw = world + // .query_one::<&Label>(entity) + // .get() + // .map(|label| label.as_str().to_string()) + // .unwrap_or_else(|_| "unnamed_entity".to_string()); + // + // let sanitize_segment = |segment: &str| -> String { + // let mut result = String::with_capacity(segment.len()); + // for ch in segment.chars() { + // if ch.is_ascii_alphanumeric() { + // result.push(ch.to_ascii_lowercase()); + // } else if ch == '_' || ch == '-' { + // result.push(ch); + // } else { + // result.push('_'); + // } + // } + // + // let trimmed = result.trim_matches('_').to_string(); + // if trimmed.is_empty() { + // "unnamed_entity".to_string() + // } else { + // trimmed + // } + // }; + + // let proc_obj_type_name = |ty: &ProcObjType| -> &'static str { + // match ty { + // ProcObjType::Cuboid => "cuboid", + // } + // }; + // + // let entity_label = sanitize_segment(entity_label_raw.as_str()); let save_reference = |reference: ResourceReference, context: &str| -> ResourceReference { match &reference.ref_type { - ResourceReferenceType::None | ResourceReferenceType::File(_) => reference, - ResourceReferenceType::Bytes(_) | ResourceReferenceType::ProcObj(_) => { - let desired_ref = match &reference.ref_type { - ResourceReferenceType::ProcObj(obj) => Some(format!( - "gen/{}.{}.eucmdl", - entity_label, - proc_obj_type_name(&obj.ty) - )), - _ => None, - }; - + ResourceReferenceType::None | ResourceReferenceType::File(_) | ResourceReferenceType::ProcObj(_) => reference, + ResourceReferenceType::Bytes(_) => { match reference - .as_file(desired_ref) + .as_file(None) .and_then(|path| ResourceReference::from_path(path.as_path())) { Ok(file_ref) => file_ref, Err(err) => { log::warn!( - "Failed to save {} as file-backed reference: {}", - context, - err - ); + "Failed to save {} as file-backed reference: {}", + context, + err + ); ResourceReference::default() } } @@ -921,16 +913,6 @@ impl InspectableComponent for MeshRenderer { || uri.ends_with(".fbx") } - // fn is_probably_texture_uri(uri: &str) -> bool { - // let uri = uri.to_ascii_lowercase(); - // uri.ends_with(".png") - // || uri.ends_with(".jpg") - // || uri.ends_with(".jpeg") - // || uri.ends_with(".tga") - // || uri.ends_with(".bmp") - // || uri.ends_with(".webp") - // } - fn proc_obj_size(obj: &ProcedurallyGeneratedObject) -> Option<[f32; 3]> { if obj.ty != ProcObjType::Cuboid { return None; @@ -1262,10 +1244,8 @@ impl InspectableComponent for MeshRenderer { .changed(); if changed { - // Preserve material customizations across cuboid size change let saved_materials = self.material_snapshot.clone(); apply_cuboid(self, size, false); - // Re-apply material customizations to the new model for (name, saved_mat) in saved_materials { if let Some(mat) = self.material_snapshot.get_mut(&name) { mat.tint = saved_mat.tint; @@ -8,7 +8,7 @@ use crate::scripting::result::DropbearNativeResult; use crate::states::SerializedLight; use crate::types::NVector3; use dropbear_engine::attenuation::ATTENUATION_PRESETS; -use dropbear_engine::entity::{EntityTransform, Transform}; +use dropbear_engine::entity::{EntityTransform, Transform, inspect_rotation_dquat}; use dropbear_engine::graphics::SharedGraphicsContext; use dropbear_engine::lighting::{Light, LightType}; use egui::{CollapsingHeader, ComboBox, DragValue, Ui}; @@ -18,6 +18,8 @@ use jni::JNIEnv; use jni::objects::{JObject, JValue}; use std::sync::Arc; +const LIGHT_FORWARD_AXIS: DVec3 = DVec3::new(0.0, -1.0, 0.0); + #[typetag::serde] impl SerializedComponent for SerializedLight {} @@ -64,12 +66,10 @@ impl Component for Light { 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(); + synced.direction = (transform.rotation * LIGHT_FORWARD_AXIS).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(); + synced.direction = (transform.rotation * LIGHT_FORWARD_AXIS).normalize_or_zero(); } self.update(&graphics); @@ -87,7 +87,7 @@ impl Component for Light { impl InspectableComponent for Light { fn inspect( &mut self, - _world: &World, + world: &World, entity: Entity, ui: &mut Ui, _graphics: Arc<SharedGraphicsContext>, @@ -121,33 +121,119 @@ impl InspectableComponent for Light { }); let mut display_pos = |yueye: &mut Ui| { + let pos_id = yueye.make_persistent_id(("light_pos", entity.to_bits())); + let stored = yueye + .ctx() + .data(|d| d.get_temp::<[f64; 3]>(pos_id)); + let [mut px, mut py, mut pz] = stored.unwrap_or([ + self.component.position.x, + self.component.position.y, + self.component.position.z, + ]); + + let mut changed = false; + let mut any_dragging = false; + let mut reset = false; + 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 rx = yueye.add(DragValue::new(&mut px).speed(0.01)); + changed |= rx.changed(); + any_dragging |= rx.dragged(); + + let ry = yueye.add(DragValue::new(&mut py).speed(0.01)); + changed |= ry.changed(); + any_dragging |= ry.dragged(); + + let rz = yueye.add(DragValue::new(&mut pz).speed(0.01)); + changed |= rz.changed(); + any_dragging |= rz.dragged(); + + if yueye.button("Reset").clicked() { + px = 0.0; + py = 0.0; + pz = 0.0; + changed = true; + reset = true; + } }); + + if any_dragging || changed || reset { + yueye.ctx().data_mut(|d| d.insert_temp(pos_id, [px, py, pz])); + self.component.position = DVec3::new(px, py, pz); + } else { + yueye.ctx().data_mut(|d| d.insert_temp(pos_id, [ + self.component.position.x, + self.component.position.y, + self.component.position.z, + ])); + } + + changed }; - 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)); - }); + let mut display_rot = |yueye: &mut Ui| { + yueye.label("Rotation"); + let mut direction = self.component.direction.normalize_or_zero(); + if direction.length_squared() < 1e-12 { + direction = LIGHT_FORWARD_AXIS; + } + + let mut rotation = DQuat::from_rotation_arc(LIGHT_FORWARD_AXIS, direction); + let mut changed = inspect_rotation_dquat( + yueye, + ("light_rotation", entity.to_bits()), + &mut rotation, + ); + + if changed { + self.component.direction = (rotation * LIGHT_FORWARD_AXIS).normalize_or_zero(); + } + + if yueye.button("Reset Rotation").clicked() { + self.component.direction = LIGHT_FORWARD_AXIS; + changed = true; + } + + changed }; + let mut position_changed = false; + let mut direction_changed = false; + match self.component.light_type { LightType::Directional => { - display_dir(ui); + direction_changed |= display_rot(ui); } LightType::Point => { - display_pos(ui); + position_changed |= display_pos(ui); } LightType::Spot => { - display_pos(ui); - display_dir(ui); + position_changed |= display_pos(ui); + direction_changed |= display_rot(ui); + } + } + + if position_changed { + if let Ok(entity_transform) = world.query_one::<&mut EntityTransform>(entity).get() { + entity_transform.local_mut().position = self.component.position; + } else if let Ok(transform) = world.query_one::<&mut Transform>(entity).get() { + transform.position = self.component.position; + } + } + + if direction_changed { + let desired = self.component.direction.normalize_or_zero(); + if desired.length_squared() >= 1e-12 { + self.component.direction = desired; + let rotation = DQuat::from_rotation_arc(LIGHT_FORWARD_AXIS, desired); + + if let Ok(entity_transform) = world.query_one::<&mut EntityTransform>(entity).get() { + entity_transform.local_mut().rotation = rotation; + } else if let Ok(transform) = world.query_one::<&mut Transform>(entity).get() + { + transform.rotation = rotation; + } } } @@ -517,7 +603,7 @@ fn get_direction( #[dropbear_macro::entity] entity: Entity, ) -> DropbearNativeResult<NVector3> { let transform = get_transform(world, entity)?; - let forward = DVec3::new(0.0, 0.0, -1.0); + let forward = LIGHT_FORWARD_AXIS; let dir = (transform.rotation * forward).normalize_or_zero(); Ok(NVector3::from(dir)) } @@ -537,7 +623,7 @@ fn set_direction( return Err(DropbearNativeError::InvalidArgument); } - let forward = DVec3::new(0.0, 0.0, -1.0); + let forward = LIGHT_FORWARD_AXIS; let rotation = DQuat::from_rotation_arc(forward, desired); set_transform_rotation(world, entity, rotation) } @@ -27,6 +27,7 @@ use crate::states::Label; use crate::types::{NCollider, NVector3}; use ::jni::JNIEnv; use ::jni::objects::{JObject, JValue}; +use dropbear_engine::entity::inspect_rotation_dquat; use dropbear_engine::graphics::SharedGraphicsContext; use dropbear_engine::wgpu::util::{BufferInitDescriptor, DeviceExt}; use dropbear_engine::wgpu::{Buffer, BufferUsages}; @@ -339,33 +340,17 @@ impl Collider { ui.add(egui::DragValue::new(&mut self.translation[2]).speed(0.01)); }); - ui.label("Rotation (degrees):"); - ui.horizontal(|ui| { - ui.label("X:"); - let mut deg_x = self.rotation[0].to_degrees(); - if ui - .add(egui::DragValue::new(&mut deg_x).speed(1.0)) - .changed() - { - self.rotation[0] = deg_x.to_radians(); - } - ui.label("Y:"); - let mut deg_y = self.rotation[1].to_degrees(); - if ui - .add(egui::DragValue::new(&mut deg_y).speed(1.0)) - .changed() - { - self.rotation[1] = deg_y.to_radians(); - } - ui.label("Z:"); - let mut deg_z = self.rotation[2].to_degrees(); - if ui - .add(egui::DragValue::new(&mut deg_z).speed(1.0)) - .changed() - { - self.rotation[2] = deg_z.to_radians(); - } - }); + ui.label("Rotation:"); + let mut rotation = DQuat::from_euler( + glam::EulerRot::XYZ, + self.rotation[0] as f64, + self.rotation[1] as f64, + self.rotation[2] as f64, + ); + if inspect_rotation_dquat(ui, "collider_local_rotation", &mut rotation) { + let (x, y, z) = rotation.to_euler(glam::EulerRot::XYZ); + self.rotation = [x as f32, y as f32, z as f32]; + } }); } } @@ -2,6 +2,7 @@ mod window; +use std::collections::VecDeque; use crate::debug::window::DebugWindow; use crate::editor::Signal; use dropbear_engine::DropbearWindowBuilder; @@ -10,7 +11,7 @@ use parking_lot::RwLock; use std::rc::Rc; use winit::window::WindowAttributes; -pub(crate) fn show_menu_bar(ui: &mut Ui, signal: &mut Signal) { +pub(crate) fn show_menu_bar(ui: &mut Ui, signal: &mut VecDeque<Signal>) { ui.menu_button("Debug", |ui_debug| { if ui_debug.button("Panic").clicked() { log::warn!("Panic caused on purpose from Menu Button Click"); @@ -35,7 +36,7 @@ pub(crate) fn show_menu_bar(ui: &mut Ui, signal: &mut Signal) { .set_initial_scene("debug_window") .build(); - *signal = Signal::RequestNewWindow(window_data); + signal.push_back(Signal::RequestNewWindow(window_data)); } if ui_debug.button("Show TypeID of components").clicked() { @@ -17,6 +17,7 @@ use crate::editor::{ 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> { @@ -965,10 +966,10 @@ impl<'a> EditorTabViewer<'a> { return; } } - *self.signal = Signal::AssetPaste { + self.signal.push_back(Signal::AssetPaste { target_dir: info.path.clone(), division: info.division, - }; + }); } if ui.button("Reveal Folder").clicked() { @@ -1032,10 +1033,10 @@ impl<'a> EditorTabViewer<'a> { if ui.button("Copy").clicked() { ui.close(); - *self.signal = Signal::AssetCopy { + self.signal.push_back(Signal::AssetCopy { source: info.path.clone(), division: info.division, - }; + }); } if ui.button("Delete").clicked() { @@ -1229,7 +1230,7 @@ impl<'a> EditorTabViewer<'a> { .get_model_handle_by_reference(&reference) .is_some() { - info!("Model already loaded: {}", label); + eucalyptus_core::info!("Model already loaded: {}", label); return; } @@ -1276,6 +1277,7 @@ impl<'a> EditorTabViewer<'a> { let mut registry = ASSET_REGISTRY.write(); registry.label_model(label.clone(), handle); + eucalyptus_core::success!("Loaded model {}", label); Ok::<(), anyhow::Error>(()) }); } @@ -1356,16 +1358,31 @@ impl<'a> EditorTabViewer<'a> { .first() .and_then(|node_id| cfg.component_selection(*node_id)) }); - + if let Some(selection) = selection { self.inspect_component_selection(cfg, selection); - if let Some(target_entity) = Self::entity_from_node_id(drag.target) { - let v = if crate::features::is_enabled(crate::features::ShowComponentTypeIDInEditor) { format!(" id #{}", selection.component_type_id) } else { "".to_string() }; - eucalyptus_core::info!( - "Component{} ready to drop onto entity {:?}", - v, - target_entity - ); + 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); } } } @@ -22,7 +22,7 @@ pub struct EditorTabViewer<'a> { pub selected_entity: &'a mut Option<Entity>, pub viewport_mode: &'a mut ViewportMode, pub undo_stack: &'a mut Vec<UndoableAction>, - pub signal: &'a mut Signal, + pub signal: &'a mut VecDeque<Signal>, pub gizmo_mode: &'a mut EnumSet<GizmoMode>, pub gizmo_orientation: &'a mut GizmoOrientation, pub editor_mode: &'a mut EditorState, @@ -6,7 +6,7 @@ use eucalyptus_core::{ states::{Label, PROJECT}, }; use hecs::{Entity, World}; -use std::collections::{BTreeMap, HashMap}; +use std::collections::{BTreeMap, HashMap, VecDeque}; use crate::editor::{Editor, EditorTabDock, EditorTabDockDescriptor, EditorTabViewer, Signal, StaticallyKept, TABS_GLOBAL}; use crate::editor::page::EditorTabVisibility; @@ -50,7 +50,7 @@ impl<'a> EditorTabViewer<'a> { component_ids_by_entity: &HashMap<Entity, Vec<u64>>, rigidbody_component_id: Option<u64>, cfg: &mut StaticallyKept, - signal: &mut Signal, + signal: &mut VecDeque<Signal>, ) -> anyhow::Result<()> { puffin::profile_scope!("entity_list.add_entity_to_tree"); let entity_id = entity.to_bits().get(); @@ -106,8 +106,7 @@ impl<'a> EditorTabViewer<'a> { if let Some(component) = registry.create_default_component(*id) { - *signal = - Signal::AddComponent(entity, component); + signal.push_back(Signal::AddComponent(entity, component)); } ui.close(); } @@ -1,6 +1,5 @@ use super::*; use dropbear_engine::input::{Controller, Keyboard, Mouse}; -use eucalyptus_core::states::Label; use eucalyptus_core::success_without_console; use gilrs::{Button, GamepadId}; use log; @@ -84,7 +83,7 @@ impl Keyboard for Editor { .map_or(false, |id| *tab == id) { if self.selected_entity.is_some() { - self.signal = Signal::Delete; + self.signal.push_back(Signal::Delete); } else { warn!("Failed to delete: No entity selected"); } @@ -95,7 +94,7 @@ impl Keyboard for Editor { } KeyCode::Escape => { if is_playing { - self.signal = Signal::StopPlaying; + self.signal.push_back(Signal::StopPlaying); } else if is_double_press { if self.selected_entity.is_some() { self.selected_entity = None; @@ -165,7 +164,6 @@ impl Keyboard for Editor { } } KeyCode::KeyC => { - // todo: fix this if ctrl_pressed && !is_playing { if let Some((_, tab)) = self.game_editor_dock_state.find_active_focused() && self @@ -174,24 +172,20 @@ impl Keyboard for Editor { .map_or(false, |id| *tab == id) { if let Some(entity) = &self.selected_entity { - let Ok(label) = self.world.get::<&Label>(*entity) else { + let entity = *entity; + let (entities, parent_map) = Editor::collect_entity_subtree( + self.world.as_ref(), + entity, + &self.component_registry, + ); + if entities.is_empty() { warn!("Unable to copy entity: Unable to obtain label"); - return; - }; - - let components = self - .component_registry - .extract_all_components(self.world.as_ref(), *entity); - let s_entity = SceneEntity { - label: Label::new(label.as_str()), - components, - entity_id: None, - }; - self.signal = Signal::Copy(s_entity); - - info!("Copied!"); - - log::debug!("Copied selected entity"); + } else { + self.signal.retain(|s| !matches!(s, Signal::Copy(_, _))); + self.signal.push_back(Signal::Copy(entities, parent_map)); + info!("Copied!"); + log::debug!("Copied selected entity"); + } } else { warn!("Unable to copy entity: None selected"); } @@ -205,8 +199,10 @@ impl Keyboard for Editor { } KeyCode::KeyV => { if ctrl_pressed && !is_playing { - if let Signal::Copy(entity) = &self.signal { - self.signal = Signal::Paste(entity.clone()); + if let Some(Signal::Copy(entities, parent_map)) = self.signal.iter().find(|s| matches!(s, Signal::Copy(_, _))) { + let entities = entities.clone(); + let parent_map = parent_map.clone(); + self.signal.push_back(Signal::Paste(entities, parent_map)); } } else { self.input_state.pressed_keys.insert(key); @@ -240,7 +236,7 @@ impl Keyboard for Editor { } else { // undo log::debug!("Undo signal sent"); - self.signal = Signal::Undo; + self.signal.push_back(Signal::Undo); } } else if matches!(self.viewport_mode, ViewportMode::Gizmo) && !is_playing { info!("GizmoMode set to translate"); @@ -270,14 +266,14 @@ impl Keyboard for Editor { } KeyCode::KeyP => { if !is_playing && ctrl_pressed { - self.signal = Signal::Play + self.signal.push_back(Signal::Play); } else { self.input_state.pressed_keys.insert(key); } } KeyCode::F12 => { if is_playing { - self.signal = Signal::StopPlaying; + self.signal.push_back(Signal::StopPlaying); info!("Stopping play mode"); } else { self.input_state.pressed_keys.insert(key); @@ -66,7 +66,7 @@ use kino_ui::windowing::KinoWinitWindowing; use log::{debug, error}; use parking_lot::{Mutex, RwLock}; use rfd::FileDialog; -use std::collections::HashSet; +use std::collections::{HashSet, VecDeque}; use std::rc::Rc; use std::{collections::HashMap, fs, path::PathBuf, sync::Arc, time::Instant}; use std::cmp::PartialEq; @@ -79,6 +79,7 @@ use dropbear_engine::multisampling::AntiAliasingMode; use winit::window::{CursorGrabMode, WindowAttributes}; use winit::{keyboard::KeyCode, window::Window}; use dropbear_engine::animation::{MorphTargetInfo, MAX_MORPH_WEIGHTS}; +use dropbear_engine::asset::ASSET_REGISTRY; use crate::editor::page::EditorTabVisibility; use crate::editor::ui::UiEditor; @@ -137,7 +138,7 @@ pub struct Editor { pub selected_entity: Option<hecs::Entity>, pub viewport_mode: ViewportMode, - pub(crate) signal: Signal, + pub(crate) signal: VecDeque<Signal>, pub(crate) undo_stack: Vec<UndoableAction>, // todo: add redo (later) // redo_stack: Vec<UndoableAction>, @@ -280,7 +281,7 @@ impl Editor { previously_selected_entity: None, selected_entity: None, viewport_mode: ViewportMode::None, - signal: Signal::None, + signal: VecDeque::new(), undo_stack: Vec::new(), // script_manager: ScriptManager::new()?, editor_state: EditorState::Editing, @@ -396,6 +397,49 @@ impl Editor { } } + /// Collects `entity` and all of its descendants into a flat list of [`SceneEntity`]s + /// (root-first, DFS), plus a map of `child_label -> parent_label` for the subtree. + pub(crate) fn collect_entity_subtree( + world: &World, + entity: hecs::Entity, + registry: &ComponentRegistry, + ) -> (Vec<SceneEntity>, HashMap<Label, Label>) { + let mut entities: Vec<SceneEntity> = Vec::new(); + let mut parent_map: HashMap<Label, Label> = HashMap::new(); + + fn visit( + world: &World, + entity: hecs::Entity, + registry: &ComponentRegistry, + parent_label: Option<&Label>, + entities: &mut Vec<SceneEntity>, + parent_map: &mut HashMap<Label, Label>, + ) { + let Some(scene_entity) = SceneEntity::from_world(world, entity, registry) else { + return; + }; + + if let Some(pl) = parent_label { + parent_map.insert(scene_entity.label.clone(), pl.clone()); + } + + let own_label = scene_entity.label.clone(); + entities.push(scene_entity); + + let children: Vec<hecs::Entity> = world + .get::<&Children>(entity) + .map(|c| c.children().to_vec()) + .unwrap_or_default(); + + for child in children { + visit(world, child, registry, Some(&own_label), entities, parent_map); + } + } + + visit(world, entity, registry, None, &mut entities, &mut parent_map); + (entities, parent_map) + } + fn double_key_pressed(&mut self, key: KeyCode) -> bool { let now = Instant::now(); @@ -890,6 +934,9 @@ impl Editor { .and_then(|stem| stem.to_str()) .ok_or_else(|| anyhow::anyhow!("Scene file name is invalid"))?; + { + ASSET_REGISTRY.write().flush_everything(); + } self.queue_scene_load_by_name(scene_name)?; info!("Queued scene '{}' for loading", scene_name); Ok(()) @@ -997,7 +1044,7 @@ impl Editor { ui.separator(); if matches!(self.editor_state, EditorState::Playing) { if ui.button("Stop").clicked() { - self.signal = Signal::StopPlaying; + self.signal.push_back(Signal::StopPlaying); } } else if ui.button("Play").clicked() { // run prechecks to ensure a starting camera exists and stuff @@ -1011,7 +1058,7 @@ impl Editor { if !found_starting { fatal!("Unable to start play mode: No initial camera set for this scene"); } else { - self.signal = Signal::Play; + self.signal.push_back(Signal::Play); } } ui.menu_button("Export", |ui| { @@ -1056,51 +1103,45 @@ impl Editor { ui.menu_button("Edit", |ui| { if ui.button("Copy").clicked() { if let Some(entity) = &self.selected_entity { - let Ok(label) = self.world.get::<&Label>(*entity) else { + let entity = *entity; + let (entities, parent_map) = Editor::collect_entity_subtree( + self.world.as_ref(), + entity, + &self.component_registry, + ); + if entities.is_empty() { warn!("Unable to copy entity: Unable to obtain label"); - return; - }; - - let mut components = self - .component_registry - .extract_all_components(self.world.as_ref(), *entity); - components.retain(|component| { - self.component_registry - .id_for_component(component.as_ref()) - .and_then(|id| { - self.component_registry.get_descriptor_by_numeric_id(id) - }) - .map(|desc| { - desc.fqtn != "dropbear_engine::entity::EntityTransform" - }) - .unwrap_or(true) - }); - let s_entity = SceneEntity { - label: Label::new(label.as_str()), - components, - entity_id: None, - }; - self.signal = Signal::Copy(s_entity); - - info!("Copied selected entity!"); + } else { + self.signal.retain(|s| !matches!(s, Signal::Copy(_, _))); + self.signal.push_back(Signal::Copy(entities, parent_map)); + info!("Copied selected entity!"); + } } else { warn!("Unable to copy entity: None selected"); } } if ui.button("Paste").clicked() { - match &self.signal { - Signal::Copy(entity) => { - self.signal = Signal::Paste(entity.clone()); + let clipboard = self.signal.iter().find_map(|s| { + if let Signal::Copy(entities, parent_map) = s { + Some((entities.clone(), parent_map.clone())) + } else { + None + } + }); + + match clipboard { + Some((entities, parent_map)) => { + self.signal.push_back(Signal::Paste(entities, parent_map)); } - _ => { + None => { warn!("Unable to paste: You haven't selected anything!"); } } } if ui.button("Undo").clicked() { - self.signal = Signal::Undo; + self.signal.push_back(Signal::Undo); } ui.label("Redo"); }); @@ -1151,6 +1192,13 @@ impl Editor { } }); + ui.menu_button("Assets", |ui| { + if ui.button("Flush unused assets").clicked() { + let mut asset = ASSET_REGISTRY.write(); + let count = asset.flush_unused(); + success!("Flushed {} unused assets", count); + } + }); ui.menu_button("Help", |ui| { if ui.button("Show AppData folder").clicked() { match app_dirs2::app_root(app_dirs2::AppDataType::UserData, &APP_INFO) { @@ -1168,18 +1216,6 @@ impl Editor { log::debug!("Requested nerd stats window"); self.nerd_stats.write().show_window = true; - - // let window_data = DropbearWindowBuilder::new() - // .with_attributes( - // WindowAttributes::default() - // .with_title("Nerd Stats") - // .with_inner_size(PhysicalSize::new(500, 600)) - // ) - // .add_scene_with_input(self.nerd_stats.clone(), "nerd_stats") - // .set_initial_scene("nerd_stats") - // .build(); - - // self.scene_command = SceneCommand::RequestWindow(window_data); } if ui.button("About").clicked() { @@ -1251,7 +1287,7 @@ impl Editor { ui.add_enabled_ui(can_play, |ui| { if ui.button("⏹").clicked() { log::debug!("Menu button Stop button pressed"); - self.signal = Signal::StopPlaying; + self.signal.push_back(Signal::StopPlaying); } }); @@ -1272,7 +1308,7 @@ impl Editor { if !found_starting { fatal!("Unable to start play mode: No initial camera set for this scene"); } else { - self.signal = Signal::Play; + self.signal.push_back(Signal::Play); } } }); @@ -1768,26 +1804,17 @@ impl Editor { } /// Describes an action that is undoable -#[derive(Debug)] pub enum UndoableAction { /// A change in transform. The entity + the old transform. Undoing will revert the transform Transform(hecs::Entity, Transform), /// A change in EntityTransform. The entity + the old transform. Undoing will revert the transform EntityTransform(hecs::Entity, EntityTransform), - #[allow(dead_code)] // don't know why its considered dead code, todo: check the cause - /// A spawn of the entity. Undoing will delete the entity - Spawn(hecs::Entity), /// A change of label of the entity. Undoing will revert its label Label(hecs::Entity, String), RemoveStartingCamera(Entity), } impl UndoableAction { - pub fn push_to_undo(undo_stack: &mut Vec<UndoableAction>, action: Self) { - undo_stack.push(action); - // log::debug!("Undo Stack contents: {:#?}", undo_stack); - } - pub fn undo(&self, world: &mut World) -> anyhow::Result<()> { match self { UndoableAction::Transform(entity, transform) => { @@ -1808,14 +1835,6 @@ impl UndoableAction { Err(anyhow::anyhow!("Could not find an entity to query")) } } - UndoableAction::Spawn(entity) => { - if world.despawn(*entity).is_ok() { - log::debug!("Undid spawn by despawning entity {:?}", entity); - Ok(()) - } else { - Err(anyhow::anyhow!("Failed to despawn entity {:?}", entity)) - } - } UndoableAction::Label(entity, original_label) => { if let Ok(label) = world.query_one_mut::<&mut Label>(*entity) { label.set(original_label.clone()); @@ -1846,8 +1865,10 @@ impl UndoableAction { pub enum Signal { #[default] None, - Copy(SceneEntity), - Paste(SceneEntity), + /// Carries the entity subtree to be pasted: a flat list of entities (root first, DFS order) + /// and a map of child_label -> parent_label within the subtree. + Copy(Vec<SceneEntity>, HashMap<Label, Label>), + Paste(Vec<SceneEntity>, HashMap<Label, Label>), AssetCopy { source: PathBuf, division: AssetDivision, @@ -178,11 +178,11 @@ impl Scene for Editor { self.scene_command = SceneCommand::SetAntialiasing(desired); self.pending_aa_reload = Some(desired); } - } else if self.pending_aa_reload.is_some() && matches!(self.signal, Signal::None) { + } else if self.pending_aa_reload.is_some() && self.signal.is_empty() { log::debug!("Anti aliasing mode applied, reloading WGPU data"); - self.signal = Signal::ReloadWGPUData { + self.signal.push_back(Signal::ReloadWGPUData { skybox_texture: None, - }; + }); self.pending_aa_reload = None; } } @@ -0,0 +1,21 @@ +use egui::Ui; +use crate::editor::{EditorTabDock, EditorTabDockDescriptor, EditorTabViewer}; +use crate::editor::page::EditorTabVisibility; + +pub struct UIInspector { + +} + +impl EditorTabDock for UIInspector { + fn desc() -> EditorTabDockDescriptor { + EditorTabDockDescriptor { + id: "Widget Inspector", + title: "Widget Inspector".to_string(), + visibility: EditorTabVisibility::UIEditor, + } + } + + fn display(_viewer: &mut EditorTabViewer<'_>, ui: &mut Ui) { + ui.label("Not implemented yet."); + } +} @@ -7,6 +7,8 @@ use kino_ui::camera::Camera2D; use wgpu::TextureFormat; pub mod viewport; +pub mod inspector; +pub mod widget_tree; pub struct UiEditor { pub grid_pipeline: Option<UIGridPipeline>, @@ -3,13 +3,13 @@ use glam::Vec2; use crate::editor::{EditorTabDock, EditorTabDockDescriptor, EditorTabViewer}; use crate::editor::page::EditorTabVisibility; -pub struct UIViewport; +pub struct UICanvas; -impl EditorTabDock for UIViewport { +impl EditorTabDock for UICanvas { fn desc() -> EditorTabDockDescriptor { EditorTabDockDescriptor { - id: "UI Viewport", - title: "UI Viewport".to_string(), + id: "UI Viewport", // kept for some reason, change on release + title: "UI Canvas".to_string(), visibility: EditorTabVisibility::UIEditor, } } @@ -36,17 +36,34 @@ impl EditorTabDock for UIViewport { let viewport_pixels = Vec2::new(available_size.x * ppp, available_size.y * ppp); if response.hovered() { - let scroll_y = ui.ctx().input(|i| i.raw_scroll_delta.y); - if scroll_y.abs() > 0.0 { - let zoom_delta = scroll_y * 0.0025; - viewer.ui_editor.zoom_by(zoom_delta); - } + ui.ctx().input(|i| { + // pinch + let zoom_delta = i.zoom_delta(); + if (zoom_delta - 1.0).abs() > f32::EPSILON { + viewer.ui_editor.zoom_by(zoom_delta - 1.0); + } + + // scroll wheel + let scroll_y = i.raw_scroll_delta.y; + let is_trackpad_panning = i.smooth_scroll_delta != egui::Vec2::ZERO; + if scroll_y.abs() > 0.0 && !is_trackpad_panning { + viewer.ui_editor.zoom_by(scroll_y * 0.0025); + } + + // trackpad pan + let smooth_scroll = i.smooth_scroll_delta; + if smooth_scroll != egui::Vec2::ZERO { + let delta_pixels = Vec2::new(smooth_scroll.x * ppp, smooth_scroll.y * ppp); + viewer.ui_editor.pan_by_pixels(delta_pixels); + } + }); } let is_middle_down = ui .ctx() .input(|i| i.pointer.button_down(egui::PointerButton::Middle)); + // middle mouse button if response.hovered() && is_middle_down { let pointer_delta_points = ui.ctx().input(|i| i.pointer.delta()); if pointer_delta_points != egui::Vec2::ZERO { @@ -0,0 +1,21 @@ +use egui::Ui; +use crate::editor::{EditorTabDock, EditorTabDockDescriptor, EditorTabViewer}; +use crate::editor::page::EditorTabVisibility; + +pub struct UIWidgetTree { + +} + +impl EditorTabDock for UIWidgetTree { + fn desc() -> EditorTabDockDescriptor { + EditorTabDockDescriptor { + id: "Widget Tree", + title: "Widget Tree".to_string(), + visibility: EditorTabVisibility::UIEditor, + } + } + + fn display(_viewer: &mut EditorTabViewer<'_>, ui: &mut Ui) { + ui.label("Not implemented yet."); + } +} @@ -6,6 +6,7 @@ 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> { @@ -21,8 +22,8 @@ impl<'a> EditorTabViewer<'a> { 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 matches!(*self.signal, Signal::None) { - *self.signal = Signal::UpdateViewportSize((desired_width as f32, desired_height as f32)); + if self.signal.is_empty() { + self.signal.push_back(Signal::UpdateViewportSize((desired_width as f32, desired_height as f32))); } } @@ -62,9 +63,6 @@ impl<'a> EditorTabViewer<'a> { let snapping = ui.input(|input| input.modifiers.shift); - // Note to self: fuck you >:( - // Note to self: ok wow thats pretty rude im trying my best >﹏< - // Note to self: finally holy shit i got it working let active_cam = self.active_camera.lock(); if let Some(active_camera) = *active_cam { let camera_data = { @@ -93,16 +91,16 @@ impl<'a> EditorTabViewer<'a> { }); } } + 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(entity_transform) = self + if let Ok(mut entity_transform) = self .world - .query_one::<&mut EntityTransform>(*entity_id) - .get() + .get::<&mut EntityTransform>(*entity_id) { let was_focused = cfg.is_focused; cfg.is_focused = self.gizmo.is_focused(); @@ -111,7 +109,7 @@ impl<'a> EditorTabViewer<'a> { cfg.entity_transform_original = Some(*entity_transform); } - let synced = entity_transform.sync(); + let synced = entity_transform.propagate(&self.world, *entity_id); let gizmo_transform = transform_gizmo_egui::math::Transform::from_scale_rotation_translation( synced.scale, @@ -126,71 +124,44 @@ impl<'a> EditorTabViewer<'a> { let new_synced_rot: glam::DQuat = new_transform.rotation.into(); let new_synced_scale: glam::DVec3 = new_transform.scale.into(); - match *self.gizmo_orientation { - GizmoOrientation::Global => { - let local = *entity_transform.local(); - - let safe_local_scale = glam::DVec3::new( - if local.scale.x.abs() < 1e-6 { - 1.0 - } else { - local.scale.x - }, - if local.scale.y.abs() < 1e-6 { - 1.0 - } else { - local.scale.y - }, - if local.scale.z.abs() < 1e-6 { - 1.0 - } else { - local.scale.z - }, - ); + let prev_world_pos = synced.position; + let prev_world_rot = synced.rotation; + let prev_world_scale = synced.scale; - let new_world_scale = new_synced_scale / safe_local_scale; - let new_world_rot = new_synced_rot * local.rotation.inverse(); + let safe = |v: f64| if v.abs() < 1e-9 { 1.0 } else { v }; - let scaled_local_pos = local.position * new_world_scale; - let rotated_local_pos = new_world_rot * scaled_local_pos; - let new_world_pos = new_synced_pos - rotated_local_pos; + 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), + ); - let world_transform = entity_transform.world_mut(); - world_transform.position = new_world_pos; - world_transform.rotation = new_world_rot; - world_transform.scale = new_world_scale; + 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_transform = entity_transform.world(); - let world_scale = world_transform.scale; - let world_rot = world_transform.rotation; - let world_pos = world_transform.position; - - let safe_world_scale = glam::DVec3::new( - if world_scale.x.abs() < 1e-6 { - 1.0 - } else { - world_scale.x - }, - if world_scale.y.abs() < 1e-6 { - 1.0 - } else { - world_scale.y - }, - if world_scale.z.abs() < 1e-6 { - 1.0 - } else { - world_scale.z - }, + 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_transform = entity_transform.local_mut(); - local_transform.scale = new_synced_scale / safe_world_scale; - local_transform.rotation = world_rot.inverse() * new_synced_rot; + 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; - let delta_pos = new_synced_pos - world_pos; - let unrotated_delta = world_rot.inverse() * delta_pos; - local_transform.position = unrotated_delta / safe_world_scale; + local.scale *= delta_scale; } } @@ -200,14 +171,12 @@ impl<'a> EditorTabViewer<'a> { if was_focused && !cfg.is_focused { if let Some(original) = cfg.entity_transform_original { if original != *entity_transform { - UndoableAction::push_to_undo( - self.undo_stack, - UndoableAction::EntityTransform(*entity_id, original), - ); + self.undo_stack.push(UndoableAction::EntityTransform(*entity_id, original)); log::debug!("Pushed entity transform action to stack"); } } } + handled = true; } @@ -233,19 +202,18 @@ impl<'a> EditorTabViewer<'a> { { transform.position = new_transform.translation.into(); transform.rotation = new_transform.rotation.into(); - transform.scale = new_transform.scale.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; + || cfg.old_pos.scale != transform.scale; if transform_changed { - UndoableAction::push_to_undo( - self.undo_stack, - UndoableAction::Transform(*entity_id, cfg.old_pos), + self.undo_stack.push( + UndoableAction::Transform(*entity_id, cfg.old_pos) ); log::debug!("Pushed transform action to stack"); } @@ -255,8 +223,8 @@ impl<'a> EditorTabViewer<'a> { 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, 0.0, -1.0); - light.component.position = updated_transform.position; + 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(); } @@ -279,4 +247,4 @@ impl EditorTabDock for ViewportDock { fn display(viewer: &mut EditorTabViewer<'_>, ui: &mut egui::Ui) { viewer.viewport_tab(ui); } -} +} @@ -13,8 +13,10 @@ pub mod utils; 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::UIViewport, viewport::ViewportDock + EditorTabRegistry, asset_viewer::AssetViewerDock, build_console::BuildConsoleDock, dock::ConsoleDock, entity_list::EntityListDock, resource::ResourceInspectorDock, ui::viewport::UICanvas, viewport::ViewportDock }; +use crate::editor::ui::inspector::UIInspector; +use crate::editor::ui::widget_tree::UIWidgetTree; dropbear_engine::features! { pub mod features { @@ -29,5 +31,7 @@ pub fn register_docks(registry: &mut EditorTabRegistry) { registry.register::<ResourceInspectorDock>(); registry.register::<BuildConsoleDock>(); registry.register::<ConsoleDock>(); - registry.register::<UIViewport>(); + registry.register::<UICanvas>(); + registry.register::<UIInspector>(); + registry.register::<UIWidgetTree>(); } @@ -3,9 +3,11 @@ use crate::spawn::{PendingSpawn, push_pending_spawn}; use dropbear_engine::graphics::SharedGraphicsContext; use egui::Align2; use eucalyptus_core::camera::{CameraComponent, CameraType}; +use eucalyptus_core::scene::SceneEntity; use eucalyptus_core::scripting::{BuildStatus, build_jvm}; -use eucalyptus_core::states::{PROJECT}; +use eucalyptus_core::states::{Label, PROJECT}; use eucalyptus_core::{fatal, info, success, success_without_console, warn, warn_without_console}; +use std::collections::HashMap; use std::fs; use std::path::PathBuf; use std::sync::Arc; @@ -17,588 +19,623 @@ pub trait SignalController { impl SignalController for Editor { fn run_signal(&mut self, graphics: Arc<SharedGraphicsContext>) -> anyhow::Result<()> { - let local_signal: Option<Signal> = None; - let show = true; - - match std::mem::replace(&mut self.signal, Signal::None) { - Signal::None => { - // returns absolutely nothing because no signal is set. - 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 mut requeue = vec![]; + while let Some(signal) = self.signal.pop_front() { + let local_signal: Option<Signal> = None; + let show = true; + + match signal { + Signal::None => { + // returns absolutely nothing because no signal is set. + Ok::<(), anyhow::Error>(()) } - - let clipboard = clipboard.unwrap(); - if clipboard.division != division { - warn!("Cannot paste across different asset divisions"); - self.signal = Signal::None; - return Ok(()); + Signal::Copy(entities, parent_map) => { + requeue.push(Signal::Copy(entities, parent_map)); + Ok(()) + }, + Signal::AssetCopy { source, division } => { + self.asset_clipboard = Some(AssetClipboard { + source: source.clone(), + division: division, + }); + + Ok(()) } + Signal::AssetPaste { + target_dir, + division, + } => { + let clipboard = self.asset_clipboard.clone(); + if clipboard.is_none() { + warn!("Nothing copied to paste"); + + return Ok(()); + } - if !clipboard.source.is_file() { - warn!("Copied asset is not a file"); - self.signal = Signal::None; - return Ok(()); - } + let clipboard = clipboard.unwrap(); + if clipboard.division != division { + warn!("Cannot paste across different asset divisions"); + + return Ok(()); + } - if !target_dir.exists() { - warn!("Target directory does not exist"); - self.signal = Signal::None; - return Ok(()); - } + if !clipboard.source.is_file() { + warn!("Copied asset is not a file"); + + 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 !target_dir.exists() { + warn!("Target directory does not exist"); + + return Ok(()); + } + + let Some(file_name) = clipboard.source.file_name() else { + warn!("Unable to paste: invalid file name"); + + return Ok(()); + }; + + let target_path = target_dir.join(file_name); + if target_path.exists() { + warn!("Target already exists: {}", target_path.display()); + + return Ok(()); + } + + if let Err(err) = fs::copy(&clipboard.source, &target_path) { + warn!("Unable to paste file: {}", err); + + 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()); + + Ok(()) } + Signal::Paste(entities, parent_map) => { + log::debug!("Paste requested for {} entity(ies)", entities.len()); + + // Rename all entities to ensure uniqueness, tracking old→new label mapping. + let mut label_remap: HashMap<Label, Label> = HashMap::new(); + let mut renamed: Vec<SceneEntity> = Vec::new(); + for mut scene_entity in entities { + let new_label = Editor::unique_label_for_world( + self.world.as_ref(), + scene_entity.label.as_str(), + ); + label_remap.insert(scene_entity.label.clone(), new_label.clone()); + scene_entity.label = new_label; + renamed.push(scene_entity); + } - 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( - self.world.as_ref(), - scene_entity.label.as_str(), - ); - let spawn = PendingSpawn { - scene_entity: scene_entity.clone(), - handle: None, - }; - push_pending_spawn(spawn); - self.signal = Signal::Copy(scene_entity.clone()); - Ok(()) - } - Signal::Delete => { - if let Some(sel_e) = &self.selected_entity { - let is_viewport_cam = - if let Ok(c) = self.world.query_one::<&CameraComponent>(*sel_e).get() { - matches!(c.camera_type, CameraType::Debug) + // Rebuild parent map with renamed labels. + let renamed_parent_map: HashMap<Label, Label> = parent_map + .into_iter() + .filter_map(|(child, parent)| { + let new_child = label_remap.get(&child)?.clone(); + let new_parent = label_remap.get(&parent)?.clone(); + Some((new_child, new_parent)) + }) + .collect(); + + // Keep clipboard alive so the user can paste again. + self.signal.push_back(Signal::Copy(renamed.clone(), renamed_parent_map.clone())); + + // Queue a PendingSpawn for each entity, with parent_label set as needed. + for scene_entity in renamed { + let parent_label = renamed_parent_map.get(&scene_entity.label).cloned(); + push_pending_spawn(PendingSpawn { + scene_entity, + handle: None, + parent_label, + }); + } + Ok(()) + } + Signal::Delete => { + if let Some(sel_e) = &self.selected_entity { + let is_viewport_cam = + if let Ok(c) = self.world.query_one::<&CameraComponent>(*sel_e).get() { + matches!(c.camera_type, CameraType::Debug) + } else { + false + }; + if is_viewport_cam { + warn!("You can't delete the viewport camera"); + + Ok(()) } else { - false - }; - if is_viewport_cam { - warn!("You can't delete the viewport camera"); - self.signal = Signal::None; - Ok(()) + match self.world.despawn(*sel_e) { + Ok(_) => { + info!("Decimated entity"); + + Ok(()) + } + Err(e) => { + + fatal!("Failed to delete entity: {}", e); + Err(anyhow::anyhow!(e)) + } + } + } } else { - match self.world.despawn(*sel_e) { + // no entity has been selected, so all good + Ok(()) + } + } + Signal::Undo => { + if let Some(action) = self.undo_stack.pop() { + match action.undo(&mut self.world) { Ok(_) => { - info!("Decimated entity"); - self.signal = Signal::None; - Ok(()) + info!("Undid action"); } Err(e) => { - self.signal = Signal::None; - fatal!("Failed to delete entity: {}", e); - Err(anyhow::anyhow!(e)) + warn!("Failed to undo action: {}", e); } } + } else { + warn_without_console!("Nothing to undo"); + log::debug!("No undoable actions in stack"); } - } else { - // no entity has been selected, so all good + Ok(()) } - } - Signal::Undo => { - if let Some(action) = self.undo_stack.pop() { - match action.undo(&mut self.world) { - Ok(_) => { - info!("Undid action"); - } - Err(e) => { - warn!("Failed to undo action: {}", e); - } + Signal::Play => { + if matches!(self.editor_state, EditorState::Playing) { + log::warn!("Unable to play: already in playing mode"); + + return Err(anyhow::anyhow!("Unable to play: already in playing mode")); } - } else { - warn_without_console!("Nothing to undo"); - log::debug!("No undoable actions in stack"); - } - self.signal = Signal::None; - Ok(()) - } - Signal::Play => { - if matches!(self.editor_state, EditorState::Playing) { - log::warn!("Unable to play: already in playing mode"); - self.signal = Signal::None; - return Err(anyhow::anyhow!("Unable to play: already in playing mode")); - } - if matches!(self.editor_state, EditorState::Editing) { - log::debug!("Project save"); - match self.save_project_config() { - Ok(_) => {} - Err(e) => { - fatal!("Error saving project: {}", e); + if matches!(self.editor_state, EditorState::Editing) { + log::debug!("Project save"); + match self.save_project_config() { + Ok(_) => {} + Err(e) => { + fatal!("Error saving project: {}", e); + } } - } - log::debug!("Starting build process"); - let (tx, rx) = crossbeam_channel::unbounded(); - self.progress_rx = Some(rx); + log::debug!("Starting build process"); + let (tx, rx) = crossbeam_channel::unbounded(); + self.progress_rx = Some(rx); - self.build_logs.clear(); - self.build_progress = 0.0; - self.show_build_window = true; - self.last_build_error = None; + self.build_logs.clear(); + self.build_progress = 0.0; + self.show_build_window = true; + self.last_build_error = None; - let project_root = { - let cfg = PROJECT.read(); - cfg.project_path.clone() - }; + let project_root = { + let cfg = PROJECT.read(); + cfg.project_path.clone() + }; - let project_root = project_root.to_path_buf(); - let status_tx = tx.clone(); + let project_root = project_root.to_path_buf(); + let status_tx = tx.clone(); - let handle = graphics - .future_queue - .push(async move { build_jvm(project_root, status_tx).await }); + let handle = graphics + .future_queue + .push(async move { build_jvm(project_root, status_tx).await }); - log::debug!( - "Pushed future to future_queue, received handle: {:?}", - handle - ); + log::debug!( + "Pushed future to future_queue, received handle: {:?}", + handle + ); - self.handle_created = Some(handle); + self.handle_created = Some(handle); - self.editor_state = EditorState::Building; - log::debug!("Set editor state to EditorState::Building"); - } + self.editor_state = EditorState::Building; + log::debug!("Set editor state to EditorState::Building"); + } - if matches!(self.editor_state, EditorState::Building) { - #[cfg(not(target_os = "macos"))] - let ctrl_pressed = self - .input_state - .pressed_keys - .contains(&KeyCode::ControlLeft) - || self + if matches!(self.editor_state, EditorState::Building) { + #[cfg(not(target_os = "macos"))] + let ctrl_pressed = self + .input_state + .pressed_keys + .contains(&KeyCode::ControlLeft) + || self .input_state .pressed_keys .contains(&KeyCode::ControlRight); - #[cfg(target_os = "macos")] - let ctrl_pressed = self.input_state.pressed_keys.contains(&KeyCode::SuperLeft) - || self.input_state.pressed_keys.contains(&KeyCode::SuperRight); - - let alt_pressed = self.input_state.pressed_keys.contains(&KeyCode::AltLeft) - || self.input_state.pressed_keys.contains(&KeyCode::AltRight); - - // Ctrl+Alt+P skips build process and starts running, such as if using cached jar - if ctrl_pressed - && alt_pressed - && self.input_state.pressed_keys.contains(&KeyCode::KeyP) - { - if let Some(handle) = self.handle_created { - log::debug!("Cancelling build task due to manual intervention"); - graphics.future_queue.cancel(&handle); - } else { - log::warn!("No handle was created during this time. Weird...") - } + #[cfg(target_os = "macos")] + let ctrl_pressed = self.input_state.pressed_keys.contains(&KeyCode::SuperLeft) + || self.input_state.pressed_keys.contains(&KeyCode::SuperRight); + + let alt_pressed = self.input_state.pressed_keys.contains(&KeyCode::AltLeft) + || self.input_state.pressed_keys.contains(&KeyCode::AltRight); + + // Ctrl+Alt+P skips build process and starts running, such as if using cached jar + if ctrl_pressed + && alt_pressed + && self.input_state.pressed_keys.contains(&KeyCode::KeyP) + { + if let Some(handle) = self.handle_created { + log::debug!("Cancelling build task due to manual intervention"); + graphics.future_queue.cancel(&handle); + } else { + log::warn!("No handle was created during this time. Weird...") + } - let project_root = { - let cfg = PROJECT.read(); - cfg.project_path.clone() - }; - let libs_dir = project_root.join("build").join("libs"); - if !libs_dir.exists() { - let err = - "Build succeeded but 'build/libs' directory is missing".to_string(); - return Err(anyhow::anyhow!(err)); - } + let project_root = { + let cfg = PROJECT.read(); + cfg.project_path.clone() + }; + let libs_dir = project_root.join("build").join("libs"); + if !libs_dir.exists() { + let err = + "Build succeeded but 'build/libs' directory is missing".to_string(); + return Err(anyhow::anyhow!(err)); + } - let jar_files: Vec<PathBuf> = std::fs::read_dir(&libs_dir)? - .filter_map(|entry| entry.ok().map(|e| e.path())) - .filter(|path| { - path.extension() - .map_or(false, |ext| ext.eq_ignore_ascii_case("jar")) - && !path + let jar_files: Vec<PathBuf> = std::fs::read_dir(&libs_dir)? + .filter_map(|entry| entry.ok().map(|e| e.path())) + .filter(|path| { + path.extension() + .map_or(false, |ext| ext.eq_ignore_ascii_case("jar")) + && !path .file_name() .unwrap_or_default() .to_string_lossy() .contains("-sources") - && !path + && !path .file_name() .unwrap_or_default() .to_string_lossy() .contains("-javadoc") - }) - .collect(); + }) + .collect(); - if jar_files.is_empty() { - let err = "No JAR artifact found in 'build/libs'".to_string(); - return Err(anyhow::anyhow!(err)); - } + if jar_files.is_empty() { + let err = "No JAR artifact found in 'build/libs'".to_string(); + return Err(anyhow::anyhow!(err)); + } - let fat_jar = jar_files.iter().find(|path| { - path.file_name() - .and_then(|n| n.to_str()) - .map_or(false, |name| name.contains("-all")) - }); + let fat_jar = jar_files.iter().find(|path| { + path.file_name() + .and_then(|n| n.to_str()) + .map_or(false, |name| name.contains("-all")) + }); - let jar_path = if let Some(fat) = fat_jar { - fat.clone() - } else { - jar_files - .into_iter() - .max_by_key(|path| { - std::fs::metadata(path).map(|m| m.len()).unwrap_or(0) - }) - .unwrap() - }; + let jar_path = if let Some(fat) = fat_jar { + fat.clone() + } else { + jar_files + .into_iter() + .max_by_key(|path| { + std::fs::metadata(path).map(|m| m.len()).unwrap_or(0) + }) + .unwrap() + }; - info!("Using cached JAR: {}", jar_path.display()); + info!("Using cached JAR: {}", jar_path.display()); - self.show_build_window = false; - self.signal = Signal::None; + self.show_build_window = false; + - self.load_play_mode()?; - return Ok(()); - } + self.load_play_mode()?; + return Ok(()); + } - let mut local_handle_exchanged: Option<anyhow::Result<PathBuf>> = None; - if let Some(rx) = &self.progress_rx { - while let Ok(status) = rx.try_recv() { - match status { - BuildStatus::Started => { - self.build_logs.push("Build started...".to_string()); - self.build_progress = 0.1; - log::info!("Build started"); - } - BuildStatus::Building(msg) => { - log::info!("[BUILD] {}", msg); - self.build_logs.push(msg.clone()); - self.build_progress = (self.build_progress + 0.01).min(0.9); - } - BuildStatus::Completed => { - self.build_logs - .push("Build completed successfully!".to_string()); - self.build_progress = 1.0; - success_without_console!("Build completed"); - log::info!("Build completed successfully!"); - - if let Some(handle) = self.handle_created { - if let Some(result) = graphics - .future_queue - .exchange_owned_as::<anyhow::Result<PathBuf>>(&handle) - { - local_handle_exchanged = Some(result); + let mut local_handle_exchanged: Option<anyhow::Result<PathBuf>> = None; + if let Some(rx) = &self.progress_rx { + while let Ok(status) = rx.try_recv() { + match status { + BuildStatus::Started => { + self.build_logs.push("Build started...".to_string()); + self.build_progress = 0.1; + log::info!("Build started"); + } + BuildStatus::Building(msg) => { + log::info!("[BUILD] {}", msg); + self.build_logs.push(msg.clone()); + self.build_progress = (self.build_progress + 0.01).min(0.9); + } + BuildStatus::Completed => { + self.build_logs + .push("Build completed successfully!".to_string()); + self.build_progress = 1.0; + success_without_console!("Build completed"); + log::info!("Build completed successfully!"); + + if let Some(handle) = self.handle_created { + if let Some(result) = graphics + .future_queue + .exchange_owned_as::<anyhow::Result<PathBuf>>(&handle) + { + local_handle_exchanged = Some(result); + } + } else { + + self.show_build_window = false; + self.editor_state = EditorState::Editing; } - } else { - self.signal = Signal::None; - self.show_build_window = false; - self.editor_state = EditorState::Editing; } - } - BuildStatus::Failed(_e) => { - let error_msg = format!("Build failed, check logs"); - self.build_logs.push(error_msg.clone()); + BuildStatus::Failed(_e) => { + let error_msg = format!("Build failed, check logs"); + self.build_logs.push(error_msg.clone()); - self.build_progress = 0.0; - fatal!("Failed to build gradle, check logs"); + self.build_progress = 0.0; + fatal!("Failed to build gradle, check logs"); - self.signal = Signal::None; - self.show_build_window = false; - self.editor_state = EditorState::Editing; - // self.dock_state - // .push_to_focused_leaf(EditorTab::ErrorConsole); // getting too problematic + + self.show_build_window = false; + self.editor_state = EditorState::Editing; + // self.dock_state + // .push_to_focused_leaf(EditorTab::ErrorConsole); // getting too problematic + } } } } - } - - if self.show_build_window { - let mut window_open = true; - egui::Window::new("Building Project") - .collapsible(false) - .resizable(false) - .fixed_size([500.0, 400.0]) - .anchor(Align2::CENTER_CENTER, [0.0, 0.0]) - .open(&mut window_open) - .show(&graphics.get_egui_context(), |ui| { - ui.vertical_centered(|ui| { - ui.heading("Gradle Build Progress"); - ui.add_space(10.0); - - let progress_bar = egui::ProgressBar::new(self.build_progress) - .show_percentage() - .animate(true); - ui.add(progress_bar); - - ui.add_space(15.0); - ui.separator(); - ui.add_space(5.0); - - ui.heading("Build Log"); - ui.add_space(5.0); - - egui::ScrollArea::vertical() - .stick_to_bottom(true) - .max_height(200.0) - .auto_shrink([false, false]) - .show(ui, |ui| { - for log_line in &self.build_logs { - ui.label( - egui::RichText::new(log_line) - .family(egui::FontFamily::Monospace) - .size(12.0), - ); - } - - if !self.build_logs.is_empty() { - ui.add_space(10.0); - ui.label( - egui::RichText::new(format!( - "Total log entries: {}", - self.build_logs.len() - )) - .italics() - .color(egui::Color32::GRAY), - ); - ui.label( - "Tip: Press Ctrl+Alt+P to skip build and start running", - ); - } - }); - ui.add_space(10.0); + if self.show_build_window { + let mut window_open = true; + egui::Window::new("Building Project") + .collapsible(false) + .resizable(false) + .fixed_size([500.0, 400.0]) + .anchor(Align2::CENTER_CENTER, [0.0, 0.0]) + .open(&mut window_open) + .show(&graphics.get_egui_context(), |ui| { + ui.vertical_centered(|ui| { + ui.heading("Gradle Build Progress"); + ui.add_space(10.0); + + let progress_bar = egui::ProgressBar::new(self.build_progress) + .show_percentage() + .animate(true); + ui.add(progress_bar); + + ui.add_space(15.0); + ui.separator(); + ui.add_space(5.0); + + ui.heading("Build Log"); + ui.add_space(5.0); + + egui::ScrollArea::vertical() + .stick_to_bottom(true) + .max_height(200.0) + .auto_shrink([false, false]) + .show(ui, |ui| { + for log_line in &self.build_logs { + ui.label( + egui::RichText::new(log_line) + .family(egui::FontFamily::Monospace) + .size(12.0), + ); + } + + if !self.build_logs.is_empty() { + ui.add_space(10.0); + ui.label( + egui::RichText::new(format!( + "Total log entries: {}", + self.build_logs.len() + )) + .italics() + .color(egui::Color32::GRAY), + ); + ui.label( + "Tip: Press Ctrl+Alt+P to skip build and start running", + ); + } + }); + + ui.add_space(10.0); + }); }); - }); - if !window_open { - if let Some(handle) = self.handle_created { - log::warn!("Cancelling build task due to window close"); - graphics.future_queue.cancel(&handle); + if !window_open { + if let Some(handle) = self.handle_created { + log::warn!("Cancelling build task due to window close"); + graphics.future_queue.cancel(&handle); + } + + self.show_build_window = false; + self.handle_created = None; + self.progress_rx = None; + self.editor_state = EditorState::Editing; + } + } - self.show_build_window = false; + if let Some(result) = local_handle_exchanged { + log::debug!("Build future completed, processing result"); self.handle_created = None; self.progress_rx = None; - self.editor_state = EditorState::Editing; - self.signal = Signal::None; - } - } - - if let Some(result) = local_handle_exchanged { - log::debug!("Build future completed, processing result"); - self.handle_created = None; - self.progress_rx = None; - match result { - Ok(path) => { - log::debug!("Path is valid, JAR location as {}", path.display()); - success!("Build completed successfully!"); - self.show_build_window = false; - self.signal = Signal::None; + match result { + Ok(path) => { + log::debug!("Path is valid, JAR location as {}", path.display()); + success!("Build completed successfully!"); + self.show_build_window = false; + - self.load_play_mode()?; - } - Err(e) => { - let error_msg = format!("Build process error: {}", e); - self.build_logs.push(error_msg.clone()); - self.last_build_error = Some(self.build_logs.join("\n")); + self.load_play_mode()?; + } + Err(e) => { + let error_msg = format!("Build process error: {}", e); + self.build_logs.push(error_msg.clone()); + self.last_build_error = Some(self.build_logs.join("\n")); - fatal!("Failed to ready script manager interface because {}", e); - self.show_build_window = false; - self.show_build_error_window = true; - self.editor_state = EditorState::Editing; + fatal!("Failed to ready script manager interface because {}", e); + self.show_build_window = false; + self.show_build_error_window = true; + self.editor_state = EditorState::Editing; + } } } } - } - if self.show_build_error_window { - if let Some(error_log) = &self.last_build_error { - let mut window_open = true; - let mut close_clicked = false; - - egui::Window::new("Build Error") - .collapsible(true) - .resizable(false) - .fixed_size([700.0, 500.0]) - .anchor(Align2::CENTER_CENTER, [0.0, 0.0]) - .open(&mut window_open) - .show(&graphics.get_egui_context(), |ui| { - ui.vertical(|ui| { - ui.heading("Build Failed"); - ui.add_space(5.0); - ui.label("The Gradle build failed. See the error log below:"); - ui.add_space(10.0); - ui.separator(); - ui.add_space(10.0); - - egui::ScrollArea::both() - .auto_shrink([false, false]) - .max_height(300.0) - .show(ui, |ui| { - ui.add( - egui::TextEdit::multiline(&mut error_log.as_str()) - .font(egui::TextStyle::Monospace) - .desired_width(f32::INFINITY) - .desired_rows(20), - ); - }); - - ui.add_space(10.0); - - if ui.button("Close").clicked() { - close_clicked = true; - } + if self.show_build_error_window { + if let Some(error_log) = &self.last_build_error { + let mut window_open = true; + let mut close_clicked = false; + + egui::Window::new("Build Error") + .collapsible(true) + .resizable(false) + .fixed_size([700.0, 500.0]) + .anchor(Align2::CENTER_CENTER, [0.0, 0.0]) + .open(&mut window_open) + .show(&graphics.get_egui_context(), |ui| { + ui.vertical(|ui| { + ui.heading("Build Failed"); + ui.add_space(5.0); + ui.label("The Gradle build failed. See the error log below:"); + ui.add_space(10.0); + ui.separator(); + ui.add_space(10.0); + + egui::ScrollArea::both() + .auto_shrink([false, false]) + .max_height(300.0) + .show(ui, |ui| { + ui.add( + egui::TextEdit::multiline(&mut error_log.as_str()) + .font(egui::TextStyle::Monospace) + .desired_width(f32::INFINITY) + .desired_rows(20), + ); + }); + + ui.add_space(10.0); + + if ui.button("Close").clicked() { + close_clicked = true; + } + }); }); - }); - if !window_open || close_clicked { + if !window_open || close_clicked { + self.show_build_error_window = false; + } + } else { self.show_build_error_window = false; } - } else { - self.show_build_error_window = false; } - } - if matches!(self.editor_state, EditorState::Building) || self.show_build_error_window { - self.signal = Signal::Play; - } - Ok(()) - } - Signal::StopPlaying => { - self.editor_state = EditorState::Editing; - - if let Some(pid) = self.play_mode_pid { - log::debug!( - "Play mode requested to be exited, killing processes [{}]", - pid - ); - let _ = crate::process::kill_process(pid); + if matches!(self.editor_state, EditorState::Building) || self.show_build_error_window { + self.signal.push_back(Signal::Play); + } + Ok(()) } + Signal::StopPlaying => { + self.editor_state = EditorState::Editing; + + if let Some(pid) = self.play_mode_pid { + log::debug!( + "Play mode requested to be exited, killing processes [{}]", + pid + ); + let _ = crate::process::kill_process(pid); + } - self.play_mode_pid = None; - self.play_mode_exit_rx = None; + self.play_mode_pid = None; + self.play_mode_exit_rx = None; - success!("Exited play mode"); - log::info!("Back to the editor you go..."); + success!("Exited play mode"); + log::info!("Back to the editor you go..."); - self.signal = Signal::None; - Ok(()) - } - Signal::AddComponent(entity, component) => { - let registry = self.component_registry.clone(); - - let Some(component_id) = registry.id_for_component(component.as_ref()) else { - warn!( - "Failed to resolve component type for add request on entity {:?}", - entity - ); - self.signal = Signal::None; - return Ok(()); - }; - - if registry - .find_entities_by_numeric_id(&self.world, component_id) - .contains(&entity) - { - let component_name = registry - .get_descriptor_by_numeric_id(component_id) - .map(|desc| desc.type_name.as_str()) - .unwrap_or("Unknown"); - - warn!( - "Entity {:?} already has component '{}'", - entity, - component_name - ); - self.signal = Signal::None; - return Ok(()); + + Ok(()) } + Signal::AddComponent(entity, component) => { + let registry = self.component_registry.clone(); + + let Some(component_id) = registry.id_for_component(component.as_ref()) else { + warn!( + "Failed to resolve component type for add request on entity {:?}", + entity + ); + + return Ok(()); + }; - let graphics_clone = graphics.clone(); - let init_future = async move { - let Some(loader_future) = - registry.load_component(component.as_ref(), graphics_clone.clone()) - else { - return Err(anyhow::anyhow!( - "Component type is not registered in ComponentRegistry" - )); + if registry + .find_entities_by_numeric_id(&self.world, component_id) + .contains(&entity) + { + let component_name = registry + .get_descriptor_by_numeric_id(component_id) + .map(|desc| desc.type_name.as_str()) + .unwrap_or("Unknown"); + + warn!( + "Entity {:?} already has component '{}'", + entity, + component_name + ); + + return Ok(()); + } + + let graphics_clone = graphics.clone(); + let init_future = async move { + let Some(loader_future) = + registry.load_component(component.as_ref(), graphics_clone.clone()) + else { + return Err(anyhow::anyhow!( + "Component type is not registered in ComponentRegistry" + )); + }; + + loader_future.await }; + let handle = graphics.future_queue.push(init_future); + self.pending_components.push((entity, handle)); - loader_future.await - }; - let handle = graphics.future_queue.push(init_future); - self.pending_components.push((entity, handle)); + success!("Queued component addition for entity {:?}", entity); + + Ok(()) + } + Signal::RequestNewWindow(window_data) => { + use dropbear_engine::scene::SceneCommand; + self.scene_command = SceneCommand::RequestWindow(window_data.clone()); + + Ok(()) + } + Signal::UpdateViewportSize((x, y)) => { + let width = x.max(1.0).round() as u32; + let height = y.max(1.0).round() as u32; + let current_size = graphics.viewport_texture.size; + + if current_size.width != width || current_size.height != height { + self.scene_command = + dropbear_engine::scene::SceneCommand::ResizeViewport((width, height)); + } + + Ok(()) + } + Signal::ReloadWGPUData { skybox_texture } => { + self.main_render_pipeline = None; + self.light_cube_pipeline = None; + self.shader_globals = None; + self.collider_wireframe_pipeline = None; + self.mipmapper = None; + self.texture_id = None; + self.window = None; + self.sky_pipeline = None; + self.load_wgpu_nerdy_stuff(graphics.clone(), skybox_texture.as_ref()); - success!("Queued component addition for entity {:?}", entity); - self.signal = Signal::None; - Ok(()) - } - Signal::RequestNewWindow(window_data) => { - use dropbear_engine::scene::SceneCommand; - self.scene_command = SceneCommand::RequestWindow(window_data.clone()); - self.signal = Signal::None; - Ok(()) - } - Signal::UpdateViewportSize((x, y)) => { - let width = x.max(1.0).round() as u32; - let height = y.max(1.0).round() as u32; - let current_size = graphics.viewport_texture.size; - - if current_size.width != width || current_size.height != height { - self.scene_command = - dropbear_engine::scene::SceneCommand::ResizeViewport((width, height)); + Ok(()) } - self.signal = Signal::None; - Ok(()) - } - Signal::ReloadWGPUData { skybox_texture } => { - self.main_render_pipeline = None; - self.light_cube_pipeline = None; - self.shader_globals = None; - self.collider_wireframe_pipeline = None; - self.mipmapper = None; - self.texture_id = None; - self.window = None; - self.sky_pipeline = None; - self.load_wgpu_nerdy_stuff(graphics.clone(), skybox_texture.as_ref()); - - self.signal = Signal::None; + }?; + if !show { - Ok(()) } - }?; - if !show { - self.signal = Signal::None; + if let Some(signal) = local_signal { + self.signal.push_back(signal); + } } - if let Some(signal) = local_signal { - self.signal = signal; + + for s in requeue { + self.signal.push_front(s); } + Ok(()) } } @@ -4,11 +4,11 @@ use dropbear_engine::entity::{EntityTransform, MeshRenderer}; use dropbear_engine::future::{FutureHandle, FutureQueue}; use dropbear_engine::graphics::SharedGraphicsContext; use dropbear_engine::model::Model; -use eucalyptus_core::hierarchy::Parent; +use eucalyptus_core::hierarchy::{Hierarchy, Parent}; use eucalyptus_core::scene::SceneEntity; use eucalyptus_core::states::Label; use eucalyptus_core::{fatal, success, warn}; -use hecs::EntityBuilder; +use hecs::{Entity, EntityBuilder}; use parking_lot::Mutex; use std::sync::{Arc, LazyLock}; @@ -16,6 +16,8 @@ use std::sync::{Arc, LazyLock}; pub struct PendingSpawn { pub scene_entity: SceneEntity, pub handle: Option<FutureHandle>, + /// If set, the spawned entity will be reparented to the entity whose label matches this. + pub parent_label: Option<Label>, } pub static PENDING_SPAWNS: LazyLock<Mutex<Vec<PendingSpawn>>> = @@ -108,6 +110,22 @@ impl PendingSpawnController for Editor { self.world.insert_one(entity, EntityTransform::default()); } + // Attach to parent if requested. + if let Some(ref parent_label) = spawn.parent_label { + let parent_entity = self + .world + .query::<(&Label, Entity)>() + .iter() + .find_map(|(l, e)| { + if l == parent_label { Some(e) } else { None } + }); + if let Some(parent_entity) = parent_entity { + Hierarchy::set_parent(&mut self.world, entity, parent_entity); + } else { + log::warn!("Parent '{}' not found when spawning '{}'", parent_label.as_str(), label.as_str()); + } + } + success!("Spawned '{}' from pending queue", label); completed.push(index); } @@ -187,6 +205,7 @@ impl PendingSpawnController for Editor { } success!("Swapped MeshRenderer model for entity {:?}", entity); + self.selected_entity = Some(*entity); completed_swaps.push(index); } Ok(Err(err)) => { @@ -27,7 +27,7 @@ use eucalyptus_core::scene::loading::{IsSceneLoaded, SCENE_LOADER, SceneLoadResu use eucalyptus_core::states::SCENES; use eucalyptus_core::states::{Label, PROJECT}; use eucalyptus_core::ui::HUDComponent; -use glam::{DVec3, Mat4, Quat, Vec2, Vec3}; +use glam::{DVec3, Mat4, Quat, Vec2}; use hecs::Entity; use kino_ui::WidgetTree; use kino_ui::rendering::KinoRenderTargetId;