tirbofish/dropbear · commit
a0917a8410128ca2709ea918069d563b37003692
wip: finally got eucalyptus-core all smoothened out, need to fix up the editor now.
Signature present but could not be verified.
Unverified
@@ -1,13 +1,14 @@ -use dropbear_traits::SerializableComponent; +use dropbear_traits::{Component, ComponentDescriptor, ComponentInitContext, ComponentInitFuture, InsertBundle, SerializableComponent}; use std::collections::HashMap; use std::sync::Arc; +use egui::{CollapsingHeader, Ui}; use glam::Mat4; use wgpu::util::DeviceExt; -use dropbear_macro::SerializableComponent; use crate::graphics::SharedGraphicsContext; use crate::model::{AnimationInterpolation, ChannelValues, Model, NodeTransform}; +use std::any::Any; -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, SerializableComponent)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct AnimationComponent { #[serde(default)] pub active_animation_index: Option<usize>, @@ -31,6 +32,49 @@ pub struct AnimationComponent { pub bind_group: Option<wgpu::BindGroup>, } +impl Component for AnimationComponent { + type Serialized = AnimationComponent; + + fn static_descriptor() -> ComponentDescriptor { + ComponentDescriptor { + fqtn: "dropbear_engine::animation::AnimationComponent".to_string(), + type_name: "AnimationComponent".to_string(), + category: Some("Animation".to_string()), + description: Some("A component that allows playback of a component".to_string()), + } + } + + fn deserialize(serialized: &Self::Serialized) -> Self { + serialized.clone() + } + + fn serialize(&self) -> Self::Serialized { + self.clone() + } + + fn inspect(&mut self, ui: &mut Ui) { + CollapsingHeader::new("Animation").default_open(true).show(ui, |ui| { + ui.label("Active animation:"); + let mut enabled = self.active_animation_index.is_some(); + let mut value = self.active_animation_index.unwrap_or(0); + + ui.horizontal(|ui| { + if ui.checkbox(&mut enabled, "Enable").changed() { + self.active_animation_index = if enabled { Some(value) } else { None }; + } + + ui.add_enabled(enabled, egui::DragValue::new(&mut value)); + + if enabled && self.active_animation_index != Some(value) { + self.active_animation_index = Some(value); + } + }); + + // todo: complete some more + }); + } +} + impl Default for AnimationComponent { fn default() -> Self { Self { @@ -47,6 +91,26 @@ impl Default for AnimationComponent { } } +#[typetag::serde] +impl SerializableComponent for AnimationComponent { + fn as_any(&self) -> &dyn Any { + self + } + + fn clone_box(&self) -> Box<dyn SerializableComponent> { + Box::new(self.clone()) + } + + fn init(&self, _ctx: ComponentInitContext) -> ComponentInitFuture { + let value = self.clone(); + Box::pin(async move { + let insert: Box<dyn dropbear_traits::ComponentInsert> = + Box::new(InsertBundle((value,))); + Ok(insert) + }) + } +} + impl AnimationComponent { pub fn new() -> Self { Self::default() @@ -77,7 +141,6 @@ impl AnimationComponent { let count = channel.times.len(); if count == 0 { continue; } - // Handle out of bounds / single keyframe if count == 1 || self.time <= channel.times[0] { Self::apply_single_keyframe(channel, 0, &mut self.local_pose, model); continue; @@ -1,4 +1,3 @@ -use dropbear_traits::SerializableComponent; use glam::{DMat4, DQuat, DVec3, Mat4, Quat, Vec3}; use parking_lot::Mutex; use serde::{Deserialize, Serialize}; @@ -11,16 +10,18 @@ use std::{ use crate::{ asset::{ASSET_REGISTRY, AssetRegistry}, graphics::{Instance, SharedGraphicsContext}, - model::{Model}, - utils::ResourceReference, + model::Model, texture::Texture, + utils::{ResourceReference, ResourceReferenceType, EUCA_SCHEME}, }; use anyhow::anyhow; -use dropbear_macro::SerializableComponent; +use egui::{CollapsingHeader, Ui}; +use dropbear_traits::{Component, ComponentDescriptor, ComponentInitContext, ComponentInitFuture, InsertBundle, SerializableComponent}; +use std::any::Any; use crate::asset::Handle; /// 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, SerializableComponent)] +#[derive(Default, Debug, Deserialize, Serialize, Copy, PartialEq, Clone)] pub struct EntityTransform { local: Transform, world: Transform, @@ -77,6 +78,71 @@ impl EntityTransform { } } +impl Component for EntityTransform { + type Serialized = EntityTransform; + + fn static_descriptor() -> ComponentDescriptor { + ComponentDescriptor { + fqtn: "dropbear_engine::entity::EntityTransform".to_string(), + type_name: "EntityTransform".to_string(), + category: Some("Transform".to_string()), + description: Some("A component that allows the entity to be transformed both locally and globally".to_string()), + } + } + + fn deserialize(serialized: &Self::Serialized) -> Self { + serialized.clone() + } + + fn serialize(&self) -> Self::Serialized { + self.clone() + } + + fn inspect(&mut self, ui: &mut Ui) { + CollapsingHeader::new("Entity Transform") + .default_open(true) + .show(ui, |ui| { + ui.set_min_width(300.0); + + ui.group(|ui| { + ui.strong("Local Transform"); + ui.add_space(4.0); + + self.local.inspect(ui); + }); + + ui.add_space(8.0); + + ui.group(|ui| { + ui.strong("World Transform"); + ui.add_space(4.0); + + self.world.inspect(ui); + }); + }); + } +} + +#[typetag::serde] +impl SerializableComponent for EntityTransform { + fn as_any(&self) -> &dyn Any { + self + } + + fn clone_box(&self) -> Box<dyn SerializableComponent> { + Box::new(*self) + } + + fn init(&self, _ctx: ComponentInitContext) -> ComponentInitFuture { + let value = *self; + Box::pin(async move { + let insert: Box<dyn dropbear_traits::ComponentInsert> = + Box::new(InsertBundle((value,))); + Ok(insert) + }) + } +} + /// A type that represents a position, rotation and scale of an entity /// /// This type is the most primitive model, as it implements most traits. @@ -159,6 +225,118 @@ impl Transform { pub fn scale(&mut self, scale: DVec3) { self.scale *= scale; } + + fn inspect(&mut self, ui: &mut Ui) { + ui.horizontal(|ui| { + ui.label("Position:"); + }); + ui.horizontal(|ui| { + ui.colored_label(egui::Color32::from_rgb(200, 80, 80), "X:"); + ui.add(egui::DragValue::new(&mut self.position.x) + .speed(0.1) + .fixed_decimals(2)); + + ui.colored_label(egui::Color32::from_rgb(80, 200, 80), "Y:"); + ui.add(egui::DragValue::new(&mut self.position.y) + .speed(0.1) + .fixed_decimals(2)); + + ui.colored_label(egui::Color32::from_rgb(80, 120, 220), "Z:"); + ui.add(egui::DragValue::new(&mut self.position.z) + .speed(0.1) + .fixed_decimals(2)); + }); + + ui.add_space(4.0); + + ui.horizontal(|ui| { + 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; + + 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 + ui.horizontal(|ui| { + ui.label("Scale:"); + }); + + let mut uniform_scale = self.scale.x == self.scale.y + && self.scale.y == self.scale.z; + + ui.horizontal(|ui| { + if ui.checkbox(&mut uniform_scale, "Uniform").changed() { + if uniform_scale { + let avg = (self.scale.x + self.scale.y + self.scale.z) / 3.0; + self.scale = DVec3::splat(avg); + } + } + }); + + if uniform_scale { + ui.horizontal(|ui| { + ui.label("XYZ:"); + if ui.add(egui::DragValue::new(&mut self.scale.x) + .speed(0.01) + .fixed_decimals(3)).changed() + { + self.scale = DVec3::splat(self.scale.x); + } + }); + } else { + ui.horizontal(|ui| { + ui.colored_label(egui::Color32::from_rgb(200, 80, 80), "X:"); + ui.add(egui::DragValue::new(&mut self.scale.x) + .speed(0.01) + .fixed_decimals(3)); + + ui.colored_label(egui::Color32::from_rgb(80, 200, 80), "Y:"); + ui.add(egui::DragValue::new(&mut self.scale.y) + .speed(0.01) + .fixed_decimals(3)); + + ui.colored_label(egui::Color32::from_rgb(80, 120, 220), "Z:"); + ui.add(egui::DragValue::new(&mut self.scale.z) + .speed(0.01) + .fixed_decimals(3)); + }); + } + } } #[derive(Clone)] @@ -170,7 +348,7 @@ pub struct MeshRenderer { import_scale: f32, handle: Handle<Model>, - instance: Instance, + pub instance: Instance, previous_matrix: DMat4, texture_override: Option<Handle<Texture>>, } @@ -282,6 +460,81 @@ impl MeshRenderer { } } +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +pub struct SerializedMeshRenderer { + pub handle: ResourceReference, + pub import_scale: Option<f32>, + pub texture_override: Option<ResourceReference>, +} + +impl Component for MeshRenderer { + type Serialized = SerializedMeshRenderer; + + fn static_descriptor() -> ComponentDescriptor { + ComponentDescriptor { + fqtn: "dropbear_engine::entity::MeshRenderer".to_string(), + type_name: "MeshRenderer".to_string(), + category: Some("Meshes".to_string()), + description: Some("Renders a 3D model".to_string()), + } + } + + fn deserialize(serialized: &Self::Serialized) -> Self { + let handle = match serialized.handle.ref_type { + ResourceReferenceType::Unassigned { id } => Handle::new(id), + _ => Handle::NULL, + }; + + let mut renderer = MeshRenderer::from_handle(handle); + if let Some(scale) = serialized.import_scale { + renderer.set_import_scale(scale); + } + + renderer + } + + fn serialize(&self) -> Self::Serialized { + let handle = self.model(); + let handle_ref = if handle.is_null() { + ResourceReference::from_reference(ResourceReferenceType::Unassigned { id: handle.id }) + } else { + let registry = ASSET_REGISTRY.read(); + registry + .get_model(handle) + .map(|model| model.path.clone()) + .unwrap_or_else(|| { + ResourceReference::from_reference(ResourceReferenceType::Unassigned { id: handle.id }) + }) + }; + + let texture_override = self.texture_override().map(|handle| { + let registry = ASSET_REGISTRY.read(); + let label = registry.get_label_from_texture_handle(handle); + let reference = label.and_then(|value| { + if value.starts_with(EUCA_SCHEME) { + Some(ResourceReference::from_reference(ResourceReferenceType::File(value))) + } else { + None + } + }); + + reference.unwrap_or_else(|| { + ResourceReference::from_reference(ResourceReferenceType::Unassigned { id: handle.id }) + }) + }); + + SerializedMeshRenderer { + handle: handle_ref, + import_scale: Some(self.import_scale()), + texture_override, + } + } + + fn inspect(&mut self, ui: &mut Ui) { + let _ = ui; + } +} + #[repr(C)] #[derive(Debug, Copy, Clone, Default, bytemuck::Pod, bytemuck::Zeroable)] pub struct ModelUniform { @@ -1,4 +1,4 @@ -use crate::attenuation::{Attenuation, RANGE_50}; +use crate::attenuation::{Attenuation, RANGE_50, ATTENUATION_PRESETS}; use crate::buffer::{ResizableBuffer, UniformBuffer}; use crate::graphics::SharedGraphicsContext; use crate::pipelines::light_cube::InstanceInput; @@ -6,12 +6,12 @@ use crate::{ entity::Transform, model::Model, }; -use dropbear_macro::SerializableComponent; -use dropbear_traits::SerializableComponent; use glam::{DMat4, DVec3}; use std::fmt::{Display, Formatter}; use std::sync::Arc; +use egui::{CollapsingHeader, Ui}; use wgpu::{BindGroup}; +use dropbear_traits::{Component, ComponentDescriptor}; use crate::asset::{Handle, ASSET_REGISTRY}; use crate::model::Material; use crate::procedural::{ProcObj, ProcedurallyGeneratedObject}; @@ -120,7 +120,7 @@ impl From<LightType> for u32 { } } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, SerializableComponent)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct LightComponent { #[serde(default)] pub position: DVec3, // point, spot @@ -178,6 +178,112 @@ impl Default for LightComponent { } } +impl Component for LightComponent { + type Serialized = LightComponent; + + fn static_descriptor() -> ComponentDescriptor { + ComponentDescriptor { + fqtn: "dropbear_engine::lighting::LightComponent".to_string(), + type_name: "LightComponent".to_string(), + category: Some("Lighting".to_string()), + description: Some("Light parameters used by the renderer".to_string()), + } + } + + fn deserialize(serialized: &Self::Serialized) -> Self { + serialized.clone() + } + + fn serialize(&self) -> Self::Serialized { + self.clone() + } + + fn inspect(&mut self, ui: &mut Ui) { + CollapsingHeader::new("Light") + .default_open(true) + .show(ui, |ui| { + egui::ComboBox::from_id_salt("light_type") + .selected_text(self.light_type.to_string()) + .show_ui(ui, |ui| { + ui.selectable_value( + &mut self.light_type, + LightType::Directional, + "Directional", + ); + ui.selectable_value(&mut self.light_type, LightType::Point, "Point"); + ui.selectable_value(&mut self.light_type, LightType::Spot, "Spot"); + }); + + ui.separator(); + + let mut colour = self.colour.as_vec3().to_array(); + ui.horizontal(|ui| { + ui.label("Colour"); + egui::color_picker::color_edit_button_rgb(ui, &mut colour); + }); + self.colour = glam::Vec3::from_array(colour).as_dvec3(); + + ui.horizontal(|ui| { + ui.label("Intensity"); + ui.add(egui::Slider::new(&mut self.intensity, 0.0..=10.0)); + }); + + ui.horizontal(|ui| { + ui.checkbox(&mut self.enabled, "Enabled"); + ui.checkbox(&mut self.visible, "Visible"); + }); + + let is_point = matches!(self.light_type, LightType::Point); + let is_spot = matches!(self.light_type, LightType::Spot); + + if is_point || is_spot { + ui.separator(); + egui::ComboBox::from_id_salt("attenuation_range") + .selected_text(format!("Range {}", self.attenuation.range)) + .show_ui(ui, |ui| { + for (preset, label) in ATTENUATION_PRESETS { + ui.selectable_value(&mut self.attenuation, *preset, *label); + } + }); + } + + if is_spot { + ui.separator(); + ui.add( + egui::Slider::new(&mut self.cutoff_angle, 1.0..=89.0) + .text("Inner") + .suffix("\u{00B0}") + .step_by(0.1), + ); + ui.add( + egui::Slider::new(&mut self.outer_cutoff_angle, 1.0..=90.0) + .text("Outer") + .suffix("\u{00B0}") + .step_by(0.1), + ); + + if self.outer_cutoff_angle <= self.cutoff_angle { + self.outer_cutoff_angle = self.cutoff_angle + 1.0; + } + } + + ui.separator(); + ui.label("Shadows"); + ui.checkbox(&mut self.cast_shadows, "Cast Shadows"); + ui.horizontal(|ui| { + ui.label("Depth"); + ui.add(egui::DragValue::new(&mut self.depth.start).speed(0.1)); + ui.label(".."); + ui.add(egui::DragValue::new(&mut self.depth.end).speed(0.1)); + }); + + if self.depth.end < self.depth.start { + self.depth.end = self.depth.start; + } + }); + } +} + impl LightComponent { pub fn default_direction() -> DVec3 { let dir = DVec3::new(-0.35, -1.0, -0.25); @@ -22,7 +22,7 @@ use wgpu::{BufferAddress, VertexAttribute, VertexBufferLayout, util::DeviceExt, #[derive(Clone)] pub struct Model { - pub(crate) hash: u64, // also the id related to the handle + pub hash: u64, // also the id related to the handle pub label: String, pub path: ResourceReference, pub meshes: Vec<Mesh>, @@ -9,88 +9,6 @@ use syn::{ }; use std::path::Path; -/// A `derive` macro that converts a struct to a usable [SerializableComponent]. -/// -/// You have to implement `serde::Serialize`, `serde::Deserialize` and `Clone` for the -/// struct to be usable (it will throw errors anyway). -/// -/// # Usage -/// ``` -/// use dropbear_macro::SerializableComponent; -/// -/// #[derive(Serialize, Deserialize, Clone, SerializableComponent)] // required to be implemented -/// struct MyComponent { -/// value1: String, -/// value2: i32, -/// } -/// ``` -#[proc_macro_derive(SerializableComponent)] -pub fn derive_component(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as DeriveInput); - let name = &input.ident; - let name_str = name.to_string(); - - let expanded = quote! { - #[typetag::serde] - impl SerializableComponent for #name { - fn as_any(&self) -> &dyn std::any::Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn std::any::Any { - self - } - - fn clone_boxed(&self) -> Box<dyn SerializableComponent> { - Box::new(self.clone()) - } - - fn type_name(&self) -> &'static str { - #name_str - } - } - }; - - TokenStream::from(expanded) -} - -/// Converts a module's functions into C API compatible functions. -/// -/// Each function must require a return type of `DropbearNativeResult<T>`. If the return type -/// is not `DropbearNativeResult<T>`, it is recommended that you move it to another module (such as `super::shared`). -/// -/// A function like that of: -/// ``` -/// pub fn dropbear_mesh_renderer_exists_for_entity( -/// world_ptr: *mut hecs::World, -/// entity_id: u64, -/// ) -> DropbearNativeResult<bool> {...} -/// ``` -/// will get converted to something like: -/// ``` -/// pub unsafe extern "C" fn dropbear_mesh_renderer_exists_for_entity(world_ptr: WorldPtr, entity_id: u64, out_result: *mut bool) -> i32 {...} -/// ``` -#[proc_macro_attribute] -pub fn impl_c_api(_attr: TokenStream, item: TokenStream) -> TokenStream { - let mut module = parse_macro_input!(item as ItemMod); - - let mut new_content = Vec::new(); - - if let Some((brace, content)) = module.content { - for item in content { - match item { - Item::Fn(func) => { - new_content.push(Item::Fn(transform_function(func))); - } - _ => new_content.push(item), - } - } - module.content = Some((brace, new_content)); - } - - TokenStream::from(quote! { #module }) -} - struct ExportArgs { c: Option<CArgs>, kotlin: Option<KotlinArgs>, @@ -7,8 +7,8 @@ repository.workspace = true readme = "README.md" [dependencies] -typetag.workspace = true +serde.workspace = true +egui.workspace = true +anyhow.workspace = true hecs.workspace = true -log.workspace = true -dyn-hash.workspace = true -anyhow.workspace = true +typetag.workspace = true @@ -1,262 +1,133 @@ -pub mod registry; - -use anyhow::{Result, anyhow}; -use hecs::{Entity, EntityBuilder, World}; use std::any::{Any, TypeId}; -use std::fmt::Debug; - -/// A type of component that gets serialized and deserialized into a scene config file. -#[typetag::serde(tag = "type")] -pub trait SerializableComponent: Send + Sync + Debug { - /// Converts a [SerializableComponent] to an [Any] type. - fn as_any(&self) -> &dyn Any; - /// Converts a [SerializableComponent] to a mutable [Any] type - fn as_any_mut(&mut self) -> &mut dyn Any; - /// Fetches the type name of that component - fn type_name(&self) -> &'static str; - /// Allows you to clone the dynamic object. - fn clone_boxed(&self) -> Box<dyn SerializableComponent>; - - /// Returns the display name of the component. - fn display_name(&self) -> String { - let type_name = self.type_name(); - type_name - .split("::") - .last() - .unwrap_or(type_name) - .to_string() - } -} - -impl Clone for Box<dyn SerializableComponent> { - fn clone(&self) -> Self { - self.clone_boxed() - } - - fn clone_from(&mut self, source: &Self) { - *self = source.clone_boxed(); - } -} - -pub trait ComponentConverter: Send + Sync { - fn type_id(&self) -> TypeId; - fn type_name(&self) -> &'static str; - fn serializable_type_id(&self) -> TypeId; +use std::collections::HashMap; +use std::future::Future; +use std::pin::Pin; +use hecs::{Entity, World, DynamicBundle}; +use std::sync::Arc; +use serde::{Deserialize, Serialize}; - fn extract_serializable( - &self, - world: &World, - entity: Entity, - ) -> Option<Box<dyn SerializableComponent>>; - - fn remove_component(&self, world: &mut World, entity: Entity); +pub struct ComponentDescriptor { + pub fqtn: String, + pub type_name: String, + pub category: Option<String>, + pub description: Option<String>, } -pub trait ComponentDeserializer: Send + Sync { - fn serializable_type_id(&self) -> TypeId; - fn serializable_type_name(&self) -> &'static str; - - fn insert_into_builder( - &self, - component: &dyn SerializableComponent, - builder: &mut EntityBuilder, - ) -> Result<()>; +pub struct ComponentRepository { + components: HashMap<TypeId, ComponentDescriptor>, } -struct DirectConverter<T: SerializableComponent + hecs::Component + Clone> { - _phantom: std::marker::PhantomData<T>, -} - -impl<T: SerializableComponent + hecs::Component + Clone + 'static> DirectConverter<T> { - fn new() -> Self { +impl ComponentRepository { + pub fn new() -> Self { Self { - _phantom: std::marker::PhantomData, + components: Default::default(), } } -} -impl<T: SerializableComponent + hecs::Component + Clone + 'static> ComponentConverter - for DirectConverter<T> -{ - fn type_id(&self) -> TypeId { - TypeId::of::<T>() - } + pub fn register<T: Component + 'static>(&mut self) { + let type_id = TypeId::of::<T>(); - fn type_name(&self) -> &'static str { - std::any::type_name::<T>() - } + let descriptor = T::static_descriptor(); - fn serializable_type_id(&self) -> TypeId { - TypeId::of::<T>() + self.components.insert(type_id, descriptor); } - fn extract_serializable( - &self, - world: &World, - entity: Entity, - ) -> Option<Box<dyn SerializableComponent>> { - if let Ok(ty) = world.query_one::<&T>(entity).get() - { - return Some(ty.clone_boxed()); - } - None + pub fn get_descriptor<T: Component + 'static>(&self) -> Option<&ComponentDescriptor> { + self.components.get(&TypeId::of::<T>()) } - fn remove_component(&self, world: &mut World, entity: Entity) { - let _ = world.remove_one::<T>(entity); + pub fn iter(&self) -> impl Iterator<Item = (&TypeId, &ComponentDescriptor)> { + self.components.iter() } -} - -/// Custom converter that has special logic for converting `T` to a [`SerializableComponent`] -struct CustomConverter<From, To, F> -where - From: hecs::Component, - To: SerializableComponent, - F: Fn(&World, Entity, &From) -> Option<To> + Send + Sync, -{ - converter_fn: F, - _phantom: std::marker::PhantomData<(From, To)>, -} -impl<From, To, F> CustomConverter<From, To, F> -where - From: hecs::Component + 'static, - To: SerializableComponent + 'static, - F: Fn(&World, Entity, &From) -> Option<To> + Send + Sync, -{ - fn new(converter_fn: F) -> Self { - Self { - converter_fn, - _phantom: std::marker::PhantomData, - } + pub fn descriptors(&self) -> impl Iterator<Item = &ComponentDescriptor> { + self.components.values() } } -impl<From, To, F> ComponentConverter for CustomConverter<From, To, F> -where - From: hecs::Component + 'static, - To: SerializableComponent + 'static, - F: Fn(&World, Entity, &From) -> Option<To> + Send + Sync, -{ - fn type_id(&self) -> TypeId { - TypeId::of::<From>() - } +pub trait Component { + type Serialized: Serialize + for<'de> Deserialize<'de>; - fn type_name(&self) -> &'static str { - std::any::type_name::<From>() - } + fn static_descriptor() -> ComponentDescriptor; - fn serializable_type_id(&self) -> TypeId { - TypeId::of::<To>() - } - - fn extract_serializable( - &self, - world: &World, - entity: Entity, - ) -> Option<Box<dyn SerializableComponent>> { - let component = world.get::<&From>(entity).ok()?; - (self.converter_fn)(world, entity, &component) - .map(|converted| Box::new(converted) as Box<dyn SerializableComponent>) - } - - fn remove_component(&self, world: &mut World, entity: Entity) { - let _ = world.remove_one::<From>(entity); - } + fn deserialize(serialized: &Self::Serialized) -> Self; + fn serialize(&self) -> Self::Serialized; + fn inspect(&mut self, ui: &mut egui::Ui); } -struct DirectDeserializer<T: SerializableComponent + hecs::Component + Clone> { - _phantom: std::marker::PhantomData<T>, +pub mod registry; + +pub struct ComponentResources { + map: HashMap<TypeId, Box<dyn Any + Send + Sync>>, } -impl<T: SerializableComponent + hecs::Component + Clone + 'static> DirectDeserializer<T> { +impl ComponentResources { pub fn new() -> Self { Self { - _phantom: std::marker::PhantomData, + map: HashMap::new(), } } -} -impl<T: SerializableComponent + hecs::Component + Clone + 'static> ComponentDeserializer - for DirectDeserializer<T> -{ - fn serializable_type_id(&self) -> TypeId { - TypeId::of::<T>() + pub fn insert<T: Any + Send + Sync>(&mut self, value: T) { + self.map.insert(TypeId::of::<T>(), Box::new(value)); } - fn serializable_type_name(&self) -> &'static str { - std::any::type_name::<T>() + pub fn get<T: Any + Send + Sync>(&self) -> Option<&T> { + self.map + .get(&TypeId::of::<T>()) + .and_then(|boxed| boxed.downcast_ref::<T>()) } - fn insert_into_builder( - &self, - component: &dyn SerializableComponent, - builder: &mut EntityBuilder, - ) -> Result<()> { - let typed = component.as_any().downcast_ref::<T>().ok_or_else(|| { - anyhow!( - "Component '{}' does not match registered type '{}'", - component.type_name(), - std::any::type_name::<T>() - ) - })?; - builder.add(typed.clone()); - Ok(()) + pub fn get_mut<T: Any + Send + Sync>(&mut self) -> Option<&mut T> { + self.map + .get_mut(&TypeId::of::<T>()) + .and_then(|boxed| boxed.downcast_mut::<T>()) } } -struct CustomDeserializer<From, To, F> -where - From: SerializableComponent, - To: hecs::Component, - F: Fn(&From) -> To + Send + Sync, -{ - converter_fn: F, - _phantom: std::marker::PhantomData<(From, To)>, +#[derive(Clone)] +pub struct ComponentInitContext { + pub entity: Entity, + pub resources: Arc<ComponentResources>, } -impl<From, To, F> CustomDeserializer<From, To, F> -where - From: SerializableComponent + 'static, - To: hecs::Component + Clone + 'static, - F: Fn(&From) -> To + Send + Sync + 'static, -{ - pub fn new(converter_fn: F) -> Self { - Self { - converter_fn, - _phantom: std::marker::PhantomData, - } - } +pub type ComponentInitFuture = Pin<Box<dyn Future<Output = anyhow::Result<Box<dyn ComponentInsert>>> + Send + 'static>>; + +pub trait ComponentInsert: Send { + fn insert(self: Box<Self>, world: &mut World, entity: Entity) -> anyhow::Result<()>; } -impl<From, To, F> ComponentDeserializer for CustomDeserializer<From, To, F> -where - From: SerializableComponent + 'static, - To: hecs::Component + Clone + 'static, - F: Fn(&From) -> To + Send + Sync + 'static, -{ - fn serializable_type_id(&self) -> TypeId { - TypeId::of::<From>() +pub struct InsertBundle<T: DynamicBundle + Send + 'static>(pub T); + +impl<T: DynamicBundle + Send + 'static> ComponentInsert for InsertBundle<T> { + fn insert(self: Box<Self>, world: &mut World, entity: Entity) -> anyhow::Result<()> { + world.insert(entity, self.0).map_err(|e| anyhow::anyhow!(e.to_string()))?; + Ok(()) } +} - fn serializable_type_name(&self) -> &'static str { - std::any::type_name::<From>() +#[typetag::serde(tag = "type")] +pub trait SerializableComponent: Send + Sync { + fn as_any(&self) -> &dyn Any; + fn clone_box(&self) -> Box<dyn SerializableComponent>; + + fn type_name(&self) -> &'static str { + std::any::type_name::<Self>() } - fn insert_into_builder( - &self, - component: &dyn SerializableComponent, - builder: &mut EntityBuilder, - ) -> Result<()> { - let typed = component.as_any().downcast_ref::<From>().ok_or_else(|| { - anyhow!( - "Component '{}' cannot be deserialized by '{}'", - component.type_name(), - std::any::type_name::<From>() - ) - })?; - let rebuild = (self.converter_fn)(typed); - builder.add(rebuild); - Ok(()) + fn display_name(&self) -> String { + self.type_name() + .split("::") + .last() + .unwrap_or(self.type_name()) + .to_string() } + + fn init(&self, ctx: ComponentInitContext) -> ComponentInitFuture; } + +impl Clone for Box<dyn SerializableComponent> { + fn clone(&self) -> Self { + self.clone_box() + } +} @@ -1,367 +1,147 @@ -use crate::{ - ComponentConverter, ComponentDeserializer, CustomConverter, CustomDeserializer, - DirectConverter, DirectDeserializer, SerializableComponent, -}; -use anyhow::Result; -use hecs::{Entity, EntityBuilder, World}; use std::any::TypeId; use std::collections::HashMap; -// note: to anyone viewing this, yes i did use AI to generate the documentation for this module because -// sometimes i kept forgetting what function some did. mb +use hecs::{Entity, World}; + +use crate::SerializableComponent; -/// Registry of conversions between ECS components (`hecs`) and [`SerializableComponent`] -/// values. -/// -/// # What this type does -/// -/// `ComponentRegistry` is an adapter layer around a `hecs::World` that lets you: -/// -/// - **Extract** one or more [`SerializableComponent`] values from an entity. -/// - **Deserialize** a [`SerializableComponent`] back into a `hecs` component -/// and insert it into an [`EntityBuilder`]. -/// - **Address component "kinds" by numeric IDs** (useful for editor UI, network -/// messages, prefab formats, etc.). -/// - **Provide default/factory construction** for editor "Add component" flows. -/// -/// # Numeric IDs and stability -/// -/// Component IDs are assigned lazily (on registration) starting from `1`. -/// -/// - IDs are **stable only for the lifetime of this registry instance**. -/// - IDs are **not guaranteed to be stable across runs** (or across different -/// registration orders), because they're assigned incrementally. -/// - Display / editor lists returned by [`iter_available_components`] will be in -/// **arbitrary order**, because `HashMap` iteration order is not deterministic. -/// -/// If you need cross-run stable identifiers (e.g., long-lived save files), you -/// should layer an explicit, user-assigned ID scheme on top. -/// -/// # Converters vs deserializers -/// -/// A **converter** looks at an entity and tries to produce a serializable value. -/// A **deserializer** takes a serializable value and inserts a real ECS component -/// into an [`EntityBuilder`]. -/// -/// For directly-serializable components, [`register`] wires up both. -/// For custom flows, [`register_converter`] and [`register_deserializer`] can be -/// used independently. pub struct ComponentRegistry { - converters: HashMap<TypeId, Box<dyn ComponentConverter>>, - deserializers: HashMap<TypeId, Box<dyn ComponentDeserializer>>, - serializable_ids: HashMap<TypeId, u64>, - id_to_serializable: HashMap<u64, TypeId>, - default_creators: HashMap<u64, Box<dyn Fn() -> Box<dyn SerializableComponent> + Send + Sync>>, - next_component_id: u64, + next_id: u32, + entries: HashMap<u32, ComponentEntry>, + type_to_id: HashMap<TypeId, u32>, + converters: Vec<ConverterEntry>, +} + +struct ComponentEntry { + type_id: TypeId, + fqtn: &'static str, + display_name: String, + create_default: Option<fn() -> Box<dyn SerializableComponent>>, + extract_fn: Option<fn(&World, Entity) -> Option<Box<dyn SerializableComponent>>>, + remove_fn: Option<fn(&mut World, Entity)>, +} + +struct ConverterEntry { + from_type: TypeId, + convert_fn: Box<dyn Fn(&World, Entity) -> Option<Box<dyn SerializableComponent>> + Send + Sync>, } impl ComponentRegistry { - /// Creates an empty registry. - /// - /// No components can be extracted or deserialized until they are registered. pub fn new() -> Self { Self { - converters: HashMap::new(), - deserializers: HashMap::new(), - serializable_ids: HashMap::new(), - id_to_serializable: HashMap::new(), - default_creators: HashMap::new(), - next_component_id: 1, + next_id: 1, + entries: HashMap::new(), + type_to_id: HashMap::new(), + converters: Vec::new(), } } - /// Registers a component type that is already a [`SerializableComponent`]. - /// - /// This is the common case: `T` is both a `hecs` component and a serializable - /// value. The registry will: - /// - /// - Assign a numeric ID to `T` (if it doesn't already have one). - /// - Register a direct converter (extract `T` from an entity and clone it). - /// - Register a direct deserializer (insert a cloned `T` into a builder). - /// - /// Note: numeric IDs are assigned based on registration order; see the type - /// docs for stability caveats. - pub fn register<T>(&mut self) - where - T: SerializableComponent + hecs::Component + Clone + 'static, - { - let type_id = TypeId::of::<T>(); - self.ensure_serializable_id(type_id); - self.converters - .insert(type_id, Box::new(DirectConverter::<T>::new())); - self.deserializers - .insert(type_id, Box::new(DirectDeserializer::<T>::new())); - } - - /// Registers `T` and also exposes it as an "available" component with a - /// default constructor. - /// - /// This is primarily meant for editor tooling: values registered via this - /// method will appear in [`iter_available_components`] and can be created via - /// [`create_default_component`]. pub fn register_with_default<T>(&mut self) where - T: SerializableComponent + hecs::Component + Clone + Default + 'static, - { - self.register::<T>(); - let id = self.id_for_type::<T>().unwrap(); - self.default_creators - .insert(id, Box::new(|| Box::new(T::default()))); - } - - /// Registers `T` for extraction/deserialization (if needed) and associates a - /// custom factory used to create new instances of `T`. - /// - /// Like [`register_with_default`], this is intended for editor tooling. - /// Use this when the best "empty" value can't be expressed as `Default`. - pub fn register_factory<T, F>(&mut self, factory: F) - where - T: SerializableComponent + hecs::Component + Clone + 'static, - F: Fn() -> Box<dyn SerializableComponent> + Send + Sync + 'static, + T: SerializableComponent + Default + Clone + 'static, { - // Ensure registered first - if self.id_for_type::<T>().is_none() { - self.register::<T>(); - } - let id = self.id_for_type::<T>().unwrap(); - self.default_creators.insert(id, Box::new(factory)); - } - - /// Creates a new component instance using the default constructor/factory - /// registered for `component_id`. - /// - /// Returns `None` if no factory/default was registered for that ID. - pub fn create_default_component( - &self, - component_id: u64, - ) -> Option<Box<dyn SerializableComponent>> { - self.default_creators.get(&component_id).map(|f| f()) - } - - /// Removes the (source) ECS component associated with the given numeric ID - /// from `entity`. - /// - /// The registry resolves `component_id` to a *serializable* type, then finds - /// the first registered converter whose output type matches and asks it to - /// remove the underlying ECS component. - /// - /// ## Notes / edge cases - /// - /// - If no ID mapping exists or no converter matches, this is a no-op. - /// - If multiple converters map to the same serializable type, only the first - /// match (in arbitrary `HashMap` order) will be used. - pub fn remove_component_by_id(&self, world: &mut World, entity: Entity, component_id: u64) { - if let Some(expected_type) = self.serializable_type_from_numeric(component_id) { - // Find the converter that produces this serializable type - for converter in self.converters.values() { - if converter.serializable_type_id() == expected_type { - converter.remove_component(world, entity); - // We can stop after finding one, assuming one-to-one mapping for removal - // Or should we continue? Usually one component type per entity. - // But multiple converters might produce same serializable type? - // If so, we might try to remove all possible source components. - // But usually it's 1:1. - return; - } - } + let type_id = TypeId::of::<T>(); + if self.type_to_id.contains_key(&type_id) { + return; } - } - /// Iterates the set of components that are considered "addable" via defaults - /// or factories. - /// - /// Yields pairs of `(numeric_id, type_name)`. - /// - /// The returned iterator only includes components for which: - /// - /// - a default/factory was registered (via [`register_with_default`] or - /// [`register_factory`]), and - /// - a deserializer exists to provide a human-friendly type name. - /// - /// Ordering is arbitrary. - pub fn iter_available_components(&self) -> impl Iterator<Item = (u64, &str)> { - self.default_creators.keys().filter_map(move |id| { - let type_id = self.id_to_serializable.get(id)?; - let deserializer = self.deserializers.get(type_id)?; - Some((*id, deserializer.serializable_type_name())) - }) - } - - /// Registers a custom converter that extracts a serializable value `To` from - /// an ECS component `From`. - /// - /// Use this when the runtime ECS component isn't directly serializable, but - /// you can derive a serializable representation from it. - /// - /// The provided function receives `(world, entity, &From)` and may return - /// `None` to indicate "not present / not applicable". - /// - /// Note: this registers an ID for `To` (the serializable output type), not for - /// `From`. - pub fn register_converter<From, To, F>(&mut self, converter_fn: F) + let id = self.next_id; + self.next_id = self.next_id.saturating_add(1); + + let entry = ComponentEntry { + type_id, + fqtn: std::any::type_name::<T>(), + display_name: short_name(std::any::type_name::<T>()), + create_default: Some(|| Box::new(T::default())), + extract_fn: Some(|world, entity| { + world + .get::<&T>(entity) + .ok() + .map(|component| component.clone_box()) + }), + remove_fn: Some(|world, entity| { + let _ = world.remove_one::<T>(entity); + }), + }; + + self.entries.insert(id, entry); + self.type_to_id.insert(type_id, id); + } + + pub fn register_converter<From, To, F>(&mut self, converter: F) where - From: hecs::Component + 'static, + From: Sync + Send + 'static, To: SerializableComponent + 'static, - F: Fn(&World, Entity, &From) -> Option<To> + Send + Sync + 'static, + F: Fn(&World, Entity, &From) -> Option<To> + Sync + Send + 'static, { - let type_id = TypeId::of::<From>(); - // converter output is To, so track its serializable id - self.ensure_serializable_id(TypeId::of::<To>()); - self.converters - .insert(type_id, Box::new(CustomConverter::new(converter_fn))); - } + let from_id = TypeId::of::<From>(); + let convert_fn = move |world: &World, entity: Entity| -> Option<Box<dyn SerializableComponent>> { + world + .get::<&From>(entity) + .ok() + .and_then(|component| converter(world, entity, &component)) + .map(|converted| Box::new(converted) as Box<dyn SerializableComponent>) + }; - /// Registers a custom deserializer that converts a serializable `From` into - /// a concrete ECS component `To`. - /// - /// This is the inverse of [`register_converter`]. Use it when `From` is the - /// type you store/transport, and `To` is the type you actually attach to an - /// entity. - pub fn register_deserializer<From, To, F>(&mut self, converter_fn: F) - where - From: SerializableComponent + 'static, - To: hecs::Component + Clone + 'static, - F: Fn(&From) -> To + Send + Sync + 'static, - { - let type_id = TypeId::of::<From>(); - self.ensure_serializable_id(type_id); - self.deserializers - .insert(type_id, Box::new(CustomDeserializer::new(converter_fn))); + self.converters.push(ConverterEntry { + from_type: from_id, + convert_fn: Box::new(convert_fn), + }); } - /// Extracts all registered serializable components from `entity`. - /// - /// This calls every registered converter and collects the values it returns. - /// - /// Ordering is arbitrary (depends on `HashMap` iteration order). - pub fn extract_all_components( - &self, - world: &World, - entity: Entity, - ) -> Vec<Box<dyn SerializableComponent>> { - let mut vec = vec![]; - for converter in self.converters.values() { - if let Some(component) = converter.extract_serializable(world, entity) { - vec.push(component); - } - } - return vec; - } - - /// Ensures a numeric ID exists for the given serializable `TypeId` and - /// returns it. - /// - /// IDs are assigned incrementally and wrap on overflow. `0` is never used. - fn ensure_serializable_id(&mut self, type_id: TypeId) -> u64 { - if let Some(id) = self.serializable_ids.get(&type_id) { - *id - } else { - let id = self.next_component_id; - self.next_component_id = self.next_component_id.wrapping_add(1).max(1); - self.serializable_ids.insert(type_id, id); - self.id_to_serializable.insert(id, type_id); - id - } + pub fn iter_available_components(&self) -> impl Iterator<Item = (u32, &str)> { + self.entries + .iter() + .map(|(id, entry)| (*id, entry.fqtn)) } - /// Returns the numeric identifier assigned to the dynamic component value. - /// - /// Returns `None` if the component's concrete type has not been registered. - pub fn id_for_component(&self, component: &dyn SerializableComponent) -> Option<u64> { - let type_id = component.as_any().type_id(); - self.serializable_ids.get(&type_id).copied() + pub fn create_default_component(&self, id: u32) -> Option<Box<dyn SerializableComponent>> { + self.entries + .get(&id) + .and_then(|entry| entry.create_default.map(|ctor| ctor())) } - /// Returns the numeric identifier assigned to the serializable type `T`. - /// - /// Returns `None` if `T` has not been registered (directly or as a converter - /// output). - pub fn id_for_type<T>(&self) -> Option<u64> - where - T: SerializableComponent + 'static, - { - self.serializable_ids.get(&TypeId::of::<T>()).copied() + pub fn id_for_component(&self, component: &dyn SerializableComponent) -> Option<u32> { + self.type_to_id.get(&component.as_any().type_id()).copied() } - /// Looks up the serializable `TypeId` associated with a numeric identifier. - fn serializable_type_from_numeric(&self, component_id: u64) -> Option<TypeId> { - self.id_to_serializable.get(&component_id).copied() + pub fn remove_component_by_id(&self, world: &mut World, entity: Entity, id: u32) { + if let Some(entry) = self.entries.get(&id) { + if let Some(remove_fn) = entry.remove_fn { + remove_fn(world, entity); + } + } } - /// Extracts a single serializable component from `entity` by numeric ID. - /// - /// Returns `None` if: - /// - /// - `component_id` is unknown, or - /// - none of the registered converters produce a value of that type for the - /// given entity. - /// - /// If multiple converters can produce the same serializable type, the first - /// match (in arbitrary order) wins. - pub fn extract_component_by_numeric_id( + pub fn extract_all_components( &self, world: &World, entity: Entity, - component_id: u64, - ) -> Option<Box<dyn SerializableComponent>> { - let expected_type = self.serializable_type_from_numeric(component_id)?; + ) -> Vec<Box<dyn SerializableComponent>> { + let mut components = Vec::new(); - for converter in self.converters.values() { - if let Some(component) = converter.extract_serializable(world, entity) { - if component.as_any().type_id() == expected_type { - return Some(component); + for entry in self.entries.values() { + if let Some(extract_fn) = entry.extract_fn { + if let Some(component) = extract_fn(world, entity) { + components.push(component); } } } - None - } - - /// Finds every entity in `world` that has a component matching `component_id`. - /// - /// This is a convenience wrapper that iterates all entities and uses - /// [`extract_component_by_numeric_id`] to test each one. - pub fn find_components_by_numeric_id( - &self, - world: &World, - component_id: u64, - ) -> Vec<(Entity, Box<dyn SerializableComponent>)> { - let mut matches = Vec::new(); - for entity in world.query::<Entity>().iter() { - if let Some(component) = - self.extract_component_by_numeric_id(world, entity, component_id) - { - matches.push((entity, component)); + for converter in &self.converters { + if let Some(component) = (converter.convert_fn)(world, entity) { + components.push(component); } } - matches + + components } - /// Deserializes a [`SerializableComponent`] into an ECS component and inserts - /// it into `builder`. - /// - /// Returns: - /// - /// - `Ok(true)` if a deserializer was found and insertion succeeded. - /// - `Ok(false)` if no deserializer is registered for this component type. - /// - `Err(_)` if a deserializer was found but it failed. - pub fn deserialize_into_builder( - &self, - component: &dyn SerializableComponent, - builder: &mut EntityBuilder, - ) -> Result<bool> { - let type_id = component.as_any().type_id(); - if let Some(deserializer) = self.deserializers.get(&type_id) { - deserializer.insert_into_builder(component, builder)?; - Ok(true) - } else { - Ok(false) - } + pub fn iter(&self) -> impl Iterator<Item = (u32, &str)> { + self.iter_available_components() } } -impl Default for ComponentRegistry { - fn default() -> Self { - Self::new() - } +fn short_name(fqtn: &str) -> String { + fqtn.split("::").last().unwrap_or(fqtn).to_string() } @@ -1,15 +1,15 @@ //! Additional information and context for cameras from the [`dropbear_engine::camera`] use crate::states::Camera3D; -use crate::traits::SerializableComponent; use dropbear_engine::camera::{Camera, CameraBuilder, CameraSettings}; -use dropbear_macro::SerializableComponent; +use dropbear_traits::{ComponentInitContext, ComponentInitFuture, InsertBundle, SerializableComponent}; use glam::DVec3; use serde::{Deserialize, Serialize}; +use std::any::Any; use crate::ptr::WorldPtr; use crate::scripting::result::DropbearNativeResult; use crate::types::NVector3; -#[derive(Debug, Clone, Serialize, Deserialize, SerializableComponent)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct CameraComponent { pub settings: CameraSettings, pub camera_type: CameraType, @@ -36,6 +36,26 @@ impl CameraComponent { } } +#[typetag::serde] +impl SerializableComponent for CameraComponent { + fn as_any(&self) -> &dyn Any { + self + } + + fn clone_box(&self) -> Box<dyn SerializableComponent> { + Box::new(self.clone()) + } + + fn init(&self, _ctx: ComponentInitContext) -> ComponentInitFuture { + let value = self.clone(); + Box::pin(async move { + let insert: Box<dyn dropbear_traits::ComponentInsert> = + Box::new(InsertBundle((value,))); + Ok(insert) + }) + } +} + impl From<Camera3D> for CameraBuilder { fn from(value: Camera3D) -> Self { let forward = value.transform.rotation * DVec3::Z; @@ -21,8 +21,8 @@ use crate::states::Label; use dropbear_engine::graphics::SharedGraphicsContext; use dropbear_engine::wgpu::util::{BufferInitDescriptor, DeviceExt}; use dropbear_engine::wgpu::{Buffer, BufferUsages}; -use dropbear_macro::SerializableComponent; -use dropbear_traits::SerializableComponent; +use dropbear_traits::{ComponentInitContext, ComponentInitFuture, InsertBundle, SerializableComponent}; +use std::any::Any; use ::jni::objects::{JObject, JValue}; use ::jni::JNIEnv; use rapier3d::prelude::ColliderBuilder; @@ -37,7 +37,7 @@ use rapier3d::prelude::{Rotation, SharedShape, TypedShape, Vector}; use crate::physics::PhysicsState; use crate::ptr::PhysicsStatePtr; -#[derive(Debug, Default, Serialize, Deserialize, Clone, SerializableComponent)] +#[derive(Debug, Default, Serialize, Deserialize, Clone)] pub struct ColliderGroup { #[serde(default)] pub colliders: Vec<Collider>, @@ -53,6 +53,26 @@ impl ColliderGroup { } } +#[typetag::serde] +impl SerializableComponent for ColliderGroup { + fn as_any(&self) -> &dyn Any { + self + } + + fn clone_box(&self) -> Box<dyn SerializableComponent> { + Box::new(self.clone()) + } + + fn init(&self, _ctx: ComponentInitContext) -> ComponentInitFuture { + let value = self.clone(); + Box::pin(async move { + let insert: Box<dyn dropbear_traits::ComponentInsert> = + Box::new(InsertBundle((value,))); + Ok(insert) + }) + } +} + #[repr(C)] #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Collider { @@ -3,11 +3,11 @@ pub mod character_collision; -use crate::traits::SerializableComponent; use rapier3d::control::{CharacterCollision, KinematicCharacterController}; use serde::{Deserialize, Serialize}; -use dropbear_macro::SerializableComponent; +use dropbear_traits::{ComponentInitContext, ComponentInitFuture, InsertBundle, SerializableComponent}; use crate::states::Label; +use std::any::Any; use crate::physics::PhysicsState; use crate::scripting::jni::utils::ToJObject; use crate::scripting::native::DropbearNativeError; @@ -21,7 +21,7 @@ use rapier3d::prelude::QueryFilter; use crate::ptr::WorldPtr; /// The kinematic character controller (kcc) component. -#[derive(Debug, Default, Serialize, Deserialize, Clone, SerializableComponent)] +#[derive(Debug, Default, Serialize, Deserialize, Clone)] pub struct KCC { pub entity: Label, pub controller: KinematicCharacterController, @@ -29,6 +29,26 @@ pub struct KCC { pub collisions: Vec<CharacterCollision>, } +#[typetag::serde] +impl SerializableComponent for KCC { + fn as_any(&self) -> &dyn Any { + self + } + + fn clone_box(&self) -> Box<dyn SerializableComponent> { + Box::new(self.clone()) + } + + fn init(&self, _ctx: ComponentInitContext) -> ComponentInitFuture { + let value = self.clone(); + Box::pin(async move { + let insert: Box<dyn dropbear_traits::ComponentInsert> = + Box::new(InsertBundle((value,))); + Ok(insert) + }) + } +} + impl KCC { pub fn new(label: &Label) -> Self { KCC { @@ -4,8 +4,8 @@ use crate::scripting::jni::utils::{FromJObject, ToJObject}; use crate::scripting::native::DropbearNativeError; use crate::scripting::result::DropbearNativeResult; use crate::states::Label; -use dropbear_macro::SerializableComponent; -use dropbear_traits::SerializableComponent; +use dropbear_traits::{ComponentInitContext, ComponentInitFuture, InsertBundle, SerializableComponent}; +use std::any::Any; use ::jni::objects::{JObject, JValue}; use ::jni::JNIEnv; use rapier3d::prelude::RigidBodyType; @@ -116,7 +116,7 @@ impl ToJObject for AxisLock { /// - The body's initial pose should typically come from your `EntityTransform`/`Transform`. /// - Colliders/material (shape, friction, restitution, sensor, etc.) should usually be a separate /// component (e.g. `Collider`). -#[derive(Debug, Serialize, Deserialize, Clone, SerializableComponent)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct RigidBody { /// The entity this component is attached to. #[serde(default)] @@ -190,6 +190,26 @@ impl Default for RigidBody { } } +#[typetag::serde] +impl SerializableComponent for RigidBody { + fn as_any(&self) -> &dyn Any { + self + } + + fn clone_box(&self) -> Box<dyn SerializableComponent> { + Box::new(self.clone()) + } + + fn init(&self, _ctx: ComponentInitContext) -> ComponentInitFuture { + let value = self.clone(); + Box::pin(async move { + let insert: Box<dyn dropbear_traits::ComponentInsert> = + Box::new(InsertBundle((value,))); + Ok(insert) + }) + } +} + impl RigidBody { const fn default_gravity_scale() -> f32 { 1.0 @@ -1,8 +1,8 @@ use std::fmt; use std::fmt::{Display, Formatter}; use serde::{Deserialize, Serialize}; -use dropbear_macro::SerializableComponent; -use dropbear_traits::SerializableComponent; +use dropbear_traits::{ComponentInitContext, ComponentInitFuture, InsertBundle, SerializableComponent}; +use std::any::Any; use egui::Ui; use hecs::{Entity, World}; use crate::ptr::WorldPtr; @@ -12,7 +12,7 @@ use crate::states::Property; use crate::types::NVector3; /// Properties for an entity, typically queries with `entity.getProperty<Float>` and `entity.setProperty(21)` -#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, SerializableComponent)] +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] pub struct CustomProperties { pub custom_properties: Vec<Property>, pub next_id: u64, @@ -128,6 +128,26 @@ impl Default for CustomProperties { } } +#[typetag::serde] +impl SerializableComponent for CustomProperties { + fn as_any(&self) -> &dyn Any { + self + } + + fn clone_box(&self) -> Box<dyn SerializableComponent> { + Box::new(self.clone()) + } + + fn init(&self, _ctx: ComponentInitContext) -> ComponentInitFuture { + let value = self.clone(); + Box::pin(async move { + let insert: Box<dyn dropbear_traits::ComponentInsert> = + Box::new(InsertBundle((value,))); + Ok(insert) + }) + } +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum Value { String(String), @@ -6,16 +6,11 @@ pub mod scripting; use crate::camera::CameraComponent; use crate::hierarchy::{Children, Parent, SceneHierarchy}; use crate::states::{Camera3D, Label, Light, Script, SerializedMeshRenderer, WorldLoadingStatus, PROJECT}; -use crate::utils::ResolveReference; -use dropbear_engine::asset::ASSET_REGISTRY; -use dropbear_engine::camera::{Camera, CameraBuilder}; +use dropbear_engine::camera::Camera; use dropbear_engine::entity::{EntityTransform, MeshRenderer, Transform}; -use dropbear_engine::texture::Texture; use dropbear_engine::graphics::SharedGraphicsContext; use dropbear_engine::lighting::{Light as EngineLight, LightComponent}; -use dropbear_engine::model::{Material, Model}; -use dropbear_engine::utils::{ResourceReference, ResourceReferenceType}; -use dropbear_traits::SerializableComponent; +use dropbear_traits::{ComponentInitContext, ComponentResources, SerializableComponent}; use dropbear_traits::registry::ComponentRegistry; use glam::{DQuat, DVec3}; use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator}; @@ -26,15 +21,15 @@ use std::fs; use std::path::{Path, PathBuf}; use std::sync::Arc; use crossbeam_channel::Sender; +use egui::Ui; use hecs::Entity; -use dropbear_engine::procedural::ProcObj; use crate::physics::collider::ColliderGroup; use crate::physics::kcc::KCC; use crate::physics::PhysicsState; use crate::physics::rigidbody::RigidBody; use crate::properties::CustomProperties; -#[derive(Default, Debug, Serialize, Deserialize, Clone)] +#[derive(Default, Serialize, Deserialize, Clone)] pub struct SceneEntity { #[serde(default)] pub label: Label, @@ -130,241 +125,24 @@ impl SceneConfig { } } - /// Helper function to load a component and add it to the entity builder - async fn load_component( - component: Box<dyn SerializableComponent>, - builder: &mut hecs::EntityBuilder, - graphics: Arc<SharedGraphicsContext>, - registry: Option<&ComponentRegistry>, + /// Helper function to init a component and insert it into the world. + async fn init_component( + component: &Box<dyn SerializableComponent>, + entity: Entity, + world: &mut hecs::World, + resources: Arc<ComponentResources>, label: &str, ) -> anyhow::Result<()> { - if let Some(transform) = component.as_any().downcast_ref::<EntityTransform>() { - builder.add(*transform); - } else if let Some(renderer) = component.as_any().downcast_ref::<SerializedMeshRenderer>() { - let renderer = renderer.clone(); - let import_scale = renderer.import_scale.unwrap_or(1.0); - let mut model = match &renderer.handle.ref_type { - ResourceReferenceType::None => { - log::error!( - "Resource reference type is None for entity '{}', not supported, skipping", - label - ); - return Ok(()); - } - ResourceReferenceType::Unassigned { id } => { - log::debug!("Loading entity '{}' with no model selected", label); - - let model = std::sync::Arc::new(Model { - label: "None".to_string(), - hash: *id, - path: ResourceReference::from_reference(ResourceReferenceType::Unassigned { id: *id }), - meshes: Vec::new(), - materials: Vec::new(), - skins: Vec::new(), - animations: Vec::new(), - nodes: Vec::new(), - }); - let loaded = LoadedModel::new_raw(&ASSET_REGISTRY, model); - MeshRenderer::from_handle_with_import_scale(loaded, import_scale) - } - ResourceReferenceType::File(reference) => { - let path = &renderer.handle.resolve()?; - - log::debug!( - "Path for entity {} is {} from reference {}", - label, - path.display(), - reference - ); - - let loaded = Model::load(graphics.clone(), path, Some(label), None).await?; - MeshRenderer::from_handle_with_import_scale(loaded, import_scale) - } - ResourceReferenceType::Bytes(bytes) => { - log::info!("Loading entity from bytes [Len: {}]", bytes.len()); - - let loaded = - Model::load_from_memory(graphics.clone(), bytes.clone(), Some(label), None) - .await?; - MeshRenderer::from_handle_with_import_scale(loaded, import_scale) - } - ResourceReferenceType::ProcObj(obj) => { - match obj { - ProcObj::Cuboid { size_bits } => { - let size = [ - f32::from_bits(size_bits[0]), - f32::from_bits(size_bits[1]), - f32::from_bits(size_bits[2]), - ]; - log::info!("Loading entity from cuboid: {:?}", size); - { - let mut cache_guard = MODEL_CACHE.lock(); - cache_guard.remove(label); - } - - let size_vec = glam::DVec3::new(size[0] as f64, size[1] as f64, size[2] as f64); - let mut loaded_model = dropbear_engine::procedural::ProcedurallyGeneratedObject::cuboid(size_vec) - .build_model(graphics.clone(), None, Some(label)); - - let model = loaded_model.make_mut(); - model.path = ResourceReference::from_reference(ResourceReferenceType::ProcObj(ProcObj::Cuboid { size_bits: *size_bits })); - - loaded_model.refresh_registry(); - - MeshRenderer::from_handle_with_import_scale(loaded_model, import_scale) - } - } - } - }; - - if !renderer.material_override.is_empty() { - for override_entry in &renderer.material_override { - if ASSET_REGISTRY - .model_handle_from_reference(&override_entry.source_model) - .is_none() - { - if matches!( - override_entry.source_model.ref_type, - ResourceReferenceType::File(_) - ) { - let source_path = override_entry.source_model.resolve()?; - let label_hint = override_entry.source_model.as_uri(); - Model::load(graphics.clone(), &source_path, label_hint, None).await?; - } else { - log::warn!( - "Material override for '{}' references unsupported resource {:?}", - label, - override_entry.source_model - ); - continue; - } - } - - if let Err(err) = model.apply_material_override( - &override_entry.target_material, - override_entry.source_model.clone(), - &override_entry.source_material, - ) { - log::warn!( - "Failed to apply material override '{}' on '{}': {}", - override_entry.target_material, - label, - err - ); - } - } - } - - if !renderer.material_customisation.is_empty() { - for custom in &renderer.material_customisation { - { - let model_mut = model.make_model_mut(); - let name_index = model_mut - .materials - .iter() - .position(|mat| mat.name == custom.target_material); - - let index = name_index.or(custom.material_index); - - if let Some(material) = index.and_then(|idx| model_mut.materials.get_mut(idx)) { - material.set_tint(graphics.as_ref(), custom.tint); - material.set_uv_tiling(graphics.as_ref(), custom.uv_tiling); - - if let Some(reference) = &custom.diffuse_texture { - if let Ok(path) = reference.resolve() { - if let Ok(bytes) = std::fs::read(&path) { - let diffuse = Texture::from_bytes_verbose_mipmapped( - graphics.clone(), - bytes.as_slice(), - None, - None, - Some(Texture::sampler_from_wrap(custom.wrap_mode)), - Some(material.name.as_str()) - ); - let flat_normal = (*ASSET_REGISTRY - .solid_texture_rgba8( - graphics.clone(), - [128, 128, 255, 255], - )) - .clone(); - - material.diffuse_texture = diffuse; - material.normal_texture = flat_normal; - material.bind_group = Material::create_bind_group( - graphics.as_ref(), - &material.diffuse_texture, - &material.normal_texture, - &material.name, - ); - material.texture_tag = reference.as_uri().map(|s| s.to_string()); - material.wrap_mode = custom.wrap_mode; - material.set_uv_tiling(graphics.as_ref(), custom.uv_tiling); - } else { - log::warn!( - "Failed to read custom texture '{}' for '{}'", - path.display(), - label - ); - } - } else { - log::warn!( - "Failed to resolve custom texture reference {:?} for '{}'", - reference, - label - ); - } - } - } - } - - } - - model.sync_asset_registry(); - } - - builder.add(model); - } else if let Some(props) = component.as_any().downcast_ref::<CustomProperties>() { - builder.add(props.clone()); - } else if let Some(camera_comp) = component.as_any().downcast_ref::<Camera3D>() { - let cam_builder = CameraBuilder::from(camera_comp.clone()); - let comp = CameraComponent::from(camera_comp.clone()); - let camera = Camera::new(graphics.clone(), cam_builder, Some(label)); - builder.add_bundle((camera, comp)); - } else if let Some(light_conf) = component.as_any().downcast_ref::<Light>() { - let light = EngineLight::new( - graphics.clone(), - light_conf.light_component.clone(), - light_conf.transform, - Some(label), - ) - .await; - builder.add_bundle((light_conf.light_component.clone(), light)); - builder.add(light_conf.clone()); - builder.add(light_conf.transform); - } else if let Some(script) = component.as_any().downcast_ref::<Script>() { - builder.add(script.clone()); - } else if let Some(body) = component.as_any().downcast_ref::<RigidBody>() { - builder.add(body.clone()); - } else if component.as_any().downcast_ref::<Parent>().is_some() { - log::debug!( - "Skipping Parent component for '{}' - will be rebuilt from hierarchy_map", - label - ); - } else if let Some(registry) = registry { - if !registry.deserialize_into_builder(component.as_ref(), builder)? { - log::warn!( - "Unknown component type '{}' for entity '{}' - skipping", - component.type_name(), - label - ); - } - } else { - log::warn!( - "Unknown component type '{}' for entity '{}' - skipping", + let ctx = ComponentInitContext { entity, resources }; + let insert = component.init(ctx).await?; + insert.insert(world, entity).map_err(|e| { + anyhow::anyhow!( + "Failed to insert component '{}' for '{}': {}", component.type_name(), - label - ); - } + label, + e + ) + })?; Ok(()) } @@ -402,7 +180,7 @@ impl SceneConfig { &mut self, world: &mut hecs::World, graphics: Arc<SharedGraphicsContext>, - registry: Option<&ComponentRegistry>, + _registry: Option<&ComponentRegistry>, progress_sender: Option<Sender<WorldLoadingStatus>>, is_play_mode: bool, ) -> anyhow::Result<hecs::Entity> { @@ -428,6 +206,10 @@ impl SceneConfig { log::info!("World cleared, now has {} entities", world.len()); + let mut resources = ComponentResources::new(); + resources.insert(graphics.clone()); + let resources = Arc::new(resources); + let entity_configs: Vec<(usize, SceneEntity)> = { let cloned = self.entities.clone(); cloned @@ -461,9 +243,7 @@ impl SceneConfig { }); } - let mut builder = hecs::EntityBuilder::new(); - - builder.add(label_for_map.clone()); + let entity = world.spawn((label_for_map.clone(),)); let mut has_entity_transform = false; @@ -476,18 +256,24 @@ impl SceneConfig { has_entity_transform = true; } - Self::load_component( - component, - &mut builder, - graphics.clone(), - registry, + if component.as_any().downcast_ref::<Parent>().is_some() { + log::debug!( + "Skipping Parent component for '{}' - will be rebuilt from hierarchy_map", + label_for_logs + ); + continue; + } + + Self::init_component( + &component, + entity, + world, + resources.clone(), &label_for_logs, ) .await?; } - let entity = world.spawn(builder.build()); - if has_entity_transform { if let Ok(( entity_transform, @@ -24,20 +24,4 @@ pub fn button_from_ordinal(ordinal: i32) -> Result<gilrs::Button, ()> { 19 => Ok(gilrs::Button::DPadRight), _ => Err(()), } -} - -#[dropbear_macro::impl_c_api] -mod string { - use crate::scripting::result::DropbearNativeResult; - use std::ffi::{c_char, CString}; - - pub fn dropbear_free_string(ptr: *mut c_char) -> DropbearNativeResult<()> { - if ptr.is_null() { - return Ok(()); - } - unsafe { - let _ = CString::from_raw(ptr); - } - Ok(()) - } } @@ -11,7 +11,7 @@ pub static PENDING_SPAWNS: LazyLock<Mutex<Vec<PendingSpawn>>> = LazyLock::new(|| Mutex::new(Vec::new())); /// A spawn that's waiting to be added into the world. -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct PendingSpawn { pub scene_entity: SceneEntity, /// An optional future handle to an object. @@ -5,22 +5,27 @@ use crate::camera::{CameraComponent, CameraType}; use crate::config::{ProjectConfig, ResourceConfig, SourceConfig}; use crate::scene::SceneConfig; -use crate::traits::SerializableComponent; use dropbear_engine::camera::Camera; +use dropbear_engine::camera::CameraBuilder; use dropbear_engine::entity::{MeshRenderer, Transform}; use dropbear_engine::lighting::LightComponent; +use dropbear_engine::graphics::SharedGraphicsContext; use dropbear_engine::asset::ASSET_REGISTRY; use dropbear_engine::utils::{ResourceReference, ResourceReferenceType, EUCA_SCHEME}; -use dropbear_macro::SerializableComponent; +use dropbear_traits::{ComponentInitContext, ComponentInitFuture, InsertBundle, SerializableComponent}; use once_cell::sync::Lazy; use parking_lot::RwLock; use serde::{Deserialize, Serialize}; use std::borrow::Borrow; +use std::any::Any; use std::fmt; use std::fmt::{Display, Formatter}; use std::ops::{Deref, DerefMut}; use std::path::PathBuf; +use std::sync::Arc; +use egui::Ui; use hecs::{Entity, World}; +use dropbear_traits::{Component, ComponentDescriptor}; use crate::properties::Value; /// A global "singleton" that contains the configuration of a project. @@ -146,12 +151,12 @@ impl Display for ResourceType { } } -#[derive(Default, Debug, Serialize, Deserialize, Clone, SerializableComponent)] +#[derive(Default, Debug, Serialize, Deserialize, Clone)] pub struct Script { pub tags: Vec<String>, } -#[derive(Debug, Serialize, Deserialize, Clone, SerializableComponent)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct Camera3D { pub label: String, pub transform: Transform, @@ -227,7 +232,7 @@ pub struct Property { } // A serializable configuration struct for the [Light] type -#[derive(Debug, Serialize, Deserialize, Clone, SerializableComponent)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct Light { pub label: String, pub transform: Transform, @@ -280,7 +285,7 @@ pub enum WorldLoadingStatus { Completed, } -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Hash, Clone, SerializableComponent)] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Hash, Clone)] pub struct Label(String); impl Default for Label { @@ -370,7 +375,8 @@ impl DerefMut for Label { } /// A [MeshRenderer] that is serialized into a file to be stored as a value for config. -#[derive(Default, Debug, Clone, Serialize, Deserialize, SerializableComponent)] + +#[derive(Default, Debug, Clone, Serialize, Deserialize)] pub struct SerializedMeshRenderer { pub handle: ResourceReference, pub import_scale: Option<f32>, @@ -415,4 +421,143 @@ impl SerializedMeshRenderer { texture_override, } } +} + +#[typetag::serde] +impl SerializableComponent for Script { + fn as_any(&self) -> &dyn Any { + self + } + + fn clone_box(&self) -> Box<dyn SerializableComponent> { + Box::new(self.clone()) + } + + fn init(&self, _ctx: ComponentInitContext) -> ComponentInitFuture { + let value = self.clone(); + Box::pin(async move { + let insert: Box<dyn dropbear_traits::ComponentInsert> = + Box::new(InsertBundle((value,))); + Ok(insert) + }) + } +} + +#[typetag::serde] +impl SerializableComponent for Camera3D { + fn as_any(&self) -> &dyn Any { + self + } + + fn clone_box(&self) -> Box<dyn SerializableComponent> { + Box::new(self.clone()) + } + + fn init(&self, ctx: ComponentInitContext) -> ComponentInitFuture { + let value = self.clone(); + Box::pin(async move { + let graphics = ctx + .resources + .get::<Arc<SharedGraphicsContext>>() + .ok_or_else(|| anyhow::anyhow!("SharedGraphicsContext missing for Camera3D init"))?; + + let builder = CameraBuilder::from(value.clone()); + let camera = Camera::new(graphics.clone(), builder, Some(value.label.as_str())); + let component = CameraComponent::from(value); + + let insert: Box<dyn dropbear_traits::ComponentInsert> = + Box::new(InsertBundle((camera, component))); + Ok(insert) + }) + } +} + +#[typetag::serde] +impl SerializableComponent for Light { + fn as_any(&self) -> &dyn Any { + self + } + + fn clone_box(&self) -> Box<dyn SerializableComponent> { + Box::new(self.clone()) + } + + fn init(&self, ctx: ComponentInitContext) -> ComponentInitFuture { + let value = self.clone(); + Box::pin(async move { + let graphics = ctx + .resources + .get::<Arc<SharedGraphicsContext>>() + .ok_or_else(|| anyhow::anyhow!("SharedGraphicsContext missing for Light init"))?; + + let engine_light = dropbear_engine::lighting::Light::new( + graphics.clone(), + value.light_component.clone(), + value.transform, + Some(value.label.as_str()), + ) + .await; + + let insert: Box<dyn dropbear_traits::ComponentInsert> = + Box::new(InsertBundle(( + value.light_component.clone(), + engine_light, + value.clone(), + value.transform, + ))); + Ok(insert) + }) + } +} + +#[typetag::serde] +impl SerializableComponent for Label { + fn as_any(&self) -> &dyn Any { + self + } + + fn clone_box(&self) -> Box<dyn SerializableComponent> { + Box::new(self.clone()) + } + + fn init(&self, _ctx: ComponentInitContext) -> ComponentInitFuture { + let value = self.clone(); + Box::pin(async move { + let insert: Box<dyn dropbear_traits::ComponentInsert> = + Box::new(InsertBundle((value,))); + Ok(insert) + }) + } +} + +#[typetag::serde] +impl SerializableComponent for SerializedMeshRenderer { + fn as_any(&self) -> &dyn Any { + self + } + + fn clone_box(&self) -> Box<dyn SerializableComponent> { + Box::new(self.clone()) + } + + fn init(&self, ctx: ComponentInitContext) -> ComponentInitFuture { + let value = self.clone(); + Box::pin(async move { + let graphics = ctx + .resources + .get::<Arc<SharedGraphicsContext>>() + .ok_or_else(|| anyhow::anyhow!("SharedGraphicsContext missing for MeshRenderer init"))?; + + let label = format!("Entity {:?}", ctx.entity); + let renderer = crate::utils::mesh_loader::load_mesh_renderer_from_serialized( + &value, + graphics.clone(), + &label, + ) + .await?; + let insert: Box<dyn dropbear_traits::ComponentInsert> = + Box::new(InsertBundle((renderer,))); + Ok(insert) + }) + } } @@ -1,3 +1,5 @@ +use crate::hierarchy::EntityTransformExt; +use crate::ptr::WorldPtr; use crate::scripting::jni::utils::{FromJObject, ToJObject}; use crate::scripting::native::DropbearNativeError; use crate::scripting::result::DropbearNativeResult; @@ -5,7 +7,7 @@ use dropbear_engine::entity::{EntityTransform, Transform}; use glam::{DQuat, DVec3}; use ::jni::objects::{JObject, JValue}; use ::jni::JNIEnv; -use crate::types::NVector3; +use crate::types::{NTransform, NVector3}; impl FromJObject for Transform { fn from_jobject(env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Self> { @@ -131,247 +133,107 @@ impl ToJObject for EntityTransform { } } -pub mod shared { - use dropbear_engine::entity::EntityTransform; - use hecs::{Entity, World}; - - pub fn entity_transform_exists_for_entity(world: &World, entity: Entity) -> bool { - world.get::<&EntityTransform>(entity).is_ok() - } +#[dropbear_macro::export( + kotlin(class = "com.dropbear.components.EntityTransformNative", func = "entityTransformExistsForEntity"), + c +)] +fn exists_for_entity( + #[dropbear_macro::define(WorldPtr)] + world: &hecs::World, + #[dropbear_macro::entity] + entity: hecs::Entity, +) -> DropbearNativeResult<bool> { + Ok(world.get::<&EntityTransform>(entity).is_ok()) } -pub mod jni { - #![allow(non_snake_case)] - use crate::convert_jlong_to_entity; - use crate::hierarchy::EntityTransformExt; - use crate::scripting::jni::utils::{FromJObject, ToJObject}; - use dropbear_engine::entity::EntityTransform; - use hecs::World; - use jni::objects::{JClass, JObject}; - use jni::sys::{jboolean, jlong, jobject}; - use jni::JNIEnv; - - #[unsafe(no_mangle)] - pub extern "system" fn Java_com_dropbear_components_EntityTransformNative_entityTransformExistsForEntity( - _env: JNIEnv, - _class: JClass, - world_ptr: jlong, - entity_id: jlong, - ) -> jboolean { - let world = crate::convert_ptr!(world_ptr => World); - let entity = convert_jlong_to_entity!(entity_id); - - if world.get::<&EntityTransform>(entity).is_ok() { 1 } else { 0 } - } - - #[unsafe(no_mangle)] - pub extern "system" fn Java_com_dropbear_components_EntityTransformNative_getLocalTransform( - mut env: JNIEnv, - _class: JClass, - world_ptr: jlong, - entity_id: jlong, - ) -> jobject { - let world = crate::convert_ptr!(world_ptr => World); - let entity = convert_jlong_to_entity!(entity_id); - - if let Ok(et) = world.get::<&EntityTransform>(entity) { - match et.local().to_jobject(&mut env) { - Ok(obj) => obj.into_raw(), - Err(_) => std::ptr::null_mut(), - } - } else { - let _ = env.throw_new("java/lang/RuntimeException", "Entity missing EntityTransform"); - std::ptr::null_mut() - } - } - - #[unsafe(no_mangle)] - pub extern "system" fn Java_com_dropbear_components_EntityTransformNative_setLocalTransform( - mut env: JNIEnv, - _class: JClass, - world_ptr: jlong, - entity_id: jlong, - transform_obj: JObject, - ) { - let world = crate::convert_ptr!(world_ptr => World); - let entity = convert_jlong_to_entity!(entity_id); - - let new_transform = match dropbear_engine::entity::Transform::from_jobject(&mut env, &transform_obj) { - Ok(t) => t, - Err(e) => { - let _ = env.throw_new("java/lang/IllegalArgumentException", format!("Invalid Transform: {:?}", e)); - return; - } - }; - - if let Ok(mut et) = world.get::<&mut EntityTransform>(entity) { - *et.local_mut() = new_transform; - } else { - let _ = env.throw_new("java/lang/RuntimeException", "Entity missing EntityTransform"); - } - } - - #[unsafe(no_mangle)] - pub extern "system" fn Java_com_dropbear_components_EntityTransformNative_getWorldTransform( - mut env: JNIEnv, - _class: JClass, - world_ptr: jlong, - entity_id: jlong, - ) -> jobject { - let world = crate::convert_ptr!(world_ptr => World); - let entity = convert_jlong_to_entity!(entity_id); - - if let Ok(et) = world.get::<&EntityTransform>(entity) { - match et.world().to_jobject(&mut env) { - Ok(obj) => obj.into_raw(), - Err(_) => std::ptr::null_mut(), - } - } else { - let _ = env.throw_new("java/lang/RuntimeException", "Entity missing EntityTransform"); - std::ptr::null_mut() - } - } - - #[unsafe(no_mangle)] - pub extern "system" fn Java_com_dropbear_components_EntityTransformNative_setWorldTransform( - mut env: JNIEnv, - _class: JClass, - world_ptr: jlong, - entity_id: jlong, - transform_obj: JObject, - ) { - let world = crate::convert_ptr!(world_ptr => World); - let entity = convert_jlong_to_entity!(entity_id); - - let new_transform = match dropbear_engine::entity::Transform::from_jobject(&mut env, &transform_obj) { - Ok(t) => t, - Err(_) => return, - }; - - if let Ok(mut et) = world.get::<&mut EntityTransform>(entity) { - *et.world_mut() = new_transform; - } else { - let _ = env.throw_new("java/lang/RuntimeException", "Entity missing EntityTransform"); - } - } - - #[unsafe(no_mangle)] - pub extern "system" fn Java_com_dropbear_components_EntityTransformNative_propagateTransform( - mut env: JNIEnv, - _class: JClass, - world_ptr: jlong, - entity_id: jlong, - ) -> jobject { - let world = crate::convert_ptr!(world_ptr => World); - let entity = convert_jlong_to_entity!(entity_id); - - if let Ok(et) = world.get::<&EntityTransform>(entity) { - let propagated = et.propagate(&world, entity); - match propagated.to_jobject(&mut env) { - Ok(obj) => obj.into_raw(), - Err(_) => std::ptr::null_mut(), - } - } else { - let _ = env.throw_new("java/lang/RuntimeException", "Entity missing EntityTransform"); - std::ptr::null_mut() - } +#[dropbear_macro::export( + kotlin(class = "com.dropbear.components.EntityTransformNative", func = "getLocalTransform"), + c +)] +fn get_local_transform( + #[dropbear_macro::define(WorldPtr)] + world: &hecs::World, + #[dropbear_macro::entity] + entity: hecs::Entity, +) -> DropbearNativeResult<NTransform> { + if let Ok(et) = world.get::<&EntityTransform>(entity) { + Ok((*et.local()).into()) + } else { + Err(DropbearNativeError::MissingComponent) } } -#[dropbear_macro::impl_c_api] -pub mod native { - use crate::hierarchy::EntityTransformExt; - use crate::types::{NTransform as Transform}; - use dropbear_engine::entity::EntityTransform; - use hecs::{Entity, World}; - - use crate::convert_ptr; - use crate::ptr::WorldPtr; - use crate::scripting::native::DropbearNativeError; - use crate::scripting::result::DropbearNativeResult; - - pub fn dropbear_entity_transform_exists_for_entity( - world_ptr: WorldPtr, - entity_id: u64, - ) -> DropbearNativeResult<bool> { - let world = convert_ptr!(world_ptr => World); - let entity = Entity::from_bits(entity_id).ok_or(DropbearNativeError::InvalidEntity)?; - DropbearNativeResult::Ok(world.get::<&EntityTransform>(entity).is_ok()) - } - - pub fn dropbear_get_local_transform( - world_ptr: WorldPtr, - entity_id: u64, - ) -> DropbearNativeResult<Transform> { - let world = convert_ptr!(world_ptr => World); - let entity = Entity::from_bits(entity_id).ok_or(DropbearNativeError::InvalidEntity)?; - - if let Ok(et) = world.get::<&EntityTransform>(entity) { - DropbearNativeResult::Ok(Transform::from(et.local().clone())) - } else { - DropbearNativeResult::Err(DropbearNativeError::NoSuchComponent) - } - } - - - pub fn dropbear_set_local_transform( - world_ptr: WorldPtr, - entity_id: u64, - value: Transform, - ) -> DropbearNativeResult<()> { - let world = convert_ptr!(world_ptr => World); - let entity = Entity::from_bits(entity_id).ok_or(DropbearNativeError::InvalidEntity)?; - - if let Ok(mut et) = world.get::<&mut EntityTransform>(entity) { - *et.local_mut() = dropbear_engine::entity::Transform::from(value); - DropbearNativeResult::Ok(()) - } else { - DropbearNativeResult::Err(DropbearNativeError::NoSuchComponent) - } +#[dropbear_macro::export( + kotlin(class = "com.dropbear.components.EntityTransformNative", func = "setLocalTransform"), + c +)] +fn set_local_transform( + #[dropbear_macro::define(WorldPtr)] + world: &hecs::World, + #[dropbear_macro::entity] + entity: hecs::Entity, + transform: &NTransform +) -> DropbearNativeResult<()> { + if let Ok(mut et) = world.get::<&mut EntityTransform>(entity) { + *et.local_mut() = (*transform).into(); + + Ok(()) + } else { + Err(DropbearNativeError::MissingComponent) } +} - pub fn dropbear_get_world_transform( - world_ptr: WorldPtr, - entity_id: u64, - ) -> DropbearNativeResult<Transform> { - let world = convert_ptr!(world_ptr => World); - let entity = Entity::from_bits(entity_id).ok_or(DropbearNativeError::InvalidEntity)?; - - if let Ok(et) = world.get::<&EntityTransform>(entity) { - DropbearNativeResult::Ok(Transform::from(et.world().clone())) - } else { - DropbearNativeResult::Err(DropbearNativeError::NoSuchComponent) - } +#[dropbear_macro::export( + kotlin(class = "com.dropbear.components.EntityTransformNative", func = "getWorldTransform"), + c +)] +fn get_world_transform( + #[dropbear_macro::define(WorldPtr)] + world: &hecs::World, + #[dropbear_macro::entity] + entity: hecs::Entity, +) -> DropbearNativeResult<NTransform> { + if let Ok(et) = world.get::<&EntityTransform>(entity) { + Ok((*et.world()).into()) + } else { + Err(DropbearNativeError::MissingComponent) } +} - pub fn dropbear_set_world_transform( - world_ptr: WorldPtr, - entity_id: u64, - value: Transform, - ) -> DropbearNativeResult<()> { - let world = convert_ptr!(world_ptr => World); - let entity = Entity::from_bits(entity_id).ok_or(DropbearNativeError::InvalidEntity)?; - - if let Ok(mut et) = world.get::<&mut EntityTransform>(entity) { - *et.world_mut() = dropbear_engine::entity::Transform::from(value); - DropbearNativeResult::Ok(()) - } else { - DropbearNativeResult::Err(DropbearNativeError::NoSuchComponent) - } +#[dropbear_macro::export( + kotlin(class = "com.dropbear.components.EntityTransformNative", func = "setWorldTransform"), + c +)] +fn set_world_transform( + #[dropbear_macro::define(WorldPtr)] + world: &hecs::World, + #[dropbear_macro::entity] + entity: hecs::Entity, + transform: &NTransform +) -> DropbearNativeResult<()> { + if let Ok(mut et) = world.get::<&mut EntityTransform>(entity) { + *et.world_mut() = (*transform).into(); + + Ok(()) + } else { + Err(DropbearNativeError::MissingComponent) } +} - pub fn dropbear_propagate_transform( - world_ptr: WorldPtr, - entity_id: u64, - ) -> DropbearNativeResult<Transform> { - let world = convert_ptr!(world_ptr => World); - let entity = Entity::from_bits(entity_id).ok_or(DropbearNativeError::InvalidEntity)?; - - if let Ok(et) = world.get::<&EntityTransform>(entity) { - let propagated = et.propagate(world, entity); - DropbearNativeResult::Ok(Transform::from(propagated)) - } else { - DropbearNativeResult::Err(DropbearNativeError::NoSuchComponent) - } +#[dropbear_macro::export( + kotlin(class = "com.dropbear.components.EntityTransformNative", func = "propogateTransform"), + c +)] +fn propogate_transform( + #[dropbear_macro::define(WorldPtr)] + world: &hecs::World, + #[dropbear_macro::entity] + entity: hecs::Entity, +) -> DropbearNativeResult<NTransform> { + if let Ok(mut et) = world.get::<&mut EntityTransform>(entity) { + let result = et.propagate(world, entity); + Ok(result.into()) + } else { + Err(DropbearNativeError::MissingComponent) } } @@ -472,6 +472,51 @@ impl From<NTransform> for Transform { } } +impl FromJObject for NTransform { + fn from_jobject(env: &mut JNIEnv, obj: &JObject) -> DropbearNativeResult<Self> { + let pos_val = env.get_field(obj, "position", "Lcom/dropbear/math/Vector3d;") + .map_err(|_| DropbearNativeError::JNIFailedToGetField)?; + + let pos_obj = pos_val.l() + .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?; + + let rot_val = env.get_field(obj, "rotation", "Lcom/dropbear/math/Quaterniond;") + .map_err(|_| DropbearNativeError::JNIFailedToGetField)?; + + let rot_obj = rot_val.l() + .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?; + + let scale_val = env.get_field(obj, "scale", "Lcom/dropbear/math/Vector3d;") + .map_err(|_| DropbearNativeError::JNIFailedToGetField)?; + + let scale_obj = scale_val.l() + .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?; + + let position: DVec3 = NVector3::from_jobject(env, &pos_obj)?.into(); + let scale: DVec3 = NVector3::from_jobject(env, &scale_obj)?.into(); + + let mut get_double = |field: &str| -> DropbearNativeResult<f64> { + env.get_field(&rot_obj, field, "D") + .map_err(|_| DropbearNativeError::JNIFailedToGetField)? + .d() + .map_err(|_| DropbearNativeError::JNIUnwrapFailed) + }; + + let rx = get_double("x")?; + let ry = get_double("y")?; + let rz = get_double("z")?; + let rw = get_double("w")?; + + let rotation = DQuat::from_xyzw(rx, ry, rz, rw); + + Ok(NTransform { + position: position.into(), + rotation: rotation.into(), + scale: scale.into(), + }) + } +} + impl ToJObject for NTransform { fn to_jobject<'a>(&self, env: &mut JNIEnv<'a>) -> DropbearNativeResult<JObject<'a>> { let class = env @@ -2,6 +2,7 @@ pub mod option; pub mod hashmap; +pub mod mesh_loader; use crate::states::Node; use dropbear_engine::utils::{ResourceReference, ResourceReferenceType, relative_path_from_euca}; @@ -0,0 +1,84 @@ +use std::sync::Arc; + +use dropbear_engine::asset::ASSET_REGISTRY; +use dropbear_engine::entity::MeshRenderer; +use dropbear_engine::graphics::SharedGraphicsContext; +use dropbear_engine::model::Model; +use dropbear_engine::procedural::ProcedurallyGeneratedObject; +use dropbear_engine::utils::{ResourceReference, ResourceReferenceType}; + +use crate::states::SerializedMeshRenderer; +use crate::utils::ResolveReference; + +pub async fn load_mesh_renderer_from_serialized( + serialized: &SerializedMeshRenderer, + graphics: Arc<SharedGraphicsContext>, + label: &str, +) -> anyhow::Result<MeshRenderer> { + let import_scale = serialized.import_scale.unwrap_or(1.0); + + let handle = match &serialized.handle.ref_type { + ResourceReferenceType::None => { + anyhow::bail!("Resource reference type is None for '{}'", label); + } + ResourceReferenceType::Unassigned { id } => { + let model = Model { + label: "None".to_string(), + hash: *id, + path: ResourceReference::from_reference(ResourceReferenceType::Unassigned { id: *id }), + meshes: Vec::new(), + materials: Vec::new(), + skins: Vec::new(), + animations: Vec::new(), + nodes: Vec::new(), + }; + + let mut registry = ASSET_REGISTRY.write(); + if let Some(existing) = registry.model_handle_by_hash(*id) { + existing + } else { + registry.add_model(model) + } + } + ResourceReferenceType::File(reference) => { + let path = serialized.handle.resolve()?; + log::debug!("Path for entity {} is {} from reference {}", label, path.display(), reference); + let buffer = std::fs::read(&path)?; + Model::load_from_memory_raw(graphics.clone(), buffer, Some(label), ASSET_REGISTRY.clone()).await? + } + ResourceReferenceType::Bytes(bytes) => { + log::info!("Loading entity '{}' from bytes [Len: {}]", label, bytes.len()); + Model::load_from_memory_raw(graphics.clone(), bytes, Some(label), ASSET_REGISTRY.clone()).await? + } + ResourceReferenceType::ProcObj(obj) => match obj { + dropbear_engine::procedural::ProcObj::Cuboid { size_bits } => { + let size = [ + f32::from_bits(size_bits[0]), + f32::from_bits(size_bits[1]), + f32::from_bits(size_bits[2]), + ]; + log::info!("Loading entity '{}' from cuboid: {:?}", label, size); + + let size_vec = glam::DVec3::new(size[0] as f64, size[1] as f64, size[2] as f64); + ProcedurallyGeneratedObject::cuboid(size_vec).build_model( + graphics.clone(), + None, + Some(label), + ASSET_REGISTRY.clone(), + ) + } + }, + }; + + let mut renderer = MeshRenderer::from_handle(handle); + renderer.set_import_scale(import_scale); + + if serialized.texture_override.is_some() { + log::debug!( + "texture_override is set for '{}' but not applied yet", + label + ); + } + + Ok(renderer) +} @@ -0,0 +1,137 @@ +impl<'a> EditorTabViewer<'a> { + pub fn build_console(&mut self, ui: &mut egui::Ui) { + fn analyse_error(log: &Vec<String>) -> Vec<ConsoleItem> { + fn parse_compiler_location( + line: &str, + ) -> Option<(ErrorLevel, PathBuf, String)> { + let trimmed = line.trim_start(); + let (error_level, rest) = + if let Some(r) = trimmed.strip_prefix("e: file:///") { + (ErrorLevel::Error, r) + } else if let Some(r) = trimmed.strip_prefix("w: file:///") { + (ErrorLevel::Warn, r) + } else { + return None; + }; + + let location = rest.split_whitespace().next()?; + + let mut segments = location.rsplitn(3, ':'); + let column = segments.next()?; + let row = segments.next()?; + let path = segments.next()?; + + Some((error_level, PathBuf::from(path), format!("{row}:{column}"))) + } + + let mut list: Vec<ConsoleItem> = Vec::new(); + for (index, line) in log.iter().enumerate() { + if line.contains("The required library") { + list.push(ConsoleItem { + error_level: ErrorLevel::Error, + msg: line.clone(), + file_location: None, + line_ref: None, + id: index as u64, + }); + } else if let Some((error_level, path, loc)) = parse_compiler_location(line) { + list.push(ConsoleItem { + error_level, + msg: line.clone(), + file_location: Some(path), + line_ref: Some(loc), + id: index as u64, + }); + } else { + list.push(ConsoleItem { + error_level: ErrorLevel::Info, + msg: line.clone(), + file_location: None, + line_ref: None, + id: index as u64, + }); + } + } + list + } + + let logs = analyse_error(&self.build_logs); + + egui::ScrollArea::vertical() + .auto_shrink([false, false]) + .stick_to_bottom(true) + .show(ui, |ui| { + if logs.is_empty() { + ui.label("Build output will appear here once available."); + return; + } + + for item in &logs { + let (bg_color, text_color, stroke_color) = match item.error_level { + ErrorLevel::Error => ( + egui::Color32::from_rgb(60, 20, 20), + egui::Color32::from_rgb(255, 200, 200), + egui::Color32::from_rgb(255, 200, 200), + ), + ErrorLevel::Warn => ( + egui::Color32::from_rgb(40, 40, 10), + egui::Color32::from_rgb(255, 255, 200), + egui::Color32::from_rgb(255, 255, 200), + ), + ErrorLevel::Info => ( + egui::Color32::TRANSPARENT, + ui.style().visuals.text_color(), + egui::Color32::TRANSPARENT, + ), + }; + + if matches!(item.error_level, ErrorLevel::Info) { + ui.label(RichText::new(&item.msg).monospace()); + } else { + let available_width = ui.available_width(); + let frame = egui::Frame::new() + .inner_margin(Margin::symmetric(8, 6)) + .fill(bg_color) + .stroke(egui::Stroke::new(1.0, stroke_color)); + + let response = frame + .show(ui, |ui| { + ui.set_width(available_width - 10.0); + ui.horizontal(|ui| { + ui.label(RichText::new(&item.msg).color(text_color).monospace()); + }); + }) + .response; + + if response.clicked() { + log::debug!("Log item clicked: {}", &item.id); + if let (Some(path), Some(loc)) = + (&item.file_location, &item.line_ref) + { + let location_arg = format!("{}:{}", path.display(), loc); + + match std::process::Command::new("code") + .args(["-g", &location_arg]) + .spawn() + .map(|_| ()) + { + Ok(()) => { + log::info!( + "Launched Visual Studio Code at the error: {}", + &location_arg + ); + } + Err(e) => { + warn!( + "Failed to open '{}' in VS Code: {}", + location_arg, e + ); + } + } + } + } + } + } + }); + } +} @@ -214,751 +214,16 @@ impl<'a> TabViewer for EditorTabViewer<'a> { match tab { EditorTab::Viewport => { - log_once::debug_once!("Viewport focused"); - - let available_rect = ui.available_rect_before_wrap(); - let available_size = available_rect.size(); - - let tex_aspect = self.tex_size.width as f32 / self.tex_size.height as f32; - let available_aspect = available_size.x / available_size.y; - - let (display_width, display_height) = if available_aspect > tex_aspect { - let height = available_size.y * 0.95; - let width = height * tex_aspect; - (width, height) - } else { - let width = available_size.x * 0.95; - let height = width / tex_aspect; - (width, height) - }; - - let center_x = available_rect.center().x; - let center_y = available_rect.center().y; - - let image_rect = egui::Rect::from_center_size( - egui::pos2(center_x, center_y), - egui::vec2(display_width, display_height), - ); - - let (_rect, _response) = - ui.allocate_exact_size(available_size, egui::Sense::click_and_drag()); - - let _image_response = ui.allocate_rect(image_rect, egui::Sense::click_and_drag()); - - ui.scope_builder(egui::UiBuilder::new().max_rect(image_rect), |ui| { - ui.add_sized( - [display_width, display_height], - egui::Image::new((self.view, [display_width, display_height].into())) - .fit_to_exact_size([display_width, display_height].into()), - ) - }); - - let 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 = { - if let Ok((cam, _comp)) = self - .world - .query_one::<(&Camera, &CameraComponent)>(active_camera).get() - { - Some(cam.clone()) - } else { - log::warn!("Queried camera but found no value"); - None - } - }; - - if let Some(camera) = camera_data { - self.gizmo.update_config(GizmoConfig { - view_matrix: camera.view_mat.into(), - projection_matrix: camera.proj_mat.into(), - viewport: image_rect, - modes: *self.gizmo_mode, - orientation: *self.gizmo_orientation, - snapping, - snap_distance: 1.0, - ..Default::default() - }); - } - } - if !matches!(self.viewport_mode, ViewportMode::None) - && let Some(entity_id) = self.selected_entity - { - let mut handled = false; - - if let Ok(entity_transform) = self.world.query_one::<&mut EntityTransform>(*entity_id).get() - { - let was_focused = cfg.is_focused; - cfg.is_focused = self.gizmo.is_focused(); - - if cfg.is_focused && !was_focused { - cfg.entity_transform_original = Some(*entity_transform); - } - - let synced = entity_transform.sync(); - let gizmo_transform = - transform_gizmo_egui::math::Transform::from_scale_rotation_translation( - synced.scale, - synced.rotation, - synced.position, - ); - - if let Some((_result, new_transforms)) = - self.gizmo.interact(ui, &[gizmo_transform]) - && let Some(new_transform) = new_transforms.first() - { - let new_synced_pos: glam::DVec3 = new_transform.translation.into(); - let new_synced_rot: glam::DQuat = new_transform.rotation.into(); - let new_synced_scale: glam::DVec3 = new_transform.scale.into(); - - 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 new_world_scale = new_synced_scale / safe_local_scale; - let new_world_rot = new_synced_rot * local.rotation.inverse(); - - 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 world_transform = entity_transform.world_mut(); - world_transform.position = new_world_pos; - world_transform.rotation = new_world_rot; - world_transform.scale = new_world_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 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 delta_pos = new_synced_pos - world_pos; - let unrotated_delta = world_rot.inverse() * delta_pos; - local_transform.position = unrotated_delta / safe_world_scale; - } - } - } - - 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), - ); - log::debug!("Pushed entity transform action to stack"); - } - } - } - handled = true; - } - - if !handled { - if let Ok(transform) = self.world.query_one::<&mut Transform>(*entity_id).get() - { - let was_focused = cfg.is_focused; - cfg.is_focused = self.gizmo.is_focused(); - - if cfg.is_focused && !was_focused { - cfg.old_pos = *transform; - } - - let gizmo_transform = - transform_gizmo_egui::math::Transform::from_scale_rotation_translation( - transform.scale, - transform.rotation, - transform.position, - ); - - if let Some((_result, new_transforms)) = - self.gizmo.interact(ui, &[gizmo_transform]) - && let Some(new_transform) = new_transforms.first() - { - transform.position = new_transform.translation.into(); - transform.rotation = new_transform.rotation.into(); - transform.scale = new_transform.scale.into(); - } - - if was_focused && !cfg.is_focused { - let transform_changed = cfg.old_pos.position != transform.position - || cfg.old_pos.rotation != transform.rotation - || cfg.old_pos.scale != transform.scale; - - if transform_changed { - UndoableAction::push_to_undo( - self.undo_stack, - UndoableAction::Transform(*entity_id, cfg.old_pos), - ); - log::debug!("Pushed transform action to stack"); - } - } - } - } - } + self.viewport_tab(ui); } EditorTab::ModelEntityList => { - let (_response, action) = egui_ltreeview::TreeView::new(egui::Id::new( - "model_entity_list", - )) - .show(ui, |builder| { - let current_scene_name = { - PROJECT - .read() - .last_opened_scene - .clone() - .unwrap_or("Scene".to_string()) - }; - builder.node( - NodeBuilder::dir(u64::MAX) - .label(format!("Scene: {}", current_scene_name)) - .context_menu(|ui| { - if ui.button("New Empty Entity").clicked() { - self.world.spawn((Label::new("Blank Entity"),)); - ui.close(); - } - }), - ); - // the root scene must be the biggest number possible to remove any ambiguity - - fn add_entity_to_tree( - builder: &mut TreeViewBuilder<u64>, - entity: Entity, - world: &mut World, - registry: &ComponentRegistry, - cfg: &mut StaticallyKept, - signal: &mut Signal, - ) -> anyhow::Result<()> { - let entity_id = entity.to_bits().get(); - let label = if let Ok(label) = world.query_one::<&Label>(entity).get() - { - label.clone() - } else { - anyhow::bail!( - "This entity [{}] is expected to contain Label", - entity_id - ); - }; - - builder.node( - NodeBuilder::dir(entity_id) - .label(label.as_str()) - .context_menu(|ui| { - ui.menu_button("New", |ui| { - if ui.button("Child").clicked() { - let child = world.spawn((Label::new("New Entity"),)); - Hierarchy::set_parent(world, child, entity); - ui.close(); - } - }); - ui.menu_button("Add", |ui| { - log_once::debug_once!("Available components: "); - for (id, name) in registry.iter_available_components() { - log_once::debug_once!("id: {}, name: {}", id, name); - // if name.contains("EntityTransform") { - // continue; - // } - let short_name = - name.split("::").last().unwrap_or(name); - let display_name = - if short_name == "SerializedMeshRenderer" { - "MeshRenderer" - } else { - short_name - }; - - if ui.button(display_name).clicked() { - if name.contains("MeshRenderer") { - *signal = Signal::AddComponent( - entity, - "MeshRenderer".to_string(), - ); - } else if name.contains("CameraComponent") - || name.contains("Camera3D") - { - *signal = Signal::AddComponent( - entity, - "CameraComponent".to_string(), - ); - } else if name.contains("Light") { - *signal = Signal::AddComponent( - entity, - "Light".to_string(), - ); - } else { - if let Some(comp) = - registry.create_default_component(id) - { - let mut builder = EntityBuilder::new(); - if let Ok(_) = registry - .deserialize_into_builder( - comp.as_ref(), - &mut builder, - ) - { - let _ = world - .insert(entity, builder.build()); - } - } - } - ui.close(); - } - } - }); - }), - ); - - let components = registry.extract_all_components(world, entity); - - for component in components.iter() { - // if component.type_name().contains("EntityTransform") { - // continue; - // } - let Some(component_type_id) = - registry.id_for_component(component.as_ref()) - else { - log_once::warn_once!( - "Component '{}' missing registry id, skipping tree entry", - component.type_name() - ); - continue; - }; - let component_node_id = - cfg.component_node_id(entity, component_type_id); - let display = - format!("{} (id #{component_type_id})", component.display_name()); - - let has_rigidbody = world.get::<&RigidBody>(entity).is_ok(); - let has_collider = world.get::<&ColliderGroup>(entity).is_ok(); - - let node = NodeBuilder::leaf(component_node_id) - .label_ui(|ui| { - ui.label(display.clone()); - - if has_rigidbody && !has_collider && component.type_name().contains("RigidBody") { - ui.add_space(4.0); - ui.small_button("⚠") - .on_hover_text("RigidBody has no colliders! Add the ColliderGroup component"); - } - }) - .context_menu(|ui| { - if ui.button("Remove Component").clicked() { - registry.remove_component_by_id( - world, - entity, - component_type_id, - ); - ui.close(); - } - }); - - builder.node(node); - } - - let children_entities = if let Ok(children) = world.get::<&Children>(entity) { - children.children().to_vec() - } else { - Vec::new() - }; - - for child in children_entities { - if let Err(e) = - add_entity_to_tree(builder, child, world, registry, cfg, signal) - { - log_once::error_once!( - "Failed to add child entity to tree, skipping: {}", - e - ); - continue; - } - } - - builder.close_dir(); - Ok(()) - } - - let root_entities: Vec<Entity> = self - .world - .query::<Entity>() - .without::<&Parent>() - .iter() - .map(|e| e) - .collect(); - - for entity in root_entities { - if let Err(e) = add_entity_to_tree( - builder, - entity, - &mut self.world, - &self.component_registry, - &mut cfg, - self.signal, - ) { - log_once::error_once!( - "Failed to add child entity to tree, skipping: {}", - e - ); - } - } - - builder.close_dir(); - }); - - for i in action { - match i { - egui_ltreeview::Action::SetSelected(items) => { - log_once::debug_once!("Selected: {:?}", items); - self.handle_tree_selection(&mut cfg, &items); - } - egui_ltreeview::Action::Move(drag_and_drop) => { - log_once::debug_once!("Moved: {:?}", drag_and_drop); - self.handle_tree_move(&mut cfg, &drag_and_drop); - } - egui_ltreeview::Action::Drag(drag_and_drop) => { - log_once::debug_once!("Dragged: {:?}", drag_and_drop); - self.handle_tree_drag(&mut cfg, &drag_and_drop); - } - egui_ltreeview::Action::Activate(activate) => { - log_once::debug_once!("Activated: {:?}", activate); - self.handle_tree_activate(&mut cfg, &activate); - } - egui_ltreeview::Action::DragExternal(_drag_and_drop_external) => {} - egui_ltreeview::Action::MoveExternal(_drag_and_drop_external) => {} - } - } + self.entity_list(ui); } EditorTab::AssetViewer => { self.show_asset_viewer(&mut cfg, ui); } EditorTab::ResourceInspector => { - let local_scene_settings = cfg.root_node_selected; - let mut local_add_collider: Option<Entity> = None; - - if let Some(entity) = self.selected_entity { - let mut local_set_initial_camera = false; - let mut inspect_entity = *entity; - let world = &self.world; - - if !cfg.root_node_selected { - if let Ok((label, )) = world.query_one::<(&mut Label,)>(inspect_entity).get() { - label.inspect( - &mut inspect_entity, - &mut cfg, - ui, - self.undo_stack, - self.signal, - &mut String::new(), - ); - - ui.label(format!("Entity ID: {}", inspect_entity.id())); - - ui.separator(); - - // mesh renderer - if let Ok(e) = world.query_one::<&mut MeshRenderer>(inspect_entity).get() - { - CollapsingHeader::new("MeshRenderer").default_open(true).show(ui, |ui| { - e.inspect( - &mut inspect_entity, - &mut cfg, - ui, - self.undo_stack, - self.signal, - &mut String::new(), - ); - }); - } - - // entity transform - if let Ok(t) = world.query_one::<&mut EntityTransform>(inspect_entity).get() - { - CollapsingHeader::new("Transform").default_open(true).show(ui, |ui| { - t.inspect( - &mut inspect_entity, - &mut cfg, - ui, - self.undo_stack, - self.signal, - &mut String::new(), - ); - }); - ui.separator(); - } - - // custom properties - if let Ok(props) = world.query_one::<&mut CustomProperties>(inspect_entity).get() - { - CollapsingHeader::new("Custom Properties").default_open(true).show(ui, |ui| { - props.inspect( - &mut inspect_entity, - &mut cfg, - ui, - self.undo_stack, - self.signal, - label.as_mut_string(), - ); - }); - ui.separator(); - } - - // camera - if let Ok((camera, camera_component)) = world - .query_one::<(&mut Camera, &mut CameraComponent)>(inspect_entity).get() - { - CollapsingHeader::new("Camera").default_open(true).show(ui, |ui| { - camera.inspect( - &mut inspect_entity, - &mut cfg, - ui, - self.undo_stack, - self.signal, - &mut String::new(), - ); - - ui.separator(); - - camera_component.inspect( - &mut inspect_entity, - &mut cfg, - ui, - self.undo_stack, - self.signal, - &mut camera.label.clone(), - ); - - ui.separator(); - - // camera controller - let mut active_camera = self.active_camera.lock(); - - if active_camera.equivalent(&Some(*entity)) { - ui.label("Status: Currently viewing through camera"); - } else { - ui.label("Status: Not viewing through this camera"); - } - - if ui.button("Set as active camera").clicked() { - *active_camera = Some(*entity); - log::info!( - "Currently viewing from camera angle '{}'", - camera.label - ); - } - - if camera_component.starting_camera { - if ui.button("Stop making camera initial").clicked() { - log::debug!("'Stop making camera initial' button clicked"); - camera_component.starting_camera = false; - success!("Removed {} from starting camera", camera.label); - } - } else if ui.button("Set as initial camera").clicked() { - log::debug!("'Set as initial camera' button clicked"); - if matches!(camera_component.camera_type, CameraType::Debug) { - warn!( - "Cannot set any cameras of type 'Debug' to initial camera" - ); - } else { - local_set_initial_camera = true - } - } - }); - ui.separator(); - } - - // light - if let Ok((light, comp, transform)) = world.query_one::<(&mut Light, &mut LightComponent, &mut Transform)>(inspect_entity).get() - { - light.transform = *transform; - light.light_component = comp.clone(); - - light.inspect( - entity, - &mut cfg, - ui, - self.undo_stack, - self.signal, - &mut String::new(), - ); - - *transform = light.transform; - *comp = light.light_component.clone(); - ui.separator(); - } - - // script - if let Ok((script, ui_c)) = world.query_one::<(Option<&mut Script>, Option<&mut UIComponent>)>(*entity).get() - { - CollapsingHeader::new("Script").default_open(true).show(ui, |ui| { - if let Some(s) = script { - s.inspect( - entity, - &mut cfg, - ui, - self.undo_stack, - self.signal, - label.as_mut_string(), - ); - } - - if let Some(ui_c) = ui_c { - CollapsingHeader::new("UI").default_open(true).show(ui, |ui| { - ui_c.inspect( - entity, - &mut cfg, - ui, - self.undo_stack, - self.signal, - label.as_mut_string(), - ); - }); - } - }); - ui.separator(); - } - - // physics - if let Ok((rigid, colliders, kcc)) = world.query_one::<(Option<&mut RigidBody>, Option<&mut ColliderGroup>, Option<&mut KCC>)>(*entity).get() - { - if rigid.is_some() || colliders.is_some() || kcc.is_some() { - CollapsingHeader::new("Physics").default_open(true).show(ui, |ui| { - - if let Some(kcc) = kcc { - CollapsingHeader::new("Kinematic Character Controller").default_open(true).show(ui, |ui| { - kcc.inspect( - entity, - &mut cfg, - ui, - self.undo_stack, - self.signal, - label.as_mut_string(), - ); - }); - ui.separator(); - } - - if let Some(rigid) = rigid { - CollapsingHeader::new("RigidBody").default_open(true).show(ui, |ui| { - rigid.inspect( - entity, - &mut cfg, - ui, - self.undo_stack, - self.signal, - label.as_mut_string(), - ); - }); - ui.separator(); - } - - if let Some(col) = colliders { - CollapsingHeader::new("Colliders").default_open(true).show(ui, |ui| { - let mut to_remove: Option<usize> = None; - - for (index, c) in col.colliders.iter_mut().enumerate() { - ui.horizontal(|ui| { - let header = CollapsingHeader::new(format!("Collider {}", index + 1)) - .default_open(true); - - header.show(ui, |ui| { - c.inspect( - entity, - &mut cfg, - ui, - self.undo_stack, - self.signal, - label.as_mut_string(), - ); - }); - - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - if ui.button("🗑").on_hover_text("Remove collider").clicked() { - to_remove = Some(index); - } - }); - }); - - ui.separator(); - } - - if let Some(index) = to_remove { - col.colliders.remove(index); - } - - if ui.button("➕ Add new collider").clicked() { - local_add_collider = Some(*entity); - } - }); - } - - ui.separator(); - }); - } - ui.separator(); - } - } else { - log_once::debug_once!("Unable to query entity inside resource inspector"); - } - } - - if local_set_initial_camera { - for (id, comp) in self.world.query::<(Entity, &mut CameraComponent)>().iter() { - comp.starting_camera = false; - self.undo_stack - .push(UndoableAction::RemoveStartingCamera(id)) - } - - if let Ok(comp) = self.world.query_one_mut::<&mut CameraComponent>(*entity) - { - success!("This camera is currently set as the initial camera"); - comp.starting_camera = true; - } - } - } else if !local_scene_settings { - ui.label("No entity selected, therefore no info to provide. Go on, what are you waiting for? Click an entity!"); - } - - if local_scene_settings { - log_once::debug_once!("Rendering scene settings"); - self.scene_settings(&mut cfg, ui); - } - - if let Some(e) = local_add_collider { - let mut manual_edit = false; - if let Ok(col) = self.world.query_one::<Option<&mut ColliderGroup>>(e).get() - { - if let Some(col) = col { - let mut collider = Collider::new(); - collider.id = col.colliders.len() as u32 + 1; - col.insert(collider); - } else { - manual_edit = true; - } - } - - if manual_edit { - let _ = self.world.insert_one(e, ColliderGroup::new()); - } - } + self.resource_inspector(ui); } EditorTab::Plugin(dock_info) => { if self.editor.is_null() { @@ -975,139 +240,7 @@ impl<'a> TabViewer for EditorTabViewer<'a> { } } EditorTab::ErrorConsole => { - fn analyse_error(log: &Vec<String>) -> Vec<ConsoleItem> { - fn parse_compiler_location( - line: &str, - ) -> Option<(ErrorLevel, PathBuf, String)> { - let trimmed = line.trim_start(); - let (error_level, rest) = - if let Some(r) = trimmed.strip_prefix("e: file:///") { - (ErrorLevel::Error, r) - } else if let Some(r) = trimmed.strip_prefix("w: file:///") { - (ErrorLevel::Warn, r) - } else { - return None; - }; - - let location = rest.split_whitespace().next()?; - - let mut segments = location.rsplitn(3, ':'); - let column = segments.next()?; - let row = segments.next()?; - let path = segments.next()?; - - Some((error_level, PathBuf::from(path), format!("{row}:{column}"))) - } - - let mut list: Vec<ConsoleItem> = Vec::new(); - for (index, line) in log.iter().enumerate() { - if line.contains("The required library") { - list.push(ConsoleItem { - error_level: ErrorLevel::Error, - msg: line.clone(), - file_location: None, - line_ref: None, - id: index as u64, - }); - } else if let Some((error_level, path, loc)) = parse_compiler_location(line) { - list.push(ConsoleItem { - error_level, - msg: line.clone(), - file_location: Some(path), - line_ref: Some(loc), - id: index as u64, - }); - } else { - list.push(ConsoleItem { - error_level: ErrorLevel::Info, - msg: line.clone(), - file_location: None, - line_ref: None, - id: index as u64, - }); - } - } - list - } - - let logs = analyse_error(&self.build_logs); - - egui::ScrollArea::vertical() - .auto_shrink([false, false]) - .stick_to_bottom(true) - .show(ui, |ui| { - if logs.is_empty() { - ui.label("Build output will appear here once available."); - return; - } - - for item in &logs { - let (bg_color, text_color, stroke_color) = match item.error_level { - ErrorLevel::Error => ( - egui::Color32::from_rgb(60, 20, 20), - egui::Color32::from_rgb(255, 200, 200), - egui::Color32::from_rgb(255, 200, 200), - ), - ErrorLevel::Warn => ( - egui::Color32::from_rgb(40, 40, 10), - egui::Color32::from_rgb(255, 255, 200), - egui::Color32::from_rgb(255, 255, 200), - ), - ErrorLevel::Info => ( - egui::Color32::TRANSPARENT, - ui.style().visuals.text_color(), - egui::Color32::TRANSPARENT, - ), - }; - - if matches!(item.error_level, ErrorLevel::Info) { - ui.label(RichText::new(&item.msg).monospace()); - } else { - let available_width = ui.available_width(); - let frame = egui::Frame::new() - .inner_margin(Margin::symmetric(8, 6)) - .fill(bg_color) - .stroke(egui::Stroke::new(1.0, stroke_color)); - - let response = frame - .show(ui, |ui| { - ui.set_width(available_width - 10.0); - ui.horizontal(|ui| { - ui.label(RichText::new(&item.msg).color(text_color).monospace()); - }); - }) - .response; - - if response.clicked() { - log::debug!("Log item clicked: {}", &item.id); - if let (Some(path), Some(loc)) = - (&item.file_location, &item.line_ref) - { - let location_arg = format!("{}:{}", path.display(), loc); - - match std::process::Command::new("code") - .args(["-g", &location_arg]) - .spawn() - .map(|_| ()) - { - Ok(()) => { - log::info!( - "Launched Visual Studio Code at the error: {}", - &location_arg - ); - } - Err(e) => { - warn!( - "Failed to open '{}' in VS Code: {}", - location_arg, e - ); - } - } - } - } - } - } - }); + self.build_console(ui); } EditorTab::Console => { ui.heading("Console"); @@ -74,38 +74,8 @@ impl<'a> EditorTabViewer<'a> { }; if ui.button(display_name).clicked() { - if name.contains("MeshRenderer") { - *signal = Signal::AddComponent( - entity, - "MeshRenderer".to_string(), - ); - } else if name.contains("CameraComponent") - || name.contains("Camera3D") - { - *signal = Signal::AddComponent( - entity, - "CameraComponent".to_string(), - ); - } else if name.contains("Light") { - *signal = Signal::AddComponent( - entity, - "Light".to_string(), - ); - } else { - if let Some(comp) = - registry.create_default_component(id) - { - let mut builder = EntityBuilder::new(); - if let Ok(_) = registry - .deserialize_into_builder( - comp.as_ref(), - &mut builder, - ) - { - let _ = world - .insert(entity, builder.build()); - } - } + if let Some(component) = registry.create_default_component(id) { + *signal = Signal::AddComponent(entity, component); } ui.close(); } @@ -1,10 +1,15 @@ +pub mod asset_viewer; +pub mod build_console; pub mod component; pub mod console_error; +pub mod console; pub mod dock; +pub mod entity_list; pub mod input; +pub mod resource; pub mod scene; pub mod settings; -mod console; +pub mod viewport; pub(crate) use crate::editor::dock::*; @@ -1531,10 +1536,8 @@ pub enum Signal { Play, StopPlaying, LogEntities, - /// This only applies to builders with specific behaviours that the standard component - /// registry is unable to have. Most don't apply to this signal, however some are supported, - /// such as [`MeshRenderer`] (which uses async loading). - AddComponent(hecs::Entity, String), + /// Adds a new component instance using the async init pipeline. + AddComponent(hecs::Entity, Box<dyn SerializableComponent>), /// Loads a model from a URI/path and swaps it onto an existing MeshRenderer (or adds one if missing). ReplaceModel(hecs::Entity, String), @@ -1,25 +1,27 @@ use crate::editor::{Editor, EditorState, Signal}; +use dropbear_engine::asset::ASSET_REGISTRY; use dropbear_engine::camera::Camera; -use dropbear_engine::entity::{MeshRenderer, Transform}; -use dropbear_engine::graphics::{SharedGraphicsContext}; +use dropbear_engine::entity::{EntityTransform, MeshRenderer, Transform}; +use dropbear_engine::graphics::SharedGraphicsContext; use dropbear_engine::lighting::{Light as EngineLight, LightComponent}; -use dropbear_engine::model::{LoadedModel, Material, Model, ModelId, MODEL_CACHE}; +use dropbear_engine::model::{LoadedModel, Material, Model, MODEL_CACHE}; use dropbear_engine::procedural::{ProcedurallyGeneratedObject, ProcObj}; use dropbear_engine::texture::{Texture, TextureWrapMode}; -use dropbear_engine::utils::{relative_path_from_euca, EUCA_SCHEME, ResourceReference, ResourceReferenceType}; +use dropbear_engine::utils::{ + relative_path_from_euca, EUCA_SCHEME, ResourceReference, ResourceReferenceType, +}; +use dropbear_traits::{ComponentInitContext, ComponentInsert, ComponentResources}; use egui::Align2; use eucalyptus_core::camera::{CameraComponent, CameraType}; +use eucalyptus_core::properties::CustomProperties; use eucalyptus_core::scripting::{build_jvm, BuildStatus}; use eucalyptus_core::spawn::{push_pending_spawn, PendingSpawn}; -use eucalyptus_core::states::{ - EditorTab, Label, Light, Script, PROJECT, -}; +use eucalyptus_core::states::{EditorTab, Label, Script, PROJECT}; use eucalyptus_core::{fatal, info, success, success_without_console, warn, warn_without_console}; use std::any::TypeId; use std::path::PathBuf; use std::sync::Arc; use winit::keyboard::KeyCode; -use eucalyptus_core::properties::CustomProperties; fn resolve_editor_path(uri: &str) -> PathBuf { if uri.starts_with(EUCA_SCHEME) { @@ -32,6 +34,33 @@ fn resolve_editor_path(uri: &str) -> PathBuf { } } +struct InsertMeshRenderer { + renderer: MeshRenderer, + ensure_transform: bool, +} + +impl ComponentInsert for InsertMeshRenderer { + fn insert( + self: Box<Self>, + world: &mut hecs::World, + entity: hecs::Entity, + ) -> anyhow::Result<()> { + if world.get::<&MeshRenderer>(entity).is_ok() { + let _ = world.remove_one::<MeshRenderer>(entity); + } + + world + .insert_one(entity, self.renderer) + .map_err(|e| anyhow::anyhow!(e.to_string()))?; + + if self.ensure_transform && world.get::<&EntityTransform>(entity).is_err() { + let _ = world.insert_one(entity, EntityTransform::default()); + } + + Ok(()) + } +} + pub trait SignalController { fn run_signal(&mut self, graphics: Arc<SharedGraphicsContext>) -> anyhow::Result<()>; } @@ -455,15 +484,15 @@ impl SignalController for Editor { } 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; - + success!("Exited play mode"); log::info!("Back to the editor you go..."); @@ -543,79 +572,19 @@ impl SignalController for Editor { self.signal = Signal::None; Ok(()) } - Signal::AddComponent(entity, component_name) => { - if component_name == "MeshRenderer" { - let unassigned_id = (*entity).to_bits().get(); - let reference = ResourceReference::from_reference( - ResourceReferenceType::Unassigned { id: unassigned_id }, - ); - - let model = std::sync::Arc::new(Model { - label: "None".to_string(), - hash: unassigned_id, - path: reference, - meshes: Vec::new(), - materials: Vec::new(), - skins: Vec::new(), - animations: Vec::new(), - nodes: Vec::new(), - }); - - let loaded_model = LoadedModel::new_raw( - &dropbear_engine::asset::ASSET_REGISTRY, - model, - ); + Signal::AddComponent(entity, component) => { + let mut resources = ComponentResources::new(); + resources.insert(graphics.clone()); + let ctx = ComponentInitContext { + entity: *entity, + resources: Arc::new(resources), + }; - let renderer = dropbear_engine::entity::MeshRenderer::from_handle(loaded_model); - let _ = self.world.insert_one(*entity, renderer); - success!("Added MeshRenderer (unassigned) for entity {:?}", entity); - } else if component_name == "CameraComponent" { - let graphics_clone = graphics.clone(); - let future = async move { - let camera = Camera::predetermined(graphics_clone, Some("New Camera")); - let component = CameraComponent::new(); - Ok::<(Camera, CameraComponent), anyhow::Error>((camera, component)) - }; - let handle = graphics.future_queue.push(Box::pin(future)); - self.pending_components.push((*entity, handle)); - success!("Queued Camera addition for entity {:?}", entity); - } else if component_name == "Light" { - let graphics_clone = graphics.clone(); - let future = async move { - let light_comp = LightComponent::default(); - let transform = Transform::default(); - let engine_light = EngineLight::new( - graphics_clone, - light_comp.clone(), - transform, - Some("New Light"), - ) - .await; - - let light_config = Light { - label: "New Light".to_string(), - transform, - light_component: light_comp.clone(), - enabled: true, - entity_id: None, - }; + let init_future = component.init(ctx); + let handle = graphics.future_queue.push(init_future); + self.pending_components.push((*entity, handle)); - Ok::<(LightComponent, EngineLight, Light, Transform), anyhow::Error>(( - light_comp, - engine_light, - light_config, - transform, - )) - }; - let handle = graphics.future_queue.push(Box::pin(future)); - self.pending_components.push((*entity, handle)); - success!("Queued Light addition for entity {:?}", entity); - } else { - warn!( - "Unknown component type for AddComponent signal: {}", - component_name - ); - } + success!("Queued component addition for entity {:?}", entity); self.signal = Signal::None; Ok(()) } @@ -656,7 +625,7 @@ impl SignalController for Editor { let size = glam::DVec3::new(1.0, 1.0, 1.0); let size_bits = [1.0f32.to_bits(), 1.0f32.to_bits(), 1.0f32.to_bits()]; let mut loaded = ProcedurallyGeneratedObject::cuboid(size) - .build_model(graphics_clone.clone(), None, Some("Cuboid")); + .build_model(graphics_clone.clone(), None, Some("Cuboid"), ASSET_REGISTRY.clone()); let model = loaded.make_mut(); model.path = ResourceReference::from_reference(ResourceReferenceType::ProcObj(ProcObj::Cuboid { size_bits })); @@ -690,11 +659,11 @@ impl SignalController for Editor { let graphics_clone = graphics.clone(); let uri_clone = uri.clone(); let future = async move { - if is_legacy_internal_cube_uri(&uri_clone) { + let renderer = if is_legacy_internal_cube_uri(&uri_clone) { let size = glam::DVec3::new(1.0, 1.0, 1.0); let size_bits = [1.0f32.to_bits(), 1.0f32.to_bits(), 1.0f32.to_bits()]; let mut loaded_model = ProcedurallyGeneratedObject::cuboid(size) - .build_model(graphics_clone.clone(), None, Some("Cuboid")); + .build_model(graphics_clone.clone(), None, Some("Cuboid"), ASSET_REGISTRY.clone()); { let model = loaded_model.make_mut(); @@ -705,7 +674,7 @@ impl SignalController for Editor { } loaded_model.refresh_registry(); - Ok::<MeshRenderer, anyhow::Error>(MeshRenderer::from_handle(loaded_model)) + MeshRenderer::from_handle(loaded_model) } else { let path = resolve_editor_path(&uri_clone); let mut model = dropbear_engine::model::Model::load( @@ -724,11 +693,13 @@ impl SignalController for Editor { } model.refresh_registry(); + dropbear_engine::entity::MeshRenderer::from_handle(model) + }; - Ok::<MeshRenderer, anyhow::Error>( - dropbear_engine::entity::MeshRenderer::from_handle(model), - ) - } + Ok::<Box<dyn ComponentInsert>, anyhow::Error>(Box::new(InsertMeshRenderer { + renderer, + ensure_transform: true, + })) }; let handle = graphics.future_queue.push(Box::pin(future)); @@ -738,7 +709,8 @@ impl SignalController for Editor { Ok(()) } - Signal::SetProceduralCuboid(entity, size) | Signal::UpdateProceduralCuboid(entity, size) => { + Signal::SetProceduralCuboid(entity, size) + | Signal::UpdateProceduralCuboid(entity, size) => { let previous_customisation: Option< Vec<(String, [f32; 4], Option<String>, TextureWrapMode, [f32; 2])>, > = @@ -777,7 +749,7 @@ impl SignalController for Editor { let size_vec = glam::DVec3::new(size[0] as f64, size[1] as f64, size[2] as f64); let mut loaded_model = ProcedurallyGeneratedObject::cuboid(size_vec) - .build_model(graphics.clone(), None, Some(&label)); + .build_model(graphics.clone(), None, Some(&label), ASSET_REGISTRY.clone()); { let model = loaded_model.make_mut(); @@ -791,7 +763,9 @@ impl SignalController for Editor { if let Some(previous) = previous_customisation { let model = renderer.make_model_mut(); for (mat_name, tint, texture_tag, wrap_mode, uv_tiling) in previous { - if let Some(material) = model.materials.iter_mut().find(|m| m.name == mat_name) { + if let Some(material) = + model.materials.iter_mut().find(|m| m.name == mat_name) + { material.wrap_mode = wrap_mode; material.set_tint(graphics.as_ref(), tint); material.set_uv_tiling(graphics.as_ref(), uv_tiling); @@ -812,7 +786,7 @@ impl SignalController for Editor { None, None, Some(Texture::sampler_from_wrap(wrap_mode)), - Some(mat_name.as_str()) + Some(mat_name.as_str()), ); let flat_normal = (*dropbear_engine::asset::ASSET_REGISTRY .solid_texture_rgba8( @@ -823,12 +797,14 @@ impl SignalController for Editor { material.diffuse_texture = diffuse; material.normal_texture = flat_normal; - material.bind_group = dropbear_engine::model::Material::create_bind_group( - graphics.as_ref(), - &material.diffuse_texture, - &material.normal_texture, - &material.name, - ); + material.bind_group = + dropbear_engine::model::Material::create_bind_group( + graphics.as_ref(), + &material.diffuse_texture, + &material.normal_texture, + &material.tint_buffer, + &material.name, + ); material.texture_tag = Some(uri); } } else { @@ -867,7 +843,7 @@ impl SignalController for Editor { None, None, Some(Texture::sampler_from_wrap(wrap_mode.clone())), - Some(target_material) + Some(target_material), ); let flat_normal = (*dropbear_engine::asset::ASSET_REGISTRY .solid_texture_rgba8(graphics.clone(), [128, 128, 255, 255])) @@ -886,6 +862,7 @@ impl SignalController for Editor { graphics.as_ref(), &material.diffuse_texture, &material.normal_texture, + &material.tint_buffer, &material.name, ); material.texture_tag = Some(uri.clone()); @@ -921,13 +898,14 @@ impl SignalController for Editor { None, None, Some(Texture::sampler_from_wrap(wrap_mode.clone())), - Some(target_material) + Some(target_material), ); material.diffuse_texture = diffuse; material.bind_group = Material::create_bind_group( graphics.as_ref(), &material.diffuse_texture, &material.normal_texture, + &material.tint_buffer, &material.name, ); } else { @@ -969,7 +947,9 @@ impl SignalController for Editor { } Signal::ClearMaterialTexture(entity, target_material) => { - let diffuse = (*dropbear_engine::asset::ASSET_REGISTRY.grey_texture(graphics.clone())).clone(); + let diffuse = + (*dropbear_engine::asset::ASSET_REGISTRY.grey_texture(graphics.clone())) + .clone(); let flat_normal = (*dropbear_engine::asset::ASSET_REGISTRY .solid_texture_rgba8(graphics.clone(), [128, 128, 255, 255])) .clone(); @@ -987,6 +967,7 @@ impl SignalController for Editor { graphics.as_ref(), &material.diffuse_texture, &material.normal_texture, + &material.tint_buffer, &material.name, ); material.texture_tag = None; @@ -1,36 +1,13 @@ use crate::editor::Editor; -use dropbear_engine::asset::ASSET_REGISTRY; -use dropbear_engine::camera::Camera; -use dropbear_engine::entity::{EntityTransform, MeshRenderer, Transform}; +use dropbear_engine::entity::{EntityTransform, MeshRenderer}; use dropbear_engine::future::FutureQueue; use dropbear_engine::graphics::{SharedGraphicsContext}; -use dropbear_engine::lighting::{Light, LightComponent}; -use dropbear_engine::model::{LoadedModel, Material, Model, ModelId}; -use dropbear_engine::texture::Texture; -use dropbear_engine::utils::{ResourceReference, ResourceReferenceType}; -use dropbear_engine::procedural::ProcObj; -use eucalyptus_core::camera::CameraComponent; -use eucalyptus_core::scene::SceneEntity; +use dropbear_engine::model::LoadedModel; +use dropbear_traits::{ComponentInitContext, ComponentInsert, ComponentResources}; pub(crate) use eucalyptus_core::spawn::{PendingSpawnController, PENDING_SPAWNS}; -use eucalyptus_core::states::{ - Light as LightConfig, Script, SerializedMeshRenderer, -}; -use eucalyptus_core::utils::ResolveReference; use eucalyptus_core::{fatal, success}; -use hecs::EntityBuilder; +use hecs::Entity; use std::sync::Arc; -use eucalyptus_core::properties::CustomProperties; - -fn component_ref<'a, T: 'static>(entity: &'a SceneEntity) -> Option<&'a T> { - entity - .components - .iter() - .find_map(|component| component.as_any().downcast_ref::<T>()) -} - -fn component_cloned<T: Clone + 'static>(entity: &SceneEntity) -> Option<T> { - component_ref::<T>(entity).cloned() -} impl PendingSpawnController for Editor { fn check_up( @@ -48,52 +25,58 @@ impl PendingSpawnController for Editor { spawn.scene_entity.label ); - let serialized_renderer = - component_cloned::<SerializedMeshRenderer>(&spawn.scene_entity); + if spawn.handle.is_none() { + let entity = self.world.spawn((spawn.scene_entity.label.clone(),)); + let components = spawn.scene_entity.components.clone(); + + let mut resources = ComponentResources::new(); + resources.insert(graphics.clone()); + let resources = Arc::new(resources); + + let future = async move { + let mut inserts: Vec<Box<dyn ComponentInsert>> = Vec::new(); + for component in components { + let ctx = ComponentInitContext { + entity, + resources: resources.clone(), + }; + let insert = component.init(ctx).await?; + inserts.push(insert); + } - if serialized_renderer.is_none() && spawn.handle.is_none() { - log::debug!( - "No renderer component found for '{}', spawning immediately", - spawn.scene_entity.label - ); - self.spawn_scene_entity(&spawn.scene_entity, None); - completed.push(index); - continue; - } + Ok::<(Entity, Vec<Box<dyn ComponentInsert>>), anyhow::Error>((entity, inserts)) + }; - if spawn.handle.is_none() { - if let Some(renderer) = serialized_renderer.clone() { - let graphics_clone = graphics.clone(); - let label = spawn.scene_entity.label.to_string(); - let future = async move { - load_renderer_from_serialized(renderer, graphics_clone, label).await - }; - let handle = queue.push(Box::pin(future)); - spawn.handle = Some(handle); - } + let handle = queue.push(Box::pin(future)); + spawn.handle = Some(handle); } if let Some(handle) = &spawn.handle { if let Some(result) = queue.exchange_owned(handle) { - if let Ok(r) = result.downcast::<anyhow::Result<MeshRenderer>>() { + if let Ok(r) = result.downcast::<anyhow::Result<(Entity, Vec<Box<dyn ComponentInsert>>)>>() { match Arc::try_unwrap(r) { - Ok(outcome) => match outcome { - Ok(renderer) => { - self.spawn_scene_entity(&spawn.scene_entity, Some(renderer)); - success!( - "Spawned '{}' from pending queue", - spawn.scene_entity.label - ); - completed.push(index); + Ok(Ok((entity, inserts))) => { + for insert in inserts { + insert.insert(&mut self.world, entity)?; } - Err(err) => { - fatal!("Unable to load mesh renderer: {}", err); - completed.push(index); + + if self.world.get::<&EntityTransform>(entity).is_err() { + let _ = self.world.insert_one(entity, EntityTransform::default()); } - }, + + success!( + "Spawned '{}' from pending queue", + spawn.scene_entity.label + ); + completed.push(index); + } + Ok(Err(err)) => { + fatal!("Unable to init components for '{}': {}", spawn.scene_entity.label, err); + completed.push(index); + } Err(_) => { log_once::warn_once!( - "Renderer future for '{}' still shared, deferring", + "Spawn future for '{}' still shared, deferring", spawn.scene_entity.label ); } @@ -116,69 +99,22 @@ impl PendingSpawnController for Editor { let mut completed_components = Vec::new(); for (index, (entity, handle)) in self.pending_components.iter().enumerate() { if let Some(result) = queue.exchange_owned(handle) { - match result.downcast::<anyhow::Result<MeshRenderer>>() { - Ok(r) => { - match Arc::try_unwrap(r) { - Ok(Ok(renderer)) => { - let _ = self.world.insert_one(*entity, renderer); - let _ = self.world.insert_one(*entity, EntityTransform::default()); - success!("Added MeshRenderer to entity {:?}", entity); - completed_components.push(index); - } - Ok(Err(e)) => { - fatal!("Failed to load MeshRenderer: {}", e); - completed_components.push(index); - } - Err(_) => {} // Still shared + if let Ok(r) = result.downcast::<anyhow::Result<Box<dyn ComponentInsert>>>() { + match Arc::try_unwrap(r) { + Ok(Ok(insert)) => { + insert.insert(&mut self.world, *entity)?; + success!("Added component to entity {:?}", entity); + completed_components.push(index); } - } - Err(result) => { - match result.downcast::<anyhow::Result<(Camera, CameraComponent)>>() { - Ok(r) => { - match Arc::try_unwrap(r) { - Ok(Ok((camera, component))) => { - let _ = self.world.insert(*entity, (camera, component)); - success!("Added Camera to entity {:?}", entity); - completed_components.push(index); - } - Ok(Err(e)) => { - fatal!("Failed to create Camera: {}", e); - completed_components.push(index); - } - Err(_) => {} // Still shared - } - } - Err(result) => { - if let Ok(r) = result.downcast::<anyhow::Result<( - LightComponent, - Light, - LightConfig, - Transform, - )>>() { - match Arc::try_unwrap(r) { - Ok(Ok(( - light_comp, - engine_light, - light_config, - transform, - ))) => { - let _ = self.world.insert( - *entity, - (light_comp, engine_light, light_config, transform), - ); - success!("Added Light to entity {:?}", entity); - completed_components.push(index); - } - Ok(Err(e)) => { - fatal!("Failed to create Light: {}", e); - completed_components.push(index); - } - Err(_) => {} // Still shared - } - } - } + Ok(Err(e)) => { + fatal!("Failed to add component: {}", e); + completed_components.push(index); } + Err(_) => {} // Still shared } + } else { + fatal!("Pending component result could not be downcasted"); + completed_components.push(index); } } } @@ -224,227 +160,3 @@ impl PendingSpawnController for Editor { Ok(()) } } - -impl Editor { - fn spawn_scene_entity( - &mut self, - scene_entity: &SceneEntity, - mesh_renderer: Option<MeshRenderer>, - ) { - let mut builder = EntityBuilder::new(); - builder.add(scene_entity.label.clone()); - - if let Some(transform) = component_ref::<EntityTransform>(scene_entity).copied() { - builder.add(transform); - } else { - builder.add(EntityTransform::default()); - } - - if let Some(renderer) = mesh_renderer { - builder.add(renderer); - } - - if let Some(props) = component_cloned::<CustomProperties>(scene_entity) { - builder.add(props); - } - - if let Some(script) = component_cloned::<Script>(scene_entity) { - builder.add(script); - } - - if let Some(camera) = component_cloned::<CameraComponent>(scene_entity) { - builder.add(camera); - } - - self.world.spawn(builder.build()); - } -} - -async fn load_renderer_from_serialized( - renderer: SerializedMeshRenderer, - graphics: Arc<SharedGraphicsContext>, - label: String, -) -> anyhow::Result<MeshRenderer> { - fn is_legacy_internal_cube_uri(uri: &str) -> bool { - let uri = uri.replace('\\', "/"); - uri.ends_with("internal/dropbear/models/cube") - } - - let import_scale = renderer.import_scale.unwrap_or(1.0); - - let mut mesh_renderer = match &renderer.handle.ref_type { - ResourceReferenceType::None => anyhow::bail!( - "Renderer for '{}' does not specify an asset reference", - label - ), - ResourceReferenceType::Unassigned { id } => { - let model = std::sync::Arc::new(Model { - label: "None".to_string(), - hash: *id, - path: ResourceReference::from_reference(ResourceReferenceType::Unassigned { id: *id }), - meshes: Vec::new(), - materials: Vec::new(), - skins: Vec::new(), - animations: Vec::new(), - nodes: Vec::new(), - }); - - let loaded = LoadedModel::new_raw(&ASSET_REGISTRY, model); - MeshRenderer::from_handle_with_import_scale(loaded, import_scale) - } - ResourceReferenceType::File(reference) => { - if is_legacy_internal_cube_uri(reference) { - let size = glam::DVec3::new(1.0, 1.0, 1.0); - let size_bits = [1.0f32.to_bits(), 1.0f32.to_bits(), 1.0f32.to_bits()]; - let mut loaded_model = dropbear_engine::procedural::ProcedurallyGeneratedObject::cuboid(size) - .build_model(graphics.clone(), None, Some(&label)); - - let model = loaded_model.make_mut(); - model.path = ResourceReference::from_reference(ResourceReferenceType::ProcObj(ProcObj::Cuboid { size_bits })); - - loaded_model.refresh_registry(); - - MeshRenderer::from_handle(loaded_model) - } else { - let path = renderer.handle.resolve()?; - let loaded = Model::load(graphics.clone(), &path, Some(&label), None).await?; - MeshRenderer::from_handle_with_import_scale(loaded, import_scale) - } - } - ResourceReferenceType::Bytes(bytes) => { - let loaded = - Model::load_from_memory(graphics.clone(), bytes.clone(), Some(&label), None) - .await?; - MeshRenderer::from_handle_with_import_scale(loaded, import_scale) - } - ResourceReferenceType::ProcObj(ProcObj::Cuboid { size_bits }) => { - let size = [ - f32::from_bits(size_bits[0]), - f32::from_bits(size_bits[1]), - f32::from_bits(size_bits[2]), - ]; - let size_vec = glam::DVec3::new(size[0] as f64, size[1] as f64, size[2] as f64); - let mut loaded_model = dropbear_engine::procedural::ProcedurallyGeneratedObject::cuboid(size_vec) - .build_model(graphics.clone(), None, Some(&label)); - - let model = loaded_model.make_mut(); - model.path = ResourceReference::from_reference(ResourceReferenceType::ProcObj(ProcObj::Cuboid { size_bits: *size_bits })); - - loaded_model.refresh_registry(); - - MeshRenderer::from_handle_with_import_scale(loaded_model, import_scale) - } - }; - - for override_entry in renderer.material_override { - if ASSET_REGISTRY - .model_handle_from_reference(&override_entry.source_model) - .is_none() - { - if matches!( - override_entry.source_model.ref_type, - ResourceReferenceType::File(_) - ) { - let source_path = override_entry.source_model.resolve()?; - let label_hint = override_entry.source_model.as_uri(); - if let Err(err) = Model::load(graphics.clone(), &source_path, label_hint, None).await { - log::warn!( - "Failed to preload source model {:?} for override '{}': {}", - override_entry.source_model, - override_entry.target_material, - err - ); - continue; - } - } else { - log::warn!( - "Unsupported override source {:?} for '{}'", - override_entry.source_model, - label - ); - continue; - } - } - - if let Err(err) = mesh_renderer.apply_material_override( - &override_entry.target_material, - override_entry.source_model.clone(), - &override_entry.source_material, - ) { - log::warn!( - "Failed to apply material override '{}' on '{}': {}", - override_entry.target_material, - label, - err - ); - } - } - - if !renderer.material_customisation.is_empty() { - for custom in &renderer.material_customisation { - let model_mut = mesh_renderer.make_model_mut(); - let name_index = model_mut - .materials - .iter() - .position(|mat| mat.name == custom.target_material); - let index = name_index.or(custom.material_index); - - if let Some(material) = index.and_then(|idx| model_mut.materials.get_mut(idx)) { - material.set_tint(graphics.as_ref(), custom.tint); - material.set_uv_tiling(graphics.as_ref(), custom.uv_tiling); - - if let Some(reference) = &custom.diffuse_texture { - if let Ok(path) = reference.resolve() { - match std::fs::read(&path) { - Ok(bytes) => { - let diffuse = Texture::from_bytes_verbose_mipmapped( - graphics.clone(), - &bytes, - None, - None, - Some(Texture::sampler_from_wrap(custom.wrap_mode)), - Some(material.name.as_str()), - ); - let flat_normal = (*ASSET_REGISTRY - .solid_texture_rgba8(graphics.clone(), [128, 128, 255, 255])) - .clone(); - - material.diffuse_texture = diffuse; - material.normal_texture = flat_normal; - material.bind_group = Material::create_bind_group( - graphics.as_ref(), - &material.diffuse_texture, - &material.normal_texture, - &material.name, - ); - material.texture_tag = reference.as_uri().map(|s| s.to_string()); - material.wrap_mode = custom.wrap_mode; - material.set_uv_tiling(graphics.as_ref(), custom.uv_tiling); - } - Err(err) => { - log::warn!( - "Failed to read custom texture '{}' for '{}': {}", - path.display(), - label, - err - ); - } - } - } else { - log::warn!( - "Failed to resolve custom texture reference {:?} for '{}'", - reference, - label - ); - } - } - } - } - - mesh_renderer.sync_asset_registry(); - } - - mesh_renderer.set_import_scale(import_scale); - - Ok(mesh_renderer) -} @@ -126,6 +126,8 @@ pub struct PlayMode { shader_globals: Option<GlobalsUniform>, collider_wireframe_pipeline: Option<ColliderWireframePipeline>, sky_pipeline: Option<SkyPipeline>, + default_skinning_buffer: Option<wgpu::Buffer>, + default_skinning_bind_group: Option<wgpu::BindGroup>, initial_scene: Option<String>, current_scene: Option<String>, @@ -211,6 +213,8 @@ impl PlayMode { }, kino: None, sky_pipeline: None, + default_skinning_buffer: None, + default_skinning_bind_group: None, }; log::debug!("Created new play mode instance"); @@ -234,7 +238,7 @@ impl PlayMode { graphics.viewport_texture.size.height as f32, ], ), - KinoWinitWindowing::new(graphics.window.clone()), + KinoWinitWindowing::new(graphics.window.clone(), None), )); let sky_texture = HdrLoader::from_equirectangular_bytes( @@ -438,7 +442,7 @@ impl PlayMode { progress.camera_received = true; self.scene_progress = Some(progress); - self.load_wgpu_nerdy_stuff(graphics); + self.load_wgpu_nerdy_stuff(graphics.clone()); self.reload_scripts_for_current_world(graphics.clone()); @@ -461,7 +465,7 @@ impl PlayMode { self.active_camera = Some(new_camera); } - self.load_wgpu_nerdy_stuff(graphics); + self.load_wgpu_nerdy_stuff(graphics.clone()); self.reload_scripts_for_current_world(graphics.clone()); self.current_scene = Some(scene_progress.requested_scene.clone()); @@ -7,19 +7,21 @@ use eucalyptus_core::egui::CentralPanel; use eucalyptus_core::physics::collider::ColliderGroup; use eucalyptus_core::physics::collider::ColliderShapeKey; use eucalyptus_core::physics::collider::shader::ColliderInstanceRaw; -use glam::{vec2, DMat4, DQuat, DVec3, Quat, Vec2}; +use glam::{vec2, DMat4, DQuat, DVec3, Mat4, Quat, Vec2}; use hecs::Entity; use wgpu::Color; use wgpu::util::DeviceExt; use winit::event_loop::ActiveEventLoop; use winit::event::WindowEvent; +use dropbear_engine::animation::AnimationComponent; +use dropbear_engine::asset::{ASSET_REGISTRY, Handle}; use dropbear_engine::camera::Camera; use dropbear_engine::buffer::ResizableBuffer; use dropbear_engine::entity::{EntityTransform, MeshRenderer, Transform}; use dropbear_engine::graphics::{InstanceRaw, SharedGraphicsContext}; use dropbear_engine::lighting::{Light, LightComponent}; use dropbear_engine::lighting::MAX_LIGHTS; -use dropbear_engine::model::{DrawLight, DrawModel, ModelId, MODEL_CACHE}; +use dropbear_engine::model::{DrawLight, DrawModel, Model}; use dropbear_engine::scene::{Scene, SceneCommand}; use eucalyptus_core::camera::CameraComponent; use eucalyptus_core::command::CommandBufferPoller; @@ -32,7 +34,6 @@ use eucalyptus_core::states::SCENES; use eucalyptus_core::scene::loading::{IsSceneLoaded, SceneLoadResult, SCENE_LOADER}; use crate::PlayMode; use eucalyptus_core::physics::collider::shader::create_wireframe_geometry; -use eucalyptus_core::ui::UI_CONTEXT; use kino_ui::widgets::{Anchor, Border, Fill}; use kino_ui::widgets::rect::Rectangle; @@ -354,6 +355,27 @@ impl Scene for PlayMode { } { + let registry = ASSET_REGISTRY.read(); + let mut query = self + .world + .query::<(&MeshRenderer, &mut AnimationComponent)>(); + + for (renderer, animation) in query.iter() { + let handle = renderer.model(); + if handle.is_null() { + continue; + } + + let Some(model) = registry.get_model(handle) else { + continue; + }; + + animation.update(dt, model); + animation.prepare_gpu_resources(graphics.clone()); + } + } + + { let mut light_query = self .world .query::<(&mut LightComponent, Option<&Transform>, Option<&EntityTransform>, &mut Light)>(); @@ -683,32 +705,34 @@ impl Scene for PlayMode { globals.write(&graphics.queue); } - let renderers = { - let mut renderers = Vec::new(); - let mut query = self.world.query::<&MeshRenderer>(); - for renderer in query.iter() { - renderers.push(renderer.clone()); - } - renderers - }; + let mut static_batches: HashMap<u64, Vec<InstanceRaw>> = HashMap::new(); + let mut animated_instances: Vec<(u64, InstanceRaw, wgpu::BindGroup)> = Vec::new(); + + { + let mut query = self + .world + .query::<(&MeshRenderer, Option<&AnimationComponent>)>(); + + for (renderer, animation) in query.iter() { + let handle = renderer.model(); + if handle.is_null() { + continue; + } - let mut model_batches: HashMap<ModelId, Vec<InstanceRaw>> = HashMap::new(); - for renderer in &renderers { - model_batches - .entry(renderer.model_id()) - .or_default() - .push(renderer.instance.to_raw()); + let instance = renderer.instance.to_raw(); + if let Some(bind_group) = animation.and_then(|anim| anim.bind_group.clone()) { + animated_instances.push((handle.id, instance, bind_group)); + } else { + static_batches.entry(handle.id).or_default().push(instance); + } + } } + let registry = ASSET_REGISTRY.read(); let mut prepared_models = Vec::new(); - for (model_id, instances) in model_batches { - let model_opt = { - let cache = MODEL_CACHE.lock(); - cache.values().find(|model| model.id == model_id).cloned() - }; - - let Some(model) = model_opt else { - log_once::error_once!("Missing model {:?} in cache", model_id); + for (handle, instances) in static_batches { + let Some(model) = registry.get_model(Handle::new(handle)).cloned() else { + log_once::error_once!("Missing model handle {} in registry", handle); continue; }; @@ -744,6 +768,7 @@ impl Scene for PlayMode { } } + let registry = ASSET_REGISTRY.read(); { let mut render_pass = encoder .begin_render_pass(&wgpu::RenderPassDescriptor { @@ -772,17 +797,53 @@ impl Scene for PlayMode { render_pass.set_pipeline(light_pipeline.pipeline()); for (light, component) in &lights { render_pass.set_vertex_buffer(1, light.instance_buffer.buffer().slice(..)); - if component.visible { - render_pass.draw_light_model( - &light.cube_model, - &camera.bind_group, - &light.bind_group - ); + if !component.visible { + continue; } + + let Some(model) = registry.get_model(light.cube_model) else { + log_once::error_once!( + "Missing light cube model handle {} in registry", + light.cube_model.id + ); + continue; + }; + + render_pass.draw_light_model( + model, + &camera.bind_group, + &light.bind_group, + ); } } } + if self.default_skinning_bind_group.is_none() { + let identity = [Mat4::IDENTITY.to_cols_array_2d()]; + let buffer = graphics.device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("default skinning buffer"), + contents: bytemuck::cast_slice(&identity), + usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, + }); + + let bind_group = graphics.device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("default skinning bind group"), + layout: &graphics.layouts.skinning_bind_group_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: buffer.as_entire_binding(), + }], + }); + + self.default_skinning_buffer = Some(buffer); + self.default_skinning_bind_group = Some(bind_group); + } + + let default_skinning_bind_group = self + .default_skinning_bind_group + .as_ref() + .expect("Default skinning bind group not initialized"); + // model rendering if let Some(lcp) = &self.light_cube_pipeline { for (model, instance_buffer, instance_count) in prepared_models { @@ -817,13 +878,82 @@ impl Scene for PlayMode { }); render_pass.set_pipeline(pipeline.pipeline()); render_pass.set_vertex_buffer(1, instance_buffer.slice(..)); - render_pass.set_bind_group(4, globals_bind_group, &[]); - render_pass.draw_model_instanced( - &model, - 0..instance_count, - &camera.bind_group, - lcp.bind_group(), + render_pass.set_bind_group(3, globals_bind_group, &[]); + + for mesh in &model.meshes { + let material = &model.materials[mesh.material]; + render_pass.draw_mesh_instanced( + mesh, + material, + 0..instance_count, + &camera.bind_group, + lcp.bind_group(), + Some(default_skinning_bind_group), + ); + } + } + } + + if let Some(lcp) = &self.light_cube_pipeline { + let globals_bind_group = &self + .shader_globals + .as_ref() + .expect("Shader globals not initialised") + .bind_group; + + for (handle, instance, skin_bind_group) in animated_instances { + let Some(model) = registry.get_model(Handle::new(handle)).cloned() else { + log_once::error_once!("Missing model handle {} in registry", handle); + continue; + }; + + let instance_buffer = graphics.device.create_buffer_init( + &wgpu::util::BufferInitDescriptor { + label: Some("Runtime Animated Instance Buffer"), + contents: bytemuck::cast_slice(&[instance]), + usage: wgpu::BufferUsages::VERTEX, + }, ); + + let mut render_pass = encoder + .begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("model render pass (animated)"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: hdr.view(), + depth_slice: None, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { + view: &graphics.depth_texture.view, + depth_ops: Some(wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }), + stencil_ops: None, + }), + occlusion_query_set: None, + timestamp_writes: None, + }); + + render_pass.set_pipeline(pipeline.pipeline()); + render_pass.set_vertex_buffer(1, instance_buffer.slice(..)); + render_pass.set_bind_group(3, globals_bind_group, &[]); + + for mesh in &model.meshes { + let material = &model.materials[mesh.material]; + render_pass.draw_mesh_instanced( + mesh, + material, + 0..1, + &camera.bind_group, + lcp.bind_group(), + Some(&skin_bind_group), + ); + } } } @@ -179,42 +179,53 @@ typedef struct NChannelValuesFfi { typedef NChannelValuesFfi NChannelValues; -typedef void* SceneLoaderPtr; +typedef void* WorldPtr; -typedef void* AssetRegistryPtr; +typedef struct i32Array { + int32_t* values; + size_t length; + size_t capacity; +} i32Array; -typedef struct NVector2 { - double x; - double y; -} NVector2; +typedef struct f64ArrayArray { + double* values; + size_t length; + size_t capacity; +} f64ArrayArray; -typedef struct Progress { - size_t current; - size_t total; - const char* message; -} Progress; +typedef struct NSkin { + const char* name; + i32Array joints; + f64ArrayArray inverse_bind_matrices; + const int32_t* skeleton_root; +} NSkin; -typedef struct u64Array { - uint64_t* values; +typedef struct NSkinArray { + NSkin* values; size_t length; size_t capacity; -} u64Array; +} NSkinArray; -typedef struct ConnectedGamepadIds { - u64Array ids; -} ConnectedGamepadIds; +typedef struct NTransform { + NVector3 position; + NQuaternion rotation; + NVector3 scale; +} NTransform; -typedef struct NAttenuation { - float constant; - float linear; - float quadratic; -} NAttenuation; +typedef struct NColour { + uint8_t r; + uint8_t g; + uint8_t b; + uint8_t a; +} NColour; typedef struct NRange { float start; float end; } NRange; +typedef void* SceneLoaderPtr; + typedef struct IndexNative { uint32_t index; uint32_t generation; @@ -226,19 +237,96 @@ typedef struct NCollider { uint32_t id; } NCollider; +typedef struct NShapeCastHit { + NCollider collider; + double distance; + NVector3 witness1; + NVector3 witness2; + NVector3 normal1; + NVector3 normal2; + NShapeCastStatus status; +} NShapeCastHit; + +typedef struct u64Array { + uint64_t* values; + size_t length; + size_t capacity; +} u64Array; + +typedef void* CommandBufferPtr; + +typedef struct Progress { + size_t current; + size_t total; + const char* message; +} Progress; + typedef struct NColliderArray { NCollider* values; size_t length; size_t capacity; } NColliderArray; -typedef struct NTransform { +typedef struct NVector4 { + double x; + double y; + double z; + double w; +} NVector4; + +typedef struct NVector2 { + double x; + double y; +} NVector2; + +typedef struct NModelVertex { NVector3 position; + NVector3 normal; + NVector4 tangent; + NVector2 tex_coords0; + NVector2 tex_coords1; + NVector4 colour0; + i32Array joints0; + NVector4 weights0; +} NModelVertex; + +typedef struct NModelVertexArray { + NModelVertex* values; + size_t length; + size_t capacity; +} NModelVertexArray; + +typedef struct NMesh { + const char* name; + int32_t num_elements; + int32_t material_index; + NModelVertexArray vertices; +} NMesh; + +typedef struct NMeshArray { + NMesh* values; + size_t length; + size_t capacity; +} NMeshArray; + +typedef struct NNodeTransform { + NVector3 translation; NQuaternion rotation; NVector3 scale; -} NTransform; +} NNodeTransform; -typedef void* InputStatePtr; +typedef struct NNode { + const char* name; + const int32_t* parent; + i32Array children; + NNodeTransform transform; +} NNode; + +typedef struct NNodeArray { + NNode* values; + size_t length; + size_t capacity; +} NNodeArray; typedef struct IndexNativeArray { IndexNative* values; @@ -251,30 +339,15 @@ typedef struct CharacterCollisionArray { IndexNativeArray collisions; } CharacterCollisionArray; -typedef struct NShapeCastHit { - NCollider collider; - double distance; - NVector3 witness1; - NVector3 witness2; - NVector3 normal1; - NVector3 normal2; - NShapeCastStatus status; -} NShapeCastHit; +typedef void* InputStatePtr; -typedef struct AxisLock { - bool x; - bool y; - bool z; -} AxisLock; +typedef void* PhysicsStatePtr; -typedef void* CommandBufferPtr; +typedef struct ConnectedGamepadIds { + u64Array ids; +} ConnectedGamepadIds; -typedef struct NVector4 { - double x; - double y; - double z; - double w; -} NVector4; +typedef void* AssetRegistryPtr; typedef struct NMaterial { const char* name; @@ -300,30 +373,16 @@ typedef struct NMaterialArray { size_t capacity; } NMaterialArray; -typedef struct i32Array { - int32_t* values; - size_t length; - size_t capacity; -} i32Array; - -typedef struct f64ArrayArray { - double* values; - size_t length; - size_t capacity; -} f64ArrayArray; - -typedef struct NSkin { - const char* name; - i32Array joints; - f64ArrayArray inverse_bind_matrices; - const int32_t* skeleton_root; -} NSkin; +typedef struct NAttenuation { + float constant; + float linear; + float quadratic; +} NAttenuation; -typedef struct NSkinArray { - NSkin* values; - size_t length; - size_t capacity; -} NSkinArray; +typedef struct RayHit { + NCollider collider; + double distance; +} RayHit; typedef struct f64Array { double* values; @@ -356,78 +415,19 @@ typedef struct NAnimationArray { size_t capacity; } NAnimationArray; -typedef struct NColour { - uint8_t r; - uint8_t g; - uint8_t b; - uint8_t a; -} NColour; - -typedef void* PhysicsStatePtr; - -typedef struct NNodeTransform { - NVector3 translation; - NQuaternion rotation; - NVector3 scale; -} NNodeTransform; - -typedef struct NNode { - const char* name; - const int32_t* parent; - i32Array children; - NNodeTransform transform; -} NNode; - -typedef struct NNodeArray { - NNode* values; - size_t length; - size_t capacity; -} NNodeArray; - -typedef void* GraphicsContextPtr; - -typedef struct NModelVertex { - NVector3 position; - NVector3 normal; - NVector4 tangent; - NVector2 tex_coords0; - NVector2 tex_coords1; - NVector4 colour0; - i32Array joints0; - NVector4 weights0; -} NModelVertex; - -typedef struct NModelVertexArray { - NModelVertex* values; - size_t length; - size_t capacity; -} NModelVertexArray; - -typedef struct NMesh { - const char* name; - int32_t num_elements; - int32_t material_index; - NModelVertexArray vertices; -} NMesh; - -typedef struct NMeshArray { - NMesh* values; - size_t length; - size_t capacity; -} NMeshArray; - -typedef void* WorldPtr; - -typedef struct RayHit { - NCollider collider; - double distance; -} RayHit; +typedef struct AxisLock { + bool x; + bool y; + bool z; +} AxisLock; typedef struct RigidBodyContext { IndexNative index; uint64_t entity_id; } RigidBodyContext; +typedef void* GraphicsContextPtr; + int32_t dropbear_gamepad_is_button_pressed(InputStatePtr input, uint64_t gamepad_id, int32_t button_ordinal, bool* out0); int32_t dropbear_gamepad_get_left_stick_position(InputStatePtr input, uint64_t gamepad_id, NVector2* out0); int32_t dropbear_gamepad_get_right_stick_position(InputStatePtr input, uint64_t gamepad_id, NVector2* out0); @@ -571,6 +571,12 @@ int32_t dropbear_properties_set_double_property(WorldPtr world, uint64_t entity, int32_t dropbear_properties_set_float_property(WorldPtr world, uint64_t entity, const char* key, double value); int32_t dropbear_properties_set_bool_property(WorldPtr world, uint64_t entity, const char* key, bool value); int32_t dropbear_properties_set_vec3_property(WorldPtr world, uint64_t entity, const char* key, const NVector3* value); +int32_t dropbear_transform_exists_for_entity(WorldPtr world, uint64_t entity, bool* out0); +int32_t dropbear_transform_get_local_transform(WorldPtr world, uint64_t entity, NTransform* out0); +int32_t dropbear_transform_set_local_transform(WorldPtr world, uint64_t entity, const NTransform* transform); +int32_t dropbear_transform_get_world_transform(WorldPtr world, uint64_t entity, NTransform* out0); +int32_t dropbear_transform_set_world_transform(WorldPtr world, uint64_t entity, const NTransform* transform); +int32_t dropbear_transform_propogate_transform(WorldPtr world, uint64_t entity, NTransform* out0); int32_t dropbear_engine_get_entity(WorldPtr world, const char* label, uint64_t* out0); int32_t dropbear_engine_quit(CommandBufferPtr command_buffer); int32_t dropbear_engine_get_asset(AssetRegistryPtr asset, const char* label, const AssetKind* kind, uint64_t* out0, bool* out0_present);