tirbofish/dropbear · commit
a52fdb77c0dd10eb5c54caf481e61d764454dce6
Merge remote-tracking branch 'origin/main'
# Conflicts:
# eucalyptus-core/src/scripting/jni/exports.rs
# src/commonMain/kotlin/com/dropbear/EntityRef.kt
# src/commonMain/kotlin/com/dropbear/ffi/NativeEngine.kt
# src/jvmMain/java/com/dropbear/ffi/JNINative.java
# src/jvmMain/kotlin/com/dropbear/ffi/NativeEngine.jvm.kt
# src/nativeMain/kotlin/com/dropbear/ffi/NativeEngine.native.kt
Signature present but could not be verified.
Unverified
@@ -6,7 +6,7 @@ package.repository = "https://github.com/4tkbytes/dropbear" package.readme = "README.md" resolver = "3" -members = [ "dropbear-engine", "dropbear-shader", "dropbear_future-queue", "eucalyptus-core", "eucalyptus-editor", "magna-carta", "redback-runtime"] +members = [ "dropbear-engine", "dropbear-macro", "dropbear-shader", "dropbear-traits", "dropbear_future-queue", "eucalyptus-core", "eucalyptus-editor", "magna-carta", "redback-runtime"] [workspace.dependencies] anyhow = { version = "1.0", features = ["backtrace"] } @@ -64,6 +64,9 @@ dashmap = "6.1" open = "5.3" egui_plot = "0.34" memory-stats = "1.2" +typetag = "0.2" +syn = { version = "2.0", features = ["full"] } +quote = "1.0" [workspace.dependencies.image] version = "0.25" @@ -79,4 +82,4 @@ lto = false # makes the debug builds so much more faster [profile.dev.package."*"] -opt-level = 3 +opt-level = 3 @@ -11,6 +11,8 @@ readme.workspace = true [dependencies] dropbear_future-queue = { path = "../dropbear_future-queue" } dropbear-shader = { path = "../dropbear-shader" } +dropbear-traits = { path = "../dropbear-traits" } +dropbear-macro = { path = "../dropbear-macro" } anyhow.workspace = true app_dirs2.workspace = true @@ -33,7 +35,6 @@ wgpu.workspace = true winit.workspace = true hecs.workspace = true parking_lot.workspace = true -lazy_static.workspace = true gltf.workspace = true rayon.workspace = true backtrace.workspace = true @@ -41,6 +42,7 @@ os_info.workspace = true rustc_version_runtime.workspace = true ron.workspace = true dashmap.workspace = true +typetag.workspace = true [target.'cfg(not(target_os = "android"))'.dependencies] rfd.workspace = true @@ -1,3 +1,4 @@ +use dropbear_traits::SerializableComponent; use glam::{DMat4, DQuat, DVec3, Mat4}; use parking_lot::Mutex; use serde::{Deserialize, Serialize}; @@ -14,6 +15,54 @@ use crate::{ utils::ResourceReference, }; use anyhow::anyhow; +use dropbear_macro::SerializableComponent; + +/// 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)] +pub struct EntityTransform { + local: Transform, + world: Transform, +} + +impl EntityTransform { + pub fn new(local: Transform, world: Transform) -> Self { + Self { local, world } + } + + /// Gets a reference to the local transform + pub fn local(&self) -> &Transform { + &self.local + } + + /// Gets a reference to the world transform + pub fn world(&self) -> &Transform { + &self.world + } + + /// Gets a mutable reference to the local transform + pub fn local_mut(&mut self) -> &mut Transform { + &mut self.local + } + + /// Gets a mutable reference to the world transform + pub fn world_mut(&mut self) -> &mut Transform { + &mut self.world + } + + /// Combines both transforms into one, propagating the local transform + /// to the world transform and returning a uniform [Transform] + pub fn sync(&self) -> Transform { + let scaled_pos = self.local.position * self.world.scale; + let rotated_pos = self.world.rotation * scaled_pos; + let position = self.world.position + rotated_pos; + + Transform { + position, + rotation: self.world.rotation * self.local.rotation, + scale: self.world.scale * self.local.scale, + } + } +} /// A type that represents a position, rotation and scale of an entity /// @@ -32,9 +81,9 @@ pub struct Transform { impl Default for Transform { fn default() -> Self { Self { - position: DVec3::new(0.0, 0.0, 0.0), + position: DVec3::ZERO, rotation: DQuat::IDENTITY, - scale: DVec3::new(1.0, 1.0, 1.0), + scale: DVec3::ONE, } } } @@ -1,11 +1,13 @@ +use dropbear_traits::SerializableComponent; use glam::{DMat4, DQuat, DVec3}; use std::fmt::{Display, Formatter}; use std::sync::Arc; +use serde::{Deserialize, Serialize}; use wgpu::{ BindGroup, BindGroupLayout, Buffer, BufferAddress, CompareFunction, DepthBiasState, RenderPipeline, StencilState, VertexBufferLayout, util::DeviceExt, }; - +use dropbear_macro::SerializableComponent; use crate::attenuation::{Attenuation, RANGE_50}; use crate::graphics::SharedGraphicsContext; use crate::shader::Shader; @@ -118,7 +120,7 @@ impl From<LightType> for u32 { } } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, SerializableComponent)] pub struct LightComponent { pub position: DVec3, // point, spot pub direction: DVec3, // directional, spot @@ -100,6 +100,9 @@ pub enum ResourceReferenceType { /// In specifics, the plane (as from [`crate::procedural::plane::PlaneBuilder`]) is a model that /// has meshes and a textured material, but is created "in house" (during runtime instead of loaded). Plane, + + /// A cube. + Cube, } impl Default for ResourceReferenceType { @@ -0,0 +1,14 @@ +[package] +name = "dropbear-macro" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +readme = "README.md" + +[lib] +proc-macro = true + +[dependencies] +syn.workspace = true +quote.workspace = true @@ -0,0 +1,3 @@ +# dropbear-macro + +Contains the dropbear macros that would be used by other crates, such as derive macros. @@ -0,0 +1,48 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, DeriveInput}; + +/// 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) +} @@ -0,0 +1,10 @@ +[package] +name = "dropbear-traits" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +readme = "README.md" + +[dependencies] +typetag.workspace = true @@ -0,0 +1,3 @@ +# dropbear-traits + +Contains all the traits that other crates would use. @@ -0,0 +1,21 @@ +use std::any::Any; +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>; +} + +impl Clone for Box<dyn SerializableComponent> { + fn clone(&self) -> Self { + self.clone_boxed() + } +} @@ -10,6 +10,9 @@ readme = "README.md" crate-type = ["rlib", "dylib"] [dependencies] +dropbear-traits = { path = "../dropbear-traits" } +dropbear-macro = { path = "../dropbear-macro" } + anyhow.workspace = true bincode.workspace = true chrono.workspace = true @@ -34,6 +37,7 @@ crossbeam-channel.workspace = true app_dirs2.workspace = true log-once.workspace = true rfd = { workspace = true, optional = true } +typetag.workspace = true [features] # editor only stuff @@ -1,8 +1,11 @@ -use dropbear_engine::camera::{Camera, CameraSettings}; +use crate::traits::SerializableComponent; +use dropbear_engine::camera::{Camera, CameraBuilder, CameraSettings}; use glam::DVec3; use serde::{Deserialize, Serialize}; +use dropbear_macro::SerializableComponent; +use crate::states::CameraConfig; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize, SerializableComponent)] pub struct CameraComponent { pub settings: CameraSettings, pub camera_type: CameraType, @@ -27,9 +30,35 @@ impl CameraComponent { pub fn update(&mut self, camera: &mut Camera) { camera.settings = self.settings; } +} + +impl From<CameraConfig> for CameraBuilder { + fn from(value: CameraConfig) -> Self { + Self { + eye: value.eye.into(), + target: value.target.into(), + up: value.up.into(), + aspect: value.aspect, + znear: value.near as f64, + zfar: value.far as f64, + settings: CameraSettings { + speed: value.speed as f64, + sensitivity: value.sensitivity as f64, + fov_y: value.fov as f64, + }, + } + } +} - // setting camera offset is just adding the CameraFollowTarget struct - // to the ecs system +impl From<CameraConfig> for CameraComponent { + fn from(value: CameraConfig) -> Self { + let settings = CameraSettings::new(value.speed as f64, value.sensitivity as f64, value.fov as f64); + Self { + settings, + camera_type: value.camera_type, + starting_camera: value.starting_camera, + } + } } pub struct PlayerCamera; @@ -0,0 +1,93 @@ +/// Get a component and execute a closure if it exists +/// +/// # Usage +/// ``` +/// use eucalyptus_core::with_component; +/// +/// with_component!(scene_entity, Transform, /*mut*/ |transform| { +/// transform.position.x += 1.0; +/// }); +#[macro_export] +macro_rules! with_component { + // immutable + ($entity:expr, $comp_type:ty, $closure:expr) => { + if let Some(comp) = $entity.get_component::<$comp_type>() { + $closure(comp) + } + }; + + // mutable + ($entity:expr, $comp_type:ty, mut $closure:expr) => { + if let Some(comp) = $entity.get_component_mut::<$comp_type>() { + $closure(comp) + } + }; +} + +/// Get a component or return early from the function +/// +/// # Usage +/// ``` +/// use dropbear_engine::entity::MeshRenderer; +/// use eucalyptus_core::get_component; +/// +/// fn process_entity(entity: &SceneEntity) { +/// let renderer = get_component!(entity, MeshRenderer); +/// println!("Processing mesh: {:?}", renderer.handle); +/// } +#[macro_export] +macro_rules! get_component { + ($entity:expr, $comp_type:ty) => { + match $entity.get_component::<$comp_type>() { + Some(comp) => comp, + None => return, + } + }; + + ($entity:expr, $comp_type:ty, mut) => { + match $entity.get_component_mut::<$comp_type>() { + Some(comp) => comp, + None => return, + } + }; +} + +/// Try to get a component, or execute else block +/// +/// # Usage +/// ``` +/// if_component!(scene_entity, Transform, |/*mut*/ transform| { +/// transform.position = Vec3::new(1.0, 2.0, 3.0); +/// } else { +/// println!("No transform found"); +/// }); +#[macro_export] +macro_rules! if_component { + ($entity:expr, $comp_type:ty, |$comp:ident| $then:block else $else:block) => { + if let Some($comp) = $entity.get_component::<$comp_type>() { + $then + } else { + $else + } + }; + + ($entity:expr, $comp_type:ty, |$comp:ident| $then:block) => { + if let Some($comp) = $entity.get_component::<$comp_type>() { + $then + } + }; + + ($entity:expr, $comp_type:ty, |mut $comp:ident| $then:block else $else:block) => { + if let Some(mut $comp) = $entity.get_component_mut::<$comp_type>() { + $then + } else { + $else + } + }; + + ($entity:expr, $comp_type:ty, |mut $comp:ident| $then:block) => { + if let Some(mut $comp) = $entity.get_component_mut::<$comp_type>() { + $then + } + }; +} @@ -1,3 +1,8 @@ +use std::collections::HashMap; +use serde::{Deserialize, Serialize}; +use dropbear_engine::entity::{EntityTransform, Transform}; +use crate::states::Label; + /// A component that is added to all entities to show all child entities #[derive(Default, Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct Parent(Vec<hecs::Entity>); @@ -33,3 +38,132 @@ impl Parent { self.0.is_empty() } } + +/// A component that points to the parent entity of an entity. +#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, PartialEq, Eq)] +pub struct Child(hecs::Entity); + +impl Child { + /// Creates a new child component with the provided parent entity. + pub fn new(parent: hecs::Entity) -> Self { + Self(parent) + } + + /// Returns the parent entity of this child component. + pub fn parent(&self) -> hecs::Entity { + self.0 + } +} + +/// An extension trait for [EntityTransform] that allows for propagation of entities into a target transform. +pub trait EntityTransformExt { + /// Walks up the [`hecs::World`] and calculates the final [Transform] for the entity based off its parents. + fn propagate(&self, world: &hecs::World, target_entity: hecs::Entity) -> Transform; +} + +impl EntityTransformExt for EntityTransform { + fn propagate(&self, world: &hecs::World, target_entity: hecs::Entity) -> Transform { + let mut result = self.local().clone(); + + let mut current = target_entity; + while let Ok(child) = world.get::<&Child>(current) { + let parent_entity = child.parent(); + + if let Ok(parent_transform) = world.get::<&EntityTransform>(parent_entity) { + let parent_world = parent_transform.world(); + + result = Transform { + position: parent_world.position + parent_world.rotation * (result.position * parent_world.scale), + rotation: parent_world.rotation * result.rotation, + scale: parent_world.scale * result.scale, + }; + } + + current = parent_entity; + } + + result + } +} + +#[derive(Default, Serialize, Deserialize, Clone, Debug)] +pub struct SceneHierarchy { + /// Maps entity labels to their parent label + parent_map: HashMap<Label, Label>, + /// Maps entity labels to their children labels + children_map: HashMap<Label, Vec<Label>>, +} + +impl SceneHierarchy { + pub fn new() -> Self { + Self { + parent_map: HashMap::new(), + children_map: HashMap::new(), + } + } + + /// Set the parent of an entity + pub fn set_parent(&mut self, child: Label, parent: Label) { + if let Some(old_parent) = self.parent_map.get(&child) { + if let Some(children) = self.children_map.get_mut(old_parent) { + children.retain(|c| c != &child); + } + } + + self.parent_map.insert(child.clone(), parent.clone()); + + self.children_map + .entry(parent) + .or_insert_with(Vec::new) + .push(child); + } + + /// Remove parent relationship + pub fn remove_parent(&mut self, child: &Label) { + if let Some(parent) = self.parent_map.remove(child) { + if let Some(children) = self.children_map.get_mut(&parent) { + children.retain(|c| c != child); + } + } + } + + /// Get the parent of an entity + pub fn get_parent(&self, child: &Label) -> Option<&Label> { + self.parent_map.get(child) + } + + /// Get the children of an entity + pub fn get_children(&self, parent: &Label) -> &[Label] { + self.children_map + .get(parent) + .map(|v| v.as_slice()) + .unwrap_or(&[]) + } + + /// Get all ancestors of an entity (parent, grandparent, etc.) + pub fn get_ancestors(&self, entity: &Label) -> Vec<Label> { + let mut ancestors = Vec::new(); + let mut current = entity.clone(); + + while let Some(parent) = self.parent_map.get(¤t) { + ancestors.push(parent.clone()); + current = parent.clone(); + } + + ancestors + } + + /// Check if an entity is a descendant of another + pub fn is_descendant_of(&self, entity: &Label, potential_ancestor: &Label) -> bool { + let mut current = entity.clone(); + + while let Some(parent) = self.parent_map.get(¤t) { + if parent == potential_ancestor { + return true; + } + current = parent.clone(); + } + + false + } +} @@ -10,6 +10,11 @@ pub mod spawn; pub mod states; pub mod utils; pub mod window; +pub mod scene; +mod component; + +pub use dropbear_traits as traits; +pub use dropbear_macro as macros; pub use egui; @@ -1,4 +1,4 @@ -use crate::states::SceneConfig; +use crate::scene::SceneConfig; use std::collections::HashMap; // #[derive(bincode::Decode, bincode::Encode, serde::Serialize, serde::Deserialize, Debug)] // pub struct RuntimeData { @@ -0,0 +1,497 @@ +use std::collections::HashMap; +use std::fs; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use serde::{Deserialize, Serialize}; +use ron::ser::PrettyConfig; +use dropbear_engine::graphics::SharedGraphicsContext; +use tokio::sync::mpsc::UnboundedSender; +use dropbear_engine::camera::{Camera, CameraBuilder}; +use dropbear_engine::entity::{EntityTransform, MeshRenderer, Transform}; +use glam::{DQuat, DVec3}; +use dropbear_engine::lighting::{Light, LightComponent}; +use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator}; +use dropbear_engine::asset::ASSET_REGISTRY; +use dropbear_engine::model::Model; +use dropbear_engine::utils::ResourceReferenceType; +use crate::camera::{CameraComponent, CameraType}; +use crate::hierarchy::{Parent, SceneHierarchy}; +use crate::states::{CameraConfig, Label, LightConfig, ModelProperties, ScriptComponent, SerializedMeshRenderer, WorldLoadingStatus, PROJECT}; +use crate::utils::ResolveReference; + +#[derive(Default, Debug, Serialize, Deserialize, Clone)] +pub struct SceneEntity { + #[serde(default)] + pub label: Label, + #[serde(default)] + pub components: Vec<Box<dyn dropbear_traits::SerializableComponent>>, + + #[serde(skip)] + pub entity_id: Option<hecs::Entity>, +} + +#[derive(Default, Debug, Serialize, Deserialize, Clone)] +pub struct SceneSettings { /* *crickets* */ } + +impl SceneSettings { + pub fn new() -> Self { + Self {} + } +} + +#[derive(Default, Debug, Serialize, Deserialize, Clone)] +pub struct SceneConfig { + #[serde(default)] + pub scene_name: String, + + #[serde(default)] + pub entities: Vec<SceneEntity>, + + #[serde(default)] + pub hierarchy_map: SceneHierarchy, + + #[serde(default)] + pub settings: SceneSettings, + + #[serde(skip)] + pub path: PathBuf, +} + +impl SceneConfig { + /// Creates a new instance of the scene config + pub fn new(scene_name: String, path: impl AsRef<Path>) -> Self { + Self { + scene_name, + path: path.as_ref().to_path_buf(), + entities: Vec::new(), + hierarchy_map: SceneHierarchy::new(), + settings: SceneSettings::new(), + } + } + + /// Helper function to load a component and add it to the entity builder + async fn load_component( + component: Box<dyn dropbear_traits::SerializableComponent>, + builder: &mut hecs::EntityBuilder, + graphics: Arc<SharedGraphicsContext>, + 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 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::Plane => { + log::error!("Resource reference type is Plane for entity '{}', not supported (being remade), skipping", label); + return Ok(()); + } + ResourceReferenceType::File(reference) => { + let path = &renderer.handle.resolve()?; + + log::debug!( + "Path for entity {} is {} from reference {}", + label, + path.display(), + reference + ); + + MeshRenderer::from_path(graphics.clone(), &path, Some(label)).await? + } + ResourceReferenceType::Bytes(bytes) => { + log::info!("Loading entity from bytes [Len: {}]", bytes.len()); + + let model = Model::load_from_memory( + graphics.clone(), + bytes.clone(), + Some(label), + ).await?; + MeshRenderer::from_handle(model) + } + ResourceReferenceType::Cube => { + log::info!("Loading entity from cube"); + + let model = Model::load_from_memory( + graphics.clone(), + include_bytes!("../../resources/models/cube.glb"), + Some(label), + ).await?; + MeshRenderer::from_handle(model) + } + }; + + 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).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 + ); + } + } + } + + builder.add(model); + } else if let Some(props) = component.as_any().downcast_ref::<ModelProperties>() { + builder.add(props.clone()); + } else if let Some(camera_comp) = component.as_any().downcast_ref::<CameraConfig>() { + 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::<LightConfig>() { + let light = Light::new( + graphics.clone(), + light_conf.light_component.clone(), + light_conf.transform, + Some(label) + ).await; + builder.add_bundle((light_conf.light_component.clone(), light)); + } else if let Some(script) = component.as_any().downcast_ref::<ScriptComponent>() { + builder.add(script.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 { + log::warn!( + "Unknown component type '{}' for entity '{}' - skipping", + component.type_name(), + label + ); + } + + Ok(()) + } + + /// Write the scene config to a .eucs file + pub fn write_to(&self, project_path: impl AsRef<Path>) -> anyhow::Result<()> { + let ron_str = ron::ser::to_string_pretty(&self, PrettyConfig::default()) + .map_err(|e| anyhow::anyhow!("RON serialization error: {}", e))?; + + let scenes_dir = project_path.as_ref().join("scenes"); + fs::create_dir_all(&scenes_dir)?; + + let config_path = scenes_dir.join(format!("{}.eucs", self.scene_name)); + fs::write(&config_path, ron_str).map_err(|e| anyhow::anyhow!(e.to_string()))?; + Ok(()) + } + + /// Read a scene config from a .eucs file + pub fn read_from(scene_path: impl AsRef<Path>) -> anyhow::Result<Self> { + let ron_str = fs::read_to_string(scene_path.as_ref())?; + let mut config: SceneConfig = ron::de::from_str(&ron_str) + .map_err(|e| anyhow::anyhow!("RON deserialization error: {}", e))?; + + config.path = scene_path.as_ref().to_path_buf(); + Ok(config) + } + + pub async fn load_into_world( + &self, + world: &mut hecs::World, + graphics: Arc<SharedGraphicsContext>, + progress_sender: Option<UnboundedSender<WorldLoadingStatus>>, + ) -> anyhow::Result<hecs::Entity> { + if let Some(ref s) = progress_sender { + let _ = s.send(WorldLoadingStatus::Idle); + } + + log::info!( + "Loading scene [{}], clearing world with {} entities", + self.scene_name, + world.len() + ); + world.clear(); + + #[allow(unused_variables)] + let project_config = if cfg!(feature = "editor") { + let cfg = PROJECT.read(); + cfg.project_path.clone() + } else { + log::debug!("Not using the editor feature, returning empty pathbuffer"); + PathBuf::new() + }; + + log::info!("World cleared, now has {} entities", world.len()); + + let entity_configs: Vec<(usize, SceneEntity)> = { + let cloned = self.entities.clone(); + cloned + .into_par_iter() + .enumerate() + .map(|(i, e)| (i, e)) + .collect() + }; + + let mut label_to_entity: HashMap<Label, hecs::Entity> = HashMap::new(); + + for (index, entity_config) in entity_configs { + let SceneEntity { + label, components, entity_id: _, + } = entity_config; + + let label_for_map = label.clone(); + let label_for_logs = label_for_map.to_string(); + + log::debug!("Loading entity: {}", label_for_logs); + + let total = self.entities.len(); + + if let Some(ref s) = progress_sender { + let _ = s.send(WorldLoadingStatus::LoadingEntity { + index, + name: label_for_logs.clone(), + total, + }); + } + + let mut builder = hecs::EntityBuilder::new(); + + builder.add(label_for_map.clone()); + + let mut has_entity_transform = false; + + for component in components { + if component.as_any().downcast_ref::<EntityTransform>().is_some() { + has_entity_transform = true; + } + + Self::load_component(component, &mut builder, graphics.clone(), &label_for_logs).await?; + } + + let entity = world.spawn(builder.build()); + + if has_entity_transform { + if let Ok(mut query) = world.query_one::<(&EntityTransform, Option<&mut MeshRenderer>, Option<&mut Light>, Option<&mut LightComponent>)>(entity) { + if let Some((entity_transform, renderer_opt, light_opt, light_comp_opt)) = query.get() { + let transform = entity_transform.sync(); + + if let Some(renderer) = renderer_opt { + renderer.update(&transform); + log::debug!("Updated renderer transform for '{}'", label_for_logs); + } + + if let (Some(light), Some(light_comp)) = (light_opt, light_comp_opt) { + light.update(light_comp, &transform); + log::debug!("Updated light transform for '{}'", label_for_logs); + } + } + } + } + + if let Some(previous) = label_to_entity.insert(label_for_map.clone(), entity) { + log::warn!( + "Duplicate entity label '{}' detected; previous entity {:?} will be overwritten in hierarchy mapping", + label_for_logs, + previous + ); + } + + log::debug!("Loaded entity '{}'", label_for_logs); + } + + let mut parent_children_map: HashMap<Label, Vec<Label>> = HashMap::new(); + + for entity_label in label_to_entity.keys() { + let children: Vec<Label> = self.hierarchy_map.get_children(entity_label).to_vec(); + if !children.is_empty() { + parent_children_map.insert(entity_label.clone(), children); + } + } + + for (parent_label, child_labels) in parent_children_map { + let Some(&parent_entity) = label_to_entity.get(&parent_label) else { + log::warn!( + "Unable to resolve parent entity '{}' while rebuilding hierarchy", + parent_label + ); + continue; + }; + + let mut resolved_children = Vec::new(); + for child_label in child_labels { + if let Some(&child_entity) = label_to_entity.get(&child_label) { + resolved_children.push(child_entity); + } else { + log::warn!( + "Unable to resolve child '{}' for parent '{}'", + child_label, + parent_label + ); + } + } + + if resolved_children.is_empty() { + continue; + } + + let mut local_insert_one: Option<hecs::Entity> = None; + + match world.query_one::<&mut Parent>(parent_entity) { + Ok(mut parent_query) => { + if let Some(parent_component) = parent_query.get() { + parent_component.clear(); + parent_component + .children_mut() + .extend(resolved_children.iter().copied()); + } else { + local_insert_one = Some(parent_entity); + } + } + Err(e) => { + log::warn!( + "Failed to query Parent component for entity {:?}: {}", + parent_entity, + e + ); + local_insert_one = Some(parent_entity); + } + } + + if let Some(parent_entity) = local_insert_one + && let Err(e) = world.insert_one(parent_entity, Parent::new(resolved_children)) + { + log::error!( + "Failed to attach Parent component to entity {:?}: {}", + parent_entity, + e + ); + } + } + + { + let mut has_light = false; + if world + .query::<(&LightComponent, &Light)>() + .iter() + .next() + .is_some() + { + has_light = true; + } + + if !has_light { + log::info!("No lights in scene, spawning default light"); + if let Some(ref s) = progress_sender { + let _ = s.send(WorldLoadingStatus::LoadingEntity { + index: 0, + name: String::from("Default Light"), + total: 1, + }); + } + let comp = LightComponent::directional(glam::DVec3::ONE, 1.0); + let light_direction = LightComponent::default_direction(); + let rotation = + DQuat::from_rotation_arc(DVec3::new(0.0, 0.0, -1.0), light_direction); + let trans = Transform { + position: glam::DVec3::new(2.0, 4.0, 2.0), + rotation, + ..Default::default() + }; + let entity_trans = EntityTransform::new(trans, Transform::default()); + let light = + Light::new(graphics.clone(), comp.clone(), trans, Some("Default Light")).await; + + { + world.spawn(( + Label::from("Default Light"), + comp, + entity_trans, + light, + ModelProperties::default(), + )); + } + } + } + + log::info!( + "Loaded {} entities from scene", + self.entities.len() + ); + #[cfg(feature = "editor")] + { + let debug_camera = { + world + .query::<(&Camera, &CameraComponent)>() + .iter() + .find_map(|(entity, (_, component))| { + if matches!(component.camera_type, CameraType::Debug) { + Some(entity) + } else { + None + } + }) + }; + + { + if let Some(camera_entity) = debug_camera { + log::info!("Using existing debug camera for editor"); + Ok(camera_entity) + } else { + log::info!("No debug camera found, creating viewport camera for editor"); + if let Some(ref s) = progress_sender { + let _ = s.send(WorldLoadingStatus::LoadingEntity { + index: 0, + name: String::from("Viewport Camera"), + total: 1, + }); + } + let camera = Camera::predetermined(graphics.clone(), Some("Viewport Camera")); + let component = crate::camera::DebugCamera::new(); + let camera_entity = { world.spawn((camera, component)) }; + Ok(camera_entity) + } + } + } + + #[cfg(not(feature = "editor"))] + { + let player_camera = world + .query::<(&Camera, &CameraComponent)>() + .iter() + .find_map(|(entity, (_, component))| { + if matches!(component.camera_type, CameraType::Player) { + Some(entity) + } else { + None + } + }); + + if let Some(camera_entity) = player_camera { + log::info!("Using player camera for runtime"); + Ok(camera_entity) + } else { + panic!("Runtime mode requires a player camera, but none was found in the scene!"); + } + } + } +} @@ -14,7 +14,7 @@ use crate::{convert_jlong_to_entity, convert_jstring, convert_ptr}; use dropbear_engine::asset::PointerKind::Const; use dropbear_engine::asset::{ASSET_REGISTRY, AssetHandle, AssetRegistry}; use dropbear_engine::camera::Camera; -use dropbear_engine::entity::{MeshRenderer, Transform}; +use dropbear_engine::entity::{EntityTransform, MeshRenderer, Transform}; use dropbear_engine::model::Model; use dropbear_engine::utils::ResourceReference; use glam::{DQuat, DVec3}; @@ -27,6 +27,8 @@ use jni::sys::{ use parking_lot::Mutex; use std::collections::{HashMap, HashSet}; use std::sync::Arc; +use dropbear_engine::lighting::Light; +use crate::hierarchy::EntityTransformExt; /// `JNIEXPORT jlong JNICALL Java_com_dropbear_ffi_JNINative_getEntity /// (JNIEnv *, jclass, jlong, jstring);` @@ -75,10 +77,10 @@ pub fn Java_com_dropbear_ffi_JNINative_getEntity( 0 } -/// `JNIEXPORT jobject JNICALL Java_com_dropbear_ffi_JNINative_getWorldTransform +/// `JNIEXPORT jobject JNICALL Java_com_dropbear_ffi_JNINative_getTransform /// (JNIEnv *, jclass, jlong, jlong);` #[unsafe(no_mangle)] -pub fn Java_com_dropbear_ffi_JNINative_getWorldTransform( +pub fn Java_com_dropbear_ffi_JNINative_getTransform( mut env: JNIEnv, _class: jclass, world_handle: jlong, @@ -87,9 +89,7 @@ pub fn Java_com_dropbear_ffi_JNINative_getWorldTransform( let world = world_handle as *mut World; if world.is_null() { - println!( - "[Java_com_dropbear_ffi_JNINative_getWorldTransform] [ERROR] World pointer is null" - ); + println!("[Java_com_dropbear_ffi_JNINative_getTransform] [ERROR] World pointer is null"); return JObject::null(); } @@ -97,327 +97,249 @@ pub fn Java_com_dropbear_ffi_JNINative_getWorldTransform( let entity = convert_jlong_to_entity!(entity_id); - if let Ok(mut q) = world.query_one::<(&WorldTransform, &LocalTransform)>(entity) - && let Some((wt, lt)) = q.get() + if let Ok(mut q) = world.query_one::<&EntityTransform>(entity) + && let Some(transform) = q.get() { - let new_transform = *transform; + let wt = *transform.world(); let transform_class = match env.find_class("com/dropbear/math/Transform") { Ok(c) => c, Err(_) => return JObject::null(), }; - - return match env.new_object( + + let world_transform_java = match env.new_object( &transform_class, "(DDDDDDDDDD)V", &[ - new_transform.position.x.into(), - new_transform.position.y.into(), - new_transform.position.z.into(), - new_transform.rotation.x.into(), - new_transform.rotation.y.into(), - new_transform.rotation.z.into(), - new_transform.rotation.w.into(), - new_transform.scale.x.into(), - new_transform.scale.y.into(), - new_transform.scale.z.into(), + wt.position.x.into(), + wt.position.y.into(), + wt.position.z.into(), + wt.rotation.x.into(), + wt.rotation.y.into(), + wt.rotation.z.into(), + wt.rotation.w.into(), + wt.scale.x.into(), + wt.scale.y.into(), + wt.scale.z.into(), ], ) { Ok(java_transform) => java_transform, Err(_) => { println!( - "[Java_com_dropbear_ffi_JNINative_getWorldTransform] [ERROR] Failed to create Transform object" + "[Java_com_dropbear_ffi_JNINative_getTransform] [ERROR] Failed to create world transform object" ); - JObject::null() + return JObject::null(); } }; - } - - println!( - "[Java_com_dropbear_ffi_JNINative_getWorldTransform] [ERROR] Failed to query for world transform value for entity: {}", - entity_id - ); - JObject::null() -} - -/// `JNIEXPORT jobject JNICALL Java_com_dropbear_ffi_JNINative_getLocalTransform -/// (JNIEnv *, jclass, jlong, jlong);` -#[unsafe(no_mangle)] -pub fn Java_com_dropbear_ffi_JNINative_getLocalTransform( - mut env: JNIEnv, - _class: jclass, - world_handle: jlong, - entity_id: jlong, -) -> JObject { - let world = world_handle as *mut World; - - if world.is_null() { - println!( - "[Java_com_dropbear_ffi_JNINative_getLocalTransform] [ERROR] World pointer is null" - ); - return JObject::null(); - } - - let world = unsafe { &mut *world }; - let entity = convert_jlong_to_entity!(entity_id); + let lt = *transform.local(); - if let Ok(mut q) = world.query_one::<(&WorldTransform, &LocalTransform)>(entity) - && let Some((wt, lt)) = q.get() - { - let new_transform = *transform; + let local_transform_java = match env.new_object( + &transform_class, + "(DDDDDDDDDD)V", + &[ + lt.position.x.into(), + lt.position.y.into(), + lt.position.z.into(), + lt.rotation.x.into(), + lt.rotation.y.into(), + lt.rotation.z.into(), + lt.rotation.w.into(), + lt.scale.x.into(), + lt.scale.y.into(), + lt.scale.z.into(), + ], + ) { + Ok(java_transform) => java_transform, + Err(_) => { + println!( + "[Java_com_dropbear_ffi_JNINative_getTransform] [ERROR] Failed to create local transform object" + ); + return JObject::null(); + } + }; - let transform_class = match env.find_class("com/dropbear/math/Transform") { + let entity_transform_class = match env.find_class("com/dropbear/EntityTransform") { Ok(c) => c, Err(_) => return JObject::null(), }; return match env.new_object( - &transform_class, - "(DDDDDDDDDD)V", + &entity_transform_class, + "(Lcom/dropbear/math/Transform;Lcom/dropbear/math/Transform)V", &[ - new_transform.position.x.into(), - new_transform.position.y.into(), - new_transform.position.z.into(), - new_transform.rotation.x.into(), - new_transform.rotation.y.into(), - new_transform.rotation.z.into(), - new_transform.rotation.w.into(), - new_transform.scale.x.into(), - new_transform.scale.y.into(), - new_transform.scale.z.into(), - ], + JValue::Object(&local_transform_java), + JValue::Object(&world_transform_java), + ] ) { Ok(java_transform) => java_transform, Err(_) => { println!( - "[Java_com_dropbear_ffi_JNINative_getLocalTransform] [ERROR] Failed to create Transform object" + "[Java_com_dropbear_ffi_JNINative_getTransform] [ERROR] Failed to create Transform object" ); JObject::null() } - }; + } } println!( - "[Java_com_dropbear_ffi_JNINative_getLocalTransform] [ERROR] Failed to query for local transform value for entity: {}", + "[Java_com_dropbear_ffi_JNINative_getTransform] [ERROR] Failed to query for transform value for entity: {}", entity_id ); JObject::null() } -/// `JNIEXPORT void JNICALL Java_com_dropbear_ffi_JNINative_commitWorldTransform +/// `JNIEXPORT void JNICALL Java_com_dropbear_ffi_JNINative_setTransform /// (JNIEnv *, jclass, jlong, jlong, jobject);` #[unsafe(no_mangle)] -pub fn Java_com_dropbear_ffi_JNINative_commitWorldTransform( +pub fn Java_com_dropbear_ffi_JNINative_setTransform( mut env: JNIEnv, _class: JClass, world_handle: jlong, entity_id: jlong, - transform_obj: JObject, + entity_transform_obj: JObject, ) { let world = world_handle as *mut World; if world.is_null() { - println!( - "[Java_com_dropbear_ffi_JNINative_commitWorldTransform] [ERROR] World pointer is null" - ); + println!("[Java_com_dropbear_ffi_JNINative_setTransform] [ERROR] World pointer is null"); return; } let world = unsafe { &mut *world }; let entity = convert_jlong_to_entity!(entity_id); - let get_number_field = |env: &mut JNIEnv, obj: &JObject, field_name: &str| -> f64 { - match env.get_field(obj, field_name, "Ljava/lang/Number;") { - Ok(v) => match v.l() { - Ok(num_obj) => { - match env.call_method(&num_obj, "doubleValue", "()D", &[]) { - Ok(result) => result.d().unwrap_or_else(|_| { - println!("[Java_com_dropbear_ffi_JNINative_commitWorldTransform] [ERROR] Failed to extract double from {}", field_name); - 0.0 - }), - Err(_) => { - println!("[Java_com_dropbear_ffi_JNINative_commitWorldTransform] [ERROR] Failed to call doubleValue on {}", field_name); - 0.0 + let extract_transform = |env: &mut JNIEnv, transform_obj: &JObject| -> Option<Transform> { + let get_number_field = |env: &mut JNIEnv, obj: &JObject, field_name: &str| -> f64 { + match env.get_field(obj, field_name, "Ljava/lang/Number;") { + Ok(v) => match v.l() { + Ok(num_obj) => { + match env.call_method(&num_obj, "doubleValue", "()D", &[]) { + Ok(result) => result.d().unwrap_or(0.0), + Err(_) => 0.0, } } - } - Err(_) => { - println!("[Java_com_dropbear_ffi_JNINative_commitWorldTransform] [ERROR] Failed to extract Number object for {}", field_name); - 0.0 - } - }, - Err(_) => { - println!("[Java_com_dropbear_ffi_JNINative_commitWorldTransform] [ERROR] Failed to get field {}", field_name); - 0.0 + Err(_) => 0.0, + }, + Err(_) => 0.0, } - } - }; - - let position_obj: JObject = - match env.get_field(&transform_obj, "position", "Lcom/dropbear/math/Vector3;") { - Ok(v) => v.l().unwrap_or_else(|_| JObject::null()), - Err(_) => JObject::null(), }; - let rotation_obj: JObject = - match env.get_field(&transform_obj, "rotation", "Lcom/dropbear/math/Quaternion;") { - Ok(v) => v.l().unwrap_or_else(|_| JObject::null()), - Err(_) => JObject::null(), - }; + let position_obj: JObject = + match env.get_field(transform_obj, "position", "Lcom/dropbear/math/Vector3;") { + Ok(v) => v.l().ok()?, + Err(_) => return None, + }; - let scale_obj: JObject = - match env.get_field(&transform_obj, "scale", "Lcom/dropbear/math/Vector3;") { - Ok(v) => v.l().unwrap_or_else(|_| JObject::null()), - Err(_) => JObject::null(), - }; + let rotation_obj: JObject = + match env.get_field(transform_obj, "rotation", "Lcom/dropbear/math/Quaternion;") { + Ok(v) => v.l().ok()?, + Err(_) => return None, + }; - if position_obj.is_null() || rotation_obj.is_null() || scale_obj.is_null() { - println!( - "[Java_com_dropbear_ffi_JNINative_commitWorldTransform] [ERROR] Failed to extract position/rotation/scale objects" - ); - return; - } + let scale_obj: JObject = + match env.get_field(transform_obj, "scale", "Lcom/dropbear/math/Vector3;") { + Ok(v) => v.l().ok()?, + Err(_) => return None, + }; + + let px = get_number_field(env, &position_obj, "x"); + let py = get_number_field(env, &position_obj, "y"); + let pz = get_number_field(env, &position_obj, "z"); + + let rx = get_number_field(env, &rotation_obj, "x"); + let ry = get_number_field(env, &rotation_obj, "y"); + let rz = get_number_field(env, &rotation_obj, "z"); + let rw = get_number_field(env, &rotation_obj, "w"); + + let sx = get_number_field(env, &scale_obj, "x"); + let sy = get_number_field(env, &scale_obj, "y"); + let sz = get_number_field(env, &scale_obj, "z"); + + Some(Transform { + position: DVec3::new(px, py, pz), + rotation: DQuat::from_xyzw(rx, ry, rz, rw), + scale: DVec3::new(sx, sy, sz), + }) + }; - let px = get_number_field(&mut env, &position_obj, "x"); - let py = get_number_field(&mut env, &position_obj, "y"); - let pz = get_number_field(&mut env, &position_obj, "z"); + let local_obj: JObject = match env.get_field(&entity_transform_obj, "local", "Lcom/dropbear/math/Transform;") { + Ok(v) => v.l().unwrap_or_else(|_| JObject::null()), + Err(_) => { + println!("[Java_com_dropbear_ffi_JNINative_setTransform] [ERROR] Failed to get local transform field"); + return; + } + }; + + let world_obj: JObject = match env.get_field(&entity_transform_obj, "world", "Lcom/dropbear/math/Transform;") { + Ok(v) => v.l().unwrap_or_else(|_| JObject::null()), + Err(_) => { + println!("[Java_com_dropbear_ffi_JNINative_setTransform] [ERROR] Failed to get world transform field"); + return; + } + }; - let rx = get_number_field(&mut env, &rotation_obj, "x"); - let ry = get_number_field(&mut env, &rotation_obj, "y"); - let rz = get_number_field(&mut env, &rotation_obj, "z"); - let rw = get_number_field(&mut env, &rotation_obj, "w"); + if local_obj.is_null() || world_obj.is_null() { + println!("[Java_com_dropbear_ffi_JNINative_setTransform] [ERROR] local or world transform is null"); + return; + } - let sx = get_number_field(&mut env, &scale_obj, "x"); - let sy = get_number_field(&mut env, &scale_obj, "y"); - let sz = get_number_field(&mut env, &scale_obj, "z"); + let local_transform = match extract_transform(&mut env, &local_obj) { + Some(t) => t, + None => { + println!("[Java_com_dropbear_ffi_JNINative_setTransform] [ERROR] Failed to extract local transform"); + return; + } + }; - let new_transform = WorldTransform::new(Transform { - position: DVec3::new(px, py, pz), - rotation: DQuat::from_xyzw(rx, ry, rz, rw), - scale: DVec3::new(sx, sy, sz), - }); + let world_transform = match extract_transform(&mut env, &world_obj) { + Some(t) => t, + None => { + println!("[Java_com_dropbear_ffi_JNINative_setTransform] [ERROR] Failed to extract world transform"); + return; + } + }; - if let Ok(mut q) = world.query_one::<&mut WorldTransform>(entity) { - if let Some(transform) = q.get() { - *transform = new_transform; + if let Ok(mut q) = world.query_one::<&mut EntityTransform>(entity) { + if let Some(entity_transform) = q.get() { + *entity_transform.local_mut() = local_transform; + *entity_transform.world_mut() = world_transform; } else { - println!( - "[Java_com_dropbear_ffi_JNINative_commitWorldTransform] [ERROR] Failed to query for world transform component" - ); + println!("[Java_com_dropbear_ffi_JNINative_setTransform] [ERROR] Failed to get entity transform"); } } else { - println!( - "[Java_com_dropbear_ffi_JNINative_commitWorldTransform] [ERROR] Failed to query entity for world transform component" - ); + println!("[Java_com_dropbear_ffi_JNINative_setTransform] [ERROR] Entity does not have EntityTransform component"); } } -/// `JNIEXPORT void JNICALL Java_com_dropbear_ffi_JNINative_commitLocalTransform -/// (JNIEnv *, jclass, jlong, jlong, jobject);` +/// `JNIEXPORT void JNICALL Java_com_dropbear_ffi_JNINative_propagateTransform +/// (JNIEnv *, jclass, jlong, jlong);` #[unsafe(no_mangle)] -pub fn Java_com_dropbear_ffi_JNINative_commitLocalTransform( - mut env: JNIEnv, +pub fn Java_com_dropbear_ffi_JNINative_propagateTransform( + _env: JNIEnv, _class: JClass, world_handle: jlong, entity_id: jlong, - transform_obj: JObject, ) { let world = world_handle as *mut World; if world.is_null() { - println!( - "[Java_com_dropbear_ffi_JNINative_commitLocalTransform] [ERROR] World pointer is null" - ); + println!("[Java_com_dropbear_ffi_JNINative_propagateTransform] [ERROR] World pointer is null"); return; } let world = unsafe { &mut *world }; let entity = convert_jlong_to_entity!(entity_id); - let get_number_field = |env: &mut JNIEnv, obj: &JObject, field_name: &str| -> f64 { - match env.get_field(obj, field_name, "Ljava/lang/Number;") { - Ok(v) => match v.l() { - Ok(num_obj) => { - match env.call_method(&num_obj, "doubleValue", "()D", &[]) { - Ok(result) => result.d().unwrap_or_else(|_| { - println!("[Java_com_dropbear_ffi_JNINative_commitLocalTransform] [ERROR] Failed to extract double from {}", field_name); - 0.0 - }), - Err(_) => { - println!("[Java_com_dropbear_ffi_JNINative_commitLocalTransform] [ERROR] Failed to call doubleValue on {}", field_name); - 0.0 - } - } - } - Err(_) => { - println!("[Java_com_dropbear_ffi_JNINative_commitLocalTransform] [ERROR] Failed to extract Number object for {}", field_name); - 0.0 - } - }, - Err(_) => { - println!("[Java_com_dropbear_ffi_JNINative_commitLocalTransform] [ERROR] Failed to get field {}", field_name); - 0.0 - } - } - }; - - let position_obj: JObject = - match env.get_field(&transform_obj, "position", "Lcom/dropbear/math/Vector3;") { - Ok(v) => v.l().unwrap_or_else(|_| JObject::null()), - Err(_) => JObject::null(), - }; - - let rotation_obj: JObject = - match env.get_field(&transform_obj, "rotation", "Lcom/dropbear/math/Quaternion;") { - Ok(v) => v.l().unwrap_or_else(|_| JObject::null()), - Err(_) => JObject::null(), - }; - - let scale_obj: JObject = - match env.get_field(&transform_obj, "scale", "Lcom/dropbear/math/Vector3;") { - Ok(v) => v.l().unwrap_or_else(|_| JObject::null()), - Err(_) => JObject::null(), - }; - - if position_obj.is_null() || rotation_obj.is_null() || scale_obj.is_null() { - println!( - "[Java_com_dropbear_ffi_JNINative_commitLocalTransform] [ERROR] Failed to extract position/rotation/scale objects" - ); - return; - } - - let px = get_number_field(&mut env, &position_obj, "x"); - let py = get_number_field(&mut env, &position_obj, "y"); - let pz = get_number_field(&mut env, &position_obj, "z"); - - let rx = get_number_field(&mut env, &rotation_obj, "x"); - let ry = get_number_field(&mut env, &rotation_obj, "y"); - let rz = get_number_field(&mut env, &rotation_obj, "z"); - let rw = get_number_field(&mut env, &rotation_obj, "w"); - - let sx = get_number_field(&mut env, &scale_obj, "x"); - let sy = get_number_field(&mut env, &scale_obj, "y"); - let sz = get_number_field(&mut env, &scale_obj, "z"); - - let new_transform = LocalTransform::new(Transform { - position: DVec3::new(px, py, pz), - rotation: DQuat::from_xyzw(rx, ry, rz, rw), - scale: DVec3::new(sx, sy, sz), - }); - - if let Ok(mut q) = world.query_one::<&mut LocalTransform>(entity) { - if let Some(transform) = q.get() { - *transform = new_transform; + if let Ok(mut q) = world.query_one::<&mut EntityTransform>(entity) { + if let Some(entity_transform) = q.get() { + let new_world = entity_transform.propagate(world, entity); + *entity_transform.world_mut() = new_world; } else { - println!( - "[Java_com_dropbear_ffi_JNINative_commitLocalTransform] [ERROR] Failed to query for local transform component" - ); + println!("[Java_com_dropbear_ffi_JNINative_propagateTransform] [ERROR] Failed to get entity transform"); } } else { - println!( - "[Java_com_dropbear_ffi_JNINative_commitLocalTransform] [ERROR] Failed to query entity for local transform component" - ); + println!("[Java_com_dropbear_ffi_JNINative_propagateTransform] [ERROR] Entity does not have EntityTransform component"); } } @@ -2303,8 +2225,7 @@ pub fn Java_com_dropbear_ffi_JNINative_setTexture( let Some(target_material) = resolved_target_name else { let message = format!( "[Java_com_dropbear_ffi_JNINative_setTexture] [ERROR] Unable to resolve material '{}' on model '{}'", - target_identifier, - renderer.model().label + target_identifier, renderer.model().label ); set_last_error_message(&message); println!("{}", message); @@ -1,5 +1,5 @@ use crate::states::ModelProperties; -use dropbear_engine::entity::Transform; +use dropbear_engine::entity::{EntityTransform}; use dropbear_engine::future::{FutureHandle, FutureQueue}; use dropbear_engine::graphics::SharedGraphicsContext; use dropbear_engine::utils::ResourceReference; @@ -17,8 +17,8 @@ pub struct PendingSpawn { pub asset_path: ResourceReference, /// The name/label of the asset pub asset_name: String, - /// The [`Transform`] properties (position) - pub transform: Transform, + /// The [`EntityTransform`] properties (position) + pub transform: EntityTransform, /// The properties of a model, as specified in [`ModelProperties`] pub properties: ModelProperties, /// An optional future handle to an object. @@ -1,6 +1,7 @@ +use crate::traits::SerializableComponent; use crate::camera::{CameraComponent, CameraType}; -use crate::hierarchy::Parent; -use crate::utils::{PROTO_TEXTURE, ResolveReference}; +use crate::hierarchy::{Parent, SceneHierarchy}; +use crate::utils::{ResolveReference, PROTO_TEXTURE}; use chrono::Utc; use dropbear_engine::asset::ASSET_REGISTRY; use dropbear_engine::camera::{Camera, CameraBuilder, CameraSettings}; @@ -26,6 +27,8 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; use std::{fmt, fs}; use tokio::sync::mpsc::UnboundedSender; +use dropbear_macro::SerializableComponent; +use crate::scene::SceneConfig; pub static PROJECT: Lazy<RwLock<ProjectConfig>> = Lazy::new(|| RwLock::new(ProjectConfig::default())); @@ -574,7 +577,7 @@ pub enum EntityNode { }, } -#[derive(Default, Debug, Serialize, Deserialize, Clone)] +#[derive(Default, Debug, Serialize, Deserialize, Clone, SerializableComponent)] pub struct ScriptComponent { pub tags: Vec<String>, } @@ -692,12 +695,12 @@ impl EntityNode { } } -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, SerializableComponent)] pub struct CameraConfig { pub label: String, pub camera_type: CameraType, - pub position: [f64; 3], + pub eye: [f64; 3], pub target: [f64; 3], pub up: [f64; 3], pub aspect: f64, @@ -708,8 +711,6 @@ pub struct CameraConfig { pub speed: f32, pub sensitivity: f32, - // pub follow_target_entity_label: Option<String>, - // pub follow_offset: Option<[f64; 3]>, pub starting_camera: bool, } @@ -717,15 +718,13 @@ impl Default for CameraConfig { fn default() -> Self { let default = CameraComponent::new(); Self { - position: [0.0, 1.0, 2.0], + eye: [0.0, 1.0, 2.0], target: [0.0, 0.0, 0.0], up: [0.0, 1.0, 0.0], aspect: 16.0 / 9.0, fov: 45.0, near: 0.1, far: 100.0, - // follow_target_entity_label: None, - // follow_offset: None, label: String::new(), camera_type: CameraType::Normal, speed: default.settings.speed as f32, @@ -742,7 +741,7 @@ impl CameraConfig { // follow_target: Option<&CameraFollowTarget>, ) -> Self { Self { - position: camera.position().to_array(), + eye: camera.position().to_array(), target: camera.target.to_array(), label: camera.label.clone(), camera_type: component.camera_type, @@ -753,38 +752,12 @@ impl CameraConfig { far: camera.zfar as f32, speed: component.settings.speed as f32, sensitivity: component.settings.sensitivity as f32, - // follow_target_entity_label: follow_target.map(|target| target.follow_target.clone()), - // follow_offset: follow_target.map(|target| target.offset.to_array()), starting_camera: component.starting_camera, } } } -#[derive(Default, Debug, Serialize, Deserialize, Clone)] -pub struct SceneEntity { - #[serde(default)] - pub model_path: ResourceReference, - #[serde(default)] - pub label: Label, - #[serde(default)] - pub transform: Transform, - #[serde(default)] - pub properties: ModelProperties, - #[serde(default)] - pub script: Option<ScriptComponent>, - #[serde(default)] - pub camera: Option<CameraConfig>, - #[serde(default)] - pub children: Option<Vec<Label>>, - #[serde(default)] - pub material_overrides: Vec<MaterialOverride>, - - #[serde(skip)] - #[allow(dead_code)] - pub entity_id: Option<hecs::Entity>, -} - -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, SerializableComponent)] pub struct ModelProperties { pub custom_properties: Vec<Property>, pub next_id: u64, @@ -813,7 +786,7 @@ impl Default for Value { } impl Display for Value { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { let string: String = match self { Value::String(_) => "String".into(), Value::Int(_) => "Int".into(), @@ -923,540 +896,7 @@ impl Default for ModelProperties { } } -#[derive(Default, Debug, Serialize, Deserialize, Clone)] -pub struct SceneConfig { - #[serde(default)] - pub scene_name: String, - #[serde(default)] - pub entities: Vec<SceneEntity>, - #[serde(default)] - pub cameras: Vec<CameraConfig>, - #[serde(default)] - pub lights: Vec<LightConfig>, - #[serde(default)] - // todo later - // pub settings: SceneSettings, - #[serde(skip)] - pub path: PathBuf, -} - -impl SceneConfig { - /// Creates a new instance of the scene config - pub fn new(scene_name: String, path: impl AsRef<Path>) -> Self { - Self { - scene_name, - path: path.as_ref().to_path_buf(), - entities: Vec::new(), - cameras: Vec::new(), - lights: Vec::new(), - } - } - - /// Write the scene config to a .eucs file - pub fn write_to(&self, project_path: impl AsRef<Path>) -> anyhow::Result<()> { - let ron_str = ron::ser::to_string_pretty(&self, PrettyConfig::default()) - .map_err(|e| anyhow::anyhow!("RON serialization error: {}", e))?; - - let scenes_dir = project_path.as_ref().join("scenes"); - fs::create_dir_all(&scenes_dir)?; - - let config_path = scenes_dir.join(format!("{}.eucs", self.scene_name)); - fs::write(&config_path, ron_str).map_err(|e| anyhow::anyhow!(e.to_string()))?; - Ok(()) - } - - /// Read a scene config from a .eucs file - pub fn read_from(scene_path: impl AsRef<Path>) -> anyhow::Result<Self> { - let ron_str = fs::read_to_string(scene_path.as_ref())?; - let mut config: SceneConfig = ron::de::from_str(&ron_str) - .map_err(|e| anyhow::anyhow!("RON deserialization error: {}", e))?; - - config.path = scene_path.as_ref().to_path_buf(); - Ok(config) - } - - pub async fn load_into_world( - &self, - world: &mut hecs::World, - graphics: Arc<SharedGraphicsContext>, - progress_sender: Option<UnboundedSender<WorldLoadingStatus>>, - ) -> anyhow::Result<hecs::Entity> { - if let Some(ref s) = progress_sender { - let _ = s.send(WorldLoadingStatus::Idle); - } - - log::info!( - "Loading scene [{}], clearing world with {} entities", - self.scene_name, - world.len() - ); - { - world.clear(); - } - - #[allow(unused_variables)] - let project_config = if cfg!(feature = "editor") { - let cfg = PROJECT.read(); - cfg.project_path.clone() - } else { - log::debug!("Not using the editor feature, returning empty pathbuffer"); - PathBuf::new() - }; - - log::info!("World cleared, now has {} entities", world.len()); - - let entity_configs: Vec<(usize, SceneEntity)> = { - let cloned = self.entities.clone(); - cloned - .into_par_iter() - .enumerate() - .map(|(i, e)| (i, e)) - .collect() - }; - - let mut label_to_entity: HashMap<Label, hecs::Entity> = HashMap::new(); - let mut pending_parent_links: Vec<(Label, Vec<Label>)> = Vec::new(); - - for (index, entity_config) in entity_configs { - let SceneEntity { - model_path, - label, - transform, - properties, - script, - camera, - children, - material_overrides, - .. - } = entity_config; - - let label_for_map = label.clone(); - let label_for_logs = label_for_map.to_string(); - - log::debug!("Loading entity: {}", label_for_logs); - - let total = self.entities.len(); - - if let Some(ref s) = progress_sender { - let _ = s.send(WorldLoadingStatus::LoadingEntity { - index, - name: label_for_logs.clone(), - total, - }); - } - - let mut renderer = match &model_path.ref_type { - ResourceReferenceType::File(reference) => { - let path = model_path.resolve()?; - - log::debug!( - "Path for entity {} is {} from reference {}", - label_for_logs, - path.display(), - reference - ); - - MeshRenderer::from_path(graphics.clone(), &path, Some(label_for_map.as_str())) - .await? - } - ResourceReferenceType::Bytes(bytes) => { - log::info!("Loading entity from bytes [Len: {}]", bytes.len()); - - let model = Model::load_from_memory( - graphics.clone(), - bytes.clone(), - Some(label_for_map.as_str()), - ) - .await?; - MeshRenderer::from_handle(model) - } - ResourceReferenceType::Plane => { - let width = properties.get_float("width").ok_or_else(|| { - anyhow::anyhow!( - "Entity '{}' has no width property or it's not a float", - label_for_logs - ) - })?; - - let height = properties.get_float("height").ok_or_else(|| { - anyhow::anyhow!( - "Entity '{}' has no height property or it's not a float", - label_for_logs - ) - })?; - - let tiles_x = properties.get_int("tiles_x").ok_or_else(|| { - anyhow::anyhow!( - "Entity '{}' has no tiles_x property or it's not an int", - label_for_logs - ) - })?; - - let tiles_z = properties.get_int("tiles_z").ok_or_else(|| { - anyhow::anyhow!( - "Entity '{}' has no tiles_z property or it's not an int", - label_for_logs - ) - })?; - - PlaneBuilder::new() - .with_size(width as f32, height as f32) - .with_tiles(tiles_x as u32, tiles_z as u32) - .build( - graphics.clone(), - PROTO_TEXTURE, - Some(label_for_map.as_str()), - ) - .await? - } - ResourceReferenceType::None => { - anyhow::bail!( - "Entity '{}' has a resource reference of None, which cannot be loaded or referenced", - label_for_logs - ); - } - }; - - if !material_overrides.is_empty() { - for override_entry in &material_overrides { - 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).await?; - } else { - log::warn!( - "Material override for '{}' references unsupported resource {:?}", - label_for_logs, - override_entry.source_model - ); - continue; - } - } - - if let Err(err) = 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_for_logs, - err - ); - } - } - } - - renderer.update(&transform); - - let entity = world.spawn((label, renderer, transform, properties)); - - if let Some(script_component) = script { - world.insert_one(entity, script_component)?; - } - - if let Some(camera_config) = camera { - let camera = Camera::new( - graphics.clone(), - CameraBuilder { - eye: DVec3::from_array(camera_config.position), - target: DVec3::from_array(camera_config.target), - up: DVec3::from_array(camera_config.up), - aspect: camera_config.aspect, - znear: camera_config.near as f64, - zfar: camera_config.far as f64, - settings: CameraSettings::new( - camera_config.speed as f64, - camera_config.sensitivity as f64, - camera_config.fov as f64, - ), - }, - Some(&camera_config.label), - ); - - let camera_component = CameraComponent { - settings: CameraSettings::new( - camera_config.speed as f64, - camera_config.sensitivity as f64, - camera_config.fov as f64, - ), - camera_type: camera_config.camera_type, - starting_camera: camera_config.starting_camera, - }; - - world.insert(entity, (camera, camera_component))?; - } - - if let Some(children_labels) = children { - if !children_labels.is_empty() { - pending_parent_links.push((label_for_map.clone(), children_labels)); - } - } - - if let Some(previous) = label_to_entity.insert(label_for_map.clone(), entity) { - log::warn!( - "Duplicate entity label '{}' detected; previous entity {:?} will be overwritten in hierarchy mapping", - label_for_logs, - previous - ); - } - - log::debug!("Loaded entity '{}'", label_for_logs); - } - - for (parent_label, child_labels) in pending_parent_links { - let Some(&parent_entity) = label_to_entity.get(&parent_label) else { - log::warn!( - "Unable to resolve parent entity '{}' while rebuilding hierarchy", - parent_label - ); - continue; - }; - - let mut resolved_children = Vec::new(); - for child_label in child_labels { - if let Some(&child_entity) = label_to_entity.get(&child_label) { - resolved_children.push(child_entity); - } else { - log::warn!( - "Unable to resolve child '{}' for parent '{}'", - child_label, - parent_label - ); - } - } - - if resolved_children.is_empty() { - continue; - } - - let mut local_insert_one: Option<hecs::Entity> = None; - - match world.query_one::<&mut Parent>(parent_entity) { - Ok(mut parent_query) => { - if let Some(parent_component) = parent_query.get() { - parent_component.clear(); - parent_component - .children_mut() - .extend(resolved_children.iter().copied()); - } else { - local_insert_one = Some(parent_entity); - } - } - Err(e) => { - log::warn!( - "Failed to query Parent component for entity {:?}: {}", - parent_entity, - e - ); - local_insert_one = Some(parent_entity); - } - } - - if let Some(parent_entity) = local_insert_one { - if let Err(e) = world.insert_one(parent_entity, Parent::new(resolved_children)) { - log::error!( - "Failed to attach Parent component to entity {:?}: {}", - parent_entity, - e - ); - } - } - } - - let total = self.lights.len(); - - for (index, light_config) in self.lights.iter().enumerate() { - log::debug!("Loading light: {}", light_config.label); - if let Some(ref s) = progress_sender { - let _ = s.send(WorldLoadingStatus::LoadingLight { - index, - name: light_config.label.clone(), - total, - }); - } - - let light = Light::new( - graphics.clone(), - light_config.light_component.clone(), - light_config.transform, - Some(&light_config.label), - ) - .await; - { - world.spawn(( - Label::from(light_config.label.clone()), - light_config.light_component.clone(), - light_config.transform, - light, - ModelProperties::default(), - )); - } - } - - let total = self.cameras.len(); - for (index, camera_config) in self.cameras.iter().enumerate() { - log::debug!( - "Loading camera {} of type {:?}", - camera_config.label, - camera_config.camera_type - ); - if let Some(ref s) = progress_sender { - let _ = s.send(WorldLoadingStatus::LoadingCamera { - index, - name: camera_config.label.clone(), - total, - }); - } - - let camera = Camera::new( - graphics.clone(), - CameraBuilder { - eye: DVec3::from_array(camera_config.position), - target: DVec3::from_array(camera_config.target), - up: DVec3::from_array(camera_config.up), - aspect: camera_config.aspect, - znear: camera_config.near as f64, - zfar: camera_config.far as f64, - settings: CameraSettings::new( - camera_config.speed as f64, - camera_config.sensitivity as f64, - camera_config.fov as f64, - ), - }, - Some(&camera_config.label), - ); - - let component = CameraComponent { - settings: CameraSettings::new( - camera_config.speed as f64, - camera_config.sensitivity as f64, - camera_config.fov as f64, - ), - camera_type: camera_config.camera_type, - starting_camera: camera_config.starting_camera, - }; - world.spawn((Label::from(camera_config.label.clone()), camera, component)); - } - - { - let mut is_none = false; - if world - .query::<(&LightComponent, &Light)>() - .iter() - .next() - .is_none() - { - log::info!("No lights in scene, spawning default light"); - is_none = true; - } - - if is_none { - if let Some(ref s) = progress_sender { - let _ = s.send(WorldLoadingStatus::LoadingLight { - index: 0, - name: String::from("Default Light"), - total: 1, - }); - } - let comp = LightComponent::directional(glam::DVec3::ONE, 1.0); - let light_direction = LightComponent::default_direction(); - let rotation = - DQuat::from_rotation_arc(DVec3::new(0.0, 0.0, -1.0), light_direction); - let trans = Transform { - position: glam::DVec3::new(2.0, 4.0, 2.0), - rotation, - ..Default::default() - }; - let light = - Light::new(graphics.clone(), comp.clone(), trans, Some("Default Light")).await; - - { - world.spawn(( - Label::from("Default Light"), - comp, - trans, - light, - ModelProperties::default(), - )); - } - } - } - - log::info!( - "Loaded {} entities, {} lights and {} cameras", - self.entities.len(), - self.lights.len(), - self.cameras.len() - ); - #[cfg(feature = "editor")] - { - let debug_camera = { - world - .query::<(&Camera, &CameraComponent)>() - .iter() - .find_map(|(entity, (_, component))| { - if matches!(component.camera_type, CameraType::Debug) { - Some(entity) - } else { - None - } - }) - }; - - { - if let Some(camera_entity) = debug_camera { - log::info!("Using existing debug camera for editor"); - Ok(camera_entity) - } else { - log::info!("No debug camera found, creating viewport camera for editor"); - if let Some(ref s) = progress_sender { - let _ = s.send(WorldLoadingStatus::LoadingCamera { - index: 0, - name: String::from("Viewport Camera"), - total: 1, - }); - } - let camera = Camera::predetermined(graphics.clone(), Some("Viewport Camera")); - let component = crate::camera::DebugCamera::new(); - let camera_entity = { world.spawn((camera, component)) }; - Ok(camera_entity) - } - } - } - - #[cfg(not(feature = "editor"))] - { - let player_camera = world - .query::<(&Camera, &CameraComponent)>() - .iter() - .find_map(|(entity, (_, component))| { - if matches!(component.camera_type, CameraType::Player) { - Some(entity) - } else { - None - } - }); - - if let Some(camera_entity) = player_camera { - log::info!("Using player camera for runtime"); - Ok(camera_entity) - } else { - panic!("Runtime mode requires a player camera, but none was found in the scene!"); - } - } - } -} - -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, SerializableComponent)] pub struct LightConfig { pub label: String, pub transform: Transform, @@ -1510,16 +950,6 @@ pub enum WorldLoadingStatus { name: String, total: usize, }, - LoadingLight { - index: usize, - name: String, - total: usize, - }, - LoadingCamera { - index: usize, - name: String, - total: usize, - }, Completed, } @@ -1535,7 +965,7 @@ pub struct RuntimeData { pub scripts: HashMap<String, String>, } -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Hash, Clone, SerializableComponent)] pub struct Label(String); impl Default for Label { @@ -1619,3 +1049,9 @@ impl DerefMut for Label { self.as_mut_string() } } + +#[derive(Default, Debug, Clone, Serialize, Deserialize, SerializableComponent)] +pub struct SerializedMeshRenderer { + pub handle: ResourceReference, + pub material_override: Vec<MaterialOverride>, +} @@ -21,14 +21,14 @@ use dropbear_engine::{ future::FutureHandle, graphics::{RenderContext, SharedGraphicsContext}, lighting::{Light, LightManager}, - model::{MODEL_CACHE, ModelId}, + model::{ModelId, MODEL_CACHE}, scene::SceneCommand, }; use egui::{self, Context}; use egui_dock::{DockArea, DockState, NodeIndex, Style}; use eucalyptus_core::APP_INFO; -use eucalyptus_core::hierarchy::Parent; -use eucalyptus_core::states::Label; +use eucalyptus_core::hierarchy::{Parent, SceneHierarchy}; +use eucalyptus_core::states::{Label, SerializedMeshRenderer}; use eucalyptus_core::{ camera::{CameraComponent, CameraType, DebugCamera}, fatal, info, @@ -37,8 +37,8 @@ use eucalyptus_core::{ scripting::{BuildStatus, ScriptManager, ScriptTarget}, states, states::{ - CameraConfig, EditorTab, EntityNode, LightConfig, ModelProperties, PROJECT, SCENES, - SceneConfig, SceneEntity, ScriptComponent, WorldLoadingStatus, + CameraConfig, EditorTab, EntityNode, LightConfig, ModelProperties, ScriptComponent, WorldLoadingStatus, + PROJECT, SCENES, }, success, success_without_console, utils::ViewportMode, @@ -62,6 +62,10 @@ use transform_gizmo_egui::{EnumSet, Gizmo, GizmoMode}; use wgpu::{Color, Extent3d, RenderPipeline}; use winit::window::CursorGrabMode; use winit::{keyboard::KeyCode, window::Window}; +use dropbear_engine::entity::EntityTransform; +use dropbear_engine::lighting::LightComponent; +use eucalyptus_core::scene::{SceneConfig, SceneEntity}; +use eucalyptus_core::traits::SerializableComponent; pub struct Editor { pub scene_command: SceneCommand, @@ -269,44 +273,66 @@ impl Editor { .iter_mut() .find(|scene| scene.scene_name == target_scene_name) .ok_or_else(|| anyhow::anyhow!("Active scene '{}' is not loaded", target_scene_name))?; + scene.entities.clear(); - scene.lights.clear(); - scene.cameras.clear(); + scene.hierarchy_map = SceneHierarchy::new(); - for (id, (label, renderer, transform, properties, script, parent)) in self - .world - .query::<( - &Label, - &MeshRenderer, - Option<&Transform>, - &ModelProperties, - Option<&ScriptComponent>, - Option<&Parent>, - )>() - .iter() - { - let transform = *transform.unwrap_or(&Transform::default()); + let labels = self.world.query::<&Label>().iter().map(|(e, l)| (e, l.clone())).collect::<Vec<_>>(); + + for (id, label) in labels { let entity_label = label.clone(); + let mut components: Vec<Box<dyn SerializableComponent>> = Vec::new(); - let camera_config = if let Ok(mut camera_query) = - self.world.query_one::<(&Camera, &CameraComponent)>(id) + if let Ok(transform) = self.world.get::<&EntityTransform>(id) { + components.push(Box::new(*transform)); + } + + if let Ok(renderer) = self.world.get::<&MeshRenderer>(id) { + let serialized = SerializedMeshRenderer { + handle: renderer.handle().path.clone(), + material_override: renderer.material_overrides().to_vec(), + }; + components.push(Box::new(serialized)); + } + + if let Ok(mut q) = self.world.query_one::<&ModelProperties>(id) && let Some(properties) = q.get() { + components.push(Box::new(properties.clone())); + } + + if let Ok(mut camera_query) = self.world.query_one::<(&Camera, &CameraComponent)>(id) + && let Some((camera, component)) = camera_query.get() { - if let Some((camera, component)) = camera_query.get() { - Some(CameraConfig::from_ecs_camera(camera, component)) - } else { - None + let camera_config = CameraConfig::from_ecs_camera(camera, component); + components.push(Box::new(camera_config)); + } + + if let Ok(mut light_query) = self.world.query_one::<(&Light, &LightComponent, &EntityTransform)>(id) { + if let Some((light, light_component, entity_transform)) = light_query.get() { + let light_config = LightConfig { + label: light.cube_model.label.to_string(), + transform: entity_transform.sync(), + light_component: light_component.clone(), + enabled: light_component.enabled, + entity_id: Some(id), + }; + components.push(Box::new(light_config)); } - } else { - None - }; + } - let children = if let Some(parent_component) = parent { - let mut collected = Vec::new(); + if let Ok(mut q) = self.world.query_one::<&ScriptComponent>(id) && let Some(script) = q.get() { + components.push(Box::new(script.clone())); + } + + let mut parent_component = None; + + if let Ok(comp) = self.world.query_one_mut::<&Parent>(id) { + parent_component = Some(comp.clone()); + } + + if let Some(parent_component) = parent_component { for &child_entity in parent_component.children() { - if let Ok(mut child_query) = self.world.query_one::<&Label>(child_entity) - && let Some(child_label) = child_query.get() - { - collected.push(child_label.clone()); + if let Ok(child_label) = self.world.query_one_mut::<&Label>(child_entity) { + scene.hierarchy_map.set_parent(child_label.clone(), entity_label.clone()); } else { log::warn!( "Unable to resolve child entity {:?} for parent '{}' when saving scene", @@ -315,69 +341,27 @@ impl Editor { ); } } - - if collected.is_empty() { - None - } else { - Some(collected) - } - } else { - None - }; + } let scene_entity = SceneEntity { - model_path: renderer.handle().path.clone(), label: entity_label.clone(), - transform, - properties: properties.clone(), - script: script.cloned(), - camera: camera_config, - children, - material_overrides: renderer.material_overrides().to_vec(), + components, entity_id: Some(id), }; scene.entities.push(scene_entity); - log::debug!("Pushed entity: {}", entity_label); - } - - for (id, (light_component, transform, light)) in self - .world - .query::<( - &dropbear_engine::lighting::LightComponent, - &Transform, - &Light, - )>() - .iter() - { - let light_config = LightConfig { - label: light.cube_model.label.to_string(), - transform: *transform, - light_component: light_component.clone(), - enabled: light_component.enabled, - entity_id: Some(id), - }; - - scene.lights.push(light_config); - log::debug!("Pushed light into lights: {}", light.cube_model.label); - } - - for (id, (camera, component)) in self.world.query::<(&Camera, &CameraComponent)>().iter() { - if self.world.get::<&MeshRenderer>(id).is_err() { - let camera_config = CameraConfig::from_ecs_camera(camera, component); - scene.cameras.push(camera_config); - log::debug!("Pushed standalone camera into cameras: {}", camera.label); - } + log::debug!("Saved entity: {}", entity_label); } log::info!( - "Saved {} entities and camera configs to scene '{}'", + "Saved {} entities to scene '{}'", scene.entities.len(), scene.scene_name ); Ok(()) } + fn persist_active_scene_to_disk(&self) -> anyhow::Result<()> { let target_scene_name = self.current_scene_name.clone().or_else(|| { let scenes = SCENES.read(); @@ -441,16 +425,6 @@ impl Editor { self.current_state = WorldLoadingStatus::LoadingEntity { index, name, total }; } - WorldLoadingStatus::LoadingLight { index, name, total } => { - log::debug!("Loading light: {} ({}/{})", name, index + 1, total); - self.current_state = - WorldLoadingStatus::LoadingLight { index, name, total }; - } - WorldLoadingStatus::LoadingCamera { index, name, total } => { - log::debug!("Loading camera: {} ({}/{})", name, index + 1, total); - self.current_state = - WorldLoadingStatus::LoadingCamera { index, name, total }; - } WorldLoadingStatus::Completed => { log::debug!( "Received WorldLoadingStatus::Completed - project loading finished" @@ -494,12 +468,6 @@ impl Editor { WorldLoadingStatus::LoadingEntity { name, .. } => { ui.label(format!("Loading entity: {}", name)); } - WorldLoadingStatus::LoadingLight { name, .. } => { - ui.label(format!("Loading light: {}", name)); - } - WorldLoadingStatus::LoadingCamera { name, .. } => { - ui.label(format!("Loading camera: {}", name)); - } WorldLoadingStatus::Completed => { ui.label("Done!"); } @@ -560,9 +528,8 @@ impl Editor { *a_c = Some(cam); log::info!( - "Successfully loaded scene with {} entities and {} camera configs", + "Successfully loaded scene with {} entities", first_scene.entities.len(), - first_scene.cameras.len(), ); } else { let existing_debug_camera = { @@ -906,20 +873,26 @@ impl Editor { let query = self.world.query_one::<( &Label, &MeshRenderer, - &Transform, + &EntityTransform, &ModelProperties, )>(*entity); if let Ok(mut q) = query { - if let Some((entity_label, e, t, props)) = q.get() { - let s_entity = states::SceneEntity { - model_path: e.handle().path.clone(), + if let Some((entity_label, renderer, transform, props)) = q.get() { + let mut components: Vec<Box<dyn SerializableComponent>> = Vec::new(); + + components.push(Box::new(*transform)); + + let serialized_renderer = SerializedMeshRenderer { + handle: renderer.handle().path.clone(), + material_override: renderer.material_overrides().to_vec(), + }; + components.push(Box::new(serialized_renderer)); + + components.push(Box::new(props.clone())); + + let s_entity = SceneEntity { label: entity_label.clone(), - transform: *t, - properties: props.clone(), - script: None, - camera: None, - children: None, - material_overrides: e.material_overrides().to_vec(), + components, entity_id: None, }; self.signal = Signal::Copy(s_entity); @@ -1783,7 +1756,7 @@ pub enum Signal { RemoveComponent(hecs::Entity, Box<ComponentType>), CreateEntity, LogEntities, - Spawn(PendingSpawn2), + Spawn(PendingSpawnType), } #[derive(Debug)] @@ -1817,7 +1790,7 @@ struct PendingSceneLoad { scene: SceneConfig, } -pub enum PendingSpawn2 { +pub enum PendingSpawnType { Light, Plane, Cube, @@ -1,5 +1,5 @@ use crate::editor::{ - ComponentType, Editor, EditorState, EntityType, PendingSpawn2, Signal, UndoableAction, + ComponentType, Editor, EditorState, EntityType, PendingSpawnType, Signal, UndoableAction, }; use dropbear_engine::camera::Camera; use dropbear_engine::entity::{MeshRenderer, Transform}; @@ -667,22 +667,22 @@ impl SignalController for Editor { if ui.add_sized([ui.available_width(), 30.0], egui::Button::new("Light")).clicked() { log::debug!("Creating new lighting"); - self.signal = Signal::Spawn(PendingSpawn2::Light); + self.signal = Signal::Spawn(PendingSpawnType::Light); } if ui.add_sized([ui.available_width(), 30.0], egui::Button::new("Plane")).clicked() { log::debug!("Creating new plane"); - self.signal = Signal::Spawn(PendingSpawn2::Plane); + self.signal = Signal::Spawn(PendingSpawnType::Plane); } if ui.add_sized([ui.available_width(), 30.0], egui::Button::new("Cube")).clicked() { log::debug!("Creating new cube"); - self.signal = Signal::Spawn(PendingSpawn2::Cube); + self.signal = Signal::Spawn(PendingSpawnType::Cube); } if ui.add_sized([ui.available_width(), 30.0], egui::Button::new("Camera")).clicked() { log::debug!("Creating new cube"); - self.signal = Signal::Spawn(PendingSpawn2::Camera); + self.signal = Signal::Spawn(PendingSpawnType::Camera); } }); if !show { @@ -768,7 +768,7 @@ impl SignalController for Editor { } Signal::Spawn(entity_type) => { match entity_type { - crate::editor::PendingSpawn2::Light => { + crate::editor::PendingSpawnType::Light => { let light = Light::new( graphics.clone(), LightComponent::default(), @@ -779,7 +779,7 @@ impl SignalController for Editor { self.light_spawn_queue.push(handle); success!("Pushed light to queue"); } - crate::editor::PendingSpawn2::Plane => { + crate::editor::PendingSpawnType::Plane => { let transform = Transform::new(); let mut props = ModelProperties::new(); props.add_property("width".to_string(), Value::Float(500.0)); @@ -798,7 +798,7 @@ impl SignalController for Editor { }); success!("Pushed plane to queue"); } - PendingSpawn2::Cube => { + PendingSpawnType::Cube => { let pending = PendingSpawn { asset_path: ResourceReference::from_bytes(include_bytes!( "../../resources/models/cube.glb" @@ -811,7 +811,7 @@ impl SignalController for Editor { push_pending_spawn(pending); success!("Pushed cube to queue"); } - PendingSpawn2::Camera => { + PendingSpawnType::Camera => { let camera = Camera::predetermined(graphics.clone(), None); let component = CameraComponent::new(); { @@ -33,7 +33,6 @@ impl PendingSpawnController for Editor { let graphics_clone = graphics.clone(); let asset_name = spawn.asset_name.clone(); let asset_path = spawn.asset_path.ref_type.clone(); - let properties = spawn.properties.clone(); let func = async move { match asset_path { @@ -60,44 +59,48 @@ impl PendingSpawnController for Editor { Ok(MeshRenderer::from_handle(model)) } ResourceReferenceType::Plane => { - let get_float = |key: &str| -> anyhow::Result<f32> { - let val = properties - .custom_properties - .iter() - .find(|p| p.key == key) - .ok_or_else(|| { - anyhow::anyhow!("Entity has no {} property", key) - })?; - match val.value { - Value::Float(f) => Ok(f as f32), - _ => Err(anyhow::anyhow!("{} is not a float", key)), - } - }; - - let get_int = |key: &str| -> anyhow::Result<u32> { - let val = properties - .custom_properties - .iter() - .find(|p| p.key == key) - .ok_or_else(|| { - anyhow::anyhow!("Entity has no {} property", key) - })?; - match val.value { - Value::Int(i) => Ok(i as u32), - _ => Err(anyhow::anyhow!("{} is not an int", key)), - } - }; - - let width = get_float("width")?; - let height = get_float("height")?; - let tiles_x = get_int("tiles_x")?; - let tiles_z = get_int("tiles_z")?; - - PlaneBuilder::new() - .with_size(width, height) - .with_tiles(tiles_x, tiles_z) - .build(graphics_clone, PROTO_TEXTURE, Some(&asset_name)) - .await + anyhow::bail!("Unable to load plane: Not supported anymore, rebuilding it"); + // let get_float = |key: &str| -> anyhow::Result<f32> { + // let val = properties + // .custom_properties + // .iter() + // .find(|p| p.key == key) + // .ok_or_else(|| { + // anyhow::anyhow!("Entity has no {} property", key) + // })?; + // match val.value { + // Value::Float(f) => Ok(f as f32), + // _ => Err(anyhow::anyhow!("{} is not a float", key)), + // } + // }; + // + // let get_int = |key: &str| -> anyhow::Result<u32> { + // let val = properties + // .custom_properties + // .iter() + // .find(|p| p.key == key) + // .ok_or_else(|| { + // anyhow::anyhow!("Entity has no {} property", key) + // })?; + // match val.value { + // Value::Int(i) => Ok(i as u32), + // _ => Err(anyhow::anyhow!("{} is not an int", key)), + // } + // }; + // + // let width = get_float("width")?; + // let height = get_float("height")?; + // let tiles_x = get_int("tiles_x")?; + // let tiles_z = get_int("tiles_z")?; + // + // PlaneBuilder::new() + // .with_size(width, height) + // .with_tiles(tiles_x, tiles_z) + // .build(graphics_clone, PROTO_TEXTURE, Some(&asset_name)) + // .await + } + ResourceReferenceType::Cube => { + Model::load_from_memory(graphics_clone, include_bytes!("../../resources/models/cube.glb"), Some(&asset_name)).await.map(MeshRenderer::from_handle) } } }; @@ -23,33 +23,26 @@ class EntityRef(val id: EntityId = EntityId(0L)) { } /** - * Fetches the world transform component for the entity. + * Fetches the [EntityTransform] component for the entity. */ - fun getWorldTransform(): Transform? { - return engine.native.getWorldTransform(id) + fun getTransform(): EntityTransform? { + return engine.native.getTransform(id) } /** - * Fetches the local transform component for the entity + * Walks up the hierarchy to find the transform of the parent, then multiply to create a propagated [Transform]. + * This will update the entity's world transform based on its parent's world transform. */ - fun getLocalTransform(): Transform? { - return engine.native.getLocalTransform(id) + fun propagate() { + engine.native.propagateTransform(id) } /** - * Sets and replaces the world transform component for the entity. + * Sets and replaces the [EntityTransform] component for the entity. */ - fun commitWorld(transform: Transform?) { + fun setTransform(transform: EntityTransform?) { if (transform == null) return - return engine.native.setWorldTransform(id, transform) - } - - /** - * Sets and replaces the local transfrom component for the entity - */ - fun commitLocal(transform: Transform?) { - if (transform == null) return - return engine.native.setLocalTransform(id, transform) + return engine.native.setTransform(id, transform) } /** @@ -170,4 +163,4 @@ class EntityRef(val id: EntityId = EntityId(0L)) { fun setTextureOverride(materialName: String, textureHandle: TextureHandle) { engine.native.setTextureOverride(id.id, materialName, textureHandle) } -} +} @@ -0,0 +1,8 @@ +package com.dropbear + +import com.dropbear.math.Transform + +/** + * A component that contains the local and world [Transform] of an entity. + */ +class EntityTransform(var local: Transform, var world: Transform) @@ -2,6 +2,7 @@ package com.dropbear.ffi import com.dropbear.Camera import com.dropbear.EntityId +import com.dropbear.EntityTransform import com.dropbear.asset.AssetHandle import com.dropbear.asset.ModelHandle import com.dropbear.asset.TextureHandle @@ -12,7 +13,7 @@ import com.dropbear.math.Vector2D /** * Native functions - * + * * Class that describes all the functions that can * be communicated with the `eucalyptus_core` dynamic library */ @@ -36,10 +37,9 @@ expect class NativeEngine { fun getAttachedCamera(entityId: EntityId): Camera? fun setCamera(camera: Camera); - fun getWorldTransform(entityId: EntityId): Transform? - fun getLocalTransform(entityId: EntityId): Transform? - fun commitWorldTransform(entityId: EntityId, transform: Transform) - fun commitLocalTransform(entityId: EntityId, transform: Transform) + fun getTransform(entityId: EntityId): EntityTransform? + fun propagateTransform(entityId: EntityId) + fun setTransform(entityId: EntityId, transform: EntityTransform) // ------------------------ MODEL PROPERTIES ------------------------- @@ -1,11 +1,13 @@ package com.dropbear.ffi; import com.dropbear.Camera; +import com.dropbear.EntityTransform; import com.dropbear.math.Transform; +import org.jetbrains.annotations.Nullable; /** * Describes all the functions that are available in - * the `eucalyptus_core` dynamic library. + * the `eucalyptus_core` dynamic library. */ public class JNINative { static { @@ -36,10 +38,9 @@ public class JNINative { public static native void setCamera(long worldHandle, Camera camera); // transformations - public static native Transform getWorldTransform(long handle, long entityHandle); - public static native void commitWorldTransform(long worldHandle, long id, Transform transform); - public static native Transform getLocalTransform(long handle, long entityHandle); - public static native void commitLocalTransform(long worldHandle, long id, Transform transform); + public static native EntityTransform getTransform(long handle, long entityHandle); + public static native void propagateTransform(long worldHandle, long id); + public static native void setTransform(long worldHandle, long id, EntityTransform transform); // properties public static native String getStringProperty(long worldHandle, long entityHandle, String label); @@ -3,6 +3,7 @@ package com.dropbear.ffi import com.dropbear.Camera import com.dropbear.DropbearEngine import com.dropbear.EntityId +import com.dropbear.EntityTransform import com.dropbear.asset.TextureHandle import com.dropbear.exception.DropbearNativeException import com.dropbear.exceptionOnError @@ -76,20 +77,12 @@ actual class NativeEngine { } - actual fun getWorldTransform(entityId: EntityId): Transform? { - return JNINative.getWorldTransform(worldHandle, entityId.id) + actual fun getTransform(entityId: EntityId): EntityTransform? { + return JNINative.getTransform(worldHandle, entityId.id) } - actual fun getLocalTransform(entityId: EntityId): Transform? { - return JNINative.getLocalTransform(worldHandle, entityId.id) - } - - actual fun commitWorldTransform(entityId: EntityId, transform: Transform) { - return JNINative.commitWorldTransform(worldHandle, entityId.id, transform) - } - - actual fun commitLocalTransform(entityId: EntityId, transform: Transform) { - return JNINative.commitLocalTransform(worldHandle, entityId.id, transform) + actual fun setTransform(entityId: EntityId, transform: EntityTransform) { + return JNINative.setTransform(worldHandle, entityId.id, transform) } actual fun printInputState() { @@ -336,4 +329,8 @@ actual class NativeEngine { actual fun getAllTextures(entityHandle: Long): Array<String> { return JNINative.getAllTextures(worldHandle, entityHandle) ?: emptyArray() } -} + + actual fun propagateTransform(entityId: EntityId) { + JNINative.propagateTransform(worldHandle, entityId.id) + } +} @@ -9,6 +9,7 @@ package com.dropbear.ffi import com.dropbear.Camera import com.dropbear.EntityId +import com.dropbear.EntityTransform import com.dropbear.asset.TextureHandle import com.dropbear.exception.DropbearNativeException import com.dropbear.exceptionOnError @@ -61,7 +62,7 @@ actual class NativeEngine { } } - actual fun getWorldTransform(entityId: EntityId): Transform? { + actual fun getTransform(entityId: EntityId): EntityTransform? { val world = worldHandle ?: return null memScoped { val outTransform = alloc<NativeTransform>() @@ -70,114 +71,60 @@ actual class NativeEngine { entity_id = entityId.id, out_transform = outTransform.ptr ) - if (result == 0) { - return Transform( - position = com.dropbear.math.Vector3D( - outTransform.position_x, - outTransform.position_y, - outTransform.position_z - ), - rotation = com.dropbear.math.QuaternionD( - outTransform.rotation_x, - outTransform.rotation_y, - outTransform.rotation_z, - outTransform.rotation_w - ), - scale = com.dropbear.math.Vector3D( - outTransform.scale_x, - outTransform.scale_y, - outTransform.scale_z - ) - ) - } else { - return null - } - } - } - - actual fun getLocalTransform(entityId: EntityId): Transform? { - val world = worldHandle ?: return null - memScoped { - val outTransform = alloc<NativeTransform>() - val result = dropbear_get_transform( - world_ptr = world.reinterpret(), - entity_id = entityId.id, - out_transform = outTransform.ptr - ) - if (result == 0) { - return Transform( - position = com.dropbear.math.Vector3D( - outTransform.position_x, - outTransform.position_y, - outTransform.position_z - ), - rotation = com.dropbear.math.QuaternionD( - outTransform.rotation_x, - outTransform.rotation_y, - outTransform.rotation_z, - outTransform.rotation_w - ), - scale = com.dropbear.math.Vector3D( - outTransform.scale_x, - outTransform.scale_y, - outTransform.scale_z - ) - ) - } else { - return null - } - } - } - - actual fun commitWorldTransform(entityId: EntityId, transform: Transform) { - val world = worldHandle ?: return - memScoped { - val nativeTransform = cValue<NativeTransform> { - position_x = transform.position.x - position_y = transform.position.y - position_z = transform.position.z - - rotation_w = transform.rotation.w - rotation_x = transform.rotation.x - rotation_y = transform.rotation.y - rotation_z = transform.rotation.z - - scale_x = transform.scale.x - scale_y = transform.scale.y - scale_z = transform.scale.z - } - - dropbear_set_transform( - world_ptr = world.reinterpret(), - entity_id = entityId.id, - transform = nativeTransform - ) - } +// if (result == 0) { +// return Transform( +// position = com.dropbear.math.Vector3D( +// outTransform.position_x, +// outTransform.position_y, +// outTransform.position_z +// ), +// rotation = com.dropbear.math.QuaternionD( +// outTransform.rotation_x, +// outTransform.rotation_y, +// outTransform.rotation_z, +// outTransform.rotation_w +// ), +// scale = com.dropbear.math.Vector3D( +// outTransform.scale_x, +// outTransform.scale_y, +// outTransform.scale_z +// ) +// ) +// } else { +// return null +// } + TODO("Function com.dropbear.ffi.NativeEngine.native.getTransform() not completed yet, will return NULL") + } + } + + actual fun propagateTransform(entityId: EntityId) { + TODO("Not yet implemented") } - actual fun commitLocalTransform(entityId: EntityId, transform: Transform) { + actual fun setTransform(entityId: EntityId, transform: EntityTransform) { val world = worldHandle ?: return memScoped { - val nativeTransform = cValue<NativeTransform> { - position_x = transform.position.x - position_y = transform.position.y - position_z = transform.position.z - - rotation_w = transform.rotation.w - rotation_x = transform.rotation.x - rotation_y = transform.rotation.y - rotation_z = transform.rotation.z - - scale_x = transform.scale.x - scale_y = transform.scale.y - scale_z = transform.scale.z - } - - dropbear_set_transform( - world_ptr = world.reinterpret(), - entity_id = entityId.id, - transform = nativeTransform - ) +// val nativeTransform = cValue<NativeTransform> { +// position_x = transform.position.x +// position_y = transform.position.y +// position_z = transform.position.z +// +// rotation_w = transform.rotation.w +// rotation_x = transform.rotation.x +// rotation_y = transform.rotation.y +// rotation_z = transform.rotation.z +// +// scale_x = transform.scale.x +// scale_y = transform.scale.y +// scale_z = transform.scale.z +// } +// +// dropbear_set_transform( +// world_ptr = world.reinterpret(), +// entity_id = entityId.id, +// transform = nativeTransform +// ) + TODO("Function com.dropbear.ffi.NativeEngine.native.setTransform() not completed yet, will not do any action") } }