tirbofish/dropbear · commit
c2a063c300e280200f96a7075af98a77334d774b
feature: improved copy and pasting.
feature: improved onrails
perf: changed backend of hitbox wireframe to use DebugDraw instead.
bug: too much GPU usage and redback-runtime is not putting the entities correctly (likely because of the physics).
Signature present but could not be verified.
Unverified
@@ -5,10 +5,10 @@ use glam::{Mat4, Quat, Vec3, Vec4}; use std::sync::Arc; use wgpu::{ BindGroupDescriptor, BindGroupEntry, BindGroupLayoutDescriptor, BindGroupLayoutEntry, - BindingResource, BindingType, BufferBindingType, BufferUsages, CompareFunction, - DepthStencilState, LoadOp, MultisampleState, Operations, PrimitiveState, PrimitiveTopology, - RenderPassColorAttachment, RenderPassDepthStencilAttachment, RenderPassDescriptor, - RenderPipeline, RenderPipelineDescriptor, ShaderStages, StoreOp, TextureFormat, + BindingResource, BindingType, BufferBindingType, BufferUsages, + LoadOp, MultisampleState, Operations, PrimitiveState, PrimitiveTopology, + RenderPassColorAttachment, RenderPassDescriptor, + RenderPipeline, RenderPipelineDescriptor, ShaderStages, StoreOp, VertexBufferLayout, VertexState, }; @@ -510,13 +510,7 @@ impl DebugDrawPipeline { polygon_mode: Default::default(), conservative: false, }, - depth_stencil: Some(DepthStencilState { - format: TextureFormat::Depth32Float, - depth_write_enabled: Some(false), // don't write to depth, just read - depth_compare: Some(CompareFunction::Less), - stencil: Default::default(), - bias: Default::default(), - }), + depth_stencil: None, // do not use depth_stentil multisample: MultisampleState { count: sample_count, mask: !0, @@ -561,14 +555,7 @@ impl DebugDrawPipeline { store: StoreOp::Store, }, })], - depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { - view: &graphics.depth_texture.view, - depth_ops: Some(Operations { - load: LoadOp::Load, // read existing depth - store: StoreOp::Store, - }), - stencil_ops: None, - }), + depth_stencil_attachment: None, timestamp_writes: None, occlusion_query_set: None, multiview_mask: None, @@ -1,5 +1,4 @@ use crate::buffer::StorageBuffer; -use crate::entity::{EntityTransform, Transform}; use crate::graphics::SharedGraphicsContext; use crate::lighting::{Light, LightArrayUniform, MAX_LIGHTS}; use crate::model::{ModelVertex, Vertex}; @@ -131,20 +130,11 @@ impl LightCubePipeline { let mut light_index: usize = 0; - for (s_trans, e_trans, light) in world - .query::<(Option<&Transform>, Option<&EntityTransform>, &mut Light)>() - .iter() - { + for light in world.query::<&mut Light>().iter() { light.update(graphics.as_ref()); - let instance: InstanceInput = if let Some(transform) = e_trans { - let sync_transform = transform.sync(); - sync_transform.matrix().into() - } else if let Some(transform) = s_trans { - transform.matrix().into() - } else { - panic!("No Transform or EntityTransform available for this light cube"); - }; + + let instance: InstanceInput = light.component.to_transform().matrix().into(); light .instance_buffer @@ -10,6 +10,7 @@ use crate::states::SerializedLight; use crate::types::{NColour, NVector3}; use dropbear_engine::attenuation::ATTENUATION_PRESETS; use dropbear_engine::entity::{EntityTransform, Transform, inspect_rotation_dquat}; +use crate::hierarchy::EntityTransformExt; use dropbear_engine::graphics::SharedGraphicsContext; use dropbear_engine::lighting::{Light, LightType}; use egui::{CollapsingHeader, ComboBox, DragValue, Ui}; @@ -67,7 +68,7 @@ impl Component for Light { ) { let synced = &mut self.component; if let Ok(entity_transform) = world.query_one::<&EntityTransform>(entity).get() { - let transform = entity_transform.sync(); + let transform = entity_transform.propagate(world, entity); synced.position = transform.position; synced.direction = (transform.rotation * LIGHT_FORWARD_AXIS).normalize_or_zero(); } else if let Ok(transform) = world.query_one::<&Transform>(entity).get() { @@ -266,6 +266,18 @@ impl JavaContext { let lib_path_arg = format!("-Djava.library.path={}", combined_path); args_log.push(lib_path_arg.clone()); jvm_args = jvm_args.option(lib_path_arg); + + let lib_name = if cfg!(target_os = "windows") { + "eucalyptus_core.dll" + } else if cfg!(target_os = "macos") { + "libeucalyptus_core.dylib" + } else { + "libeucalyptus_core.so" + }; + let core_lib_path = path.join(lib_name); + let core_lib_arg = format!("-Deucalyptus.core.lib={}", core_lib_path.display()); + args_log.push(core_lib_arg.clone()); + jvm_args = jvm_args.option(core_lib_arg); } }; @@ -1,3 +1,4 @@ +use crate::camera::{CameraComponent, CameraType}; use crate::component::{ Component, ComponentDescriptor, ComponentInitFuture, DisabilityFlags, InspectableComponent, SerializedComponent, @@ -8,11 +9,11 @@ use crate::ptr::WorldPtr; use crate::scripting::jni::utils::{FromJObject, ToJObject}; use crate::scripting::native::DropbearNativeError; use crate::scripting::result::DropbearNativeResult; -use crate::types::{NTransform, NVector3}; +use crate::types::{NQuaternion, NTransform, NVector3}; use ::jni::objects::{JObject, JValue}; use ::jni::{Env, jni_sig, jni_str}; use dropbear_engine::camera::Camera; -use dropbear_engine::entity::{EntityTransform, Transform}; +use dropbear_engine::entity::{EntityTransform, Transform, inspect_rotation_dquat}; use dropbear_engine::graphics::SharedGraphicsContext; use egui::{CollapsingHeader, ComboBox, Ui}; use glam::{DMat3, DQuat, DVec3, Vec3}; @@ -21,23 +22,45 @@ use splines::{Interpolation, Key, Spline}; use std::fmt::{Display, Formatter}; use std::sync::Arc; +/// A single point on an [`OnRails`] path, capturing both position and an optional explicit +/// rotation. When `rotation` is `None`, the sampler derives orientation from the path tangent. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct RailPoint { + pub position: DVec3, + /// Explicit look-at rotation captured from the camera. `None` means tangent-derived. + pub rotation: Option<DQuat>, +} + +impl Default for RailPoint { + fn default() -> Self { + Self { + position: DVec3::ZERO, + rotation: None, + } + } +} + /// Allows for the entity to have constricted movement, such as a Camera that follows a player /// on a set path. #[derive(Default, Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct OnRails { /// Whether the component actively moves the entity along the path each frame. pub enabled: bool, - pub path: Vec<DVec3>, + pub path: Vec<RailPoint>, /// Progress of the entity on the spline between 0.0 to 1.0. pub progress: f32, // between 0.0 to 1.0 pub drive: RailDrive, /// Allows for showing the debug path of the rail. #[serde(skip)] pub debug: bool, + /// Stores the desired position and rotation to be applied to [`EntityTransform`] after the + /// component update loop, avoiding a double-borrow of the same archetype in hecs. + #[serde(skip)] + pub pending_transform: Option<(DVec3, DQuat)>, } /// Returns the progress `t` (0.0–1.0) of the path point closest to `pos`. -fn project_to_path(path: &[DVec3], pos: DVec3) -> f32 { +fn project_to_path(path: &[RailPoint], pos: DVec3) -> f32 { let n = path.len(); if n < 2 { return 0.0; @@ -45,8 +68,8 @@ fn project_to_path(path: &[DVec3], pos: DVec3) -> f32 { let mut best_t = 0.0f32; let mut best_dist_sq = f64::MAX; for i in 0..(n - 1) { - let a = path[i]; - let b = path[i + 1]; + let a = path[i].position; + let b = path[i + 1].position; let t_a = i as f32 / (n - 1) as f32; let t_b = (i + 1) as f32 / (n - 1) as f32; let ab = b - a; @@ -66,14 +89,14 @@ fn project_to_path(path: &[DVec3], pos: DVec3) -> f32 { best_t } -fn path_tangent_rotation(path: &[DVec3], t: f32) -> DQuat { +fn path_tangent_rotation(path: &[RailPoint], t: f32) -> DQuat { let n = path.len(); if n < 2 { return DQuat::IDENTITY; } let t_clamped = (t as f64 * (n - 1) as f64).clamp(0.0, (n - 2) as f64); let i = t_clamped as usize; - let forward = (path[i + 1] - path[i]).normalize_or_zero(); + let forward = (path[i + 1].position - path[i].position).normalize_or_zero(); if forward.length_squared() < 1e-10 { return DQuat::IDENTITY; } @@ -87,19 +110,66 @@ fn path_tangent_rotation(path: &[DVec3], t: f32) -> DQuat { DQuat::from_mat3(&DMat3::from_cols(right, up, -forward)) } -fn sample_path_linear(path: &[DVec3], t: f32) -> DVec3 { + +fn sample_path_rotation(path: &[RailPoint], t: f32) -> DQuat { + let n = path.len(); + if n < 2 { + return DQuat::IDENTITY; + } + let t_clamped = t.clamp(0.0, 1.0); + let seg_f = t_clamped as f64 * (n - 1) as f64; + let i = (seg_f as usize).min(n - 2); + let frac = (seg_f - i as f64) as f32; + + match (path[i].rotation, path[i + 1].rotation) { + (Some(a), Some(b)) => a.slerp(b, frac as f64), + (Some(a), None) => a, + (None, Some(b)) => b, + (None, None) => path_tangent_rotation(path, t), + } +} + +/// Build a look-rotation quaternion (same -Z-forward convention as [`path_tangent_rotation`]) +/// from an arbitrary forward direction vector. +fn look_rot_from_forward(forward: DVec3) -> DQuat { + let forward = forward.normalize_or_zero(); + if forward.length_squared() < 1e-10 { + return DQuat::IDENTITY; + } + let world_up = if forward.dot(DVec3::Y).abs() > 0.99 { + DVec3::Z + } else { + DVec3::Y + }; + let right = world_up.cross(forward).normalize(); + let up = forward.cross(right).normalize(); + DQuat::from_mat3(&DMat3::from_cols(right, up, -forward)) +} + +/// Apply a sampled OnRails rotation to a [`Camera`], keeping `yaw`/`pitch` in sync so +/// user mouse-drag doesn't snap after the rails set the orientation. +fn apply_rot_to_camera(camera: &mut Camera, new_eye: DVec3, rot: DQuat) { + let forward = rot * DVec3::NEG_Z; + camera.eye = new_eye; + camera.target = new_eye + forward; + let dir = forward.normalize(); + camera.pitch = dir.y.clamp(-1.0, 1.0).asin(); + camera.yaw = dir.z.atan2(dir.x); +} + +fn sample_path_linear(path: &[RailPoint], t: f32) -> DVec3 { let n = path.len(); if n == 0 { return DVec3::ZERO; } if n == 1 { - return path[0]; + return path[0].position; } let t = t.clamp(0.0, 1.0) as f64; let seg_f = t * (n - 1) as f64; let i = (seg_f as usize).min(n - 2); let frac = seg_f - i as f64; - path[i].lerp(path[i + 1], frac) + path[i].position.lerp(path[i + 1].position, frac) } /// How the progression of the [`OnRails`] component is handled. @@ -186,6 +256,16 @@ impl Component for OnRails { dt: f32, _graphics: Arc<SharedGraphicsContext>, ) { + // this might be bad practice, idk + for (rails, et) in + world.query::<(&mut OnRails, &mut EntityTransform)>().iter() + { + if let Some((pos, rot)) = rails.pending_transform.take() { + et.world_mut().position = pos; + et.world_mut().rotation = rot; + } + } + let n = self.path.len(); if n < 2 { return; @@ -197,7 +277,7 @@ impl Component for OnRails { .iter() .enumerate() .map(|(i, p)| { - Key::new(i as f64 / (n - 1) as f64, get(p), Interpolation::Linear) + Key::new(i as f64 / (n - 1) as f64, get(&p.position), Interpolation::Linear) }) .collect(), ) @@ -227,8 +307,8 @@ impl Component for OnRails { let mut best_t = self.progress; let mut best_dist_sq = f64::MAX; for i in 0..(n - 1) { - let a = self.path[i]; - let b = self.path[i + 1]; + let a = self.path[i].position; + let b = self.path[i + 1].position; let t_a = i as f32 / (n - 1) as f32; let t_b = (i + 1) as f32 / (n - 1) as f32; let ab = b - a; @@ -276,15 +356,11 @@ impl Component for OnRails { sz.clamped_sample(t), ) { let new_pos = DVec3::new(x, y, z); - let rot = path_tangent_rotation(&self.path, self.progress); - if let Ok(mut et) = world.get::<&mut EntityTransform>(entity) { - et.world_mut().position = new_pos; - et.world_mut().rotation = rot; - } + let rot = sample_path_rotation(&self.path, self.progress); + + self.pending_transform = Some((new_pos, rot)); if let Ok(mut camera) = world.get::<&mut Camera>(entity) { - camera.eye = new_pos; - let forward = rot * DVec3::NEG_Z; - camera.target = new_pos + forward; + apply_rot_to_camera(&mut camera, new_pos, rot); } } } @@ -314,15 +390,13 @@ impl InspectableComponent for OnRails { if pos.distance_squared(expected) > 1e-8 { self.progress = project_to_path(&self.path, pos); let snapped = sample_path_linear(&self.path, self.progress); - let rot = path_tangent_rotation(&self.path, self.progress); + let rot = sample_path_rotation(&self.path, self.progress); if let Ok(mut et) = world.get::<&mut EntityTransform>(entity) { et.world_mut().position = snapped; et.world_mut().rotation = rot; } if let Ok(mut camera) = world.get::<&mut Camera>(entity) { - camera.eye = snapped; - let forward = rot * DVec3::NEG_Z; - camera.target = snapped + forward; + apply_rot_to_camera(&mut camera, snapped, rot); } } } @@ -343,15 +417,13 @@ impl InspectableComponent for OnRails { ); if slider.changed() && n >= 2 { let pos = sample_path_linear(&self.path, self.progress); - let rot = path_tangent_rotation(&self.path, self.progress); + let rot = sample_path_rotation(&self.path, self.progress); if let Ok(mut et) = world.get::<&mut EntityTransform>(entity) { et.world_mut().position = pos; et.world_mut().rotation = rot; } if let Ok(mut camera) = world.get::<&mut Camera>(entity) { - camera.eye = pos; - let forward = rot * DVec3::NEG_Z; - camera.target = pos + forward; + apply_rot_to_camera(&mut camera, pos, rot); } } @@ -438,15 +510,39 @@ impl InspectableComponent for OnRails { .default_open(false) .id_salt(format!("OnRails Path {}", entity.to_bits())) .show(ui, |ui| { + ui.style_mut().interaction.selectable_labels = false; let mut remove_idx: Option<usize> = None; for (i, point) in self.path.iter_mut().enumerate() { - ui.horizontal(|ui| { - ui.label(format!("[{}]", i)); - ui.add(egui::DragValue::new(&mut point.x).speed(0.1).prefix("X:")); - ui.add(egui::DragValue::new(&mut point.y).speed(0.1).prefix("Y:")); - ui.add(egui::DragValue::new(&mut point.z).speed(0.1).prefix("Z:")); - if ui.small_button("-").clicked() { - remove_idx = Some(i); + ui.push_id(i, |ui| { + ui.horizontal(|ui| { + ui.label(format!("[{}]", i)); + ui.add(egui::DragValue::new(&mut point.position.x).speed(0.1).prefix("X:")); + ui.add(egui::DragValue::new(&mut point.position.y).speed(0.1).prefix("Y:")); + ui.add(egui::DragValue::new(&mut point.position.z).speed(0.1).prefix("Z:")); + let has_rot = point.rotation.is_some(); + if ui + .small_button(if has_rot { "R\u{2713}" } else { "R" }) + .on_hover_text(if has_rot { + "Explicit rotation set \u{2014} click to clear (use tangent)" + } else { + "No explicit rotation (tangent-derived) \u{2014} click to pin" + }) + .clicked() + { + point.rotation = if has_rot { None } else { Some(DQuat::IDENTITY) }; + } + if ui.small_button("-").clicked() { + remove_idx = Some(i); + } + }); + if let Some(ref mut rot) = point.rotation { + ui.indent(format!("rail_rot_{}", i), |ui| { + inspect_rotation_dquat( + ui, + ("rail_point_rot", entity.to_bits(), i), + rot, + ); + }); } }); } @@ -454,8 +550,33 @@ impl InspectableComponent for OnRails { self.path.remove(idx); } if ui.button("+ Add Point").clicked() { - let last = self.path.last().copied().unwrap_or(DVec3::ZERO); - self.path.push(last + DVec3::new(1.0, 0.0, 0.0)); + let last = self.path.last().map(|p| p.position).unwrap_or(DVec3::ZERO); + self.path.push(RailPoint { + position: last + DVec3::new(1.0, 0.0, 0.0), + rotation: None, + }); + } + if ui.button("+ Add Point from Camera").clicked() { + let cam_data = world + .query::<(&Camera, &CameraComponent)>() + .iter() + .find_map(|(cam, comp)| { + matches!(comp.camera_type, CameraType::Debug) + .then_some((cam.eye, (cam.target - cam.eye).normalize_or_zero())) + }) + .or_else(|| { + world + .query::<&Camera>() + .iter() + .next() + .map(|cam| (cam.eye, (cam.target - cam.eye).normalize_or_zero())) + }); + if let Some((pos, fwd)) = cam_data { + self.path.push(RailPoint { + position: pos, + rotation: Some(look_rot_from_forward(fwd)), + }); + } } }); @@ -465,10 +586,10 @@ impl InspectableComponent for OnRails { if self.debug && self.path.len() >= 2 { if let Some(dd) = graphics.debug_draw.lock().as_mut() { let red = [1.0, 0.0, 0.0, 1.0]; - dd.draw_point(self.path[0].as_vec3(), 0.1, red); + dd.draw_point(self.path[0].position.as_vec3(), 0.1, red); for i in 0..(self.path.len() - 1) { - let a = self.path[i].as_vec3(); - let b = self.path[i + 1].as_vec3(); + let a = self.path[i].position.as_vec3(); + let b = self.path[i + 1].position.as_vec3(); dd.draw_line(a, b, red); dd.draw_point(b, 0.1, red); } @@ -936,7 +1057,7 @@ fn on_rails_get_path_point( .path .get(index as usize) .ok_or(DropbearNativeError::InvalidArgument)?; - Ok(NVector3::from(point)) + Ok(NVector3::from(&point.position)) } #[dropbear_macro::export( @@ -972,7 +1093,74 @@ fn on_rails_push_path_point( let mut rails = world .get::<&mut OnRails>(entity) .map_err(|_| DropbearNativeError::MissingComponent)?; - rails.path.push(DVec3::from(point)); + rails.path.push(RailPoint { position: DVec3::from(point), rotation: None }); + Ok(()) +} + +#[dropbear_macro::export( + kotlin( + class = "com.dropbear.components.camera.OnRailsNative", + func = "getPathPointHasRotation" + ), + c +)] +fn on_rails_get_path_point_has_rotation( + #[dropbear_macro::define(WorldPtr)] world: &hecs::World, + #[dropbear_macro::entity] entity: hecs::Entity, + index: i32, +) -> DropbearNativeResult<bool> { + let rails = world + .get::<&OnRails>(entity) + .map_err(|_| DropbearNativeError::MissingComponent)?; + let point = rails + .path + .get(index as usize) + .ok_or(DropbearNativeError::InvalidArgument)?; + Ok(point.rotation.is_some()) +} + +#[dropbear_macro::export( + kotlin( + class = "com.dropbear.components.camera.OnRailsNative", + func = "getPathPointRotation" + ), + c +)] +fn on_rails_get_path_point_rotation( + #[dropbear_macro::define(WorldPtr)] world: &hecs::World, + #[dropbear_macro::entity] entity: hecs::Entity, + index: i32, +) -> DropbearNativeResult<NQuaternion> { + let rails = world + .get::<&OnRails>(entity) + .map_err(|_| DropbearNativeError::MissingComponent)?; + let point = rails + .path + .get(index as usize) + .ok_or(DropbearNativeError::InvalidArgument)?; + Ok(NQuaternion::from(point.rotation.unwrap_or(DQuat::IDENTITY))) +} + +#[dropbear_macro::export( + kotlin( + class = "com.dropbear.components.camera.OnRailsNative", + func = "pushPathPointWithRotation" + ), + c +)] +fn on_rails_push_path_point_with_rotation( + #[dropbear_macro::define(WorldPtr)] world: &hecs::World, + #[dropbear_macro::entity] entity: hecs::Entity, + point: &NVector3, + rotation: &NQuaternion, +) -> DropbearNativeResult<()> { + let mut rails = world + .get::<&mut OnRails>(entity) + .map_err(|_| DropbearNativeError::MissingComponent)?; + rails.path.push(RailPoint { + position: DVec3::from(point), + rotation: Some(DQuat::from(*rotation)), + }); Ok(()) } @@ -37,6 +37,7 @@ pub struct EditorTabViewer<'a> { pub gizmo: &'a mut Gizmo, pub world: &'a mut World, pub selected_entity: &'a mut Option<Entity>, + pub selected_entities: &'a mut Vec<Entity>, pub viewport_mode: &'a mut ViewportMode, pub undo_stack: &'a mut Vec<UndoableAction>, pub signal: &'a mut VecDeque<Signal>, @@ -1366,6 +1366,7 @@ impl<'a> EditorTabViewer<'a> { } pub(crate) fn handle_tree_selection(&mut self, cfg: &mut StaticallyKept, items: &[u64]) { + self.selected_entities.clear(); for node_id in items { self.resolve_tree_node(cfg, *node_id); } @@ -1445,6 +1446,7 @@ impl<'a> EditorTabViewer<'a> { } else if let Some(entity) = Self::entity_from_node_id(node_id) { cfg.root_node_selected = false; *self.selected_entity = Some(entity); + self.selected_entities.push(entity); } } @@ -32,21 +32,29 @@ impl<'a> EditorTabViewer<'a> { .clone() .unwrap_or("Scene".to_string()) }; - builder.node( - // root node/scene display - NodeBuilder::dir(u64::MAX) - .label(format!("Scene: {}", current_scene_name)) - .context_menu(|ui| { - if ui.button("New Empty Entity").clicked() { - let label = Editor::unique_label_for_world(self.world, "Blank Entity"); - self.world.spawn((label,)); - ui.close(); - } - ui.menu_button("Import Template", |_| { - - }); - }), - ); + builder.node( + // root node/scene display + NodeBuilder::dir(u64::MAX) + .label(format!("Scene: {}", current_scene_name)) + .context_menu(|ui| { + if ui.button("New Empty Entity").clicked() { + let label = Editor::unique_label_for_world(self.world, "Blank Entity"); + self.world.spawn((label,)); + ui.close(); + } + ui.menu_button("Import Template", |_| {}); + ui.separator(); + if ui.button("Paste to Root").clicked() { + let copied = self.signal.iter().find_map(|s| { + if let Signal::Copy(e, pm) = s { Some((e.clone(), pm.clone())) } else { None } + }); + if let Some((entities, parent_map)) = copied { + self.signal.push_back(Signal::Paste(entities, parent_map, None)); + } + ui.close(); + } + }), + ); // the root scene must be the biggest number possible to remove any ambiguity fn add_entity_to_tree( @@ -125,6 +133,25 @@ impl<'a> EditorTabViewer<'a> { if ui.button("Create Template").clicked() { eucalyptus_core::fatal!("Not implemented yet"); } + ui.separator(); + if ui.button("Copy").clicked() { + let (sub_e, sub_pm) = Editor::collect_entity_subtree(world, entity, registry); + if !sub_e.is_empty() { + signal.retain(|s| !matches!(s, Signal::Copy(_, _))); + signal.push_back(Signal::Copy(sub_e, sub_pm)); + } + ui.close(); + } + if ui.button("Paste as Child").clicked() { + let copied = signal.iter().find_map(|s| { + if let Signal::Copy(e, pm) = s { Some((e.clone(), pm.clone())) } else { None } + }); + if let Some((entities, parent_map)) = copied { + let paste_parent = world.get::<&Label>(entity).ok().map(|l| (*l).clone()); + signal.push_back(Signal::Paste(entities, parent_map, paste_parent)); + } + ui.close(); + } }), ); @@ -282,7 +282,7 @@ impl<'a> EditorTabViewer<'a> { let mut handled = false; let mut updated_light_transform: Option<Transform> = None; - if let Ok(mut entity_transform) = self.world.get::<&mut EntityTransform>(*entity_id) { + let entity_transform = if let Ok(entity_transform) = self.world.get::<&mut EntityTransform>(*entity_id) { let was_focused = cfg.is_focused; cfg.is_focused = self.gizmo.is_focused(); @@ -290,13 +290,22 @@ impl<'a> EditorTabViewer<'a> { cfg.entity_transform_original = Some(*entity_transform); } - let synced = entity_transform.propagate(&self.world, *entity_id); - let gizmo_transform = - transform_gizmo_egui::math::Transform::from_scale_rotation_translation( - synced.scale, - synced.rotation, - synced.position, - ); + entity_transform.clone() + } else { + EntityTransform::default() + }; + + let synced = entity_transform.propagate(&self.world, *entity_id); + let gizmo_transform = + transform_gizmo_egui::math::Transform::from_scale_rotation_translation( + synced.scale, + synced.rotation, + synced.position, + ); + + if let Ok(mut entity_transform) = self.world.get::<&mut EntityTransform>(*entity_id) { + let was_focused = cfg.is_focused; + cfg.is_focused = self.gizmo.is_focused(); if let Some((_result, new_transforms)) = self.gizmo.interact(ui, &[gizmo_transform]) && let Some(new_transform) = new_transforms.first() @@ -363,6 +372,7 @@ impl<'a> EditorTabViewer<'a> { handled = true; } + if !handled { if let Ok(transform) = self.world.query_one::<&mut Transform>(*entity_id).get() { let was_focused = cfg.is_focused; @@ -171,23 +171,48 @@ impl Keyboard for Editor { .id_for_title("Model/Entity List") .map_or(false, |id| *tab == id) { - if let Some(entity) = &self.selected_entity { - let entity = *entity; - let (entities, parent_map) = Editor::collect_entity_subtree( - self.world.as_ref(), - entity, - &self.component_registry, - ); - if entities.is_empty() { + use eucalyptus_core::hierarchy::Parent; + let entities_to_copy: Vec<Entity> = if !self.selected_entities.is_empty() { + self.selected_entities.clone() + } else if let Some(e) = self.selected_entity { + vec![e] + } else { + vec![] + }; + if entities_to_copy.is_empty() { + warn!("Unable to copy entity: None selected"); + } else { + let sel_bits: HashSet<u64> = entities_to_copy.iter() + .map(|e| e.to_bits().get()).collect(); + let mut all_entities = Vec::new(); + let mut all_parent_map = HashMap::new(); + let mut seen_labels: HashSet<String> = HashSet::new(); + for entity in &entities_to_copy { + let mut skip = false; + let mut cur = *entity; + while let Ok(p) = self.world.get::<&Parent>(cur) { + let pe = p.parent(); + if sel_bits.contains(&pe.to_bits().get()) { skip = true; break; } + cur = pe; + } + if skip { continue; } + let (sub_e, sub_pm) = Editor::collect_entity_subtree( + self.world.as_ref(), *entity, &self.component_registry); + for se in sub_e { + if seen_labels.insert(se.label.to_string()) { + all_entities.push(se); + } + } + all_parent_map.extend(sub_pm); + } + if all_entities.is_empty() { warn!("Unable to copy entity: Unable to obtain label"); } else { self.signal.retain(|s| !matches!(s, Signal::Copy(_, _))); - self.signal.push_back(Signal::Copy(entities, parent_map)); - info!("Copied!"); - log::debug!("Copied selected entity"); + self.signal.push_back(Signal::Copy(all_entities, all_parent_map)); + info!("Copied {} entity(ies)!", entities_to_copy.len()); + log::debug!("Copied selected entities"); } - } else { - warn!("Unable to copy entity: None selected"); } } } else if matches!(self.viewport_mode, ViewportMode::Gizmo) { @@ -204,7 +229,9 @@ impl Keyboard for Editor { { let entities = entities.clone(); let parent_map = parent_map.clone(); - self.signal.push_back(Signal::Paste(entities, parent_map)); + let paste_parent = self.selected_entity + .and_then(|e| self.world.get::<&Label>(e).ok().map(|l| (*l).clone())); + self.signal.push_back(Signal::Paste(entities, parent_map, paste_parent)); } } else { self.input_state.pressed_keys.insert(key); @@ -38,11 +38,8 @@ use dropbear_engine::{ use egui::{self, Ui}; use egui_dock::{DockArea, DockState, NodeIndex, Style}; use eucalyptus_core::component::{ComponentRegistry, SerializedComponent}; -use eucalyptus_core::hierarchy::{Children, SceneHierarchy}; +use eucalyptus_core::hierarchy::{Children, Parent, SceneHierarchy}; use eucalyptus_core::physics::PhysicsState; -use eucalyptus_core::physics::collider::shader::ColliderInstanceRaw; -use eucalyptus_core::physics::collider::shader::ColliderWireframePipeline; -use eucalyptus_core::physics::collider::{ColliderShapeKey, WireframeGeometry}; use eucalyptus_core::scene::{SceneConfig, SceneEntity}; use eucalyptus_core::states::Label; use eucalyptus_core::{APP_INFO, register_components}; @@ -88,8 +85,6 @@ pub struct Editor { pub size: Extent3d, pub instance_buffer_cache: HashMap<u64, ResizableBuffer<InstanceRaw>>, pub animated_instance_buffers: HashMap<Entity, ResizableBuffer<InstanceRaw>>, - pub collider_wireframe_geometry_cache: HashMap<ColliderShapeKey, WireframeGeometry>, - pub collider_instance_buffer: Option<ResizableBuffer<ColliderInstanceRaw>>, pub color: Color, pub ui_editor: UiEditor, @@ -100,7 +95,6 @@ pub struct Editor { pub light_cube_pipeline: Option<LightCubePipeline>, pub main_render_pipeline: Option<MainRenderPipeline>, pub shader_globals: Option<GlobalsUniform>, - pub collider_wireframe_pipeline: Option<ColliderWireframePipeline>, pub mipmapper: Option<MipMapper>, pub sky_pipeline: Option<SkyPipeline>, pub billboard_pipeline: Option<BillboardPipeline>, @@ -125,6 +119,8 @@ pub struct Editor { pub active_camera: Arc<Mutex<Option<Entity>>>, + pub selected_entities: Vec<Entity>, + pub is_viewport_focused: bool, // is_cursor_locked: bool, pub window: Option<Arc<Window>>, @@ -208,6 +204,8 @@ pub struct Editor { pub(crate) asset_clipboard: Option<AssetClipboard>, pub(crate) pending_aa_reload: Option<AntiAliasingMode>, + + pub(crate) last_active_camera_for_per_frame: Option<hecs::Entity>, } impl Editor { @@ -295,6 +293,7 @@ impl Editor { input_state: Box::new(InputState::new()), light_cube_pipeline: None, active_camera: Arc::new(Mutex::new(None)), + selected_entities: Vec::new(), progress_tx: None, is_world_loaded: IsWorldLoadedYet::new(), current_state: WorldLoadingStatus::Idle, @@ -326,11 +325,8 @@ impl Editor { play_mode_exit_rx: None, asset_clipboard: None, pending_aa_reload: None, - collider_wireframe_pipeline: None, instance_buffer_cache: HashMap::new(), animated_instance_buffers: HashMap::new(), - collider_wireframe_geometry_cache: HashMap::new(), - collider_instance_buffer: None, mipmapper: None, sky_pipeline: None, billboard_pipeline: None, @@ -348,10 +344,19 @@ impl Editor { current_page: EditorTabVisibility::GameEditor, ui_editor: UiEditor::new(), last_morph_info_per_mesh: Default::default(), + last_active_camera_for_per_frame: None, }) } pub(crate) fn unique_label_for_world(world: &World, base: &str) -> Label { + Self::unique_label_for_world_with_extra(world, base, &HashSet::new()) + } + + pub(crate) fn unique_label_for_world_with_extra( + world: &World, + base: &str, + extra_taken: &HashSet<String>, + ) -> Label { fn parse_suffix(name: &str) -> Option<(&str, u32)> { let mut parts = name.rsplitn(2, '.'); let suffix = parts.next()?; @@ -368,6 +373,13 @@ impl Editor { for (_, label) in world.query::<(Entity, &Label)>().iter() { existing.insert(label.as_str().to_string()); } + existing.extend(extra_taken.iter().cloned()); + { + let pending = crate::spawn::PENDING_SPAWNS.lock(); + for spawn in pending.iter() { + existing.insert(spawn.scene_entity.label.as_str().to_string()); + } + } if !existing.contains(base) { return Label::new(base); @@ -1100,22 +1112,46 @@ impl Editor { }); ui.menu_button("Edit", |ui| { if ui.button("Copy").clicked() { - if let Some(entity) = &self.selected_entity { - let entity = *entity; - let (entities, parent_map) = Editor::collect_entity_subtree( - self.world.as_ref(), - entity, - &self.component_registry, - ); - if entities.is_empty() { + let entities_to_copy: Vec<Entity> = if !self.selected_entities.is_empty() { + self.selected_entities.clone() + } else if let Some(e) = self.selected_entity { + vec![e] + } else { + vec![] + }; + if entities_to_copy.is_empty() { + warn!("Unable to copy entity: None selected"); + } else { + let sel_bits: HashSet<u64> = entities_to_copy.iter() + .map(|e| e.to_bits().get()).collect(); + let mut all_entities = Vec::new(); + let mut all_parent_map = HashMap::new(); + let mut seen_labels: HashSet<String> = HashSet::new(); + for entity in &entities_to_copy { + let mut skip = false; + let mut cur = *entity; + while let Ok(p) = self.world.get::<&Parent>(cur) { + let pe = p.parent(); + if sel_bits.contains(&pe.to_bits().get()) { skip = true; break; } + cur = pe; + } + if skip { continue; } + let (sub_e, sub_pm) = Editor::collect_entity_subtree( + self.world.as_ref(), *entity, &self.component_registry); + for se in sub_e { + if seen_labels.insert(se.label.to_string()) { + all_entities.push(se); + } + } + all_parent_map.extend(sub_pm); + } + if all_entities.is_empty() { warn!("Unable to copy entity: Unable to obtain label"); } else { self.signal.retain(|s| !matches!(s, Signal::Copy(_, _))); - self.signal.push_back(Signal::Copy(entities, parent_map)); - info!("Copied selected entity!"); + self.signal.push_back(Signal::Copy(all_entities, all_parent_map)); + info!("Copied {} entity(ies)!", entities_to_copy.len()); } - } else { - warn!("Unable to copy entity: None selected"); } } @@ -1130,7 +1166,9 @@ impl Editor { match clipboard { Some((entities, parent_map)) => { - self.signal.push_back(Signal::Paste(entities, parent_map)); + let paste_parent = self.selected_entity + .and_then(|e| self.world.get::<&Label>(e).ok().map(|l| (*l).clone())); + self.signal.push_back(Signal::Paste(entities, parent_map, paste_parent)); } None => { warn!("Unable to paste: You haven't selected anything!"); @@ -1421,6 +1459,7 @@ impl Editor { tex_size: self.size, world: &mut self.world, selected_entity: &mut self.selected_entity, + selected_entities: &mut self.selected_entities, viewport_mode: &mut self.viewport_mode, undo_stack: &mut self.undo_stack, signal: &mut self.signal, @@ -1608,7 +1647,6 @@ impl Editor { graphics.clone(), Some("editor shader globals"), )); - self.collider_wireframe_pipeline = Some(ColliderWireframePipeline::new(graphics.clone())); self.mipmapper = None; self.billboard_pipeline = Some(BillboardPipeline::new(graphics.clone())); *graphics.debug_draw.lock() = @@ -1910,10 +1948,9 @@ impl UndoableAction { pub enum Signal { #[default] None, - /// Carries the entity subtree to be pasted: a flat list of entities (root first, DFS order) - /// and a map of child_label -> parent_label within the subtree. Copy(Vec<SceneEntity>, HashMap<Label, Label>), - Paste(Vec<SceneEntity>, HashMap<Label, Label>), + + Paste(Vec<SceneEntity>, HashMap<Label, Label>, Option<Label>), AssetCopy { source: PathBuf, division: AssetDivision, @@ -16,11 +16,11 @@ use egui::UiBuilder; use eucalyptus_core::billboard::BillboardComponent; use eucalyptus_core::component::KotlinComponentDecl; use eucalyptus_core::entity_status::EntityStatus; -use eucalyptus_core::physics::collider::ColliderGroup; -use eucalyptus_core::physics::collider::ColliderShapeKey; -use eucalyptus_core::physics::collider::shader::{ColliderInstanceRaw, create_wireframe_geometry}; +use eucalyptus_core::hierarchy::EntityTransformExt; +use eucalyptus_core::physics::collider::{ColliderGroup, ColliderShape}; use eucalyptus_core::properties::CustomProperties; use eucalyptus_core::states::{Label, SCENES, WorldLoadingStatus}; +use eucalyptus_core::transform::OnRails; use eucalyptus_core::ui::HUDComponent; use hecs::Entity; use kino_ui::rendering::KinoRenderTargetId; @@ -201,6 +201,17 @@ impl Scene for Editor { } } + { + for (rails, et) in + self.world.query_mut::<(&mut OnRails, &mut EntityTransform)>() + { + if let Some((pos, rot)) = rails.pending_transform.take() { + et.world_mut().position = pos; + et.world_mut().rotation = rot; + } + } + } + self.component_registry.update_components( self.world.as_mut(), &mut self.physics_state, @@ -535,7 +546,6 @@ impl Scene for Editor { self.static_batches.clear(); self.animated_instances.clear(); - { puffin::profile_scope!("finding all renderers and animation components"); let mut query = self @@ -725,6 +735,23 @@ impl Scene for Editor { } } + if self.last_active_camera_for_per_frame != Some(active_camera) { + self.last_active_camera_for_per_frame = Some(active_camera); + if let (Some(pipeline), Some(globals), Some(light_pipeline)) = ( + self.main_render_pipeline.as_mut(), + self.shader_globals.as_ref(), + self.light_cube_pipeline.as_ref(), + ) { + pipeline.per_frame = None; + pipeline.per_frame_bind_group( + graphics.clone(), + globals.buffer.buffer(), + camera.buffer(), + light_pipeline.light_buffer(), + ); + } + } + let sky = self .sky_pipeline .as_ref() @@ -1064,7 +1091,7 @@ impl Scene for Editor { render_pass.draw(0..3, 0..1); } - // collider pipeline + // collider debug draw { let show_hitboxes = self .current_scene_name @@ -1079,118 +1106,87 @@ impl Scene for Editor { .unwrap_or(false); if show_hitboxes { - puffin::profile_scope!("collider wireframe pipeline"); - if let Some(collider_pipeline) = &self.collider_wireframe_pipeline { - log_once::debug_once!("Found collider wireframe pipeline"); - let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("collider wireframe render pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: hdr.render_view(), - depth_slice: None, - resolve_target: hdr.resolve_target(), - 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, - multiview_mask: None, - }); - - render_pass.set_pipeline(&collider_pipeline.pipeline); - render_pass.set_bind_group(0, Some(&camera.bind_group), &[]); - - let mut instances_by_shape: HashMap< - ColliderShapeKey, - Vec<ColliderInstanceRaw>, - > = HashMap::new(); - - let mut q = self.world.query::<(&EntityTransform, &ColliderGroup)>(); - for (entity_transform, group) in q.iter() { - for collider in &group.colliders { - let world_tf = entity_transform.sync(); - + puffin::profile_scope!("collider debug draw"); + if let Some(debug_draw) = graphics.debug_draw.lock().as_mut() { + let colour = [0.0, 1.0, 0.0, 1.0]; + let to_draw: Vec<_> = { + let mut q = self.world.query::<(Entity, &ColliderGroup)>(); + q.iter().map(|(e, cg)| (e, cg.colliders.clone())).collect() + }; + for (entity, colliders) in to_draw { + let Ok(et) = self.world.get::<&EntityTransform>(entity) else { + continue; + }; + let world_tf = et.propagate(&self.world, entity); + drop(et); + for collider in &colliders { let entity_matrix = world_tf.matrix().as_mat4(); - let offset_transform = Transform::new() .with_offset(collider.translation, collider.rotation); let offset_matrix = offset_transform.matrix().as_mat4(); - let final_matrix = entity_matrix * offset_matrix; - - let color = [0.0, 1.0, 0.0, 1.0]; - let instance = ColliderInstanceRaw::from_matrix(final_matrix, color); - - let key = ColliderShapeKey::from(&collider.shape); - instances_by_shape.entry(key).or_default().push(instance); - - self.collider_wireframe_geometry_cache - .entry(key) - .or_insert_with(|| { - create_wireframe_geometry(graphics.clone(), &collider.shape) - }); - } - } - - if !instances_by_shape.is_empty() { - let total_instances: usize = - instances_by_shape.values().map(|v| v.len()).sum(); - let mut all_instances = Vec::with_capacity(total_instances); - let mut draws: Vec<(ColliderShapeKey, usize, usize)> = Vec::new(); - - for (key, instances) in instances_by_shape { - let start = all_instances.len(); - all_instances.extend_from_slice(&instances); - let count = instances.len(); - draws.push((key, start, count)); - } - - let instance_buffer = - self.collider_instance_buffer.get_or_insert_with(|| { - ResizableBuffer::new( - &graphics.device, - all_instances.len().max(10), - wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, - "Collider Instance Buffer", - ) - }); - instance_buffer.write(&graphics.device, &graphics.queue, &all_instances); - - for (key, start, count) in draws { - let Some(geometry) = self.collider_wireframe_geometry_cache.get(&key) - else { - continue; - }; - - let start_bytes = (start * std::mem::size_of::<ColliderInstanceRaw>()) - as wgpu::BufferAddress; - let end_bytes = ((start + count) - * std::mem::size_of::<ColliderInstanceRaw>()) - as wgpu::BufferAddress; - - render_pass.set_vertex_buffer( - 1, - instance_buffer.buffer().slice(start_bytes..end_bytes), - ); - render_pass.set_vertex_buffer(0, geometry.vertex_buffer.slice(..)); - render_pass.set_index_buffer( - geometry.index_buffer.slice(..), - wgpu::IndexFormat::Uint16, - ); - render_pass.draw_indexed(0..geometry.index_count, 0, 0..count as u32); + let (scale, rotation, translation) = + final_matrix.to_scale_rotation_translation(); + + match &collider.shape { + ColliderShape::Box { half_extents } => { + let he = glam::Vec3::new( + half_extents.x as f32 * scale.x, + half_extents.y as f32 * scale.y, + half_extents.z as f32 * scale.z, + ); + debug_draw.draw_obb(translation, he, rotation, colour); + } + ColliderShape::Sphere { radius } => { + let r = radius * scale.x.max(scale.y).max(scale.z); + debug_draw.draw_sphere(translation, r, colour); + } + ColliderShape::Capsule { + half_height, + radius, + } => { + let axis = rotation * glam::Vec3::Y; + let top = translation + axis * (half_height * scale.y); + let bottom = translation - axis * (half_height * scale.y); + debug_draw.draw_capsule( + bottom, + top, + radius * scale.x.max(scale.z), + colour, + ); + } + ColliderShape::Cylinder { + half_height, + radius, + } => { + let axis = rotation * glam::Vec3::Y; + debug_draw.draw_cylinder( + translation, + half_height * scale.y, + radius * scale.x.max(scale.z), + axis, + colour, + ); + } + ColliderShape::Cone { + half_height, + radius, + } => { + let axis = rotation * glam::Vec3::Y; + let apex = translation + axis * (half_height * scale.y); + let height = 2.0 * half_height * scale.y; + let r = radius * scale.x.max(scale.z); + debug_draw.draw_cone( + apex, + -(rotation * glam::Vec3::Y), + (r / height).atan(), + height, + colour, + ); + } + } } } - } else { - log_once::warn_once!("No collider pipeline found"); } } } @@ -97,23 +97,25 @@ impl SignalController for Editor { Ok(()) } - Signal::Paste(entities, parent_map) => { + Signal::Paste(entities, parent_map, paste_parent) => { log::debug!("Paste requested for {} entity(ies)", entities.len()); - // Rename all entities to ensure uniqueness, tracking old→new label mapping. + let mut label_remap: HashMap<Label, Label> = HashMap::new(); let mut renamed: Vec<SceneEntity> = Vec::new(); + let mut batch_taken: HashSet<String> = HashSet::new(); for mut scene_entity in entities { - let new_label = Editor::unique_label_for_world( + let new_label = Editor::unique_label_for_world_with_extra( self.world.as_ref(), scene_entity.label.as_str(), + &batch_taken, ); + batch_taken.insert(new_label.as_str().to_string()); label_remap.insert(scene_entity.label.clone(), new_label.clone()); scene_entity.label = new_label; renamed.push(scene_entity); } - // Rebuild parent map with renamed labels. let renamed_parent_map: HashMap<Label, Label> = parent_map .into_iter() .filter_map(|(child, parent)| { @@ -123,13 +125,15 @@ impl SignalController for Editor { }) .collect(); - // Keep clipboard alive so the user can paste again. self.signal .push_back(Signal::Copy(renamed.clone(), renamed_parent_map.clone())); - // Queue a PendingSpawn for each entity, with parent_label set as needed. + for scene_entity in renamed { - let parent_label = renamed_parent_map.get(&scene_entity.label).cloned(); + let parent_label = renamed_parent_map + .get(&scene_entity.label) + .cloned() + .or_else(|| paste_parent.clone()); push_pending_spawn(PendingSpawn { scene_entity, handle: None, @@ -676,7 +680,6 @@ impl SignalController for Editor { self.main_render_pipeline = None; self.light_cube_pipeline = None; self.shader_globals = None; - self.collider_wireframe_pipeline = None; self.mipmapper = None; self.texture_id = None; self.window = None; @@ -156,6 +156,8 @@ pub struct PlayMode { pub(crate) animated_bind_group_cache: HashMap<Entity, (u64, wgpu::BindGroup)>, pub(crate) last_morph_info_per_mesh: HashMap<u32, MorphTargetInfo>, + last_active_camera_for_per_frame: Option<Entity>, + initial_scene: Option<String>, current_scene: Option<String>, world_loading_progress: Option<Receiver<WorldLoadingStatus>>, @@ -246,6 +248,7 @@ impl PlayMode { animated_instances: vec![], animated_bind_group_cache: Default::default(), last_morph_info_per_mesh: Default::default(), + last_active_camera_for_per_frame: None, }; log::debug!("Created new play mode instance"); @@ -406,10 +406,6 @@ impl Scene for PlayMode { graphics.clone(), ); - if let Some(l) = &mut self.light_cube_pipeline { - l.update(graphics.clone(), &self.world); - } - let mut ui = egui::Ui::new(graphics.get_egui_context(), egui::Id::new("redback-runtime ui"), egui::UiBuilder::default()); #[cfg(feature = "debug")] @@ -696,7 +692,6 @@ impl Scene for PlayMode { self.static_batches.clear(); self.animated_instances.clear(); - { puffin::profile_scope!("finding all renderers and animation components"); let mut query = self @@ -887,6 +882,23 @@ impl Scene for PlayMode { } } + if self.last_active_camera_for_per_frame != Some(active_camera) { + self.last_active_camera_for_per_frame = Some(active_camera); + if let (Some(pipeline), Some(globals), Some(light_pipeline)) = ( + self.main_pipeline.as_mut(), + self.shader_globals.as_ref(), + self.light_cube_pipeline.as_ref(), + ) { + pipeline.per_frame = None; + pipeline.per_frame_bind_group( + graphics.clone(), + globals.buffer.buffer(), + camera.buffer(), + light_pipeline.light_buffer(), + ); + } + } + let sky = self .sky_pipeline .as_ref() @@ -1244,10 +1256,17 @@ impl Scene for PlayMode { puffin::profile_scope!("collider debug draw"); if let Some(debug_draw) = graphics.debug_draw.lock().as_mut() { let colour = [0.0, 1.0, 0.0, 1.0]; - let mut q = self.world.query::<(&EntityTransform, &ColliderGroup)>(); - for (entity_transform, group) in q.iter() { - for collider in &group.colliders { - let world_tf = entity_transform.sync(); + let to_draw: Vec<_> = { + let mut q = self.world.query::<(Entity, &ColliderGroup)>(); + q.iter().map(|(e, cg)| (e, cg.colliders.clone())).collect() + }; + for (entity, colliders) in to_draw { + let Ok(et) = self.world.get::<&EntityTransform>(entity) else { + continue; + }; + let world_tf = et.propagate(&self.world, entity); + drop(et); + for collider in &colliders { let entity_matrix = world_tf.matrix().as_mat4(); let offset_transform = Transform::new() .with_offset(collider.translation, collider.rotation); @@ -655,8 +655,11 @@ int32_t dropbear_transform_on_rails_get_drive_type(WorldPtr world, uint64_t enti int32_t dropbear_transform_on_rails_get_enabled(WorldPtr world, uint64_t entity, bool* out0); int32_t dropbear_transform_on_rails_get_path_len(WorldPtr world, uint64_t entity, int32_t* out0); int32_t dropbear_transform_on_rails_get_path_point(WorldPtr world, uint64_t entity, int32_t index, NVector3* out0); +int32_t dropbear_transform_on_rails_get_path_point_has_rotation(WorldPtr world, uint64_t entity, int32_t index, bool* out0); +int32_t dropbear_transform_on_rails_get_path_point_rotation(WorldPtr world, uint64_t entity, int32_t index, NQuaternion* out0); int32_t dropbear_transform_on_rails_get_progress(WorldPtr world, uint64_t entity, float* out0); int32_t dropbear_transform_on_rails_push_path_point(WorldPtr world, uint64_t entity, const NVector3* point); +int32_t dropbear_transform_on_rails_push_path_point_with_rotation(WorldPtr world, uint64_t entity, const NVector3* point, const NQuaternion* rotation); int32_t dropbear_transform_on_rails_set_drive_automatic(WorldPtr world, uint64_t entity, float speed, bool looping); int32_t dropbear_transform_on_rails_set_drive_axis_driven(WorldPtr world, uint64_t entity, uint64_t target, const NVector3* axis, float range_min, float range_max); int32_t dropbear_transform_on_rails_set_drive_follow_entity(WorldPtr world, uint64_t entity, uint64_t target, bool monotonic); @@ -3,7 +3,7 @@ package com.dropbear.components.camera import com.dropbear.EntityId import com.dropbear.ecs.ComponentType import com.dropbear.ecs.ExternalComponent -import com.dropbear.math.Vector3d +import com.dropbear.math.Point import com.dropbear.math.Vector3f /** @@ -90,9 +90,10 @@ class OnRails( /** * The ordered list of world-space waypoints that define the rail. * - * Requires at least 2 points. Internally stored as `Vec<DVec3>` on the Rust side. + * Requires at least 2 points. Each [com.dropbear.math.Point] carries a position and an optional explicit + * rotation. When [com.dropbear.math.Point.rotation] is `null`, orientation is derived from the path tangent. */ - var path: List<Vector3d> + var path: List<Point> get() = onRailsGetPath() set(value) = onRailsSetPath(value) @@ -127,8 +128,8 @@ internal expect fun onRailsExistsForEntity(entityId: EntityId): Boolean internal expect fun OnRails.onRailsGetEnabled(): Boolean internal expect fun OnRails.onRailsSetEnabled(enabled: Boolean) -internal expect fun OnRails.onRailsGetPath(): List<Vector3d> -internal expect fun OnRails.onRailsSetPath(path: List<Vector3d>) +internal expect fun OnRails.onRailsGetPath(): List<Point> +internal expect fun OnRails.onRailsSetPath(path: List<Point>) internal expect fun OnRails.onRailsGetProgress(): Float internal expect fun OnRails.onRailsSetProgress(progress: Float) @@ -0,0 +1,13 @@ +package com.dropbear.math + +/** + * Defined a position and a rotation in world-space. + * + * @param position World-space position of the waypoint. + * @param rotation Explicit orientation to hold at this point. When `null`, the runtime derives + * orientation from the path tangent (i.e., the camera looks in the direction of travel). + */ +data class Point( + val position: Vector3d, + val rotation: Quaterniond? = null, +) @@ -12,7 +12,9 @@ public class EucalyptusCoreLoader implements DynamicLibraryLoader { public void ensureLoaded() { if (!loaded) { Logger.info("Initialising \"eucalyptus_core\"", "EucalyptusCoreLoader::ensureLoaded"); - System.loadLibrary("eucalyptus_core"); + String path = System.getProperty("eucalyptus.core.lib"); + if (path == null) throw new RuntimeException("eucalyptus.core.lib not set"); + Runtime.getRuntime().load(path); Logger.info("Loaded!", "EucalyptusCoreLoader::ensureLoaded"); loaded = true; } @@ -18,8 +18,11 @@ public class OnRailsNative { public static native int getPathLen(long worldPtr, long entityId); public static native Vector3d getPathPoint(long worldPtr, long entityId, int index); + public static native boolean getPathPointHasRotation(long worldPtr, long entityId, int index); + public static native com.dropbear.math.Quaterniond getPathPointRotation(long worldPtr, long entityId, int index); public static native void clearPath(long worldPtr, long entityId); public static native void pushPathPoint(long worldPtr, long entityId, Vector3d point); + public static native void pushPathPointWithRotation(long worldPtr, long entityId, Vector3d point, com.dropbear.math.Quaterniond rotation); // 0=Automatic, 1=FollowEntity, 2=AxisDriven, 3=Manual public static native int getDriveType(long worldPtr, long entityId); @@ -2,6 +2,7 @@ package com.dropbear.components.camera import com.dropbear.DropbearEngine import com.dropbear.EntityId +import com.dropbear.math.Point import com.dropbear.math.Vector3d import com.dropbear.math.Vector3f @@ -20,16 +21,29 @@ internal actual fun OnRails.onRailsGetProgress(): Float = internal actual fun OnRails.onRailsSetProgress(progress: Float) = OnRailsNative.setProgress(DropbearEngine.native.worldHandle, entity.raw, progress) -internal actual fun OnRails.onRailsGetPath(): List<Vector3d> { +internal actual fun OnRails.onRailsGetPath(): List<Point> { val world = DropbearEngine.native.worldHandle val len = OnRailsNative.getPathLen(world, entity.raw) - return (0 until len).map { i -> OnRailsNative.getPathPoint(world, entity.raw, i) } + return (0 until len).map { i -> + val pos = OnRailsNative.getPathPoint(world, entity.raw, i) + val rot = if (OnRailsNative.getPathPointHasRotation(world, entity.raw, i)) + OnRailsNative.getPathPointRotation(world, entity.raw, i) + else + null + Point(pos, rot) + } } -internal actual fun OnRails.onRailsSetPath(path: List<Vector3d>) { +internal actual fun OnRails.onRailsSetPath(path: List<Point>) { val world = DropbearEngine.native.worldHandle OnRailsNative.clearPath(world, entity.raw) - path.forEach { p -> OnRailsNative.pushPathPoint(world, entity.raw, p) } + path.forEach { p -> + val rot = p.rotation + if (rot != null) + OnRailsNative.pushPathPointWithRotation(world, entity.raw, p.position, rot) + else + OnRailsNative.pushPathPoint(world, entity.raw, p.position) + } } internal actual fun OnRails.onRailsGetDrive(): RailDrive { @@ -5,6 +5,8 @@ package com.dropbear.components.camera import com.dropbear.DropbearEngine import com.dropbear.EntityId import com.dropbear.ffi.generated.* +import com.dropbear.math.Point +import com.dropbear.math.Quaterniond import com.dropbear.math.Vector3d import com.dropbear.math.Vector3f import kotlinx.cinterop.* @@ -40,26 +42,42 @@ internal actual fun OnRails.onRailsSetProgress(progress: Float) = memScoped { dropbear_transform_on_rails_set_progress(world, entity.raw.toULong(), progress) } -internal actual fun OnRails.onRailsGetPath(): List<Vector3d> = memScoped { +internal actual fun OnRails.onRailsGetPath(): List<Point> = memScoped { val world = DropbearEngine.native.worldHandle ?: return@memScoped emptyList() val lenOut = alloc<IntVar>() val rc = dropbear_transform_on_rails_get_path_len(world, entity.raw.toULong(), lenOut.ptr) if (rc != 0) return@memScoped emptyList() val len = lenOut.value (0 until len).mapNotNull { i -> - val out = alloc<NVector3>() - val rc2 = dropbear_transform_on_rails_get_path_point(world, entity.raw.toULong(), i, out.ptr) - if (rc2 != 0) null else Vector3d(out.x, out.y, out.z) + val posOut = alloc<NVector3>() + val rc2 = dropbear_transform_on_rails_get_path_point(world, entity.raw.toULong(), i, posOut.ptr) + if (rc2 != 0) return@mapNotNull null + val pos = Vector3d(posOut.x, posOut.y, posOut.z) + val hasRotOut = alloc<BooleanVar>() + dropbear_transform_on_rails_get_path_point_has_rotation(world, entity.raw.toULong(), i, hasRotOut.ptr) + val rot = if (hasRotOut.value) { + val rotOut = alloc<NQuaternion>() + dropbear_transform_on_rails_get_path_point_rotation(world, entity.raw.toULong(), i, rotOut.ptr) + Quaterniond(rotOut.x, rotOut.y, rotOut.z, rotOut.w) + } else null + Point(pos, rot) } } -internal actual fun OnRails.onRailsSetPath(path: List<Vector3d>) = memScoped { +internal actual fun OnRails.onRailsSetPath(path: List<Point>) = memScoped { val world = DropbearEngine.native.worldHandle ?: return@memScoped dropbear_transform_on_rails_clear_path(world, entity.raw.toULong()) - for (point in path) { + for (railPoint in path) { val v = alloc<NVector3>() - v.x = point.x; v.y = point.y; v.z = point.z - dropbear_transform_on_rails_push_path_point(world, entity.raw.toULong(), v.ptr) + v.x = railPoint.position.x; v.y = railPoint.position.y; v.z = railPoint.position.z + val rot = railPoint.rotation + if (rot != null) { + val q = alloc<NQuaternion>() + q.x = rot.x; q.y = rot.y; q.z = rot.z; q.w = rot.w + dropbear_transform_on_rails_push_path_point_with_rotation(world, entity.raw.toULong(), v.ptr, q.ptr) + } else { + dropbear_transform_on_rails_push_path_point(world, entity.raw.toULong(), v.ptr) + } } }